code wiki / (root) / nx_folkgame_race_lib.nx

nx_folkgame_race_lib.nx source

↩ module page · 267 lines · 10849 B

1// nx_folkgame_race_lib.nx -- THE RACE FAMILY: a track, a randomiser, entry, bearing off, hitting 2// and blocking. 3// 4// SCOPE STATED BEFORE ANY CLAIM. Twenty-two registry rows are race games. This engine covers the 5// TRACK sub-family -- a linear course both players run in opposite directions: Backgammon, Nard, the 6// Royal Game of Ur, Senet, Sugoroku, Liubo, Zohn Ahl, Main Kabul. The cross-and-circle boards 7// (Pachisi, Chaupar, Ashta Chamma, Ludo, Yut Nori, Patolli, Pancha Keliya) fold four arms onto a 8// per-player track and are a SEPARATE rung; the spiral and ladder boards (the Game of the Goose, 9// Hyena Chase, Mehen, Snakes and Ladders) need special-square rules and are a THIRD. Saying which 10// eight of the twenty-two this reaches is the difference between a family row and a family claim. 11// 12// STATE ENCODING: state[c] is a SIGNED count -- positive is the first player, negative the second. 13// That is the representation the game itself has, and it makes a point's owner and its height one 14// number instead of two that can disagree. 15// DIRECTION: the first player runs from index 0 toward the end and bears off past track-1; the second 16// runs the other way and bears off past 0. So a point's PIP VALUE differs per player, which is why 17// fgr_pip takes a side rather than reading one number off the board. 18// 19// THE RANDOMISER IS SEEDED AND LIVES IN THE SAVED POSITION. A race is not deterministic, but a 20// REPLAY of one must be: the seed is scalar slot 12, inside what fg_save and fg_load copy, so 21// restoring a position restores the die that follows it. A seed kept beside the position instead of 22// inside it is a seed a save silently loses, and the replay then diverges from the game it claims to 23// be replaying. 24// license_tier: ORIGINAL No hw writes (Rule 26). 25import "nx_folkgame_lib.nx" 26 27const FGR_S_BAR1: i64 = 8 28const FGR_S_BAR2: i64 = 9 29const FGR_S_OFF1: i64 = 10 30const FGR_S_OFF2: i64 = 11 31const FGR_S_SEED: i64 = 12 32 33// Numerical Recipes' linear congruential parameters. They are CITED rather than invented: a 34// hand-picked multiplier is exactly the magic number this estate refuses, and an LCG with bad 35// constants has short cycles that would make a "random" race quietly repeat. 36const FGR_LCG_MUL: i64 = 1664525 37const FGR_LCG_ADD: i64 = 1013904223 38const FGR_LCG_MOD: i64 = 4294967296 39 40func fgr_startarr(base: i64) -> *i64 { 41 let hd: *i64 = fg_hdr(base) 42 return (base + hd[FG_H_OFF_START]) as *i64 43} 44func fgr_track(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_TRACK] } 45func fgr_pieces(base: i64) -> i64 { let hd: *i64 = fg_hdr(base); return hd[FG_H_PIECES] } 46 47func fgr_bar(base: i64, side: i64) -> i64 { 48 let sc: *i64 = fg_scal(base) 49 if side == 1 { return sc[FGR_S_BAR1] } 50 return sc[FGR_S_BAR2] 51} 52func fgr_off(base: i64, side: i64) -> i64 { 53 let sc: *i64 = fg_scal(base) 54 if side == 1 { return sc[FGR_S_OFF1] } 55 return sc[FGR_S_OFF2] 56} 57 58// How many of a side's pieces stand on point c. Signed encoding, so this is where the sign is read 59// and nowhere else. 60func fgr_on(base: i64, side: i64, c: i64) -> i64 { 61 let v: i64 = fg_at(base, c) 62 if side == 1 { if v > 0 { return v } return 0 } 63 if v < 0 { return 0 - v } 64 return 0 65} 66 67// THE PIP COUNT: distance every piece still has to travel. The first player's piece on point c has 68// track-c pips to go, the second's has c+1. Pieces on the bar have the full track plus one. 69func fgr_pip(base: i64, side: i64) -> i64 { 70 let t: i64 = fgr_track(base) 71 var p: i64 = 0 72 var c: i64 = 0 73 while c < t { 74 let n: i64 = fgr_on(base, side, c) 75 if n > 0 { 76 if side == 1 { p = p + n * (t - c) } 77 if side == 2 { p = p + n * (c + 1) } 78 } 79 c = c + 1 80 } 81 p = p + fgr_bar(base, side) * (t + 1) 82 return p 83} 84 85// CONSERVATION: pieces on the track plus on the bar plus borne off. A race moves pieces and never 86// creates or destroys one, so this must equal the declared piece count at every position. 87func fgr_total(base: i64, side: i64) -> i64 { 88 let t: i64 = fgr_track(base) 89 var n: i64 = 0 90 var c: i64 = 0 91 while c < t { n = n + fgr_on(base, side, c); c = c + 1 } 92 return n + fgr_bar(base, side) + fgr_off(base, side) 93} 94 95func fgr_reset(base: i64, seed: i64) -> i64 { 96 let hd: *i64 = fg_hdr(base) 97 let st: *i64 = fg_state(base) 98 let sc: *i64 = fg_scal(base) 99 let s0: *i64 = fgr_startarr(base) 100 var c: i64 = 0 101 while c < hd[FG_H_CELLS] { st[c] = s0[c]; c = c + 1 } 102 var k: i64 = 0 103 while k < FG_S_N { sc[k] = 0; k = k + 1 } 104 sc[FG_S_SIDE] = 1 105 sc[FG_S_PHASE] = FG_PH_MOVE 106 sc[FGR_S_SEED] = seed 107 return 0 108} 109 110// One die face, advancing the seed that lives in the position. Returns 1..sides. 111func fgr_die(base: i64) -> i64 { 112 let hd: *i64 = fg_hdr(base) 113 let sc: *i64 = fg_scal(base) 114 var s: i64 = sc[FGR_S_SEED] 115 s = (s * FGR_LCG_MUL + FGR_LCG_ADD) % FGR_LCG_MOD 116 if s < 0 { s = 0 - s } 117 sc[FGR_S_SEED] = s 118 // The HIGH bits of an LCG are the good ones; taking the low bits modulo a small number is the 119 // classic way to get a die that is not uniform. Shifting first is not an optimisation, it is the 120 // difference between a die and a pattern. 121 return ((s / 65536) % hd[FG_H_DICESIDES]) + 1 122} 123 124// Where a piece on point c lands after moving d. Off the end is bearing off, reported as the track 125// length for the first player and -1 for the second so a caller can tell it from a real point. 126func fgr_dest(base: i64, side: i64, c: i64, d: i64) -> i64 { 127 if side == 1 { return c + d } 128 return c - d 129} 130func fgr_is_off(base: i64, side: i64, dest: i64) -> i64 { 131 if side == 1 { if dest >= fgr_track(base) { return 1 } return 0 } 132 if dest < 0 { return 1 } 133 return 0 134} 135 136// A point is BLOCKED for a side when the opponent stands on it in numbers at or above the spec's 137// block threshold. Below that threshold a lone opponent piece is a blot and may be hit where the 138// spec allows hitting; where it does not, any opposing piece blocks. 139func fgr_blocked(base: i64, side: i64, c: i64) -> i64 { 140 let hd: *i64 = fg_hdr(base) 141 let opp: i64 = 3 - side 142 let n: i64 = fgr_on(base, opp, c) 143 if n == 0 { return 0 } 144 if hd[FG_H_HIT] == 0 { return 1 } 145 if hd[FG_H_BLOCK] <= 0 { return 1 } 146 if n >= hd[FG_H_BLOCK] { return 1 } 147 return 0 148} 149 150// The home quarter, DERIVED from the track rather than declared: a quarter of the course, which is 151// six points on a twenty-four point board and stays correct on a shorter one. A bearing-off rule 152// with a hardcoded six would be right for exactly one game in this family. 153func fgr_home_size(base: i64) -> i64 { 154 let t: i64 = fgr_track(base) 155 var h: i64 = t / 4 156 if h < 1 { h = 1 } 157 return h 158} 159func fgr_can_bearoff(base: i64, side: i64) -> i64 { 160 let hd: *i64 = fg_hdr(base) 161 if hd[FG_H_BEAROFF] == 0 { return 0 } 162 if fgr_bar(base, side) > 0 { return 0 } 163 let t: i64 = fgr_track(base) 164 let h: i64 = fgr_home_size(base) 165 var c: i64 = 0 166 while c < t { 167 if fgr_on(base, side, c) > 0 { 168 if side == 1 { if c < t - h { return 0 } } 169 if side == 2 { if c >= h { return 0 } } 170 } 171 c = c + 1 172 } 173 return 1 174} 175 176// The point a piece re-enters on from the bar, which is the far end of the runner's own direction. 177const FGR_FROM_BAR: i64 = 0 - 2 178func fgr_entry_point(base: i64, side: i64, d: i64) -> i64 { 179 if side == 1 { return d - 1 } 180 return fgr_track(base) - d 181} 182 183// Legal FROM-points for one die value. A piece on the bar must enter before anything else may move, 184// which is why that case returns early rather than being folded in as one more candidate: folding it 185// in would let a position with a piece on the bar generate moves the rules forbid. 186func fgr_moves(base: i64, d: i64, out: *i64) -> i64 { 187 let sc: *i64 = fg_scal(base) 188 let side: i64 = sc[FG_S_SIDE] 189 if sc[FG_S_WINNER] != 0 { return 0 } 190 var n: i64 = 0 191 if fgr_bar(base, side) > 0 { 192 let e: i64 = fgr_entry_point(base, side, d) 193 if e >= 0 { if e < fgr_track(base) { if fgr_blocked(base, side, e) == 0 { out[n] = FGR_FROM_BAR; n = n + 1 } } } 194 return n 195 } 196 let t: i64 = fgr_track(base) 197 let canoff: i64 = fgr_can_bearoff(base, side) 198 var c: i64 = 0 199 while c < t { 200 if fgr_on(base, side, c) > 0 { 201 let dest: i64 = fgr_dest(base, side, c, d) 202 if fgr_is_off(base, side, dest) == 1 { 203 if canoff == 1 { out[n] = c; n = n + 1 } 204 } 205 if fgr_is_off(base, side, dest) == 0 { 206 if fgr_blocked(base, side, dest) == 0 { out[n] = c; n = n + 1 } 207 } 208 } 209 c = c + 1 210 } 211 return n 212} 213 214func fgr_put(base: i64, side: i64, c: i64, delta: i64) -> i64 { 215 let st: *i64 = fg_state(base) 216 if side == 1 { st[c] = st[c] + delta; return 0 } 217 st[c] = st[c] - delta 218 return 0 219} 220 221func fgr_apply(base: i64, from: i64, d: i64) -> i64 { 222 let hd: *i64 = fg_hdr(base) 223 let sc: *i64 = fg_scal(base) 224 let side: i64 = sc[FG_S_SIDE] 225 let opp: i64 = 3 - side 226 var dest: i64 = 0 227 if from == FGR_FROM_BAR { 228 dest = fgr_entry_point(base, side, d) 229 if side == 1 { sc[FGR_S_BAR1] = sc[FGR_S_BAR1] - 1 } 230 if side == 2 { sc[FGR_S_BAR2] = sc[FGR_S_BAR2] - 1 } 231 } 232 if from != FGR_FROM_BAR { 233 dest = fgr_dest(base, side, from, d) 234 fgr_put(base, side, from, 0 - 1) 235 if fgr_is_off(base, side, dest) == 1 { 236 if side == 1 { sc[FGR_S_OFF1] = sc[FGR_S_OFF1] + 1 } 237 if side == 2 { sc[FGR_S_OFF2] = sc[FGR_S_OFF2] + 1 } 238 sc[FG_S_PLIES] = sc[FG_S_PLIES] + 1 239 return dest 240 } 241 } 242 // HIT: a single opponent piece on the landing point goes to the bar. Checked AFTER the mover has 243 // left its origin so a piece can never hit itself, and only where the spec allows hitting. 244 if hd[FG_H_HIT] == 1 { 245 if fgr_on(base, opp, dest) == 1 { 246 fgr_put(base, opp, dest, 0 - 1) 247 if opp == 1 { sc[FGR_S_BAR1] = sc[FGR_S_BAR1] + 1 } 248 if opp == 2 { sc[FGR_S_BAR2] = sc[FGR_S_BAR2] + 1 } 249 } 250 } 251 fgr_put(base, side, dest, 1) 252 sc[FG_S_PLIES] = sc[FG_S_PLIES] + 1 253 return dest 254} 255 256// A side that has borne off every piece has won. Where the spec has no bearing off, the race ends 257// when a side has no piece left short of the end -- and a position with no legal move for either die 258// is NOT a loss, it is a turn passed, which is why this asks about pieces and never about moves. 259func fgr_terminal(base: i64) -> i64 { 260 let sc: *i64 = fg_scal(base) 261 if sc[FG_S_WINNER] == 1 { return FG_T_P1 } 262 if sc[FG_S_WINNER] == 2 { return FG_T_P2 } 263 let p: i64 = fgr_pieces(base) 264 if fgr_off(base, 1) >= p { return FG_T_P1 } 265 if fgr_off(base, 2) >= p { return FG_T_P2 } 266 return FG_T_ONGOING 267}