mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 10:58: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