Skip to content

Instantly share code, notes, and snippets.

@Lambeaux
Created August 24, 2022 16:41
Show Gist options
  • Save Lambeaux/3d890f830a5cf4839afeb1fba440b150 to your computer and use it in GitHub Desktop.
Save Lambeaux/3d890f830a5cf4839afeb1fba440b150 to your computer and use it in GitHub Desktop.
Simple ClojureScript rendition of the game GO, drawn with tile squares instead of stones on intersections.

Basic implementation of GO using vanilla CLJS. No frameworks. No libraries.

Run to start the game at the REPL:

clj -M --main cljs.main --compile game-of-go.core --repl
(in-ns 'game-of-go.core)
(draw-board!)
{:deps {org.clojure/clojurescript {:mvn/version "1.10.758"}}}
(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!)))
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="out/main.js" type="text/javascript"></script>
<h1>Go</h1>
<hr/>
<div id="board">
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment