diff --git a/packages/hardhat/contracts/TicTacToe.sol b/packages/hardhat/contracts/TicTacToe.sol index e3b8616..3717931 100644 --- a/packages/hardhat/contracts/TicTacToe.sol +++ b/packages/hardhat/contracts/TicTacToe.sol @@ -2,20 +2,22 @@ pragma solidity 0.8.17; /** - * @title A Tic Tac Toe game + * @title TicTacToe * @author Lulox - * @notice This contract is for creating a bet between two parts on the outcome of a Tic Tac Toe Game + * @notice A betting TicTacToe contract. + * @dev Currently for using with one transaction per move, + * in a future may be replaced with signatures */ + contract TicTacToe { - uint256 public gameIdCounter = 1; + uint256 public gameIdCounter = 0; enum GameState { PENDING, PLAYING, PLAYER1WON, PLAYER2WON, - TIE, - CANCELED + TIE } struct Game { @@ -23,10 +25,9 @@ contract TicTacToe { address player2; GameState state; uint256 bet; - uint256 lastMoveTime; - bool player1Withdrawn; // Indicates whether player 1 has withdrawn or not - bool player2Withdrawn; // Indicates whether player 2 has withdrawn or not - uint8[9] board; // 0: empty, 1: X, 2: O + bool player1Withdrawn; + 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 } @@ -39,93 +40,79 @@ contract TicTacToe { /* MODIFIERS */ - modifier onlyPlayers(uint256 gameId) { - require(msg.sender == games[gameId].player1 || msg.sender == games[gameId].player2, "Not a player"); + modifier checkForTie(uint256 gameId) { + // If all moves were used and no victory was gotten + if (games[gameId].moves == 9) { + // Set the game as a Tie and finish it so prizes can be withdrawn + finishGame(gameId, address(0)); + } _; } - modifier onlyValidMove(uint256 gameId, uint8 position) { - require(games[gameId].board[position] == 0, "Invalid move"); - require(position < 9, "Invalid position"); + modifier onlyPlayers(uint256 gameId) { + require(msg.sender == games[gameId].player1 || msg.sender == games[gameId].player2, "Not a player"); _; } - modifier gameNotCancelled(uint256 gameId) { - require(games[gameId].state != GameState.CANCELED, "Game was canceled!"); + modifier onlyValidMove(uint256 gameId, uint8 position) { + require(position < 9, "Position not valid"); + require(games[gameId].board[position] == 0, "Position not empty"); + _; } /* EXTERNAL AND PUBLIC FUNCTIONS */ function createGame(address _player2) external payable { + // Increase gameIdCounter by one, as a new game is created + gameIdCounter++; + // Fill the information as a blank game with the data for the Game struct games[gameIdCounter] = Game({ player1: msg.sender, player2: _player2, state: GameState.PENDING, bet: msg.value, - lastMoveTime: block.timestamp, player1Withdrawn: false, player2Withdrawn: false, board: [0, 0, 0, 0, 0, 0, 0, 0, 0], moves: 0 }); - - emit GameCreated(gameIdCounter, msg.sender, _player2, msg.value); - gameIdCounter++; + // Emit an event + // can be used by the frontend to know that something happened and react to it + emit GameCreated(gameIdCounter, msg.sender, _player2, msg.value); } function makeMove(uint256 _gameId, uint8 position) external payable + checkForTie(_gameId) onlyPlayers(_gameId) - gameNotCancelled(_gameId) onlyValidMove(_gameId, position) { - if (games[_gameId].player2 == msg.sender && games[_gameId].state == GameState.PENDING) { + GameState currentState = games[_gameId].state; + + if (currentState == GameState.PENDING) { acceptGame(_gameId); } else { require(msg.value == 0, "Cannot send ETH with move"); - require(games[_gameId].state == GameState.PLAYING, "Game not in progress"); + require(currentState == GameState.PLAYING, "Game already ended!"); } - require(position < 9, "Invalid position"); - - uint8 currentPlayerSymbol = games[_gameId].moves % 2 == 0 ? 1 : 2; - games[_gameId].board[position] = currentPlayerSymbol; + // Determine the current Player symbol + uint8 currentPlayer = games[_gameId].moves % 2 == 0 ? 1 : 2; + // Add the corresponding mark in the position of the board + games[_gameId].board[position] = currentPlayer; + // Check if after adding that symbol, a win is achieved, and react to it if that's the case + checkWin(_gameId, position, currentPlayer); + // And add 1 to the number of moves made in the game games[_gameId].moves++; - games[_gameId].lastMoveTime = block.timestamp; emit MoveMade(_gameId, msg.sender, position); - - // Check for win - if (checkWin(_gameId, position, currentPlayerSymbol)) { - finishGame(_gameId, msg.sender, currentPlayerSymbol == 1 ? GameState.PLAYER1WON : GameState.PLAYER2WON); - } else if (games[_gameId].moves == 9) { - // Check for a draw - finishGame(_gameId, address(0), GameState.TIE); - } + } /* INTERNAL FUNCTIONS */ - function acceptGame(uint256 _gameId) internal { - require(games[_gameId].state == GameState.PENDING, "Game not in pending state"); - require(games[_gameId].player2 == msg.sender, "Not player2"); - require(msg.value == games[_gameId].bet, "Haven't sent enough ETH!"); - - games[_gameId].state = GameState.PLAYING; - - emit GameAccepted(_gameId, games[_gameId].player1, games[_gameId].player2); - } - - function finishGame(uint256 gameId, address winner, GameState state) internal { - games[gameId].state = state; - emit GameFinished(gameId, winner, state); - } - - // This used to be the array to check against if there was a win condition already. - // It is now replaced by the internal function checkWin - // // uint8[3][8] private winConditions = [ // [0, 1, 2], // [3, 4, 5], @@ -137,7 +124,7 @@ contract TicTacToe { // [2, 4, 6] // Diagonals // ]; - function checkWin(uint256 gameId, uint8 position, uint8 playerSymbol) internal view returns (bool) { + function checkWin(uint256 gameId, uint8 position, uint8 playerSymbol) internal returns (bool) { uint8 row = position / 3; uint8 col = position % 3; @@ -146,6 +133,7 @@ contract TicTacToe { games[gameId].board[row * 3] == playerSymbol && games[gameId].board[row * 3 + 1] == playerSymbol && games[gameId].board[row * 3 + 2] == playerSymbol ) { + finishGame(gameId, msg.sender); return true; } @@ -154,6 +142,7 @@ contract TicTacToe { games[gameId].board[col] == playerSymbol && games[gameId].board[col + 3] == playerSymbol && games[gameId].board[col + 6] == playerSymbol ) { + finishGame(gameId, msg.sender); return true; } @@ -170,16 +159,35 @@ contract TicTacToe { && games[gameId].board[6] == playerSymbol ) ) + ) { + finishGame(gameId, msg.sender); return true; } return false; } + function acceptGame(uint256 _gameId) internal { + require(games[_gameId].player2 == msg.sender, "You must be player 2 to accept!"); + require(msg.value == games[_gameId].bet, "You haven't sent enough ETH to accept!"); + + games[_gameId].state = GameState.PLAYING; + + emit GameAccepted(_gameId, games[_gameId].player1, games[_gameId].player2); + } + + function finishGame(uint256 gameId, address winner) internal { + // Incliude a check for state assuming the winner will be the msg.sender + // In the case of a tie call with address(0) as the winner, add a condition for that too + + GameState state = games[gameId].state; + emit GameFinished(gameId, winner, state); + } + /* VIEW AND PURE FUNCTIONS */ - function getCurrentPlayer(uint256 _gameId) external view returns (uint256) { + function getCurrentPlayer(uint256 _gameId) public view returns (uint256) { return games[_gameId].moves % 2 == 0 ? 1 : 2; } diff --git a/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx b/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx index 1109583..4f469f4 100755 --- a/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx +++ b/packages/nextjs/components/tictactoe/TicTacToeBoard.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Address } from "../scaffold-eth"; import { Button, Flex, Grid } from "@chakra-ui/react"; import { ethers } from "ethers"; -import { useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; +import { useScaffoldContractRead, useScaffoldContractWrite } from "~~/hooks/scaffold-eth"; import { MoveMadeProps, TicTacToeBoardProps } from "~~/types/TicTacToeTypes"; const TicTacToeBoard: React.FC = ({ @@ -16,7 +16,13 @@ const TicTacToeBoard: React.FC = ({ const [betPayment, setBetPayment] = useState(game.bet); const [board, setBoard] = useState(Array(9).fill(0)); // Initialize an empty board - console.log("Moves list for game ID #", game.gameId, ": ", movesList); + const { data: getBoard } = useScaffoldContractRead({ + contractName: "TicTacToe", + functionName: "getBoard", + args: [BigInt(game.gameId)], + }); + + console.log("getBoard reads for #", game.gameId, ": ", getBoard); const { writeAsync: makeMove } = useScaffoldContractWrite({ contractName: "TicTacToe", diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 70091da..13a7790 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: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", + address: "0x0165878A594ca255338adfa4d48449f69242Eb8F", abi: [ { anonymous: false,