code wiki / (root) / nx_folkgame_play.nx

nx_folkgame_play.nx source

↩ module page · 289 lines · 11362 B

1// nx_folkgame_play.nx -- NATIVE terminal player for any align-family folk game. THE POINT OF THIS 2// ORGAN IS THAT IT CAN BE PLAYED, and the operator's own standing correction is why it exists: 3// an internal GREEN gate is not a thing anyone can open and play. Built by the Nishi toolchain 4// (nx_cc -> nxasm_x86, no gcc) to a NATIVE binary that runs on this host -- no WebAssembly, no 5// browser, no borrowed VM anywhere in the path. 6// 7// usage: nx_folkgame_play <slug> [depth] slug is a section of knowledge/compare/folkgames.align 8// 9// The opponent is full-width alpha-beta over the SAME move generator the gate validated against the 10// published Tic-Tac-Toe game-tree constant. It is not a second rules implementation: if it could 11// make a move the gate's generator does not admit, the two would disagree and this file would be 12// the duplicate-ruler defect. 13// Depth and evaluation weights are conf rows (knowledge/folkgames.conf), never literals. 14// license_tier: ORIGINAL No hw writes (Rule 26). 15import "nx_folkgame_lib.nx" 16// The opponent lives in a shared lib so the gate proves the SAME search this organ plays with. 17import "nx_folkgame_ai_lib.nx" 18import "nx_lineconf_lib.nx" 19 20const FGP_SPEC: *u8 = "knowledge/compare/folkgames.align" 21const FGP_CONF: *u8 = "knowledge/folkgames.conf" 22const FGP_LINECAP: i64 = 256 23const FGP_GRIDCAP: i64 = 4096 24const FGP_HUMAN: i64 = 1 25const FGP_AI: i64 = 2 26const FGP_SP: i64 = 32 27const FGP_DOT: i64 = 46 28const FGP_X: i64 = 88 29const FGP_O: i64 = 79 30 31func fgp_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 32func fgp_num(v: i64) -> i64 { 33 let b: *u8 = sys_mmap(FG_NUMSCRATCH) 34 let t: *u8 = sys_mmap(FG_NUMSCRATCH) 35 var m: i64 = v 36 if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) } 37 var k: i64 = 0 38 if m == 0 { t[0] = FG_ZERO as u8; k = 1 } 39 while m > 0 { t[k] = (FG_ZERO + (m % FG_DECIMAL)) as u8; m = m / FG_DECIMAL; k = k + 1 } 40 var i: i64 = 0 41 while i < k { b[i] = t[k - 1 - i]; i = i + 1 } 42 sys_write(1, b, k) 43 sys_munmap(b, FG_NUMSCRATCH) 44 sys_munmap(t, FG_NUMSCRATCH) 45 return 0 46} 47 48// The board is drawn from the (x,y) every cell carries -- set explicitly by an xy row for the Morris 49// boards and derived from the grid for Tic-Tac-Toe and Gomoku. One renderer serves both because the 50// layout is data either way. 51func fgp_render(base: i64) -> i64 { 52 var maxx: i64 = 0 53 var maxy: i64 = 0 54 var c: i64 = 0 55 while c < fg_cells(base) { 56 if fg_xy_x(base, c) > maxx { maxx = fg_xy_x(base, c) } 57 if fg_xy_y(base, c) > maxy { maxy = fg_xy_y(base, c) } 58 c = c + 1 59 } 60 let w: i64 = maxx + 1 61 let h: i64 = maxy + 1 62 let g: *u8 = sys_mmap(FGP_GRIDCAP) 63 var i: i64 = 0 64 while i < w * h { g[i] = FGP_SP as u8; i = i + 1 } 65 var k: i64 = 0 66 while k < fg_cells(base) { 67 let p: i64 = fg_xy_y(base, k) * w + fg_xy_x(base, k) 68 var ch: i64 = FGP_DOT 69 if fg_at(base, k) == 1 { ch = FGP_X } 70 if fg_at(base, k) == 2 { ch = FGP_O } 71 g[p] = ch as u8 72 k = k + 1 73 } 74 fgp_puts("\n" as *u8) 75 var y: i64 = 0 76 while y < h { 77 fgp_puts(" " as *u8) 78 var x: i64 = 0 79 while x < w { 80 sys_write(1, ((g as i64) + y * w + x) as *u8, 1) 81 fgp_puts(" " as *u8) 82 x = x + 1 83 } 84 fgp_puts("\n" as *u8) 85 y = y + 1 86 } 87 sys_munmap(g, FGP_GRIDCAP) 88 fgp_puts("\n" as *u8) 89 return 0 90} 91 92// Cell numbers, printed on request, so a player can name a square without counting. 93func fgp_render_ids(base: i64) -> i64 { 94 fgp_puts(" cell ids by row:\n" as *u8) 95 var maxy: i64 = 0 96 var c: i64 = 0 97 while c < fg_cells(base) { 98 if fg_xy_y(base, c) > maxy { maxy = fg_xy_y(base, c) } 99 c = c + 1 100 } 101 var y: i64 = 0 102 while y <= maxy { 103 fgp_puts(" " as *u8) 104 var k: i64 = 0 105 while k < fg_cells(base) { 106 if fg_xy_y(base, k) == y { fgp_num(k); fgp_puts(" " as *u8) } 107 k = k + 1 108 } 109 fgp_puts("\n" as *u8) 110 y = y + 1 111 } 112 return 0 113} 114 115func fgp_readline(buf: *u8, cap: i64) -> i64 { 116 var n: i64 = 0 117 var done: i64 = 0 118 while done == 0 { 119 if n >= cap - 1 { done = 1 } 120 if done == 0 { 121 let got: i64 = sys_read(0, ((buf as i64) + n) as *u8, 1) 122 if got <= 0 { done = 1 } 123 if got > 0 { 124 if buf[n] == (FG_NL as u8) { done = 1 } 125 if done == 0 { n = n + 1 } 126 } 127 } 128 } 129 buf[n] = 0 as u8 130 return n 131} 132 133// Reads up to two integers from a line. Returns how many it found, so the caller can tell a place 134// (one number) from a move (two) without guessing. 135func fgp_ints(buf: *u8, n: i64, out: *i64) -> i64 { 136 var found: i64 = 0 137 var i: i64 = 0 138 while i < n { 139 let ch: i64 = buf[i] as i64 140 var isd: i64 = 0 141 if ch >= FG_ZERO { if ch <= FG_NINE { isd = 1 } } 142 if isd == 1 { 143 var v: i64 = 0 144 while i < n { 145 let c2: i64 = buf[i] as i64 146 if c2 < FG_ZERO { break } 147 if c2 > FG_NINE { break } 148 v = v * FG_DECIMAL + (c2 - FG_ZERO) 149 i = i + 1 150 } 151 if found < 2 { out[found] = v; found = found + 1 } 152 } 153 if isd == 0 { i = i + 1 } 154 } 155 return found 156} 157 158func fgp_streq(a: *u8, b: *u8) -> i64 { 159 var i: i64 = 0 160 while a[i] != (0 as u8) { 161 if a[i] != b[i] { return 0 } 162 i = i + 1 163 } 164 if b[i] != (0 as u8) { return 0 } 165 return 1 166} 167 168func main(argc: i64, argv: *i64) -> i64 { 169 if argc < 2 { 170 fgp_puts("usage: nx_folkgame_play <slug> [depth] | nx_folkgame_play selfplay <slug> [depth]\n" as *u8) 171 return 2 172 } 173 // selfplay runs the opponent against itself to a finish. It exists for two reasons: an 174 // interactive game cannot be proven from a tool call, and Tic-Tac-Toe under perfect play is a 175 // DRAW -- another published constant, so the SEARCH gets an external check instead of a 176 // self-grade. A search that blunders anywhere in a 9-cell game loses one of those selfplays. 177 var selfplay: i64 = 0 178 if fgp_streq(argv[1] as *u8, "selfplay" as *u8) == 1 { selfplay = 1 } 179 var slugi: i64 = 1 180 if selfplay == 1 { slugi = 2 } 181 if argc < slugi + 1 { 182 fgp_puts("usage: nx_folkgame_play selfplay <slug> [depth]\n" as *u8) 183 return 2 184 } 185 let slug: *u8 = argv[slugi] as *u8 186 let base: i64 = fg_parse_named_file(FGP_SPEC, slug) 187 if base == 0 { fgp_puts("no such game in the family spec\n" as *u8); return 3 } 188 189 let pw: i64 = lcf_int_of(FGP_CONF, "ai-piece-weight" as *u8) 190 let win: i64 = lcf_int_of(FGP_CONF, "ai-win-score" as *u8) 191 var depth: i64 = lcf_int_of(FGP_CONF, "ai-depth" as *u8) 192 if pw == LCF_MISS { fgp_puts("REFUSED: ai-piece-weight missing from knowledge/folkgames.conf\n" as *u8); return 4 } 193 if win == LCF_MISS { fgp_puts("REFUSED: ai-win-score missing from knowledge/folkgames.conf\n" as *u8); return 4 } 194 if depth == LCF_MISS { fgp_puts("REFUSED: ai-depth missing from knowledge/folkgames.conf\n" as *u8); return 4 } 195 let maxplies: i64 = lcf_int_of(FGP_CONF, "selfplay-max-plies" as *u8) 196 if maxplies == LCF_MISS { fgp_puts("REFUSED: selfplay-max-plies missing from knowledge/folkgames.conf\n" as *u8); return 4 } 197 if fg_cells(base) == 9 { 198 let d9: i64 = lcf_int_of(FGP_CONF, "ai-depth-tictactoe" as *u8) 199 if d9 != LCF_MISS { depth = d9 } 200 } 201 if argc >= slugi + 2 { 202 let a2: *u8 = argv[slugi + 1] as *u8 203 var nn: i64 = 0 204 while a2[nn] != (0 as u8) { nn = nn + 1 } 205 let ov: *i64 = sys_mmap(FG_WORD * 2) as *i64 206 if fgp_ints(a2, nn, ov) == 1 { depth = ov[0] } 207 } 208 209 let sw: i64 = fg_snapwords(base) 210 let mm: i64 = fg_maxmoves(base) 211 let plies: i64 = fg_cells(base) + fg_cells(base) + depth + 2 212 let snap: *i64 = sys_mmap(plies * sw * FG_WORD) as *i64 213 let mvb: *i64 = sys_mmap(plies * mm * FG_WORD) as *i64 214 let scr: *i64 = sys_mmap(mm * FG_WORD) as *i64 215 let line: *u8 = sys_mmap(FGP_LINECAP) 216 let ints: *i64 = sys_mmap(FG_WORD * 2) as *i64 217 218 fgp_puts("\n=== " as *u8) 219 fgp_puts(slug) 220 fgp_puts(" === you are X and move first; the opponent is alpha-beta at depth " as *u8) 221 fgp_num(depth) 222 fgp_puts("\n" as *u8) 223 fgp_render_ids(base) 224 225 var running: i64 = 1 226 while running == 1 { 227 if selfplay == 1 { 228 let scl: *i64 = fg_scal(base) 229 if scl[FG_S_PLIES] > maxplies { 230 fgp_puts("UNRESOLVED: selfplay reached the ply cap without a terminal position\n" as *u8) 231 running = 0 232 } 233 } 234 let t: i64 = fg_terminal(base, scr) 235 if t != FG_T_ONGOING { 236 fgp_render(base) 237 if t == FG_T_P1 { fgp_puts("YOU WIN\n" as *u8) } 238 if t == FG_T_P2 { fgp_puts("THE OPPONENT WINS\n" as *u8) } 239 if t == FG_T_DRAW { fgp_puts("DRAW\n" as *u8) } 240 running = 0 241 } 242 if running == 1 { 243 var human_turn: i64 = 0 244 if selfplay == 0 { if fg_side(base) == FGP_HUMAN { human_turn = 1 } } 245 if human_turn == 1 { 246 fgp_render(base) 247 if fg_phase(base) == FG_PH_PLACE { fgp_puts("place a piece -- type a cell id: " as *u8) } 248 if fg_phase(base) == FG_PH_MOVE { fgp_puts("move -- type two cell ids, from and to: " as *u8) } 249 if fg_phase(base) == FG_PH_CAPTURE { fgp_puts("you made a mill -- type an enemy cell id to remove: " as *u8) } 250 let ln: i64 = fgp_readline(line, FGP_LINECAP) 251 let got: i64 = fgp_ints(line, ln, ints) 252 if got == 0 { fgp_puts("no cell id in that line\n" as *u8) } 253 if got > 0 { 254 let n: i64 = fg_moves(base, mvb) 255 var picked: i64 = 0 - 1 256 var i: i64 = 0 257 while i < n { 258 let mv: i64 = mvb[i] 259 let kd: i64 = fg_mv_kind(mv) 260 if kd == FG_MK_MOVE { 261 if got == 2 { 262 if fg_mv_from(mv) == ints[0] { if fg_mv_to(mv) == ints[1] { picked = mv } } 263 } 264 } 265 if kd != FG_MK_MOVE { 266 if fg_mv_to(mv) == ints[0] { picked = mv } 267 } 268 i = i + 1 269 } 270 if picked < 0 { fgp_puts("that is not a legal move here\n" as *u8) } 271 if picked >= 0 { fg_apply(base, picked) } 272 } 273 } 274 if human_turn == 0 { 275 let mv: i64 = fgai_best(base, depth, snap, sw, mvb, mm, scr, pw, win) 276 if mv < 0 { running = 0 } 277 if mv >= 0 { 278 fgp_puts("opponent plays " as *u8) 279 if fg_mv_kind(mv) == FG_MK_MOVE { fgp_num(fg_mv_from(mv)); fgp_puts(" to " as *u8) } 280 if fg_mv_kind(mv) == FG_MK_REMOVE { fgp_puts("remove " as *u8) } 281 fgp_num(fg_mv_to(mv)) 282 fgp_puts("\n" as *u8) 283 fg_apply(base, mv) 284 } 285 } 286 } 287 } 288 return 0 289}