view org/lpsolve.org @ 21:f88b7d2c25d9

lol.
author Robert McIntyre <rlm@mit.edu>
date Tue, 17 Sep 2013 17:42:51 -0400
parents 47fa6dc56e30
children 8992278bf399
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 lol
130 #+begin_src sh :exports both :results scalar
131 lp_solve ~/proj/pokemon-types/lp/farmer.lp
132 #+end_src
134 #+results:
135 :
136 : Value of objective function: 6315.62500000
137 :
138 : Actual values of the variables:
139 : wheat 21.875
140 : barley 53.125
143 This shows that the farmer can maximize his profit by planting 21.875
144 of the available acres with wheat and the remaining 53.125 acres with
145 barley; by doing so, he will make $6315.62(5) in profit.
147 * Incorporating =lp_solve= into Clojure
149 There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java
150 programs to use =lp_solve=. Although Clojure can use this Java API
151 directly, the interaction between Java, C, and Clojure is clumsy:
153 ** The Farmer's Problem in Clojure
155 We are going to solve the same problem involving wheat and barley,
156 that we did above, but this time using clojure and the =lp_solve= API.
158 #+name: intro
159 #+begin_src clojure :results silent
160 (ns pokemon.lpsolve
161 (:import lpsolve.LpSolve)
162 (:require pokemon.types)
163 ;;(:require incanter.core)
164 (:require rlm.map-utils))
165 #+end_src
167 The =lp_solve= Java interface is available from the same site as
168 =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the
169 same as many other =C= programs. There are excellent instructions to
170 get set up. The short version is that you must call Java with
171 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the
172 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For
173 example, in my =.bashrc= file, I have the line
174 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything
175 is set-up correctly,
177 #+name: body
178 #+begin_src clojure :results verbatim :exports both
179 (import 'lpsolve.LpSolve)
180 #+end_src
182 #+results:
183 : lpsolve.LpSolve
185 should run with no problems.
187 ** Making a DSL to talk with LpSolve
188 *** Problems
189 Since we are using a =C= wrapper, we have to deal with manual memory
190 management for the =C= structures which are wrapped by the =LpSolve=
191 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's
192 very important to get it right. Also, the Java wrapper follows the
193 =C= tradition closely and defines many =static final int= constants
194 for the different states of the =LpSolve= instance instead of using Java
195 enums. The calling convention for adding rows and columns to
196 the constraint matrix is rather complicated and must be done column by
197 column or row by row, which can be error prone. Finally, I'd like to
198 gather all the important output information from the =LpSolve= instance
199 into a final, immutable structure.
201 In summary, the issues I'd like to address are:
203 - reliable memory management
204 - functional interface to =LpSolve=
205 - intelligible, immutable output
207 To deal with these issues I'll create four functions for interfacing
208 with =LpSolve=
210 #+name: declares
211 #+begin_src clojure :results silent
212 (in-ns 'pokemon.lpsolve)
214 ;; deal with automatic memory management for LpSolve instance.
215 (declare linear-program)
217 ;; functional interface to LpSolve
218 (declare lp-solve)
220 ;; immutable output from lp-solve
221 (declare solve get-results)
222 #+end_src
225 *** Memory Management
227 Every instance of =LpSolve= must be manually garbage collected via a
228 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=
229 to ensure that =deleteLP= is always called.
231 #+name: memory-management
232 #+begin_src clojure :results silent
233 (in-ns 'pokemon.lpsolve)
234 (defmacro linear-program
235 "solve a linear programming problem using LpSolve syntax.
236 within the macro, the variable =lps= is bound to the LpSolve instance."
237 [& statements]
238 (list 'let '[lps (LpSolve/makeLp 0 0)]
239 (concat '(try)
240 statements
241 ;; always free the =C= data structures.
242 '((finally (.deleteLp lps))))))
243 #+end_src
246 The macro captures the variable =lps= within its body, providing for a
247 convenient way to access the object using any of the methods of the
248 =LpSolve= API without having to worry about when to call
249 =deleteLP=.
251 *** Sensible Results
252 The =linear-program= macro deletes the actual =lps= object once it is
253 done working, so it's important to collect the important results and
254 add return them in an immutable structure at the end.
256 #+name: get-results
257 #+begin_src clojure :results silent
258 (in-ns 'pokemon.lpsolve)
260 (defrecord LpSolution
261 [objective-value
262 optimal-values
263 variable-names
264 solution
265 status
266 model])
268 (defn model
269 "Returns a textual representation of the problem suitable for
270 direct input to the =lp_solve= program (lps format)"
271 [#^LpSolve lps]
272 (let [target (java.io.File/createTempFile "lps" ".lp")]
273 (.writeLp lps (.getPath target))
274 (slurp target)))
276 (defn results
277 "Given an LpSolve object, solves the object and returns a map of the
278 essential values which compose the solution."
279 [#^LpSolve lps]
280 (locking lps
281 (let [status (solve lps)
282 number-of-variables (.getNcolumns lps)
283 optimal-values (double-array number-of-variables)
284 optimal-values (do (.getVariables lps optimal-values)
285 (seq optimal-values))
286 variable-names
287 (doall
288 ;; The doall is necessary since the lps object might
289 ;; soon be deleted.
290 (map
291 #(.getColName lps (inc %))
292 (range number-of-variables)))
293 model (model lps)]
294 (LpSolution.
295 (.getObjective lps)
296 optimal-values
297 variable-names
298 (zipmap variable-names optimal-values)
299 status
300 model))))
302 #+end_src
304 Here I've created an object called =LpSolution= which stores the
305 important results from a session with =lp_solve=. Of note is the
306 =model= function which returns the problem in a form that can be
307 solved by other linear programming packages.
309 *** Solution Status of an LpSolve Object
311 #+name: solve
312 #+begin_src clojure :results silent
313 (in-ns 'pokemon.lpsolve)
315 (defn static-integer?
316 "does the field represent a static integer constant?"
317 [#^java.lang.reflect.Field field]
318 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))
319 (integer? (.get field nil))))
321 (defn integer-constants [class]
322 (filter static-integer? (.getFields class)))
324 (defn constant-map
325 "Takes a class and creates a map of the static constant integer
326 fields with their names. This helps with C wrappers where they have
327 just defined a bunch of integer constants instead of enums."
328 [class]
329 (let [integer-fields (integer-constants class)]
330 (into (sorted-map)
331 (zipmap (map #(.get % nil) integer-fields)
332 (map #(.getName %) integer-fields)))))
334 (alter-var-root #'constant-map memoize)
336 (defn solve
337 "Solve an instance of LpSolve and return a string representing the
338 status of the computation. Will only solve a particular LpSolve
339 instance once."
340 [#^LpSolve lps]
341 ((constant-map LpSolve)
342 (.solve lps)))
344 #+end_src
346 The =.solve= method of an =LpSolve= object only returns an integer code
347 to specify the status of the computation. The =solve= method here
348 uses reflection to look up the actual name of the status code and
349 returns a more helpful status message that is also resistant to
350 changes in the meanings of the code numbers.
352 *** The Farmer Example in Clojure, Pass 1
354 Now we can implement a nicer version of the examples from the
355 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less
356 line-by-line translation of the Java code from that example.
358 #+name: farmer-example
359 #+begin_src clojure :results silent
360 (in-ns 'pokemon.lpsolve)
361 (defn farmer-example []
362 (linear-program
363 (results
364 (doto lps
365 ;; name the columns
366 (.setColName 1 "wheat")
367 (.setColName 2 "barley")
368 (.setAddRowmode true)
369 ;; row 1 : 120x + 210y <= 15000
370 (.addConstraintex 2
371 (double-array [120 210])
372 (int-array [1 2])
373 LpSolve/LE
374 15e3)
375 ;; row 2 : 110x + 30y <= 4000
376 (.addConstraintex 2
377 (double-array [110 30])
378 (int-array [1 2])
379 LpSolve/LE
380 4e3)
381 ;; ;; row 3 : x + y <= 75
382 (.addConstraintex 2
383 (double-array [1 1])
384 (int-array [1 2])
385 LpSolve/LE
386 75)
387 (.setAddRowmode false)
389 ;; add constraints
390 (.setObjFnex 2
391 (double-array [143 60])
392 (int-array [1 2]))
394 ;; set this as a maximization problem
395 (.setMaxim)))))
397 #+end_src
399 #+begin_src clojure :results output :exports both
400 (clojure.pprint/pprint
401 (:solution (pokemon.lpsolve/farmer-example)))
402 #+end_src
404 #+results:
405 : {"barley" 53.12499999999999, "wheat" 21.875}
408 And it works as expected!
410 *** The Farmer Example in Clojure, Pass 2
411 We don't have to worry about memory management anymore, and the farmer
412 example is about half as long as the example from the =LpSolve=
413 website, but we can still do better. Solving linear problems is all
414 about the constraint matrix $A$ , the objective function $c$, and the
415 right-hand-side $b$, plus whatever other options one cares to set for
416 the particular instance of =lp_solve=. Why not make a version of
417 =linear-program= that takes care of initialization?
421 #+name: lp-solve
422 #+begin_src clojure :results silent
423 (in-ns 'pokemon.lpsolve)
424 (defn initialize-lpsolve-row-oriented
425 "fill in an lpsolve instance using a constraint matrix =A=, the
426 objective function =c=, and the right-hand-side =b="
427 [#^lpsolve.LpSolve lps A b c]
428 ;; set the name of the last column to _something_
429 ;; this appears to be necessary to ensure proper initialization.
430 (.setColName lps (count c) (str "C" (count c)))
432 ;; This is the recommended way to "fill-in" an lps instance from the
433 ;; documentation. First, set row mode, then set the objective
434 ;; function, then set each row of the problem, and then turn off row
435 ;; mode.
436 (.setAddRowmode lps true)
437 (.setObjFnex lps (count c)
438 (double-array c)
439 (int-array (range 1 (inc (count c)))))
440 (dorun
441 (for [n (range (count A))]
442 (let [row (nth A n)
443 row-length (int (count row))]
444 (.addConstraintex lps
445 row-length
446 (double-array row)
447 (int-array (range 1 (inc row-length)))
448 LpSolve/LE
449 (double (nth b n))))))
450 (.setAddRowmode lps false)
451 lps)
454 (defmacro lp-solve
455 "by default:,
456 minimize (* c x), subject to (<= (* A x) b),
457 using continuous variables. You may set any number of
458 other options as in the LpSolve API."
459 [A b c & lp-solve-forms]
460 ;; assume that A is a vector of vectors
461 (concat
462 (list 'linear-program
463 (list 'initialize-lpsolve-row-oriented 'lps A b c))
464 `~lp-solve-forms))
465 #+end_src
467 Now, we can use a much more functional approach to solving the
468 farmer's problem:
470 #+name: better-farmer
471 #+begin_src clojure :results silent
472 (in-ns 'pokemon.lpsolve)
473 (defn better-farmer-example []
474 (lp-solve [[120 210]
475 [110 30]
476 [1 1]]
477 [15000
478 4000
479 75]
480 [143 60]
481 (.setColName lps 1 "wheat")
482 (.setColName lps 2 "barley")
483 (.setMaxim lps)
484 (results lps)))
485 #+end_src
487 #+begin_src clojure :exports both :results verbatim
488 (vec (:solution (pokemon.lpsolve/better-farmer-example)))
489 #+end_src
491 #+results:
492 : [["barley" 53.12499999999999] ["wheat" 21.875]]
494 Notice that both the inputs to =better-farmer-example= and the results
495 are immutable.
497 * Using LpSolve to find Immortal Types
498 ** Converting the Pok\eacute{}mon problem into a linear form
499 How can the original question about pok\eacute{}mon types be converted
500 into a linear problem?
502 Pokemon types can be considered to be vectors of numbers representing
503 their susceptances to various attacking types, so Water might look
504 something like this.
506 #+begin_src clojure :results scalar :exports both
507 (:water (pokemon.types/defense-strengths))
508 #+end_src
510 #+results:
511 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]
513 Where the numbers represent the susceptibility of Water to the
514 attacking types in the following order:
516 #+begin_src clojure :results output :exports both
517 (clojure.pprint/pprint
518 (pokemon.types/type-names))
519 #+end_src
521 #+results:
522 #+begin_example
523 [:normal
524 :fire
525 :water
526 :electric
527 :grass
528 :ice
529 :fighting
530 :poison
531 :ground
532 :flying
533 :psychic
534 :bug
535 :rock
536 :ghost
537 :dragon
538 :dark
539 :steel]
540 #+end_example
543 So, for example, Water is resistant (x0.5) against Fire, which is
544 the second element in the list.
546 To combine types, these sorts of vectors are multiplied together
547 pair-wise to yield the resulting combination.
549 Unfortunately, we need some way to add two type vectors together
550 instead of multiplying them if we want to solve the problem with
551 =lp_solve=. Taking the log of the vector does just the trick.
553 If we make a matrix with each column being the log (base 2) of the
554 susceptance of each type, then finding an immortal type corresponds to
555 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)
556 and setting the constraint vector $c$ to all ones, which means that we
557 want to find the immortal type which uses the least amount of types.
559 #+name: pokemon-lp
560 #+begin_src clojure :results silent
561 (in-ns 'pokemon.lpsolve)
563 (defn log-clamp-matrix [matrix]
564 ;; we have to clamp the Infinities to a more reasonable negative
565 ;; value because lp_solve does not play well with infinities in its
566 ;; constraint matrix.
567 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %)
568 (map #(/ (Math/log %) (Math/log 2)) row)))
569 (apply mapv vector ;; transpose
570 matrix)))
572 ;; constraint matrices
573 (defn log-defense-matrix []
574 (log-clamp-matrix
575 (doall (map (pokemon.types/defense-strengths)
576 (pokemon.types/type-names)))))
578 (defn log-attack-matrix []
579 (apply mapv vector (log-defense-matrix)))
581 ;; target vectors
582 (defn all-resistant []
583 (doall (map (constantly -1) (pokemon.types/type-names))))
585 (defn all-weak []
586 (doall (map (constantly 1) (pokemon.types/type-names))))
588 (defn all-neutral []
589 (doall (map (constantly 0) (pokemon.types/type-names))))
591 ;; objective functions
592 (defn number-of-types []
593 (doall (map (constantly 1) (pokemon.types/type-names))))
595 (defn set-constraints
596 "sets all the constraints for an lpsolve instance to the given
597 constraint. =constraint= here is one of the LpSolve constants such
598 as LpSolve/EQ."
599 [#^LpSolve lps constraint]
600 (dorun (map (fn [index] (.setConstrType lps index constraint))
601 ;; ONE based indexing!!!
602 (range 1 (inc (.getNrows lps))))))
604 (defn set-discrete
605 "sets every variable in an lps problem to be a discrete rather than
606 continuous variable"
607 [#^LpSolve lps]
608 (dorun (map (fn [index] (.setInt lps index true))
609 ;; ONE based indexing!!!
610 (range 1 (inc (.getNcolumns lps))))))
612 (defn set-variable-names
613 "sets the variable names of the problem given a vector of names"
614 [#^LpSolve lps names]
615 (dorun
616 (keep-indexed
617 (fn [index name]
618 (.setColName lps (inc index) (str name)))
619 ;; ONE based indexing!!!
620 names)))
622 (defn poke-solve
623 ([poke-matrix target objective-function constraint min-num-types]
624 ;; must have at least one type
625 (let [poke-matrix
626 (concat poke-matrix
627 [(map (constantly 1)
628 (range (count (first poke-matrix))))])
629 target (concat target [min-num-types])]
630 (lp-solve poke-matrix target objective-function
631 (set-constraints lps constraint)
632 ;; must have more than min-num-types
633 (.setConstrType lps (count target) LpSolve/GE)
634 (set-discrete lps)
635 (set-variable-names lps (pokemon.types/type-names))
636 (results lps))))
637 ([poke-matrix target objective-function constraint]
638 ;; at least one type
639 (poke-solve poke-matrix target objective-function constraint 1)))
641 (defn solution
642 "If the results of an lpsolve operation are feasible, returns the
643 results. Otherwise, returns the error."
644 [results]
645 (if (not (= (:status results) "OPTIMAL"))
646 (:status results)
647 (:solution results)))
648 #+end_src
650 With this, we are finally able to get some results.
652 ** Results
653 #+name: results
654 #+begin_src clojure :results silent
655 (in-ns 'pokemon.lpsolve)
657 (defn best-defense-type
658 "finds a type combination which is resistant to all attacks."
659 []
660 (poke-solve
661 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))
663 (defn worst-attack-type
664 "finds the attack type which is not-very-effective against all pure
665 defending types (each single defending type is resistant to this
666 attack combination"
667 []
668 (poke-solve
669 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))
671 (defn worst-defense-type
672 "finds a defending type that is weak to all single attacking types."
673 []
674 (poke-solve
675 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))
677 (defn best-attack-type
678 "finds an attack type which is super effective against all single
679 defending types"
680 []
681 (poke-solve
682 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))
684 (defn solid-defense-type
685 "finds a defense type which is either neutral or resistant to all
686 single attacking types"
687 []
688 (poke-solve
689 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))
691 (defn solid-attack-type
692 "finds an attack type which is either neutral or super-effective to
693 all single attacking types."
694 []
695 (poke-solve
696 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))
698 (defn weak-defense-type
699 "finds a defense type which is either neutral or weak to all single
700 attacking types"
701 []
702 (poke-solve
703 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))
705 (defn neutral-defense-type
706 "finds a defense type which is perfectly neutral to all attacking
707 types."
708 []
709 (poke-solve
710 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))
712 #+end_src
714 *** Strongest Attack/Defense Combinations
716 #+begin_src clojure :results output :exports both
717 (clojure.pprint/pprint
718 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))
719 #+end_src
721 #+results:
722 #+begin_example
723 {":normal" 0.0,
724 ":ground" 1.0,
725 ":poison" 2.0,
726 ":flying" 1.0,
727 ":fighting" 0.0,
728 ":dragon" 0.0,
729 ":fire" 0.0,
730 ":dark" 1.0,
731 ":ice" 0.0,
732 ":steel" 1.0,
733 ":ghost" 0.0,
734 ":electric" 0.0,
735 ":bug" 0.0,
736 ":psychic" 0.0,
737 ":grass" 0.0,
738 ":water" 2.0,
739 ":rock" 0.0}
740 #+end_example
742 # #+results-old:
743 # : [[":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]]
746 This is the immortal type combination we've been looking for. By
747 combining Steel, Water, Poison, and three types which each have complete
748 immunities to various other types, we've created a type that is resistant to
749 all attacking types.
751 #+begin_src clojure :results output :exports both
752 (clojure.pprint/pprint
753 (pokemon.types/susceptibility
754 [:poison :poison :water :water :steel :ground :flying :dark]))
755 #+end_src
757 #+results:
758 #+begin_example
759 {:water 1/2,
760 :psychic 0,
761 :dragon 1/2,
762 :fire 1/2,
763 :ice 1/2,
764 :grass 1/2,
765 :ghost 1/4,
766 :poison 0,
767 :flying 1/2,
768 :normal 1/2,
769 :rock 1/2,
770 :electric 0,
771 :ground 0,
772 :fighting 1/2,
773 :dark 1/4,
774 :steel 1/8,
775 :bug 1/8}
776 #+end_example
778 # #+results-old:
779 # : {: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}
782 Cool!
784 #+begin_src clojure :results output :exports both
785 (clojure.pprint/pprint
786 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))
787 #+end_src
789 #+results:
790 #+begin_example
791 {":normal" 0.0,
792 ":ground" 0.0,
793 ":poison" 0.0,
794 ":flying" 0.0,
795 ":fighting" 0.0,
796 ":dragon" 0.0,
797 ":fire" 0.0,
798 ":dark" 1.0,
799 ":ice" 0.0,
800 ":steel" 0.0,
801 ":ghost" 1.0,
802 ":electric" 0.0,
803 ":bug" 0.0,
804 ":psychic" 0.0,
805 ":grass" 0.0,
806 ":water" 0.0,
807 ":rock" 0.0}
808 #+end_example
810 Dark and Ghost are the best dual-type combo, and are resistant or
811 neutral to all types.
813 #+begin_src clojure :results output :exports both
814 (clojure.pprint/pprint
815 (pokemon.types/old-school
816 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))
817 #+end_src
819 #+results:
820 #+begin_example
821 {":normal" 0.0,
822 ":ground" 0.0,
823 ":poison" 0.0,
824 ":flying" 0.0,
825 ":fighting" 0.0,
826 ":dragon" 0.0,
827 ":fire" 0.0,
828 ":ice" 0.0,
829 ":ghost" 1.0,
830 ":electric" 0.0,
831 ":bug" 0.0,
832 ":psychic" 1.0,
833 ":grass" 0.0,
834 ":water" 0.0,
835 ":rock" 0.0}
836 #+end_example
838 Ghost and Psychic are a powerful dual type combo in the original games,
839 due to a glitch which made Psychic immune to Ghost type attacks, even
840 though the game claims that Ghost is strong against Psychic.
842 #+begin_src clojure :results verbatim :exports both
843 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))
844 #+end_src
846 #+results:
847 : INFEASIBLE
849 #+begin_src clojure :results verbatim :exports both
850 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))
851 #+end_src
853 #+results:
854 : INFEASIBLE
857 #+begin_src clojure :results verbatim :exports both
858 (pokemon.types/old-school
859 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))
860 #+end_src
862 #+results:
863 : INFEASIBLE
866 #+begin_src clojure :results output :exports both
867 (clojure.pprint/pprint
868 (pokemon.types/old-school
869 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))
870 #+end_src
872 #+results:
873 #+begin_example
874 {":normal" 0.0,
875 ":ground" 0.0,
876 ":poison" 0.0,
877 ":flying" 0.0,
878 ":fighting" 0.0,
879 ":dragon" 1.0,
880 ":fire" 0.0,
881 ":ice" 0.0,
882 ":ghost" 0.0,
883 ":electric" 0.0,
884 ":bug" 0.0,
885 ":psychic" 0.0,
886 ":grass" 0.0,
887 ":water" 0.0,
888 ":rock" 0.0}
889 #+end_example
891 The best attacking type combination is Dragon from the original games.
892 It is neutral against all the original types except for Dragon, which
893 it is strong against. There is no way to make an attacking type that
894 is strong against every type, or even one that is strong or neutral
895 against every type, in the new games.
898 *** Weakest Attack/Defense Combinations
900 #+begin_src clojure :results output :exports both
901 (clojure.pprint/pprint
902 (pokemon.types/old-school
903 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))
904 #+end_src
906 #+results:
907 #+begin_example
908 {":normal" 5.0,
909 ":ground" 0.0,
910 ":poison" 0.0,
911 ":flying" 0.0,
912 ":fighting" 0.0,
913 ":dragon" 0.0,
914 ":fire" 1.0,
915 ":ice" 2.0,
916 ":ghost" 1.0,
917 ":electric" 1.0,
918 ":bug" 1.0,
919 ":psychic" 0.0,
920 ":grass" 3.0,
921 ":water" 2.0,
922 ":rock" 0.0}
923 #+end_example
925 # #+results-old:
926 # : [[":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]]
928 #+begin_src clojure :results output :exports both
929 (clojure.pprint/pprint
930 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))
931 #+end_src
933 #+results:
934 #+begin_example
935 {":normal" 4.0,
936 ":ground" 1.0,
937 ":poison" 1.0,
938 ":flying" 0.0,
939 ":fighting" 1.0,
940 ":dragon" 0.0,
941 ":fire" 0.0,
942 ":dark" 0.0,
943 ":ice" 4.0,
944 ":steel" 0.0,
945 ":ghost" 1.0,
946 ":electric" 3.0,
947 ":bug" 0.0,
948 ":psychic" 1.0,
949 ":grass" 1.0,
950 ":water" 1.0,
951 ":rock" 2.0}
952 #+end_example
954 # #+results-old:
955 # : [[":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]]
958 This is an extremely interesting type combination, in that it uses
959 quite a few types.
961 #+begin_src clojure :results verbatim :exports both
962 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))
963 #+end_src
965 #+results:
966 : 20.0
968 20 types is the /minimum/ number of types before the attacking
969 combination is not-very-effective or worse against all defending
970 types. This would probably have been impossible to discover using
971 best-first search, since it involves such an intricate type
972 combination.
974 It's so interesting that it takes 20 types to make an attack type that
975 is weak to all types that the combination merits further
976 investigation.
978 Unfortunately, all of the tools that we've written so far are focused
979 on defense type combinations. However, it is possible to make every
980 tool attack-oriented via a simple macro.
982 #+name: attack-oriented
983 #+begin_src clojure :results silent
984 (in-ns 'pokemon.lpsolve)
986 (defmacro attack-mode [& forms]
987 `(let [attack-strengths# pokemon.types/attack-strengths
988 defense-strengths# pokemon.types/defense-strengths]
989 (binding [pokemon.types/attack-strengths
990 defense-strengths#
991 pokemon.types/defense-strengths
992 attack-strengths#]
993 ~@forms)))
994 #+end_src
996 Now all the tools from =pokemon.types= will work for attack
997 combinations.
999 #+begin_src clojure :results output :exports both
1000 (clojure.pprint/pprint
1001 (pokemon.types/susceptibility [:water]))
1002 #+end_src
1004 #+results:
1005 #+begin_example
1006 {:water 1/2,
1007 :psychic 1,
1008 :dragon 1,
1009 :fire 1/2,
1010 :ice 1/2,
1011 :grass 2,
1012 :ghost 1,
1013 :poison 1,
1014 :flying 1,
1015 :normal 1,
1016 :rock 1,
1017 :electric 2,
1018 :ground 1,
1019 :fighting 1,
1020 :dark 1,
1021 :steel 1/2,
1022 :bug 1}
1023 #+end_example
1026 #+begin_src clojure :results output :exports both
1027 (clojure.pprint/pprint
1028 (pokemon.lpsolve/attack-mode
1029 (pokemon.types/susceptibility [:water])))
1030 #+end_src
1032 #+results:
1033 #+begin_example
1034 {:water 1/2,
1035 :psychic 1,
1036 :dragon 1/2,
1037 :fire 2,
1038 :ice 1,
1039 :grass 1/2,
1040 :ghost 1,
1041 :poison 1,
1042 :flying 1,
1043 :normal 1,
1044 :rock 2,
1045 :electric 1,
1046 :ground 2,
1047 :fighting 1,
1048 :dark 1,
1049 :steel 1,
1050 :bug 1}
1051 #+end_example
1053 Now =pokemon.types/susceptibility= reports the /attack-type/
1054 combination's effectiveness against other types.
1056 The 20 type combo achieves its goal in a very clever way.
1058 First, it weakens its effectiveness to other types at the expense of
1059 making it very strong against flying.
1061 #+begin_src clojure :results output :exports both
1062 (clojure.pprint/pprint
1063 (pokemon.lpsolve/attack-mode
1064 (pokemon.types/susceptibility
1065 [:normal :normal :normal :normal
1066 :ice :ice :ice :ice
1067 :electric :electric :electric
1068 :rock :rock])))
1069 #+end_src
1071 #+results:
1072 #+begin_example
1073 {:water 1/2,
1074 :psychic 1,
1075 :dragon 2,
1076 :fire 1/4,
1077 :ice 1/4,
1078 :grass 2,
1079 :ghost 0,
1080 :poison 1,
1081 :flying 512,
1082 :normal 1,
1083 :rock 1/16,
1084 :electric 1/8,
1085 :ground 0,
1086 :fighting 1/4,
1087 :dark 1,
1088 :steel 1/1024,
1089 :bug 4}
1090 #+end_example
1092 Then, it removes it's strengths against Flying, Normal, and Fighting
1093 by adding Ghost and Ground.
1095 #+begin_src clojure :results output :exports both
1096 (clojure.pprint/pprint
1097 (pokemon.lpsolve/attack-mode
1098 (pokemon.types/susceptibility
1099 [:normal :normal :normal :normal
1100 :ice :ice :ice :ice
1101 :electric :electric :electric
1102 :rock :rock
1103 ;; Spot resistances
1104 :ghost :ground])))
1105 #+end_src
1107 #+results:
1108 #+begin_example
1109 {:water 1/2,
1110 :psychic 2,
1111 :dragon 2,
1112 :fire 1/2,
1113 :ice 1/4,
1114 :grass 1,
1115 :ghost 0,
1116 :poison 2,
1117 :flying 0,
1118 :normal 0,
1119 :rock 1/8,
1120 :electric 1/4,
1121 :ground 0,
1122 :fighting 1/4,
1123 :dark 1/2,
1124 :steel 1/1024,
1125 :bug 2}
1126 #+end_example
1128 Adding the pair Psychic and Fighting takes care of its strength
1129 against Psychic and makes it ineffective against Dark, which is immune
1130 to Psychic.
1132 Adding the pair Grass and Poison makes takes care of its strength
1133 against poison and makes it ineffective against Steel, which is immune
1134 to poison.
1136 #+begin_src clojure :results output :exports both
1137 (clojure.pprint/pprint
1138 (pokemon.lpsolve/attack-mode
1139 (pokemon.types/susceptibility
1140 [;; setup
1141 :normal :normal :normal :normal
1142 :ice :ice :ice :ice
1143 :electric :electric :electric
1144 :rock :rock
1145 ;; Spot resistances
1146 :ghost :ground
1147 ;; Pair resistances
1148 :psychic :fighting
1149 :grass :poison])))
1150 #+end_src
1152 #+results:
1153 #+begin_example
1154 {:water 1,
1155 :psychic 1/2,
1156 :dragon 1,
1157 :fire 1/4,
1158 :ice 1/2,
1159 :grass 1,
1160 :ghost 0,
1161 :poison 1/2,
1162 :flying 0,
1163 :normal 0,
1164 :rock 1/4,
1165 :electric 1/4,
1166 :ground 0,
1167 :fighting 1/2,
1168 :dark 0,
1169 :steel 0,
1170 :bug 1/2}
1171 #+end_example
1173 Can you see the final step?
1175 It's adding the Water type, which is weak against Water, Dragon, and
1176 Grass and strong against Rock and Fire.
1178 #+begin_src clojure :results output :exports both
1179 (clojure.pprint/pprint
1180 (pokemon.lpsolve/attack-mode
1181 (pokemon.types/susceptibility
1182 [;; setup
1183 :normal :normal :normal :normal
1184 :ice :ice :ice :ice
1185 :electric :electric :electric
1186 :rock :rock
1187 ;; Spot resistances
1188 :ghost :ground
1189 ;; Pair resistances
1190 :psychic :fighting
1191 :grass :poison
1192 ;; completion
1193 :water])))
1194 #+end_src
1196 #+results:
1197 #+begin_example
1198 {:water 1/2,
1199 :psychic 1/2,
1200 :dragon 1/2,
1201 :fire 1/2,
1202 :ice 1/2,
1203 :grass 1/2,
1204 :ghost 0,
1205 :poison 1/2,
1206 :flying 0,
1207 :normal 0,
1208 :rock 1/2,
1209 :electric 1/4,
1210 :ground 0,
1211 :fighting 1/2,
1212 :dark 0,
1213 :steel 0,
1214 :bug 1/2}
1215 #+end_example
1217 Which makes a particularly beautiful combination which is ineffective
1218 against all defending types.
1221 # #+begin_src clojure :results scalar :exports both
1222 # (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])))))
1223 # #+end_src
1225 # #+results:
1226 # | [: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] |
1229 Is there anything else that's interesting?
1231 #+begin_src clojure :exports both
1232 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))
1233 #+end_src
1235 #+results:
1236 : INFEASIBLE
1238 #+begin_src clojure :exports both
1239 (pokemon.types/old-school
1240 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))
1241 #+end_src
1243 #+results:
1244 : INFEASIBLE
1246 #+begin_src clojure :exports both
1247 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))
1248 #+end_src
1250 #+results:
1251 : INFEASIBLE
1253 #+begin_src clojure :exports both
1254 (pokemon.types/old-school
1255 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))
1256 #+end_src
1258 #+results:
1259 : INFEASIBLE
1261 #+begin_src clojure :exports both
1262 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))
1263 #+end_src
1265 #+results:
1266 : INFEASIBLE
1268 #+begin_src clojure :exports both
1269 (pokemon.types/old-school
1270 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))
1271 #+end_src
1273 #+results:
1274 : INFEASIBLE
1276 There is no way to produce a defense-type that is weak to all types.
1277 This is probably because there are many types that are completely
1278 immune to some types, such as Flying, which is immune to Ground. A
1279 perfectly weak type could not use any of these types.
1281 * Summary
1283 Overall, the pok\eacute{}mon type system is slanted towards defense
1284 rather than offense. While it is possible to create superior
1285 defensive types and exceptionally weak attack types, it is not
1286 possible to create exceptionally weak defensive types or very powerful
1287 attack types.
1289 Using the =lp_solve= library was more complicated than the best-first
1290 search, but yielded results quickly and efficiently. Expressing the
1291 problem in a linear form does have its drawbacks, however --- it's
1292 hard to ask questions such as "what is the best 3-type defensive combo
1293 in terms of susceptibility?", since susceptibility is not a linear
1294 function of a combo's types. It is also hard to get all the solutions
1295 to a particular problem, such as all the pokemon type combinations of
1296 length 8 which are immortal defense types.
1298 * COMMENT main-program
1299 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none
1300 <<intro>>
1301 <<body>>
1302 <<declares>>
1303 <<memory-management>>
1304 <<get-results>>
1305 <<solve>>
1306 <<farmer-example>>
1307 <<lp-solve>>
1308 <<better-farmer>>
1309 <<pokemon-lp>>
1310 <<results>>
1311 <<attack-oriented>>
1312 #+end_src