Compare commits
10 Commits
2a196943aa
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d1012806 | ||
|
|
39740a1d36 | ||
|
|
bb728d0ab0 | ||
|
|
6f7d2f4db2 | ||
|
|
1ab74c7d08 | ||
|
|
38815c7d50 | ||
|
|
a7b41ccbca | ||
|
|
5a0627bb7c | ||
|
|
69e2c1f50d | ||
|
|
cd89a5c9c0 |
29
.vscode/ios-sim-debug.sh
vendored
Executable file
29
.vscode/ios-sim-debug.sh
vendored
Executable 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
27
.vscode/launch.json
vendored
Normal 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
23
.vscode/tasks.json
vendored
Normal 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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
audio.sx
18
audio.sx
@@ -10,17 +10,17 @@
|
|||||||
// other targets never reference these symbols nor need the frameworks.
|
// other targets never reference these symbols nor need the frameworks.
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/std/objc.sx";
|
#import "modules/ffi/objc.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/build.sx";
|
||||||
|
|
||||||
// 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(); } }
|
||||||
|
|||||||
181
board.sx
181
board.sx
@@ -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,14 +149,14 @@ 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;
|
||||||
self.move_limit = DEFAULT_MOVE_LIMIT;
|
self.move_limit = DEFAULT_MOVE_LIMIT;
|
||||||
self.target_score = DEFAULT_TARGET_SCORE;
|
self.target_score = DEFAULT_TARGET_SCORE;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
self.set(col, row, pick_gem(self, @self.rng, col, row));
|
self.set(col, row, pick_gem(self, @self.rng, col, row));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,31 +165,31 @@ 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; }
|
||||||
|
|
||||||
// Two same gems immediately to the left → a third of that type matches.
|
// Two same gems immediately to the left → a third of that type matches.
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
allowed := 0;
|
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.
|
// Pick the k-th still-allowed type; single RNG draw, always terminates.
|
||||||
k := rng.next_range(allowed);
|
k := rng.next_range(allowed);
|
||||||
for 0..GEM_COUNT: (t) {
|
for 0..GEM_COUNT (t) {
|
||||||
if !forbidden[t] {
|
if !forbidden[t] {
|
||||||
if k == 0 { return cast(Gem) t; }
|
if k == 0 { return cast(Gem) t; }
|
||||||
k -= 1;
|
k -= 1;
|
||||||
@@ -202,10 +202,10 @@ pick_gem :: (board: *Board, rng: *Rng, col: s64, row: s64) -> Gem {
|
|||||||
// single gem character per cell. Suitable for snapshotting.
|
// single gem character per cell. Suitable for snapshotting.
|
||||||
board_dump :: (self: *Board) -> string {
|
board_dump :: (self: *Board) -> string {
|
||||||
line_w := BOARD_COLS + 1; // 8 gem chars + newline
|
line_w := BOARD_COLS + 1; // 8 gem chars + newline
|
||||||
buf := cstring(BOARD_ROWS * line_w);
|
buf := alloc_string(BOARD_ROWS * line_w);
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
base := row * line_w;
|
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 + col] = gem_char(self.at(col, row));
|
||||||
}
|
}
|
||||||
buf[base + BOARD_COLS] = 10; // '\n'
|
buf[base + BOARD_COLS] = 10; // '\n'
|
||||||
@@ -223,13 +223,13 @@ 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,8 +237,8 @@ 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;
|
||||||
} else {
|
} 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.
|
// break runs of real gems, since a hole differs from every gem type.
|
||||||
find_matches :: (b: *Board) -> MatchMask {
|
find_matches :: (b: *Board) -> MatchMask {
|
||||||
m : 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.
|
// Horizontal: walk each row left-to-right in maximal same-type spans.
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
col := 0;
|
col := 0;
|
||||||
while col < BOARD_COLS {
|
while col < BOARD_COLS {
|
||||||
g := b.at(col, row);
|
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.
|
// Vertical: walk each column top-to-bottom in maximal same-type spans.
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
row := 0;
|
row := 0;
|
||||||
while row < BOARD_ROWS {
|
while row < BOARD_ROWS {
|
||||||
g := b.at(col, row);
|
g := b.at(col, row);
|
||||||
@@ -298,10 +298,10 @@ find_matches :: (b: *Board) -> MatchMask {
|
|||||||
// unambiguously as the empty set. Suitable for snapshotting.
|
// unambiguously as the empty set. Suitable for snapshotting.
|
||||||
dump_matches :: (b: *Board, m: *MatchMask) -> string {
|
dump_matches :: (b: *Board, m: *MatchMask) -> string {
|
||||||
line_w := BOARD_COLS + 1; // 8 cells + newline
|
line_w := BOARD_COLS + 1; // 8 cells + newline
|
||||||
buf := cstring(BOARD_ROWS * line_w);
|
buf := alloc_string(BOARD_ROWS * line_w);
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
base := row * line_w;
|
base := row * line_w;
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
if m.at(col, row) {
|
if m.at(col, row) {
|
||||||
buf[base + col] = gem_char(b.at(col, row));
|
buf[base + col] = gem_char(b.at(col, row));
|
||||||
} else {
|
} else {
|
||||||
@@ -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
|
||||||
@@ -371,8 +371,8 @@ Swap :: struct {
|
|||||||
// the snapshot can depend on it.
|
// the snapshot can depend on it.
|
||||||
legal_swaps :: (board: *Board) -> List(Swap) {
|
legal_swaps :: (board: *Board) -> List(Swap) {
|
||||||
result := List(Swap).{};
|
result := List(Swap).{};
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
here := Cell.{ col = col, row = row };
|
here := Cell.{ col = col, row = row };
|
||||||
if col + 1 < BOARD_COLS {
|
if col + 1 < BOARD_COLS {
|
||||||
right := Cell.{ col = col + 1, row = row };
|
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.
|
// as just "0 legal swaps", which reads unambiguously. Suitable for snapshotting.
|
||||||
dump_swaps :: (swaps: *List(Swap)) -> string {
|
dump_swaps :: (swaps: *List(Swap)) -> string {
|
||||||
result := format("{} legal swaps\n", swaps.len);
|
result := format("{} legal swaps\n", swaps.len);
|
||||||
for 0..swaps.len: (i) {
|
for 0..swaps.len (i) {
|
||||||
s := swaps.items[i];
|
s := swaps.items[i];
|
||||||
result = concat(result, format("({},{})-({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row));
|
result = concat(result, format("({},{})-({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row));
|
||||||
}
|
}
|
||||||
@@ -413,9 +413,9 @@ 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;
|
||||||
cleared += 1;
|
cleared += 1;
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -449,7 +449,7 @@ clear_matches :: (board: *Board) -> s64 {
|
|||||||
// this to know when gravity has stopped.
|
// this to know when gravity has stopped.
|
||||||
collapse :: (board: *Board) -> bool {
|
collapse :: (board: *Board) -> bool {
|
||||||
moved := false;
|
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
|
// 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
|
// 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`
|
// bottom. A gem whose source row differs from `w` actually fell. `w`
|
||||||
@@ -489,11 +489,11 @@ 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 {
|
||||||
board.set(col, row, cast(Gem) rng.next_range(GEM_COUNT));
|
board.set(col, row, cast(Gem) rng.next_range(GEM_COUNT));
|
||||||
filled += 1;
|
filled += 1;
|
||||||
@@ -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
|
||||||
@@ -628,7 +628,7 @@ run_score :: (len: s64) -> s64 {
|
|||||||
find_runs :: (b: *Board) -> List(Run) {
|
find_runs :: (b: *Board) -> List(Run) {
|
||||||
runs := List(Run).{};
|
runs := List(Run).{};
|
||||||
|
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
col := 0;
|
col := 0;
|
||||||
while col < BOARD_COLS {
|
while col < BOARD_COLS {
|
||||||
g := b.at(col, row);
|
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;
|
row := 0;
|
||||||
while row < BOARD_ROWS {
|
while row < BOARD_ROWS {
|
||||||
g := b.at(col, row);
|
g := b.at(col, row);
|
||||||
@@ -666,10 +666,10 @@ 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);
|
||||||
}
|
}
|
||||||
total
|
total
|
||||||
@@ -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
|
||||||
@@ -724,7 +724,7 @@ SpecialCounts :: struct {
|
|||||||
count_specials :: (board: *Board) -> SpecialCounts {
|
count_specials :: (board: *Board) -> SpecialCounts {
|
||||||
runs := find_runs(board);
|
runs := find_runs(board);
|
||||||
counts := SpecialCounts.{ len4 = 0, len5_plus = 0 };
|
counts := SpecialCounts.{ len4 = 0, len5_plus = 0 };
|
||||||
for 0..runs.len: (i) {
|
for 0..runs.len (i) {
|
||||||
len := runs.items[i].len;
|
len := runs.items[i].len;
|
||||||
if len == 4 {
|
if len == 4 {
|
||||||
counts.len4 += 1;
|
counts.len4 += 1;
|
||||||
@@ -741,7 +741,7 @@ count_specials :: (board: *Board) -> SpecialCounts {
|
|||||||
// "0 runs". Suitable for snapshotting.
|
// "0 runs". Suitable for snapshotting.
|
||||||
dump_runs :: (runs: *List(Run)) -> string {
|
dump_runs :: (runs: *List(Run)) -> string {
|
||||||
result := format("{} runs\n", runs.len);
|
result := format("{} runs\n", runs.len);
|
||||||
for 0..runs.len: (i) {
|
for 0..runs.len (i) {
|
||||||
r := runs.items[i];
|
r := runs.items[i];
|
||||||
axis := if r.vertical then "V" else "H";
|
axis := if r.vertical then "V" else "H";
|
||||||
result = concat(result, format("{} len {} at fixed {} start {}\n", axis, r.len, r.fixed, r.start));
|
result = concat(result, format("{} len {} at fixed {} start {}\n", axis, r.len, r.fixed, r.start));
|
||||||
@@ -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);
|
||||||
@@ -829,8 +829,8 @@ level_status :: (board: *Board) -> Status {
|
|||||||
// a throwaway list each call. The trial swaps inside `swap_legal` are reverted,
|
// a throwaway list each call. The trial swaps inside `swap_legal` are reverted,
|
||||||
// so the board is left unchanged.
|
// so the board is left unchanged.
|
||||||
has_legal_swap :: (board: *Board) -> bool {
|
has_legal_swap :: (board: *Board) -> bool {
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
here := Cell.{ col = col, row = row };
|
here := Cell.{ col = col, row = row };
|
||||||
if col + 1 < BOARD_COLS {
|
if col + 1 < BOARD_COLS {
|
||||||
right := Cell.{ col = col + 1, row = row };
|
right := Cell.{ col = col + 1, row = row };
|
||||||
@@ -882,11 +882,25 @@ reshuffle :: (board: *Board) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After a committed move's cascade has settled, recover a deadlocked board so the
|
||||||
|
// player is never stranded: if the level is still in progress yet no legal swap
|
||||||
|
// remains, `reshuffle` the gems in place. A reshuffle is NOT a move and never runs
|
||||||
|
// on a finished (won/lost) level, so win/lose and turn accounting are untouched.
|
||||||
|
// Returns whether a reshuffle ran. BOTH the headless turn loop (`play_turn`) and
|
||||||
|
// the animated UI commit (`plan_and_commit`) call this, so the rendered game obeys
|
||||||
|
// the identical no-moves rule — neither path can leave the board stuck.
|
||||||
|
reshuffle_if_deadlocked :: (board: *Board) -> bool {
|
||||||
|
if level_status(board) == .in_progress and !has_legal_swap(board) {
|
||||||
|
return reshuffle(board);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
// Reset to a fresh, reproducible level: `init(seed)` reseeds the board (same
|
// Reset to a fresh, reproducible level: `init(seed)` reseeds the board (same
|
||||||
// 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 :: ufcs (board: *Board, seed: i64) {
|
||||||
board.init(seed);
|
board.init(seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -916,14 +930,11 @@ 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 };
|
||||||
}
|
}
|
||||||
move := commit_swap(board, a, b);
|
move := commit_swap(board, a, b);
|
||||||
reshuffled := false;
|
reshuffled := reshuffle_if_deadlocked(board);
|
||||||
if level_status(board) == .in_progress and !has_legal_swap(board) {
|
|
||||||
reshuffled = reshuffle(board);
|
|
||||||
}
|
|
||||||
TurnResult.{ accepted = true, move = move, status = level_status(board), reshuffled = reshuffled }
|
TurnResult.{ accepted = true, move = move, status = level_status(board), reshuffled = reshuffled }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// Board motion animation (P6.1) — a PURELY VISUAL timeline the view plays over
|
// Board motion animation (P6.1) — a PURELY VISUAL timeline the view plays over
|
||||||
// one player move. The logical model (commit_swap / resolve) stays authoritative:
|
// one player move. The logical model (commit_swap / resolve) stays authoritative:
|
||||||
// `plan_and_commit` commits the move on the real board exactly as before, then
|
// `plan_and_commit` commits the move on the real board (and, like the headless
|
||||||
// replays the SAME operations on a value-copy of the pre-move board to record the
|
// turn loop, reshuffles a deadlocked board afterwards), then replays the SAME
|
||||||
// per-step geometry (the swap, each cascade round's matched cells, and each
|
// commit operations on a value-copy of the pre-move board to record the per-step
|
||||||
// round's per-column fall provenance). Because the copy starts from the identical
|
// geometry (the swap, each cascade round's matched cells, and each round's
|
||||||
// cells AND RNG state and runs the identical primitives, its recorded `final`
|
// per-column fall provenance). Because the copy starts from the identical cells
|
||||||
// board equals the model's settled board gem-for-gem — the animation only ever
|
// AND RNG state and runs the identical primitives, its recorded `final` board
|
||||||
// ends ON the already-decided result, never changes it.
|
// equals the move's settled (pre-reshuffle) board gem-for-gem — the animation only
|
||||||
|
// ever ends ON the already-decided cascade result, never changes it.
|
||||||
//
|
//
|
||||||
// Per-gem idle/select/clear gem animations (P6.3) and score popups / particle FX
|
// Per-gem idle/select/clear gem animations (P6.3) and score popups / particle FX
|
||||||
// (P6.2) are NOT here; this step animates board MOTION only: swap slide, matched
|
// (P6.2) are NOT here; this step animates board MOTION only: swap slide, matched
|
||||||
@@ -108,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;
|
||||||
@@ -122,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,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
|
||||||
}
|
}
|
||||||
@@ -158,11 +159,11 @@ 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);
|
||||||
if d < lo { lo = d; }
|
if d < lo { lo = d; }
|
||||||
@@ -177,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)
|
||||||
}
|
}
|
||||||
@@ -192,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,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
|
||||||
@@ -220,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 {
|
||||||
@@ -231,9 +232,13 @@ delivering_round :: (mv: *AnimMove, i: s64, kmax: s64) -> s64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Commit the player's swap authoritatively AND record its visual timeline. The
|
// Commit the player's swap authoritatively AND record its visual timeline. The
|
||||||
// real board is mutated by `commit_swap` exactly as the non-animated path did;
|
// real board is mutated by `commit_swap`, then — exactly like the headless
|
||||||
// the recording runs on a separate value-copy taken BEFORE the commit, so it
|
// `play_turn` — `reshuffle_if_deadlocked` recovers a stranded board so the rendered
|
||||||
// replays the identical cells + RNG stream and its `final` equals `board.cells`.
|
// game obeys the same no-moves rule. The recording runs on a value-copy taken
|
||||||
|
// BEFORE the commit, so it replays the identical cells + RNG stream; the recorded
|
||||||
|
// `final` is the SETTLED board the animation ends on. It equals the live board
|
||||||
|
// unless a deadlock reshuffle then re-arranged it: that reshuffle is a model step,
|
||||||
|
// not part of this move's timeline, so it renders on the next settled frame.
|
||||||
plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
|
plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
|
||||||
move : AnimMove = ---;
|
move : AnimMove = ---;
|
||||||
move.a = a;
|
move.a = a;
|
||||||
@@ -251,6 +256,7 @@ plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
|
|||||||
move.awarded = mv.cascade.awarded;
|
move.awarded = mv.cascade.awarded;
|
||||||
if !mv.legal {
|
if !mv.legal {
|
||||||
move.final = board.cells;
|
move.final = board.cells;
|
||||||
|
reshuffle_if_deadlocked(board);
|
||||||
return move;
|
return move;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,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
|
// 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
|
// 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.
|
// `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;
|
w := BOARD_ROWS - 1;
|
||||||
r := BOARD_ROWS - 1;
|
r := BOARD_ROWS - 1;
|
||||||
while r >= 0 {
|
while r >= 0 {
|
||||||
@@ -296,6 +302,7 @@ plan_and_commit :: (board: *Board, a: Cell, b: Cell) -> AnimMove {
|
|||||||
}
|
}
|
||||||
|
|
||||||
move.final = scratch.cells;
|
move.final = scratch.cells;
|
||||||
|
reshuffle_if_deadlocked(board);
|
||||||
move
|
move
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,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;
|
||||||
@@ -355,7 +362,7 @@ BoardAnim :: struct {
|
|||||||
return AnimPhase.{ kind = .swap, round = 0, t = e / SWAP_ANIM_DUR };
|
return AnimPhase.{ kind = .swap, round = 0, t = e / SWAP_ANIM_DUR };
|
||||||
}
|
}
|
||||||
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 {
|
if e < CLEAR_ANIM_DUR {
|
||||||
return AnimPhase.{ kind = .clear, round = k, t = e / CLEAR_ANIM_DUR };
|
return AnimPhase.{ kind = .clear, round = k, t = e / CLEAR_ANIM_DUR };
|
||||||
}
|
}
|
||||||
@@ -376,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
|
||||||
}
|
}
|
||||||
|
|||||||
56
board_fx.sx
56
board_fx.sx
@@ -12,8 +12,8 @@
|
|||||||
// shrink to nothing) rather than alpha — the soft texture edges carry the fade.
|
// shrink to nothing) rather than alpha — the soft texture edges carry the fade.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/ffi/opengl.sx";
|
||||||
#import "modules/stb.sx";
|
#import "vendors/stb_image/stb_image.sx";
|
||||||
#import "modules/gpu/types.sx";
|
#import "modules/gpu/types.sx";
|
||||||
#import "modules/gpu/api.sx";
|
#import "modules/gpu/api.sx";
|
||||||
#import "modules/ui/types.sx";
|
#import "modules/ui/types.sx";
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -123,28 +123,28 @@ BoardFxAssets :: struct {
|
|||||||
loaded: bool;
|
loaded: bool;
|
||||||
|
|
||||||
init :: (self: *BoardFxAssets) {
|
init :: (self: *BoardFxAssets) {
|
||||||
for 0..GEM_COUNT: (t) { self.tex[t] = 0; }
|
for 0..GEM_COUNT (t) { self.tex[t] = 0; }
|
||||||
self.loaded = false;
|
self.loaded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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(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;
|
||||||
while i < n {
|
while i < n {
|
||||||
@@ -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;
|
||||||
@@ -219,7 +219,7 @@ BoardFx :: struct {
|
|||||||
// Whole-move depth boost: a deeper cascade makes every burst bigger from
|
// Whole-move depth boost: a deeper cascade makes every burst bigger from
|
||||||
// its first round, escalating in lockstep with the cascade SFX cue.
|
// its first round, escalating in lockstep with the cascade SFX cue.
|
||||||
depth_boost := FX_BURST_DEPTH * cast(f32) fx_combo_level(mv.rounds.len);
|
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];
|
rd := @mv.rounds.items[k];
|
||||||
t0 := SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR);
|
t0 := SWAP_ANIM_DUR + cast(f32) k * (CLEAR_ANIM_DUR + FALL_ANIM_DUR);
|
||||||
extra := depth_boost + FX_BURST_COMBO * cast(f32) min(k, 2);
|
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
|
// 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.
|
// one simultaneous flash. The round's audio cue still fires once at t0.
|
||||||
span := clear_diag_span(@rd.matched);
|
span := clear_diag_span(@rd.matched);
|
||||||
for 0..BOARD_CELLS: (idx) {
|
for 0..BOARD_CELLS (idx) {
|
||||||
if rd.matched.cells[idx] {
|
if rd.matched.cells[idx] {
|
||||||
g := rd.before[idx];
|
g := rd.before[idx];
|
||||||
if g != .empty {
|
if g != .empty {
|
||||||
@@ -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,10 +250,10 @@ 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;
|
||||||
sr += idx / BOARD_COLS;
|
sr += 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;
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
// the grid is a centered square inside the safe-area inset.
|
// the grid is a centered square inside the safe-area inset.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/ffi/opengl.sx";
|
||||||
#import "modules/stb.sx";
|
#import "vendors/stb_image/stb_image.sx";
|
||||||
#import "modules/gpu/types.sx";
|
#import "modules/gpu/types.sx";
|
||||||
#import "modules/gpu/api.sx";
|
#import "modules/gpu/api.sx";
|
||||||
#import "modules/ui/types.sx";
|
#import "modules/ui/types.sx";
|
||||||
@@ -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;
|
||||||
@@ -357,13 +357,13 @@ BoardView :: struct {
|
|||||||
// Settled-board gems: one sprite per non-empty cell, drawn with its live
|
// Settled-board gems: one sprite per non-empty cell, drawn with its live
|
||||||
// per-gem animation pose. Used whenever no move is animating.
|
// per-gem animation pose. Used whenever no move is animating.
|
||||||
render_gems :: (self: *BoardView, ctx: *RenderContext, dim: f32) {
|
render_gems :: (self: *BoardView, ctx: *RenderContext, dim: f32) {
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
g := self.board.at(col, row);
|
g := self.board.at(col, row);
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,12 +431,12 @@ BoardView :: struct {
|
|||||||
// (which resumes the same back-dated stamp). tick() normally clears
|
// (which resumes the same back-dated stamp). tick() normally clears
|
||||||
// `active` before this is reached.
|
// `active` before this is reached.
|
||||||
last := mv.rounds.len - 1;
|
last := mv.rounds.len - 1;
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
g := mv.final[i];
|
g := mv.final[i];
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,12 +452,12 @@ BoardView :: struct {
|
|||||||
ai := Board.idx(mv.a.col, mv.a.row);
|
ai := Board.idx(mv.a.col, mv.a.row);
|
||||||
bi := Board.idx(mv.b.col, mv.b.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; }
|
if i == ai or i == bi { continue; }
|
||||||
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,20 +482,20 @@ 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];
|
||||||
if g == .empty { continue; }
|
if g == .empty { continue; }
|
||||||
col := i % BOARD_COLS;
|
col := i % BOARD_COLS;
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -532,7 +532,7 @@ BoardView :: struct {
|
|||||||
cs * cast(f32) BOARD_COLS, cs * cast(f32) BOARD_ROWS
|
cs * cast(f32) BOARD_COLS, cs * cast(f32) BOARD_ROWS
|
||||||
);
|
);
|
||||||
ctx.push_clip(grid);
|
ctx.push_clip(grid);
|
||||||
for 0..self.fx.particles.len: (i) {
|
for 0..self.fx.particles.len (i) {
|
||||||
p := self.fx.particles.items[i];
|
p := self.fx.particles.items[i];
|
||||||
lt := (p.age - p.delay) / p.life;
|
lt := (p.age - p.delay) / p.life;
|
||||||
env := fx_pop_env(lt);
|
env := fx_pop_env(lt);
|
||||||
@@ -556,7 +556,7 @@ BoardView :: struct {
|
|||||||
render_fx_popups :: (self: *BoardView, ctx: *RenderContext) {
|
render_fx_popups :: (self: *BoardView, ctx: *RenderContext) {
|
||||||
if self.fx == null or self.fx.popups.len == 0 { return; }
|
if self.fx == null or self.fx.popups.len == 0 { return; }
|
||||||
cs := self.layout.cell_size;
|
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];
|
q := self.fx.popups.items[i];
|
||||||
lt := (q.age - q.delay) / q.life;
|
lt := (q.age - q.delay) / q.life;
|
||||||
if lt >= 0.0 {
|
if lt >= 0.0 {
|
||||||
@@ -593,8 +593,8 @@ 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; }
|
||||||
col := i % BOARD_COLS;
|
col := i % BOARD_COLS;
|
||||||
@@ -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;
|
||||||
@@ -731,8 +731,8 @@ impl View for BoardView {
|
|||||||
gem_inset := self.layout.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
|
gem_inset := self.layout.cell_size * (1.0 - GEM_FILL_FRAC) * 0.5;
|
||||||
gem_dim := self.layout.cell_size * GEM_FILL_FRAC;
|
gem_dim := self.layout.cell_size * GEM_FILL_FRAC;
|
||||||
if self.assets.cell_tex != 0 {
|
if self.assets.cell_tex != 0 {
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
ctx.add_image(self.layout.cell_frame(col, row), self.assets.cell_tex);
|
ctx.add_image(self.layout.cell_frame(col, row), self.assets.cell_tex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
build.sx
2
build.sx
@@ -1,4 +1,4 @@
|
|||||||
#import "modules/compiler.sx";
|
#import "modules/build.sx";
|
||||||
#import "modules/platform/bundle.sx";
|
#import "modules/platform/bundle.sx";
|
||||||
|
|
||||||
configure_build :: () {
|
configure_build :: () {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
12
gem_anim.sx
12
gem_anim.sx
@@ -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;
|
||||||
@@ -132,10 +132,10 @@ GemMotion :: struct {
|
|||||||
// its resting pose instead of replaying the prior move's landing wobble; the
|
// 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.
|
// idle clock keeps running, so the always-on idle simply resumes from rest.
|
||||||
reset_landings :: (self: *GemMotion) {
|
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) {
|
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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
main.sx
97
main.sx
@@ -1,11 +1,13 @@
|
|||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "build.sx";
|
#import "build.sx";
|
||||||
#import "modules/compiler.sx";
|
#import "modules/build.sx";
|
||||||
#import "modules/opengl.sx";
|
#import "modules/ffi/opengl.sx";
|
||||||
#import "modules/sdl3.sx";
|
#import "modules/ffi/sdl3.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "modules/stb.sx";
|
#import "vendors/stb_image/stb_image.sx";
|
||||||
#import "modules/stb_truetype.sx";
|
#import "vendors/stb_truetype/stb_truetype.sx";
|
||||||
|
#import "vendors/kb_text_shape/kb_text_shape.sx";
|
||||||
|
#import "vendors/file_utils/file_utils.sx";
|
||||||
#import "modules/gpu/api.sx";
|
#import "modules/gpu/api.sx";
|
||||||
#import "modules/gpu/types.sx";
|
#import "modules/gpu/types.sx";
|
||||||
#import "modules/gpu/metal.sx";
|
#import "modules/gpu/metal.sx";
|
||||||
@@ -24,8 +26,6 @@
|
|||||||
|
|
||||||
// libc is the implicit foreign-library handle the std allocators bind against;
|
// libc is the implicit foreign-library handle the std allocators bind against;
|
||||||
// reused here to read the deterministic-capture environment variables at startup.
|
// reused here to read the deterministic-capture environment variables at startup.
|
||||||
getenv :: (name: [:0]u8) -> *u8 #foreign libc "getenv";
|
|
||||||
strlen :: (s: *u8) -> usize #foreign libc "strlen";
|
|
||||||
|
|
||||||
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
|
// Fixed seed for the rendered board — the same seed tests/board_init.sx locks
|
||||||
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
|
// as a snapshot, so the on-screen layout matches that golden gem-for-gem.
|
||||||
@@ -116,41 +116,34 @@ build_ui :: () -> View {
|
|||||||
// M3TE_SELECT=<cellIndex 0..63> forces a selection so the select-pop reaction can
|
// M3TE_SELECT=<cellIndex 0..63> forces a selection so the select-pop reaction can
|
||||||
// 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);
|
process.env(name)
|
||||||
addr : s64 = xx p;
|
|
||||||
if addr == 0 { return null; }
|
|
||||||
n := cast(s64) strlen(p);
|
|
||||||
if n == 0 { return ""; }
|
|
||||||
buf := cstring(n);
|
|
||||||
memcpy(buf.ptr, xx p, n);
|
|
||||||
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 +156,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;
|
||||||
@@ -183,8 +176,8 @@ parse_s64 :: (s: string) -> s64 {
|
|||||||
// trial swaps inside `swap_legal` are reverted, so the board is left unchanged.
|
// trial swaps inside `swap_legal` are reverted, so the board is left unchanged.
|
||||||
illegal_swaps :: (board: *Board) -> List(Swap) {
|
illegal_swaps :: (board: *Board) -> List(Swap) {
|
||||||
result := List(Swap).{};
|
result := List(Swap).{};
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
here := Cell.{ col = col, row = row };
|
here := Cell.{ col = col, row = row };
|
||||||
if col + 1 < BOARD_COLS {
|
if col + 1 < BOARD_COLS {
|
||||||
right := Cell.{ col = col + 1, row = row };
|
right := Cell.{ col = col + 1, row = row };
|
||||||
@@ -222,7 +215,7 @@ frame :: () {
|
|||||||
g_pipeline.resize(fc.viewport_w, fc.viewport_h);
|
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 {
|
inline if OS != .ios {
|
||||||
if ev == {
|
if ev == {
|
||||||
case .key_up: (e) {
|
case .key_up: (e) {
|
||||||
@@ -277,7 +270,7 @@ frame :: () {
|
|||||||
mv := @g_anim.move;
|
mv := @g_anim.move;
|
||||||
total := g_anim.total();
|
total := g_anim.total();
|
||||||
last := mv.rounds.len - 1;
|
last := mv.rounds.len - 1;
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
m := delivering_round(mv, i, last);
|
m := delivering_round(mv, i, last);
|
||||||
if m >= 0 {
|
if m >= 0 {
|
||||||
col := i % BOARD_COLS;
|
col := i % BOARD_COLS;
|
||||||
@@ -327,7 +320,7 @@ frame :: () {
|
|||||||
|
|
||||||
main :: () -> void {
|
main :: () -> void {
|
||||||
inline if OS == .ios {
|
inline if OS == .ios {
|
||||||
u : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform));
|
u : *UIKitPlatform = xx context.allocator.alloc_bytes(size_of(UIKitPlatform));
|
||||||
u.gpu_mode = .metal;
|
u.gpu_mode = .metal;
|
||||||
if !u.init("m3te", 800, 600) { return; }
|
if !u.init("m3te", 800, 600) { return; }
|
||||||
g_plat = xx u;
|
g_plat = xx u;
|
||||||
@@ -337,13 +330,13 @@ main :: () -> void {
|
|||||||
// return into UIApplicationMain, so attach lazily on the first frame.
|
// return into UIApplicationMain, so attach lazily on the first frame.
|
||||||
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
|
// init(null, 0, 0) only needs the MTLDevice, which is enough for the
|
||||||
// texture uploads below.
|
// texture uploads below.
|
||||||
g_metal_gpu = xx context.allocator.alloc(size_of(MetalGPU));
|
g_metal_gpu = xx context.allocator.alloc_bytes(size_of(MetalGPU));
|
||||||
// alloc returns uninitialized memory; struct field defaults are NOT
|
// alloc returns uninitialized memory; struct field defaults are NOT
|
||||||
// applied, so List caps/lens would be garbage without this memset.
|
// applied, so List caps/lens would be garbage without this memset.
|
||||||
memset(xx g_metal_gpu, 0, size_of(MetalGPU));
|
memset(xx g_metal_gpu, 0, size_of(MetalGPU));
|
||||||
if !g_metal_gpu.init(null, 0, 0) { return; }
|
if !g_metal_gpu.init(null, 0, 0) { return; }
|
||||||
} else {
|
} else {
|
||||||
s : *SdlPlatform = xx context.allocator.alloc(size_of(SdlPlatform));
|
s : *SdlPlatform = xx context.allocator.alloc_bytes(size_of(SdlPlatform));
|
||||||
if !s.init("m3te", 800, 600) { return; }
|
if !s.init("m3te", 800, 600) { return; }
|
||||||
g_plat = xx s;
|
g_plat = xx s;
|
||||||
}
|
}
|
||||||
@@ -353,7 +346,7 @@ main :: () -> void {
|
|||||||
g_viewport_h = fc.viewport_h;
|
g_viewport_h = fc.viewport_h;
|
||||||
g_safe_insets = g_plat.safe_insets();
|
g_safe_insets = g_plat.safe_insets();
|
||||||
|
|
||||||
g_pipeline = xx context.allocator.alloc(size_of(UIPipeline));
|
g_pipeline = xx context.allocator.alloc_bytes(size_of(UIPipeline));
|
||||||
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
|
// Same alloc caveat as above: zero so the optional `gpu` reads as null on
|
||||||
// the desktop path (where set_gpu is not called) and the Lists start empty.
|
// the desktop path (where set_gpu is not called) and the Lists start empty.
|
||||||
memset(xx g_pipeline, 0, size_of(UIPipeline));
|
memset(xx g_pipeline, 0, size_of(UIPipeline));
|
||||||
@@ -363,37 +356,37 @@ main :: () -> void {
|
|||||||
g_pipeline.init(fc.viewport_w, fc.viewport_h);
|
g_pipeline.init(fc.viewport_w, fc.viewport_h);
|
||||||
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
|
g_pipeline.init_font("assets/fonts/default.ttf", 32.0, fc.dpi_scale);
|
||||||
|
|
||||||
g_board = xx context.allocator.alloc(size_of(Board));
|
g_board = xx context.allocator.alloc_bytes(size_of(Board));
|
||||||
g_board.init(BOARD_SEED);
|
g_board.init(BOARD_SEED);
|
||||||
|
|
||||||
g_assets = xx context.allocator.alloc(size_of(BoardAssets));
|
g_assets = xx context.allocator.alloc_bytes(size_of(BoardAssets));
|
||||||
g_assets.init();
|
g_assets.init();
|
||||||
g_assets.load(g_pipeline.gpu);
|
g_assets.load(g_pipeline.gpu);
|
||||||
|
|
||||||
g_sel = xx context.allocator.alloc(size_of(BoardSelection));
|
g_sel = xx context.allocator.alloc_bytes(size_of(BoardSelection));
|
||||||
g_sel.init();
|
g_sel.init();
|
||||||
|
|
||||||
g_drag = xx context.allocator.alloc(size_of(DragInput));
|
g_drag = xx context.allocator.alloc_bytes(size_of(DragInput));
|
||||||
g_drag.init();
|
g_drag.init();
|
||||||
|
|
||||||
g_anim = xx context.allocator.alloc(size_of(BoardAnim));
|
g_anim = xx context.allocator.alloc_bytes(size_of(BoardAnim));
|
||||||
g_anim.init();
|
g_anim.init();
|
||||||
|
|
||||||
g_fx = xx context.allocator.alloc(size_of(BoardFx));
|
g_fx = xx context.allocator.alloc_bytes(size_of(BoardFx));
|
||||||
g_fx.init();
|
g_fx.init();
|
||||||
|
|
||||||
g_fxassets = xx context.allocator.alloc(size_of(BoardFxAssets));
|
g_fxassets = xx context.allocator.alloc_bytes(size_of(BoardFxAssets));
|
||||||
g_fxassets.init();
|
g_fxassets.init();
|
||||||
g_fxassets.load(g_pipeline.gpu);
|
g_fxassets.load(g_pipeline.gpu);
|
||||||
|
|
||||||
g_motion = xx context.allocator.alloc(size_of(GemMotion));
|
g_motion = xx context.allocator.alloc_bytes(size_of(GemMotion));
|
||||||
g_motion.init();
|
g_motion.init();
|
||||||
|
|
||||||
// SFX (P10.2). Loads the System Sound Services cue bank once; board_view
|
// SFX (P10.2). Loads the System Sound Services cue bank once; board_view
|
||||||
// plays a cue per event. Purely additive — never touches score/board/move
|
// plays a cue per event. Purely additive — never touches score/board/move
|
||||||
// state. On iOS the platform has already chdir'd to the bundle, so each
|
// state. On iOS the platform has already chdir'd to the bundle, so each
|
||||||
// cue's relative path resolves. No-op off iOS.
|
// cue's relative path resolves. No-op off iOS.
|
||||||
g_audio = xx context.allocator.alloc(size_of(GameAudio));
|
g_audio = xx context.allocator.alloc_bytes(size_of(GameAudio));
|
||||||
memset(xx g_audio, 0, size_of(GameAudio));
|
memset(xx g_audio, 0, size_of(GameAudio));
|
||||||
g_audio.init();
|
g_audio.init();
|
||||||
|
|
||||||
@@ -405,7 +398,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 +411,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 +426,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 +450,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 +466,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));
|
||||||
|
|||||||
@@ -24,14 +24,14 @@
|
|||||||
SEED :: 1337;
|
SEED :: 1337;
|
||||||
|
|
||||||
boards_equal :: (x: *Board, y: *Board) -> bool {
|
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; }
|
if !(x.cells[i] == y.cells[i]) { return false; }
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -61,7 +61,7 @@ main :: () -> s32 {
|
|||||||
|
|
||||||
// move.final equals the model board.
|
// move.final equals the model board.
|
||||||
final_eq := true;
|
final_eq := true;
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
if !(move.final[i] == bm.cells[i]) { final_eq = false; }
|
if !(move.final[i] == bm.cells[i]) { final_eq = false; }
|
||||||
}
|
}
|
||||||
if !final_eq { fails += 1; }
|
if !final_eq { fails += 1; }
|
||||||
@@ -74,21 +74,21 @@ main :: () -> s32 {
|
|||||||
ai := Board.idx(a.col, a.row);
|
ai := Board.idx(a.col, a.row);
|
||||||
bi := Board.idx(b.col, b.row);
|
bi := Board.idx(b.col, b.row);
|
||||||
r0 := @move.rounds.items[0];
|
r0 := @move.rounds.items[0];
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
expect : Gem = move.pre[i];
|
expect : Gem = move.pre[i];
|
||||||
if i == ai { expect = move.pre[bi]; }
|
if i == ai { expect = move.pre[bi]; }
|
||||||
else if i == bi { expect = move.pre[ai]; }
|
else if i == bi { expect = move.pre[ai]; }
|
||||||
if !(r0.before[i] == expect) { contiguous = false; }
|
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];
|
prev := @move.rounds.items[k - 1];
|
||||||
cur := @move.rounds.items[k];
|
cur := @move.rounds.items[k];
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
if !(cur.before[i] == prev.after[i]) { contiguous = false; }
|
if !(cur.before[i] == prev.after[i]) { contiguous = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last := @move.rounds.items[move.rounds.len - 1];
|
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; }
|
if !(last.after[i] == move.final[i]) { contiguous = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -13,16 +13,17 @@
|
|||||||
// shape and rationale as tests/hit_test.sx. Failure is signalled via a non-zero
|
// shape and rationale as tests/hit_test.sx. Failure is signalled via a non-zero
|
||||||
// exit code (the runner checks exit code AND stdout).
|
// exit code (the runner checks exit code AND stdout).
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
#import "modules/ui/types.sx";
|
||||||
#import "board.sx";
|
#import "board.sx";
|
||||||
#import "board_layout.sx";
|
#import "board_layout.sx";
|
||||||
|
|
||||||
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 = ---;
|
||||||
@@ -35,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; }
|
||||||
@@ -66,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; }
|
||||||
|
|||||||
@@ -10,16 +10,16 @@ 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);
|
||||||
if g == b.at(col + 1, row) and g == b.at(col + 2, row) { runs += 1; }
|
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_ROWS - 2) (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
g := b.at(col, row);
|
g := b.at(col, row);
|
||||||
if g == b.at(col, row + 1) and g == b.at(col, row + 2) { runs += 1; }
|
if g == b.at(col, row + 1) and g == b.at(col, row + 2) { runs += 1; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ EXPECTED_DEPTH :: 2;
|
|||||||
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -34,9 +34,9 @@ char_to_gem :: (c: u8) -> Gem {
|
|||||||
// The RNG is left unseeded — callers seed it before resolving.
|
// The RNG is left unseeded — callers seed it before resolving.
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[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 {
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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; }
|
||||||
@@ -120,7 +120,7 @@ main :: () -> s32 {
|
|||||||
t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
|
t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
|
||||||
same_counts := c.cleared.len == counts.len;
|
same_counts := c.cleared.len == counts.len;
|
||||||
if same_counts {
|
if same_counts {
|
||||||
for 0..counts.len: (i) {
|
for 0..counts.len (i) {
|
||||||
if c.cleared.items[i] != counts.items[i] { same_counts = false; }
|
if c.cleared.items[i] != counts.items[i] { same_counts = false; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@
|
|||||||
#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));
|
||||||
// The mapping must never step down as depth grows.
|
// The mapping must never step down as depth grows.
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -57,7 +57,7 @@ main :: () -> s32 {
|
|||||||
|
|
||||||
// Deep chain: the cue tail clamps at combo5 for round >= 5 (cascade_cue_index).
|
// Deep chain: the cue tail clamps at combo5 for round >= 5 (cascade_cue_index).
|
||||||
print("-- deep-chain cue clamp --\n");
|
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");
|
print("ok: one ascending combo cue per cascade round, clamped at combo5\n");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ t :: #import "test.sx";
|
|||||||
// clear) for the holes-never-match regression.
|
// clear) for the holes-never-match regression.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -26,9 +26,9 @@ char_to_gem :: (c: u8) -> Gem {
|
|||||||
// characters).
|
// characters).
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ scene :: (name: string, rows: []string, want_cleared: s64) {
|
|||||||
|
|
||||||
cleared_holes := true; // every matched cell is now a hole
|
cleared_holes := true; // every matched cell is now a hole
|
||||||
others_intact := true; // every other cell is byte-identical
|
others_intact := true; // every other cell is byte-identical
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
if m.cells[i] {
|
if m.cells[i] {
|
||||||
if !(b.cells[i] == .empty) { cleared_holes = false; }
|
if !(b.cells[i] == .empty) { cleared_holes = false; }
|
||||||
} else {
|
} else {
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ t :: #import "test.sx";
|
|||||||
// maps to `.empty`, so a board can be hand-written with holes in any position.
|
// maps to `.empty`, so a board can be hand-written with holes in any position.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.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 an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[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-
|
// 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.
|
// bottom, order-preservation, and the all-holes / no-holes edge columns at once.
|
||||||
check_collapsed :: (orig: *Board, b: *Board) -> bool {
|
check_collapsed :: (orig: *Board, b: *Board) -> bool {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
gems : [BOARD_ROWS]Gem = ---;
|
gems : [BOARD_ROWS]Gem = ---;
|
||||||
n := 0;
|
n := 0;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
g := orig.at(col, row);
|
g := orig.at(col, row);
|
||||||
if g != .empty { gems[n] = g; n += 1; }
|
if g != .empty { gems[n] = g; n += 1; }
|
||||||
}
|
}
|
||||||
boundary := BOARD_ROWS - n; // first row that must hold a gem
|
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 row < boundary {
|
||||||
if b.at(col, row) != .empty { return false; }
|
if b.at(col, row) != .empty { return false; }
|
||||||
} else {
|
} else {
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ t :: #import "test.sx";
|
|||||||
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -31,11 +31,11 @@ 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];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
@@ -51,7 +51,7 @@ main :: () -> s32 {
|
|||||||
p_io := ease_in_out_cubic(0.0);
|
p_io := ease_in_out_cubic(0.0);
|
||||||
p_oc := ease_out_cubic(0.0);
|
p_oc := ease_out_cubic(0.0);
|
||||||
p_iq := ease_in_quad(0.0);
|
p_iq := ease_in_quad(0.0);
|
||||||
for 1..21: (i) {
|
for 1..21 (i) {
|
||||||
t := cast(f32) i / 20.0;
|
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_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;
|
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);
|
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_mx := spring(0.0); spr_mn := spring(0.0);
|
||||||
spr_wobble := false;
|
spr_wobble := false;
|
||||||
for 1..21: (i) {
|
for 1..21 (i) {
|
||||||
t := cast(f32) i / 20.0;
|
t := cast(f32) i / 20.0;
|
||||||
b := ease_out_back(t);
|
b := ease_out_back(t);
|
||||||
if b > back_mx { back_mx = b; }
|
if b > back_mx { back_mx = b; }
|
||||||
@@ -98,7 +98,7 @@ main :: () -> s32 {
|
|||||||
// squash (positive) and a stretch (negative) lobe, and stays bounded.
|
// squash (positive) and a stretch (negative) lobe, and stays bounded.
|
||||||
print("== squash envelope bounded ==\n");
|
print("== squash envelope bounded ==\n");
|
||||||
sq_mx : f32 = 0.0; sq_mn : f32 = 0.0; sq_moves := false;
|
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;
|
t := cast(f32) i / 20.0;
|
||||||
s := squash_envelope(t);
|
s := squash_envelope(t);
|
||||||
if s > sq_mx { sq_mx = s; }
|
if s > sq_mx { sq_mx = s; }
|
||||||
@@ -121,7 +121,7 @@ main :: () -> s32 {
|
|||||||
print("== illegal-swap bounce ==\n");
|
print("== illegal-swap bounce ==\n");
|
||||||
bb_ends := bad_swap_bounce(0.0) == 0.0 and bad_swap_bounce(1.0) == 0.0;
|
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;
|
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;
|
t := cast(f32) i / 100.0;
|
||||||
v := bad_swap_bounce(t);
|
v := bad_swap_bounce(t);
|
||||||
if v > bb_mx { bb_mx = v; bb_mx_t = 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.
|
// starts later), the opposite of a flat lockstep row sharing one progress.
|
||||||
print("== fall stagger bounded ==\n");
|
print("== fall stagger bounded ==\n");
|
||||||
stg_t0 := true; stg_t1 := true;
|
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(0.0, c) != 0.0 { stg_t0 = false; }
|
||||||
if fall_stagger_t(1.0, c) != 1.0 { stg_t1 = false; }
|
if fall_stagger_t(1.0, c) != 1.0 { stg_t1 = false; }
|
||||||
}
|
}
|
||||||
stg_cascade := true;
|
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; }
|
if !(fall_stagger_t(0.5, c) < fall_stagger_t(0.5, c - 1)) { stg_cascade = false; }
|
||||||
}
|
}
|
||||||
stg_mono := true;
|
stg_mono := true;
|
||||||
for 0..BOARD_COLS: (c) {
|
for 0..BOARD_COLS (c) {
|
||||||
pp := fall_stagger_t(0.0, c);
|
pp := fall_stagger_t(0.0, c);
|
||||||
for 1..21: (i) {
|
for 1..21 (i) {
|
||||||
tt := cast(f32) i / 20.0;
|
tt := cast(f32) i / 20.0;
|
||||||
vv := fall_stagger_t(tt, c);
|
vv := fall_stagger_t(tt, c);
|
||||||
if vv < pp - 0.000001 { stg_mono = false; }
|
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_last := approx(fall_landing_frac(BOARD_COLS - 1), 1.0);
|
||||||
lf_mono := true;
|
lf_mono := true;
|
||||||
lf_seam := 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; }
|
if c >= 1 and !(fall_landing_frac(c) > fall_landing_frac(c - 1)) { lf_mono = false; }
|
||||||
lf := fall_landing_frac(c);
|
lf := fall_landing_frac(c);
|
||||||
if !approx(fall_stagger_t(lf, c), 1.0) { lf_seam = false; } // landed at lf
|
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
|
if fall_stagger_t(lf - 0.05, c) >= 1.0 { lf_seam = false; } // still in air just before
|
||||||
}
|
}
|
||||||
rlt_col_mono := true;
|
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; }
|
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);
|
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).
|
// gem 0..1 by diagonal across the round (lowest-diagonal = 0, the first to pop).
|
||||||
print("== clear ripple bounded ==\n");
|
print("== clear ripple bounded ==\n");
|
||||||
rip_t0 := true; rip_t1 := true;
|
rip_t0 := true; rip_t1 := true;
|
||||||
for 0..6: (j) {
|
for 0..6 (j) {
|
||||||
u := cast(f32) j / 5.0;
|
u := cast(f32) j / 5.0;
|
||||||
if clear_ripple_t(0.0, u) != 0.0 { rip_t0 = false; }
|
if clear_ripple_t(0.0, u) != 0.0 { rip_t0 = false; }
|
||||||
if clear_ripple_t(1.0, u) != 1.0 { rip_t1 = false; }
|
if clear_ripple_t(1.0, u) != 1.0 { rip_t1 = false; }
|
||||||
}
|
}
|
||||||
rip_ripple := true;
|
rip_ripple := true;
|
||||||
for 1..6: (j) {
|
for 1..6 (j) {
|
||||||
u := cast(f32) j / 5.0;
|
u := cast(f32) j / 5.0;
|
||||||
up := cast(f32) (j - 1) / 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; }
|
if !(clear_ripple_t(0.5, u) < clear_ripple_t(0.5, up)) { rip_ripple = false; }
|
||||||
}
|
}
|
||||||
rip_mono := true;
|
rip_mono := true;
|
||||||
for 0..6: (j) {
|
for 0..6 (j) {
|
||||||
u := cast(f32) j / 5.0;
|
u := cast(f32) j / 5.0;
|
||||||
pp := clear_ripple_t(0.0, u);
|
pp := clear_ripple_t(0.0, u);
|
||||||
for 1..21: (i) {
|
for 1..21 (i) {
|
||||||
tt := cast(f32) i / 20.0;
|
tt := cast(f32) i / 20.0;
|
||||||
vv := clear_ripple_t(tt, u);
|
vv := clear_ripple_t(tt, u);
|
||||||
if vv < pp - 0.000001 { rip_mono = false; }
|
if vv < pp - 0.000001 { rip_mono = false; }
|
||||||
@@ -245,7 +245,7 @@ main :: () -> s32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mm : MatchMask = ---;
|
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, 0)] = true; // diagonal 5 — first to pop
|
||||||
mm.cells[Board.idx(5, 1)] = true; // diagonal 6
|
mm.cells[Board.idx(5, 1)] = true; // diagonal 6
|
||||||
mm.cells[Board.idx(5, 2)] = true; // diagonal 7 — last to pop
|
mm.cells[Board.idx(5, 2)] = true; // diagonal 7 — last to pop
|
||||||
|
|||||||
1
tests/expected/swipe_reshuffle.exit
Normal file
1
tests/expected/swipe_reshuffle.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
23
tests/expected/swipe_reshuffle.stdout
Normal file
23
tests/expected/swipe_reshuffle.stdout
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
== deadlocked board: UI swipe-commit path must reshuffle ==
|
||||||
|
ROYGBPRO
|
||||||
|
PROYGBPR
|
||||||
|
BPROYGBP
|
||||||
|
GBPROYGB
|
||||||
|
YGBPROYG
|
||||||
|
OYGBPROY
|
||||||
|
ROYGBPRO
|
||||||
|
PROYGBPR
|
||||||
|
before: matches 0 legal_swaps 0 has_legal_swap false status in_progress
|
||||||
|
intent (0,0)->(1,0)
|
||||||
|
commit: legal false rounds 0 awarded 0
|
||||||
|
after: matches 0 legal_swaps 9 has_legal_swap true status in_progress
|
||||||
|
after: score 0 moves_made 0 moves_remaining 10
|
||||||
|
BGGYORYR
|
||||||
|
RRYGOPBY
|
||||||
|
YRYBPRGB
|
||||||
|
OOBGBPRG
|
||||||
|
RPRPYRPO
|
||||||
|
OBBPOOPG
|
||||||
|
OBGGOPGY
|
||||||
|
YPRYBORP
|
||||||
|
ok: UI swipe-commit path reshuffles a deadlocked board
|
||||||
@@ -13,15 +13,15 @@
|
|||||||
#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);
|
||||||
combo := depth > 1;
|
combo := depth > 1;
|
||||||
@@ -45,7 +45,7 @@ main :: () -> s32 {
|
|||||||
// larger and the font never shrinks as the cascade deepens.
|
// 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; }
|
if fx_popup_font(1) != FX_POPUP_FONT { print("FAIL: single-clear popup not plain font\n"); return 1; }
|
||||||
pf : f32 = 0.0;
|
pf : f32 = 0.0;
|
||||||
for 2..10: (depth) {
|
for 2..10 (depth) {
|
||||||
f := fx_popup_font(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 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; }
|
if depth > 2 and f < pf { print("FAIL: popup font shrank at depth {}\n", depth); return 1; }
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
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");
|
||||||
rest_ok := true;
|
rest_ok := true;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
p := idle_pose(0.0, col, row);
|
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) {
|
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;
|
rest_ok = false;
|
||||||
@@ -39,8 +39,8 @@ main :: () -> s32 {
|
|||||||
print("== idle mid-phase deforms, bounded ==\n");
|
print("== idle mid-phase deforms, bounded ==\n");
|
||||||
moved := false;
|
moved := false;
|
||||||
bounded := true;
|
bounded := true;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
p := idle_pose(0.6, col, row);
|
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.0005 { moved = true; }
|
||||||
if fabs(p.scale_x - 1.0) > 0.05 { bounded = false; }
|
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_peak := clear_pop_scale(0.30) > 1.1;
|
||||||
c_collapse := true;
|
c_collapse := true;
|
||||||
pc := clear_pop_scale(CLEAR_POP_RISE);
|
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;
|
tt := CLEAR_POP_RISE + (1.0 - CLEAR_POP_RISE) * cast(f32) i / 20.0;
|
||||||
vv := clear_pop_scale(tt);
|
vv := clear_pop_scale(tt);
|
||||||
if vv > pc + 0.000001 { c_collapse = false; }
|
if vv > pc + 0.000001 { c_collapse = false; }
|
||||||
|
|||||||
@@ -10,24 +10,25 @@
|
|||||||
// second `Frame` struct that collides with the UI `Frame`. Failure is signalled
|
// 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).
|
// via a non-zero exit code (the runner checks exit code AND stdout).
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
#import "modules/ui/types.sx";
|
||||||
#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);
|
||||||
center := Point.{ x = cf.mid_x(), y = cf.mid_y() };
|
center := Point.{ x = cf.mid_x(), y = cf.mid_y() };
|
||||||
if h := lay.point_to_cell(center) {
|
if h := lay.point_to_cell(center) {
|
||||||
@@ -41,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);
|
||||||
@@ -52,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); }
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ RESHUFFLE_SEED :: 1337;
|
|||||||
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -34,11 +34,11 @@ 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];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,11 +51,11 @@ load_board :: (rows: []string, seed: s64, move_limit: s64, target_score: s64) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
boards_equal :: (a: *Board, b: *Board) -> bool {
|
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
|
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 ──
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ t :: #import "test.sx";
|
|||||||
// Inverse of `gem_char`: map a gem character back to its Gem so each board can
|
// 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.
|
// be written as a human-readable grid of GEM_CHARS.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -20,11 +20,11 @@ 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];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ SEED :: 1337;
|
|||||||
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -32,23 +32,23 @@ char_to_gem :: (c: u8) -> Gem {
|
|||||||
// The RNG is left unseeded — callers seed it before drawing.
|
// The RNG is left unseeded — callers seed it before drawing.
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
boards_equal :: (a: *Board, b: *Board) -> bool {
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -101,7 +101,7 @@ main :: () -> s32 {
|
|||||||
distinct := false;
|
distinct := false;
|
||||||
have_first := false;
|
have_first := false;
|
||||||
first : Gem = .empty;
|
first : Gem = .empty;
|
||||||
for 0..BOARD_CELLS: (i) {
|
for 0..BOARD_CELLS (i) {
|
||||||
if pre.cells[i] == .empty {
|
if pre.cells[i] == .empty {
|
||||||
want := cast(Gem) v.next_range(GEM_COUNT);
|
want := cast(Gem) v.next_range(GEM_COUNT);
|
||||||
if b.cells[i] != want { stream_ok = false; }
|
if b.cells[i] != want { stream_ok = false; }
|
||||||
@@ -123,19 +123,19 @@ 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 {
|
||||||
hole_idx[holes_n] = i;
|
hole_idx[holes_n] = i;
|
||||||
fill1[holes_n] = b.cells[i];
|
fill1[holes_n] = b.cells[i];
|
||||||
holes_n += 1;
|
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);
|
refill(@b);
|
||||||
differs := false;
|
differs := false;
|
||||||
for 0..holes_n: (k) {
|
for 0..holes_n (k) {
|
||||||
if b.cells[hole_idx[k]] != fill1[k] { differs = true; }
|
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)");
|
t.expect(differs, "refill: a second refill of the same holes draws new gems (RNG threads, no reseed)");
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ t :: #import "test.sx";
|
|||||||
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.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.
|
// with the running score zeroed so the accumulation check starts from a known base.
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ SEED :: 1337;
|
|||||||
// Inverse of `gem_char`: map a gem character back to its Gem so each board can
|
// 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.
|
// be written as a human-readable grid of GEM_CHARS.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -26,9 +26,9 @@ char_to_gem :: (c: u8) -> Gem {
|
|||||||
// characters).
|
// characters).
|
||||||
load_board :: (rows: []string) -> Board {
|
load_board :: (rows: []string) -> Board {
|
||||||
b : Board = ---;
|
b : Board = ---;
|
||||||
for 0..BOARD_ROWS: (row) {
|
for 0..BOARD_ROWS (row) {
|
||||||
line := rows[row];
|
line := rows[row];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,17 +38,17 @@ load_board :: (rows: []string) -> Board {
|
|||||||
// Whole-board equality, cell by cell — used to prove a trial swap leaves the
|
// Whole-board equality, cell by cell — used to prove a trial swap leaves the
|
||||||
// board untouched.
|
// board untouched.
|
||||||
boards_equal :: (x: *Board, y: *Board) -> bool {
|
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; }
|
if !(x.cells[i] == y.cells[i]) { return false; }
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
@@ -13,32 +13,33 @@
|
|||||||
// its trace.sx pulls in a second `Frame` that collides with the UI one. Failure
|
// 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).
|
// is signalled via a non-zero exit code (the runner checks exit code AND stdout).
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
#import "modules/ui/types.sx";
|
||||||
#import "board.sx";
|
#import "board.sx";
|
||||||
#import "board_layout.sx";
|
#import "board_layout.sx";
|
||||||
#import "swipe.sx";
|
#import "swipe.sx";
|
||||||
|
|
||||||
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() }
|
||||||
}
|
}
|
||||||
|
|
||||||
boards_equal :: (x: *Board, y: *Board) -> bool {
|
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; }
|
if !(x.cells[i] == y.cells[i]) { return false; }
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|||||||
@@ -9,18 +9,19 @@
|
|||||||
// clears it; a 10px drag does not. Failure is signalled via a non-zero exit code
|
// clears it; a 10px drag does not. Failure is signalled via a non-zero exit code
|
||||||
// (the runner checks exit code AND stdout).
|
// (the runner checks exit code AND stdout).
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
#import "modules/ui/types.sx";
|
||||||
#import "board.sx";
|
#import "board.sx";
|
||||||
#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;
|
||||||
@@ -38,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);
|
||||||
|
|||||||
144
tests/swipe_reshuffle.sx
Normal file
144
tests/swipe_reshuffle.sx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// UI deadlock-recovery golden (final-review F1): prove the RENDERED swipe-commit
|
||||||
|
// path reshuffles a deadlocked board, just like the headless turn loop's no-moves
|
||||||
|
// rule (tests/level.sx). The iOS/macOS view commits a swipe through
|
||||||
|
// `plan_and_commit` (NOT `play_turn`), so the reshuffle must live on THAT shared
|
||||||
|
// path or a stuck board stays stuck on screen. This test drives exactly what
|
||||||
|
// BoardView.handle_event does — resolve a drag with `swipe_intent`, feed the intent
|
||||||
|
// into `plan_and_commit` — on the provably deadlocked diagonal-Latin-square board
|
||||||
|
// from tests/level.sx, asserting:
|
||||||
|
// - BEFORE: no immediate match, zero legal swaps, status in_progress (stuck);
|
||||||
|
// - AFTER the UI commit path resolves: the board RESHUFFLED — `has_legal_swap`
|
||||||
|
// is true and >=1 legal swap exists, still with no immediate match — and the
|
||||||
|
// reshuffle itself spent no move and no score (turn accounting unchanged).
|
||||||
|
// Pre-fix `plan_and_commit` skipped the reshuffle, so `has_legal_swap` stayed false
|
||||||
|
// after and this FAILS; with the shared `reshuffle_if_deadlocked` wired in it PASSES.
|
||||||
|
// No rendering, no model reach-around. Links headless like tests/anim_plan.sx;
|
||||||
|
// 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";
|
||||||
|
#import "swipe.sx";
|
||||||
|
|
||||||
|
SEED :: 1337;
|
||||||
|
|
||||||
|
// Inverse of `gem_char`: map a board character back to its Gem so the deadlocked
|
||||||
|
// 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) {
|
||||||
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
|
}
|
||||||
|
.red
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
load_board :: (rows: []string, seed: i64, move_limit: i64, target_score: i64) -> Board {
|
||||||
|
b : Board = ---;
|
||||||
|
for 0..BOARD_ROWS (row) {
|
||||||
|
line := rows[row];
|
||||||
|
for 0..BOARD_COLS (col) {
|
||||||
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.rng = rng_seeded(seed);
|
||||||
|
b.score = 0;
|
||||||
|
b.moves_made = 0;
|
||||||
|
b.move_limit = move_limit;
|
||||||
|
b.target_score = target_score;
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
cell_center :: (lay: *BoardLayout, col: i64, row: i64) -> Point {
|
||||||
|
cf := lay.cell_frame(col, row);
|
||||||
|
Point.{ x = cf.mid_x(), y = cf.mid_y() }
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
fails : i64 = 0;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// SAME layout/threshold tests/swipe_commit.sx drives the UI path through.
|
||||||
|
lay : BoardLayout = ---;
|
||||||
|
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
|
||||||
|
D : f32 = 60.0;
|
||||||
|
|
||||||
|
// The provably deadlocked board from tests/level.sx: gem(col,row)=(col-row) mod
|
||||||
|
// 6, a diagonal Latin square. Equal gems lie only on slope-1 diagonals, so no
|
||||||
|
// row/column holds two of a kind within reach — no immediate match and no
|
||||||
|
// orthogonal swap can line up three.
|
||||||
|
print("== deadlocked board: UI swipe-commit path must reshuffle ==\n");
|
||||||
|
b := load_board(.[
|
||||||
|
"ROYGBPRO",
|
||||||
|
"PROYGBPR",
|
||||||
|
"BPROYGBP",
|
||||||
|
"GBPROYGB",
|
||||||
|
"YGBPROYG",
|
||||||
|
"OYGBPROY",
|
||||||
|
"ROYGBPRO",
|
||||||
|
"PROYGBPR",
|
||||||
|
], SEED, 10, 1500);
|
||||||
|
out(board_dump(@b));
|
||||||
|
|
||||||
|
pre_m := find_matches(@b);
|
||||||
|
pre_sw := legal_swaps(@b);
|
||||||
|
print("before: matches {} legal_swaps {} has_legal_swap {} status {}\n",
|
||||||
|
pre_m.count(), pre_sw.len, has_legal_swap(@b), status_name(level_status(@b)));
|
||||||
|
if pre_m.count() != 0 { fails += 1; }
|
||||||
|
if pre_sw.len != 0 { fails += 1; }
|
||||||
|
if has_legal_swap(@b) { fails += 1; }
|
||||||
|
if level_status(@b) != .in_progress { fails += 1; }
|
||||||
|
|
||||||
|
score_before := b.score;
|
||||||
|
made_before := b.moves_made;
|
||||||
|
remain_before := b.moves_remaining();
|
||||||
|
|
||||||
|
// Drive the EXACT UI path: a rightward drag on (0,0) resolves to the (0,0)->(1,0)
|
||||||
|
// swap intent, which BoardView.handle_event feeds straight into plan_and_commit.
|
||||||
|
// On this deadlocked board every swap is illegal (no match), so the swipe itself
|
||||||
|
// commits nothing — the recovery must come from the post-settle reshuffle.
|
||||||
|
a0 := cell_center(@lay, 0, 0);
|
||||||
|
if s := swipe_intent(@lay, a0, Point.{ x = a0.x + D, y = a0.y }) {
|
||||||
|
print("intent ({},{})->({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row);
|
||||||
|
if !(s.a.col == 0 and s.a.row == 0 and s.b.col == 1 and s.b.row == 0) { fails += 1; }
|
||||||
|
mv := plan_and_commit(@b, s.a, s.b);
|
||||||
|
print("commit: legal {} rounds {} awarded {}\n", mv.legal, mv.rounds.len, mv.awarded);
|
||||||
|
// The illegal swipe spends nothing; its timeline is the bare ping-back.
|
||||||
|
if mv.legal { fails += 1; }
|
||||||
|
if mv.rounds.len != 0 { fails += 1; }
|
||||||
|
} else {
|
||||||
|
print("intent none\n");
|
||||||
|
fails += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
post_m := find_matches(@b);
|
||||||
|
post_sw := legal_swaps(@b);
|
||||||
|
print("after: matches {} legal_swaps {} has_legal_swap {} status {}\n",
|
||||||
|
post_m.count(), post_sw.len, has_legal_swap(@b), status_name(level_status(@b)));
|
||||||
|
print("after: score {} moves_made {} moves_remaining {}\n",
|
||||||
|
b.score, b.moves_made, b.moves_remaining());
|
||||||
|
out(board_dump(@b));
|
||||||
|
|
||||||
|
// The fix: the UI commit path reshuffled the deadlock away. has_legal_swap flips
|
||||||
|
// false -> true; >=1 legal swap exists; still no immediate match.
|
||||||
|
if !has_legal_swap(@b) { fails += 1; }
|
||||||
|
if post_sw.len <= 0 { fails += 1; }
|
||||||
|
if post_m.count() != 0 { fails += 1; }
|
||||||
|
|
||||||
|
// The reshuffle is NOT a move: score, moves spent, and budget are untouched.
|
||||||
|
if b.score != score_before { fails += 1; }
|
||||||
|
if b.moves_made != made_before { fails += 1; }
|
||||||
|
if b.moves_remaining() != remain_before { fails += 1; }
|
||||||
|
if level_status(@b) != .in_progress { fails += 1; }
|
||||||
|
|
||||||
|
if fails == 0 {
|
||||||
|
print("ok: UI swipe-commit path reshuffles a deadlocked board\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
print("FAIL: {} UI-reshuffle checks failed\n", fails);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
// terminates the process NON-ZERO (exit 1) via process.exit, so a broken
|
// terminates the process NON-ZERO (exit 1) via process.exit, so a broken
|
||||||
// assertion fails `tools/run_tests.sh` and the build gate.
|
// assertion fails `tools/run_tests.sh` and the build gate.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
proc :: #import "modules/process.sx";
|
proc :: #import "modules/std/process.sx";
|
||||||
|
|
||||||
expect :: (cond: bool, msg: string, loc: Source_Location = #caller_location) {
|
expect :: (cond: bool, msg: string, loc: Source_Location = #caller_location) {
|
||||||
if !cond {
|
if !cond {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ LIMIT :: 5;
|
|||||||
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
// be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
char_to_gem :: (c: u8) -> Gem {
|
char_to_gem :: (c: u8) -> Gem {
|
||||||
if c == EMPTY_CHAR { return .empty; }
|
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; }
|
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||||
}
|
}
|
||||||
.red
|
.red
|
||||||
@@ -37,11 +37,11 @@ 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];
|
||||||
for 0..BOARD_COLS: (col) {
|
for 0..BOARD_COLS (col) {
|
||||||
b.set(col, row, char_to_gem(line[col]));
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,14 +53,14 @@ load_board :: (rows: []string, seed: s64, move_limit: s64) -> Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boards_equal :: (a: *Board, b: *Board) -> bool {
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) ──────────────────────
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
// never hits this — its loops run over 64 board cells, not millions of pixels.
|
// never hits this — its loops run over 64 board cells, not millions of pixels.
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
#import "modules/stb.sx";
|
#import "vendors/stb_image/stb_image.sx";
|
||||||
|
|
||||||
SRC_PATH :: "/Users/agra/Downloads/m3te_particle.png";
|
SRC_PATH :: "/Users/agra/Downloads/m3te_particle.png";
|
||||||
OUT_PATH :: "assets/fx/particle.png";
|
OUT_PATH :: "assets/fx/particle.png";
|
||||||
@@ -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(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;
|
||||||
@@ -102,10 +102,10 @@ main :: () -> s32 {
|
|||||||
|
|
||||||
// 8-connected flood fill of the edge-connected checker, seeded from every
|
// 8-connected flood fill of the edge-connected checker, seeded from every
|
||||||
// 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(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(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];
|
||||||
@@ -153,9 +153,9 @@ main :: () -> s32 {
|
|||||||
// checker shade up to pure white, giving the glow its smooth falloff.
|
// checker shade up to pure white, giving the glow its smooth falloff.
|
||||||
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(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 {
|
||||||
@@ -174,30 +174,30 @@ main :: () -> s32 {
|
|||||||
|
|
||||||
// Area-averaged downscale to OUT_DIM. RGB stays white; only the averaged
|
// Area-averaged downscale to OUT_DIM. RGB stays white; only the averaged
|
||||||
// alpha carries the sprite, so no premultiply is needed (white*cov == white).
|
// alpha carries the sprite, so no premultiply is needed (white*cov == white).
|
||||||
out_px : [*]u8 = xx context.allocator.alloc(OUT_DIM * OUT_DIM * 4);
|
out_px : [*]u8 = xx context.allocator.alloc_bytes(OUT_DIM * OUT_DIM * 4);
|
||||||
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;
|
||||||
|
|||||||
55
vendors/file_utils/file_utils.c
vendored
55
vendors/file_utils/file_utils.c
vendored
@@ -1,55 +0,0 @@
|
|||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
|
||||||
#include <android/asset_manager.h>
|
|
||||||
|
|
||||||
// Caller-installed AAssetManager pointer. Chess's android_main extracts
|
|
||||||
// it from `app->activity->assetManager` (via sx-side platform module's
|
|
||||||
// `g_android_asset_manager` global) and feeds it here once at startup.
|
|
||||||
// Until the setter has been called, Android falls through to fopen —
|
|
||||||
// gives a predictable "file not found" rather than a NULL-deref.
|
|
||||||
static AAssetManager* g_aam = NULL;
|
|
||||||
|
|
||||||
void sx_android_set_asset_manager(void* m) {
|
|
||||||
g_aam = (AAssetManager*)m;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
unsigned char* read_file_bytes(const char* path, int* out_size) {
|
|
||||||
#ifdef __ANDROID__
|
|
||||||
if (g_aam != NULL) {
|
|
||||||
// AAssetManager paths are relative to the APK's `assets/`
|
|
||||||
// directory. Strip a leading "assets/" so callers can use the
|
|
||||||
// same paths across iOS/macOS/Android (those platforms read
|
|
||||||
// assets via `assets/...` rooted in the bundle or CWD).
|
|
||||||
const char* lookup = path;
|
|
||||||
if (strncmp(path, "assets/", 7) == 0) {
|
|
||||||
lookup = path + 7;
|
|
||||||
}
|
|
||||||
AAsset* a = AAssetManager_open(g_aam, lookup, AASSET_MODE_BUFFER);
|
|
||||||
if (a != NULL) {
|
|
||||||
off_t n = AAsset_getLength(a);
|
|
||||||
*out_size = (int)n;
|
|
||||||
unsigned char* buf = (unsigned char*)malloc((size_t)n);
|
|
||||||
if (buf != NULL) {
|
|
||||||
memcpy(buf, AAsset_getBuffer(a), (size_t)n);
|
|
||||||
}
|
|
||||||
AAsset_close(a);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
// Falls through to fopen — useful when assets land in the data
|
|
||||||
// dir via extraction or app updates.
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
FILE* f = fopen(path, "rb");
|
|
||||||
if (!f) return 0;
|
|
||||||
fseek(f, 0, SEEK_END);
|
|
||||||
*out_size = (int)ftell(f);
|
|
||||||
fseek(f, 0, SEEK_SET);
|
|
||||||
unsigned char* buf = (unsigned char*)malloc(*out_size);
|
|
||||||
fread(buf, 1, *out_size, f);
|
|
||||||
fclose(f);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
13
vendors/file_utils/file_utils.h
vendored
13
vendors/file_utils/file_utils.h
vendored
@@ -1,13 +0,0 @@
|
|||||||
#ifndef FILE_UTILS_H
|
|
||||||
#define FILE_UTILS_H
|
|
||||||
|
|
||||||
unsigned char* read_file_bytes(const char* path, int* out_size);
|
|
||||||
|
|
||||||
#ifdef __ANDROID__
|
|
||||||
// Install the AAssetManager that `read_file_bytes` consults for paths
|
|
||||||
// rooted inside the APK. Caller is responsible for passing the manager
|
|
||||||
// from `ANativeActivity->assetManager` before any read_file_bytes call.
|
|
||||||
void sx_android_set_asset_manager(void* m);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
||||||
19
vendors/kb_text_shape/kb/LICENSE
vendored
19
vendors/kb_text_shape/kb/LICENSE
vendored
@@ -1,19 +0,0 @@
|
|||||||
zlib License
|
|
||||||
|
|
||||||
(C) Copyright 2024-2025 Jimmy Lefevre
|
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied
|
|
||||||
warranty. In no event will the authors be held liable for any damages
|
|
||||||
arising from the use of this software.
|
|
||||||
|
|
||||||
Permission is granted to anyone to use this software for any purpose,
|
|
||||||
including commercial applications, and to alter it and redistribute it
|
|
||||||
freely, subject to the following restrictions:
|
|
||||||
|
|
||||||
1. The origin of this software must not be misrepresented; you must not
|
|
||||||
claim that you wrote the original software. If you use this software
|
|
||||||
in a product, an acknowledgment in the product documentation would be
|
|
||||||
appreciated but is not required.
|
|
||||||
2. Altered source versions must be plainly marked as such, and must not be
|
|
||||||
misrepresented as being the original software.
|
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
|
||||||
30737
vendors/kb_text_shape/kb/kb_text_shape.h
vendored
30737
vendors/kb_text_shape/kb/kb_text_shape.h
vendored
File diff suppressed because it is too large
Load Diff
2
vendors/kb_text_shape/kb_text_shape_impl.c
vendored
2
vendors/kb_text_shape/kb_text_shape_impl.c
vendored
@@ -1,2 +0,0 @@
|
|||||||
#define KB_TEXT_SHAPE_IMPLEMENTATION
|
|
||||||
#include "kb/kb_text_shape.h"
|
|
||||||
15
vendors/kb_text_shape/kbts_api.h
vendored
15
vendors/kb_text_shape/kbts_api.h
vendored
@@ -1,15 +0,0 @@
|
|||||||
// Minimal API declarations for SX import.
|
|
||||||
// Only the functions/types we actually use — avoids parsing the full 30k-line header.
|
|
||||||
|
|
||||||
typedef struct kbts_shape_context kbts_shape_context;
|
|
||||||
typedef struct kbts_font kbts_font;
|
|
||||||
|
|
||||||
kbts_shape_context *kbts_CreateShapeContext(void *Allocator, void *AllocatorData);
|
|
||||||
void kbts_DestroyShapeContext(kbts_shape_context *Context);
|
|
||||||
kbts_font *kbts_ShapePushFontFromMemory(kbts_shape_context *Context, void *Memory, int Size, int FontIndex);
|
|
||||||
void kbts_GetFontInfo2(kbts_font *Font, void *Info);
|
|
||||||
void kbts_ShapeBegin(kbts_shape_context *Context, unsigned int ParagraphDirection, unsigned int Language);
|
|
||||||
void kbts_ShapeUtf8(kbts_shape_context *Context, const char *Utf8, int Length, unsigned int UserIdGenerationMode);
|
|
||||||
void kbts_ShapeEnd(kbts_shape_context *Context);
|
|
||||||
int kbts_ShapeRun(kbts_shape_context *Context, void *Run);
|
|
||||||
int kbts_GlyphIteratorNext(void *It, void **Glyph);
|
|
||||||
7988
vendors/stb_image/stb_image.h
vendored
7988
vendors/stb_image/stb_image.h
vendored
File diff suppressed because it is too large
Load Diff
2
vendors/stb_image/stb_image_impl.c
vendored
2
vendors/stb_image/stb_image_impl.c
vendored
@@ -1,2 +0,0 @@
|
|||||||
#define STB_IMAGE_IMPLEMENTATION
|
|
||||||
#include "stb_image.h"
|
|
||||||
1724
vendors/stb_image/stb_image_write.h
vendored
1724
vendors/stb_image/stb_image_write.h
vendored
File diff suppressed because it is too large
Load Diff
2
vendors/stb_image/stb_image_write_impl.c
vendored
2
vendors/stb_image/stb_image_write_impl.c
vendored
@@ -1,2 +0,0 @@
|
|||||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
||||||
#include "stb_image_write.h"
|
|
||||||
5079
vendors/stb_truetype/stb_truetype.h
vendored
5079
vendors/stb_truetype/stb_truetype.h
vendored
File diff suppressed because it is too large
Load Diff
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
2
vendors/stb_truetype/stb_truetype_impl.c
vendored
@@ -1,2 +0,0 @@
|
|||||||
#define STB_TRUETYPE_IMPLEMENTATION
|
|
||||||
#include "stb_truetype.h"
|
|
||||||
Reference in New Issue
Block a user