280 lines
8.6 KiB
Markdown
280 lines
8.6 KiB
Markdown
|
|
# tssr Engine Reference
|
|||
|
|
|
|||
|
|
The tssr engine generates soccer match results. It operates in two modes: **random** (everything fabricated on the spot) and **scripted** (driven by a `.tsr`c file).
|
|||
|
|
|
|||
|
|
## 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
|
|||
|
|
|
|||
|
|
Two paths depending on whether `goal()` was scripted:
|
|||
|
|
|
|||
|
|
**Scripted goals present:**
|
|||
|
|
- 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
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Override Behavior
|
|||
|
|
|
|||
|
|
| Field | Source when scripted | Source when random |
|
|||
|
|
|-------|---------------------|-------------------|
|
|||
|
|
| Team names | `s.teams[home_var]` | Random from TEAM_NAMES |
|
|||
|
|
| Player roster | `s.roster[team]` | Empty (random names) |
|
|||
|
|
| Goal scorers | Exact from `s.goals` | `pick_scorers()` |
|
|||
|
|
| Score | Count from `s.goals` | `rng.gen_range(0..=5)` |
|
|||
|
|
| Shots | `max(score, random)` | `rng.gen_range(4..=18)` |
|
|||
|
|
| Possession | Always random | Always random |
|
|||
|
|
|
|||
|
|
## 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
|
|||
|
|
├── 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)
|
|||
|
|
- All error conditions produce correct error messages
|
|||
|
|
- Happy paths for valid scripts
|