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

383 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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