Mercurial > pokemon-types
view org/lpsolve.org @ 10:eedd6897197d
fixed spelling errors for pokemon.types
author | Robert McIntyre <rlm@mit.edu> |
---|---|
date | Wed, 02 Nov 2011 08:02:11 -0700 |
parents | a227fe337e83 |
children | 4ea23241ff5b |
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 #+SETUPFILE: ../../aurellem/org/setup.org5 #+INCLUDE: ../../aurellem/org/level-0.org9 * Introduction10 In the [[./types.org][previous post]], we used the best-first search algorithm to11 locate the most effective Pok\eacute{}mon type12 combinations. Afterwards, we realized that we could transform this13 search problem into a /linear optimization problem/. This conversion14 offeres several advantages: first, search algorithms are comparatively15 slow, whereas linear optimization algorithms are extremely fast;16 second, it is difficult to determine whether a search problem has any17 solution, whereas it is straightforward to determine whether a linear18 optimization problem has any solution; finally, because systems of19 linear equations are so common, many programming languages have linear20 equation solvers written for them.22 In this article, we will23 - Solve a simple linear optimization problem in C :: We demonstrate24 how to use the linear programming C library, =lp_solve=, to25 solve a simple linear26 optimization problem.27 - Incorporate a C library into Clojure :: We will show how we gave28 Clojure access to the linear programming C library, =lp_solve=.29 - Find effective Pokemon types using linear programming :: Building30 on our earlier code, (...)31 - Present our results ::33 #which can be represented and solved as a system of linear equations.35 * COMMENT36 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.37 Pok\eacute{}mon is a game in which adorable creatures battle each38 other using fantastic attacks. It was made into a several gameboy39 games that all share the same battle system. Every pok\eacute{}mon in the40 gameboy game has one or two /types/, such as Ground, Fire, Water,41 etc. Every pok\eacute{}mon attack has exactly one type. Certain defending42 types are weak or strong to other attacking types. For example, Water43 attacks are strong against Fire pok\eacute{}mon, while Electric attacks are44 weak against Ground Pok\eacute{}mon. In the games, attacks can be either45 twice as effective as normal (Water vs. Fire), neutrally effective46 (Normal vs. Normal), half as effective (Fire vs. Water), or not47 effective at all (Electric vs. Ground). Thus the range of defense48 values for a particular type is the set 0, 1/2, 1, 2. These are49 referred to in the game as being immune, resistant, neutral, and50 weak, respectively. I call them the /susceptance/ of one type to another.52 If a pokemon has two types, then the strengths and weakness of each53 type are /multiplied/ together. Thus Electric (2x weak to Ground)54 combined with Flying (immune to Ground (0x)) is immune to55 Ground. Fire (2x weak to Water) combined with Water (1/2x resistant56 to Water) is neutral to Water. If both types are resistant to another57 type, then the combination is doubly-resistant (1/4x) to that type. If58 both types are weak to a certain type then the combination is59 double-weak (4x) to that type.61 ** Immortal Types63 In the game, pok\eacute{}mon can have either one type, or two types. If this64 restriction is lifted, is there any combination of types that is65 resistant to all types? I call such a combination an /Immortal Type/,66 since if that type's pattern was repeated over and over again towards67 infinity, the resulting type would be immune to all attack types.69 * Linear Programming71 ** Terminology72 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 In linear programming terminology, the function to be extremized is77 the /objective function/.79 ** COMMENT80 First, we'll give a small example of a linear optimization problem,81 and show how it can be solved with Clojure and =lp_solve=. Then,82 we'll show how finding immortal pok\eacute{}mon types can be converted83 into a linear problem suitable for solving in the same way.85 ** The Farmer's Problem87 Let's solve the Farmer's Problem, an example linear programming problem88 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.91 #+BEGIN_QUOTE92 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to93 plant two crops: wheat and barley. To produce these crops, it costs94 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat95 and $210 per acre for the barley. The farmer has $15000 available for96 expenses. But after the harvest, the farmer must store the crops while97 awaiting favorable market conditions. The farmer has storage space98 for 4000 bushels. Each acre yields an average of 110 bushels of wheat99 or 30 bushels of barley. If the net profit per bushel of wheat (after100 all expenses have been subtracted) is $1.30 and for barley is $2.00,101 how should the farmer plant the 75 acres to maximize profit?102 #+END_QUOTE104 The Farmer's Problem is to maximize profit subject to constraints on105 available farmland, funds for expenses, and storage space.107 | | Wheat | Barley | Maximum total |108 |----------+----------------------+---------------------+--------------|109 | / | < | > | <> |110 | Farmland | \(w\) acres | \(b\) acres | 75 acres |111 | Expense | $120 per acre | $210 per acre | $15000 |112 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |113 |----------+----------------------+---------------------+--------------|114 | Profit | $1.30 per bushel | $2.00 per bushel | |116 *** COMMENT117 can be represented as a linear optimization118 problem. In this form, it is a problem with two variables\mdash{}the number of119 acres of wheat, \(w\), and the number of acres of barley, \(b\). The120 aim is to maximize profit, which122 subject to three constraints: the farmer can't spend more money123 than he has, the farmer can't use more acres than he owns, and the harvest has124 to fit in his storage space.126 We can express these constraints succinctly using matrix127 notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\),128 we want to maximize the expression \(143 w + 60 b\) subject to130 \(131 \begin{cases}132 120 w + 210 b & \leq & 1500\\133 110 w + 30 b & \leq & 4000\\134 1 w + 1 w & \leq & 75135 \end{cases}136 \)138 #\(\begin{bmatrix}120&210\\110&30\\1 &139 #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix}140 #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\)142 ** Solution using LP Solve143 #(LP solve is available at http://www.example.com.)144 In a new file, =farmer.lp=, we list the variables and constraints145 of our problem using LP Solve syntax.147 #+begin_src lpsolve :tangle ../lp/farmer.lp148 /* Maximize Total Profit */149 max: +143 wheat +60 barley;152 /* -------- Constraints --------*/154 /* the farmer can't spend more money than he has */155 +120 wheat +210 barley <= 15000;157 /* the harvest has to fit in his storage space */158 +110 wheat +30 barley <= 4000;160 /* he can't use more acres than he owns */161 +wheat +barley <= 75;162 #+end_src165 #This is a set of linear equations ideal for solving using a program like166 #=lp_solve=. In Linear Algebra terms we are maximizing the linear function168 #\(\text{profit} = 143\text{ wheat} + 60\text{ barley}\), subject to the constraints170 #Ax <= b,172 #where A is [120 210 110 30 1 1], x is [wheat barley] and b is [15000173 #4000 75].175 Running the =lp_solve= program on =farmer.lp= yields the following output.177 #+begin_src sh :exports both :results scalar178 ~/roBin/lpsolve/lp_solve ~/aurellem/src/pokemon/farmer.lp179 #+end_src181 #+results:182 :183 : Value of objective function: 6315.62500000184 :185 : Actual values of the variables:186 : wheat 21.875187 : barley 53.125189 This shows that the farmer can maximize his profit by planting 21.875190 of the available acres with wheat and the remaining 53.125 acres with191 barley; by doing so, he will make $6315.62(5) in profit.194 #The farmer can make a profit of $6315.62 by planting 21.875 acres of195 #his land with wheat and the other 53.125 acres of his land with barley.197 * Incorporating =lp_solve= into Clojure199 There is a Java API available which enables Java programs to use Lp200 Solve. Although Clojure can use this Java API directly, the201 interaction between Java, C, and Clojure is clumsy:204 The Java API for LP Solve makes it possible to use Lp Solve algorithms205 within Java. Although Clojure can use this Java API directly,208 ** The Farmer's Problem in Clojure209 We are going to solve the same problem involving wheat and barley,210 that we did above, but this time using clojure and the lpsolve API.212 #Because we ultimately intend to use Lp Solve to find optimal Pokemon type combinations.213 # we want to solve linear optimization problems within Clojure, the language215 ** Setup216 =lp_solve= is a crufty =C= program which already comes with a JNI217 interface written by Juergen Ebert. It's API is not218 particularly friendly from a functional/immutable perspective, but219 with a little work, we can build something that works great with220 clojure.222 #+srcname: intro223 #+begin_src clojure :results silent224 (ns pokemon.lpsolve225 (:use rlm.ns-rlm))226 (rlm.ns-rlm/ns-clone rlm.light-base)227 (use 'clojure.set)228 (import 'lpsolve.LpSolve)229 (use '[clojure [pprint :only [pprint]]])230 #+end_src232 The LpSolve Java interface is available from the same site as233 =lp_solve= itself, http://lpsolve.sourceforge.net/234 Using it is the same as many other =C=235 programs. There are excellent instructions to get set236 up. The short version is that you must call Java with237 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the238 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For239 example, in my =.bashrc= file, I have the line240 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=.241 If everything is set-up correctly,243 #+srcname: body244 #+begin_src clojure :results verbatim :exports both245 (import 'lpsolve.LpSolve)246 #+end_src248 #+results: body249 : lpsolve.LpSolve251 should run with no problems.253 ** Making a DSL to talk with LpSolve254 *** Problems255 Since we are using a =C= wrapper, we have to deal with manual memory256 management for the =C= structures which are wrapped by the =LpSolve=257 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's258 very important to get it right. Also, the Java wrapper follows the259 =C= tradition closely and defines many =static final int= constants260 for the different states of the =LpSolve= instance instead of using Java261 enums. The calling convention for adding rows and columns to262 the constraint matrix is rather complicated and must be done column by263 column or row by row, which can be error prone. Finally, I'd like to264 gather all the important output information from the =LpSolve= instance265 into a final, immutable structure.267 In summary, the issues I'd like to address are:269 - reliable memory management270 - functional interface to =LpSolve=271 - intelligible, immutable output273 To deal with these issues I'll create four functions for interfacing274 with =LpSolve=276 #+srcname: declares277 #+begin_src clojure :results silent278 (in-ns 'pokemon.lpsolve)280 ;; deal with automatic memory management for LpSolve instance.281 (declare linear-program)283 ;; functional interface to LpSolve284 (declare lp-solve)286 ;; immutable output from lp-solve287 (declare solve get-results)288 #+end_src290 *** Memory Management292 Every instance of =LpSolve= must be manually garbage collected via a293 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=294 to ensure that =deleteLP= is always called.296 #+srcname: memory-management297 #+begin_src clojure :results silent298 (in-ns 'pokemon.lpsolve)299 (defmacro linear-program300 "solve a linear programming problem using LpSolve syntax.301 within the macro, the variable =lps= is bound to the LpSolve instance."302 [& statements]303 (list 'let '[lps (LpSolve/makeLp 0 0)]304 (concat '(try)305 statements306 ;; always free the =C= data structures.307 '((finally (.deleteLp lps))))))308 #+end_src311 The macro captures the variable =lps= within its body, providing for a312 convenient way to access the object using any of the methods of the313 =LpSolve= API without having to worry about when to call314 =deleteLP=.316 *** Sensible Results317 The =linear-program= macro deletes the actual =lps= object once it is318 done working, so it's important to collect the important results and319 add return them in an immutable structure at the end.321 #+srcname: get-results322 #+begin_src clojure :results silent323 (in-ns 'pokemon.lpsolve)325 (defrecord LpSolution326 [objective-value327 optimal-values328 variable-names329 solution330 status331 model])333 (defn model334 "Returns a textual representation of the problem suitable for335 direct input to the =lp_solve= program (lps format)"336 [#^LpSolve lps]337 (let [target (java.io.File/createTempFile "lps" ".lp")]338 (.writeLp lps (.getPath target))339 (slurp target)))341 (defn results342 "given an LpSolve object, solves the object and returns a map of the343 essential values which compose the solution."344 [#^LpSolve lps]345 (locking lps346 (let [status (solve lps)347 number-of-variables (.getNcolumns lps)348 optimal-values (double-array number-of-variables)349 optimal-values (do (.getVariables lps optimal-values)350 (seq optimal-values))351 variable-names352 (doall ;; the doall is necessary since the lps object might353 ;; soon be deleted354 (map355 #(.getColName lps (inc %))356 (range number-of-variables)))357 model (model lps)]358 (LpSolution.359 (.getObjective lps)360 optimal-values361 variable-names362 (zipmap variable-names optimal-values)363 status364 model))))366 #+end_src368 Here I've created an object called =LpSolution= which stores the369 important results from a session with =lp_solve=. Of note is the370 =model= function which returns the problem in a form that can be371 solved by other linear programming packages.373 *** Solution Status of an LpSolve Object375 #+srcname: solve376 #+begin_src clojure :results silent377 (in-ns 'pokemon.lpsolve)379 (defn static-integer?380 "does the field represent a static integer constant?"381 [#^java.lang.reflect.Field field]382 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))383 (integer? (.get field nil))))385 (defn integer-constants [class]386 (filter static-integer? (.getFields class)))388 (defn-memo constant-map389 "Takes a class and creates a map of the static constant integer390 fields with their names. This helps with C wrappers where they have391 just defined a bunch of integer constants instead of enums"392 [class]393 (let [integer-fields (integer-constants class)]394 (into (sorted-map)395 (zipmap (map #(.get % nil) integer-fields)396 (map #(.getName %) integer-fields)))))398 (defn solve399 "Solve an instance of LpSolve and return a string representing the400 status of the computation. Will only solve a particular LpSolve401 instance once."402 [#^LpSolve lps]403 ((constant-map LpSolve)404 (.solve lps)))406 #+end_src408 The =.solve= method of an =LpSolve= object only returns an integer code409 to specify the status of the computation. The =solve= method here410 uses reflection to look up the actual name of the status code and411 returns a more helpful status message that is also resistant to412 changes in the meanings of the code numbers.414 *** The Farmer Example in Clojure, Pass 1416 Now we can implement a nicer version of the examples from the417 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less418 line-by-line translation of the Java code from that example.420 #+srcname: farmer-example421 #+begin_src clojure :results silent422 (in-ns 'pokemon.lpsolve)423 (defn farmer-example []424 (linear-program425 (results426 (doto lps427 ;; name the columns428 (.setColName 1 "wheat")429 (.setColName 2 "barley")430 (.setAddRowmode true)431 ;; row 1 : 120x + 210y <= 15000432 (.addConstraintex 2433 (double-array [120 210])434 (int-array [1 2])435 LpSolve/LE436 15e3)437 ;; row 2 : 110x + 30y <= 4000438 (.addConstraintex 2439 (double-array [110 30])440 (int-array [1 2])441 LpSolve/LE442 4e3)443 ;; ;; row 3 : x + y <= 75444 (.addConstraintex 2445 (double-array [1 1])446 (int-array [1 2])447 LpSolve/LE448 75)449 (.setAddRowmode false)451 ;; add constraints452 (.setObjFnex 2453 (double-array [143 60])454 (int-array [1 2]))456 ;; set this as a maximization problem457 (.setMaxim)))))459 #+end_src461 #+begin_src clojure :results output :exports both462 (clojure.pprint/pprint463 (:solution (pokemon.lpsolve/farmer-example)))464 #+end_src466 #+results:467 : {"barley" 53.12499999999999, "wheat" 21.875}469 And it works as expected!471 *** The Farmer Example in Clojure, Pass 2472 We don't have to worry about memory management anymore, and the farmer473 example is about half as long as the example from the =LpSolve=474 website, but we can still do better. Solving linear problems is all475 about the constraint matrix $A$ , the objective function $c$, and the476 right-hand-side $b$, plus whatever other options one cares to set for477 the particular instance of =lp_solve=. Why not make a version of478 =linear-program= that takes care of initialization?482 #+srcname: lp-solve483 #+begin_src clojure :results silent484 (in-ns 'pokemon.lpsolve)485 (defn initialize-lpsolve-row-oriented486 "fill in an lpsolve instance using a constraint matrix =A=, the487 objective function =c=, and the right-hand-side =b="488 [#^LpSolve lps A b c]489 ;; set the name of the last column to _something_490 ;; this appears to be necessary to ensure proper initialization.491 (.setColName lps (count c) (str "C" (count c)))493 ;; This is the recommended way to "fill-in" an lps instance from the494 ;; documentation. First, set row mode, then set the objective495 ;; function, then set each row of the problem, and then turn off row496 ;; mode.497 (.setAddRowmode lps true)498 (.setObjFnex lps (count c)499 (double-array c)500 (int-array (range 1 (inc (count c)))))501 (dorun502 (for [n (range (count A))]503 (let [row (nth A n)504 row-length (int (count row))]505 (.addConstraintex lps506 row-length507 (double-array row)508 (int-array (range 1 (inc row-length)))509 LpSolve/LE510 (double (nth b n))))))511 (.setAddRowmode lps false)512 lps)515 (defmacro lp-solve516 "by default:,517 minimize (* c x), subject to (<= (* A x) b),518 using continuous variables. You may set any number of519 other options as in the LpSolve API."520 [A b c & lp-solve-forms]521 ;; assume that A is a vector of vectors522 (concat523 (list 'linear-program524 (list 'initialize-lpsolve-row-oriented 'lps A b c))525 `~lp-solve-forms))526 #+end_src528 Now, we can use a much more functional approach to solving the529 farmer's problem:531 #+srcname: better-farmer532 #+begin_src clojure :results silent533 (in-ns 'pokemon.lpsolve)534 (defn better-farmer-example []535 (lp-solve [[120 210]536 [110 30]537 [1 1]]538 [15000539 4000540 75]541 [143 60]542 (.setColName lps 1 "wheat")543 (.setColName lps 2 "barley")544 (.setMaxim lps)545 (results lps)))546 #+end_src548 #+begin_src clojure :exports both :results verbatim549 (vec (:solution (pokemon.lpsolve/better-farmer-example)))550 #+end_src552 #+results:553 : [["barley" 53.12499999999999] ["wheat" 21.875]]555 Notice that both the inputs to =better-farmer-example= and the results556 are immutable.558 * Using LpSolve to find Immortal Types559 ** Converting the Pokemon problem into a linear form560 How can the original question about pok\eacute{}mon types be converted561 into a linear problem?563 Pokemon types can be considered to be vectors of numbers representing564 their susceptances to various attacking types, so Water might look565 something like this.567 #+begin_src clojure :results scalar :exports both568 (:water (pokemon.types/defense-strengths))569 #+end_src571 #+results:572 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]574 Where the numbers represent the susceptibility of Water to the575 attacking types in the following order:577 #+begin_src clojure :results output :exports both578 (clojure.pprint/pprint579 (pokemon.types/type-names))580 #+end_src582 #+results:583 #+begin_example584 [:normal585 :fire586 :water587 :electric588 :grass589 :ice590 :fighting591 :poison592 :ground593 :flying594 :psychic595 :bug596 :rock597 :ghost598 :dragon599 :dark600 :steel]601 #+end_example604 So, for example, Water is /resistant/ (x0.5) against Fire, which is605 the second element in the list.607 To combine types, these sorts of vectors are multiplied together608 pair-wise to yield the resulting combination.610 Unfortunately, we need some way to add two type vectors together611 instead of multiplying them if we want to solve the problem with612 =lp_solve=. Taking the log of the vector does just the trick.614 If we make a matrix with each column being the log (base 2) of the615 susceptance of each type, then finding an immortal type corresponds to616 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)617 and setting the constraint vector $c$ to all ones, which means that we618 want to find the immortal type which uses the least amount of types.620 #+srcname: pokemon-lp621 #+begin_src clojure :results silent622 (in-ns 'pokemon.lpsolve)624 (require 'pokemon.types)625 (require 'incanter.core)627 (defn log-clamp-matrix [matrix]628 ;; we have to clamp the Infinities to a more reasonable negative629 ;; value because lp_solve does not play well with infinities in its630 ;; constraint matrix.631 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))632 (incanter.core/log2633 (incanter.core/trans634 matrix))))636 ;; constraint matrices637 (defn log-defense-matrix []638 (log-clamp-matrix639 (doall (map (pokemon.types/defense-strengths)640 (pokemon.types/type-names)))))642 (defn log-attack-matrix []643 (incanter.core/trans (log-defense-matrix)))645 ;; target vectors646 (defn all-resistant []647 (doall (map (constantly -1) (pokemon.types/type-names))))649 (defn all-weak []650 (doall (map (constantly 1) (pokemon.types/type-names))))652 (defn all-neutral []653 (doall (map (constantly 0) (pokemon.types/type-names))))656 ;; objective functions657 (defn number-of-types []658 (doall (map (constantly 1) (pokemon.types/type-names))))660 (defn set-constraints661 "sets all the constraints for an lpsolve instance to the given662 constraint. =constraint= here is one of the LpSolve constants such663 as LpSolve/EQ."664 [#^LpSolve lps constraint]665 (dorun (map (fn [index] (.setConstrType lps index constraint))666 ;; ONE based indexing!!!667 (range 1 (inc (.getNrows lps))))))670 (defn set-discrete671 "sets every variable in an lps problem to be a discrete rather than672 continuous variable"673 [#^LpSolve lps]674 (dorun (map (fn [index] (.setInt lps index true))675 ;; ONE based indexing!!!676 (range 1 (inc (.getNcolumns lps))))))678 (defn set-variable-names679 "sets the variable names of the problem given a vector of names"680 [#^LpSolve lps names]681 (dorun682 (map (fn [[index name]]683 (.setColName lps (inc index) (str name)))684 ;; ONE based indexing!!!685 (indexed names))))687 (defn poke-solve688 ([poke-matrix target objective-function constraint min-num-types]689 ;; must have at least one type690 (let [poke-matrix691 (concat poke-matrix692 [(map (constantly 1)693 (range (count (first poke-matrix))))])694 target (concat target [min-num-types])]695 (lp-solve poke-matrix target objective-function696 (set-constraints lps constraint)697 ;; must have more than min-num-types698 (.setConstrType lps (count target) LpSolve/GE)699 (set-discrete lps)700 (set-variable-names lps (pokemon.types/type-names))701 (results lps))))702 ([poke-matrix target objective-function constraint]703 ;; at least one type704 (poke-solve poke-matrix target objective-function constraint 1)))706 (defn solution707 "If the results of an lpsolve operation are feasible, returns the708 results. Otherwise, returns the error."709 [results]710 (if (not (= (:status results) "OPTIMAL"))711 (:status results)712 (:solution results)))714 #+end_src716 With this, we are finally able to get some results.718 ** Results719 #+srcname: results720 #+begin_src clojure :results silent721 (in-ns 'pokemon.lpsolve)723 (defn best-defense-type724 "finds a type combination which is resistant to all attacks."725 []726 (poke-solve727 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))729 (defn worst-attack-type730 "finds the attack type which is not-very-effective against all pure731 defending types (each single defending type is resistant to this732 attack combination"733 []734 (poke-solve735 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))737 (defn worst-defense-type738 "finds a defending type that is weak to all single attacking types."739 []740 (poke-solve741 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))743 (defn best-attack-type744 "finds an attack type which is super effective against all single745 defending types"746 []747 (poke-solve748 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))750 (defn solid-defense-type751 "finds a defense type which is either neutral or resistant to all752 single attacking types"753 []754 (poke-solve755 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))757 (defn solid-attack-type758 "finds an attack type which is either neutral or super-effective to759 all single attacking types."760 []761 (poke-solve762 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))764 (defn weak-defense-type765 "finds a defense type which is either neutral or weak to all single766 attacking types"767 []768 (poke-solve769 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))771 (defn neutral-defense-type772 "finds a defense type which is perfectly neutral to all attacking773 types."774 []775 (poke-solve776 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))778 #+end_src780 *** Strongest Attack/Defense Combinations782 #+begin_src clojure :results output :exports both783 (clojure.pprint/pprint784 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))785 #+end_src787 #+results:788 #+begin_example789 {":normal" 0.0,790 ":ground" 1.0,791 ":poison" 2.0,792 ":flying" 1.0,793 ":fighting" 0.0,794 ":dragon" 0.0,795 ":fire" 0.0,796 ":dark" 1.0,797 ":ice" 0.0,798 ":steel" 1.0,799 ":ghost" 0.0,800 ":electric" 0.0,801 ":bug" 0.0,802 ":psychic" 0.0,803 ":grass" 0.0,804 ":water" 2.0,805 ":rock" 0.0}806 #+end_example808 # #+results-old:809 # : [[":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]]812 This is the immortal type combination we've been looking for. By813 combining Steel, Water, Poison, and three types which each have complete814 immunities to various other types, we've created a type that is resistant to815 all attacking types.817 #+begin_src clojure :results output :exports both818 (clojure.pprint/pprint819 (pokemon.types/susceptibility820 [:poison :poison :water :water :steel :ground :flying :dark]))821 #+end_src823 #+results:824 #+begin_example825 {:water 1/2,826 :psychic 0,827 :dragon 1/2,828 :fire 1/2,829 :ice 1/2,830 :grass 1/2,831 :ghost 1/4,832 :poison 0,833 :flying 1/2,834 :normal 1/2,835 :rock 1/2,836 :electric 0,837 :ground 0,838 :fighting 1/2,839 :dark 1/4,840 :steel 1/8,841 :bug 1/8}842 #+end_example844 # #+results-old:845 # : {: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}848 Cool!850 #+begin_src clojure :results output :exports both851 (clojure.pprint/pprint852 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))853 #+end_src855 #+results:856 #+begin_example857 {":normal" 0.0,858 ":ground" 0.0,859 ":poison" 0.0,860 ":flying" 0.0,861 ":fighting" 0.0,862 ":dragon" 0.0,863 ":fire" 0.0,864 ":dark" 1.0,865 ":ice" 0.0,866 ":steel" 0.0,867 ":ghost" 1.0,868 ":electric" 0.0,869 ":bug" 0.0,870 ":psychic" 0.0,871 ":grass" 0.0,872 ":water" 0.0,873 ":rock" 0.0}874 #+end_example876 Dark and Ghost are the best dual-type combo, and are resistant or877 neutral to all types.879 #+begin_src clojure :results output :exports both880 (clojure.pprint/pprint881 (pokemon.types/old-school882 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))883 #+end_src885 #+results:886 #+begin_example887 {":normal" 0.0,888 ":ground" 0.0,889 ":poison" 0.0,890 ":flying" 0.0,891 ":fighting" 0.0,892 ":dragon" 0.0,893 ":fire" 0.0,894 ":ice" 0.0,895 ":ghost" 1.0,896 ":electric" 0.0,897 ":bug" 0.0,898 ":psychic" 1.0,899 ":grass" 0.0,900 ":water" 0.0,901 ":rock" 0.0}902 #+end_example904 Ghost and Psychic are a powerful dual type combo in the original games,905 due to a glitch which made Psychic immune to Ghost type attacks, even906 though the game claims that Ghost is strong to Psychic.908 #+begin_src clojure :results verbatim :exports both909 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))910 #+end_src912 #+results:913 : INFEASIBLE915 #+begin_src clojure :results verbatim :exports both916 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))917 #+end_src919 #+results:920 : INFEASIBLE923 #+begin_src clojure :results verbatim :exports both924 (pokemon.types/old-school925 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))926 #+end_src928 #+results:929 : INFEASIBLE932 #+begin_src clojure :results output :exports both933 (clojure.pprint/pprint934 (pokemon.types/old-school935 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))936 #+end_src938 #+results:939 #+begin_example940 {":normal" 0.0,941 ":ground" 0.0,942 ":poison" 0.0,943 ":flying" 0.0,944 ":fighting" 0.0,945 ":dragon" 1.0,946 ":fire" 0.0,947 ":ice" 0.0,948 ":ghost" 0.0,949 ":electric" 0.0,950 ":bug" 0.0,951 ":psychic" 0.0,952 ":grass" 0.0,953 ":water" 0.0,954 ":rock" 0.0}955 #+end_example957 The best attacking type combination is strangely Dragon from the958 original games. It is neutral against all the original types except959 for Dragon, which it is strong against. There is no way to make an960 attacking type that is strong against every type, or even one that is961 strong or neutral against every type, in the new games.964 *** Weakest Attack/Defense Combinations966 #+begin_src clojure :results output :exports both967 (clojure.pprint/pprint968 (pokemon.types/old-school969 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))970 #+end_src972 #+results:973 #+begin_example974 {":normal" 5.0,975 ":ground" 0.0,976 ":poison" 0.0,977 ":flying" 0.0,978 ":fighting" 0.0,979 ":dragon" 0.0,980 ":fire" 1.0,981 ":ice" 2.0,982 ":ghost" 1.0,983 ":electric" 1.0,984 ":bug" 1.0,985 ":psychic" 0.0,986 ":grass" 3.0,987 ":water" 2.0,988 ":rock" 0.0}989 #+end_example991 # #+results-old:992 # : [[":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]]994 #+begin_src clojure :results output :exports both995 (clojure.pprint/pprint996 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))997 #+end_src999 #+results:1000 #+begin_example1001 {":normal" 4.0,1002 ":ground" 1.0,1003 ":poison" 1.0,1004 ":flying" 0.0,1005 ":fighting" 1.0,1006 ":dragon" 0.0,1007 ":fire" 0.0,1008 ":dark" 0.0,1009 ":ice" 4.0,1010 ":steel" 0.0,1011 ":ghost" 1.0,1012 ":electric" 3.0,1013 ":bug" 0.0,1014 ":psychic" 1.0,1015 ":grass" 1.0,1016 ":water" 1.0,1017 ":rock" 2.0}1018 #+end_example1020 # #+results-old:1021 # : [[":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]]1024 This is an extremely interesting type combination, in that it uses1025 quite a few types.1027 #+begin_src clojure :results verbatim :exports both1028 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))1029 #+end_src1031 #+results:1032 : 20.01034 20 types is the /minimum/ number of types before the attacking1035 combination is not-very-effective or worse against all defending1036 types. This would probably have been impossible to discover using1037 best-first search, since it involves such an intricate type1038 combination.1040 It's so interesting that it takes 20 types to make an attack type that1041 is weak to all types that the combination merits further investigation.1043 Unfortunately, all of the tools that we've written so far are focused1044 on defense type combinations. However, it is possible to make every1045 tool attack-oriented via a simple macro.1047 #+srcname: attack-oriented1048 #+begin_src clojure :results silent1049 (in-ns 'pokemon.lpsolve)1051 (defmacro attack-mode [& forms]1052 `(let [attack-strengths# pokemon.types/attack-strengths1053 defense-strengths# pokemon.types/defense-strengths]1054 (binding [pokemon.types/attack-strengths1055 defense-strengths#1056 pokemon.types/defense-strengths1057 attack-strengths#]1058 ~@forms)))1059 #+end_src1061 Now all the tools from =pokemon.types= will work for attack1062 combinations.1064 #+begin_src clojure :results output :exports both1065 (clojure.pprint/pprint1066 (pokemon.types/susceptibility [:water]))1067 #+end_src1069 #+results:1070 #+begin_example1071 {:water 1/2,1072 :psychic 1,1073 :dragon 1,1074 :fire 1/2,1075 :ice 1/2,1076 :grass 2,1077 :ghost 1,1078 :poison 1,1079 :flying 1,1080 :normal 1,1081 :rock 1,1082 :electric 2,1083 :ground 1,1084 :fighting 1,1085 :dark 1,1086 :steel 1/2,1087 :bug 1}1088 #+end_example1091 #+begin_src clojure :results output :exports both1092 (clojure.pprint/pprint1093 (pokemon.lpsolve/attack-mode1094 (pokemon.types/susceptibility [:water])))1095 #+end_src1097 #+results:1098 #+begin_example1099 {:water 1/2,1100 :psychic 1,1101 :dragon 1/2,1102 :fire 2,1103 :ice 1,1104 :grass 1/2,1105 :ghost 1,1106 :poison 1,1107 :flying 1,1108 :normal 1,1109 :rock 2,1110 :electric 1,1111 :ground 2,1112 :fighting 1,1113 :dark 1,1114 :steel 1,1115 :bug 1}1116 #+end_example1118 Now =pokemon.types/susceptibility= reports the /attack-type/1119 combination's effectiveness against other types.1121 The 20 type combo achieves its goal in a very clever way.1123 First, it weakens its effectiveness to other types at the expense of1124 making it very strong against flying.1126 #+begin_src clojure :results output :exports both1127 (clojure.pprint/pprint1128 (pokemon.lpsolve/attack-mode1129 (pokemon.types/susceptibility1130 [:normal :normal :normal :normal1131 :ice :ice :ice :ice1132 :electric :electric :electric1133 :rock :rock])))1134 #+end_src1136 #+results:1137 #+begin_example1138 {:water 1/2,1139 :psychic 1,1140 :dragon 2,1141 :fire 1/4,1142 :ice 1/4,1143 :grass 2,1144 :ghost 0,1145 :poison 1,1146 :flying 512,1147 :normal 1,1148 :rock 1/16,1149 :electric 1/8,1150 :ground 0,1151 :fighting 1/4,1152 :dark 1,1153 :steel 1/1024,1154 :bug 4}1155 #+end_example1157 Then, it removes it's strengths against Flying, Normal, and Fighting1158 by adding Ghost and Ground.1160 #+begin_src clojure :results output :exports both1161 (clojure.pprint/pprint1162 (pokemon.lpsolve/attack-mode1163 (pokemon.types/susceptibility1164 [:normal :normal :normal :normal1165 :ice :ice :ice :ice1166 :electric :electric :electric1167 :rock :rock1168 ;; Spot resistances1169 :ghost :ground])))1170 #+end_src1172 #+results:1173 #+begin_example1174 {:water 1/2,1175 :psychic 2,1176 :dragon 2,1177 :fire 1/2,1178 :ice 1/4,1179 :grass 1,1180 :ghost 0,1181 :poison 2,1182 :flying 0,1183 :normal 0,1184 :rock 1/8,1185 :electric 1/4,1186 :ground 0,1187 :fighting 1/4,1188 :dark 1/2,1189 :steel 1/1024,1190 :bug 2}1191 #+end_example1193 Adding the pair Psychic and Fighting takes care of its strength1194 against Psychic and makes it ineffective against Dark, which is immune1195 to Psychic.1197 Adding the pair Grass and Poison makes takes care of its strength1198 against poison and makes it ineffective against Steel, which is immune1199 to poison.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 :poison])))1215 #+end_src1217 #+results:1218 #+begin_example1219 {:water 1,1220 :psychic 1/2,1221 :dragon 1,1222 :fire 1/4,1223 :ice 1/2,1224 :grass 1,1225 :ghost 0,1226 :poison 1/2,1227 :flying 0,1228 :normal 0,1229 :rock 1/4,1230 :electric 1/4,1231 :ground 0,1232 :fighting 1/2,1233 :dark 0,1234 :steel 0,1235 :bug 1/2}1236 #+end_example1238 Can you see the final step?1240 It's adding the Water type, which is weak against Water and Dragon and1241 strong against Rock and Fire.1243 #+begin_src clojure :results output :exports both1244 (clojure.pprint/pprint1245 (pokemon.lpsolve/attack-mode1246 (pokemon.types/susceptibility1247 [;; setup1248 :normal :normal :normal :normal1249 :ice :ice :ice :ice1250 :electric :electric :electric1251 :rock :rock1252 ;; Spot resistances1253 :ghost :ground1254 ;; Pair resistances1255 :psychic :fighting1256 :grass :poison1257 ;; completion1258 :water])))1259 #+end_src1261 #+results:1262 #+begin_example1263 {:water 1/2,1264 :psychic 1/2,1265 :dragon 1/2,1266 :fire 1/2,1267 :ice 1/2,1268 :grass 1/2,1269 :ghost 0,1270 :poison 1/2,1271 :flying 0,1272 :normal 0,1273 :rock 1/2,1274 :electric 1/4,1275 :ground 0,1276 :fighting 1/2,1277 :dark 0,1278 :steel 0,1279 :bug 1/2}1280 #+end_example1282 Which makes a particularly beautiful combination which is ineffective1283 against all defending types.1286 # #+begin_src clojure :results scalar :exports both1287 # (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])))))1288 # #+end_src1290 # #+results:1291 # | [: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] |1294 Is there anything else that's interesting?1296 #+begin_src clojure :exports both1297 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))1298 #+end_src1300 #+results:1301 : INFEASIBLE1303 #+begin_src clojure :exports both1304 (pokemon.types/old-school1305 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))1306 #+end_src1308 #+results:1309 : INFEASIBLE1311 #+begin_src clojure :exports both1312 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))1313 #+end_src1315 #+results:1316 : INFEASIBLE1318 #+begin_src clojure :exports both1319 (pokemon.types/old-school1320 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))1321 #+end_src1323 #+results:1324 : INFEASIBLE1326 #+begin_src clojure :exports both1327 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))1328 #+end_src1330 #+results:1331 : INFEASIBLE1333 #+begin_src clojure :exports both1334 (pokemon.types/old-school1335 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))1336 #+end_src1338 #+results:1339 : INFEASIBLE1341 There is no way to produce a defense-type that is weak to all types.1342 This is probably because there are many types that are completely1343 immune to some types, such as Flying, which is immune to Ground. A1344 perfectly weak type could not use any of these types.1346 * Summary1348 Overall, the pok\eacute{}mon type system is slanted more towards defense1349 rather than offense. While it is possible to create superior1350 defensive types and exceptionally weak attack types, it is not possible to1351 create exceptionally weak defensive types or very powerful attack1352 types.1354 Using the =lp_solve= library was more complicated than the best-first1355 search, but yielded results quickly and efficiently. Expressing the1356 problem in a linear form does have its drawbacks, however --- it's1357 hard to ask questions such as "what is the best 3-type defensive combo1358 in terms of susceptibility?", since susceptibility is not a linear1359 function of a combo's types. It is also hard to get all the solutions1360 to a particular problem, such as all the pokemon type combinations of1361 length 8 which are immortal defense types.1364 * COMMENT main-program1365 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none1366 <<intro>>1367 <<body>>1368 <<declares>>1369 <<memory-management>>1370 <<get-results>>1371 <<solve>>1372 <<farmer-example>>1373 <<lp-solve>>1374 <<better-farmer>>1375 <<pokemon-lp>>1376 <<results>>1377 <<attack-oriented>>1378 #+end_src1381 * COMMENT Stuff to do.1382 ** TODO fix namespaces to not use rlm.light-base