Add docs, new define directive and ASCII table output as an option
This commit is contained in:
parent
bf5f448897
commit
62c941b4e8
12 changed files with 1215 additions and 106 deletions
280
docs/engine/README.md
Normal file
280
docs/engine/README.md
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# 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
|
||||
230
docs/tsrc-language/README.md
Normal file
230
docs/tsrc-language/README.md
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# `.tsrc` Language Reference
|
||||
|
||||
`.tsrc` (Tiny Soccer Sim Rust C) is a minimal C-like DSL for scripting soccer matches in tssr. It lets you define teams, assign players, script goals, and control output format — all from a plain text file.
|
||||
|
||||
## Invocation
|
||||
|
||||
### Via `--script` flag
|
||||
|
||||
```bash
|
||||
tssr --script match.tsrc # table output
|
||||
tssr --script match.tsrc --json # JSON output
|
||||
```
|
||||
|
||||
### Via shebang
|
||||
|
||||
Add this as the first line of your `.tsrc` file:
|
||||
|
||||
```tsrc
|
||||
#!/usr/bin/env tssr
|
||||
```
|
||||
|
||||
Then make it executable and run directly:
|
||||
|
||||
```bash
|
||||
chmod +x match.tsrc
|
||||
./match.tsrc
|
||||
```
|
||||
|
||||
## Language Syntax
|
||||
|
||||
### Team Declaration
|
||||
|
||||
```tsrc
|
||||
string var_name = "Team Name";
|
||||
```
|
||||
|
||||
Declares a variable that holds a team name. Variables can be referenced later in `players[]`, `goal()`, and `match()`.
|
||||
|
||||
**Rules:**
|
||||
- Variable names must be alphanumeric + underscores
|
||||
- Duplicate variable names are an error
|
||||
- The string value is the team's display name
|
||||
|
||||
### Player Assignment
|
||||
|
||||
```tsrc
|
||||
players["Player Name"] = var_name;
|
||||
players["Player Name"] = "Team Name";
|
||||
```
|
||||
|
||||
Assigns a player to a team. The right side can be either a previously declared variable or a string literal.
|
||||
|
||||
**Rules:**
|
||||
- The team (right side) must resolve to a team that appears in `match()`
|
||||
- The same player cannot be added to the same team twice
|
||||
- Variables must be declared before use (forward references are an error)
|
||||
|
||||
### Scripted Goals
|
||||
|
||||
```tsrc
|
||||
goal(minute, team, player);
|
||||
```
|
||||
|
||||
Scripts a specific goal event. When any `goal()` appears, the match score becomes deterministic.
|
||||
|
||||
| Argument | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `minute` | integer (1-90) | Minute the goal was scored |
|
||||
| `team` | variable or string | The team that scored |
|
||||
| `player` | string literal | The player who scored |
|
||||
|
||||
**Rules:**
|
||||
- Minute must be between 1 and 90
|
||||
- Team must be one of the two match teams
|
||||
- Player must be on that team's roster
|
||||
- The same player cannot score at the same minute twice
|
||||
- Variables must be declared before use
|
||||
|
||||
### Match Selection
|
||||
|
||||
```tsrc
|
||||
match(var_home, var_away);
|
||||
```
|
||||
|
||||
Selects which two teams play. **Required** — every `.tsrc` script must contain exactly one `match()`.
|
||||
|
||||
**Rules:**
|
||||
- Both variables must be previously declared
|
||||
- Home and away must be different teams
|
||||
- Both teams must have at least one player assigned
|
||||
|
||||
### Output Mode Directive
|
||||
|
||||
```tsrc
|
||||
#define JSON
|
||||
#define ASCII_TABLE
|
||||
#define UNICODE_TABLE
|
||||
```
|
||||
|
||||
Sets the output format for the match. Optional — defaults to `UNICODE_TABLE`.
|
||||
|
||||
**Rules:**
|
||||
- Only one `#define` per script
|
||||
- Must be one of the three known modes
|
||||
- Unknown names produce an error
|
||||
|
||||
The `--json` CLI flag always overrides the script's `#define`.
|
||||
|
||||
## Complete Grammar
|
||||
|
||||
```
|
||||
script = statement*
|
||||
statement = define | string_decl | player_insert | goal_stmt | match_stmt
|
||||
define = "#define" identifier
|
||||
string_decl = "string" identifier "=" string_literal ";"
|
||||
player_insert = "players" "[" string_literal "]" "=" (identifier | string_literal) ";"
|
||||
goal_stmt = "goal" "(" number "," (identifier | string_literal) "," string_literal ")" ";"
|
||||
match_stmt = "match" "(" identifier "," identifier ")" ";"
|
||||
comment = "//" .* "\n"
|
||||
shebang = "#!" .* "\n"
|
||||
```
|
||||
|
||||
## Output Modes
|
||||
|
||||
| Mode | `#define` | CLI flag | Description |
|
||||
|------|-----------|----------|-------------|
|
||||
| Unicode table | `UNICODE_TABLE` (default) | — | Box-drawing characters (`╔`, `═`, `║`) |
|
||||
| ASCII table | `ASCII_TABLE` | — | ASCII-only (`+`, `-`, `|`) |
|
||||
| JSON | `JSON` | `--json` | Machine-readable JSON |
|
||||
|
||||
## Scripted Goals Behavior
|
||||
|
||||
When `goal()` is used, the match becomes deterministic:
|
||||
|
||||
| Field | Behavior with `goal()` | Behavior without |
|
||||
|-------|------------------------|-------------------|
|
||||
| Score | Count of goals per team | Random 0-5 |
|
||||
| Scorers | Exact `(player, minute)` from script | Random names + minutes |
|
||||
| Shots | `max(score, random(4..=18))` — floor = goals | Random 4-18 |
|
||||
| Shots on target | `max(score, random(1..shots))` | Random |
|
||||
| Possession | Still random | Random |
|
||||
|
||||
## Variable Resolution
|
||||
|
||||
Variables are resolved at parse time by looking up their declared value:
|
||||
|
||||
```tsrc
|
||||
string home = "GNU United";
|
||||
string away = "BSD Athletic";
|
||||
|
||||
players["Richard Stallman"] = home; // resolves to "GNU United"
|
||||
players["Theo de Raadt"] = away; // resolves to "BSD Athletic"
|
||||
|
||||
goal(12, home, "Richard Stallman"); // goal for "GNU United"
|
||||
goal(34, "BSD Athletic", "Theo de Raadt"); // literal also works
|
||||
|
||||
match(home, away); // GNU United vs BSD Athletic
|
||||
```
|
||||
|
||||
You can mix variables and literals freely. Forward references (using a variable before its `string` declaration) are an error.
|
||||
|
||||
## Validation
|
||||
|
||||
The parser is strict. Any inconsistency produces an error and exits immediately.
|
||||
|
||||
### Error Conditions
|
||||
|
||||
| Error | Cause |
|
||||
|-------|-------|
|
||||
| `undefined variable 'x'` | Variable used before `string x = ...` declaration |
|
||||
| `variable 'x' redefined` | Two `string x = ...` declarations |
|
||||
| `players[...] = "X" which is not a match team` | Roster team isn't in `match()` |
|
||||
| `duplicate player "X" on team "Y"` | Same player added to same team twice |
|
||||
| `"X" has no players assigned` | A match team has zero players |
|
||||
| `match() has the same team for both home and away` | Both `match()` args resolve to same team |
|
||||
| `no match() directive found` | Missing `match()` |
|
||||
| `goal() minute must be between 1 and 90` | Invalid minute |
|
||||
| `player "X" is not on team "Y"` | Goal references unregistered player |
|
||||
| `duplicate goal` | Same player scores same minute twice |
|
||||
| `undefined variable 'x' in goal()` | Goal uses undeclared team variable |
|
||||
| `duplicate #define` | More than one `#define` in script |
|
||||
| `unknown #define 'FOO'` | Unrecognized output mode |
|
||||
|
||||
## Full Example
|
||||
|
||||
```tsrc
|
||||
#!/usr/bin/env tssr
|
||||
|
||||
// GNU United vs BSD Athletic
|
||||
#define ASCII_TABLE
|
||||
|
||||
string home = "GNU United";
|
||||
string away = "BSD Athletic";
|
||||
|
||||
players["Richard Stallman"] = home;
|
||||
players["Brian Fox"] = home;
|
||||
players["Linus Torvalds"] = home;
|
||||
players["Theo de Raadt"] = away;
|
||||
players["Jordan Hubbard"] = away;
|
||||
players["Ken Thompson"] = away;
|
||||
|
||||
goal(12, home, "Richard Stallman");
|
||||
goal(34, away, "Theo de Raadt");
|
||||
goal(56, home, "Brian Fox");
|
||||
goal(78, "BSD Athletic", "Ken Thompson");
|
||||
goal(89, home, "Linus Torvalds");
|
||||
|
||||
match(home, away);
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```
|
||||
+------------------------------------+
|
||||
+ TSSR MATCH RESULT +
|
||||
+------------------------------------+
|
||||
+ GNU United 3-2 BSD Athletic +
|
||||
+------------------------------------+
|
||||
+ +
|
||||
+Shots 4 5 +
|
||||
+On Target 3 2 +
|
||||
+Possession 48% 52% +
|
||||
+ +
|
||||
+Goals: +
|
||||
+GNU United: Richard Stallman (12') +
|
||||
+GNU United: Brian Fox (56') +
|
||||
+GNU United: Linus Torvalds (89') +
|
||||
+BSD Athletic: Theo de Raadt (34') +
|
||||
+BSD Athletic: Ken Thompson (78') +
|
||||
+------------------------------------+
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue