P16.2: organic illegal swap — springy bounce-back (+ M3TE_BADSWAP hook)
render_swap's rejected-swap branch now drives the two gems with a P15.1 spring-based bounce-back (bad_swap_bounce): a quick lunge toward the neighbour, then a damped spring home that overshoots rest by a bounded amount and settles to exactly 0. f(0)=f(1)=0, so the move stays purely visual — board byte-identical to pre-swap, no score/move spent. - board_anim.sx: add bad_swap_bounce envelope (lunge via ease_out_cubic, settle via 1 - spring(u)); BADSWAP_LUNGE_T/AMP constants. - board_view.sx: replace the linear ping-out illegal branch with the bounce. - main.sx: add illegal_swaps (complement of legal_swaps, same row-major order) + the startup-only M3TE_BADSWAP=n capture hook; mirrors M3TE_FX. - tests/easing.sx: append bounce-envelope assertions (endpoints, single lunge peak + location, damped settle); regenerate expected snapshot. - README.md: document the M3TE_BADSWAP recipe + goldens/p16_badswap.png. Gate green: ios-sim build links, 22 logic snapshots pass (anim_plan model invariants unchanged; SWAP_ANIM_DUR untouched so cascade-cue snapshots do not churn).
This commit is contained in:
50
main.sx
50
main.sx
@@ -165,6 +165,34 @@ parse_s64 :: (s: string) -> s64 {
|
||||
v
|
||||
}
|
||||
|
||||
// The orthogonally-adjacent pairs that are currently ILLEGAL — the exact
|
||||
// COMPLEMENT of `legal_swaps`, enumerated in the SAME stable row-major order (each
|
||||
// cell's right neighbour before its down neighbour, each adjacency visited once).
|
||||
// Drives the M3TE_BADSWAP capture hook, which needs a KNOWN rejected pair on the
|
||||
// fixed seed to screenshot the springy bounce-back. Headless and read-only — the
|
||||
// trial swaps inside `swap_legal` are reverted, so the board is left unchanged.
|
||||
illegal_swaps :: (board: *Board) -> List(Swap) {
|
||||
result := List(Swap).{};
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
for 0..BOARD_COLS: (col) {
|
||||
here := Cell.{ col = col, row = row };
|
||||
if col + 1 < BOARD_COLS {
|
||||
right := Cell.{ col = col + 1, row = row };
|
||||
if !swap_legal(board, here, right) {
|
||||
result.append(Swap.{ a = here, b = right });
|
||||
}
|
||||
}
|
||||
if row + 1 < BOARD_ROWS {
|
||||
down := Cell.{ col = col, row = row + 1 };
|
||||
if !swap_legal(board, here, down) {
|
||||
result.append(Swap.{ a = here, b = down });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
frame :: () {
|
||||
fc := g_plat.begin_frame();
|
||||
g_delta_time = fc.delta_time;
|
||||
@@ -376,6 +404,28 @@ main :: () -> void {
|
||||
}
|
||||
}
|
||||
|
||||
// Illegal-swap bounce capture hook (P16.2). The springy bounce-back plays only
|
||||
// for a REJECTED swap, which the sim can't script (no public touch injection),
|
||||
// so M3TE_BADSWAP forces one at startup the way a swipe would. It commits the
|
||||
// n-th currently-ILLEGAL orthogonally-adjacent pair — the complement of
|
||||
// legal_swaps, enumerated in the SAME row-major order, 1-based + clamped — via
|
||||
// the normal plan_and_commit (which reverts an illegal swap: zero rounds, board
|
||||
// byte-identical, no score/move spent), then begins the move timeline and ticks
|
||||
// it to M3TE_ANIM_TIME so the swap-segment bounce screenshots identically. No FX
|
||||
// begins — a rejected swap clears nothing. Startup-only and unset → fully live.
|
||||
if bs := read_env("M3TE_BADSWAP") {
|
||||
bad := illegal_swaps(g_board);
|
||||
if bad.len > 0 {
|
||||
n := parse_s64(bs);
|
||||
if n < 1 { n = 1; }
|
||||
if n > bad.len { n = bad.len; }
|
||||
sw := bad.items[n - 1];
|
||||
mv := plan_and_commit(g_board, sw.a, sw.b);
|
||||
g_anim.begin(mv);
|
||||
g_anim.tick(g_motion.clock);
|
||||
}
|
||||
}
|
||||
|
||||
// Level-state capture hooks (P7.2): override the goal / move budget so a
|
||||
// terminal status can be screenshot without scripting a swipe. M3TE_TARGET=0
|
||||
// makes the fresh board read WON immediately (score 0 ≥ goal 0);
|
||||
|
||||
Reference in New Issue
Block a user