From 0358ce26b0d8c6b527daed506f5585cf1a32b2dd Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Thu, 24 Sep 2026 23:27:43 -0400 Subject: [PATCH] Add custom formatter and hybrid/situation mode --- .cargo/config.toml | 2 + Cargo.toml | 4 ++ README.md | 12 ++++ STYLE.md | 49 ++++++++++++++ situation-demo.tsrc | 24 +++++++ src/bin/fmt-tssr.rs | 152 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 28 +++++++- src/tsrc.rs | 25 ++++++-- 8 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 STYLE.md create mode 100755 situation-demo.tsrc create mode 100644 src/bin/fmt-tssr.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..977b1a6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +fmt-tssr = "run --bin fmt-tssr --" diff --git a/Cargo.toml b/Cargo.toml index 86248e0..29e8b7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 7383c1b..0dd900a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 0000000..9ed3135 --- /dev/null +++ b/STYLE.md @@ -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. diff --git a/situation-demo.tsrc b/situation-demo.tsrc new file mode 100755 index 0000000..f0ef9e9 --- /dev/null +++ b/situation-demo.tsrc @@ -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); diff --git a/src/bin/fmt-tssr.rs b/src/bin/fmt-tssr.rs new file mode 100644 index 0000000..6d1e21a --- /dev/null +++ b/src/bin/fmt-tssr.rs @@ -0,0 +1,152 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn format_file(path: &Path) -> Result { + 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 = 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 { + 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 { + 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 = 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); +} diff --git a/src/main.rs b/src/main.rs index 064fcb9..65d8b05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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)); } } diff --git a/src/tsrc.rs b/src/tsrc.rs index a8f5cbd..f438d89 100644 --- a/src/tsrc.rs +++ b/src/tsrc.rs @@ -23,6 +23,7 @@ pub struct TsrcScript { pub away_var: String, pub goals: Option>, pub output_mode: OutputMode, + pub situation_mode: bool, } pub fn parse_tsrc(path: &str) -> Result { @@ -40,6 +41,7 @@ pub fn parse_tsrc(path: &str) -> Result { let mut goals: Vec = 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 { } 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 { 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 { away_var, goals: if goals.is_empty() { None } else { Some(goals) }, output_mode, + situation_mode, }) }