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

29
.vscode/ios-sim-debug.sh vendored Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# preLaunchTask for "Debug SxChess (iOS sim)": build SxChess for the iOS
# simulator with debug info, install it to a single simulator, and launch it
# PAUSED for a debugger (--wait-for-debugger). The launch.json `custom` config
# then `target create`s the local binary (so the .dSYM resolves breakpoints)
# and `process attach -n SxChess`es the waiting process.
set -e
SX="/Users/agra/projects/sx/zig-out/bin/sx"
DSYMUTIL="/opt/homebrew/opt/llvm@19/bin/dsymutil"
# Reuse ONE simulator (sx-test-ios18). Swap the UDID for a different device.
SIM="E8DF755C-997D-49D7-9DB0-CFA48F8254CD"
APP="sx-out/ios/M3te.app"
BUNDLE="co.swipelab.m3te"
# Debug build: -O0 + DWARF (--emit-obj keeps the object), then a portable .dSYM.
"$SX" build --target ios-sim --emit-obj main.sx
"$DSYMUTIL" "$APP/M3te"
# Boot the sim (idempotent) and surface the Simulator UI.
xcrun simctl boot "$SIM" 2>/dev/null || true
xcrun simctl bootstatus "$SIM" >/dev/null 2>&1 || true
open -a Simulator
xcrun simctl install "$SIM" "$APP"
xcrun simctl terminate "$SIM" "$BUNDLE" >/dev/null 2>&1 || true
# Launch paused; the app waits at its entry until lldb attaches and continues.
xcrun simctl launch --wait-for-debugger "$SIM" "$BUNDLE"

27
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,27 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug M3te (macOS)",
"program": "${workspaceFolder}/sx-out/macos/M3te.app/Contents/MacOS/M3te",
"cwd": "${workspaceFolder}",
"preLaunchTask": "sx build (macos debug)"
},
{
"type": "lldb",
"request": "custom",
"name": "Debug M3te (iOS sim)",
"preLaunchTask": "sx build+launch (ios-sim debug)",
"targetCreateCommands": [
"target create '${workspaceFolder}/sx-out/ios/M3te.app/M3te'"
],
"processCreateCommands": [
"process attach -n M3te",
"process handle SIGSTOP --notify false --stop false --pass false",
"process continue"
]
}
]
}

23
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "sx build (macos debug)",
"detail": "Build M3te for macOS with DWARF (-O0) and collect a .dSYM for lldb.",
"type": "shell",
"command": "/Users/agra/projects/sx/zig-out/bin/sx build --target macos --emit-obj main.sx && /opt/homebrew/opt/llvm@19/bin/dsymutil sx-out/macos/M3te.app/Contents/MacOS/M3te",
"options": { "cwd": "${workspaceFolder}" },
"group": "build",
"problemMatcher": []
},
{
"label": "sx build+launch (ios-sim debug)",
"detail": "Build for the iOS simulator (-O0 + .dSYM), install, and launch paused for the debugger.",
"type": "shell",
"command": "${workspaceFolder}/.vscode/ios-sim-debug.sh",
"options": { "cwd": "${workspaceFolder}" },
"group": "build",
"problemMatcher": []
}
]
}

View File

