nx_folkgame_ai_lib.nx source
↩ module page · 101 lines · 4443 B
1// nx_folkgame_ai_lib.nx -- THE OPPONENT, as a shared lib so there is exactly ONE of it.
2// The player organ and the gate both import this. If the search lived in the player only, the gate
3// could test it solely by forking the binary, mutation coverage would stop at the fork boundary, and
4// a second copy would eventually drift from the first -- the duplicate-ruler defect this estate
5// keeps paying for.
6//
7// It plays through fg_moves and fg_apply, the SAME generator the align gate validated against the
8// published Tic-Tac-Toe game-tree constant. It is not a second rules implementation and must never
9// become one: if this file could produce a move fg_moves does not admit, the two would disagree.
10// Weights and depth are passed IN, never read here -- the caller owns the conf.
11// license_tier: ORIGINAL No hw writes (Rule 26).
12import "nx_folkgame_lib.nx"
13
14// Material only, deliberately. A mobility term costs a move generation per leaf and this is a first
15// rung, not a strong engine; naming what it does NOT measure is the point.
16func fgai_eval(base: i64, pw: i64) -> i64 {
17 let me: i64 = fg_side(base)
18 let opp: i64 = 3 - me
19 return (fg_count(base, me) - fg_count(base, opp)) * pw
20}
21
22// Negamax with alpha-beta. THE SIGN FLIP IS CONDITIONAL and that is the whole subtlety: completing a
23// mill leaves the SAME side to move, so negating there would score the position as the opponent's and
24// the engine would deliberately walk into captures.
25func fgai_search(base: i64, depth: i64, alpha: i64, beta: i64, snap: *i64, sw: i64, mvb: *i64, mm: i64, scr: *i64, d: i64, pw: i64, win: i64) -> i64 {
26 let t: i64 = fg_terminal(base, scr)
27 if t != FG_T_ONGOING {
28 if t == FG_T_DRAW { return 0 }
29 if t == fg_side(base) { return win - d }
30 return 0 - (win - d)
31 }
32 if depth <= 0 { return fgai_eval(base, pw) }
33 let out: *i64 = ((mvb as i64) + d * mm * FG_WORD) as *i64
34 let n: i64 = fg_moves(base, out)
35 if n == 0 { return fgai_eval(base, pw) }
36 let sv: *i64 = ((snap as i64) + d * sw * FG_WORD) as *i64
37 var best: i64 = 0 - win - win
38 var a: i64 = alpha
39 var i: i64 = 0
40 while i < n {
41 let before: i64 = fg_side(base)
42 fg_save(base, sv)
43 fg_apply(base, out[i])
44 var sc: i64 = 0
45 if fg_side(base) == before {
46 sc = fgai_search(base, depth - 1, a, beta, snap, sw, mvb, mm, scr, d + 1, pw, win)
47 }
48 if fg_side(base) != before {
49 sc = 0 - fgai_search(base, depth - 1, 0 - beta, 0 - a, snap, sw, mvb, mm, scr, d + 1, pw, win)
50 }
51 fg_load(base, sv)
52 if sc > best { best = sc }
53 if best > a { a = best }
54 if a >= beta { break }
55 i = i + 1
56 }
57 return best
58}
59
60func fgai_best(base: i64, depth: i64, snap: *i64, sw: i64, mvb: *i64, mm: i64, scr: *i64, pw: i64, win: i64) -> i64 {
61 let out: *i64 = mvb
62 let n: i64 = fg_moves(base, out)
63 if n == 0 { return 0 - 1 }
64 let sv: *i64 = snap
65 var bestmv: i64 = out[0]
66 var best: i64 = 0 - win - win
67 var i: i64 = 0
68 while i < n {
69 let before: i64 = fg_side(base)
70 fg_save(base, sv)
71 fg_apply(base, out[i])
72 var sc: i64 = 0
73 if fg_side(base) == before {
74 sc = fgai_search(base, depth - 1, best, win + win, snap, sw, mvb, mm, scr, 1, pw, win)
75 }
76 if fg_side(base) != before {
77 sc = 0 - fgai_search(base, depth - 1, 0 - win - win, 0 - best, snap, sw, mvb, mm, scr, 1, pw, win)
78 }
79 fg_load(base, sv)
80 if sc > best { best = sc; bestmv = out[i] }
81 i = i + 1
82 }
83 return bestmv
84}
85
86// Plays the opponent against itself from the current position and returns the terminal code, or -1
87// if it hit the ply cap. -1 is NOT a draw: "I stopped looking" and "the game is drawn" are different
88// facts and a function that returned FG_T_DRAW here would be acquitting on absent evidence.
89func fgai_selfplay(base: i64, depth: i64, snap: *i64, sw: i64, mvb: *i64, mm: i64, scr: *i64, pw: i64, win: i64, maxplies: i64) -> i64 {
90 let sc: *i64 = fg_scal(base)
91 var guard: i64 = 0
92 while guard == 0 {
93 let t: i64 = fg_terminal(base, scr)
94 if t != FG_T_ONGOING { return t }
95 if sc[FG_S_PLIES] > maxplies { return 0 - 1 }
96 let mv: i64 = fgai_best(base, depth, snap, sw, mvb, mm, scr, pw, win)
97 if mv < 0 { return 0 - 1 }
98 fg_apply(base, mv)
99 }
100 return 0 - 1
101}