tiny-soccersim-rust/docs/engine/README.md

305 lines
9.7 KiB
Markdown
Raw Permalink 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.

# tssr Engine Reference
The tssr engine generates soccer match results. It operates in three modes: **random** (everything fabricated on the spot), **scripted** (driven by `.tsrc` goals), and **situation** (hybrid — scripted goals guaranteed plus random extras).
## Overview
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CLI / .tsrc │────▶│ Match Gen │────▶│ Output │
│ Input │ │ Engine │ │ Formatter │
└──────────────┘ └──────────────┘ └──────────────┘
```
1. **Input** — CLI flags and/or `.tsrc` script parsed into `TsrcScript`
2. **Match Generation** — `gen_match()` produces a `Match` struct
3. **Output** — `print_table`, `print_ascii_table`, or `print_json` renders the result
## Random Mode
When no `.tsrc` script is provided, everything is randomized.
### Team Selection
Two distinct teams are picked from a hardcoded pool of 30 FOSS-parody names:
- Man Page City, Libre Pool, ArsenAL, Shellsea, TomThumb FC
- Real Madread, BarcelON, BayErR, Door Mount, Inter Mission
- Man Unixited, A-JaX, CellTick, RanGERS, LaZIO, RomaAEM
- Port OS, BenFICA, Fine Node, AC M1Lan, PSGROOT
- Free BSD Athletic, Kernel Panic, Tor Valdes, Debian United
- Arch Lisbon, NixOS City, Fork Enoord, Branchand FC, Merge Conflict
### Player Name Generation
Names are fabricated from two pools (48 first names, 50 last names):
```rust
fn gen_name(rng) -> String {
format!("{} {}", FIRST_NAMES.choose(rng), LAST_NAMES.choose(rng))
}
```
### Stat Ranges
| Stat | Range | Distribution |
|------|-------|--------------|
| Score (per team) | 0–5 | Uniform random |
| Shots (per team) | 4–18 | Uniform random |
| Shots on target | 1–min(shots, 10) | Uniform random |
| Possession (home) | 38–62% | Uniform random |
| Possession (away) | 100 - home | Derived |
| Goal minute | 1–90 | Uniform random |
## Match Generation Flow
```
gen_match(rng, script) -> Match
```
### 1. Team Resolution
```rust
let (home_name, away_name) = match script {
Some(s) => (s.teams[home_var].clone(), s.teams[away_var].clone()),
None => (random_team(), random_team_excluding(home)),
};
```
### 2. Score & Scorers
Three paths depending on script configuration:
**Situation mode (SITUATION + goal()):**
- Partition scripted goals by team
- Add 0-5 random extra goals per team from roster/name pools
- Score = scripted + random
**Scripted goals only (goal() without SITUATION):**
- Partition goals by team
- Score = count of goals per team
- Scorers = exact `(player, minute)` from script
**No scripted goals:**
- Score = `rng.gen_range(0..=5)` per team
- Scorers = `pick_scorers(roster, score, rng)` — random selection from roster or name pools
### 3. Shots
**Scripted goals present** — shots floored at goal count:
```rust
let shots = score.max(rng.gen_range(4..=18));
let on_target = score.max(rng.gen_range(1..=shots.min(10)));
```
**No scripted goals** — fully random:
```rust
let shots = rng.gen_range(4..=18);
let on_target = rng.gen_range(1..=shots.min(10));
```
### 4. Possession
Always random: `rng.gen_range(38..=62)` for home, `100 - home` for away.
## Scripted Mode
When a `.tsrc` script is provided, parsed into `TsrcScript`:
```rust
struct TsrcScript {
teams: HashMap<String, String>, // var_name -> team_name
roster: HashMap<String, Vec<String>>, // team_name -> [players]
home_var: String, // variable name for home
away_var: String, // variable name for away
goals: Option<Vec<ScriptedGoal>>, // scripted goals (None = random)
output_mode: OutputMode, // UnicodeTable, AsciiTable, Json
situation_mode: bool, // hybrid deterministic/stochastic
}
```
### Override Behavior
| Field | Scripted | Situation | Random |
|-------|-----------|------------|--------|
| Team names | `s.teams[home_var]` | `s.teams[home_var]` | Random from TEAM_NAMES |
| Player roster | `s.roster[team]` | `s.roster[team]` | Empty (random names) |
| Goal scorers | Exact from `s.goals` | Scripted + 0-5 random/team | `pick_scorers()` |
| Score | Count from `s.goals` | Scripted + random | `rng.gen_range(0..=5)` |
| Shots | `max(score, random)` | `max(score, random)` | `rng.gen_range(4..=18)` |
| Possession | Always random | Always random | Always random |
## Situation Mode
Hybrid deterministic/stochastic mode enabled by `#define SITUATION`. When combined with `goal()`:
```rust
// Partition scripted goals
let (home_goals, away_goals) = partition_by_team(&s.goals, home_name, away_name);
// Add 0-5 random extra goals per team
let extra_home = rng.gen_range(0..=5);
let extra_away = rng.gen_range(0..=5);
home_goals.extend(pick_scorers(home_roster, extra_home, rng));
away_goals.extend(pick_scorers(away_roster, extra_away, rng));
```
Requires at least one `goal()` — error if SITUATION is defined without goals.
## Output Formatting
Three output modes, selected by:
1. `--json` CLI flag (highest priority)
2. Script's `#define` directive
3. Default: `UNICODE_TABLE`
### Unicode Table
Uses box-drawing characters:
```
╔════════════════════════════════════════╗
║ TSSR MATCH RESULT ║
╠════════════════════════════════════════╣
║ Man Page City 2 - 1 Libre Pool║
╠════════════════════════════════════════╣
║ ║
║ Shots 10 8 ║
║ On Target 4 3 ║
║ Possession 55% 45% ║
║ ║
╠════════════════════════════════════════╣
║ Goals: ║
║ Man Page City: Marco Smith (23') ║
║ Man Page City: Leo Brown (67') ║
╚════════════════════════════════════════╝
```
### ASCII Table
Same layout, pure ASCII characters:
```
+--------------------------------------------+
| TSSR MATCH RESULT |
+--------------------------------------------+
| Man Page City 2 - 1 Libre Pool |
+--------------------------------------------+
+--------------------------------------------+
| Goals: |
| Man Page City: Marco Smith (23') |
+--------------------------------------------+
```
### JSON
```json
{
"home": {
"team": "Man Page City",
"score": 2,
"shots": 10,
"shots_on_target": 4,
"possession": 55,
"scorers": [
{"player": "Marco Smith", "minute": 23},
{"player": "Leo Brown", "minute": 67}
]
},
"away": {
"team": "Libre Pool",
"score": 1,
"shots": 8,
"shots_on_target": 3,
"possession": 45,
"scorers": [
{"player": "Dan Garcia", "minute": 45}
]
}
}
```
## Table Rendering Implementation
Both table modes share a single function parameterized by border characters:
```rust
fn print_table_chars(m: &Match, horiz, vert, tl, tr, bl, br, cross)
```
| Mode | horiz | vert | corners | cross |
|------|-------|------|---------|-------|
| Unicode | `═` | `║` | `╔╗╚╝` | `╠` |
| ASCII | `-` | `\|` | `++++` | `+` |
Layout is content-driven — `inner_width` is calculated from the longest line (score, title, or goal text) plus 2 padding characters.
## CLI Argument Parsing
```rust
fn main() {
let args: Vec<String> = env::args().collect();
// --help / -h → print usage, exit
// --json → JSON output
// --script <file> → parse .tsrc
// bare file path → shebang mode (auto-detect)
}
```
Shebang detection: if any non-flag argument is a path to an existing file, it's treated as a script.
## Build Configuration
```toml
[profile.release]
opt-level = 3 # Maximum optimization
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
panic = "abort" # Smaller binary, no unwinding
strip = true # Remove debug symbols
```
## Project Layout
```
tssr/
├── Cargo.toml # Package config (lib + bin)
├── src/
│ ├── lib.rs # Library root (pub mod tsrc)
│ ├── main.rs # CLI binary
│ └── tsrc.rs # .tsrc parser + validation
├── tests/
│ └── tssr.rs # 19 integration tests
├── docs/
│ ├── tsrc-language/
│ │ └── README.md # Language reference
│ └── engine/
│ └── README.md # Engine reference
├── scripted_match.tsrc # Example: scripted goals
├── defines-test-match.tsrc # Example: ASCII_TABLE mode
├── situation-demo.tsrc # Example: SITUATION hybrid mode
├── gnu-bsd-soccer.tsrc # Example: variable references
└── test_match.tsrc # Intentionally broken (for testing errors)
```
## Dependencies
| Crate | Version | Purpose | Scope |
|-------|---------|---------|-------|
| `rand` | 0.8 | Random number generation | Runtime |
| `tempfile` | 3 | Temporary files for tests | Dev only |
## Testing
```bash
cargo test
```
19 integration tests cover:
- `.tsrc` language validation (13 tests)
- `#define` directive handling (6 tests)
- SITUATION mode validation
- All error conditions produce correct error messages
- Happy paths for valid scripts