Skip to content

Commit

Permalink
TicTacToe game based played just on events
Browse files Browse the repository at this point in the history
  • Loading branch information
hamdiallam committed Oct 3, 2024
1 parent c1ba976 commit ea8442d
Show file tree
Hide file tree
Showing 4 changed files with 641 additions and 1 deletion.
3 changes: 2 additions & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
src = "src"
out = "out"
libs = ["lib"]
evm_version = "cancun"

fs_permissions = [
{ access='read-write', path='../generated' },
Expand All @@ -11,4 +12,4 @@ remappings = [
"@contracts-bedrock/=lib/optimism/packages/contracts-bedrock/src/",
"@solady/=lib/optimism/packages/contracts-bedrock/lib/solady/src/",
"@openzeppelin/contracts=lib/optimism/packages/contracts-bedrock/lib/openzeppelin-contracts/contracts"
]
]
15 changes: 15 additions & 0 deletions contracts/script/tictactoe/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";
import {TicTacToe} from "../../src/tictactoe/TicTacToe.sol";

contract DeployScript is Script {
function setUp() public {}

function run() public {
vm.broadcast();
TicTacToe game = new TicTacToe{ salt: "tictactoe" }();
console.log("Deployed at: ", address(game));
}
}
238 changes: 238 additions & 0 deletions contracts/src/tictactoe/TicTacToe.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import { Predeploys } from "@contracts-bedrock/libraries/Predeploys.sol";
import { ICrossL2Inbox } from "@contracts-bedrock/L2/interfaces/ICrossL2Inbox.sol";

/// @notice Thrown when cross l2 origin is not the TicTacToe contract
error IdOriginNotTicTacToe();

/// @notice Thrown when the reference chain is mismatched
error IdChainMismatch();

/// @notice Thrown when a player tries to play themselves;
error SenderIsOpponent();

/// @notice Thrown when the exepcted event is not NewGame
error DataNotNewGame();

/// @notice Thrown when the exepcted event is not AcceptedGame
error DataNotAcceptedGame();

/// @notice Thrown when the exepcted event is not MovePlayed
error DataNotMovePlayed();

/// @notice Thrown when the caller is not allowed to act
error SenderNotPlayer();

/// @notice Thrown when trying to start a game on the wrong chain
error GameChainMismatch();

/// @notice Thrown when the game has already been started
error GameStarted();

/// @notice Thrown when a game does not exist
error GameNotExists();

/// @notice Thrown when the player makes an invalid move
error MoveInvalid();

/// @notice Thrown when the consumed event is not forward progressing the game.
error MoveNotForwardProgressing();

/// @notice Thrown when the player make a move that's already been played
error MoveTaken();

/// @title TicTacToe
/// @notice TicTacToe is a Superchain interoprable implementation of TicTacToe where players
/// can play each other from any two chains in each others interopable dependency sets
/// without needing to pass messages between themselves. Since a chain is be default in
/// its own dependency set, players on the same chain can also play each other :)
contract TicTacToe {
uint256 public nextGameId;

/// @notice 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 = 15;

/// @notice Structure for a local view of a game
struct Game {
address player;
address opponent;

// `1` for the player's moves, `2` opposing.
uint8[3][3] moves;
uint8 movesLeft;

ICrossL2Inbox.Identifier lastId;
}

/// @notice A game is identifed from the (chainId, gameId) tuple from the chain it was initiated on
/// Since players on the same chain can play each other, we need to subspace by address as well.
mapping(uint256 => mapping(uint256 => mapping(address => Game))) games;

/// @notice Emitted when broadcasting a new game invitation. Anyone is allowed to accept
event NewGame(uint256 chainId, uint256 gameId, address player);

/// @notice Emitted when a player accepts an opponent's game
event AcceptedGame(uint256 chainId, uint256 gameId, address opponent, address player);

/// @notice Emitted when a player makes a move in a game
event MovePlayed(uint256 chainId, uint256 gameId, address player, uint8 _x, uint8 _y);

/// @notice Emitted when a player has won the game with their latest move
event GameWon(uint256 chainId, uint256 gameId, address winner, uint8 _x, uint8 _y);

/// @notice Emitted when all spots on the board were played with no winner with their lastest move
event GameDraw(uint256 chainId, uint256 gameId, uint8 _x, uint8 _y);

/// @notice Creates a new game that any player can accept
function newGame() external {
emit NewGame(block.chainid, nextGameId, msg.sender);
nextGameId++;
}

/// @notice Send out an acceptance event for a new game
function acceptGame(ICrossL2Inbox.Identifier calldata _newGameId, bytes calldata _newGameData) external {
// Validate Cross Chain Log
if (_newGameId.origin != address(this)) revert IdOriginNotTicTacToe();
ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_newGameId, keccak256(_newGameData));

// Decode `NewGame` Event
bytes32 selector = abi.decode(_newGameData[:32], (bytes32));
if (selector != NewGame.selector) revert DataNotNewGame();

(uint256 chainId, uint256 gameId, address opponent) = abi.decode(_newGameData[32:], (uint256, uint256, address));
if (opponent == msg.sender) revert SenderIsOpponent();

// Record Game Metadata (no moves)
Game storage game = games[chainId][gameId][msg.sender];
game.player = msg.sender;
game.opponent = opponent;
game.lastId = _newGameId;
game.movesLeft = 9;

emit AcceptedGame(chainId, gameId, game.opponent, game.player);
}

