mirror of
https://github.com/averel10/crypto_clash.git
synced 2026-03-12 19:08:11 +01:00
draft of new mode
This commit is contained in:
@@ -40,6 +40,7 @@ contract Game {
|
|||||||
uint initialBet;
|
uint initialBet;
|
||||||
uint gameId;
|
uint gameId;
|
||||||
bool isActive;
|
bool isActive;
|
||||||
|
string gameMode; // "classic" for this contract
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapping from game ID to game state
|
// Mapping from game ID to game state
|
||||||
@@ -112,6 +113,7 @@ contract Game {
|
|||||||
|
|
||||||
games[gameId].gameId = gameId;
|
games[gameId].gameId = gameId;
|
||||||
games[gameId].isActive = true;
|
games[gameId].isActive = true;
|
||||||
|
games[gameId].gameMode = "classic";
|
||||||
gameIds.push(gameId);
|
gameIds.push(gameId);
|
||||||
|
|
||||||
return gameId;
|
return gameId;
|
||||||
@@ -454,7 +456,8 @@ contract Game {
|
|||||||
uint initialBet,
|
uint initialBet,
|
||||||
Outcomes outcome,
|
Outcomes outcome,
|
||||||
bool isActive,
|
bool isActive,
|
||||||
uint returnGameId
|
uint returnGameId,
|
||||||
|
string memory gameMode
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
GameState storage game = games[gameId];
|
GameState storage game = games[gameId];
|
||||||
@@ -465,7 +468,8 @@ contract Game {
|
|||||||
game.initialBet,
|
game.initialBet,
|
||||||
game.outcome,
|
game.outcome,
|
||||||
game.isActive,
|
game.isActive,
|
||||||
game.gameId
|
game.gameId,
|
||||||
|
game.gameMode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
709
crypto_clash_contract/contracts/GameMinusOne.sol
Normal file
709
crypto_clash_contract/contracts/GameMinusOne.sol
Normal file
@@ -0,0 +1,709 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
pragma solidity >=0.7.3;
|
||||||
|
|
||||||
|
contract GameMinusOne {
|
||||||
|
uint public constant BET_MIN = 1e16; // The minimum bet (0.01 ETH)
|
||||||
|
uint public constant REVEAL_TIMEOUT = 10 minutes; // Max delay of revelation phase
|
||||||
|
uint public constant COMMIT_TIMEOUT = 10 minutes; // Max delay of commit phase
|
||||||
|
uint public constant WITHDRAW_TIMEOUT = 10 minutes; // Max delay for withdrawal phase
|
||||||
|
|
||||||
|
enum Moves {
|
||||||
|
None,
|
||||||
|
Rock,
|
||||||
|
Paper,
|
||||||
|
Scissors
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Outcomes {
|
||||||
|
None,
|
||||||
|
PlayerA,
|
||||||
|
PlayerB,
|
||||||
|
Draw,
|
||||||
|
PlayerATimeout,
|
||||||
|
PlayerBTimeout
|
||||||
|
} // Possible outcomes
|
||||||
|
|
||||||
|
enum GamePhase {
|
||||||
|
Registration, // Waiting for players
|
||||||
|
InitialCommit, // Players commit 2 moves each
|
||||||
|
FirstReveal, // Players reveal both moves
|
||||||
|
Withdrawal, // Players choose which move to withdraw
|
||||||
|
FinalCommit, // Players commit their remaining move again (for fairness)
|
||||||
|
FinalReveal, // Players reveal final move
|
||||||
|
Completed // Game finished
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Player {
|
||||||
|
address payable addr;
|
||||||
|
uint bet;
|
||||||
|
// Initial phase - 2 moves
|
||||||
|
bytes32 encrMove1;
|
||||||
|
bytes32 encrMove2;
|
||||||
|
Moves move1;
|
||||||
|
Moves move2;
|
||||||
|
// Withdrawal phase
|
||||||
|
uint withdrawnMoveIndex; // 1 or 2
|
||||||
|
// Final phase
|
||||||
|
bytes32 encrFinalMove;
|
||||||
|
Moves finalMove;
|
||||||
|
string nickname;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GameState {
|
||||||
|
Player playerA;
|
||||||
|
Player playerB;
|
||||||
|
Outcomes outcome;
|
||||||
|
GamePhase phase;
|
||||||
|
uint firstCommit;
|
||||||
|
uint firstReveal;
|
||||||
|
uint firstWithdraw;
|
||||||
|
uint firstFinalCommit;
|
||||||
|
uint firstFinalReveal;
|
||||||
|
uint initialBet;
|
||||||
|
uint gameId;
|
||||||
|
bool isActive;
|
||||||
|
string gameMode; // "minusone" for this contract
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapping from game ID to game state
|
||||||
|
mapping(uint => GameState) private games;
|
||||||
|
|
||||||
|
// Array to track all game IDs (for enumeration)
|
||||||
|
uint[] private gameIds;
|
||||||
|
|
||||||
|
// Counter for generating unique game IDs
|
||||||
|
uint private nextGameId = 1;
|
||||||
|
|
||||||
|
// ------------------------- Registration ------------------------- //
|
||||||
|
|
||||||
|
modifier validBet(uint gameId) {
|
||||||
|
require(msg.value >= BET_MIN, "Minimum bet not met");
|
||||||
|
require(
|
||||||
|
games[gameId].initialBet == 0 ||
|
||||||
|
msg.value == games[gameId].initialBet,
|
||||||
|
"Bet value must match initial bet"
|
||||||
|
);
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a player to an existing game or create a new game.
|
||||||
|
// If gameId is 0, player will join or create the first available game.
|
||||||
|
// Return player's ID and game ID upon successful registration.
|
||||||
|
function register(
|
||||||
|
uint gameId,
|
||||||
|
string memory nickname
|
||||||
|
)
|
||||||
|
public
|
||||||
|
payable
|
||||||
|
validBet(gameId)
|
||||||
|
returns (uint playerId, uint returnGameId)
|
||||||
|
{
|
||||||
|
// If gameId is 0, find an open game or create a new one
|
||||||
|
if (gameId == 0) {
|
||||||
|
gameId = createNewGame();
|
||||||
|
}
|
||||||
|
|
||||||
|
require(games[gameId].isActive, "Game is not active");
|
||||||
|
require(games[gameId].phase == GamePhase.Registration, "Game already started");
|
||||||
|
require(bytes(nickname).length > 0, "Nickname cannot be empty");
|
||||||
|
require(bytes(nickname).length <= 20, "Nickname too long (max 20 characters)");
|
||||||
|
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
if (game.playerA.addr == address(0x0)) {
|
||||||
|
game.playerA.addr = payable(msg.sender);
|
||||||
|
game.playerA.nickname = nickname;
|
||||||
|
game.initialBet = msg.value;
|
||||||
|
return (1, gameId);
|
||||||
|
} else if (game.playerB.addr == address(0x0)) {
|
||||||
|
require(
|
||||||
|
msg.sender != game.playerA.addr,
|
||||||
|
"Cannot play against yourself"
|
||||||
|
);
|
||||||
|
game.playerB.addr = payable(msg.sender);
|
||||||
|
game.playerB.nickname = nickname;
|
||||||
|
|
||||||
|
// Both players registered, automatically start the game
|
||||||
|
game.phase = GamePhase.InitialCommit;
|
||||||
|
|
||||||
|
return (2, gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
revert("Game is full");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Create a new game
|
||||||
|
function createNewGame() private returns (uint) {
|
||||||
|
uint gameId = nextGameId;
|
||||||
|
nextGameId++;
|
||||||
|
|
||||||
|
games[gameId].gameId = gameId;
|
||||||
|
games[gameId].isActive = true;
|
||||||
|
games[gameId].phase = GamePhase.Registration;
|
||||||
|
games[gameId].gameMode = "minusone";
|
||||||
|
gameIds.push(gameId);
|
||||||
|
|
||||||
|
return gameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Initial Commit (2 moves) ------------------------- //
|
||||||
|
|
||||||
|
modifier isRegistered(uint gameId) {
|
||||||
|
require(gameId != 0, "Invalid game ID");
|
||||||
|
require(
|
||||||
|
msg.sender == games[gameId].playerA.addr ||
|
||||||
|
msg.sender == games[gameId].playerB.addr,
|
||||||
|
"Player not registered in this game"
|
||||||
|
);
|
||||||
|
_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit two moves for the initial phase
|
||||||
|
// encrMove1 and encrMove2 must be "<1|2|3>-password" hashed with keccak256
|
||||||
|
function commitInitialMoves(uint gameId, bytes32 encrMove1, bytes32 encrMove2)
|
||||||
|
public
|
||||||
|
isRegistered(gameId)
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is no longer active");
|
||||||
|
require(game.phase == GamePhase.InitialCommit, "Not in initial commit phase");
|
||||||
|
require(encrMove1 != bytes32(0) && encrMove2 != bytes32(0), "Encrypted moves cannot be zero");
|
||||||
|
require(encrMove1 != encrMove2, "Both moves must be different");
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (game.firstCommit != 0) {
|
||||||
|
require(block.timestamp <= game.firstCommit + COMMIT_TIMEOUT, "Commit phase timeout expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track first commit timestamp
|
||||||
|
if (game.firstCommit == 0) {
|
||||||
|
game.firstCommit = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store encrypted moves
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
require(game.playerA.encrMove1 == bytes32(0), "Player A already committed");
|
||||||
|
game.playerA.encrMove1 = encrMove1;
|
||||||
|
game.playerA.encrMove2 = encrMove2;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
require(game.playerB.encrMove1 == bytes32(0), "Player B already committed");
|
||||||
|
game.playerB.encrMove1 = encrMove1;
|
||||||
|
game.playerB.encrMove2 = encrMove2;
|
||||||
|
} else {
|
||||||
|
revert("Caller not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players have committed, advance to reveal phase
|
||||||
|
if (game.playerA.encrMove1 != bytes32(0) && game.playerB.encrMove1 != bytes32(0)) {
|
||||||
|
game.phase = GamePhase.FirstReveal;
|
||||||
|
game.firstReveal = 0; // Reset for reveal phase
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- First Reveal (2 moves) ------------------------- //
|
||||||
|
|
||||||
|
// Reveal both initial moves
|
||||||
|
// clearMove1 and clearMove2 must be the original strings used for hashing
|
||||||
|
function revealInitialMoves(uint gameId, string memory clearMove1, string memory clearMove2)
|
||||||
|
public
|
||||||
|
isRegistered(gameId)
|
||||||
|
returns (Moves, Moves)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is no longer active");
|
||||||
|
require(game.phase == GamePhase.FirstReveal, "Not in first reveal phase");
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (game.firstReveal != 0) {
|
||||||
|
require(block.timestamp <= game.firstReveal + REVEAL_TIMEOUT, "Reveal phase timeout expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes32 encrMove1 = keccak256(abi.encodePacked(clearMove1));
|
||||||
|
bytes32 encrMove2 = keccak256(abi.encodePacked(clearMove2));
|
||||||
|
Moves move1 = Moves(getFirstChar(clearMove1));
|
||||||
|
Moves move2 = Moves(getFirstChar(clearMove2));
|
||||||
|
|
||||||
|
require(move1 != Moves.None && move2 != Moves.None, "Invalid moves");
|
||||||
|
require(move1 != move2, "Both moves must be different");
|
||||||
|
|
||||||
|
// Verify and store moves
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
require(encrMove1 == game.playerA.encrMove1 && encrMove2 == game.playerA.encrMove2,
|
||||||
|
"Hash mismatch for Player A");
|
||||||
|
game.playerA.move1 = move1;
|
||||||
|
game.playerA.move2 = move2;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
require(encrMove1 == game.playerB.encrMove1 && encrMove2 == game.playerB.encrMove2,
|
||||||
|
"Hash mismatch for Player B");
|
||||||
|
game.playerB.move1 = move1;
|
||||||
|
game.playerB.move2 = move2;
|
||||||
|
} else {
|
||||||
|
revert("Caller not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start reveal timer on first reveal
|
||||||
|
if (game.firstReveal == 0) {
|
||||||
|
game.firstReveal = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players have revealed, advance to withdrawal phase
|
||||||
|
if (game.playerA.move1 != Moves.None && game.playerB.move1 != Moves.None) {
|
||||||
|
game.phase = GamePhase.Withdrawal;
|
||||||
|
game.firstWithdraw = 0; // Reset for withdrawal phase
|
||||||
|
}
|
||||||
|
|
||||||
|
return (move1, move2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Withdrawal Phase ------------------------- //
|
||||||
|
|
||||||
|
// Choose which move to withdraw (1 or 2)
|
||||||
|
function withdrawMove(uint gameId, uint moveIndex)
|
||||||
|
public
|
||||||
|
isRegistered(gameId)
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is no longer active");
|
||||||
|
require(game.phase == GamePhase.Withdrawal, "Not in withdrawal phase");
|
||||||
|
require(moveIndex == 1 || moveIndex == 2, "Move index must be 1 or 2");
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (game.firstWithdraw != 0) {
|
||||||
|
require(block.timestamp <= game.firstWithdraw + WITHDRAW_TIMEOUT, "Withdrawal phase timeout expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store withdrawal choice
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
require(game.playerA.withdrawnMoveIndex == 0, "Player A already withdrew");
|
||||||
|
game.playerA.withdrawnMoveIndex = moveIndex;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
require(game.playerB.withdrawnMoveIndex == 0, "Player B already withdrew");
|
||||||
|
game.playerB.withdrawnMoveIndex = moveIndex;
|
||||||
|
} else {
|
||||||
|
revert("Caller not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start withdrawal timer on first withdrawal
|
||||||
|
if (game.firstWithdraw == 0) {
|
||||||
|
game.firstWithdraw = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players have withdrawn, advance to final commit phase
|
||||||
|
if (game.playerA.withdrawnMoveIndex != 0 && game.playerB.withdrawnMoveIndex != 0) {
|
||||||
|
game.phase = GamePhase.FinalCommit;
|
||||||
|
game.firstFinalCommit = 0; // Reset for final commit
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Final Commit (remaining move) ------------------------- //
|
||||||
|
|
||||||
|
// Commit the remaining move again (for fairness and to prevent cheating)
|
||||||
|
function commitFinalMove(uint gameId, bytes32 encrFinalMove)
|
||||||
|
public
|
||||||
|
isRegistered(gameId)
|
||||||
|
returns (bool)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is no longer active");
|
||||||
|
require(game.phase == GamePhase.FinalCommit, "Not in final commit phase");
|
||||||
|
require(encrFinalMove != bytes32(0), "Encrypted move cannot be zero");
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (game.firstFinalCommit != 0) {
|
||||||
|
require(block.timestamp <= game.firstFinalCommit + COMMIT_TIMEOUT, "Final commit timeout expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store encrypted final move
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
require(game.playerA.encrFinalMove == bytes32(0), "Player A already committed final move");
|
||||||
|
game.playerA.encrFinalMove = encrFinalMove;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
require(game.playerB.encrFinalMove == bytes32(0), "Player B already committed final move");
|
||||||
|
game.playerB.encrFinalMove = encrFinalMove;
|
||||||
|
} else {
|
||||||
|
revert("Caller not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start final commit timer on first commit
|
||||||
|
if (game.firstFinalCommit == 0) {
|
||||||
|
game.firstFinalCommit = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players have committed, advance to final reveal phase
|
||||||
|
if (game.playerA.encrFinalMove != bytes32(0) && game.playerB.encrFinalMove != bytes32(0)) {
|
||||||
|
game.phase = GamePhase.FinalReveal;
|
||||||
|
game.firstFinalReveal = 0; // Reset for final reveal
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Final Reveal ------------------------- //
|
||||||
|
|
||||||
|
// Reveal the final move and determine winner
|
||||||
|
function revealFinalMove(uint gameId, string memory clearFinalMove)
|
||||||
|
public
|
||||||
|
isRegistered(gameId)
|
||||||
|
returns (Moves)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is no longer active");
|
||||||
|
require(game.phase == GamePhase.FinalReveal, "Not in final reveal phase");
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (game.firstFinalReveal != 0) {
|
||||||
|
require(block.timestamp <= game.firstFinalReveal + REVEAL_TIMEOUT, "Final reveal timeout expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes32 encrFinalMove = keccak256(abi.encodePacked(clearFinalMove));
|
||||||
|
Moves finalMove = Moves(getFirstChar(clearFinalMove));
|
||||||
|
|
||||||
|
require(finalMove != Moves.None, "Invalid move");
|
||||||
|
|
||||||
|
// Verify and store final move
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
require(encrFinalMove == game.playerA.encrFinalMove, "Hash mismatch for Player A final move");
|
||||||
|
// Verify this is the non-withdrawn move
|
||||||
|
Moves expectedMove = game.playerA.withdrawnMoveIndex == 1 ? game.playerA.move2 : game.playerA.move1;
|
||||||
|
require(finalMove == expectedMove, "Final move must be the non-withdrawn move");
|
||||||
|
game.playerA.finalMove = finalMove;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
require(encrFinalMove == game.playerB.encrFinalMove, "Hash mismatch for Player B final move");
|
||||||
|
// Verify this is the non-withdrawn move
|
||||||
|
Moves expectedMove = game.playerB.withdrawnMoveIndex == 1 ? game.playerB.move2 : game.playerB.move1;
|
||||||
|
require(finalMove == expectedMove, "Final move must be the non-withdrawn move");
|
||||||
|
game.playerB.finalMove = finalMove;
|
||||||
|
} else {
|
||||||
|
revert("Caller not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start final reveal timer on first reveal
|
||||||
|
if (game.firstFinalReveal == 0) {
|
||||||
|
game.firstFinalReveal = block.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if both players have revealed final moves, determine outcome
|
||||||
|
if (game.playerA.finalMove != Moves.None && game.playerB.finalMove != Moves.None) {
|
||||||
|
determineOutcome(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalMove;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the final outcome based on both players' final moves
|
||||||
|
function determineOutcome(uint gameId) private {
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
if (game.playerA.finalMove == game.playerB.finalMove) {
|
||||||
|
game.outcome = Outcomes.Draw;
|
||||||
|
} else if (
|
||||||
|
(game.playerA.finalMove == Moves.Rock && game.playerB.finalMove == Moves.Scissors) ||
|
||||||
|
(game.playerA.finalMove == Moves.Paper && game.playerB.finalMove == Moves.Rock) ||
|
||||||
|
(game.playerA.finalMove == Moves.Scissors && game.playerB.finalMove == Moves.Paper)
|
||||||
|
) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
} else {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
}
|
||||||
|
|
||||||
|
game.phase = GamePhase.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return first character of a given string.
|
||||||
|
// Returns 0 if the string is empty or the first character is not '1','2' or '3'.
|
||||||
|
function getFirstChar(string memory str) private pure returns (uint) {
|
||||||
|
bytes memory b = bytes(str);
|
||||||
|
if (b.length == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
bytes1 firstByte = b[0];
|
||||||
|
if (firstByte == 0x31) {
|
||||||
|
return 1;
|
||||||
|
} else if (firstByte == 0x32) {
|
||||||
|
return 2;
|
||||||
|
} else if (firstByte == 0x33) {
|
||||||
|
return 3;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Result ------------------------- //
|
||||||
|
|
||||||
|
// Compute the outcome and pay the winner(s).
|
||||||
|
// Return the outcome.
|
||||||
|
function getOutcome(uint gameId) public returns (Outcomes) {
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
require(game.isActive, "Game is not active");
|
||||||
|
require(game.phase == GamePhase.Completed, "Game not completed yet");
|
||||||
|
require(game.outcome != Outcomes.None, "Outcome not yet determined");
|
||||||
|
|
||||||
|
address payable addrA = game.playerA.addr;
|
||||||
|
address payable addrB = game.playerB.addr;
|
||||||
|
uint betPlayerA = game.initialBet;
|
||||||
|
|
||||||
|
// Reset and cleanup
|
||||||
|
resetGame(gameId); // Reset game before paying to avoid reentrancy attacks
|
||||||
|
pay(addrA, addrB, betPlayerA, game.outcome);
|
||||||
|
|
||||||
|
return game.outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pay the winner(s).
|
||||||
|
function pay(
|
||||||
|
address payable addrA,
|
||||||
|
address payable addrB,
|
||||||
|
uint betPlayerA,
|
||||||
|
Outcomes outcome
|
||||||
|
) private {
|
||||||
|
if (outcome == Outcomes.PlayerA) {
|
||||||
|
addrA.transfer(betPlayerA * 2);
|
||||||
|
} else if (outcome == Outcomes.PlayerB) {
|
||||||
|
addrB.transfer(betPlayerA * 2);
|
||||||
|
} else {
|
||||||
|
addrA.transfer(betPlayerA);
|
||||||
|
addrB.transfer(betPlayerA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pay to one player and slash the other (timeout resolution).
|
||||||
|
function payWithSlash(
|
||||||
|
address payable winner,
|
||||||
|
address payable loser,
|
||||||
|
uint betAmount
|
||||||
|
) private {
|
||||||
|
// Winner gets both bets
|
||||||
|
winner.transfer(betAmount * 2);
|
||||||
|
// Loser gets nothing (slashed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset a specific game.
|
||||||
|
function resetGame(uint gameId) private {
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
// Mark game as inactive
|
||||||
|
game.isActive = false;
|
||||||
|
|
||||||
|
// Note: We keep the game data in the mapping for reference
|
||||||
|
// but players are now free to join other games
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Helpers ------------------------- //
|
||||||
|
|
||||||
|
// Return contract balance
|
||||||
|
function getContractBalance() public view returns (uint) {
|
||||||
|
return address(this).balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return player's ID in their active game
|
||||||
|
function whoAmI(uint gameId) public view returns (uint) {
|
||||||
|
if (gameId == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
if (msg.sender == game.playerA.addr) {
|
||||||
|
return 1;
|
||||||
|
} else if (msg.sender == game.playerB.addr) {
|
||||||
|
return 2;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current phase of the game
|
||||||
|
function getGamePhase(uint gameId) public view returns (GamePhase) {
|
||||||
|
return games[gameId].phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get time remaining in current phase
|
||||||
|
function getTimeLeft(uint gameId) public view returns (int) {
|
||||||
|
if (gameId == 0) return 0;
|
||||||
|
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
|
||||||
|
if (game.phase == GamePhase.InitialCommit && game.firstCommit != 0) {
|
||||||
|
uint deadline = game.firstCommit + COMMIT_TIMEOUT;
|
||||||
|
if (block.timestamp >= deadline) return 0;
|
||||||
|
return int(deadline - block.timestamp);
|
||||||
|
} else if (game.phase == GamePhase.FirstReveal && game.firstReveal != 0) {
|
||||||
|
uint deadline = game.firstReveal + REVEAL_TIMEOUT;
|
||||||
|
if (block.timestamp >= deadline) return 0;
|
||||||
|
return int(deadline - block.timestamp);
|
||||||
|
} else if (game.phase == GamePhase.Withdrawal && game.firstWithdraw != 0) {
|
||||||
|
uint deadline = game.firstWithdraw + WITHDRAW_TIMEOUT;
|
||||||
|
if (block.timestamp >= deadline) return 0;
|
||||||
|
return int(deadline - block.timestamp);
|
||||||
|
} else if (game.phase == GamePhase.FinalCommit && game.firstFinalCommit != 0) {
|
||||||
|
uint deadline = game.firstFinalCommit + COMMIT_TIMEOUT;
|
||||||
|
if (block.timestamp >= deadline) return 0;
|
||||||
|
return int(deadline - block.timestamp);
|
||||||
|
} else if (game.phase == GamePhase.FinalReveal && game.firstFinalReveal != 0) {
|
||||||
|
uint deadline = game.firstFinalReveal + REVEAL_TIMEOUT;
|
||||||
|
if (block.timestamp >= deadline) return 0;
|
||||||
|
return int(deadline - block.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(COMMIT_TIMEOUT); // Default timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a game that has timed out
|
||||||
|
function resolveTimeout(uint gameId) public isRegistered(gameId) {
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
require(game.isActive, "Game is not active");
|
||||||
|
|
||||||
|
address caller = msg.sender;
|
||||||
|
address payable winner = payable(caller);
|
||||||
|
|
||||||
|
bool timeoutOccurred = false;
|
||||||
|
|
||||||
|
// Check for timeout in various phases
|
||||||
|
if (game.phase == GamePhase.InitialCommit && game.firstCommit != 0) {
|
||||||
|
if (block.timestamp > game.firstCommit + COMMIT_TIMEOUT) {
|
||||||
|
// Player who didn't commit loses
|
||||||
|
if (caller == game.playerA.addr && game.playerB.encrMove1 == bytes32(0)) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
} else if (caller == game.playerB.addr && game.playerA.encrMove1 == bytes32(0)) {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (game.phase == GamePhase.FirstReveal && game.firstReveal != 0) {
|
||||||
|
if (block.timestamp > game.firstReveal + REVEAL_TIMEOUT) {
|
||||||
|
// Player who didn't reveal loses
|
||||||
|
if (caller == game.playerA.addr && game.playerB.move1 == Moves.None) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
} else if (caller == game.playerB.addr && game.playerA.move1 == Moves.None) {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (game.phase == GamePhase.Withdrawal && game.firstWithdraw != 0) {
|
||||||
|
if (block.timestamp > game.firstWithdraw + WITHDRAW_TIMEOUT) {
|
||||||
|
// Player who didn't withdraw loses
|
||||||
|
if (caller == game.playerA.addr && game.playerB.withdrawnMoveIndex == 0) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
} else if (caller == game.playerB.addr && game.playerA.withdrawnMoveIndex == 0) {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (game.phase == GamePhase.FinalCommit && game.firstFinalCommit != 0) {
|
||||||
|
if (block.timestamp > game.firstFinalCommit + COMMIT_TIMEOUT) {
|
||||||
|
// Player who didn't commit final move loses
|
||||||
|
if (caller == game.playerA.addr && game.playerB.encrFinalMove == bytes32(0)) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
} else if (caller == game.playerB.addr && game.playerA.encrFinalMove == bytes32(0)) {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (game.phase == GamePhase.FinalReveal && game.firstFinalReveal != 0) {
|
||||||
|
if (block.timestamp > game.firstFinalReveal + REVEAL_TIMEOUT) {
|
||||||
|
// Player who didn't reveal final move loses
|
||||||
|
if (caller == game.playerA.addr && game.playerB.finalMove == Moves.None) {
|
||||||
|
game.outcome = Outcomes.PlayerA;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
} else if (caller == game.playerB.addr && game.playerA.finalMove == Moves.None) {
|
||||||
|
game.outcome = Outcomes.PlayerB;
|
||||||
|
timeoutOccurred = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require(timeoutOccurred, "No timeout has occurred or caller is not the non-offending player");
|
||||||
|
|
||||||
|
address payable loser = winner == game.playerA.addr ? game.playerB.addr : game.playerA.addr;
|
||||||
|
|
||||||
|
// Reset game
|
||||||
|
resetGame(gameId);
|
||||||
|
|
||||||
|
// Pay winner and slash offender
|
||||||
|
payWithSlash(winner, loser, game.initialBet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------- Game Management ------------------------- //
|
||||||
|
|
||||||
|
// Get details of a specific game (for viewing any game)
|
||||||
|
function getGameDetails(
|
||||||
|
uint gameId
|
||||||
|
)
|
||||||
|
public
|
||||||
|
view
|
||||||
|
returns (
|
||||||
|
Player memory playerA,
|
||||||
|
Player memory playerB,
|
||||||
|
uint initialBet,
|
||||||
|
Outcomes outcome,
|
||||||
|
GamePhase phase,
|
||||||
|
bool isActive,
|
||||||
|
uint returnGameId,
|
||||||
|
string memory gameMode
|
||||||
|
)
|
||||||
|
{
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
require(game.gameId != 0, "Game does not exist");
|
||||||
|
return (
|
||||||
|
game.playerA,
|
||||||
|
game.playerB,
|
||||||
|
game.initialBet,
|
||||||
|
game.outcome,
|
||||||
|
game.phase,
|
||||||
|
game.isActive,
|
||||||
|
game.gameId,
|
||||||
|
game.gameMode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all active game IDs
|
||||||
|
function getActiveGameIds() public view returns (uint[] memory) {
|
||||||
|
uint activeCount = 0;
|
||||||
|
|
||||||
|
// Count active games
|
||||||
|
for (uint i = 0; i < gameIds.length; i++) {
|
||||||
|
if (games[gameIds[i]].isActive) {
|
||||||
|
activeCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build array of active game IDs
|
||||||
|
uint[] memory activeIds = new uint[](activeCount);
|
||||||
|
uint index = 0;
|
||||||
|
for (uint i = 0; i < gameIds.length; i++) {
|
||||||
|
if (games[gameIds[i]].isActive) {
|
||||||
|
activeIds[index] = gameIds[i];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance game to initial commit phase (called automatically after both players register)
|
||||||
|
function startGame(uint gameId) public {
|
||||||
|
GameState storage game = games[gameId];
|
||||||
|
require(game.isActive, "Game is not active");
|
||||||
|
require(game.phase == GamePhase.Registration, "Game already started");
|
||||||
|
require(game.playerA.addr != address(0) && game.playerB.addr != address(0), "Both players must be registered");
|
||||||
|
|
||||||
|
game.phase = GamePhase.InitialCommit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ export default function GameList({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [newGameBet, setNewGameBet] = useState<string>("0.01");
|
const [newGameBet, setNewGameBet] = useState<string>("0.01");
|
||||||
const [newGameNickname, setNewGameNickname] = useState<string>("");
|
const [newGameNickname, setNewGameNickname] = useState<string>("");
|
||||||
|
const [gameMode, setGameMode] = useState<string>("classic"); // "classic" or "minusone"
|
||||||
const [joinNicknames, setJoinNicknames] = useState<Map<number, string>>(new Map());
|
const [joinNicknames, setJoinNicknames] = useState<Map<number, string>>(new Map());
|
||||||
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
const [refreshInterval, setRefreshInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
const [userGameIds, setUserGameIds] = useState<Set<number>>(new Set());
|
const [userGameIds, setUserGameIds] = useState<Set<number>>(new Set());
|
||||||
@@ -39,7 +40,16 @@ export default function GameList({
|
|||||||
|
|
||||||
for (const gameId of activeGameIds) {
|
for (const gameId of activeGameIds) {
|
||||||
const details = await contract.methods.getGameDetails(gameId).call();
|
const details = await contract.methods.getGameDetails(gameId).call();
|
||||||
gameDetails.push(details);
|
// Map contract response to GameDetails type
|
||||||
|
gameDetails.push({
|
||||||
|
playerA: details[0],
|
||||||
|
playerB: details[1],
|
||||||
|
initialBet: details[2],
|
||||||
|
outcome: Number(details[3]),
|
||||||
|
isActive: details[4],
|
||||||
|
returnGameId: Number(details[5]),
|
||||||
|
gameMode: details[6] || "classic", // Fallback to "classic" for older contracts
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setGames(gameDetails);
|
setGames(gameDetails);
|
||||||
@@ -210,6 +220,14 @@ export default function GameList({
|
|||||||
onChange={(e) => setNewGameBet(e.target.value)}
|
onChange={(e) => setNewGameBet(e.target.value)}
|
||||||
className="flex-1 min-w-[200px]"
|
className="flex-1 min-w-[200px]"
|
||||||
/>
|
/>
|
||||||
|
<select
|
||||||
|
value={gameMode}
|
||||||
|
onChange={(e) => setGameMode(e.target.value)}
|
||||||
|
className="flex-1 min-w-[200px] px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="classic">Classic Mode</option>
|
||||||
|
<option value="minusone">Minus One Mode (Squid Game)</option>
|
||||||
|
</select>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleCreateGame}
|
onClick={handleCreateGame}
|
||||||
disabled={loading || !account || !contract}
|
disabled={loading || !account || !contract}
|
||||||
@@ -220,7 +238,7 @@ export default function GameList({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
<p className="text-xs text-slate-600 dark:text-slate-400 mt-2">
|
||||||
Enter your nickname and bet amount in ETH (e.g., 0.01 for 0.01 ETH).
|
Enter your nickname, bet amount, and choose game mode. Classic for standard Rock-Paper-Scissors, or Minus One for the Squid Game variant (2 moves, withdraw 1).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -250,9 +268,14 @@ export default function GameList({
|
|||||||
>
|
>
|
||||||
{/* Game ID Header */}
|
{/* Game ID Header */}
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
|
<p className="font-semibold text-lg text-indigo-600 dark:text-indigo-400">
|
||||||
Game #{game.returnGameId}
|
Game #{game.returnGameId}
|
||||||
</p>
|
</p>
|
||||||
|
<span className="text-xs font-semibold px-2 py-1 rounded bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200">
|
||||||
|
{game.gameMode === "minusone" ? "Minus One" : "Classic"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
<span className={`text-xs font-semibold px-2 py-1 rounded ${getGamePhase(game).color}`}>
|
<span className={`text-xs font-semibold px-2 py-1 rounded ${getGamePhase(game).color}`}>
|
||||||
{getGamePhase(game).phase}
|
{getGamePhase(game).phase}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export type GameDetails = {
|
|||||||
outcome: number;
|
outcome: number;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
returnGameId: number;
|
returnGameId: number;
|
||||||
|
gameMode?: string; // "classic" or "minusone"
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GameModalProps {
|
interface GameModalProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user