code wiki / (root) / nx_folkgame_lib.nx

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}