Finishing the basic game

Here's the final production code for our basic game:

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.10;

contract TicTacToken {
    uint256[9] public board;
    
    uint256 internal constant EMPTY = 0;
    uint256 internal constant X = 1;
    uint256 internal constant O = 2;
    uint256 internal _turns;

    function getBoard() public view returns (uint256[9] memory) {
        return board;
    }

    function markSpace(uint256 space, uint256 symbol) public {
        require(_validSymbol(symbol), "Invalid symbol");
        require(_validTurn(symbol), "Not your turn");
        require(_emptySpace(space), "Already marked");
        board[space] = symbol;
        _turns++;
    }

    function currentTurn() public view returns (uint256) {
        return (_turns % 2 == 0) ? X : O;
    }

    function winner() public view returns (uint256) {
        uint256[8] memory wins = [
            _row(0),
            _row(1),
            _row(2),
            _col(0),
            _col(1),
            _col(2),
            _diag(),
            _antiDiag()
        ];
        for (uint256 i; i < wins.length; i++) {
            uint256 win = _checkWin(wins[i]);
            if (win == X || win == O) return win;
        } 
        return 0;
    }

    function _checkWin(uint256 product) internal pure returns (uint256) {
        if (product == 1) {
            return X;
        }
        if (product == 8) {
            return O;
        }
        return 0;
    }

    function _row(uint256 row) internal view returns (uint256) {
        require(row < 3, "Invalid row");
        uint256 idx = 3 * row;
        return board[idx] * board[idx+1] * board[idx+2];
    }

    function _col(uint256 col) internal view returns (uint256) {
        require(col < 3, "Invalid column");
        return board[col] * board[col+3] * board[col+6];
    }

    function _diag() internal view returns (uint256) {
        return board[0] * board[4] * board[8];
    }
    
    function _antiDiag() internal view returns (uint256) {
        return board[2] * board[4] * board[6];
    }


    function _validTurn(uint256 symbol) internal view returns (bool) {
        return symbol == currentTurn();
    }

    function _emptySpace(uint256 i) internal view returns (bool) {
        return board[i] == EMPTY;
    }

    function _validSymbol(uint256 symbol) internal pure returns (bool) {
        return symbol == X || symbol == O;
    }
}

And the full code for our unit tests:

// SPDX-License-Identifier: Apache-2.0
pragma solidity 0.8.10;

import "ds-test/test.sol";
import "forge-std/Vm.sol";

import "../TicTacToken.sol";

contract TicTacTokenTest is DSTest {
  Vm internal vm = Vm(HEVM_ADDRESS);
  TicTacToken internal ttt;

  uint256 internal constant EMPTY = 0;
  uint256 internal constant X = 1;
  uint256 internal constant O = 2;

  function setUp() public {
    ttt = new TicTacToken();
  }

  function test_has_empty_board() public {
    for (uint256 i = 0; i < 9; i++) {
      assertEq(ttt.board(i), EMPTY);
    }
  }

  function test_get_board() public {
    uint256[9] memory expected = [
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY,
      EMPTY
    ];
    uint256[9] memory actual = ttt.getBoard();

    for (uint256 i = 0; i < 9; i++) {
      assertEq(actual[i], expected[i]);
    }
  }

  function test_can_mark_space_with_X() public {
    ttt.markSpace(0, X);
    assertEq(ttt.board(0), X);
  }

  function test_can_mark_space_with_O() public {
    ttt.markSpace(0, X);
    ttt.markSpace(1, O);
    assertEq(ttt.board(1), O);
  }

  function test_cannot_mark_space_with_Z() public {
    vm.expectRevert("Invalid symbol");
    ttt.markSpace(0, 3);
  }

  function test_cannot_overwrite_marked_space() public {
    ttt.markSpace(0, X);

    vm.expectRevert("Already marked");
    ttt.markSpace(0, O);
  }

  function test_symbols_must_alternate() public {
    ttt.markSpace(0, X);
    vm.expectRevert("Not your turn");
    ttt.markSpace(1, X);
  }

  function test_tracks_current_turn() public {
    assertEq(ttt.currentTurn(), X);
    ttt.markSpace(0, X);
    assertEq(ttt.currentTurn(), O);
    ttt.markSpace(1, O);
    assertEq(ttt.currentTurn(), X);
  }

  function test_checks_for_horizontal_win() public {
    ttt.markSpace(0, X);
    ttt.markSpace(3, O);
    ttt.markSpace(1, X);
    ttt.markSpace(4, O);
    ttt.markSpace(2, X);
    assertEq(ttt.winner(), X);
  }
  
  function test_checks_for_horizontal_win_row2() public {
    ttt.markSpace(3, X);
    ttt.markSpace(0, O);
    ttt.markSpace(4, X);
    ttt.markSpace(1, O);
    ttt.markSpace(5, X);
    assertEq(ttt.winner(), X);
  }
  
  function test_checks_for_vertical_win() public {
    ttt.markSpace(1, X);
    ttt.markSpace(0, O);
    ttt.markSpace(2, X);
    ttt.markSpace(3, O);
    ttt.markSpace(4, X);
    ttt.markSpace(6, O);
    assertEq(ttt.winner(), O);
  }
  
  function test_checks_for_diagonal_win() public {
    ttt.markSpace(0, X);
    ttt.markSpace(1, O);
    ttt.markSpace(4, X);
    ttt.markSpace(5, O);
    ttt.markSpace(8, X);
    assertEq(ttt.winner(), X);
  }
  
  function test_checks_for_antidiagonal_win() public {
    ttt.markSpace(1, X);
    ttt.markSpace(2, O);
    ttt.markSpace(3, X);
    ttt.markSpace(4, O);
    ttt.markSpace(5, X);
    ttt.markSpace(6, O);
    assertEq(ttt.winner(), O);
  }
  
  function test_draw_returns_no_winner() public {
    ttt.markSpace(4, X);
    ttt.markSpace(0, O);
    ttt.markSpace(1, X);
    ttt.markSpace(7, O);
    ttt.markSpace(2, X);
    ttt.markSpace(6, O);
    ttt.markSpace(8, X);
    ttt.markSpace(5, O);
    assertEq(ttt.winner(), 0);
  }
  
  function test_empty_board_returns_no_winner() public {
    assertEq(ttt.winner(), 0);
  }
  
  function test_game_in_progress_returns_no_winner() public {
    ttt.markSpace(1, X);
    assertEq(ttt.winner(), 0);
  }
}

This is a great start on our core game logic, but we're missing a few important features. At the moment, anyone interacting with our contract can mark the board, as long as they're making a valid move! In the next chapter, we'll explore this permissionless paradigm in more detail, and look at a few patterns for restricting access to specific user addresses.