Mercurial > pokemon-types
view org/lpsolve.org @ 16:7698e9bdff2b
upgraded pokemon-types to clojure version 1.3
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Mon, 06 Aug 2012 17:22:39 -0400 |
parents | ecb6e3f9b7d6 |
children | 0f6ace87343a |
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 find effective pokemon types in clojure.5 #+keywords: Pokemon, clojure, linear optimization, lp_solve, LpSolve6 #+SETUPFILE: ../../aurellem/org/setup.org7 #+INCLUDE: ../../aurellem/org/level-0.org9 * Introduction10 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.11 Pok\eacute{}mon is a game in which adorable creatures battle each12 other using fantastic attacks. It was made into a several gameboy13 games that all share the same battle system. Every pok\eacute{}mon in14 the gameboy game has one or two /types/, such as Ground, Fire, Water,15 etc. Every pok\eacute{}mon attack has exactly one type. Certain16 defending types are weak or strong to other attacking types. For17 example, Water attacks are strong against Fire pok\eacute{}mon, while18 Electric attacks are weak against Ground Pok\eacute{}mon. In the19 games, attacks can be either twice as effective as normal (Water20 vs. Fire), neutrally effective (Normal vs. Normal), half as effective21 (Fire vs. Water), or not effective at all (Electric vs. Ground). We22 represent these strengths and weaknesses as the numbers 2, 1,23 $\frac{1}{2}$, and 0, and call them the /susceptance/ of one type to24 another.26 If a pokemon has two types, then the strengths and weakness of each27 type are /multiplied/ together. Thus Electric (2x weak to Ground)28 combined with Flying (immune to Ground (0x)) is immune to Ground.29 Fire (2x weak to Water) combined with Water (1/2x resistant to Water)30 is neutral to Water. If both types are resistant to another type, then31 the combination is doubly-resistant (1/4x) to that type. If both types32 are weak to a certain type then the combination is double-weak (4x) to33 that type.35 In the [[./types.org][previous post]], we used the best-first search algorithm to find36 the most effective Pok\eacute{}mon type combinations. Afterwards, we37 realized that we could transform this search problem into a /linear38 optimization problem/. This conversion offers several advantages:39 first, search algorithms are comparatively slow, whereas linear40 optimization algorithms are extremely fast; second, it is difficult to41 determine whether a search problem has any solution, whereas it is42 straightforward to determine whether a linear optimization problem has43 any solution; finally, because systems of linear equations are so44 common, many programming languages have linear equation solvers45 written for them.47 In this article, we will:48 - Solve a simple linear optimization problem in C :: We demonstrate49 how to use the linear programming C library, =lp_solve=, to50 solve a simple linear optimization problem.51 - Incorporate a C library into Clojure :: We will show how we gave52 Clojure access to the linear programming C library, =lp_solve=.53 - Find effective Pokemon types using linear programming :: Building54 on our earlier code, we answer some questions that were55 impossible to answer using best-first search.56 - Present our results :: We found some cool examples and learned a lot57 about the pok\eacute{}mon type system as a whole.60 ** Immortal Types62 In the game, pok\eacute{}mon can have either one type or two types. If63 this restriction is lifted, is there any combination of types that is64 resistant to all types? I call such a combination an /Immortal Type/,65 since if that type's pattern was repeated over and over again towards66 infinity, the resulting type would be immune to all attack types.68 * Linear Programming70 Linear programming is the process of finding an optimal solution to a71 linear equation of several variables which are constrained by some linear72 inequalities.74 ** The Farmer's Problem76 Let's solve the Farmer's Problem, an example linear programming problem77 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.80 #+BEGIN_QUOTE81 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to82 plant two crops: wheat and barley. To produce these crops, it costs83 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat84 and $210 per acre for the barley. The farmer has $15000 available for85 expenses. But after the harvest, the farmer must store the crops while86 awaiting favorable market conditions. The farmer has storage space87 for 4000 bushels. Each acre yields an average of 110 bushels of wheat88 or 30 bushels of barley. If the net profit per bushel of wheat (after89 all expenses have been subtracted) is $1.30 and for barley is $2.00,90 how should the farmer plant the 75 acres to maximize profit?91 #+END_QUOTE93 The Farmer's Problem is to maximize profit subject to constraints on94 available farmland, funds for expenses, and storage space.96 | | Wheat | Barley | Maximum total |97 |----------+----------------------+---------------------+--------------|98 | / | < | > | <> |99 | Farmland | \(w\) acres | \(b\) acres | 75 acres |100 | Expense | $120 per acre | $210 per acre | $15000 |101 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |102 |----------+----------------------+---------------------+--------------|103 | Profit | $1.30 per bushel | $2.00 per bushel | |105 ** Solution using LP Solve106 In a new file, =farmer.lp=, we list the variables and constraints107 of our problem using LP Solve syntax.109 #+begin_src lpsolve :tangle ../lp/farmer.lp110 /* Maximize Total Profit */111 max: +143 wheat +60 barley;114 /* -------- Constraints --------*/116 /* the farmer can't spend more money than he has */117 +120 wheat +210 barley <= 15000;119 /* the harvest has to fit in his storage space */120 +110 wheat +30 barley <= 4000;122 /* he can't use more acres than he owns */123 +wheat +barley <= 75;124 #+end_src126 Running the =lp_solve= program on =farmer.lp= yields the following output.128 #+begin_src sh :exports both :results scalar129 lp_solve ~/proj/pokemon-types/lp/farmer.lp130 #+end_src132 #+results:133 :134 : Value of objective function: 6315.62500000135 :136 : Actual values of the variables:137 : wheat 21.875138 : barley 53.125141 This shows that the farmer can maximize his profit by planting 21.875142 of the available acres with wheat and the remaining 53.125 acres with143 barley; by doing so, he will make $6315.62(5) in profit.145 * Incorporating =lp_solve= into Clojure147 There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java148 programs to use =lp_solve=. Although Clojure can use this Java API149 directly, the interaction between Java, C, and Clojure is clumsy:151 ** The Farmer's Problem in Clojure153 We are going to solve the same problem involving wheat and barley,154 that we did above, but this time using clojure and the =lp_solve= API.156 #+name: intro157 #+begin_src clojure :results silent158 (ns pokemon.lpsolve159 ;;(:use [clojure.contrib def set [seq :only [indexed]] pprint])160 (:import lpsolve.LpSolve)161 (:require pokemon.types)162 (:require incanter.core)163 (:require rlm.map-utils))164 #+end_src166 The =lp_solve= Java interface is available from the same site as167 =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the168 same as many other =C= programs. There are excellent instructions to169 get set up. The short version is that you must call Java with170 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the171 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For172 example, in my =.bashrc= file, I have the line173 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything174 is set-up correctly,176 #+name: body177 #+begin_src clojure :results verbatim :exports both178 (import 'lpsolve.LpSolve)179 #+end_src181 #+results: body182 : lpsolve.LpSolve184 should run with no problems.186 ** Making a DSL to talk with LpSolve187 *** Problems188 Since we are using a =C= wrapper, we have to deal with manual memory189 management for the =C= structures which are wrapped by the =LpSolve=190 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's191 very important to get it right. Also, the Java wrapper follows the192 =C= tradition closely and defines many =static final int= constants193 for the different states of the =LpSolve= instance instead of using Java194 enums. The calling convention for adding rows and columns to195 the constraint matrix is rather complicated and must be done column by196 column or row by row, which can be error prone. Finally, I'd like to197 gather all the important output information from the =LpSolve= instance198 into a final, immutable structure.200 In summary, the issues I'd like to address are:202 - reliable memory management203 - functional interface to =LpSolve=204 - intelligible, immutable output206 To deal with these issues I'll create four functions for interfacing207 with =LpSolve=209 #+name: declares210 #+begin_src clojure :results silent211 (in-ns 'pokemon.lpsolve)213 ;; deal with automatic memory management for LpSolve instance.214 (declare linear-program)216 ;; functional interface to LpSolve217 (declare lp-solve)219 ;; immutable output from lp-solve220 (declare solve get-results)221 #+end_src224 *** Memory Management226 Every instance of =LpSolve= must be manually garbage collected via a227 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=228 to ensure that =deleteLP= is always called.230 #+name: memory-management231 #+begin_src clojure :results silent232 (in-ns 'pokemon.lpsolve)233 (defmacro linear-program234 "solve a linear programming problem using LpSolve syntax.235 within the macro, the variable =lps= is bound to the LpSolve instance."236 [& statements]237 (list 'let '[lps (LpSolve/makeLp 0 0)]238 (concat '(try)239 statements240 ;; always free the =C= data structures.241 '((finally (.deleteLp lps))))))242 #+end_src245 The macro captures the variable =lps= within its body, providing for a246 convenient way to access the object using any of the methods of the247 =LpSolve= API without having to worry about when to call248 =deleteLP=.250 *** Sensible Results251 The =linear-program= macro deletes the actual =lps= object once it is252 done working, so it's important to collect the important results and253 add return them in an immutable structure at the end.255 #+name: get-results256 #+begin_src clojure :results silent257 (in-ns 'pokemon.lpsolve)259 (defrecord LpSolution260 [objective-value261 optimal-values262 variable-names263 solution264 status265 model])267 (defn model268 "Returns a textual representation of the problem suitable for269 direct input to the =lp_solve= program (lps format)"270 [#^LpSolve lps]271 (let [target (java.io.File/createTempFile "lps" ".lp")]272 (.writeLp lps (.getPath target))273 (slurp target)))275 (defn results276 "Given an LpSolve object, solves the object and returns a map of the277 essential values which compose the solution."278 [#^LpSolve lps]279 (locking lps280 (let [status (solve lps)281 number-of-variables (.getNcolumns lps)282 optimal-values (double-array number-of-variables)283 optimal-values (do (.getVariables lps optimal-values)284 (seq optimal-values))285 variable-names286 (doall287 ;; The doall is necessary since the lps object might288 ;; soon be deleted.289 (map290 #(.getColName lps (inc %))291 (range number-of-variables)))292 model (model lps)]293 (LpSolution.294 (.getObjective lps)295 optimal-values296 variable-names297 (zipmap variable-names optimal-values)298 status299 model))))301 #+end_src303 Here I've created an object called =LpSolution= which stores the304 important results from a session with =lp_solve=. Of note is the305 =model= function which returns the problem in a form that can be306 solved by other linear programming packages.308 *** Solution Status of an LpSolve Object310 #+name: solve311 #+begin_src clojure :results silent312 (in-ns 'pokemon.lpsolve)314 (defn static-integer?315 "does the field represent a static integer constant?"316 [#^java.lang.reflect.Field field]317 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))318 (integer? (.get field nil))))320 (defn integer-constants [class]321 (filter static-integer? (.getFields class)))323 (defn constant-map324 "Takes a class and creates a map of the static constant integer325 fields with their names. This helps with C wrappers where they have326 just defined a bunch of integer constants instead of enums."327 [class]328 (let [integer-fields (integer-constants class)]329 (into (sorted-map)330 (zipmap (map #(.get % nil) integer-fields)331 (map #(.getName %) integer-fields)))))333 (alter-var-root #'constant-map memoize)335 (defn solve336 "Solve an instance of LpSolve and return a string representing the337 status of the computation. Will only solve a particular LpSolve338 instance once."339 [#^LpSolve lps]340 ((constant-map LpSolve)341 (.solve lps)))343 #+end_src345 The =.solve= method of an =LpSolve= object only returns an integer code346 to specify the status of the computation. The =solve= method here347 uses reflection to look up the actual name of the status code and348 returns a more helpful status message that is also resistant to349 changes in the meanings of the code numbers.351 *** The Farmer Example in Clojure, Pass 1353 Now we can implement a nicer version of the examples from the354 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less355 line-by-line translation of the Java code from that example.357 #+name: farmer-example358 #+begin_src clojure :results silent359 (in-ns 'pokemon.lpsolve)360 (defn farmer-example []361 (linear-program362 (results363 (doto lps364 ;; name the columns365 (.setColName 1 "wheat")366 (.setColName 2 "barley")367 (.setAddRowmode true)368 ;; row 1 : 120x + 210y <= 15000369 (.addConstraintex 2370 (double-array [120 210])371 (int-array [1 2])372 LpSolve/LE373 15e3)374 ;; row 2 : 110x + 30y <= 4000375 (.addConstraintex 2376 (double-array [110 30])377 (int-array [1 2])378 LpSolve/LE379 4e3)380 ;; ;; row 3 : x + y <= 75381 (.addConstraintex 2382 (double-array [1 1])383 (int-array [1 2])384 LpSolve/LE385 75)386 (.setAddRowmode false)388 ;; add constraints389 (.setObjFnex 2390 (double-array [143 60])391 (int-array [1 2]))393 ;; set this as a maximization problem394 (.setMaxim)))))396 #+end_src398 #+begin_src clojure :results output :exports both399 (clojure.pprint/pprint400 (:solution (pokemon.lpsolve/farmer-example)))401 #+end_src403 #+results:404 : {"barley" 53.12499999999999, "wheat" 21.875}406 And it works as expected!408 *** The Farmer Example in Clojure, Pass 2409 We don't have to worry about memory management anymore, and the farmer410 example is about half as long as the example from the =LpSolve=411 website, but we can still do better. Solving linear problems is all412 about the constraint matrix $A$ , the objective function $c$, and the413 right-hand-side $b$, plus whatever other options one cares to set for414 the particular instance of =lp_solve=. Why not make a version of415 =linear-program= that takes care of initialization?419 #+name: lp-solve420 #+begin_src clojure :results silent421 (in-ns 'pokemon.lpsolve)422 (defn initialize-lpsolve-row-oriented423 "fill in an lpsolve instance using a constraint matrix =A=, the424 objective function =c=, and the right-hand-side =b="425 [#^LpSolve lps A b c]426 ;; set the name of the last column to _something_427 ;; this appears to be necessary to ensure proper initialization.428 (.setColName lps (count c) (str "C" (count c)))430 ;; This is the recommended way to "fill-in" an lps instance from the431 ;; documentation. First, set row mode, then set the objective432 ;; function, then set each row of the problem, and then turn off row433 ;; mode.434 (.setAddRowmode lps true)435 (.setObjFnex lps (count c)436 (double-array c)437 (int-array (range 1 (inc (count c)))))438 (dorun439 (for [n (range (count A))]440 (let [row (nth A n)441 row-length (int (count row))]442 (.addConstraintex lps443 row-length444 (double-array row)445 (int-array (range 1 (inc row-length)))446 LpSolve/LE447 (double (nth b n))))))448 (.setAddRowmode lps false)449 lps)452 (defmacro lp-solve453 "by default:,454 minimize (* c x), subject to (<= (* A x) b),455 using continuous variables. You may set any number of456 other options as in the LpSolve API."457 [A b c & lp-solve-forms]458 ;; assume that A is a vector of vectors459 (concat460 (list 'linear-program461 (list 'initialize-lpsolve-row-oriented 'lps A b c))462 `~lp-solve-forms))463 #+end_src465 Now, we can use a much more functional approach to solving the466 farmer's problem:468 #+name: better-farmer469 #+begin_src clojure :results silent470 (in-ns 'pokemon.lpsolve)471 (defn better-farmer-example []472 (lp-solve [[120 210]473 [110 30]474 [1 1]]475 [15000476 4000477 75]478 [143 60]479 (.setColName lps 1 "wheat")480 (.setColName lps 2 "barley")481 (.setMaxim lps)482 (results lps)))483 #+end_src485 #+begin_src clojure :exports both :results verbatim486 (vec (:solution (pokemon.lpsolve/better-farmer-example)))487 #+end_src489 #+results:490 : [["barley" 53.12499999999999] ["wheat" 21.875]]492 Notice that both the inputs to =better-farmer-example= and the results493 are immutable.495 * Using LpSolve to find Immortal Types496 ** Converting the Pok\eacute{}mon problem into a linear form497 How can the original question about pok\eacute{}mon types be converted498 into a linear problem?500 Pokemon types can be considered to be vectors of numbers representing501 their susceptances to various attacking types, so Water might look502 something like this.504 #+begin_src clojure :results scalar :exports both505 (:water (pokemon.types/defense-strengths))506 #+end_src508 #+results:509 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]511 Where the numbers represent the susceptibility of Water to the512 attacking types in the following order:514 #+begin_src clojure :results output :exports both515 (clojure.pprint/pprint516 (pokemon.types/type-names))517 #+end_src519 #+results:520 #+begin_example521 [:normal522 :fire523 :water524 :electric525 :grass526 :ice527 :fighting528 :poison529 :ground530 :flying531 :psychic532 :bug533 :rock534 :ghost535 :dragon536 :dark537 :steel]538 #+end_example541 So, for example, Water is resistant (x0.5) against Fire, which is542 the second element in the list.544 To combine types, these sorts of vectors are multiplied together545 pair-wise to yield the resulting combination.547 Unfortunately, we need some way to add two type vectors together548 instead of multiplying them if we want to solve the problem with549 =lp_solve=. Taking the log of the vector does just the trick.551 If we make a matrix with each column being the log (base 2) of the552 susceptance of each type, then finding an immortal type corresponds to553 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)554 and setting the constraint vector $c$ to all ones, which means that we555 want to find the immortal type which uses the least amount of types.557 #+name: pokemon-lp558 #+begin_src clojure :results silent559 (in-ns 'pokemon.lpsolve)561 (defn log-clamp-matrix [matrix]562 ;; we have to clamp the Infinities to a more reasonable negative563 ;; value because lp_solve does not play well with infinities in its564 ;; constraint matrix.565 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))566 (incanter.core/log2567 (incanter.core/trans568 matrix))))570 ;; constraint matrices571 (defn log-defense-matrix []572 (log-clamp-matrix573 (doall (map (pokemon.types/defense-strengths)574 (pokemon.types/type-names)))))576 (defn log-attack-matrix []577 (incanter.core/trans (log-defense-matrix)))579 ;; target vectors580 (defn all-resistant []581 (doall (map (constantly -1) (pokemon.types/type-names))))583 (defn all-weak []584 (doall (map (constantly 1) (pokemon.types/type-names))))586 (defn all-neutral []587 (doall (map (constantly 0) (pokemon.types/type-names))))589 ;; objective functions590 (defn number-of-types []591 (doall (map (constantly 1) (pokemon.types/type-names))))593 (defn set-constraints594 "sets all the constraints for an lpsolve instance to the given595 constraint. =constraint= here is one of the LpSolve constants such596 as LpSolve/EQ."597 [#^LpSolve lps constraint]598 (dorun (map (fn [index] (.setConstrType lps index constraint))599 ;; ONE based indexing!!!600 (range 1 (inc (.getNrows lps))))))603 (defn set-discrete604 "sets every variable in an lps problem to be a discrete rather than605 continuous variable"606 [#^LpSolve lps]607 (dorun (map (fn [index] (.setInt lps index true))608 ;; ONE based indexing!!!609 (range 1 (inc (.getNcolumns lps))))))611 (defn set-variable-names612 "sets the variable names of the problem given a vector of names"613 [#^LpSolve lps names]614 (dorun615 (keep-indexed616 (fn [index name]617 (.setColName lps (inc index) (str name)))618 ;; ONE based indexing!!!619 names)))621 (defn poke-solve622 ([poke-matrix target objective-function constraint min-num-types]623 ;; must have at least one type624 (let [poke-matrix625 (concat poke-matrix626 [(map (constantly 1)627 (range (count (first poke-matrix))))])628 target (concat target [min-num-types])]629 (lp-solve poke-matrix target objective-function630 (set-constraints lps constraint)631 ;; must have more than min-num-types632 (.setConstrType lps (count target) LpSolve/GE)633 (set-discrete lps)634 (set-variable-names lps (pokemon.types/type-names))635 (results lps))))636 ([poke-matrix target objective-function constraint]637 ;; at least one type638 (poke-solve poke-matrix target objective-function constraint 1)))640 (defn solution641 "If the results of an lpsolve operation are feasible, returns the642 results. Otherwise, returns the error."643 [results]644 (if (not (= (:status results) "OPTIMAL"))645 (:status results)646 (:solution results)))647 #+end_src649 With this, we are finally able to get some results.651 ** Results652 #+name: results653 #+begin_src clojure :results silent654 (in-ns 'pokemon.lpsolve)656 (defn best-defense-type657 "finds a type combination which is resistant to all attacks."658 []659 (poke-solve660 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))662 (defn worst-attack-type663 "finds the attack type which is not-very-effective against all pure664 defending types (each single defending type is resistant to this665 attack combination"666 []667 (poke-solve668 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))670 (defn worst-defense-type671 "finds a defending type that is weak to all single attacking types."672 []673 (poke-solve674 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))676 (defn best-attack-type677 "finds an attack type which is super effective against all single678 defending types"679 []680 (poke-solve681 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))683 (defn solid-defense-type684 "finds a defense type which is either neutral or resistant to all685 single attacking types"686 []687 (poke-solve688 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))690 (defn solid-attack-type691 "finds an attack type which is either neutral or super-effective to692 all single attacking types."693 []694 (poke-solve695 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))697 (defn weak-defense-type698 "finds a defense type which is either neutral or weak to all single699 attacking types"700 []701 (poke-solve702 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))704 (defn neutral-defense-type705 "finds a defense type which is perfectly neutral to all attacking706 types."707 []708 (poke-solve709 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))711 #+end_src713 *** Strongest Attack/Defense Combinations715 #+begin_src clojure :results output :exports both716 (clojure.pprint/pprint717 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))718 #+end_src720 #+results:721 #+begin_example722 {":normal" 0.0,723 ":ground" 1.0,724 ":poison" 2.0,725 ":flying" 1.0,726 ":fighting" 0.0,727 ":dragon" 0.0,728 ":fire" 0.0,729 ":dark" 1.0,730 ":ice" 0.0,731 ":steel" 1.0,732 ":ghost" 0.0,733 ":electric" 0.0,734 ":bug" 0.0,735 ":psychic" 0.0,736 ":grass" 0.0,737 ":water" 2.0,738 ":rock" 0.0}739 #+end_example741 # #+results-old:742 # : [[":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]]745 This is the immortal type combination we've been looking for. By746 combining Steel, Water, Poison, and three types which each have complete747 immunities to various other types, we've created a type that is resistant to748 all attacking types.750 #+begin_src clojure :results output :exports both751 (clojure.pprint/pprint752 (pokemon.types/susceptibility753 [:poison :poison :water :water :steel :ground :flying :dark]))754 #+end_src756 #+results:757 #+begin_example758 {:water 1/2,759 :psychic 0,760 :dragon 1/2,761 :fire 1/2,762 :ice 1/2,763 :grass 1/2,764 :ghost 1/4,765 :poison 0,766 :flying 1/2,767 :normal 1/2,768 :rock 1/2,769 :electric 0,770 :ground 0,771 :fighting 1/2,772 :dark 1/4,773 :steel 1/8,774 :bug 1/8}775 #+end_example777 # #+results-old:778 # : {: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}781 Cool!783 #+begin_src clojure :results output :exports both784 (clojure.pprint/pprint785 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))786 #+end_src788 #+results:789 #+begin_example790 {":normal" 0.0,791 ":ground" 0.0,792 ":poison" 0.0,793 ":flying" 0.0,794 ":fighting" 0.0,795 ":dragon" 0.0,796 ":fire" 0.0,797 ":dark" 1.0,798 ":ice" 0.0,799 ":steel" 0.0,800 ":ghost" 1.0,801 ":electric" 0.0,802 ":bug" 0.0,803 ":psychic" 0.0,804 ":grass" 0.0,805 ":water" 0.0,806 ":rock" 0.0}807 #+end_example809 Dark and Ghost are the best dual-type combo, and are resistant or810 neutral to all types.812 #+begin_src clojure :results output :exports both813 (clojure.pprint/pprint814 (pokemon.types/old-school815 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))816 #+end_src818 #+results:819 #+begin_example820 {":normal" 0.0,821 ":ground" 0.0,822 ":poison" 0.0,823 ":flying" 0.0,824 ":fighting" 0.0,825 ":dragon" 0.0,826 ":fire" 0.0,827 ":ice" 0.0,828 ":ghost" 1.0,829 ":electric" 0.0,830 ":bug" 0.0,831 ":psychic" 1.0,832 ":grass" 0.0,833 ":water" 0.0,834 ":rock" 0.0}835 #+end_example837 Ghost and Psychic are a powerful dual type combo in the original games,838 due to a glitch which made Psychic immune to Ghost type attacks, even839 though the game claims that Ghost is strong against Psychic.841 #+begin_src clojure :results verbatim :exports both842 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))843 #+end_src845 #+results:846 : INFEASIBLE848 #+begin_src clojure :results verbatim :exports both849 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))850 #+end_src852 #+results:853 : INFEASIBLE856 #+begin_src clojure :results verbatim :exports both857 (pokemon.types/old-school858 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))859 #+end_src861 #+results:862 : INFEASIBLE865 #+begin_src clojure :results output :exports both866 (clojure.pprint/pprint867 (pokemon.types/old-school868 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))869 #+end_src871 #+results:872 #+begin_example873 {":normal" 0.0,874 ":ground" 0.0,875 ":poison" 0.0,876 ":flying" 0.0,877 ":fighting" 0.0,878 ":dragon" 1.0,879 ":fire" 0.0,880 ":ice" 0.0,881 ":ghost" 0.0,882 ":electric" 0.0,883 ":bug" 0.0,884 ":psychic" 0.0,885 ":grass" 0.0,886 ":water" 0.0,887 ":rock" 0.0}888 #+end_example890 The best attacking type combination is Dragon from the original games.891 It is neutral against all the original types except for Dragon, which892 it is strong against. There is no way to make an attacking type that893 is strong against every type, or even one that is strong or neutral894 against every type, in the new games.897 *** Weakest Attack/Defense Combinations899 #+begin_src clojure :results output :exports both900 (clojure.pprint/pprint901 (pokemon.types/old-school902 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))903 #+end_src905 #+results:906 #+begin_example907 {":normal" 5.0,908 ":ground" 0.0,909 ":poison" 0.0,910 ":flying" 0.0,911 ":fighting" 0.0,912 ":dragon" 0.0,913 ":fire" 1.0,914 ":ice" 2.0,915 ":ghost" 1.0,916 ":electric" 1.0,917 ":bug" 1.0,918 ":psychic" 0.0,919 ":grass" 3.0,920 ":water" 2.0,921 ":rock" 0.0}922 #+end_example924 # #+results-old:925 # : [[":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]]927 #+begin_src clojure :results output :exports both928 (clojure.pprint/pprint929 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))930 #+end_src932 #+results:933 #+begin_example934 {":normal" 4.0,935 ":ground" 1.0,936 ":poison" 1.0,937 ":flying" 0.0,938 ":fighting" 1.0,939 ":dragon" 0.0,940 ":fire" 0.0,941 ":dark" 0.0,942 ":ice" 4.0,943 ":steel" 0.0,944 ":ghost" 1.0,945 ":electric" 3.0,946 ":bug" 0.0,947 ":psychic" 1.0,948 ":grass" 1.0,949 ":water" 1.0,950 ":rock" 2.0}951 #+end_example953 # #+results-old:954 # : [[":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]]957 This is an extremely interesting type combination, in that it uses958 quite a few types.960 #+begin_src clojure :results verbatim :exports both961 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))962 #+end_src964 #+results:965 : 20.0967 20 types is the /minimum/ number of types before the attacking968 combination is not-very-effective or worse against all defending969 types. This would probably have been impossible to discover using970 best-first search, since it involves such an intricate type971 combination.973 It's so interesting that it takes 20 types to make an attack type that974 is weak to all types that the combination merits further975 investigation.977 Unfortunately, all of the tools that we've written so far are focused978 on defense type combinations. However, it is possible to make every979 tool attack-oriented via a simple macro.981 #+name: attack-oriented982 #+begin_src clojure :results silent983 (in-ns 'pokemon.lpsolve)985 (defmacro attack-mode [& forms]986 `(let [attack-strengths# pokemon.types/attack-strengths987 defense-strengths# pokemon.types/defense-strengths]988 (binding [pokemon.types/attack-strengths989 defense-strengths#990 pokemon.types/defense-strengths991 attack-strengths#]992 ~@forms)))993 #+end_src995 Now all the tools from =pokemon.types= will work for attack996 combinations.998 #+begin_src clojure :results output :exports both999 (clojure.pprint/pprint1000 (pokemon.types/susceptibility [:water]))1001 #+end_src1003 #+results:1004 #+begin_example1005 {:water 1/2,1006 :psychic 1,1007 :dragon 1,1008 :fire 1/2,1009 :ice 1/2,1010 :grass 2,1011 :ghost 1,1012 :poison 1,1013 :flying 1,1014 :normal 1,1015 :rock 1,1016 :electric 2,1017 :ground 1,1018 :fighting 1,1019 :dark 1,1020 :steel 1/2,1021 :bug 1}1022 #+end_example1025 #+begin_src clojure :results output :exports both1026 (clojure.pprint/pprint1027 (pokemon.lpsolve/attack-mode1028 (pokemon.types/susceptibility [:water])))1029 #+end_src1031 #+results:1032 #+begin_example1033 {:water 1/2,1034 :psychic 1,1035 :dragon 1/2,1036 :fire 2,1037 :ice 1,1038 :grass 1/2,1039 :ghost 1,1040 :poison 1,1041 :flying 1,1042 :normal 1,1043 :rock 2,1044 :electric 1,1045 :ground 2,1046 :fighting 1,1047 :dark 1,1048 :steel 1,1049 :bug 1}1050 #+end_example1052 Now =pokemon.types/susceptibility= reports the /attack-type/1053 combination's effectiveness against other types.1055 The 20 type combo achieves its goal in a very clever way.1057 First, it weakens its effectiveness to other types at the expense of1058 making it very strong against flying.1060 #+begin_src clojure :results output :exports both1061 (clojure.pprint/pprint1062 (pokemon.lpsolve/attack-mode1063 (pokemon.types/susceptibility1064 [:normal :normal :normal :normal1065 :ice :ice :ice :ice1066 :electric :electric :electric1067 :rock :rock])))1068 #+end_src1070 #+results:1071 #+begin_example1072 {:water 1/2,1073 :psychic 1,1074 :dragon 2,1075 :fire 1/4,1076 :ice 1/4,1077 :grass 2,1078 :ghost 0,1079 :poison 1,1080 :flying 512,1081 :normal 1,1082 :rock 1/16,1083 :electric 1/8,1084 :ground 0,1085 :fighting 1/4,1086 :dark 1,1087 :steel 1/1024,1088 :bug 4}1089 #+end_example1091 Then, it removes it's strengths against Flying, Normal, and Fighting1092 by adding Ghost and Ground.1094 #+begin_src clojure :results output :exports both1095 (clojure.pprint/pprint1096 (pokemon.lpsolve/attack-mode1097 (pokemon.types/susceptibility1098 [:normal :normal :normal :normal1099 :ice :ice :ice :ice1100 :electric :electric :electric1101 :rock :rock1102 ;; Spot resistances1103 :ghost :ground])))1104 #+end_src1106 #+results:1107 #+begin_example1108 {:water 1/2,1109 :psychic 2,1110 :dragon 2,1111 :fire 1/2,1112 :ice 1/4,1113 :grass 1,1114 :ghost 0,1115 :poison 2,1116 :flying 0,1117 :normal 0,1118 :rock 1/8,1119 :electric 1/4,1120 :ground 0,1121 :fighting 1/4,1122 :dark 1/2,1123 :steel 1/1024,1124 :bug 2}1125 #+end_example1127 Adding the pair Psychic and Fighting takes care of its strength1128 against Psychic and makes it ineffective against Dark, which is immune1129 to Psychic.1131 Adding the pair Grass and Poison makes takes care of its strength1132 against poison and makes it ineffective against Steel, which is immune1133 to poison.1135 #+begin_src clojure :results output :exports both1136 (clojure.pprint/pprint1137 (pokemon.lpsolve/attack-mode1138 (pokemon.types/susceptibility1139 [;; setup1140 :normal :normal :normal :normal1141 :ice :ice :ice :ice1142 :electric :electric :electric1143 :rock :rock1144 ;; Spot resistances1145 :ghost :ground1146 ;; Pair resistances1147 :psychic :fighting1148 :grass :poison])))1149 #+end_src1151 #+results:1152 #+begin_example1153 {:water 1,1154 :psychic 1/2,1155 :dragon 1,1156 :fire 1/4,1157 :ice 1/2,1158 :grass 1,1159 :ghost 0,1160 :poison 1/2,1161 :flying 0,1162 :normal 0,1163 :rock 1/4,1164 :electric 1/4,1165 :ground 0,1166 :fighting 1/2,1167 :dark 0,1168 :steel 0,1169 :bug 1/2}1170 #+end_example1172 Can you see the final step?1174 It's adding the Water type, which is weak against Water, Dragon, and1175 Grass and strong against Rock and Fire.1177 #+begin_src clojure :results output :exports both1178 (clojure.pprint/pprint1179 (pokemon.lpsolve/attack-mode1180 (pokemon.types/susceptibility1181 [;; setup1182 :normal :normal :normal :normal1183 :ice :ice :ice :ice1184 :electric :electric :electric1185 :rock :rock1186 ;; Spot resistances1187 :ghost :ground1188 ;; Pair resistances1189 :psychic :fighting1190 :grass :poison1191 ;; completion1192 :water])))1193 #+end_src1195 #+results:1196 #+begin_example1197 {:water 1/2,1198 :psychic 1/2,1199 :dragon 1/2,1200 :fire 1/2,1201 :ice 1/2,1202 :grass 1/2,1203 :ghost 0,1204 :poison 1/2,1205 :flying 0,1206 :normal 0,1207 :rock 1/2,1208 :electric 1/4,1209 :ground 0,1210 :fighting 1/2,1211 :dark 0,1212 :steel 0,1213 :bug 1/2}1214 #+end_example1216 Which makes a particularly beautiful combination which is ineffective1217 against all defending types.1220 # #+begin_src clojure :results scalar :exports both1221 # (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])))))1222 # #+end_src1224 # #+results:1225 # | [: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] |1228 Is there anything else that's interesting?1230 #+begin_src clojure :exports both1231 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))1232 #+end_src1234 #+results:1235 : INFEASIBLE1237 #+begin_src clojure :exports both1238 (pokemon.types/old-school1239 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))1240 #+end_src1242 #+results:1243 : INFEASIBLE1245 #+begin_src clojure :exports both1246 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))1247 #+end_src1249 #+results:1250 : INFEASIBLE1252 #+begin_src clojure :exports both1253 (pokemon.types/old-school1254 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))1255 #+end_src1257 #+results:1258 : INFEASIBLE1260 #+begin_src clojure :exports both1261 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))1262 #+end_src1264 #+results:1265 : INFEASIBLE1267 #+begin_src clojure :exports both1268 (pokemon.types/old-school1269 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))1270 #+end_src1272 #+results:1273 : INFEASIBLE1275 There is no way to produce a defense-type that is weak to all types.1276 This is probably because there are many types that are completely1277 immune to some types, such as Flying, which is immune to Ground. A1278 perfectly weak type could not use any of these types.1280 * Summary1282 Overall, the pok\eacute{}mon type system is slanted towards defense1283 rather than offense. While it is possible to create superior1284 defensive types and exceptionally weak attack types, it is not1285 possible to create exceptionally weak defensive types or very powerful1286 attack types.1288 Using the =lp_solve= library was more complicated than the best-first1289 search, but yielded results quickly and efficiently. Expressing the1290 problem in a linear form does have its drawbacks, however --- it's1291 hard to ask questions such as "what is the best 3-type defensive combo1292 in terms of susceptibility?", since susceptibility is not a linear1293 function of a combo's types. It is also hard to get all the solutions1294 to a particular problem, such as all the pokemon type combinations of1295 length 8 which are immortal defense types.1297 * COMMENT main-program1298 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none1299 <<intro>>1300 <<body>>1301 <<declares>>1302 <<memory-management>>1303 <<get-results>>1304 <<solve>>1305 <<farmer-example>>1306 <<lp-solve>>1307 <<better-farmer>>1308 <<pokemon-lp>>1309 <<results>>1310 <<attack-oriented>>1311 #+end_src