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}