pokautomata/src/main.rs
2025-03-11 17:36:34 -04:00

322 lines
9.7 KiB
Rust

#![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:?}")
}
}