From 82dcfc8e98a047dddfb7eace3e583c0862d65d6d Mon Sep 17 00:00:00 2001 From: Average-user Date: Mon, 22 Nov 2021 01:13:44 -0300 Subject: [PATCH 1/7] Replaced 'child-move' for its unsafe version 'move-stone' in ai files, since there is safe --- src/cljc/game_of_ur/ai/ai.cljc | 2 +- src/cljc/game_of_ur/ai/minmax.cljc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cljc/game_of_ur/ai/ai.cljc b/src/cljc/game_of_ur/ai/ai.cljc index bb1176b..d9d5777 100644 --- a/src/cljc/game_of_ur/ai/ai.cljc +++ b/src/cljc/game_of_ur/ai/ai.cljc @@ -29,7 +29,7 @@ black-fn)) (next-board [[board _]] (let [move ((decide-fn board) board (roll))] - [(game/child-board board move) move]))] + [(game/move-stone board move) move]))] (->> [game/initial-board nil] (iterate next-board) (take-until (comp game/game-ended? first))))) diff --git a/src/cljc/game_of_ur/ai/minmax.cljc b/src/cljc/game_of_ur/ai/minmax.cljc index 8ea98af..86b0fba 100644 --- a/src/cljc/game_of_ur/ai/minmax.cljc +++ b/src/cljc/game_of_ur/ai/minmax.cljc @@ -56,7 +56,7 @@ [board-eval-fn depth board roll] (->> (game/valid-moves board roll) (map (comp (partial trampoline evaluate-board-branch board-eval-fn depth) - (partial game/child-board board))) + (partial game/move-stone board))) (reduce max))) (defn best-move @@ -66,6 +66,6 @@ (let [eval-fn (comp (if (= (:turn board) :white) - +) board-eval-fn)] (->> (game/valid-moves board roll) (map (comp (fn [[child-board move]] [(trampoline evaluate-board-branch eval-fn depth child-board) move]) - (fn [move] [(game/child-board board move) move]))) + (fn [move] [(game/move-stone board move) move]))) (reduce (partial max-key first)) (second)))) From f7ca72737707655f89360079346ae374aed0387d Mon Sep 17 00:00:00 2001 From: Average-user Date: Tue, 23 Nov 2021 17:29:32 -0300 Subject: [PATCH 2/7] Several changes related to AI functioning: Events, Interface, Eval functions and tests. Nothing complicated, refactors mainly --- src/cljc/game_of_ur/ai/ai.cljc | 8 ++--- src/cljc/game_of_ur/ai/eval.cljc | 18 ++++++++++ src/cljc/game_of_ur/ai/minmax.cljc | 25 +++----------- src/cljc/game_of_ur/game/board.cljc | 46 ++++++++++++++++--------- src/cljs/game_of_ur/db.cljs | 3 +- src/cljs/game_of_ur/events.cljs | 32 ++++++++++++----- src/cljs/game_of_ur/views/board.cljs | 2 +- test/clj/game_of_ur/test/ai/ai.clj | 23 +++++++++---- test/clj/game_of_ur/test/game/board.clj | 44 +++++++++++++++-------- 9 files changed, 127 insertions(+), 74 deletions(-) create mode 100644 src/cljc/game_of_ur/ai/eval.cljc diff --git a/src/cljc/game_of_ur/ai/ai.cljc b/src/cljc/game_of_ur/ai/ai.cljc index d9d5777..c4cbca7 100644 --- a/src/cljc/game_of_ur/ai/ai.cljc +++ b/src/cljc/game_of_ur/ai/ai.cljc @@ -22,14 +22,12 @@ "Returns a lazy-sequence like '([board-1 move-1] [board-2 move-2] ... [board-n move-n]) where board-n is a finished game. `black-fn` and `white-fn` should be functions that take a board and a roll and return a valid move." - [{:keys [black-fn white-fn]}] + [fns] (letfn [(roll [] (rand-nth [0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 4])) - (decide-fn [board] (if (= :white (:turn board)) - white-fn - black-fn)) + (decide-fn [board] (get fns (:turn board))) (next-board [[board _]] (let [move ((decide-fn board) board (roll))] - [(game/move-stone board move) move]))] + [(game/unsafe-child-board board move) move]))] (->> [game/initial-board nil] (iterate next-board) (take-until (comp game/game-ended? first))))) diff --git a/src/cljc/game_of_ur/ai/eval.cljc b/src/cljc/game_of_ur/ai/eval.cljc new file mode 100644 index 0000000..61141e4 --- /dev/null +++ b/src/cljc/game_of_ur/ai/eval.cljc @@ -0,0 +1,18 @@ +(ns game-of-ur.ai.eval + #?(:clj + (:require + [clojure.spec.alpha :as spec] + [game-of-ur.game.board :as game]) + :cljs + (:require + [cljs.spec.alpha :as spec] + [game-of-ur.game.board :as game]))) + +(defn dumb-evaluation-fn + "sample board evaluation function" + [board] + (spec/assert ::game/board board) + (if (game/player-won? board :black) + 10e6 + (- (+ (* 7 (get-in board [:goal :black])) (* 3 (game/stones-on-board board :black))) + (+ (* 7 (get-in board [:goal :white])) (* 3 (game/stones-on-board board :white)))))) diff --git a/src/cljc/game_of_ur/ai/minmax.cljc b/src/cljc/game_of_ur/ai/minmax.cljc index 86b0fba..1ee9343 100644 --- a/src/cljc/game_of_ur/ai/minmax.cljc +++ b/src/cljc/game_of_ur/ai/minmax.cljc @@ -8,23 +8,6 @@ [cljs.spec.alpha :as spec] [game-of-ur.game.board :as game]))) -(def roll-probability - {0 0.0625 - 1 0.25 - 2 0.375 - 3 0.25 - 4 0.0625}) - -(defn dumb-evaluation-fn - "sample board evaluation function" - [board] - (spec/assert ::game/board board) - (if (game/player-won? board :black) - 10e6 - (- (+ (* 7 (:black (game/stones-in-goal board))) (* 3 (game/stones-on-board board :black))) - (+ (* 7 (:white (game/stones-in-goal board))) (* 3 (game/stones-on-board board :white)))))) - - ;; The following functions build a board search tree ;; The tree's root is the current board and branches into possible rolls, weighted by probability, these branches, ;; in turn branch into possible child boards for that roll. @@ -45,9 +28,9 @@ "Recursively evaluates the provided board's score using the given function up to `depth` levels. Calls `expected-child-board-value` to determine the value of the best move for a given roll." [board-eval-fn depth board] - (if (or (game/game-ended? board) (zero? depth)) + (if (or (zero? depth) (game/game-ended? board)) (board-eval-fn board) - #(->> roll-probability + #(->> game/roll-probability (map (fn [[roll prob]] (* prob (expected-child-board-value board-eval-fn (dec depth) board roll)))) (reduce +)))) @@ -56,7 +39,7 @@ [board-eval-fn depth board roll] (->> (game/valid-moves board roll) (map (comp (partial trampoline evaluate-board-branch board-eval-fn depth) - (partial game/move-stone board))) + (partial game/unsafe-child-board board))) (reduce max))) (defn best-move @@ -66,6 +49,6 @@ (let [eval-fn (comp (if (= (:turn board) :white) - +) board-eval-fn)] (->> (game/valid-moves board roll) (map (comp (fn [[child-board move]] [(trampoline evaluate-board-branch eval-fn depth child-board) move]) - (fn [move] [(game/move-stone board move) move]))) + (fn [move] [(game/unsafe-child-board board move) move]))) (reduce (partial max-key first)) (second)))) diff --git a/src/cljc/game_of_ur/game/board.cljc b/src/cljc/game_of_ur/game/board.cljc index 4179190..f270723 100755 --- a/src/cljc/game_of_ur/game/board.cljc +++ b/src/cljc/game_of_ur/game/board.cljc @@ -22,19 +22,20 @@ [3 -1] [4 -1] [4 0] [4 1] [3 1] :goal]}) +(def valid-origins + {:white (-> :white paths set (disj :goal)) + :black (-> :black paths set (disj :goal))}) + (def rosettes #{[-3 -1] [-3 1] [0 0] [3 -1] [3 1]}) -(defn valid-origins [player] - (-> (set (paths player)) - (disj :goal))) - (def valid-positions (-> (set/union (set (:white paths)) (set (:black paths))) (disj :home :goal))) (def initial-board {:home {:black 7, :white 7} + :goal {:black 0, :white 0} :turn :white :stones (into {} (map (fn [k] [k nil]) valid-positions))}) @@ -42,6 +43,13 @@ {:white :black :black :white}) +(def roll-probability + {0 0.0625 + 1 0.25 + 2 0.375 + 3 0.25 + 4 0.0625}) + (def stone-colors #{:white :black}) (spec/def ::position (spec/with-gen #(contains? valid-positions %) #(spec/gen valid-positions))) @@ -63,7 +71,8 @@ (spec/def ::destination (spec/or :h ::home-position :p ::position :g ::goal-position :pass ::pass-position)) (spec/def ::home (spec/keys :req-un [::white ::black])) -(spec/def ::board (spec/keys :req-un [::home ::turn ::stones])) +(spec/def ::goal (spec/keys :req-un [::white ::black])) +(spec/def ::board (spec/keys :req-un [::home ::goal ::turn ::stones])) (spec/def ::move (spec/keys :req-un [::roll ::origin])) (spec/def ::full-move (spec/keys :req-un [::roll ::origin ::destination ::player])) @@ -78,15 +87,6 @@ (count) (+ (get home player)))) -(defn stones-in-goal - "Returns number of stones in goal for the given board and player" - [board] - (spec/assert ::board board) - {:white (- (get-in initial-board [:home :white]) - (stones-in-play board :white)) - :black (- (get-in initial-board [:home :black]) - (stones-in-play board :black))}) - (defn stones-on-board "Return the number of stones not in goal or home for the given player" [{:keys [stones]} player] @@ -103,7 +103,8 @@ [board player] (spec/assert ::board board) (spec/assert ::player player) - (= 0 (stones-in-play board player))) + (= (get-in initial-board [:home player]) + (get-in board [:goal player]))) (defn game-ended? "Returns true iff either white or black wins" @@ -172,7 +173,20 @@ (not (#{:home :pass} origin)) (assoc-in [:stones origin] nil) (not (#{:home :goal :pass} destination)) (assoc-in [:stones destination] player) (= opponent-color (get stones destination)) (update-in [:home opponent-color] inc) - (= :home origin) (update-in [:home player] dec)))) + (= :home origin) (update-in [:home player] dec) + (= :goal destination) (update-in [:goal player] inc)))) + +(defn unsafe-child-board + "As move-stone, but changing player's turn. As child-move, except + it assumes the move is 'full-move' and valid" + [{:keys [turn] :as board} {:keys [roll] :as move}] + (spec/assert ::board board) + (spec/assert ::full-move move) + (let [destination (:destination move) + next-turn (if (and (not= 0 roll) (rosettes destination)) + turn + (opponent turn))] + (assoc (move-stone board move) :turn next-turn))) (defn child-board "It takes a move and a board (see specs), and returns diff --git a/src/cljs/game_of_ur/db.cljs b/src/cljs/game_of_ur/db.cljs index d01e07a..9d4a143 100755 --- a/src/cljs/game_of_ur/db.cljs +++ b/src/cljs/game_of_ur/db.cljs @@ -1,3 +1,4 @@ (ns game-of-ur.db) -(def default-db {:moves []}) +(def default-db {:moves [] + :ai {:black false, :white false}}) diff --git a/src/cljs/game_of_ur/events.cljs b/src/cljs/game_of_ur/events.cljs index 472dba5..d2ff5ef 100755 --- a/src/cljs/game_of_ur/events.cljs +++ b/src/cljs/game_of_ur/events.cljs @@ -2,7 +2,8 @@ (:require [re-frame.core :as re-frame] [game-of-ur.db :as db] [game-of-ur.ai.minmax :as mm] - [game-of-ur.game.board :as board])) + [game-of-ur.game.board :as board] + [game-of-ur.ai.eval :as ev])) (re-frame/reg-fx :sound @@ -15,12 +16,17 @@ db/default-db)) (re-frame/reg-event-db - :make-move + :make-move' (fn [db [_ move]] (-> db (update :moves conj move) (dissoc :roll)))) +(re-frame/reg-event-fx + :make-move + (fn [{{:keys [auto-roll]} :db} [_ move]] + {:dispatch-n [[:make-move' move] (when auto-roll [:roll-dice]) [:ai-move]]})) + (defn roll-dice "Rolls the dice" [db] @@ -39,17 +45,15 @@ (re-frame/reg-event-fx :play-stone - (fn [{{:keys [roll moves auto-roll]} :db} [_ coords]] + (fn [{{:keys [roll moves ai auto-roll]} :db} [_ coords]] (when roll (let [board-state (reduce board/child-board board/initial-board moves) move (board/full-move {:roll roll :player (:turn board-state) :origin coords})] (when (board/valid-move? board-state move) - {:sound "sfx/clack.mp3" - :dispatch-n (filter identity - (list [:make-move move] - (when auto-roll [:roll-dice])))}))))) + {:sound "sfx/clack.mp3" + :dispatch [:make-move move]}))))) (defn moves->board "Given a seq of moves, returns the current state of the board" @@ -61,8 +65,18 @@ (fn [{{:keys [roll moves auto-roll]} :db} [_]] (when roll (let [board (moves->board moves) - move (mm/best-move mm/dumb-evaluation-fn 3 board roll)] - {:dispatch-n (if auto-roll [[:make-move move] [:roll-dice]] [:make-move move])})))) + move (case (:turn board) + :black (mm/best-move ev/dumb-evaluation-fn 3 board roll) + :white (mm/best-move ev/dumb-evaluation-fn 3 board roll))] + {:dispatch-n (if auto-roll [[:make-move move] [:roll-dice]] [[:make-move move]])})))) + +(re-frame/reg-event-fx + :ai-move + (fn [{{:keys [roll moves ai]} :db} [_]] + (let [board-state (reduce board/child-board board/initial-board moves)] + (when (and (get ai (:turn board-state)) (not (board/game-ended? board-state))) + {:dispatch-n [(when-not roll [:roll-dice])] + :dispatch-later {:ms 100 :dispatch [:play-best-move]}})))) (re-frame/reg-event-db :set-ai diff --git a/src/cljs/game_of_ur/views/board.cljs b/src/cljs/game_of_ur/views/board.cljs index 7cbd8b6..9296323 100644 --- a/src/cljs/game_of_ur/views/board.cljs +++ b/src/cljs/game_of_ur/views/board.cljs @@ -132,7 +132,7 @@ (when last-move [move-path last-move]) [off-board-stones home player-home] [:g {:transform "scale(0.45)"} - [off-board-stones (game-board/stones-in-goal current-board) player-goal]] + [off-board-stones (get current-board :goal) player-goal]] [in-play-stones stones end] (when-not end [:g {:transform (str "translate(2.25, " (second (player-home turn)) ")")} diff --git a/test/clj/game_of_ur/test/ai/ai.clj b/test/clj/game_of_ur/test/ai/ai.clj index f0225e8..c011854 100644 --- a/test/clj/game_of_ur/test/ai/ai.clj +++ b/test/clj/game_of_ur/test/ai/ai.clj @@ -1,36 +1,45 @@ (ns game-of-ur.test.ai.ai (:require [clojure.test :refer [deftest testing is]] + [clojure.spec.alpha :as spec] [game-of-ur.ai.minmax :as mm] + [game-of-ur.ai.eval :as ev] [game-of-ur.ai.ai :as ai] [game-of-ur.game.board :as b])) +(spec/check-asserts true) + (def white-turn {:home {:white 3, :black 5} + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 3 1)), + :black (- (get-in b/initial-board [:home :black]) (+ 5 2))} :turn :white :stones {[-2 -1] :white, [-3 -1] :black, [-2 0] :black}}) (def white-turn-2 {:home {:white 3, :black 0} + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 3 1)), + :black (- (get-in b/initial-board [:home :black]) (+ 0 2))} :turn :white :stones {[-2 -1] :white, [-3 -1] :black, [-2 0] :black}}) (deftest evaluate-board-fn - (is (= -18 (mm/dumb-evaluation-fn white-turn))) - (is (= 17 (mm/dumb-evaluation-fn white-turn-2)))) + (is (= -18 (ev/dumb-evaluation-fn white-turn))) + (is (= 17 (ev/dumb-evaluation-fn white-turn-2)))) (def endgame-black-turn {:home {:white 1 :black 0} + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 1 0)), + :black (- (get-in b/initial-board [:home :black]) (+ 0 1))} :turn :black :stones {[4 1] :black}}) (deftest endgame-evaluation - (is (= :goal (:destination (mm/best-move mm/dumb-evaluation-fn 3 endgame-black-turn 2)))) - (is (= [3 1] (:destination (mm/best-move mm/dumb-evaluation-fn 3 endgame-black-turn 1))))) + (is (= :goal (:destination (mm/best-move ev/dumb-evaluation-fn 3 endgame-black-turn 2)))) + (is (= [3 1] (:destination (mm/best-move ev/dumb-evaluation-fn 3 endgame-black-turn 1))))) (deftest simulation-ends-in-finished-game - (is (-> (ai/simulate-game {:black-fn (fn [b r] (mm/best-move mm/dumb-evaluation-fn 1 b r)) - :white-fn (fn [b r] (mm/best-move mm/dumb-evaluation-fn 1 b r))}) + (is (-> (ai/simulate-game {:black (partial mm/best-move ev/dumb-evaluation-fn 1) + :white (partial mm/best-move ev/dumb-evaluation-fn 1)}) (last) (first) (b/game-ended?)))) - diff --git a/test/clj/game_of_ur/test/game/board.clj b/test/clj/game_of_ur/test/game/board.clj index 6dd7d8d..6322fff 100644 --- a/test/clj/game_of_ur/test/game/board.clj +++ b/test/clj/game_of_ur/test/game/board.clj @@ -1,8 +1,12 @@ (ns game-of-ur.test.game.board (:import [clojure.lang ExceptionInfo]) (:require [clojure.test :refer [deftest testing is]] + [clojure.spec.alpha :as spec] [game-of-ur.game.board :as b])) +(spec/check-asserts true) + + (def move-white-home-0 {:roll 0, :origin :home, :player :white}) (def move-black-home-0 {:roll 0, :origin :home, :player :black}) @@ -27,26 +31,36 @@ (def white-turn {:home {:white 3, :black 5} + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 3 1)), + :black (- (get-in b/initial-board [:home :black]) (+ 5 2))} :turn :white :stones {[-2 -1] :white, [-3 -1] :black, [-2 0] :black}}) (def black-turn-1 {:home {:white 4, :black 4} + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 4 2)), + :black (- (get-in b/initial-board [:home :black]) (+ 4 1))} :turn :black :stones {[-3 -1] :white, [3 0] :black, [-3 1] :white}}) (def black-turn-2 {:home {:white 3, :black 0}, + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 3 0)), + :black (- (get-in b/initial-board [:home :black]) (+ 0 2))} :turn :black :stones {[-2 0] :black, [0 0] :black}}) (def black-turn-3 {:home {:white 3, :black 0}, + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 3 0)), + :black (- (get-in b/initial-board [:home :black]) (+ 0 1))} :turn :black :stones {[-2 0] :black}}) (def no-valid-move-for-black {:home {:white 0, :black 0}, + :goal {:white (- (get-in b/initial-board [:home :black]) (+ 0 1)), + :black (- (get-in b/initial-board [:home :black]) (+ 0 2))} :turn :black :stones {[-3 0] :black, [0 0] :white, [4, 1] :black}}) @@ -68,27 +82,29 @@ (is (not (b/valid-move? black-turn-2 (b/full-move move-black-2)))) ; there is already a black stone at [0 0] (is (b/valid-move? black-turn-3 (b/full-move move-black-2))) (is (not (b/valid-move? black-turn-1 (b/full-move pass-move)))) ; non of these - (is (not (b/valid-move? black-turn-2 (b/full-move pass-move)))) ; are valid + (is (not (b/valid-move? black-turn-2 (b/full-move pass-move)))) ; are valid (is (not (b/valid-move? black-turn-3 (b/full-move pass-move)))) ; passing positions (is (b/valid-move? no-valid-move-for-black (b/full-move pass-move)))) ; :pass is valid iff there are no other valid-moves -(defn clear-nil-stones [board] - (when board (update board :stones #(into {} (filter second %))))) +(defn clear-nil-stones-and-goal [board] + (when board (-> board + (update :stones #(into {} (filter second %))) + (dissoc :goal)))) (deftest child-board - (is (= (clear-nil-stones (assoc b/initial-board :turn :black)) - (clear-nil-stones (b/child-board b/initial-board (b/full-move pass-move-0))))) + (is (= (clear-nil-stones-and-goal (assoc b/initial-board :turn :black)) + (clear-nil-stones-and-goal (b/child-board b/initial-board (b/full-move pass-move-0))))) (is (= {:home {:black 7, :white 6}, :turn :black, :stones {[-2 -1] :white}} - (clear-nil-stones (b/child-board b/initial-board (b/full-move move-white-home-3))))) - (is (= (clear-nil-stones (assoc white-turn :turn :black)) - (clear-nil-stones (b/child-board white-turn (b/full-move pass-move-0))))) + (clear-nil-stones-and-goal (b/child-board b/initial-board (b/full-move move-white-home-3))))) + (is (= (clear-nil-stones-and-goal (assoc white-turn :turn :black)) + (clear-nil-stones-and-goal (b/child-board white-turn (b/full-move pass-move-0))))) (is (= {:home {:white 3, :black 6}, :turn :black, :stones {[-3 -1] :black, [-2 0] :white}} ; black captured - (clear-nil-stones (b/child-board white-turn (b/full-move move-white-2))))) - (is (= (clear-nil-stones (assoc black-turn-1 :turn :white)) - (clear-nil-stones (b/child-board black-turn-1 (b/full-move move-black-home-0))))) + (clear-nil-stones-and-goal (b/child-board white-turn (b/full-move move-white-2))))) + (is (= (clear-nil-stones-and-goal (assoc black-turn-1 :turn :white)) + (clear-nil-stones-and-goal (b/child-board black-turn-1 (b/full-move move-black-home-0))))) (is (= {:home {:white 3, :black 0}, :turn :white, :stones {[0 0] :black, [-1 0] :black}} - (clear-nil-stones (b/child-board black-turn-2 (b/full-move move-black-1))))) + (clear-nil-stones-and-goal (b/child-board black-turn-2 (b/full-move move-black-1))))) (is (= {:home {:white 3, :black 0}, :turn :black, :stones {[0 0] :black}} ; lands on rosette and plays again - (clear-nil-stones (b/child-board black-turn-3 (b/full-move move-black-2))))) + (clear-nil-stones-and-goal (b/child-board black-turn-3 (b/full-move move-black-2))))) (is (= {:home {:white 0 :black 0}, :turn :white, :stones {[-3 0] :black, [0 0] :white, [4, 1] :black}} ; If there is no possible valid move - (b/child-board no-valid-move-for-black (b/full-move pass-move))))) ; should only change :turn + (clear-nil-stones-and-goal (b/child-board no-valid-move-for-black (b/full-move pass-move)))))) ; should only change :turn From f1b17ec4ee9afb5f431dca36f191e8d49363efb3 Mon Sep 17 00:00:00 2001 From: Average-user Date: Tue, 23 Nov 2021 18:28:54 -0300 Subject: [PATCH 3/7] Fixed minimax --- src/cljc/game_of_ur/ai/minmax.cljc | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cljc/game_of_ur/ai/minmax.cljc b/src/cljc/game_of_ur/ai/minmax.cljc index 1ee9343..50d0271 100644 --- a/src/cljc/game_of_ur/ai/minmax.cljc +++ b/src/cljc/game_of_ur/ai/minmax.cljc @@ -38,17 +38,21 @@ "Recursively evaluates the value of the possible child boards for the given board and roll." [board-eval-fn depth board roll] (->> (game/valid-moves board roll) - (map (comp (partial trampoline evaluate-board-branch board-eval-fn depth) - (partial game/unsafe-child-board board))) - (reduce max))) + (map (fn [move] (trampoline evaluate-board-branch board-eval-fn depth + (game/unsafe-child-board board move)))) + (reduce (if (= :black (:turn board)) max min)))) (defn best-move "Using the provided function evaluates the board's possible child boards, and returns the best move. Calls the mutually recursive `evaluate-board-branch` and `expected-child-board-value`" - [board-eval-fn depth board roll] - (let [eval-fn (comp (if (= (:turn board) :white) - +) board-eval-fn)] - (->> (game/valid-moves board roll) - (map (comp (fn [[child-board move]] [(trampoline evaluate-board-branch eval-fn depth child-board) move]) - (fn [move] [(game/unsafe-child-board board move) move]))) - (reduce (partial max-key first)) - (second)))) + [eval-fn depth board roll] + (if (game/game-ended? board) + board + (let [move-val (fn [m] (trampoline evaluate-board-branch eval-fn depth + (game/unsafe-child-board board m))) + options (map (juxt identity move-val) (game/valid-moves board roll)) + best-score (reduce (if (= :black (:turn board)) max min) (map second options))] + (->> options + (filter (comp (partial = best-score) second)) + (rand-nth) + (first))))) From e338f3624174f776aba5240a3c7691a8e24c0851 Mon Sep 17 00:00:00 2001 From: Average-user Date: Wed, 24 Nov 2021 09:48:43 -0300 Subject: [PATCH 4/7] fixed ascii.clj, which was broken after the changes --- src/clj/game_of_ur/ascii.clj | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/clj/game_of_ur/ascii.clj b/src/clj/game_of_ur/ascii.clj index c0850bd..623e6fa 100644 --- a/src/clj/game_of_ur/ascii.clj +++ b/src/clj/game_of_ur/ascii.clj @@ -2,6 +2,7 @@ (:require [game-of-ur.game.board :as board] [game-of-ur.ai.minmax :as mm] + [game-of-ur.ai.ai :as ai] [clojure.string :as cs])) ;; rectangle board coordinates @@ -33,7 +34,7 @@ "Prints a game-state given a board, player, roll, and destination. Example: - -3 -2 -1 0 1 2 3 4 + -3 -2 -1 0 1 2 3 4 -------------------------- | x xx -1| [~][ ][x][ ] [~][ ] @@ -44,27 +45,26 @@ black rolled 2 from :home to [-1 1] " [{:keys [home stones] :as board} player roll origin destination] - (let [in-goal (board/stones-in-goal board) + (let [in-goal (get board :goal) white-goal (apply str (concat (repeat (- 18 (:white home)) " ") (repeat (:white in-goal) \x))) black-goal (apply str (concat (repeat (- 18 (:black home)) " ") (repeat (:black in-goal) \o))) board-str (stones->string stones) board-str' (map #(cons (format "%2d| " %1) %2) (range -1 2) board-str)] - (do - (println "\n -3 -2 -1 0 1 2 3 4 ") - (println " --------------------------") - (println (apply str (concat (cons " | " (repeat (:white home) "x")) white-goal))) - (doall (map (comp println (partial apply str)) board-str')) - (println (apply str (concat (cons " | " (repeat (:black home) "o")) black-goal))) - (print (str "\n" (apply str (rest (str player))) " rolled " roll)) - (println (str " from " origin " to " destination "\n"))))) + (println "\n -3 -2 -1 0 1 2 3 4 ") + (println " --------------------------") + (println (apply str (concat (cons " | " (repeat (:white home) "x")) white-goal))) + (doall (map (comp println (partial apply str)) board-str')) + (println (apply str (concat (cons " | " (repeat (:black home) "o")) black-goal))) + (print (str "\n" (apply str (rest (str player))) " rolled " roll)) + (println (str " from " origin " to " destination "\n")))) (defn graphic-simulation "Prints every step of an AI vs AI game simulation, until the game is ended. see: ai/simulate-game" - [eval-fn-black eval-fn-white depth] - (let [game (ai/simulate-game eval-fn-black eval-fn-white depth) + [black-fn white-fn] + (let [game (ai/simulate-game {:black black-fn :white white-fn}) show (fn [[board {:keys [origin destination player roll]}]] (print-game-state board player roll origin destination))] (doall (map show game)))) @@ -76,15 +76,15 @@ that is two numbers separated by spaces like '3 -1'. If the coordinate is out of range the specs will throw an error too" [board valid-moves roll] - (do (println (str "\nyour roll is " roll ". Enter coordinates:")) - (let [entry (read-line) - origin (cond (= entry "home") :home - (= entry "pass") :pass - :else (mapv read-string (cs/split entry #" ")))] - (if (contains? (board/valid-moves board roll) - (board/full-move {:roll roll :origin origin :player (:turn board)})) - (board/full-move {:roll roll :origin origin :player (:turn board)}) - (recur board valid-moves roll))))) + (println (str "\nyour roll is " roll ". Enter coordinates:")) + (let [entry (read-line) + origin (cond (= entry "home") :home + (= entry "pass") :pass + :else (mapv read-string (cs/split entry #" ")))] + (if (contains? (board/valid-moves board roll) + (board/full-move {:roll roll :origin origin :player (:turn board)})) + (board/full-move {:roll roll :origin origin :player (:turn board)}) + (recur board valid-moves roll)))) (defn game-loop "Given a starting board, a color chosen by the player and a depth for the AI, @@ -92,11 +92,11 @@ the game ends" [board evaluation-fn color depth] (loop [board board, player nil, roll nil, origin nil, destination nil] - (do (print-game-state board player roll origin destination) - (when-not (board/game-ended? board) - (let [roll (rand-nth [0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 4]) - move (if (= (:turn board) color) - (player-move board (board/valid-moves board roll) roll) - (mm/best-move evaluation-fn depth board roll)) - nboard (board/child-board board move)] - (recur nboard (:turn board) roll (:origin move) (:destination move))))))) + (print-game-state board player roll origin destination) + (when-not (board/game-ended? board) + (let [roll (rand-nth [0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 4]) + move (if (= (:turn board) color) + (player-move board (board/valid-moves board roll) roll) + (mm/best-move evaluation-fn depth board roll)) + nboard (board/child-board board move)] + (recur nboard (:turn board) roll (:origin move) (:destination move)))))) From b6eefc10ea65525b1786142244103676fffb7cf3 Mon Sep 17 00:00:00 2001 From: Average-user Date: Wed, 24 Nov 2021 18:11:57 -0300 Subject: [PATCH 5/7] implemented alpha-beta pruning --- .gitignore | 1 + src/cljc/game_of_ur/ai/ai.cljc | 14 +++++++ src/cljc/game_of_ur/ai/minmax.cljc | 61 ++++++++++++++++++++++-------- src/cljs/game_of_ur/events.cljs | 11 ++++-- test/clj/game_of_ur/test/ai/ai.clj | 15 ++++++-- 5 files changed, 78 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 186ade1..61f58d3 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ pom.xml /.lein-* /.env .nrepl-port +/src/clj/game_of_ur/experiments.clj diff --git a/src/cljc/game_of_ur/ai/ai.cljc b/src/cljc/game_of_ur/ai/ai.cljc index c4cbca7..26070a2 100644 --- a/src/cljc/game_of_ur/ai/ai.cljc +++ b/src/cljc/game_of_ur/ai/ai.cljc @@ -31,3 +31,17 @@ (->> [game/initial-board nil] (iterate next-board) (take-until (comp game/game-ended? first))))) + +(defn best-move + "Evaluates which is the best move for a given player, board, and roll. Given + the criteria of `rank-fn`. We assume that the greater the number, the better the + positions is for black." + [rank-fn board roll] + (if (game/game-ended? board) + board + (let [options (map (fn [m] [m (rank-fn board m)]) (game/valid-moves board roll)) + best-score (reduce (if (= :black (:turn board)) max min) (map second options))] + (->> options + (filter (comp (partial = best-score) second)) + (rand-nth) + (first))))) diff --git a/src/cljc/game_of_ur/ai/minmax.cljc b/src/cljc/game_of_ur/ai/minmax.cljc index 50d0271..5582c0b 100644 --- a/src/cljc/game_of_ur/ai/minmax.cljc +++ b/src/cljc/game_of_ur/ai/minmax.cljc @@ -8,6 +8,9 @@ [cljs.spec.alpha :as spec] [game-of-ur.game.board :as game]))) + +;;;;; BASIC MINIMAX + ;; The following functions build a board search tree ;; The tree's root is the current board and branches into possible rolls, weighted by probability, these branches, ;; in turn branch into possible child boards for that roll. @@ -24,7 +27,7 @@ (declare expected-child-board-value) -(defn evaluate-board-branch +(defn- evaluate-board-branch "Recursively evaluates the provided board's score using the given function up to `depth` levels. Calls `expected-child-board-value` to determine the value of the best move for a given roll." [board-eval-fn depth board] @@ -34,7 +37,7 @@ (map (fn [[roll prob]] (* prob (expected-child-board-value board-eval-fn (dec depth) board roll)))) (reduce +)))) -(defn expected-child-board-value +(defn- expected-child-board-value "Recursively evaluates the value of the possible child boards for the given board and roll." [board-eval-fn depth board roll] (->> (game/valid-moves board roll) @@ -42,17 +45,43 @@ (game/unsafe-child-board board move)))) (reduce (if (= :black (:turn board)) max min)))) -(defn best-move - "Using the provided function evaluates the board's possible child boards, and returns the best move. - Calls the mutually recursive `evaluate-board-branch` and `expected-child-board-value`" - [eval-fn depth board roll] - (if (game/game-ended? board) - board - (let [move-val (fn [m] (trampoline evaluate-board-branch eval-fn depth - (game/unsafe-child-board board m))) - options (map (juxt identity move-val) (game/valid-moves board roll)) - best-score (reduce (if (= :black (:turn board)) max min) (map second options))] - (->> options - (filter (comp (partial = best-score) second)) - (rand-nth) - (first))))) +(defn minimax-rank-move [eval-fn depth board move] + (trampoline evaluate-board-branch eval-fn depth + (game/unsafe-child-board board move))) + + +;;;; MINMAX WITH ALPHA BETA PRUNING + +;; This is semantically equivalent to minimax, but somewhat more efficient due to +;; pruning of the search tree. Thus, `search` and `prune` are the analogous to +;; `evaluate-board-branch` and `expected-child-board-balue`. +;; TODO: change names, find out if trampoline would make any difference. + +(def +INF #?(:clj Double/POSITIVE_INFINITY :cljs js/Infinity)) +(def -INF #?(:clj Double/NEGATIVE_INFINITY :cljs (- js/Infinity))) + +(declare prune) + +(defn- search [eval-fn board depth α β] + (if (or (zero? depth) (game/game-ended? board)) + (eval-fn board) + (-> (fn [k [roll p]] + (as-> (game/valid-moves board roll) $ + (map (partial game/unsafe-child-board board) $) + (prune eval-fn α β (dec depth) (= :black (:turn board)) $) + (+ k (* p $)))) + (reduce 0 game/roll-probability)))) + +(defn- prune [eval-fn α β depth m? childs] + (loop [[c & cs] childs, α α, β β, v (if m? -INF +INF)] + (if (nil? c) + v + (let [v' ((if m? max min) v (search eval-fn c depth α β)) + α' (max α v') + β' (min β v')] + (if m? + (if (>= α' β) v' (recur cs α' β v')) + (if (>= α β') v' (recur cs α β' v'))))))) + +(defn alpha-beta-rank-move [eval-fn depth board move] + (search eval-fn (game/unsafe-child-board board move) depth -INF +INF)) diff --git a/src/cljs/game_of_ur/events.cljs b/src/cljs/game_of_ur/events.cljs index d2ff5ef..372b0ad 100755 --- a/src/cljs/game_of_ur/events.cljs +++ b/src/cljs/game_of_ur/events.cljs @@ -1,8 +1,9 @@ (ns game-of-ur.events (:require [re-frame.core :as re-frame] [game-of-ur.db :as db] - [game-of-ur.ai.minmax :as mm] [game-of-ur.game.board :as board] + [game-of-ur.ai.minmax :as mm] + [game-of-ur.ai.ai :as ai] [game-of-ur.ai.eval :as ev])) (re-frame/reg-fx @@ -60,14 +61,16 @@ [moves] (reduce board/child-board board/initial-board moves)) +(def computer-move + {:white (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3)) + :black (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3))}) + (re-frame/reg-event-fx :play-best-move (fn [{{:keys [roll moves auto-roll]} :db} [_]] (when roll (let [board (moves->board moves) - move (case (:turn board) - :black (mm/best-move ev/dumb-evaluation-fn 3 board roll) - :white (mm/best-move ev/dumb-evaluation-fn 3 board roll))] + move ((computer-move (:turn board)) board roll)] {:dispatch-n (if auto-roll [[:make-move move] [:roll-dice]] [[:make-move move]])})))) (re-frame/reg-event-fx diff --git a/test/clj/game_of_ur/test/ai/ai.clj b/test/clj/game_of_ur/test/ai/ai.clj index c011854..202dc7a 100644 --- a/test/clj/game_of_ur/test/ai/ai.clj +++ b/test/clj/game_of_ur/test/ai/ai.clj @@ -34,12 +34,19 @@ :stones {[4 1] :black}}) (deftest endgame-evaluation - (is (= :goal (:destination (mm/best-move ev/dumb-evaluation-fn 3 endgame-black-turn 2)))) - (is (= [3 1] (:destination (mm/best-move ev/dumb-evaluation-fn 3 endgame-black-turn 1))))) + (is (= :goal (:destination (ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 2)))) + (is (= [3 1] (:destination (ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 1)))) + (is (= :goal (:destination (ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 2)))) + (is (= [3 1] (:destination (ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 1))))) (deftest simulation-ends-in-finished-game - (is (-> (ai/simulate-game {:black (partial mm/best-move ev/dumb-evaluation-fn 1) - :white (partial mm/best-move ev/dumb-evaluation-fn 1)}) + (is (-> (ai/simulate-game {:black (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 1)) + :white (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 1))}) + (last) + (first) + (b/game-ended?))) + (is (-> (ai/simulate-game {:black (partial ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 1)) + :white (partial ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 1))}) (last) (first) (b/game-ended?)))) From 4afd076d9048efd43b911afdc1505e7e4565b262 Mon Sep 17 00:00:00 2001 From: Average-user Date: Wed, 24 Nov 2021 18:13:25 -0300 Subject: [PATCH 6/7] Panel for chosing AI and auto-roll available in non-debug mode --- src/cljs/game_of_ur/views.cljs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/cljs/game_of_ur/views.cljs b/src/cljs/game_of_ur/views.cljs index f6e8e57..e79a64c 100755 --- a/src/cljs/game_of_ur/views.cljs +++ b/src/cljs/game_of_ur/views.cljs @@ -12,8 +12,21 @@ [:main {:style style/main} [board/board board-state last-move] - ;;Stuff for development purposes below - (when config/debug? + (if-not config/debug? + [:div#dev-helpers {:style {:border "1px solid grey"}} + [:span + [:input#auto-roll {:type :checkbox + :on-change #(re-frame/dispatch [:set-auto-roll (aget % "target" "checked")])}] + [:label {:for :auto-roll} "Auto-roll"]] + [:span + [:input#ai-white {:type :checkbox + :on-change #(re-frame/dispatch [:set-ai :white (aget % "target" "checked")])}] + [:label {:for :ai-white} "AI White"]] + [:span + [:input#ai-black {:type :checkbox + :on-change #(re-frame/dispatch [:set-ai :black (aget % "target" "checked")])}] + [:label {:for :ai-black} "AI Black"]]] + ;;Stuff for development purposes below (->> (assoc board-state :last-move last-move) (map (fn [[title obj]] [:div [:h4 title] [:tt (str obj)]])) (into [:div#dev-helpers {:style {:border "1px solid grey"}} From e7350e719f898d1b89043fc0606e02f805b1d97f Mon Sep 17 00:00:00 2001 From: average-user Date: Wed, 8 Dec 2021 22:35:44 -0300 Subject: [PATCH 7/7] Some refactors on the AI side, getting things ready to do some more hopefuly serious work --- .gitignore | 1 + README.md | 17 +++++++++++++ src/clj/game_of_ur/ascii.clj | 2 +- src/cljc/game_of_ur/ai/ai.cljc | 27 ++++++++++++--------- src/cljc/game_of_ur/ai/eval.cljc | 34 +++++++++++++++++++------- src/cljc/game_of_ur/ai/minmax.cljc | 37 ----------------------------- src/cljc/game_of_ur/game/board.cljc | 9 ++++--- src/cljs/game_of_ur/events.cljs | 4 ++-- test/clj/game_of_ur/test/ai/ai.clj | 7 ------ 9 files changed, 69 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 61f58d3..7e1fdcd 100755 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ pom.xml /.env .nrepl-port /src/clj/game_of_ur/experiments.clj +README.html diff --git a/README.md b/README.md index daf305a..f584336 100755 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ [Live Demo](https://polymeris.github.io/game-of-ur/) +## How to improve? + +There are two main things to improve. The first one is simple, and is to improve +user interface/experience by getting the UI to work better. The second and more +interesting one is to improve the AI. For this we've thought about two +approaches. One is to improve the search function which as for now is a +simple _expectiminimax_, either by implementing some pruning or by +memoizing some redundant information somehow. The other and again more +interesting, is to either create better evaluation functions at +`cljc/game_of_ur/ai/eval.cljc`, or to make them evolve using some kind of +genetic algorithm. + ## Development Mode ### Run application: @@ -16,6 +28,11 @@ Figwheel will automatically push cljs changes to the browser. Wait a bit, then browse to [http://localhost:3449](http://localhost:3449). +We also encourage to set `(spec/check-asserts true)` +to `false` in `cljc/game_of_ur/game/board.cljc` when working on the AI, since it +makes the program run considerably faster. + + ## Production Build diff --git a/src/clj/game_of_ur/ascii.clj b/src/clj/game_of_ur/ascii.clj index 623e6fa..861f917 100644 --- a/src/clj/game_of_ur/ascii.clj +++ b/src/clj/game_of_ur/ascii.clj @@ -97,6 +97,6 @@ (let [roll (rand-nth [0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 4]) move (if (= (:turn board) color) (player-move board (board/valid-moves board roll) roll) - (mm/best-move evaluation-fn depth board roll)) + (ai/best-move evaluation-fn depth board roll)) nboard (board/child-board board move)] (recur nboard (:turn board) roll (:origin move) (:destination move)))))) diff --git a/src/cljc/game_of_ur/ai/ai.cljc b/src/cljc/game_of_ur/ai/ai.cljc index 26070a2..7da7581 100644 --- a/src/cljc/game_of_ur/ai/ai.cljc +++ b/src/cljc/game_of_ur/ai/ai.cljc @@ -24,7 +24,7 @@ that take a board and a roll and return a valid move." [fns] (letfn [(roll [] (rand-nth [0 1 1 1 1 2 2 2 2 2 2 3 3 3 3 4])) - (decide-fn [board] (get fns (:turn board))) + (decide-fn [board] (fns (:turn board))) (next-board [[board _]] (let [move ((decide-fn board) board (roll))] [(game/unsafe-child-board board move) move]))] @@ -32,16 +32,21 @@ (iterate next-board) (take-until (comp game/game-ended? first))))) -(defn best-move - "Evaluates which is the best move for a given player, board, and roll. Given +(defn ranked-moves + "Evaluates which is/are the best move/s for a given player, board, and roll. Given the criteria of `rank-fn`. We assume that the greater the number, the better the positions is for black." [rank-fn board roll] - (if (game/game-ended? board) - board - (let [options (map (fn [m] [m (rank-fn board m)]) (game/valid-moves board roll)) - best-score (reduce (if (= :black (:turn board)) max min) (map second options))] - (->> options - (filter (comp (partial = best-score) second)) - (rand-nth) - (first))))) + (map (fn [m] [m (rank-fn board m)]) + (game/valid-moves board roll))) + +(defn best-moves + [rank-fn board roll] + (let [options (ranked-moves rank-fn board roll) + best-score (reduce (if (= :black (:turn board)) max min) (map second options))] + (keep (fn [[move score]] (when (= score best-score) move)) + options))) + +(defn best-move [rank-fn board roll] + (when-not (game/game-ended? board) + (rand-nth (best-moves rank-fn board roll)))) diff --git a/src/cljc/game_of_ur/ai/eval.cljc b/src/cljc/game_of_ur/ai/eval.cljc index 61141e4..b27f8c9 100644 --- a/src/cljc/game_of_ur/ai/eval.cljc +++ b/src/cljc/game_of_ur/ai/eval.cljc @@ -8,11 +8,29 @@ [cljs.spec.alpha :as spec] [game-of-ur.game.board :as game]))) -(defn dumb-evaluation-fn - "sample board evaluation function" - [board] - (spec/assert ::game/board board) - (if (game/player-won? board :black) - 10e6 - (- (+ (* 7 (get-in board [:goal :black])) (* 3 (game/stones-on-board board :black))) - (+ (* 7 (get-in board [:goal :white])) (* 3 (game/stones-on-board board :white)))))) +(defn- ev-wrap [win-score f] + (fn [board] + (spec/assert ::game/board board) + (cond (game/player-won? board :black) win-score + (game/player-won? board :white) (- win-score) + :else (f board)))) + +;; Decided to call evaluation functions by jazz album's/song's. + +(def dumb-evaluation-fn + (ev-wrap 10e6 + #(- (+ (* 7 (get-in % [:goal :black])) (* 3 (game/stones-on-board % :black))) + (+ (* 7 (get-in % [:goal :white])) (* 3 (game/stones-on-board % :white)))))) + +(defn- stones-value [{:keys [stones goal]} player f] + (let [p (game/paths player)] + (->> (map-indexed vector p) + (keep (fn [[n x]] (when (= player (stones x)) (f n)))) + (reduce +) + (+ (* (get goal player) (f (dec (count p)))))))) + +;; Wins about 70% of games against dumb-evaluation-fn +(def inner-urge + (ev-wrap 10e6 + #(- (stones-value % :black (partial * 0.5)) + (stones-value % :white (partial * 0.5))))) diff --git a/src/cljc/game_of_ur/ai/minmax.cljc b/src/cljc/game_of_ur/ai/minmax.cljc index 5582c0b..5e93ef4 100644 --- a/src/cljc/game_of_ur/ai/minmax.cljc +++ b/src/cljc/game_of_ur/ai/minmax.cljc @@ -48,40 +48,3 @@ (defn minimax-rank-move [eval-fn depth board move] (trampoline evaluate-board-branch eval-fn depth (game/unsafe-child-board board move))) - - -;;;; MINMAX WITH ALPHA BETA PRUNING - -;; This is semantically equivalent to minimax, but somewhat more efficient due to -;; pruning of the search tree. Thus, `search` and `prune` are the analogous to -;; `evaluate-board-branch` and `expected-child-board-balue`. -;; TODO: change names, find out if trampoline would make any difference. - -(def +INF #?(:clj Double/POSITIVE_INFINITY :cljs js/Infinity)) -(def -INF #?(:clj Double/NEGATIVE_INFINITY :cljs (- js/Infinity))) - -(declare prune) - -(defn- search [eval-fn board depth α β] - (if (or (zero? depth) (game/game-ended? board)) - (eval-fn board) - (-> (fn [k [roll p]] - (as-> (game/valid-moves board roll) $ - (map (partial game/unsafe-child-board board) $) - (prune eval-fn α β (dec depth) (= :black (:turn board)) $) - (+ k (* p $)))) - (reduce 0 game/roll-probability)))) - -(defn- prune [eval-fn α β depth m? childs] - (loop [[c & cs] childs, α α, β β, v (if m? -INF +INF)] - (if (nil? c) - v - (let [v' ((if m? max min) v (search eval-fn c depth α β)) - α' (max α v') - β' (min β v')] - (if m? - (if (>= α' β) v' (recur cs α' β v')) - (if (>= α β') v' (recur cs α β' v'))))))) - -(defn alpha-beta-rank-move [eval-fn depth board move] - (search eval-fn (game/unsafe-child-board board move) depth -INF +INF)) diff --git a/src/cljc/game_of_ur/game/board.cljc b/src/cljc/game_of_ur/game/board.cljc index f270723..5dafc0d 100755 --- a/src/cljc/game_of_ur/game/board.cljc +++ b/src/cljc/game_of_ur/game/board.cljc @@ -215,7 +215,7 @@ (->> (valid-origins turn) (map (fn [origin] (full-move {:roll roll, :origin origin, :player turn}))))) -(defn valid-moves +(defn valid-moves' "returns all valid moves with a given roll" [{:keys [turn] :as board} roll] (spec/assert ::board board) @@ -223,8 +223,11 @@ (let [moves (->> (all-moves board roll) (filter (partial valid-non-pass-move? board)))] (if-not (empty? moves) - (set moves) - #{(pass-move roll turn)}))) + moves + (list (pass-move roll turn))))) + +(defn valid-moves [board roll] + (set (valid-moves' board roll))) (defn must-pass? "returns true if pass is a valid move" diff --git a/src/cljs/game_of_ur/events.cljs b/src/cljs/game_of_ur/events.cljs index 372b0ad..993a3f6 100755 --- a/src/cljs/game_of_ur/events.cljs +++ b/src/cljs/game_of_ur/events.cljs @@ -62,8 +62,8 @@ (reduce board/child-board board/initial-board moves)) (def computer-move - {:white (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3)) - :black (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3))}) + {:white (partial ai/best-move (partial mm/minimax-rank-move ev/inner-urge 3)) + :black (partial ai/best-move (partial mm/minimax-rank-move ev/inner-urge 3))}) (re-frame/reg-event-fx :play-best-move diff --git a/test/clj/game_of_ur/test/ai/ai.clj b/test/clj/game_of_ur/test/ai/ai.clj index 202dc7a..90810b2 100644 --- a/test/clj/game_of_ur/test/ai/ai.clj +++ b/test/clj/game_of_ur/test/ai/ai.clj @@ -34,17 +34,10 @@ :stones {[4 1] :black}}) (deftest endgame-evaluation - (is (= :goal (:destination (ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 2)))) - (is (= [3 1] (:destination (ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 1)))) (is (= :goal (:destination (ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 2)))) (is (= [3 1] (:destination (ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 3) endgame-black-turn 1))))) (deftest simulation-ends-in-finished-game - (is (-> (ai/simulate-game {:black (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 1)) - :white (partial ai/best-move (partial mm/alpha-beta-rank-move ev/dumb-evaluation-fn 1))}) - (last) - (first) - (b/game-ended?))) (is (-> (ai/simulate-game {:black (partial ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 1)) :white (partial ai/best-move (partial mm/minimax-rank-move ev/dumb-evaluation-fn 1))}) (last)