Mercurial > pokemon-types
view org/lpsolve.org @ 11:4ea23241ff5b
cleaned up prose
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Wed, 02 Nov 2011 10:28:50 -0700 |
parents | eedd6897197d |
children | 89462678a932 |
line wrap: on
line source
1 #+title: Discovering Effective Pok\eacute{}mon Types Using Linear Optimization2 #+author: Robert McIntyre & Dylan Holmes3 #+EMAIL: rlm@mit.edu4 #+description: Using Lpsolve to func effective pokemon types in clojure.5 #+keywords: Pokemon, clojure, linear optimization, lp_spolve, LpSolve6 #+SETUPFILE: ../../aurellem/org/setup.org7 #+INCLUDE: ../../aurellem/org/level-0.org11 * Introduction12 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.13 Pok\eacute{}mon is a game in which adorable creatures battle each14 other using fantastic attacks. It was made into a several gameboy15 games that all share the same battle system. Every pok\eacute{}mon in16 the gameboy game has one or two /types/, such as Ground, Fire, Water,17 etc. Every pok\eacute{}mon attack has exactly one type. Certain18 defending types are weak or strong to other attacking types. For19 example, Water attacks are strong against Fire pok\eacute{}mon, while20 Electric attacks are weak against Ground Pok\eacute{}mon. In the21 games, attacks can be either twice as effective as normal (Water22 vs. Fire), neutrally effective (Normal vs. Normal), half as effective23 (Fire vs. Water), or not effective at all (Electric vs. Ground). We24 represent these strengths and weakessness as the numbers 2, 1,25 $\frac{1}{2}$, and 0, and call them the /susceptance/ of one type to26 another.28 If a pokemon has two types, then the strengths and weakness of each29 type are /multiplied/ together. Thus Electric (2x weak to Ground)30 combined with Flying (immune to Ground (0x)) is immune to Ground.31 Fire (2x weak to Water) combined with Water (1/2x resistant to Water)32 is neutral to Water. If both types are resistant to another type, then33 the combination is doubly-resistant (1/4x) to that type. If both types34 are weak to a certain type then the combination is double-weak (4x) to35 that type.37 In the [[./types.org][previous post]], we used the best-first search algorithm to find38 the most effective Pok\eacute{}mon type combinations. Afterwards, we39 realized that we could transform this search problem into a /linear40 optimization problem/. This conversion offeres several advantages:41 first, search algorithms are comparatively slow, whereas linear42 optimization algorithms are extremely fast; second, it is difficult to43 determine whether a search problem has any solution, whereas it is44 straightforward to determine whether a linear optimization problem has45 any solution; finally, because systems of linear equations are so46 common, many programming languages have linear equation solvers47 written for them.49 In this article, we will:50 - Solve a simple linear optimization problem in C :: We demonstrate51 how to use the linear programming C library, =lp_solve=, to52 solve a simple linear optimization problem.53 - Incorporate a C library into Clojure :: We will show how we gave54 Clojure access to the linear programming C library, =lp_solve=.55 - Find effective Pokemon types using linear programming :: Building56 on our earlier code, we answer some questions that were57 impossible to answer using best-first search.58 - Present our results :: We found some cool examples and learned a lot59 about the pok\eacute{}mon type system as a whole.62 ** Immortal Types64 In the game, pok\eacute{}mon can have either one type or two types. If65 this restriction is lifted, is there any combination of types that is66 resistant to all types? I call such a combination an /Immortal Type/,67 since if that type's pattern was repeated over and over again towards68 infinity, the resulting type would be immune to all attack types.70 * Linear Programming72 Linear programming is the process of finding an optimal solution to a73 linear equation of several variables which are constrained by some linear74 inequalities.76 ** The Farmer's Problem78 Let's solve the Farmer's Problem, an example linear programming problem79 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.82 #+BEGIN_QUOTE83 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to84 plant two crops: wheat and barley. To produce these crops, it costs85 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat86 and $210 per acre for the barley. The farmer has $15000 available for87 expenses. But after the harvest, the farmer must store the crops while88 awaiting favorable market conditions. The farmer has storage space89 for 4000 bushels. Each acre yields an average of 110 bushels of wheat90 or 30 bushels of barley. If the net profit per bushel of wheat (after91 all expenses have been subtracted) is $1.30 and for barley is $2.00,92 how should the farmer plant the 75 acres to maximize profit?93 #+END_QUOTE95 The Farmer's Problem is to maximize profit subject to constraints on96 available farmland, funds for expenses, and storage space.98 | | Wheat | Barley | Maximum total |99 |----------+----------------------+---------------------+--------------|100 | / | < | > | <> |101 | Farmland | \(w\) acres | \(b\) acres | 75 acres |102 | Expense | $120 per acre | $210 per acre | $15000 |103 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |104 |----------+----------------------+---------------------+--------------|105 | Profit | $1.30 per bushel | $2.00 per bushel | |107 *** COMMENT108 can be represented as a linear optimization109 problem. In this form, it is a problem with two variables\mdash{}the number of110 acres of wheat, \(w\), and the number of acres of barley, \(b\). The111 aim is to maximize profit, which113 subject to three constraints: the farmer can't spend more money114 than he has, the farmer can't use more acres than he owns, and the harvest has115 to fit in his storage space.117 We can express these constraints succinctly using matrix118 notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\),119 we want to maximize the expression \(143 w + 60 b\) subject to121 \(122 \begin{cases}123 120 w + 210 b & \leq & 1500\\124 110 w + 30 b & \leq & 4000\\125 1 w + 1 w & \leq & 75126 \end{cases}127 \)129 #\(\begin{bmatrix}120&210\\110&30\\1 &130 #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix}131 #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\)133 ** Solution using LP Solve134 #(LP solve is available at http://www.example.com.)135 In a new file, =farmer.lp=, we list the variables and constraints136 of our problem using LP Solve syntax.138 #+begin_src lpsolve :tangle ../lp/farmer.lp139 /* Maximize Total Profit */140 max: +143 wheat +60 barley;143 /* -------- Constraints --------*/145 /* the farmer can't spend more money than he has */146 +120 wheat +210 barley <= 15000;148 /* the harvest has to fit in his storage space */149 +110 wheat +30 barley <= 4000;151 /* he can't use more acres than he owns */152 +wheat +barley <= 75;153 #+end_src155 Running the =lp_solve= program on =farmer.lp= yields the following output.157 #+begin_src sh :exports both :results scalar158 lp_solve ~/proj/pokemon-types/lp/farmer.lp159 #+end_src161 #+results:162 :163 : Value of objective function: 6315.62500000164 :165 : Actual values of the variables:166 : wheat 21.875167 : barley 53.125169 This shows that the farmer can maximize his profit by planting 21.875170 of the available acres with wheat and the remaining 53.125 acres with171 barley; by doing so, he will make $6315.62(5) in profit.173 * Incorporating =lp_solve= into Clojure175 There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java176 programs to use =lp_solve=. Although Clojure can use this Java API177 directly, the interaction between Java, C, and Clojure is clumsy:179 ** The Farmer's Problem in Clojure181 We are going to solve the same problem involving wheat and barley,182 that we did above, but this time using clojure and the =lp_solve= API.184 #+srcname: intro185 #+begin_src clojure :results silent186 (ns pokemon.lpsolve187 (:use [clojure.contrib def set [seq :only [indexed]] pprint])188 (:import lpsolve.LpSolve)189 (:require pokemon.types)190 (:require incanter.core))191 #+end_src193 The =lp_solve= Java interface is available from the same site as194 =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the195 same as many other =C= programs. There are excellent instructions to196 get set up. The short version is that you must call Java with197 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the198 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For199 example, in my =.bashrc= file, I have the line200 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything201 is set-up correctly,203 #+srcname: body204 #+begin_src clojure :results verbatim :exports both205 (import 'lpsolve.LpSolve)206 #+end_src208 #+results: body209 : lpsolve.LpSolve211 should run with no problems.213 ** Making a DSL to talk with LpSolve214 *** Problems215 Since we are using a =C= wrapper, we have to deal with manual memory216 management for the =C= structures which are wrapped by the =LpSolve=217 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's218 very important to get it right. Also, the Java wrapper follows the219 =C= tradition closely and defines many =static final int= constants220 for the different states of the =LpSolve= instance instead of using Java221 enums. The calling convention for adding rows and columns to222 the constraint matrix is rather complicated and must be done column by223 column or row by row, which can be error prone. Finally, I'd like to224 gather all the important output information from the =LpSolve= instance225 into a final, immutable structure.227 In summary, the issues I'd like to address are:229 - reliable memory management230 - functional interface to =LpSolve=231 - intelligible, immutable output233 To deal with these issues I'll create four functions for interfacing234 with =LpSolve=236 #+srcname: declares237 #+begin_src clojure :results silent238 (in-ns 'pokemon.lpsolve)240 ;; deal with automatic memory management for LpSolve instance.241 (declare linear-program)243 ;; functional interface to LpSolve244 (declare lp-solve)246 ;; immutable output from lp-solve247 (declare solve get-results)248 #+end_src251 *** Memory Management253 Every instance of =LpSolve= must be manually garbage collected via a254 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=255 to ensure that =deleteLP= is always called.257 #+srcname: memory-management258 #+begin_src clojure :results silent259 (in-ns 'pokemon.lpsolve)260 (defmacro linear-program261 "solve a linear programming problem using LpSolve syntax.262 within the macro, the variable =lps= is bound to the LpSolve instance."263 [& statements]264 (list 'let '[lps (LpSolve/makeLp 0 0)]265 (concat '(try)266 statements267 ;; always free the =C= data structures.268 '((finally (.deleteLp lps))))))269 #+end_src272 The macro captures the variable =lps= within its body, providing for a273 convenient way to access the object using any of the methods of the274 =LpSolve= API without having to worry about when to call275 =deleteLP=.277 *** Sensible Results278 The =linear-program= macro deletes the actual =lps= object once it is279 done working, so it's important to collect the important results and280 add return them in an immutable structure at the end.282 #+srcname: get-results283 #+begin_src clojure :results silent284 (in-ns 'pokemon.lpsolve)286 (defrecord LpSolution287 [objective-value288 optimal-values289 variable-names290 solution291 status292 model])294 (defn model295 "Returns a textual representation of the problem suitable for296 direct input to the =lp_solve= program (lps format)"297 [#^LpSolve lps]298 (let [target (java.io.File/createTempFile "lps" ".lp")]299 (.writeLp lps (.getPath target))300 (slurp target)))302 (defn results303 "Given an LpSolve object, solves the object and returns a map of the304 essential values which compose the solution."305 [#^LpSolve lps]306 (locking lps307 (let [status (solve lps)308 number-of-variables (.getNcolumns lps)309 optimal-values (double-array number-of-variables)310 optimal-values (do (.getVariables lps optimal-values)311 (seq optimal-values))312 variable-names313 (doall314 ;; The doall is necessary since the lps object might315 ;; soon be deleted.316 (map317 #(.getColName lps (inc %))318 (range number-of-variables)))319 model (model lps)]320 (LpSolution.321 (.getObjective lps)322 optimal-values323 variable-names324 (zipmap variable-names optimal-values)325 status326 model))))328 #+end_src330 Here I've created an object called =LpSolution= which stores the331 important results from a session with =lp_solve=. Of note is the332 =model= function which returns the problem in a form that can be333 solved by other linear programming packages.335 *** Solution Status of an LpSolve Object337 #+srcname: solve338 #+begin_src clojure :results silent339 (in-ns 'pokemon.lpsolve)341 (defn static-integer?342 "does the field represent a static integer constant?"343 [#^java.lang.reflect.Field field]344 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))345 (integer? (.get field nil))))347 (defn integer-constants [class]348 (filter static-integer? (.getFields class)))350 (defn-memo constant-map351 "Takes a class and creates a map of the static constant integer352 fields with their names. This helps with C wrappers where they have353 just defined a bunch of integer constants instead of enums"354 [class]355 (let [integer-fields (integer-constants class)]356 (into (sorted-map)357 (zipmap (map #(.get % nil) integer-fields)358 (map #(.getName %) integer-fields)))))360 (defn solve361 "Solve an instance of LpSolve and return a string representing the362 status of the computation. Will only solve a particular LpSolve363 instance once."364 [#^LpSolve lps]365 ((constant-map LpSolve)366 (.solve lps)))368 #+end_src370 The =.solve= method of an =LpSolve= object only returns an integer code371 to specify the status of the computation. The =solve= method here372 uses reflection to look up the actual name of the status code and373 returns a more helpful status message that is also resistant to374 changes in the meanings of the code numbers.376 *** The Farmer Example in Clojure, Pass 1378 Now we can implement a nicer version of the examples from the379 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less380 line-by-line translation of the Java code from that example.382 #+srcname: farmer-example383 #+begin_src clojure :results silent384 (in-ns 'pokemon.lpsolve)385 (defn farmer-example []386 (linear-program387 (results388 (doto lps389 ;; name the columns390 (.setColName 1 "wheat")391 (.setColName 2 "barley")392 (.setAddRowmode true)393 ;; row 1 : 120x + 210y <= 15000394 (.addConstraintex 2395 (double-array [120 210])396 (int-array [1 2])397 LpSolve/LE398 15e3)399 ;; row 2 : 110x + 30y <= 4000400 (.addConstraintex 2401 (double-array [110 30])402 (int-array [1 2])403 LpSolve/LE404 4e3)405 ;; ;; row 3 : x + y <= 75406 (.addConstraintex 2407 (double-array [1 1])408 (int-array [1 2])409 LpSolve/LE410 75)411 (.setAddRowmode false)413 ;; add constraints414 (.setObjFnex 2415 (double-array [143 60])416 (int-array [1 2]))418 ;; set this as a maximization problem419 (.setMaxim)))))421 #+end_src423 #+begin_src clojure :results output :exports both424 (clojure.pprint/pprint425 (:solution (pokemon.lpsolve/farmer-example)))426 #+end_src428 #+results:429 : {"barley" 53.12499999999999, "wheat" 21.875}431 And it works as expected!433 *** The Farmer Example in Clojure, Pass 2434 We don't have to worry about memory management anymore, and the farmer435 example is about half as long as the example from the =LpSolve=436 website, but we can still do better. Solving linear problems is all437 about the constraint matrix $A$ , the objective function $c$, and the438 right-hand-side $b$, plus whatever other options one cares to set for439 the particular instance of =lp_solve=. Why not make a version of440 =linear-program= that takes care of initialization?444 #+srcname: lp-solve445 #+begin_src clojure :results silent446 (in-ns 'pokemon.lpsolve)447 (defn initialize-lpsolve-row-oriented448 "fill in an lpsolve instance using a constraint matrix =A=, the449 objective function =c=, and the right-hand-side =b="450 [#^LpSolve lps A b c]451 ;; set the name of the last column to _something_452 ;; this appears to be necessary to ensure proper initialization.453 (.setColName lps (count c) (str "C" (count c)))455 ;; This is the recommended way to "fill-in" an lps instance from the456 ;; documentation. First, set row mode, then set the objective457 ;; function, then set each row of the problem, and then turn off row458 ;; mode.459 (.setAddRowmode lps true)460 (.setObjFnex lps (count c)461 (double-array c)462 (int-array (range 1 (inc (count c)))))463 (dorun464 (for [n (range (count A))]465 (let [row (nth A n)466 row-length (int (count row))]467 (.addConstraintex lps468 row-length469 (double-array row)470 (int-array (range 1 (inc row-length)))471 LpSolve/LE472 (double (nth b n))))))473 (.setAddRowmode lps false)474 lps)477 (defmacro lp-solve478 "by default:,479 minimize (* c x), subject to (<= (* A x) b),480 using continuous variables. You may set any number of481 other options as in the LpSolve API."482 [A b c & lp-solve-forms]483 ;; assume that A is a vector of vectors484 (concat485 (list 'linear-program486 (list 'initialize-lpsolve-row-oriented 'lps A b c))487 `~lp-solve-forms))488 #+end_src490 Now, we can use a much more functional approach to solving the491 farmer's problem:493 #+srcname: better-farmer494 #+begin_src clojure :results silent495 (in-ns 'pokemon.lpsolve)496 (defn better-farmer-example []497 (lp-solve [[120 210]498 [110 30]499 [1 1]]500 [15000501 4000502 75]503 [143 60]504 (.setColName lps 1 "wheat")505 (.setColName lps 2 "barley")506 (.setMaxim lps)507 (results lps)))508 #+end_src510 #+begin_src clojure :exports both :results verbatim511 (vec (:solution (pokemon.lpsolve/better-farmer-example)))512 #+end_src514 #+results:515 : [["barley" 53.12499999999999] ["wheat" 21.875]]517 Notice that both the inputs to =better-farmer-example= and the results518 are immutable.520 * Using LpSolve to find Immortal Types521 ** Converting the Pok\eacute{}mon problem into a linear form522 How can the original question about pok\eacute{}mon types be converted523 into a linear problem?525 Pokemon types can be considered to be vectors of numbers representing526 their susceptances to various attacking types, so Water might look527 something like this.529 #+begin_src clojure :results scalar :exports both530 (:water (pokemon.types/defense-strengths))531 #+end_src533 #+results:534 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]536 Where the numbers represent the susceptibility of Water to the537 attacking types in the following order:539 #+begin_src clojure :results output :exports both540 (clojure.pprint/pprint541 (pokemon.types/type-names))542 #+end_src544 #+results:545 #+begin_example546 [:normal547 :fire548 :water549 :electric550 :grass551 :ice552 :fighting553 :poison554 :ground555 :flying556 :psychic557 :bug558 :rock559 :ghost560 :dragon561 :dark562 :steel]563 #+end_example566 So, for example, Water is /resistant/ (x0.5) against Fire, which is567 the second element in the list.569 To combine types, these sorts of vectors are multiplied together570 pair-wise to yield the resulting combination.572 Unfortunately, we need some way to add two type vectors together573 instead of multiplying them if we want to solve the problem with574 =lp_solve=. Taking the log of the vector does just the trick.576 If we make a matrix with each column being the log (base 2) of the577 susceptance of each type, then finding an immortal type corresponds to578 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)579 and setting the constraint vector $c$ to all ones, which means that we580 want to find the immortal type which uses the least amount of types.582 #+srcname: pokemon-lp583 #+begin_src clojure :results silent584 (in-ns 'pokemon.lpsolve)586 (defn log-clamp-matrix [matrix]587 ;; we have to clamp the Infinities to a more reasonable negative588 ;; value because lp_solve does not play well with infinities in its589 ;; constraint matrix.590 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))591 (incanter.core/log2592 (incanter.core/trans593 matrix))))595 ;; constraint matrices596 (defn log-defense-matrix []597 (log-clamp-matrix598 (doall (map (pokemon.types/defense-strengths)599 (pokemon.types/type-names)))))601 (defn log-attack-matrix []602 (incanter.core/trans (log-defense-matrix)))604 ;; target vectors605 (defn all-resistant []606 (doall (map (constantly -1) (pokemon.types/type-names))))608 (defn all-weak []609 (doall (map (constantly 1) (pokemon.types/type-names))))611 (defn all-neutral []612 (doall (map (constantly 0) (pokemon.types/type-names))))614 ;; objective functions615 (defn number-of-types []616 (doall (map (constantly 1) (pokemon.types/type-names))))618 (defn set-constraints619 "sets all the constraints for an lpsolve instance to the given620 constraint. =constraint= here is one of the LpSolve constants such621 as LpSolve/EQ."622 [#^LpSolve lps constraint]623 (dorun (map (fn [index] (.setConstrType lps index constraint))624 ;; ONE based indexing!!!625 (range 1 (inc (.getNrows lps))))))628 (defn set-discrete629 "sets every variable in an lps problem to be a discrete rather than630 continuous variable"631 [#^LpSolve lps]632 (dorun (map (fn [index] (.setInt lps index true))633 ;; ONE based indexing!!!634 (range 1 (inc (.getNcolumns lps))))))636 (defn set-variable-names637 "sets the variable names of the problem given a vector of names"638 [#^LpSolve lps names]639 (dorun640 (map (fn [[index name]]641 (.setColName lps (inc index) (str name)))642 ;; ONE based indexing!!!643 (indexed names))))645 (defn poke-solve646 ([poke-matrix target objective-function constraint min-num-types]647 ;; must have at least one type648 (let [poke-matrix649 (concat poke-matrix650 [(map (constantly 1)651 (range (count (first poke-matrix))))])652 target (concat target [min-num-types])]653 (lp-solve poke-matrix target objective-function654 (set-constraints lps constraint)655 ;; must have more than min-num-types656 (.setConstrType lps (count target) LpSolve/GE)657 (set-discrete lps)658 (set-variable-names lps (pokemon.types/type-names))659 (results lps))))660 ([poke-matrix target objective-function constraint]661 ;; at least one type662 (poke-solve poke-matrix target objective-function constraint 1)))664 (defn solution665 "If the results of an lpsolve operation are feasible, returns the666 results. Otherwise, returns the error."667 [results]668 (if (not (= (:status results) "OPTIMAL"))669 (:status results)670 (:solution results)))671 #+end_src673 With this, we are finally able to get some results.675 ** Results676 #+srcname: results677 #+begin_src clojure :results silent678 (in-ns 'pokemon.lpsolve)680 (defn best-defense-type681 "finds a type combination which is resistant to all attacks."682 []683 (poke-solve684 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))686 (defn worst-attack-type687 "finds the attack type which is not-very-effective against all pure688 defending types (each single defending type is resistant to this689 attack combination"690 []691 (poke-solve692 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))694 (defn worst-defense-type695 "finds a defending type that is weak to all single attacking types."696 []697 (poke-solve698 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))700 (defn best-attack-type701 "finds an attack type which is super effective against all single702 defending types"703 []704 (poke-solve705 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))707 (defn solid-defense-type708 "finds a defense type which is either neutral or resistant to all709 single attacking types"710 []711 (poke-solve712 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))714 (defn solid-attack-type715 "finds an attack type which is either neutral or super-effective to716 all single attacking types."717 []718 (poke-solve719 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))721 (defn weak-defense-type722 "finds a defense type which is either neutral or weak to all single723 attacking types"724 []725 (poke-solve726 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))728 (defn neutral-defense-type729 "finds a defense type which is perfectly neutral to all attacking730 types."731 []732 (poke-solve733 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))735 #+end_src737 *** Strongest Attack/Defense Combinations739 #+begin_src clojure :results output :exports both740 (clojure.pprint/pprint741 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))742 #+end_src744 #+results:745 #+begin_example746 {":normal" 0.0,747 ":ground" 1.0,748 ":poison" 2.0,749 ":flying" 1.0,750 ":fighting" 0.0,751 ":dragon" 0.0,752 ":fire" 0.0,753 ":dark" 1.0,754 ":ice" 0.0,755 ":steel" 1.0,756 ":ghost" 0.0,757 ":electric" 0.0,758 ":bug" 0.0,759 ":psychic" 0.0,760 ":grass" 0.0,761 ":water" 2.0,762 ":rock" 0.0}763 #+end_example765 # #+results-old:766 # : [[":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]]769 This is the immortal type combination we've been looking for. By770 combining Steel, Water, Poison, and three types which each have complete771 immunities to various other types, we've created a type that is resistant to772 all attacking types.774 #+begin_src clojure :results output :exports both775 (clojure.pprint/pprint776 (pokemon.types/susceptibility777 [:poison :poison :water :water :steel :ground :flying :dark]))778 #+end_src780 #+results:781 #+begin_example782 {:water 1/2,783 :psychic 0,784 :dragon 1/2,785 :fire 1/2,786 :ice 1/2,787 :grass 1/2,788 :ghost 1/4,789 :poison 0,790 :flying 1/2,791 :normal 1/2,792 :rock 1/2,793 :electric 0,794 :ground 0,795 :fighting 1/2,796 :dark 1/4,797 :steel 1/8,798 :bug 1/8}799 #+end_example801 # #+results-old:802 # : {: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}805 Cool!807 #+begin_src clojure :results output :exports both808 (clojure.pprint/pprint809 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))810 #+end_src812 #+results:813 #+begin_example814 {":normal" 0.0,815 ":ground" 0.0,816 ":poison" 0.0,817 ":flying" 0.0,818 ":fighting" 0.0,819 ":dragon" 0.0,820 ":fire" 0.0,821 ":dark" 1.0,822 ":ice" 0.0,823 ":steel" 0.0,824 ":ghost" 1.0,825 ":electric" 0.0,826 ":bug" 0.0,827 ":psychic" 0.0,828 ":grass" 0.0,829 ":water" 0.0,830 ":rock" 0.0}831 #+end_example833 Dark and Ghost are the best dual-type combo, and are resistant or834 neutral to all types.836 #+begin_src clojure :results output :exports both837 (clojure.pprint/pprint838 (pokemon.types/old-school839 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))840 #+end_src842 #+results:843 #+begin_example844 {":normal" 0.0,845 ":ground" 0.0,846 ":poison" 0.0,847 ":flying" 0.0,848 ":fighting" 0.0,849 ":dragon" 0.0,850 ":fire" 0.0,851 ":ice" 0.0,852 ":ghost" 1.0,853 ":electric" 0.0,854 ":bug" 0.0,855 ":psychic" 1.0,856 ":grass" 0.0,857 ":water" 0.0,858 ":rock" 0.0}859 #+end_example861 Ghost and Psychic are a powerful dual type combo in the original games,862 due to a glitch which made Psychic immune to Ghost type attacks, even863 though the game claims that Ghost is strong to Psychic.865 #+begin_src clojure :results verbatim :exports both866 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))867 #+end_src869 #+results:870 : INFEASIBLE872 #+begin_src clojure :results verbatim :exports both873 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))874 #+end_src876 #+results:877 : INFEASIBLE880 #+begin_src clojure :results verbatim :exports both881 (pokemon.types/old-school882 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))883 #+end_src885 #+results:886 : INFEASIBLE889 #+begin_src clojure :results output :exports both890 (clojure.pprint/pprint891 (pokemon.types/old-school892 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))893 #+end_src895 #+results:896 #+begin_example897 {":normal" 0.0,898 ":ground" 0.0,899 ":poison" 0.0,900 ":flying" 0.0,901 ":fighting" 0.0,902 ":dragon" 1.0,903 ":fire" 0.0,904 ":ice" 0.0,905 ":ghost" 0.0,906 ":electric" 0.0,907 ":bug" 0.0,908 ":psychic" 0.0,909 ":grass" 0.0,910 ":water" 0.0,911 ":rock" 0.0}912 #+end_example914 The best attacking type combination is Dragon from the original games.915 It is neutral against all the original types except for Dragon, which916 it is strong against. There is no way to make an attacking type that917 is strong against every type, or even one that is strong or neutral918 against every type, in the new games.921 *** Weakest Attack/Defense Combinations923 #+begin_src clojure :results output :exports both924 (clojure.pprint/pprint925 (pokemon.types/old-school926 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))927 #+end_src929 #+results:930 #+begin_example931 {":normal" 5.0,932 ":ground" 0.0,933 ":poison" 0.0,934 ":flying" 0.0,935 ":fighting" 0.0,936 ":dragon" 0.0,937 ":fire" 1.0,938 ":ice" 2.0,939 ":ghost" 1.0,940 ":electric" 1.0,941 ":bug" 1.0,942 ":psychic" 0.0,943 ":grass" 3.0,944 ":water" 2.0,945 ":rock" 0.0}946 #+end_example948 # #+results-old:949 # : [[":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]]951 #+begin_src clojure :results output :exports both952 (clojure.pprint/pprint953 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))954 #+end_src956 #+results:957 #+begin_example958 {":normal" 4.0,959 ":ground" 1.0,960 ":poison" 1.0,961 ":flying" 0.0,962 ":fighting" 1.0,963 ":dragon" 0.0,964 ":fire" 0.0,965 ":dark" 0.0,966 ":ice" 4.0,967 ":steel" 0.0,968 ":ghost" 1.0,969 ":electric" 3.0,970 ":bug" 0.0,971 ":psychic" 1.0,972 ":grass" 1.0,973 ":water" 1.0,974 ":rock" 2.0}975 #+end_example977 # #+results-old:978 # : [[":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]]981 This is an extremely interesting type combination, in that it uses982 quite a few types.984 #+begin_src clojure :results verbatim :exports both985 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))986 #+end_src988 #+results:989 : 20.0991 20 types is the /minimum/ number of types before the attacking992 combination is not-very-effective or worse against all defending993 types. This would probably have been impossible to discover using994 best-first search, since it involves such an intricate type995 combination.997 It's so interesting that it takes 20 types to make an attack type that998 is weak to all types that the combination merits further999 investigation.1001 Unfortunately, all of the tools that we've written so far are focused1002 on defense type combinations. However, it is possible to make every1003 tool attack-oriented via a simple macro.1005 #+srcname: attack-oriented1006 #+begin_src clojure :results silent1007 (in-ns 'pokemon.lpsolve)1009 (defmacro attack-mode [& forms]1010 `(let [attack-strengths# pokemon.types/attack-strengths1011 defense-strengths# pokemon.types/defense-strengths]1012 (binding [pokemon.types/attack-strengths1013 defense-strengths#1014 pokemon.types/defense-strengths1015 attack-strengths#]1016 ~@forms)))1017 #+end_src1019 Now all the tools from =pokemon.types= will work for attack1020 combinations.1022 #+begin_src clojure :results output :exports both1023 (clojure.pprint/pprint1024 (pokemon.types/susceptibility [:water]))1025 #+end_src1027 #+results:1028 #+begin_example1029 {:water 1/2,1030 :psychic 1,1031 :dragon 1,1032 :fire 1/2,1033 :ice 1/2,1034 :grass 2,1035 :ghost 1,1036 :poison 1,1037 :flying 1,1038 :normal 1,1039 :rock 1,1040 :electric 2,1041 :ground 1,1042 :fighting 1,1043 :dark 1,1044 :steel 1/2,1045 :bug 1}1046 #+end_example1049 #+begin_src clojure :results output :exports both1050 (clojure.pprint/pprint1051 (pokemon.lpsolve/attack-mode1052 (pokemon.types/susceptibility [:water])))1053 #+end_src1055 #+results:1056 #+begin_example1057 {:water 1/2,1058 :psychic 1,1059 :dragon 1/2,1060 :fire 2,1061 :ice 1,1062 :grass 1/2,1063 :ghost 1,1064 :poison 1,1065 :flying 1,1066 :normal 1,1067 :rock 2,1068 :electric 1,1069 :ground 2,1070 :fighting 1,1071 :dark 1,1072 :steel 1,1073 :bug 1}1074 #+end_example1076 Now =pokemon.types/susceptibility= reports the /attack-type/1077 combination's effectiveness against other types.1079 The 20 type combo achieves its goal in a very clever way.1081 First, it weakens its effectiveness to other types at the expense of1082 making it very strong against flying.1084 #+begin_src clojure :results output :exports both1085 (clojure.pprint/pprint1086 (pokemon.lpsolve/attack-mode1087 (pokemon.types/susceptibility1088 [:normal :normal :normal :normal1089 :ice :ice :ice :ice1090 :electric :electric :electric1091 :rock :rock])))1092 #+end_src1094 #+results:1095 #+begin_example1096 {:water 1/2,1097 :psychic 1,1098 :dragon 2,1099 :fire 1/4,1100 :ice 1/4,1101 :grass 2,1102 :ghost 0,1103 :poison 1,1104 :flying 512,1105 :normal 1,1106 :rock 1/16,1107 :electric 1/8,1108 :ground 0,1109 :fighting 1/4,1110 :dark 1,1111 :steel 1/1024,1112 :bug 4}1113 #+end_example1115 Then, it removes it's strengths against Flying, Normal, and Fighting1116 by adding Ghost and Ground.1118 #+begin_src clojure :results output :exports both1119 (clojure.pprint/pprint1120 (pokemon.lpsolve/attack-mode1121 (pokemon.types/susceptibility1122 [:normal :normal :normal :normal1123 :ice :ice :ice :ice1124 :electric :electric :electric1125 :rock :rock1126 ;; Spot resistances1127 :ghost :ground])))1128 #+end_src1130 #+results:1131 #+begin_example1132 {:water 1/2,1133 :psychic 2,1134 :dragon 2,1135 :fire 1/2,1136 :ice 1/4,1137 :grass 1,1138 :ghost 0,1139 :poison 2,1140 :flying 0,1141 :normal 0,1142 :rock 1/8,1143 :electric 1/4,1144 :ground 0,1145 :fighting 1/4,1146 :dark 1/2,1147 :steel 1/1024,1148 :bug 2}1149 #+end_example1151 Adding the pair Psychic and Fighting takes care of its strength1152 against Psychic and makes it ineffective against Dark, which is immune1153 to Psychic.1155 Adding the pair Grass and Poison makes takes care of its strength1156 against poison and makes it ineffective against Steel, which is immune1157 to poison.1159 #+begin_src clojure :results output :exports both1160 (clojure.pprint/pprint1161 (pokemon.lpsolve/attack-mode1162 (pokemon.types/susceptibility1163 [;; setup1164 :normal :normal :normal :normal1165 :ice :ice :ice :ice1166 :electric :electric :electric1167 :rock :rock1168 ;; Spot resistances1169 :ghost :ground1170 ;; Pair resistances1171 :psychic :fighting1172 :grass :poison])))1173 #+end_src1175 #+results:1176 #+begin_example1177 {:water 1,1178 :psychic 1/2,1179 :dragon 1,1180 :fire 1/4,1181 :ice 1/2,1182 :grass 1,1183 :ghost 0,1184 :poison 1/2,1185 :flying 0,1186 :normal 0,1187 :rock 1/4,1188 :electric 1/4,1189 :ground 0,1190 :fighting 1/2,1191 :dark 0,1192 :steel 0,1193 :bug 1/2}1194 #+end_example1196 Can you see the final step?1198 It's adding the Water type, which is weak against Water and Dragon and1199 strong against Rock and Fire.1201 #+begin_src clojure :results output :exports both1202 (clojure.pprint/pprint1203 (pokemon.lpsolve/attack-mode1204 (pokemon.types/susceptibility1205 [;; setup1206 :normal :normal :normal :normal1207 :ice :ice :ice :ice1208 :electric :electric :electric1209 :rock :rock1210 ;; Spot resistances1211 :ghost :ground1212 ;; Pair resistances1213 :psychic :fighting1214 :grass :poison1215 ;; completion1216 :water])))1217 #+end_src1219 #+results:1220 #+begin_example1221 {:water 1/2,1222 :psychic 1/2,1223 :dragon 1/2,1224 :fire 1/2,1225 :ice 1/2,1226 :grass 1/2,1227 :ghost 0,1228 :poison 1/2,1229 :flying 0,1230 :normal 0,1231 :rock 1/2,1232 :electric 1/4,1233 :ground 0,1234 :fighting 1/2,1235 :dark 0,1236 :steel 0,1237 :bug 1/2}1238 #+end_example1240 Which makes a particularly beautiful combination which is ineffective1241 against all defending types.1244 # #+begin_src clojure :results scalar :exports both1245 # (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])))))1246 # #+end_src1248 # #+results:1249 # | [: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] |1252 Is there anything else that's interesting?1254 #+begin_src clojure :exports both1255 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))1256 #+end_src1258 #+results:1259 : INFEASIBLE1261 #+begin_src clojure :exports both1262 (pokemon.types/old-school1263 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))1264 #+end_src1266 #+results:1267 : INFEASIBLE1269 #+begin_src clojure :exports both1270 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))1271 #+end_src1273 #+results:1274 : INFEASIBLE1276 #+begin_src clojure :exports both1277 (pokemon.types/old-school1278 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))1279 #+end_src1281 #+results:1282 : INFEASIBLE1284 #+begin_src clojure :exports both1285 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))1286 #+end_src1288 #+results:1289 : INFEASIBLE1291 #+begin_src clojure :exports both1292 (pokemon.types/old-school1293 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))1294 #+end_src1296 #+results:1297 : INFEASIBLE1299 There is no way to produce a defense-type that is weak to all types.1300 This is probably because there are many types that are completely1301 immune to some types, such as Flying, which is immune to Ground. A1302 perfectly weak type could not use any of these types.1304 * Summary1306 Overall, the pok\eacute{}mon type system is slanted more towards1307 defense rather than offense. While it is possible to create superior1308 defensive types and exceptionally weak attack types, it is not1309 possible to create exceptionally weak defensive types or very powerful1310 attack types.1312 Using the =lp_solve= library was more complicated than the best-first1313 search, but yielded results quickly and efficiently. Expressing the1314 problem in a linear form does have its drawbacks, however --- it's1315 hard to ask questions such as "what is the best 3-type defensive combo1316 in terms of susceptibility?", since susceptibility is not a linear1317 function of a combo's types. It is also hard to get all the solutions1318 to a particular problem, such as all the pokemon type combinations of1319 length 8 which are immortal defense types.1322 * COMMENT main-program1323 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none1324 <<intro>>1325 <<body>>1326 <<declares>>1327 <<memory-management>>1328 <<get-results>>1329 <<solve>>1330 <<farmer-example>>1331 <<lp-solve>>1332 <<better-farmer>>1333 <<pokemon-lp>>1334 <<results>>1335 <<attack-oriented>>1336 #+end_src