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 Optimization
2 #+author: Robert McIntyre & Dylan Holmes
3 #+EMAIL: rlm@mit.edu
4 #+description: Using Lpsolve to find effective pokemon types in clojure.
5 #+keywords: Pokemon, clojure, linear optimization, lp_solve, LpSolve
6 #+SETUPFILE: ../../aurellem/org/setup.org
7 #+INCLUDE: ../../aurellem/org/level-0.org
9 * Introduction
10 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 each
12 other using fantastic attacks. It was made into a several gameboy
13 games that all share the same battle system. Every pok\eacute{}mon in
14 the gameboy game has one or two /types/, such as Ground, Fire, Water,
15 etc. Every pok\eacute{}mon attack has exactly one type. Certain
16 defending types are weak or strong to other attacking types. For
17 example, Water attacks are strong against Fire pok\eacute{}mon, while
18 Electric attacks are weak against Ground Pok\eacute{}mon. In the
19 games, attacks can be either twice as effective as normal (Water
20 vs. Fire), neutrally effective (Normal vs. Normal), half as effective
21 (Fire vs. Water), or not effective at all (Electric vs. Ground). We
22 represent these strengths and weaknesses as the numbers 2, 1,
23 $\frac{1}{2}$, and 0, and call them the /susceptance/ of one type to
24 another.
26 If a pokemon has two types, then the strengths and weakness of each
27 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, then
31 the combination is doubly-resistant (1/4x) to that type. If both types
32 are weak to a certain type then the combination is double-weak (4x) to
33 that type.
35 In the [[./types.org][previous post]], we used the best-first search algorithm to find
36 the most effective Pok\eacute{}mon type combinations. Afterwards, we
37 realized that we could transform this search problem into a /linear
38 optimization problem/. This conversion offers several advantages:
39 first, search algorithms are comparatively slow, whereas linear
40 optimization algorithms are extremely fast; second, it is difficult to
41 determine whether a search problem has any solution, whereas it is
42 straightforward to determine whether a linear optimization problem has
43 any solution; finally, because systems of linear equations are so
44 common, many programming languages have linear equation solvers
45 written for them.
47 In this article, we will:
48 - Solve a simple linear optimization problem in C :: We demonstrate
49 how to use the linear programming C library, =lp_solve=, to
50 solve a simple linear optimization problem.
51 - Incorporate a C library into Clojure :: We will show how we gave
52 Clojure access to the linear programming C library, =lp_solve=.
53 - Find effective Pokemon types using linear programming :: Building
54 on our earlier code, we answer some questions that were
55 impossible to answer using best-first search.
56 - Present our results :: We found some cool examples and learned a lot
57 about the pok\eacute{}mon type system as a whole.
60 ** Immortal Types
62 In the game, pok\eacute{}mon can have either one type or two types. If
63 this restriction is lifted, is there any combination of types that is
64 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 towards
66 infinity, the resulting type would be immune to all attack types.
68 * Linear Programming
70 Linear programming is the process of finding an optimal solution to a
71 linear equation of several variables which are constrained by some linear
72 inequalities.
74 ** The Farmer's Problem
76 Let's solve the Farmer's Problem, an example linear programming problem
77 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.
80 #+BEGIN_QUOTE
81 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to
82 plant two crops: wheat and barley. To produce these crops, it costs
83 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat
84 and $210 per acre for the barley. The farmer has $15000 available for
85 expenses. But after the harvest, the farmer must store the crops while
86 awaiting favorable market conditions. The farmer has storage space
87 for 4000 bushels. Each acre yields an average of 110 bushels of wheat
88 or 30 bushels of barley. If the net profit per bushel of wheat (after
89 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_QUOTE
93 The Farmer's Problem is to maximize profit subject to constraints on
94 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 Solve
106 In a new file, =farmer.lp=, we list the variables and constraints
107 of our problem using LP Solve syntax.
109 #+begin_src lpsolve :tangle ../lp/farmer.lp
110 /* 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_src
126 Running the =lp_solve= program on =farmer.lp= yields the following output.
128 #+begin_src sh :exports both :results scalar
129 lp_solve ~/proj/pokemon-types/lp/farmer.lp
130 #+end_src
132 #+results:
133 :
134 : Value of objective function: 6315.62500000
135 :
136 : Actual values of the variables:
137 : wheat 21.875
138 : barley 53.125
141 This shows that the farmer can maximize his profit by planting 21.875
142 of the available acres with wheat and the remaining 53.125 acres with
143 barley; by doing so, he will make $6315.62(5) in profit.
145 * Incorporating =lp_solve= into Clojure
147 There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java
148 programs to use =lp_solve=. Although Clojure can use this Java API
149 directly, the interaction between Java, C, and Clojure is clumsy:
151 ** The Farmer's Problem in Clojure
153 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: intro
157 #+begin_src clojure :results silent
158 (ns pokemon.lpsolve
159 ;;(: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_src
166 The =lp_solve= Java interface is available from the same site as
167 =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the
168 same as many other =C= programs. There are excellent instructions to
169 get set up. The short version is that you must call Java with
170 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the
171 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For
172 example, in my =.bashrc= file, I have the line
173 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything
174 is set-up correctly,
176 #+name: body
177 #+begin_src clojure :results verbatim :exports both
178 (import 'lpsolve.LpSolve)
179 #+end_src
181 #+results: body
182 : lpsolve.LpSolve
184 should run with no problems.
186 ** Making a DSL to talk with LpSolve
187 *** Problems
188 Since we are using a =C= wrapper, we have to deal with manual memory
189 management for the =C= structures which are wrapped by the =LpSolve=
190 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's
191 very important to get it right. Also, the Java wrapper follows the
192 =C= tradition closely and defines many =static final int= constants
193 for the different states of the =LpSolve= instance instead of using Java
194 enums. The calling convention for adding rows and columns to
195 the constraint matrix is rather complicated and must be done column by
196 column or row by row, which can be error prone. Finally, I'd like to
197 gather all the important output information from the =LpSolve= instance
198 into a final, immutable structure.
200 In summary, the issues I'd like to address are:
202 - reliable memory management
203 - functional interface to =LpSolve=
204 - intelligible, immutable output
206 To deal with these issues I'll create four functions for interfacing
207 with =LpSolve=
209 #+name: declares
210 #+begin_src clojure :results silent
211 (in-ns 'pokemon.lpsolve)
213 ;; deal with automatic memory management for LpSolve instance.
214 (declare linear-program)
216 ;; functional interface to LpSolve
217 (declare lp-solve)
219 ;; immutable output from lp-solve
220 (declare solve get-results)
221 #+end_src
224 *** Memory Management
226 Every instance of =LpSolve= must be manually garbage collected via a
227 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=
228 to ensure that =deleteLP= is always called.
230 #+name: memory-management
231 #+begin_src clojure :results silent
232 (in-ns 'pokemon.lpsolve)
233 (defmacro linear-program
234 "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 statements
240 ;; always free the =C= data structures.
241 '((finally (.deleteLp lps))))))
242 #+end_src
245 The macro captures the variable =lps= within its body, providing for a
246 convenient way to access the object using any of the methods of the
247 =LpSolve= API without having to worry about when to call
248 =deleteLP=.
250 *** Sensible Results
251 The =linear-program= macro deletes the actual =lps= object once it is
252 done working, so it's important to collect the important results and
253 add return them in an immutable structure at the end.
255 #+name: get-results
256 #+begin_src clojure :results silent
257 (in-ns 'pokemon.lpsolve)
259 (defrecord LpSolution
260 [objective-value
261 optimal-values
262 variable-names
263 solution
264 status
265 model])
267 (defn model
268 "Returns a textual representation of the problem suitable for
269 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 results
276 "Given an LpSolve object, solves the object and returns a map of the
277 essential values which compose the solution."
278 [#^LpSolve lps]
279 (locking lps
280 (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-names
286 (doall
287 ;; The doall is necessary since the lps object might
288 ;; soon be deleted.
289 (map
290 #(.getColName lps (inc %))
291 (range number-of-variables)))
292 model (model lps)]
293 (LpSolution.
294 (.getObjective lps)
295 optimal-values
296 variable-names
297 (zipmap variable-names optimal-values)
298 status
299 model))))
301 #+end_src
303 Here I've created an object called =LpSolution= which stores the
304 important results from a session with =lp_solve=. Of note is the
305 =model= function which returns the problem in a form that can be
306 solved by other linear programming packages.
308 *** Solution Status of an LpSolve Object
310 #+name: solve
311 #+begin_src clojure :results silent
312 (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-map
324 "Takes a class and creates a map of the static constant integer
325 fields with their names. This helps with C wrappers where they have
326 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 solve
336 "Solve an instance of LpSolve and return a string representing the
337 status of the computation. Will only solve a particular LpSolve
338 instance once."
339 [#^LpSolve lps]
340 ((constant-map LpSolve)
341 (.solve lps)))
343 #+end_src
345 The =.solve= method of an =LpSolve= object only returns an integer code
346 to specify the status of the computation. The =solve= method here
347 uses reflection to look up the actual name of the status code and
348 returns a more helpful status message that is also resistant to
349 changes in the meanings of the code numbers.
351 *** The Farmer Example in Clojure, Pass 1
353 Now we can implement a nicer version of the examples from the
354 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less
355 line-by-line translation of the Java code from that example.
357 #+name: farmer-example
358 #+begin_src clojure :results silent
359 (in-ns 'pokemon.lpsolve)
360 (defn farmer-example []
361 (linear-program
362 (results
363 (doto lps
364 ;; name the columns
365 (.setColName 1 "wheat")
366 (.setColName 2 "barley")
367 (.setAddRowmode true)
368 ;; row 1 : 120x + 210y <= 15000
369 (.addConstraintex 2
370 (double-array [120 210])
371 (int-array [1 2])
372 LpSolve/LE
373 15e3)
374 ;; row 2 : 110x + 30y <= 4000
375 (.addConstraintex 2
376 (double-array [110 30])
377 (int-array [1 2])
378 LpSolve/LE
379 4e3)
380 ;; ;; row 3 : x + y <= 75
381 (.addConstraintex 2
382 (double-array [1 1])
383 (int-array [1 2])
384 LpSolve/LE
385 75)
386 (.setAddRowmode false)
388 ;; add constraints
389 (.setObjFnex 2
390 (double-array [143 60])
391 (int-array [1 2]))
393 ;; set this as a maximization problem
394 (.setMaxim)))))
396 #+end_src
398 #+begin_src clojure :results output :exports both
399 (clojure.pprint/pprint
400 (:solution (pokemon.lpsolve/farmer-example)))
401 #+end_src
403 #+results:
404 : {"barley" 53.12499999999999, "wheat" 21.875}
406 And it works as expected!
408 *** The Farmer Example in Clojure, Pass 2
409 We don't have to worry about memory management anymore, and the farmer
410 example is about half as long as the example from the =LpSolve=
411 website, but we can still do better. Solving linear problems is all
412 about the constraint matrix $A$ , the objective function $c$, and the
413 right-hand-side $b$, plus whatever other options one cares to set for
414 the particular instance of =lp_solve=. Why not make a version of
415 =linear-program= that takes care of initialization?
419 #+name: lp-solve
420 #+begin_src clojure :results silent
421 (in-ns 'pokemon.lpsolve)
422 (defn initialize-lpsolve-row-oriented
423 "fill in an lpsolve instance using a constraint matrix =A=, the
424 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 the
431 ;; documentation. First, set row mode, then set the objective
432 ;; function, then set each row of the problem, and then turn off row
433 ;; mode.
434 (.setAddRowmode lps true)
435 (.setObjFnex lps (count c)
436 (double-array c)
437 (int-array (range 1 (inc (count c)))))
438 (dorun
439 (for [n (range (count A))]
440 (let [row (nth A n)
441 row-length (int (count row))]
442 (.addConstraintex lps
443 row-length
444 (double-array row)
445 (int-array (range 1 (inc row-length)))
446 LpSolve/LE
447 (double (nth b n))))))
448 (.setAddRowmode lps false)
449 lps)
452 (defmacro lp-solve
453 "by default:,
454 minimize (* c x), subject to (<= (* A x) b),
455 using continuous variables. You may set any number of
456 other options as in the LpSolve API."
457 [A b c & lp-solve-forms]
458 ;; assume that A is a vector of vectors
459 (concat
460 (list 'linear-program
461 (list 'initialize-lpsolve-row-oriented 'lps A b c))
462 `~lp-solve-forms))
463 #+end_src
465 Now, we can use a much more functional approach to solving the
466 farmer's problem:
468 #+name: better-farmer
469 #+begin_src clojure :results silent
470 (in-ns 'pokemon.lpsolve)
471 (defn better-farmer-example []
472 (lp-solve [[120 210]
473 [110 30]
474 [1 1]]
475 [15000
476 4000
477 75]
478 [143 60]
479 (.setColName lps 1 "wheat")
480 (.setColName lps 2 "barley")
481 (.setMaxim lps)
482 (results lps)))
483 #+end_src
485 #+begin_src clojure :exports both :results verbatim
486 (vec (:solution (pokemon.lpsolve/better-farmer-example)))
487 #+end_src
489 #+results:
490 : [["barley" 53.12499999999999] ["wheat" 21.875]]
492 Notice that both the inputs to =better-farmer-example= and the results
493 are immutable.
495 * Using LpSolve to find Immortal Types
496 ** Converting the Pok\eacute{}mon problem into a linear form
497 How can the original question about pok\eacute{}mon types be converted
498 into a linear problem?
500 Pokemon types can be considered to be vectors of numbers representing
501 their susceptances to various attacking types, so Water might look
502 something like this.
504 #+begin_src clojure :results scalar :exports both
505 (:water (pokemon.types/defense-strengths))
506 #+end_src
508 #+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 the
512 attacking types in the following order:
514 #+begin_src clojure :results output :exports both
515 (clojure.pprint/pprint
516 (pokemon.types/type-names))
517 #+end_src
519 #+results:
520 #+begin_example
521 [:normal
522 :fire
523 :water
524 :electric
525 :grass
526 :ice
527 :fighting
528 :poison
529 :ground
530 :flying
531 :psychic
532 :bug
533 :rock
534 :ghost
535 :dragon
536 :dark
537 :steel]
538 #+end_example
541 So, for example, Water is resistant (x0.5) against Fire, which is
542 the second element in the list.
544 To combine types, these sorts of vectors are multiplied together
545 pair-wise to yield the resulting combination.
547 Unfortunately, we need some way to add two type vectors together
548 instead of multiplying them if we want to solve the problem with
549 =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 the
552 susceptance of each type, then finding an immortal type corresponds to
553 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 we
555 want to find the immortal type which uses the least amount of types.
557 #+name: pokemon-lp
558 #+begin_src clojure :results silent
559 (in-ns 'pokemon.lpsolve)
561 (defn log-clamp-matrix [matrix]
562 ;; we have to clamp the Infinities to a more reasonable negative
563 ;; value because lp_solve does not play well with infinities in its
564 ;; constraint matrix.
565 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))
566 (incanter.core/log2
567 (incanter.core/trans
568 matrix))))
570 ;; constraint matrices
571 (defn log-defense-matrix []
572 (log-clamp-matrix
573 (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 vectors
580 (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 functions
590 (defn number-of-types []
591 (doall (map (constantly 1) (pokemon.types/type-names))))
593 (defn set-constraints
594 "sets all the constraints for an lpsolve instance to the given
595 constraint. =constraint= here is one of the LpSolve constants such
596 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-discrete
604 "sets every variable in an lps problem to be a discrete rather than
605 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-names
612 "sets the variable names of the problem given a vector of names"
613 [#^LpSolve lps names]
614 (dorun
615 (keep-indexed
616 (fn [index name]
617 (.setColName lps (inc index) (str name)))
618 ;; ONE based indexing!!!
619 names)))
621 (defn poke-solve
622 ([poke-matrix target objective-function constraint min-num-types]
623 ;; must have at least one type
624 (let [poke-matrix
625 (concat poke-matrix
626 [(map (constantly 1)
627 (range (count (first poke-matrix))))])
628 target (concat target [min-num-types])]
629 (lp-solve poke-matrix target objective-function
630 (set-constraints lps constraint)
631 ;; must have more than min-num-types
632 (.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 type
638 (poke-solve poke-matrix target objective-function constraint 1)))
640 (defn solution
641 "If the results of an lpsolve operation are feasible, returns the
642 results. Otherwise, returns the error."
643 [results]
644 (if (not (= (:status results) "OPTIMAL"))
645 (:status results)
646 (:solution results)))
647 #+end_src
649 With this, we are finally able to get some results.
651 ** Results
652 #+name: results
653 #+begin_src clojure :results silent
654 (in-ns 'pokemon.lpsolve)
656 (defn best-defense-type
657 "finds a type combination which is resistant to all attacks."
658 []
659 (poke-solve
660 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))
662 (defn worst-attack-type
663 "finds the attack type which is not-very-effective against all pure
664 defending types (each single defending type is resistant to this
665 attack combination"
666 []
667 (poke-solve
668 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))
670 (defn worst-defense-type
671 "finds a defending type that is weak to all single attacking types."
672 []
673 (poke-solve
674 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))
676 (defn best-attack-type
677 "finds an attack type which is super effective against all single
678 defending types"
679 []
680 (poke-solve
681 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))
683 (defn solid-defense-type
684 "finds a defense type which is either neutral or resistant to all
685 single attacking types"
686 []
687 (poke-solve
688 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))
690 (defn solid-attack-type
691 "finds an attack type which is either neutral or super-effective to
692 all single attacking types."
693 []
694 (poke-solve
695 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))
697 (defn weak-defense-type
698 "finds a defense type which is either neutral or weak to all single
699 attacking types"
700 []
701 (poke-solve
702 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))
704 (defn neutral-defense-type
705 "finds a defense type which is perfectly neutral to all attacking
706 types."
707 []
708 (poke-solve
709 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))
711 #+end_src
713 *** Strongest Attack/Defense Combinations
715 #+begin_src clojure :results output :exports both
716 (clojure.pprint/pprint
717 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))
718 #+end_src
720 #+results:
721 #+begin_example
722 {":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_example
741 # #+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. By
746 combining Steel, Water, Poison, and three types which each have complete
747 immunities to various other types, we've created a type that is resistant to
748 all attacking types.
750 #+begin_src clojure :results output :exports both
751 (clojure.pprint/pprint
752 (pokemon.types/susceptibility
753 [:poison :poison :water :water :steel :ground :flying :dark]))
754 #+end_src
756 #+results:
757 #+begin_example
758 {: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_example
777 # #+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 both
784 (clojure.pprint/pprint
785 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))
786 #+end_src
788 #+results:
789 #+begin_example
790 {":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_example
809 Dark and Ghost are the best dual-type combo, and are resistant or
810 neutral to all types.
812 #+begin_src clojure :results output :exports both
813 (clojure.pprint/pprint
814 (pokemon.types/old-school
815 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))
816 #+end_src
818 #+results:
819 #+begin_example
820 {":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_example
837 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, even
839 though the game claims that Ghost is strong against Psychic.
841 #+begin_src clojure :results verbatim :exports both
842 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))
843 #+end_src
845 #+results:
846 : INFEASIBLE
848 #+begin_src clojure :results verbatim :exports both
849 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))
850 #+end_src
852 #+results:
853 : INFEASIBLE
856 #+begin_src clojure :results verbatim :exports both
857 (pokemon.types/old-school
858 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))
859 #+end_src
861 #+results:
862 : INFEASIBLE
865 #+begin_src clojure :results output :exports both
866 (clojure.pprint/pprint
867 (pokemon.types/old-school
868 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))
869 #+end_src
871 #+results:
872 #+begin_example
873 {":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_example
890 The best attacking type combination is Dragon from the original games.
891 It is neutral against all the original types except for Dragon, which
892 it is strong against. There is no way to make an attacking type that
893 is strong against every type, or even one that is strong or neutral
894 against every type, in the new games.
897 *** Weakest Attack/Defense Combinations
899 #+begin_src clojure :results output :exports both
900 (clojure.pprint/pprint
901 (pokemon.types/old-school
902 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))
903 #+end_src
905 #+results:
906 #+begin_example
907 {":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_example
924 # #+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 both
928 (clojure.pprint/pprint
929 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))
930 #+end_src
932 #+results:
933 #+begin_example
934 {":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_example
953 # #+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 uses
958 quite a few types.
960 #+begin_src clojure :results verbatim :exports both
961 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))
962 #+end_src
964 #+results:
965 : 20.0
967 20 types is the /minimum/ number of types before the attacking
968 combination is not-very-effective or worse against all defending
969 types. This would probably have been impossible to discover using
970 best-first search, since it involves such an intricate type
971 combination.
973 It's so interesting that it takes 20 types to make an attack type that
974 is weak to all types that the combination merits further
975 investigation.
977 Unfortunately, all of the tools that we've written so far are focused
978 on defense type combinations. However, it is possible to make every
979 tool attack-oriented via a simple macro.
981 #+name: attack-oriented
982 #+begin_src clojure :results silent
983 (in-ns 'pokemon.lpsolve)
985 (defmacro attack-mode [& forms]
986 `(let [attack-strengths# pokemon.types/attack-strengths
987 defense-strengths# pokemon.types/defense-strengths]
988 (binding [pokemon.types/attack-strengths
989 defense-strengths#
990 pokemon.types/defense-strengths
991 attack-strengths#]
992 ~@forms)))
993 #+end_src
995 Now all the tools from =pokemon.types= will work for attack
996 combinations.
998 #+begin_src clojure :results output :exports both
999 (clojure.pprint/pprint
1000 (pokemon.types/susceptibility [:water]))
1001 #+end_src
1003 #+results:
1004 #+begin_example
1005 {: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_example
1025 #+begin_src clojure :results output :exports both
1026 (clojure.pprint/pprint
1027 (pokemon.lpsolve/attack-mode
1028 (pokemon.types/susceptibility [:water])))
1029 #+end_src
1031 #+results:
1032 #+begin_example
1033 {: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_example
1052 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 of
1058 making it very strong against flying.
1060 #+begin_src clojure :results output :exports both
1061 (clojure.pprint/pprint
1062 (pokemon.lpsolve/attack-mode
1063 (pokemon.types/susceptibility
1064 [:normal :normal :normal :normal
1065 :ice :ice :ice :ice
1066 :electric :electric :electric
1067 :rock :rock])))
1068 #+end_src
1070 #+results:
1071 #+begin_example
1072 {: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_example
1091 Then, it removes it's strengths against Flying, Normal, and Fighting
1092 by adding Ghost and Ground.
1094 #+begin_src clojure :results output :exports both
1095 (clojure.pprint/pprint
1096 (pokemon.lpsolve/attack-mode
1097 (pokemon.types/susceptibility
1098 [:normal :normal :normal :normal
1099 :ice :ice :ice :ice
1100 :electric :electric :electric
1101 :rock :rock
1102 ;; Spot resistances
1103 :ghost :ground])))
1104 #+end_src
1106 #+results:
1107 #+begin_example
1108 {: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_example
1127 Adding the pair Psychic and Fighting takes care of its strength
1128 against Psychic and makes it ineffective against Dark, which is immune
1129 to Psychic.
1131 Adding the pair Grass and Poison makes takes care of its strength
1132 against poison and makes it ineffective against Steel, which is immune
1133 to poison.
1135 #+begin_src clojure :results output :exports both
1136 (clojure.pprint/pprint
1137 (pokemon.lpsolve/attack-mode
1138 (pokemon.types/susceptibility
1139 [;; setup
1140 :normal :normal :normal :normal
1141 :ice :ice :ice :ice
1142 :electric :electric :electric
1143 :rock :rock
1144 ;; Spot resistances
1145 :ghost :ground
1146 ;; Pair resistances
1147 :psychic :fighting
1148 :grass :poison])))
1149 #+end_src
1151 #+results:
1152 #+begin_example
1153 {: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_example
1172 Can you see the final step?
1174 It's adding the Water type, which is weak against Water, Dragon, and
1175 Grass and strong against Rock and Fire.
1177 #+begin_src clojure :results output :exports both
1178 (clojure.pprint/pprint
1179 (pokemon.lpsolve/attack-mode
1180 (pokemon.types/susceptibility
1181 [;; setup
1182 :normal :normal :normal :normal
1183 :ice :ice :ice :ice
1184 :electric :electric :electric
1185 :rock :rock
1186 ;; Spot resistances
1187 :ghost :ground
1188 ;; Pair resistances
1189 :psychic :fighting
1190 :grass :poison
1191 ;; completion
1192 :water])))
1193 #+end_src
1195 #+results:
1196 #+begin_example
1197 {: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_example
1216 Which makes a particularly beautiful combination which is ineffective
1217 against all defending types.
1220 # #+begin_src clojure :results scalar :exports both
1221 # (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_src
1224 # #+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 both
1231 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))
1232 #+end_src
1234 #+results:
1235 : INFEASIBLE
1237 #+begin_src clojure :exports both
1238 (pokemon.types/old-school
1239 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))
1240 #+end_src
1242 #+results:
1243 : INFEASIBLE
1245 #+begin_src clojure :exports both
1246 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))
1247 #+end_src
1249 #+results:
1250 : INFEASIBLE
1252 #+begin_src clojure :exports both
1253 (pokemon.types/old-school
1254 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))
1255 #+end_src
1257 #+results:
1258 : INFEASIBLE
1260 #+begin_src clojure :exports both
1261 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))
1262 #+end_src
1264 #+results:
1265 : INFEASIBLE
1267 #+begin_src clojure :exports both
1268 (pokemon.types/old-school
1269 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))
1270 #+end_src
1272 #+results:
1273 : INFEASIBLE
1275 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 completely
1277 immune to some types, such as Flying, which is immune to Ground. A
1278 perfectly weak type could not use any of these types.
1280 * Summary
1282 Overall, the pok\eacute{}mon type system is slanted towards defense
1283 rather than offense. While it is possible to create superior
1284 defensive types and exceptionally weak attack types, it is not
1285 possible to create exceptionally weak defensive types or very powerful
1286 attack types.
1288 Using the =lp_solve= library was more complicated than the best-first
1289 search, but yielded results quickly and efficiently. Expressing the
1290 problem in a linear form does have its drawbacks, however --- it's
1291 hard to ask questions such as "what is the best 3-type defensive combo
1292 in terms of susceptibility?", since susceptibility is not a linear
1293 function of a combo's types. It is also hard to get all the solutions
1294 to a particular problem, such as all the pokemon type combinations of
1295 length 8 which are immortal defense types.
1297 * COMMENT main-program
1298 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none
1299 <<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