Checking for wins
To wrap up our basic board, let's add the ability to check for a winner. Let's start with a test for a horizontal win:
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);
}
The tests guide us to our next step: we'll need to add a public winner
function.
$ forge test
[⠊] Compiling...
[⠒] Compiling 1 files with 0.8.10
[⠢] Solc finished in 10.24ms
Error:
0: Compiler run failed
TypeError: Member "winner" not found or not visible after argument-dependent lookup in contract TicTacToken.
--> /Users/ecm/Projects/ttt-book-code/src/test/TicTacToken.t.sol:89:14:
|
89 | assertEq(ttt.winner(), X);
| ^^^^^^^^^^
There are many ways to check for a winner. As we did with currentTurn
, let's not worry just yet about the most efficient one. (We'll get to that later, when we learn about gas optimization). For now, let's add a helper to get each row, a helper to check if a row has a winner, and a loop to iterate over each row.
We can use some arithmetic to check if a row contains a win by multiplying its three values. If a row is all X
, its product will be 1 * 1 * 1 = 1
. If it's all O
, it will be 2 * 2 * 2 = 8
.
Here's a helper to get the product for a row:
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];
}
And one to check if the product is a win:
function _checkWin(uint256 product) internal pure returns (uint256) {
if (product == 1) {
return X;
}
if (product == 8) {
return O;
}
return 0;
}
To check for a winner, let's iterate over every combination.
function winner() public view returns (uint256) {
uint256[3] memory wins = [
_row(0),
_row(1),
_row(2)
];
for (uint256 i; i < wins.length; i++) {
uint256 win = _checkWin(wins[i]);
if (win == X || win == O) return win;
}
return 0;
}
Not super elegant, but it works:
Running 9 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74806)
[PASS] test_can_mark_space_with_X() (gas: 51096)
[PASS] test_cannot_mark_space_with_Z() (gas: 10709)
[PASS] test_cannot_overwrite_marked_space() (gas: 56634)
[PASS] test_checks_for_horizontal_win() (gas: 148184)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160329)
[PASS] test_get_board() (gas: 29128)
[PASS] test_has_empty_board() (gas: 31965)
[PASS] test_symbols_must_alternate() (gas: 56537)
[PASS] test_tracks_current_turn() (gas: 76825)
Test result: ok. 9 passed; 0 failed; finished in 1.24ms
While we're at it, let's add columns:
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);
}
We can add another helper and add the columns to our array of winning combinations:
function winner() public view returns (uint256) {
uint256[6] memory wins = [
_row(0),
_row(1),
_row(2),
_col(0),
_col(1),
_col(2)
];
for (uint256 i; i < wins.length; i++) {
uint256 win = _checkWin(wins[i]);
if (win == X || win == O) return win;
}
return 0;
}
function _col(uint256 col) internal view returns (uint256) {
require(col < 3, "Invalid column");
return board[col] * board[col+3] * board[col+6];
}
Finally, we can add the diagonals:
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 _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];
}
Finally, let's cover our edge cases and make sure games in progress and draws return no winner:
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);
}
Everything looks good!
Running 15 tests for src/test/TicTacToken.t.sol:TicTacTokenTest
[PASS] test_can_mark_space_with_O() (gas: 74788)
[PASS] test_can_mark_space_with_X() (gas: 51123)
[PASS] test_cannot_mark_space_with_Z() (gas: 10687)
[PASS] test_cannot_overwrite_marked_space() (gas: 56749)
[PASS] test_checks_for_antidiagonal_win() (gas: 183587)
[PASS] test_checks_for_diagonal_win() (gas: 161625)
[PASS] test_checks_for_horizontal_win() (gas: 159719)
[PASS] test_checks_for_horizontal_win_row2() (gas: 160329)
[PASS] test_checks_for_vertical_win() (gas: 182374)
[PASS] test_draw_returns_no_winner() (gas: 226962)
[PASS] test_empty_board_returns_no_winner() (gas: 31997)
[PASS] test_game_in_progress_returns_no_winner() (gas: 75509)
[PASS] test_get_board() (gas: 29151)
[PASS] test_has_empty_board() (gas: 31988)
[PASS] test_symbols_must_alternate() (gas: 56519)
[PASS] test_tracks_current_turn() (gas: 76815)
Test result: ok. 15 passed; 0 failed; finished in 1.59ms