code wiki / (root) / nx_folkgame_ai_lib.nx

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}