migrate to the new for-loop syntax

Drop the ':' before captures (for xs (x) / for 0..n (i)); the index
capture becomes the trailing open range (for xs, 0.. (x, i)). 136
headers across 26 files, mechanical.

Five headless tests (banner_layout, hit_test, swipe_commit,
swipe_intent, swipe_reshuffle) also gain a direct
#import "modules/ui/types.sx" — they named Point/Frame through a
transitive import, which bare visibility no longer permits.

Gates: sx build --target ios-sim main.sx links; tools/run_tests.sh
23/23.
This commit is contained in:
swipelab
2026-06-10 20:39:59 +03:00
parent 5a0627bb7c
commit a7b41ccbca
28 changed files with 141 additions and 136 deletions

View File

@@ -155,8 +155,8 @@ Board :: struct {
self.moves_made = 0;
self.move_limit = DEFAULT_MOVE_LIMIT;
self.target_score = DEFAULT_TARGET_SCORE;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
self.set(col, row, pick_gem(self, @self.rng, col, row));
}
}
@@ -167,7 +167,7 @@ Board :: struct {
// upward. Pure given the board's already-placed prefix and the RNG state.
pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
forbidden : [GEM_COUNT]bool = ---;
for 0..GEM_COUNT: (t) { forbidden[t] = false; }
for 0..GEM_COUNT (t) { forbidden[t] = false; }
// Two same gems immediately to the left → a third of that type matches.
if col >= 2 {
@@ -185,11 +185,11 @@ pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
}
allowed := 0;
for 0..GEM_COUNT: (t) { if !forbidden[t] { allowed += 1; } }
for 0..GEM_COUNT (t) { if !forbidden[t] { allowed += 1; } }
// Pick the k-th still-allowed type; single RNG draw, always terminates.
k := rng.next_range(allowed);
for 0..GEM_COUNT: (t) {
for 0..GEM_COUNT (t) {
if !forbidden[t] {
if k == 0 { return cast(Gem) t; }
k -= 1;
@@ -203,9 +203,9 @@ pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
board_dump :: (self: *Board) -> string {
line_w := BOARD_COLS + 1; // 8 gem chars + newline
buf := cstring(BOARD_ROWS * line_w);
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
base := row * line_w;
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
buf[base + col] = gem_char(self.at(col, row));
}
buf[base + BOARD_COLS] = 10; // '\n'
@@ -229,7 +229,7 @@ MatchMask :: struct {
count :: (self: *MatchMask) -> s64 {
n : s64 = 0;
for 0..BOARD_CELLS: (i) { if self.cells[i] { n += 1; } }
for 0..BOARD_CELLS (i) { if self.cells[i] { n += 1; } }
n
}
}
@@ -238,7 +238,7 @@ MatchMask :: struct {
// is the constant coordinate (the row for a horizontal span, the column for a
// vertical one) and the span covers `start..end` of the moving coordinate.
mark_run :: (m: *MatchMask, vertical: bool, fixed: s64, start: s64, end: s64) {
for start..end: (i) {
for start..end (i) {
if vertical {
m.cells[Board.idx(fixed, i)] = true;
} else {
@@ -259,10 +259,10 @@ mark_run :: (m: *MatchMask, vertical: bool, fixed: s64, start: s64, end: s64) {
// break runs of real gems, since a hole differs from every gem type.
find_matches :: (b: *Board) -> MatchMask {
m : MatchMask = ---;
for 0..BOARD_CELLS: (i) { m.cells[i] = false; }
for 0..BOARD_CELLS (i) { m.cells[i] = false; }
// Horizontal: walk each row left-to-right in maximal same-type spans.
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
col := 0;
while col < BOARD_COLS {
g := b.at(col, row);
@@ -276,7 +276,7 @@ find_matches :: (b: *Board) -> MatchMask {
}
// Vertical: walk each column top-to-bottom in maximal same-type spans.
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
row := 0;
while row < BOARD_ROWS {
g := b.at(col, row);
@@ -299,9 +299,9 @@ find_matches :: (b: *Board) -> MatchMask {
dump_matches :: (b: *Board, m: *MatchMask) -> string {
line_w := BOARD_COLS + 1; // 8 cells + newline
buf := cstring(BOARD_ROWS * line_w);
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
base := row * line_w;
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
if m.at(col, row) {
buf[base + col] = gem_char(b.at(col, row));
} else {
@@ -371,8 +371,8 @@ Swap :: struct {
// the snapshot can depend on it.
legal_swaps :: (board: *Board) -> List(Swap) {
result := List(Swap).{};
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
here := Cell.{ col = col, row = row };
if col + 1 < BOARD_COLS {
right := Cell.{ col = col + 1, row = row };
@@ -397,7 +397,7 @@ legal_swaps :: (board: *Board) -> List(Swap) {
// as just "0 legal swaps", which reads unambiguously. Suitable for snapshotting.
dump_swaps :: (swaps: *List(Swap)) -> string {
result := format("{} legal swaps\n", swaps.len);
for 0..swaps.len: (i) {
for 0..swaps.len (i) {
s := swaps.items[i];
result = concat(result, format("({},{})-({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row));
}
@@ -415,7 +415,7 @@ dump_swaps :: (swaps: *List(Swap)) -> string {
// `true` per shared cell) clear as one set with no double-counting.
clear_cells :: (board: *Board, mask: *MatchMask) -> s64 {
cleared : s64 = 0;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if mask.cells[i] {
board.cells[i] = .empty;
cleared += 1;
@@ -449,7 +449,7 @@ clear_matches :: (board: *Board) -> s64 {
// this to know when gravity has stopped.
collapse :: (board: *Board) -> bool {
moved := false;
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
// Pack this column's gems toward the bottom: scan it bottom-to-top and
// write each gem at the falling cursor `w`, which also descends from the
// bottom. A gem whose source row differs from `w` actually fell. `w`
@@ -492,8 +492,8 @@ collapse :: (board: *Board) -> bool {
refill :: (board: *Board) -> s64 {
rng := @board.rng;
filled : s64 = 0;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
if board.at(col, row) == .empty {
board.set(col, row, cast(Gem) rng.next_range(GEM_COUNT));
filled += 1;
@@ -628,7 +628,7 @@ run_score :: (len: s64) -> s64 {
find_runs :: (b: *Board) -> List(Run) {
runs := List(Run).{};
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
col := 0;
while col < BOARD_COLS {
g := b.at(col, row);
@@ -643,7 +643,7 @@ find_runs :: (b: *Board) -> List(Run) {
}
}
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
row := 0;
while row < BOARD_ROWS {
g := b.at(col, row);
@@ -669,7 +669,7 @@ find_runs :: (b: *Board) -> List(Run) {
score_round :: (board: *Board) -> s64 {
runs := find_runs(board);
total : s64 = 0;
for 0..runs.len: (i) {
for 0..runs.len (i) {
total += run_score(runs.items[i].len);
}
total
@@ -724,7 +724,7 @@ SpecialCounts :: struct {
count_specials :: (board: *Board) -> SpecialCounts {
runs := find_runs(board);
counts := SpecialCounts.{ len4 = 0, len5_plus = 0 };
for 0..runs.len: (i) {
for 0..runs.len (i) {
len := runs.items[i].len;
if len == 4 {
counts.len4 += 1;
@@ -741,7 +741,7 @@ count_specials :: (board: *Board) -> SpecialCounts {
// "0 runs". Suitable for snapshotting.
dump_runs :: (runs: *List(Run)) -> string {
result := format("{} runs\n", runs.len);
for 0..runs.len: (i) {
for 0..runs.len (i) {
r := runs.items[i];
axis := if r.vertical then "V" else "H";
result = concat(result, format("{} len {} at fixed {} start {}\n", axis, r.len, r.fixed, r.start));
@@ -829,8 +829,8 @@ level_status :: (board: *Board) -> Status {
// a throwaway list each call. The trial swaps inside `swap_legal` are reverted,
// so the board is left unchanged.
has_legal_swap :: (board: *Board) -> bool {
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
here := Cell.{ col = col, row = row };
if col + 1 < BOARD_COLS {
right := Cell.{ col = col + 1, row = row };

View File

@@ -163,7 +163,7 @@ ClearDiag :: struct { lo: s64; hi: s64; }
clear_diag_span :: (m: *MatchMask) -> ClearDiag {
lo : s64 = (BOARD_COLS - 1) + (BOARD_ROWS - 1) + 1;
hi : s64 = -1;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if m.cells[i] {
d := (i % BOARD_COLS) + (i / BOARD_COLS);
if d < lo { lo = d; }
@@ -277,7 +277,7 @@ plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
// came from source row `r`. The rows left above the survivors (0..w) are
// refilled, so they drop in from above: a dest row `j` there starts at
// `j - n_refill`, i.e. stacked just off the top edge.
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
w := BOARD_ROWS - 1;
r := BOARD_ROWS - 1;
while r >= 0 {
@@ -362,7 +362,7 @@ BoardAnim :: struct {
return AnimPhase.{ kind = .swap, round = 0, t = e / SWAP_ANIM_DUR };
}
e -= SWAP_ANIM_DUR;
for 0..self.move.rounds.len: (k) {
for 0..self.move.rounds.len (k) {
if e < CLEAR_ANIM_DUR {
return AnimPhase.{ kind = .clear, round = k, t = e / CLEAR_ANIM_DUR };
}

View File

@@ -123,7 +123,7 @@ BoardFxAssets :: struct {
loaded: bool;
init :: (self: *BoardFxAssets) {
for 0..GEM_COUNT: (t) { self.tex[t] = 0; }
for 0..GEM_COUNT (t) { self.tex[t] = 0; }
self.loaded = false;
}
@@ -144,7 +144,7 @@ BoardFxAssets :: struct {
// (sx codegen), so the per-pixel tint loop only ASSIGNS pre-declared vars.
i : s64 = 0;
o : s64 = 0;
for 0..GEM_COUNT: (t) {
for 0..GEM_COUNT (t) {
col := fx_tint(t);
i = 0;
while i < n {
@@ -219,7 +219,7 @@ BoardFx :: struct {
// Whole-move depth boost: a deeper cascade makes every burst bigger from
// its first round, escalating in lockstep with the cascade SFX cue.
depth_boost := FX_BURST_DEPTH * cast(f32) fx_combo_level(mv.rounds.len);
for 0..mv.rounds.len: (k) {
for 0..mv.rounds.len (k) {
rd := @mv.rounds.items[k];
t0 := SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR);
extra := depth_boost + FX_BURST_COMBO * cast(f32) min(k, 2);
@@ -227,7 +227,7 @@ BoardFx :: struct {
// bursts ripple in lockstep with the staggered pops (P18.2) instead of
// one simultaneous flash. The round's audio cue still fires once at t0.
span := clear_diag_span(@rd.matched);
for 0..BOARD_CELLS: (idx) {
for 0..BOARD_CELLS (idx) {
if rd.matched.cells[idx] {
g := rd.before[idx];
if g != .empty {
@@ -253,7 +253,7 @@ BoardFx :: struct {
sc : s64 = 0;
sr : s64 = 0;
cnt : s64 = 0;
for 0..BOARD_CELLS: (idx) {
for 0..BOARD_CELLS (idx) {
if rd0.matched.cells[idx] {
sc += idx % BOARD_COLS;
sr += idx / BOARD_COLS;

View File

@@ -357,8 +357,8 @@ BoardView :: struct {
// Settled-board gems: one sprite per non-empty cell, drawn with its live
// per-gem animation pose. Used whenever no move is animating.
render_gems :: (self: *BoardView, ctx: *RenderContext, dim: f32) {
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
g := self.board.at(col, row);
if g != .empty {
pose := self.gem_pose_at(col, row);
@@ -431,7 +431,7 @@ BoardView :: struct {
// (which resumes the same back-dated stamp). tick() normally clears
// `active` before this is reached.
last := mv.rounds.len - 1;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
g := mv.final[i];
if g != .empty {
sq := self.rest_squash(i, last, e);
@@ -452,7 +452,7 @@ BoardView :: struct {
ai := Board.idx(mv.a.col, mv.a.row);
bi := Board.idx(mv.b.col, mv.b.row);
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if i == ai or i == bi { continue; }
g := mv.pre[i];
if g != .empty {
@@ -495,7 +495,7 @@ BoardView :: struct {
// 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) {
span := clear_diag_span(@rd.matched);
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
g := rd.before[i];
if g == .empty { continue; }
col := i % BOARD_COLS;
@@ -532,7 +532,7 @@ BoardView :: struct {
cs * cast(f32) BOARD_COLS, cs * cast(f32) BOARD_ROWS
);
ctx.push_clip(grid);
for 0..self.fx.particles.len: (i) {
for 0..self.fx.particles.len (i) {
p := self.fx.particles.items[i];
lt := (p.age - p.delay) / p.life;
env := fx_pop_env(lt);
@@ -556,7 +556,7 @@ BoardView :: struct {
render_fx_popups :: (self: *BoardView, ctx: *RenderContext) {
if self.fx == null or self.fx.popups.len == 0 { return; }
cs := self.layout.cell_size;
for 0..self.fx.popups.len: (i) {
for 0..self.fx.popups.len (i) {
q := self.fx.popups.items[i];
lt := (q.age - q.delay) / q.life;
if lt >= 0.0 {
@@ -594,7 +594,7 @@ BoardView :: struct {
// 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.
render_fall :: (self: *BoardView, ctx: *RenderContext, rd: *AnimRound, k: s64, e: f32, dim: f32, t: f32) {
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
g := rd.after[i];
if g == .empty { continue; }
col := i % BOARD_COLS;
@@ -731,8 +731,8 @@ impl View for BoardView {
gem_inset := self.layout.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
gem_dim := self.layout.cell_size * GEM_FILL_FRAC;
if self.assets.cell_tex != 0 {
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
ctx.add_image(self.layout.cell_frame(col, row), self.assets.cell_tex);
}
}

View File

@@ -132,7 +132,7 @@ GemMotion :: struct {
// its resting pose instead of replaying the prior move's landing wobble; the
// idle clock keeps running, so the always-on idle simply resumes from rest.
reset_landings :: (self: *GemMotion) {
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) {

View File

@@ -183,8 +183,8 @@ parse_s64 :: (s: string) -> s64 {
// trial swaps inside `swap_legal` are reverted, so the board is left unchanged.
illegal_swaps :: (board: *Board) -> List(Swap) {
result := List(Swap).{};
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
here := Cell.{ col = col, row = row };
if col + 1 < BOARD_COLS {
right := Cell.{ col = col + 1, row = row };
@@ -222,7 +222,7 @@ frame :: () {
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
}
for g_plat.poll_events(): (*ev) {
for g_plat.poll_events() (*ev) {
inline if OS != .ios {
if ev == {
case .key_up: (e) {
@@ -277,7 +277,7 @@ frame :: () {
mv := @g_anim.move;
total := g_anim.total();
last := mv.rounds.len - 1;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
m := delivering_round(mv, i, last);
if m >= 0 {
col := i % BOARD_COLS;

View File

@@ -24,7 +24,7 @@
SEED :: 1337;
boards_equal :: (x: *Board, y: *Board) -> bool {
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(x.cells[i] == y.cells[i]) { return false; }
}
true
@@ -61,7 +61,7 @@ main :: () -> s32 {
// move.final equals the model board.
final_eq := true;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(move.final[i] == bm.cells[i]) { final_eq = false; }
}
if !final_eq { fails += 1; }
@@ -74,21 +74,21 @@ main :: () -> s32 {
ai := Board.idx(a.col, a.row);
bi := Board.idx(b.col, b.row);
r0 := @move.rounds.items[0];
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
expect : Gem = move.pre[i];
if i == ai { expect = move.pre[bi]; }
else if i == bi { expect = move.pre[ai]; }
if !(r0.before[i] == expect) { contiguous = false; }
}
for 1..move.rounds.len: (k) {
for 1..move.rounds.len (k) {
prev := @move.rounds.items[k - 1];
cur := @move.rounds.items[k];
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(cur.before[i] == prev.after[i]) { contiguous = false; }
}
}
last := @move.rounds.items[move.rounds.len - 1];
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(last.after[i] == move.final[i]) { contiguous = false; }
}
}

View File

@@ -13,6 +13,7 @@
// shape and rationale as tests/hit_test.sx. Failure is signalled via a non-zero
// exit code (the runner checks exit code AND stdout).
#import "modules/std.sx";
#import "modules/ui/types.sx";
#import "board.sx";
#import "board_layout.sx";

View File

@@ -12,14 +12,14 @@ SEED :: 1337;
// independently of the placement logic, so it's a real check, not a tautology.
count_three_runs :: (b: *Board) -> s32 {
runs : s32 = 0;
for 0..BOARD_ROWS: (row) {
for 0..(BOARD_COLS - 2): (col) {
for 0..BOARD_ROWS (row) {
for 0..(BOARD_COLS - 2) (col) {
g := b.at(col, row);
if g == b.at(col + 1, row) and g == b.at(col + 2, row) { runs += 1; }
}
}
for 0..(BOARD_ROWS - 2): (row) {
for 0..BOARD_COLS: (col) {
for 0..(BOARD_ROWS - 2) (row) {
for 0..BOARD_COLS (col) {
g := b.at(col, row);
if g == b.at(col, row + 1) and g == b.at(col, row + 2) { runs += 1; }
}

View File

@@ -24,7 +24,7 @@ EXPECTED_DEPTH :: 2;
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -34,9 +34,9 @@ char_to_gem :: (c: u8) -> Gem {
// The RNG is left unseeded — callers seed it before resolving.
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -44,7 +44,7 @@ load_board :: (rows: []string) -> Board {
}
boards_equal :: (a: *Board, b: *Board) -> bool {
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
true
}
@@ -120,7 +120,7 @@ main :: () -> s32 {
t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
same_counts := c.cleared.len == counts.len;
if same_counts {
for 0..counts.len: (i) {
for 0..counts.len (i) {
if c.cleared.items[i] != counts.items[i] { same_counts = false; }
}
}

View File

@@ -15,7 +15,7 @@ main :: () -> s32 {
// 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.
prev : s64 = -1;
for 0..10: (depth) {
for 0..10 (depth) {
idx := cascade_cue_index(depth);
print("depth {} -> idx {} ({})\n", depth, idx, cascade_cue_name(idx));
// The mapping must never step down as depth grows.

View File

@@ -57,7 +57,7 @@ main :: () -> s32 {
// Deep chain: the cue tail clamps at combo5 for round >= 5 (cascade_cue_index).
print("-- deep-chain cue clamp --\n");
for 1..8: (r) { print("round {} -> {}\n", r, cascade_cue_name(cascade_cue_index(r))); }
for 1..8 (r) { print("round {} -> {}\n", r, cascade_cue_name(cascade_cue_index(r))); }
print("ok: one ascending combo cue per cascade round, clamped at combo5\n");
return 0;

View File

@@ -16,7 +16,7 @@ t :: #import "test.sx";
// clear) for the holes-never-match regression.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -26,9 +26,9 @@ char_to_gem :: (c: u8) -> Gem {
// characters).
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -53,7 +53,7 @@ scene :: (name: string, rows: []string, want_cleared: s64) {
cleared_holes := true; // every matched cell is now a hole
others_intact := true; // every other cell is byte-identical
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if m.cells[i] {
if !(b.cells[i] == .empty) { cleared_holes = false; }
} else {

View File

@@ -18,7 +18,7 @@ t :: #import "test.sx";
// maps to `.empty`, so a board can be hand-written with holes in any position.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -27,9 +27,9 @@ char_to_gem :: (c: u8) -> Gem {
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -42,15 +42,15 @@ load_board :: (rows: []string) -> Board {
// them a hole. This single check covers holes-bubble-to-top, gems-settle-to-
// bottom, order-preservation, and the all-holes / no-holes edge columns at once.
check_collapsed :: (orig: *Board, b: *Board) -> bool {
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
gems : [BOARD_ROWS]Gem = ---;
n := 0;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
g := orig.at(col, row);
if g != .empty { gems[n] = g; n += 1; }
}
boundary := BOARD_ROWS - n; // first row that must hold a gem
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
if row < boundary {
if b.at(col, row) != .empty { return false; }
} else {

View File

@@ -23,7 +23,7 @@ t :: #import "test.sx";
// be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -33,9 +33,9 @@ char_to_gem :: (c: u8) -> Gem {
// seeded RNG, running score zeroed so `board.score` ends equal to the payout.
load_board :: (rows: []string, seed: s64) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}

View File

@@ -51,7 +51,7 @@ main :: () -> s32 {
p_io := ease_in_out_cubic(0.0);
p_oc := ease_out_cubic(0.0);
p_iq := ease_in_quad(0.0);
for 1..21: (i) {
for 1..21 (i) {
t := cast(f32) i / 20.0;
v_in := ease_in_cubic(t); if v_in < p_in - 0.000001 { mono_in = false; } p_in = v_in;
v_io := ease_in_out_cubic(t); if v_io < p_io - 0.000001 { mono_io = false; } p_io = v_io;
@@ -72,7 +72,7 @@ main :: () -> s32 {
back_mx := ease_out_back(0.0); back_mn := ease_out_back(0.0);
spr_mx := spring(0.0); spr_mn := spring(0.0);
spr_wobble := false;
for 1..21: (i) {
for 1..21 (i) {
t := cast(f32) i / 20.0;
b := ease_out_back(t);
if b > back_mx { back_mx = b; }
@@ -98,7 +98,7 @@ main :: () -> s32 {
// squash (positive) and a stretch (negative) lobe, and stays bounded.
print("== squash envelope bounded ==\n");
sq_mx : f32 = 0.0; sq_mn : f32 = 0.0; sq_moves := false;
for 0..21: (i) {
for 0..21 (i) {
t := cast(f32) i / 20.0;
s := squash_envelope(t);
if s > sq_mx { sq_mx = s; }
@@ -121,7 +121,7 @@ main :: () -> s32 {
print("== illegal-swap bounce ==\n");
bb_ends := bad_swap_bounce(0.0) == 0.0 and bad_swap_bounce(1.0) == 0.0;
bb_mx : f32 = 0.0; bb_mx_t : f32 = 0.0; bb_mn : f32 = 0.0;
for 0..101: (i) {
for 0..101 (i) {
t := cast(f32) i / 100.0;
v := bad_swap_bounce(t);
if v > bb_mx { bb_mx = v; bb_mx_t = t; }
@@ -151,18 +151,18 @@ main :: () -> s32 {
// starts later), the opposite of a flat lockstep row sharing one progress.
print("== fall stagger bounded ==\n");
stg_t0 := true; stg_t1 := true;
for 0..BOARD_COLS: (c) {
for 0..BOARD_COLS (c) {
if fall_stagger_t(0.0, c) != 0.0 { stg_t0 = false; }
if fall_stagger_t(1.0, c) != 1.0 { stg_t1 = false; }
}
stg_cascade := true;
for 1..BOARD_COLS: (c) {
for 1..BOARD_COLS (c) {
if !(fall_stagger_t(0.5, c) < fall_stagger_t(0.5, c - 1)) { stg_cascade = false; }
}
stg_mono := true;
for 0..BOARD_COLS: (c) {
for 0..BOARD_COLS (c) {
pp := fall_stagger_t(0.0, c);
for 1..21: (i) {
for 1..21 (i) {
tt := cast(f32) i / 20.0;
vv := fall_stagger_t(tt, c);
if vv < pp - 0.000001 { stg_mono = false; }
@@ -190,14 +190,14 @@ main :: () -> s32 {
lf_last := approx(fall_landing_frac(BOARD_COLS - 1), 1.0);
lf_mono := true;
lf_seam := true;
for 0..BOARD_COLS: (c) {
for 0..BOARD_COLS (c) {
if c >= 1 and !(fall_landing_frac(c) > fall_landing_frac(c - 1)) { lf_mono = false; }
lf := fall_landing_frac(c);
if !approx(fall_stagger_t(lf, c), 1.0) { lf_seam = false; } // landed at lf
if fall_stagger_t(lf - 0.05, c) >= 1.0 { lf_seam = false; } // still in air just before
}
rlt_col_mono := true;
for 1..BOARD_COLS: (c) {
for 1..BOARD_COLS (c) {
if !(round_land_time(0, c) > round_land_time(0, c - 1)) { rlt_col_mono = false; }
}
rlt_round_after := round_land_time(1, 0) > round_land_time(0, BOARD_COLS - 1);
@@ -222,22 +222,22 @@ main :: () -> s32 {
// gem 0..1 by diagonal across the round (lowest-diagonal = 0, the first to pop).
print("== clear ripple bounded ==\n");
rip_t0 := true; rip_t1 := true;
for 0..6: (j) {
for 0..6 (j) {
u := cast(f32) j / 5.0;
if clear_ripple_t(0.0, u) != 0.0 { rip_t0 = false; }
if clear_ripple_t(1.0, u) != 1.0 { rip_t1 = false; }
}
rip_ripple := true;
for 1..6: (j) {
for 1..6 (j) {
u := cast(f32) j / 5.0;
up := cast(f32) (j - 1) / 5.0;
if !(clear_ripple_t(0.5, u) < clear_ripple_t(0.5, up)) { rip_ripple = false; }
}
rip_mono := true;
for 0..6: (j) {
for 0..6 (j) {
u := cast(f32) j / 5.0;
pp := clear_ripple_t(0.0, u);
for 1..21: (i) {
for 1..21 (i) {
tt := cast(f32) i / 20.0;
vv := clear_ripple_t(tt, u);
if vv < pp - 0.000001 { rip_mono = false; }
@@ -245,7 +245,7 @@ main :: () -> s32 {
}
}
mm : MatchMask = ---;
for 0..BOARD_CELLS: (i) { mm.cells[i] = false; }
for 0..BOARD_CELLS (i) { mm.cells[i] = false; }
mm.cells[Board.idx(5, 0)] = true; // diagonal 5 — first to pop
mm.cells[Board.idx(5, 1)] = true; // diagonal 6
mm.cells[Board.idx(5, 2)] = true; // diagonal 7 — last to pop

View File

@@ -21,7 +21,7 @@ main :: () -> s32 {
expect_level : [10]s64 = .{ 0, 0, 1, 2, 3, 4, 4, 4, 4, 4 };
prev : s64 = -1;
for 0..10: (depth) {
for 0..10 (depth) {
lvl := fx_combo_level(depth);
font := fx_popup_font(depth);
combo := depth > 1;
@@ -45,7 +45,7 @@ main :: () -> s32 {
// larger and the font never shrinks as the cascade deepens.
if fx_popup_font(1) != FX_POPUP_FONT { print("FAIL: single-clear popup not plain font\n"); return 1; }
pf : f32 = 0.0;
for 2..10: (depth) {
for 2..10 (depth) {
f := fx_popup_font(depth);
if f <= FX_POPUP_FONT { print("FAIL: combo popup not larger than plain at depth {}\n", depth); return 1; }
if depth > 2 and f < pf { print("FAIL: popup font shrank at depth {}\n", depth); return 1; }

View File

@@ -24,8 +24,8 @@ main :: () -> s32 {
// 1. t==0 idle pose is EXACTLY rest for every cell (the determinism invariant).
print("== idle t=0 is rest for all cells ==\n");
rest_ok := true;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
p := idle_pose(0.0, col, row);
if !(p.scale_x == 1.0 and p.scale_y == 1.0 and p.dx == 0.0 and p.dy == 0.0) {
rest_ok = false;
@@ -39,8 +39,8 @@ main :: () -> s32 {
print("== idle mid-phase deforms, bounded ==\n");
moved := false;
bounded := true;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
p := idle_pose(0.6, col, row);
if fabs(p.scale_x - 1.0) > 0.0005 { moved = true; }
if fabs(p.scale_x - 1.0) > 0.05 { bounded = false; }
@@ -82,7 +82,7 @@ main :: () -> s32 {
c_peak := clear_pop_scale(0.30) > 1.1;
c_collapse := true;
pc := clear_pop_scale(CLEAR_POP_RISE);
for 1..21: (i) {
for 1..21 (i) {
tt := CLEAR_POP_RISE + (1.0 - CLEAR_POP_RISE) * cast(f32) i / 20.0;
vv := clear_pop_scale(tt);
if vv > pc + 0.000001 { c_collapse = false; }

View File

@@ -10,6 +10,7 @@
// second `Frame` struct that collides with the UI `Frame`. Failure is signalled
// via a non-zero exit code (the runner checks exit code AND stdout).
#import "modules/std.sx";
#import "modules/ui/types.sx";
#import "board.sx";
#import "board_layout.sx";
@@ -26,8 +27,8 @@ main :: () -> s32 {
// Every cell center must map back to its own cell.
hits : s64 = 0;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
for 0..BOARD_ROWS (row) {
for 0..BOARD_COLS (col) {
cf := lay.cell_frame(col, row);
center := Point.{ x = cf.mid_x(), y = cf.mid_y() };
if h := lay.point_to_cell(center) {

View File

@@ -25,7 +25,7 @@ RESHUFFLE_SEED :: 1337;
// be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -36,9 +36,9 @@ char_to_gem :: (c: u8) -> Gem {
// the per-level goal set.
load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -51,7 +51,7 @@ load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) ->
}
boards_equal :: (a: *Board, b: *Board) -> bool {
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
true
}

View File

@@ -11,7 +11,7 @@ t :: #import "test.sx";
// Inverse of `gem_char`: map a gem character back to its Gem so each board can
// be written as a human-readable grid of GEM_CHARS.
char_to_gem :: (c: u8) -> Gem {
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -22,9 +22,9 @@ char_to_gem :: (c: u8) -> Gem {
// assert the matched-cell count.
scene :: (name: string, rows: []string, want_count: s64) {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}

View File

@@ -22,7 +22,7 @@ SEED :: 1337;
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -32,9 +32,9 @@ char_to_gem :: (c: u8) -> Gem {
// The RNG is left unseeded — callers seed it before drawing.
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -43,12 +43,12 @@ load_board :: (rows: []string) -> Board {
count_empties :: (b: *Board) -> s64 {
n : s64 = 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
}
boards_equal :: (a: *Board, b: *Board) -> bool {
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
true
}
@@ -101,7 +101,7 @@ main :: () -> s32 {
distinct := false;
have_first := false;
first : Gem = .empty;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if pre.cells[i] == .empty {
want := cast(Gem) v.next_range(GEM_COUNT);
if b.cells[i] != want { stream_ok = false; }
@@ -125,17 +125,17 @@ main :: () -> s32 {
holes_n := 0;
hole_idx : [BOARD_CELLS]s64 = ---;
fill1 : [BOARD_CELLS]Gem = ---;
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if pre.cells[i] == .empty {
hole_idx[holes_n] = i;
fill1[holes_n] = b.cells[i];
holes_n += 1;
}
}
for 0..holes_n: (k) { b.cells[hole_idx[k]] = .empty; }
for 0..holes_n (k) { b.cells[hole_idx[k]] = .empty; }
refill(@b);
differs := false;
for 0..holes_n: (k) {
for 0..holes_n (k) {
if b.cells[hole_idx[k]] != fill1[k] { differs = true; }
}
t.expect(differs, "refill: a second refill of the same holes draws new gems (RNG threads, no reseed)");

View File

@@ -20,7 +20,7 @@ t :: #import "test.sx";
// be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -30,9 +30,9 @@ char_to_gem :: (c: u8) -> Gem {
// with the running score zeroed so the accumulation check starts from a known base.
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}

View File

@@ -16,7 +16,7 @@ SEED :: 1337;
// Inverse of `gem_char`: map a gem character back to its Gem so each board can
// be written as a human-readable grid of GEM_CHARS.
char_to_gem :: (c: u8) -> Gem {
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -26,9 +26,9 @@ char_to_gem :: (c: u8) -> Gem {
// characters).
load_board :: (rows: []string) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -38,7 +38,7 @@ load_board :: (rows: []string) -> Board {
// Whole-board equality, cell by cell — used to prove a trial swap leaves the
// board untouched.
boards_equal :: (x: *Board, y: *Board) -> bool {
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(x.cells[i] == y.cells[i]) { return false; }
}
true

View File

@@ -13,6 +13,7 @@
// its trace.sx pulls in a second `Frame` that collides with the UI one. Failure
// is signalled via a non-zero exit code (the runner checks exit code AND stdout).
#import "modules/std.sx";
#import "modules/ui/types.sx";
#import "board.sx";
#import "board_layout.sx";
#import "swipe.sx";
@@ -25,7 +26,7 @@ cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point {
}
boards_equal :: (x: *Board, y: *Board) -> bool {
for 0..BOARD_CELLS: (i) {
for 0..BOARD_CELLS (i) {
if !(x.cells[i] == y.cells[i]) { return false; }
}
true

View File

@@ -9,6 +9,7 @@
// clears it; a 10px drag does not. Failure is signalled via a non-zero exit code
// (the runner checks exit code AND stdout).
#import "modules/std.sx";
#import "modules/ui/types.sx";
#import "board.sx";
#import "board_layout.sx";
#import "swipe.sx";

View File

@@ -16,6 +16,7 @@
// avoids tests/test.sx (its trace.sx pulls in a second `Frame` that collides with
// the UI one). Failure is a non-zero exit code (the runner checks exit + stdout).
#import "modules/std.sx";
#import "modules/ui/types.sx";
#import "board.sx";
#import "board_anim.sx";
#import "board_layout.sx";
@@ -27,7 +28,7 @@ SEED :: 1337;
// board can be written as a human-readable grid (mirrors tests/level.sx).
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -37,9 +38,9 @@ char_to_gem :: (c: u8) -> Gem {
// 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 {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}

View File

@@ -28,7 +28,7 @@ LIMIT :: 5;
// be written as a human-readable grid. The hole glyph maps to `.empty`.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
for 0..GEM_COUNT (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
.red
@@ -39,9 +39,9 @@ char_to_gem :: (c: u8) -> Gem {
// (no moves made, the given move budget).
load_board :: (rows: []string, seed: s64, move_limit: s64) -> Board {
b : Board = ---;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_ROWS (row) {
line := rows[row];
for 0..BOARD_COLS: (col) {
for 0..BOARD_COLS (col) {
b.set(col, row, char_to_gem(line[col]));
}
}
@@ -53,7 +53,7 @@ load_board :: (rows: []string, seed: s64, move_limit: s64) -> Board {
}
boards_equal :: (a: *Board, b: *Board) -> bool {
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
true
}