add first implementation of game contract

This commit is contained in:
SamKry
2025-11-14 19:35:58 +01:00
parent bd97cc096d
commit 37bce9cb5d

View File

@@ -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;
}
}