forked from TomBers/Eve-Experiments
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tic-tac-toe.eve
214 lines (158 loc) · 7.84 KB
/
tic-tac-toe.eve
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# Tic-Tac-Toe
## Game logic
Tic-Tac-Toe is a classic game played by two players, "X" and "O", who take turns marking their letter on a 3x3 grid. The first player to mark 3 adjacent cells in a line wins. The game can potentially result in a draw, where all grid cells are marked, but neither player has 3 adjacent cells. To build this game in Eve, we need several parts:
- A game board with cells
- A way to mark a cell as "X" or "O"
- A way to recognize that a player has won the game.
### Game settings
To begin, we initialize the board. We commit an object named @board to hold our global state and create a set of #cells. These #cells will keep track of the moves players have made. Common connect-N games (a generalized tic-tac-toe for any `NxN` grid) are scored along 4 axes (horizontal, vertical, the diagonal, and the anti-diagonal). We group cells together along each axis up front to make scoring easier later.
The game board is square, with a given size. It contains size ^ 2 cells,
each with a row and column index.
~~~
search
// board constants
size = 3
starting-player = "X"
// generate the cells
i = range[from: 1, to: size]
j = range[from: 1, to: size]
commit
board = [#board size player: starting-player]
[#cell board row: i column: j]
~~~
A subtlety here is the last line, [#cell board row: i column: j]. Thanks to our relational semantics, this line actually generates all 9 cells. Since the sets of values computed in i and j have no relation to each other, when we use them together we get the cartesian product of their values. This means that if `i = {0, 1, 2}` and `j = {0, 1, 2}`, then `i x j = {(1, 1), (1, 2), ... (3, 2), (3, 3)}`. These are exactly the indices we need for our grid!
Now we tag some special cell groupings: diagonal and anti-diagonal cells. The diagonal cells are `(1, 1)`, `(2, 2)`, and `(3, 3)`. From this we can see that diagonal cells have a row index equal to its column index
~~~
search
cells = [#cell row column]
row = column
bind
cells += #diagonal
~~~
Similarly, the anti-diagonal cells are `(1, 3)`, `(2, 2)`, and `(3, 1)`.
Anti-diagonal cells satisfy the equation `row + col = N + 1`,
where `N` is the size of the board.
~~~
search
cells = [#cell row column]
[#board size: N]
row + column = N + 1
bind
cells += #anti-diagonal
~~~
### Winning condition
A game is won when a player marks N cells in a row, column, or diagonal.
The game can end in a tie, where no player has N in a row.
~~~
search
board = [#board size: N, not(winner)]
(winner, cell) =
// Check for a winning row
if cell = [#cell row player]
N = count[given: cell, per: (row, player)]
then (player, cell)
// Check for a winning column
else if cell = [#cell column player]
N = count[given: cell, per: (column, player)]
then (player, cell)
// Check for a diagonal win
else if cell = [#diagonal row column player]
N = count[given: cell, per: player]
then (player, cell)
// Check for an anti-diagonal win
else if cell = [#anti-diagonal row column player]
N = count[given: cell, per: player]
then (player, cell)
// If all cells are filled but there are no winners
else if cell = [#cell player]
N * N = count[given: cell]
then ("nobody", cell)
commit
board.winner := winner
cell += #winner
~~~
We use the count aggregate in the above block. Count returns the number of discrete values (the cardinality) of the variables in given. The optional per attribute allows you to specify groupings, which yield one result for each set of values in the group.
For example, in count[given: cell, per: player] we group by player, which returns two values: the count of cells marked by player X and those marked by O. This can be read "count the cells per player". In the scoring block, we group by column and player. This will return the count of cells marked by a player in a particular column. Like wise with the row case. By equating this with N, we ensure the winning player is only returned when she has marked N cells in the given direction.
This is how Eve works without looping. Rather than writing a nested for loop and iterating over the cells, we can use Eve's semantics to our advantage.
We first search every row, then every column. Finally we check the diagonal and anti-diagonal. To do this, we leverage the `#diagonal` and `#anti-diagonal` tags we created earlier; instead of searching for `#cell`, we can search for these new tags to select only a subset of cells.
## React to Events
Next, we handle user input. Any time a cell is directly clicked, we:
- Ensure the cell hasn't already been played
- Check for a winner
- Switch to the next player
Then update the cell to reflect its new owner, and switch board's player to the next player.
### Marking a cell
Click on a cell to make your move
~~~
search @event @session @browser
[#click #direct-target element: [#div cell]]
// Ensures the cell hasn't been played
not(cell.player)
// Ensures the game has not been won
board = [#board player: current, not(winner)]
// Switches to the next player
next_player = if current = "X" then "O"
else "X"
commit
board.player := next_player
cell.player := current
~~~
### Reset the game
Since games of tic-tac-toe are often very short and extremely competitive, it's imperative that it be quick and easy to begin a new match. When the game is over (the board has a winner attribute), a click anywhere on the drawing area will reset the game for another round of play.
A reset consists of:
- Clearing the board of a winner
- Clearing all of the cells
- Removing the #winner tag from the winning cell set
~~~
search @event @browser @session
[#click element: [#div board]]
board = [#board winner]
cell = [#cell player]
commit
board.winner -= winner
cell.player -= player
cell -= #winner
~~~
## Drawing the Game
We've implemented the game logic, but now we need to actually draw the board so players have something to see and interact with. Our general strategy will be that the game board is a #div with one child `#div` for each cell. Each cell will be drawn with an "X", "O", or empty string as text. We also add a `#status` div, which we'll write game state into later. Our cells have the CSS inlined, but you could just as easily link to an external file.
### Draw the board
~~~
search
board = [#board]
cell = [#cell board row column]
contents = if cell.player then cell.player
else ""
bind @browser
[#div board #container style: [font-family: "sans-serif"], children:
[#div #status board class: "status", style: [text-align: "center" width: 150 height: 50, padding-bottom: 10]]
[#div class: "board", style: [color: "black"] children:
[#div class: "row", sort: row, children:
[#div #cell cell class: "cell", text: contents, sort: column,
style: [display: "inline-block", width: 50, height: 50, border: "1px solid black", background: "white", font-size: "2em", line-height: "50px", text-align: "center", vertical-align: "top"]]]]]
~~~
Winning cells are drawn in a different color
~~~
search @session @browser
winning-cells = [#cell #winner]
cell-elements = [#div cell: winning-cells style]
bind @browser
style.color := "blue"
~~~
### Draw status message
Finally, we fill the previously mentioned #status div with our current game state. If no winner has been declared, we remind the competitors of whose turn it is, and once a winner is found we announce her newly-acquired bragging rights.
Display the current player if the game isn't won
~~~
search @session @browser
status = [#status board]
not(board.winner)
bind @browser
status.text += "It's {{board.player}}'s turn!"
~~~
When the game is won, display the winner
~~~
search @session @browser
status = [#status board]
winner = board.winner
bind @browser
status.text += "{{winner}} wins! Click anywhere to restart!"
~~~