From 37bce9cb5d3e5d04934741dd8bf3afb8f72caac1 Mon Sep 17 00:00:00 2001 From: SamKry <87971518+SamKry@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:35:58 +0100 Subject: [PATCH] add first implementation of game contract --- crypto_clash_contract/contracts/Game.sol | 324 +++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 crypto_clash_contract/contracts/Game.sol diff --git a/crypto_clash_contract/contracts/Game.sol b/crypto_clash_contract/contracts/Game.sol new file mode 100644 index 0000000..48e354c --- /dev/null +++ b/crypto_clash_contract/contracts/Game.sol @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.30; + +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; + } + + GameState private currentGame; + + GameState[] private pastGames; + GameState[] private activeGames; + + // ------------------------- Registration ------------------------- // + + modifier validBet() { + require(msg.value >= BET_MIN, "Minimum bet not met"); + require( + currentGame.initialBet == 0 || msg.value >= currentGame.initialBet, + "Bet value too low" + ); + _; + } + + modifier notAlreadyRegistered() { + require( + msg.sender != currentGame.playerA.addr && + msg.sender != currentGame.playerB.addr, + "Player already registered" + ); + _; + } + + // Register a player. + // Return player's ID upon successful registration. + function register() + public + payable + validBet + notAlreadyRegistered + returns (uint) + { + 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; + } + return 0; + } + + // ------------------------- Commit ------------------------- // + + modifier isRegistered() { + require( + msg.sender == currentGame.playerA.addr || + msg.sender == currentGame.playerB.addr, + "Player not registered" + ); + _; + } + + // 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) { + // 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) { + require( + currentGame.playerA.encrMove == bytes32(0), + "Player A already committed" + ); + currentGame.playerA.encrMove = encrMove; + } else if (msg.sender == currentGame.playerB.addr) { + require( + currentGame.playerB.encrMove == bytes32(0), + "Player B already committed" + ); + currentGame.playerB.encrMove = encrMove; + } else { + revert("Caller not registered"); + } + return true; + } + + // ------------------------- Reveal ------------------------- // + + modifier commitPhaseEnded() { + require( + currentGame.playerA.encrMove != bytes32(0) && + currentGame.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) { + 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 == currentGame.playerA.addr && + encrMove == currentGame.playerA.encrMove + ) { + currentGame.playerA.move = move; + } else if ( + msg.sender == currentGame.playerB.addr && + encrMove == currentGame.playerB.encrMove + ) { + currentGame.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; + } + + 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() { + require( + (currentGame.playerA.move != Moves.None && + currentGame.playerB.move != Moves.None) || + (currentGame.firstReveal != 0 && + block.timestamp > currentGame.firstReveal + REVEAL_TIMEOUT), + "Reveal phase not ended" + ); + _; + } + + // Compute the outcome and pay the winner(s). + // Return the outcome. + function getOutcome() public revealPhaseEnded returns (Outcomes) { + // Only calculate outcome once + require( + currentGame.outcome == Outcomes.None, + "Outcome already determined" + ); + + Outcomes outcome; + + if (currentGame.playerA.move == currentGame.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) + ) { + outcome = Outcomes.PlayerA; + } else { + outcome = Outcomes.PlayerB; + } + + // Store the outcome permanently before resetting + currentGame.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 + pay(addrA, addrB, betPlayerA, outcome); + + return outcome; + } + + // Pay the winner(s). + function pay( + address payable addrA, + address payable addrB, + 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; + } + + // ------------------------- Helpers ------------------------- // + + // Return contract balance + function getContractBalance() public view returns (uint) { + return address(this).balance; + } + + // Return player's ID + function whoAmI() public view returns (uint) { + if (msg.sender == currentGame.playerA.addr) { + return 1; + } else if (msg.sender == currentGame.playerB.addr) { + return 2; + } else { + return 0; + } + } + + // 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); + } + + // 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); + } + + // Return 'true' if player A has revealed their move, 'false' otherwise. + function playerARevealed() public view returns (bool) { + return (currentGame.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); + } + + // 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 + ); + } + return int(REVEAL_TIMEOUT); + } + + function getLastWinner() public view returns (Outcomes) { + return currentGame.outcome; + } +}