diff --git a/Cargo.lock b/Cargo.lock
index 2e60f1b..2d355a5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -134,12 +134,6 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
 
-[[package]]
-name = "color_quant"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
-
 [[package]]
 name = "colorchoice"
 version = "1.0.3"
@@ -186,16 +180,6 @@ dependencies = [
  "windows-targets",
 ]
 
-[[package]]
-name = "gif"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
-dependencies = [
- "color_quant",
- "weezl",
-]
-
 [[package]]
 name = "heck"
 version = "0.5.0"
@@ -210,8 +194,6 @@ checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
 dependencies = [
  "bytemuck",
  "byteorder-lite",
- "color_quant",
- "gif",
  "num-traits",
  "png",
 ]
@@ -228,12 +210,6 @@ version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
-[[package]]
-name = "lazy_static"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
-
 [[package]]
 name = "libc"
 version = "0.2.171"
@@ -290,7 +266,6 @@ version = "0.1.0"
 dependencies = [
  "clap",
  "image",
- "lazy_static",
  "rand",
  "serde",
  "serde_json",
@@ -435,12 +410,6 @@ dependencies = [
  "wit-bindgen-rt",
 ]
 
-[[package]]
-name = "weezl"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
-
 [[package]]
 name = "windows-sys"
 version = "0.59.0"
diff --git a/Cargo.toml b/Cargo.toml
index 95e0b0c..228530c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,8 +5,7 @@ edition = "2021"
 
 [dependencies]
 clap = { version = "4.5.32", features = ["derive"] }
-image = { version = "0.25.5", default-features = false, features = ["png", "gif"] }
-lazy_static = "1.5.0"
+image = { version = "0.25.5", default-features = false, features = ["png"] }
 rand = "0.9.0"
 serde = { version = "1.0.219", features = ["derive"] }
 serde_json = "1.0.140"
diff --git a/gen_efficacy.rb b/gen_efficacy.rb
new file mode 100644
index 0000000..5480e18
--- /dev/null
+++ b/gen_efficacy.rb
@@ -0,0 +1,25 @@
+require 'json'
+
+eff = JSON.parse(File.read("src/types.json"))
+
+def make_branches(type, defenders, efficacy)
+  if defenders.empty?
+    []
+  else 
+    ["(#{type}, #{defenders.join(' | ')}) => #{efficacy},"]
+  end
+end
+
+branches = eff.flat_map do |eff|
+  type = eff['name']
+  [
+    *make_branches(type, eff['immunes'], 'Immune'),
+    *make_branches(type, eff['weaknesses'], 'Weak'),
+    *make_branches(type, eff['strengths'], 'Strong'),
+    *make_branches(type, ['_'], 'Neutral'),
+  ]
+end
+
+puts "match (self, defender) {
+  #{branches.join("\n  ")}
+}"
diff --git a/src/main.rs b/src/main.rs
index 13ba984..adc6ed4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,23 +1,14 @@
 #![allow(
     clippy::cast_sign_loss,
     clippy::cast_possible_wrap,
-    clippy::cast_possible_truncation
+    clippy::cast_possible_truncation,
+    clippy::enum_glob_use
 )]
-// translate type efficacies to probabilities
 
-// https://github.com/filipekiss/pokemon-type-chart/blob/master/types.json
-const TYPE_ADVANTAGES_SRC: &str = include_str!("types.json");
-// https://gist.github.com/apaleslimghost/0d25ec801ca4fc43317bcff298af43c3 (titlecased)
-const TYPE_COLORS_SRC: &str = include_str!("type-colors.json");
-
-use std::{
-    collections::{HashMap, HashSet},
-    fmt, process,
-};
+use std::{fmt, process};
 
 use clap::Parser;
 use image::{ImageBuffer, Rgb};
-use lazy_static::lazy_static;
 use rand::{
     distr::{Distribution, StandardUniform},
     Rng,
@@ -36,39 +27,21 @@ fn main() {
     } = 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 {
-        game.step(&mut rng);
+        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);
         }
     }
