diff --git a/docs/data-structure-diagram.md b/docs/data-structure-diagram.md index f394382..1506bec 100644 --- a/docs/data-structure-diagram.md +++ b/docs/data-structure-diagram.md @@ -2,7 +2,7 @@ ## Overview -This diagram illustrates how the Game contract manages multiple concurrent games using mappings and arrays. +This diagram illustrates how the Game contract manages multiple concurrent Rock-Paper-Scissors games using mappings, arrays, and structs. ## Data Structure Visualization @@ -12,71 +12,50 @@ This diagram illustrates how the Game contract manages multiple concurrent games └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ -│ 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) │ +│ 1. 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 │ │ -│ │ } │ │ -│ └────────────────────────────────────┘ │ +│ ┌───────┐ ┌──────────────────────────────────────┐ │ +│ │ 1 │ ───────────→ │ GameState { │ │ +│ └───────┘ │ gameId: 1 │ │ +│ │ isActive: true │ │ +│ │ firstReveal: 0 │ │ +│ │ initialBet: 0.01 ETH │ │ +│ │ outcome: None │ │ +│ │ playerA: Player { │ │ +│ │ addr: 0xABC...123 │ │ +│ │ bet: 0.01 ETH │ │ +│ │ encrMove: 0x4f2a... │ │ +│ │ move: None │ │ +│ │ nickname: "Alice" │ │ +│ │ } │ │ +│ │ playerB: Player { │ │ +│ │ addr: 0xDEF...456 │ │ +│ │ bet: 0.01 ETH │ │ +│ │ encrMove: 0x8b3c... │ │ +│ │ move: None │ │ +│ │ nickname: "Bob" │ │ +│ │ } │ │ +│ │ } │ │ +│ └──────────────────────────────────────┘ │ │ │ -│ ┌───────┐ ┌────────────────────────────────────┐ │ -│ │ 2 │ ───────────→ │ GameState { │ │ -│ └───────┘ │ gameId: 2 │ │ -│ │ isActive: true │ │ -│ │ playerA: { addr: 0xGHI...789 } │ │ -│ │ playerB: { addr: 0x000...000 } │ │ -│ │ outcome: None │ │ -│ │ ... │ │ -│ │ } │ │ -│ └────────────────────────────────────┘ │ +│ ┌───────┐ ┌──────────────────────────────────────┐ │ +│ │ 2 │ ───────────→ │ GameState { │ │ +│ └───────┘ │ gameId: 2 │ │ +│ │ isActive: true │ │ +│ │ playerA: { addr: 0xGHI...789 } │ │ +│ │ playerB: { addr: 0x000...000 } │ │ +│ │ (waiting for playerB) │ │ +│ │ ... │ │ +│ │ } │ │ +│ └──────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ -│ 3. gameIds (array: uint[]) │ -│ Tracks all game IDs for enumeration │ +│ 2. gameIds (array: uint[]) │ +│ Tracks all game IDs ever created (for enumeration) │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ Index: 0 1 2 3 4 │ @@ -84,36 +63,19 @@ This diagram illustrates how the Game contract manages multiple concurrent games │ Value: │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ ... │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ │ -│ Used to iterate over all games (active and inactive) │ +│ Entries are never removed (enables iteration over all games) │ +│ Use isActive flag to distinguish active from completed games │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ -│ 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) │ +│ 3. 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() │ +│ Starts at 1 and never resets (prevents ID collisions) │ └─────────────────────────────────────────────────────────────────────────────┘ ``` @@ -124,49 +86,49 @@ This diagram illustrates how the Game contract manages multiple concurrent games │ PLAYER GAME LIFECYCLE │ └──────────────────────────────────────────────────────────────────────────┘ - Player: 0xABC...123 + Player: 0xABC...123 (msg.sender) │ - │ register(0) + │ register(gameId, {value: 0.01 ETH}) ↓ - ┌─────────────────────────────────────┐ - │ 1. Check playerToActiveGame │ - │ 0xABC...123 → 0 (not in game) │ - └─────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────┐ + │ 1. Register with Game │ + │ - If gameId == 0: createNewGame() │ + │ - Validate bet >= BET_MIN (0.01 ETH) │ + │ - If playerA slot empty: become playerA │ + │ - Else if playerB slot empty: become playerB │ + │ - Else: revert "Game is full" │ + └─────────────────────────────────────────────────────┘ │ ↓ - ┌─────────────────────────────────────┐ - │ 2. findOrCreateGame() │ - │ - Search for open game │ - │ - Or create new game ID: 1 │ - └─────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────┐ + │ 2. Commit Phase │ + │ play(gameId, encrMove) │ + │ - Store encrypted move for player │ + │ - Wait for both players to commit │ + └─────────────────────────────────────────────────────┘ │ + │ (once both players committed) ↓ - ┌─────────────────────────────────────┐ - │ 3. Update mappings │ - │ playerToActiveGame[0xABC] = 1 │ - │ games[1].playerA = 0xABC...123 │ - │ games[1].isActive = true │ - │ gameIds.push(1) │ - └─────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────┐ + │ 3. Reveal Phase │ + │ reveal(gameId, "move-password") │ + │ - Hash and verify move │ + │ - Store clear move │ + │ - Start firstReveal timer on first reveal │ + │ - Auto-calculate outcome if both revealed │ + └─────────────────────────────────────────────────────┘ │ - │ play(encrMove) → reveal(clearMove) + │ (once both players revealed OR timeout reached) ↓ - ┌─────────────────────────────────────┐ - │ 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 │ - └─────────────────────────────────────┘ + ┌─────────────────────────────────────────────────────┐ + │ 4. Game completion │ + │ getOutcome(gameId) │ + │ - Verify reveal phase ended │ + │ - Mark game as inactive │ + │ - Transfer winnings (2x bet for winner, │ + │ 1x bet each for draw) │ + │ - Return outcome (PlayerA/PlayerB/Draw) │ + └─────────────────────────────────────────────────────┘ │ ↓ Player can register for new game @@ -176,19 +138,10 @@ This diagram illustrates how the Game contract manages multiple concurrent games ``` ┌───────────────────────────────┐ - │ Player Addresses │ - │ (External participants) │ - └──────────────┬────────────────┘ - │ - playerToActiveGame - │ (mapping) - ↓ - ┌───────────────────────────────┐ │ Game IDs │ │ (1, 2, 3, 4, 5...) │ └──────────────┬────────────────┘ │ - │ ┌──────────────┴────────────────┐ │ │ games (mapping) gameIds (array) @@ -197,61 +150,73 @@ This diagram illustrates how the Game contract manages multiple concurrent games ┌─────────────────────┐ ┌────────────────┐ │ GameState Objects │ │ For iteration │ │ - Player data │ │ over all games│ - │ - Moves │ └────────────────┘ - │ - Outcomes │ - │ - isActive flag │ - └──────────┬──────────┘ - │ - When game completes (isActive = false) - │ - ↓ - ┌──────────────────────┐ - │ pastGames array │ - │ (Historical record) │ - └──────────────────────┘ + │ - Moves │ │ (active/ │ + │ - Outcomes │ │ inactive) │ + │ - isActive flag │ └────────────────┘ + │ - Timestamps │ + └─────────────────────┘ ``` ## Key Relationships -1. **playerToActiveGame → games**: +1. **gameIds array → games mapping**: - - A player's address maps to a game ID - - That game ID is used to access the full game state in `games` mapping + - `gameIds` maintains a list of all game IDs created + - Each ID in `gameIds[i]` can be used to look up the game state: `games[gameIds[i]]` + - Enables iteration over all games regardless of active status -2. **gameIds array**: +2. **GameState structure**: - - Maintains list of all game IDs ever created - - Enables iteration over games (e.g., `getActiveGameIds()`) - - Never removes entries, only marks games inactive + - Each game has exactly 2 players (playerA and playerB) + - Players are stored as `Player` structs with address, bet amount, moves, and nickname + - The `isActive` flag indicates if the game is ongoing or completed + - `outcome` is calculated when both players reveal or after reveal timeout -3. **pastGames array**: +3. **nextGameId counter**: + - Ensures unique game IDs across all games + - Increments with each new game creation + - Never resets, preventing ID collisions even in long-running contracts - - 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 } +Step 1: Player A registers with gameId=0 (create new game) + createNewGame() → gameId = 1, nextGameId = 2 + games[1] = { + gameId: 1, + isActive: true, + playerA: { addr: PlayerA, bet: 0.01 ETH, encrMove: 0, move: None, nickname: "" }, + playerB: { addr: 0x0, bet: 0, encrMove: 0, move: None, nickname: "" }, + outcome: None, + firstReveal: 0, + initialBet: 0 + } 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 2: Player B joins game 1 + games[1] = { + gameId: 1, + isActive: true, + playerA: { addr: PlayerA, bet: 0.01 ETH, ... }, + playerB: { addr: PlayerB, bet: 0.01 ETH, ... }, + outcome: None, + ... + } + gameIds = [1] (unchanged - game ID already exists) -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) +Step 3: Players commit moves + game.playerA.encrMove = hash("1-password") + game.playerB.encrMove = hash("2-password") + +Step 4: Players reveal moves + game.playerA.move = Rock (1) + game.playerB.move = Paper (2) + game.firstReveal = block.timestamp + game.outcome = PlayerB (Paper beats Rock) + +Step 5: Game completes via getOutcome() + games[1].isActive = false + Transfer 0.02 ETH to PlayerB + gameIds = [1] (unchanged - game ID remains for historical reference) ```