/// @notice Start a game accepted by an opponent with a starting move
function startGame(ICrossL2Inbox.Identifier calldata _acceptedGameId, bytes calldata _acceptedGameData, uint8 _x, uint8 _y) external {
// Validate Cross Chain Log
if (_acceptedGameId.origin != address(this)) revert IdOriginNotTicTacToe();
ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_acceptedGameId, keccak256(_acceptedGameData));

// Decode `AcceptedGame` event
bytes32 selector = abi.decode(_acceptedGameData[:32], (bytes32));
if (selector != AcceptedGame.selector) revert DataNotAcceptedGame();

(uint256 chainId, uint256 gameId, address player, address opponent) = // player, opponent swapped in local view
abi.decode(_acceptedGameData[32:], (uint256,uint256,address,address));

// The accepted game was started from this chain, from the sender
if (chainId != block.chainid) revert GameChainMismatch();
if (msg.sender != player) revert SenderNotPlayer();

// Game has not already been started with an opponent.
Game storage game = games[chainId][gameId][msg.sender];
if (game.opponent != address(0)) revert GameStarted();

// Record Game Metadata
game.player = msg.sender;
game.opponent = opponent;
game.lastId = _acceptedGameId;
game.movesLeft = 9;

// Make the first move (any spot on the board)
if (_x >= 3 || _y >= 3) revert MoveInvalid();
game.moves[_x][_y] = 1;
game.movesLeft--;
emit MovePlayed(chainId, gameId, game.player, _x, _y);
}

/// @notice Make a move for a game.
function makeMove(ICrossL2Inbox.Identifier calldata _movePlayedId, bytes calldata _movePlayedData, uint8 _x, uint8 _y) external {
// Validate Cross Chain Log
if (_movePlayedId.origin != address(this)) revert IdOriginNotTicTacToe();
ICrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_movePlayedId, keccak256(_movePlayedData));

// Decode `MovePlayed` event
bytes32 selector = abi.decode(_movePlayedData[:32], (bytes32));
if (selector != MovePlayed.selector) revert DataNotMovePlayed();

(uint256 chainId, uint256 gameId,, uint8 oppX, uint8 oppY) =
abi.decode(_movePlayedData[32:], (uint256,uint256,address,uint8,uint8));

// Game was instantiated for this player
Game storage game = games[chainId][gameId][msg.sender];
if (game.player != msg.sender) revert GameNotExists();

// The move played is forward progressing from the same chain
if (_movePlayedId.chainId != game.lastId.chainId) revert IdChainMismatch();
if (_movePlayedId.blockNumber <= game.lastId.blockNumber) revert MoveNotForwardProgressing();
game.lastId = _movePlayedId;

// NOTE: Since the supplied move is valid, `movesLeft > 0` as the code below will emit
// `GameDrawn` when there are no moves left to play and `GameWon` on the winning move

// Mark the opponents move
game.moves[oppX][oppY] = 2;
game.movesLeft--;

// Make a move and mark the latest seen opposing move.
if (_x >= 3 || _y >= 3) revert MoveInvalid();
if (game.moves[_x][_y] != 0) revert MoveTaken();
game.moves[_x][_y] = 1;
game.movesLeft--;

if (_isGameWon(game)) {
emit GameWon(chainId, gameId, game.player, _x, _y);
}
else if (game.movesLeft == 0) {
emit GameDraw(chainId, gameId, _x, _y);
}
else {
emit MovePlayed(chainId, gameId, game.player, _x, _y);
}
}

function gameState(uint256 chainId, uint256 gameId, address player) public view returns (Game memory) {
return games[chainId][gameId][player];
}

/// @notice helper to check if a game has been won for the game's local player; moves == 1
function _isGameWon(Game memory _game) internal view returns (bool) {
// Check for a row/col win
for (uint8 i = 0; i < 3; i++) {
uint8 rowSum =
(_game.moves[i][0] * MAGIC_SQUARE[i][0]) +
(_game.moves[i][1] * MAGIC_SQUARE[i][1]) +
(_game.moves[i][2] * MAGIC_SQUARE[i][2]);

if (rowSum == MAGIC_SUM) return true;

uint8 colSum =
(_game.moves[0][i] * MAGIC_SQUARE[0][i]) +
(_game.moves[1][i] * MAGIC_SQUARE[1][i]) +
(_game.moves[2][i] * MAGIC_SQUARE[2][i]);

if (colSum == MAGIC_SUM) return true;
}

// Check for a diag win
uint8 leftToRightDiagSum =
(_game.moves[0][0] * MAGIC_SQUARE[0][0]) +
(_game.moves[1][1] * MAGIC_SQUARE[1][1]) +
(_game.moves[2][2] * MAGIC_SQUARE[2][2]);

if (leftToRightDiagSum == MAGIC_SUM) return true;

uint8 rightToLeftDiagSum =
(_game.moves[0][2] * MAGIC_SQUARE[0][2]) +
(_game.moves[1][1] * MAGIC_SQUARE[1][1]) +
(_game.moves[2][0] * MAGIC_SQUARE[2][0]);

if (rightToLeftDiagSum == MAGIC_SUM) return true;

return false;
}
}
Loading

0 comments on commit ea8442d

Please sign in to comment.