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

8.6 KiB
Raw Blame History

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 .tsrc 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):

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

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:

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:

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:

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

{
  "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:

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

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

[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

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