-
-
Notifications
You must be signed in to change notification settings - Fork 8
/
ConnectFour.sol
142 lines (113 loc) · 4.85 KB
/
ConnectFour.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
/// @title Connect Four
/// @author Miguel Piedrafita
/// @notice An optimised connect four game implementation on Solidity
contract ConnectFour {
/// ERRORS ///
/// @notice Thrown when trying to make an invalid move
error InvalidMove();
/// @notice Thrown when trying to make a move during someone else's turn
error Unauthorized();
/// @notice Thrown when trying to make a move after the game has ended
error GameFinished();
/// EVENTS ///
/// @notice Emited when a new game is created
/// @param challenger The address that created the game
/// @param challenged The address proposed as a rival, and that should make the first move
event GameProposed(address indexed challenger, address indexed challenged);
/// @notice Emitted when a move is made
/// @param mover The address that performed the move
/// @param gameId The ID of the game in play
/// @param row The row the user dropped its piece in
event MovePerformed(address indexed mover, uint256 gameId, uint8 row);
/// @notice Emitted when a game is won
/// @param winner The address that won the game
/// @param gameId The ID of the game that was won
event GameWon(address indexed winner, uint256 gameId);
/// @dev Parameters for games
/// @param player1 The address of the first player
/// @param player2 The address of the second player
/// @param height A helper, used to track which position to assign to pieces for each row
/// @param board Two bitboards (one for each player), each representing a 7x6 board with an extra column at the top to check for invalid moves.
/// @param moves A counter of the amount of moves so far
/// @param finished Wether the game has been won
struct Game {
address player1;
address player2;
uint64[7] height;
uint64[2] board;
uint8 moves;
bool finished;
}
/// @notice The initial value of `Game.height`, representing the indexes of the bottom column of the 7x6(+1) board
/// @dev Solidity doesn't support array immutable variables or constants yet, so we're forced to compute this at runtime (see constructor).
uint64[7] internal initialHeight;
/// @notice The indexes of the helper top column of the 7x6(+1) board
uint64 internal constant topColumn = 283691315109952;
/// @notice Used as a counter for the next game index.
/// @dev Initialised at 1 because it makes the first transaction slightly cheaper.
uint256 internal gameId = 1;
/// @notice An indexed list of games
/// @dev This automatically generates a getter for us, which will return `Game.player1`, `Game.player2`, `Game.moves`, and `Game.finished` (the arrays are skipped)
mapping(uint256 => Game) public getGame;
/// @notice Deploys a ConnectFour instance
/// @dev Used to compute the value of `initialHeight`, since we cannot make it a constant (or immutable).
constructor() payable {
unchecked {
for (uint8 i = 0; i < 7; i++) {
initialHeight[i] = uint64(7 * i);
}
}
}
/// @notice Challenge another address to a game of connect four
/// @param opponent The address you want to play against
/// @return The ID of the newly-created game
function challenge(address opponent) public payable returns (uint256) {
Game memory game = Game({
player1: opponent,
player2: msg.sender,
height: initialHeight,
board: [uint64(0), uint64(0)],
moves: 0,
finished: false
});
emit GameProposed(msg.sender, opponent);
getGame[gameId] = game;
return gameId++;
}
/// @notice Perform a move on an active game
/// @param gameId The ID of the game you want to perform your move on
/// @param row The row on where you want to drop your piece
function makeMove(uint256 gameId, uint8 row) public payable {
Game storage game = getGame[gameId];
if (msg.sender != (game.moves & 1 == 0 ? game.player1 : game.player2)) revert Unauthorized();
if (game.finished) revert GameFinished();
emit MovePerformed(msg.sender, gameId, row);
game.board[game.moves & 1] ^= uint64(1) << game.height[row]++;
if ((game.board[game.moves & 1] & topColumn) != 0) revert InvalidMove();
if (didPlayerWin(gameId, game.moves++ & 1)) {
game.finished = true;
emit GameWon(msg.sender, gameId);
}
}
/// @notice Check wether one of the players for a certain game has won the match
/// @param gameId The ID for the game you want to perform the check on
/// @param side Which side of the board you want to check (0 or 1).
function didPlayerWin(uint256 gameId, uint8 side) public view returns (bool) {
uint64 board = getGame[gameId].board[side];
uint8[4] memory directions = [1, 7, 6, 8];
uint64 bb;
unchecked {
for (uint8 i = 0; i < 4; i++) {
bb = board & (board >> directions[i]);
if ((bb & (bb >> (directions[i] << 1))) != 0) return true;
}
}
return false;
}
function getBoards(uint256 gameId) public view returns (uint64, uint64) {
uint64[2] memory boards = getGame[gameId].board;
return (boards[0], boards[1]);
}
}