# 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, // var_name -> team_name roster: HashMap>, // team_name -> [players] home_var: String, // variable name for home away_var: String, // variable name for away goals: Option>, // 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 = env::args().collect(); // --help / -h → print usage, exit // --json → JSON output // --script → 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