Add custom formatter and hybrid/situation mode
This commit is contained in:
parent
62c941b4e8
commit
0358ce26b0
8 changed files with 288 additions and 8 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[alias]
|
||||
fmt-tssr = "run --bin fmt-tssr --"
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
12
README.md
12
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
|
||||
|
|
|
|||
49
STYLE.md
Normal file
49
STYLE.md
Normal 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
24
situation-demo.tsrc
Executable 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
152
src/bin/fmt-tssr.rs
Normal 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);
|
||||
}
|
||||
28
src/main.rs
28
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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
src/tsrc.rs
25
src/tsrc.rs
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue