/
core.cljs
427 lines (308 loc) · 12.9 KB
/
core.cljs
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
(ns tictactoe-reagent.core
(:require [reagent.core :as reagent :refer [atom]]))
(enable-console-print!)
;; (devtools.core/set-pref! :dont-detect-custom-formatters true)
;; Testing output to the web developer console in the browser
(println "This text is printed from src/tictactoe-reagent/core.cljs. Go ahead and edit it and see reloading in action.")
;; Hard coded game board size
;; As the game board is square, we only use one value for height and width
(def board-dimension 3)
(defn game-board
"Create a data structure to represent the values of cells in the game board.
A vector is used to hold the overall game board
Each nested vector represents a line of the game board.
Dimension specifies the size of the game board."
[dimension]
(vec (repeat dimension (vec (repeat dimension :empty)))))
;; define your app data so that it doesn't get over-written on reload
(defonce app-state (atom {:text "Lets Play TicTacToe"
:board (game-board board-dimension)}))
(defn computer-move
"Takes a turn for the computer, adding an X shape to the board"
[]
(let [available-cells
(for [row (range board-dimension)
column (range board-dimension)
:when (=
:empty
(get-in (@app-state :board) [column row]))]
[column row])
next-move (when (seq available-cells)
(rand-nth available-cells))]
(if next-move
(do
(prn "Computer move at:" next-move)
(swap! app-state assoc-in [:board (first next-move) (second next-move)] :cross))
(prn "Computer move: no more moves available"))))
(defn cell-empty
"Generate a cell that has not yet been clicked on"
[x-cell y-cell]
^{:key (str x-cell y-cell)}
[:rect {:width 0.9
:height 0.9
:fill "grey"
:x x-cell
:y y-cell
:on-click
(fn rectangle-click [e]
(prn "Human player moved:" x-cell y-cell "was clicked!")
(swap! app-state assoc-in [:board y-cell x-cell] :nought)
(computer-move))}])
(defn cell-nought
"A cell with a nought inside it"
[x-cell y-cell]
^{:key (str x-cell y-cell)}
[:circle {:r 0.36
:fill "white"
:stroke "green"
:stroke-width 0.1
:cx (+ 0.42 x-cell)
:cy (+ 0.42 y-cell)}])
(defn cell-cross
"A cell with a cross inside it"
[x-cell y-cell]
^{:key (str x-cell y-cell)}
[:g {:stroke "purple"
:stroke-width 0.4
:stroke-linecap "round"
:transform
(str "translate(" (+ 0.42 x-cell) "," (+ 0.42 y-cell) ") "
"scale(0.3)")}
[:line {:x1 -1 :y1 -1 :x2 1 :y2 1}]
[:line {:x1 1 :y1 -1 :x2 -1 :y2 1}]])
(defn tictactoe-game []
[:div
[:div
[:h1 (:text @app-state)]
[:p "Do you want to play a game?"]]
[:button {:on-click (fn new-game-click [e]
(swap! app-state assoc :board (game-board board-dimension)))}
"Start a new game"]
[:center
[:svg {:view-box "0 0 3 3"
:width 500
:height 500}
(for [x-cell (range (count (:board @app-state)))
y-cell (range (count (:board @app-state)))]
(case (get-in @app-state [:board y-cell x-cell])
:empty [cell-empty x-cell y-cell]
:cross [cell-cross x-cell y-cell]
:nought [cell-nought x-cell y-cell]))]]])
(reagent/render-component [tictactoe-game]
(. js/document (getElementById "app")))
(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
(prn "Game board state:" (@app-state :board)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; REPL Experiments
;;;;;;;;;;;;;;;;;;;;;;;;
;; Generating a data structure to represet the game board
;; We could just hard code the board as follows, although that limits us to a specific size of board:
#_[[:empty :empty :empty]
[:empty :empty :empty]
[:empty :empty :empty]]
;; To create a row is simple to do using the repeat function to generate 3 :empty keywords and return them as a list
#_(repeat 3 :empty)
;; => (:empty :empty :empty)
;; To make this a vector we can just wrap that in a vec function
#_(vec (repeat 3 :empty))
;; => [:empty :empty :empty]
;; To create three rows we just repeat the code above 3 times
#_(vec (repeat 3 (vec (repeat 3 :empty))))
;; => [[:empty :empty :empty] [:empty :empty :empty] [:empty :empty :empty]]
;; we can use the above code in a function and replace 3 with a local name that takes the value of the argument passed in
;; so lets write a game-board function.
#_(println (game-board 3))
;;;;;;;;;;;;;;;;;;;;;;;;
;; Iterate over board data structure
;; Retrieve the app state by defererencing the name app-state, (dref app-state) or @app-state
#_@app-state
#_(count (:board @app-state))
#_(range 3)
;;;;;;;;;;;;;;;;;;;;;;;;
;; Redering shapes with SVG
#_[:svg
:circle {:r 30}]
;; Warning in browser
;; Every element in a sequence should have a unique key
#_([:rect {:width 0.9, :height 0.9, :fill "purple", :x 0, :y 0}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 0, :y 1}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 0, :y 2}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 1, :y 0}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 1, :y 1}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 1, :y 2}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 2, :y 0}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 2, :y 1}]
[:rect {:width 0.9, :height 0.9, :fill "purple", :x 2, :y 2}])
;; To fix this issue, add a piece of metadata to each rectangle definition
;; ^{:key (str x-cell y-cell)} ; generate a unique metadata :key for each rectangle, ie. 00, 01, 02, etc
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Testing the Game Board
;; As we drive the board by changes in the app-state, we can test by simply updating the app-state directly
;; To stop the REPL from running all these app-state updates, we comment them out with the reader macro #_ as it will ignore the next expression (so we dont need to comment out each line)
;; Nought winner - center column
#_(swap! app-state assoc :board
[[:cross :nought :empty]
[:empty :nought :empty]
[:cross :nought :empty]])
#_(swap! app-state assoc :board
[[:cross :nought :nought]
[:empty :cross :empty]
[:cross :nought :cross]])
;; Reset board by setting all cell values back to :empty
#_(reset! app-state {:text "Lets Play TicTacToe"
:board (game-board board-dimension)})
(defn set-game-board! [game-board-state]
(swap! app-state assoc :board game-board-state))
;; A reset function is a nice helper function for development
;; To reset the game board simply call this function any time
(defn reset-game-board!
"Resets the app-state to an empty game board"
[]
(reset! app-state {:text "Lets Play TicTacToe"
:board (game-board board-dimension)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Determine winner by pattern matching
;; As there are only 8 winning combinations, we could just create a pattern for each win
;; As the patterns are the same for :nought and :cross we could just compare that each value is equal
;; For just a row we can compare the values in the vector with each other
#_(apply = [:empty :empty :empty])
#_(apply = [:cross :cross :cross])
#_(apply = [:nought :nought :nought])
#_(apply = [:empty :cross :empty])
;; => false
#_(apply = [:cross :nought :empty])
;; However, this approach also matches an :empty row or column
;; Using an anonymous function over the collection allows us to compare each value,
;; if all values are not= :empty then we can return true
#_(apply (fn [cell-value] (not= :empty cell-value))[:empty :cross :cross])
;; As we will probably call this multiple times, lets convert it into a named function.
(defn cell-empty?
[cell-value] (not= :empty cell-value))
;; Hmm, still not idea, as any combination that does not contain :empty will return true
#_(cell-empty? [[:cross :nought :cross]])
;; Applying both checks will give the right results
(defn winning-line? [cell-row]
(and
(apply = cell-row)
(apply cell-empty? cell-row)))
#_(winning-line? [:cross :cross :cross])
;; => true
#_(winning-line? [:nought :nought :nought])
;; => true
#_(winning-line? [:cross :nought :cross])
;; => false
#_(winning-line? [:empty :empty :empty])
;; => false
#_(winning-line? [:empty :nought :cross])
;; => false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Pattern matching diagonals
#_(= [:cross :cross :cross] [:cross :cross :cross])
(def board-of-crosses [[:cross :cross :cross]
[:cross :cross :cross]
[:cross :cross :cross]])
(def board-of-stalemate [[:cross :cross :nought]
[:nought :nought :cross]
[:cross :cross :nought]])
#_(= [[:cross :cross :cross] [:cross :cross :cross] [:cross :cross :cross]]
[[:cross :cross :cross] [:cross :cross :cross] [:cross :cross :cross]])
;; => true
#_(= [[:cross :cross :cross] [:cross :cross :cross] [:cross :cross :cross]]
[[:cross :cross :cross] [:cross :cross :cross] [:cross :cross :nought]])
#_(=
[[:cross :cross :cross] [:cross :cross :cross] [:cross :cross :cross]]
[[:cross _ _] [_ :cross _] [_ _ :cross]])
;; Using destructuring we can just look at the values we are interested in
;; The let destructuring pattern pulls out the values for the diagonal line,
;; from top left to bottom right
(let [[[cell-00 _ _][_ cell-11 _][_ _ cell-22]] board-of-crosses]
(= cell-00 cell-11 cell-22))
;; => true
(let [[[cell-00 _ _][_ cell-11 _][_ _ cell-22]] board-of-stalemate]
(println cell-00 ":" cell-11 ":" cell-22)
(= cell-00 cell-11 cell-22))
;; => false
;; Not sure naming these patterns with a def is that useful
;; Not this way, as its not correct clojure (names are not defined)
;; Should create two functions instead
#_(def diagonal-row--top-left-to-bottom-right
[[cell-00 _ _]
[_ cell-11 _]
[_ _ cell-22]])
#_(def diagonal-row--top-right-to-bottom-left
[[_ _ cell-02]
[_ cell-11 _]
[cell-20_ _]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Computer moves
;; A sample game board
(def game-board-example-mid-game [[:cross :nought :empty]
[:empty :nought :empty]
[:cross :nought :empty]])
;; Is a cell empty?
(= :empty (get-in game-board-example-mid-game [1 0]))
;; => true
(= :empty (get-in game-board-example-mid-game [0 2]))
;; => true
;; lets get the co-ordinates for all the cells on the board that are empty,
;; so we know which cells are available
#_(for [row (range board-dimension)
column (range board-dimension)
:when (= :empty
(get-in game-board-example-mid-game [row column]))]
[row column])
;; => ([0 2] [1 0] [1 2] [2 2])
;; the same approach, but assigning it to a local name with let,
;; so we can do more with the result
#_(let [available-cells (for [row (range 3)
column (range 3)
:when (= :empty
(get-in game-board-example-mid-game [row column]))]
[row column])]
available-cells)
;; => ([0 2] [1 0] [1 2] [2 2])
#_(let [available-cells
(for [row (range board-dimension)
column (range board-dimension)
:when (=
(get-in game-board-example-mid-game [column row])
:empty)]
[column row])]
available-cells)
;; => ([1 0] [0 2] [1 2] [2 2])
;; Check to see if we got any empty cells
;; We can check with empty?
#_(empty? '([0 2] [1 0] [1 2] [2 2]))
;; and therefore check if there are positions with
#_(not (empty? '([0 2] [1 0] [1 2] [2 2])))
;; However there is a Clojure idiom to use seq (mentioned in the docs for empty?)
#_(seq '([0 2] [1 0] [1 2] [2 2]))
;; => ([0 2] [1 0] [1 2] [2 2])
#_(seq '())
;; => nil
;; So with seq if there are empty cells, those values are returned, otherwise `nil` is returned (which is falsey)
;; To get one of the co-ordinates assuming there are available cells, we can use rand-nth
#_(rand-nth '([0 2] [1 0] [1 2] [2 2]))
;; => [0 2]
;; putting seq and rand-nth together we can get one of the available positions
(def available-cells-example '([1 0] [0 2] [1 2] [2 2]))
#_(when (seq available-cells-example)
(rand-nth available-cells-example))
;; => [1 2]
;; The computer can now make moves until no more cells are available
#_(let [available-cells
(for [row (range board-dimension)
column (range board-dimension)
:when (=
:empty
(get-in game-board-example-mid-game [column row]))]
[column row])
next-move (when (seq available-cells)
(rand-nth available-cells))]
(if next-move
(str "update app-state")
(str "display messages saying no more moves")))
;; => "update app-state"