@@ -15,12 +15,12 @@
// AudioToolbox — System Sound Services. SystemSoundID is a UInt32; OSStatus a // AudioToolbox — System Sound Services. SystemSoundID is a UInt32; OSStatus a
// SInt32 (0 == noErr); the clip's file is passed as a CFURLRef (opaque ptr). // SInt32 (0 == noErr); the clip's file is passed as a CFURLRef (opaque ptr).
AudioServicesCreateSystemSoundID :: (url: *void, out_id: *u32) -> s32 #foreign; AudioServicesCreateSystemSoundID :: (url: *void, out_id: *u32) -> i32 #foreign;
AudioServicesPlaySystemSound :: (sound_id: u32) #foreign; AudioServicesPlaySystemSound :: (sound_id: u32) #foreign;
// CoreFoundation — build a file CFURL from an absolute path. `len` is a CFIndex // CoreFoundation — build a file CFURL from an absolute path. `len` is a CFIndex
// (long); `is_dir` a Boolean (unsigned char); a NULL allocator = default. // (long); `is_dir` a Boolean (unsigned char); a NULL allocator = default.
CFURLCreateFromFileSystemRepresentation :: (allocator: *void, buffer: *u8, len: s64, is_dir: s8) -> *void #foreign; CFURLCreateFromFileSystemRepresentation :: (allocator: *void, buffer: *u8, len: i64, is_dir: i8) -> *void #foreign;
CFRelease :: (cf: *void) #foreign; CFRelease :: (cf: *void) #foreign;
// libc — getcwd to absolutize the bundle-relative asset path. The platform // libc — getcwd to absolutize the bundle-relative asset path. The platform
@@ -80,7 +80,7 @@ GameAudio :: struct {
// Pick the ascending cascade clip by clamping the cascade depth into the // Pick the ascending cascade clip by clamping the cascade depth into the
// combo1..combo5 range (see `cascade_cue_index`). // combo1..combo5 range (see `cascade_cue_index`).
play_cascade :: (self: *GameAudio, depth: s64) { play_cascade :: (self: *GameAudio, depth: i64) {
inline if OS != .ios { return; } inline if OS != .ios { return; }
if !self.loaded { return; } if !self.loaded { return; }
idx := cascade_cue_index(depth); idx := cascade_cue_index(depth);
@@ -107,7 +107,7 @@ GameAudio :: struct {
// `log show` shows the clip stepping up with cascade depth. Literals only — the // `log show` shows the clip stepping up with cascade depth. Literals only — the
// string→NSString bridge needs NUL-terminated bytes (a formatted string may not // string→NSString bridge needs NUL-terminated bytes (a formatted string may not
// be). `idx` is a clamped `cascade_cue_index`, so it is always 0..COMBO_CLIPS-1. // be). `idx` is a clamped `cascade_cue_index`, so it is always 0..COMBO_CLIPS-1.
cascade_cue_name :: (idx: s64) -> string { cascade_cue_name :: (idx: i64) -> string {
if idx <= 0 { return "[sx] audio: cue combo1"; } if idx <= 0 { return "[sx] audio: cue combo1"; }
if idx == 1 { return "[sx] audio: cue combo2"; } if idx == 1 { return "[sx] audio: cue combo2"; }
if idx == 2 { return "[sx] audio: cue combo3"; } if idx == 2 { return "[sx] audio: cue combo3"; }
@@ -118,7 +118,7 @@ cascade_cue_name :: (idx: s64) -> string {
// Cascade depth (number of cleared rounds) → combo clip index 0..COMBO_CLIPS-1 // Cascade depth (number of cleared rounds) → combo clip index 0..COMBO_CLIPS-1
// (combo1..combo5). Clamps: depth <= 1 → 0, depth >= 5 → 4. Pure arithmetic and // (combo1..combo5). Clamps: depth <= 1 → 0, depth >= 5 → 4. Pure arithmetic and
// OS-agnostic so it can be snapshot-tested headlessly (P10.4). // OS-agnostic so it can be snapshot-tested headlessly (P10.4).
cascade_cue_index :: (depth: s64) -> s64 { cascade_cue_index :: (depth: i64) -> i64 {
if depth <= 1 { return 0; } if depth <= 1 { return 0; }
if depth >= COMBO_CLIPS { return COMBO_CLIPS - 1; } if depth >= COMBO_CLIPS { return COMBO_CLIPS - 1; }
depth - 1 depth - 1
@@ -145,7 +145,7 @@ load_system_sound :: (name: string) -> u32 {
if getcwd(@cwd_buf[0], 1024) == null { return 0; } if getcwd(@cwd_buf[0], 1024) == null { return 0; }
cwd : string = ---; cwd : string = ---;
cwd.ptr = @cwd_buf[0]; cwd.ptr = @cwd_buf[0];
cwd.len = cast(s64) c_strlen(@cwd_buf[0]); cwd.len = cast(i64) c_strlen(@cwd_buf[0]);
// CFURLCreateFromFileSystemRepresentation takes an explicit byte length, so // CFURLCreateFromFileSystemRepresentation takes an explicit byte length, so
// the formatted path needs no NUL terminator. // the formatted path needs no NUL terminator.
@@ -169,6 +169,6 @@ g_audio : *GameAudio = null;
sfx_swap :: () { if g_audio != null { g_audio.play_swap(); } } sfx_swap :: () { if g_audio != null { g_audio.play_swap(); } }
sfx_match :: () { if g_audio != null { g_audio.play_match(); } } sfx_match :: () { if g_audio != null { g_audio.play_match(); } }
sfx_cascade :: (depth: s64) { if g_audio != null { g_audio.play_cascade(depth); } } sfx_cascade :: (depth: i64) { if g_audio != null { g_audio.play_cascade(depth); } }
sfx_win :: () { if g_audio != null { g_audio.play_win(); } } sfx_win :: () { if g_audio != null { g_audio.play_win(); } }
sfx_lose :: () { if g_audio != null { g_audio.play_lose(); } } sfx_lose :: () { if g_audio != null { g_audio.play_lose(); } }

102
board.sx
View File

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

View File

@@ -109,7 +109,7 @@ bad_swap_bounce :: (t: f32) -> f32 {
// ease_in_cubic so each column still accelerates under gravity within its window. // ease_in_cubic so each column still accelerates under gravity within its window.
// `tests/easing.sx` pins f(0)=0, f(1)=1, monotonicity, and the cascade ordering. // `tests/easing.sx` pins f(0)=0, f(1)=1, monotonicity, and the cascade ordering.
FALL_STAGGER_MAX :f32: 0.30; FALL_STAGGER_MAX :f32: 0.30;
fall_stagger_t :: (t: f32, col: s64) -> f32 { fall_stagger_t :: (t: f32, col: i64) -> f32 {
delay := FALL_STAGGER_MAX * (cast(f32) col / cast(f32) (BOARD_COLS - 1)); delay := FALL_STAGGER_MAX * (cast(f32) col / cast(f32) (BOARD_COLS - 1));
window := 1.0 - FALL_STAGGER_MAX; window := 1.0 - FALL_STAGGER_MAX;
lt := (t - delay) / window; lt := (t - delay) / window;
@@ -123,7 +123,7 @@ fall_stagger_t :: (t: f32, col: s64) -> f32 {
// at `1 - FALL_STAGGER_MAX`; the last column lands exactly at 1.0. The landing // at `1 - FALL_STAGGER_MAX`; the last column lands exactly at 1.0. The landing
// squash-bounce (P17.3) ages from this instant per column, so the squash begins // squash-bounce (P17.3) ages from this instant per column, so the squash begins
// the moment a gem touches its cell rather than at a flat whole-row settle. // the moment a gem touches its cell rather than at a flat whole-row settle.
fall_landing_frac :: (col: s64) -> f32 { fall_landing_frac :: (col: i64) -> f32 {
(1.0 - FALL_STAGGER_MAX) + FALL_STAGGER_MAX * (cast(f32) col / cast(f32) (BOARD_COLS - 1)) (1.0 - FALL_STAGGER_MAX) + FALL_STAGGER_MAX * (cast(f32) col / cast(f32) (BOARD_COLS - 1))
} }
@@ -132,7 +132,7 @@ fall_landing_frac :: (col: s64) -> f32 {
// per-round bounce ages from. Round k's fall starts after the swap, k clear+fall // per-round bounce ages from. Round k's fall starts after the swap, k clear+fall
// pairs, and that round's own clear; column `col` then lands `fall_landing_frac` // pairs, and that round's own clear; column `col` then lands `fall_landing_frac`
// of the fall window into it. Pure + headless, mirrors `phase`'s segment walk. // of the fall window into it. Pure + headless, mirrors `phase`'s segment walk.
round_land_time :: (k: s64, col: s64) -> f32 { round_land_time :: (k: i64, col: i64) -> f32 {
SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR) + CLEAR_ANIM_DUR SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR) + CLEAR_ANIM_DUR
+ fall_landing_frac(col) * FALL_ANIM_DUR + fall_landing_frac(col) * FALL_ANIM_DUR
} }
@@ -159,10 +159,10 @@ clear_ripple_t :: (t: f32, u: f32) -> f32 {
// The diagonal (col+row) extent of a round's matched cells — the span the ripple // The diagonal (col+row) extent of a round's matched cells — the span the ripple
// ranks each matched gem across. `hi < lo` only if the mask is empty. // ranks each matched gem across. `hi < lo` only if the mask is empty.
ClearDiag :: struct { lo: s64; hi: s64; } ClearDiag :: struct { lo: i64; hi: i64; }
clear_diag_span :: (m: *MatchMask) -> ClearDiag { clear_diag_span :: (m: *MatchMask) -> ClearDiag {
lo : s64 = (BOARD_COLS - 1) + (BOARD_ROWS - 1) + 1; lo : i64 = (BOARD_COLS - 1) + (BOARD_ROWS - 1) + 1;
hi : s64 = -1; hi : i64 = -1;
for 0..BOARD_CELLS (i) { for 0..BOARD_CELLS (i) {
if m.cells[i] { if m.cells[i] {
d := (i % BOARD_COLS) + (i / BOARD_COLS); d := (i % BOARD_COLS) + (i / BOARD_COLS);
@@ -178,7 +178,7 @@ clear_diag_span :: (m: *MatchMask) -> ClearDiag {
// PER ROUND (not across the board) lets even a small 3-match ripple across the // PER ROUND (not across the board) lets even a small 3-match ripple across the
// full stagger budget. A degenerate span (every matched cell on one diagonal) // full stagger budget. A degenerate span (every matched cell on one diagonal)
// ranks all gems 0, so they pop together rather than dividing by zero. // ranks all gems 0, so they pop together rather than dividing by zero.
clear_rank :: (span: ClearDiag, col: s64, row: s64) -> f32 { clear_rank :: (span: ClearDiag, col: i64, row: i64) -> f32 {
if span.hi <= span.lo { return 0.0; } if span.hi <= span.lo { return 0.0; }
cast(f32) ((col + row) - span.lo) / cast(f32) (span.hi - span.lo) cast(f32) ((col + row) - span.lo) / cast(f32) (span.hi - span.lo)
} }
@@ -193,7 +193,7 @@ clear_rank :: (span: ClearDiag, col: s64, row: s64) -> f32 {
AnimRound :: struct { AnimRound :: struct {
before: [BOARD_CELLS]Gem; before: [BOARD_CELLS]Gem;
matched: MatchMask; matched: MatchMask;
src: [BOARD_CELLS]s64; src: [BOARD_CELLS]i64;
after: [BOARD_CELLS]Gem; after: [BOARD_CELLS]Gem;
} }
@@ -210,7 +210,7 @@ AnimMove :: struct {
pre: [BOARD_CELLS]Gem; pre: [BOARD_CELLS]Gem;
rounds: List(AnimRound); rounds: List(AnimRound);
final: [BOARD_CELLS]Gem; final: [BOARD_CELLS]Gem;
awarded: s64; awarded: i64;
} }
// The most recent round at or before `kmax` that dropped a MOVED gem onto // The most recent round at or before `kmax` that dropped a MOVED gem onto
@@ -221,7 +221,7 @@ AnimMove :: struct {
// ages from its LATEST arrival, never a stale earlier one. Pure + headless: the // ages from its LATEST arrival, never a stale earlier one. Pure + headless: the
// per-round bounce (render_fall/clear) and the final-settle stamp share this so // per-round bounce (render_fall/clear) and the final-settle stamp share this so
// one envelope plays continuously across every seam. // one envelope plays continuously across every seam.
delivering_round :: (mv: *AnimMove, i: s64, kmax: s64) -> s64 { delivering_round :: (mv: *AnimMove, i: i64, kmax: i64) -> i64 {
row := i / BOARD_COLS; row := i / BOARD_COLS;
k := kmax; k := kmax;
while k >= 0 { while k >= 0 {
@@ -312,7 +312,7 @@ AnimPhaseKind :: enum { swap; clear; fall; done; }
AnimPhase :: struct { AnimPhase :: struct {
kind: AnimPhaseKind; kind: AnimPhaseKind;
round: s64; round: i64;
t: f32; t: f32;
} }
@@ -327,7 +327,7 @@ BoardAnim :: struct {
// so the frame loop's per-round SFX is edge-triggered: a round's cue fires once, // so the frame loop's per-round SFX is edge-triggered: a round's cue fires once,
// when its clear begins, never re-fired every frame. Reset whenever a move // when its clear begins, never re-fired every frame. Reset whenever a move
// (re)starts; advanced by the frame loop as rounds clear. // (re)starts; advanced by the frame loop as rounds clear.
cascade_fired: s64; cascade_fired: i64;
init :: (self: *BoardAnim) { init :: (self: *BoardAnim) {
self.active = false; self.active = false;
@@ -383,11 +383,11 @@ BoardAnim :: struct {
// have fired by now (clamped to the move's round count). The frame loop diffs it // have fired by now (clamped to the move's round count). The frame loop diffs it
// against `BoardAnim.cascade_fired` to play one cue per newly-cleared round. Pure + // against `BoardAnim.cascade_fired` to play one cue per newly-cleared round. Pure +
// headless so the per-round playback is snapshot-testable without audio. // headless so the per-round playback is snapshot-testable without audio.
cascade_rounds_started :: (elapsed: f32, num_rounds: s64) -> s64 { cascade_rounds_started :: (elapsed: f32, num_rounds: i64) -> i64 {
if num_rounds <= 0 { return 0; } if num_rounds <= 0 { return 0; }
if elapsed < SWAP_ANIM_DUR { return 0; } if elapsed < SWAP_ANIM_DUR { return 0; }
seg := CLEAR_ANIM_DUR + FALL_ANIM_DUR; seg := CLEAR_ANIM_DUR + FALL_ANIM_DUR;
started := cast(s64) ((elapsed - SWAP_ANIM_DUR) / seg) + 1; started := cast(i64) ((elapsed - SWAP_ANIM_DUR) / seg) + 1;
if started > num_rounds { return num_rounds; } if started > num_rounds { return num_rounds; }
started started
} }

View File

@@ -48,7 +48,7 @@ FX_COMBO_LABEL_GAP :f32: 0.12; // gap (cell units) between the label and +poin
// order (red, orange, yellow, green, blue, purple). Saturated a touch past the // order (red, orange, yellow, green, blue, purple). Saturated a touch past the
// pastel — the low channel is trimmed while the dominant/mid channel is lifted — // pastel — the low channel is trimmed while the dominant/mid channel is lifted —
// so every burst pops as a punchier colour without losing luminance. // so every burst pops as a punchier colour without losing luminance.
fx_tint :: (i: s64) -> Color { fx_tint :: (i: i64) -> Color {
if i == 0 { return Color.{ r = 255, g = 92, b = 62, a = 255 }; } if i == 0 { return Color.{ r = 255, g = 92, b = 62, a = 255 }; }
if i == 1 { return Color.{ r = 255, g = 164, b = 44, a = 255 }; } if i == 1 { return Color.{ r = 255, g = 164, b = 44, a = 255 }; }
if i == 2 { return Color.{ r = 255, g = 240, b = 72, a = 255 }; } if i == 2 { return Color.{ r = 255, g = 240, b = 72, a = 255 }; }
@@ -67,7 +67,7 @@ FX_POPUP_COMBO_HOT :: Color.{ r = 255, g = 248, b = 214, a = 255 }; // hot n
// lockstep with the cascade SFX cue. Pure arithmetic, OS-agnostic, and the // lockstep with the cascade SFX cue. Pure arithmetic, OS-agnostic, and the
// equivalence to `cascade_cue_index` is locked headlessly (tests/fx_combo.sx). // equivalence to `cascade_cue_index` is locked headlessly (tests/fx_combo.sx).
FX_COMBO_MAX_LEVEL :: 4; // == audio.sx COMBO_CLIPS - 1 FX_COMBO_MAX_LEVEL :: 4; // == audio.sx COMBO_CLIPS - 1
fx_combo_level :: (depth: s64) -> s64 { fx_combo_level :: (depth: i64) -> i64 {
if depth <= 1 { return 0; } if depth <= 1 { return 0; }
if depth >= FX_COMBO_MAX_LEVEL + 1 { return FX_COMBO_MAX_LEVEL; } if depth >= FX_COMBO_MAX_LEVEL + 1 { return FX_COMBO_MAX_LEVEL; }
depth - 1 depth - 1
@@ -76,14 +76,14 @@ fx_combo_level :: (depth: s64) -> s64 {
// Popup font size for a cascade `depth` rounds deep: a single clear (depth <= 1) // Popup font size for a cascade `depth` rounds deep: a single clear (depth <= 1)
// uses the plain size; a combo starts at the base combo size and grows one step // uses the plain size; a combo starts at the base combo size and grows one step
// per combo level past the first, clamped at the deepest level. // per combo level past the first, clamped at the deepest level.
fx_popup_font :: (depth: s64) -> f32 { fx_popup_font :: (depth: i64) -> f32 {
if depth <= 1 { return FX_POPUP_FONT; } if depth <= 1 { return FX_POPUP_FONT; }
FX_POPUP_COMBO_FONT + FX_POPUP_COMBO_STEP * cast(f32) (fx_combo_level(depth) - 1) FX_POPUP_COMBO_FONT + FX_POPUP_COMBO_STEP * cast(f32) (fx_combo_level(depth) - 1)
} }
// Popup colour for a cascade `depth` rounds deep: white for a single clear, else // Popup colour for a cascade `depth` rounds deep: white for a single clear, else
// the gold lerped toward a hot near-white as the cascade deepens. // the gold lerped toward a hot near-white as the cascade deepens.
fx_popup_color :: (depth: s64) -> Color { fx_popup_color :: (depth: i64) -> Color {
if depth <= 1 { return FX_POPUP_COLOR; } if depth <= 1 { return FX_POPUP_COLOR; }
t := cast(f32) (fx_combo_level(depth) - 1) / cast(f32) (FX_COMBO_MAX_LEVEL - 1); t := cast(f32) (fx_combo_level(depth) - 1) / cast(f32) (FX_COMBO_MAX_LEVEL - 1);
Color.{ Color.{
@@ -101,7 +101,7 @@ fx_lerp_u8 :: (lo: u8, hi: u8, t: f32) -> u8 {
// Upload an RGBA buffer as a texture, returning its handle. Mirrors // Upload an RGBA buffer as a texture, returning its handle. Mirrors
// board_view.load_texture's upload half but takes an in-memory buffer (the // board_view.load_texture's upload half but takes an in-memory buffer (the
// per-colour tinted particle) instead of a file path. // per-colour tinted particle) instead of a file path.
upload_rgba :: (pixels: [*]u8, w: s32, h: s32, gpu: ?GPU) -> u32 { upload_rgba :: (pixels: [*]u8, w: i32, h: i32, gpu: ?GPU) -> u32 {
if gpu != null { if gpu != null {
return xx gpu.create_texture(w, h, .rgba8, xx pixels); return xx gpu.create_texture(w, h, .rgba8, xx pixels);
} }
@@ -128,22 +128,22 @@ BoardFxAssets :: struct {
} }
load :: (self: *BoardFxAssets, gpu: ?GPU) { load :: (self: *BoardFxAssets, gpu: ?GPU) {
w : s32 = 0; w : i32 = 0;
h : s32 = 0; h : i32 = 0;
ch : s32 = 0; ch : i32 = 0;
src : [*]u8 = xx stbi_load("assets/fx/particle.png", @w, @h, @ch, 4); src : [*]u8 = xx stbi_load("assets/fx/particle.png", @w, @h, @ch, 4);
if xx src == 0 { if xx src == 0 {
out("WARNING: could not load assets/fx/particle.png\n"); out("WARNING: could not load assets/fx/particle.png\n");
self.loaded = false; self.loaded = false;
return; return;
} }
n := cast(s64) w * cast(s64) h; n := cast(i64) w * cast(i64) h;
buf : [*]u8 = xx context.allocator.alloc_bytes(n * 4); buf : [*]u8 = xx context.allocator.alloc_bytes(n * 4);
// Loop locals are hoisted: a block-scoped local declared inside a body // Loop locals are hoisted: a block-scoped local declared inside a body
// that runs hundreds of thousands of times grows the stack per iteration // that runs hundreds of thousands of times grows the stack per iteration
// (sx codegen), so the per-pixel tint loop only ASSIGNS pre-declared vars. // (sx codegen), so the per-pixel tint loop only ASSIGNS pre-declared vars.
i : s64 = 0; i : i64 = 0;
o : s64 = 0; o : i64 = 0;
for 0..GEM_COUNT (t) { for 0..GEM_COUNT (t) {
col := fx_tint(t); col := fx_tint(t);
i = 0; i = 0;
@@ -168,7 +168,7 @@ BoardFxAssets :: struct {
FxParticle :: struct { FxParticle :: struct {
col: f32; col: f32;
row: f32; row: f32;
tint: s64; tint: i64;
delay: f32; delay: f32;
age: f32; age: f32;
life: f32; life: f32;
@@ -184,8 +184,8 @@ FxParticle :: struct {
FxPopup :: struct { FxPopup :: struct {
col: f32; col: f32;
row: f32; row: f32;
points: s64; points: i64;
depth: s64; depth: i64;
delay: f32; delay: f32;
age: f32; age: f32;
life: f32; life: f32;
@@ -237,7 +237,7 @@ BoardFx :: struct {
self.particles.append(FxParticle.{ self.particles.append(FxParticle.{
col = cast(f32) col + 0.5, col = cast(f32) col + 0.5,
row = cast(f32) row + 0.5, row = cast(f32) row + 0.5,
tint = cast(s64) g, tint = cast(i64) g,
delay = t0 + rdelay, delay = t0 + rdelay,
age = 0.0, age = 0.0,
life = FX_BURST_LIFE, life = FX_BURST_LIFE,
@@ -250,9 +250,9 @@ BoardFx :: struct {
// One popup for the whole move at the first clear's centroid. // One popup for the whole move at the first clear's centroid.
rd0 := @mv.rounds.items[0]; rd0 := @mv.rounds.items[0];
sc : s64 = 0; sc : i64 = 0;
sr : s64 = 0; sr : i64 = 0;
cnt : s64 = 0; cnt : i64 = 0;
for 0..BOARD_CELLS (idx) { for 0..BOARD_CELLS (idx) {
if rd0.matched.cells[idx] { if rd0.matched.cells[idx] {
sc += idx % BOARD_COLS; sc += idx % BOARD_COLS;
@@ -275,8 +275,8 @@ BoardFx :: struct {
// Advance every live FX by `dt` and drop those past their lifetime. Kept // Advance every live FX by `dt` and drop those past their lifetime. Kept
// simple: compact each list in place by overwriting dead entries. // simple: compact each list in place by overwriting dead entries.
tick :: (self: *BoardFx, dt: f32) { tick :: (self: *BoardFx, dt: f32) {
w : s64 = 0; w : i64 = 0;
i : s64 = 0; i : i64 = 0;
while i < self.particles.len { while i < self.particles.len {
p := self.particles.items[i]; p := self.particles.items[i];
p.age += dt; p.age += dt;

View File

@@ -25,7 +25,7 @@ BoardLayout :: struct {
}; };
} }
cell_frame :: (self: *BoardLayout, col: s64, row: s64) -> Frame { cell_frame :: (self: *BoardLayout, col: i64, row: i64) -> Frame {
Frame.make( Frame.make(
self.origin.x + xx col * self.cell_size, self.origin.x + xx col * self.cell_size,
self.origin.y + xx row * self.cell_size, self.origin.y + xx row * self.cell_size,
@@ -44,8 +44,8 @@ BoardLayout :: struct {
fx := (p.x - self.origin.x) / self.cell_size; fx := (p.x - self.origin.x) / self.cell_size;
fy := (p.y - self.origin.y) / self.cell_size; fy := (p.y - self.origin.y) / self.cell_size;
if fx < 0.0 or fy < 0.0 { return null; } if fx < 0.0 or fy < 0.0 { return null; }
col : s64 = xx fx; col : i64 = xx fx;
row : s64 = xx fy; row : i64 = xx fy;
if col >= BOARD_COLS or row >= BOARD_ROWS { return null; } if col >= BOARD_COLS or row >= BOARD_ROWS { return null; }
Cell.{ col = col, row = row } Cell.{ col = col, row = row }
} }

View File

@@ -126,7 +126,7 @@ BoardAssets :: struct {
self.loaded = self.bg_tex != 0 and self.cell_tex != 0 and self.gems_tex != 0; self.loaded = self.bg_tex != 0 and self.cell_tex != 0 and self.gems_tex != 0;
} }
gem_uv :: (self: *BoardAssets, index: s64) -> GemUV { gem_uv :: (self: *BoardAssets, index: i64) -> GemUV {
u0 : f32 = xx index * self.cell_u; u0 : f32 = xx index * self.cell_u;
GemUV.{ GemUV.{
uv_min = Point.{ x = u0, y = 0.0 }, uv_min = Point.{ x = u0, y = 0.0 },
@@ -139,9 +139,9 @@ BoardAssets :: struct {
// failure). When a GPU backend is bound (iOS Metal) it owns the upload; the // failure). When a GPU backend is bound (iOS Metal) it owns the upload; the
// desktop GL path falls back to a plain GL_TEXTURE_2D. // desktop GL path falls back to a plain GL_TEXTURE_2D.
load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 { load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
w : s32 = 0; w : i32 = 0;
h : s32 = 0; h : i32 = 0;
ch : s32 = 0; ch : i32 = 0;
pixels := stbi_load(path, @w, @h, @ch, 4); pixels := stbi_load(path, @w, @h, @ch, 4);
if pixels == null { if pixels == null {
out("WARNING: could not load texture: "); out("WARNING: could not load texture: ");
@@ -237,7 +237,7 @@ BoardView :: struct {
safe: EdgeInsets; safe: EdgeInsets;
// Seed for `restart`: the same fixed seed main seeded the board with, so the // Seed for `restart`: the same fixed seed main seeded the board with, so the
// restart button reproduces the identical starting level. // restart button reproduces the identical starting level.
seed: s64; seed: i64;
// FPS dev overlay (P20.1). `fps_on` gates the corner readout (off by default, // FPS dev overlay (P20.1). `fps_on` gates the corner readout (off by default,
// set only by the M3TE_FPS env pin); `fps` is the smoothed reciprocal frame // set only by the M3TE_FPS env pin); `fps` is the smoothed reciprocal frame
// rate computed in the frame loop. Purely a render overlay. // rate computed in the frame loop. Purely a render overlay.
@@ -266,7 +266,7 @@ BoardView :: struct {
} }
// Draw gem `gem_index`'s sprite-sheet column into `gf`. // Draw gem `gem_index`'s sprite-sheet column into `gf`.
draw_gem :: (self: *BoardView, ctx: *RenderContext, gf: Frame, gem_index: s64) { draw_gem :: (self: *BoardView, ctx: *RenderContext, gf: Frame, gem_index: i64) {
uv := self.assets.gem_uv(gem_index); uv := self.assets.gem_uv(gem_index);
ctx.add_image_uv(gf, self.assets.gems_tex, uv.uv_min, uv.uv_max); ctx.add_image_uv(gf, self.assets.gems_tex, uv.uv_min, uv.uv_max);
} }
@@ -285,7 +285,7 @@ BoardView :: struct {
// Frame for a gem shrunk by `scale` about its cell centre — the clear // Frame for a gem shrunk by `scale` about its cell centre — the clear
// scale-out. At scale 0 the gem is a zero-size frame (gone). // scale-out. At scale 0 the gem is a zero-size frame (gone).
gem_frame_scaled :: (self: *BoardView, col: s64, row: s64, dim: f32, scale: f32) -> Frame { gem_frame_scaled :: (self: *BoardView, col: i64, row: i64, dim: f32, scale: f32) -> Frame {
cs := self.layout.cell_size; cs := self.layout.cell_size;
cx := self.layout.origin.x + cast(f32) col * cs + cs * 0.5; cx := self.layout.origin.x + cast(f32) col * cs + cs * 0.5;
cy := self.layout.origin.y + cast(f32) row * cs + cs * 0.5; cy := self.layout.origin.y + cast(f32) row * cs + cs * 0.5;
@@ -297,7 +297,7 @@ BoardView :: struct {
// sprite is scaled about its cell centre and nudged by the pose offset (both // sprite is scaled about its cell centre and nudged by the pose offset (both
// in cell units). A resting pose reproduces gem_frame exactly, so the t==0 // in cell units). A resting pose reproduces gem_frame exactly, so the t==0
// idle pose draws identically to the static sprite. // idle pose draws identically to the static sprite.
gem_pose_frame :: (self: *BoardView, col: s64, row: s64, dim: f32, pose: GemPose) -> Frame { gem_pose_frame :: (self: *BoardView, col: i64, row: i64, dim: f32, pose: GemPose) -> Frame {
cs := self.layout.cell_size; cs := self.layout.cell_size;
cx := self.layout.origin.x + (cast(f32) col + 0.5) * cs + pose.dx * cs; cx := self.layout.origin.x + (cast(f32) col + 0.5) * cs + pose.dx * cs;
cy := self.layout.origin.y + (cast(f32) row + 0.5) * cs + pose.dy * cs; cy := self.layout.origin.y + (cast(f32) row + 0.5) * cs + pose.dy * cs;
@@ -311,7 +311,7 @@ BoardView :: struct {
// — the wide-and-short landing impact. sq==0 reproduces gem_frame's centred // — the wide-and-short landing impact. sq==0 reproduces gem_frame's centred
// placement EXACTLY, so a gem still mid-fall (or one that never moved) draws // placement EXACTLY, so a gem still mid-fall (or one that never moved) draws
// byte-identically to the plain fall; only a landed gem flattens. // byte-identically to the plain fall; only a landed gem flattens.
gem_squash_frame :: (self: *BoardView, col: s64, frow: f32, dim: f32, sq: f32) -> Frame { gem_squash_frame :: (self: *BoardView, col: i64, frow: f32, dim: f32, sq: f32) -> Frame {
cs := self.layout.cell_size; cs := self.layout.cell_size;
cx := self.layout.origin.x + (cast(f32) col + 0.5) * cs; cx := self.layout.origin.x + (cast(f32) col + 0.5) * cs;
cy := self.layout.origin.y + (frow + 0.5) * cs; cy := self.layout.origin.y + (frow + 0.5) * cs;
@@ -323,7 +323,7 @@ BoardView :: struct {
// The per-gem animation pose for a settled cell: the always-on idle breath, // The per-gem animation pose for a settled cell: the always-on idle breath,
// plus a squash-bounce if the cell landed recently, plus a pop if it is the // plus a squash-bounce if the cell landed recently, plus a pop if it is the
// selected cell. Purely visual — composed from gem_anim's pure functions. // selected cell. Purely visual — composed from gem_anim's pure functions.
gem_pose_at :: (self: *BoardView, col: s64, row: s64) -> GemPose { gem_pose_at :: (self: *BoardView, col: i64, row: i64) -> GemPose {
pose := idle_pose(self.motion.clock, col, row); pose := idle_pose(self.motion.clock, col, row);
sq := land_squash(self.motion.land_local(Board.idx(col, row))); sq := land_squash(self.motion.land_local(Board.idx(col, row)));
@@ -347,7 +347,7 @@ BoardView :: struct {
// (land_squash → 0, so it draws unsquashed) and one that never moved reads 0. // (land_squash → 0, so it draws unsquashed) and one that never moved reads 0.
// render_fall passes the current round; render_clear the previous (its board is // render_fall passes the current round; render_clear the previous (its board is
// that round's `after`), so the one bounce plays on across the fall→clear seam. // that round's `after`), so the one bounce plays on across the fall→clear seam.
rest_squash :: (self: *BoardView, i: s64, kmax: s64, elapsed: f32) -> f32 { rest_squash :: (self: *BoardView, i: i64, kmax: i64, elapsed: f32) -> f32 {
m := delivering_round(@self.anim.move, i, kmax); m := delivering_round(@self.anim.move, i, kmax);
if m < 0 { return 0.0; } if m < 0 { return 0.0; }
col := i % BOARD_COLS; col := i % BOARD_COLS;
@@ -363,7 +363,7 @@ BoardView :: struct {
if g != .empty { if g != .empty {
pose := self.gem_pose_at(col, row); pose := self.gem_pose_at(col, row);
gf := self.gem_pose_frame(col, row, dim, pose); gf := self.gem_pose_frame(col, row, dim, pose);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} }
} }
} }
@@ -436,7 +436,7 @@ BoardView :: struct {
if g != .empty { if g != .empty {
sq := self.rest_squash(i, last, e); sq := self.rest_squash(i, last, e);
gf := self.gem_squash_frame(i % BOARD_COLS, cast(f32) (i / BOARD_COLS), dim, sq); gf := self.gem_squash_frame(i % BOARD_COLS, cast(f32) (i / BOARD_COLS), dim, sq);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} }
} }
} }
@@ -457,7 +457,7 @@ BoardView :: struct {
g := mv.pre[i]; g := mv.pre[i];
if g != .empty { if g != .empty {
gf := self.gem_frame(cast(f32) (i % BOARD_COLS), cast(f32) (i / BOARD_COLS), inset, dim); gf := self.gem_frame(cast(f32) (i % BOARD_COLS), cast(f32) (i / BOARD_COLS), inset, dim);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} }
} }
@@ -482,18 +482,18 @@ BoardView :: struct {
ga := mv.pre[ai]; ga := mv.pre[ai];
if ga != .empty { if ga != .empty {
gf := self.gem_frame(afc + (bfc - afc) * p, afr + (bfr - afr) * p, inset, dim); gf := self.gem_frame(afc + (bfc - afc) * p, afr + (bfr - afr) * p, inset, dim);
self.draw_gem(ctx, gf, cast(s64) ga); self.draw_gem(ctx, gf, cast(i64) ga);
} }
gb := mv.pre[bi]; gb := mv.pre[bi];
if gb != .empty { if gb != .empty {
gf := self.gem_frame(bfc + (afc - bfc) * p, bfr + (afr - bfr) * p, inset, dim); gf := self.gem_frame(bfc + (afc - bfc) * p, bfr + (afr - bfr) * p, inset, dim);
self.draw_gem(ctx, gf, cast(s64) gb); self.draw_gem(ctx, gf, cast(i64) gb);
} }
} }
// Clear segment: matched gems pop outward then collapse to nothing (a // Clear segment: matched gems pop outward then collapse to nothing (a
// satisfying pop, composing with the particle burst); the rest hold position. // satisfying pop, composing with the particle burst); the rest hold position.
render_clear :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, k: s64, e: f32, dim: f32, t: f32) { render_clear :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, k: i64, e: f32, dim: f32, t: f32) {
span := clear_diag_span(@rd.matched); span := clear_diag_span(@rd.matched);
for 0..BOARD_CELLS (i) { for 0..BOARD_CELLS (i) {
g := rd.before[i]; g := rd.before[i];
@@ -507,7 +507,7 @@ BoardView :: struct {
// reaches scale 0 by t==1, keeping the seam to the fall clean. // reaches scale 0 by t==1, keeping the seam to the fall clean.
pop := clear_pop_scale(clear_ripple_t(t, clear_rank(span, col, row))); pop := clear_pop_scale(clear_ripple_t(t, clear_rank(span, col, row)));
gf := self.gem_frame_scaled(col, row, dim, pop); gf := self.gem_frame_scaled(col, row, dim, pop);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} else { } else {
// before[k] is round k-1's settled board, so a survivor here still // before[k] is round k-1's settled board, so a survivor here still
// carries the bounce from the round that dropped it in — continue it // carries the bounce from the round that dropped it in — continue it
@@ -515,7 +515,7 @@ BoardView :: struct {
// (nothing has fallen yet), keeping that frame byte-identical. // (nothing has fallen yet), keeping that frame byte-identical.
sq := self.rest_squash(i, k - 1, e); sq := self.rest_squash(i, k - 1, e);
gf := self.gem_squash_frame(col, cast(f32) row, dim, sq); gf := self.gem_squash_frame(col, cast(f32) row, dim, sq);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} }
} }
} }
@@ -593,7 +593,7 @@ BoardView :: struct {
// lockstep row; ease_in_cubic pins each column's f(1)=1, and fall_stagger_t // lockstep row; ease_in_cubic pins each column's f(1)=1, and fall_stagger_t
// guarantees every column reaches 1 by t==1, so each gem lands exactly on its // guarantees every column reaches 1 by t==1, so each gem lands exactly on its
// cell and the seam to the next round / settled board stays invisible. // cell and the seam to the next round / settled board stays invisible.
render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, k: s64, e: f32, dim: f32, t: f32) { render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, k: i64, e: f32, dim: f32, t: f32) {
for 0..BOARD_CELLS (i) { for 0..BOARD_CELLS (i) {
g := rd.after[i]; g := rd.after[i];
if g == .empty { continue; } if g == .empty { continue; }
@@ -608,7 +608,7 @@ BoardView :: struct {
// has reached its cell flattens wide-and-short, then wobbles out. // has reached its cell flattens wide-and-short, then wobbles out.
sq := self.rest_squash(i, k, e); sq := self.rest_squash(i, k, e);
gf := self.gem_squash_frame(col, cur_row, dim, sq); gf := self.gem_squash_frame(col, cur_row, dim, sq);
self.draw_gem(ctx, gf, cast(s64) g); self.draw_gem(ctx, gf, cast(i64) g);
} }
} }
@@ -668,7 +668,7 @@ BoardView :: struct {
// pin) is set, so the unset render path is byte-identical. A bright halo under // pin) is set, so the unset render path is byte-identical. A bright halo under
// the dark text keeps the digits legible over the light background art. // the dark text keeps the digits legible over the light background art.
render_fps_overlay :: (self: *BoardView, ctx: *RenderContext, frame: Frame) { render_fps_overlay :: (self: *BoardView, ctx: *RenderContext, frame: Frame) {
n := cast(s64) (self.fps + 0.5); n := cast(i64) (self.fps + 0.5);
txt := format("FPS {}", n); txt := format("FPS {}", n);
sz := measure_text(txt, FPS_FONT); sz := measure_text(txt, FPS_FONT);
x := frame.origin.x + self.safe.left + FPS_PAD; x := frame.origin.x + self.safe.left + FPS_PAD;

View File

@@ -25,9 +25,9 @@ the step asks for an honest accounting.
## Category: Numerics / parsing ## Category: Numerics / parsing
### 1. String → number parsing (`parse_s64`, `parse_f32`) ### 1. String → number parsing (`parse_i64`, `parse_f32`)
- **m3te location:** `main.sx:156` (`parse_s64`), `main.sx:122` (`parse_f32`). - **m3te location:** `main.sx:156` (`parse_i64`), `main.sx:122` (`parse_f32`).
- **What it does:** decimal ASCII `string``s64` / `f32` (sign + integer + - **What it does:** decimal ASCII `string``i64` / `f32` (sign + integer +
optional fractional part). optional fractional part).
- **Why general-purpose:** the exact inverse of the formatters everyone already - **Why general-purpose:** the exact inverse of the formatters everyone already
uses; needed by any program that reads numbers from argv, env, config, or a uses; needed by any program that reads numbers from argv, env, config, or a
@@ -46,7 +46,7 @@ the step asks for an honest accounting.
- **m3te location:** `board.sx:55` (`Rng` struct: `next_u32` `board.sx:59`, - **m3te location:** `board.sx:55` (`Rng` struct: `next_u32` `board.sx:59`,
`next_range` `board.sx:66`), `board.sx:71` (`rng_seeded`), constants `next_range` `board.sx:66`), `board.sx:71` (`rng_seeded`), constants
`board.sx:51-53`. `board.sx:51-53`.
- **What it does:** a 32-bit linear-congruential generator carried in `s64` - **What it does:** a 32-bit linear-congruential generator carried in `i64`
and masked to 32 bits — `next_u32()`, `next_range(n)` (uniform-ish `[0,n)`), and masked to 32 bits — `next_u32()`, `next_range(n)` (uniform-ish `[0,n)`),
`rng_seeded(seed)`. `rng_seeded(seed)`.
- **Why general-purpose:** a seedable, reproducible RNG is a textbook stdlib - **Why general-purpose:** a seedable, reproducible RNG is a textbook stdlib
@@ -172,7 +172,7 @@ them, so they are explicitly **not** counted above:
## Summary (top gaps, ranked) ## Summary (top gaps, ranked)
1. **String → number parsing** (`parse_s64` / `parse_f32`) — clean, universally 1. **String → number parsing** (`parse_i64` / `parse_f32`) — clean, universally
needed, the missing inverse of the existing formatters. needed, the missing inverse of the existing formatters.
2. **Seedable PRNG** (`Rng` LCG) — textbook stdlib primitive, entirely absent. 2. **Seedable PRNG** (`Rng` LCG) — textbook stdlib primitive, entirely absent.
3. **`from_cstr` C-string bridge** (+ public `strlen`) — the most-repeated FFI 3. **`from_cstr` C-string bridge** (+ public `strlen`) — the most-repeated FFI

View File

@@ -38,11 +38,11 @@ IDLE_BOB_A :f32: 0.024; // vertical bob amplitude (cell units)
IDLE_RAMP :f32: 0.45; // seconds to ease the idle up from full rest IDLE_RAMP :f32: 0.45; // seconds to ease the idle up from full rest
// Smooth per-cell phase: a diagonal gradient wrapped into one breath period. // Smooth per-cell phase: a diagonal gradient wrapped into one breath period.
gem_idle_phase :: (col: s64, row: s64) -> f32 { gem_idle_phase :: (col: i64, row: i64) -> f32 {
cast(f32) ((col * 2 + row * 3) % 8) / 8.0 * TAU cast(f32) ((col * 2 + row * 3) % 8) / 8.0 * TAU
} }
idle_pose :: (t: f32, col: s64, row: s64) -> GemPose { idle_pose :: (t: f32, col: i64, row: i64) -> GemPose {
ramp := clamp(t / IDLE_RAMP, 0.0, 1.0); ramp := clamp(t / IDLE_RAMP, 0.0, 1.0);
w := t / IDLE_PERIOD * TAU + gem_idle_phase(col, row); w := t / IDLE_PERIOD * TAU + gem_idle_phase(col, row);
s := IDLE_SCALE_A * sin(w) * ramp; s := IDLE_SCALE_A * sin(w) * ramp;
@@ -135,7 +135,7 @@ GemMotion :: struct {
for 0..BOARD_CELLS (i) { self.land_at[i] = -1000.0; } for 0..BOARD_CELLS (i) { self.land_at[i] = -1000.0; }
} }
stamp_land :: (self: *GemMotion, i: s64) { stamp_land :: (self: *GemMotion, i: i64) {
self.stamp_land_at(i, self.clock); self.stamp_land_at(i, self.clock);
} }
@@ -143,11 +143,11 @@ GemMotion :: struct {
// can BACK-DATE the stamp to when the gem actually touched down mid-fall (each // can BACK-DATE the stamp to when the gem actually touched down mid-fall (each
// column lands at a staggered instant): land_squash then resumes the per-round // column lands at a staggered instant): land_squash then resumes the per-round
// bounce exactly where render_fall left it, with no double-pop at the seam. // bounce exactly where render_fall left it, with no double-pop at the seam.
stamp_land_at :: (self: *GemMotion, i: s64, at: f32) { stamp_land_at :: (self: *GemMotion, i: i64, at: f32) {
self.land_at[i] = at; self.land_at[i] = at;
} }
land_local :: (self: *GemMotion, i: s64) -> f32 { land_local :: (self: *GemMotion, i: i64) -> f32 {
self.clock - self.land_at[i] self.clock - self.land_at[i]
} }
} }

44
main.sx
View File

@@ -117,40 +117,40 @@ build_ui :: () -> View {
// be captured without injecting a tap. Absent → normal live behaviour. // be captured without injecting a tap. Absent → normal live behaviour.
read_env :: (name: [:0]u8) -> ?string { read_env :: (name: [:0]u8) -> ?string {
p := getenv(name); p := getenv(name);
addr : s64 = xx p; addr : i64 = xx p;
if addr == 0 { return null; } if addr == 0 { return null; }
n := cast(s64) strlen(p); n := cast(i64) strlen(p);
if n == 0 { return ""; } if n == 0 { return ""; }
buf := cstring(n); buf := cstring(n);
memcpy(buf.ptr, xx p, n); memcpy(buf.ptr, xx p, n);
buf buf
} }
// Digit arithmetic runs entirely in s64; the result converts to f32 only once at // Digit arithmetic runs entirely in i64; the result converts to f32 only once at
// the end. Doing the digit math in f32 would unify the ASCII literals (45/46/48/ // the end. Doing the digit math in f32 would unify the ASCII literals (45/46/48/
// 57) to f32 across the comparisons, which mis-types the byte compares. // 57) to f32 across the comparisons, which mis-types the byte compares.
parse_f32 :: (s: string) -> f32 { parse_f32 :: (s: string) -> f32 {
i : s64 = 0; i : i64 = 0;
neg : bool = false; neg : bool = false;
if s.len > 0 { if s.len > 0 {
c0 : s64 = xx s[0]; c0 : i64 = xx s[0];
if c0 == 45 { neg = true; i = 1; } // '-' if c0 == 45 { neg = true; i = 1; } // '-'
} }
intval : s64 = 0; intval : i64 = 0;
while i < s.len { while i < s.len {
c : s64 = xx s[i]; c : i64 = xx s[i];
if c < 48 or c > 57 { break; } if c < 48 or c > 57 { break; }
intval = intval * 10 + (c - 48); intval = intval * 10 + (c - 48);
i += 1; i += 1;
} }
fracval : s64 = 0; fracval : i64 = 0;
fracdiv : s64 = 1; fracdiv : i64 = 1;
if i < s.len { if i < s.len {
d : s64 = xx s[i]; d : i64 = xx s[i];
if d == 46 { // '.' if d == 46 { // '.'
i += 1; i += 1;
while i < s.len { while i < s.len {
c : s64 = xx s[i]; c : i64 = xx s[i];
if c < 48 or c > 57 { break; } if c < 48 or c > 57 { break; }
fracval = fracval * 10 + (c - 48); fracval = fracval * 10 + (c - 48);
fracdiv = fracdiv * 10; fracdiv = fracdiv * 10;
@@ -163,11 +163,11 @@ parse_f32 :: (s: string) -> f32 {
v v
} }
parse_s64 :: (s: string) -> s64 { parse_i64 :: (s: string) -> i64 {
i : s64 = 0; i : i64 = 0;
v : s64 = 0; v : i64 = 0;
while i < s.len { while i < s.len {
c : s64 = xx s[i]; c : i64 = xx s[i];
if c < 48 or c > 57 { break; } if c < 48 or c > 57 { break; }
v = v * 10 + (c - 48); v = v * 10 + (c - 48);
i += 1; i += 1;
@@ -405,7 +405,7 @@ main :: () -> void {
g_motion.clock = parse_f32(t); g_motion.clock = parse_f32(t);
} }
if sc := read_env("M3TE_SELECT") { if sc := read_env("M3TE_SELECT") {
idx := parse_s64(sc); idx := parse_i64(sc);
if idx >= 0 and idx < BOARD_CELLS { if idx >= 0 and idx < BOARD_CELLS {
g_sel.active = true; g_sel.active = true;
g_sel.cell = Cell.{ col = idx % BOARD_COLS, row = idx / BOARD_COLS }; g_sel.cell = Cell.{ col = idx % BOARD_COLS, row = idx / BOARD_COLS };
@@ -418,7 +418,7 @@ main :: () -> void {
// committed golden stay byte-identical. Purely a render overlay — no board / // committed golden stay byte-identical. Purely a render overlay — no board /
// score / move / animation state changes and it never gates input. // score / move / animation state changes and it never gates input.
if fp := read_env("M3TE_FPS") { if fp := read_env("M3TE_FPS") {
if parse_s64(fp) != 0 { g_fps_on = true; } if parse_i64(fp) != 0 { g_fps_on = true; }
} }
// Match-FX capture hook (P11.1). The bursts/popups spawn off a committed move, // Match-FX capture hook (P11.1). The bursts/popups spawn off a committed move,
@@ -433,7 +433,7 @@ main :: () -> void {
if fx := read_env("M3TE_FX") { if fx := read_env("M3TE_FX") {
swaps := legal_swaps(g_board); swaps := legal_swaps(g_board);
if swaps.len > 0 { if swaps.len > 0 {
n := parse_s64(fx); n := parse_i64(fx);
if n < 1 { n = 1; } if n < 1 { n = 1; }
if n > swaps.len { n = swaps.len; } if n > swaps.len { n = swaps.len; }
sw := swaps.items[n - 1]; sw := swaps.items[n - 1];
@@ -457,7 +457,7 @@ main :: () -> void {
if bs := read_env("M3TE_BADSWAP") { if bs := read_env("M3TE_BADSWAP") {
bad := illegal_swaps(g_board); bad := illegal_swaps(g_board);
if bad.len > 0 { if bad.len > 0 {
n := parse_s64(bs); n := parse_i64(bs);
if n < 1 { n = 1; } if n < 1 { n = 1; }
if n > bad.len { n = bad.len; } if n > bad.len { n = bad.len; }
sw := bad.items[n - 1]; sw := bad.items[n - 1];
@@ -473,10 +473,10 @@ main :: () -> void {
// M3TE_MOVE_LIMIT=0 makes it read LOST (budget spent below the goal). With // M3TE_MOVE_LIMIT=0 makes it read LOST (budget spent below the goal). With
// M3TE_RESTART set non-zero the board is then restart()-ed, capturing the // M3TE_RESTART set non-zero the board is then restart()-ed, capturing the
// fresh in_progress board the restart button produces. // fresh in_progress board the restart button produces.
if tg := read_env("M3TE_TARGET") { g_board.target_score = parse_s64(tg); } if tg := read_env("M3TE_TARGET") { g_board.target_score = parse_i64(tg); }
if ml := read_env("M3TE_MOVE_LIMIT") { g_board.move_limit = parse_s64(ml); } if ml := read_env("M3TE_MOVE_LIMIT") { g_board.move_limit = parse_i64(ml); }
if rs := read_env("M3TE_RESTART") { if rs := read_env("M3TE_RESTART") {
if parse_s64(rs) != 0 { g_board.restart(BOARD_SEED); } if parse_i64(rs) != 0 { g_board.restart(BOARD_SEED); }
} }
g_pipeline.set_body(closure(build_ui)); g_pipeline.set_body(closure(build_ui));

View File

@@ -30,8 +30,8 @@ boards_equal :: (x: *Board, y: *Board) -> bool {
true true
} }
main :: () -> s32 { main :: () -> i32 {
fails : s64 = 0; fails : i64 = 0;
// ── Legal swap: plan == model, timeline ends on the model ─────────────── // ── Legal swap: plan == model, timeline ends on the model ───────────────
// (5,4)->(6,4): brings R into (5,4), completing R,R,R across cols 3-5 of row // (5,4)->(6,4): brings R into (5,4), completing R,R,R across cols 3-5 of row

View File

@@ -4,7 +4,7 @@
#import "modules/std.sx"; #import "modules/std.sx";
t :: #import "test.sx"; t :: #import "test.sx";
main :: () -> s32 { main :: () -> i32 {
t.expect(2 + 2 == 4, "two plus two is four"); t.expect(2 + 2 == 4, "two plus two is four");
t.expect(7 % 3 == 1, "seven mod three is one"); t.expect(7 % 3 == 1, "seven mod three is one");
t.expect(10 - 4 == 6, "ten minus four is six"); t.expect(10 - 4 == 6, "ten minus four is six");

View File

@@ -19,11 +19,11 @@
irect :: (f: Frame) -> string { irect :: (f: Frame) -> string {
format("({},{},{},{})", format("({},{},{},{})",
cast(s64) f.origin.x, cast(s64) f.origin.y, cast(i64) f.origin.x, cast(i64) f.origin.y,
cast(s64) f.size.width, cast(s64) f.size.height) cast(i64) f.size.width, cast(i64) f.size.height)
} }
main :: () -> s32 { main :: () -> i32 {
// 800×600, no safe inset → 600px square grid, cell 75, origin (100,0): the // 800×600, no safe inset → 600px square grid, cell 75, origin (100,0): the
// same layout tests/hit_test.sx pins, so the numbers are checkable by hand. // same layout tests/hit_test.sx pins, so the numbers are checkable by hand.
lay : BoardLayout = ---; lay : BoardLayout = ---;
@@ -36,18 +36,18 @@ main :: () -> s32 {
print("title {}\n", irect(bl.title)); print("title {}\n", irect(bl.title));
print("button {}\n", irect(bl.button)); print("button {}\n", irect(bl.button));
fails : s64 = 0; fails : i64 = 0;
// The button is horizontally centered on the grid (centred banner). // The button is horizontally centered on the grid (centred banner).
bcx := bl.button.mid_x(); bcx := bl.button.mid_x();
if cast(s64) bcx != cast(s64) grid.mid_x() { fails += 1; } if cast(i64) bcx != cast(i64) grid.mid_x() { fails += 1; }
print("button mid_x {} grid mid_x {}\n", cast(s64) bcx, cast(s64) grid.mid_x()); print("button mid_x {} grid mid_x {}\n", cast(i64) bcx, cast(i64) grid.mid_x());
// The whole button sits inside the panel — its four corners are contained, // The whole button sits inside the panel — its four corners are contained,
// so it can never spill outside the drawn card. // so it can never spill outside the drawn card.
bx0 := bl.button.origin.x; by0 := bl.button.origin.y; bx0 := bl.button.origin.x; by0 := bl.button.origin.y;
bx1 := bl.button.max_x(); by1 := bl.button.max_y(); bx1 := bl.button.max_x(); by1 := bl.button.max_y();
corners_in : s64 = 0; corners_in : i64 = 0;
if bl.panel.contains(Point.{ x = bx0, y = by0 }) { corners_in += 1; } if bl.panel.contains(Point.{ x = bx0, y = by0 }) { corners_in += 1; }
if bl.panel.contains(Point.{ x = bx1, y = by0 }) { corners_in += 1; } if bl.panel.contains(Point.{ x = bx1, y = by0 }) { corners_in += 1; }
if bl.panel.contains(Point.{ x = bx0, y = by1 }) { corners_in += 1; } if bl.panel.contains(Point.{ x = bx0, y = by1 }) { corners_in += 1; }
@@ -67,7 +67,7 @@ main :: () -> s32 {
// in the button, so each leaves the level frozen. // in the button, so each leaves the level frozen.
corner_cell := Point.{ x = grid.origin.x + lay.cell_size * 0.5, y = grid.origin.y + lay.cell_size * 0.5 }; corner_cell := Point.{ x = grid.origin.x + lay.cell_size * 0.5, y = grid.origin.y + lay.cell_size * 0.5 };
outside := Point.{ x = bl.panel.origin.x - 5.0, y = bl.panel.mid_y() }; outside := Point.{ x = bl.panel.origin.x - 5.0, y = bl.panel.mid_y() };
off_hits : s64 = 0; off_hits : i64 = 0;
if bl.button.contains(corner_cell) { off_hits += 1; } if bl.button.contains(corner_cell) { off_hits += 1; }
if bl.button.contains(outside) { off_hits += 1; } if bl.button.contains(outside) { off_hits += 1; }
if off_hits != 0 { fails += 1; } if off_hits != 0 { fails += 1; }

View File

@@ -10,8 +10,8 @@ SEED :: 1337;
// Count every horizontal or vertical window of three consecutive same-type // Count every horizontal or vertical window of three consecutive same-type
// gems. A correctly initialized board has zero. This walks the finished board // gems. A correctly initialized board has zero. This walks the finished board
// independently of the placement logic, so it's a real check, not a tautology. // independently of the placement logic, so it's a real check, not a tautology.
count_three_runs :: (b: *Board) -> s32 { count_three_runs :: (b: *Board) -> i32 {
runs : s32 = 0; runs : i32 = 0;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
for 0..(BOARD_COLS - 2) (col) { for 0..(BOARD_COLS - 2) (col) {
g := b.at(col, row); g := b.at(col, row);
@@ -27,7 +27,7 @@ count_three_runs :: (b: *Board) -> s32 {
runs runs
} }
main :: () -> s32 { main :: () -> i32 {
board : Board = ---; board : Board = ---;
board.init(SEED); board.init(SEED);

View File

@@ -84,7 +84,7 @@ checker_board :: () -> Board {
b b
} }
main :: () -> s32 { main :: () -> i32 {
print("== cascade (resolution loop) ==\n"); print("== cascade (resolution loop) ==\n");
// Drive the loop one round at a time so each post-round board is visible in // Drive the loop one round at a time so each post-round board is visible in
@@ -94,7 +94,7 @@ main :: () -> s32 {
out(board_dump(@b)); out(board_dump(@b));
depth := 0; depth := 0;
counts := List(s64).{}; counts := List(i64).{};
while true { while true {
n := resolve_step(@b); n := resolve_step(@b);
if n == 0 { break; } if n == 0 { break; }

View File

@@ -8,13 +8,13 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "audio.sx"; #import "audio.sx";
main :: () -> s32 { main :: () -> i32 {
print("== cascade cue selection (depth -> combo cue) ==\n"); print("== cascade cue selection (depth -> combo cue) ==\n");
// Walk a representative depth range (0..9) so both clamps and the monotonic // Walk a representative depth range (0..9) so both clamps and the monotonic
// middle are visible: depths 0,1 pin to the first cue; depths >= 5 pin to // middle are visible: depths 0,1 pin to the first cue; depths >= 5 pin to
// the last; 2,3,4 step up one cue at a time. // the last; 2,3,4 step up one cue at a time.
prev : s64 = -1; prev : i64 = -1;
for 0..10 (depth) { for 0..10 (depth) {
idx := cascade_cue_index(depth); idx := cascade_cue_index(depth);
print("depth {} -> idx {} ({})\n", depth, idx, cascade_cue_name(idx)); print("depth {} -> idx {} ({})\n", depth, idx, cascade_cue_name(idx));

View File

@@ -13,7 +13,7 @@
#import "board_anim.sx"; #import "board_anim.sx";
#import "audio.sx"; #import "audio.sx";
main :: () -> s32 { main :: () -> i32 {
print("== per-round cascade cue timing ==\n"); print("== per-round cascade cue timing ==\n");
// `cascade_rounds_started` = how many cascade rounds have BEGUN clearing by // `cascade_rounds_started` = how many cascade rounds have BEGUN clearing by
@@ -21,7 +21,7 @@ main :: () -> s32 {
// Round k (0-based) starts clearing at 0.16 + k*0.36; sampled safely INSIDE // Round k (0-based) starts clearing at 0.16 + k*0.36; sampled safely INSIDE
// each round window so the integer step is unambiguous. Locked for 5 rounds. // each round window so the integer step is unambiguous. Locked for 5 rounds.
print("-- started-count across a 5-round chain --\n"); print("-- started-count across a 5-round chain --\n");
rounds : s64 = 5; rounds : i64 = 5;
print("e=0.00 -> {}\n", cascade_rounds_started(0.00, rounds)); print("e=0.00 -> {}\n", cascade_rounds_started(0.00, rounds));
print("e=0.10 -> {}\n", cascade_rounds_started(0.10, rounds)); print("e=0.10 -> {}\n", cascade_rounds_started(0.10, rounds));
print("e=0.20 -> {}\n", cascade_rounds_started(0.20, rounds)); print("e=0.20 -> {}\n", cascade_rounds_started(0.20, rounds));
@@ -37,7 +37,7 @@ main :: () -> s32 {
// ascending. This IS the loop main's frame loop runs; the emitted run is the // ascending. This IS the loop main's frame loop runs; the emitted run is the
// locked acceptance ordering. // locked acceptance ordering.
print("-- ascending per-round run --\n"); print("-- ascending per-round run --\n");
fired : s64 = 0; fired : i64 = 0;
elapsed : f32 = 0.0; elapsed : f32 = 0.0;
while fired < rounds { while fired < rounds {
started := cascade_rounds_started(elapsed, rounds); started := cascade_rounds_started(elapsed, rounds);

View File

@@ -38,7 +38,7 @@ load_board :: (rows: []string) -> Board {
// Detect→clear one scene, snapshot before/after, and assert the three clear // Detect→clear one scene, snapshot before/after, and assert the three clear
// invariants against the matched-cell set: every flagged cell is now a hole, // invariants against the matched-cell set: every flagged cell is now a hole,
// every unflagged cell is unchanged, and the returned count is exact. // every unflagged cell is unchanged, and the returned count is exact.
scene :: (name: string, rows: []string, want_cleared: s64) { scene :: (name: string, rows: []string, want_cleared: i64) {
b := load_board(rows); b := load_board(rows);
orig := load_board(rows); // pristine copy for the unchanged check orig := load_board(rows); // pristine copy for the unchanged check
@@ -65,7 +65,7 @@ scene :: (name: string, rows: []string, want_cleared: s64) {
t.expect(cleared == want_cleared, concat(name, ": cleared count exact")); t.expect(cleared == want_cleared, concat(name, ": cleared count exact"));
} }
main :: () -> s32 { main :: () -> i32 {
print("== clear (detect -> clear) ==\n"); print("== clear (detect -> clear) ==\n");
// Single horizontal 3-run (row 3, cols 2-4) → three holes there only. // Single horizontal 3-run (row 3, cols 2-4) → three holes there only.

View File

@@ -79,7 +79,7 @@ scene :: (name: string, rows: []string, want_moved: bool) {
t.expect(moved == want_moved, concat(name, ": moved flag exact")); t.expect(moved == want_moved, concat(name, ": moved flag exact"));
} }
main :: () -> s32 { main :: () -> i32 {
print("== collapse (gravity) ==\n"); print("== collapse (gravity) ==\n");
// Eight independent columns, one case each (top-to-bottom): // Eight independent columns, one case each (top-to-bottom):

View File

@@ -31,7 +31,7 @@ char_to_gem :: (c: u8) -> Gem {
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars), // Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars),
// seeded RNG, running score zeroed so `board.score` ends equal to the payout. // seeded RNG, running score zeroed so `board.score` ends equal to the payout.
load_board :: (rows: []string, seed: s64) -> Board { load_board :: (rows: []string, seed: i64) -> Board {
b : Board = ---; b : Board = ---;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
line := rows[row]; line := rows[row];
@@ -51,14 +51,14 @@ load_board :: (rows: []string, seed: s64) -> Board {
// identical board awards `want_mult` into `Board.score` and reports it as // identical board awards `want_mult` into `Board.score` and reports it as
// `Cascade.awarded` at the same depth. A depth-1 settle must equal the flat sum // `Cascade.awarded` at the same depth. A depth-1 settle must equal the flat sum
// (no bonus); a deeper chain must strictly exceed it. // (no bonus); a deeper chain must strictly exceed it.
scene :: (name: string, rows: []string, seed: s64, want_flat: s64, want_mult: s64) { scene :: (name: string, rows: []string, seed: i64, want_flat: i64, want_mult: i64) {
print("== {} ==\n", name); print("== {} ==\n", name);
b := load_board(rows, seed); b := load_board(rows, seed);
out(board_dump(@b)); out(board_dump(@b));
flat : s64 = 0; flat : i64 = 0;
mult : s64 = 0; mult : i64 = 0;
depth : s64 = 0; depth : i64 = 0;
while true { while true {
base := score_round(@b); base := score_round(@b);
n := resolve_step(@b); n := resolve_step(@b);
@@ -90,7 +90,7 @@ scene :: (name: string, rows: []string, seed: s64, want_flat: s64, want_mult: s6
t.expect(b2.score == want_mult, concat(name, ": resolve accumulates into board.score")); t.expect(b2.score == want_mult, concat(name, ": resolve accumulates into board.score"));
} }
main :: () -> s32 { main :: () -> i32 {
print("== combo (cascade multiplier) ==\n"); print("== combo (cascade multiplier) ==\n");
// Single-round clear (seed 0): one RRR clears and the refill makes no new // Single-round clear (seed 0): one RRR clears and the refill makes no new

View File

@@ -19,8 +19,8 @@
fabs :: (x: f32) -> f32 { if x < 0.0 then 0.0 - x else x } fabs :: (x: f32) -> f32 { if x < 0.0 then 0.0 - x else x }
approx :: (a: f32, b: f32) -> bool { fabs(a - b) < 0.0001 } approx :: (a: f32, b: f32) -> bool { fabs(a - b) < 0.0001 }
main :: () -> s32 { main :: () -> i32 {
fails : s64 = 0; fails : i64 = 0;
// 1. Endpoints are locked: every curve starts/ends exactly on its rest value // 1. Endpoints are locked: every curve starts/ends exactly on its rest value
// (the in/out curves at 1, the spring at 1, the squash envelope at 0). // (the in/out curves at 1, the spring at 1, the squash envelope at 0).

View File

@@ -13,14 +13,14 @@
#import "modules/std.sx"; #import "modules/std.sx";
#import "board_fx.sx"; #import "board_fx.sx";
main :: () -> s32 { main :: () -> i32 {
print("== combo emphasis selection (depth -> fx level / popup font) ==\n"); print("== combo emphasis selection (depth -> fx level / popup font) ==\n");
// The cascade-cue index per depth 0..9, copied from cascade_cue.stdout. The // The cascade-cue index per depth 0..9, copied from cascade_cue.stdout. The
// FX level must equal this entry for entry — the audio/visual lockstep. // FX level must equal this entry for entry — the audio/visual lockstep.
expect_level : [10]s64 = .{ 0, 0, 1, 2, 3, 4, 4, 4, 4, 4 }; expect_level : [10]i64 = .{ 0, 0, 1, 2, 3, 4, 4, 4, 4, 4 };
prev : s64 = -1; prev : i64 = -1;
for 0..10 (depth) { for 0..10 (depth) {
lvl := fx_combo_level(depth); lvl := fx_combo_level(depth);
font := fx_popup_font(depth); font := fx_popup_font(depth);

View File

@@ -18,8 +18,8 @@
fabs :: (x: f32) -> f32 { if x < 0.0 then 0.0 - x else x } fabs :: (x: f32) -> f32 { if x < 0.0 then 0.0 - x else x }
approx :: (a: f32, b: f32) -> bool { fabs(a - b) < 0.0001 } approx :: (a: f32, b: f32) -> bool { fabs(a - b) < 0.0001 }
main :: () -> s32 { main :: () -> i32 {
fails : s64 = 0; fails : i64 = 0;
// 1. t==0 idle pose is EXACTLY rest for every cell (the determinism invariant). // 1. t==0 idle pose is EXACTLY rest for every cell (the determinism invariant).
print("== idle t=0 is rest for all cells ==\n"); print("== idle t=0 is rest for all cells ==\n");

View File

@@ -14,19 +14,19 @@
#import "board.sx"; #import "board.sx";
#import "board_layout.sx"; #import "board_layout.sx";
main :: () -> s32 { main :: () -> i32 {
// 800×600 with no safe inset → a 600px square grid, cell 75, centered: the // 800×600 with no safe inset → a 600px square grid, cell 75, centered: the
// grid origin lands at (100, 0). Integer math keeps the dump deterministic. // grid origin lands at (100, 0). Integer math keeps the dump deterministic.
lay : BoardLayout = ---; lay : BoardLayout = ---;
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero()); lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
print("grid origin ({},{}) cell {}\n", print("grid origin ({},{}) cell {}\n",
cast(s64) lay.origin.x, cast(s64) lay.origin.y, cast(s64) lay.cell_size); cast(i64) lay.origin.x, cast(i64) lay.origin.y, cast(i64) lay.cell_size);
fails : s64 = 0; fails : i64 = 0;
// Every cell center must map back to its own cell. // Every cell center must map back to its own cell.
hits : s64 = 0; hits : i64 = 0;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) { for 0..BOARD_COLS (col) {
cf := lay.cell_frame(col, row); cf := lay.cell_frame(col, row);
@@ -42,8 +42,8 @@ main :: () -> s32 {
// A cell's top-left corner belongs to that cell (the leading edge is // A cell's top-left corner belongs to that cell (the leading edge is
// inclusive), so corner-of-(3,5) resolves to (3,5). // inclusive), so corner-of-(3,5) resolves to (3,5).
corner := Point.{ x = lay.origin.x + 3.0 * lay.cell_size, y = lay.origin.y + 5.0 * lay.cell_size }; corner := Point.{ x = lay.origin.x + 3.0 * lay.cell_size, y = lay.origin.y + 5.0 * lay.cell_size };
corner_col : s64 = -1; corner_col : i64 = -1;
corner_row : s64 = -1; corner_row : i64 = -1;
if h := lay.point_to_cell(corner) { corner_col = h.col; corner_row = h.row; } if h := lay.point_to_cell(corner) { corner_col = h.col; corner_row = h.row; }
if corner_col != 3 or corner_row != 5 { fails += 1; } if corner_col != 3 or corner_row != 5 { fails += 1; }
print("corner maps to ({},{})\n", corner_col, corner_row); print("corner maps to ({},{})\n", corner_col, corner_row);
@@ -53,7 +53,7 @@ main :: () -> s32 {
off_left := Point.{ x = lay.origin.x - 5.0, y = lay.origin.y + 10.0 }; off_left := Point.{ x = lay.origin.x - 5.0, y = lay.origin.y + 10.0 };
off_above := Point.{ x = lay.origin.x + 10.0, y = lay.origin.y - 5.0 }; off_above := Point.{ x = lay.origin.x + 10.0, y = lay.origin.y - 5.0 };
off_right := Point.{ x = lay.origin.x + 8.0 * lay.cell_size + 1.0, y = lay.origin.y + 10.0 }; off_right := Point.{ x = lay.origin.x + 8.0 * lay.cell_size + 1.0, y = lay.origin.y + 10.0 };
on_board : s64 = 0; on_board : i64 = 0;
if h := lay.point_to_cell(off_left) { on_board += 1; print("off_left hit ({},{})\n", h.col, h.row); } if h := lay.point_to_cell(off_left) { on_board += 1; print("off_left hit ({},{})\n", h.col, h.row); }
if h := lay.point_to_cell(off_above) { on_board += 1; print("off_above hit ({},{})\n", h.col, h.row); } if h := lay.point_to_cell(off_above) { on_board += 1; print("off_above hit ({},{})\n", h.col, h.row); }
if h := lay.point_to_cell(off_right) { on_board += 1; print("off_right hit ({},{})\n", h.col, h.row); } if h := lay.point_to_cell(off_right) { on_board += 1; print("off_right hit ({},{})\n", h.col, h.row); }

View File

@@ -34,7 +34,7 @@ char_to_gem :: (c: u8) -> Gem {
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars), // Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars),
// seeded RNG, running score zeroed, the turn counters reset to a fresh game, and // seeded RNG, running score zeroed, the turn counters reset to a fresh game, and
// the per-level goal set. // the per-level goal set.
load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) -> Board { load_board :: (rows: []string, seed: i64, move_limit: i64, target_score: i64) -> Board {
b : Board = ---; b : Board = ---;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
line := rows[row]; line := rows[row];
@@ -55,7 +55,7 @@ boards_equal :: (a: *Board, b: *Board) -> bool {
true true
} }
main :: () -> s32 { main :: () -> i32 {
print("== level (turn / goal state machine) ==\n"); print("== level (turn / goal state machine) ==\n");
// ── Start: a fresh seeded board reads in_progress with the default goal ── // ── Start: a fresh seeded board reads in_progress with the default goal ──

View File

@@ -20,7 +20,7 @@ char_to_gem :: (c: u8) -> Gem {
// Build a scene: load an 8x8 board from `rows` (top row first, each exactly // Build a scene: load an 8x8 board from `rows` (top row first, each exactly
// BOARD_COLS gem characters), detect matches, print board + matched dump, and // BOARD_COLS gem characters), detect matches, print board + matched dump, and
// assert the matched-cell count. // assert the matched-cell count.
scene :: (name: string, rows: []string, want_count: s64) { scene :: (name: string, rows: []string, want_count: i64) {
b : Board = ---; b : Board = ---;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
line := rows[row]; line := rows[row];
@@ -38,7 +38,7 @@ scene :: (name: string, rows: []string, want_count: s64) {
t.expect(m.count() == want_count, name); t.expect(m.count() == want_count, name);
} }
main :: () -> s32 { main :: () -> i32 {
// Single horizontal 3-run (row 3, cols 2-4). // Single horizontal 3-run (row 3, cols 2-4).
scene("horizontal-3", .[ scene("horizontal-3", .[
"OGOGOGOG", "OGOGOGOG",

View File

@@ -41,8 +41,8 @@ load_board :: (rows: []string) -> Board {
b b
} }
count_empties :: (b: *Board) -> s64 { count_empties :: (b: *Board) -> i64 {
n : s64 = 0; n : i64 = 0;
for 0..BOARD_CELLS (i) { if b.cells[i] == .empty { n += 1; } } for 0..BOARD_CELLS (i) { if b.cells[i] == .empty { n += 1; } }
n n
} }
@@ -72,7 +72,7 @@ fresh_board :: () -> Board {
b b
} }
main :: () -> s32 { main :: () -> i32 {
print("== refill (seeded) ==\n"); print("== refill (seeded) ==\n");
// Pipeline, snapshotting each stage. // Pipeline, snapshotting each stage.
@@ -123,7 +123,7 @@ main :: () -> s32 {
// filled, then refill again. The board's RNG has advanced past the first // filled, then refill again. The board's RNG has advanced past the first
// fill, so the second fill draws new gems — proof it does NOT reseed per call. // fill, so the second fill draws new gems — proof it does NOT reseed per call.
holes_n := 0; holes_n := 0;
hole_idx : [BOARD_CELLS]s64 = ---; hole_idx : [BOARD_CELLS]i64 = ---;
fill1 : [BOARD_CELLS]Gem = ---; fill1 : [BOARD_CELLS]Gem = ---;
for 0..BOARD_CELLS (i) { for 0..BOARD_CELLS (i) {
if pre.cells[i] == .empty { if pre.cells[i] == .empty {

View File

@@ -42,7 +42,7 @@ load_board :: (rows: []string) -> Board {
// Score one scene: snapshot board + enumerated runs + points, then assert // Score one scene: snapshot board + enumerated runs + points, then assert
// `score_round` is exact and `add_round_score` accumulates it into `board.score`. // `score_round` is exact and `add_round_score` accumulates it into `board.score`.
scene :: (name: string, rows: []string, want_points: s64) { scene :: (name: string, rows: []string, want_points: i64) {
b := load_board(rows); b := load_board(rows);
runs := find_runs(@b); runs := find_runs(@b);
@@ -58,7 +58,7 @@ scene :: (name: string, rows: []string, want_points: s64) {
concat(name, ": add_round_score accumulates into board.score")); concat(name, ": add_round_score accumulates into board.score"));
} }
main :: () -> s32 { main :: () -> i32 {
print("== score (base match scoring) ==\n"); print("== score (base match scoring) ==\n");
// Single length-3 horizontal run (row 3, cols 2-4) -> SCORE_RUN_3 = 30. // Single length-3 horizontal run (row 3, cols 2-4) -> SCORE_RUN_3 = 30.

View File

@@ -44,11 +44,11 @@ boards_equal :: (x: *Board, y: *Board) -> bool {
true true
} }
cell :: (col: s64, row: s64) -> Cell { cell :: (col: i64, row: i64) -> Cell {
Cell.{ col = col, row = row } Cell.{ col = col, row = row }
} }
main :: () -> s32 { main :: () -> i32 {
print("== swap & legality ==\n"); print("== swap & legality ==\n");
// Board whose ONLY swap-formable match is the adjacent (2,3)<->(3,3) // Board whose ONLY swap-formable match is the adjacent (2,3)<->(3,3)

View File

@@ -20,7 +20,7 @@
SEED :: 1337; SEED :: 1337;
cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point { cell_center :: (lay: *BoardLayout, col: i64, row: i64) -> Point {
cf := lay.cell_frame(col, row); cf := lay.cell_frame(col, row);
Point.{ x = cf.mid_x(), y = cf.mid_y() } Point.{ x = cf.mid_x(), y = cf.mid_y() }
} }
@@ -32,14 +32,14 @@ boards_equal :: (x: *Board, y: *Board) -> bool {
true true
} }
main :: () -> s32 { main :: () -> i32 {
// 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0). A // 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0). A
// 60px drag clears the cell*0.5 = 37.5px swipe threshold on the dominant axis. // 60px drag clears the cell*0.5 = 37.5px swipe threshold on the dominant axis.
lay : BoardLayout = ---; lay : BoardLayout = ---;
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero()); lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
D : f32 = 60.0; D : f32 = 60.0;
fails : s64 = 0; fails : i64 = 0;
// ── ILLEGAL swipe reverts ────────────────────────────────────────────── // ── ILLEGAL swipe reverts ──────────────────────────────────────────────
// (0,0) and (1,0) are both red on the seed board, so swapping them forms no // (0,0) and (1,0) are both red on the seed board, so swapping them forms no

View File

@@ -14,14 +14,14 @@
#import "board_layout.sx"; #import "board_layout.sx";
#import "swipe.sx"; #import "swipe.sx";
cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point { cell_center :: (lay: *BoardLayout, col: i64, row: i64) -> Point {
cf := lay.cell_frame(col, row); cf := lay.cell_frame(col, row);
Point.{ x = cf.mid_x(), y = cf.mid_y() } Point.{ x = cf.mid_x(), y = cf.mid_y() }
} }
// Print the resolved intent (locked in the golden) and report whether it matches // Print the resolved intent (locked in the golden) and report whether it matches
// the expected adjacent pair (A, B). Drives the exit code alongside the dump. // the expected adjacent pair (A, B). Drives the exit code alongside the dump.
expect_swap :: (label: string, got: ?Swap, ac: s64, ar: s64, bc: s64, br: s64) -> bool { expect_swap :: (label: string, got: ?Swap, ac: i64, ar: i64, bc: i64, br: i64) -> bool {
if s := got { if s := got {
print("{}: ({},{})->({},{})\n", label, s.a.col, s.a.row, s.b.col, s.b.row); print("{}: ({},{})->({},{})\n", label, s.a.col, s.a.row, s.b.col, s.b.row);
return s.a.col == ac and s.a.row == ar and s.b.col == bc and s.b.row == br; return s.a.col == ac and s.a.row == ar and s.b.col == bc and s.b.row == br;
@@ -39,14 +39,14 @@ expect_none :: (label: string, got: ?Swap) -> bool {
true true
} }
main :: () -> s32 { main :: () -> i32 {
lay : BoardLayout = ---; lay : BoardLayout = ---;
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero()); lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
print("grid origin ({},{}) cell {} threshold {}\n", print("grid origin ({},{}) cell {} threshold {}\n",
cast(s64) lay.origin.x, cast(s64) lay.origin.y, cast(s64) lay.cell_size, cast(i64) lay.origin.x, cast(i64) lay.origin.y, cast(i64) lay.cell_size,
cast(s64) (lay.cell_size * SWIPE_THRESHOLD_FRACTION)); cast(i64) (lay.cell_size * SWIPE_THRESHOLD_FRACTION));
fails : s64 = 0; fails : i64 = 0;
// A known interior cell; every cardinal swipe from it stays on the board. // A known interior cell; every cardinal swipe from it stays on the board.
start := cell_center(@lay, 3, 5); start := cell_center(@lay, 3, 5);

View File

@@ -36,7 +36,7 @@ char_to_gem :: (c: u8) -> Gem {
// Load an 8x8 board from `rows` (top row first), seeded RNG, score zeroed, turn // Load an 8x8 board from `rows` (top row first), seeded RNG, score zeroed, turn
// counters reset to a fresh game, and the per-level goal set. // counters reset to a fresh game, and the per-level goal set.
load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) -> Board { load_board :: (rows: []string, seed: i64, move_limit: i64, target_score: i64) -> Board {
b : Board = ---; b : Board = ---;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
line := rows[row]; line := rows[row];
@@ -52,13 +52,13 @@ load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) ->
b b
} }
cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point { cell_center :: (lay: *BoardLayout, col: i64, row: i64) -> Point {
cf := lay.cell_frame(col, row); cf := lay.cell_frame(col, row);
Point.{ x = cf.mid_x(), y = cf.mid_y() } Point.{ x = cf.mid_x(), y = cf.mid_y() }
} }
main :: () -> s32 { main :: () -> i32 {
fails : s64 = 0; fails : i64 = 0;
// 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0). A 60px // 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0). A 60px
// drag clears the cell*0.5 = 37.5px swipe threshold on the dominant axis — the // drag clears the cell*0.5 = 37.5px swipe threshold on the dominant axis — the

View File

@@ -37,7 +37,7 @@ char_to_gem :: (c: u8) -> Gem {
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars), // Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars),
// seeded RNG, running score zeroed, and the turn counters reset to a fresh game // seeded RNG, running score zeroed, and the turn counters reset to a fresh game
// (no moves made, the given move budget). // (no moves made, the given move budget).
load_board :: (rows: []string, seed: s64, move_limit: s64) -> Board { load_board :: (rows: []string, seed: i64, move_limit: i64) -> Board {
b : Board = ---; b : Board = ---;
for 0..BOARD_ROWS (row) { for 0..BOARD_ROWS (row) {
line := rows[row]; line := rows[row];
@@ -60,7 +60,7 @@ boards_equal :: (a: *Board, b: *Board) -> bool {
// One flag scene: snapshot the board, then count its single round's special // One flag scene: snapshot the board, then count its single round's special
// runs and assert the tallies (and the boolean flags derived from them) are // runs and assert the tallies (and the boolean flags derived from them) are
// exactly the documented values. No RNG, no clear — pure detection. // exactly the documented values. No RNG, no clear — pure detection.
flag_scene :: (name: string, rows: []string, want_len4: s64, want_len5_plus: s64) { flag_scene :: (name: string, rows: []string, want_len4: i64, want_len5_plus: i64) {
print("== {} ==\n", name); print("== {} ==\n", name);
b := load_board(rows, 0, LIMIT); b := load_board(rows, 0, LIMIT);
out(board_dump(@b)); out(board_dump(@b));
@@ -71,7 +71,7 @@ flag_scene :: (name: string, rows: []string, want_len4: s64, want_len5_plus: s64
t.expect(sp.len5_plus == want_len5_plus, concat(name, ": len5_plus count exact")); t.expect(sp.len5_plus == want_len5_plus, concat(name, ": len5_plus count exact"));
} }
main :: () -> s32 { main :: () -> i32 {
print("== turn (accounting + special-match flagging) ==\n"); print("== turn (accounting + special-match flagging) ==\n");
// ── Special-match flagging (single round, no RNG) ────────────────────── // ── Special-match flagging (single round, no RNG) ──────────────────────

View File

@@ -33,14 +33,14 @@ GRAY_TOL :: 24; // max channel spread still considered neutral gray
LUM_MARGIN :: 4; // lum headroom above the light checker shade LUM_MARGIN :: 4; // lum headroom above the light checker shade
is_gray :: (r: u8, g: u8, b: u8) -> bool { is_gray :: (r: u8, g: u8, b: u8) -> bool {
hi := max(max(cast(s64) r, cast(s64) g), cast(s64) b); hi := max(max(cast(i64) r, cast(i64) g), cast(i64) b);
lo := min(min(cast(s64) r, cast(s64) g), cast(s64) b); lo := min(min(cast(i64) r, cast(i64) g), cast(i64) b);
hi - lo <= GRAY_TOL hi - lo <= GRAY_TOL
} }
// Mark pixel `i` as removed background and queue it, if it is unvisited checker // Mark pixel `i` as removed background and queue it, if it is unvisited checker
// (near-neutral gray no brighter than the light checker shade + margin). // (near-neutral gray no brighter than the light checker shade + margin).
fd_seed :: (i: s64, bg: [*]u8, lum: [*]s64, src: [*]u8, stack: [*]s64, sp: *s64, lim: s64) { fd_seed :: (i: i64, bg: [*]u8, lum: [*]i64, src: [*]u8, stack: [*]i64, sp: *i64, lim: i64) {
if bg[i] != 0 { return; } if bg[i] != 0 { return; }
p := i * 4; p := i * 4;
if lum[i] <= lim and is_gray(src[p], src[p+1], src[p+2]) { if lum[i] <= lim and is_gray(src[p], src[p+1], src[p+2]) {
@@ -50,35 +50,35 @@ fd_seed :: (i: s64, bg: [*]u8, lum: [*]s64, src: [*]u8, stack: [*]s64, sp: *s64,
} }
} }
main :: () -> s32 { main :: () -> i32 {
w : s32 = 0; w : i32 = 0;
h : s32 = 0; h : i32 = 0;
ch : s32 = 0; ch : i32 = 0;
src : [*]u8 = xx stbi_load(SRC_PATH, @w, @h, @ch, 4); src : [*]u8 = xx stbi_load(SRC_PATH, @w, @h, @ch, 4);
if xx src == 0 { if xx src == 0 {
print("FATAL: could not load {}\n", SRC_PATH); print("FATAL: could not load {}\n", SRC_PATH);
return 1; return 1;
} }
W := cast(s64) w; W := cast(i64) w;
H := cast(s64) h; H := cast(i64) h;
N := W * H; N := W * H;
print("loaded {}x{} ({} src channels)\n", w, h, ch); print("loaded {}x{} ({} src channels)\n", w, h, ch);
// Hoisted working locals (see codegen note above). // Hoisted working locals (see codegen note above).
y : s64 = 0; y : i64 = 0;
x : s64 = 0; x : i64 = 0;
i : s64 = 0; i : i64 = 0;
p : s64 = 0; p : i64 = 0;
r : s64 = 0; r : i64 = 0;
g : s64 = 0; g : i64 = 0;
b : s64 = 0; b : i64 = 0;
l : s64 = 0; l : i64 = 0;
// Per-pixel luminance, plus the checker shades read off the border ring // Per-pixel luminance, plus the checker shades read off the border ring
// (the border is pure checker — the glow never reaches the corners). // (the border is pure checker — the glow never reaches the corners).
lum : [*]s64 = xx context.allocator.alloc_bytes(N * size_of(s64)); lum : [*]i64 = xx context.allocator.alloc_bytes(N * size_of(i64));
c_lo : s64 = 255; c_lo : i64 = 255;
c_hi : s64 = 0; c_hi : i64 = 0;
y = 0; y = 0;
while y < H { while y < H {
x = 0; x = 0;
@@ -104,8 +104,8 @@ main :: () -> s32 {
// border pixel. `bg[i]==1` marks a removed (transparent) background pixel. // border pixel. `bg[i]==1` marks a removed (transparent) background pixel.
bg : [*]u8 = xx context.allocator.alloc_bytes(N); bg : [*]u8 = xx context.allocator.alloc_bytes(N);
memset(xx bg, 0, N); memset(xx bg, 0, N);
stack : [*]s64 = xx context.allocator.alloc_bytes(N * size_of(s64)); stack : [*]i64 = xx context.allocator.alloc_bytes(N * size_of(i64));
sp : s64 = 0; sp : i64 = 0;
checker_lim := c_hi + LUM_MARGIN; checker_lim := c_hi + LUM_MARGIN;
x = 0; x = 0;
@@ -121,12 +121,12 @@ main :: () -> s32 {
y += 1; y += 1;
} }
cx : s64 = 0; cx : i64 = 0;
cy : s64 = 0; cy : i64 = 0;
dx : s64 = 0; dx : i64 = 0;
dy : s64 = 0; dy : i64 = 0;
nx : s64 = 0; nx : i64 = 0;
ny : s64 = 0; ny : i64 = 0;
while sp > 0 { while sp > 0 {
sp -= 1; sp -= 1;
i = stack[sp]; i = stack[sp];
@@ -154,8 +154,8 @@ main :: () -> s32 {
denom := cast(f32) (255 - c_hi); denom := cast(f32) (255 - c_hi);
if denom < 1.0 { denom = 1.0; } if denom < 1.0 { denom = 1.0; }
alpha : [*]f32 = xx context.allocator.alloc_bytes(N * size_of(f32)); alpha : [*]f32 = xx context.allocator.alloc_bytes(N * size_of(f32));
kept : s64 = 0; kept : i64 = 0;
n_bg : s64 = 0; n_bg : i64 = 0;
a : f32 = 0.0; a : f32 = 0.0;
i = 0; i = 0;
while i < N { while i < N {
@@ -178,26 +178,26 @@ main :: () -> s32 {
sxf := cast(f32) W / cast(f32) OUT_DIM; sxf := cast(f32) W / cast(f32) OUT_DIM;
syf := cast(f32) H / cast(f32) OUT_DIM; syf := cast(f32) H / cast(f32) OUT_DIM;
max_a : f32 = 0.0; max_a : f32 = 0.0;
ty : s64 = 0; ty : i64 = 0;
tx : s64 = 0; tx : i64 = 0;
x0 : s64 = 0; x0 : i64 = 0;
x1 : s64 = 0; x1 : i64 = 0;
y0 : s64 = 0; y0 : i64 = 0;
y1 : s64 = 0; y1 : i64 = 0;
sum : f32 = 0.0; sum : f32 = 0.0;
cnt : s64 = 0; cnt : i64 = 0;
sy : s64 = 0; sy : i64 = 0;
sx : s64 = 0; sx : i64 = 0;
av : f32 = 0.0; av : f32 = 0.0;
o : s64 = 0; o : i64 = 0;
ty = 0; ty = 0;
while ty < OUT_DIM { while ty < OUT_DIM {
tx = 0; tx = 0;
while tx < OUT_DIM { while tx < OUT_DIM {
x0 = cast(s64) (cast(f32) tx * sxf); x0 = cast(i64) (cast(f32) tx * sxf);
x1 = cast(s64) (cast(f32) (tx + 1) * sxf); x1 = cast(i64) (cast(f32) (tx + 1) * sxf);
y0 = cast(s64) (cast(f32) ty * syf); y0 = cast(i64) (cast(f32) ty * syf);
y1 = cast(s64) (cast(f32) (ty + 1) * syf); y1 = cast(i64) (cast(f32) (ty + 1) * syf);
if x1 <= x0 { x1 = x0 + 1; } if x1 <= x0 { x1 = x0 + 1; }
if y1 <= y0 { y1 = y0 + 1; } if y1 <= y0 { y1 = y0 + 1; }
sum = 0.0; sum = 0.0;