379 lines
12 KiB
Rust
379 lines
12 KiB
Rust
use rand::seq::SliceRandom;
|
|
use rand::Rng;
|
|
use std::env;
|
|
use std::path::Path;
|
|
use std::process;
|
|
|
|
use tssr::tsrc;
|
|
|
|
const TEAM_NAMES: &[&str] = &[
|
|
"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",
|
|
];
|
|
|
|
const FIRST_NAMES: &[&str] = &[
|
|
"Marco", "Luis", "Carlos", "Pedro", "James", "John", "Tom", "Alex",
|
|
"Sam", "Dan", "Mike", "Ben", "Jack", "Luke", "Max", "Leo",
|
|
"Kai", "Rio", "Zion", "Finn", "Ade", "Noah", "Evan", "Oscar",
|
|
"Hugo", "Iker", "Pau", "Dani", "Ivan", "Raul", "Diego", "Bruno",
|
|
"Felix", "Niklas", "Leroy", "Florin", "Jamal", "Mason", "Phil",
|
|
"David", "Paul", "Mark", "Luke", "John", "Simon", "Adam", "Aaron",
|
|
];
|
|
|
|
const LAST_NAMES: &[&str] = &[
|
|
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
|
|
"Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
|
|
"Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
|
|
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark",
|
|
"Ramirez", "Lewis", "Robinson", "Walker", "Young", "Allen", "King",
|
|
"Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores", "Green",
|
|
"Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell",
|
|
"Carter", "Roberts",
|
|
];
|
|
|
|
struct Team {
|
|
name: String,
|
|
score: u8,
|
|
shots: u8,
|
|
shots_on_target: u8,
|
|
possession: u8,
|
|
scorers: Vec<(String, u8)>,
|
|
}
|
|
|
|
struct Match {
|
|
home: Team,
|
|
away: Team,
|
|
}
|
|
|
|
fn gen_name(rng: &mut rand::rngs::ThreadRng) -> String {
|
|
let first = FIRST_NAMES.choose(rng).unwrap();
|
|
let last = LAST_NAMES.choose(rng).unwrap();
|
|
format!("{} {}", first, last)
|
|
}
|
|
|
|
fn pick_scorers(roster: &[String], count: usize, rng: &mut rand::rngs::ThreadRng) -> Vec<(String, u8)> {
|
|
(0..count)
|
|
.map(|_| {
|
|
let name = if roster.is_empty() {
|
|
gen_name(rng)
|
|
}
|
|
else {
|
|
roster.choose(rng).unwrap().clone()
|
|
};
|
|
(name, rng.gen_range(1..=90u8))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn gen_match(
|
|
rng: &mut rand::rngs::ThreadRng,
|
|
script: Option<&tsrc::TsrcScript>,
|
|
) -> Match {
|
|
let (home_name, away_name) = match script {
|
|
Some(s) => {
|
|
let h = s.teams.get(&s.home_var).unwrap().clone();
|
|
let a = s.teams.get(&s.away_var).unwrap().clone();
|
|
(h, a)
|
|
}
|
|
None => {
|
|
let idx1 = rng.gen_range(0..TEAM_NAMES.len());
|
|
let mut idx2 = rng.gen_range(0..TEAM_NAMES.len());
|
|
while idx2 == idx1 {
|
|
idx2 = rng.gen_range(0..TEAM_NAMES.len());
|
|
}
|
|
(TEAM_NAMES[idx1].to_string(), TEAM_NAMES[idx2].to_string())
|
|
}
|
|
};
|
|
|
|
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();
|
|
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 hs = home_g.len() as u8;
|
|
let as_ = away_g.len() as u8;
|
|
(hs, as_, home_g, away_g, true)
|
|
}
|
|
_ => {
|
|
let hs = rng.gen_range(0..=5u8);
|
|
let as_ = rng.gen_range(0..=5u8);
|
|
let home_roster = script.and_then(|s| s.roster.get(&home_name)).map(|v| v.as_slice()).unwrap_or(&[]);
|
|
let away_roster = script.and_then(|s| s.roster.get(&away_name)).map(|v| v.as_slice()).unwrap_or(&[]);
|
|
(
|
|
hs,
|
|
as_,
|
|
pick_scorers(home_roster, hs as usize, rng),
|
|
pick_scorers(away_roster, as_ as usize, rng),
|
|
false,
|
|
)
|
|
}
|
|
};
|
|
|
|
let possession = rng.gen_range(38..=62u8);
|
|
|
|
let (home_shots, home_on_target) = if scripted {
|
|
let shots = home_score.max(rng.gen_range(4..=18u8));
|
|
let on_target = home_score.max(rng.gen_range(1..=shots.min(10)));
|
|
(shots, on_target)
|
|
}
|
|
else {
|
|
let shots = rng.gen_range(4..=18u8);
|
|
let on_target = rng.gen_range(1..=shots.min(10));
|
|
(shots, on_target)
|
|
};
|
|
|
|
let (away_shots, away_on_target) = if scripted {
|
|
let shots = away_score.max(rng.gen_range(4..=18u8));
|
|
let on_target = away_score.max(rng.gen_range(1..=shots.min(10)));
|
|
(shots, on_target)
|
|
}
|
|
else {
|
|
let shots = rng.gen_range(4..=18u8);
|
|
let on_target = rng.gen_range(1..=shots.min(10));
|
|
(shots, on_target)
|
|
};
|
|
|
|
Match {
|
|
home: Team {
|
|
name: home_name,
|
|
score: home_score,
|
|
shots: home_shots,
|
|
shots_on_target: home_on_target,
|
|
possession,
|
|
scorers: home_scorers,
|
|
},
|
|
away: Team {
|
|
name: away_name,
|
|
score: away_score,
|
|
shots: away_shots,
|
|
shots_on_target: away_on_target,
|
|
possession: 100 - possession,
|
|
scorers: away_scorers,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn pad_line(content: &str, inner_width: usize) -> String {
|
|
let len = content.chars().count();
|
|
format!("{}{}", content, " ".repeat(inner_width.saturating_sub(len)))
|
|
}
|
|
|
|
fn print_table_chars(m: &Match, horiz: char, vert: char, tl: char, tr: char, bl: char, br: char, cross: char) {
|
|
let home = &m.home.name;
|
|
let away = &m.away.name;
|
|
let score_line = format!("{home} {}-{} {away}", m.home.score, m.away.score);
|
|
|
|
let shots_col = format!("{:>4} {:>4}", m.home.shots, m.away.shots);
|
|
let ontarget_col = format!("{:>4} {:>4}", m.home.shots_on_target, m.away.shots_on_target);
|
|
let possession_col = format!("{:>3}% {:>3}%", m.home.possession, m.away.possession);
|
|
|
|
let stat_rows: Vec<String> = vec![
|
|
format!("{:<12}{}", "Shots", shots_col),
|
|
format!("{:<12}{}", "On Target", ontarget_col),
|
|
format!("{:<12}{}", "Possession", possession_col),
|
|
];
|
|
|
|
let mut lines: Vec<String> = Vec::new();
|
|
lines.push(String::new());
|
|
lines.extend(stat_rows);
|
|
lines.push(String::new());
|
|
lines.push("Goals:".to_string());
|
|
if m.home.scorers.is_empty() && m.away.scorers.is_empty() {
|
|
lines.push("No goals scored.".to_string());
|
|
}
|
|
else {
|
|
for (name, min) in &m.home.scorers {
|
|
lines.push(format!("{home}: {name} ({min}')"));
|
|
}
|
|
for (name, min) in &m.away.scorers {
|
|
lines.push(format!("{away}: {name} ({min}')"));
|
|
}
|
|
}
|
|
|
|
let title = "TSSR MATCH RESULT";
|
|
let max_content = score_line.len().max(title.len()).max(
|
|
lines.iter().map(|l| l.chars().count()).max().unwrap_or(0)
|
|
);
|
|
let inner_width = max_content + 2;
|
|
|
|
let h = horiz.to_string().repeat(inner_width);
|
|
println!("{}{}{}", tl, h, tr);
|
|
println!("{}{}{}", vert, pad_line(&format!("{:^w$}", title, w = inner_width), inner_width), vert);
|
|
println!("{}{}{}", cross, h, cross);
|
|
println!("{}{}{}", vert, pad_line(&format!("{:^w$}", score_line, w = inner_width), inner_width), vert);
|
|
println!("{}{}{}", cross, h, cross);
|
|
for line in &lines {
|
|
println!("{}{}{}", vert, pad_line(line, inner_width), vert);
|
|
}
|
|
println!("{}{}{}", bl, h, br);
|
|
}
|
|
|
|
fn print_table(m: &Match) {
|
|
print_table_chars(m, '═', '║', '╔', '╗', '╚', '╝', '╠');
|
|
}
|
|
|
|
fn print_ascii_table(m: &Match) {
|
|
print_table_chars(m, '-', '+', '+', '+', '+', '+', '+');
|
|
}
|
|
|
|
fn print_json(m: &Match) {
|
|
fn scorers_json(scorers: &[(String, u8)]) -> String {
|
|
let entries: Vec<String> = scorers
|
|
.iter()
|
|
.map(|(n, m)| format!(r#"{{"player":"{}","minute":{}}}"#, n, m))
|
|
.collect();
|
|
format!("[{}]", entries.join(","))
|
|
}
|
|
|
|
println!(
|
|
r#"{{
|
|
"home": {{
|
|
"team": "{}",
|
|
"score": {},
|
|
"shots": {},
|
|
"shots_on_target": {},
|
|
"possession": {},
|
|
"scorers": {}
|
|
}},
|
|
"away": {{
|
|
"team": "{}",
|
|
"score": {},
|
|
"shots": {},
|
|
"shots_on_target": {},
|
|
"possession": {},
|
|
"scorers": {}
|
|
}}
|
|
}}"#,
|
|
m.home.name,
|
|
m.home.score,
|
|
m.home.shots,
|
|
m.home.shots_on_target,
|
|
m.home.possession,
|
|
scorers_json(&m.home.scorers),
|
|
m.away.name,
|
|
m.away.score,
|
|
m.away.shots,
|
|
m.away.shots_on_target,
|
|
m.away.possession,
|
|
scorers_json(&m.away.scorers),
|
|
);
|
|
}
|
|
|
|
fn print_usage() {
|
|
println!("Usage: tssr [--json] [--script <file>] [--help]");
|
|
println!();
|
|
println!("Options:");
|
|
println!(" --script FILE Use a .tsrc script to define teams and players");
|
|
println!(" --json Output match as JSON");
|
|
println!(" -h, --help Show this help message");
|
|
println!();
|
|
println!("Run a .tsrc script directly with a shebang:");
|
|
println!(" #!/usr/bin/env tssr");
|
|
}
|
|
|
|
fn main() {
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
if args.iter().any(|a| a == "--help" || a == "-h") {
|
|
print_usage();
|
|
return;
|
|
}
|
|
|
|
let json_mode = args.iter().any(|a| a == "--json");
|
|
|
|
let script = if let Some(pos) = args.iter().position(|a| a == "--script") {
|
|
match args.get(pos + 1) {
|
|
Some(path) => Some(tsrc::parse_tsrc(path).unwrap_or_else(|e| {
|
|
eprintln!("{}", e);
|
|
process::exit(1);
|
|
})),
|
|
None => {
|
|
eprintln!("tsrc error: --script requires a file path");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
args.iter()
|
|
.skip(1)
|
|
.find(|a| !a.starts_with("--") && Path::new(a).is_file())
|
|
.map(|path| {
|
|
tsrc::parse_tsrc(path).unwrap_or_else(|e| {
|
|
eprintln!("{}", e);
|
|
process::exit(1);
|
|
})
|
|
})
|
|
};
|
|
|
|
let mut rng = rand::thread_rng();
|
|
let m = gen_match(&mut rng, script.as_ref());
|
|
|
|
if json_mode {
|
|
print_json(&m);
|
|
}
|
|
else {
|
|
match script.as_ref().map(|s| &s.output_mode) {
|
|
Some(tsrc::OutputMode::Json) => print_json(&m),
|
|
Some(tsrc::OutputMode::AsciiTable) => print_ascii_table(&m),
|
|
_ => print_table(&m),
|
|
}
|
|
}
|
|
}
|