322 lines
9.7 KiB
Rust
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:?}")
|
|
}
|
|
}
|