takeone-event-managment/doc/scoring-engine.md

9.9 KiB
Raw Blame History

🎯 Taekwondo Scoring System Scoring Engine Guide

Version: 1.0
Last Updated: July 2025
Target Developer: .NET Android (MAUI/Xamarin) Developer
Purpose: Detailed specification of the Scoring Engine — the logic that validates, awards, and logs points in real-time


📌 Table of Contents


1. Overview

The Scoring Engine is the core logic module that decides when a point is awarded.

It runs on the Android tablet (score control unit) and:

  • Receives impact data from ESP32 sensors
  • Receives votes from corner referee joysticks
  • Applies WT rules
  • Validates timing and technique
  • Awards points automatically
  • Logs all events

No automatic scoring — even with sensors, referee validation is required.


2. World Taekwondo (WT) Scoring Rules

Technique Points
Kick to trunk (hogu) 1
Kick to head 3
Spinning kick to trunk 4
Spinning kick to head 5
Punch to trunk 1 (rare)
Gam-jeom (penalty) on opponent +1 to opponent

Conditions for a Valid Point

  • Impact must be legal (correct surface, no grabbing)
  • Force must exceed threshold
  • At least 3 out of 4 corner referees must press within 1 second
  • For spinning kicks: rotation > 180° confirmed by MPU6050
  • No manual input from center referee

3. Inputs to the Scoring Engine

The engine receives two types of input:

📥 1. Sensor Data (from ESP32)

{
  "type": "impact",
  "device": "head_blue",
  "force": 900,
  "spinning": true,
  "time": 1712345678901
}

📥 2. Referee Votes (from ESP32 Joysticks)

{
  "type": "button",
  "device": "corner_2",
  "pressed": true,
  "time": 1712345678905
}

All timestamps use DateTime.UtcNow.Ticks or Environment.TickCount.


4. Point Validation Logic

The engine checks for valid scoring events by correlating sensor impact with referee votes.

Valid Point Conditions

Technique Required Votes Notes
Trunk Kick ≥3 corner refs Within 1 sec of impact
Head Kick ≥3 corner refs Same
Spinning Kick ≥3 votes + spinning:true Award 4 or 5 pts
Punch ≥3 votes + force > threshold Rarely scored

🔄 Validation Flow

1. Sensor detects impact → record time T
2. Wait 1 second for referee votes
3. Count votes within [T-100ms, T+1000ms]
4. If ≥3 votes → award points
5. Broadcast to scoreboard

Use a sliding window to handle timing jitter.


5. Timing & Synchronization

⏱️ Timing Window

  • Referee button presses must occur within ±1 second of impact
  • Use UTC ticks for precision:
    long now = DateTime.UtcNow.Ticks;
    

🕒 Window Logic

bool IsInWindow(long impactTime, long voteTime)
{
    long windowStart = impactTime - TimeSpan.TicksPerSecond / 10; // -100ms (early press)
    long windowEnd = impactTime + TimeSpan.TicksPerSecond;        // +1000ms
    return voteTime >= windowStart && voteTime <= windowEnd;
}

🔄 Debouncing

  • Ignore repeated impacts from same sensor within 500ms
  • Prevents double-counting
if (lastImpactTime[device] + 5000000 > now) // 500ms in ticks
    return; // Too soon

6. Spinning Kick Detection

Spinning kicks require rotation > 180° detected by MPU6050.

📊 Sensor Data

{"type":"impact","device":"head_red","force":850,"spinning":true,"time":...}

🧠 Detection Logic (on ESP32)

  • Use gyroscope (Z-axis) to measure rotation
  • If angular change > 180° during kick → set spinning: true

The tablet scoring engine trusts this flag — no re-calculation.


7. Gam-jeom (Penalty) Handling

Penalties are manually awarded by the scorekeeper.

Rules

  • 10 Gam-jeoms = 1 point to opponent
  • But in practice, each Gam-jeom adds 1 point immediately

🖥️ UI

  • Button: Gam-jeom (Red) or Gam-jeom (Blue)
  • On tap:
    AddPenalty("red"); // Adds 1 to blue's score
    BroadcastScoreUpdate();
    

📡 JSON Event

{
  "type": "penalty",
  "player": "red",
  "reason": "Grabbing",
  "time": 1712345679000
}

8. Manual Override (Emergency Mode)

For system failures, the scorekeeper can manually award points.

🔐 Access

  • Hidden button (e.g., long-press "Settings")
  • Requires PIN (optional)

🎯 Manual Actions

