diff --git a/crypto_clash_contract/contracts/GameMinusOne.sol b/crypto_clash_contract/contracts/GameMinusOne.sol index a8437ef..5b92ea3 100644 --- a/crypto_clash_contract/contracts/GameMinusOne.sol +++ b/crypto_clash_contract/contracts/GameMinusOne.sol @@ -9,420 +9,295 @@ contract GameMinusOne { enum Moves { None, Rock, Paper, Scissors } enum Outcomes { None, A, B, D, AT, BT } - enum GamePhase { Reg, InitC, FirstR, WithdC, WithdR, FinalC, FinalR, Done } + enum GamePhase { Reg, InitC, FirstR, WithdC, WithdR, Done } struct Player { address payable addr; uint bet; - bytes32 e1; - bytes32 e2; - Moves m1; - Moves m2; - bytes32 ew; - uint w; - bytes32 ef; - Moves mf; - string n; + bytes32 hash1; + bytes32 hash2; + Moves move1; + Moves move2; + bytes32 wHash; + uint withdrawn; + string nick; } struct GameState { - Player a; - Player b; - Outcomes o; - GamePhase p; - uint fc; - uint fr; - uint fwc; - uint fwr; - uint ffc; - uint ffr; - uint ib; - uint id; - bool act; + 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 g; - uint[] private ids; + mapping(uint => GameState) private games; + uint[] private gameIds; uint private nextId = 1; - modifier validBet(uint id_) { - require(msg.value >= BET_MIN, "min"); - require(g[id_].ib == 0 || msg.value == g[id_].ib, "bet"); + 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 id_, string memory n_) public payable validBet(id_) returns (uint, uint) { - if (id_ == 0) id_ = createNewGame(); - require(g[id_].act, "act"); - require(g[id_].p == GamePhase.Reg, "st"); - require(bytes(n_).length > 0 && bytes(n_).length <= 20, "n"); - GameState storage gm = g[id_]; - if (gm.a.addr == address(0)) { - gm.a.addr = payable(msg.sender); - gm.a.n = n_; - gm.ib = msg.value; - return (1, id_); - } else if (gm.b.addr == address(0)) { - require(msg.sender != gm.a.addr, "self"); - gm.b.addr = payable(msg.sender); - gm.b.n = n_; - gm.p = GamePhase.InitC; - return (2, id_); + 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("full"); + revert("Game full"); } function createNewGame() private returns (uint) { - uint id_ = nextId++; - g[id_].id = id_; - g[id_].act = true; - g[id_].p = GamePhase.Reg; - g[id_].mode = "minusone"; - ids.push(id_); - return id_; + 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 id_) { - require(id_ != 0, "id"); - require(msg.sender == g[id_].a.addr || msg.sender == g[id_].b.addr, "reg"); + 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) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.InitC, "ph"); - require(e1_ != bytes32(0) && e2_ != bytes32(0), "0"); - require(e1_ != e2_, "eq"); - if (gm.fc != 0) require(block.timestamp <= gm.fc + COMMIT_TIMEOUT, "to"); - if (gm.fc == 0) gm.fc = block.timestamp; - if (msg.sender == gm.a.addr) { - require(gm.a.e1 == bytes32(0), "a"); - gm.a.e1 = e1_; - gm.a.e2 = e2_; - } else if (msg.sender == gm.b.addr) { - require(gm.b.e1 == bytes32(0), "b"); - gm.b.e1 = e1_; - gm.b.e2 = e2_; - } else revert("reg"); - if (gm.a.e1 != bytes32(0) && gm.b.e1 != bytes32(0)) { - gm.p = GamePhase.FirstR; - gm.fr = 0; - } + doCommit(id_, e1_, e2_, 0); return true; } - function revealInitialMoves(uint id_, string memory c1, string memory c2) public isRegistered(id_) returns (Moves, Moves) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.FirstR, "ph"); - if (gm.fr != 0) require(block.timestamp <= gm.fr + REVEAL_TIMEOUT, "to"); - bytes32 e1_ = keccak256(abi.encodePacked(c1)); - bytes32 e2_ = keccak256(abi.encodePacked(c2)); - Moves m1_ = Moves(getFirstChar(c1)); - Moves m2_ = Moves(getFirstChar(c2)); - require(m1_ != Moves.None && m2_ != Moves.None, "m"); - require(m1_ != m2_, "eq"); - if (msg.sender == gm.a.addr) { - require(e1_ == gm.a.e1 && e2_ == gm.a.e2, "h"); - gm.a.m1 = m1_; - gm.a.m2 = m2_; - } else if (msg.sender == gm.b.addr) { - require(e1_ == gm.b.e1 && e2_ == gm.b.e2, "h"); - gm.b.m1 = m1_; - gm.b.m2 = m2_; - } else revert("reg"); - if (gm.fr == 0) gm.fr = block.timestamp; - if (gm.a.m1 != Moves.None && gm.b.m1 != Moves.None) { - gm.p = GamePhase.WithdC; - gm.fwc = 0; + 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 (m1_, m2_); + return (move1, move2); } - function commitWithdraw(uint id_, bytes32 ew_) public isRegistered(id_) returns (bool) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.WithdC, "ph"); - require(ew_ != bytes32(0), "0"); - if (gm.fwc != 0) require(block.timestamp <= gm.fwc + COMMIT_TIMEOUT, "to"); - if (msg.sender == gm.a.addr) { - require(gm.a.ew == bytes32(0), "a"); - gm.a.ew = ew_; - } else if (msg.sender == gm.b.addr) { - require(gm.b.ew == bytes32(0), "b"); - gm.b.ew = ew_; - } else revert("reg"); - if (gm.fwc == 0) gm.fwc = block.timestamp; - if (gm.a.ew != bytes32(0) && gm.b.ew != bytes32(0)) { - gm.p = GamePhase.WithdR; - gm.fwr = 0; - } + function commitWithdraw(uint gameId, bytes32 wHash) public isRegistered(gameId) returns (bool) { + doCommit(gameId, wHash, bytes32(0), 1); return true; } - function withdrawMove(uint id_, string memory cw) public isRegistered(id_) returns (uint) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.WithdR, "ph"); - if (gm.fwr != 0) require(block.timestamp <= gm.fwr + REVEAL_TIMEOUT, "to"); - bytes32 ew_ = keccak256(abi.encodePacked(cw)); - uint idx = getFirstChar(cw); - require(idx == 1 || idx == 2, "idx"); - if (msg.sender == gm.a.addr) { - require(ew_ == gm.a.ew, "h"); - require(gm.a.w == 0, "a"); - gm.a.w = idx; - } else if (msg.sender == gm.b.addr) { - require(ew_ == gm.b.ew, "h"); - require(gm.b.w == 0, "b"); - gm.b.w = idx; - } else revert("reg"); - if (gm.fwr == 0) gm.fwr = block.timestamp; - if (gm.a.w != 0 && gm.b.w != 0) { - gm.p = GamePhase.FinalC; - gm.ffc = 0; - } + 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 commitFinalMove(uint id_, bytes32 ef_) public isRegistered(id_) returns (bool) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.FinalC, "ph"); - require(ef_ != bytes32(0), "0"); - if (gm.ffc != 0) require(block.timestamp <= gm.ffc + COMMIT_TIMEOUT, "to"); - if (msg.sender == gm.a.addr) { - require(gm.a.ef == bytes32(0), "a"); - gm.a.ef = ef_; - } else if (msg.sender == gm.b.addr) { - require(gm.b.ef == bytes32(0), "b"); - gm.b.ef = ef_; - } else revert("reg"); - if (gm.ffc == 0) gm.ffc = block.timestamp; - if (gm.a.ef != bytes32(0) && gm.b.ef != bytes32(0)) { - gm.p = GamePhase.FinalR; - gm.ffr = 0; - } - return true; + + 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 revealFinalMove(uint id_, string memory cf_) public isRegistered(id_) returns (Moves) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.FinalR, "ph"); - if (gm.ffr != 0) require(block.timestamp <= gm.ffr + REVEAL_TIMEOUT, "to"); - bytes32 ef_ = keccak256(abi.encodePacked(cf_)); - Moves mf_ = Moves(getFirstChar(cf_)); - require(mf_ != Moves.None, "m"); - if (msg.sender == gm.a.addr) { - require(ef_ == gm.a.ef, "h"); - Moves exp = gm.a.w == 1 ? gm.a.m2 : gm.a.m1; - require(mf_ == exp, "w"); - gm.a.mf = mf_; - } else if (msg.sender == gm.b.addr) { - require(ef_ == gm.b.ef, "h"); - Moves exp = gm.b.w == 1 ? gm.b.m2 : gm.b.m1; - require(mf_ == exp, "w"); - gm.b.mf = mf_; - } else revert("reg"); - if (gm.ffr == 0) gm.ffr = block.timestamp; - if (gm.a.mf != Moves.None && gm.b.mf != Moves.None) determineOutcome(id_); - return mf_; - } - - function determineOutcome(uint id_) private { - GameState storage gm = g[id_]; - if (gm.a.mf == gm.b.mf) gm.o = Outcomes.D; - else if ((gm.a.mf == Moves.Rock && gm.b.mf == Moves.Scissors) || - (gm.a.mf == Moves.Paper && gm.b.mf == Moves.Rock) || - (gm.a.mf == Moves.Scissors && gm.b.mf == Moves.Paper)) - gm.o = Outcomes.A; - else gm.o = Outcomes.B; - gm.p = GamePhase.Done; - } - - function getFirstChar(string memory s) private pure returns (uint) { - bytes memory b = bytes(s); + function getFirstChar(string memory str) private pure returns (uint) { + bytes memory b = bytes(str); if (b.length == 0) return 0; - bytes1 f = b[0]; - if (f == 0x31) return 1; - if (f == 0x32) return 2; - if (f == 0x33) return 3; + bytes1 first = b[0]; + if (first == 0x31) return 1; + if (first == 0x32) return 2; + if (first == 0x33) return 3; return 0; } - function getOutcome(uint id_) public returns (Outcomes) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.Done, "dn"); - require(gm.o != Outcomes.None, "o"); - address payable a = gm.a.addr; - address payable b = gm.b.addr; - uint bet = gm.ib; - Outcomes out = gm.o; - resetGame(id_); - pay(a, b, bet, out); - return out; + 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 a, address payable b, uint bet, Outcomes o) private { - if (o == Outcomes.A) a.transfer(bet * 2); - else if (o == Outcomes.B) b.transfer(bet * 2); - else { a.transfer(bet); b.transfer(bet); } + 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 w, address payable, uint bet) private { - w.transfer(bet * 2); + function payWithSlash(address payable winner, address payable, uint bet) private { + winner.transfer(bet * 2); } - function resetGame(uint id_) private { - g[id_].act = false; + function resetGame(uint gameId) private { + games[gameId].active = false; } function getContractBalance() public view returns (uint) { return address(this).balance; } - function whoAmI(uint id_) public view returns (uint) { - if (id_ == 0) return 0; - GameState storage gm = g[id_]; - if (msg.sender == gm.a.addr) return 1; - if (msg.sender == gm.b.addr) return 2; + 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 id_) public view returns (GamePhase) { - return g[id_].p; + function getGamePhase(uint gameId) public view returns (GamePhase) { + return games[gameId].phase; } - function getTimeLeft(uint id_) public view returns (int) { - if (id_ == 0) return 0; - GameState storage gm = g[id_]; - if (gm.p == GamePhase.InitC && gm.fc != 0) { - uint d = gm.fc + COMMIT_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); - } else if (gm.p == GamePhase.FirstR && gm.fr != 0) { - uint d = gm.fr + REVEAL_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); - } else if (gm.p == GamePhase.WithdC && gm.fwc != 0) { - uint d = gm.fwc + COMMIT_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); - } else if (gm.p == GamePhase.WithdR && gm.fwr != 0) { - uint d = gm.fwr + REVEAL_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); - } else if (gm.p == GamePhase.FinalC && gm.ffc != 0) { - uint d = gm.ffc + COMMIT_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); - } else if (gm.p == GamePhase.FinalR && gm.ffr != 0) { - uint d = gm.ffr + REVEAL_TIMEOUT; - if (block.timestamp >= d) return 0; - return int(d - block.timestamp); + 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); } - return int(COMMIT_TIMEOUT); + + 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 resolveTimeout(uint id_) public isRegistered(id_) { - GameState storage gm = g[id_]; - require(gm.act, "act"); - address c = msg.sender; - address payable w = payable(c); - bool to = false; - if (gm.p == GamePhase.InitC && gm.fc != 0) { - if (block.timestamp > gm.fc + COMMIT_TIMEOUT) { - if (c == gm.a.addr && gm.b.e1 == bytes32(0)) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.e1 == bytes32(0)) { - gm.o = Outcomes.B; - to = true; - } - } - } else if (gm.p == GamePhase.FirstR && gm.fr != 0) { - if (block.timestamp > gm.fr + REVEAL_TIMEOUT) { - if (c == gm.a.addr && gm.b.m1 == Moves.None) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.m1 == Moves.None) { - gm.o = Outcomes.B; - to = true; - } - } - } else if (gm.p == GamePhase.WithdC && gm.fwc != 0) { - if (block.timestamp > gm.fwc + COMMIT_TIMEOUT) { - if (c == gm.a.addr && gm.b.ew == bytes32(0)) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.ew == bytes32(0)) { - gm.o = Outcomes.B; - to = true; - } - } - } else if (gm.p == GamePhase.WithdR && gm.fwr != 0) { - if (block.timestamp > gm.fwr + REVEAL_TIMEOUT) { - if (c == gm.a.addr && gm.b.w == 0) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.w == 0) { - gm.o = Outcomes.B; - to = true; - } - } - } else if (gm.p == GamePhase.FinalC && gm.ffc != 0) { - if (block.timestamp > gm.ffc + COMMIT_TIMEOUT) { - if (c == gm.a.addr && gm.b.ef == bytes32(0)) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.ef == bytes32(0)) { - gm.o = Outcomes.B; - to = true; - } - } - } else if (gm.p == GamePhase.FinalR && gm.ffr != 0) { - if (block.timestamp > gm.ffr + REVEAL_TIMEOUT) { - if (c == gm.a.addr && gm.b.mf == Moves.None) { - gm.o = Outcomes.A; - to = true; - } else if (c == gm.b.addr && gm.a.mf == Moves.None) { - gm.o = Outcomes.B; - to = true; - } - } - } - require(to, "to"); - address payable l = w == gm.a.addr ? gm.b.addr : gm.a.addr; - uint bet = gm.ib; - resetGame(id_); - payWithSlash(w, l, bet); - } - - function getGameDetails(uint id_) public view returns ( + function getGameDetails(uint gameId) public view returns ( Player memory, Player memory, uint, Outcomes, GamePhase, bool, uint, string memory) { - GameState storage gm = g[id_]; - require(gm.id != 0, "no"); - return (gm.a, gm.b, gm.ib, gm.o, gm.p, gm.act, gm.id, gm.mode); + 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 c = 0; - for (uint i = 0; i < ids.length; i++) if (g[ids[i]].act) c++; - uint[] memory a = new uint[](c); - uint ix = 0; - for (uint i = 0; i < ids.length; i++) if (g[ids[i]].act) a[ix++] = ids[i]; - return a; - } - - function startGame(uint id_) public { - GameState storage gm = g[id_]; - require(gm.act, "act"); - require(gm.p == GamePhase.Reg, "st"); - require(gm.a.addr != address(0) && gm.b.addr != address(0), "r"); - gm.p = GamePhase.InitC; + 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; } } diff --git a/docs/minusOne-example-gameplay.md b/docs/minusOne-example-gameplay.md new file mode 100644 index 0000000..ca5e4b1 --- /dev/null +++ b/docs/minusOne-example-gameplay.md @@ -0,0 +1,528 @@ +# Example Gameplay: Rock-Paper-Scissors Minus One + +This document provides a **step-by-step playbook** for two players (Alice and Bob) to play the "Minus One" variant. Follow along to understand the exact order of operations and function calls. + +## Quick Reference + +### Move Encoding + +- `1` = Rock +- `2` = Paper +- `3` = Scissors + +### Game Flow + +``` +1. Registration (both players) +2. Initial Commit (both players) +3. First Reveal (both players) +4. Withdrawal Commit (both players) +5. Withdrawal Reveal (both players) +6. Get Outcome (either player) +``` + +### Hash Generation Tool + +🔗 **https://emn178.github.io/online-tools/keccak_256.html** + +- Input Type: **UTF-8** +- Output Type: **Hex** +- Format: `-` (e.g., `1-mypass` for Rock) + +### Game Rules + +1. Each player commits 2 **different** moves (e.g., Rock + Paper) +2. Both players reveal their 2 moves +3. Each player secretly withdraws 1 move (commits which index: 1 or 2) +4. Both players reveal which move they withdrew +5. The **remaining move** for each player battles to determine winner + +--- + +## Example Game Scenario + +**Players:** + +- 👩 **Alice** (will choose Rock + Paper, keep Rock) +- 👨 **Bob** (will choose Rock + Scissors, keep Scissors) + +**Expected Winner:** Alice (Rock beats Scissors) + +**Minimum Bet:** 0.01 ETH (10000000000000000 wei) + +--- + +## 🎮 PHASE 1: REGISTRATION + +> **Goal:** Both players join the game with their nicknames and bets + +### Step 1.1: Alice Registers (Player 1) + +**Function:** `register(uint gameId, string memory name)` + +**Alice's Input:** + +``` +gameId: 0 (creates new game) +name: "Alice" +value: 0.01 ETH +``` + +**Smart Contract Call:** + +```javascript +register(0, "Alice"); +// Send 0.01 ETH with transaction +``` + +**Returns:** `(1, 1)` + +- Position: Player 1 +- Game ID: 1 + +✅ Alice is now registered as Player 1 in Game 1 + +--- + +### Step 1.2: Bob Registers (Player 2) + +**Function:** `register(uint gameId, string memory name)` + +**Bob's Input:** + +``` +gameId: 1 (joins Alice's game) +name: "Bob" +value: 0.01 ETH (must match Alice's bet!) +``` + +**Smart Contract Call:** + +```javascript +register(1, "Bob"); +// Send 0.01 ETH with transaction +``` + +**Returns:** `(2, 1)` + +- Position: Player 2 +- Game ID: 1 + +✅ Bob is now registered as Player 2 +✅ **Game automatically advances to InitialCommit phase** + +--- + +## 🎮 PHASE 2: INITIAL COMMIT + +> **Goal:** Both players commit their 2 encrypted moves (hashes) + +### Step 2.1: Alice Prepares Her Hashes (Off-chain) + +**Alice's Strategy:** + +- Move 1: Rock (1) +- Move 2: Paper (2) + +**Generate Hash for Move 1 (Rock):** + +1. Go to: https://emn178.github.io/online-tools/keccak_256.html +2. Input: `1-alicepass1` (UTF-8) +3. Output: `0x7364263d5fc729b4709129564a2c516f2eb40f55d8704860e46f597c91b6b264` + +**Generate Hash for Move 2 (Paper):** + +1. Input: `2-alicepass2` (UTF-8) +2. Output: `0xc9ad7a6c99b3f5af24991d9c66c0cc2b731869f4b7399653f0820f99e34d45ef` + +💾 **Alice must save these:** + +- Cleartext for reveal: `1-alicepass1` and `2-alicepass2` +- Hashes for commit: `0x7364...` and `0xc9ad...` + +--- + +### Step 2.2: Alice Commits + +**Function:** `commitInitialMoves(uint gameId, bytes32 hash1, bytes32 hash2)` + +**Alice's Input:** + +``` +gameId: 1 +hash1: 0x7364263d5fc729b4709129564a2c516f2eb40f55d8704860e46f597c91b6b264 +hash2: 0xc9ad7a6c99b3f5af24991d9c66c0cc2b731869f4b7399653f0820f99e34d45ef +``` + +**Smart Contract Call:** + +```javascript +commitInitialMoves( + 1, + "0x7364263d5fc729b4709129564a2c516f2eb40f55d8704860e46f597c91b6b264", + "0xc9ad7a6c99b3f5af24991d9c66c0cc2b731869f4b7399653f0820f99e34d45ef" +); +``` + +**Returns:** `true` + +✅ Alice has committed her two moves + +--- + +### Step 2.3: Bob Prepares His Hashes (Off-chain) + +**Bob's Strategy:** + +- Move 1: Rock (1) +- Move 2: Scissors (3) + +**Generate Hash for Move 1 (Rock):** + +1. Go to: https://emn178.github.io/online-tools/keccak_256.html +2. Input: `1-bobsecret1` (UTF-8) +3. Output: `0x16864f12ec74b4fac1cd9fd5b0db1959e4df91cf55e9180cc13cdf20a134af16` + +**Generate Hash for Move 2 (Scissors):** + +1. Input: `3-bobsecret2` (UTF-8) +2. Output: `0x86d1def3d3f9bed5f2de22161040c10c75dcb52f263f3c3ec08cdc8ba10d2103` + +💾 **Bob must save these:** + +- Cleartext for reveal: `1-bobsecret1` and `3-bobsecret2` +- Hashes for commit: `0x1686...` and `0x86d1...` + +--- + +### Step 2.4: Bob Commits + +**Function:** `commitInitialMoves(uint gameId, bytes32 hash1, bytes32 hash2)` + +**Bob's Input:** + +``` +gameId: 1 +hash1: 0x16864f12ec74b4fac1cd9fd5b0db1959e4df91cf55e9180cc13cdf20a134af16 +hash2: 0x86d1def3d3f9bed5f2de22161040c10c75dcb52f263f3c3ec08cdc8ba10d2103 +``` + +**Smart Contract Call:** + +```javascript +commitInitialMoves( + 1, + "0x16864f12ec74b4fac1cd9fd5b0db1959e4df91cf55e9180cc13cdf20a134af16", + "0x86d1def3d3f9bed5f2de22161040c10c75dcb52f263f3c3ec08cdc8ba10d2103" +); +``` + +**Returns:** `true` + +✅ Bob has committed his two moves +✅ **Game automatically advances to FirstReveal phase** + +--- + +## 🎮 PHASE 3: FIRST REVEAL + +> **Goal:** Both players reveal their original cleartext strings to prove their moves + +### Step 3.1: Alice Reveals + +**Function:** `revealInitialMoves(uint gameId, string memory clear1, string memory clear2)` + +**Alice's Input:** + +``` +gameId: 1 +clear1: "1-alicepass1" (the exact string she hashed) +clear2: "2-alicepass2" (the exact string she hashed) +``` + +**Smart Contract Call:** + +```javascript +revealInitialMoves(1, "1-alicepass1", "2-alicepass2"); +``` + +**Returns:** `(Rock, Paper)` (enum values for moves 1 and 3) + +✅ Alice's moves are now public: + +- Move 1: Rock +- Move 2: Paper + +--- + +### Step 3.2: Bob Reveals + +**Function:** `revealInitialMoves(uint gameId, string memory clear1, string memory clear2)` + +**Bob's Input:** + +``` +gameId: 1 +clear1: "1-bobsecret1" (the exact string he hashed) +clear2: "3-bobsecret2" (the exact string he hashed) +``` + +**Smart Contract Call:** + +```javascript +revealInitialMoves(1, "1-bobsecret1", "3-bobsecret2"); +``` + +**Returns:** `(Rock, Scissors)` (enum values for moves 1 and 3) + +✅ Bob's moves are now public: + +- Move 1: Rock +- Move 2: Scissors + +**Current Game State:** + +- Alice has: Rock (move 1) and Paper (move 2) +- Bob has: Rock (move 1) and Scissors (move 2) + +✅ **Game automatically advances to WithdrawalCommit phase** + +--- + +## 🎮 PHASE 4: WITHDRAWAL COMMIT + +> **Goal:** Both players secretly commit which move to withdraw (1 or 2) + +### Step 4.1: Alice Prepares Withdrawal Hash (Off-chain) + +**Alice's Decision:** + +- She wants to keep Rock (move 1) +- So she will withdraw move index **2** (Paper) + +**Generate Withdrawal Hash:** + +1. Go to: https://emn178.github.io/online-tools/keccak_256.html +2. Input: `2-alicewith` (UTF-8) ← Note: "2" is the index to withdraw +3. Output: `0x7c9ff59a4d298766d9480c130f6ed67c06f135a1c2f45326583593bb9c2e6363` + +💾 **Alice must save:** + +- Cleartext: `2-alicewith` +- Hash: `0x9a3f...` + +--- + +### Step 4.2: Alice Commits Withdrawal + +**Function:** `commitWithdraw(uint gameId, bytes32 wHash)` + +**Alice's Input:** + +``` +gameId: 1 +wHash: 0x7c9ff59a4d298766d9480c130f6ed67c06f135a1c2f45326583593bb9c2e6363 +``` + +**Smart Contract Call:** + +```javascript +commitWithdraw( + 1, + "0x7c9ff59a4d298766d9480c130f6ed67c06f135a1c2f45326583593bb9c2e6363" +); +``` + +**Returns:** `true` + +✅ Alice has secretly committed to withdraw move 2 + +--- + +### Step 4.3: Bob Prepares Withdrawal Hash (Off-chain) + +**Bob's Decision:** + +- He wants to keep Scissors (move 2) +- So he will withdraw move index **1** (Rock) + +**Generate Withdrawal Hash:** + +1. Go to: https://emn178.github.io/online-tools/keccak_256.html +2. Input: `1-bobwith` (UTF-8) ← Note: "1" is the index to withdraw +3. Output: `0x29e944e8859972eb35622d3149120d878d7931aaa9ac2e213d0b321ee564b7dc` + +💾 **Bob must save:** + +- Cleartext: `1-bobwith` +- Hash: `0x29e9...` + +--- + +### Step 4.4: Bob Commits Withdrawal + +**Function:** `commitWithdraw(uint gameId, bytes32 wHash)` + +**Bob's Input:** + +``` +gameId: 1 +wHash: 0x29e944e8859972eb35622d3149120d878d7931aaa9ac2e213d0b321ee564b7dc +``` + +**Smart Contract Call:** + +```javascript +commitWithdraw( + 1, + "0x29e944e8859972eb35622d3149120d878d7931aaa9ac2e213d0b321ee564b7dc" +); +``` + +**Returns:** `true` + +✅ Bob has secretly committed to withdraw move 1 +✅ **Game automatically advances to WithdrawalReveal phase** + +--- + +## 🎮 PHASE 5: WITHDRAWAL REVEAL + +> **Goal:** Both players reveal which move they withdrew + +### Step 5.1: Alice Reveals Withdrawal + +**Function:** `withdrawMove(uint gameId, string memory clear)` + +**Alice's Input:** + +``` +gameId: 1 +clear: "2-alicewith" (the exact string she hashed for withdrawal) +``` + +**Smart Contract Call:** + +```javascript +withdrawMove(1, "2-alicewith"); +``` + +**Returns:** `2` + +✅ Alice withdrew move 2 (Paper) +✅ Alice keeps: **Rock** (move 1) + +--- + +### Step 5.2: Bob Reveals Withdrawal + +**Function:** `withdrawMove(uint gameId, string memory clear)` + +**Bob's Input:** + +``` +gameId: 1 +clear: "1-bobwith" (the exact string he hashed for withdrawal) +``` + +**Smart Contract Call:** + +```javascript +withdrawMove(1, "1-bobwith"); +``` + +**Returns:** `1` + +✅ Bob withdrew move 1 (Rock) +✅ Bob keeps: **Scissors** (move 2) + +**Final Battle:** + +- Alice's final move: **Rock** +- Bob's final move: **Scissors** +- **Winner: Alice!** (Rock beats Scissors) + +✅ **Game automatically determines outcome and advances to Done phase** + +--- + +## 🎮 PHASE 6: GET OUTCOME + +> **Goal:** Either player calls to finalize and receive payout + +### Step 6.1: Get Outcome (Either Player Can Call) + +**Function:** `getOutcome(uint gameId)` + +**Input:** + +``` +gameId: 1 +``` + +**Smart Contract Call:** + +```javascript +getOutcome(1); +``` + +**Returns:** `Outcomes.A` (Alice wins) + +**What Happens:** + +1. Contract calculates winner: Rock beats Scissors +2. Alice receives **0.02 ETH** (both players' bets) +3. Game is marked inactive +4. Contract balance reduced by payout + +✅ **Game Complete!** Alice won and received 0.02 ETH + +--- + +## 📊 Summary Table + +| Step | Who Acts | Function | Key Inputs | Result | +| ---- | -------- | --------------------------------------- | ------------------------------------------- | ----------------------------- | +| 1.1 | Alice | `register(0, "Alice")` | gameId=0, name="Alice", value=0.01 ETH | Player 1 in Game 1 | +| 1.2 | Bob | `register(1, "Bob")` | gameId=1, name="Bob", value=0.01 ETH | Player 2 in Game 1 | +| 2.1 | Alice | `commitInitialMoves(1, hash1, hash2)` | Hashes of "1-alicepass1" and "2-alicepass2" | Committed | +| 2.2 | Bob | `commitInitialMoves(1, hash1, hash2)` | Hashes of "1-bobsecret1" and "3-bobsecret2" | Committed | +| 3.1 | Alice | `revealInitialMoves(1, clear1, clear2)` | "1-alicepass1", "2-alicepass2" | Reveals Rock + Paper | +| 3.2 | Bob | `revealInitialMoves(1, clear1, clear2)` | "1-bobsecret1", "3-bobsecret2" | Reveals Rock + Scissors | +| 4.1 | Alice | `commitWithdraw(1, wHash)` | Hash of "2-alicewith" | Committed withdrawal | +| 4.2 | Bob | `commitWithdraw(1, wHash)` | Hash of "1-bobwith" | Committed withdrawal | +| 5.1 | Alice | `withdrawMove(1, clear)` | "2-alicewith" | Withdrew Paper, keeps Rock | +| 5.2 | Bob | `withdrawMove(1, clear)` | "1-bobwith" | Withdrew Rock, keeps Scissors | +| 6.1 | Either | `getOutcome(1)` | gameId=1 | Alice wins, gets 0.02 ETH | + +--- + +## ⚠️ Important Notes + +### Must Save These Strings! + +- **Initial moves cleartext:** You need them in Phase 3 +- **Withdrawal cleartext:** You need it in Phase 5 +- **If you lose these strings, you cannot reveal and will timeout!** + +### Order Matters + +- Either player can act first within each phase +- Game won't advance until **both players** complete the current phase +- You have **10 minutes** per phase (commit or reveal) + +### Common Mistakes + +❌ Using different strings in reveal than in commit → "Hash mismatch" +❌ Committing same move twice (e.g., Rock + Rock) → "Moves must differ" +❌ Second player betting different amount → "Bet must match initial" +❌ Withdrawing index 0 or 3 → "Index must be 1 or 2" +❌ Losing your cleartext strings → Cannot reveal, will timeout + +### Timeout Protection + +If your opponent doesn't act within 10 minutes: + +```javascript +resolveTimeout(1); // Call this to win by default +```