| .. | ||
| README.md | ||
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 │
└──────────────┘ └──────────────┘ └──────────────┘
- Input — CLI flags and/or
.tsrcscript parsed intoTsrcScript - Match Generation —
gen_match()produces aMatchstruct - Output —
print_table,print_ascii_table, orprint_jsonrenders 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
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:
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
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():
// 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:
--jsonCLI flag (highest priority)- Script's
#definedirective - 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
├── 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
cargo test
19 integration tests cover:
.tsrclanguage validation (13 tests)#definedirective handling (6 tests)- SITUATION mode validation
- All error conditions produce correct error messages
- Happy paths for valid scripts