play games with/against agents
CI / Test + coverage (push) Successful in 51s

This commit is contained in:
2026-09-19 15:48:06 -09:00
parent a76f326ae3
commit 7f0e58240d
8 changed files with 300 additions and 8 deletions
+72
View File
@@ -0,0 +1,72 @@
(ns tbd.ui.tictactoe-test
(:require [cljs.test :refer [deftest is testing]]
[re-frame.core :as rf]
[re-frame.db :as rf-db]
[tbd.ui.events :as events]
[tbd.ui.tictactoe :as ttt]))
(defn- board
"A board from a string of 9 characters: x, o or anything else for an empty cell."
[s]
(mapv {\x :x \o :o} s))
(deftest winner-test
(testing "every row, column and diagonal"
(doseq [line [[0 1 2] [3 4 5] [6 7 8] [0 3 6] [1 4 7] [2 5 8] [0 4 8] [2 4 6]]
player [:x :o]]
(let [b (reduce #(assoc %1 %2 player) ttt/new-board line)]
(is (= {:player player :line line} (ttt/winner b))))))
(testing "no winner"
(is (nil? (ttt/winner ttt/new-board)))
(is (nil? (ttt/winner (board "xo.ox.xo."))))
(is (nil? (ttt/winner (board "xxo.oxxox"))))))
(deftest draw-test
(is (ttt/draw? (board "xoxxoooxx")))
(is (not (ttt/draw? (board "xxxooxoxo"))) "a full board with a winner is not a draw")
(is (not (ttt/draw? (board "xoxxooox.")))))
(deftest outcome-test
(is (nil? (ttt/outcome ttt/new-board)))
(is (= :draw (ttt/outcome (board "xoxxoooxx"))))
(is (= {:player :o :line [0 4 8]} (ttt/outcome (board "oxxxoxxxo")))))
(deftest move-test
(is (= :x (get (ttt/move ttt/new-board :x 4) 4)))
(is (nil? (ttt/move (board "x........") :o 0)) "taken cell")
(is (nil? (ttt/move ttt/new-board :x 9)) "off the board")
(is (nil? (ttt/move ttt/new-board :x -1)) "off the board")
(is (nil? (ttt/move (board "xxxoo....") :o 8)) "game already won"))
(defn- move! [& cells]
(doseq [i cells] (rf/dispatch-sync [::events/ttt-move i])))
(deftest game-flow-test
(rf/dispatch-sync [::events/initialize])
(let [game #(let [{:keys [board turn]} (:ttt @rf-db/app-db)]
{:board board :turn turn :outcome (ttt/outcome board)})]
(testing "a new game"
(is (= :x (:turn (game))))
(is (nil? (:outcome (game)))))
(testing "players alternate"
(move! 0)
(is (= :o (:turn (game))))
(move! 3)
(is (= [:x nil nil :o] (subvec (:board (game)) 0 4))))
(testing "a taken cell is ignored and doesn't pass the turn"
(move! 0)
(is (= :x (:turn (game))))
(is (= :o (get (:board (game)) 3))))
(testing "X wins on the top row; the game then ignores moves"
(move! 1 4 2)
(is (= {:player :x :line [0 1 2]} (:outcome (game))))
(let [before (:board (game))]
(move! 8)
(is (= before (:board (game))))))
(testing "new game"
(rf/dispatch-sync [::events/ttt-reset])
(is (= ttt/new-board (:board (game))))
(is (= :x (:turn (game)))))
(testing "a draw"
(move! 0 1 2 4 3 5 7 6 8)
(is (= :draw (:outcome (game)))))))
+49
View File
@@ -228,3 +228,52 @@
(is (re-find #"Invalid body" (:text r)))
(is (empty? (get-in @rf-db/app-db [:todos :items])))
(done))))))
(deftest tic-tac-toe
(open-page!)
(async done
(-> (call "get_tic_tac_toe" {})
(.then (fn [r]
(is (false? (:error? r)))
(is (= {:board (vec (repeat 9 nil)) :status "in_progress" :turn "x"} (json (:text r))))
(call "move_tic_tac_toe" {:cell 4})))
(.then (fn [r]
;; It pressed the cell, as a user would, and says it is O's turn now.
(is (= :x (get-in @rf-db/app-db [:ttt :board 4])))
(is (= {:board [nil nil nil nil "x" nil nil nil nil] :status "in_progress" :turn "o"}
(json (:text r))))
(call "move_tic_tac_toe" {:cell 4})))
(.then (fn [r]
(is (= {:text "cell 4 is taken" :error? true} r))
(call "move_tic_tac_toe" {:cell 0 :player "x"})))
(.then (fn [r]
(is (= {:text "it is o's turn, not x's" :error? true} r))
(is (nil? (get-in @rf-db/app-db [:ttt :board 0])) "nothing was moved")
(call "move_tic_tac_toe" {:cell 9})))
(.then (fn [r]
(is (:error? r))
(is (re-find #"0 to 8" (:text r)))
(call "move_tic_tac_toe" {:cell 1.5})))
(.then (fn [r]
(is (:error? r))
(call "move_tic_tac_toe" {:cell 0 :player "z"})))
(.then (fn [r]
(is (:error? r))
(call "move_tic_tac_toe" {:cell 0 :player "o"})))
(.then (fn [r]
(is (= "x" (:turn (json (:text r)))))
;; The person moves X to 3 and O to 1; the agent's X then completes the middle row.
(rf/dispatch-sync [::events/ttt-move 3])
(rf/dispatch-sync [::events/ttt-move 1])
(call "move_tic_tac_toe" {:cell 5 :player "x"})))
(.then (fn [r]
(is (= {:board ["o" "o" nil "x" "x" "x" nil nil nil]
:status "won" :winner "x" :line [3 4 5]}
(json (:text r))))
(call "move_tic_tac_toe" {:cell 8})))
(.then (fn [r]
(is (= {:text "the game is over: start a new game" :error? true} r))
(call "new_tic_tac_toe_game" {})))
(.then (fn [r]
(is (= {:board (vec (repeat 9 nil)) :status "in_progress" :turn "x"} (json (:text r))))
(done))))))