Add doc/scoring engine.md
This commit is contained in:
parent
02c93b323f
commit
1d6c3dcd14
408
doc/scoring engine.md
Normal file
408
doc/scoring engine.md
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
# 🎯 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🎯 **This is a complete, production-ready Scoring Engine specification.**
|
||||||
|
You now have everything to build a **fair, accurate, and tournament-compliant** scoring system.
|
||||||
|
📬 For help: Contact project lead.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ How to Use
|
||||||
|
|
||||||
|
1. Save as `SCORING_ENGINE.md`
|
||||||
|
2. Place in `/docs` folder
|
||||||
|
3. Commit to GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Let me know if you want:
|
||||||
|
- A **state diagram** of the scoring flow
|
||||||
|
- A **unit test suite** for the engine
|
||||||
|
- A **real-time event log UI**
|
||||||
|
- A **penalty reason selector**
|
||||||
|
|
||||||
|
You're now building the **brain of the system** — the **Scoring Engine** that makes it all work. 🏆
|
||||||
Loading…
x
Reference in New Issue
Block a user