A gas-optimized Tic-Tac-Toe implementation on Ethereum using bit manipulation and assembly code. README.md by Claude Code
This project explores smart contract optimization techniques by implementing a simple game with minimal gas consumption. The entire game state is packed into 24 bits, demonstrating efficient storage patterns for blockchain applications.
- 24 bits to store entire game state (vs. traditional 288+ bits)
- Significant gas savings compared to standard implementations
- Yul assembly for optimized operations
- Efficient storage using bit manipulation
// Traditional approach
mapping(uint256 => uint8[9]) public boards; // 288 bits per game
mapping(uint256 => address) public currentPlayer;
// Bit-packed approach
struct Game {
bytes3 board; // 24 bits total
address player1;
address player2;
}
The game state is packed into 24 bits:
Bit Layout (24 bits total):
├─ Bits 0-8: Player 1 marks (9 positions)
├─ Bits 9-17: Player 2 marks (9 positions)
├─ Bit 18-22: Unused (future expansion)
└─ Bit 23: Turn indicator (0=P1, 1=P2)
Example Game State:
Player 1 plays positions [0,4,8]: 0b100010001 (bits 0-8)
Player 2 plays positions [1,5]: 0b000100010 (bits 9-17, shifted)
Current turn: Player 2 0b1 (bit 23)
Combined: 0b1000001000100100010001 (binary)
= 0x808121 (hex)
// Check if position is occupied by either player
if (iszero(iszero(and(shl(index, 513), board)))) {
revert() // 513 = 0b1000000001 - checks both player bits
}
Critical functions use Yul assembly for gas efficiency:
// Get player's board state in assembly
switch iszero(and(shl(23, 1), board))
case 0 {
// Player 2's turn - extract bits 9-17
playerBoard := shl(223, and(261632, board))
}
default {
// Player 1's turn - extract bits 0-8
playerBoard := shl(232, and(511, board))
}
Winning combinations are pre-computed for efficiency:
winningMarks[bytes3(0x000007)] = true; // [0,1,2] - top row
winningMarks[bytes3(0x000111)] = true; // [0,4,8] - diagonal
// ... all 8 winning combinations
event NewGame(address indexed player1, address indexed player2, uint256 gameId);
event Played(uint256 indexed gameId, bytes3 board);
-
Gas Efficiency
- Reduced gas costs compared to standard implementations
- Single SSTORE operation for moves
- Bit-packed storage minimizes storage costs
-
Performance
- Board state fits in CPU registers
- Atomic operations prevent race conditions
- Predictable gas costs
-
Scalability
- Minimal storage per game
- Supports multiple concurrent games
- Suitable for high-frequency gaming
-
Code Quality
- Pure functions where possible
- Predictable execution paths
- Documented assembly code
-
Development Complexity
- Requires understanding of bit manipulation
- Assembly debugging is challenging
- Higher learning curve for contributors
-
Code Readability
- Assembly code is less readable than Solidity
- Bit operations require careful documentation
- More complex to understand and maintain
-
Flexibility Limitations
- Difficult to modify game rules
- Adding features requires careful planning
- Less suitable for rapid prototyping
The project uses comprehensive fuzz testing to verify correctness:
function testFuzz_ValidMoves(uint8 position) public {
vm.assume(position < 9);
// Tests various game state combinations
}
- Valid Scenarios: Legal moves and game flows
- Invalid Positions: Out-of-bounds and occupied cells
- Wrong Players: Unauthorized and out-of-turn attempts
- Game Isolation: Multiple games don't interfere
- State Invariants: Game state consistency
- Event Emissions: Proper logging verification
- 15+ fuzz test functions
- Extensive randomized test cases
- Critical path coverage
- Edge case detection
- Foundry installed
- Basic understanding of Solidity and EVM
# Clone and build
git clone <repository>
cd tic-tac-toe
# Compile the contract
forge build
# Run the test suite
forge test
# Run with maximum verbosity for debugging
forge test -vvvv
# Generate coverage report
forge coverage
# Format code
forge fmt
# Deploy to your favorite testnet
forge script script/TicTacToe.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcast
- Create Game: Call
newGame(player1, player2)
- Make Moves: Call
play(gameId, position)
where position is 0-8 - Check State: Call
getBoard(gameId)
to see current state - Find Winner: Call
whoWon(gameId)
after game ends
// Example game flow
uint256 gameId = ttt.newGame(alice, bob);
ttt.play(gameId, 4); // Alice plays center
ttt.play(gameId, 0); // Bob plays top-left
// ... continue until someone wins or draws
- Solidity Version: ^0.8.13
- Framework: Foundry
- Dependencies: forge-std, solady
- Gas Limit: ~50,000 per move
- Storage: 1 slot per game (32 bytes)
- Max Concurrent Games: Limited by Ethereum storage
Contributions are welcome. When contributing:
- Understand the bit layout before making changes
- Test thoroughly - gas optimizations can be fragile
- Document assembly code clearly
- Benchmark changes to ensure performance is maintained
- NFT Integration: Game states as collectibles
README.md by Claude Code