use std::fmt::Write as _;
use std::path::{Path, PathBuf};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
self.0 >> 17
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n.max(1) as u64) as usize
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[self.below(xs.len())]
}
fn range(&mut self, lo: i64, hi: i64) -> i64 {
lo + (self.next() % (hi - lo).max(1) as u64) as i64
}
}
struct Species {
name: &'static str,
primary: &'static str,
secondary: Option<&'static str>,
height_m: f64,
weight_kg: f64,
}
const SPECIES: &[Species] = &[
Species { name: "Registeel", primary: "steel", secondary: None, height_m: 1.9, weight_kg: 205.0 },
Species { name: "Regirock", primary: "rock", secondary: None, height_m: 1.7, weight_kg: 230.0 },
Species { name: "Regice", primary: "ice", secondary: None, height_m: 1.8, weight_kg: 175.0 },
Species { name: "Metagross", primary: "steel", secondary: Some("psychic"), height_m: 1.6, weight_kg: 550.0 },
Species { name: "Scizor", primary: "bug", secondary: Some("steel"), height_m: 1.8, weight_kg: 118.0 },
Species { name: "Charizard", primary: "fire", secondary: Some("flying"), height_m: 1.7, weight_kg: 90.5 },
Species { name: "Blastoise", primary: "water", secondary: None, height_m: 1.6, weight_kg: 85.5 },
Species { name: "Venusaur", primary: "grass", secondary: Some("poison"), height_m: 2.0, weight_kg: 100.0 },
Species { name: "Pikachu", primary: "electric", secondary: None, height_m: 0.4, weight_kg: 6.0 },
Species { name: "Snorlax", primary: "normal", secondary: None, height_m: 2.1, weight_kg: 460.0 },
Species { name: "Gengar", primary: "ghost", secondary: Some("poison"), height_m: 1.5, weight_kg: 40.5 },
Species { name: "Dragonite", primary: "dragon", secondary: Some("flying"), height_m: 2.2, weight_kg: 210.0 },
Species { name: "Tyranitar", primary: "rock", secondary: Some("dark"), height_m: 2.0, weight_kg: 202.0 },
Species { name: "Lucario", primary: "fighting", secondary: Some("steel"), height_m: 1.2, weight_kg: 54.0 },
Species { name: "Garchomp", primary: "dragon", secondary: Some("ground"), height_m: 1.9, weight_kg: 95.0 },
Species { name: "Milotic", primary: "water", secondary: None, height_m: 6.2, weight_kg: 162.0 },
Species { name: "Aggron", primary: "steel", secondary: Some("rock"), height_m: 2.1, weight_kg: 360.0 },
Species { name: "Salamence", primary: "dragon", secondary: Some("flying"), height_m: 1.5, weight_kg: 102.6 },
];
const WEAKNESS: &[(&str, &[&str])] = &[
("steel", &["fire", "fighting", "ground"]),
("rock", &["water", "grass", "fighting", "ground", "steel"]),
("ice", &["fire", "fighting", "rock", "steel"]),
("fire", &["water", "rock", "ground"]),
("water", &["electric", "grass"]),
("grass", &["fire", "ice", "poison", "flying", "bug"]),
("electric", &["ground"]),
("normal", &["fighting"]),
("ghost", &["ghost", "dark"]),
("dragon", &["ice", "dragon", "fairy"]),
("psychic", &["bug", "ghost", "dark"]),
("bug", &["fire", "flying", "rock"]),
("dark", &["fighting", "bug", "fairy"]),
("fighting", &["flying", "psychic", "fairy"]),
("ground", &["water", "grass", "ice"]),
("flying", &["electric", "ice", "rock"]),
("poison", &["ground", "psychic"]),
];
const MOVES: &[(&str, &str, i64)] = &[
("Flash Cannon", "steel", 80),
("Iron Head", "steel", 80),
("Flamethrower", "fire", 90),
("Surf", "water", 90),
("Thunderbolt", "electric", 90),
("Earthquake", "ground", 100),
("Ice Beam", "ice", 90),
("Shadow Ball", "ghost", 80),
("Dragon Claw", "dragon", 80),
("Close Combat", "fighting", 120),
("Stone Edge", "rock", 100),
("Leaf Storm", "grass", 130),
];
const PLACES: &[(&str, &str)] = &[
("kanto", "Pewter City"),
("kanto", "Cerulean City"),
("kanto", "Saffron City"),
("kanto", "Cinnabar Island"),
("johto", "Violet City"),
("johto", "Ecruteak City"),
("johto", "Blackthorn City"),
("hoenn", "Rustboro City"),
("hoenn", "Mauville City"),
("hoenn", "Sootopolis City"),
("sinnoh", "Oreburgh City"),
("sinnoh", "Snowpoint City"),
];
const TRAINERS: &[&str] = &[
"Brock Harrow", "Misty Cascade", "Sabrina Vale", "Blaine Ember", "Falkner Gale", "Morty Shade",
"Clair Wyvern", "Roxanne Slate", "Wattson Volt", "Juan Tide", "Roark Quarry", "Candice Frost",
"Iris Draco", "Marnie Dusk", "Nessa Reef", "Bea Strike",
];
const TOURNAMENTS: &[&str] = &[
"Indigo Invitational", "Silver Conference", "Ever Grande Cup", "Lily of the Valley Open",
"Steel Type Masters", "Regional Doubles Series",
];
fn quarter(month: i64) -> i64 {
(month - 1) / 3 + 1
}
fn write_doc(dir: &Path, idx: usize, slug: &str, body: &str) -> std::io::Result<()> {
let name = format!("{idx:05}_{slug}.md");
std::fs::write(dir.join(name), body)
}
fn main() -> std::io::Result<()> {
let args: Vec<String> = std::env::args().collect();
let out = PathBuf::from(args.get(1).cloned().unwrap_or_else(|| "pokemon_corpus".to_string()));
let n_docs: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(400);
let seed: u64 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0x9E3779B97F4A7C15);
std::fs::create_dir_all(&out)?;
let mut rng = Rng(seed);
let mut idx = 0usize;
let mut roster_rows: Vec<String> = vec!["trainer,region,city,species,type,level,cp,weight_kg,tournament,result".to_string()];
while idx < n_docs {
match idx % 6 {
0 => {
let sp = rng.pick(SPECIES);
let (region, city) = rng.pick(PLACES);
let temp = rng.range(-5, 38);
let types = match sp.secondary {
Some(s) => format!("{}/{}", sp.primary, s),
None => sp.primary.to_string(),
};
let weak = WEAKNESS.iter().find(|(t, _)| *t == sp.primary).map(|(_, w)| *w).unwrap_or(&[]);
let mut body = String::new();
let _ = write!(
body,
"# Pokédex Entry — {}\n\n\
- **Species:** {}\n- **Type:** {}\n- **Height:** {} m\n- **Weight:** {} kg\n\
- **Habitat:** {} in the {} region\n- **Recorded habitat temperature:** {} °C\n\n\
## Field notes\n\n\
The {} specimen observed near {} weighs {} kg and stands {} m tall. \
Field researchers logged an ambient temperature of {} °C during the survey. \
{} is a {} type and takes increased damage from {} attacks.\n",
sp.name, sp.name, types, sp.height_m, sp.weight_kg, city, region, temp,
sp.name, city, sp.weight_kg, sp.height_m, temp, sp.name, sp.primary,
weak.join(", ")
);
write_doc(&out, idx, &format!("pokedex_{}", sp.name.to_lowercase()), &body)?;
}
1 => {
let (region, city) = rng.pick(PLACES);
let leader = rng.pick(TRAINERS);
let sp = rng.pick(SPECIES);
let badge_count = rng.range(1, 9);
let mut body = String::new();
let _ = write!(
body,
"# Gym Record — {}\n\n\
- **City:** {}\n- **Region:** {}\n- **Leader:** {}\n- **Specialty type:** {}\n\
- **Badges awarded this season:** {}\n\n\
## Summary\n\n\
{} operates the {} gym in the {} region and specialises in {} type battles. \
The gym awarded {} badges this season. {} registers {} as the gym's signature Pokémon.\n",
city, city, region, leader, sp.primary, badge_count,
leader, city, region, sp.primary, badge_count, leader, sp.name
);
write_doc(&out, idx, &format!("gym_{}", city.to_lowercase().replace(' ', "_")), &body)?;
}
2 => {
let a = rng.pick(TRAINERS);
let b = loop {
let c = rng.pick(TRAINERS);
if c != a {
break c;
}
};
let (region, city) = rng.pick(PLACES);
let (sp_a, sp_b) = (rng.pick(SPECIES), rng.pick(SPECIES));
let (mv, mv_type, power) = *rng.pick(MOVES);
let year = rng.range(2025, 2027);
let month = rng.range(1, 13);
let day = rng.range(1, 28);
let minutes = rng.range(6, 41);
let tour = rng.pick(TOURNAMENTS);
let mut body = String::new();
let _ = write!(
body,
"# Battle Report — {} vs {}\n\n\
- **Date:** {year}-{month:02}-{day:02}\n- **Venue:** {}\n- **Region:** {}\n\
- **Tournament:** {}\n- **Duration:** {} minutes\n\n\
## Account\n\n\
{} faced {} at {} in the {} region during the {} on {year}-{month:02}-{day:02}. \
{} led with {} while {} answered with {}. The deciding move was {}, a {} type attack \
with {} base power. The match lasted {} minutes and {} took the win.\n",
a, b, city, region, tour, minutes,
a, b, city, region, tour,
a, sp_a.name, b, sp_b.name, mv, mv_type, power, minutes, a
);
write_doc(&out, idx, "battle_report", &body)?;
roster_rows.push(format!(
"{a},{region},{city},{},{},{},{},{},{tour},win",
sp_a.name, sp_a.primary, rng.range(50, 101), rng.range(200, 900), sp_a.weight_kg
));
roster_rows.push(format!(
"{b},{region},{city},{},{},{},{},{},{tour},loss",
sp_b.name, sp_b.primary, rng.range(50, 101), rng.range(200, 900), sp_b.weight_kg
));
}
3 => {
let tour = rng.pick(TOURNAMENTS);
let (region, city) = rng.pick(PLACES);
let year = rng.range(2025, 2027);
let month = rng.range(1, 13);
let q = quarter(month);
let entrants = rng.range(32, 257);
let fee = rng.range(10, 80);
let mut body = String::new();
let _ = write!(
body,
"# Tournament Notice — {} {year}\n\n\
- **Series:** Q{q} {year}\n- **Venue:** {}\n- **Region:** {}\n\
- **Entrants:** {}\n- **Entry fee:** {} credits\n- **Regulation set:** Series {q}\n\n\
## Announcement\n\n\
The {} will be held at {} in the {} region in Q{q} {year}. \
Registration closes {year}-{month:02}-15 and {} entrants are expected. \
Teams must comply with the Series {q} regulation set. The entry fee is {} credits.\n",
tour, city, region, entrants, fee,
tour, city, region, entrants, fee
);
write_doc(&out, idx, "tournament_notice", &body)?;
}
4 => {
let q = rng.range(1, 5);
let year = rng.range(2025, 2027);
let team = rng.range(4, 7);
let bring = team - rng.range(1, 3);
let timer = rng.range(15, 31);
let level_cap = rng.range(50, 101);
let weight_cap = rng.range(300, 601);
let sp = rng.pick(SPECIES);
let banned = rng.pick(SPECIES);
let mut body = String::new();
let _ = write!(
body,
"# Competition Regulations — Series {q} {year}\n\n\
## 1. Team composition\n\n\
Each trainer registers a team of {team} Pokémon and brings {bring} to each battle. \
No Pokémon above level {level_cap} is permitted. A Pokémon heavier than {weight_cap} kg \
may not be entered in the doubles bracket.\n\n\
## 2. Restrictions\n\n\
{} is not permitted in Series {q} play. {} does not qualify for the open division. \
Restricted legendaries are reportedly under review and may be allowed in a later series.\n\n\
## 3. Timing\n\n\
The battle timer is {timer} minutes. A match that exceeds {timer} minutes is decided on \
remaining team count. Registration closes 14 days before the event.\n",
banned.name, sp.name
);
write_doc(&out, idx, &format!("regulations_series{q}_{year}"), &body)?;
}
_ => {
let (region, city) = rng.pick(PLACES);
let sp1 = rng.pick(SPECIES);
let sp2 = rng.pick(SPECIES);
let elevation = rng.range(5, 2400);
let temp = rng.range(-12, 36);
let rainfall = rng.range(200, 2600);
let year = rng.range(2025, 2027);
let month = rng.range(1, 13);
let mut body = String::new();
let _ = write!(
body,
"# Habitat Survey — {} ({})\n\n\
- **Region:** {}\n- **Survey date:** {year}-{month:02}-08\n\
- **Elevation:** {} m\n- **Mean temperature:** {} °C\n- **Annual rainfall:** {} mm\n\n\
## Observations\n\n\
The survey team recorded {} and {} near {} in the {} region at an elevation of {} m. \
Mean temperature was {} °C with {} mm of annual rainfall. \
{} was not observed above {} m during this survey window.\n",
city, region, region, elevation, temp, rainfall,
sp1.name, sp2.name, city, region, elevation, temp, rainfall, sp2.name, elevation
);
write_doc(&out, idx, "habitat_survey", &body)?;
}
}
idx += 1;
}
std::fs::write(out.join("rosters.csv"), roster_rows.join("\n") + "\n")?;
println!("wrote {} documents + rosters.csv ({} rows) → {}", n_docs, roster_rows.len() - 1, out.display());
println!("doc types: pokedex, gym record, battle report, tournament notice, regulations, habitat survey");
Ok(())
}