// Easing-toolkit math foundation (P15.1): pin the pure, headless easing curves in // board_anim.sx that the organic-animation pass (P16/P17/P18) builds on. NO render // code calls these yet, so this test is the only consumer — it locks, for each // curve: the endpoints f(0)/f(1) (and f(0.5) where it's a fixed point), the // overshoot/undershoot range (bounded + tasteful), and monotonicity where the // curve must not reverse. Mirrors how tests/gem_pose.sx pins the gem poses; prints // only booleans (no raw floats) so the snapshot is platform-stable. // // P16.2 will APPEND illegal-swap bounce-back assertions here: add a new numbered // section above the final fails check, in the same `print(...); if !x { fails += 1; }` // shape. No rendering — pure math over board_anim.sx. Failure is a non-zero exit. #import "modules/std.sx"; #import "board.sx"; #import "board_anim.sx"; // Local f32 abs (the stdlib generic `abs` mis-types its untyped `0` literal under // f32; the shipped game never calls abs, so the tests roll their own — matches // tests/gem_pose.sx). 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 } main :: () -> s32 { fails : s64 = 0; // 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). print("== endpoints locked ==\n"); e_in := ease_in_cubic(0.0) == 0.0 and ease_in_cubic(1.0) == 1.0; e_io := ease_in_out_cubic(0.0) == 0.0 and ease_in_out_cubic(1.0) == 1.0 and ease_in_out_cubic(0.5) == 0.5; e_back := ease_out_back(0.0) == 0.0 and ease_out_back(1.0) == 1.0; e_spring := spring(0.0) == 0.0 and spring(1.0) == 1.0; e_squash := squash_envelope(0.0) == 0.0 and squash_envelope(1.0) == 0.0; e_exist := ease_out_cubic(0.0) == 0.0 and ease_out_cubic(1.0) == 1.0 and ease_in_quad(0.0) == 0.0 and ease_in_quad(1.0) == 1.0; print("ease_in {} ease_in_out {} back {} spring {} squash {} existing {}\n", e_in, e_io, e_back, e_spring, e_squash, e_exist); if !e_in { fails += 1; } if !e_io { fails += 1; } if !e_back { fails += 1; } if !e_spring { fails += 1; } if !e_squash { fails += 1; } if !e_exist { fails += 1; } // 2. Monotonicity where required: the four plain eases never reverse over a // fine sweep of [0,1] (the overshoot/spring/squash curves are exempt — they // are meant to reverse). print("== monotonic where required ==\n"); mono_in := true; mono_io := true; mono_oc := true; mono_iq := true; p_in := ease_in_cubic(0.0); p_io := ease_in_out_cubic(0.0); p_oc := ease_out_cubic(0.0); p_iq := ease_in_quad(0.0); for 1..21: (i) { t := cast(f32) i / 20.0; v_in := ease_in_cubic(t); if v_in < p_in - 0.000001 { mono_in = false; } p_in = v_in; v_io := ease_in_out_cubic(t); if v_io < p_io - 0.000001 { mono_io = false; } p_io = v_io; v_oc := ease_out_cubic(t); if v_oc < p_oc - 0.000001 { mono_oc = false; } p_oc = v_oc; v_iq := ease_in_quad(t); if v_iq < p_iq - 0.000001 { mono_iq = false; } p_iq = v_iq; } print("ease_in {} ease_in_out {} ease_out_cubic {} ease_in_quad {}\n", mono_in, mono_io, mono_oc, mono_iq); if !mono_in { fails += 1; } if !mono_io { fails += 1; } if !mono_oc { fails += 1; } if !mono_iq { fails += 1; } // 3. Back/overshoot + spring: each shoots above 1 then settles to exactly 1, // with a BOUNDED peak (tasteful) and no dip below 0. The spring additionally // wobbles back below 1 after its overshoot (the damped decay). print("== overshoot bounded + settles ==\n"); back_mx := ease_out_back(0.0); back_mn := ease_out_back(0.0); spr_mx := spring(0.0); spr_mn := spring(0.0); spr_wobble := false; for 1..21: (i) { t := cast(f32) i / 20.0; b := ease_out_back(t); if b > back_mx { back_mx = b; } if b < back_mn { back_mn = b; } s := spring(t); if s > spr_mx { spr_mx = s; } if s < spr_mn { spr_mn = s; } if t > 0.6 and s < 1.0 { spr_wobble = true; } } back_overshoots := back_mx > 1.0; back_bounded := back_mx < 1.15 and back_mn >= -0.0001; spr_overshoots := spr_mx > 1.0; spr_bounded := spr_mx < 1.25 and spr_mn >= -0.0001; print("back_overshoots {} back_bounded {} spring_overshoots {} spring_bounded {} spring_wobbles {}\n", back_overshoots, back_bounded, spr_overshoots, spr_bounded, spr_wobble); if !back_overshoots { fails += 1; } if !back_bounded { fails += 1; } if !spr_overshoots { fails += 1; } if !spr_bounded { fails += 1; } if !spr_wobble { fails += 1; } // 4. Squash envelope: rests at both ends, actually moves in between, has both a // squash (positive) and a stretch (negative) lobe, and stays bounded. print("== squash envelope bounded ==\n"); sq_mx : f32 = 0.0; sq_mn : f32 = 0.0; sq_moves := false; for 0..21: (i) { t := cast(f32) i / 20.0; s := squash_envelope(t); if s > sq_mx { sq_mx = s; } if s < sq_mn { sq_mn = s; } if fabs(s) > 0.01 { sq_moves = true; } } sq_two_sided := sq_mx > 0.0 and sq_mn < 0.0; sq_bounded := sq_mx < 0.75 and sq_mn > -0.75; print("squash_moves {} squash_two_sided {} squash_bounded {}\n", sq_moves, sq_two_sided, sq_bounded); if !sq_moves { fails += 1; } if !sq_two_sided { fails += 1; } if !sq_bounded { fails += 1; } // 5. Illegal-swap bounce-back (P16.2): the springy lunge-and-settle render_swap // plays for a REJECTED swap. Lock its envelope end to end — rests at BOTH // ends (f(0)=f(1)=0, so the move stays purely visual), a SINGLE lunge peak of // exactly BADSWAP_LUNGE_AMP at BADSWAP_LUNGE_T, then a damped settle that // overshoots past rest by a BOUNDED amount and leaves NO residual at t=1. print("== illegal-swap bounce ==\n"); bb_ends := bad_swap_bounce(0.0) == 0.0 and bad_swap_bounce(1.0) == 0.0; bb_mx : f32 = 0.0; bb_mx_t : f32 = 0.0; bb_mn : f32 = 0.0; for 0..101: (i) { t := cast(f32) i / 100.0; v := bad_swap_bounce(t); if v > bb_mx { bb_mx = v; bb_mx_t = t; } if v < bb_mn { bb_mn = v; } } bb_peak_amp := approx(bb_mx, BADSWAP_LUNGE_AMP); bb_peak_loc := fabs(bb_mx_t - BADSWAP_LUNGE_T) < 0.011; bb_overshoots := bb_mn < -0.01; // springs PAST rest after the peak bb_overshoot_bounded := bb_mn > -0.12; // but the recoil stays tasteful bb_settles := approx(bad_swap_bounce(1.0), 0.0); // no residual displacement print("bounce_ends {} peak_amp {} peak_loc {} overshoots {} overshoot_bounded {} settles {}\n", bb_ends, bb_peak_amp, bb_peak_loc, bb_overshoots, bb_overshoot_bounded, bb_settles); if !bb_ends { fails += 1; } if !bb_peak_amp { fails += 1; } if !bb_peak_loc { fails += 1; } if !bb_overshoots { fails += 1; } if !bb_overshoot_bounded { fails += 1; } if !bb_settles { fails += 1; } if fails == 0 { print("ok: easing toolkit endpoints locked + amplitudes bounded\n"); return 0; } print("FAIL: {} easing checks failed\n", fails); return 1; }