# 🎯 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<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

```csharp
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)
```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 |