tiny-soccersim-rust/src/main.rs
2026-09-24 18:04:07 -04:00

244 lines
6.9 KiB
Rust

use rand::seq::SliceRandom;
use rand::Rng;
use std::env;
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: &'static str,
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 gen_match(rng: &mut rand::rngs::ThreadRng) -> Match {
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());
}
let home_name = TEAM_NAMES[idx1];
let away_name = TEAM_NAMES[idx2];
let home_score = rng.gen_range(0..=5u8);
let away_score = rng.gen_range(0..=5u8);
let possession = rng.gen_range(38..=62u8);
let home_shots = rng.gen_range(4..=18u8);
let away_shots = rng.gen_range(4..=18u8);
let home_on_target = rng.gen_range(1..=home_shots.min(10));
let away_on_target = rng.gen_range(1..=away_shots.min(10));
let home_scorers: Vec<(String, u8)> = (0..home_score)
.map(|_| (gen_name(rng), rng.gen_range(1..=90u8)))
.collect();
let away_scorers: Vec<(String, u8)> = (0..away_score)
.map(|_| (gen_name(rng), rng.gen_range(1..=90u8)))
.collect();
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(m: &Match) {
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;
println!("╔{}╗", "═".repeat(inner_width));
println!("║{}║", pad_line(&format!("{:^w$}", title, w = inner_width), inner_width));
println!("╠{}╣", "═".repeat(inner_width));
println!("║{}║", pad_line(&format!("{:^w$}", score_line, w = inner_width), inner_width));
println!("╠{}╣", "═".repeat(inner_width));
for line in &lines {
println!("║{}║", pad_line(line, inner_width));
}
println!("╚{}╝", "═".repeat(inner_width));
}
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] [--help]");
println!();
println!("Options:");
println!(" --json Output match as JSON");
println!(" -h, --help Show this help message");
}
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 mut rng = rand::thread_rng();
let m = gen_match(&mut rng);
if json_mode {
print_json(&m);
} else {
print_table(&m);
}
}