Button Effect
+1 Add 1 to red or blue
+3 Add 3 (head kick)
+5 Add 5 (spinning head kick)
Undo Last Revert last point

Use sparingly — logs all manual actions.


9. Score Fusion & Conflict Resolution

🔄 Fusion Logic

The engine fuses sensor data and referee votes into a single event.

public class ImpactEvent
{
    public string Device { get; set; } // head_red, hogu_blue
    public long Time { get; set; }
    public int Force { get; set; }
    public bool Spinning { get; set; }
    public List<string> Voters { get; set; } // corner_1, corner_3
}

void ProcessImpact(ImpactEvent impact)
{
    var validVotes = GetVotesInRange(impact.Time);
    if (validVotes.Count >= 3)
    {
        AwardPoints(impact, validVotes);
    }
}

🛑 Conflict Resolution

Scenario Action
2 votes only No point
3+ votes but wrong target No point
Force too low Ignore
Duplicate impact Debounce (500ms)

10. C# Scoring Engine Implementation

public class ScoringEngine
{
    private Dictionary<string, long> _lastImpactTime = new();
    private List<ImpactData> _impacts = new();
    private List<RefereeVote> _votes = new();
    private int _redScore = 0, _blueScore = 0;
    private int _redFouls = 0, _blueFouls = 0;

    public void AddImpact(string device, int force, bool spinning, long time)
    {
        // Debounce
        if (_lastImpactTime.ContainsKey(device) && 
            time - _lastImpactTime[device] < 5000000) // 500ms
            return;

        _lastImpactTime[device] = time;
        _impacts.Add(new ImpactData { Device = device, Force = force, Spinning = spinning, Time = time });
        CheckForValidPoint(device, force, spinning, time);
    }

    public void AddRefereeVote(string corner, long time)
    {
        _votes.Add(new RefereeVote { Corner = corner, Time = time });
    }

    private void CheckForValidPoint(string device, int force, bool spinning, long impactTime)
    {
        // Force threshold
        if ((device.Contains("hogu") && force < 500) || 
            (device.Contains("head") && force < 400)) return;

        // Get votes within ±1 sec
        var votes = _votes
            .Where(v => IsInWindow(impactTime, v.Time))
            .Select(v => v.Corner)
            .ToList();

        if (votes.Count < 3) return;

        // Determine attacker
        string attacker = device.Contains("_red") ? "red" : "blue";
        int points = CalculatePoints(device, spinning);

        // Award
        if (attacker == "red") _redScore += points;
        else _blueScore += points;

        // Log and broadcast
        LogEvent($"✅ +{points} to {attacker} (Voters: {string.Join(",", votes)})");
        BroadcastScoreUpdate();
    }

    private int CalculatePoints(string device, bool spinning)
    {
        if (device.Contains("head"))
            return spinning ? 5 : 3;
        else
            return spinning ? 4 : 1;
    }

    private bool IsInWindow(long impactTime, long voteTime)
    {
        long start = impactTime - 100000;  // -100ms
        long end = impactTime + 10000000;  // +1000ms
        return voteTime >= start && voteTime <= end;
    }

    public void AddPenalty(string player)
    {
        if (player == "red") _blueScore++;
        else _redScore++;
        BroadcastScoreUpdate();
    }

    private void BroadcastScoreUpdate()
    {
        var json = new {
            mode = "score",
            red = new { score = _redScore, fouls = _redFouls },
            blue = new { score = _blueScore, fouls = _blueFouls },
            timer = GetCurrentTime()
        };
        // Send to Android TV via TCP
    }

    private void LogEvent(string message)
    {
        // Write to event log
    }
}

// Data Classes
public class ImpactData { public string Device; public int Force; public bool Spinning; public long Time; }
public class RefereeVote { public string Corner; public long Time; }

11. Integration with Display System

The scoring engine broadcasts updates to the Android TV scoreboard.

📡 Message Format (TCP to Port 5001)

{
  "mode": "score",
  "red": { "score": 8, "fouls": 1 },
  "blue": { "score": 5, "fouls": 2 },
  "timer": "01:45",
  "last_point": "Head Kick +3"
}

🔄 Trigger Events

  • Point awarded
  • Penalty given
  • Round change
  • Match start/end

12. Best Practices

Practice Why
Use DateTime.UtcNow.Ticks High precision, no timezone issues
Debounce sensor inputs Prevent double-counting
Log all events Debugging and auditing
Validate force thresholds Prevent false positives
Require 3/4 referee votes WT compliance
Keep manual override limited Prevent abuse
Broadcast immediately Real-time display