diff --git a/README.md b/README.md index 542c0f4..6eeacde 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,27 @@ -# ๐Ÿ— Scaffold-ETH 2 +# โญ• Tic Tac Toe โŒ -๐Ÿงช An open-source, up-to-date toolkit for building decentralized applications (dapps) on the Ethereum blockchain. It's designed to make it easier for developers to create and deploy smart contracts and build user interfaces that interact with those contracts. +dApp for betting on the outcome of a Tic Tac Toe game. -โš™๏ธ Built using NextJS, RainbowKit, Hardhat, Wagmi, and Typescript. +- Frontend inspired by [Sportsbook](https://github.com/luloxi/sportsbook) +- Board (pending development by BuidlGuidl members) +- Game idea by [freeCodeCamp Frontend Web Development Tutorial](https://www.youtube.com/watch?v=MsnQ5uepIaE) -- โœ… **Contract Hot Reload**: Your frontend auto-adapts to your smart contract as you edit it. -- ๐Ÿ”ฅ **Burner Wallet & Local Faucet**: Quickly test your application with a burner wallet and local faucet. -- ๐Ÿ” **Integration with Wallet Providers**: Connect to different wallet providers and interact with the Ethereum network. +## Development notes -## Contents - -- [Requirements](#requirements) -- [Quickstart](#quickstart) -- [Deploying your Smart Contracts to a Live Network](#deploying-your-smart-contracts-to-a-live-network) -- [Deploying your NextJS App](#deploying-your-nextjs-app) -- [Interacting with your Smart Contracts: SE-2 Custom Hooks](#interacting-with-your-smart-contracts-se-2-custom-hooks) -- [Disabling Type & Linting Error Checks](#disabling-type-and-linting-error-checks) - - [Disabling commit checks](#disabling-commit-checks) - - [Deploying to Vercel without any checks](#deploying-to-vercel-without-any-checks) - - [Disabling Github Workflow](#disabling-github-workflow) -- [Contributing to Scaffold-ETH 2](#contributing-to-scaffold-eth-2) - -## Requirements - -Before you begin, you need to install the following tools: - -- [Node (v18 LTS)](https://nodejs.org/en/download/) -- Yarn ([v1](https://classic.yarnpkg.com/en/docs/install/) or [v2+](https://yarnpkg.com/getting-started/install)) -- [Git](https://git-scm.com/downloads) +- ๐Ÿฃ Project being developed by [Newbies Lounge](https://lulox.notion.site/Newbie-s-Lounge-68ea7c4c5f1a4ec29786be6a76516878) +- ๐Ÿ‘ทโ€โ™€๏ธ To view current development tasks, [join this Trello board](https://trello.com/invite/b/s0vot1BA/ATTI366c508087a404ccf9343def4d76d1ce6F7899AA/newbies-lounge). +- ๐Ÿงฐ To chat with other buidlers about this project, [join our Telegram group](https://t.me/+FwCZPG51UhwzOTZh) +- ๐Ÿ› ๏ธ To collaborate, [fork and pull](https://github.com/susam/gitpr) a request to this repo. ## Quickstart -To get started with Scaffold-ETH 2, follow the steps below: +To get started with Tic Tac Toe development, follow the steps below: 1. Clone this repo & install dependencies ``` -git clone https://github.com/scaffold-eth/scaffold-eth-2.git -cd scaffold-eth-2 +git clone https://github.com/luloxi/TicTacToe.git +cd TicTacToe yarn install ``` @@ -47,16 +31,12 @@ yarn install yarn chain ``` -This command starts a local Ethereum network using Hardhat. The network runs on your local machine and can be used for testing and development. You can customize the network configuration in `hardhat.config.ts`. - 3. On a second terminal, deploy the test contract: ``` yarn deploy ``` -This command deploys a test smart contract to the local network. The contract is located in `packages/hardhat/contracts` and can be modified to suit your needs. The `yarn deploy` command uses the deploy script located in `packages/hardhat/deploy` to deploy the contract to the network. You can also customize the deploy script. - 4. On a third terminal, start your NextJS app: ``` @@ -65,11 +45,9 @@ yarn start Visit your app on: `http://localhost:3000`. You can interact with your smart contract using the contract component or the example ui in the frontend. You can tweak the app config in `packages/nextjs/scaffold.config.ts`. -Run smart contract test with `yarn hardhat:test` +## Smart contract tests (pending deveopment) -- Edit your smart contract `YourContract.sol` in `packages/hardhat/contracts` -- Edit your frontend in `packages/nextjs/pages` -- Edit your deployment scripts in `packages/hardhat/deploy` +Run smart contract test with `yarn hardhat:test` ## Deploying your Smart Contracts to a Live Network diff --git a/packages/hardhat/contracts/TicTacToe.sol b/packages/hardhat/contracts/TicTacToe.sol new file mode 100644 index 0000000..1a15d0b --- /dev/null +++ b/packages/hardhat/contracts/TicTacToe.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/** + * @title A Tic Tac Toe game + * @author Lulox + * @notice This contract is for creating a bet between two parts on the outcome of a Tic Tac Toe Game + */ +contract TicTacToe { + uint256 public gameIdCounter = 1; + + enum GameState { + PENDING, + PLAYING, + PLAYER1WON, + PLAYER2WON, + TIE, + CANCELED + } + + struct Game { + address player1; + 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 + uint8 moves; // Counter or the number of moves made + } + + mapping(uint256 => Game) public games; + uint8[3][8] private winConditions = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], // Rows + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], // Columns + [0, 4, 8], + [2, 4, 6] // Diagonals + ]; + + event GameCreated(uint256 indexed gameId, address indexed player1, address indexed player2, uint256 bet); + event GameAccepted(uint256 indexed gameId, address indexed team1, address indexed team2); + event MoveMade(uint256 indexed gameId, address indexed player, uint8 position); + event GameFinished(uint256 indexed gameId, address indexed winner, GameState state); + + /* 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) { + require(games[gameId].board[position] == 0, "Invalid move"); + require(position < 9, "Invalid position"); + _; + } + + modifier gameNotCancelled(uint256 gameId) { + require(games[gameId].state != GameState.CANCELED, "Game is canceled"); + _; + } + + /* EXTERNAL AND PUBLIC FUNCTIONS */ + + function createGame(address _player2) external payable { + // gameId = keccak256(abi.encodePacked(gameIdCounter, block.timestamp, msg.sender, _player2)); + + 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++; + } + + function makeMove(uint256 _gameId, uint8 position) + external + payable + onlyPlayers(_gameId) + gameNotCancelled(_gameId) + onlyValidMove(_gameId, position) + { + if (games[_gameId].player2 == msg.sender && games[_gameId].state == 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(position < 9, "Invalid position"); + + uint8 currentPlayerSymbol = games[_gameId].moves % 2 == 0 ? 1 : 2; + games[_gameId].board[position] = currentPlayerSymbol; + 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); + } + + function checkWin(uint256 gameId, uint8 position, uint8 playerSymbol) internal view returns (bool) { + uint8 row = position / 3; + uint8 col = position % 3; + + // Check row + if ( + games[gameId].board[row * 3] == playerSymbol && games[gameId].board[row * 3 + 1] == playerSymbol + && games[gameId].board[row * 3 + 2] == playerSymbol + ) { + return true; + } + + // Check column + if ( + games[gameId].board[col] == playerSymbol && games[gameId].board[col + 3] == playerSymbol + && games[gameId].board[col + 6] == playerSymbol + ) { + return true; + } + + // Check diagonals + if ( + (row == col || row + col == 2) + && ( + ( + games[gameId].board[0] == playerSymbol && games[gameId].board[4] == playerSymbol + && games[gameId].board[8] == playerSymbol + ) + || ( + games[gameId].board[2] == playerSymbol && games[gameId].board[4] == playerSymbol + && games[gameId].board[6] == playerSymbol + ) + ) + ) { + return true; + } + + return false; + } + + /* VIEW AND PURE FUNCTIONS */ + + function getCurrentPlayer(uint256 _gameId) internal view returns (uint256) { + return games[_gameId].moves % 2 == 0 ? 1 : 2; + } + + function getBoard(uint256 _gameId) external view returns (uint8[9] memory) { + return games[_gameId].board; + } +} \ No newline at end of file diff --git a/packages/hardhat/contracts/YourContract.sol b/packages/hardhat/contracts/YourContract.sol deleted file mode 100644 index 7f1a1cd..0000000 --- a/packages/hardhat/contracts/YourContract.sol +++ /dev/null @@ -1,87 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; - -// Useful for debugging. Remove when deploying to a live network. -import "hardhat/console.sol"; - -// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc) -// import "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * A smart contract that allows changing a state variable of the contract and tracking the changes - * It also allows the owner to withdraw the Ether in the contract - * @author BuidlGuidl - */ -contract YourContract { - // State Variables - address public immutable owner; - string public greeting = "Building Unstoppable Apps!!!"; - bool public premium = false; - uint256 public totalCounter = 0; - mapping(address => uint) public userGreetingCounter; - - // Events: a way to emit log statements from smart contract that can be listened to by external parties - event GreetingChange( - address indexed greetingSetter, - string newGreeting, - bool premium, - uint256 value - ); - - // Constructor: Called once on contract deployment - // Check packages/hardhat/deploy/00_deploy_your_contract.ts - constructor(address _owner) { - owner = _owner; - } - - // Modifier: used to define a set of rules that must be met before or after a function is executed - // Check the withdraw() function - modifier isOwner() { - // msg.sender: predefined variable that represents address of the account that called the current function - require(msg.sender == owner, "Not the Owner"); - _; - } - - /** - * Function that allows anyone to change the state variable "greeting" of the contract and increase the counters - * - * @param _newGreeting (string memory) - new greeting to save on the contract - */ - function setGreeting(string memory _newGreeting) public payable { - // Print data to the hardhat chain console. Remove when deploying to a live network. - console.log( - "Setting new greeting '%s' from %s", - _newGreeting, - msg.sender - ); - - // Change state variables - greeting = _newGreeting; - totalCounter += 1; - userGreetingCounter[msg.sender] += 1; - - // msg.value: built-in global variable that represents the amount of ether sent with the transaction - if (msg.value > 0) { - premium = true; - } else { - premium = false; - } - - // emit: keyword used to trigger an event - emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, 0); - } - - /** - * Function that allows the owner to withdraw all the Ether in the contract - * The function can only be called by the owner of the contract as defined by the isOwner modifier - */ - function withdraw() public isOwner { - (bool success, ) = owner.call{ value: address(this).balance }(""); - require(success, "Failed to send Ether"); - } - - /** - * Function that allows the contract to receive ETH - */ - receive() external payable {} -} diff --git a/packages/hardhat/deploy/00_deploy_your_contract.ts b/packages/hardhat/deploy/01_deploy_tictactoe.ts old mode 100644 new mode 100755 similarity index 86% rename from packages/hardhat/deploy/00_deploy_your_contract.ts rename to packages/hardhat/deploy/01_deploy_tictactoe.ts index 418b912..e7b38ec --- a/packages/hardhat/deploy/00_deploy_your_contract.ts +++ b/packages/hardhat/deploy/01_deploy_tictactoe.ts @@ -7,7 +7,7 @@ import { DeployFunction } from "hardhat-deploy/types"; * * @param hre HardhatRuntimeEnvironment object. */ -const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { +const deployTicTacToe: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { /* On localhost, the deployer account is the one that comes with Hardhat, which is already funded. @@ -21,10 +21,10 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn const { deployer } = await hre.getNamedAccounts(); const { deploy } = hre.deployments; - await deploy("YourContract", { + await deploy("TicTacToe", { from: deployer, // Contract constructor arguments - args: [deployer], + args: [], log: true, // autoMine: can be passed to the deploy function to make the deployment process faster on local networks by // automatically mining the contract deployment transaction. There is no effect on live networks. @@ -35,8 +35,8 @@ const deployYourContract: DeployFunction = async function (hre: HardhatRuntimeEn // const yourContract = await hre.ethers.getContract("YourContract", deployer); }; -export default deployYourContract; +export default deployTicTacToe; // Tags are useful if you have multiple deploy files and only want to run one of them. // e.g. yarn deploy --tags YourContract -deployYourContract.tags = ["YourContract"]; +deployTicTacToe.tags = ["TicTacToe"]; diff --git a/packages/nextjs/contracts/deployedContracts.ts b/packages/nextjs/contracts/deployedContracts.ts index 008d4eb..b521703 100644 --- a/packages/nextjs/contracts/deployedContracts.ts +++ b/packages/nextjs/contracts/deployedContracts.ts @@ -4,6 +4,238 @@ */ import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; -const deployedContracts = {} as const; +const deployedContracts = { + 31337: { + TicTacToe: { + address: "0x5FbDB2315678afecb367f032d93F642f64180aa3", + abi: [ + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "gameId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "team1", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "team2", + type: "address", + }, + ], + name: "GameAccepted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "gameId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "player1", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "player2", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "bet", + type: "uint256", + }, + ], + name: "GameCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "gameId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "winner", + type: "address", + }, + { + indexed: false, + internalType: "enum TicTacToe.GameState", + name: "state", + type: "uint8", + }, + ], + name: "GameFinished", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "gameId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "player", + type: "address", + }, + { + indexed: false, + internalType: "uint8", + name: "position", + type: "uint8", + }, + ], + name: "MoveMade", + type: "event", + }, + { + inputs: [ + { + internalType: "address", + name: "_player2", + type: "address", + }, + ], + name: "createGame", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "gameIdCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "games", + outputs: [ + { + internalType: "address", + name: "player1", + type: "address", + }, + { + internalType: "address", + name: "player2", + type: "address", + }, + { + internalType: "enum TicTacToe.GameState", + name: "state", + type: "uint8", + }, + { + internalType: "uint256", + name: "bet", + type: "uint256", + }, + { + internalType: "uint256", + name: "lastMoveTime", + type: "uint256", + }, + { + internalType: "bool", + name: "player1Withdrawn", + type: "bool", + }, + { + internalType: "bool", + name: "player2Withdrawn", + type: "bool", + }, + { + internalType: "uint8", + name: "moves", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_gameId", + type: "uint256", + }, + ], + name: "getBoard", + outputs: [ + { + internalType: "uint8[9]", + name: "", + type: "uint8[9]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_gameId", + type: "uint256", + }, + { + internalType: "uint8", + name: "position", + type: "uint8", + }, + ], + name: "makeMove", + outputs: [], + stateMutability: "payable", + type: "function", + }, + ], + inheritedFunctions: {}, + }, + }, +} as const; export default deployedContracts satisfies GenericContractsDeclaration;