lang migration: rename signed integer types sN -> iN

Mechanical sweep of all .sx sources, plan docs, and tests/expected
snapshots for the sx language rename (s8/s16/s32/s64 -> i8/i16/i32/i64).
Verified: tools/run_tests.sh 23/23.

Note: the ios-sim build has 2 pre-existing 'restart' dot-call errors
from the sx opt-in UFCS change (sx a47ea14) — independent of this
rename (present pre-sweep); migrated in the follow-up commit.
This commit is contained in:
swipelab
2026-06-12 09:36:51 +03:00
parent 1ab74c7d08
commit 6f7d2f4db2
36 changed files with 344 additions and 265 deletions

102
board.sx
View File

@@ -39,36 +39,36 @@ EMPTY_CHAR :: 46; // '.'
gem_char :: (g: Gem) -> u8 {
if g == .empty { return EMPTY_CHAR; }
GEM_CHARS[cast(s64) g]
GEM_CHARS[cast(i64) g]
}
// ── Deterministic RNG ─────────────────────────────────────────────────────
// A 32-bit linear congruential generator (Numerical Recipes constants),
// carried in an s64 and masked back to 32 bits after every step so the
// carried in an i64 and masked back to 32 bits after every step so the
// stream is identical regardless of host integer width. The state*MUL+ADD
// product stays well under s64 range, so no intermediate overflow. Any seed
// product stays well under i64 range, so no intermediate overflow. Any seed
// (including 0) yields a valid stream — an LCG has no forbidden state.
RNG_MASK32 :: 0xFFFFFFFF;
RNG_MUL :: 1664525;
RNG_ADD :: 1013904223;
Rng :: struct {
state: s64;
state: i64;
// Advance and return the next 32-bit value.
next_u32 :: (self: *Rng) -> s64 {
next_u32 :: (self: *Rng) -> i64 {
self.state = (self.state * RNG_MUL + RNG_ADD) & RNG_MASK32;
self.state
}
// Uniform-ish value in [0, n). Uses the high bits, whose period is far
// longer than the low bits of an LCG.
next_range :: (self: *Rng, n: s64) -> s64 {
next_range :: (self: *Rng, n: i64) -> i64 {
(self.next_u32() >> 16) % n
}
}
rng_seeded :: (seed: s64) -> Rng {
rng_seeded :: (seed: i64) -> Rng {
Rng.{ state = seed & RNG_MASK32 }
}
@@ -106,7 +106,7 @@ Board :: struct {
// round's base points (see `score_round`), and `resolve` adds each cascade
// round's base scaled by `combo_multiplier` (P3.2). The HUD (P4.4) reads this
// field. A hand-built board must zero this before accumulating.
score: s64;
score: i64;
// Turn accounting (P3.3). `moves_made` counts the swaps actually COMMITTED —
// only a legal swap (one that resolved into >=1 match) via `commit_swap`
@@ -115,15 +115,15 @@ Board :: struct {
// is derived from the two, so there is a single source of truth and the
// counters can never drift apart. A hand-built board must set both before
// committing swaps.
moves_made: s64;
move_limit: s64;
moves_made: i64;
move_limit: i64;
// Per-level score goal (P7.1). `init` sets it to DEFAULT_TARGET_SCORE;
// `level_status` reads it to decide a win (`score >= target_score`). A
// hand-built board must set this before its status is read.
target_score: s64;
target_score: i64;
idx :: (col: s64, row: s64) -> s64 {
idx :: (col: i64, row: i64) -> i64 {
row * BOARD_COLS + col
}
@@ -131,15 +131,15 @@ Board :: struct {
// to 0 when the budget is spent (and below it only if a caller keeps
// committing past the budget — see DEFAULT_MOVE_LIMIT). The turn/goal loop
// (P7) reads this to decide when the game ends.
moves_remaining :: (self: *Board) -> s64 {
moves_remaining :: (self: *Board) -> i64 {
self.move_limit - self.moves_made
}
at :: (self: *Board, col: s64, row: s64) -> Gem {
at :: (self: *Board, col: i64, row: i64) -> Gem {
self.cells[Board.idx(col, row)]
}
set :: (self: *Board, col: s64, row: s64, g: Gem) {
set :: (self: *Board, col: i64, row: i64, g: Gem) {
self.cells[Board.idx(col, row)] = g;
}
@@ -149,7 +149,7 @@ Board :: struct {
// already-placed cells to its left or above is excluded, and the gem is
// drawn from the remaining allowed types. At most two types are ever
// excluded, so a choice always remains.
init :: (self: *Board, seed: s64) {
init :: (self: *Board, seed: i64) {
self.rng = rng_seeded(seed);
self.score = 0;
self.moves_made = 0;
@@ -165,7 +165,7 @@ Board :: struct {
// Choose a gem for (col, row) that can't extend an existing run leftward or
// upward. Pure given the board's already-placed prefix and the RNG state.
pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
pick_gem :: (board: *Board, rng: *Rng, col: i64, row: i64) -> Gem {
forbidden : [GEM_COUNT]bool = ---;
for 0..GEM_COUNT (t) { forbidden[t] = false; }
@@ -173,14 +173,14 @@ pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
if col >= 2 {
left := board.at(col - 1, row);
if left == board.at(col - 2, row) {
forbidden[cast(s64) left] = true;
forbidden[cast(i64) left] = true;
}
}
// Two same gems immediately above → a third of that type matches.
if row >= 2 {
up := board.at(col, row - 1);
if up == board.at(col, row - 2) {
forbidden[cast(s64) up] = true;
forbidden[cast(i64) up] = true;
}
}
@@ -223,12 +223,12 @@ board_dump :: (self: *Board) -> string {
MatchMask :: struct {
cells: [BOARD_CELLS]bool;
at :: (self: *MatchMask, col: s64, row: s64) -> bool {
at :: (self: *MatchMask, col: i64, row: i64) -> bool {
self.cells[Board.idx(col, row)]
}
count :: (self: *MatchMask) -> s64 {
n : s64 = 0;
count :: (self: *MatchMask) -> i64 {
n : i64 = 0;
for 0..BOARD_CELLS (i) { if self.cells[i] { n += 1; } }
n
}
@@ -237,7 +237,7 @@ MatchMask :: struct {
// Mark a closed span of cells along one axis. `vertical` picks the axis; `fixed`
// is the constant coordinate (the row for a horizontal span, the column for a
// vertical one) and the span covers `start..end` of the moving coordinate.
mark_run :: (m: *MatchMask, vertical: bool, fixed: s64, start: s64, end: s64) {
mark_run :: (m: *MatchMask, vertical: bool, fixed: i64, start: i64, end: i64) {
for start..end (i) {
if vertical {
m.cells[Board.idx(fixed, i)] = true;
@@ -317,8 +317,8 @@ dump_matches :: (b: *Board, m: *MatchMask) -> string {
// A board cell address. Kept separate from the row-major index so swap callers
// and the move enumeration speak in (col, row) like the rest of the model.
Cell :: struct {
col: s64;
row: s64;
col: i64;
row: i64;
}
// Exchange the gems of two cells, in place. `swap` is its own inverse: calling
@@ -413,8 +413,8 @@ dump_swaps :: (swaps: *List(Swap)) -> string {
// unchanged. Returns the number of cells cleared. `mask` is the matched-cell SET
// from find_matches, so overlapping L/T shapes (already unioned into a single
// `true` per shared cell) clear as one set with no double-counting.
clear_cells :: (board: *Board, mask: *MatchMask) -> s64 {
cleared : s64 = 0;
clear_cells :: (board: *Board, mask: *MatchMask) -> i64 {
cleared : i64 = 0;
for 0..BOARD_CELLS (i) {
if mask.cells[i] {
board.cells[i] = .empty;
@@ -428,7 +428,7 @@ clear_cells :: (board: *Board, mask: *MatchMask) -> s64 {
// cells cleared — 0 when there are no matches, in which case the board is left
// unchanged. The count drives later cascade/scoring (P2.2+): a non-zero result
// means the board changed and the resolution loop should continue.
clear_matches :: (board: *Board) -> s64 {
clear_matches :: (board: *Board) -> i64 {
m := find_matches(board);
clear_cells(board, @m)
}
@@ -489,9 +489,9 @@ collapse :: (board: *Board) -> bool {
// only ever yields ordinals 0..GEM_COUNT, so a hole is never refilled with
// `.empty`; afterwards the board has no holes left. Returns the number of cells
// filled (0 on a board that had none).
refill :: (board: *Board) -> s64 {
refill :: (board: *Board) -> i64 {
rng := @board.rng;
filled : s64 = 0;
filled : i64 = 0;
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
if board.at(col, row) == .empty {
@@ -526,11 +526,11 @@ refill :: (board: *Board) -> s64 {
// make "did this settle clear a 4 / 5+ run" observable. `had_len4` /
// `had_len5_plus` are the boolean view of the same counts.
Cascade :: struct {
depth: s64;
cleared: List(s64);
awarded: s64;
len4: s64;
len5_plus: s64;
depth: i64;
cleared: List(i64);
awarded: i64;
len4: i64;
len5_plus: i64;
had_len4 :: (self: *Cascade) -> bool {
self.len4 > 0
@@ -546,7 +546,7 @@ Cascade :: struct {
// number of cells cleared this round — 0 iff the board was already stable, in
// which case nothing moves and no gem is drawn. `resolve` repeats this until it
// returns 0.
resolve_step :: (board: *Board) -> s64 {
resolve_step :: (board: *Board) -> i64 {
cleared := clear_matches(board);
if cleared == 0 { return 0; }
collapse(board);
@@ -560,7 +560,7 @@ resolve_step :: (board: *Board) -> s64 {
// Each round adds `score_round * combo_multiplier(round)` (round 1-based) to
// `Board.score`; an already-stable board returns depth 0, awards 0, untouched.
resolve :: (board: *Board) -> Cascade {
result := Cascade.{ depth = 0, cleared = List(s64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
result := Cascade.{ depth = 0, cleared = List(i64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
while true {
// Read the round's base points AND its special-match tally while the runs
// are still on the board: `resolve_step` clears them, so both have to be
@@ -604,15 +604,15 @@ SCORE_RUN_5_PLUS :: 100;
// vertical one) and the run covers `start..start+len` of the moving coordinate.
Run :: struct {
vertical: bool;
fixed: s64;
start: s64;
len: s64;
fixed: i64;
start: i64;
len: i64;
}
// Base points for a single maximal run, by length. Runs are always length >= 3
// (shorter spans are not enumerated), so 3 is the floor; 5 and longer all score
// the top tier.
run_score :: (len: s64) -> s64 {
run_score :: (len: i64) -> i64 {
if len <= 3 { return SCORE_RUN_3; }
if len == 4 { return SCORE_RUN_4; }
SCORE_RUN_5_PLUS
@@ -666,9 +666,9 @@ find_runs :: (b: *Board) -> List(Run) {
// read-only — it inspects the board but changes nothing, so it must be called
// BEFORE the round's clear, while the runs are still on the board. A board with
// no run scores 0.
score_round :: (board: *Board) -> s64 {
score_round :: (board: *Board) -> i64 {
runs := find_runs(board);
total : s64 = 0;
total : i64 = 0;
for 0..runs.len (i) {
total += run_score(runs.items[i].len);
}
@@ -679,7 +679,7 @@ score_round :: (board: *Board) -> s64 {
// `score` total and return them. The single-round accumulation primitive; the
// cascade loop (`resolve`) instead scales each round by `combo_multiplier`
// (P3.2). Neither path changes `score_round`.
add_round_score :: (board: *Board) -> s64 {
add_round_score :: (board: *Board) -> i64 {
points := score_round(board);
board.score += points;
points
@@ -694,7 +694,7 @@ add_round_score :: (board: *Board) -> s64 {
// multi-round chain strictly beats the same clears scored flat. `resolve`
// accumulates `score_round * combo_multiplier(round)` per round into `Board.score`
// and reports the sum as `Cascade.awarded`.
combo_multiplier :: (round: s64) -> s64 {
combo_multiplier :: (round: i64) -> i64 {
round
}
@@ -711,8 +711,8 @@ combo_multiplier :: (round: s64) -> s64 {
// "did any occur" lives on `Cascade` (`had_len4` / `had_len5_plus`) for the
// whole settle; a single round reads these counts directly.
SpecialCounts :: struct {
len4: s64;
len5_plus: s64;
len4: i64;
len5_plus: i64;
}
// Count the board's currently-matched runs that hit a special length, by the
@@ -766,7 +766,7 @@ dump_runs :: (runs: *List(Run)) -> string {
PlayerMove :: struct {
legal: bool;
cascade: Cascade;
moves_remaining: s64;
moves_remaining: i64;
}
// Attempt the player's intended swap of two adjacent cells. If the swap is legal
@@ -779,7 +779,7 @@ PlayerMove :: struct {
// spent (that is the P7 turn-loop's call) — see DEFAULT_MOVE_LIMIT.
commit_swap :: (board: *Board, a: Cell, b: Cell) -> PlayerMove {
if !swap_legal(board, a, b) {
empty := Cascade.{ depth = 0, cleared = List(s64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
empty := Cascade.{ depth = 0, cleared = List(i64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
return PlayerMove.{ legal = false, cascade = empty, moves_remaining = board.moves_remaining() };
}
swap(board, a, b);
@@ -900,7 +900,7 @@ reshuffle_if_deadlocked :: (board: *Board) -> bool {
// seed → identical starting layout), zeroes `score` and `moves_made`, and
// restores the default move budget and score goal, so `level_status` reads
// `in_progress` again. The entry point P7.2's restart button calls.
restart :: (board: *Board, seed: s64) {
restart :: (board: *Board, seed: i64) {
board.init(seed);
}
@@ -930,7 +930,7 @@ TurnResult :: struct {
play_turn :: (board: *Board, a: Cell, b: Cell) -> TurnResult {
status := level_status(board);
if status != .in_progress {
empty := Cascade.{ depth = 0, cleared = List(s64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
empty := Cascade.{ depth = 0, cleared = List(i64).{}, awarded = 0, len4 = 0, len5_plus = 0 };
frozen := PlayerMove.{ legal = false, cascade = empty, moves_remaining = board.moves_remaining() };
return TurnResult.{ accepted = false, move = frozen, status = status, reshuffled = false };
}