diff --git a/packages/hardhat/contracts/TicTacToe.sol b/packages/hardhat/contracts/TicTacToe.sol index 8350b9c..51e9183 100644 --- a/packages/hardhat/contracts/TicTacToe.sol +++ b/packages/hardhat/contracts/TicTacToe.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -import "hardhat/console.sol"; /** * @title TicTacToe @@ -13,14 +12,15 @@ import "hardhat/console.sol"; contract TicTacToe { uint256 public gameIdCounter = 0; - uint256 public immutable timeOutValue = 10 seconds; + uint256 public immutable timeOutValue = 20 minutes; enum GameState { PENDING, PLAYING, PLAYER1WON, PLAYER2WON, - TIE + TIE, + DELETED } struct Game { @@ -32,7 +32,7 @@ contract TicTacToe { bool player2Withdrawn; uint8[9] board; // 0 (no player): empty, 1 (player 1): X, 2 (player 2): O uint8 moves; // Counter or the number of moves made - uint256 lastTimePlayed; + uint256 lastTimePlayed; } mapping(uint256 => Game) public games; @@ -48,10 +48,7 @@ contract TicTacToe { address indexed team1, address indexed team2 ); - event GameDeleted( - uint256 indexed gameId, - address indexed player1 - ); + event GameDeleted(uint256 indexed gameId); event MoveMade( uint256 indexed gameId, address indexed player, @@ -118,7 +115,7 @@ contract TicTacToe { player2Withdrawn: false, board: [0, 0, 0, 0, 0, 0, 0, 0, 0], moves: 0, - lastTimePlayed : 0 + lastTimePlayed: 0 }); // This event can be used by the frontend to know that something happened and react to it @@ -137,34 +134,14 @@ contract TicTacToe { game.bet == msg.value, "You haven't sent the required ETH to accept" ); - // Set the initial time count to check the timeout - games[_gameId].lastTimePlayed = block.timestamp; + // Set the initial time count to check the timeout + games[_gameId].lastTimePlayed = block.timestamp; // Set the game state to PLAYING and emit an event games[_gameId].state = GameState.PLAYING; emit GameAccepted(_gameId, game.player1, game.player2); } - function deleteGame(uint256 _gameId) external { - Game memory game = games[_gameId]; - require(game.player1 == msg.sender, "You must be player 1 to delete the request"); - require( - game.state == GameState.PENDING, - "Game must be PENDING to be deleted" - ); - require( - block.timestamp - games[_gameId].lastTimePlayed <= timeOutValue, - "Timeout value hasnt been reached yet!" - ); - - games[_gameId].state = GameState.TIE; - uint256 paidBetAmount = calculatePrize(_gameId); - require(paidBetAmount > 0, "Invalid bet amount"); - // Transfer the bet to the player - payable(msg.sender).transfer(paidBetAmount); - emit GameDeleted(_gameId, game.player1); - } - function makeMove( uint256 _gameId, uint8 position @@ -172,17 +149,46 @@ contract TicTacToe { // Determine the current Player symbol // 1 is player1, 2 is player2 uint8 playerSymbol = games[_gameId].moves % 2 == 0 ? 1 : 2; - // Add the corresponding mark in the position of the game board - games[_gameId].board[position] = playerSymbol; - // And add 1 to the number of moves made in the game - games[_gameId].moves++; - games[_gameId].lastTimePlayed = block.timestamp; + // Add the corresponding mark in the position of the game board + games[_gameId].board[position] = playerSymbol; + // And add 1 to the number of moves made in the game + games[_gameId].moves++; + games[_gameId].lastTimePlayed = block.timestamp; - emit MoveMade(_gameId, msg.sender, position); + emit MoveMade(_gameId, msg.sender, position); // Check if after adding that symbol, a win is achieved, and react to it if that's the case checkWin(_gameId, position, msg.sender); } + // For when game wasn't accepted and player wants to recover betted ammount + function deleteGame(uint256 _gameId) external { + Game memory game = games[_gameId]; + require( + msg.sender == game.player1, + "You must be player 1 to delete the request" + ); + require( + game.state == GameState.PENDING, + "Game must be PENDING to be deleted" + ); + + games[_gameId].state = GameState.DELETED; + + payable(msg.sender).transfer(game.bet); + emit GameDeleted(_gameId); + } + + // To incentivize players to keep playing or not quit half a game to not allow the other part to withdraw prize + function winByTimeout(uint256 _gameId) external { + require( + block.timestamp - games[_gameId].lastTimePlayed > timeOutValue, + "Timeout value hasnt been reached yet!" + ); + games[_gameId].moves % 2 == 0 + ? finishGame(_gameId, games[_gameId].player2) + : finishGame(_gameId, games[_gameId].player1); + } + // Function to withdraw the prize based on game state function withdrawPrize(uint256 _gameId) external { Game storage game = games[_gameId]; @@ -202,7 +208,7 @@ contract TicTacToe { "You have already withdrawn the prize!" ); game.player1Withdrawn = true; - } + } // WITHDRAW RULES FOR PLAYER 2 VICTORY if (game.state == GameState.PLAYER2WON && msg.sender == game.player2) { @@ -211,17 +217,23 @@ contract TicTacToe { "You have already withdrawn the prize!" ); game.player2Withdrawn = true; - } + } // WITHDRAW RULES FOR TIE RESULT if (game.state == GameState.TIE) { if (msg.sender == game.player1) { - require(!game.player1Withdrawn,"You have already withdrawn the prize!"); + require( + !game.player1Withdrawn, + "You have already withdrawn the prize!" + ); game.player1Withdrawn = true; } else if (msg.sender == game.player2) { - require(!game.player2Withdrawn, "You have already withdrawn the prize!"); + require( + !game.player2Withdrawn, + "You have already withdrawn the prize!" + ); game.player2Withdrawn = true; - } + } } // Calculate and transfer the prize based on the game state @@ -234,11 +246,6 @@ contract TicTacToe { /* INTERNAL FUNCTIONS */ - function winByTimeout(uint256 _gameId) internal { - require(block.timestamp - games[_gameId].lastTimePlayed > timeOutValue, "Timeout value hasnt been reached yet!"); - games[_gameId].moves % 2 == 0 ? finishGame(_gameId, games[_gameId].player2) : finishGame(_gameId, games[_gameId].player1); - } - function checkWin( uint256 _gameId, uint8 _position, @@ -330,7 +337,7 @@ contract TicTacToe { return totalBet; } else if (game.state == GameState.TIE) { // In the case of a tie, split the total bet equally between players - return totalBet / 2; + return game.bet; } else { // Invalid game state revert("Invalid game state"); @@ -351,15 +358,23 @@ contract TicTacToe { return games[_gameId].board; } - function getGameState(uint256 _gameId) public view returns (GameState) { + function getGameState(uint256 _gameId) public view returns (GameState) { return games[_gameId].state; } - function hasPlayer1WithdrawnPrize(uint256 _gameId) public view returns(bool) { + function getLastTimePlayed(uint256 _gameId) public view returns (uint256) { + return games[_gameId].lastTimePlayed; + } + + function hasPlayer1WithdrawnPrize( + uint256 _gameId + ) public view returns (bool) { return games[_gameId].player1Withdrawn; } - function hasPlayer2WithdrawnPrize(uint256 _gameId) public view returns(bool) { + function hasPlayer2WithdrawnPrize( + uint256 _gameId + ) public view returns (bool) { return games[_gameId].player2Withdrawn; } } diff --git a/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx b/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx index 35df368..9845fde 100755 --- a/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx +++ b/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx @@ -9,10 +9,12 @@ const TicTacToeBoard: React.FC = ({ game, isGameAccepted, isGameFinished, + isGameDeleted, currentPlayer, // movesMade, }) => { const [board, setBoard] = useState(Array(9).fill(0)); // Initialize an empty board + // const [timeRemaining, setTimeRemaining] = useState(""); const { data: boardFromContract } = useScaffoldContractRead({ contractName: "TicTacToe", @@ -44,6 +46,37 @@ const TicTacToeBoard: React.FC = ({ args: [BigInt(game.gameId)], }); + // const { data: lastTimePlayed } = useScaffoldContractRead({ + // contractName: "TicTacToe", + // functionName: "getLastTimePlayed", + // args: [BigInt(game.gameId)], + // }); + + // useEffect(() => { + // let deadline: number; + + // if (lastTimePlayed) { + // deadline = parseInt(lastTimePlayed.toString(), 10); + // } + + // const timer = setInterval(() => { + // const now = Math.floor(new Date().getTime() / 1000); + + // if (now >= deadline) { + // setTimeRemaining("Deadline has passed"); + // clearInterval(timer); + // } else { + // const timeRemainingSeconds = deadline - now; + // const hours = Math.floor(timeRemainingSeconds / 3600); + // const minutes = Math.floor((timeRemainingSeconds % 3600) / 60); + // const seconds = Math.floor(timeRemainingSeconds % 60); + // setTimeRemaining(`${hours}:${minutes}:${seconds}`); + // } + // }, 1000); + + // return () => clearInterval(timer); + // }, [lastTimePlayed]); + const { writeAsync: makeMove } = useScaffoldContractWrite({ contractName: "TicTacToe", functionName: "makeMove", @@ -57,6 +90,18 @@ const TicTacToeBoard: React.FC = ({ value: BigInt(game.bet), }); + const { writeAsync: deleteGame } = useScaffoldContractWrite({ + contractName: "TicTacToe", + functionName: "deleteGame", + args: [BigInt(game.gameId)], + }); + + // const { writeAsync: winByTimeout } = useScaffoldContractWrite({ + // contractName: "TicTacToe", + // functionName: "winByTimeout", + // args: [BigInt(game.gameId)], + // }); + const { writeAsync: withdrawPrize } = useScaffoldContractWrite({ contractName: "TicTacToe", functionName: "withdrawPrize", @@ -76,8 +121,19 @@ const TicTacToeBoard: React.FC = ({ {isGameAccepted ? (isGameFinished ? "played against" : "is playing against") : "challenged"}
+ {/* {isGameAccepted ? ( + + {timeRemaining} + + ) : ( + "" + )} */} {isGameAccepted ? ( "" + ) : isGameDeleted ? ( + + Game was deleted + ) : ( = ({ + ) : game.player1 === currentPlayer ? ( + <> + + Recover your betted amount (if any) + ) : ( "" )} diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index b3aa915..eab5458 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -7,7 +7,7 @@ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; const deployedContracts = { 31337: { TicTacToe: { - address: "0x5FbDB2315678afecb367f032d93F642f64180aa3", + address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", abi: [ { anonymous: false, @@ -74,12 +74,6 @@ const deployedContracts = { name: "gameId", type: "uint256", }, - { - indexed: true, - internalType: "address", - name: "player1", - type: "address", - }, ], name: "GameDeleted", type: "event", @@ -297,6 +291,25 @@ const deployedContracts = { stateMutability: "view", type: "function", }, + { + inputs: [ + { + internalType: "uint256", + name: "_gameId", + type: "uint256", + }, + ], + name: "getLastTimePlayed", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, { inputs: [ { diff --git a/packages/nextjs/pages/index.tsx b/packages/nextjs/pages/index.tsx index c681360..3404f15 100644 --- a/packages/nextjs/pages/index.tsx +++ b/packages/nextjs/pages/index.tsx @@ -23,6 +23,13 @@ const Home: NextPage = () => { watch: true, }); + const { data: GameDeletedHistory } = useScaffoldEventHistory({ + contractName: "TicTacToe", + eventName: "GameDeleted", + fromBlock: BigInt(process.env.NEXT_PUBLIC_DEPLOY_BLOCK || "0"), + watch: true, + }); + const { data: GameFinishedHistory } = useScaffoldEventHistory({ contractName: "TicTacToe", eventName: "GameFinished", @@ -40,8 +47,9 @@ const Home: NextPage = () => { const gameCards = GameCreatedHistory?.map(game => { const isGameAccepted = GameAcceptedHistory?.some(acceptedGame => acceptedGame.args[0] === game.args[0]); const isGameFinished = GameFinishedHistory?.some(finishedGame => finishedGame.args[0] === game.args[0]); + const isGameDeleted = GameDeletedHistory?.some(deletedGame => deletedGame.args[0] === game.args[0]); const movesMade = MoveMadeHistory?.filter(moveMade => moveMade.args[0] == game.args[0]); - return { game, isGameAccepted, isGameFinished, movesMade }; + return { game, isGameAccepted, isGameFinished, isGameDeleted, movesMade }; }); return ( @@ -82,7 +90,7 @@ const Home: NextPage = () => { {/* ⭕ See your active challenges! ❌ */} - {gameCards?.map(({ game, isGameAccepted, isGameFinished, movesMade }) => ( + {gameCards?.map(({ game, isGameAccepted, isGameFinished, isGameDeleted, movesMade }) => ( { }} isGameAccepted={isGameAccepted} isGameFinished={isGameFinished} + isGameDeleted={isGameDeleted} currentPlayer={connectedAddress} movesMade={movesMade} /> diff --git a/packages/nextjs/types/TicTacToeTypes.ts b/packages/nextjs/types/TicTacToeTypes.ts index 512a063..3acbd29 100644 --- a/packages/nextjs/types/TicTacToeTypes.ts +++ b/packages/nextjs/types/TicTacToeTypes.ts @@ -27,6 +27,7 @@ export type TicTacToeBoardProps = { game: GameCreatedProps; isGameAccepted?: boolean; isGameFinished?: boolean; + isGameDeleted?: boolean; currentPlayer?: string; movesMade?: any; };