train-kit / src / ardegazu / train / episodes.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
;; ported-from: src/episodes.ts @ v1.0.0 (extracted-from: bot/src/rl/episodes.ts @ fa686ee)
;;
;; Episode transitions as append-only JSONL, one file per game per month under
;; <dataDir>/episodes/. Live bots buffer in memory and flush at match end; the
;; trainer tails every bot's files, resuming from byte offsets it keeps.
;;
;; Transition shapes (KeyedTransition / FeatureTransition) are documented in
;; types/index.d.ts; the runtime treats them as opaque JSON lines.
(ns ardegazu.train.episodes
  (:require ["node:fs" :as fs]
            ["node:path" :as path]
            [shadow.cljs.modern :refer (defclass)]))

(defclass EpisodeWriter
  (field dir)
  (field buf)

  (constructor [this data-dir]
    (set! dir (path/join data-dir "episodes"))
    (set! buf #js []))

  Object
  (add [this t]
    (.push buf t)
    js/undefined)

  ;; Write the buffered match to disk (one line per transition).
  (flush [this]
    (when (pos? (.-length buf))
      (let [lines (str (.join (.map buf (fn [t] (js/JSON.stringify t))) "\n") "\n")
            month (.slice (.toISOString (js/Date.)) 0 7)
            ^js head (aget buf 0)
            game (.-g head)]
        (set! buf #js [])
        (try
          (fs/mkdirSync dir #js {:recursive true})
          (fs/appendFileSync (path/join dir (str game "-" month ".jsonl")) lines)
          (catch :default _
            ;; a full disk never kills a match
            nil))))
    js/undefined))

;; `pending` is a getter in the TS API (w.pending, no call) — defclass has no
;; getter form, so it is attached to the prototype directly.
(js/Object.defineProperty
 (.-prototype EpisodeWriter) "pending"
 #js {:get (fn [] (this-as ^js self (.-length (.-buf self))))
      :configurable true})

(defn read-new-transitions
  "Trainer side: read new transitions from every bot's episode files."
  [episode-dirs game ^js state]
  (let [out #js []
        offsets (.-offsets state)
        prefix (str game "-")]
    (dotimes [di (.-length ^js episode-dirs)]
      (let [dir (aget episode-dirs di)
            files (try
                    (.filter (fs/readdirSync dir)
                             (fn [f] (and (.startsWith ^js f prefix)
                                          (.endsWith ^js f ".jsonl"))))
                    (catch :default _ nil))]
        (when (some? files)
          (dotimes [fi (.-length ^js files)]
            (let [f (aget files fi)
                  file-path (path/join dir f)
                  start (let [v (unchecked-get offsets file-path)]
                          (if (nil? v) 0 v))
                  size (try
                         (.-size (fs/statSync file-path))
                         (catch :default _ nil))]
              (when (and (some? size) (> size start))
                (let [chunk (try
                              (.slice ^js (fs/readFileSync file-path "utf8") start)
                              (catch :default _ nil))]
                  (when (some? chunk)
                    (let [end (.lastIndexOf ^js chunk "\n")]
                      ;; end < 0: no complete line yet
                      (when (>= end 0)
                        (let [lines (.split ^js (.slice ^js chunk 0 end) "\n")]
                          (dotimes [li (.-length lines)]
                            (let [line (aget lines li)]
                              (when (pos? (.-length ^js line))
                                (try
                                  (let [t (js/JSON.parse line)]
                                    (when (identical? (.-g ^js t) game)
                                      (.push out t)))
                                  (catch :default _
                                    ;; torn or foreign line — skip
                                    nil))))))
                        (unchecked-set offsets file-path (+ (+ start end) 1))))))))))))
    out))

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