|
(ns game-of-go.core) |
|
|
|
(defn create-blank-tile [coords] |
|
{:claimed-by nil |
|
:claimed-turn nil |
|
:coords coords}) |
|
|
|
(defn create-tile-grid [n] |
|
(let [coords (for [x (range 0 n) y (range 0 n)] [x y]) |
|
tiles (map create-blank-tile coords)] |
|
(apply hash-map (interleave coords tiles)))) |
|
|
|
(defn valid-coord-fn |
|
"Determine if coords are in-bounds. On ctx as 'valid-coord?'." |
|
[board-size] |
|
(fn [[x y]] (and (< -1 x board-size) (< -1 y board-size)))) |
|
|
|
(defn valid-neighbors-fn |
|
"Generate valid neighbors of a coord; diagonals are not considered neighbors. |
|
On ctx as 'valid-neighbors'." |
|
[valid-coord?] |
|
(fn [[cx cy]] (filter valid-coord? [[cx (inc cy)] [cx (dec cy)] [(inc cx) cy] [(dec cx) cy]]))) |
|
|
|
(defn create-blank-game-state [n] |
|
(let [valid-coord? (valid-coord-fn n) |
|
valid-neighbors (valid-neighbors-fn valid-coord?)] |
|
{:next-turn-user :blue |
|
:next-turn-count 1 |
|
:board-size n |
|
:grid (create-tile-grid n) |
|
:valid-coord? valid-coord? |
|
:valid-neighbors valid-neighbors})) |
|
|
|
;; ===================================================================================================== |
|
|
|
(def board-size 19) |
|
(def game-state (atom (create-blank-game-state board-size))) |
|
|
|
(declare process-next-move) |
|
|
|
(defn tile->element [{:keys [claimed-by coords] :as tile}] |
|
(let [tile-el (.createElement js/document "div") |
|
style (.-style tile-el)] |
|
(set! (.-height style) "30px") |
|
(set! (.-width style) "30px") |
|
(set! (.-border style) "2px solid black") |
|
(case claimed-by |
|
:red (set! (.-backgroundColor style) "red") |
|
:blue (set! (.-backgroundColor style) "blue") |
|
nil (do |
|
(set! (.-backgroundColor style) "white") |
|
(.addEventListener tile-el "mouseover" (fn [_] (set! (.-backgroundColor style) "gray"))) |
|
(.addEventListener tile-el "mouseleave" (fn [_] (set! (.-backgroundColor style) "white"))) |
|
(.addEventListener tile-el "click" (fn [_] (process-next-move coords))))) |
|
tile-el)) |
|
|
|
(defn tile-grid->element [n grid] |
|
(let [grid-el (.createElement js/document "div") |
|
grid-style (.-style grid-el)] |
|
(dotimes [i n] |
|
(let [row-keys (for [y (range 0 n)] [i y]) |
|
row-el (.createElement js/document "div") |
|
row-style (.-style row-el)] |
|
(doall (map (comp #(.appendChild row-el %) tile->element grid) row-keys)) |
|
(set! (.-display row-style) "flex") |
|
(.appendChild grid-el row-el))) |
|
(set! (.-display grid-style) "flex") |
|
(set! (.-flexDirection grid-style) "column") |
|
grid-el)) |
|
|
|
(defn clear-board! [] |
|
(println "Clearing board") |
|
(let [root (.getElementById js/document "board")] |
|
(set! (.-innerHTML root) "") |
|
nil)) |
|
|
|
(defn draw-board! [] |
|
(println "Drawing board") |
|
(let [{:keys [grid board-size]} @game-state |
|
root (.getElementById js/document "board")] |
|
(.appendChild root (tile-grid->element board-size grid)) |
|
nil)) |
|
|
|
(defn coord->struct-report |
|
"Given a coord, report its connected structure and if that structure is alive. |
|
Returns nil for unclaimed territory." |
|
([ctx coord] |
|
(coord->struct-report ctx coord #{})) |
|
([{:keys [grid valid-neighbors] :as ctx} coord visited] |
|
(let [{:keys [claimed-by claimed-turn]} (grid coord)] |
|
(if (nil? claimed-by) |
|
nil |
|
(let [neighbors (map grid (remove visited (valid-neighbors coord))) |
|
blanks (filter #(nil? (:claimed-by %)) neighbors) |
|
;; TODO - Fix the "real" recursion, use "recur" instead |
|
allies (map #(coord->struct-report ctx (:coords %) (conj visited coord)) |
|
(filter #(= claimed-by (:claimed-by %)) neighbors))] |
|
{:found-life? (if (empty? allies) |
|
(boolean (seq blanks)) |
|
(reduce #(or %1 %2) (concat (list (boolean (seq blanks))) |
|
(map :found-life? allies)))) |
|
:claimed-by claimed-by |
|
:claimed-turn (if (empty? allies) |
|
claimed-turn |
|
(apply max (concat (list claimed-turn) (map :claimed-turn allies)))) |
|
:struct-coords (if (empty? allies) |
|
#{coord} |
|
(apply conj #{coord} (apply concat (map :struct-coords allies))))}))))) |
|
|
|
(defn update-selected-space [{:keys [clicked-coords next-turn-user next-turn-count] :as ctx}] |
|
(-> ctx |
|
(assoc-in [:grid clicked-coords :claimed-by] next-turn-user) |
|
(assoc-in [:grid clicked-coords :claimed-turn] next-turn-count))) |
|
|
|
(defn remove-dead-stones [{:keys [grid clicked-coords valid-neighbors] :as ctx}] |
|
(let [home-team (get-in grid [clicked-coords :claimed-by]) |
|
candidates (filter #(not= home-team (:claimed-by %)) |
|
(map grid (valid-neighbors clicked-coords))) |
|
reports (map #(coord->struct-report ctx (:coords %)) candidates) |
|
spaces-to-reset (apply concat (map :struct-coords |
|
(filter #(false? (:found-life? %)) reports)))] |
|
(reduce #(assoc-in %1 [:grid %2 :claimed-by] nil) |
|
ctx |
|
spaces-to-reset))) |
|
|
|
(defn tick-game-state [{:keys [next-turn-user next-turn-count] :as ctx}] |
|
(-> ctx |
|
(assoc :next-turn-user (if (= :blue next-turn-user) :red :blue)) |
|
(assoc :next-turn-count (inc next-turn-count)))) |
|
|
|
(defn process-next-move [clicked-coords] |
|
(println "Processing click for " (str clicked-coords)) |
|
(let [do-update #(-> % |
|
(assoc :clicked-coords clicked-coords) |
|
update-selected-space |
|
remove-dead-stones |
|
tick-game-state)] |
|
(swap! game-state do-update) |
|
(clear-board!) |
|
(draw-board!))) |
|
|
|
(.addEventListener js/document "load" (fn [_] (draw-board!))) |