-
-    // TODO: refactor to error enum to avoid this
-    // TODO: animation (at least fast image sequence)
-    // TODO: don't constantly alloc buffers
-    // if we need more errors
-}
-
-lazy_static! {
-    static ref TYPE_ADVANTAGES: HashMap<Type, TypeAdvantage> = {
-        let rows: Vec<TypeAdvantageRow> = serde_json::from_str(TYPE_ADVANTAGES_SRC).unwrap();
-        rows.into_iter().map(|r| (r.name, r.into())).collect()
-    };
-    static ref TYPE_COLORS: HashMap<Type, Rgb<u8>> = {
-        let colors: HashMap<Type, String> = serde_json::from_str(TYPE_COLORS_SRC).unwrap();
-        colors
-            .into_iter()
-            .map(|(typ, color)| {
-                let color = u32::from_str_radix(&color[1..], 16).unwrap();
-                let [_, r, g, b] = color.to_be_bytes();
-                (typ, Rgb([r, g, b]))
-            })
-            .collect()
-    };
 }
 
 #[derive(Debug, clap::Parser)]
@@ -93,6 +66,12 @@ struct Game {
     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));
@@ -123,8 +102,7 @@ impl Game {
 
     // let's see if we can get away with not swapping the field
     // nah it's easy
-    fn step(&mut self, rng: &mut impl Rng) {
-        let mut next = self.clone();
+    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);
@@ -147,7 +125,10 @@ impl Game {
                 }
             }
         }
-        self.field = next.field;
+        GameStep {
+            old: self,
+            new: next,
+        }
     }
 }
 
@@ -183,9 +164,9 @@ enum Type {
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 enum Efficacy {
-    Super,
+    Strong,
     Neutral,
-    NotVery,
+    Weak,
     Immune,
 }
 
@@ -211,59 +192,119 @@ impl Type {
         Self::Fairy,
     ];
 
+    // https://gist.github.com/apaleslimghost/0d25ec801ca4fc43317bcff298af43c3
     fn color(self) -> Rgb<u8> {
-        TYPE_COLORS[&self]
-    }
-
-    fn advantage(self) -> &'static TypeAdvantage {
-        &TYPE_ADVANTAGES[&self]
+        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, NotVery, Super};
+        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) {
-            (Super, Super) | (Neutral, Neutral) | (NotVery, NotVery) | (Immune, Immune) => 0.5,
+            (Strong, Strong) | (Neutral, Neutral) | (Weak, Weak) | (Immune, Immune) => 0.5,
             (Immune, _) => 1.0,
             (_, Immune) => 0.0,
-            (Efficacy::Super, Efficacy::Neutral) | (Efficacy::Neutral, Efficacy::NotVery) => {
-                2. / 3.
-            }
-            (Efficacy::Neutral, Efficacy::Super) | (Efficacy::NotVery, Efficacy::Neutral) => {
-                1. / 3.
-            }
-            (Efficacy::Super, Efficacy::NotVery) => 4. / 5.,
-            (Efficacy::NotVery, Efficacy::Super) => 1. / 5.,
+            (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 {
-        if defender.is_immune_to(self) {
-            return Efficacy::Immune;
+        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,
         }
-        if self.is_strong_against(defender) {
-            return Efficacy::Super;
-        }
-        if self.is_weak_against(defender) {
-            return Efficacy::NotVery;
-        }
-        Efficacy::Neutral
-    }
-
-    fn is_immune_to(self, other: Type) -> bool {
-        other.advantage().immunes.contains(&self)
-    }
-
-    fn is_strong_against(self, other: Type) -> bool {
-        self.advantage().strengths.contains(&other)
-    }
-
-    fn is_weak_against(self, other: Type) -> bool {
-        self.advantage().weaknesses.contains(&other)
     }
 }
 
@@ -278,36 +319,3 @@ impl fmt::Display for Type {
         write!(f, "{self:?}")
     }
 }
-
-#[derive(Debug, Deserialize)]
-struct TypeAdvantageRow {
-    name: Type,
-    immunes: HashSet<Type>,
-    strengths: HashSet<Type>,
-    weaknesses: HashSet<Type>,
-}
-
-/// Stats for the _attacker_.
-#[derive(Debug)]
-struct TypeAdvantage {
-    immunes: HashSet<Type>,
-    strengths: HashSet<Type>,
-    weaknesses: HashSet<Type>,
-}
-
-impl From<TypeAdvantageRow> for TypeAdvantage {
-    fn from(
-        TypeAdvantageRow {
-            immunes,
-            strengths,
-            weaknesses,
-            ..
-        }: TypeAdvantageRow,
-    ) -> Self {
-        Self {
-            immunes,
-            strengths,
-            weaknesses,
-        }
-    }
-}