diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f80cc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/* +*/.states/* diff --git a/crypto_clash_contract/contracts/Game.sol b/crypto_clash_contract/contracts/Game.sol index 48e354c..c089b15 100644 --- a/crypto_clash_contract/contracts/Game.sol +++ b/crypto_clash_contract/contracts/Game.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.30; +pragma solidity ^0.7.3; contract Game { uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD) @@ -34,60 +34,124 @@ contract Game { Outcomes outcome; uint firstReveal; uint initialBet; + uint gameId; + bool isActive; } - GameState private currentGame; + // Mapping from player address to their active game ID + mapping(address => uint) private playerToActiveGame; + // Mapping from game ID to game state + mapping(uint => GameState) private games; + + // Array to track all game IDs (for enumeration) + uint[] private gameIds; + + // Counter for generating unique game IDs + uint private nextGameId = 1; + + // Array to store completed games GameState[] private pastGames; - GameState[] private activeGames; // ------------------------- Registration ------------------------- // - modifier validBet() { + modifier validBet(uint gameId) { require(msg.value >= BET_MIN, "Minimum bet not met"); require( - currentGame.initialBet == 0 || msg.value >= currentGame.initialBet, + games[gameId].initialBet == 0 || + msg.value >= games[gameId].initialBet, "Bet value too low" ); _; } - modifier notAlreadyRegistered() { + modifier notAlreadyInGame() { require( - msg.sender != currentGame.playerA.addr && - msg.sender != currentGame.playerB.addr, - "Player already registered" + playerToActiveGame[msg.sender] == 0, + "Player already in an active game" ); _; } - // Register a player. - // Return player's ID upon successful registration. - function register() + // Register a player to an existing game or create a new game. + // If gameId is 0, player will join or create the first available game. + // Return player's ID and game ID upon successful registration. + function register( + uint gameId + ) public payable - validBet - notAlreadyRegistered - returns (uint) + validBet(gameId) + notAlreadyInGame + returns (uint playerId, uint returnGameId) { - if (currentGame.playerA.addr == address(0x0)) { - currentGame.playerA.addr = payable(msg.sender); - currentGame.initialBet = msg.value; - return 1; - } else if (currentGame.playerB.addr == address(0x0)) { - currentGame.playerB.addr = payable(msg.sender); - return 2; + // If gameId is 0, find an open game or create a new one + if (gameId == 0) { + gameId = findOrCreateGame(); } - return 0; + + require(games[gameId].isActive, "Game is not active"); + + GameState storage game = games[gameId]; + + if (game.playerA.addr == address(0x0)) { + game.playerA.addr = payable(msg.sender); + game.initialBet = msg.value; + playerToActiveGame[msg.sender] = gameId; + return (1, gameId); + } else if (game.playerB.addr == address(0x0)) { + require( + msg.sender != game.playerA.addr, + "Cannot play against yourself" + ); + game.playerB.addr = payable(msg.sender); + playerToActiveGame[msg.sender] = gameId; + return (2, gameId); + } + + revert("Game is full"); + } + + // Find an open game or create a new one + function findOrCreateGame() private returns (uint) { + // Look for a game with only one player + for (uint i = 0; i < gameIds.length; i++) { + uint gId = gameIds[i]; + GameState storage game = games[gId]; + if ( + game.isActive && + game.playerA.addr != address(0x0) && + game.playerB.addr == address(0x0) + ) { + return gId; + } + } + + // No open game found, create a new one + return createNewGame(); + } + + // Create a new game + function createNewGame() private returns (uint) { + uint gameId = nextGameId; + nextGameId++; + + games[gameId].gameId = gameId; + games[gameId].isActive = true; + gameIds.push(gameId); + + return gameId; } // ------------------------- Commit ------------------------- // modifier isRegistered() { + uint gameId = playerToActiveGame[msg.sender]; + require(gameId != 0, "Player not in any active game"); require( - msg.sender == currentGame.playerA.addr || - msg.sender == currentGame.playerB.addr, - "Player not registered" + msg.sender == games[gameId].playerA.addr || + msg.sender == games[gameId].playerB.addr, + "Player not registered in this game" ); _; } @@ -95,21 +159,24 @@ contract Game { // Save player's encrypted move. encrMove must be "<1|2|3>-password" hashed with sha256. // Return 'true' if move was valid, 'false' otherwise. function play(bytes32 encrMove) public isRegistered returns (bool) { + uint gameId = playerToActiveGame[msg.sender]; + GameState storage game = games[gameId]; + // Basic sanity checks with explicit errors to help debugging require(encrMove != bytes32(0), "Encrypted move cannot be zero"); // Ensure the caller hasn't already committed a move - if (msg.sender == currentGame.playerA.addr) { + if (msg.sender == game.playerA.addr) { require( - currentGame.playerA.encrMove == bytes32(0), + game.playerA.encrMove == bytes32(0), "Player A already committed" ); - currentGame.playerA.encrMove = encrMove; - } else if (msg.sender == currentGame.playerB.addr) { + game.playerA.encrMove = encrMove; + } else if (msg.sender == game.playerB.addr) { require( - currentGame.playerB.encrMove == bytes32(0), + game.playerB.encrMove == bytes32(0), "Player B already committed" ); - currentGame.playerB.encrMove = encrMove; + game.playerB.encrMove = encrMove; } else { revert("Caller not registered"); } @@ -119,9 +186,11 @@ contract Game { // ------------------------- Reveal ------------------------- // modifier commitPhaseEnded() { + uint gameId = playerToActiveGame[msg.sender]; + require(gameId != 0, "Player not in any active game"); require( - currentGame.playerA.encrMove != bytes32(0) && - currentGame.playerB.encrMove != bytes32(0), + games[gameId].playerA.encrMove != bytes32(0) && + games[gameId].playerB.encrMove != bytes32(0), "Commit phase not ended" ); _; @@ -132,6 +201,9 @@ contract Game { function reveal( string memory clearMove ) public isRegistered commitPhaseEnded returns (Moves) { + uint gameId = playerToActiveGame[msg.sender]; + GameState storage game = games[gameId]; + bytes32 encrMove = sha256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password") Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors) @@ -140,22 +212,20 @@ contract Game { // If hashes match, clear move is saved if ( - msg.sender == currentGame.playerA.addr && - encrMove == currentGame.playerA.encrMove + msg.sender == game.playerA.addr && encrMove == game.playerA.encrMove ) { - currentGame.playerA.move = move; + game.playerA.move = move; } else if ( - msg.sender == currentGame.playerB.addr && - encrMove == currentGame.playerB.encrMove + msg.sender == game.playerB.addr && encrMove == game.playerB.encrMove ) { - currentGame.playerB.move = move; + game.playerB.move = move; } else { return Moves.None; } // Timer starts after first revelation from one of the player - if (currentGame.firstReveal == 0) { - currentGame.firstReveal = block.timestamp; + if (game.firstReveal == 0) { + game.firstReveal = block.timestamp; } return move; @@ -183,11 +253,14 @@ contract Game { // ------------------------- Result ------------------------- // modifier revealPhaseEnded() { + uint gameId = playerToActiveGame[msg.sender]; + require(gameId != 0, "Player not in any active game"); require( - (currentGame.playerA.move != Moves.None && - currentGame.playerB.move != Moves.None) || - (currentGame.firstReveal != 0 && - block.timestamp > currentGame.firstReveal + REVEAL_TIMEOUT), + (games[gameId].playerA.move != Moves.None && + games[gameId].playerB.move != Moves.None) || + (games[gameId].firstReveal != 0 && + block.timestamp > + games[gameId].firstReveal + REVEAL_TIMEOUT), "Reveal phase not ended" ); _; @@ -196,25 +269,24 @@ contract Game { // Compute the outcome and pay the winner(s). // Return the outcome. function getOutcome() public revealPhaseEnded returns (Outcomes) { + uint gameId = playerToActiveGame[msg.sender]; + GameState storage game = games[gameId]; + // Only calculate outcome once - require( - currentGame.outcome == Outcomes.None, - "Outcome already determined" - ); + require(game.outcome == Outcomes.None, "Outcome already determined"); Outcomes outcome; - if (currentGame.playerA.move == currentGame.playerB.move) { + if (game.playerA.move == game.playerB.move) { outcome = Outcomes.Draw; } else if ( - (currentGame.playerA.move == Moves.Rock && - currentGame.playerB.move == Moves.Scissors) || - (currentGame.playerA.move == Moves.Paper && - currentGame.playerB.move == Moves.Rock) || - (currentGame.playerA.move == Moves.Scissors && - currentGame.playerB.move == Moves.Paper) || - (currentGame.playerA.move != Moves.None && - currentGame.playerB.move == Moves.None) + (game.playerA.move == Moves.Rock && + game.playerB.move == Moves.Scissors) || + (game.playerA.move == Moves.Paper && + game.playerB.move == Moves.Rock) || + (game.playerA.move == Moves.Scissors && + game.playerB.move == Moves.Paper) || + (game.playerA.move != Moves.None && game.playerB.move == Moves.None) ) { outcome = Outcomes.PlayerA; } else { @@ -222,12 +294,17 @@ contract Game { } // Store the outcome permanently before resetting - currentGame.outcome = outcome; + game.outcome = outcome; - address payable addrA = currentGame.playerA.addr; - address payable addrB = currentGame.playerB.addr; - uint betPlayerA = currentGame.initialBet; - reset(); // Reset game before paying to avoid reentrancy attacks + address payable addrA = game.playerA.addr; + address payable addrB = game.playerB.addr; + uint betPlayerA = game.initialBet; + + // Move game to past games before resetting + pastGames.push(game); + + // Reset and cleanup + resetGame(gameId); // Reset game before paying to avoid reentrancy attacks pay(addrA, addrB, betPlayerA, outcome); return outcome; @@ -240,31 +317,33 @@ contract Game { uint betPlayerA, Outcomes outcome ) private { - // Uncomment lines below if you need to adjust the gas limit if (outcome == Outcomes.PlayerA) { addrA.transfer(address(this).balance); - // addrA.call.value(address(this).balance).gas(1000000)(""); } else if (outcome == Outcomes.PlayerB) { addrB.transfer(address(this).balance); - // addrB.call.value(address(this).balance).gas(1000000)(""); } else { addrA.transfer(betPlayerA); addrB.transfer(address(this).balance); - // addrA.call.value(betPlayerA).gas(1000000)(""); - // addrB.call.value(address(this).balance).gas(1000000)(""); } } - // Reset the game. - function reset() private { - currentGame.initialBet = 0; - currentGame.firstReveal = 0; - currentGame.playerA.addr = payable(address(0x0)); - currentGame.playerB.addr = payable(address(0x0)); - currentGame.playerA.encrMove = 0x0; - currentGame.playerB.encrMove = 0x0; - currentGame.playerA.move = Moves.None; - currentGame.playerB.move = Moves.None; + // Reset a specific game. + function resetGame(uint gameId) private { + GameState storage game = games[gameId]; + + // Clear player mappings + if (game.playerA.addr != address(0x0)) { + playerToActiveGame[game.playerA.addr] = 0; + } + if (game.playerB.addr != address(0x0)) { + playerToActiveGame[game.playerB.addr] = 0; + } + + // Mark game as inactive + game.isActive = false; + + // Note: We keep the game data in the mapping for reference + // but players are now free to join other games } // ------------------------- Helpers ------------------------- // @@ -274,51 +353,160 @@ contract Game { return address(this).balance; } - // Return player's ID + // Return player's ID in their active game function whoAmI() public view returns (uint) { - if (msg.sender == currentGame.playerA.addr) { + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) { + return 0; + } + + GameState storage game = games[gameId]; + if (msg.sender == game.playerA.addr) { return 1; - } else if (msg.sender == currentGame.playerB.addr) { + } else if (msg.sender == game.playerB.addr) { return 2; } else { return 0; } } + // Get the active game ID for the caller + function getMyActiveGameId() public view returns (uint) { + return playerToActiveGame[msg.sender]; + } + // Return 'true' if both players have commited a move, 'false' otherwise. function bothPlayed() public view returns (bool) { - return (currentGame.playerA.encrMove != 0x0 && - currentGame.playerB.encrMove != 0x0); + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return false; + + GameState storage game = games[gameId]; + return (game.playerA.encrMove != 0x0 && game.playerB.encrMove != 0x0); } // Return 'true' if both players have revealed their move, 'false' otherwise. function bothRevealed() public view returns (bool) { - return (currentGame.playerA.move != Moves.None && - currentGame.playerB.move != Moves.None); + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return false; + + GameState storage game = games[gameId]; + return (game.playerA.move != Moves.None && + game.playerB.move != Moves.None); } // Return 'true' if player A has revealed their move, 'false' otherwise. function playerARevealed() public view returns (bool) { - return (currentGame.playerA.move != Moves.None); + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return false; + + GameState storage game = games[gameId]; + return (game.playerA.move != Moves.None); } // Return 'true' if player B has revealed their move, 'false' otherwise. function playerBRevealed() public view returns (bool) { - return (currentGame.playerB.move != Moves.None); + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return false; + + GameState storage game = games[gameId]; + return (game.playerB.move != Moves.None); } // Return time left before the end of the revelation phase. function revealTimeLeft() public view returns (int) { - if (currentGame.firstReveal != 0) { - return - int( - (currentGame.firstReveal + REVEAL_TIMEOUT) - block.timestamp - ); + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return int(REVEAL_TIMEOUT); + + GameState storage game = games[gameId]; + if (game.firstReveal != 0) { + return int((game.firstReveal + REVEAL_TIMEOUT) - block.timestamp); } return int(REVEAL_TIMEOUT); } function getLastWinner() public view returns (Outcomes) { - return currentGame.outcome; + uint gameId = playerToActiveGame[msg.sender]; + if (gameId == 0) return Outcomes.None; + + return games[gameId].outcome; + } + + // ------------------------- Game Management ------------------------- // + + // Get details of a specific game (for viewing any game) + function getGameDetails( + uint gameId + ) + public + view + returns ( + address playerAAddr, + address playerBAddr, + uint initialBet, + Outcomes outcome, + bool isActive + ) + { + GameState storage game = games[gameId]; + require(game.gameId != 0, "Game does not exist"); + return ( + game.playerA.addr, + game.playerB.addr, + game.initialBet, + game.outcome, + game.isActive + ); + } + + // Get all active game IDs + function getActiveGameIds() public view returns (uint[] memory) { + uint activeCount = 0; + + // Count active games + for (uint i = 0; i < gameIds.length; i++) { + if (games[gameIds[i]].isActive) { + activeCount++; + } + } + + // Build array of active game IDs + uint[] memory activeIds = new uint[](activeCount); + uint index = 0; + for (uint i = 0; i < gameIds.length; i++) { + if (games[gameIds[i]].isActive) { + activeIds[index] = gameIds[i]; + index++; + } + } + + return activeIds; + } + + // Get number of past games + function getPastGamesCount() public view returns (uint) { + return pastGames.length; + } + + // Get details of a past game by index + function getPastGame( + uint index + ) + public + view + returns ( + address playerAAddr, + address playerBAddr, + uint initialBet, + Outcomes outcome + ) + { + require(index < pastGames.length, "Index out of bounds"); + GameState storage game = pastGames[index]; + return ( + game.playerA.addr, + game.playerB.addr, + game.initialBet, + game.outcome + ); } } diff --git a/docs/MULTI_GAME_IMPLEMENTATION.md b/docs/MULTI_GAME_IMPLEMENTATION.md new file mode 100644 index 0000000..ea391c1 --- /dev/null +++ b/docs/MULTI_GAME_IMPLEMENTATION.md @@ -0,0 +1,168 @@ +# Multi-Game Implementation Summary + +![Image of Game Loop](res/Gameloop.jpg) + +## Overview + +The Game smart contract has been updated to support multiple concurrent games. Each player can participate in only one active game at a time, and games are identified by unique game IDs and tracked by player addresses. + +## Key Changes + +### 1. Data Structure Updates + +**Previous Structure:** + +- Single `currentGame` variable + +**New Structure:** + +See [Data Structure Diagram](data-structure-diagram.md) for visual representation. + +```solidity +// Mapping from player address to their active game ID +mapping(address => uint) private playerToActiveGame; + +// Mapping from game ID to game state +mapping(uint => GameState) private games; + +// Array to track all game IDs (for enumeration) +uint[] private gameIds; + +// Counter for generating unique game IDs +uint private nextGameId = 1; + +// Array to store completed games +GameState[] private pastGames; +``` + +**GameState struct updated with:** + +- `uint gameId` - Unique identifier for each game +- `bool isActive` - Flag to track if game is currently active + +### 2. Registration System + +**New `register(uint gameId)` function:** + +- If `gameId = 0`: Automatically finds an open game or creates a new one +- If `gameId > 0`: Joins the specified game (if valid and has space) +- Returns both player ID (1 or 2) and the game ID +- Enforces one active game per address + +**Helper functions:** + +- `findOrCreateGame()` - Finds a game with one player or creates new game +- `createNewGame()` - Creates a new game with unique ID + +### 3. Game Flow Updates + +All game functions now work with the player's active game: + +- **`play(bytes32 encrMove)`** - Commits move to player's active game +- **`reveal(string memory clearMove)`** - Reveals move in player's active game +- **`getOutcome()`** - Calculates outcome for player's active game + +### 4. Game Lifecycle + +**Active Games:** + +- Players are automatically assigned to their active game via `playerToActiveGame` mapping +- All modifiers check the player's active game ID + +**Game Completion:** + +- When `getOutcome()` is called: + 1. Game outcome is calculated + 2. Game is moved to `pastGames` array + 3. `resetGame()` clears player mappings and marks game as inactive + 4. Winners are paid + 5. Players are free to join new games + +### 5. New Helper Functions + +**Game Management:** + +- `getMyActiveGameId()` - Returns caller's active game ID +- `getGameDetails(uint gameId)` - View any game's details +- `getActiveGameIds()` - Returns array of all active game IDs +- `getPastGamesCount()` - Returns number of completed games +- `getPastGame(uint index)` - Returns details of a past game + +**Updated Helper Functions:** +All existing helper functions now operate on the caller's active game: + +- `whoAmI()` - Returns player ID in their active game +- `bothPlayed()` - Checks if both players committed in caller's game +- `bothRevealed()` - Checks if both players revealed in caller's game +- `playerARevealed()` - Check player A status in caller's game +- `playerBRevealed()` - Check player B status in caller's game +- `revealTimeLeft()` - Time remaining in caller's game +- `getLastWinner()` - Outcome of caller's game + +## Usage Examples + +### Example 1: Auto-join or create game + +```solidity +// Player registers with gameId = 0 to auto-find/create game +(uint playerId, uint gameId) = game.register{value: 0.01 ether}(0); +// Returns: (1, 1) if creating new game, or (2, X) if joining existing game +``` + +### Example 2: Join specific game + +```solidity +// Player joins game ID 5 +(uint playerId, uint gameId) = game.register{value: 0.01 ether}(5); +// Returns: (2, 5) if successful +``` + +### Example 3: Query active games + +```solidity +// Get all active game IDs +uint[] memory activeGames = game.getActiveGameIds(); + +// Check details of a specific game +(address playerA, address playerB, uint bet, Outcomes outcome, bool isActive) + = game.getGameDetails(gameId); +``` + +### Example 4: View game history + +```solidity +// Get number of completed games +uint totalPastGames = game.getPastGamesCount(); + +// Get details of a specific past game +(address playerA, address playerB, uint bet, Outcomes outcome) + = game.getPastGame(0); +``` + +## Benefits + +1. **Concurrent Games**: Multiple games can run simultaneously +2. **Player Isolation**: Each player can only be in one game at a time +3. **Game Tracking**: All games are tracked with unique IDs +4. **History**: Completed games are preserved in `pastGames` +5. **Flexibility**: Players can auto-join available games or specify game IDs +6. **Backwards Compatible**: Existing game flow (commit-reveal-outcome) unchanged + +## Security Considerations + +1. **Reentrancy Protection**: Payment happens after game state is reset +2. **One Game Per Address**: Enforced via `notAlreadyInGame` modifier +3. **Game Isolation**: Players can only interact with their active game +4. **State Consistency**: Game marked inactive before clearing mappings + +## Migration Notes + +**Breaking Changes:** + +- `register()` now returns `(uint playerId, uint gameId)` instead of just `uint playerId` +- `register()` now requires a `uint gameId` parameter (use 0 for auto-join) + +**Non-Breaking:** + +- All other function signatures remain the same +- Existing game flow unchanged diff --git a/docs/data-structure-diagram.md b/docs/data-structure-diagram.md new file mode 100644 index 0000000..f394382 --- /dev/null +++ b/docs/data-structure-diagram.md @@ -0,0 +1,257 @@ +# Game Contract Data Structure Diagram + +## Overview + +This diagram illustrates how the Game contract manages multiple concurrent games using mappings and arrays. + +## Data Structure Visualization + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ GAME CONTRACT STATE │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 1. playerToActiveGame (mapping: address => uint) │ +│ Maps each player address to their active game ID │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Player Address → Game ID │ +│ ┌──────────────────┐ ┌───────┐ │ +│ │ 0xABC...123 │ ──────→ │ 1 │ │ +│ └──────────────────┘ └───────┘ │ +│ │ +│ ┌──────────────────┐ ┌───────┐ │ +│ │ 0xDEF...456 │ ──────→ │ 1 │ (same game) │ +│ └──────────────────┘ └───────┘ │ +│ │ +│ ┌──────────────────┐ ┌───────┐ │ +│ │ 0xGHI...789 │ ──────→ │ 2 │ │ +│ └──────────────────┘ └───────┘ │ +│ │ +│ ┌──────────────────┐ ┌───────┐ │ +│ │ 0xJKL...012 │ ──────→ │ 0 │ (no active game) │ +│ └──────────────────┘ └───────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 2. games (mapping: uint => GameState) │ +│ Maps game ID to the complete game state │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Game ID Game State │ +│ ┌───────┐ ┌────────────────────────────────────┐ │ +│ │ 1 │ ───────────→ │ GameState { │ │ +│ └───────┘ │ gameId: 1 │ │ +│ │ isActive: true │ │ +│ │ playerA: { │ │ +│ │ addr: 0xABC...123 │ │ +│ │ bet: 0.01 ETH │ │ +│ │ encrMove: 0x4f2a... │ │ +│ │ move: Rock │ │ +│ │ } │ │ +│ │ playerB: { │ │ +│ │ addr: 0xDEF...456 │ │ +│ │ bet: 0.01 ETH │ │ +│ │ encrMove: 0x8b3c... │ │ +│ │ move: Paper │ │ +│ │ } │ │ +│ │ outcome: PlayerB │ │ +│ │ firstReveal: 1699876543 │ │ +│ │ initialBet: 0.01 ETH │ │ +│ │ } │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ ┌───────┐ ┌────────────────────────────────────┐ │ +│ │ 2 │ ───────────→ │ GameState { │ │ +│ └───────┘ │ gameId: 2 │ │ +│ │ isActive: true │ │ +│ │ playerA: { addr: 0xGHI...789 } │ │ +│ │ playerB: { addr: 0x000...000 } │ │ +│ │ outcome: None │ │ +│ │ ... │ │ +│ │ } │ │ +│ └────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 3. gameIds (array: uint[]) │ +│ Tracks all game IDs for enumeration │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Index: 0 1 2 3 4 │ +│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ +│ Value: │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ ... │ +│ └───┘ └───┘ └───┘ └───┘ └───┘ │ +│ │ +│ Used to iterate over all games (active and inactive) │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 4. pastGames (array: GameState[]) │ +│ Stores completed games for historical reference │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Index: 0 1 │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ GameState { │ │ GameState { │ │ +│ │ gameId: 1 │ │ gameId: 3 │ │ +│ │ isActive: false │ │ isActive: false │ │ +│ │ playerA: ... │ │ playerA: ... │ │ +│ │ playerB: ... │ │ playerB: ... │ │ +│ │ outcome: PlayerB │ │ outcome: Draw │ │ +│ │ } │ │ } │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ +│ Grows as games are completed via getOutcome() │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 5. nextGameId (uint counter) │ +│ Auto-incrementing counter for unique game IDs │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Current Value: 6 ──→ Next game created will have ID = 6 │ +│ │ +│ Increments with each new game: createNewGame() │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +## Flow Diagram: Player Lifecycle + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ PLAYER GAME LIFECYCLE │ +└──────────────────────────────────────────────────────────────────────────┘ + + Player: 0xABC...123 + │ + │ register(0) + ↓ + ┌─────────────────────────────────────┐ + │ 1. Check playerToActiveGame │ + │ 0xABC...123 → 0 (not in game) │ + └─────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────┐ + │ 2. findOrCreateGame() │ + │ - Search for open game │ + │ - Or create new game ID: 1 │ + └─────────────────────────────────────┘ + │ + ↓ + ┌─────────────────────────────────────┐ + │ 3. Update mappings │ + │ playerToActiveGame[0xABC] = 1 │ + │ games[1].playerA = 0xABC...123 │ + │ games[1].isActive = true │ + │ gameIds.push(1) │ + └─────────────────────────────────────┘ + │ + │ play(encrMove) → reveal(clearMove) + ↓ + ┌─────────────────────────────────────┐ + │ 4. Game progresses │ + │ Both players commit & reveal │ + └─────────────────────────────────────┘ + │ + │ getOutcome() + ↓ + ┌─────────────────────────────────────┐ + │ 5. Game completion │ + │ - Calculate winner │ + │ - pastGames.push(games[1]) │ + │ - playerToActiveGame[0xABC] = 0 │ + │ - playerToActiveGame[0xDEF] = 0 │ + │ - games[1].isActive = false │ + │ - Pay winners │ + └─────────────────────────────────────┘ + │ + ↓ + Player can register for new game +``` + +## Relationship Diagram + +``` + ┌───────────────────────────────┐ + │ Player Addresses │ + │ (External participants) │ + └──────────────┬────────────────┘ + │ + playerToActiveGame + │ (mapping) + ↓ + ┌───────────────────────────────┐ + │ Game IDs │ + │ (1, 2, 3, 4, 5...) │ + └──────────────┬────────────────┘ + │ + │ + ┌──────────────┴────────────────┐ + │ │ + games (mapping) gameIds (array) + │ │ + ↓ ↓ + ┌─────────────────────┐ ┌────────────────┐ + │ GameState Objects │ │ For iteration │ + │ - Player data │ │ over all games│ + │ - Moves │ └────────────────┘ + │ - Outcomes │ + │ - isActive flag │ + └──────────┬──────────┘ + │ + When game completes (isActive = false) + │ + ↓ + ┌──────────────────────┐ + │ pastGames array │ + │ (Historical record) │ + └──────────────────────┘ +``` + +## Key Relationships + +1. **playerToActiveGame → games**: + + - A player's address maps to a game ID + - That game ID is used to access the full game state in `games` mapping + +2. **gameIds array**: + + - Maintains list of all game IDs ever created + - Enables iteration over games (e.g., `getActiveGameIds()`) + - Never removes entries, only marks games inactive + +3. **pastGames array**: + + - Snapshot of completed games + - Grows with each completed game + - Provides historical game data + +4. **nextGameId counter**: + - Ensures unique game IDs + - Increments with each new game + - Never resets, preventing ID collisions + +## Data Flow Example: Two Players Join Game + +``` +Step 1: Player A registers + playerToActiveGame[PlayerA] = 0 → 1 + games[1] = { playerA: PlayerA, playerB: null, isActive: true } + gameIds = [1] + +Step 2: Player B joins same game + playerToActiveGame[PlayerB] = 0 → 1 + games[1] = { playerA: PlayerA, playerB: PlayerB, isActive: true } + gameIds = [1] (unchanged) + +Step 3: Game completes + pastGames.push(games[1]) → pastGames[0] = games[1] + playerToActiveGame[PlayerA] = 1 → 0 + playerToActiveGame[PlayerB] = 1 → 0 + games[1].isActive = true → false + gameIds = [1] (unchanged, but game is inactive) +``` diff --git a/docs/res/Gameloop.jpg b/docs/res/Gameloop.jpg new file mode 100644 index 0000000..74ceb17 Binary files /dev/null and b/docs/res/Gameloop.jpg differ