# 🎯 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](#1-overview) - [2. World Taekwondo (WT) Scoring Rules](#2-world-taekwondo-wt-scoring-rules) - [3. Inputs to the Scoring Engine](#3-inputs-to-the-scoring-engine) - [4. Point Validation Logic](#4-point-validation-logic) - [5. Timing & Synchronization](#5-timing--synchronization) - [6. Spinning Kick Detection](#6-spinning-kick-detection) - [7. Gam-jeom (Penalty) Handling](#7-gam-jeom-penalty-handling) - [8. Manual Override (Emergency Mode)](#8-manual-override-emergency-mode) - [9. Score Fusion & Conflict Resolution](#9-score-fusion--conflict-resolution) - [10. C# Scoring Engine Implementation](#10-c-scoring-engine-implementation) - [11. Integration with Display System](#11-integration-with-display-system) - [12. Best Practices](#12-best-practices) --- ## 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) ```json { "type": "impact", "device": "head_blue", "force": 900, "spinning": true, "time": 1712345678901 } ``` ### 📥 2. Referee Votes (from ESP32 Joysticks) ```json { "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 ```text 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: ```csharp long now = DateTime.UtcNow.Ticks; ``` ### 🕒 Window Logic ```csharp 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 ```csharp 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 ```json {"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: ```csharp AddPenalty("red"); // Adds 1 to blue's score BroadcastScoreUpdate(); ``` ### 📡 JSON Event ```json { "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. ```csharp 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 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 ```csharp public class ScoringEngine { private Dictionary _lastImpactTime = new(); private List _impacts = new(); private List _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) ```json { "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 |