peer-kit / src / ardegazu / peer / games / tessera.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
;; ported-from: src/games/tessera.ts @ v1.3.0
;;
;; tessera (game3) — the mosaic race. Pilgrims wander a wrapping torus grid,
;; painting the cell they stand on; blooms ("nb") spawn and splash the seats
;; that reach them ("sp"); the biggest mosaic at round end wins. No deaths — a
;; bot just has to keep moving with intent.
;;
;; Client side: send "hi"/"in" — receive "ro","st","tk","ov","ma","lo","fu","sn".
;;
;; Host election: tessera browsers put `live: 1` on their "ro" while they host a
;; round, so claimBeats below overrides the base lowest-id rule with
;; live-beats-idle — the bot and the browsers must apply the SAME comparator or a
;; mixed room partitions (see room.cljs's header).
(ns ardegazu.peer.games.tessera
  (:require [ardegazu.peer.games.room :as room]
            [shadow.cljs.modern :refer (defclass)]))

(declare wire steer)

(defclass TesseraClient
  (extends room/GameRoom)
  (constructor [this peer opts]
    (super peer (let [o (js/Object.assign (js-obj) opts)]
                  (unchecked-set o "hiExtra" (fn [] (js-obj "score" (unchecked-get this "points"))))
                  o))
    (unchecked-set this "points" 0)
    (unchecked-set this "_seat" -1)
    (unchecked-set this "_dir" (js/Math.floor (* (js/Math.random) 4)))
    (unchecked-set this "_pos" #js [0 0])
    (unchecked-set this "_blooms" #js [])
    (unchecked-set this "_tick" 0)
    (unchecked-set this "_phase" "idle")
    (wire this)))

(unchecked-set TesseraClient "joinTessera"
               (fn [opts]
                 (let [o (js/Object.assign (js-obj) opts)]
                   (unchecked-set o "app" "tessera")
                   ((unchecked-get TesseraClient "join") o))))

(defn- steer
  "Head toward the nearest bloom; otherwise wander with direction persistence."
  [self]
  ;; ~3 turns/s at 125 ms ticks is plenty
  (when (identical? 0 (mod (unchecked-get self "_tick") 3))
    (let [pos (unchecked-get self "_pos")
          x (aget pos 0)
          y (aget pos 1)
          b (aget (unchecked-get self "_blooms") 0)
          want (cond
                 ^boolean (js* "!!(~{})" b)
                 (let [bx (aget b 0) by (aget b 1)]
                   (if (> (js/Math.abs (- bx x)) (js/Math.abs (- by y)))
                     (if (> bx x) 1 3)
                     (if (> by y) 2 0)))

                 (< (js/Math.random) 0.25) (js/Math.floor (* (js/Math.random) 4))

                 :else (unchecked-get self "_dir"))]
      (when-not (identical? want (unchecked-get self "_dir"))
        (unchecked-set self "_dir" want)
        (.sendToHost ^js self (js-obj "t" "in" "d" want)))))
  js/undefined)

(defn- wire [self]
  (.on ^js self "frame"
       (fn [from m]
         (when (identical? from (unchecked-get self "hostId"))
           (let [t (unchecked-get m "t")
                 nn (fn [v d] (if ^boolean (js* "~{} == null" v) d v))
                 my-id (unchecked-get (unchecked-get self "peer") "myId")]
             (cond
               (identical? t "st")
               (let [order (nn (unchecked-get m "order") #js [])
                     starts (nn (unchecked-get m "starts") #js [])]
                 (unchecked-set self "_seat" (.indexOf ^js order my-id))
                 (unchecked-set self "_blooms" #js [])
                 (unchecked-set self "_tick" 0)
                 (unchecked-set self "_phase" "countdown")
                 (let [seat (unchecked-get self "_seat")
                       mine (aget starts seat)]
                   (when (and (>= seat 0) ^boolean (js* "!!(~{})" mine))
                     (unchecked-set self "_pos" #js [(aget mine 0) (aget mine 1)])
                     (unchecked-set self "_dir" (nn (aget mine 2) (unchecked-get self "_dir"))))))

               (identical? t "tk")
               (do
                 (unchecked-set self "_phase" "playing")
                 (unchecked-set self "_tick" (nn (unchecked-get m "n")
                                                 (inc (unchecked-get self "_tick"))))
                 (let [seat (unchecked-get self "_seat")]
                   (.forEach ^js (nn (unchecked-get m "h") #js [])
                             (fn [e] (when (identical? (aget e 0) seat)
                                       (unchecked-set self "_pos" #js [(aget e 1) (aget e 2)]))))
                   (when (js/Array.isArray (unchecked-get m "nb"))
                     (.forEach ^js (unchecked-get m "nb")
                               (fn [b] (.push ^js (unchecked-get self "_blooms") b))))
                   ;; someone splashed the one we chased
                   (when (and (js/Array.isArray (unchecked-get m "sp"))
                              (pos? (.-length ^js (unchecked-get m "sp"))))
                     (.shift ^js (unchecked-get self "_blooms")))
                   (when (>= seat 0) (steer self))))

               (or (identical? t "ov") (identical? t "ma"))
               (let [scores (nn (unchecked-get m "scores") #js [])
                     mine (.find ^js scores (fn [e] (identical? (aget e 0) my-id)))]
                 (unchecked-set self "_phase" (if (identical? t "ov") "roundover" "matchover"))
                 (when ^boolean (js* "!!(~{})" mine) (unchecked-set self "points" (aget mine 1))))

               (identical? t "lo")
               (unchecked-set self "_phase" "lobby")

               (identical? t "sn")
               (let [order (nn (unchecked-get m "order") #js [])]
                 (unchecked-set self "_seat" (.indexOf ^js order my-id))
                 (unchecked-set self "_phase" "playing")
                 (unchecked-set self "_blooms" (.slice ^js (nn (unchecked-get m "bl") #js []))))

               :else nil)))
         js/undefined))
  js/undefined)

(let [proto (.-prototype TesseraClient)]
  (unchecked-set proto "score" (fn [] (this-as self (unchecked-get self "points"))))

  ;; live round beats idle claim, ties to lowest id — game3's bystander rule
  ;; (sim/bystander-claim-wins over round-live?: playing | countdown |
  ;; roundover). Identical in shape to neon-grid's and lampion's; the browsers
  ;; apply it too, and a bot on the base lowest-id default would partition a
  ;; mixed room.
  (unchecked-set
   proto "claimBeats"
   (fn [from m]
     (this-as self
       (if (nil? (unchecked-get self "hostId"))
         true
         (let [their-live (identical? 1 (unchecked-get m "live"))
               phase (unchecked-get self "_phase")
               my-host-live (or (identical? phase "playing")
                                (identical? phase "countdown")
                                (identical? phase "roundover"))]
           (if-not (identical? their-live my-host-live)
             their-live
             (< from (unchecked-get self "hostId")))))))))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/peer-kit.git