From f0855fc2b07d75d00f6902d85959bbce38336b92 Mon Sep 17 00:00:00 2001 From: mrkmntal Date: Thu, 24 Sep 2026 18:04:07 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 9 ++ Cargo.lock | 133 ++++++++++++++++++++++++++++ Cargo.toml | 19 ++++ README.md | 93 ++++++++++++++++++++ src/main.rs | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 498 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b5ca167 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target/ +*.rlib +*.d +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..36cd508 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tssr" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..da68254 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tssr" +version = "0.1.0" +edition = "2021" +description = "Tiny Soccer Simulator Rust - generates random soccer matches on the spot" + +[[bin]] +name = "tssr" +path = "src/main.rs" + +[dependencies] +rand = "0.8" + +[profile.release] +opt-level = 3 +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..12df1b1 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# tssr — Tiny Soccer Simulator Rust + +A blazing-fast CLI that generates completely random, fabricated soccer matches on the spot. Run it, get a match. That's it. + +## Build + +```bash +cargo build --release +``` + +The binary will be at `target/release/tssr`. + +## Usage + +```bash +./tssr # Print a unicode table match report +./tssr --json # Print the same match as JSON +``` + +## Output + +### Table mode (default) + +``` +╔════════════════════════════════════════╗ +║ TSSR MATCH RESULT ║ +╠════════════════════════════════════════╣ +║ Man Page City 2 - 1 Libre Pool ║ +╠════════════════════════════════════════╣ +║ ║ +║ Shots 10 8 ║ +║ On Target 4 3 ║ +║ Possession 55% 45% ║ +║ ║ +╠════════════════════════════════════════╣ +║ Goals: ║ +║ Man Page City: Marco Smith (23') ║ +║ Man Page City: Leo Brown (67') ║ +║ Libre Pool: Dan Garcia (45') ║ +╚════════════════════════════════════════╝ +``` + +### JSON mode (`--json`) + +```json +{ + "home": { + "team": "Man Page City", + "score": 2, + "shots": 10, + "shots_on_target": 4, + "possession": 55, + "scorers": [ + {"player": "Marco Smith", "minute": 23}, + {"player": "Leo Brown", "minute": 67} + ] + }, + "away": { + "team": "Libre Pool", + "score": 1, + "shots": 8, + "shots_on_target": 3, + "possession": 45, + "scorers": [ + {"player": "Dan Garcia", "minute": 45} + ] + } +} +``` + +## Team Names + +All team names are PES-style parodies referencing free software and open source: + +- 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 + +## Stats Included + +- Score +- Shots & shots on target +- Possession percentage +- Goal scorers with minute of goal + +## Performance + +- Single dependency (`rand`) +- Release profile uses LTO, single codegen unit, panic=abort, and symbol stripping +- Designed for sub-second cold compilation and instant execution diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2161a3b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,244 @@ +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 = vec![ + format!("{:<12}{}", "Shots", shots_col), + format!("{:<12}{}", "On Target", ontarget_col), + format!("{:<12}{}", "Possession", possession_col), + ]; + + let mut lines: Vec = 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 = 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 = 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); + } +}