Add custom formatter and hybrid/situation mode

This commit is contained in:
mrkmntal 2026-09-24 23:27:43 -04:00
commit 0358ce26b0
8 changed files with 288 additions and 8 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
fmt-tssr = "run --bin fmt-tssr --"

View file

@ -13,6 +13,10 @@ path = "src/lib.rs"
name = "tssr"
path = "src/main.rs"
[[bin]]
name = "fmt-tssr"
path = "src/bin/fmt-tssr.rs"
[dependencies]
rand = "0.8"

View file

@ -150,6 +150,18 @@ The parser is strict: any inconsistency produces a clear error and exits immedia
See [`docs/tsrc-language/README.md`](docs/tsrc-language/README.md) for the full language reference.
## Formatting
This project uses a custom style (Anti-GNU, Anti-1TBS) — see [STYLE.md](STYLE.md). rustfmt is disabled.
A custom formatter binary is provided:
```bash
cargo fmt-tssr # format all .rs files in src/ and tests/
cargo fmt-tssr path/to/file.rs # format a specific file
cargo fmt-tssr src/ dir/ # format specific directories
```
## Testing
```bash

49
STYLE.md Normal file
View file

@ -0,0 +1,49 @@
# tssr Code Style
## Opening Braces
Opening braces always attach to the declaration line. Never on their own line.
```rust
// CORRECT
fn foo() {
if condition {
bar();
}
}
// FORBIDDEN (GNU / Allman)
fn foo()
{
if condition
{
bar();
}
}
```
## Else Statements
The `else` keyword always starts on a new line after the closing brace of the preceding block.
```rust
// CORRECT
if condition {
foo();
}
else {
bar();
}
// FORBIDDEN (1TBS / Egyptian Else)
if condition {
foo();
} else {
bar();
}
```
## Enforcement
This project uses `.rustfmt.toml` with `disable_all_formatting = true`.
Formatting is manual — follow this guide rather than relying on rustfmt.

24
situation-demo.tsrc Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env tssr
// SITUATION mode demo — hybrid deterministic/stochastic
// Some goals are scripted (guaranteed), others are left to chance
#define SITUATION
string home = "GNU United";
string away = "BSD Athletic";
players["Richard Stallman"] = home;
players["Linus Torvalds"] = home;
players["Jordan Hubbard"] = away;
players["Ken Thompson"] = away;
// These goals are guaranteed
goal(12, home, "Richard Stallman");
goal(32, home, "Linus Torvalds");
goal(52, home, "Linus Torvalds");
goal(67, away, "Ken Thompson");
// Up to 5 additional random goals per team will be added
match(home, away);

152
src/bin/fmt-tssr.rs Normal file
View file

@ -0,0 +1,152 @@
use std::env;
use std::fs;
use std::path::Path;
fn format_file(path: &Path) -> Result<bool, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("failed to read {:?}: {}", path, e))?;
let lines: Vec<&str> = content.lines().collect();
let mut out: Vec<String> = Vec::new();
let mut changed = false;
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim();
if trimmed == "{"
&& !line.trim_start().starts_with("//")
&& !line.trim_start().starts_with("/*")
{
if let Some(last) = out.last() {
let merged = format!("{} {{", last.trim_end());
out.pop();
out.push(merged);
changed = true;
i += 1;
continue;
}
}
if (trimmed.starts_with("} else ") || trimmed == "} else" || trimmed.starts_with("} else if"))
&& !line.trim_start().starts_with("//")
{
let idx = line.rfind('}').unwrap();
let before = &line[..idx + 1];
let after = &line[idx + 1..];
out.push(before.to_string());
let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect();
let next_line = if after.trim().starts_with("else if") {
let else_idx = after.find("else").unwrap();
let rest = &after[else_idx..];
if rest.ends_with('{') {
let header = rest.trim_end_matches('{').trim_end();
format!("{}{} {{", indent, header)
} else {
format!("{}{}", indent, rest.trim())
}
} else if after.trim() == "else {" || after.trim() == "else" {
if after.trim().ends_with('{') {
format!("{}else {{", indent)
} else {
format!("{}else", indent)
}
} else {
format!("{}{}", indent, after.trim())
};
out.push(next_line);
changed = true;
i += 1;
continue;
}
out.push(line.to_string());
i += 1;
}
let result = out.join("\n");
let final_content = if content.ends_with('\n') {
format!("{}\n", result)
} else {
result
};
if changed {
fs::write(path, final_content)
.map_err(|e| format!("failed to write {:?}: {}", path, e))?;
}
Ok(changed)
}
fn find_rs_files(dir: &Path) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(find_rs_files(&path));
} else if path.extension().map_or(false, |e| e == "rs") {
if let Some(name) = path.file_name() {
if name != "fmt-tssr.rs" {
files.push(path);
}
}
}
}
}
files
}
fn collect_targets(args: &[String]) -> Vec<std::path::PathBuf> {
let mut targets = Vec::new();
if args.len() > 1 {
for arg in &args[1..] {
let p = std::path::PathBuf::from(arg);
if p.is_file() {
targets.push(p);
} else if p.is_dir() {
targets.extend(find_rs_files(&p));
}
}
} else {
let src_dir = std::path::PathBuf::from("src");
if src_dir.exists() {
targets.extend(find_rs_files(&src_dir));
}
let tests_dir = std::path::PathBuf::from("tests");
if tests_dir.exists() {
targets.extend(find_rs_files(&tests_dir));
}
}
targets
}
fn main() {
let args: Vec<String> = env::args().collect();
let targets = collect_targets(&args);
let mut formatted = 0;
let mut errors = 0;
for path in &targets {
match format_file(path) {
Ok(true) => {
println!("formatted: {}", path.display());
formatted += 1;
}
Ok(false) => {}
Err(e) => {
eprintln!("error: {}", e);
errors += 1;
}
}
}
if errors > 0 {
std::process::exit(1);
}
println!("done: {} file(s) reformatted", formatted);
}

View file

@ -84,7 +84,8 @@ fn pick_scorers(roster: &[String], count: usize, rng: &mut rand::rngs::ThreadRng
.map(|_| {
let name = if roster.is_empty() {
gen_name(rng)
} else {
}
else {
roster.choose(rng).unwrap().clone()
};
(name, rng.gen_range(1..=90u8))
@ -113,6 +114,28 @@ fn gen_match(
};
let (home_score, away_score, home_scorers, away_scorers, scripted) = match script {
Some(s) if s.situation_mode && s.goals.is_some() => {
let goals = s.goals.as_ref().unwrap();
let mut home_g: Vec<(String, u8)> = Vec::new();
let mut away_g: Vec<(String, u8)> = Vec::new();
for g in goals {
if g.team == home_name {
home_g.push((g.player.clone(), g.minute));
}
else {
away_g.push((g.player.clone(), g.minute));
}
}
let home_roster = s.roster.get(&home_name).map(|v| v.as_slice()).unwrap_or(&[]);
let away_roster = s.roster.get(&away_name).map(|v| v.as_slice()).unwrap_or(&[]);
let extra_home = rng.gen_range(0..=5usize);
let extra_away = rng.gen_range(0..=5usize);
home_g.extend(pick_scorers(home_roster, extra_home, rng));
away_g.extend(pick_scorers(away_roster, extra_away, rng));
let hs = home_g.len() as u8;
let as_ = away_g.len() as u8;
(hs, as_, home_g, away_g, true)
}
Some(s) if s.goals.is_some() => {
let goals = s.goals.as_ref().unwrap();
let mut home_g: Vec<(String, u8)> = Vec::new();
@ -120,7 +143,8 @@ fn gen_match(
for g in goals {
if g.team == home_name {
home_g.push((g.player.clone(), g.minute));
} else {
}
else {
away_g.push((g.player.clone(), g.minute));
}
}

View file

@ -23,6 +23,7 @@ pub struct TsrcScript {
pub away_var: String,
pub goals: Option<Vec<ScriptedGoal>>,
pub output_mode: OutputMode,
pub situation_mode: bool,
}
pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
@ -40,6 +41,7 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
let mut goals: Vec<ScriptedGoal> = Vec::new();
let mut goal_entries: HashMap<(u8, String), usize> = HashMap::new();
let mut output_mode: OutputMode = OutputMode::UnicodeTable;
let mut situation_mode: bool = false;
for (line_num, raw_line) in content.lines().enumerate() {
let n = line_num + 1;
@ -50,13 +52,19 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
}
if let Some(rest) = line.strip_prefix("#define ") {
if output_mode != OutputMode::UnicodeTable {
return Err(format!(
"tsrc error: output mode already defined on line {} (duplicate #define)",
n
));
let def = rest.trim();
if def == "SITUATION" {
situation_mode = true;
}
else {
if output_mode != OutputMode::UnicodeTable {
return Err(format!(
"tsrc error: output mode already defined on line {} (duplicate #define)",
n
));
}
output_mode = parse_define(def, n)?;
}
output_mode = parse_define(rest, n)?;
}
else if let Some(rest) = line.strip_prefix("string ") {
parse_string_decl(rest, n, &mut teams, &mut var_lines)?;
@ -90,6 +98,10 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
validate_goals(&goals, home_team, away_team, &roster)?;
}
if situation_mode && goals.is_empty() {
return Err("tsrc error: #define SITUATION requires at least one goal() call".to_string());
}
Ok(TsrcScript {
teams,
roster,
@ -97,6 +109,7 @@ pub fn parse_tsrc(path: &str) -> Result<TsrcScript, String> {
away_var,
goals: if goals.is_empty() { None } else { Some(goals) },
output_mode,
situation_mode,
})
}