mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
fix game and add playbook
This commit is contained in:
@@ -9,420 +9,295 @@ contract GameMinusOne {
|
|||||||
|
|
||||||
enum Moves { None, Rock, Paper, Scissors }
|
enum Moves { None, Rock, Paper, Scissors }
|
||||||
enum Outcomes { None, A, B, D, AT, BT }
|
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 {
|
struct Player {
|
||||||
address payable addr;
|
address payable addr;
|
||||||
uint bet;
|
uint bet;
|
||||||
bytes32 e1;
|
bytes32 hash1;
|
||||||
bytes32 e2;
|
bytes32 hash2;
|
||||||
Moves m1;
|
Moves move1;
|
||||||
Moves m2;
|
Moves move2;
|
||||||
bytes32 ew;
|
bytes32 wHash;
|
||||||
uint w;
|
uint withdrawn;
|
||||||
bytes32 ef;
|
string nick;
|
||||||
Moves mf;
|
|
||||||
string n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GameState {
|
struct GameState {
|
||||||
Player a;
|
Player pA;
|
||||||
Player b;
|
Player pB;
|
||||||
Outcomes o;
|
Outcomes outcome;
|
||||||
GamePhase p;
|
GamePhase phase;
|
||||||
uint fc;
|
uint tCommit;
|
||||||
uint fr;
|
uint tReveal;
|
||||||
uint fwc;
|
uint tWithdC;
|
||||||
uint fwr;
|
uint tWithdR;
|
||||||
uint ffc;
|
uint bet;
|
||||||
uint ffr;
|
uint gameId;
|
||||||
uint ib;
|
bool active;
|
||||||
uint id;
|
|
||||||
bool act;
|
|
||||||
string mode;
|
string mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping(uint => GameState) private g;
|
mapping(uint => GameState) private games;
|
||||||
uint[] private ids;
|
uint[] private gameIds;
|
||||||
uint private nextId = 1;
|
uint private nextId = 1;
|
||||||
|
|
||||||
modifier validBet(uint id_) {
|
modifier validBet(uint gameId) {
|
||||||
require(msg.value >= BET_MIN, "min");
|
require(msg.value >= BET_MIN, "Bet below minimum");
|
||||||
require(g[id_].ib == 0 || msg.value == g[id_].ib, "bet");
|
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) {
|
function register(uint gameId, string memory name) public payable validBet(gameId) returns (uint, uint) {
|
||||||
if (id_ == 0) id_ = createNewGame();
|
if (gameId == 0) gameId = createNewGame();
|
||||||
require(g[id_].act, "act");
|
require(games[gameId].active, "Game not active");
|
||||||
require(g[id_].p == GamePhase.Reg, "st");
|
require(games[gameId].phase == GamePhase.Reg, "Game started");
|
||||||
require(bytes(n_).length > 0 && bytes(n_).length <= 20, "n");
|
require(bytes(name).length > 0 && bytes(name).length <= 20, "Invalid nickname");
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
if (gm.a.addr == address(0)) {
|
if (game.pA.addr == address(0)) {
|
||||||
gm.a.addr = payable(msg.sender);
|
game.pA.addr = payable(msg.sender);
|
||||||
gm.a.n = n_;
|
game.pA.nick = name;
|
||||||
gm.ib = msg.value;
|
game.bet = msg.value;
|
||||||
return (1, id_);
|
return (1, gameId);
|
||||||
} else if (gm.b.addr == address(0)) {
|
} else if (game.pB.addr == address(0)) {
|
||||||
require(msg.sender != gm.a.addr, "self");
|
require(msg.sender != game.pA.addr, "Cannot play yourself");
|
||||||
gm.b.addr = payable(msg.sender);
|
game.pB.addr = payable(msg.sender);
|
||||||
gm.b.n = n_;
|
game.pB.nick = name;
|
||||||
gm.p = GamePhase.InitC;
|
game.phase = GamePhase.InitC;
|
||||||
return (2, id_);
|
return (2, gameId);
|
||||||
}
|
}
|
||||||
revert("full");
|
revert("Game full");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNewGame() private returns (uint) {
|
function createNewGame() private returns (uint) {
|
||||||
uint id_ = nextId++;
|
uint gameId = nextId++;
|
||||||
g[id_].id = id_;
|
games[gameId].gameId = gameId;
|
||||||
g[id_].act = true;
|
games[gameId].active = true;
|
||||||
g[id_].p = GamePhase.Reg;
|
games[gameId].phase = GamePhase.Reg;
|
||||||
g[id_].mode = "minusone";
|
games[gameId].mode = "minusone";
|
||||||
ids.push(id_);
|
gameIds.push(gameId);
|
||||||
return id_;
|
return gameId;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier isRegistered(uint id_) {
|
modifier isRegistered(uint gameId) {
|
||||||
require(id_ != 0, "id");
|
require(gameId != 0, "Invalid game ID");
|
||||||
require(msg.sender == g[id_].a.addr || msg.sender == g[id_].b.addr, "reg");
|
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) {
|
function commitInitialMoves(uint id_, bytes32 e1_, bytes32 e2_) public isRegistered(id_) returns (bool) {
|
||||||
GameState storage gm = g[id_];
|
doCommit(id_, e1_, e2_, 0);
|
||||||
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;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function revealInitialMoves(uint id_, string memory c1, string memory c2) public isRegistered(id_) returns (Moves, Moves) {
|
function revealInitialMoves(uint gameId, string memory clear1, string memory clear2) public isRegistered(gameId) returns (Moves, Moves) {
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
require(gm.act, "act");
|
require(game.active, "Game not active");
|
||||||
require(gm.p == GamePhase.FirstR, "ph");
|
require(game.phase == GamePhase.FirstR, "Wrong phase");
|
||||||
if (gm.fr != 0) require(block.timestamp <= gm.fr + REVEAL_TIMEOUT, "to");
|
if (game.tReveal != 0) require(block.timestamp <= game.tReveal + REVEAL_TIMEOUT, "Reveal timeout");
|
||||||
bytes32 e1_ = keccak256(abi.encodePacked(c1));
|
bytes32 hash1 = keccak256(abi.encodePacked(clear1));
|
||||||
bytes32 e2_ = keccak256(abi.encodePacked(c2));
|
bytes32 hash2 = keccak256(abi.encodePacked(clear2));
|
||||||
Moves m1_ = Moves(getFirstChar(c1));
|
Moves move1 = Moves(getFirstChar(clear1));
|
||||||
Moves m2_ = Moves(getFirstChar(c2));
|
Moves move2 = Moves(getFirstChar(clear2));
|
||||||
require(m1_ != Moves.None && m2_ != Moves.None, "m");
|
require(move1 != Moves.None && move2 != Moves.None, "Invalid moves");
|
||||||
require(m1_ != m2_, "eq");
|
require(move1 != move2, "Moves must differ");
|
||||||
if (msg.sender == gm.a.addr) {
|
if (msg.sender == game.pA.addr) {
|
||||||
require(e1_ == gm.a.e1 && e2_ == gm.a.e2, "h");
|
require(hash1 == game.pA.hash1 && hash2 == game.pA.hash2, "Hash mismatch");
|
||||||
gm.a.m1 = m1_;
|
game.pA.move1 = move1;
|
||||||
gm.a.m2 = m2_;
|
game.pA.move2 = move2;
|
||||||
} else if (msg.sender == gm.b.addr) {
|
} else if (msg.sender == game.pB.addr) {
|
||||||
require(e1_ == gm.b.e1 && e2_ == gm.b.e2, "h");
|
require(hash1 == game.pB.hash1 && hash2 == game.pB.hash2, "Hash mismatch");
|
||||||
gm.b.m1 = m1_;
|
game.pB.move1 = move1;
|
||||||
gm.b.m2 = m2_;
|
game.pB.move2 = move2;
|
||||||
} else revert("reg");
|
} else revert("Not registered");
|
||||||
if (gm.fr == 0) gm.fr = block.timestamp;
|
if (game.tReveal == 0) game.tReveal = block.timestamp;
|
||||||
if (gm.a.m1 != Moves.None && gm.b.m1 != Moves.None) {
|
if (game.pA.move1 != Moves.None && game.pB.move1 != Moves.None) {
|
||||||
gm.p = GamePhase.WithdC;
|
game.phase = GamePhase.WithdC;
|
||||||
gm.fwc = 0;
|
game.tWithdC = 0;
|
||||||
}
|
}
|
||||||
return (m1_, m2_);
|
return (move1, move2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitWithdraw(uint id_, bytes32 ew_) public isRegistered(id_) returns (bool) {
|
function commitWithdraw(uint gameId, bytes32 wHash) public isRegistered(gameId) returns (bool) {
|
||||||
GameState storage gm = g[id_];
|
doCommit(gameId, wHash, bytes32(0), 1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function withdrawMove(uint id_, string memory cw) public isRegistered(id_) returns (uint) {
|
function withdrawMove(uint gameId, string memory clear) public isRegistered(gameId) returns (uint) {
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
require(gm.act, "act");
|
require(game.active, "Game not active");
|
||||||
require(gm.p == GamePhase.WithdR, "ph");
|
require(game.phase == GamePhase.WithdR, "Wrong phase");
|
||||||
if (gm.fwr != 0) require(block.timestamp <= gm.fwr + REVEAL_TIMEOUT, "to");
|
if (game.tWithdR != 0) require(block.timestamp <= game.tWithdR + REVEAL_TIMEOUT, "Reveal timeout");
|
||||||
bytes32 ew_ = keccak256(abi.encodePacked(cw));
|
bytes32 wHash = keccak256(abi.encodePacked(clear));
|
||||||
uint idx = getFirstChar(cw);
|
uint idx = getFirstChar(clear);
|
||||||
require(idx == 1 || idx == 2, "idx");
|
require(idx == 1 || idx == 2, "Index must be 1 or 2");
|
||||||
if (msg.sender == gm.a.addr) {
|
if (msg.sender == game.pA.addr) {
|
||||||
require(ew_ == gm.a.ew, "h");
|
require(wHash == game.pA.wHash, "Hash mismatch");
|
||||||
require(gm.a.w == 0, "a");
|
require(game.pA.withdrawn == 0, "Already withdrew");
|
||||||
gm.a.w = idx;
|
game.pA.withdrawn = idx;
|
||||||
} else if (msg.sender == gm.b.addr) {
|
} else if (msg.sender == game.pB.addr) {
|
||||||
require(ew_ == gm.b.ew, "h");
|
require(wHash == game.pB.wHash, "Hash mismatch");
|
||||||
require(gm.b.w == 0, "b");
|
require(game.pB.withdrawn == 0, "Already withdrew");
|
||||||
gm.b.w = idx;
|
game.pB.withdrawn = idx;
|
||||||
} else revert("reg");
|
} else revert("Not registered");
|
||||||
if (gm.fwr == 0) gm.fwr = block.timestamp;
|
if (game.tWithdR == 0) game.tWithdR = block.timestamp;
|
||||||
if (gm.a.w != 0 && gm.b.w != 0) {
|
if (game.pA.withdrawn != 0 && game.pB.withdrawn != 0) determineOutcome(gameId);
|
||||||
gm.p = GamePhase.FinalC;
|
|
||||||
gm.ffc = 0;
|
|
||||||
}
|
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
function commitFinalMove(uint id_, bytes32 ef_) public isRegistered(id_) returns (bool) {
|
|
||||||
GameState storage gm = g[id_];
|
function determineOutcome(uint gameId) private {
|
||||||
require(gm.act, "act");
|
GameState storage game = games[gameId];
|
||||||
require(gm.p == GamePhase.FinalC, "ph");
|
Moves finalA = game.pA.withdrawn == 1 ? game.pA.move2 : game.pA.move1;
|
||||||
require(ef_ != bytes32(0), "0");
|
Moves finalB = game.pB.withdrawn == 1 ? game.pB.move2 : game.pB.move1;
|
||||||
if (gm.ffc != 0) require(block.timestamp <= gm.ffc + COMMIT_TIMEOUT, "to");
|
if (finalA == finalB) game.outcome = Outcomes.D;
|
||||||
if (msg.sender == gm.a.addr) {
|
else if ((finalA == Moves.Rock && finalB == Moves.Scissors) ||
|
||||||
require(gm.a.ef == bytes32(0), "a");
|
(finalA == Moves.Paper && finalB == Moves.Rock) ||
|
||||||
gm.a.ef = ef_;
|
(finalA == Moves.Scissors && finalB == Moves.Paper))
|
||||||
} else if (msg.sender == gm.b.addr) {
|
game.outcome = Outcomes.A;
|
||||||
require(gm.b.ef == bytes32(0), "b");
|
else game.outcome = Outcomes.B;
|
||||||
gm.b.ef = ef_;
|
game.phase = GamePhase.Done;
|
||||||
} 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 revealFinalMove(uint id_, string memory cf_) public isRegistered(id_) returns (Moves) {
|
function getFirstChar(string memory str) private pure returns (uint) {
|
||||||
GameState storage gm = g[id_];
|
bytes memory b = bytes(str);
|
||||||
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);
|
|
||||||
if (b.length == 0) return 0;
|
if (b.length == 0) return 0;
|
||||||
bytes1 f = b[0];
|
bytes1 first = b[0];
|
||||||
if (f == 0x31) return 1;
|
if (first == 0x31) return 1;
|
||||||
if (f == 0x32) return 2;
|
if (first == 0x32) return 2;
|
||||||
if (f == 0x33) return 3;
|
if (first == 0x33) return 3;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOutcome(uint id_) public returns (Outcomes) {
|
function getOutcome(uint gameId) public returns (Outcomes) {
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
require(gm.act, "act");
|
require(game.active, "Game not active");
|
||||||
require(gm.p == GamePhase.Done, "dn");
|
require(game.phase == GamePhase.Done, "Game not finished");
|
||||||
require(gm.o != Outcomes.None, "o");
|
require(game.outcome != Outcomes.None, "No outcome yet");
|
||||||
address payable a = gm.a.addr;
|
address payable addrA = game.pA.addr;
|
||||||
address payable b = gm.b.addr;
|
address payable addrB = game.pB.addr;
|
||||||
uint bet = gm.ib;
|
uint betAmount = game.bet;
|
||||||
Outcomes out = gm.o;
|
Outcomes result = game.outcome;
|
||||||
resetGame(id_);
|
resetGame(gameId);
|
||||||
pay(a, b, bet, out);
|
pay(addrA, addrB, betAmount, result);
|
||||||
return out;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pay(address payable a, address payable b, uint bet, Outcomes o) private {
|
function pay(address payable addrA, address payable addrB, uint bet, Outcomes outcome) private {
|
||||||
if (o == Outcomes.A) a.transfer(bet * 2);
|
if (outcome == Outcomes.A) addrA.transfer(bet * 2);
|
||||||
else if (o == Outcomes.B) b.transfer(bet * 2);
|
else if (outcome == Outcomes.B) addrB.transfer(bet * 2);
|
||||||
else { a.transfer(bet); b.transfer(bet); }
|
else { addrA.transfer(bet); addrB.transfer(bet); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function payWithSlash(address payable w, address payable, uint bet) private {
|
function payWithSlash(address payable winner, address payable, uint bet) private {
|
||||||
w.transfer(bet * 2);
|
winner.transfer(bet * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetGame(uint id_) private {
|
function resetGame(uint gameId) private {
|
||||||
g[id_].act = false;
|
games[gameId].active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContractBalance() public view returns (uint) {
|
function getContractBalance() public view returns (uint) {
|
||||||
return address(this).balance;
|
return address(this).balance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function whoAmI(uint id_) public view returns (uint) {
|
function whoAmI(uint gameId) public view returns (uint) {
|
||||||
if (id_ == 0) return 0;
|
if (gameId == 0) return 0;
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
if (msg.sender == gm.a.addr) return 1;
|
if (msg.sender == game.pA.addr) return 1;
|
||||||
if (msg.sender == gm.b.addr) return 2;
|
if (msg.sender == game.pB.addr) return 2;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGamePhase(uint id_) public view returns (GamePhase) {
|
function getGamePhase(uint gameId) public view returns (GamePhase) {
|
||||||
return g[id_].p;
|
return games[gameId].phase;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTimeLeft(uint id_) public view returns (int) {
|
function getTimeLeft(uint gameId) public view returns (int) {
|
||||||
if (id_ == 0) return 0;
|
if (gameId == 0) return 0;
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
if (gm.p == GamePhase.InitC && gm.fc != 0) {
|
uint timeout; uint start;
|
||||||
uint d = gm.fc + COMMIT_TIMEOUT;
|
if (game.phase == GamePhase.InitC && game.tCommit != 0) { timeout = COMMIT_TIMEOUT; start = game.tCommit; }
|
||||||
if (block.timestamp >= d) return 0;
|
else if (game.phase == GamePhase.FirstR && game.tReveal != 0) { timeout = REVEAL_TIMEOUT; start = game.tReveal; }
|
||||||
return int(d - block.timestamp);
|
else if (game.phase == GamePhase.WithdC && game.tWithdC != 0) { timeout = COMMIT_TIMEOUT; start = game.tWithdC; }
|
||||||
} else if (gm.p == GamePhase.FirstR && gm.fr != 0) {
|
else if (game.phase == GamePhase.WithdR && game.tWithdR != 0) { timeout = REVEAL_TIMEOUT; start = game.tWithdR; }
|
||||||
uint d = gm.fr + REVEAL_TIMEOUT;
|
else return int(COMMIT_TIMEOUT);
|
||||||
if (block.timestamp >= d) return 0;
|
uint deadline = start + timeout;
|
||||||
return int(d - block.timestamp);
|
if (block.timestamp >= deadline) return 0;
|
||||||
} else if (gm.p == GamePhase.WithdC && gm.fwc != 0) {
|
return int(deadline - block.timestamp);
|
||||||
uint d = gm.fwc + COMMIT_TIMEOUT;
|
}
|
||||||
if (block.timestamp >= d) return 0;
|
|
||||||
return int(d - block.timestamp);
|
function resolveTimeout(uint gameId) public isRegistered(gameId) {
|
||||||
} else if (gm.p == GamePhase.WithdR && gm.fwr != 0) {
|
GameState storage game = games[gameId];
|
||||||
uint d = gm.fwr + REVEAL_TIMEOUT;
|
require(game.active, "Game not active");
|
||||||
if (block.timestamp >= d) return 0;
|
address caller = msg.sender;
|
||||||
return int(d - block.timestamp);
|
bool isPlayerA = caller == game.pA.addr;
|
||||||
} else if (gm.p == GamePhase.FinalC && gm.ffc != 0) {
|
bool timedOut = false;
|
||||||
uint d = gm.ffc + COMMIT_TIMEOUT;
|
|
||||||
if (block.timestamp >= d) return 0;
|
if (game.phase == GamePhase.InitC && game.tCommit != 0 && block.timestamp > game.tCommit + COMMIT_TIMEOUT) {
|
||||||
return int(d - block.timestamp);
|
timedOut = (isPlayerA && game.pB.hash1 == bytes32(0)) || (!isPlayerA && game.pA.hash1 == bytes32(0));
|
||||||
} else if (gm.p == GamePhase.FinalR && gm.ffr != 0) {
|
} else if (game.phase == GamePhase.FirstR && game.tReveal != 0 && block.timestamp > game.tReveal + REVEAL_TIMEOUT) {
|
||||||
uint d = gm.ffr + REVEAL_TIMEOUT;
|
timedOut = (isPlayerA && game.pB.move1 == Moves.None) || (!isPlayerA && game.pA.move1 == Moves.None);
|
||||||
if (block.timestamp >= d) return 0;
|
} else if (game.phase == GamePhase.WithdC && game.tWithdC != 0 && block.timestamp > game.tWithdC + COMMIT_TIMEOUT) {
|
||||||
return int(d - block.timestamp);
|
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_) {
|
function getGameDetails(uint gameId) public view returns (
|
||||||
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 (
|
|
||||||
Player memory, Player memory, uint, Outcomes, GamePhase, bool, uint, string memory) {
|
Player memory, Player memory, uint, Outcomes, GamePhase, bool, uint, string memory) {
|
||||||
GameState storage gm = g[id_];
|
GameState storage game = games[gameId];
|
||||||
require(gm.id != 0, "no");
|
require(game.gameId != 0, "Game does not exist");
|
||||||
return (gm.a, gm.b, gm.ib, gm.o, gm.p, gm.act, gm.id, gm.mode);
|
return (game.pA, game.pB, game.bet, game.outcome, game.phase, game.active, game.gameId, game.mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActiveGameIds() public view returns (uint[] memory) {
|
function getActiveGameIds() public view returns (uint[] memory) {
|
||||||
uint c = 0;
|
uint count = 0;
|
||||||
for (uint i = 0; i < ids.length; i++) if (g[ids[i]].act) c++;
|
for (uint i = 0; i < gameIds.length; i++) if (games[gameIds[i]].active) count++;
|
||||||
uint[] memory a = new uint[](c);
|
uint[] memory active = new uint[](count);
|
||||||
uint ix = 0;
|
uint idx = 0;
|
||||||
for (uint i = 0; i < ids.length; i++) if (g[ids[i]].act) a[ix++] = ids[i];
|
for (uint i = 0; i < gameIds.length; i++) if (games[gameIds[i]].active) active[idx++] = gameIds[i];
|
||||||
return a;
|
return active;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
528
docs/minusOne-example-gameplay.md
Normal file
528
docs/minusOne-example-gameplay.md
Normal file
@@ -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: `<move>-<password>` (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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user