#![allow( clippy::cast_sign_loss, clippy::cast_possible_wrap, clippy::cast_possible_truncation, clippy::enum_glob_use )] use std::{fmt, process}; use clap::Parser; use image::{ImageBuffer, Rgb}; use rand::{ distr::{Distribution, StandardUniform}, Rng, }; use serde::Deserialize; // i'm putting my foot down const OUTPUT_DIGITS: usize = 5; fn main() { let Args { width, height, output_prefix, steps, } = Args::parse(); let mut rng = rand::rng(); let mut game = Game::new_random(width, height, &mut rng); let mut last = game.clone(); for i in 0..steps { let GameStep { old, new } = game.step(last, &mut rng); game = new; last = old; let path = format!("{output_prefix}{i:0OUTPUT_DIGITS$}.png"); // TODO: reuse `ImageBuffer` if let Err(e) = ImageBuffer::from(&game).save(path) { eprintln!("error saving image: {e}"); process::exit(1); } } } #[derive(Debug, clap::Parser)] #[command(version, about, long_about = None)] struct Args { /// Width of the output images (pixel-widths) width: u16, /// Width of the output images (pixel-widths) height: u16, /// Prefix for image paths; ex: /tmp/example- -> /tmp/example-000.png ... #[arg(short, long)] output_prefix: String, /// How many steps to run the automata #[arg(short, long)] steps: u64, } #[derive(Debug, Clone)] struct Game { width: i64, height: i64, field: Vec<Type>, } #[derive(Debug, Clone)] struct GameStep { old: Game, new: Game, } impl Game { fn new_random(width: u16, height: u16, rng: &mut impl Rng) -> Self { let (width, height) = (i64::from(width), i64::from(height)); Self { width, height, field: (0..(width * height)).map(|_| rng.random()).collect(), } } fn in_bounds(&self, x: i64, y: i64) -> bool { (0..self.width).contains(&x) && (0..self.height).contains(&y) } fn at(&self, x: i64, y: i64) -> Option<Type> { if self.in_bounds(x, y) { self.field.get((x + y * self.width) as usize).copied() } else { None } } fn set(&mut self, x: i64, y: i64, to: Type) { if self.in_bounds(x, y) { self.field[(x + y * self.width) as usize] = to; } } // let's see if we can get away with not swapping the field // nah it's easy fn step(self, mut next: Self, rng: &mut impl Rng) -> GameStep { for x in 0..self.width { for y in 0..self.height { let dx = rng.random_range(-1..=1); let dy = rng.random_range(-1..=1); let (ox, oy) = (x + dx, y + dy); if (x, y) == (ox, oy) { continue; } let Some(other) = self.at(ox, oy) else { continue; }; let here = self.at(x, y).unwrap(); let won = rng.random::<f32>() <= here.win_chance(other); if won { next.set(ox, oy, here); } else { next.set(x, y, other); } } } GameStep { old: self, new: next, } } } impl From<&Game> for ImageBuffer<Rgb<u8>, Vec<u8>> { fn from(val: &Game) -> Self { ImageBuffer::from_fn(val.width as u32, val.height as u32, |x, y| { val.at(i64::from(x), i64::from(y)).unwrap().color() }) } } #[derive(Debug, Deserialize, PartialEq, Eq, Hash, Clone, Copy)] enum Type { Normal, Fire, Water, Electric, Grass, Ice, Fighting, Poison, Ground, Flying, Psychic, Bug, Rock, Ghost, Dragon, Dark, Steel, Fairy, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum Efficacy { Strong, Neutral, Weak, Immune, } impl Type { const UNIVERSE: [Type; 18] = [ Self::Normal, Self::Fire, Self::Water, Self::Electric, Self::Grass, Self::Ice, Self::Fighting, Self::Poison, Self::Ground, Self::Flying, Self::Psychic, Self::Bug, Self::Rock, Self::Ghost, Self::Dragon, Self::Dark, Self::Steel, Self::Fairy, ]; // https://gist.github.com/apaleslimghost/0d25ec801ca4fc43317bcff298af43c3 fn color(self) -> Rgb<u8> { use Type::*; match self { Normal => Rgb([0xA8, 0xA7, 0x7A]), Fire => Rgb([0xEE, 0x81, 0x30]), Water => Rgb([0x63, 0x90, 0xF0]), Electric => Rgb([0xF7, 0xD0, 0x2C]), Grass => Rgb([0x7A, 0xC7, 0x4C]), Ice => Rgb([0x96, 0xD9, 0xD6]), Fighting => Rgb([0xC2, 0x2E, 0x28]), Poison => Rgb([0xA3, 0x3E, 0xA1]), Ground => Rgb([0xE2, 0xBF, 0x65]), Flying => Rgb([0xA9, 0x8F, 0xF3]), Psychic => Rgb([0xF9, 0x55, 0x87]), Bug => Rgb([0xA6, 0xB9, 0x1A]), Rock => Rgb([0xB6, 0xA1, 0x36]), Ghost => Rgb([0x73, 0x57, 0x97]), Dragon => Rgb([0x6F, 0x35, 0xFC]), Dark => Rgb([0x70, 0x57, 0x46]), Steel => Rgb([0xB7, 0xB7, 0xCE]), Fairy => Rgb([0xD6, 0x85, 0xAD]), } } // nothing is immune to itself, phew fn win_chance(self, other: Type) -> f32 { use Efficacy::{Immune, Neutral, Strong, Weak}; let self_eff = self.efficacy(other); let def_eff = other.efficacy(self); // if you are twice as effective, you should win twice as often: 2/3 vs 1/3 // if you are four times as effective, you should win four times as often: 4/5 vs 1/5 match (self_eff, def_eff) { (Strong, Strong) | (Neutral, Neutral) | (Weak, Weak) | (Immune, Immune) => 0.5, (Immune, _) => 1.0, (_, Immune) => 0.0, (Efficacy::Strong, Efficacy::Neutral) | (Efficacy::Neutral, Efficacy::Weak) => 2. / 3., (Efficacy::Neutral, Efficacy::Strong) | (Efficacy::Weak, Efficacy::Neutral) => 1. / 3., (Efficacy::Strong, Efficacy::Weak) => 4. / 5., (Efficacy::Weak, Efficacy::Strong) => 1. / 5., } } // fudge it, i'm hard-coding // https://github.com/filipekiss/pokemon-type-chart/blob/master/types.json // great lint normally but not here #[allow(clippy::match_same_arms)] fn efficacy(self, defender: Type) -> Efficacy { use Efficacy::*; use Type::*; match (self, defender) { (Normal, Ghost) => Immune, (Normal, Rock | Steel) => Weak, (Normal, _) => Neutral, (Fire, Fire | Water | Rock | Dragon) => Weak, (Fire, Grass | Ice | Bug | Steel) => Strong, (Fire, _) => Neutral, (Water, Water | Grass | Dragon) => Weak, (Water, Fire | Ground | Rock) => Strong, (Water, _) => Neutral, (Electric, Ground) => Immune, (Electric, Electric | Grass | Dragon) => Weak, (Electric, Water | Flying) => Strong, (Electric, _) => Neutral, (Grass, Fire | Grass | Poison | Flying | Bug | Dragon | Steel) => Weak, (Grass, Water | Ground | Rock) => Strong, (Grass, _) => Neutral, (Ice, Fire | Water | Ice | Steel) => Weak, (Ice, Grass | Ground | Flying | Dragon) => Strong, (Ice, _) => Neutral, (Fighting, Ghost) => Immune, (Fighting, Poison | Flying | Psychic | Bug | Fairy) => Weak, (Fighting, Normal | Ice | Rock | Dark | Steel) => Strong, (Fighting, _) => Neutral, (Poison, Steel) => Immune, (Poison, Poison | Ground | Rock | Ghost) => Weak, (Poison, Grass | Fairy) => Strong, (Poison, _) => Neutral, (Ground, Flying) => Immune, (Ground, Grass | Bug) => Weak, (Ground, Fire | Electric | Poison | Rock | Steel) => Strong, (Ground, _) => Neutral, (Flying, Electric | Rock | Steel) => Weak, (Flying, Grass | Fighting | Bug) => Strong, (Flying, _) => Neutral, (Psychic, Dark) => Immune, (Psychic, Psychic | Steel) => Weak, (Psychic, Fighting | Poison) => Strong, (Psychic, _) => Neutral, (Bug, Fire | Fighting | Poison | Flying | Ghost | Steel | Fairy) => Weak, (Bug, Grass | Psychic | Dark) => Strong, (Bug, _) => Neutral, (Rock, Fighting | Ground | Steel) => Weak, (Rock, Fire | Ice | Flying | Bug) => Strong, (Rock, _) => Neutral, (Ghost, Normal) => Immune, (Ghost, Dark) => Weak, (Ghost, Psychic | Ghost) => Strong, (Ghost, _) => Neutral, (Dragon, Fairy) => Immune, (Dragon, Steel) => Weak, (Dragon, Dragon) => Strong, (Dragon, _) => Neutral, (Dark, Fighting | Dark | Fairy) => Weak, (Dark, Psychic | Ghost) => Strong, (Dark, _) => Neutral, (Steel, Fire | Water | Electric | Steel) => Weak, (Steel, Ice | Rock | Fairy) => Strong, (Steel, _) => Neutral, (Fairy, Fire | Poison | Steel) => Weak, (Fairy, Fighting | Dragon | Dark) => Strong, (Fairy, _) => Neutral, } } } impl Distribution<Type> for StandardUniform { fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Type { Type::UNIVERSE[rng.random_range(0..(Type::UNIVERSE.len()))] } } impl fmt::Display for Type { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } }