Update docs, add tests for SITUATION #define flag
This commit is contained in:
parent
0358ce26b0
commit
94a4e6032d
3 changed files with 165 additions and 14 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# 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).
|
||||
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
|
||||
|
||||
|
|
@ -68,9 +68,14 @@ let (home_name, away_name) = match script {
|
|||
|
||||
### 2. Score & Scorers
|
||||
|
||||
Two paths depending on whether `goal()` was scripted:
|
||||
Three paths depending on script configuration:
|
||||
|
||||
**Scripted goals present:**
|
||||
**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
|
||||
|
|
@ -111,19 +116,37 @@ struct TsrcScript {
|
|||
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 | 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 |
|
||||
| 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
|
||||
|
||||
|
|
@ -256,6 +279,7 @@ tssr/
|
|||
│ └── 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)
|
||||
```
|
||||
|
|
@ -276,5 +300,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -100,18 +100,32 @@ Selects which two teams play. **Required** — every `.tsrc` script must contain
|
|||
Sets the output format for the match. Optional — defaults to `UNICODE_TABLE`.
|
||||
|
||||
**Rules:**
|
||||
- Only one `#define` per script
|
||||
- Only one output mode `#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`.
|
||||
|
||||
### Situation Mode Directive
|
||||
|
||||
```tsrc
|
||||
#define SITUATION
|
||||
```
|
||||
|
||||
Enables hybrid deterministic/stochastic mode. Scripted goals are guaranteed, but additional random goals (up to 5 per team) are also generated.
|
||||
|
||||
**Rules:**
|
||||
- Requires at least one `goal()` call — error without one
|
||||
- Can be combined with any output mode `#define`
|
||||
- Scripted goals always appear; random goals vary per run
|
||||
- Random goals are drawn from the team's roster (or name pools if roster is empty)
|
||||
|
||||
## Complete Grammar
|
||||
|
||||
```
|
||||
script = statement*
|
||||
statement = define | string_decl | player_insert | goal_stmt | match_stmt
|
||||
define = "#define" identifier
|
||||
define = "#define" (identifier | "SITUATION")
|
||||
string_decl = "string" identifier "=" string_literal ";"
|
||||
player_insert = "players" "[" string_literal "]" "=" (identifier | string_literal) ";"
|
||||
goal_stmt = "goal" "(" number "," (identifier | string_literal) "," string_literal ")" ";"
|
||||
|
|
@ -128,8 +142,18 @@ shebang = "#!" .* "\n"
|
|||
| ASCII table | `ASCII_TABLE` | — | ASCII-only (`+`, `-`, `|`) |
|
||||
| JSON | `JSON` | `--json` | Machine-readable JSON |
|
||||
|
||||
## Match Generation Modes
|
||||
|
||||
| Mode | `#define` | Behavior |
|
||||
|------|-----------|----------|
|
||||
| Random | (none) | Everything is randomized |
|
||||
| Scripted | (none, with `goal()`) | Goals, scorers, minutes are exact |
|
||||
| Situation | `SITUATION` | Scripted goals guaranteed + 0-5 random extra goals per team |
|
||||
|
||||
## Scripted Goals Behavior
|
||||
|
||||
### Deterministic Mode (goal() without SITUATION)
|
||||
|
||||
When `goal()` is used, the match becomes deterministic:
|
||||
|
||||
| Field | Behavior with `goal()` | Behavior without |
|
||||
|
|
@ -140,6 +164,18 @@ When `goal()` is used, the match becomes deterministic:
|
|||
| Shots on target | `max(score, random(1..shots))` | Random |
|
||||
| Possession | Still random | Random |
|
||||
|
||||
### Situation Mode (goal() with SITUATION)
|
||||
|
||||
Hybrid deterministic/stochastic — scripted goals are guaranteed, plus random extras:
|
||||
|
||||
| Field | Behavior in SITUATION mode |
|
||||
|-------|---------------------------|
|
||||
| Score | Scripted goals + 0-5 random extra per team |
|
||||
| Scorers | Exact scripted goals + random from roster/name pools |
|
||||
| Shots | `max(total_score, random(4..=18))` |
|
||||
| Shots on target | `max(total_score, random(1..shots))` |
|
||||
| Possession | Still random |
|
||||
|
||||
## Variable Resolution
|
||||
|
||||
Variables are resolved at parse time by looking up their declared value:
|
||||
|
|
@ -178,8 +214,9 @@ The parser is strict. Any inconsistency produces an error and exits immediately.
|
|||
| `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 |
|
||||
| `duplicate #define` | More than one output mode `#define` |
|
||||
| `unknown #define 'FOO'` | Unrecognized output mode |
|
||||
| `#define SITUATION requires at least one goal() call` | SITUATION defined without any `goal()` |
|
||||
|
||||
## Full Example
|
||||
|
||||
|
|
@ -228,3 +265,35 @@ match(home, away);
|
|||
+BSD Athletic: Ken Thompson (78') +
|
||||
+------------------------------------+
|
||||
```
|
||||
|
||||
## Situation Mode Example
|
||||
|
||||
```tsrc
|
||||
#!/usr/bin/env tssr
|
||||
|
||||
// Hybrid mode: some goals guaranteed, others random
|
||||
#define SITUATION
|
||||
|
||||
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"); // guaranteed
|
||||
goal(67, away, "Theo de Raadt"); // guaranteed
|
||||
// Up to 5 additional random goals per team will be added
|
||||
|
||||
match(home, away);
|
||||
```
|
||||
|
||||
Each run produces different scores — scripted goals always appear, random goals vary:
|
||||
```
|
||||
Run 1: GNU United 2-3 BSD Athletic
|
||||
Run 2: GNU United 1-2 BSD Athletic
|
||||
Run 3: GNU United 2-2 BSD Athletic
|
||||
```
|
||||
|
|
|
|||
|
|
@ -309,3 +309,60 @@ fn define_unknown_error() {
|
|||
assert!(err.contains("unknown #define"), "got: {}", err);
|
||||
assert!(err.contains("FOO_BAR"), "got: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn situation_valid_with_goals() {
|
||||
let r = parse(r#"
|
||||
#define SITUATION
|
||||
string h = "Team A";
|
||||
string a = "Team B";
|
||||
players["P1"] = h;
|
||||
players["P2"] = a;
|
||||
players["P3"] = h;
|
||||
players["P4"] = a;
|
||||
goal(10, h, "P1");
|
||||
goal(45, a, "P2");
|
||||
match(h, a);
|
||||
"#);
|
||||
assert!(r.is_ok(), "got: {:?}", r.err());
|
||||
let s = r.unwrap();
|
||||
assert!(s.situation_mode);
|
||||
assert!(s.goals.is_some());
|
||||
assert_eq!(s.goals.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn situation_requires_goals_error() {
|
||||
let r = parse(r#"
|
||||
#define SITUATION
|
||||
string h = "Team A";
|
||||
string a = "Team B";
|
||||
players["P1"] = h;
|
||||
players["P2"] = a;
|
||||
match(h, a);
|
||||
"#);
|
||||
assert!(r.is_err());
|
||||
let err = r.unwrap_err();
|
||||
assert!(err.contains("SITUATION"), "got: {}", err);
|
||||
assert!(err.contains("goal()"), "got: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn situation_combinable_with_output_mode() {
|
||||
let r = parse(r#"
|
||||
#define SITUATION
|
||||
#define ASCII_TABLE
|
||||
string h = "Team A";
|
||||
string a = "Team B";
|
||||
players["P1"] = h;
|
||||
players["P2"] = a;
|
||||
players["P3"] = h;
|
||||
players["P4"] = a;
|
||||
goal(10, h, "P1");
|
||||
match(h, a);
|
||||
"#);
|
||||
assert!(r.is_ok(), "got: {:?}", r.err());
|
||||
let s = r.unwrap();
|
||||
assert!(s.situation_mode);
|
||||
assert_eq!(s.output_mode, tssr::tsrc::OutputMode::AsciiTable);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue