├── .env.example ├── .gitattributes ├── .gitignore ├── README.md ├── contracts ├── IRPSGameV2.sol ├── RPSGame.sol ├── RPSGameFactory.sol └── RPSToken.sol ├── deploy └── RPSToken.ts ├── frontend ├── .gitignore ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── RPSGame.d.ts │ ├── RPSGameFactory.d.ts │ ├── abis │ │ ├── RPSGame.json │ │ └── RPSGameFactory.json │ ├── components │ │ ├── Button.tsx │ │ ├── CircularLoader.tsx │ │ ├── DeployedContracts.tsx │ │ ├── Game.tsx │ │ ├── GameActionInfoCard.tsx │ │ ├── GameStatsCard.tsx │ │ ├── GlobalMessage.tsx │ │ ├── HiddenMove.tsx │ │ ├── InputField.tsx │ │ ├── Leaderboard.tsx │ │ ├── Loader.tsx │ │ ├── OptionButton.module.css │ │ ├── OptionButton.tsx │ │ ├── PlayerCard.tsx │ │ ├── Playground.tsx │ │ ├── RevealMove.tsx │ │ ├── SubmitMove.tsx │ │ └── layout │ │ │ ├── Footer.tsx │ │ │ └── Navbar.tsx │ ├── context │ │ ├── MessageContext.tsx │ │ ├── RPSGameContractContext │ │ │ ├── actions.ts │ │ │ ├── contractContext.d.ts │ │ │ ├── index.tsx │ │ │ ├── reducer.ts │ │ │ └── state.ts │ │ ├── RPSGameFactoryContext │ │ │ └── index.tsx │ │ ├── TransactionContext.tsx │ │ └── WalletContext.tsx │ ├── helpers.ts │ ├── images │ │ ├── bg-pentagon.svg │ │ ├── bg-triangle.svg │ │ ├── favicon-32x32.png │ │ ├── icon-close.svg │ │ ├── icon-lizard.svg │ │ ├── icon-paper.svg │ │ ├── icon-rock.svg │ │ ├── icon-scissors.svg │ │ ├── icon-spock.svg │ │ ├── image-rules-bonus.svg │ │ ├── image-rules.svg │ │ ├── logo-bonus.svg │ │ └── logo.svg │ ├── index.css │ ├── index.tsx │ ├── provider.ts │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tailwind.config.js └── tsconfig.json ├── hardhat.config.ts ├── helpers └── env_helpers.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── scripts ├── gameArgs.js ├── rpsFactory.js ├── rpsGame.js └── rpsToken.js ├── test ├── RPSGame.test.ts ├── RPSGameFactory.test.ts └── RPSToken.test.ts ├── typechain ├── ERC20.d.ts ├── IERC20.d.ts ├── IERC20Metadata.d.ts ├── IRPSGameV2.d.ts ├── RPSGame.d.ts ├── RPSGameFactory.d.ts ├── RPSGameV2.d.ts ├── RPSToken.d.ts ├── factories │ ├── ERC20__factory.ts │ ├── IERC20Metadata__factory.ts │ ├── IERC20__factory.ts │ ├── IRPSGameV2__factory.ts │ ├── RPSGameFactory__factory.ts │ ├── RPSGameV2__factory.ts │ ├── RPSGame__factory.ts │ └── RPSToken__factory.ts └── index.ts └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | PRIVATE_KEY="" 2 | INFURA_KEY="" 3 | ETHERSCAN_KEY="" 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | #hardhat files 4 | cache 5 | artifacts 6 | frontend/src/hardhat 7 | 8 | .env 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rock Papers Scissors game Implementation 2 | 3 | 4 | ## Version 1 5 | 6 | ### Features 7 | - Multiple players can create an instance of game and play ✅ 8 | - can deposit bet amount if player ✅ 9 | - can submit move ✅ 10 | - Incentivizes the winner ✅ 11 | - Should reset the game if moves are same ✅ 12 | - Submit signed move with salt and reveal later.✅ 13 | 14 | ## Improvements that can be done 15 | - Players should be able to bet with any ERC20 currencies. 16 | - Winner can mint a NFT. 17 | - Add time limit to reveal move and punish the non cooperative player by incentivizing cooperative player. 18 | 19 | 20 | ## Contract Address 21 | [Ether scan - Rinkeby](https://rinkeby.etherscan.io/address/0xb0f9Dfb7c06E2e9b9BaC5Ac397D686C64be87e7B) 22 | -------------------------------------------------------------------------------- /contracts/IRPSGameV2.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IRPSGameV2 { 5 | enum Move { 6 | None, 7 | Rock, 8 | Paper, 9 | Scissors 10 | } 11 | enum GameState { 12 | Initialized, 13 | Open, 14 | Progress 15 | } 16 | struct Player { 17 | address addr; 18 | uint256 balance; 19 | bytes32 move; 20 | bool revealed; 21 | } 22 | struct Game { 23 | Player playerA; 24 | Player playerB; 25 | address winner; 26 | GameState gameState; 27 | uint256 betAmount; 28 | } 29 | event Draw(); 30 | event Challenge(address indexed _from, address indexed _to); 31 | event DepositSuccess(address indexed _from, uint256 value); 32 | event ResetGame(); 33 | event Replay(address indexed challanger, address indexed _player); 34 | event AcceptChallenge(address indexed _challenger); 35 | event GameStarted(address indexed player1, address indexed player2); 36 | event GameEnded(address indexed _winner); 37 | 38 | function depositBet() external payable; 39 | 40 | // Should move submitted be bytes 32 hash of signed message? 41 | function submitMove(bytes32 _moveHash) external; 42 | 43 | function revealMove(Move _move, string memory salt) external; 44 | 45 | function challenge() external; 46 | 47 | function withdrawFund() external; 48 | 49 | // Internal function 50 | function resetGame() external; 51 | 52 | function getWinner() external view returns (address); 53 | 54 | function announceWinner() external; 55 | 56 | function icentivize() external; 57 | } 58 | -------------------------------------------------------------------------------- /contracts/RPSGame.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract RPSGame { 5 | enum GameStage { 6 | Open, 7 | BetsDeposited, 8 | MovesSubmitted, 9 | MoveRevealed, 10 | Completed 11 | } 12 | 13 | enum Move { 14 | None, 15 | Rock, 16 | Paper, 17 | Scissors 18 | } 19 | Player public playerA; 20 | Player public playerB; 21 | uint256 public betAmount; 22 | GameStage public gameStage; 23 | address public winner; 24 | 25 | struct Player { 26 | Move move; 27 | bytes32 hashedMove; 28 | uint256 balance; 29 | address addr; 30 | bool submitted; 31 | bool revealed; 32 | } 33 | 34 | constructor( 35 | uint256 _betAmount, 36 | address _player, 37 | address _opponent 38 | ) payable { 39 | require(_player != _opponent, "You cannot play against yourself"); 40 | betAmount = _betAmount; 41 | playerA.addr = _player; 42 | playerB.addr = _opponent; 43 | } 44 | 45 | event GameStageChanged(GameStage gameStage); 46 | event ResetGame(); 47 | event Winner(address indexed _winner); 48 | event Deposit(address indexed depositor); 49 | event GameComplete(); 50 | event SubmitMove(address indexed player); 51 | event RevealMove(address indexed player); 52 | event Withdraw(address indexed player, uint256 amount); 53 | 54 | modifier isPlayer() { 55 | require( 56 | msg.sender == playerA.addr || msg.sender == playerB.addr, 57 | "RPSGame: Not a valid player" 58 | ); 59 | _; 60 | } 61 | 62 | function getPlayer(address _player) external view returns (Player memory) { 63 | if (playerA.addr == _player) { 64 | return playerA; 65 | } else { 66 | return playerB; 67 | } 68 | } 69 | 70 | function depositBet() external payable isPlayer { 71 | require( 72 | msg.value >= betAmount, 73 | "RPSGame: Balance not enough, Send more fund" 74 | ); 75 | 76 | msg.sender == playerA.addr 77 | ? playerA.balance += msg.value 78 | : playerB.balance += msg.value; 79 | emit Deposit(msg.sender); 80 | 81 | if (playerA.balance >= betAmount && playerB.balance >= betAmount) { 82 | gameStage = GameStage.BetsDeposited; 83 | emit GameStageChanged(GameStage.BetsDeposited); 84 | } 85 | } 86 | 87 | function submitMove(bytes32 _hashedMove) external isPlayer { 88 | require( 89 | gameStage == GameStage.BetsDeposited, 90 | "RPSGame: game not under progress" 91 | ); 92 | Player storage player = playerA.addr == msg.sender ? playerA : playerB; 93 | 94 | require( 95 | !player.submitted, 96 | "RPSGame: you have already submitted the move" 97 | ); 98 | player.hashedMove = _hashedMove; 99 | player.submitted = true; 100 | emit SubmitMove(player.addr); 101 | if (playerA.submitted && playerB.submitted) { 102 | gameStage = GameStage.MovesSubmitted; 103 | emit GameStageChanged(GameStage.MovesSubmitted); 104 | } 105 | } 106 | 107 | function revealMove(uint8 _move, bytes32 _salt) external isPlayer { 108 | require( 109 | gameStage == GameStage.MovesSubmitted, 110 | "RPSGame: both players have not submitted move yet." 111 | ); 112 | // TODO: Should check the reveal time limit 113 | Player storage currentPlayer = msg.sender == playerA.addr 114 | ? playerA 115 | : playerB; 116 | bytes32 revealedHash = keccak256(abi.encodePacked(_move, _salt)); 117 | // Already revealed 118 | require(!currentPlayer.revealed, "You have already revealed your move"); 119 | // revealed data not true 120 | require( 121 | revealedHash == currentPlayer.hashedMove, 122 | "RPSGame: Either your salt or move is not same as your submitted hashed move" 123 | ); 124 | currentPlayer.move = Move(_move); 125 | currentPlayer.revealed = true; 126 | emit RevealMove(currentPlayer.addr); 127 | if (playerA.revealed && playerB.revealed) { 128 | pickWinner(); 129 | } 130 | } 131 | 132 | function pickWinner() private { 133 | require( 134 | playerA.submitted && playerB.submitted, 135 | "RPSGame: Players have not submitted their move" 136 | ); 137 | address _winner = getWinner(); 138 | if (_winner != address(0)) { 139 | winner = _winner; 140 | emit Winner(_winner); 141 | gameStage = GameStage.Completed; 142 | incentivize(_winner); 143 | } 144 | } 145 | 146 | function incentivize(address _winner) internal { 147 | // Update contract balances of winners and loosers 148 | if (_winner == playerA.addr) { 149 | playerA.balance += betAmount; 150 | playerB.balance -= betAmount; 151 | } else { 152 | playerB.balance += betAmount; 153 | playerA.balance -= betAmount; 154 | } 155 | } 156 | 157 | modifier notUnderProgress() { 158 | require( 159 | gameStage != GameStage.MovesSubmitted && 160 | gameStage != GameStage.BetsDeposited, 161 | "RPSGame: Game under progress" 162 | ); 163 | _; 164 | } 165 | 166 | function withdrawFund() external isPlayer notUnderProgress { 167 | Player storage player = msg.sender == playerA.addr ? playerA : playerB; 168 | require( 169 | player.balance > 0, 170 | "RPSGame: You don't have anything to withdraw!" 171 | ); 172 | uint256 balance = player.balance; 173 | payable(player.addr).transfer(balance); 174 | player.balance = 0; 175 | emit Withdraw(player.addr, balance); 176 | } 177 | 178 | function getWinner() internal view returns (address) { 179 | if (playerA.move == playerB.move) return address(0); 180 | if ( 181 | (playerA.move == Move.Rock && playerB.move == Move.Scissors) || 182 | (playerA.move == Move.Paper && playerB.move == Move.Rock) || 183 | (playerA.move == Move.Scissors && playerB.move == Move.Paper) 184 | ) { 185 | return playerA.addr; 186 | } 187 | return playerB.addr; 188 | } 189 | 190 | modifier isCompleted() { 191 | require(gameStage == GameStage.Completed); 192 | _; 193 | } 194 | 195 | // TODO: Should be an external function 196 | function resetGame() external isCompleted { 197 | playerA.move = Move.None; 198 | playerA.submitted = false; 199 | playerA.revealed = false; 200 | playerB.move = Move.None; 201 | playerB.submitted = false; 202 | playerB.revealed = false; 203 | if (playerA.balance >= betAmount && playerB.balance >= betAmount) { 204 | gameStage = GameStage.BetsDeposited; 205 | } else { 206 | gameStage = GameStage.Open; 207 | } 208 | emit ResetGame(); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /contracts/RPSGameFactory.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | import "./RPSGame.sol"; 4 | 5 | contract RPSGameFactory { 6 | struct Game { 7 | address gameAddress; 8 | address player; 9 | address opponent; 10 | uint256 betAmount; 11 | } 12 | Game[] deployedRPSGames; 13 | 14 | event RPSGameCreated(Game game); 15 | 16 | function createGame(uint256 betAmount, address opponent) external { 17 | address gameAddress = address( 18 | new RPSGame(betAmount, msg.sender, opponent) 19 | ); 20 | Game memory newGame = Game( 21 | gameAddress, 22 | msg.sender, 23 | opponent, 24 | betAmount 25 | ); 26 | deployedRPSGames.push(newGame); 27 | 28 | emit RPSGameCreated(newGame); 29 | } 30 | 31 | function getDeployedGames() external view returns (Game[] memory) { 32 | return deployedRPSGames; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /contracts/RPSToken.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: Unlicense 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | contract RPSToken is ERC20 { 7 | mapping(address => bool) private claimed; 8 | 9 | constructor(string memory name, string memory symbol) ERC20(name, symbol) { 10 | _mint(msg.sender, 100 * 10**decimals()); 11 | claimed[msg.sender] = true; 12 | } 13 | 14 | function mint() external returns (bool) { 15 | require( 16 | !claimed[msg.sender], 17 | "RPSToken: You have already minted your share" 18 | ); 19 | _mint(msg.sender, 100 * 10**decimals()); 20 | claimed[msg.sender] = true; 21 | return true; 22 | } 23 | 24 | function burn(uint256 amount) external returns (bool) { 25 | _burn(msg.sender, amount); 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /deploy/RPSToken.ts: -------------------------------------------------------------------------------- 1 | module.exports = async ({ 2 | getNamedAccounts, 3 | deployments, 4 | getChainId, 5 | getUnnamedAccounts, 6 | }) => { 7 | const { deploy } = deployments; 8 | const { deployer } = await getNamedAccounts(); 9 | 10 | await deploy("RPSToken", { 11 | from: deployer, 12 | gas: 4000000, 13 | args: ["RPS Token", "RPST"], 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend for Simple RPS Game Implementation 2 | -------------------------------------------------------------------------------- /frontend/craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | style: { 3 | postcss: { 4 | plugins: [require("tailwindcss"), require("autoprefixer")], 5 | }, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.2.0", 7 | "@ethersproject/providers": "^5.4.3", 8 | "@testing-library/jest-dom": "^5.14.1", 9 | "@testing-library/react": "^11.2.7", 10 | "@testing-library/user-event": "^12.8.3", 11 | "@types/jest": "^26.0.24", 12 | "@types/node": "^12.20.19", 13 | "@types/react": "^17.0.17", 14 | "@types/react-dom": "^17.0.9", 15 | "@types/react-router-dom": "^5.1.8", 16 | "ethers": "^5.4.4", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "4.0.3", 21 | "typescript": "^4.3.5", 22 | "web-vitals": "^1.1.2" 23 | }, 24 | "scripts": { 25 | "start": "craco start", 26 | "build": "craco build", 27 | "test": "craco test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | }, 48 | "devDependencies": { 49 | "autoprefixer": "^9", 50 | "postcss": "^7", 51 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.7" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiranz/rps_game/6f567203aa0e3b4eb74da47881d2e8515a3d6cff/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiranz/rps_game/6f567203aa0e3b4eb74da47881d2e8515a3d6cff/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chiranz/rps_game/6f567203aa0e3b4eb74da47881d2e8515a3d6cff/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Redirect, 7 | } from "react-router-dom"; 8 | import Footer from "./components/layout/Footer"; 9 | import Navbar from "./components/layout/Navbar"; 10 | import { joinClasses } from "./helpers"; 11 | import Game from "./components/Game"; 12 | import DeployedContracts from "./components/DeployedContracts"; 13 | import { useRPSGameFactory } from "./context/RPSGameFactoryContext"; 14 | import { useWallet } from "./context/WalletContext"; 15 | import { getProvider } from "./provider"; 16 | 17 | function App() { 18 | const { walletAddress, setWalletAddress } = useWallet(); 19 | const { selectedGameAddress } = useRPSGameFactory(); 20 | React.useEffect(() => { 21 | async function init() { 22 | try { 23 | const _provider = await getProvider(); 24 | const network = await _provider.getNetwork(); 25 | const { chainId } = network; 26 | if (chainId === 4 && setWalletAddress) { 27 | const signer = _provider.getSigner(); 28 | setWalletAddress(await signer.getAddress()); 29 | } 30 | } catch (err) { 31 | console.log(err); 32 | } 33 | } 34 | init(); 35 | }, [setWalletAddress]); 36 | return ( 37 | 38 |
50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | {walletAddress && selectedGameAddress ? ( 59 | 60 | ) : ( 61 | 62 | )} 63 | 64 | 65 |
66 |
67 |
68 |
69 | ); 70 | } 71 | 72 | export default App; 73 | -------------------------------------------------------------------------------- /frontend/src/RPSGameFactory.d.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { 6 | ethers, 7 | EventFilter, 8 | Signer, 9 | BigNumber, 10 | BigNumberish, 11 | PopulatedTransaction, 12 | } from "ethers"; 13 | import { 14 | Contract, 15 | ContractTransaction, 16 | Overrides, 17 | CallOverrides, 18 | } from "@ethersproject/contracts"; 19 | import { BytesLike } from "@ethersproject/bytes"; 20 | import { Listener, Provider } from "@ethersproject/providers"; 21 | import { FunctionFragment, EventFragment, Result } from "@ethersproject/abi"; 22 | 23 | interface RPSGameFactoryInterface extends ethers.utils.Interface { 24 | functions: { 25 | "createGame(uint256,address)": FunctionFragment; 26 | "getDeployedGames()": FunctionFragment; 27 | }; 28 | 29 | encodeFunctionData( 30 | functionFragment: "createGame", 31 | values: [BigNumberish, string] 32 | ): string; 33 | encodeFunctionData( 34 | functionFragment: "getDeployedGames", 35 | values?: undefined 36 | ): string; 37 | 38 | decodeFunctionResult(functionFragment: "createGame", data: BytesLike): Result; 39 | decodeFunctionResult( 40 | functionFragment: "getDeployedGames", 41 | data: BytesLike 42 | ): Result; 43 | 44 | events: { 45 | "RPSGameCreated(tuple)": EventFragment; 46 | }; 47 | 48 | getEvent(nameOrSignatureOrTopic: "RPSGameCreated"): EventFragment; 49 | } 50 | 51 | export class RPSGameFactory extends Contract { 52 | connect(signerOrProvider: Signer | Provider | string): this; 53 | attach(addressOrName: string): this; 54 | deployed(): Promise; 55 | 56 | on(event: EventFilter | string, listener: Listener): this; 57 | once(event: EventFilter | string, listener: Listener): this; 58 | addListener(eventName: EventFilter | string, listener: Listener): this; 59 | removeAllListeners(eventName: EventFilter | string): this; 60 | removeListener(eventName: any, listener: Listener): this; 61 | 62 | interface: RPSGameFactoryInterface; 63 | 64 | functions: { 65 | createGame( 66 | betAmount: BigNumberish, 67 | opponent: string, 68 | overrides?: Overrides 69 | ): Promise; 70 | 71 | "createGame(uint256,address)"( 72 | betAmount: BigNumberish, 73 | opponent: string, 74 | overrides?: Overrides 75 | ): Promise; 76 | 77 | getDeployedGames(overrides?: CallOverrides): Promise<{ 78 | 0: { 79 | gameAddress: string; 80 | player: string; 81 | opponent: string; 82 | betAmount: BigNumber; 83 | 0: string; 84 | 1: string; 85 | 2: string; 86 | 3: BigNumber; 87 | }[]; 88 | }>; 89 | 90 | "getDeployedGames()"(overrides?: CallOverrides): Promise<{ 91 | 0: { 92 | gameAddress: string; 93 | player: string; 94 | opponent: string; 95 | betAmount: BigNumber; 96 | 0: string; 97 | 1: string; 98 | 2: string; 99 | 3: BigNumber; 100 | }[]; 101 | }>; 102 | }; 103 | 104 | createGame( 105 | betAmount: BigNumberish, 106 | opponent: string, 107 | overrides?: Overrides 108 | ): Promise; 109 | 110 | "createGame(uint256,address)"( 111 | betAmount: BigNumberish, 112 | opponent: string, 113 | overrides?: Overrides 114 | ): Promise; 115 | 116 | getDeployedGames( 117 | overrides?: CallOverrides 118 | ): Promise< 119 | { 120 | gameAddress: string; 121 | player: string; 122 | opponent: string; 123 | betAmount: BigNumber; 124 | 0: string; 125 | 1: string; 126 | 2: string; 127 | 3: BigNumber; 128 | }[] 129 | >; 130 | 131 | "getDeployedGames()"( 132 | overrides?: CallOverrides 133 | ): Promise< 134 | { 135 | gameAddress: string; 136 | player: string; 137 | opponent: string; 138 | betAmount: BigNumber; 139 | 0: string; 140 | 1: string; 141 | 2: string; 142 | 3: BigNumber; 143 | }[] 144 | >; 145 | 146 | callStatic: { 147 | createGame( 148 | betAmount: BigNumberish, 149 | opponent: string, 150 | overrides?: CallOverrides 151 | ): Promise; 152 | 153 | "createGame(uint256,address)"( 154 | betAmount: BigNumberish, 155 | opponent: string, 156 | overrides?: CallOverrides 157 | ): Promise; 158 | 159 | getDeployedGames( 160 | overrides?: CallOverrides 161 | ): Promise< 162 | { 163 | gameAddress: string; 164 | player: string; 165 | opponent: string; 166 | betAmount: BigNumber; 167 | 0: string; 168 | 1: string; 169 | 2: string; 170 | 3: BigNumber; 171 | }[] 172 | >; 173 | 174 | "getDeployedGames()"( 175 | overrides?: CallOverrides 176 | ): Promise< 177 | { 178 | gameAddress: string; 179 | player: string; 180 | opponent: string; 181 | betAmount: BigNumber; 182 | 0: string; 183 | 1: string; 184 | 2: string; 185 | 3: BigNumber; 186 | }[] 187 | >; 188 | }; 189 | 190 | filters: { 191 | RPSGameCreated(game: null): EventFilter; 192 | }; 193 | 194 | estimateGas: { 195 | createGame( 196 | betAmount: BigNumberish, 197 | opponent: string, 198 | overrides?: Overrides 199 | ): Promise; 200 | 201 | "createGame(uint256,address)"( 202 | betAmount: BigNumberish, 203 | opponent: string, 204 | overrides?: Overrides 205 | ): Promise; 206 | 207 | getDeployedGames(overrides?: CallOverrides): Promise; 208 | 209 | "getDeployedGames()"(overrides?: CallOverrides): Promise; 210 | }; 211 | 212 | populateTransaction: { 213 | createGame( 214 | betAmount: BigNumberish, 215 | opponent: string, 216 | overrides?: Overrides 217 | ): Promise; 218 | 219 | "createGame(uint256,address)"( 220 | betAmount: BigNumberish, 221 | opponent: string, 222 | overrides?: Overrides 223 | ): Promise; 224 | 225 | getDeployedGames(overrides?: CallOverrides): Promise; 226 | 227 | "getDeployedGames()"( 228 | overrides?: CallOverrides 229 | ): Promise; 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /frontend/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ForwardedRef, 3 | forwardRef, 4 | ReactNode, 5 | ButtonHTMLAttributes, 6 | } from "react"; 7 | import { joinClasses } from "../helpers"; 8 | 9 | interface BtnPropsWithChildren {} 10 | type ColorProps = "primary" | "success" | "danger" | "warning" | "dark"; 11 | 12 | interface BtnProps 13 | extends ButtonHTMLAttributes, 14 | BtnPropsWithChildren { 15 | block?: boolean; 16 | children: ReactNode; 17 | className?: string; 18 | color?: ColorProps; 19 | disabled?: boolean; 20 | outline?: boolean; 21 | rounded?: boolean; 22 | size?: "sm" | "md" | "lg"; 23 | submit?: boolean; 24 | } 25 | 26 | type ButtonRef = ForwardedRef; 27 | 28 | const style = { 29 | base: joinClasses( 30 | "border", 31 | "rounded", 32 | "font-bold", 33 | "focus:outline-none", 34 | "transition", 35 | "duration-100", 36 | "ease-in" 37 | ), 38 | default: joinClasses("hover:bg-gray-200", "border", "text-gray-700"), 39 | block: `flex justify-center w-full`, 40 | rounded: `rounded-full`, 41 | disabled: `opacity-60 cursor-not-allowed`, 42 | sizes: { 43 | sm: "px-6 py-1 text-sm", 44 | md: "px-6 py-2", 45 | lg: "px-6 py-3 text-lg", 46 | }, 47 | primary: joinClasses("hover:bg-blue-800", "bg-blue-600", "text-white"), 48 | success: joinClasses("hover:bg-green-800", "bg-green-600", "text-white"), 49 | danger: joinClasses("hover:bg-red-800", "bg-red-600", "text-white"), 50 | warning: joinClasses("hover:bg-yellow-800", "bg-yellow-600", "text-white"), 51 | dark: joinClasses("hover:bg-gray-800", "bg-gray-600", "text-white"), 52 | }; 53 | const Button = forwardRef( 54 | ( 55 | { 56 | block = false, 57 | children, 58 | className, 59 | color, 60 | disabled = false, 61 | rounded, 62 | size = "md", 63 | submit, 64 | ...props 65 | }: BtnProps, 66 | ref: ButtonRef 67 | ) => ( 68 | 80 | ) 81 | ); 82 | 83 | export default Button; 84 | -------------------------------------------------------------------------------- /frontend/src/components/CircularLoader.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | 3 | export default function CircularLoader(): ReactElement { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/components/DeployedContracts.tsx: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import React, { ReactElement } from "react"; 3 | import { Link } from "react-router-dom"; 4 | import { useRPSGameFactory } from "../context/RPSGameFactoryContext"; 5 | import { useWallet } from "../context/WalletContext"; 6 | import { joinClasses } from "../helpers"; 7 | import Button from "./Button"; 8 | import InputField from "./InputField"; 9 | 10 | export default function DeployedContracts(): ReactElement { 11 | const { walletAddress } = useWallet(); 12 | const { deployedGames, createGame, selectGame } = useRPSGameFactory(); 13 | const [opponent, setOpponent] = React.useState(""); 14 | const [betAmount, setBetAmount] = React.useState(""); 15 | 16 | const handleCreateGame = () => { 17 | // Check if opponent address length is addr length 18 | if (opponent.length !== 42) { 19 | alert("Opponent address is invalid!!"); 20 | return; 21 | } 22 | if (!parseFloat(betAmount)) { 23 | alert("Please submit a valid bet e.g. '0.1' ETH"); 24 | return; 25 | } 26 | if (createGame) { 27 | createGame(betAmount, opponent); 28 | } 29 | setOpponent(""); 30 | setBetAmount(""); 31 | }; 32 | 33 | return ( 34 |
35 | {!walletAddress ? ( 36 |
37 | Please connect to your metamask wallet 38 |
39 | ) : null} 40 |
41 | setOpponent(e.target.value)} 45 | /> 46 | 47 | setBetAmount(e.target.value)} 52 | /> 53 | 61 |
62 | 63 | {deployedGames.length > 0 ? ( 64 |
    65 |

    Deployed Games

    66 | {deployedGames.map((game, index) => ( 67 |
  • 68 | 75 | {game.gameAddress} 76 | 77 |
    88 | {ethers.utils.formatEther(game.betAmount)} ETH 89 |
    90 | 91 | selectGame && selectGame(game.gameAddress)} 94 | > 95 | {walletAddress === game.player || 96 | walletAddress === game.opponent ? ( 97 | 104 | ) : ( 105 | 106 | )} 107 | 108 |
  • 109 | ))} 110 |
111 | ) : ( 112 |
113 |

No deployed games!

114 |
115 | )} 116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/components/Game.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { ContractProvider } from "../context/RPSGameContractContext"; 3 | import Leaderboard from "./Leaderboard"; 4 | import Playground from "./Playground"; 5 | import { useWallet } from "../context/WalletContext"; 6 | 7 | export default function Game(): ReactElement { 8 | const { walletAddress } = useWallet(); 9 | if (!walletAddress) { 10 | return ( 11 |
12 |

Please Connect your metamask first

13 |
14 | ); 15 | } 16 | return ( 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/components/GameActionInfoCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { joinClasses } from "../helpers"; 3 | import Loader from "./Loader"; 4 | 5 | interface Props { 6 | message: string; 7 | loader?: boolean; 8 | } 9 | 10 | export default function GameActionInfoCard({ 11 | message, 12 | loader, 13 | }: Props): ReactElement { 14 | return ( 15 |
27 | {loader && } 28 |

{message}

29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/GameStatsCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { useRPSGameContract } from "../context/RPSGameContractContext"; 3 | import { joinClasses } from "../helpers"; 4 | import Button from "./Button"; 5 | 6 | export enum GameStage { 7 | Open, 8 | BetsDeposited, 9 | MovesSubmitted, 10 | MoveRevealed, 11 | Completed, 12 | } 13 | export const getGameStatusText = (id: number): string => { 14 | const gameStateToText: { [key: number]: string } = { 15 | 0: "Open", 16 | 1: "Bets Deposited", 17 | 2: "Moves Submitted", 18 | 3: "Moves Revealed", 19 | 4: "Completed", 20 | }; 21 | return gameStateToText[id]; 22 | }; 23 | 24 | export default function GameStatsCard({ 25 | betAmount, 26 | }: { 27 | betAmount: string | null; 28 | }): ReactElement { 29 | const { depositBet, isPlayer, gameStage, withdrawFund, currentPlayer } = 30 | useRPSGameContract(); 31 | 32 | console.log({ balance: parseFloat(currentPlayer?.balance || "") }); 33 | return ( 34 |
49 |
50 |

51 | Rock 52 | Paper 53 | Scissors 54 |

55 |
56 |
Bet Amt: {betAmount} ETH
57 |
58 |
59 |
60 |

61 | {isPlayer ? "Player" : "Audience"} 62 |

63 |
64 | 75 | 78 |
79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /frontend/src/components/GlobalMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { useMessage } from "../context/MessageContext"; 3 | import { joinClasses } from "../helpers"; 4 | 5 | export default function GlobalMessage(): ReactElement { 6 | const { globalMessage, setGlobalMessage } = useMessage(); 7 | 8 | const color = 9 | globalMessage?.type === "error" 10 | ? "red" 11 | : globalMessage?.type === "info" 12 | ? "blue" 13 | : globalMessage?.type === "warning" 14 | ? "yellow" 15 | : "green"; 16 | const handleClose = () => { 17 | if (setGlobalMessage) { 18 | setGlobalMessage({}); 19 | } 20 | }; 21 | 22 | if (globalMessage?.message) { 23 | return ( 24 |
56 | {globalMessage?.message} 57 | 63 |
64 | ); 65 | } 66 | return
; 67 | } 68 | -------------------------------------------------------------------------------- /frontend/src/components/HiddenMove.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { joinClasses } from "../helpers"; 3 | 4 | export default function HiddenMove(): ReactElement { 5 | return ( 6 |
17 |
30 | Not Revealed 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/InputField.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef, InputHTMLAttributes } from "react"; 2 | import { joinClasses } from "../helpers"; 3 | interface InputProps extends InputHTMLAttributes {} 4 | 5 | type InputRef = ForwardedRef; 6 | 7 | const InputField = forwardRef( 8 | ( 9 | { 10 | onChange, 11 | value, 12 | type, 13 | name, 14 | placeholder, 15 | id, 16 | className, 17 | ...props 18 | }: InputProps, 19 | ref: InputRef 20 | ) => ( 21 | 34 | ) 35 | ); 36 | 37 | export default InputField; 38 | -------------------------------------------------------------------------------- /frontend/src/components/Leaderboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { useRPSGameContract } from "../context/RPSGameContractContext"; 3 | import { joinClasses } from "../helpers"; 4 | import GameStatsCard from "./GameStatsCard"; 5 | import PlayerCard from "./PlayerCard"; 6 | 7 | export default function Leaderboard(): ReactElement { 8 | const { currentPlayer, betAmount, opponent, isPlayer } = useRPSGameContract(); 9 | console.log({ isPlayer }); 10 | return ( 11 |
14 | 15 | 16 | 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | 3 | export default function Loader(): ReactElement { 4 | let circleCommonClasses = "h-4 w-4 bg-gray-400 rounded-full"; 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/OptionButton.module.css: -------------------------------------------------------------------------------- 1 | .shadowin { 2 | box-shadow: inset 0 6px rgba(0, 0, 0, 0.1) !important; 3 | } 4 | 5 | .shadowout { 6 | box-shadow: inset 0 -6px rgba(0, 0, 0, 0.1) !important; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/OptionButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { joinClasses } from "../helpers"; 3 | import Button from "./Button"; 4 | import styles from "./OptionButton.module.css"; 5 | 6 | interface Props { 7 | src: string; 8 | bgColor: string; 9 | alt?: string; 10 | onClick?: (e: React.MouseEvent) => void; 11 | value?: string; 12 | className?: string; 13 | } 14 | 15 | export default function OptionButton({ 16 | src, 17 | bgColor = "gray", 18 | alt, 19 | onClick, 20 | value, 21 | className, 22 | }: Props): ReactElement { 23 | return ( 24 |
{}} 27 | className={ 28 | `${styles.shadowout} ` + 29 | joinClasses( 30 | `${ 31 | bgColor === "yellow" 32 | ? "bg-yellow-500" 33 | : bgColor === "red" 34 | ? "bg-red-500" 35 | : bgColor === "blue" 36 | ? "bg-blue-500" 37 | : "bg-gray-500" 38 | }`, 39 | "rounded-full", 40 | "hover:opacity-80", 41 | "w-36", 42 | "h-36", 43 | "inline-flex", 44 | "justify-center", 45 | "items-center", 46 | "cursor-pointer", 47 | "m-4" 48 | ) + 49 | ` ${className}` 50 | } 51 | > 52 | 68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/components/PlayerCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { Player } from "../context/RPSGameContractContext/contractContext"; 3 | import { getTruncatedAddress, joinClasses } from "../helpers"; 4 | 5 | interface PlayerWithTag extends Player { 6 | tag: "player" | "opponent" | "audience"; 7 | betAmount: string | null; 8 | } 9 | 10 | export default function PlayerCard({ 11 | addr, 12 | balance, 13 | revealed, 14 | submitted, 15 | tag, 16 | betAmount, 17 | }: PlayerWithTag): ReactElement { 18 | console.log(addr); 19 | return ( 20 |
30 |
31 |

32 | {tag === "player" ? "Player" : "Opponent"}:{" "} 33 | {getTruncatedAddress(addr)} 34 |

35 |

Balance: {balance} ETH

36 |
37 |
38 |

39 | Deposited:{" "} 40 | 41 | {parseFloat(balance || "0") >= parseFloat(betAmount || "0") && 42 | balance 43 | ? "✅" 44 | : "❌"}{" "} 45 | {" "} 46 |

47 |

48 | Move Submitted: {submitted ? "✅" : "❌"} {" "} 49 |

50 |

51 | Move Revealed: {revealed ? "✅" : "❌"} 52 |

53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/components/Playground.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from "react"; 2 | import rock from "../images/icon-rock.svg"; 3 | import paper from "../images/icon-paper.svg"; 4 | import scissors from "../images/icon-scissors.svg"; 5 | import OptionButton from "./OptionButton"; 6 | import { useRPSGameContract } from "../context/RPSGameContractContext"; 7 | import GameActionInfoCard from "./GameActionInfoCard"; 8 | import SubmitMove from "./SubmitMove"; 9 | import RevealMove from "./RevealMove"; 10 | 11 | export type Option = { 12 | image: string; 13 | value: "paper" | "scissors" | "rock"; 14 | color: "yellow" | "red" | "blue"; 15 | alt: string; 16 | key: number; 17 | }; 18 | export const options: Option[] = [ 19 | { image: rock, key: 1, value: "rock", color: "red", alt: "rock icon" }, 20 | { image: paper, key: 2, value: "paper", color: "blue", alt: "paper icon" }, 21 | { 22 | image: scissors, 23 | key: 3, 24 | value: "scissors", 25 | color: "yellow", 26 | alt: "scissors icon", 27 | }, 28 | ]; 29 | export const getOptionButton = ( 30 | option: Option, 31 | onClick?: (e: React.MouseEvent) => void 32 | ) => { 33 | return ( 34 | 42 | ); 43 | }; 44 | 45 | export default function Playground(): ReactElement { 46 | const { gameStage, currentPlayer, betAmount, opponent } = 47 | useRPSGameContract(); 48 | const [salt, setSalt] = useState(""); 49 | const [move, setMove] = useState(0); 50 | console.log(gameStage); 51 | 52 | return ( 53 |
54 | {gameStage === 0 && 55 | (parseFloat(currentPlayer?.balance || "0") < 56 | parseFloat(betAmount || "0") ? ( 57 | 58 | ) : parseFloat(opponent?.balance || "0") < 59 | parseFloat(betAmount || "0") ? ( 60 | 64 | ) : ( 65 | "" 66 | ))} 67 | {gameStage === 1 && currentPlayer?.submitted && !opponent?.submitted ? ( 68 | 72 | ) : ( 73 | 79 | )} 80 | {!currentPlayer?.submitted && gameStage === 1 && ( 81 | 82 | )} 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/components/RevealMove.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from "react"; 2 | import { useRPSGameContract } from "../context/RPSGameContractContext"; 3 | import { joinClasses } from "../helpers"; 4 | import Button from "./Button"; 5 | import HiddenMove from "./HiddenMove"; 6 | import InputField from "./InputField"; 7 | import Loader from "./Loader"; 8 | import { getOptionButton, options } from "./Playground"; 9 | 10 | interface RevealMoveProps { 11 | salt: string; 12 | move: number; 13 | setSalt: React.Dispatch>; 14 | setMove: React.Dispatch>; 15 | } 16 | 17 | export default function RevealMove({ 18 | salt, 19 | move, 20 | setSalt, 21 | setMove, 22 | }: RevealMoveProps): ReactElement { 23 | const { revealMove, currentPlayer, opponent, isPlayer, winner, resetGame } = 24 | useRPSGameContract(); 25 | const handleMoveReveal = () => { 26 | if (move === 0) { 27 | alert("Move must be selected!!"); 28 | return; 29 | } 30 | if (revealMove && move) { 31 | revealMove(move, salt); 32 | } 33 | }; 34 | const handleResetGame = () => { 35 | if (resetGame) { 36 | resetGame(); 37 | } 38 | }; 39 | const getGameResultText = (): string => { 40 | if (winner === "0x0000000000000000000000000000000000000000") { 41 | return "Game Draw"; 42 | } 43 | if (currentPlayer?.addr === winner) { 44 | if (isPlayer) { 45 | return "You Won!!"; 46 | } else { 47 | return "Player1 Won!!"; 48 | } 49 | } else { 50 | if (isPlayer) { 51 | return "You Lost!!"; 52 | } else { 53 | return "Player2 Won!!"; 54 | } 55 | } 56 | }; 57 | return ( 58 | 59 | {currentPlayer?.submitted && ( 60 |
70 |
79 |

80 | {isPlayer ? "Your Pick" : "Player1 Pick"} 81 |

82 | {currentPlayer?.revealed && currentPlayer.move ? ( 83 | getOptionButton(options[currentPlayer.move - 1]) 84 | ) : ( 85 | 86 | )} 87 |
88 |
96 | {currentPlayer.revealed && !opponent?.revealed && ( 97 | 98 | 99 |
Waiting for the opponent to reveal move
100 |
101 | )} 102 | {!currentPlayer.revealed && ( 103 | 104 |
105 | setSalt(e.target.value)} 110 | disabled={!isPlayer} 111 | /> 112 | 131 |
132 | 140 |
141 | )} 142 | {currentPlayer.revealed && opponent?.revealed && ( 143 | 144 |

{getGameResultText()}

145 | 153 |
154 | )} 155 |
156 |
165 |

166 | {isPlayer ? "Opponent Pick" : "Player2 Pick"} 167 |

168 | {opponent?.revealed && opponent.move ? ( 169 | getOptionButton(options[opponent.move - 1]) 170 | ) : ( 171 | 172 | )} 173 | {} 174 |
175 |
176 | )} 177 |
178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /frontend/src/components/SubmitMove.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement, useState } from "react"; 2 | import rock from "../images/icon-rock.svg"; 3 | import paper from "../images/icon-paper.svg"; 4 | import scissors from "../images/icon-scissors.svg"; 5 | import OptionButton from "./OptionButton"; 6 | import { useRPSGameContract } from "../context/RPSGameContractContext"; 7 | import Button from "./Button"; 8 | import InputField from "./InputField"; 9 | 10 | type Option = { 11 | image: string; 12 | value: "paper" | "scissors" | "rock"; 13 | color: "yellow" | "red" | "blue"; 14 | alt: string; 15 | key: number; 16 | }; 17 | export const options: Option[] = [ 18 | { image: rock, key: 1, value: "rock", color: "red", alt: "rock icon" }, 19 | { image: paper, key: 2, value: "paper", color: "blue", alt: "paper icon" }, 20 | { 21 | image: scissors, 22 | key: 3, 23 | value: "scissors", 24 | color: "yellow", 25 | alt: "scissors icon", 26 | }, 27 | ]; 28 | interface SubmitMoveProps { 29 | salt: string; 30 | setSalt: React.Dispatch>; 31 | setMove: React.Dispatch>; 32 | } 33 | 34 | export default function SubmitMove({ 35 | salt, 36 | setSalt, 37 | setMove, 38 | }: SubmitMoveProps): ReactElement { 39 | const { gameStage, submitMove, currentPlayer, isPlayer } = 40 | useRPSGameContract(); 41 | const [userChoice, setUserChoice] = useState