nx_folkgame_lib.nx source
↩ module page · 1133 lines · 47516 B
1// nx_folkgame_lib.nx -- THE LUDEME SUBSTRATE: one shared board-game core that reads a game as
2// DATA and plays it, so a new folk game is a SPEC and never a new organ. Named for the ludeme --
3// Browne's unit of game rule -- because the deliverable is the VOCABULARY, not the game list: the
4// 117 rows of knowledge/compare/folkgames.registry exist to prove the vocabulary spans real human
5// game design well enough that novel games can later be SEARCHED out of it (the Ludi/Yavalath
6// result). A game that needs a new organ is a hole in the vocabulary and must be reported as one.
7//
8// NO CAPS, BY CONSTRUCTION. Every array here is sized from the spec itself in a MEASURE pass and
9// filled in a SECOND pass, so there is no FG_MAX_CELLS to guess and none to raise later. This is
10// the estate law that a ceiling which has to be guessed is a defect generator in both directions:
11// too small truncates in silence, too large wastes memory, and raising it only moves the guess.
12//
13// THE SPEC (rows, pipe-delimited, '#' comments). Every key is OPTIONAL except cells and linelen;
14// an unknown key is REFUSED BY NAME rather than ignored, because a silently-dropped rule is a game
15// that plays wrongly while looking parsed.
16// name|<slug>|<Title>
17// family|align
18// cells|<n> board size. REQUIRED.
19// linelen|<n> how many in a row makes a line. REQUIRED.
20// hand|<n> pieces each side places before movement. ABSENT = place until full.
21// win|line forming a line wins outright (Tic-Tac-Toe, Gomoku)
22// win|reduce|<n> reducing the opponent below <n> pieces wins (the Morris family)
23// capture|onmill completing a line removes one enemy piece
24// move|adj after hands are empty, step to an ADJACENT empty cell
25// fly|<n> at or below <n> pieces a side may move to ANY empty cell
26// lines|grid|<w>|<h>|<len> GENERATE every line on a w x h grid -- the generator, not 572 rows
27// line|a,b,c[,...] one explicit line (the Morris boards, which are not grids)
28// adj|grid|<w>|<h>|<mode> GENERATE grid adjacency. mode 0 orthogonal, 1 diagonal, 2 both
29// adj|<cell>|<n1,n2,...> one explicit adjacency row
30// xy|<cell>|<x>|<y> explicit draw position for a non-grid board
31//
32// license_tier: ORIGINAL No hw writes (Rule 26). Sovereign syscalls only, integer only, no float.
33import "nx_syscalls.nx"
34// THE TWO-KNOWLEDGE-TREES DEFECT, solved by composing the estate's own resolver instead of adding
35// a third private copy of the probe. A folkgame spec lives under buildroot/knowledge/ while a
36// forked organ runs with CWD=nishihost, so a bare relative read finds nothing and reports the
37// artifact absent while it sits there perfectly readable. ep_artifact_path already carries the
38// probe order and the fail-closed contract; every consumer of this lib inherits both for free.
39import "nx_estate_path.nx"
40
41// Path buffer for that resolver. 1024 is not a length chosen here -- it is EP_MAGIC_1024, the
42// buffer ep_join writes into, so this matches the contract of the function it feeds.
43const FG_PATHCAP: i64 = 1024
44
45// The nx_cc lexer forbids '#' inside a string literal, so the comment marker is a NAMED byte
46// constant. That is one named byte with a meaning, not a string spelled as character codes.
47const FG_HASH: i64 = 35
48const FG_NL: i64 = 10
49const FG_CR: i64 = 13
50const FG_PIPE: i64 = 124
51const FG_COMMA: i64 = 44
52const FG_MINUS: i64 = 45
53const FG_ZERO: i64 = 48
54const FG_NINE: i64 = 57
55const FG_DECIMAL: i64 = 10
56const FG_WORD: i64 = 8
57// A signed 64-bit value is at most 19 digits plus a sign plus a terminator; 28 is the same scratch
58// width nx_gate_verdict uses for the identical job, matched deliberately rather than re-chosen.
59const FG_NUMSCRATCH: i64 = 28
60const FG_MISS: i64 = 0 - 999999
61
62// ---- header: counts AND the computed offsets, so the image stays BASE-RELATIVE (wasm-safe) ----
63const FG_H_CELLS: i64 = 0
64const FG_H_HAND: i64 = 1
65const FG_H_LINELEN: i64 = 2
66const FG_H_WINMODE: i64 = 3
67const FG_H_REDUCE: i64 = 4
68const FG_H_CAPTURE: i64 = 5
69const FG_H_MOVEADJ: i64 = 6
70const FG_H_FLYAT: i64 = 7
71const FG_H_NLINES: i64 = 8
72const FG_H_LINEW: i64 = 9
73const FG_H_ADJW: i64 = 10
74const FG_H_OFF_LINES: i64 = 11
75const FG_H_OFF_ADJ: i64 = 12
76const FG_H_OFF_ADJN: i64 = 13
77const FG_H_OFF_XY: i64 = 14
78const FG_H_OFF_STATE: i64 = 15
79const FG_H_OFF_SCAL: i64 = 16
80const FG_H_BYTES: i64 = 17
81// Layout for the renderer, set from a lines|grid row so every cell gets an (x,y) without the spec
82// repeating them. An explicit xy| row still wins, which is how the Morris boards are drawn.
83const FG_H_GRIDW: i64 = 18
84const FG_H_GRIDH: i64 = 19
85// ---- SOW FAMILY (mancala) header slots, added 2026-08-26 ----------------------------------------
86// The image structure is REUSED rather than forked: cells are pits, state[c] is a SEED COUNT instead
87// of an occupancy code, and the scalars still carry side and terminal state. One parser, one image,
88// one save/restore -- a second sow-shaped copy of all three is the duplicate-ruler defect, and the
89// two would drift the first time a shared rule changed.
90// LAYOUT, fixed by construction so no spec has to restate it: cells 0..perside-1 are the first
91// player's pits, perside..2*perside-1 the second player's, and when store=1 cell 2*perside is the
92// first player's store and 2*perside+1 the second's. Sowing runs in increasing index and wraps.
93const FG_H_FAMILY: i64 = 20
94const FG_H_PERSIDE: i64 = 21
95const FG_H_SEEDS: i64 = 22
96const FG_H_STORE: i64 = 23
97const FG_H_LAP: i64 = 24
98const FG_H_CAPMODE: i64 = 25
99const FG_H_CAPA: i64 = 26
100const FG_H_CAPB: i64 = 27
101// ---- RACE FAMILY (track games) header slots, added 2026-08-27 ------------------------------------
102// Twenty-two registry rows are race games, from Senet and the Royal Game of Ur through Backgammon to
103// Pachisi. This engine covers the TRACK sub-family: a linear course, a randomiser, optional entry
104// from a bar, optional bearing off past the end, hitting a lone enemy piece and blocking a point.
105// The cross-and-circle boards (Pachisi, Ludo, Yut Nori, Patolli) fold their four arms onto one track
106// per player and are a later rung; the spiral and ladder boards (Goose, Snakes and Ladders) need
107// special-square rules that are a third. Saying which of the twenty-two this engine actually reaches
108// is the difference between a family row and a family claim.
109// STATE ENCODING for this family: state[c] is a SIGNED count -- positive is the first player, negative
110// the second -- which is the representation the game itself has, and it makes a point's owner and its
111// height one number instead of two that can disagree.
112const FG_H_TRACK: i64 = 28
113const FG_H_PIECES: i64 = 29
114const FG_H_DICEN: i64 = 30
115const FG_H_DICESIDES: i64 = 31
116const FG_H_ENTRY: i64 = 32
117const FG_H_BEAROFF: i64 = 33
118const FG_H_HIT: i64 = 34
119const FG_H_BLOCK: i64 = 35
120// The race family's opening layout, one signed count per track point, stored in its OWN array.
121// It is deliberately NOT folded into the xy array even though xy is unused by this family: a
122// dual-purpose array is a defect generator, and the one place it would be cheapest is exactly where
123// a later reader would be most surprised to find a second meaning.
124const FG_H_OFF_START: i64 = 36
125const FG_H_N: i64 = 40
126
127const FG_FAM_RACE: i64 = 2
128
129const FG_FAM_ALIGN: i64 = 0
130const FG_FAM_SOW: i64 = 1
131// Oware captures when the last seed leaves a pit holding exactly capA or capB (two or three).
132// Kalah captures when the last seed lands in one of your OWN empty pits, taking the pit opposite.
133const FG_CAP_NONE: i64 = 0
134const FG_CAP_COUNT: i64 = 1
135const FG_CAP_EMPTY: i64 = 2
136
137// ---- mutable scalars ----
138const FG_S_SIDE: i64 = 0
139const FG_S_HAND1: i64 = 1
140const FG_S_HAND2: i64 = 2
141const FG_S_ON1: i64 = 3
142const FG_S_ON2: i64 = 4
143const FG_S_PHASE: i64 = 5
144const FG_S_WINNER: i64 = 6
145const FG_S_PLIES: i64 = 7
146// Slots 8..11 are family-private: the sow family keeps its captured-seed scores there, the race
147// family its bar and borne-off counts. Slot 12 is the race randomiser's state, which MUST live in
148// the saved scalars rather than beside them -- a seed that is not part of the position is a seed a
149// save/restore silently loses, and replay would then diverge from the game it claims to replay.
150// Raised 12 -> 16 on 2026-08-27. fg_save and fg_load copy FG_S_N words, so the widening carries the
151// seed automatically and no caller changes.
152const FG_S_N: i64 = 16
153
154const FG_PH_PLACE: i64 = 0
155const FG_PH_MOVE: i64 = 1
156const FG_PH_CAPTURE: i64 = 2
157
158const FG_WIN_LINE: i64 = 0
159const FG_WIN_REDUCE: i64 = 1
160
161const FG_MK_PLACE: i64 = 0
162const FG_MK_MOVE: i64 = 1
163const FG_MK_REMOVE: i64 = 2
164const FG_MV_KIND_SH: i64 = 32
165const FG_MV_FROM_SH: i64 = 16
166const FG_MV_MASK: i64 = 65535
167
168// terminal codes
169const FG_T_ONGOING: i64 = 0
170const FG_T_P1: i64 = 1
171const FG_T_P2: i64 = 2
172const FG_T_DRAW: i64 = 3
173
174const FG_ADJ_ORTH: i64 = 0
175const FG_ADJ_DIAG: i64 = 1
176const FG_ADJ_BOTH: i64 = 2
177
178// Line-set modes. Dara scores rows and columns and NOT diagonals; Tic-Tac-Toe and Gomoku score
179// diagonals too. A spec that omits the trailing mode field gets FG_LINE_ALL, the commoner board,
180// so every spec written before this field existed keeps its meaning.
181const FG_LINE_ORTH: i64 = 0
182const FG_LINE_ALL: i64 = 1
183const FG_DEG_ORTH: i64 = 4
184const FG_DEG_BOTH: i64 = 8
185
186func fg_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(2, s, n); return 0 }
187// Decimal to stderr. A refusal that will not say how many bytes it wanted forces the caller to guess,
188// and guessing a buffer size is the defect this file is built to avoid. Diagnostic path only.
189func fg_num2(v: i64) -> i64 {
190 let b: *u8 = sys_mmap(FG_NUMSCRATCH)
191 let t: *u8 = sys_mmap(FG_NUMSCRATCH)
192 var m: i64 = v
193 if m < 0 { m = 0 - m; sys_write(2, "-" as *u8, 1) }
194 var k: i64 = 0
195 if m == 0 { t[0] = FG_ZERO as u8; k = 1 }
196 while m > 0 { t[k] = (FG_ZERO + (m % FG_DECIMAL)) as u8; m = m / FG_DECIMAL; k = k + 1 }
197 var i: i64 = 0
198 while i < k { b[i] = t[k - 1 - i]; i = i + 1 }
199 sys_write(2, b, k)
200 sys_munmap(b, FG_NUMSCRATCH)
201 sys_munmap(t, FG_NUMSCRATCH)
202 return 0
203}
204
205// ---- parse helpers ---------------------------------------------------------------------------
206func fg_lineend(buf: *u8, n: i64, i: i64) -> i64 {
207 var e: i64 = i
208 while e < n {
209 if buf[e] == (FG_NL as u8) { return e }
210 e = e + 1
211 }
212 return n
213}
214func fg_keyis(buf: *u8, ls: i64, le: i64, key: *u8) -> i64 {
215 var k: i64 = 0
216 while key[k] != (0 as u8) {
217 if ls + k >= le { return 0 }
218 if buf[ls + k] != key[k] { return 0 }
219 k = k + 1
220 }
221 if ls + k >= le { return 0 }
222 if buf[ls + k] != (FG_PIPE as u8) { return 0 }
223 return 1
224}
225func fg_fieldoff(buf: *u8, ls: i64, le: i64, k: i64) -> i64 {
226 if k == 0 { return ls }
227 var f: i64 = 0
228 var i: i64 = ls
229 while i < le {
230 if buf[i] == (FG_PIPE as u8) {
231 f = f + 1
232 if f == k { return i + 1 }
233 }
234 i = i + 1
235 }
236 return 0 - 1
237}
238func fg_wordis(buf: *u8, o: i64, le: i64, w: *u8) -> i64 {
239 if o < 0 { return 0 }
240 var k: i64 = 0
241 while w[k] != (0 as u8) {
242 if o + k >= le { return 0 }
243 if buf[o + k] != w[k] { return 0 }
244 k = k + 1
245 }
246 return 1
247}
248func fg_int_at(buf: *u8, o: i64, le: i64) -> i64 {
249 if o < 0 { return FG_MISS }
250 var i: i64 = o
251 var neg: i64 = 0
252 if i < le { if buf[i] == (FG_MINUS as u8) { neg = 1; i = i + 1 } }
253 var v: i64 = 0
254 var d: i64 = 0
255 while i < le {
256 let c: i64 = buf[i] as i64
257 if c < FG_ZERO { break }
258 if c > FG_NINE { break }
259 v = v * FG_DECIMAL + (c - FG_ZERO)
260 d = d + 1
261 i = i + 1
262 }
263 if d == 0 { return FG_MISS }
264 if neg == 1 { return 0 - v }
265 return v
266}
267func fg_field_int(buf: *u8, ls: i64, le: i64, k: i64) -> i64 {
268 return fg_int_at(buf, fg_fieldoff(buf, ls, le, k), le)
269}
270// count of comma-separated integers from o to le
271func fg_listn(buf: *u8, o: i64, le: i64) -> i64 {
272 if o < 0 { return 0 }
273 if o >= le { return 0 }
274 var c: i64 = 1
275 var i: i64 = o
276 while i < le {
277 if buf[i] == (FG_COMMA as u8) { c = c + 1 }
278 i = i + 1
279 }
280 return c
281}
282func fg_listat(buf: *u8, o: i64, le: i64, k: i64) -> i64 {
283 if o < 0 { return FG_MISS }
284 var f: i64 = 0
285 var i: i64 = o
286 while i < le {
287 if f == k { return fg_int_at(buf, i, le) }
288 if buf[i] == (FG_COMMA as u8) { f = f + 1 }
289 i = i + 1
290 }
291 return FG_MISS
292}
293
294// ---- the line-set GENERATOR: how many lines a w x h grid of run-length L carries -------------
295func fg_gridlines_n(w: i64, h: i64, l: i64, mode: i64) -> i64 {
296 var t: i64 = 0
297 if w >= l { t = t + h * (w - l + 1) }
298 if h >= l { t = t + w * (h - l + 1) }
299 if mode == FG_LINE_ALL {
300 if w >= l { if h >= l { t = t + 2 * (w - l + 1) * (h - l + 1) } }
301 }
302 return t
303}
304
305func fg_hdr(base: i64) -> *i64 { return base as *i64 }
306func fg_cells(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_CELLS] }
307func fg_linelen(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_LINELEN] }
308func fg_nlines(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_NLINES] }
309func fg_bytes(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_BYTES] }
310func fg_state(base: i64) -> *i64 { let hd: *i64 = fg_hdr(base); return (base + hd[FG_H_OFF_STATE]) as *i64 }
311func fg_scal(base: i64) -> *i64 { let hd: *i64 = fg_hdr(base); return (base + hd[FG_H_OFF_SCAL]) as *i64 }
312func fg_at(base: i64, c: i64) -> i64 { let s: *i64 = fg_state(base); return s[c] }
313func fg_side(base: i64) -> i64 { let s: *i64 = fg_scal(base); return s[FG_S_SIDE] }
314func fg_phase(base: i64) -> i64 { let s: *i64 = fg_scal(base); return s[FG_S_PHASE] }
315func fg_line_cell(base: i64, li: i64, k: i64) -> i64 {
316 let hd: *i64 = fg_hdr(base)
317 let ln: *i64 = (base + hd[FG_H_OFF_LINES]) as *i64
318 return ln[li * hd[FG_H_LINEW] + k]
319}
320func fg_adj_n(base: i64, c: i64) -> i64 {
321 let hd: *i64 = fg_hdr(base)
322 let an: *i64 = (base + hd[FG_H_OFF_ADJN]) as *i64
323 return an[c]
324}
325func fg_adj_at(base: i64, c: i64, k: i64) -> i64 {
326 let hd: *i64 = fg_hdr(base)
327 let aj: *i64 = (base + hd[FG_H_OFF_ADJ]) as *i64
328 return aj[c * hd[FG_H_ADJW] + k]
329}
330func fg_xy_x(base: i64, c: i64) -> i64 {
331 let hd: *i64 = fg_hdr(base)
332 let xy: *i64 = (base + hd[FG_H_OFF_XY]) as *i64
333 return xy[c * 2]
334}
335func fg_xy_y(base: i64, c: i64) -> i64 {
336 let hd: *i64 = fg_hdr(base)
337 let xy: *i64 = (base + hd[FG_H_OFF_XY]) as *i64
338 return xy[c * 2 + 1]
339}
340
341// The side to move is PLACING while either hand still holds a piece; a spec with no hand row
342// places until the board fills (Tic-Tac-Toe), and termination is then the board-full draw.
343func fg_setphase(base: i64) -> i64 {
344 let hd: *i64 = fg_hdr(base)
345 let sc: *i64 = fg_scal(base)
346 var placing: i64 = 0
347 if hd[FG_H_HAND] == 0 { placing = 1 }
348 if hd[FG_H_HAND] > 0 {
349 if sc[FG_S_HAND1] > 0 { placing = 1 }
350 if sc[FG_S_HAND2] > 0 { placing = 1 }
351 }
352 if placing == 1 { sc[FG_S_PHASE] = FG_PH_PLACE; return 0 }
353 sc[FG_S_PHASE] = FG_PH_MOVE
354 return 0
355}
356
357func fg_reset(base: i64) -> i64 {
358 let hd: *i64 = fg_hdr(base)
359 let st: *i64 = fg_state(base)
360 let sc: *i64 = fg_scal(base)
361 var i: i64 = 0
362 while i < hd[FG_H_CELLS] { st[i] = 0; i = i + 1 }
363 sc[FG_S_SIDE] = 1
364 sc[FG_S_HAND1] = hd[FG_H_HAND]
365 sc[FG_S_HAND2] = hd[FG_H_HAND]
366 sc[FG_S_ON1] = 0
367 sc[FG_S_ON2] = 0
368 sc[FG_S_WINNER] = 0
369 sc[FG_S_PLIES] = 0
370 fg_setphase(base)
371 return 0
372}
373
374func fg_count(base: i64, side: i64) -> i64 {
375 let hd: *i64 = fg_hdr(base)
376 let st: *i64 = fg_state(base)
377 var n: i64 = 0
378 var i: i64 = 0
379 while i < hd[FG_H_CELLS] {
380 if st[i] == side { n = n + 1 }
381 i = i + 1
382 }
383 return n
384}
385
386// Does `side` own a COMPLETE line that passes through `cell`? One predicate serves three callers:
387// the win test, the mill-formed test, and the Morris rule that a piece standing in a mill may not
388// be captured while a free enemy piece exists.
389func fg_forms_line(base: i64, cell: i64, side: i64) -> i64 {
390 let hd: *i64 = fg_hdr(base)
391 let st: *i64 = fg_state(base)
392 var li: i64 = 0
393 while li < hd[FG_H_NLINES] {
394 var has: i64 = 0
395 var all: i64 = 1
396 var k: i64 = 0
397 while k < hd[FG_H_LINEW] {
398 let c: i64 = fg_line_cell(base, li, k)
399 if c == cell { has = 1 }
400 if st[c] != side { all = 0 }
401 k = k + 1
402 }
403 if has == 1 { if all == 1 { return 1 } }
404 li = li + 1
405 }
406 return 0
407}
408
409// A DERIVED bound, not a guessed cap: no position can offer more than cells*cells moves, because
410// every move is at most one (from,to) pair over the board. Callers size their scratch from this.
411func fg_maxmoves(base: i64) -> i64 {
412 let hd: *i64 = fg_hdr(base)
413 return hd[FG_H_CELLS] * hd[FG_H_CELLS]
414}
415
416func fg_moves(base: i64, out: *i64) -> i64 {
417 let hd: *i64 = fg_hdr(base)
418 let sc: *i64 = fg_scal(base)
419 let side: i64 = sc[FG_S_SIDE]
420 var n: i64 = 0
421 if sc[FG_S_WINNER] != 0 { return 0 }
422 let ph: i64 = sc[FG_S_PHASE]
423 if ph == FG_PH_CAPTURE {
424 let opp: i64 = 3 - side
425 var free: i64 = 0
426 var i: i64 = 0
427 while i < hd[FG_H_CELLS] {
428 if fg_at(base, i) == opp {
429 if fg_forms_line(base, i, opp) == 0 { free = free + 1 }
430 }
431 i = i + 1
432 }
433 var j: i64 = 0
434 while j < hd[FG_H_CELLS] {
435 if fg_at(base, j) == opp {
436 var ok: i64 = 1
437 if free > 0 {
438 if fg_forms_line(base, j, opp) == 1 { ok = 0 }
439 }
440 if ok == 1 { out[n] = (FG_MK_REMOVE << FG_MV_KIND_SH) | j; n = n + 1 }
441 }
442 j = j + 1
443 }
444 return n
445 }
446 if ph == FG_PH_PLACE {
447 var i2: i64 = 0
448 while i2 < hd[FG_H_CELLS] {
449 if fg_at(base, i2) == 0 { out[n] = (FG_MK_PLACE << FG_MV_KIND_SH) | i2; n = n + 1 }
450 i2 = i2 + 1
451 }
452 return n
453 }
454 let myn: i64 = fg_count(base, side)
455 var flying: i64 = 0
456 if hd[FG_H_FLYAT] > 0 {
457 if myn <= hd[FG_H_FLYAT] { flying = 1 }
458 }
459 var f: i64 = 0
460 while f < hd[FG_H_CELLS] {
461 if fg_at(base, f) == side {
462 if flying == 1 {
463 var t: i64 = 0
464 while t < hd[FG_H_CELLS] {
465 if fg_at(base, t) == 0 {
466 out[n] = (FG_MK_MOVE << FG_MV_KIND_SH) | (f << FG_MV_FROM_SH) | t
467 n = n + 1
468 }
469 t = t + 1
470 }
471 }
472 if flying == 0 {
473 var k: i64 = 0
474 while k < fg_adj_n(base, f) {
475 let t2: i64 = fg_adj_at(base, f, k)
476 if fg_at(base, t2) == 0 {
477 out[n] = (FG_MK_MOVE << FG_MV_KIND_SH) | (f << FG_MV_FROM_SH) | t2
478 n = n + 1
479 }
480 k = k + 1
481 }
482 }
483 }
484 f = f + 1
485 }
486 return n
487}
488
489func fg_mv_kind(mv: i64) -> i64 { return mv >> FG_MV_KIND_SH }
490func fg_mv_to(mv: i64) -> i64 { return mv & FG_MV_MASK }
491func fg_mv_from(mv: i64) -> i64 { return (mv >> FG_MV_FROM_SH) & FG_MV_MASK }
492
493func fg_apply(base: i64, mv: i64) -> i64 {
494 let hd: *i64 = fg_hdr(base)
495 let st: *i64 = fg_state(base)
496 let sc: *i64 = fg_scal(base)
497 let side: i64 = sc[FG_S_SIDE]
498 let kind: i64 = fg_mv_kind(mv)
499 let to: i64 = fg_mv_to(mv)
500 let from: i64 = fg_mv_from(mv)
501 var made: i64 = 0
502 if kind == FG_MK_REMOVE { st[to] = 0 }
503 if kind == FG_MK_PLACE {
504 st[to] = side
505 if side == 1 { if sc[FG_S_HAND1] > 0 { sc[FG_S_HAND1] = sc[FG_S_HAND1] - 1 } }
506 if side == 2 { if sc[FG_S_HAND2] > 0 { sc[FG_S_HAND2] = sc[FG_S_HAND2] - 1 } }
507 if fg_forms_line(base, to, side) == 1 { made = 1 }
508 }
509 if kind == FG_MK_MOVE {
510 st[from] = 0
511 st[to] = side
512 if fg_forms_line(base, to, side) == 1 { made = 1 }
513 }
514 sc[FG_S_PLIES] = sc[FG_S_PLIES] + 1
515 if kind != FG_MK_REMOVE {
516 if made == 1 {
517 if hd[FG_H_WINMODE] == FG_WIN_LINE { sc[FG_S_WINNER] = side; return 0 }
518 if hd[FG_H_CAPTURE] == 1 { sc[FG_S_PHASE] = FG_PH_CAPTURE; return 0 }
519 }
520 }
521 sc[FG_S_SIDE] = 3 - side
522 fg_setphase(base)
523 return 0
524}
525
526// scratch must hold fg_maxmoves(base) words. Passed IN rather than allocated here: this is called
527// once per node of a search, and allocating in a hot loop is the defect that cost this estate 640MB
528// on a single census.
529func fg_terminal(base: i64, scratch: *i64) -> i64 {
530 let hd: *i64 = fg_hdr(base)
531 let sc: *i64 = fg_scal(base)
532 if sc[FG_S_WINNER] == 1 { return FG_T_P1 }
533 if sc[FG_S_WINNER] == 2 { return FG_T_P2 }
534 if hd[FG_H_WINMODE] == FG_WIN_REDUCE {
535 var placing: i64 = 0
536 if sc[FG_S_HAND1] > 0 { placing = 1 }
537 if sc[FG_S_HAND2] > 0 { placing = 1 }
538 if placing == 0 {
539 if fg_count(base, 1) < hd[FG_H_REDUCE] { return FG_T_P2 }
540 if fg_count(base, 2) < hd[FG_H_REDUCE] { return FG_T_P1 }
541 }
542 }
543 let n: i64 = fg_moves(base, scratch)
544 if n == 0 {
545 if hd[FG_H_WINMODE] == FG_WIN_LINE { return FG_T_DRAW }
546 if sc[FG_S_SIDE] == 1 { return FG_T_P2 }
547 return FG_T_P1
548 }
549 return FG_T_ONGOING
550}
551
552// ---- THE GENERATORS. A 15x15 Gomoku board carries 572 lines; enumerating them as spec rows
553// would be data entry, and data entry drifts. `lines|grid|w|h|len` states the RULE instead and
554// this function is its one expansion. The count function fg_gridlines_n above and this filler
555// walk the same four families in the same order, so the measured size and the written size
556// cannot disagree.
557func fg_fill_gridlines(base: i64, gw: i64, gh: i64, gl: i64, mode: i64, at: i64) -> i64 {
558 let hd: *i64 = fg_hdr(base)
559 let ln: *i64 = (base + hd[FG_H_OFF_LINES]) as *i64
560 let lw: i64 = hd[FG_H_LINEW]
561 var idx: i64 = at
562 var y: i64 = 0
563 while y < gh {
564 var x: i64 = 0
565 while x + gl <= gw {
566 var k: i64 = 0
567 while k < gl { ln[idx * lw + k] = y * gw + x + k; k = k + 1 }
568 idx = idx + 1
569 x = x + 1
570 }
571 y = y + 1
572 }
573 var x2: i64 = 0
574 while x2 < gw {
575 var y2: i64 = 0
576 while y2 + gl <= gh {
577 var k2: i64 = 0
578 while k2 < gl { ln[idx * lw + k2] = (y2 + k2) * gw + x2; k2 = k2 + 1 }
579 idx = idx + 1
580 y2 = y2 + 1
581 }
582 x2 = x2 + 1
583 }
584 if mode == FG_LINE_ALL {
585 var y3: i64 = 0
586 while y3 + gl <= gh {
587 var x3: i64 = 0
588 while x3 + gl <= gw {
589 var k3: i64 = 0
590 while k3 < gl { ln[idx * lw + k3] = (y3 + k3) * gw + (x3 + k3); k3 = k3 + 1 }
591 idx = idx + 1
592 x3 = x3 + 1
593 }
594 y3 = y3 + 1
595 }
596 var y4: i64 = 0
597 while y4 + gl <= gh {
598 var x4: i64 = gl - 1
599 while x4 < gw {
600 var k4: i64 = 0
601 while k4 < gl { ln[idx * lw + k4] = (y4 + k4) * gw + (x4 - k4); k4 = k4 + 1 }
602 idx = idx + 1
603 x4 = x4 + 1
604 }
605 y4 = y4 + 1
606 }
607 }
608 return idx
609}
610
611func fg_fill_gridadj(base: i64, gw: i64, gh: i64, mode: i64) -> i64 {
612 let hd: *i64 = fg_hdr(base)
613 let aj: *i64 = (base + hd[FG_H_OFF_ADJ]) as *i64
614 let an: *i64 = (base + hd[FG_H_OFF_ADJN]) as *i64
615 let aw: i64 = hd[FG_H_ADJW]
616 var y: i64 = 0
617 while y < gh {
618 var x: i64 = 0
619 while x < gw {
620 let c: i64 = y * gw + x
621 var d: i64 = 0
622 var dy: i64 = 0 - 1
623 while dy <= 1 {
624 var dx: i64 = 0 - 1
625 while dx <= 1 {
626 var use: i64 = 0
627 if dx != 0 { use = 1 }
628 if dy != 0 { use = 1 }
629 var isdiag: i64 = 0
630 if dx != 0 { if dy != 0 { isdiag = 1 } }
631 if mode == FG_ADJ_ORTH { if isdiag == 1 { use = 0 } }
632 if mode == FG_ADJ_DIAG { if isdiag == 0 { use = 0 } }
633 if use == 1 {
634 let tx: i64 = x + dx
635 let ty: i64 = y + dy
636 var ok: i64 = 1
637 if tx < 0 { ok = 0 }
638 if tx >= gw { ok = 0 }
639 if ty < 0 { ok = 0 }
640 if ty >= gh { ok = 0 }
641 if ok == 1 { aj[c * aw + d] = ty * gw + tx; d = d + 1 }
642 }
643 dx = dx + 1
644 }
645 dy = dy + 1
646 }
647 an[c] = d
648 x = x + 1
649 }
650 y = y + 1
651 }
652 return 0
653}
654
655// ---- fg_parse: MEASURE, allocate exactly, then FILL. Returns the image base, or 0 having said
656// WHY on stderr. An unknown key is REFUSED BY NAME: a silently ignored rule is a game that plays
657// wrongly while looking parsed, which is the worst of the three outcomes.
658// arena==0 means allocate here; a non-zero arena writes the image into CALLER memory instead, which
659// is what the WebAssembly target needs -- a wasm module has linear memory and no mmap, so the browser
660// build passes a fixed base and a cap. ONE parser serves native and wasm; a second wasm-shaped copy
661// would be the duplicate-ruler defect, and the two would drift the first time a rule changed.
662func fg_parse_arena(buf: *u8, n: i64, arena: i64, cap: i64) -> i64 {
663 var cells: i64 = FG_MISS
664 var linelen: i64 = FG_MISS
665 var hand: i64 = 0
666 var winmode: i64 = FG_WIN_LINE
667 var reduce: i64 = 0
668 var capture: i64 = 0
669 var moveadj: i64 = 0
670 var flyat: i64 = 0
671 var nlines: i64 = 0
672 var adjw: i64 = 0
673 var lmin: i64 = FG_MISS
674 var lmax: i64 = 0
675 var fam: i64 = FG_FAM_ALIGN
676 var track: i64 = FG_MISS
677 var pieces: i64 = FG_MISS
678 var dicen: i64 = FG_MISS
679 var dicesides: i64 = FG_MISS
680 var entry: i64 = 0
681 var bearoff: i64 = 0
682 var hitrule: i64 = 0
683 var block: i64 = 0
684 var perside: i64 = FG_MISS
685 var seeds: i64 = FG_MISS
686 var store: i64 = 0
687 var lap: i64 = 0
688 var capmode: i64 = FG_CAP_NONE
689 var capa: i64 = 0
690 var capb: i64 = 0
691 var bad: i64 = 0
692 var i: i64 = 0
693 while i < n {
694 let le: i64 = fg_lineend(buf, n, i)
695 var skip: i64 = 0
696 if i >= le { skip = 1 }
697 if skip == 0 { if buf[i] == (FG_HASH as u8) { skip = 1 } }
698 if skip == 0 { if buf[i] == (FG_CR as u8) { skip = 1 } }
699 if skip == 0 {
700 var hit: i64 = 0
701 if fg_keyis(buf, i, le, "name" as *u8) == 1 { hit = 1 }
702 if fg_keyis(buf, i, le, "family" as *u8) == 1 {
703 hit = 1
704 let ofam: i64 = fg_fieldoff(buf, i, le, 1)
705 if fg_wordis(buf, ofam, le, "sow" as *u8) == 1 { fam = FG_FAM_SOW }
706 if fg_wordis(buf, ofam, le, "race" as *u8) == 1 { fam = FG_FAM_RACE }
707 }
708 if fg_keyis(buf, i, le, "track" as *u8) == 1 { track = fg_field_int(buf, i, le, 1); hit = 1 }
709 if fg_keyis(buf, i, le, "pieces" as *u8) == 1 { pieces = fg_field_int(buf, i, le, 1); hit = 1 }
710 if fg_keyis(buf, i, le, "dice" as *u8) == 1 {
711 hit = 1
712 dicen = fg_field_int(buf, i, le, 1)
713 dicesides = fg_field_int(buf, i, le, 2)
714 }
715 if fg_keyis(buf, i, le, "entry" as *u8) == 1 { entry = fg_field_int(buf, i, le, 1); hit = 1 }
716 if fg_keyis(buf, i, le, "bearoff" as *u8) == 1 { bearoff = fg_field_int(buf, i, le, 1); hit = 1 }
717 if fg_keyis(buf, i, le, "hit" as *u8) == 1 { hitrule = fg_field_int(buf, i, le, 1); hit = 1 }
718 if fg_keyis(buf, i, le, "block" as *u8) == 1 { block = fg_field_int(buf, i, le, 1); hit = 1 }
719 // start|<point>|<signed count> -- read in the FILL pass, only counted as a known key here.
720 if fg_keyis(buf, i, le, "start" as *u8) == 1 { hit = 1 }
721 if fg_keyis(buf, i, le, "perside" as *u8) == 1 { perside = fg_field_int(buf, i, le, 1); hit = 1 }
722 if fg_keyis(buf, i, le, "seeds" as *u8) == 1 { seeds = fg_field_int(buf, i, le, 1); hit = 1 }
723 if fg_keyis(buf, i, le, "store" as *u8) == 1 { store = fg_field_int(buf, i, le, 1); hit = 1 }
724 if fg_keyis(buf, i, le, "lap" as *u8) == 1 { lap = fg_field_int(buf, i, le, 1); hit = 1 }
725 if fg_keyis(buf, i, le, "xy" as *u8) == 1 { hit = 1 }
726 if fg_keyis(buf, i, le, "cells" as *u8) == 1 { cells = fg_field_int(buf, i, le, 1); hit = 1 }
727 if fg_keyis(buf, i, le, "linelen" as *u8) == 1 { linelen = fg_field_int(buf, i, le, 1); hit = 1 }
728 if fg_keyis(buf, i, le, "hand" as *u8) == 1 { hand = fg_field_int(buf, i, le, 1); hit = 1 }
729 if fg_keyis(buf, i, le, "fly" as *u8) == 1 { flyat = fg_field_int(buf, i, le, 1); hit = 1 }
730 if fg_keyis(buf, i, le, "capture" as *u8) == 1 {
731 capture = 1
732 hit = 1
733 let ocap: i64 = fg_fieldoff(buf, i, le, 1)
734 // capture|count|2,3 is Oware: the last seed LEAVES a pit holding one of these counts.
735 // capture|empty is Kalah: the last seed lands in one of your own empty pits.
736 // capture|onmill keeps its align meaning and sets neither, which is why capmode
737 // defaults to FG_CAP_NONE rather than to either sow rule.
738 if fg_wordis(buf, ocap, le, "count" as *u8) == 1 {
739 capmode = FG_CAP_COUNT
740 let olst: i64 = fg_fieldoff(buf, i, le, 2)
741 capa = fg_listat(buf, olst, le, 0)
742 capb = fg_listat(buf, olst, le, 1)
743 if capb == FG_MISS { capb = capa }
744 }
745 if fg_wordis(buf, ocap, le, "empty" as *u8) == 1 { capmode = FG_CAP_EMPTY }
746 }
747 if fg_keyis(buf, i, le, "move" as *u8) == 1 { moveadj = 1; hit = 1 }
748 if fg_keyis(buf, i, le, "win" as *u8) == 1 {
749 hit = 1
750 let o1: i64 = fg_fieldoff(buf, i, le, 1)
751 if fg_wordis(buf, o1, le, "reduce" as *u8) == 1 {
752 winmode = FG_WIN_REDUCE
753 reduce = fg_field_int(buf, i, le, 2)
754 }
755 }
756 if fg_keyis(buf, i, le, "line" as *u8) == 1 {
757 hit = 1
758 let o1b: i64 = fg_fieldoff(buf, i, le, 1)
759 let cnt: i64 = fg_listn(buf, o1b, le)
760 nlines = nlines + 1
761 if cnt > lmax { lmax = cnt }
762 if lmin == FG_MISS { lmin = cnt }
763 if cnt < lmin { lmin = cnt }
764 }
765 if fg_keyis(buf, i, le, "lines" as *u8) == 1 {
766 hit = 1
767 let gw: i64 = fg_field_int(buf, i, le, 2)
768 let gh: i64 = fg_field_int(buf, i, le, 3)
769 let gl: i64 = fg_field_int(buf, i, le, 4)
770 var gm: i64 = fg_field_int(buf, i, le, 5)
771 if gm == FG_MISS { gm = FG_LINE_ALL }
772 nlines = nlines + fg_gridlines_n(gw, gh, gl, gm)
773 }
774 if fg_keyis(buf, i, le, "adj" as *u8) == 1 {
775 hit = 1
776 let o1c: i64 = fg_fieldoff(buf, i, le, 1)
777 let isgrid: i64 = fg_wordis(buf, o1c, le, "grid" as *u8)
778 if isgrid == 1 {
779 let md: i64 = fg_field_int(buf, i, le, 4)
780 var dg: i64 = FG_DEG_ORTH
781 if md == FG_ADJ_BOTH { dg = FG_DEG_BOTH }
782 if dg > adjw { adjw = dg }
783 }
784 if isgrid == 0 {
785 let o2: i64 = fg_fieldoff(buf, i, le, 2)
786 let cnt2: i64 = fg_listn(buf, o2, le)
787 if cnt2 > adjw { adjw = cnt2 }
788 }
789 }
790 if hit == 0 {
791 bad = bad + 1
792 fg_puts("FOLKGAME-REFUSED unknown spec key on line starting: " as *u8)
793 sys_write(2, (buf as i64 + i) as *u8, le - i)
794 fg_puts("\n" as *u8)
795 }
796 }
797 i = le + 1
798 }
799 if bad > 0 { return 0 }
800 if cells == FG_MISS { fg_puts("FOLKGAME-REFUSED spec has no cells row\n" as *u8); return 0 }
801 if cells <= 0 { fg_puts("FOLKGAME-REFUSED cells must be positive\n" as *u8); return 0 }
802 // FAMILY DISPATCH. Each family is REFUSED for missing what IT needs, never for missing what a
803 // different family needs -- a sow game has no lines and demanding one of it would be the align
804 // parser refusing a correct spec and blaming the data.
805 if fam == FG_FAM_SOW {
806 if perside == FG_MISS { fg_puts("FOLKGAME-REFUSED a sow game needs a perside row\n" as *u8); return 0 }
807 if seeds == FG_MISS { fg_puts("FOLKGAME-REFUSED a sow game needs a seeds row\n" as *u8); return 0 }
808 if perside <= 0 { fg_puts("FOLKGAME-REFUSED perside must be positive\n" as *u8); return 0 }
809 if seeds <= 0 { fg_puts("FOLKGAME-REFUSED seeds must be positive\n" as *u8); return 0 }
810 // The layout is DERIVED, and the declared cell count must agree with it. This is the sow
811 // family's arithmetic tooth: a spec whose cells do not match perside*2 plus its stores is a
812 // board that cannot be sown correctly, and it is refused with BOTH numbers named rather than
813 // silently reshaped to fit.
814 let want: i64 = perside * 2 + store * 2
815 if cells != want {
816 fg_puts("FOLKGAME-REFUSED cells does not match the derived sow layout: declared " as *u8)
817 fg_num2(cells)
818 fg_puts(" but perside*2 plus stores needs " as *u8)
819 fg_num2(want)
820 fg_puts("\n" as *u8)
821 return 0
822 }
823 // A sow board carries no lines. linelen is forced to 1 so the line stride is never zero and
824 // nlines to 0 so every line predicate is a no-op rather than a special case at each call.
825 linelen = 1
826 nlines = 0
827 }
828 if fam == FG_FAM_RACE {
829 if track == FG_MISS { fg_puts("FOLKGAME-REFUSED a race game needs a track row\n" as *u8); return 0 }
830 if pieces == FG_MISS { fg_puts("FOLKGAME-REFUSED a race game needs a pieces row\n" as *u8); return 0 }
831 if dicen == FG_MISS { fg_puts("FOLKGAME-REFUSED a race game needs a dice row: a race with no randomiser is not a race\n" as *u8); return 0 }
832 if track <= 0 { fg_puts("FOLKGAME-REFUSED track must be positive\n" as *u8); return 0 }
833 if pieces <= 0 { fg_puts("FOLKGAME-REFUSED pieces must be positive\n" as *u8); return 0 }
834 if dicesides <= 1 { fg_puts("FOLKGAME-REFUSED a die needs at least two faces or it randomises nothing\n" as *u8); return 0 }
835 // The board IS the track for this family, so the declared cells must equal it. Same arithmetic
836 // refusal the sow family gets, and for the same reason: a board that does not match its own
837 // declaration is refused with both numbers named rather than reshaped to fit.
838 if cells != track {
839 fg_puts("FOLKGAME-REFUSED cells does not match the declared track: cells " as *u8)
840 fg_num2(cells)
841 fg_puts(" but track " as *u8)
842 fg_num2(track)
843 fg_puts("\n" as *u8)
844 return 0
845 }
846 linelen = 1
847 nlines = 0
848 }
849 if fam == FG_FAM_ALIGN {
850 if linelen == FG_MISS { fg_puts("FOLKGAME-REFUSED spec has no linelen row\n" as *u8); return 0 }
851 if linelen <= 0 { fg_puts("FOLKGAME-REFUSED linelen must be positive\n" as *u8); return 0 }
852 if nlines == 0 { fg_puts("FOLKGAME-REFUSED no lines: an align game with no line can never be won\n" as *u8); return 0 }
853 }
854 if lmin != FG_MISS {
855 if lmin != linelen { fg_puts("FOLKGAME-REFUSED an explicit line row is shorter than linelen\n" as *u8); return 0 }
856 if lmax != linelen { fg_puts("FOLKGAME-REFUSED an explicit line row is longer than linelen\n" as *u8); return 0 }
857 }
858 if moveadj == 1 {
859 if adjw == 0 { fg_puts("FOLKGAME-REFUSED move|adj declared but the spec carries no adjacency\n" as *u8); return 0 }
860 }
861 let off_lines: i64 = FG_H_N * FG_WORD
862 let off_adj: i64 = off_lines + nlines * linelen * FG_WORD
863 let off_adjn: i64 = off_adj + cells * adjw * FG_WORD
864 let off_xy: i64 = off_adjn + cells * FG_WORD
865 let off_state: i64 = off_xy + cells * 2 * FG_WORD
866 let off_scal: i64 = off_state + cells * FG_WORD
867 let off_start: i64 = off_scal + FG_S_N * FG_WORD
868 let total: i64 = off_start + cells * FG_WORD
869 if arena != 0 {
870 if total > cap {
871 fg_puts("FOLKGAME-REFUSED the arena is too small for this game image: needed " as *u8)
872 fg_num2(total)
873 fg_puts(" bytes and was given " as *u8)
874 fg_num2(cap)
875 fg_puts("\n" as *u8)
876 return 0
877 }
878 }
879 var base: i64 = arena
880 if arena == 0 { base = sys_mmap(total) as i64 }
881 if base == 0 { fg_puts("FOLKGAME-REFUSED could not allocate the game image\n" as *u8); return 0 }
882 let hd: *i64 = fg_hdr(base)
883 hd[FG_H_CELLS] = cells
884 hd[FG_H_HAND] = hand
885 hd[FG_H_LINELEN] = linelen
886 hd[FG_H_WINMODE] = winmode
887 hd[FG_H_REDUCE] = reduce
888 hd[FG_H_CAPTURE] = capture
889 hd[FG_H_MOVEADJ] = moveadj
890 hd[FG_H_FLYAT] = flyat
891 hd[FG_H_NLINES] = nlines
892 hd[FG_H_LINEW] = linelen
893 hd[FG_H_ADJW] = adjw
894 hd[FG_H_OFF_LINES] = off_lines
895 hd[FG_H_OFF_ADJ] = off_adj
896 hd[FG_H_OFF_ADJN] = off_adjn
897 hd[FG_H_OFF_XY] = off_xy
898 hd[FG_H_OFF_STATE] = off_state
899 hd[FG_H_OFF_SCAL] = off_scal
900 hd[FG_H_OFF_START] = off_start
901 hd[FG_H_BYTES] = total
902 hd[FG_H_FAMILY] = fam
903 hd[FG_H_PERSIDE] = perside
904 hd[FG_H_SEEDS] = seeds
905 hd[FG_H_STORE] = store
906 hd[FG_H_LAP] = lap
907 hd[FG_H_CAPMODE] = capmode
908 hd[FG_H_CAPA] = capa
909 hd[FG_H_CAPB] = capb
910 hd[FG_H_TRACK] = track
911 hd[FG_H_PIECES] = pieces
912 hd[FG_H_DICEN] = dicen
913 hd[FG_H_DICESIDES] = dicesides
914 hd[FG_H_ENTRY] = entry
915 hd[FG_H_BEAROFF] = bearoff
916 hd[FG_H_HIT] = hitrule
917 hd[FG_H_BLOCK] = block
918 let an0: *i64 = (base + off_adjn) as *i64
919 var z: i64 = 0
920 while z < cells { an0[z] = 0; z = z + 1 }
921 let ln0: *i64 = (base + off_lines) as *i64
922 var z2: i64 = 0
923 while z2 < nlines * linelen { ln0[z2] = 0; z2 = z2 + 1 }
924 let xy0: *i64 = (base + off_xy) as *i64
925 var z3: i64 = 0
926 while z3 < cells * 2 { xy0[z3] = 0; z3 = z3 + 1 }
927 let st0: *i64 = (base + off_start) as *i64
928 var z4: i64 = 0
929 while z4 < cells { st0[z4] = 0; z4 = z4 + 1 }
930 var li: i64 = 0
931 var j: i64 = 0
932 while j < n {
933 let le2: i64 = fg_lineend(buf, n, j)
934 var skip2: i64 = 0
935 if j >= le2 { skip2 = 1 }
936 if skip2 == 0 { if buf[j] == (FG_HASH as u8) { skip2 = 1 } }
937 if skip2 == 0 { if buf[j] == (FG_CR as u8) { skip2 = 1 } }
938 if skip2 == 0 {
939 if fg_keyis(buf, j, le2, "line" as *u8) == 1 {
940 let o: i64 = fg_fieldoff(buf, j, le2, 1)
941 var k: i64 = 0
942 while k < linelen { ln0[li * linelen + k] = fg_listat(buf, o, le2, k); k = k + 1 }
943 li = li + 1
944 }
945 if fg_keyis(buf, j, le2, "lines" as *u8) == 1 {
946 let gw2: i64 = fg_field_int(buf, j, le2, 2)
947 let gh2: i64 = fg_field_int(buf, j, le2, 3)
948 let gl2: i64 = fg_field_int(buf, j, le2, 4)
949 var gm2: i64 = fg_field_int(buf, j, le2, 5)
950 if gm2 == FG_MISS { gm2 = FG_LINE_ALL }
951 li = fg_fill_gridlines(base, gw2, gh2, gl2, gm2, li)
952 hd[FG_H_GRIDW] = gw2
953 hd[FG_H_GRIDH] = gh2
954 var gc: i64 = 0
955 while gc < cells {
956 xy0[gc * 2] = gc % gw2
957 xy0[gc * 2 + 1] = gc / gw2
958 gc = gc + 1
959 }
960 }
961 if fg_keyis(buf, j, le2, "adj" as *u8) == 1 {
962 let o1d: i64 = fg_fieldoff(buf, j, le2, 1)
963 let isg: i64 = fg_wordis(buf, o1d, le2, "grid" as *u8)
964 if isg == 1 {
965 let aw2: i64 = fg_field_int(buf, j, le2, 2)
966 let ah2: i64 = fg_field_int(buf, j, le2, 3)
967 let am2: i64 = fg_field_int(buf, j, le2, 4)
968 fg_fill_gridadj(base, aw2, ah2, am2)
969 }
970 if isg == 0 {
971 let c2: i64 = fg_field_int(buf, j, le2, 1)
972 let o2b: i64 = fg_fieldoff(buf, j, le2, 2)
973 let dn: i64 = fg_listn(buf, o2b, le2)
974 let aj2: *i64 = (base + off_adj) as *i64
975 var kk: i64 = 0
976 while kk < dn { aj2[c2 * adjw + kk] = fg_listat(buf, o2b, le2, kk); kk = kk + 1 }
977 an0[c2] = dn
978 }
979 }
980 if fg_keyis(buf, j, le2, "start" as *u8) == 1 {
981 let sc4: i64 = fg_field_int(buf, j, le2, 1)
982 let sn4: i64 = fg_field_int(buf, j, le2, 2)
983 if sc4 >= 0 { if sc4 < cells { st0[sc4] = sn4 } }
984 }
985 if fg_keyis(buf, j, le2, "xy" as *u8) == 1 {
986 let c3: i64 = fg_field_int(buf, j, le2, 1)
987 xy0[c3 * 2] = fg_field_int(buf, j, le2, 2)
988 xy0[c3 * 2 + 1] = fg_field_int(buf, j, le2, 3)
989 }
990 }
991 j = le2 + 1
992 }
993 if li != nlines { fg_puts("FOLKGAME-REFUSED measured and written line counts disagree\n" as *u8); return 0 }
994 fg_reset(base)
995 return base
996}
997// The native convenience form: let the parser allocate. The browser build calls fg_parse_arena with
998// a fixed base instead. Both run the same code, so a rule fixed once is fixed on both targets.
999func fg_parse(buf: *u8, n: i64) -> i64 { return fg_parse_arena(buf, n, 0, 0) }
1000
1001// ONE reader for both entry points. Resolves through ep_artifact_path first so the spec is found
1002// from whichever working directory the caller was launched in, then composes sys_read_file, which
1003// sizes its own buffer from the file and therefore cannot short-read. Returns 0 and says which of
1004// the two failures happened -- absent everywhere, or present and unreadable.
1005func fg_read_spec(path: *u8, lp: *i64) -> *u8 {
1006 let rp: *u8 = sys_mmap(FG_PATHCAP)
1007 if ep_artifact_path(rp, path) == 0 {
1008 fg_puts("FOLKGAME-REFUSED spec absent from the cwd and from both estate roots: " as *u8)
1009 fg_puts(path)
1010 fg_puts("\n" as *u8)
1011 return 0 as *u8
1012 }
1013 let b: *u8 = sys_read_file(rp, lp)
1014 if (b as i64) == 0 { fg_puts("FOLKGAME-REFUSED spec resolved but could not be read\n" as *u8); return 0 as *u8 }
1015 return b
1016}
1017func fg_parse_file(path: *u8) -> i64 {
1018 let lp: *i64 = sys_mmap(FG_WORD * 2) as *i64
1019 let b: *u8 = fg_read_spec(path, lp)
1020 if (b as i64) == 0 { return 0 }
1021 return fg_parse(b, lp[0])
1022}
1023
1024// ---- position save/restore. A game-tree walk must not allocate per node, so the caller sizes ONE
1025// stack of fg_snapwords(base) words per ply and reuses it.
1026func fg_snapwords(base: i64) -> i64 {
1027 let hd: *i64 = fg_hdr(base)
1028 return hd[FG_H_CELLS] + FG_S_N
1029}
1030func fg_save(base: i64, out: *i64) -> i64 {
1031 let hd: *i64 = fg_hdr(base)
1032 let st: *i64 = fg_state(base)
1033 let sc: *i64 = fg_scal(base)
1034 var i: i64 = 0
1035 while i < hd[FG_H_CELLS] { out[i] = st[i]; i = i + 1 }
1036 var k: i64 = 0
1037 while k < FG_S_N { out[hd[FG_H_CELLS] + k] = sc[k]; k = k + 1 }
1038 return 0
1039}
1040func fg_load(base: i64, inp: *i64) -> i64 {
1041 let hd: *i64 = fg_hdr(base)
1042 let st: *i64 = fg_state(base)
1043 let sc: *i64 = fg_scal(base)
1044 var i: i64 = 0
1045 while i < hd[FG_H_CELLS] { st[i] = inp[i]; i = i + 1 }
1046 var k: i64 = 0
1047 while k < FG_S_N { sc[k] = inp[hd[FG_H_CELLS] + k]; k = k + 1 }
1048 return 0
1049}
1050
1051// ---- one file, many games. A `name|<slug>|<Title>` row opens a section and the next `name|` row
1052// closes it, so a family ships as ONE data file instead of a directory nobody keeps in step.
1053func fg_name_is(buf: *u8, ls: i64, le: i64, slug: *u8) -> i64 {
1054 if fg_keyis(buf, ls, le, "name" as *u8) == 0 { return 0 }
1055 let o: i64 = fg_fieldoff(buf, ls, le, 1)
1056 if o < 0 { return 0 }
1057 if fg_wordis(buf, o, le, slug) == 0 { return 0 }
1058 var k: i64 = 0
1059 while slug[k] != (0 as u8) { k = k + 1 }
1060 if o + k >= le { return 0 }
1061 if buf[o + k] != (FG_PIPE as u8) { return 0 }
1062 return 1
1063}
1064// ---- ALIASES. The central fact of folk-game history is that ONE rule set is played under many
1065// names: Twelve Mens Morris and Morabaraba are the same game, as are Nard and Backgammon. Copying a
1066// board's forty rows per name would be data entry, and data entry drifts -- edit one copy and the
1067// other silently becomes a different game. `alias|<slug>` says "my rules are that section's" and the
1068// parser follows it, so the registry can carry every real name against ONE rule set.
1069// Depth is capped at one hop and a second hop is REFUSED BY NAME rather than followed: an alias
1070// chain is how a cycle gets in, and a parser that loops on its own data is worse than one that says
1071// no. One hop covers every case the registry actually has.
1072func fg_parse_named_at(buf: *u8, n: i64, slug: *u8, depth: i64) -> i64 {
1073 var from: i64 = 0 - 1
1074 var to: i64 = n
1075 var i: i64 = 0
1076 while i < n {
1077 let le: i64 = fg_lineend(buf, n, i)
1078 if from < 0 {
1079 if fg_name_is(buf, i, le, slug) == 1 { from = i }
1080 }
1081 if from >= 0 {
1082 if i > from {
1083 if fg_keyis(buf, i, le, "name" as *u8) == 1 { to = i; i = n }
1084 }
1085 }
1086 if i < n { i = le + 1 }
1087 }
1088 if from < 0 { fg_puts("FOLKGAME-REFUSED no section named for that slug\n" as *u8); return 0 }
1089 var aoff: i64 = 0 - 1
1090 var aend: i64 = 0 - 1
1091 var j: i64 = from
1092 while j < to {
1093 let le2: i64 = fg_lineend(buf, n, j)
1094 if aoff < 0 {
1095 if fg_keyis(buf, j, le2, "alias" as *u8) == 1 {
1096 aoff = fg_fieldoff(buf, j, le2, 1)
1097 aend = le2
1098 }
1099 }
1100 j = le2 + 1
1101 }
1102 if aoff >= 0 {
1103 if depth > 0 {
1104 fg_puts("FOLKGAME-REFUSED an alias may not point at another alias\n" as *u8)
1105 return 0
1106 }
1107 var alen: i64 = 0
1108 while aoff + alen < aend {
1109 if buf[aoff + alen] == (FG_PIPE as u8) { break }
1110 if buf[aoff + alen] == (FG_CR as u8) { break }
1111 alen = alen + 1
1112 }
1113 let tgt: *u8 = sys_mmap(alen + 1)
1114 var k: i64 = 0
1115 while k < alen { tgt[k] = buf[aoff + k]; k = k + 1 }
1116 tgt[alen] = 0 as u8
1117 return fg_parse_named_at(buf, n, tgt, depth + 1)
1118 }
1119 let ln: i64 = to - from
1120 let cp: *u8 = sys_mmap(ln)
1121 var c: i64 = 0
1122 while c < ln { cp[c] = buf[from + c]; c = c + 1 }
1123 return fg_parse(cp, ln)
1124}
1125func fg_parse_named(buf: *u8, n: i64, slug: *u8) -> i64 {
1126 return fg_parse_named_at(buf, n, slug, 0)
1127}
1128func fg_parse_named_file(path: *u8, slug: *u8) -> i64 {
1129 let lp: *i64 = sys_mmap(FG_WORD * 2) as *i64
1130 let b: *u8 = fg_read_spec(path, lp)
1131 if (b as i64) == 0 { return 0 }
1132 return fg_parse_named(b, lp[0], slug)
1133}