diff --git a/README.md b/README.md index fe8d913..1eb5d14 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,12 @@ dApp for betting on the outcome of a Tic Tac Toe game. - Board (pending development by BuidlGuidl members) - Game idea by [freeCodeCamp Frontend Web Development Tutorial](https://www.youtube.com/watch?v=MsnQ5uepIaE) -## How can I contribute? - -### Requirements - -- 🐣 Make sure you know the ETH Tech Stack and understand [how to make a dApp using Scaffold-ETH 2](https://lulox.notion.site/Newbie-s-Lounge-68ea7c4c5f1a4ec29786be6a76516878). - -### How to contribute: +## How can I contribute to this build? - 👷‍♀️ To view current development tasks, [join this Trello board](https://trello.com/invite/b/s0vot1BA/ATTI366c508087a404ccf9343def4d76d1ce6F7899AA/newbies-lounge) and check the list "TicTacToe". - 🧰 To chat with other buidlers about this project, [join our Telegram group](https://t.me/+FwCZPG51UhwzOTZh) - 🛠️ To submit a PR (Pull Request), [fork and pull](https://github.com/susam/gitpr) a request to this repo. +- 🐣 Make sure you know the ETH Tech Stack and understand [how to make a dApp using Scaffold-ETH 2](https://lulox.notion.site/Newbie-s-Lounge-68ea7c4c5f1a4ec29786be6a76516878). ## Quickstart @@ -95,12 +90,12 @@ Win scenarios: Run smart contract test with `yarn hardhat:test` +## About Scaffold-ETH 2 + +See [SE2-DOCUMENTATION.md](./SE2-DOCUMENTATION.md) + ### Disabling Github Workflow We have github workflow setup checkout `.github/workflows/lint.yaml` which runs types and lint error checks every time code is **pushed** to `main` branch or **pull request** is made to `main` branch To disable it, **delete `.github` directory** - -## About Scaffold-ETH 2 - -See [SE2-DOCUMENTATION.md](./SE2-DOCUMENTATION.md) diff --git a/packages/hardhat/contracts/TicTacToe.sol b/packages/hardhat/contracts/TicTacToe.sol index 90732fd..b5748b9 100644 --- a/packages/hardhat/contracts/TicTacToe.sol +++ b/packages/hardhat/contracts/TicTacToe.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.19; +pragma solidity ^0.8.17; /** * @title TicTacToe * @author Lulox * @notice A betting TicTacToe contract. * @dev Currently for using with one transaction per move, - * in a future may be replaced with signatures + * in a future may be replaced with signatures + or other gas efficient mechanism */ contract TicTacToe { @@ -57,15 +58,6 @@ contract TicTacToe { /* MODIFIERS */ - modifier onlyPlayers(uint256 gameId) { - require( - msg.sender == games[gameId].player1 || - msg.sender == games[gameId].player2, - "Not a player" - ); - _; - } - modifier onlyValidMove(uint256 _gameId, uint8 position) { // Store the game in memory to use less storage reads Game memory game = games[_gameId]; @@ -160,6 +152,55 @@ contract TicTacToe { checkWin(_gameId, position, msg.sender); } + // Function to withdraw the prize based on game state + function withdrawPrize(uint256 _gameId) external { + Game storage game = games[_gameId]; + + // Ensure the game is in the correct state for prize withdrawal + require( + game.state == GameState.PLAYER1WON || + game.state == GameState.PLAYER2WON || + game.state == GameState.TIE, + "Invalid game state for prize withdrawal" + ); + + // Ensure the caller is one of the players or has not withdrawn yet + require( + msg.sender == game.player1 || msg.sender == game.player2, + "Not a player" + ); + + // Ensure the player has not withdrawn yet + if (msg.sender == game.player1) { + require( + game.state == GameState.PLAYER1WON, + "You haven't won this game!" + ); + require( + !game.player1Withdrawn, + "You have already withdrawn the prize!" + ); + game.player1Withdrawn = true; + } else { + require( + game.state == GameState.PLAYER2WON, + "You haven't won this game!" + ); + require( + !game.player2Withdrawn, + "You have already withdrawn the prize!" + ); + game.player2Withdrawn = true; + } + + // Calculate and transfer the prize based on the game state + uint256 prize = calculatePrize(_gameId); + require(prize > 0, "Invalid prize amount"); + + // Transfer the prize to the player + payable(msg.sender).transfer(prize); + } + /* INTERNAL FUNCTIONS */ function checkWin( @@ -215,13 +256,49 @@ contract TicTacToe { } 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 + Game storage game = games[gameId]; - // Add conditions to determine which state will the game be finished with, according to this info ^ - // These come from the enum GameState PLAYER1WON, PLAYER2WON, TIE - GameState state = games[gameId].state; - emit GameFinished(gameId, winner, state); + // Ensure the game is in the PLAYING state before finishing + require( + game.state == GameState.PLAYING, + "Game is not in PLAYING state" + ); + + // Determine the result based on the winner and update game state accordingly + if (winner == address(0)) { + // It's a tie + game.state = GameState.TIE; + } else if (winner == game.player1) { + // Player 1 won + game.state = GameState.PLAYER1WON; + } else if (winner == game.player2) { + // Player 2 won + game.state = GameState.PLAYER2WON; + } else { + // Winner address is not valid + revert("Invalid winner address"); + } + + // Emit GameFinished event + emit GameFinished(gameId, winner, game.state); + } + + // Function to calculate the prize based on the game state + function calculatePrize(uint256 _gameId) internal view returns (uint256) { + Game storage game = games[_gameId]; + uint256 totalBet = game.bet * 2; // Total amount bet in the game + + if (game.state == GameState.PLAYER1WON) { + return totalBet; + } else if (game.state == GameState.PLAYER2WON) { + return totalBet; + } else if (game.state == GameState.TIE) { + // In the case of a tie, split the total bet equally between players + return totalBet / 2; + } else { + // Invalid game state + revert("Invalid game state"); + } } /* VIEW AND PURE FUNCTIONS */ diff --git a/packages/nextjs/components/tictactoe/CreateChallengeBox.tsx b/packages/nextjs/components/tictactoe/CreateChallengeBox.tsx index 4ecd0b8..48b557b 100755 --- a/packages/nextjs/components/tictactoe/CreateChallengeBox.tsx +++ b/packages/nextjs/components/tictactoe/CreateChallengeBox.tsx @@ -16,7 +16,7 @@ const CreateChallengeBox = ({}) => { value: betAmount ? ethers.parseEther(betAmount) : undefined, }); - console.log("Bet amount: ", betAmount); + // console.log("Bet amount: ", betAmount); return ( = ({ - game, - isGameAccepted, - movesList, - movesAmount, - isGameFinished, -}) => { +const TicTacToeBoard: React.FC = ({ game, isGameAccepted, movesAmount, isGameFinished }) => { const [position, setPosition] = useState(0); - const [betPayment, setBetPayment] = useState(game.bet); const [board, setBoard] = useState(Array(9).fill(0)); // Initialize an empty board const { data: getBoard } = useScaffoldContractRead({ @@ -22,42 +15,33 @@ const TicTacToeBoard: React.FC = ({ args: [BigInt(game.gameId)], }); - console.log("getBoard reads for #", game.gameId, ": ", getBoard); + console.log("getBoard reads: ", getBoard); + + useEffect(() => { + // Update the local board based on the latest data from the contract + if (getBoard) { + setBoard(getBoard.map(Number)); + } + }, [getBoard]); const { writeAsync: makeMove } = useScaffoldContractWrite({ contractName: "TicTacToe", functionName: "makeMove", args: [BigInt(game.gameId), position], - value: BigInt(betPayment), }); - useEffect(() => { - // Update the board based on the movesList - const updatedBoard = Array(9).fill(0); - - movesList.forEach((move: MoveMadeProps) => { - const currentPlayerSymbol = move.player === game.player1 ? 1 : 2; - updatedBoard[move.position] = currentPlayerSymbol; - }); - - setBoard(updatedBoard); - }, [game.player1, movesList]); + const { writeAsync: acceptGame } = useScaffoldContractWrite({ + contractName: "TicTacToe", + functionName: "acceptGame", + args: [BigInt(game.gameId)], + value: BigInt(game.bet), + }); const handleMakeMove = async () => { try { - if (movesAmount > 0) { - setBetPayment(0); - } await makeMove(); - - // Update the local board based on the latest move - const currentPlayerSymbol = movesAmount % 2 === 0 ? 1 : 2; - const updatedBoard = [...board]; - updatedBoard[position] = currentPlayerSymbol; - setBoard(updatedBoard); } catch (error) { console.error("Error making move:", error); - // Handle error as needed } }; @@ -74,13 +58,19 @@ const TicTacToeBoard: React.FC = ({ Player 2:

- Bet:
{parseFloat(ethers.formatEther(game.bet.toString())).toFixed(4)} ETH + Each player bets:
{parseFloat(ethers.formatEther(game.bet.toString())).toFixed(4)} ETH

Is game accepted?:
- {isGameAccepted ? "Yes" : "No"} + {isGameAccepted ? ( + "Yes" + ) : ( + + )}

# of moves made:
diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index edaf47f..0aeebd6 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: "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", abi: [ { anonymous: false, @@ -115,6 +115,19 @@ const deployedContracts = { name: "MoveMade", type: "event", }, + { + inputs: [ + { + internalType: "uint256", + name: "_gameId", + type: "uint256", + }, + ], + name: "acceptGame", + outputs: [], + stateMutability: "payable", + type: "function", + }, { inputs: [ { @@ -220,9 +233,9 @@ const deployedContracts = { name: "getCurrentPlayer", outputs: [ { - internalType: "uint256", + internalType: "uint8", name: "", - type: "uint256", + type: "uint8", }, ], stateMutability: "view", @@ -243,7 +256,20 @@ const deployedContracts = { ], name: "makeMove", outputs: [], - stateMutability: "payable", + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_gameId", + type: "uint256", + }, + ], + name: "withdrawPrize", + outputs: [], + stateMutability: "nonpayable", type: "function", }, ],