mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
429 lines
15 KiB
Solidity
429 lines
15 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, FinalC, FinalR, 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;
|
|
}
|
|
|
|
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;
|
|
string mode;
|
|
}
|
|
|
|
mapping(uint => GameState) private g;
|
|
uint[] private ids;
|
|
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");
|
|
_;
|
|
}
|
|
|
|
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_);
|
|
}
|
|
revert("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_;
|
|
}
|
|
|
|
modifier isRegistered(uint id_) {
|
|
require(id_ != 0, "id");
|
|
require(msg.sender == g[id_].a.addr || msg.sender == g[id_].b.addr, "reg");
|
|
_;
|
|
}
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
return (m1_, m2_);
|
|
}
|
|
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
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 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);
|
|
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;
|
|
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 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 payWithSlash(address payable w, address payable, uint bet) private {
|
|
w.transfer(bet * 2);
|
|
}
|
|
|
|
function resetGame(uint id_) private {
|
|
g[id_].act = 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;
|
|
return 0;
|
|
}
|
|
|
|
function getGamePhase(uint id_) public view returns (GamePhase) {
|
|
return g[id_].p;
|
|
}
|
|
|
|
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);
|
|
}
|
|
return int(COMMIT_TIMEOUT);
|
|
}
|
|
|
|
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 (
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|