mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
513 lines
15 KiB
Solidity
513 lines
15 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
|
|
pragma solidity >=0.7.3;
|
|
|
|
contract Game {
|
|
uint public constant BET_MIN = 1e16; // The minimum bet (1 BLD)
|
|
uint public constant REVEAL_TIMEOUT = 10 minutes; // Max delay of revelation phase
|
|
|
|
enum Moves {
|
|
None,
|
|
Rock,
|
|
Paper,
|
|
Scissors
|
|
}
|
|
|
|
enum Outcomes {
|
|
None,
|
|
PlayerA,
|
|
PlayerB,
|
|
Draw
|
|
} // Possible outcomes
|
|
|
|
struct Player {
|
|
address payable addr;
|
|
uint bet;
|
|
bytes32 encrMove;
|
|
Moves move;
|
|
string nickname;
|
|
}
|
|
|
|
struct GameState {
|
|
Player playerA;
|
|
Player playerB;
|
|
Outcomes outcome;
|
|
uint firstReveal;
|
|
uint initialBet;
|
|
uint gameId;
|
|
bool isActive;
|
|
}
|
|
|
|
// 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;
|
|
|
|
// ------------------------- Registration ------------------------- //
|
|
|
|
modifier validBet(uint gameId) {
|
|
require(msg.value >= BET_MIN, "Minimum bet not met");
|
|
require(
|
|
games[gameId].initialBet == 0 ||
|
|
msg.value >= games[gameId].initialBet,
|
|
"Bet value too low"
|
|
);
|
|
_;
|
|
}
|
|
|
|
modifier notAlreadyInGame() {
|
|
require(
|
|
playerToActiveGame[msg.sender] == 0,
|
|
"Player already in an active game"
|
|
);
|
|
_;
|
|
}
|
|
|
|
// 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(gameId)
|
|
notAlreadyInGame
|
|
returns (uint playerId, uint returnGameId)
|
|
{
|
|
// If gameId is 0, find an open game or create a new one
|
|
if (gameId == 0) {
|
|
gameId = findOrCreateGame();
|
|
}
|
|
|
|
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 == games[gameId].playerA.addr ||
|
|
msg.sender == games[gameId].playerB.addr,
|
|
"Player not registered in this 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 == game.playerA.addr) {
|
|
require(
|
|
game.playerA.encrMove == bytes32(0),
|
|
"Player A already committed"
|
|
);
|
|
game.playerA.encrMove = encrMove;
|
|
} else if (msg.sender == game.playerB.addr) {
|
|
require(
|
|
game.playerB.encrMove == bytes32(0),
|
|
"Player B already committed"
|
|
);
|
|
game.playerB.encrMove = encrMove;
|
|
} else {
|
|
revert("Caller not registered");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ------------------------- Reveal ------------------------- //
|
|
|
|
modifier commitPhaseEnded() {
|
|
uint gameId = playerToActiveGame[msg.sender];
|
|
require(gameId != 0, "Player not in any active game");
|
|
require(
|
|
games[gameId].playerA.encrMove != bytes32(0) &&
|
|
games[gameId].playerB.encrMove != bytes32(0),
|
|
"Commit phase not ended"
|
|
);
|
|
_;
|
|
}
|
|
|
|
// Compare clear move given by the player with saved encrypted move.
|
|
// Return clear move upon success, 'Moves.None' otherwise.
|
|
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)
|
|
|
|
// If move invalid, exit
|
|
require(move != Moves.None, "Invalid move");
|
|
|
|
// If hashes match, clear move is saved
|
|
if (
|
|
msg.sender == game.playerA.addr && encrMove == game.playerA.encrMove
|
|
) {
|
|
game.playerA.move = move;
|
|
} else if (
|
|
msg.sender == game.playerB.addr && encrMove == game.playerB.encrMove
|
|
) {
|
|
game.playerB.move = move;
|
|
} else {
|
|
return Moves.None;
|
|
}
|
|
|
|
// Timer starts after first revelation from one of the player
|
|
if (game.firstReveal == 0) {
|
|
game.firstReveal = block.timestamp;
|
|
}
|
|
|
|
return move;
|
|
}
|
|
|
|
// Return first character of a given string.
|
|
// Returns 0 if the string is empty or the first character is not '1','2' or '3'.
|
|
function getFirstChar(string memory str) private pure returns (uint) {
|
|
bytes memory b = bytes(str);
|
|
if (b.length == 0) {
|
|
return 0;
|
|
}
|
|
bytes1 firstByte = b[0];
|
|
if (firstByte == 0x31) {
|
|
return 1;
|
|
} else if (firstByte == 0x32) {
|
|
return 2;
|
|
} else if (firstByte == 0x33) {
|
|
return 3;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// ------------------------- Result ------------------------- //
|
|
|
|
modifier revealPhaseEnded() {
|
|
uint gameId = playerToActiveGame[msg.sender];
|
|
require(gameId != 0, "Player not in any active game");
|
|
require(
|
|
(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"
|
|
);
|
|
_;
|
|
}
|
|
|
|
// 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(game.outcome == Outcomes.None, "Outcome already determined");
|
|
|
|
Outcomes outcome;
|
|
|
|
if (game.playerA.move == game.playerB.move) {
|
|
outcome = Outcomes.Draw;
|
|
} else if (
|
|
(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 {
|
|
outcome = Outcomes.PlayerB;
|
|
}
|
|
|
|
// Store the outcome permanently before resetting
|
|
game.outcome = outcome;
|
|
|
|
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;
|
|
}
|
|
|
|
// Pay the winner(s).
|
|
function pay(
|
|
address payable addrA,
|
|
address payable addrB,
|
|
uint betPlayerA,
|
|
Outcomes outcome
|
|
) private {
|
|
if (outcome == Outcomes.PlayerA) {
|
|
addrA.transfer(address(this).balance);
|
|
} else if (outcome == Outcomes.PlayerB) {
|
|
addrB.transfer(address(this).balance);
|
|
} else {
|
|
addrA.transfer(betPlayerA);
|
|
addrB.transfer(address(this).balance);
|
|
}
|
|
}
|
|
|
|
// 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 ------------------------- //
|
|
|
|
// Return contract balance
|
|
function getContractBalance() public view returns (uint) {
|
|
return address(this).balance;
|
|
}
|
|
|
|
// Return player's ID in their active game
|
|
function whoAmI() public view returns (uint) {
|
|
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 == 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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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
|
|
);
|
|
}
|
|
}
|