mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58:11 +01:00
implement multi game mode and document it
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.vscode/*
|
||||||
|
*/.states/*
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
pragma solidity ^0.8.30;
|
pragma solidity ^0.7.3;
|
||||||
|
|
||||||
contract Game {
|
contract Game {
|
||||||
uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD)
|
uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD)
|
||||||
@@ -34,60 +34,124 @@ contract Game {
|
|||||||
Outcomes outcome;
|
Outcomes outcome;
|
||||||
uint firstReveal;
|
uint firstReveal;
|
||||||
uint initialBet;
|
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 pastGames;
|
||||||
GameState[] private activeGames;
|
|
||||||
|
|
||||||
// ------------------------- Registration ------------------------- //
|
// ------------------------- Registration ------------------------- //
|
||||||
|
|
||||||
modifier validBet() {
|
modifier validBet(uint gameId) {
|
||||||
require(msg.value >= BET_MIN, "Minimum bet not met");
|
require(msg.value >= BET_MIN, "Minimum bet not met");
|
||||||
require(
|
require(
|
||||||
currentGame.initialBet == 0 || msg.value >= currentGame.initialBet,
|
games[gameId].initialBet == 0 ||
|
||||||
|
msg.value >= games[gameId].initialBet,
|
||||||
"Bet value too low"
|
"Bet value too low"
|
||||||
);
|
);
|
||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier notAlreadyRegistered() {
|
modifier notAlreadyInGame() {
|
||||||
require(
|
require(
|
||||||
msg.sender != currentGame.playerA.addr &&
|
playerToActiveGame[msg.sender] == 0,
|
||||||
msg.sender != currentGame.playerB.addr,
|
"Player already in an active game"
|
||||||
"Player already registered"
|
|
||||||
);
|
);
|
||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a player.
|
// Register a player to an existing game or create a new game.
|
||||||
// Return player's ID upon successful registration.
|
// If gameId is 0, player will join or create the first available game.
|
||||||
function register()
|
// Return player's ID and game ID upon successful registration.
|
||||||
|
function register(
|
||||||
|
uint gameId
|
||||||
|
)
|
||||||
public
|
public
|
||||||
payable
|
payable
|
||||||
validBet
|
validBet(gameId)
|
||||||
notAlreadyRegistered
|
notAlreadyInGame
|
||||||
returns (uint)
|
returns (uint playerId, uint returnGameId)
|
||||||
{
|
{
|
||||||
if (currentGame.playerA.addr == address(0x0)) {
|
// If gameId is 0, find an open game or create a new one
|
||||||
currentGame.playerA.addr = payable(msg.sender);
|
if (gameId == 0) {
|
||||||
currentGame.initialBet = msg.value;
|
gameId = findOrCreateGame();
|
||||||
return 1;
|
|
||||||
} else if (currentGame.playerB.addr == address(0x0)) {
|
|
||||||
currentGame.playerB.addr = payable(msg.sender);
|
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
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 ------------------------- //
|
// ------------------------- Commit ------------------------- //
|
||||||
|
|
||||||
modifier isRegistered() {
|
modifier isRegistered() {
|
||||||
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
|
require(gameId != 0, "Player not in any active game");
|
||||||
require(
|
require(
|
||||||
msg.sender == currentGame.playerA.addr ||
|
msg.sender == games[gameId].playerA.addr ||
|
||||||
msg.sender == currentGame.playerB.addr,
|
msg.sender == games[gameId].playerB.addr,
|
||||||
"Player not registered"
|
"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.
|
// Save player's encrypted move. encrMove must be "<1|2|3>-password" hashed with sha256.
|
||||||
// Return 'true' if move was valid, 'false' otherwise.
|
// Return 'true' if move was valid, 'false' otherwise.
|
||||||
function play(bytes32 encrMove) public isRegistered returns (bool) {
|
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
|
// Basic sanity checks with explicit errors to help debugging
|
||||||
require(encrMove != bytes32(0), "Encrypted move cannot be zero");
|
require(encrMove != bytes32(0), "Encrypted move cannot be zero");
|
||||||
// Ensure the caller hasn't already committed a move
|
// Ensure the caller hasn't already committed a move
|
||||||
if (msg.sender == currentGame.playerA.addr) {
|
if (msg.sender == game.playerA.addr) {
|
||||||
require(
|
require(
|
||||||
currentGame.playerA.encrMove == bytes32(0),
|
game.playerA.encrMove == bytes32(0),
|
||||||
"Player A already committed"
|
"Player A already committed"
|
||||||
);
|
);
|
||||||
currentGame.playerA.encrMove = encrMove;
|
game.playerA.encrMove = encrMove;
|
||||||
} else if (msg.sender == currentGame.playerB.addr) {
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
require(
|
require(
|
||||||
currentGame.playerB.encrMove == bytes32(0),
|
game.playerB.encrMove == bytes32(0),
|
||||||
"Player B already committed"
|
"Player B already committed"
|
||||||
);
|
);
|
||||||
currentGame.playerB.encrMove = encrMove;
|
game.playerB.encrMove = encrMove;
|
||||||
} else {
|
} else {
|
||||||
revert("Caller not registered");
|
revert("Caller not registered");
|
||||||
}
|
}
|
||||||
@@ -119,9 +186,11 @@ contract Game {
|
|||||||
// ------------------------- Reveal ------------------------- //
|
// ------------------------- Reveal ------------------------- //
|
||||||
|
|
||||||
modifier commitPhaseEnded() {
|
modifier commitPhaseEnded() {
|
||||||
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
|
require(gameId != 0, "Player not in any active game");
|
||||||
require(
|
require(
|
||||||
currentGame.playerA.encrMove != bytes32(0) &&
|
games[gameId].playerA.encrMove != bytes32(0) &&
|
||||||
currentGame.playerB.encrMove != bytes32(0),
|
games[gameId].playerB.encrMove != bytes32(0),
|
||||||
"Commit phase not ended"
|
"Commit phase not ended"
|
||||||
);
|
);
|
||||||
_;
|
_;
|
||||||
@@ -132,6 +201,9 @@ contract Game {
|
|||||||
function reveal(
|
function reveal(
|
||||||
string memory clearMove
|
string memory clearMove
|
||||||
) public isRegistered commitPhaseEnded returns (Moves) {
|
) 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")
|
bytes32 encrMove = sha256(abi.encodePacked(clearMove)); // Hash of clear input (= "move-password")
|
||||||
Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors)
|
Moves move = Moves(getFirstChar(clearMove)); // Actual move (Rock / Paper / Scissors)
|
||||||
|
|
||||||
@@ -140,22 +212,20 @@ contract Game {
|
|||||||
|
|
||||||
// If hashes match, clear move is saved
|
// If hashes match, clear move is saved
|
||||||
if (
|
if (
|
||||||
msg.sender == currentGame.playerA.addr &&
|
msg.sender == game.playerA.addr && encrMove == game.playerA.encrMove
|
||||||
encrMove == currentGame.playerA.encrMove
|
|
||||||
) {
|
) {
|
||||||
currentGame.playerA.move = move;
|
game.playerA.move = move;
|
||||||
} else if (
|
} else if (
|
||||||
msg.sender == currentGame.playerB.addr &&
|
msg.sender == game.playerB.addr && encrMove == game.playerB.encrMove
|
||||||
encrMove == currentGame.playerB.encrMove
|
|
||||||
) {
|
) {
|
||||||
currentGame.playerB.move = move;
|
game.playerB.move = move;
|
||||||
} else {
|
} else {
|
||||||
return Moves.None;
|
return Moves.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer starts after first revelation from one of the player
|
// Timer starts after first revelation from one of the player
|
||||||
if (currentGame.firstReveal == 0) {
|
if (game.firstReveal == 0) {
|
||||||
currentGame.firstReveal = block.timestamp;
|
game.firstReveal = block.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
return move;
|
return move;
|
||||||
@@ -183,11 +253,14 @@ contract Game {
|
|||||||
// ------------------------- Result ------------------------- //
|
// ------------------------- Result ------------------------- //
|
||||||
|
|
||||||
modifier revealPhaseEnded() {
|
modifier revealPhaseEnded() {
|
||||||
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
|
require(gameId != 0, "Player not in any active game");
|
||||||
require(
|
require(
|
||||||
(currentGame.playerA.move != Moves.None &&
|
(games[gameId].playerA.move != Moves.None &&
|
||||||
currentGame.playerB.move != Moves.None) ||
|
games[gameId].playerB.move != Moves.None) ||
|
||||||
(currentGame.firstReveal != 0 &&
|
(games[gameId].firstReveal != 0 &&
|
||||||
block.timestamp > currentGame.firstReveal + REVEAL_TIMEOUT),
|
block.timestamp >
|
||||||
|
games[gameId].firstReveal + REVEAL_TIMEOUT),
|
||||||
"Reveal phase not ended"
|
"Reveal phase not ended"
|
||||||
);
|
);
|
||||||
_;
|
_;
|
||||||
@@ -196,25 +269,24 @@ contract Game {
|
|||||||
// Compute the outcome and pay the winner(s).
|
// Compute the outcome and pay the winner(s).
|
||||||
// Return the outcome.
|
// Return the outcome.
|
||||||
function getOutcome() public revealPhaseEnded returns (Outcomes) {
|
function getOutcome() public revealPhaseEnded returns (Outcomes) {
|
||||||
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
// Only calculate outcome once
|
// Only calculate outcome once
|
||||||
require(
|
require(game.outcome == Outcomes.None, "Outcome already determined");
|
||||||
currentGame.outcome == Outcomes.None,
|
|
||||||
"Outcome already determined"
|
|
||||||
);
|
|
||||||
|
|
||||||
Outcomes outcome;
|
Outcomes outcome;
|
||||||
|
|
||||||
if (currentGame.playerA.move == currentGame.playerB.move) {
|
if (game.playerA.move == game.playerB.move) {
|
||||||
outcome = Outcomes.Draw;
|
outcome = Outcomes.Draw;
|
||||||
} else if (
|
} else if (
|
||||||
(currentGame.playerA.move == Moves.Rock &&
|
(game.playerA.move == Moves.Rock &&
|
||||||
currentGame.playerB.move == Moves.Scissors) ||
|
game.playerB.move == Moves.Scissors) ||
|
||||||
(currentGame.playerA.move == Moves.Paper &&
|
(game.playerA.move == Moves.Paper &&
|
||||||
currentGame.playerB.move == Moves.Rock) ||
|
game.playerB.move == Moves.Rock) ||
|
||||||
(currentGame.playerA.move == Moves.Scissors &&
|
(game.playerA.move == Moves.Scissors &&
|
||||||
currentGame.playerB.move == Moves.Paper) ||
|
game.playerB.move == Moves.Paper) ||
|
||||||
(currentGame.playerA.move != Moves.None &&
|
(game.playerA.move != Moves.None && game.playerB.move == Moves.None)
|
||||||
currentGame.playerB.move == Moves.None)
|
|
||||||
) {
|
) {
|
||||||
outcome = Outcomes.PlayerA;
|
outcome = Outcomes.PlayerA;
|
||||||
} else {
|
} else {
|
||||||
@@ -222,12 +294,17 @@ contract Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the outcome permanently before resetting
|
// Store the outcome permanently before resetting
|
||||||
currentGame.outcome = outcome;
|
game.outcome = outcome;
|
||||||
|
|
||||||
address payable addrA = currentGame.playerA.addr;
|
address payable addrA = game.playerA.addr;
|
||||||
address payable addrB = currentGame.playerB.addr;
|
address payable addrB = game.playerB.addr;
|
||||||
uint betPlayerA = currentGame.initialBet;
|
uint betPlayerA = game.initialBet;
|
||||||
reset(); // Reset game before paying to avoid reentrancy attacks
|
|
||||||
|
// 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);
|
pay(addrA, addrB, betPlayerA, outcome);
|
||||||
|
|
||||||
return outcome;
|
return outcome;
|
||||||
@@ -240,31 +317,33 @@ contract Game {
|
|||||||
uint betPlayerA,
|
uint betPlayerA,
|
||||||
Outcomes outcome
|
Outcomes outcome
|
||||||
) private {
|
) private {
|
||||||
// Uncomment lines below if you need to adjust the gas limit
|
|
||||||
if (outcome == Outcomes.PlayerA) {
|
if (outcome == Outcomes.PlayerA) {
|
||||||
addrA.transfer(address(this).balance);
|
addrA.transfer(address(this).balance);
|
||||||
// addrA.call.value(address(this).balance).gas(1000000)("");
|
|
||||||
} else if (outcome == Outcomes.PlayerB) {
|
} else if (outcome == Outcomes.PlayerB) {
|
||||||
addrB.transfer(address(this).balance);
|
addrB.transfer(address(this).balance);
|
||||||
// addrB.call.value(address(this).balance).gas(1000000)("");
|
|
||||||
} else {
|
} else {
|
||||||
addrA.transfer(betPlayerA);
|
addrA.transfer(betPlayerA);
|
||||||
addrB.transfer(address(this).balance);
|
addrB.transfer(address(this).balance);
|
||||||
// addrA.call.value(betPlayerA).gas(1000000)("");
|
|
||||||
// addrB.call.value(address(this).balance).gas(1000000)("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset the game.
|
// Reset a specific game.
|
||||||
function reset() private {
|
function resetGame(uint gameId) private {
|
||||||
currentGame.initialBet = 0;
|
GameState storage game = games[gameId];
|
||||||
currentGame.firstReveal = 0;
|
|
||||||
currentGame.playerA.addr = payable(address(0x0));
|
// Clear player mappings
|
||||||
currentGame.playerB.addr = payable(address(0x0));
|
if (game.playerA.addr != address(0x0)) {
|
||||||
currentGame.playerA.encrMove = 0x0;
|
playerToActiveGame[game.playerA.addr] = 0;
|
||||||
currentGame.playerB.encrMove = 0x0;
|
}
|
||||||
currentGame.playerA.move = Moves.None;
|
if (game.playerB.addr != address(0x0)) {
|
||||||
currentGame.playerB.move = Moves.None;
|
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 ------------------------- //
|
// ------------------------- Helpers ------------------------- //
|
||||||
@@ -274,51 +353,160 @@ contract Game {
|
|||||||
return address(this).balance;
|
return address(this).balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return player's ID
|
// Return player's ID in their active game
|
||||||
function whoAmI() public view returns (uint) {
|
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;
|
return 1;
|
||||||
} else if (msg.sender == currentGame.playerB.addr) {
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
return 2;
|
return 2;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
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.
|
// Return 'true' if both players have commited a move, 'false' otherwise.
|
||||||
function bothPlayed() public view returns (bool) {
|
function bothPlayed() public view returns (bool) {
|
||||||
return (currentGame.playerA.encrMove != 0x0 &&
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
currentGame.playerB.encrMove != 0x0);
|
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.
|
// Return 'true' if both players have revealed their move, 'false' otherwise.
|
||||||
function bothRevealed() public view returns (bool) {
|
function bothRevealed() public view returns (bool) {
|
||||||
return (currentGame.playerA.move != Moves.None &&
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
currentGame.playerB.move != Moves.None);
|
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.
|
// Return 'true' if player A has revealed their move, 'false' otherwise.
|
||||||
function playerARevealed() public view returns (bool) {
|
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.
|
// Return 'true' if player B has revealed their move, 'false' otherwise.
|
||||||
function playerBRevealed() public view returns (bool) {
|
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.
|
// Return time left before the end of the revelation phase.
|
||||||
function revealTimeLeft() public view returns (int) {
|
function revealTimeLeft() public view returns (int) {
|
||||||
if (currentGame.firstReveal != 0) {
|
uint gameId = playerToActiveGame[msg.sender];
|
||||||
return
|
if (gameId == 0) return int(REVEAL_TIMEOUT);
|
||||||
int(
|
|
||||||
(currentGame.firstReveal + REVEAL_TIMEOUT) - block.timestamp
|
GameState storage game = games[gameId];
|
||||||
);
|
if (game.firstReveal != 0) {
|
||||||
|
return int((game.firstReveal + REVEAL_TIMEOUT) - block.timestamp);
|
||||||
}
|
}
|
||||||
return int(REVEAL_TIMEOUT);
|
return int(REVEAL_TIMEOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastWinner() public view returns (Outcomes) {
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
168
docs/MULTI_GAME_IMPLEMENTATION.md
Normal file
168
docs/MULTI_GAME_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Multi-Game Implementation Summary
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
|
||||||
257
docs/data-structure-diagram.md
Normal file
257
docs/data-structure-diagram.md
Normal file
@@ -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)
|
||||||
|
```
|
||||||
BIN
docs/res/Gameloop.jpg
Normal file
BIN
docs/res/Gameloop.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
Reference in New Issue
Block a user