rlm@0: #+title: Discovering Effective Pokemon Types Using Linear Optimization rlm@0: #+author: Robert McIntyre & Dylan Holmes rlm@0: #+EMAIL: rlm@mit.edu rlm@0: #+MATHJAX: align:"left" mathml:t path:"../MathJax/MathJax.js" rlm@0: #+STYLE: rlm@0: #+OPTIONS: H:3 num:t toc:t \n:nil @:t ::t |:t ^:t -:t f:t *:t <:t rlm@0: #+SETUPFILE: ../templates/level-0.org rlm@0: #+INCLUDE: ../templates/level-0.org rlm@0: rlm@0: rlm@0: rlm@0: * Introduction rlm@0: In the [[./types.org][previous post]], we used the best-first search algorithm to rlm@0: locate the most effective Pok\eacute{}mon type rlm@0: combinations. Afterwards, we realized that we could transform this rlm@0: search problem into a /linear optimization problem/. This conversion rlm@0: offered several advantages: first, search algorithms are comparatively rlm@0: slow, whereas linear optimization algorithms are extremely fast; rlm@0: second, it is difficult to determine whether a search problem has any rlm@0: solution, whereas it is straightforward to determine whether a linear rlm@0: optimization problem has any solution; finally, because systems of rlm@0: linear equations are so common, many programming languages have linear rlm@0: equation solvers written for them. rlm@0: rlm@0: 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@0: solve a simple linear rlm@0: 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@0: on our earlier code, (...) rlm@0: - Present our results :: (!) rlm@0: rlm@0: #which can be represented and solved as a system of linear equations. rlm@0: rlm@0: * COMMENT rlm@0: This post continues the [[./types.org][previous one]] about pok\eacute{}mon types. rlm@0: Pok\eacute{}mon is a game in which adorable creatures battle each rlm@0: other using fantastic attacks. It was made into a several gameboy rlm@0: games that all share the same battle system. Every pok\eacute{}mon in the rlm@0: gameboy game has one or two /types/, such as Ground, Fire, Water, rlm@0: etc. Every pok\eacute{}mon attack has exactly one type. Certain defending rlm@0: types are weak or strong to other attacking types. For example, Water rlm@0: attacks are strong against Fire pok\eacute{}mon, while Electric attacks are rlm@0: weak against Ground Pok\eacute{}mon. In the games, attacks can be either rlm@0: twice as effective as normal (Water vs. Fire), neutrally effective rlm@0: (Normal vs. Normal), half as effective (Fire vs. Water), or not rlm@0: effective at all (Electric vs. Ground). Thus the range of defense rlm@0: values for a particular type is the set 0, 1/2, 1, 2. These are rlm@0: referred to in the game as being immune, resistant, neutral, and rlm@0: weak, respectively. I call them the /susceptance/ of one type to another. rlm@0: rlm@0: If a pokemon has two types, then the strengths and weakness of each rlm@0: type are /multiplied/ together. Thus Electric (2x weak to Ground) rlm@0: combined with Flying (immune to Ground (0x)) is immune to rlm@0: Ground. Fire (2x weak to Water) combined with Water (1/2x resistant rlm@0: to Water) is neutral to Water. If both types are resistant to another rlm@0: type, then the combination is doubly-resistant (1/4x) to that type. If rlm@0: both types are weak to a certain type then the combination is rlm@0: double-weak (4x) to that type. rlm@0: rlm@0: ** Immortal Types rlm@0: rlm@0: In the game, pok\eacute{}mon can have either one type, or two types. If this rlm@0: 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: ** Terminology 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: In linear programming terminology, the function to be extremized is rlm@0: the /objective function/. rlm@0: rlm@0: ** COMMENT rlm@0: First, we'll give a small example of a linear optimization problem, rlm@0: and show how it can be solved with Clojure and =lp_solve=. Then, rlm@0: we'll show how finding immortal pok\eacute{}mon types can be converted rlm@0: into a linear problem suitable for solving in the same way. rlm@0: rlm@0: ** The Farmer's Problem rlm@0: rlm@0: Let's solve the Farmer's Problem, a typical 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: *** COMMENT rlm@0: can be represented as a linear optimization rlm@0: problem. In this form, it is a problem with two variables\mdash{}the number of rlm@0: acres of wheat, \(w\), and the number of acres of barley, \(b\). The rlm@0: aim is to maximize profit, which rlm@0: rlm@0: subject to three constraints: the farmer can't spend more money rlm@0: than he has, the farmer can't use more acres than he owns, and the harvest has rlm@0: to fit in his storage space. rlm@0: rlm@0: We can express these constraints succinctly using matrix rlm@0: notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\), rlm@0: we want to maximize the expression \(143 w + 60 b\) subject to rlm@0: rlm@0: \( rlm@0: \begin{cases} rlm@0: 120 w + 210 b & \leq & 1500\\ rlm@0: 110 w + 30 b & \leq & 4000\\ rlm@0: 1 w + 1 w & \leq & 75 rlm@0: \end{cases} rlm@0: \) rlm@0: rlm@0: #\(\begin{bmatrix}120&210\\110&30\\1 & rlm@0: #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix} rlm@0: #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\) rlm@0: rlm@0: rlm@0: ** Solution using LP Solve rlm@0: #(LP solve is available at http://www.example.com.) 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@0: #+begin_src lpsolve :tangle 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: rlm@0: #This is a set of linear equations ideal for solving using a program like rlm@0: #=lp_solve=. In Linear Algebra terms we are maximizing the linear function rlm@0: rlm@0: #\(\text{profit} = 143\text{ wheat} + 60\text{ barley}\), subject to the constraints rlm@0: rlm@0: #Ax <= b, rlm@0: rlm@0: #where A is [120 210 110 30 1 1], x is [wheat barley] and b is [15000 rlm@0: #4000 75]. 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@0: ~/roBin/lpsolve/lp_solve ~/aurellem/src/pokemon/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: rlm@0: #The farmer can make a profit of $6315.62 by planting 21.875 acres of rlm@0: #his land with wheat and the other 53.125 acres of his land with barley. rlm@0: rlm@0: * Incorporating =lp_solve= into Clojure rlm@0: rlm@0: There is a Java API available which enables Java programs to use Lp rlm@0: Solve. Although Clojure can use this Java API directly, the rlm@0: interaction between Java, C, and Clojure is clumsy: rlm@0: rlm@0: rlm@0: The Java API for LP Solve makes it possible to use Lp Solve algorithms rlm@0: within Java. Although Clojure can use this Java API directly, rlm@0: rlm@0: rlm@0: ** The Farmer's Problem in Clojure rlm@0: We are going to solve the same problem involving wheat and barley, rlm@0: that we did above, but this time using clojure and the lpsolve API. rlm@0: rlm@0: #Because we ultimately intend to use Lp Solve to find optimal Pokemon type combinations. rlm@0: # we want to solve linear optimization problems within Clojure, the language rlm@0: rlm@0: ** Setup rlm@0: =lp_solve= is a crufty =C= program which already comes with a JNI rlm@0: interface written by Juergen Ebert. It's API is not rlm@0: particularly friendly from a functional/immutable perspective, but rlm@0: with a little work, we can build something that works great with rlm@0: clojure. rlm@0: rlm@0: #+srcname: intro rlm@0: #+begin_src clojure :results silent rlm@0: (ns pokemon.lpsolve rlm@0: (:use rlm.ns-rlm)) rlm@0: (rlm.ns-rlm/ns-clone rlm.light-base) rlm@0: (use 'clojure.set) rlm@0: (import 'lpsolve.LpSolve) rlm@0: (use '[clojure [pprint :only [pprint]]]) rlm@0: #+end_src rlm@0: rlm@0: The LpSolve Java interface is available from the same site as rlm@0: =lp_solve= itself, http://lpsolve.sourceforge.net/ rlm@0: Using it is the same as many other =C= rlm@0: programs. There are excellent instructions to get set rlm@0: 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@0: =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. rlm@0: If everything 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@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@0: "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@0: (doall ;; the doall is necessary since the lps object might rlm@0: ;; 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@0: 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@0: ** Converting the Pokemon problem into a linear form rlm@0: How can the original question about pok\eacute{}mon types be converted rlm@0: 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@0: 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: (require 'pokemon.types) rlm@0: (require 'incanter.core) 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: 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: 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@0: though the game claims that Ghost is strong to 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@0: The best attacking type combination is strangely Dragon from the rlm@0: original games. It is neutral against all the original types except rlm@0: for Dragon, which it is strong against. There is no way to make an rlm@0: attacking type that is strong against every type, or even one that is rlm@0: strong or neutral 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@0: is weak to all types that the combination merits further 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@0: It's adding the Water type, which is weak against Water and Dragon and rlm@0: 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@0: Overall, the pok\eacute{}mon type system is slanted more towards defense rlm@0: rather than offense. While it is possible to create superior rlm@0: defensive types and exceptionally weak attack types, it is not possible to rlm@0: create exceptionally weak defensive types or very powerful attack rlm@0: 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: 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: * COMMENT Stuff to do. rlm@0: ** TODO fix namespaces to not use rlm.light-base rlm@0: