mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
add first implementation of game contract
This commit is contained in:
324
crypto_clash_contract/contracts/Game.sol
Normal file
324
crypto_clash_contract/contracts/Game.sol
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user