From 17e57198aa2b6ea702028b2f060aac777be2afb2 Mon Sep 17 00:00:00 2001 From: "italiano@oplabs.co" Date: Wed, 26 Jun 2024 16:20:25 -0400 Subject: [PATCH] Adds basic tic tac toe game --- contracts/src/Counter.sol | 14 ---- contracts/src/TicTacToe.sol | 131 +++++++++++++++++++++++++++++++++ contracts/test/Counter.t.sol | 24 ------ contracts/test/TicTacToe.t.sol | 101 +++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 38 deletions(-) delete mode 100644 contracts/src/Counter.sol create mode 100644 contracts/src/TicTacToe.sol delete mode 100644 contracts/test/Counter.t.sol create mode 100644 contracts/test/TicTacToe.t.sol diff --git a/contracts/src/Counter.sol b/contracts/src/Counter.sol deleted file mode 100644 index aded7997..00000000 --- a/contracts/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/contracts/src/TicTacToe.sol b/contracts/src/TicTacToe.sol new file mode 100644 index 00000000..add6d79e --- /dev/null +++ b/contracts/src/TicTacToe.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract TicTacToe { + enum PlayerTurn { PlayerOne, PlayerTwo } + + enum GameState { + WaitingForPlayer, + Playing, + Draw, + PlayerOneWins, + PlayerTwoWins + } + + event GameCreated(uint256 indexed gameId, address indexed creator); + event PlayerJoinedGame(uint256 indexed gameId, address indexed player); + event PlayerMadeMove(uint256 indexed gameId, uint8 x, uint8 y); + event GameCompletedWithWinner(uint256 indexed gameId, address indexed winner); + event GameCompletedDraw(uint256 indexed gameId); + + struct Game { + uint256 id; + GameState state; + address player1; + address player2; + PlayerTurn currentTurn; + address winner; + uint8 numOfTurns; + uint8[3][3] board; + } + + uint256 numOfGames; + mapping(uint256 => Game) public games; + + // Magic Square: https://mathworld.wolfram.com/MagicSquare.html + uint8[3][3] private MAGIC_SQUARE = [[8, 3, 4], [1, 5, 9], [6, 7, 2]]; + uint8 private constant MAGIC_SUM_PLAYER_ONE = 15; + uint8 private constant MAGIC_SUM_PLAYER_TWO = 30; + + modifier validGame(uint256 gameId) { + require(gameId <= numOfGames, "Invalid game id"); + _; + } + + function createGame(address player1) public returns (uint256) { + numOfGames++; + + games[numOfGames] = Game({ + id: numOfGames, + state: GameState.WaitingForPlayer, + player1: player1, + player2: address(0), + currentTurn: PlayerTurn.PlayerOne, + winner: address(0), + numOfTurns: 0, + board: [[0, 0, 0], [0, 0, 0], [0, 0, 0]] + }); + + emit GameCreated(numOfGames, player1); + return numOfGames; + } + + function joinGame(address player2, uint256 gameId) public validGame(gameId) returns (bool) { + Game storage game = games[gameId]; + + require(game.state == GameState.WaitingForPlayer, "Game already has enough players"); + require(game.player1 != player2, "Players must be different"); + + game.player2 = player2; + game.state = GameState.Playing; + emit PlayerJoinedGame(game.id, game.player2); + + return true; + } + + function makeMove(address player, uint256 gameId, uint8 x, uint8 y) public validGame(gameId) returns (bool) { + Game storage game = games[gameId]; + require(game.state == GameState.Playing, "Game hasn't started or has already been completed"); + + bool isPlayerOneTurn = game.currentTurn == PlayerTurn.PlayerOne; + require(isPlayerOneTurn ? player == game.player1 : player == game.player2, "Not a valid player in the game"); + require(x >= 0 && x <= 2 || y >= 0 && y <= 2, "Move out of bounds"); + require(game.board[x][y] == 0, "Invalid move"); + + game.board[x][y] = isPlayerOneTurn ? 1 : 2; + game.currentTurn = isPlayerOneTurn ? PlayerTurn.PlayerTwo : PlayerTurn.PlayerOne; + game.numOfTurns++; + emit PlayerMadeMove(game.id, x, y); + + bool isWin = checkForWin(player, game.id); + if (isWin) { + game.state = isPlayerOneTurn ? GameState.PlayerOneWins : GameState.PlayerTwoWins; + emit GameCompletedWithWinner(game.id, player); + } else if (game.numOfTurns >= 9) { + game.state = GameState.Draw; + emit GameCompletedDraw(game.id); + } + + return true; + } + + function getGame(uint256 gameId) public view returns (Game memory) { + return games[gameId]; + } + + function checkForWin(address player, uint256 gameId) public validGame(gameId) view returns (bool) { + Game storage game = games[gameId]; + require(player == game.player1 || player == game.player2, "Not a valid player in the game"); + + uint8 magicSum = player == game.player1 ? MAGIC_SUM_PLAYER_ONE : MAGIC_SUM_PLAYER_TWO; + + // row & col check + for (uint8 i = 0; i < 3; i++) { + uint8 rowSum = (game.board[i][0] * MAGIC_SQUARE[i][0]) + (game.board[i][1] * MAGIC_SQUARE[i][1]) + (game.board[i][2] * MAGIC_SQUARE[i][2]); + uint8 colSum = (game.board[0][i] * MAGIC_SQUARE[0][i]) + (game.board[1][i] * MAGIC_SQUARE[1][i]) + (game.board[2][i] * MAGIC_SQUARE[2][i]); + + if (rowSum == magicSum || colSum == magicSum) { + return true; + } + } + + // diag check + uint8 leftToRightSum = (game.board[0][0] * MAGIC_SQUARE[0][0]) + (game.board[1][1] * MAGIC_SQUARE[1][1]) + (game.board[2][2] * MAGIC_SQUARE[2][2]); + uint8 rightToLeftSum = (game.board[0][2] * MAGIC_SQUARE[0][2]) + (game.board[1][1] * MAGIC_SQUARE[1][1]) + (game.board[2][0] * MAGIC_SQUARE[2][0]); + if (leftToRightSum == magicSum || rightToLeftSum == magicSum) { + return true; + } + + return false; + } +} diff --git a/contracts/test/Counter.t.sol b/contracts/test/Counter.t.sol deleted file mode 100644 index 54b724f7..00000000 --- a/contracts/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -} diff --git a/contracts/test/TicTacToe.t.sol b/contracts/test/TicTacToe.t.sol new file mode 100644 index 00000000..abd3f5d1 --- /dev/null +++ b/contracts/test/TicTacToe.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {TicTacToe} from "../src/TicTacToe.sol"; + +contract TicTacToeTest is Test { + address player1 = address(1); + address player2 = address(2); + + function test_CreateGame() public { + TicTacToe ticTacToe = new TicTacToe(); + uint256 gameId = ticTacToe.createGame(player1); + TicTacToe.Game memory game = ticTacToe.getGame(gameId); + assertEq(gameId, game.id); + assertEq(game.player1, player1); + assertEq(uint(game.state), uint(TicTacToe.GameState.WaitingForPlayer)); + } + + function test_JoinGameSuccessful() public { + TicTacToe ticTacToe = new TicTacToe(); + uint256 gameId = ticTacToe.createGame(player1); + + bool success = ticTacToe.joinGame(player2, gameId); + assertTrue(success); + + TicTacToe.Game memory game = ticTacToe.getGame(gameId); + assertEq(game.player1, player1); + assertEq(game.player2, player2); + } + + function test_JoinGameInvalidState() public { + TicTacToe ticTacToe = new TicTacToe(); + uint256 gameId = ticTacToe.createGame(player1); + ticTacToe.joinGame(player2, gameId); + vm.expectRevert("Game already has enough players"); + ticTacToe.joinGame(address(3), gameId); + } + + function test_JoinGameSamePlayer() public { + TicTacToe ticTacToe = new TicTacToe(); + uint256 gameId = ticTacToe.createGame(player1); + vm.expectRevert("Players must be different"); + ticTacToe.joinGame(player1, gameId); + } + + function test_PlayGameUntilWin() public { + TicTacToe ticTacToe = new TicTacToe(); + + // Game 1: row win by player 1 + uint256 gameId = ticTacToe.createGame(player1); + ticTacToe.joinGame(player2, gameId); + ticTacToe.makeMove(player1, gameId, 0, 0); + ticTacToe.makeMove(player2, gameId, 1, 0); + ticTacToe.makeMove(player1, gameId, 0, 1); + ticTacToe.makeMove(player2, gameId, 1, 1); + ticTacToe.makeMove(player1, gameId, 0, 2); + TicTacToe.Game memory game = ticTacToe.getGame(gameId); + assertEq(uint(game.state), uint(TicTacToe.GameState.PlayerOneWins)); + + // Game 2: col win by player 2 + gameId = ticTacToe.createGame(player1); + ticTacToe.joinGame(player2, gameId); + ticTacToe.makeMove(player1, gameId, 0, 0); + ticTacToe.makeMove(player2, gameId, 0, 1); + ticTacToe.makeMove(player1, gameId, 0, 2); + ticTacToe.makeMove(player2, gameId, 1, 1); + ticTacToe.makeMove(player1, gameId, 1, 0); + ticTacToe.makeMove(player2, gameId, 2, 1); + game = ticTacToe.getGame(gameId); + assertEq(uint(game.state), uint(TicTacToe.GameState.PlayerTwoWins)); + + // Game 3: dial win by player 1 + gameId = ticTacToe.createGame(player1); + ticTacToe.joinGame(player2, gameId); + ticTacToe.makeMove(player1, gameId, 0, 0); + ticTacToe.makeMove(player2, gameId, 1, 0); + ticTacToe.makeMove(player1, gameId, 1, 1); + ticTacToe.makeMove(player2, gameId, 1, 2); + ticTacToe.makeMove(player1, gameId, 2, 2); + game = ticTacToe.getGame(gameId); + assertEq(uint(game.state), uint(TicTacToe.GameState.PlayerOneWins)); + } + + function test_PlayGameUntilDraw() public { + TicTacToe ticTacToe = new TicTacToe(); + uint256 gameId = ticTacToe.createGame(player1); + ticTacToe.joinGame(player2, gameId); + ticTacToe.makeMove(player1, gameId, 0, 1); + ticTacToe.makeMove(player2, gameId, 0, 0); + ticTacToe.makeMove(player1, gameId, 1, 0); + ticTacToe.makeMove(player2, gameId, 0, 2); + ticTacToe.makeMove(player1, gameId, 1, 2); + ticTacToe.makeMove(player2, gameId, 1, 1); + ticTacToe.makeMove(player1, gameId, 2, 0); + ticTacToe.makeMove(player2, gameId, 2, 1); + ticTacToe.makeMove(player1, gameId, 2, 2); + TicTacToe.Game memory game = ticTacToe.getGame(gameId); + assertEq(uint(game.state), uint(TicTacToe.GameState.Draw)); + } +}