mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
304 lines
13 KiB
Solidity
304 lines
13 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity >=0.7.3;
|
|
|
|
contract GameMinusOne {
|
|
uint constant BET_MIN = 1e16;
|
|
uint constant REVEAL_TIMEOUT = 10 minutes;
|
|
uint constant COMMIT_TIMEOUT = 10 minutes;
|
|
uint constant WITHDRAW_TIMEOUT = 10 minutes;
|
|
|
|
enum Moves { None, Rock, Paper, Scissors }
|
|
enum Outcomes { None, A, B, D, AT, BT }
|
|
enum GamePhase { Reg, InitC, FirstR, WithdC, WithdR, Done }
|
|
|
|
struct Player {
|
|
address payable addr;
|
|
uint bet;
|
|
bytes32 hash1;
|
|
bytes32 hash2;
|
|
Moves move1;
|
|
Moves move2;
|
|
bytes32 wHash;
|
|
uint withdrawn;
|
|
string nick;
|
|
}
|
|
|
|
struct GameState {
|
|
Player pA;
|
|
Player pB;
|
|
Outcomes outcome;
|
|
GamePhase phase;
|
|
uint tCommit;
|
|
uint tReveal;
|
|
uint tWithdC;
|
|
uint tWithdR;
|
|
uint bet;
|
|
uint gameId;
|
|
bool active;
|
|
string mode;
|
|
}
|
|
|
|
mapping(uint => GameState) private games;
|
|
uint[] private gameIds;
|
|
uint private nextId = 1;
|
|
|
|
modifier validBet(uint gameId) {
|
|
require(msg.value >= BET_MIN, "Bet below minimum");
|
|
require(games[gameId].bet == 0 || msg.value == games[gameId].bet, "Bet must match initial");
|
|
_;
|
|
}
|
|
|
|
function register(uint gameId, string memory name) public payable validBet(gameId) returns (uint, uint) {
|
|
if (gameId == 0) gameId = createNewGame();
|
|
require(games[gameId].active, "Game not active");
|
|
require(games[gameId].phase == GamePhase.Reg, "Game started");
|
|
require(bytes(name).length > 0 && bytes(name).length <= 20, "Invalid nickname");
|
|
GameState storage game = games[gameId];
|
|
if (game.pA.addr == address(0)) {
|
|
game.pA.addr = payable(msg.sender);
|
|
game.pA.nick = name;
|
|
game.bet = msg.value;
|
|
return (1, gameId);
|
|
} else if (game.pB.addr == address(0)) {
|
|
require(msg.sender != game.pA.addr, "Cannot play yourself");
|
|
game.pB.addr = payable(msg.sender);
|
|
game.pB.nick = name;
|
|
game.phase = GamePhase.InitC;
|
|
return (2, gameId);
|
|
}
|
|
revert("Game full");
|
|
}
|
|
|
|
function createNewGame() private returns (uint) {
|
|
uint gameId = nextId++;
|
|
games[gameId].gameId = gameId;
|
|
games[gameId].active = true;
|
|
games[gameId].phase = GamePhase.Reg;
|
|
games[gameId].mode = "minusone";
|
|
gameIds.push(gameId);
|
|
return gameId;
|
|
}
|
|
|
|
modifier isRegistered(uint gameId) {
|
|
require(gameId != 0, "Invalid game ID");
|
|
require(msg.sender == games[gameId].pA.addr || msg.sender == games[gameId].pB.addr, "Not registered");
|
|
_;
|
|
}
|
|
|
|
function doCommit(uint gameId, bytes32 hash1, bytes32 hash2, uint8 mode) private {
|
|
GameState storage game = games[gameId];
|
|
require(game.active && hash1 != bytes32(0), "Invalid hash");
|
|
bool isPlayerA = msg.sender == game.pA.addr;
|
|
if (mode == 0) {
|
|
require(game.phase == GamePhase.InitC, "Wrong phase");
|
|
require(hash2 != bytes32(0) && hash1 != hash2, "Moves must differ");
|
|
if (game.tCommit != 0) require(block.timestamp <= game.tCommit + COMMIT_TIMEOUT, "Commit timeout");
|
|
if (game.tCommit == 0) game.tCommit = block.timestamp;
|
|
if (isPlayerA) {
|
|
require(game.pA.hash1 == bytes32(0), "Already committed");
|
|
game.pA.hash1 = hash1; game.pA.hash2 = hash2;
|
|
} else {
|
|
require(game.pB.hash1 == bytes32(0), "Already committed");
|
|
game.pB.hash1 = hash1; game.pB.hash2 = hash2;
|
|
}
|
|
if (game.pA.hash1 != bytes32(0) && game.pB.hash1 != bytes32(0)) { game.phase = GamePhase.FirstR; game.tReveal = 0; }
|
|
} else {
|
|
require(game.phase == GamePhase.WithdC, "Wrong phase");
|
|
if (game.tWithdC != 0) require(block.timestamp <= game.tWithdC + COMMIT_TIMEOUT, "Commit timeout");
|
|
if (game.tWithdC == 0) game.tWithdC = block.timestamp;
|
|
if (isPlayerA) {
|
|
require(game.pA.wHash == bytes32(0), "Already committed");
|
|
game.pA.wHash = hash1;
|
|
} else {
|
|
require(game.pB.wHash == bytes32(0), "Already committed");
|
|
game.pB.wHash = hash1;
|
|
}
|
|
if (game.pA.wHash != bytes32(0) && game.pB.wHash != bytes32(0)) { game.phase = GamePhase.WithdR; game.tWithdR = 0; }
|
|
}
|
|
}
|
|
|
|
function commitInitialMoves(uint id_, bytes32 e1_, bytes32 e2_) public isRegistered(id_) returns (bool) {
|
|
doCommit(id_, e1_, e2_, 0);
|
|
return true;
|
|
}
|
|
|
|
function revealInitialMoves(uint gameId, string memory clear1, string memory clear2) public isRegistered(gameId) returns (Moves, Moves) {
|
|
GameState storage game = games[gameId];
|
|
require(game.active, "Game not active");
|
|
require(game.phase == GamePhase.FirstR, "Wrong phase");
|
|
if (game.tReveal != 0) require(block.timestamp <= game.tReveal + REVEAL_TIMEOUT, "Reveal timeout");
|
|
bytes32 hash1 = keccak256(abi.encodePacked(clear1));
|
|
bytes32 hash2 = keccak256(abi.encodePacked(clear2));
|
|
Moves move1 = Moves(getFirstChar(clear1));
|
|
Moves move2 = Moves(getFirstChar(clear2));
|
|
require(move1 != Moves.None && move2 != Moves.None, "Invalid moves");
|
|
require(move1 != move2, "Moves must differ");
|
|
if (msg.sender == game.pA.addr) {
|
|
require(hash1 == game.pA.hash1 && hash2 == game.pA.hash2, "Hash mismatch");
|
|
game.pA.move1 = move1;
|
|
game.pA.move2 = move2;
|
|
} else if (msg.sender == game.pB.addr) {
|
|
require(hash1 == game.pB.hash1 && hash2 == game.pB.hash2, "Hash mismatch");
|
|
game.pB.move1 = move1;
|
|
game.pB.move2 = move2;
|
|
} else revert("Not registered");
|
|
if (game.tReveal == 0) game.tReveal = block.timestamp;
|
|
if (game.pA.move1 != Moves.None && game.pB.move1 != Moves.None) {
|
|
game.phase = GamePhase.WithdC;
|
|
game.tWithdC = 0;
|
|
}
|
|
return (move1, move2);
|
|
}
|
|
|
|
function commitWithdraw(uint gameId, bytes32 wHash) public isRegistered(gameId) returns (bool) {
|
|
doCommit(gameId, wHash, bytes32(0), 1);
|
|
return true;
|
|
}
|
|
|
|
function withdrawMove(uint gameId, string memory clear) public isRegistered(gameId) returns (uint) {
|
|
GameState storage game = games[gameId];
|
|
require(game.active, "Game not active");
|
|
require(game.phase == GamePhase.WithdR, "Wrong phase");
|
|
if (game.tWithdR != 0) require(block.timestamp <= game.tWithdR + REVEAL_TIMEOUT, "Reveal timeout");
|
|
bytes32 wHash = keccak256(abi.encodePacked(clear));
|
|
uint idx = getFirstChar(clear);
|
|
require(idx == 1 || idx == 2, "Index must be 1 or 2");
|
|
if (msg.sender == game.pA.addr) {
|
|
require(wHash == game.pA.wHash, "Hash mismatch");
|
|
require(game.pA.withdrawn == 0, "Already withdrew");
|
|
game.pA.withdrawn = idx;
|
|
} else if (msg.sender == game.pB.addr) {
|
|
require(wHash == game.pB.wHash, "Hash mismatch");
|
|
require(game.pB.withdrawn == 0, "Already withdrew");
|
|
game.pB.withdrawn = idx;
|
|
} else revert("Not registered");
|
|
if (game.tWithdR == 0) game.tWithdR = block.timestamp;
|
|
if (game.pA.withdrawn != 0 && game.pB.withdrawn != 0) determineOutcome(gameId);
|
|
return idx;
|
|
}
|
|
|
|
|
|
function determineOutcome(uint gameId) private {
|
|
GameState storage game = games[gameId];
|
|
Moves finalA = game.pA.withdrawn == 1 ? game.pA.move2 : game.pA.move1;
|
|
Moves finalB = game.pB.withdrawn == 1 ? game.pB.move2 : game.pB.move1;
|
|
if (finalA == finalB) game.outcome = Outcomes.D;
|
|
else if ((finalA == Moves.Rock && finalB == Moves.Scissors) ||
|
|
(finalA == Moves.Paper && finalB == Moves.Rock) ||
|
|
(finalA == Moves.Scissors && finalB == Moves.Paper))
|
|
game.outcome = Outcomes.A;
|
|
else game.outcome = Outcomes.B;
|
|
game.phase = GamePhase.Done;
|
|
}
|
|
|
|
function getFirstChar(string memory str) private pure returns (uint) {
|
|
bytes memory b = bytes(str);
|
|
if (b.length == 0) return 0;
|
|
bytes1 first = b[0];
|
|
if (first == 0x31) return 1;
|
|
if (first == 0x32) return 2;
|
|
if (first == 0x33) return 3;
|
|
return 0;
|
|
}
|
|
|
|
function getOutcome(uint gameId) public returns (Outcomes) {
|
|
GameState storage game = games[gameId];
|
|
require(game.active, "Game not active");
|
|
require(game.phase == GamePhase.Done, "Game not finished");
|
|
require(game.outcome != Outcomes.None, "No outcome yet");
|
|
address payable addrA = game.pA.addr;
|
|
address payable addrB = game.pB.addr;
|
|
uint betAmount = game.bet;
|
|
Outcomes result = game.outcome;
|
|
resetGame(gameId);
|
|
pay(addrA, addrB, betAmount, result);
|
|
return result;
|
|
}
|
|
|
|
function pay(address payable addrA, address payable addrB, uint bet, Outcomes outcome) private {
|
|
if (outcome == Outcomes.A) addrA.transfer(bet * 2);
|
|
else if (outcome == Outcomes.B) addrB.transfer(bet * 2);
|
|
else { addrA.transfer(bet); addrB.transfer(bet); }
|
|
}
|
|
|
|
function payWithSlash(address payable winner, address payable, uint bet) private {
|
|
winner.transfer(bet * 2);
|
|
}
|
|
|
|
function resetGame(uint gameId) private {
|
|
games[gameId].active = false;
|
|
}
|
|
|
|
function getContractBalance() public view returns (uint) {
|
|
return address(this).balance;
|
|
}
|
|
|
|
function whoAmI(uint gameId) public view returns (uint) {
|
|
if (gameId == 0) return 0;
|
|
GameState storage game = games[gameId];
|
|
if (msg.sender == game.pA.addr) return 1;
|
|
if (msg.sender == game.pB.addr) return 2;
|
|
return 0;
|
|
}
|
|
|
|
function getGamePhase(uint gameId) public view returns (GamePhase) {
|
|
return games[gameId].phase;
|
|
}
|
|
|
|
function getTimeLeft(uint gameId) public view returns (int) {
|
|
if (gameId == 0) return 0;
|
|
GameState storage game = games[gameId];
|
|
uint timeout; uint start;
|
|
if (game.phase == GamePhase.InitC && game.tCommit != 0) { timeout = COMMIT_TIMEOUT; start = game.tCommit; }
|
|
else if (game.phase == GamePhase.FirstR && game.tReveal != 0) { timeout = REVEAL_TIMEOUT; start = game.tReveal; }
|
|
else if (game.phase == GamePhase.WithdC && game.tWithdC != 0) { timeout = COMMIT_TIMEOUT; start = game.tWithdC; }
|
|
else if (game.phase == GamePhase.WithdR && game.tWithdR != 0) { timeout = REVEAL_TIMEOUT; start = game.tWithdR; }
|
|
else return int(COMMIT_TIMEOUT);
|
|
uint deadline = start + timeout;
|
|
if (block.timestamp >= deadline) return 0;
|
|
return int(deadline - block.timestamp);
|
|
}
|
|
|
|
function resolveTimeout(uint gameId) public isRegistered(gameId) {
|
|
GameState storage game = games[gameId];
|
|
require(game.active, "Game not active");
|
|
address caller = msg.sender;
|
|
bool isPlayerA = caller == game.pA.addr;
|
|
bool timedOut = false;
|
|
|
|
if (game.phase == GamePhase.InitC && game.tCommit != 0 && block.timestamp > game.tCommit + COMMIT_TIMEOUT) {
|
|
timedOut = (isPlayerA && game.pB.hash1 == bytes32(0)) || (!isPlayerA && game.pA.hash1 == bytes32(0));
|
|
} else if (game.phase == GamePhase.FirstR && game.tReveal != 0 && block.timestamp > game.tReveal + REVEAL_TIMEOUT) {
|
|
timedOut = (isPlayerA && game.pB.move1 == Moves.None) || (!isPlayerA && game.pA.move1 == Moves.None);
|
|
} else if (game.phase == GamePhase.WithdC && game.tWithdC != 0 && block.timestamp > game.tWithdC + COMMIT_TIMEOUT) {
|
|
timedOut = (isPlayerA && game.pB.wHash == bytes32(0)) || (!isPlayerA && game.pA.wHash == bytes32(0));
|
|
} else if (game.phase == GamePhase.WithdR && game.tWithdR != 0 && block.timestamp > game.tWithdR + REVEAL_TIMEOUT) {
|
|
timedOut = (isPlayerA && game.pB.withdrawn == 0) || (!isPlayerA && game.pA.withdrawn == 0);
|
|
}
|
|
|
|
require(timedOut, "No timeout or invalid caller");
|
|
game.outcome = isPlayerA ? Outcomes.A : Outcomes.B;
|
|
address payable winner = payable(caller);
|
|
address payable loser = isPlayerA ? game.pB.addr : game.pA.addr;
|
|
uint betAmount = game.bet;
|
|
resetGame(gameId);
|
|
payWithSlash(winner, loser, betAmount);
|
|
}
|
|
|
|
function getGameDetails(uint gameId) public view returns (
|
|
Player memory, Player memory, uint, Outcomes, GamePhase, bool, uint, string memory) {
|
|
GameState storage game = games[gameId];
|
|
require(game.gameId != 0, "Game does not exist");
|
|
return (game.pA, game.pB, game.bet, game.outcome, game.phase, game.active, game.gameId, game.mode);
|
|
}
|
|
|
|
function getActiveGameIds() public view returns (uint[] memory) {
|
|
uint count = 0;
|
|
for (uint i = 0; i < gameIds.length; i++) if (games[gameIds[i]].active) count++;
|
|
uint[] memory active = new uint[](count);
|
|
uint idx = 0;
|
|
for (uint i = 0; i < gameIds.length; i++) if (games[gameIds[i]].active) active[idx++] = gameIds[i];
|
|
return active;
|
|
}
|
|
}
|