#include "scoring_logic.h" // foil epee sabre const uint32_t LOCKOUT_US[3] = {300000, 45000, 170000}; const uint32_t CONTACT_US[3] = { 14000, 2000, 1000}; void ScoringBox::reset() { f_[0].clear(); f_[1].clear(); anyHit_ = false; firstHitTime_ = 0; signalled_ = false; } // Is this line state one that cannot occur with healthy equipment? bool ScoringBox::faultCondition(const Contacts& c) const { switch (weapon_) { case FOIL: // Foil rests with the point closed, so a permanently open point // circuit means a broken blade wire, an unplugged cord, or a // broken C line. return c.floating; case EPEE: // Epee rests with the point open. Permanently closed means a stuck // point or a short between the two blade wires. return c.toOwnLame; case SABRE: // A healthy sabre rests with B and C commoned through the weapon // body (FIE: the socket's two contacts are shorted together), so // W joined to own C is the NORMAL state, not a fault. The real // fault is the opposite: a weapon line sitting at the rail means // the cord, socket or weapon is disconnected. return c.floating; } return false; } ScoringBox::Cand ScoringBox::classify(const Contacts& c, FencerState& s, uint32_t now) const { switch (weapon_) { case FOIL: // Point still closed - no event of any kind. if (c.toOwnGround) return CAND_NONE; // Point open against earthed metal: piste, either guard, either blade, // or the fencer's own lamé. All suppressed. This is the floor judging // the reference designs omit entirely. if (c.toPiste || c.toOppGround || c.toOppWeapon || c.toOwnLame) return CAND_NONE; if (c.toOppLame) return CAND_ON; // Point open against something insulating - off target. if (c.floating) return CAND_OFF; return CAND_NONE; case EPEE: // Epee is normally open; the point closing joins the two blade wires. if (!c.toOwnLame) return CAND_NONE; // Tip simultaneously earthed - floor hit, guard hit, or blade hit. if (c.toPiste || c.toOppGround || c.toOwnGround || c.toOppWeapon) return CAND_NONE; return CAND_ON; case SABRE: { // Sabre reality (FIE): B and C are commoned at BOTH weapons. // - toOwnGround is always true on healthy equipment, so it must not // suppress anything. // - blade-to-blade contact joins us to the opponent's B *and* C at // once, so toOppGround is just another face of toOppWeapon and // must not veto a cut delivered during blade contact - that is // the whip-over window's job below. // Own lamé contact (blade resting on your own jacket) never scores. if (c.toPiste || c.toOwnLame) return CAND_NONE; if (!c.toOppLame) { s.lameActive = false; s.whipReject = false; return CAND_NONE; } // Whip-over: judged ONCE, at the instant the lamé contact begins // (FIE: register through-the-blade hits at 0-4 ms of blade contact, // prevent at 4-15 ms). The verdict then latches for the life of that // contact, so a whip that lingers past the tail cannot sneak in. if (!s.lameActive) { s.lameActive = true; uint32_t dur = 0; bool inWindow = false; if (s.bladeContact) { dur = now - s.bladeStart; inWindow = true; } else if (s.bladeDur && (now - s.bladeEnd) <= WHIPOVER_TAIL_US) { dur = s.bladeDur; inWindow = true; } s.whipReject = inWindow && dur >= WHIPOVER_MIN_US && dur <= WHIPOVER_MAX_US; } return s.whipReject ? CAND_NONE : CAND_ON; } } return CAND_NONE; } Lights ScoringBox::update(const Frame& fr) { const uint32_t now = fr.t_us; const Contacts* cs[2] = { &fr.a, &fr.b }; for (int i = 0; i < 2; ++i) { FencerState& s = f_[i]; const Contacts& c = *cs[i]; // --- blade-to-blade tracking, needed for sabre whip-over --- if (c.toOppWeapon) { if (!s.bladeContact) { s.bladeContact = true; s.bladeStart = now; } } else if (s.bladeContact) { s.bladeContact = false; s.bladeEnd = now; s.bladeDur = now - s.bladeStart; } // --- equipment fault, indicated but never buzzed --- if (faultCondition(c)) { if (!s.faulting) { s.faulting = true; s.faultStart = now; } else if (now - s.faultStart >= FAULT_US) s.fault = true; } else { s.faulting = false; s.fault = false; } // Once this fencer has registered, or the lockout has been resolved // and signalled, stop evaluating. if (s.hitOn || s.hitOff || signalled_) continue; Cand cand = classify(c, s, now); if (cand == CAND_NONE) { s.cand = CAND_NONE; s.candStart = 0; continue; } if (s.cand != cand) { // A new kind of contact restarts the clock. s.cand = cand; s.candStart = now; continue; } // Same contact still present - has it lasted long enough to register? if (now - s.candStart >= CONTACT_US[weapon_]) { if (cand == CAND_ON) s.hitOn = true; else s.hitOff = true; s.hitTime = now; if (!anyHit_) { anyHit_ = true; firstHitTime_ = now; } } } // --- lockout, measured from the moment the first hit registered --- // The reference implementations measure it from the start of the contact // instead, which shortens the effective window by the contact time. if (anyHit_ && !signalled_ && (now - firstHitTime_) >= LOCKOUT_US[weapon_]) { signalled_ = true; } Lights out; out.onA = f_[0].hitOn; out.offA = f_[0].hitOff; out.onB = f_[1].hitOn; out.offB = f_[1].hitOff; out.faultA = f_[0].fault; out.faultB = f_[1].fault; out.signalled = signalled_; return out; }