rlm@10: #+title: Discovering Effective Pok\eacute{}mon Types Using Linear Optimization rlm@0: #+author: Robert McIntyre & Dylan Holmes rlm@0: #+EMAIL: rlm@mit.edu rlm@12: #+description: Using Lpsolve to find effective pokemon types in clojure. rlm@12: #+keywords: Pokemon, clojure, linear optimization, lp_solve, LpSolve rlm@4: #+SETUPFILE: ../../aurellem/org/setup.org rlm@2: #+INCLUDE: ../../aurellem/org/level-0.org rlm@4: rlm@0: * Introduction rlm@11: This post continues the [[./types.org][previous one]] about pok\eacute{}mon types. rlm@11: Pok\eacute{}mon is a game in which adorable creatures battle each rlm@11: other using fantastic attacks. It was made into a several gameboy rlm@11: games that all share the same battle system. Every pok\eacute{}mon in rlm@11: the gameboy game has one or two /types/, such as Ground, Fire, Water, rlm@11: etc. Every pok\eacute{}mon attack has exactly one type. Certain rlm@11: defending types are weak or strong to other attacking types. For rlm@11: example, Water attacks are strong against Fire pok\eacute{}mon, while rlm@11: Electric attacks are weak against Ground Pok\eacute{}mon. In the rlm@11: games, attacks can be either twice as effective as normal (Water rlm@11: vs. Fire), neutrally effective (Normal vs. Normal), half as effective rlm@11: (Fire vs. Water), or not effective at all (Electric vs. Ground). We rlm@12: represent these strengths and weaknesses as the numbers 2, 1, rlm@11: $\frac{1}{2}$, and 0, and call them the /susceptance/ of one type to rlm@11: another. rlm@0: rlm@11: If a pokemon has two types, then the strengths and weakness of each rlm@11: type are /multiplied/ together. Thus Electric (2x weak to Ground) rlm@11: combined with Flying (immune to Ground (0x)) is immune to Ground. rlm@11: Fire (2x weak to Water) combined with Water (1/2x resistant to Water) rlm@11: is neutral to Water. If both types are resistant to another type, then rlm@11: the combination is doubly-resistant (1/4x) to that type. If both types rlm@11: are weak to a certain type then the combination is double-weak (4x) to rlm@11: that type. rlm@11: rlm@11: In the [[./types.org][previous post]], we used the best-first search algorithm to find rlm@11: the most effective Pok\eacute{}mon type combinations. Afterwards, we rlm@11: realized that we could transform this search problem into a /linear rlm@12: optimization problem/. This conversion offers several advantages: rlm@11: first, search algorithms are comparatively slow, whereas linear rlm@11: optimization algorithms are extremely fast; second, it is difficult to rlm@11: determine whether a search problem has any solution, whereas it is rlm@11: straightforward to determine whether a linear optimization problem has rlm@11: any solution; finally, because systems of linear equations are so rlm@11: common, many programming languages have linear equation solvers rlm@11: written for them. rlm@11: rlm@11: In this article, we will: rlm@0: - Solve a simple linear optimization problem in C :: We demonstrate rlm@0: how to use the linear programming C library, =lp_solve=, to rlm@11: solve a simple linear optimization problem. rlm@0: - Incorporate a C library into Clojure :: We will show how we gave rlm@0: Clojure access to the linear programming C library, =lp_solve=. rlm@0: - Find effective Pokemon types using linear programming :: Building rlm@11: on our earlier code, we answer some questions that were rlm@11: impossible to answer using best-first search. rlm@11: - Present our results :: We found some cool examples and learned a lot rlm@11: about the pok\eacute{}mon type system as a whole. rlm@0: rlm@0: rlm@0: ** Immortal Types rlm@13: rlm@11: In the game, pok\eacute{}mon can have either one type or two types. If rlm@11: this restriction is lifted, is there any combination of types that is rlm@0: resistant to all types? I call such a combination an /Immortal Type/, rlm@0: since if that type's pattern was repeated over and over again towards rlm@0: infinity, the resulting type would be immune to all attack types. rlm@0: rlm@0: * Linear Programming rlm@0: rlm@0: Linear programming is the process of finding an optimal solution to a rlm@0: linear equation of several variables which are constrained by some linear rlm@0: inequalities. rlm@0: rlm@0: ** The Farmer's Problem rlm@0: rlm@10: Let's solve the Farmer's Problem, an example linear programming problem rlm@0: borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm. rlm@0: rlm@0: rlm@0: #+BEGIN_QUOTE rlm@0: *The Farmer's Problem:* Suppose a farmer has 75 acres on which to rlm@0: plant two crops: wheat and barley. To produce these crops, it costs rlm@0: the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat rlm@0: and $210 per acre for the barley. The farmer has $15000 available for rlm@0: expenses. But after the harvest, the farmer must store the crops while rlm@0: awaiting favorable market conditions. The farmer has storage space rlm@0: for 4000 bushels. Each acre yields an average of 110 bushels of wheat rlm@0: or 30 bushels of barley. If the net profit per bushel of wheat (after rlm@0: all expenses have been subtracted) is $1.30 and for barley is $2.00, rlm@0: how should the farmer plant the 75 acres to maximize profit? rlm@0: #+END_QUOTE rlm@0: rlm@0: The Farmer's Problem is to maximize profit subject to constraints on rlm@0: available farmland, funds for expenses, and storage space. rlm@0: rlm@0: | | Wheat | Barley | Maximum total | rlm@0: |----------+----------------------+---------------------+--------------| rlm@0: | / | < | > | <> | rlm@0: | Farmland | \(w\) acres | \(b\) acres | 75 acres | rlm@0: | Expense | $120 per acre | $210 per acre | $15000 | rlm@0: | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels | rlm@0: |----------+----------------------+---------------------+--------------| rlm@0: | Profit | $1.30 per bushel | $2.00 per bushel | | rlm@0: rlm@0: ** Solution using LP Solve rlm@0: In a new file, =farmer.lp=, we list the variables and constraints rlm@0: of our problem using LP Solve syntax. rlm@0: rlm@3: #+begin_src lpsolve :tangle ../lp/farmer.lp rlm@0: /* Maximize Total Profit */ rlm@0: max: +143 wheat +60 barley; rlm@0: rlm@0: rlm@0: /* -------- Constraints --------*/ rlm@0: rlm@0: /* the farmer can't spend more money than he has */ rlm@0: +120 wheat +210 barley <= 15000; rlm@0: rlm@0: /* the harvest has to fit in his storage space */ rlm@0: +110 wheat +30 barley <= 4000; rlm@0: rlm@0: /* he can't use more acres than he owns */ rlm@0: +wheat +barley <= 75; rlm@0: #+end_src rlm@0: rlm@0: Running the =lp_solve= program on =farmer.lp= yields the following output. rlm@0: rlm@0: #+begin_src sh :exports both :results scalar rlm@11: lp_solve ~/proj/pokemon-types/lp/farmer.lp rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : rlm@0: : Value of objective function: 6315.62500000 rlm@0: : rlm@0: : Actual values of the variables: rlm@0: : wheat 21.875 rlm@0: : barley 53.125 rlm@0: rlm@0: This shows that the farmer can maximize his profit by planting 21.875 rlm@0: of the available acres with wheat and the remaining 53.125 acres with rlm@0: barley; by doing so, he will make $6315.62(5) in profit. rlm@0: rlm@0: * Incorporating =lp_solve= into Clojure rlm@0: rlm@11: There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java rlm@11: programs to use =lp_solve=. Although Clojure can use this Java API rlm@11: directly, the interaction between Java, C, and Clojure is clumsy: rlm@0: rlm@0: ** The Farmer's Problem in Clojure rlm@11: rlm@0: We are going to solve the same problem involving wheat and barley, rlm@11: that we did above, but this time using clojure and the =lp_solve= API. rlm@0: rlm@0: #+srcname: intro rlm@0: #+begin_src clojure :results silent rlm@0: (ns pokemon.lpsolve rlm@11: (:use [clojure.contrib def set [seq :only [indexed]] pprint]) rlm@11: (:import lpsolve.LpSolve) rlm@11: (:require pokemon.types) rlm@13: (:require incanter.core trans) rlm@13: (:require rlm.map-utils)) rlm@0: #+end_src rlm@0: rlm@11: The =lp_solve= Java interface is available from the same site as rlm@11: =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the rlm@11: same as many other =C= programs. There are excellent instructions to rlm@11: get set up. The short version is that you must call Java with rlm@0: =-Djava.library.path=/path/to/lpsolve/libraries= and also add the rlm@0: libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For rlm@0: example, in my =.bashrc= file, I have the line rlm@11: =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything rlm@11: is set-up correctly, rlm@0: rlm@0: #+srcname: body rlm@0: #+begin_src clojure :results verbatim :exports both rlm@0: (import 'lpsolve.LpSolve) rlm@0: #+end_src rlm@0: rlm@0: #+results: body rlm@0: : lpsolve.LpSolve rlm@0: rlm@0: should run with no problems. rlm@0: rlm@0: ** Making a DSL to talk with LpSolve rlm@0: *** Problems rlm@0: Since we are using a =C= wrapper, we have to deal with manual memory rlm@0: management for the =C= structures which are wrapped by the =LpSolve= rlm@0: object. Memory leaks in =LpSolve= instances can crash the JVM, so it's rlm@0: very important to get it right. Also, the Java wrapper follows the rlm@0: =C= tradition closely and defines many =static final int= constants rlm@0: for the different states of the =LpSolve= instance instead of using Java rlm@0: enums. The calling convention for adding rows and columns to rlm@0: the constraint matrix is rather complicated and must be done column by rlm@0: column or row by row, which can be error prone. Finally, I'd like to rlm@0: gather all the important output information from the =LpSolve= instance rlm@0: into a final, immutable structure. rlm@0: rlm@0: In summary, the issues I'd like to address are: rlm@0: rlm@0: - reliable memory management rlm@0: - functional interface to =LpSolve= rlm@0: - intelligible, immutable output rlm@0: rlm@0: To deal with these issues I'll create four functions for interfacing rlm@0: with =LpSolve= rlm@0: rlm@0: #+srcname: declares rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: ;; deal with automatic memory management for LpSolve instance. rlm@0: (declare linear-program) rlm@0: rlm@0: ;; functional interface to LpSolve rlm@0: (declare lp-solve) rlm@0: rlm@0: ;; immutable output from lp-solve rlm@0: (declare solve get-results) rlm@0: #+end_src rlm@0: rlm@11: rlm@0: *** Memory Management rlm@0: rlm@0: Every instance of =LpSolve= must be manually garbage collected via a rlm@0: call to =deleteLP=. I use a non-hygienic macro similar to =with-open= rlm@0: to ensure that =deleteLP= is always called. rlm@0: rlm@0: #+srcname: memory-management rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: (defmacro linear-program rlm@0: "solve a linear programming problem using LpSolve syntax. rlm@0: within the macro, the variable =lps= is bound to the LpSolve instance." rlm@0: [& statements] rlm@0: (list 'let '[lps (LpSolve/makeLp 0 0)] rlm@0: (concat '(try) rlm@0: statements rlm@0: ;; always free the =C= data structures. rlm@0: '((finally (.deleteLp lps)))))) rlm@0: #+end_src rlm@0: rlm@0: rlm@0: The macro captures the variable =lps= within its body, providing for a rlm@0: convenient way to access the object using any of the methods of the rlm@0: =LpSolve= API without having to worry about when to call rlm@0: =deleteLP=. rlm@0: rlm@0: *** Sensible Results rlm@0: The =linear-program= macro deletes the actual =lps= object once it is rlm@0: done working, so it's important to collect the important results and rlm@0: add return them in an immutable structure at the end. rlm@0: rlm@0: #+srcname: get-results rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: (defrecord LpSolution rlm@0: [objective-value rlm@0: optimal-values rlm@0: variable-names rlm@0: solution rlm@0: status rlm@0: model]) rlm@0: rlm@0: (defn model rlm@0: "Returns a textual representation of the problem suitable for rlm@0: direct input to the =lp_solve= program (lps format)" rlm@0: [#^LpSolve lps] rlm@0: (let [target (java.io.File/createTempFile "lps" ".lp")] rlm@0: (.writeLp lps (.getPath target)) rlm@0: (slurp target))) rlm@0: rlm@0: (defn results rlm@11: "Given an LpSolve object, solves the object and returns a map of the rlm@0: essential values which compose the solution." rlm@0: [#^LpSolve lps] rlm@0: (locking lps rlm@0: (let [status (solve lps) rlm@0: number-of-variables (.getNcolumns lps) rlm@0: optimal-values (double-array number-of-variables) rlm@0: optimal-values (do (.getVariables lps optimal-values) rlm@0: (seq optimal-values)) rlm@0: variable-names rlm@11: (doall rlm@11: ;; The doall is necessary since the lps object might rlm@11: ;; soon be deleted. rlm@0: (map rlm@0: #(.getColName lps (inc %)) rlm@0: (range number-of-variables))) rlm@0: model (model lps)] rlm@0: (LpSolution. rlm@0: (.getObjective lps) rlm@0: optimal-values rlm@0: variable-names rlm@0: (zipmap variable-names optimal-values) rlm@0: status rlm@0: model)))) rlm@0: rlm@0: #+end_src rlm@0: rlm@0: Here I've created an object called =LpSolution= which stores the rlm@0: important results from a session with =lp_solve=. Of note is the rlm@0: =model= function which returns the problem in a form that can be rlm@0: solved by other linear programming packages. rlm@0: rlm@0: *** Solution Status of an LpSolve Object rlm@0: rlm@0: #+srcname: solve rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: (defn static-integer? rlm@0: "does the field represent a static integer constant?" rlm@0: [#^java.lang.reflect.Field field] rlm@0: (and (java.lang.reflect.Modifier/isStatic (.getModifiers field)) rlm@0: (integer? (.get field nil)))) rlm@0: rlm@0: (defn integer-constants [class] rlm@0: (filter static-integer? (.getFields class))) rlm@0: rlm@0: (defn-memo constant-map rlm@0: "Takes a class and creates a map of the static constant integer rlm@0: fields with their names. This helps with C wrappers where they have rlm@13: just defined a bunch of integer constants instead of enums." rlm@0: [class] rlm@0: (let [integer-fields (integer-constants class)] rlm@0: (into (sorted-map) rlm@0: (zipmap (map #(.get % nil) integer-fields) rlm@0: (map #(.getName %) integer-fields))))) rlm@0: rlm@0: (defn solve rlm@0: "Solve an instance of LpSolve and return a string representing the rlm@0: status of the computation. Will only solve a particular LpSolve rlm@0: instance once." rlm@0: [#^LpSolve lps] rlm@0: ((constant-map LpSolve) rlm@0: (.solve lps))) rlm@0: rlm@0: #+end_src rlm@0: rlm@0: The =.solve= method of an =LpSolve= object only returns an integer code rlm@0: to specify the status of the computation. The =solve= method here rlm@0: uses reflection to look up the actual name of the status code and rlm@0: returns a more helpful status message that is also resistant to rlm@0: changes in the meanings of the code numbers. rlm@0: rlm@0: *** The Farmer Example in Clojure, Pass 1 rlm@0: rlm@0: Now we can implement a nicer version of the examples from the rlm@0: [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less rlm@0: line-by-line translation of the Java code from that example. rlm@0: rlm@0: #+srcname: farmer-example rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: (defn farmer-example [] rlm@0: (linear-program rlm@0: (results rlm@0: (doto lps rlm@0: ;; name the columns rlm@0: (.setColName 1 "wheat") rlm@0: (.setColName 2 "barley") rlm@0: (.setAddRowmode true) rlm@0: ;; row 1 : 120x + 210y <= 15000 rlm@0: (.addConstraintex 2 rlm@0: (double-array [120 210]) rlm@0: (int-array [1 2]) rlm@0: LpSolve/LE rlm@0: 15e3) rlm@0: ;; row 2 : 110x + 30y <= 4000 rlm@0: (.addConstraintex 2 rlm@0: (double-array [110 30]) rlm@0: (int-array [1 2]) rlm@0: LpSolve/LE rlm@0: 4e3) rlm@0: ;; ;; row 3 : x + y <= 75 rlm@0: (.addConstraintex 2 rlm@0: (double-array [1 1]) rlm@0: (int-array [1 2]) rlm@0: LpSolve/LE rlm@0: 75) rlm@0: (.setAddRowmode false) rlm@0: rlm@0: ;; add constraints rlm@0: (.setObjFnex 2 rlm@0: (double-array [143 60]) rlm@0: (int-array [1 2])) rlm@0: rlm@0: ;; set this as a maximization problem rlm@0: (.setMaxim))))) rlm@0: rlm@0: #+end_src rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (:solution (pokemon.lpsolve/farmer-example))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : {"barley" 53.12499999999999, "wheat" 21.875} rlm@0: rlm@0: And it works as expected! rlm@0: rlm@0: *** The Farmer Example in Clojure, Pass 2 rlm@0: We don't have to worry about memory management anymore, and the farmer rlm@0: example is about half as long as the example from the =LpSolve= rlm@0: website, but we can still do better. Solving linear problems is all rlm@0: about the constraint matrix $A$ , the objective function $c$, and the rlm@0: right-hand-side $b$, plus whatever other options one cares to set for rlm@0: the particular instance of =lp_solve=. Why not make a version of rlm@0: =linear-program= that takes care of initialization? rlm@0: rlm@0: rlm@0: rlm@0: #+srcname: lp-solve rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: (defn initialize-lpsolve-row-oriented rlm@0: "fill in an lpsolve instance using a constraint matrix =A=, the rlm@0: objective function =c=, and the right-hand-side =b=" rlm@0: [#^LpSolve lps A b c] rlm@0: ;; set the name of the last column to _something_ rlm@0: ;; this appears to be necessary to ensure proper initialization. rlm@0: (.setColName lps (count c) (str "C" (count c))) rlm@0: rlm@0: ;; This is the recommended way to "fill-in" an lps instance from the rlm@0: ;; documentation. First, set row mode, then set the objective rlm@0: ;; function, then set each row of the problem, and then turn off row rlm@0: ;; mode. rlm@0: (.setAddRowmode lps true) rlm@0: (.setObjFnex lps (count c) rlm@0: (double-array c) rlm@0: (int-array (range 1 (inc (count c))))) rlm@0: (dorun rlm@0: (for [n (range (count A))] rlm@0: (let [row (nth A n) rlm@0: row-length (int (count row))] rlm@0: (.addConstraintex lps rlm@0: row-length rlm@0: (double-array row) rlm@0: (int-array (range 1 (inc row-length))) rlm@0: LpSolve/LE rlm@0: (double (nth b n)))))) rlm@0: (.setAddRowmode lps false) rlm@0: lps) rlm@0: rlm@0: rlm@0: (defmacro lp-solve rlm@0: "by default:, rlm@0: minimize (* c x), subject to (<= (* A x) b), rlm@0: using continuous variables. You may set any number of rlm@0: other options as in the LpSolve API." rlm@0: [A b c & lp-solve-forms] rlm@0: ;; assume that A is a vector of vectors rlm@0: (concat rlm@0: (list 'linear-program rlm@0: (list 'initialize-lpsolve-row-oriented 'lps A b c)) rlm@0: `~lp-solve-forms)) rlm@0: #+end_src rlm@0: rlm@0: Now, we can use a much more functional approach to solving the rlm@0: farmer's problem: rlm@0: rlm@0: #+srcname: better-farmer rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: (defn better-farmer-example [] rlm@0: (lp-solve [[120 210] rlm@0: [110 30] rlm@0: [1 1]] rlm@0: [15000 rlm@0: 4000 rlm@0: 75] rlm@0: [143 60] rlm@0: (.setColName lps 1 "wheat") rlm@0: (.setColName lps 2 "barley") rlm@0: (.setMaxim lps) rlm@0: (results lps))) rlm@0: #+end_src rlm@0: rlm@0: #+begin_src clojure :exports both :results verbatim rlm@0: (vec (:solution (pokemon.lpsolve/better-farmer-example))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : [["barley" 53.12499999999999] ["wheat" 21.875]] rlm@0: rlm@0: Notice that both the inputs to =better-farmer-example= and the results rlm@0: are immutable. rlm@0: rlm@0: * Using LpSolve to find Immortal Types rlm@11: ** Converting the Pok\eacute{}mon problem into a linear form rlm@0: How can the original question about pok\eacute{}mon types be converted rlm@11: into a linear problem? rlm@0: rlm@0: Pokemon types can be considered to be vectors of numbers representing rlm@0: their susceptances to various attacking types, so Water might look rlm@0: something like this. rlm@0: rlm@0: #+begin_src clojure :results scalar :exports both rlm@0: (:water (pokemon.types/defense-strengths)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5] rlm@0: rlm@0: Where the numbers represent the susceptibility of Water to the rlm@0: attacking types in the following order: rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/type-names)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: [:normal rlm@0: :fire rlm@0: :water rlm@0: :electric rlm@0: :grass rlm@0: :ice rlm@0: :fighting rlm@0: :poison rlm@0: :ground rlm@0: :flying rlm@0: :psychic rlm@0: :bug rlm@0: :rock rlm@0: :ghost rlm@0: :dragon rlm@0: :dark rlm@0: :steel] rlm@0: #+end_example rlm@0: rlm@0: rlm@13: So, for example, Water is resistant (x0.5) against Fire, which is rlm@0: the second element in the list. rlm@0: rlm@0: To combine types, these sorts of vectors are multiplied together rlm@0: pair-wise to yield the resulting combination. rlm@0: rlm@0: Unfortunately, we need some way to add two type vectors together rlm@0: instead of multiplying them if we want to solve the problem with rlm@0: =lp_solve=. Taking the log of the vector does just the trick. rlm@0: rlm@0: If we make a matrix with each column being the log (base 2) of the rlm@0: susceptance of each type, then finding an immortal type corresponds to rlm@0: setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1) rlm@0: and setting the constraint vector $c$ to all ones, which means that we rlm@0: want to find the immortal type which uses the least amount of types. rlm@0: rlm@0: #+srcname: pokemon-lp rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: (defn log-clamp-matrix [matrix] rlm@0: ;; we have to clamp the Infinities to a more reasonable negative rlm@0: ;; value because lp_solve does not play well with infinities in its rlm@0: ;; constraint matrix. rlm@0: (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row)) rlm@0: (incanter.core/log2 rlm@0: (incanter.core/trans rlm@0: matrix)))) rlm@0: rlm@0: ;; constraint matrices rlm@0: (defn log-defense-matrix [] rlm@0: (log-clamp-matrix rlm@0: (doall (map (pokemon.types/defense-strengths) rlm@0: (pokemon.types/type-names))))) rlm@0: rlm@0: (defn log-attack-matrix [] rlm@0: (incanter.core/trans (log-defense-matrix))) rlm@0: rlm@0: ;; target vectors rlm@0: (defn all-resistant [] rlm@0: (doall (map (constantly -1) (pokemon.types/type-names)))) rlm@0: rlm@0: (defn all-weak [] rlm@0: (doall (map (constantly 1) (pokemon.types/type-names)))) rlm@0: rlm@0: (defn all-neutral [] rlm@0: (doall (map (constantly 0) (pokemon.types/type-names)))) rlm@0: rlm@0: ;; objective functions rlm@0: (defn number-of-types [] rlm@0: (doall (map (constantly 1) (pokemon.types/type-names)))) rlm@0: rlm@0: (defn set-constraints rlm@0: "sets all the constraints for an lpsolve instance to the given rlm@0: constraint. =constraint= here is one of the LpSolve constants such rlm@0: as LpSolve/EQ." rlm@0: [#^LpSolve lps constraint] rlm@0: (dorun (map (fn [index] (.setConstrType lps index constraint)) rlm@0: ;; ONE based indexing!!! rlm@0: (range 1 (inc (.getNrows lps)))))) rlm@0: rlm@0: rlm@0: (defn set-discrete rlm@0: "sets every variable in an lps problem to be a discrete rather than rlm@0: continuous variable" rlm@0: [#^LpSolve lps] rlm@0: (dorun (map (fn [index] (.setInt lps index true)) rlm@0: ;; ONE based indexing!!! rlm@0: (range 1 (inc (.getNcolumns lps)))))) rlm@0: rlm@0: (defn set-variable-names rlm@0: "sets the variable names of the problem given a vector of names" rlm@0: [#^LpSolve lps names] rlm@0: (dorun rlm@0: (map (fn [[index name]] rlm@0: (.setColName lps (inc index) (str name))) rlm@0: ;; ONE based indexing!!! rlm@0: (indexed names)))) rlm@0: rlm@0: (defn poke-solve rlm@0: ([poke-matrix target objective-function constraint min-num-types] rlm@0: ;; must have at least one type rlm@0: (let [poke-matrix rlm@0: (concat poke-matrix rlm@0: [(map (constantly 1) rlm@0: (range (count (first poke-matrix))))]) rlm@0: target (concat target [min-num-types])] rlm@0: (lp-solve poke-matrix target objective-function rlm@0: (set-constraints lps constraint) rlm@0: ;; must have more than min-num-types rlm@0: (.setConstrType lps (count target) LpSolve/GE) rlm@0: (set-discrete lps) rlm@0: (set-variable-names lps (pokemon.types/type-names)) rlm@0: (results lps)))) rlm@0: ([poke-matrix target objective-function constraint] rlm@0: ;; at least one type rlm@0: (poke-solve poke-matrix target objective-function constraint 1))) rlm@0: rlm@0: (defn solution rlm@0: "If the results of an lpsolve operation are feasible, returns the rlm@0: results. Otherwise, returns the error." rlm@0: [results] rlm@0: (if (not (= (:status results) "OPTIMAL")) rlm@0: (:status results) rlm@0: (:solution results))) rlm@0: #+end_src rlm@0: rlm@0: With this, we are finally able to get some results. rlm@0: rlm@0: ** Results rlm@0: #+srcname: results rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: (defn best-defense-type rlm@0: "finds a type combination which is resistant to all attacks." rlm@0: [] rlm@0: (poke-solve rlm@0: (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE)) rlm@0: rlm@0: (defn worst-attack-type rlm@0: "finds the attack type which is not-very-effective against all pure rlm@0: defending types (each single defending type is resistant to this rlm@0: attack combination" rlm@0: [] rlm@0: (poke-solve rlm@0: (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE)) rlm@0: rlm@0: (defn worst-defense-type rlm@0: "finds a defending type that is weak to all single attacking types." rlm@0: [] rlm@0: (poke-solve rlm@0: (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE)) rlm@0: rlm@0: (defn best-attack-type rlm@0: "finds an attack type which is super effective against all single rlm@0: defending types" rlm@0: [] rlm@0: (poke-solve rlm@0: (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE)) rlm@0: rlm@0: (defn solid-defense-type rlm@0: "finds a defense type which is either neutral or resistant to all rlm@0: single attacking types" rlm@0: [] rlm@0: (poke-solve rlm@0: (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE)) rlm@0: rlm@0: (defn solid-attack-type rlm@0: "finds an attack type which is either neutral or super-effective to rlm@0: all single attacking types." rlm@0: [] rlm@0: (poke-solve rlm@0: (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE)) rlm@0: rlm@0: (defn weak-defense-type rlm@0: "finds a defense type which is either neutral or weak to all single rlm@0: attacking types" rlm@0: [] rlm@0: (poke-solve rlm@0: (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE)) rlm@0: rlm@0: (defn neutral-defense-type rlm@0: "finds a defense type which is perfectly neutral to all attacking rlm@0: types." rlm@0: [] rlm@0: (poke-solve rlm@0: (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ)) rlm@0: rlm@0: #+end_src rlm@0: rlm@0: *** Strongest Attack/Defense Combinations rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 0.0, rlm@0: ":ground" 1.0, rlm@0: ":poison" 2.0, rlm@0: ":flying" 1.0, rlm@0: ":fighting" 0.0, rlm@0: ":dragon" 0.0, rlm@0: ":fire" 0.0, rlm@0: ":dark" 1.0, rlm@0: ":ice" 0.0, rlm@0: ":steel" 1.0, rlm@0: ":ghost" 0.0, rlm@0: ":electric" 0.0, rlm@0: ":bug" 0.0, rlm@0: ":psychic" 0.0, rlm@0: ":grass" 0.0, rlm@0: ":water" 2.0, rlm@0: ":rock" 0.0} rlm@0: #+end_example rlm@0: rlm@0: # #+results-old: rlm@0: # : [[":normal" 0.0] [":ground" 1.0] [":poison" 0.0] [":flying" 1.0] [":fighting" 0.0] [":dragon" 1.0] [":fire" 0.0] [":dark" 0.0] [":ice" 0.0] [":steel" 2.0] [":ghost" 1.0] [":electric" 0.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 2.0] [":rock" 0.0]] rlm@0: rlm@0: rlm@0: This is the immortal type combination we've been looking for. By rlm@0: combining Steel, Water, Poison, and three types which each have complete rlm@0: immunities to various other types, we've created a type that is resistant to rlm@0: all attacking types. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/susceptibility rlm@0: [:poison :poison :water :water :steel :ground :flying :dark])) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 0, rlm@0: :dragon 1/2, rlm@0: :fire 1/2, rlm@0: :ice 1/2, rlm@0: :grass 1/2, rlm@0: :ghost 1/4, rlm@0: :poison 0, rlm@0: :flying 1/2, rlm@0: :normal 1/2, rlm@0: :rock 1/2, rlm@0: :electric 0, rlm@0: :ground 0, rlm@0: :fighting 1/2, rlm@0: :dark 1/4, rlm@0: :steel 1/8, rlm@0: :bug 1/8} rlm@0: #+end_example rlm@0: rlm@0: # #+results-old: rlm@0: # : {:water 1/4, :psychic 1/4, :dragon 1/2, :fire 1/2, :ice 1/2, :grass 1/2, :ghost 1/2, :poison 0, :flying 1/4, :normal 0, :rock 1/4, :electric 0, :ground 0, :fighting 0, :dark 1/2, :steel 1/16, :bug 1/16} rlm@0: rlm@0: rlm@0: Cool! rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 0.0, rlm@0: ":ground" 0.0, rlm@0: ":poison" 0.0, rlm@0: ":flying" 0.0, rlm@0: ":fighting" 0.0, rlm@0: ":dragon" 0.0, rlm@0: ":fire" 0.0, rlm@0: ":dark" 1.0, rlm@0: ":ice" 0.0, rlm@0: ":steel" 0.0, rlm@0: ":ghost" 1.0, rlm@0: ":electric" 0.0, rlm@0: ":bug" 0.0, rlm@0: ":psychic" 0.0, rlm@0: ":grass" 0.0, rlm@0: ":water" 0.0, rlm@0: ":rock" 0.0} rlm@0: #+end_example rlm@0: rlm@0: Dark and Ghost are the best dual-type combo, and are resistant or rlm@0: neutral to all types. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 0.0, rlm@0: ":ground" 0.0, rlm@0: ":poison" 0.0, rlm@0: ":flying" 0.0, rlm@0: ":fighting" 0.0, rlm@0: ":dragon" 0.0, rlm@0: ":fire" 0.0, rlm@0: ":ice" 0.0, rlm@0: ":ghost" 1.0, rlm@0: ":electric" 0.0, rlm@0: ":bug" 0.0, rlm@0: ":psychic" 1.0, rlm@0: ":grass" 0.0, rlm@0: ":water" 0.0, rlm@0: ":rock" 0.0} rlm@0: #+end_example rlm@0: rlm@0: Ghost and Psychic are a powerful dual type combo in the original games, rlm@0: due to a glitch which made Psychic immune to Ghost type attacks, even rlm@14: though the game claims that Ghost is strong against Psychic. rlm@0: rlm@0: #+begin_src clojure :results verbatim :exports both rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :results verbatim :exports both rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: rlm@0: #+begin_src clojure :results verbatim :exports both rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type)))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 0.0, rlm@0: ":ground" 0.0, rlm@0: ":poison" 0.0, rlm@0: ":flying" 0.0, rlm@0: ":fighting" 0.0, rlm@0: ":dragon" 1.0, rlm@0: ":fire" 0.0, rlm@0: ":ice" 0.0, rlm@0: ":ghost" 0.0, rlm@0: ":electric" 0.0, rlm@0: ":bug" 0.0, rlm@0: ":psychic" 0.0, rlm@0: ":grass" 0.0, rlm@0: ":water" 0.0, rlm@0: ":rock" 0.0} rlm@0: #+end_example rlm@0: rlm@11: The best attacking type combination is Dragon from the original games. rlm@11: It is neutral against all the original types except for Dragon, which rlm@11: it is strong against. There is no way to make an attacking type that rlm@11: is strong against every type, or even one that is strong or neutral rlm@11: against every type, in the new games. rlm@0: rlm@0: rlm@0: *** Weakest Attack/Defense Combinations rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 5.0, rlm@0: ":ground" 0.0, rlm@0: ":poison" 0.0, rlm@0: ":flying" 0.0, rlm@0: ":fighting" 0.0, rlm@0: ":dragon" 0.0, rlm@0: ":fire" 1.0, rlm@0: ":ice" 2.0, rlm@0: ":ghost" 1.0, rlm@0: ":electric" 1.0, rlm@0: ":bug" 1.0, rlm@0: ":psychic" 0.0, rlm@0: ":grass" 3.0, rlm@0: ":water" 2.0, rlm@0: ":rock" 0.0} rlm@0: #+end_example rlm@0: rlm@0: # #+results-old: rlm@0: # : [[":normal" 5.0] [":ground" 1.0] [":poison" 0.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":ice" 4.0] [":ghost" 1.0] [":electric" 4.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 1.0] [":rock" 1.0]] rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {":normal" 4.0, rlm@0: ":ground" 1.0, rlm@0: ":poison" 1.0, rlm@0: ":flying" 0.0, rlm@0: ":fighting" 1.0, rlm@0: ":dragon" 0.0, rlm@0: ":fire" 0.0, rlm@0: ":dark" 0.0, rlm@0: ":ice" 4.0, rlm@0: ":steel" 0.0, rlm@0: ":ghost" 1.0, rlm@0: ":electric" 3.0, rlm@0: ":bug" 0.0, rlm@0: ":psychic" 1.0, rlm@0: ":grass" 1.0, rlm@0: ":water" 1.0, rlm@0: ":rock" 2.0} rlm@0: #+end_example rlm@0: rlm@0: # #+results-old: rlm@0: # : [[":normal" 4.0] [":ground" 1.0] [":poison" 1.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":dark" 0.0] [":ice" 5.0] [":steel" 0.0] [":ghost" 1.0] [":electric" 5.0] [":bug" 0.0] [":psychic" 1.0] [":grass" 0.0] [":water" 1.0] [":rock" 2.0]] rlm@0: rlm@0: rlm@0: This is an extremely interesting type combination, in that it uses rlm@0: quite a few types. rlm@0: rlm@0: #+begin_src clojure :results verbatim :exports both rlm@0: (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type)))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : 20.0 rlm@0: rlm@0: 20 types is the /minimum/ number of types before the attacking rlm@0: combination is not-very-effective or worse against all defending rlm@0: types. This would probably have been impossible to discover using rlm@0: best-first search, since it involves such an intricate type rlm@0: combination. rlm@0: rlm@0: It's so interesting that it takes 20 types to make an attack type that rlm@11: is weak to all types that the combination merits further rlm@11: investigation. rlm@0: rlm@0: Unfortunately, all of the tools that we've written so far are focused rlm@0: on defense type combinations. However, it is possible to make every rlm@0: tool attack-oriented via a simple macro. rlm@0: rlm@0: #+srcname: attack-oriented rlm@0: #+begin_src clojure :results silent rlm@0: (in-ns 'pokemon.lpsolve) rlm@0: rlm@0: (defmacro attack-mode [& forms] rlm@0: `(let [attack-strengths# pokemon.types/attack-strengths rlm@0: defense-strengths# pokemon.types/defense-strengths] rlm@0: (binding [pokemon.types/attack-strengths rlm@0: defense-strengths# rlm@0: pokemon.types/defense-strengths rlm@0: attack-strengths#] rlm@0: ~@forms))) rlm@0: #+end_src rlm@0: rlm@0: Now all the tools from =pokemon.types= will work for attack rlm@0: combinations. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.types/susceptibility [:water])) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 1, rlm@0: :dragon 1, rlm@0: :fire 1/2, rlm@0: :ice 1/2, rlm@0: :grass 2, rlm@0: :ghost 1, rlm@0: :poison 1, rlm@0: :flying 1, rlm@0: :normal 1, rlm@0: :rock 1, rlm@0: :electric 2, rlm@0: :ground 1, rlm@0: :fighting 1, rlm@0: :dark 1, rlm@0: :steel 1/2, rlm@0: :bug 1} rlm@0: #+end_example rlm@0: rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/attack-mode rlm@0: (pokemon.types/susceptibility [:water]))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 1, rlm@0: :dragon 1/2, rlm@0: :fire 2, rlm@0: :ice 1, rlm@0: :grass 1/2, rlm@0: :ghost 1, rlm@0: :poison 1, rlm@0: :flying 1, rlm@0: :normal 1, rlm@0: :rock 2, rlm@0: :electric 1, rlm@0: :ground 2, rlm@0: :fighting 1, rlm@0: :dark 1, rlm@0: :steel 1, rlm@0: :bug 1} rlm@0: #+end_example rlm@0: rlm@0: Now =pokemon.types/susceptibility= reports the /attack-type/ rlm@0: combination's effectiveness against other types. rlm@0: rlm@0: The 20 type combo achieves its goal in a very clever way. rlm@0: rlm@0: First, it weakens its effectiveness to other types at the expense of rlm@0: making it very strong against flying. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/attack-mode rlm@0: (pokemon.types/susceptibility rlm@0: [:normal :normal :normal :normal rlm@0: :ice :ice :ice :ice rlm@0: :electric :electric :electric rlm@0: :rock :rock]))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 1, rlm@0: :dragon 2, rlm@0: :fire 1/4, rlm@0: :ice 1/4, rlm@0: :grass 2, rlm@0: :ghost 0, rlm@0: :poison 1, rlm@0: :flying 512, rlm@0: :normal 1, rlm@0: :rock 1/16, rlm@0: :electric 1/8, rlm@0: :ground 0, rlm@0: :fighting 1/4, rlm@0: :dark 1, rlm@0: :steel 1/1024, rlm@0: :bug 4} rlm@0: #+end_example rlm@0: rlm@0: Then, it removes it's strengths against Flying, Normal, and Fighting rlm@0: by adding Ghost and Ground. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/attack-mode rlm@0: (pokemon.types/susceptibility rlm@0: [:normal :normal :normal :normal rlm@0: :ice :ice :ice :ice rlm@0: :electric :electric :electric rlm@0: :rock :rock rlm@0: ;; Spot resistances rlm@0: :ghost :ground]))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 2, rlm@0: :dragon 2, rlm@0: :fire 1/2, rlm@0: :ice 1/4, rlm@0: :grass 1, rlm@0: :ghost 0, rlm@0: :poison 2, rlm@0: :flying 0, rlm@0: :normal 0, rlm@0: :rock 1/8, rlm@0: :electric 1/4, rlm@0: :ground 0, rlm@0: :fighting 1/4, rlm@0: :dark 1/2, rlm@0: :steel 1/1024, rlm@0: :bug 2} rlm@0: #+end_example rlm@0: rlm@0: Adding the pair Psychic and Fighting takes care of its strength rlm@0: against Psychic and makes it ineffective against Dark, which is immune rlm@0: to Psychic. rlm@0: rlm@0: Adding the pair Grass and Poison makes takes care of its strength rlm@0: against poison and makes it ineffective against Steel, which is immune rlm@0: to poison. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/attack-mode rlm@0: (pokemon.types/susceptibility rlm@0: [;; setup rlm@0: :normal :normal :normal :normal rlm@0: :ice :ice :ice :ice rlm@0: :electric :electric :electric rlm@0: :rock :rock rlm@0: ;; Spot resistances rlm@0: :ghost :ground rlm@0: ;; Pair resistances rlm@0: :psychic :fighting rlm@0: :grass :poison]))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1, rlm@0: :psychic 1/2, rlm@0: :dragon 1, rlm@0: :fire 1/4, rlm@0: :ice 1/2, rlm@0: :grass 1, rlm@0: :ghost 0, rlm@0: :poison 1/2, rlm@0: :flying 0, rlm@0: :normal 0, rlm@0: :rock 1/4, rlm@0: :electric 1/4, rlm@0: :ground 0, rlm@0: :fighting 1/2, rlm@0: :dark 0, rlm@0: :steel 0, rlm@0: :bug 1/2} rlm@0: #+end_example rlm@0: rlm@0: Can you see the final step? rlm@0: rlm@13: It's adding the Water type, which is weak against Water, Dragon, and rlm@13: Grass and strong against Rock and Fire. rlm@0: rlm@0: #+begin_src clojure :results output :exports both rlm@0: (clojure.pprint/pprint rlm@0: (pokemon.lpsolve/attack-mode rlm@0: (pokemon.types/susceptibility rlm@0: [;; setup rlm@0: :normal :normal :normal :normal rlm@0: :ice :ice :ice :ice rlm@0: :electric :electric :electric rlm@0: :rock :rock rlm@0: ;; Spot resistances rlm@0: :ghost :ground rlm@0: ;; Pair resistances rlm@0: :psychic :fighting rlm@0: :grass :poison rlm@0: ;; completion rlm@0: :water]))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: #+begin_example rlm@0: {:water 1/2, rlm@0: :psychic 1/2, rlm@0: :dragon 1/2, rlm@0: :fire 1/2, rlm@0: :ice 1/2, rlm@0: :grass 1/2, rlm@0: :ghost 0, rlm@0: :poison 1/2, rlm@0: :flying 0, rlm@0: :normal 0, rlm@0: :rock 1/2, rlm@0: :electric 1/4, rlm@0: :ground 0, rlm@0: :fighting 1/2, rlm@0: :dark 0, rlm@0: :steel 0, rlm@0: :bug 1/2} rlm@0: #+end_example rlm@0: rlm@0: Which makes a particularly beautiful combination which is ineffective rlm@0: against all defending types. rlm@0: rlm@0: rlm@0: # #+begin_src clojure :results scalar :exports both rlm@0: # (with-out-str (clojure.contrib.pprint/pprint (seq (attack-mode (pokemon.types/susceptibility [:normal :normal :normal :normal :ice :ice :ice :ice :electric :electric :electric :rock :rock :ground :ghost :psychic :fighting :grass :poison]))))) rlm@0: # #+end_src rlm@0: rlm@0: # #+results: rlm@0: # | [:water 1] | [:psychic 1/2] | [:dragon 1] | [:fire 1/4] | [:ice 1/2] | [:grass 1] | [:ghost 0] | [:poison 1/2] | [:flying 0] | [:normal 0] | [:rock 1/4] | [:electric 1/4] | [:ground 0] | [:fighting 1/2] | [:dark 0] | [:steel 0] | [:bug 1/2] | rlm@0: rlm@0: rlm@0: Is there anything else that's interesting? rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: #+begin_src clojure :exports both rlm@0: (pokemon.types/old-school rlm@0: (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))) rlm@0: #+end_src rlm@0: rlm@0: #+results: rlm@0: : INFEASIBLE rlm@0: rlm@0: There is no way to produce a defense-type that is weak to all types. rlm@0: This is probably because there are many types that are completely rlm@0: immune to some types, such as Flying, which is immune to Ground. A rlm@0: perfectly weak type could not use any of these types. rlm@0: rlm@0: * Summary rlm@0: rlm@14: Overall, the pok\eacute{}mon type system is slanted towards defense rlm@14: rather than offense. While it is possible to create superior rlm@11: defensive types and exceptionally weak attack types, it is not rlm@11: possible to create exceptionally weak defensive types or very powerful rlm@11: attack types. rlm@0: rlm@0: Using the =lp_solve= library was more complicated than the best-first rlm@0: search, but yielded results quickly and efficiently. Expressing the rlm@0: problem in a linear form does have its drawbacks, however --- it's rlm@0: hard to ask questions such as "what is the best 3-type defensive combo rlm@0: in terms of susceptibility?", since susceptibility is not a linear rlm@0: function of a combo's types. It is also hard to get all the solutions rlm@0: to a particular problem, such as all the pokemon type combinations of rlm@0: length 8 which are immortal defense types. rlm@0: rlm@0: * COMMENT main-program rlm@0: #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: <> rlm@0: #+end_src rlm@0: rlm@0: rlm@0: