view org/lpsolve.org @ 11:4ea23241ff5b

cleaned up prose
author Robert McIntyre <rlm@mit.edu>
date Wed, 02 Nov 2011 10:28:50 -0700
parents eedd6897197d
children 89462678a932
line wrap: on
line source
1 #+title: Discovering Effective Pok\eacute{}mon Types Using Linear Optimization
2 #+author: Robert McIntyre & Dylan Holmes
3 #+EMAIL: rlm@mit.edu
4 #+description: Using Lpsolve to func effective pokemon types in clojure.
5 #+keywords: Pokemon, clojure, linear optimization, lp_spolve, LpSolve
6 #+SETUPFILE: ../../aurellem/org/setup.org
7 #+INCLUDE: ../../aurellem/org/level-0.org
11 * Introduction
12 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.
13 Pok\eacute{}mon is a game in which adorable creatures battle each
14 other using fantastic attacks. It was made into a several gameboy
15 games that all share the same battle system. Every pok\eacute{}mon in
16 the gameboy game has one or two /types/, such as Ground, Fire, Water,
17 etc. Every pok\eacute{}mon attack has exactly one type. Certain
18 defending types are weak or strong to other attacking types. For
19 example, Water attacks are strong against Fire pok\eacute{}mon, while
20 Electric attacks are weak against Ground Pok\eacute{}mon. In the
21 games, attacks can be either twice as effective as normal (Water
22 vs. Fire), neutrally effective (Normal vs. Normal), half as effective
23 (Fire vs. Water), or not effective at all (Electric vs. Ground). We
24 represent these strengths and weakessness as the numbers 2, 1,
25 $\frac{1}{2}$, and 0, and call them the /susceptance/ of one type to
26 another.
28 If a pokemon has two types, then the strengths and weakness of each
29 type are /multiplied/ together. Thus Electric (2x weak to Ground)
30 combined with Flying (immune to Ground (0x)) is immune to Ground.
31 Fire (2x weak to Water) combined with Water (1/2x resistant to Water)
32 is neutral to Water. If both types are resistant to another type, then
33 the combination is doubly-resistant (1/4x) to that type. If both types
34 are weak to a certain type then the combination is double-weak (4x) to
35 that type.
37 In the [[./types.org][previous post]], we used the best-first search algorithm to find
38 the most effective Pok\eacute{}mon type combinations. Afterwards, we
39 realized that we could transform this search problem into a /linear
40 optimization problem/. This conversion offeres several advantages:
41 first, search algorithms are comparatively slow, whereas linear
42 optimization algorithms are extremely fast; second, it is difficult to
43 determine whether a search problem has any solution, whereas it is
44 straightforward to determine whether a linear optimization problem has
45 any solution; finally, because systems of linear equations are so
46 common, many programming languages have linear equation solvers
47 written for them.
49 In this article, we will:
50 - Solve a simple linear optimization problem in C :: We demonstrate
51 how to use the linear programming C library, =lp_solve=, to
52 solve a simple linear optimization problem.
53 - Incorporate a C library into Clojure :: We will show how we gave
54 Clojure access to the linear programming C library, =lp_solve=.
55 - Find effective Pokemon types using linear programming :: Building
56 on our earlier code, we answer some questions that were
57 impossible to answer using best-first search.
58 - Present our results :: We found some cool examples and learned a lot
59 about the pok\eacute{}mon type system as a whole.
62 ** Immortal Types
64 In the game, pok\eacute{}mon can have either one type or two types. If
65 this restriction is lifted, is there any combination of types that is
66 resistant to all types? I call such a combination an /Immortal Type/,
67 since if that type's pattern was repeated over and over again towards
68 infinity, the resulting type would be immune to all attack types.
70 * Linear Programming
72 Linear programming is the process of finding an optimal solution to a
73 linear equation of several variables which are constrained by some linear
74 inequalities.
76 ** The Farmer's Problem
78 Let's solve the Farmer's Problem, an example linear programming problem
79 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.
82 #+BEGIN_QUOTE
83 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to
84 plant two crops: wheat and barley. To produce these crops, it costs
85 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat
86 and $210 per acre for the barley. The farmer has $15000 available for
87 expenses. But after the harvest, the farmer must store the crops while
88 awaiting favorable market conditions. The farmer has storage space
89 for 4000 bushels. Each acre yields an average of 110 bushels of wheat
90 or 30 bushels of barley. If the net profit per bushel of wheat (after
91 all expenses have been subtracted) is $1.30 and for barley is $2.00,
92 how should the farmer plant the 75 acres to maximize profit?
93 #+END_QUOTE
95 The Farmer's Problem is to maximize profit subject to constraints on
96 available farmland, funds for expenses, and storage space.
98 | | Wheat | Barley | Maximum total |
99 |----------+----------------------+---------------------+--------------|
100 | / | < | > | <> |
101 | Farmland | \(w\) acres | \(b\) acres | 75 acres |
102 | Expense | $120 per acre | $210 per acre | $15000 |
103 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |
104 |----------+----------------------+---------------------+--------------|
105 | Profit | $1.30 per bushel | $2.00 per bushel | |
107 *** COMMENT
108 can be represented as a linear optimization
109 problem. In this form, it is a problem with two variables\mdash{}the number of
110 acres of wheat, \(w\), and the number of acres of barley, \(b\). The
111 aim is to maximize profit, which
113 subject to three constraints: the farmer can't spend more money
114 than he has, the farmer can't use more acres than he owns, and the harvest has
115 to fit in his storage space.
117 We can express these constraints succinctly using matrix
118 notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\),
119 we want to maximize the expression \(143 w + 60 b\) subject to
121 \(
122 \begin{cases}
123 120 w + 210 b & \leq & 1500\\
124 110 w + 30 b & \leq & 4000\\
125 1 w + 1 w & \leq & 75
126 \end{cases}
127 \)
129 #\(\begin{bmatrix}120&210\\110&30\\1 &
130 #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix}
131 #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\)
133 ** Solution using LP Solve
134 #(LP solve is available at http://www.example.com.)
135 In a new file, =farmer.lp=, we list the variables and constraints
136 of our problem using LP Solve syntax.
138 #+begin_src lpsolve :tangle ../lp/farmer.lp
139 /* Maximize Total Profit */
140 max: +143 wheat +60 barley;
143 /* -------- Constraints --------*/
145 /* the farmer can't spend more money than he has */
146 +120 wheat +210 barley <= 15000;
148 /* the harvest has to fit in his storage space */
149 +110 wheat +30 barley <= 4000;
151 /* he can't use more acres than he owns */
152 +wheat +barley <= 75;
153 #+end_src
155 Running the =lp_solve= program on =farmer.lp= yields the following output.
157 #+begin_src sh :exports both :results scalar
158 lp_solve ~/proj/pokemon-types/lp/farmer.lp
159 #+end_src
161 #+results:
162 :
163 : Value of objective function: 6315.62500000
164 :
165 : Actual values of the variables:
166 : wheat 21.875
167 : barley 53.125
169 This shows that the farmer can maximize his profit by planting 21.875
170 of the available acres with wheat and the remaining 53.125 acres with
171 barley; by doing so, he will make $6315.62(5) in profit.
173 * Incorporating =lp_solve= into Clojure
175 There is a [[http://lpsolve.sourceforge.net/5.5/Java/README.html][Java API]] written by Juergen Ebert which enables Java
176 programs to use =lp_solve=. Although Clojure can use this Java API
177 directly, the interaction between Java, C, and Clojure is clumsy:
179 ** The Farmer's Problem in Clojure
181 We are going to solve the same problem involving wheat and barley,
182 that we did above, but this time using clojure and the =lp_solve= API.
184 #+srcname: intro
185 #+begin_src clojure :results silent
186 (ns pokemon.lpsolve
187 (:use [clojure.contrib def set [seq :only [indexed]] pprint])
188 (:import lpsolve.LpSolve)
189 (:require pokemon.types)
190 (:require incanter.core))
191 #+end_src
193 The =lp_solve= Java interface is available from the same site as
194 =lp_solve= itself, http://lpsolve.sourceforge.net/ Using it is the
195 same as many other =C= programs. There are excellent instructions to
196 get set up. The short version is that you must call Java with
197 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the
198 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For
199 example, in my =.bashrc= file, I have the line
200 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=. If everything
201 is set-up correctly,
203 #+srcname: body
204 #+begin_src clojure :results verbatim :exports both
205 (import 'lpsolve.LpSolve)
206 #+end_src
208 #+results: body
209 : lpsolve.LpSolve
211 should run with no problems.
213 ** Making a DSL to talk with LpSolve
214 *** Problems
215 Since we are using a =C= wrapper, we have to deal with manual memory
216 management for the =C= structures which are wrapped by the =LpSolve=
217 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's
218 very important to get it right. Also, the Java wrapper follows the
219 =C= tradition closely and defines many =static final int= constants
220 for the different states of the =LpSolve= instance instead of using Java
221 enums. The calling convention for adding rows and columns to
222 the constraint matrix is rather complicated and must be done column by
223 column or row by row, which can be error prone. Finally, I'd like to
224 gather all the important output information from the =LpSolve= instance
225 into a final, immutable structure.
227 In summary, the issues I'd like to address are:
229 - reliable memory management
230 - functional interface to =LpSolve=
231 - intelligible, immutable output
233 To deal with these issues I'll create four functions for interfacing
234 with =LpSolve=
236 #+srcname: declares
237 #+begin_src clojure :results silent
238 (in-ns 'pokemon.lpsolve)
240 ;; deal with automatic memory management for LpSolve instance.
241 (declare linear-program)
243 ;; functional interface to LpSolve
244 (declare lp-solve)
246 ;; immutable output from lp-solve
247 (declare solve get-results)
248 #+end_src
251 *** Memory Management
253 Every instance of =LpSolve= must be manually garbage collected via a
254 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=
255 to ensure that =deleteLP= is always called.
257 #+srcname: memory-management
258 #+begin_src clojure :results silent
259 (in-ns 'pokemon.lpsolve)
260 (defmacro linear-program
261 "solve a linear programming problem using LpSolve syntax.
262 within the macro, the variable =lps= is bound to the LpSolve instance."
263 [& statements]
264 (list 'let '[lps (LpSolve/makeLp 0 0)]
265 (concat '(try)
266 statements
267 ;; always free the =C= data structures.
268 '((finally (.deleteLp lps))))))
269 #+end_src
272 The macro captures the variable =lps= within its body, providing for a
273 convenient way to access the object using any of the methods of the
274 =LpSolve= API without having to worry about when to call
275 =deleteLP=.
277 *** Sensible Results
278 The =linear-program= macro deletes the actual =lps= object once it is
279 done working, so it's important to collect the important results and
280 add return them in an immutable structure at the end.
282 #+srcname: get-results
283 #+begin_src clojure :results silent
284 (in-ns 'pokemon.lpsolve)
286 (defrecord LpSolution
287 [objective-value
288 optimal-values
289 variable-names
290 solution
291 status
292 model])
294 (defn model
295 "Returns a textual representation of the problem suitable for
296 direct input to the =lp_solve= program (lps format)"
297 [#^LpSolve lps]
298 (let [target (java.io.File/createTempFile "lps" ".lp")]
299 (.writeLp lps (.getPath target))
300 (slurp target)))
302 (defn results
303 "Given an LpSolve object, solves the object and returns a map of the
304 essential values which compose the solution."
305 [#^LpSolve lps]
306 (locking lps
307 (let [status (solve lps)
308 number-of-variables (.getNcolumns lps)
309 optimal-values (double-array number-of-variables)
310 optimal-values (do (.getVariables lps optimal-values)
311 (seq optimal-values))
312 variable-names
313 (doall
314 ;; The doall is necessary since the lps object might
315 ;; soon be deleted.
316 (map
317 #(.getColName lps (inc %))
318 (range number-of-variables)))
319 model (model lps)]
320 (LpSolution.
321 (.getObjective lps)
322 optimal-values
323 variable-names
324 (zipmap variable-names optimal-values)
325 status
326 model))))
328 #+end_src
330 Here I've created an object called =LpSolution= which stores the
331 important results from a session with =lp_solve=. Of note is the
332 =model= function which returns the problem in a form that can be
333 solved by other linear programming packages.
335 *** Solution Status of an LpSolve Object
337 #+srcname: solve
338 #+begin_src clojure :results silent
339 (in-ns 'pokemon.lpsolve)
341 (defn static-integer?
342 "does the field represent a static integer constant?"
343 [#^java.lang.reflect.Field field]
344 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))
345 (integer? (.get field nil))))
347 (defn integer-constants [class]
348 (filter static-integer? (.getFields class)))
350 (defn-memo constant-map
351 "Takes a class and creates a map of the static constant integer
352 fields with their names. This helps with C wrappers where they have
353 just defined a bunch of integer constants instead of enums"
354 [class]
355 (let [integer-fields (integer-constants class)]
356 (into (sorted-map)
357 (zipmap (map #(.get % nil) integer-fields)
358 (map #(.getName %) integer-fields)))))
360 (defn solve
361 "Solve an instance of LpSolve and return a string representing the
362 status of the computation. Will only solve a particular LpSolve
363 instance once."
364 [#^LpSolve lps]
365 ((constant-map LpSolve)
366 (.solve lps)))
368 #+end_src
370 The =.solve= method of an =LpSolve= object only returns an integer code
371 to specify the status of the computation. The =solve= method here
372 uses reflection to look up the actual name of the status code and
373 returns a more helpful status message that is also resistant to
374 changes in the meanings of the code numbers.
376 *** The Farmer Example in Clojure, Pass 1
378 Now we can implement a nicer version of the examples from the
379 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less
380 line-by-line translation of the Java code from that example.
382 #+srcname: farmer-example
383 #+begin_src clojure :results silent
384 (in-ns 'pokemon.lpsolve)
385 (defn farmer-example []
386 (linear-program
387 (results
388 (doto lps
389 ;; name the columns
390 (.setColName 1 "wheat")
391 (.setColName 2 "barley")
392 (.setAddRowmode true)
393 ;; row 1 : 120x + 210y <= 15000
394 (.addConstraintex 2
395 (double-array [120 210])
396 (int-array [1 2])
397 LpSolve/LE
398 15e3)
399 ;; row 2 : 110x + 30y <= 4000
400 (.addConstraintex 2
401 (double-array [110 30])
402 (int-array [1 2])
403 LpSolve/LE
404 4e3)
405 ;; ;; row 3 : x + y <= 75
406 (.addConstraintex 2
407 (double-array [1 1])
408 (int-array [1 2])
409 LpSolve/LE
410 75)
411 (.setAddRowmode false)
413 ;; add constraints
414 (.setObjFnex 2
415 (double-array [143 60])
416 (int-array [1 2]))
418 ;; set this as a maximization problem
419 (.setMaxim)))))
421 #+end_src
423 #+begin_src clojure :results output :exports both
424 (clojure.pprint/pprint
425 (:solution (pokemon.lpsolve/farmer-example)))
426 #+end_src
428 #+results:
429 : {"barley" 53.12499999999999, "wheat" 21.875}
431 And it works as expected!
433 *** The Farmer Example in Clojure, Pass 2
434 We don't have to worry about memory management anymore, and the farmer
435 example is about half as long as the example from the =LpSolve=
436 website, but we can still do better. Solving linear problems is all
437 about the constraint matrix $A$ , the objective function $c$, and the
438 right-hand-side $b$, plus whatever other options one cares to set for
439 the particular instance of =lp_solve=. Why not make a version of
440 =linear-program= that takes care of initialization?
444 #+srcname: lp-solve
445 #+begin_src clojure :results silent
446 (in-ns 'pokemon.lpsolve)
447 (defn initialize-lpsolve-row-oriented
448 "fill in an lpsolve instance using a constraint matrix =A=, the
449 objective function =c=, and the right-hand-side =b="
450 [#^LpSolve lps A b c]
451 ;; set the name of the last column to _something_
452 ;; this appears to be necessary to ensure proper initialization.
453 (.setColName lps (count c) (str "C" (count c)))
455 ;; This is the recommended way to "fill-in" an lps instance from the
456 ;; documentation. First, set row mode, then set the objective
457 ;; function, then set each row of the problem, and then turn off row
458 ;; mode.
459 (.setAddRowmode lps true)
460 (.setObjFnex lps (count c)
461 (double-array c)
462 (int-array (range 1 (inc (count c)))))
463 (dorun
464 (for [n (range (count A))]
465 (let [row (nth A n)
466 row-length (int (count row))]
467 (.addConstraintex lps
468 row-length
469 (double-array row)
470 (int-array (range 1 (inc row-length)))
471 LpSolve/LE
472 (double (nth b n))))))
473 (.setAddRowmode lps false)
474 lps)
477 (defmacro lp-solve
478 "by default:,
479 minimize (* c x), subject to (<= (* A x) b),
480 using continuous variables. You may set any number of
481 other options as in the LpSolve API."
482 [A b c & lp-solve-forms]
483 ;; assume that A is a vector of vectors
484 (concat
485 (list 'linear-program
486 (list 'initialize-lpsolve-row-oriented 'lps A b c))
487 `~lp-solve-forms))
488 #+end_src
490 Now, we can use a much more functional approach to solving the
491 farmer's problem:
493 #+srcname: better-farmer
494 #+begin_src clojure :results silent
495 (in-ns 'pokemon.lpsolve)
496 (defn better-farmer-example []
497 (lp-solve [[120 210]
498 [110 30]
499 [1 1]]
500 [15000
501 4000
502 75]
503 [143 60]
504 (.setColName lps 1 "wheat")
505 (.setColName lps 2 "barley")
506 (.setMaxim lps)
507 (results lps)))
508 #+end_src
510 #+begin_src clojure :exports both :results verbatim
511 (vec (:solution (pokemon.lpsolve/better-farmer-example)))
512 #+end_src
514 #+results:
515 : [["barley" 53.12499999999999] ["wheat" 21.875]]
517 Notice that both the inputs to =better-farmer-example= and the results
518 are immutable.
520 * Using LpSolve to find Immortal Types
521 ** Converting the Pok\eacute{}mon problem into a linear form
522 How can the original question about pok\eacute{}mon types be converted
523 into a linear problem?
525 Pokemon types can be considered to be vectors of numbers representing
526 their susceptances to various attacking types, so Water might look
527 something like this.
529 #+begin_src clojure :results scalar :exports both
530 (:water (pokemon.types/defense-strengths))
531 #+end_src
533 #+results:
534 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]
536 Where the numbers represent the susceptibility of Water to the
537 attacking types in the following order:
539 #+begin_src clojure :results output :exports both
540 (clojure.pprint/pprint
541 (pokemon.types/type-names))
542 #+end_src
544 #+results:
545 #+begin_example
546 [:normal
547 :fire
548 :water
549 :electric
550 :grass
551 :ice
552 :fighting
553 :poison
554 :ground
555 :flying
556 :psychic
557 :bug
558 :rock
559 :ghost
560 :dragon
561 :dark
562 :steel]
563 #+end_example
566 So, for example, Water is /resistant/ (x0.5) against Fire, which is
567 the second element in the list.
569 To combine types, these sorts of vectors are multiplied together
570 pair-wise to yield the resulting combination.
572 Unfortunately, we need some way to add two type vectors together
573 instead of multiplying them if we want to solve the problem with
574 =lp_solve=. Taking the log of the vector does just the trick.
576 If we make a matrix with each column being the log (base 2) of the
577 susceptance of each type, then finding an immortal type corresponds to
578 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)
579 and setting the constraint vector $c$ to all ones, which means that we
580 want to find the immortal type which uses the least amount of types.
582 #+srcname: pokemon-lp
583 #+begin_src clojure :results silent
584 (in-ns 'pokemon.lpsolve)
586 (defn log-clamp-matrix [matrix]
587 ;; we have to clamp the Infinities to a more reasonable negative
588 ;; value because lp_solve does not play well with infinities in its
589 ;; constraint matrix.
590 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))
591 (incanter.core/log2
592 (incanter.core/trans
593 matrix))))
595 ;; constraint matrices
596 (defn log-defense-matrix []
597 (log-clamp-matrix
598 (doall (map (pokemon.types/defense-strengths)
599 (pokemon.types/type-names)))))
601 (defn log-attack-matrix []
602 (incanter.core/trans (log-defense-matrix)))
604 ;; target vectors
605 (defn all-resistant []
606 (doall (map (constantly -1) (pokemon.types/type-names))))
608 (defn all-weak []
609 (doall (map (constantly 1) (pokemon.types/type-names))))
611 (defn all-neutral []
612 (doall (map (constantly 0) (pokemon.types/type-names))))
614 ;; objective functions
615 (defn number-of-types []
616 (doall (map (constantly 1) (pokemon.types/type-names))))
618 (defn set-constraints
619 "sets all the constraints for an lpsolve instance to the given
620 constraint. =constraint= here is one of the LpSolve constants such
621 as LpSolve/EQ."
622 [#^LpSolve lps constraint]
623 (dorun (map (fn [index] (.setConstrType lps index constraint))
624 ;; ONE based indexing!!!
625 (range 1 (inc (.getNrows lps))))))
628 (defn set-discrete
629 "sets every variable in an lps problem to be a discrete rather than
630 continuous variable"
631 [#^LpSolve lps]
632 (dorun (map (fn [index] (.setInt lps index true))
633 ;; ONE based indexing!!!
634 (range 1 (inc (.getNcolumns lps))))))
636 (defn set-variable-names
637 "sets the variable names of the problem given a vector of names"
638 [#^LpSolve lps names]
639 (dorun
640 (map (fn [[index name]]
641 (.setColName lps (inc index) (str name)))
642 ;; ONE based indexing!!!
643 (indexed names))))
645 (defn poke-solve
646 ([poke-matrix target objective-function constraint min-num-types]
647 ;; must have at least one type
648 (let [poke-matrix
649 (concat poke-matrix
650 [(map (constantly 1)
651 (range (count (first poke-matrix))))])
652 target (concat target [min-num-types])]
653 (lp-solve poke-matrix target objective-function
654 (set-constraints lps constraint)
655 ;; must have more than min-num-types
656 (.setConstrType lps (count target) LpSolve/GE)
657 (set-discrete lps)
658 (set-variable-names lps (pokemon.types/type-names))
659 (results lps))))
660 ([poke-matrix target objective-function constraint]
661 ;; at least one type
662 (poke-solve poke-matrix target objective-function constraint 1)))
664 (defn solution
665 "If the results of an lpsolve operation are feasible, returns the
666 results. Otherwise, returns the error."
667 [results]
668 (if (not (= (:status results) "OPTIMAL"))
669 (:status results)
670 (:solution results)))
671 #+end_src
673 With this, we are finally able to get some results.
675 ** Results
676 #+srcname: results
677 #+begin_src clojure :results silent
678 (in-ns 'pokemon.lpsolve)
680 (defn best-defense-type
681 "finds a type combination which is resistant to all attacks."
682 []
683 (poke-solve
684 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))
686 (defn worst-attack-type
687 "finds the attack type which is not-very-effective against all pure
688 defending types (each single defending type is resistant to this
689 attack combination"
690 []
691 (poke-solve
692 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))
694 (defn worst-defense-type
695 "finds a defending type that is weak to all single attacking types."
696 []
697 (poke-solve
698 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))
700 (defn best-attack-type
701 "finds an attack type which is super effective against all single
702 defending types"
703 []
704 (poke-solve
705 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))
707 (defn solid-defense-type
708 "finds a defense type which is either neutral or resistant to all
709 single attacking types"
710 []
711 (poke-solve
712 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))
714 (defn solid-attack-type
715 "finds an attack type which is either neutral or super-effective to
716 all single attacking types."
717 []
718 (poke-solve
719 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))
721 (defn weak-defense-type
722 "finds a defense type which is either neutral or weak to all single
723 attacking types"
724 []
725 (poke-solve
726 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))
728 (defn neutral-defense-type
729 "finds a defense type which is perfectly neutral to all attacking
730 types."
731 []
732 (poke-solve
733 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))
735 #+end_src
737 *** Strongest Attack/Defense Combinations
739 #+begin_src clojure :results output :exports both
740 (clojure.pprint/pprint
741 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))
742 #+end_src
744 #+results:
745 #+begin_example
746 {":normal" 0.0,
747 ":ground" 1.0,
748 ":poison" 2.0,
749 ":flying" 1.0,
750 ":fighting" 0.0,
751 ":dragon" 0.0,
752 ":fire" 0.0,
753 ":dark" 1.0,
754 ":ice" 0.0,
755 ":steel" 1.0,
756 ":ghost" 0.0,
757 ":electric" 0.0,
758 ":bug" 0.0,
759 ":psychic" 0.0,
760 ":grass" 0.0,
761 ":water" 2.0,
762 ":rock" 0.0}
763 #+end_example
765 # #+results-old:
766 # : [[":normal" 0.0] [":ground" 1.0] [":poison" 0.0] [":flying" 1.0] [":fighting" 0.0] [":dragon" 1.0] [":fire" 0.0] [":dark" 0.0] [":ice" 0.0] [":steel" 2.0] [":ghost" 1.0] [":electric" 0.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 2.0] [":rock" 0.0]]
769 This is the immortal type combination we've been looking for. By
770 combining Steel, Water, Poison, and three types which each have complete
771 immunities to various other types, we've created a type that is resistant to
772 all attacking types.
774 #+begin_src clojure :results output :exports both
775 (clojure.pprint/pprint
776 (pokemon.types/susceptibility
777 [:poison :poison :water :water :steel :ground :flying :dark]))
778 #+end_src
780 #+results:
781 #+begin_example
782 {:water 1/2,
783 :psychic 0,
784 :dragon 1/2,
785 :fire 1/2,
786 :ice 1/2,
787 :grass 1/2,
788 :ghost 1/4,
789 :poison 0,
790 :flying 1/2,
791 :normal 1/2,
792 :rock 1/2,
793 :electric 0,
794 :ground 0,
795 :fighting 1/2,
796 :dark 1/4,
797 :steel 1/8,
798 :bug 1/8}
799 #+end_example
801 # #+results-old:
802 # : {:water 1/4, :psychic 1/4, :dragon 1/2, :fire 1/2, :ice 1/2, :grass 1/2, :ghost 1/2, :poison 0, :flying 1/4, :normal 0, :rock 1/4, :electric 0, :ground 0, :fighting 0, :dark 1/2, :steel 1/16, :bug 1/16}
805 Cool!
807 #+begin_src clojure :results output :exports both
808 (clojure.pprint/pprint
809 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))
810 #+end_src
812 #+results:
813 #+begin_example
814 {":normal" 0.0,
815 ":ground" 0.0,
816 ":poison" 0.0,
817 ":flying" 0.0,
818 ":fighting" 0.0,
819 ":dragon" 0.0,
820 ":fire" 0.0,
821 ":dark" 1.0,
822 ":ice" 0.0,
823 ":steel" 0.0,
824 ":ghost" 1.0,
825 ":electric" 0.0,
826 ":bug" 0.0,
827 ":psychic" 0.0,
828 ":grass" 0.0,
829 ":water" 0.0,
830 ":rock" 0.0}
831 #+end_example
833 Dark and Ghost are the best dual-type combo, and are resistant or
834 neutral to all types.
836 #+begin_src clojure :results output :exports both
837 (clojure.pprint/pprint
838 (pokemon.types/old-school
839 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))
840 #+end_src
842 #+results:
843 #+begin_example
844 {":normal" 0.0,
845 ":ground" 0.0,
846 ":poison" 0.0,
847 ":flying" 0.0,
848 ":fighting" 0.0,
849 ":dragon" 0.0,
850 ":fire" 0.0,
851 ":ice" 0.0,
852 ":ghost" 1.0,
853 ":electric" 0.0,
854 ":bug" 0.0,
855 ":psychic" 1.0,
856 ":grass" 0.0,
857 ":water" 0.0,
858 ":rock" 0.0}
859 #+end_example
861 Ghost and Psychic are a powerful dual type combo in the original games,
862 due to a glitch which made Psychic immune to Ghost type attacks, even
863 though the game claims that Ghost is strong to Psychic.
865 #+begin_src clojure :results verbatim :exports both
866 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))
867 #+end_src
869 #+results:
870 : INFEASIBLE
872 #+begin_src clojure :results verbatim :exports both
873 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))
874 #+end_src
876 #+results:
877 : INFEASIBLE
880 #+begin_src clojure :results verbatim :exports both
881 (pokemon.types/old-school
882 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))
883 #+end_src
885 #+results:
886 : INFEASIBLE
889 #+begin_src clojure :results output :exports both
890 (clojure.pprint/pprint
891 (pokemon.types/old-school
892 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))
893 #+end_src
895 #+results:
896 #+begin_example
897 {":normal" 0.0,
898 ":ground" 0.0,
899 ":poison" 0.0,
900 ":flying" 0.0,
901 ":fighting" 0.0,
902 ":dragon" 1.0,
903 ":fire" 0.0,
904 ":ice" 0.0,
905 ":ghost" 0.0,
906 ":electric" 0.0,
907 ":bug" 0.0,
908 ":psychic" 0.0,
909 ":grass" 0.0,
910 ":water" 0.0,
911 ":rock" 0.0}
912 #+end_example
914 The best attacking type combination is Dragon from the original games.
915 It is neutral against all the original types except for Dragon, which
916 it is strong against. There is no way to make an attacking type that
917 is strong against every type, or even one that is strong or neutral
918 against every type, in the new games.
921 *** Weakest Attack/Defense Combinations
923 #+begin_src clojure :results output :exports both
924 (clojure.pprint/pprint
925 (pokemon.types/old-school
926 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))
927 #+end_src
929 #+results:
930 #+begin_example
931 {":normal" 5.0,
932 ":ground" 0.0,
933 ":poison" 0.0,
934 ":flying" 0.0,
935 ":fighting" 0.0,
936 ":dragon" 0.0,
937 ":fire" 1.0,
938 ":ice" 2.0,
939 ":ghost" 1.0,
940 ":electric" 1.0,
941 ":bug" 1.0,
942 ":psychic" 0.0,
943 ":grass" 3.0,
944 ":water" 2.0,
945 ":rock" 0.0}
946 #+end_example
948 # #+results-old:
949 # : [[":normal" 5.0] [":ground" 1.0] [":poison" 0.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":ice" 4.0] [":ghost" 1.0] [":electric" 4.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 1.0] [":rock" 1.0]]
951 #+begin_src clojure :results output :exports both
952 (clojure.pprint/pprint
953 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))
954 #+end_src
956 #+results:
957 #+begin_example
958 {":normal" 4.0,
959 ":ground" 1.0,
960 ":poison" 1.0,
961 ":flying" 0.0,
962 ":fighting" 1.0,
963 ":dragon" 0.0,
964 ":fire" 0.0,
965 ":dark" 0.0,
966 ":ice" 4.0,
967 ":steel" 0.0,
968 ":ghost" 1.0,
969 ":electric" 3.0,
970 ":bug" 0.0,
971 ":psychic" 1.0,
972 ":grass" 1.0,
973 ":water" 1.0,
974 ":rock" 2.0}
975 #+end_example
977 # #+results-old:
978 # : [[":normal" 4.0] [":ground" 1.0] [":poison" 1.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":dark" 0.0] [":ice" 5.0] [":steel" 0.0] [":ghost" 1.0] [":electric" 5.0] [":bug" 0.0] [":psychic" 1.0] [":grass" 0.0] [":water" 1.0] [":rock" 2.0]]
981 This is an extremely interesting type combination, in that it uses
982 quite a few types.
984 #+begin_src clojure :results verbatim :exports both
985 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))
986 #+end_src
988 #+results:
989 : 20.0
991 20 types is the /minimum/ number of types before the attacking
992 combination is not-very-effective or worse against all defending
993 types. This would probably have been impossible to discover using
994 best-first search, since it involves such an intricate type
995 combination.
997 It's so interesting that it takes 20 types to make an attack type that
998 is weak to all types that the combination merits further
999 investigation.
1001 Unfortunately, all of the tools that we've written so far are focused
1002 on defense type combinations. However, it is possible to make every
1003 tool attack-oriented via a simple macro.
1005 #+srcname: attack-oriented
1006 #+begin_src clojure :results silent
1007 (in-ns 'pokemon.lpsolve)
1009 (defmacro attack-mode [& forms]
1010 `(let [attack-strengths# pokemon.types/attack-strengths
1011 defense-strengths# pokemon.types/defense-strengths]
1012 (binding [pokemon.types/attack-strengths
1013 defense-strengths#
1014 pokemon.types/defense-strengths
1015 attack-strengths#]
1016 ~@forms)))
1017 #+end_src
1019 Now all the tools from =pokemon.types= will work for attack
1020 combinations.
1022 #+begin_src clojure :results output :exports both
1023 (clojure.pprint/pprint
1024 (pokemon.types/susceptibility [:water]))
1025 #+end_src
1027 #+results:
1028 #+begin_example
1029 {:water 1/2,
1030 :psychic 1,
1031 :dragon 1,
1032 :fire 1/2,
1033 :ice 1/2,
1034 :grass 2,
1035 :ghost 1,
1036 :poison 1,
1037 :flying 1,
1038 :normal 1,
1039 :rock 1,
1040 :electric 2,
1041 :ground 1,
1042 :fighting 1,
1043 :dark 1,
1044 :steel 1/2,
1045 :bug 1}
1046 #+end_example
1049 #+begin_src clojure :results output :exports both
1050 (clojure.pprint/pprint
1051 (pokemon.lpsolve/attack-mode
1052 (pokemon.types/susceptibility [:water])))
1053 #+end_src
1055 #+results:
1056 #+begin_example
1057 {:water 1/2,
1058 :psychic 1,
1059 :dragon 1/2,
1060 :fire 2,
1061 :ice 1,
1062 :grass 1/2,
1063 :ghost 1,
1064 :poison 1,
1065 :flying 1,
1066 :normal 1,
1067 :rock 2,
1068 :electric 1,
1069 :ground 2,
1070 :fighting 1,
1071 :dark 1,
1072 :steel 1,
1073 :bug 1}
1074 #+end_example
1076 Now =pokemon.types/susceptibility= reports the /attack-type/
1077 combination's effectiveness against other types.
1079 The 20 type combo achieves its goal in a very clever way.
1081 First, it weakens its effectiveness to other types at the expense of
1082 making it very strong against flying.
1084 #+begin_src clojure :results output :exports both
1085 (clojure.pprint/pprint
1086 (pokemon.lpsolve/attack-mode
1087 (pokemon.types/susceptibility
1088 [:normal :normal :normal :normal
1089 :ice :ice :ice :ice
1090 :electric :electric :electric
1091 :rock :rock])))
1092 #+end_src
1094 #+results:
1095 #+begin_example
1096 {:water 1/2,
1097 :psychic 1,
1098 :dragon 2,
1099 :fire 1/4,
1100 :ice 1/4,
1101 :grass 2,
1102 :ghost 0,
1103 :poison 1,
1104 :flying 512,
1105 :normal 1,
1106 :rock 1/16,
1107 :electric 1/8,
1108 :ground 0,
1109 :fighting 1/4,
1110 :dark 1,
1111 :steel 1/1024,
1112 :bug 4}
1113 #+end_example
1115 Then, it removes it's strengths against Flying, Normal, and Fighting
1116 by adding Ghost and Ground.
1118 #+begin_src clojure :results output :exports both
1119 (clojure.pprint/pprint
1120 (pokemon.lpsolve/attack-mode
1121 (pokemon.types/susceptibility
1122 [:normal :normal :normal :normal
1123 :ice :ice :ice :ice
1124 :electric :electric :electric
1125 :rock :rock
1126 ;; Spot resistances
1127 :ghost :ground])))
1128 #+end_src
1130 #+results:
1131 #+begin_example
1132 {:water 1/2,
1133 :psychic 2,
1134 :dragon 2,
1135 :fire 1/2,
1136 :ice 1/4,
1137 :grass 1,
1138 :ghost 0,
1139 :poison 2,
1140 :flying 0,
1141 :normal 0,
1142 :rock 1/8,
1143 :electric 1/4,
1144 :ground 0,
1145 :fighting 1/4,
1146 :dark 1/2,
1147 :steel 1/1024,
1148 :bug 2}
1149 #+end_example
1151 Adding the pair Psychic and Fighting takes care of its strength
1152 against Psychic and makes it ineffective against Dark, which is immune
1153 to Psychic.
1155 Adding the pair Grass and Poison makes takes care of its strength
1156 against poison and makes it ineffective against Steel, which is immune
1157 to poison.
1159 #+begin_src clojure :results output :exports both
1160 (clojure.pprint/pprint
1161 (pokemon.lpsolve/attack-mode
1162 (pokemon.types/susceptibility
1163 [;; setup
1164 :normal :normal :normal :normal
1165 :ice :ice :ice :ice
1166 :electric :electric :electric
1167 :rock :rock
1168 ;; Spot resistances
1169 :ghost :ground
1170 ;; Pair resistances
1171 :psychic :fighting
1172 :grass :poison])))
1173 #+end_src
1175 #+results:
1176 #+begin_example
1177 {:water 1,
1178 :psychic 1/2,
1179 :dragon 1,
1180 :fire 1/4,
1181 :ice 1/2,
1182 :grass 1,
1183 :ghost 0,
1184 :poison 1/2,
1185 :flying 0,
1186 :normal 0,
1187 :rock 1/4,
1188 :electric 1/4,
1189 :ground 0,
1190 :fighting 1/2,
1191 :dark 0,
1192 :steel 0,
1193 :bug 1/2}
1194 #+end_example
1196 Can you see the final step?
1198 It's adding the Water type, which is weak against Water and Dragon and
1199 strong against Rock and Fire.
1201 #+begin_src clojure :results output :exports both
1202 (clojure.pprint/pprint
1203 (pokemon.lpsolve/attack-mode
1204 (pokemon.types/susceptibility
1205 [;; setup
1206 :normal :normal :normal :normal
1207 :ice :ice :ice :ice
1208 :electric :electric :electric
1209 :rock :rock
1210 ;; Spot resistances
1211 :ghost :ground
1212 ;; Pair resistances
1213 :psychic :fighting
1214 :grass :poison
1215 ;; completion
1216 :water])))
1217 #+end_src
1219 #+results:
1220 #+begin_example
1221 {:water 1/2,
1222 :psychic 1/2,
1223 :dragon 1/2,
1224 :fire 1/2,
1225 :ice 1/2,
1226 :grass 1/2,
1227 :ghost 0,
1228 :poison 1/2,
1229 :flying 0,
1230 :normal 0,
1231 :rock 1/2,
1232 :electric 1/4,
1233 :ground 0,
1234 :fighting 1/2,
1235 :dark 0,
1236 :steel 0,
1237 :bug 1/2}
1238 #+end_example
1240 Which makes a particularly beautiful combination which is ineffective
1241 against all defending types.
1244 # #+begin_src clojure :results scalar :exports both
1245 # (with-out-str (clojure.contrib.pprint/pprint (seq (attack-mode (pokemon.types/susceptibility [:normal :normal :normal :normal :ice :ice :ice :ice :electric :electric :electric :rock :rock :ground :ghost :psychic :fighting :grass :poison])))))
1246 # #+end_src
1248 # #+results:
1249 # | [:water 1] | [:psychic 1/2] | [:dragon 1] | [:fire 1/4] | [:ice 1/2] | [:grass 1] | [:ghost 0] | [:poison 1/2] | [:flying 0] | [:normal 0] | [:rock 1/4] | [:electric 1/4] | [:ground 0] | [:fighting 1/2] | [:dark 0] | [:steel 0] | [:bug 1/2] |
1252 Is there anything else that's interesting?
1254 #+begin_src clojure :exports both
1255 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))
1256 #+end_src
1258 #+results:
1259 : INFEASIBLE
1261 #+begin_src clojure :exports both
1262 (pokemon.types/old-school
1263 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))
1264 #+end_src
1266 #+results:
1267 : INFEASIBLE
1269 #+begin_src clojure :exports both
1270 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))
1271 #+end_src
1273 #+results:
1274 : INFEASIBLE
1276 #+begin_src clojure :exports both
1277 (pokemon.types/old-school
1278 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))
1279 #+end_src
1281 #+results:
1282 : INFEASIBLE
1284 #+begin_src clojure :exports both
1285 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))
1286 #+end_src
1288 #+results:
1289 : INFEASIBLE
1291 #+begin_src clojure :exports both
1292 (pokemon.types/old-school
1293 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))
1294 #+end_src
1296 #+results:
1297 : INFEASIBLE
1299 There is no way to produce a defense-type that is weak to all types.
1300 This is probably because there are many types that are completely
1301 immune to some types, such as Flying, which is immune to Ground. A
1302 perfectly weak type could not use any of these types.
1304 * Summary
1306 Overall, the pok\eacute{}mon type system is slanted more towards
1307 defense rather than offense. While it is possible to create superior
1308 defensive types and exceptionally weak attack types, it is not
1309 possible to create exceptionally weak defensive types or very powerful
1310 attack types.
1312 Using the =lp_solve= library was more complicated than the best-first
1313 search, but yielded results quickly and efficiently. Expressing the
1314 problem in a linear form does have its drawbacks, however --- it's
1315 hard to ask questions such as "what is the best 3-type defensive combo
1316 in terms of susceptibility?", since susceptibility is not a linear
1317 function of a combo's types. It is also hard to get all the solutions
1318 to a particular problem, such as all the pokemon type combinations of
1319 length 8 which are immortal defense types.
1322 * COMMENT main-program
1323 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none
1324 <<intro>>
1325 <<body>>
1326 <<declares>>
1327 <<memory-management>>
1328 <<get-results>>
1329 <<solve>>
1330 <<farmer-example>>
1331 <<lp-solve>>
1332 <<better-farmer>>
1333 <<pokemon-lp>>
1334 <<results>>
1335 <<attack-oriented>>
1336 #+end_src