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}