Guess that Pokémon!
aurellem ☉
Table of Contents
1 Asking the right questions
When I have a free moment with my friends, I like to play a game that is like 20 questions, where the purpose is to guess a particular Pokémon. This is a fun game because Pokémon occur in a variety of media: there's the trading card game, the anime, the video games, and so on, all of which lead to a number of very different questions you can ask. Even so, it's trickier than you might expect to win, because a lot of the obvious facts about Pokémon—their type(s), whether they evolve or not, what generation they're from—aren't enough to uniquely identify them1, while the not-so-obvious facts—like what trading card expansion they're from, or what level they learn certain moves — are too obscure; they're trivia that not many people know.
After playing this game for a while, we established certain rules:
- You can choose only Pokémon from Generation I or II. (We're experts about these Pokémon because we grew up with them.)
- You can either play the game by Generation I rules (151 Pokémon, Dark and Steel types do not exist, etc.) in which case you get 7 questions, or you can play the game by Generation II rules (251 Pokémon, etc.) in which case you get 8.
- You are forbidden from asking whether the Pokémon's Pokédex number is in a certain range. We added this rule because the game is trivial and boring to win if you use questions like these.
- You are discouraged from asking whether the Pokémon is one out of a certain list of Pokémon, for similar reasons.
I wanted to write a computer program which would determine the best questions to ask in order to win the game. This was my procedure:
- Get Reliable Pokémon data. I could have typed in all the Pokémon data by hand or copied it from the Internet, but those strategies are not particularly reliable. Moreover, I already had an existing framework for extracting and processing Pokémon Yellow data. So, I started by applying my existing framework to Pokémon Gold to get Pokémon data directly from the game itself.
- Construct good questions.
- Search for the best questions.
- Organize and present the results
2 Get reliable data
2.1 From Pokémon Yellow
If I wanted to play the game using only Generation I rules, I could just import all of the Pokémon Yellow data from my earlier project and develop questions based on those.
(:use com.aurellem.gb.hxc) ;; my earlier project
(defn map-vals [f m] (into {} (map (fn [[k v]] [k (f v)]) m))) ;; POKEMON YELLOW DATA (def pokemon (rest (hxc-pokedex-names))) (def evolutions (hxc-evolution-pretty)) (def types (map-vals :types (hxc-pokemon-base))) (def advantage (hxc-advantage)) (def stones (set (remove nil? (apply concat (map (comp (partial map :item) evolutions) pokemon)))))
2.2 From Pokémon Gold
On the other hand, in order to have the full experience of a game with my friends, I knew I would have to collect analogous data from Pokémon Gold so that my program could ask questions about both Generation I and II Pokémon.
The process was identical to the one I used for Yellow: I looked for the relevant data in the ROM, and when I found it, I wrote code to extract it and to put it in a more usable format.
;; HELPER FUNCTIONS (defn fork [pred f g] (fn [x] (if (pred x) (f x) (g x)))) (def format-name (fn [s] (keyword (.toLowerCase (apply str (map #(if (= % \space) "-" %) s)))))) (def unkeyword (comp (partial apply str) rest str))
2.2.1 Names, Species, and Pokédex text entries
(defn hxc-pokenames* [] (map (comp format-name character-codes->str (partial remove (partial = 0x50))) (partition 10 (take 2510 (drop 0x1B0B74 gold))))) (defn hxc-pokedex* [] (let [caps? (comp not nil? (set "ABCDEFGHIJKLMNOPQRSTUVWXYZ "))] (map (fn [[[kind] [x & xs] ]] (let [htwt (take 4 x) y (drop 4 x)] {:kind (character-codes->str kind) :text (apply str (interleave (repeat " ") (map character-codes->str (cons y xs)))) })) (partition 2 (partition-by (comp (partial every? caps?) character-codes->str) (take-nth 2 (partition-by (partial = 0x50) (take 55664 (drop 0x1A0000 gold)))))))))
2.2.2 Types and Type Advantages
(def pokeid (comp (zipmap (hxc-pokenames*) (range)))) (def type-map {0 :normal 1 :fighting 2 :flying 3 :poison 4 :ground 5 :rock 7 :bug 8 :ghost 9 :steel ;; a gap separates physical from special types 20 :fire 21 :water 22 :grass 23 :electric 24 :psychic 25 :ice 26 :dragon 27 :dark }) (defn hxc-types* "A thunk which returns a map from pokemon to their types." [] (map (comp format-name character-codes->str) (take-nth 2 (partition-by (partial = 0x50) (take 117 (drop 0x509E6 gold)))))) ;; a list of every type (def all-types (distinct (reduce concat (map types pokemon)))) (defn hxc-advantage* [] (let [types (hxc-types*)] (map (fn[[a b c]] [(get type-map a a) (get type-map b b) (/ c 10)] ) (partition 3 (remove (partial = 254) ;; why was this inserted? (take 333 (drop 0x34d01 gold))))))) (defn hxc-dex-data* "Hardcoded pokedex data from gold. Here, used only to get the types of every Pokemon; the rest of the data is ignored." [] (let [entry-size 32] (zipmap pokemon (map (fn [[dex# _ _ _ _ _ _ type-1 type-2 & _ ]] {:types (set (map type-map [type-1 type-2]))} ) (partition entry-size (take (* 251 32) (drop 0x51B0B gold)))))))
2.2.3 Item names and evolution data
(defn item-names* [] (map (comp format-name character-codes->str) (take-nth 2 (partition-by (partial = 0x50) (take 2012 (drop 0x1b0000 gold)))))) (defn evo-ptrs "Pointers to evolution data for each pokemon. The game alternates between evolution and learnset data." [] (map (comp (partial + 0x3c000) (fn [[lo hi]] (+ lo (* 0x100 hi)))) (partition 2 (take (* 2 251) (drop 0x427BD gold))))) (defn evo-data* [] (let [pokemon (hxc-pokenames*) items (item-names*)] (zipmap pokemon (map (comp (fn parse-data [[a b c & d]] (let [x (cond (= a 1) ;; level-up {:method :level-up :min-level b :into (nth pokemon (dec c))} (= a 2) ;; item {:method :item :item (nth items (dec b)) :into (nth pokemon (dec c))} (= a 3) ;; trade {:method :trade :item (if (= b 0xFF) nil (nth items (dec b))) :into (nth pokemon (dec c))} (= a 4) ;; happiness {:method :happiness :when (get [:anytime :day :night] (dec b)) :into (nth pokemon (dec c))} (= a 5) {:method :stat :min-level b :stat (get [:atk :def :equal] (dec c)) :into (nth pokemon (dec (first d))) }) ys (if (= a 5) (rest d) d) ] (cond (nil? x) '() (empty? ys) (list x) :else (cons x (parse-data ys))))) #(take-while (comp not zero?) ;;zero separates evolution from move data (drop % gold))) (evo-ptrs))))) (defn evo-data-raw [] (let [pokemon (hxc-pokenames*) items (item-names*)] (zipmap pokemon (map (comp (fn parse-data [[a b c & d]] (let [x (cond (= a 1) ;; level-up [a b c] (= a 2) ;; item [a b c] (= a 3) ;; trade [a b c] (= a 4) ;; happiness [a b c] (= a 5) [a b c (first d)]) ys (if (= a 5) (rest d) d) ] (cond (nil? x) '() (empty? ys) (list x) :else (cons x (parse-data ys))))) #(take-while (comp not zero?) ;;zero separates evolution from move data (drop % gold))) (evo-ptrs)))))
2.2.4 Pokémon Gold parameters
;; POKEMON GOLD (def pokemon (hxc-pokenames*)) (def evolutions (evo-data*)) (def advantage (hxc-advantage*)) (def stones #{:fire-stone :thunderstone :water-stone :leaf-stone :sun-stone :moon-stone}) (def types (map-vals :types (hxc-dex-data*)))
2.3 Evolutionary families: new tools required
Now that I had the necessary data, I needed to manipulate that data into usable form. For example, my program knew that bulbasaur evolves into ivysaur:
(pokeclass.idtree/evolutions :bulbasaur)
:method | :level-up | :min-level | 16 | :into | :ivysaur |
but I had no convenient way to tell that it was part of a three-stage evolution — that is, I had no automatic way to look at all of a Pokémon's pre-evolutions or post-evolutions.
First, I defined a variable all-families
, which used the individual
evolution data of each Pokémon to build a list of all the
evolutionary families. Here, each evolutionary family is stored as
a set, meaning that the Pokémon aren't in any particular
order.
;; all-families consists of sets of evolution groups (def all-families (let [twos ;; twos are adjacent evolution pairs (pmap #(cons % ((comp (partial map :into) evolutions) %)) pokemon)] (loop [open (map set twos) closed []] ;;(println (first open)) (if (empty? open) closed (let [x (first open) matches (remove (partial not-any? x) (rest open)) non-matches (filter (partial not-any? x) (rest open))] (if (empty? matches) (recur non-matches (conj closed x)) (recur (conj non-matches (set (reduce concat x matches))) closed)))))))
Check it out:
pokeclass.idtree/all-families
[#{:bulbasaur :ivysaur :venusaur} #{:charizard :charmeleon :charmander} #{:squirtle :wartortle :blastoise} #{:caterpie :butterfree :metapod} #{:weedle :kakuna :beedrill} #{:pidgeotto :pidgeot :pidgey} #{:raticate :rattata} #{:fearow :spearow} #{:arbok :ekans} #{:pikachu :pichu :raichu} #{:sandshrew :sandslash} #{:nidorina :nidoqueen :nidoran♀} #{:nidoran♂ :nidorino :nidoking} #{:clefairy :cleffa :clefable} #{:vulpix :ninetales} #{:wigglytuff :igglybuff :jigglypuff} #{:golbat :zubat :crobat} #{:bellossom :gloom :vileplume :oddish} #{:paras :parasect} #{:venomoth :venonat} #{:diglett :dugtrio} #{:persian :meowth} #{:psyduck :golduck} #{:mankey :primeape} #{:arcanine :growlithe} #{:politoed :poliwhirl :poliwrath :poliwag} #{:abra :kadabra :alakazam} #{:machamp :machop :machoke} #{:weepinbell :bellsprout :victreebel} #{:tentacool :tentacruel} #{:golem :geodude :graveler} #{:rapidash :ponyta} #{:slowking :slowpoke :slowbro} #{:magnemite :magneton} #{:farfetch'd} #{:doduo :dodrio} #{:seel :dewgong} #{:muk :grimer} #{:shellder :cloyster} #{:gengar :haunter :gastly} #{:steelix :onix} #{:drowzee :hypno} #{:krabby :kingler} #{:electrode :voltorb} #{:exeggutor :exeggcute} #{:cubone :marowak} #{:hitmonchan :hitmontop :hitmonlee :tyrogue} #{:lickitung} #{:weezing :koffing} #{:rhydon :rhyhorn} #{:chansey :blissey} #{:tangela} #{:kangaskhan} #{:seadra :kingdra :horsea} #{:seaking :goldeen} #{:starmie :staryu} #{:mr.mime} #{:scyther :scizor} #{:jynx :smoochum} #{:elekid :electabuzz} #{:magmar :magby} #{:pinsir} #{:tauros} #{:magikarp :gyarados} #{:lapras} #{:ditto} #{:flareon :espeon :vaporeon :jolteon :umbreon :eevee} #{:porygon2 :porygon} #{:omastar :omanyte} #{:kabutops :kabuto} #{:aerodactyl} #{:snorlax} #{:articuno} #{:zapdos} #{:moltres} #{:dragonair :dratini :dragonite} #{:mewtwo} #{:mew} #{:chikorita :meganium :bayleef} #{:quilava :typhlosion :cyndaquil} #{:croconaw :feraligatr :totodile} #{:sentret :furret} #{:hoothoot :noctowl} #{:ledyba :ledian} #{:spinarak :ariados} #{:lanturn :chinchou} #{:togetic :togepi} #{:xatu :natu} #{:flaaffy :ampharos :mareep} #{:marill :azumarill} #{:sudowoodo} #{:hoppip :jumpluff :skiploom} #{:aipom} #{:sunflora :sunkern} #{:yanma} #{:quagsire :wooper} #{:murkrow} #{:misdreavus} #{:unown} #{:wobbuffet} #{:girafarig} #{:pineco :forretress} #{:dunsparce} #{:gligar} #{:granbull :snubbull} #{:qwilfish} #{:shuckle} #{:heracross} #{:sneasel} #{:ursaring :teddiursa} #{:slugma :magcargo} #{:piloswine :swinub} #{:corsola} #{:octillery :remoraid} #{:delibird} #{:mantine} #{:skarmory} #{:houndoom :houndour} #{:phanpy :donphan} #{:stantler} #{:smeargle} #{:miltank} #{:raikou} #{:entei} #{:suicune} #{:tyranitar :larvitar :pupitar} #{:lugia} #{:ho-oh} #{:celebi}]
Using all-families
, you can write a function that returns an
individual Pokémon's evolutionary family; here, I call that
function evolution-group
.
(defn evolution-group [pkmn] (first (filter #(% pkmn) all-families)))
Check it out:
(pokeclass.idtree/evolution-group :charmander )
#{:charizard :charmeleon :charmander}
Finally, while having the unordered evolution data was useful, I also
wanted to be able to ask questions about the pre-evolutions and
post-evolutions of a Pokémon. So, I wrote three more
functions: pre-evolutions
, post-evolutions
, and
evolution-line
.
- The function
pre-evolutions
takes a Pokémon as an argument and returns a list that is either empty or which contains the Pokémon that directly evolves into it. - The function
post-evolutions
takes a Pokémon as an argument and returns a list of all the Pokémon that it can directly evolve into. - The function
evolution-line
combines both of these functions; it takes a Pokémon and produces a list of its evolutionary precursors and successors.
Importantly, this new function evolution-line
is different than the
function evolution-group
we defined earlier: evolution-line
returns a list containing only the precursors and successors of a
Pokémon, whereas evolution-group
returns a set that contains
the entire family, even other branches of evolution.
(defn pre-evolutions [pkmn] (filter (comp not nil? (partial some #{pkmn}) (partial map (comp :into)) evolutions) pokemon)) (def post-evolutions (comp (partial map :into) evolutions)) (defn evolution-line [pkmn] (concat (pre-evolutions (first (pre-evolutions pkmn))) (pre-evolutions pkmn) (list pkmn) (post-evolutions pkmn) (post-evolutions (first (post-evolutions pkmn)))))
2.3.1 evolution-line
returns a single evolutionary line, while evolution-group
shows the entire family.
;; EVOLUTION LINE (println (pokeclass.idtree/evolution-line :vaporeon)) (println (pokeclass.idtree/evolution-line :bellossom)) (println (pokeclass.idtree/evolution-line :poliwag)) (println) ;; EVOLUTION GROUP (println (pokeclass.idtree/evolution-group :vaporeon)) (println (pokeclass.idtree/evolution-group :bellossom)) (println (pokeclass.idtree/evolution-group :poliwag))
(:eevee :vaporeon) (:oddish :gloom :bellossom) (:poliwag :poliwhirl :poliwrath :politoed) #{:flareon :espeon :vaporeon :jolteon :umbreon :eevee} #{:bellossom :gloom :vileplume :oddish} #{:politoed :poliwhirl :poliwrath :poliwag}
3 Construct good questions
Having put the Pokémon data in a useable form, our next task is to assemble a database of all the questions that might be good to ask. Once we've built up such a database, we'll write a program that can find and select the most advantageous questions from it.
3.1 Questions about type
;; does the pokemon have this particular type? (def has-type? (fn ([type] (fn [pkmn] ( (comp not nil? (types pkmn)) type)))) ([type pkmn] ((has-type? type) pkmn))) ;; is the pokemon a pure ____ type? (defn pure-type? ([type] (comp (partial = #{type}) types)) ([type pkmn] ((pure-type? type) pkmn))) ;; does the pokemon have more than one type? (defn dual-type? [pkmn] (> (count (types pkmn)) 1)) ;; does the pokemon change type when it evolves? ;; (e.g. like magikarp, poliwhirl, nidorino, eevee) (def change-type? (comp not zero? dec count set (partial map types) #(cons % (post-evolutions %))))
3.2 Questions about evolution
;; is the pokemon unable to evolve? (defn final-form? [pkmn] (empty? (evolutions pkmn))) ;; does the pokemon have any pre- or post-evolutions? (def evolution-line? (comp pos? dec count evolution-group)) ;; can it evolve in more than one way? (def evolve-multiple? (comp pos? dec count post-evolutions)) ;; can it evolve by levelling up? (def evolve-levelup? (comp not nil? (partial some #{:level-up :stat}) (partial map :method) evolutions)) ;; can it use an item to evolve? (including held items, in gen II) (def evolve-item? (comp not nil? (partial some (comp not nil? :item)) evolutions)) ;; can it use an evolutionary stone to evolve? (def evolve-stone? (comp not nil? (partial some (comp not nil? stones)) (partial map :item) evolutions)) ;; can it use the specific item ____ in order to evolve? (defn evolve-with? [item] (comp not nil? #(% item) set (partial map :item) evolutions)) ;; can it evolve by being traded? (e.g. onix, machoke) (def evolve-trade? (comp not nil? :trade set (partial map :method) evolutions)) ;; does it evolve when traded with a particular item? (def evolve-trade-item? #(and (evolve-item? %) (evolve-trade? %))) ;; can it evolve through happiness? (def evolve-happy? (comp not nil? :happiness set (partial map :method) evolutions)) ;; can it evolve into something that evolves? (def evolve-twice? ;;at least twice = exactly twice, here. (comp not empty? (partial apply concat) (partial map post-evolutions) post-evolutions)) ;; does it evolve into something that can't evolve? (def evolve-once? ;; exactly once #(not (or (empty? (post-evolutions %)) (evolve-twice? %)))) ;; is it part of a three-stage evolution? (def three-stage? (comp (partial = 3) count evolution-group)) ;; The following is a hack that works for generation I only: ;; I ask whether the size of the evolution group is even. ;; For generation I, the only pokemon with an even-sized evolution ;; group are pokemon that are part of a pair of evolutions, including eevee. ;; This hack breaks in generation II, e.g. for politoed, bellossom. (def evolution-pair? ;; is it eevee or part of a pair of evolutions? (comp even? count evolution-group))
3.3 Questions about Gen I/II differences
(def generation-1? (comp (partial >= 150) pokeid)) (def generation-2? (comp not generation-1?)) ;; is it a GI pokemon that evolves into a GII pokemon? (def evolve-1->2? #(and (generation-2? %) (not (empty? (pre-evolutions %))) (generation-1? (first (pre-evolutions %))))) ;; is it a GII pokemon that evolves into a GI pokemon? (def evolve-2->1? #(and (generation-2? %) (not(nil?(some generation-1? (post-evolutions %)))))) ;; note: this fn returns a list of baby pokemon, but misses togepi.
3.4 Questions about type weaknesses (advanced)
(defn weakness "If type is supplied, returns the damage multiplier for that type against the given pokemon. Otherwise, returns a map from types to their damage multipliers against the pokemon." ([pkmn] (apply merge-with * (map #(hash-map (nth % 0) (nth % 2)) (apply concat (map #(filter (comp (partial = %) second) advantage) (types pkmn)))))) ([type pkmn] (get (weakness pkmn) type 1))) ;; is it immune to some type? (def has-immunity? (fn [pkmn] (let [type-match? (comp not nil? (set(types pkmn)))] (not(nil? (some (comp zero? second rest) (filter (comp type-match? second) advantage))))))) ;; is it immune to the _____ type? (def immune-to? (fn [pkmn type] (let [type-match? (comp not nil? (set(types pkmn)))] (reduce (fn [x [atk def y]] (if (type-match? def) (* x y) x)) 1 advantage))))
3.5 Hand-coded questions
Although I wanted to avoid including handwritten data as much as possible, there were some very reasonable questions which were easier to code by hand rather than to extract directly.
;; is it a baby pokemon? (def baby? (comp not nil? #{:pichu :cleffa :igglybuff :togepi ;;? :tyrogue :smoochum :elekid :magby })) ;; is it legendary? (def legendary? (comp not nil? (set [:articuno :zapdos :moltres :mewtwo :mew :raikou :entei :suicune :lugia :ho-oh :celebi ]))) ;; is it a fossil pokemon? (def fossil? (comp not nil? (set [:omanyte :omastar :kabuto :kabutops :aerodactyl])))
3.6 The genetic
operator turns questions about a Pokémon into questions about its evolutionary line.
(defn genetic "Transform a predicate about a pokemon into a predicate about its evolutionary line." [pred] (comp not nil? (partial some pred) evolution-line ))
Check it out:
;; does magikarp have the flying type? (println ((pokeclass.idtree/has-type? :flying) :magikarp)) ;; does any member of magikarp's line have the flying type? (println ((pokeclass.idtree/genetic (pokeclass.idtree/has-type? :flying)) :magikarp )) ;; is raichu a baby pokemon? (println (pokeclass.idtree/baby? :raichu)) ;; is any member of raichu's line a baby pokemon? (println ((pokeclass.idtree/genetic pokeclass.idtree/baby?) :raichu))
false true false true
4 Question databases
We use three sets of questions:
- Generation I only questions.
- Generation I only questions, with type weakness questions included, too.
- Generation II questions.
A question consists of a pair [pred txt]
, where the first element
is a predicate (here, a function that takes in a Pokémon and
returns true or false), and the second element is a plain English
description of the predicate.
(def questions (concat (list [dual-type? "Is it a dual-type?"] [final-form? "Is it the final evolved form?"] [evolve-item? "Can you evolve it with a stone?"] [evolve-trade? "Can you evolve it through trade?"] [evolve-levelup? "Can you evolve it through level?"] [evolution-line? "Is it part of an evolution line?"] [evolution-pair? "Is it :eevee or part of a two-stage evolution?" [(genetic dual-type?) "Is any part of its evolution line a dual-type?"] [(genetic evolve-item?) "Does any member of its evolution line evolve with a stone?"] [(genetic evolve-trade?) "Does any member of its evolution line evolve through trade?"] ] [(comp not empty? post-evolutions) "Can it evolve?"] [(comp not empty? pre-evolutions) "Does it evolve from something else?"] ) (map #(vector (has-type? %) (str "Does it have the " % " type?" ) ) all-types) (map #(vector (genetic (has-type? %)) (str "Does any member of its evolution line have the " % " type?")) all-types))) (def questions* (remove #(not-any? (first %) pokemon) (concat (conj questions [fossil? "Is it a fossil pokemon?"] [legendary? "Is it legendary?"] [three-stage? "Is it part of a three-stage evolution?"]) (reduce concat (for [x stones] (list [(evolve-with? x) (str "Can you evolve it with a " x "?")] [(genetic (evolve-with? x)) (str "Can a member of its line evolve with a " x "?")]))) (reduce concat (for [s all-types t (filter (comp pos? (partial compare s)) all-types)] (list [#(or (has-type? s %) (has-type? t %)) (str "Does it have the " s " or " t " types?")] [(genetic #(or (has-type? s %) (has-type? t %))) (str "Does any member of its line have the " s " or " t " types?")])))))) (def questions** (concat questions* (list [#(or (fossil? %) (legendary? %)) "Is it a fossil or legendary pokemon?"] [change-type? "Does it evolve into something with a different type?"] [(genetic change-type?) "Does a member of its line change type upon evolving?"]) (reduce concat (for [s all-types] (list [(pure-type? s) (str "Is it a pure " s " type Pokemon?")] [(genetic (pure-type? s)) (str "Is a member of its line a pure " s " type?")] [(comp (partial <= 2) (partial weakness s)) (str "Is it weak to " s "?")] [(genetic (comp (partial <= 2) (partial weakness s))) (str "Is a member of its line weak to " s "?")] ))) (reduce concat (for [s all-types t (filter (comp pos? (partial compare s)) all-types) u (filter (comp pos? (partial compare t)) all-types)] (list [#(or (has-type? s %) (has-type? t %) (has-type? u %)) (str "Does it have the " s ", " t ", or " u " types?")] [(genetic #(or (has-type? s %) (has-type? t %) (has-type? u %))) (str "Does any member of its line have the " s ", " t ", or " u " types?")] ))))) ;; maybe say old and new instead of ;; Gen I and II ;; QUESTIONS FOR GENERATION 2 (def questions-2 (map (fn [[pred txt]] [(memoize pred) txt]) ;;(filter ;;(fn [[pred txt]] ;; #(and ;; (not-every? pred pokemon) ;; (not(not-any? pred pokemon)))) (filter (fn [[pred txt]] #(and (not-every? pred pokemon) (not(not-any? pred pokemon)))) (concat (list ;; QUESTIONS ABOUT EVOLUTION [(genetic (partial = :eevee)) "Is it :eevee or one of its evolutions?"] [(comp not empty? post-evolutions) "Can it evolve?"] [(comp not empty? pre-evolutions) "Does it evolve from something else?"] [evolve-multiple? "Can it evolve in several different ways?"] [(genetic evolve-multiple?) "Can a member of its line evolve in several different ways?"] [evolve-levelup? "Can it evolve by levelling up?"] [(genetic evolve-levelup?) "Can a member of its line evolve by levelling up?"] [evolve-item? "Can it use an item to evolve into something?"] [(genetic evolve-item?) "Can any member of its line use an item to evolve?"] [evolve-trade? "Can it evolve into something through trade?"] [(genetic evolve-trade?) "Can a member of its line evolve through trade?"] [evolve-happy? "Can it evolve into something through friendship?"] [(genetic evolve-happy?) "Can a member of its line evolve through friendship?"] [(comp not nil? #{:espeon :umbreon}) "Does this Pokemon evolve from something based on the time of day?"] [baby? "Is it a baby Pokemon?"] [(comp not nil? #{:bulbasaur :charmander :squirtle :chikorita :cyndaquil :totodile}) "Is it a starter Pokemon for R/B/G/S?"] [(genetic (comp not nil? #{:bulbasaur :charmander :squirtle :chikorita :cyndaquil :totodile})) "Is a member of its line a starter Pokemon for R/B/G/S?"] [(genetic baby?) "Does its evolution line contain a baby Pokemon?"] [evolve-once? "Does it evolve into something that can't evolve?"] [evolve-twice? "Does it evolve into something that can evolve?"] [(comp not empty? post-evolutions) "Can it evolve?"] [(genetic (comp not empty? post-evolutions)) "Is it part of an evolution line?"] [(comp not empty? pre-evolutions) "Does it evolve from something else?"] ;; QUESTIONS ABOUT GENERATION I & II [generation-1? "Is it an old Pokemon?"] [(genetic generation-1?) "Is there an old Pokemon in its evolution line?"] [(genetic generation-2?) "Is there a new Pokemon in its evolution line?"] [evolve-1->2? "Is it an old Pokemon that can evolve into a new Pokemon?"] [(genetic evolve-1->2?) "Is there an old Pokemon in its line that evolves into a new Pokemon?"] [evolve-2->1? "Is it a new Pokemon that can evolve into an old Pokemon?"] [(genetic evolve-2->1?) "Is there a new Pokemon in its line that can evolve into an old Pokemon?"] ;; QUESTIONS ABOUT TYPE [dual-type? "Is it a dual-type?"] [(genetic dual-type?) "Is any member of its line a dual-type?"] [change-type? "Does it evolve into something with a different type?"] [(genetic change-type?) "Does a member of its line change type upon evolving?"] ;; SPECIFIC HAND-CODED QUESTIONS [legendary? "Is it legendary?"] [#(or (fossil? %) (legendary? %)) "Is it a fossil or legendary pokemon?"] [fossil? "Is it a fossil Pokemon?"] [final-form? "Is it the final evolved form?"]) ;; QUESTIONS ABOUT TYPE, CTD (reduce concat (for [s all-types] (list [(pure-type? s) (str "Is it a pure " s " type?")] [(genetic (pure-type? s)) (str "Is a member of its line a pure " s " type?")] [(has-type? s) (str "Does it have the " s " type?")] [(genetic (has-type? s)) (str "Does a member of its line have the " s " type?")]))) (reduce concat (for [s all-types t (filter (comp pos? (partial compare s)) all-types)] (list [#(or (has-type? s %) (has-type? t %)) (str "Does it have the " s " or " t " types?")] [(genetic #(or (has-type? s %) (has-type? t %))) (str "Does a member of its line have the " s " or " t " types?")]))) (reduce concat (for [s all-types t (filter (comp pos? (partial compare s)) all-types) u (filter (comp pos? (partial compare t)) all-types)] (list [#(or (has-type? s %) (has-type? t %) (has-type? u %)) (str "Does it have the " s ", " t ", or " u " types?")] [(genetic #(or (has-type? s %) (has-type? t %) (has-type? u %))) (str "Does a member of its line have the " s ", " t ", or " u " types?")]) )) ;; QUESTIONS ABOUT TYPE EFFECTIVENESS (list [has-immunity? "Is it immune to some types of attack?"] [(genetic has-immunity?) "Is a member of its line immune to some types of attack?"]) ;; QUESTIONS ABOUT EVOLUTION STONES (list [evolve-stone? "Can an evolution stone make it evolve into something?"] [(genetic evolve-stone?) "Can a member of its evolution line evolve using a stone?"]) (reduce concat (for [x stones] (list [(evolve-with? x) (str "Can you use a " x " to make it evolve into something?")] [(genetic (evolve-with? x)) (str "Can a member of its line evolve using a " x "?")])))))))
5 Search for the best questions
5.1 The winning strategy
(def log #(Math/log %)) (def entropy (memoize (fn [pkmns pred] (if (empty? pkmns) 0 (apply + ((fn [[ys ns]] (map (fn [p] (if (zero? p) 0 (* p (Math/log p)))) [(/ ys (+ ys ns)) (/ ns (+ ys ns))] )) (reduce (partial map +) (pmap #(if (pred %) [1 0] [0 1])pkmns)))))))) (def complexity (fn [pkmns] (juxt (comp (partial entropy pkmns) first) (comp count second))))
5.2 The best-question
subroutine
5.3 The naive best-question
subroutine has some problems.
- Some Pokémon are indistinguishable given the available questions.
best-question
sometimes asks a roundabout question, where it should really guess a Pokémon directly. (articuno example)- The subroutine is slow and inefficient, iterating over the same predicates multiple times at high cost.
5.4 The improved best-question
routine performs excellently.
(def fffirst (comp first first first)) (defn best-question [questions pkmns answers] (if (<= (count pkmns) 1) ;; one pokemon remaining (if (empty? answers) [nil (str "It's " (first pkmns) "!")] [nil ""]) ;; more than one pokemon remaining (let [half (set (take-nth 2 pkmns)) questions (->> questions ;; sort by entropy, then by question length ((comp (partial sort-by second) (partial pmap (juxt identity (complexity pkmns))))) ;; ask a partition question if there isn't a better option (#(if (<= (entropy pkmns (fffirst %)) (+ 0.1 (entropy pkmns (comp not nil? half)))) % (if (> 2 (count pkmns)) ;; how large is the group? (conj % [[(comp not nil? half) (str "Is it " (first half) "?")]]) (conj % [[(comp not nil? half) (str "Is it one of these: " (vec half) "?")]])))) ) ;; separate matching and non-matching pokemon pred (fffirst questions) [ins outs] (reduce (fn [[ins outs] x] (if (pred x) [(conj ins x) outs] [ins (conj outs x)])) [[][]] pkmns) ;; ask if it's a specific pokemon, if it would be equivalent to the ;; best question you could ask. questions (pmap first (cond (= 1 (count ins)) (cons [[pred (str "Is it " (first ins) "?")]] questions) (= 1 (count outs)) (cons [[(comp not pred) (str "Is it " (first outs) "?")]] questions) :else questions)) ] (if (empty? answers) (do (println) (first questions)) (do (println (str (second(first questions)) "\t" (first answers))) (recur (rest questions) (filter (comp (partial = (first answers)) (ffirst questions)) pkmns) (rest answers)))))))
5.5 auto-question
plays twenty questions with itself
(defn auto-question [questions pkmns pkmn!] (do (println (.toUpperCase (str pkmn!))) (loop [qns [] ans []] (let [x (best-question questions pokemon ans)] (if (nil? (first x)) (map vector (conj qns (second x)) (conj ans nil)) (recur (conj qns (second x)) (conj ans ((first x) pkmn!)))))))) (def count-auto (comp count (partial auto-question questions-2 pokemon)))
Footnotes:
1 For example, how do you tell apart gloom and weepinbell? They're both grass/poison types that are in the middle of a three-stage evolution.