// Native unit tests for the scoring state machine. // g++ -std=c++17 -O2 -o test test_scoring_logic.cpp scoring_logic.cpp && ./test // // Each test drives the box at a 10 us tick, which is the real scan period of // the F401 front end, and asserts on the lamps once the lockout resolves. #include "scoring_logic.h" #include #include #include static int passes = 0, failures = 0; static void check(const char* name, bool ok, const char* detail = "") { printf("%-6s %-52s %s\n", ok ? "PASS" : "FAIL", name, detail); ok ? ++passes : ++failures; } static const uint32_t TICK = 10; // us per scan, matches the hardware // Run the box for `dur_us`, calling shape() to build each frame. struct Sim { ScoringBox box; uint32_t t = 0; Lights last{}; void run(uint32_t dur_us, std::function shape) { uint32_t end = t + dur_us; while (t < end) { Frame f; f.a.clear(); f.b.clear(); f.t_us = t; shape(f, t); last = box.update(f); t += TICK; } } }; // ---------------------------------------------------------------- foil static void foil_contact_too_short() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); uint32_t start = s.t; s.run(13000, [](Frame& f, uint32_t) { f.a.toOppLame = true; }); // 13 ms s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil 13 ms on-target contact is rejected", !s.last.onA && !s.last.signalled); (void)start; } static void foil_contact_long_enough() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); s.run(16000, [](Frame& f, uint32_t) { f.a.toOppLame = true; }); // 16 ms s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil 16 ms on-target contact registers", s.last.onA && s.last.signalled); } static void foil_floor_hit_suppressed() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); s.run(80000, [](Frame& f, uint32_t) { f.a.toPiste = true; }); // long floor hit s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil floor hit produces no light at all", !s.last.onA && !s.last.offA && !s.last.signalled); } static void foil_off_target() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); s.run(20000, [](Frame& f, uint32_t) { f.a.floating = true; }); s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil point on insulating cloth gives off target", s.last.offA && !s.last.onA); } static void foil_guard_hit_suppressed() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); s.run(80000, [](Frame& f, uint32_t) { f.a.toOppGround = true; }); s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil hit on opponent guard produces no light", !s.last.onA && !s.last.offA); } static void foil_cloth_slide_to_lame() { Sim s; s.box.setWeapon(FOIL); s.run(50000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); // Tip lands on insulating cloth for 8 ms (under the 14 ms impact time), // then slides onto the lamé and stays 16 ms: the off-target candidate // must restart the clock, and only ON target may light. s.run(8000, [](Frame& f, uint32_t) { f.a.floating = true; }); s.run(16000, [](Frame& f, uint32_t) { f.a.toOppLame = true; }); s.run(400000, [](Frame& f, uint32_t) { f.a.toOwnGround = true; }); check("foil 8 ms cloth then 16 ms lamé gives ON target only", s.last.onA && !s.last.offA); } // ---------------------------------------------------------------- epee static void epee_contact_too_short() { Sim s; s.box.setWeapon(EPEE); s.run(50000, [](Frame&, uint32_t) {}); s.run(1000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; }); // 1 ms s.run(80000, [](Frame&, uint32_t) {}); check("epee 1 ms contact is rejected", !s.last.onA); } static void epee_contact_long_enough() { Sim s; s.box.setWeapon(EPEE); s.run(50000, [](Frame&, uint32_t) {}); s.run(4000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; }); // 4 ms s.run(80000, [](Frame&, uint32_t) {}); check("epee 4 ms contact registers", s.last.onA && s.last.signalled); } static void epee_floor_hit_suppressed() { Sim s; s.box.setWeapon(EPEE); s.run(50000, [](Frame&, uint32_t) {}); // Point closes against the metal piste: own lamé joined AND piste joined. s.run(60000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; f.a.toPiste = true; }); s.run(80000, [](Frame&, uint32_t) {}); check("epee floor hit produces no light <-- the big one", !s.last.onA && !s.last.signalled); } static void epee_double_inside_lockout() { Sim s; s.box.setWeapon(EPEE); s.run(50000, [](Frame&, uint32_t) {}); s.run(4000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; }); s.run(30000, [](Frame& f, uint32_t) { f.b.toOwnLame = true; }); // B 30 ms later s.run(80000, [](Frame&, uint32_t) {}); check("epee second hit 30 ms later gives a double", s.last.onA && s.last.onB); } static void epee_single_outside_lockout() { Sim s; s.box.setWeapon(EPEE); s.run(50000, [](Frame&, uint32_t) {}); s.run(4000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; }); s.run(60000, [](Frame&, uint32_t) {}); // past 45 ms s.run(20000, [](Frame& f, uint32_t) { f.b.toOwnLame = true; }); s.run(80000, [](Frame&, uint32_t) {}); check("epee second hit 60 ms later is locked out", s.last.onA && !s.last.onB); } // --------------------------------------------------------------- sabre // FIE reality: the sabre socket's two contacts are shorted together, so B and // C are commoned at BOTH weapons. On healthy equipment toOwnGround is true in // EVERY frame, and blade-to-blade contact raises toOppGround alongside // toOppWeapon. Every sabre test models that. static void sabre_frame(Frame& f) { f.a.toOwnGround = true; f.b.toOwnGround = true; } static void sabre_short_contact_registers() { Sim s; s.box.setWeapon(SABRE); s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); s.run(1200, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppLame = true; }); // 1.2 ms s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre 1.2 ms lamé contact registers (B-C commoned)", s.last.onA && s.last.signalled); } static void sabre_resting_state_is_not_a_fault() { Sim s; s.box.setWeapon(SABRE); // Four seconds of nothing but the commoned B-C resting state. s.run(4000000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre W joined own C at rest raises no fault, no light", !s.last.faultA && !s.last.onA && !s.last.signalled); } static void sabre_disconnected_weapon_faults() { Sim s; s.box.setWeapon(SABRE); // Weapon line at the rail: cord or socket disconnected. s.run(4000000, [](Frame& f, uint32_t) { f.a.floating = true; f.b.toOwnGround = true; }); check("sabre floating weapon line raises fault A", s.last.faultA && !s.last.faultB); } static void sabre_whipover_suppressed() { Sim s; s.box.setWeapon(SABRE); s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); // 8 ms of blade on blade (which also joins the opponent's C), then the // point whips over onto the lamé while the blades are still in contact. s.run(8000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppWeapon = true; f.a.toOppGround = true; }); s.run(3000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppLame = true; f.a.toOppWeapon = true; f.a.toOppGround = true; }); s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre whip-over after 8 ms parry is suppressed", !s.last.onA); } static void sabre_long_parry_then_hit_allowed() { Sim s; s.box.setWeapon(SABRE); s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); // 25 ms of blade contact is outside the whip-over window, so a genuine // cut landing afterwards - blades still touching - must still score. s.run(25000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppWeapon = true; f.a.toOppGround = true; }); s.run(3000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppLame = true; f.a.toOppWeapon = true; f.a.toOppGround = true; }); s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre cut after 25 ms of blade contact still scores", s.last.onA); } static void sabre_whipover_tail() { Sim s; s.box.setWeapon(SABRE); s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); // 8 ms of blade contact, blades part, and the whip lands 1 ms LATER - // inside the 2 ms tail, so it must still be suppressed. s.run(8000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppWeapon = true; f.a.toOppGround = true; }); s.run(1000, [](Frame& f, uint32_t) { sabre_frame(f); }); s.run(3000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppLame = true; }); s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre whip landing 1 ms after blades part is suppressed", !s.last.onA); } static void sabre_guard_hit_no_light() { Sim s; s.box.setWeapon(SABRE); // Hitting the opponent's guard joins their B and C but never their lamé. s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); s.run(40000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toOppGround = true; f.a.toOppWeapon = true; }); s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre hit on opponent guard produces no light", !s.last.onA && !s.last.signalled); } static void sabre_floor_hit_suppressed() { Sim s; s.box.setWeapon(SABRE); s.run(50000, [](Frame& f, uint32_t) { sabre_frame(f); }); s.run(40000, [](Frame& f, uint32_t) { sabre_frame(f); f.a.toPiste = true; }); s.run(220000, [](Frame& f, uint32_t) { sabre_frame(f); }); check("sabre blade on piste produces no light", !s.last.onA); } // -------------------------------------------------------------- timing static void lockout_measured_from_registration() { // Measure the exact instant the lamps are signalled, for every weapon. struct Case { Weapon w; const char* name; uint32_t contact; }; const Case cases[] = { { FOIL, "foil", 16000 }, { EPEE, "epee", 4000 }, { SABRE, "sabre", 1200 }, }; for (const Case& c : cases) { Sim s; s.box.setWeapon(c.w); if (c.w == FOIL) s.run(50000, [](Frame& f, uint32_t){ f.a.toOwnGround = true; }); else s.run(50000, [](Frame&, uint32_t){}); uint32_t t0 = s.t; uint32_t registeredAt = 0, signalledAt = 0; uint32_t endContact = t0 + c.contact; uint32_t limit = t0 + LOCKOUT_US[c.w] + c.contact + 50000; while (s.t < limit) { Frame f; f.a.clear(); f.b.clear(); f.t_us = s.t; if (s.t < endContact) { if (c.w == EPEE) f.a.toOwnLame = true; else f.a.toOppLame = true; } else if (c.w == FOIL) { f.a.toOwnGround = true; } Lights L = s.box.update(f); if (!registeredAt && (L.onA || L.offA)) registeredAt = s.t; if (!signalledAt && L.signalled) { signalledAt = s.t; break; } s.t += TICK; } uint32_t gap = signalledAt - registeredAt; long err = (long)gap - (long)LOCKOUT_US[c.w]; char buf[128]; snprintf(buf, sizeof buf, "registered +%u us, signalled +%u us, gap %u us (spec %u, err %+ld)", registeredAt - t0, signalledAt - t0, gap, LOCKOUT_US[c.w], err); char nm[80]; snprintf(nm, sizeof nm, "%s lockout gap equals spec within one scan", c.name); check(nm, err >= 0 && err < (long)TICK, buf); } } static void fault_detection() { Sim s; s.box.setWeapon(EPEE); // Point held closed for four seconds: stuck point or shorted blade wires. s.run(4000000, [](Frame& f, uint32_t) { f.a.toOwnLame = true; }); check("epee stuck point raises a fault indication", s.last.faultA); } int main() { printf("\n thresholds: LOW < %u, MID, HIGH >= %u (12 bit)\n", LEVEL_LOW_MAX, LEVEL_HIGH_MIN); printf(" joined() sanity: 2048/2048 -> %d, 4095/0 -> %d, 1365/1365 -> %d, 819/819 -> %d, 300/300 -> %d\n\n", joined(2048, 2048), joined(4095, 0), joined(1365, 1365), joined(819, 819), // sabre whip-over pile-up, 1/5 rail: must be joined joined(300, 300)); // untouched sense lines: must NOT read as joined check("sabre pile-up level (819) is inside the mid band", joined(819, 819)); check("near-ground level (300) is not joined", !joined(300, 300)); foil_contact_too_short(); foil_contact_long_enough(); foil_floor_hit_suppressed(); foil_off_target(); foil_guard_hit_suppressed(); foil_cloth_slide_to_lame(); epee_contact_too_short(); epee_contact_long_enough(); epee_floor_hit_suppressed(); epee_double_inside_lockout(); epee_single_outside_lockout(); sabre_short_contact_registers(); sabre_resting_state_is_not_a_fault(); sabre_disconnected_weapon_faults(); sabre_whipover_suppressed(); sabre_whipover_tail(); sabre_long_parry_then_hit_allowed(); sabre_guard_hit_no_light(); sabre_floor_hit_suppressed(); lockout_measured_from_registration(); fault_detection(); printf("\n %d passed, %d failed\n\n", passes, failures); return failures ? 1 : 0; }