view org/lpsolve.org @ 10:eedd6897197d

fixed spelling errors for pokemon.types
author Robert McIntyre <rlm@mit.edu>
date Wed, 02 Nov 2011 08:02:11 -0700
parents a227fe337e83
children 4ea23241ff5b
line wrap: on
line source
1 #+title: Discovering Effective Pok\eacute{}mon Types Using Linear Optimization
2 #+author: Robert McIntyre & Dylan Holmes
3 #+EMAIL: rlm@mit.edu
4 #+SETUPFILE: ../../aurellem/org/setup.org
5 #+INCLUDE: ../../aurellem/org/level-0.org
9 * Introduction
10 In the [[./types.org][previous post]], we used the best-first search algorithm to
11 locate the most effective Pok\eacute{}mon type
12 combinations. Afterwards, we realized that we could transform this
13 search problem into a /linear optimization problem/. This conversion
14 offeres several advantages: first, search algorithms are comparatively
15 slow, whereas linear optimization algorithms are extremely fast;
16 second, it is difficult to determine whether a search problem has any
17 solution, whereas it is straightforward to determine whether a linear
18 optimization problem has any solution; finally, because systems of
19 linear equations are so common, many programming languages have linear
20 equation solvers written for them.
22 In this article, we will
23 - Solve a simple linear optimization problem in C :: We demonstrate
24 how to use the linear programming C library, =lp_solve=, to
25 solve a simple linear
26 optimization problem.
27 - Incorporate a C library into Clojure :: We will show how we gave
28 Clojure access to the linear programming C library, =lp_solve=.
29 - Find effective Pokemon types using linear programming :: Building
30 on our earlier code, (...)
31 - Present our results ::
33 #which can be represented and solved as a system of linear equations.
35 * COMMENT
36 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.
37 Pok\eacute{}mon is a game in which adorable creatures battle each
38 other using fantastic attacks. It was made into a several gameboy
39 games that all share the same battle system. Every pok\eacute{}mon in the
40 gameboy game has one or two /types/, such as Ground, Fire, Water,
41 etc. Every pok\eacute{}mon attack has exactly one type. Certain defending
42 types are weak or strong to other attacking types. For example, Water
43 attacks are strong against Fire pok\eacute{}mon, while Electric attacks are
44 weak against Ground Pok\eacute{}mon. In the games, attacks can be either
45 twice as effective as normal (Water vs. Fire), neutrally effective
46 (Normal vs. Normal), half as effective (Fire vs. Water), or not
47 effective at all (Electric vs. Ground). Thus the range of defense
48 values for a particular type is the set 0, 1/2, 1, 2. These are
49 referred to in the game as being immune, resistant, neutral, and
50 weak, respectively. I call them the /susceptance/ of one type to another.
52 If a pokemon has two types, then the strengths and weakness of each
53 type are /multiplied/ together. Thus Electric (2x weak to Ground)
54 combined with Flying (immune to Ground (0x)) is immune to
55 Ground. Fire (2x weak to Water) combined with Water (1/2x resistant
56 to Water) is neutral to Water. If both types are resistant to another
57 type, then the combination is doubly-resistant (1/4x) to that type. If
58 both types are weak to a certain type then the combination is
59 double-weak (4x) to that type.
61 ** Immortal Types
63 In the game, pok\eacute{}mon can have either one type, or two types. If this
64 restriction is lifted, is there any combination of types that is
65 resistant to all types? I call such a combination an /Immortal Type/,
66 since if that type's pattern was repeated over and over again towards
67 infinity, the resulting type would be immune to all attack types.
69 * Linear Programming
71 ** Terminology
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 In linear programming terminology, the function to be extremized is
77 the /objective function/.
79 ** COMMENT
80 First, we'll give a small example of a linear optimization problem,
81 and show how it can be solved with Clojure and =lp_solve=. Then,
82 we'll show how finding immortal pok\eacute{}mon types can be converted
83 into a linear problem suitable for solving in the same way.
85 ** The Farmer's Problem
87 Let's solve the Farmer's Problem, an example linear programming problem
88 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.
91 #+BEGIN_QUOTE
92 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to
93 plant two crops: wheat and barley. To produce these crops, it costs
94 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat
95 and $210 per acre for the barley. The farmer has $15000 available for
96 expenses. But after the harvest, the farmer must store the crops while
97 awaiting favorable market conditions. The farmer has storage space
98 for 4000 bushels. Each acre yields an average of 110 bushels of wheat
99 or 30 bushels of barley. If the net profit per bushel of wheat (after
100 all expenses have been subtracted) is $1.30 and for barley is $2.00,
101 how should the farmer plant the 75 acres to maximize profit?
102 #+END_QUOTE
104 The Farmer's Problem is to maximize profit subject to constraints on
105 available farmland, funds for expenses, and storage space.
107 | | Wheat | Barley | Maximum total |
108 |----------+----------------------+---------------------+--------------|
109 | / | < | > | <> |
110 | Farmland | \(w\) acres | \(b\) acres | 75 acres |
111 | Expense | $120 per acre | $210 per acre | $15000 |
112 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |
113 |----------+----------------------+---------------------+--------------|
114 | Profit | $1.30 per bushel | $2.00 per bushel | |
116 *** COMMENT
117 can be represented as a linear optimization
118 problem. In this form, it is a problem with two variables\mdash{}the number of
119 acres of wheat, \(w\), and the number of acres of barley, \(b\). The
120 aim is to maximize profit, which
122 subject to three constraints: the farmer can't spend more money
123 than he has, the farmer can't use more acres than he owns, and the harvest has
124 to fit in his storage space.
126 We can express these constraints succinctly using matrix
127 notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\),
128 we want to maximize the expression \(143 w + 60 b\) subject to
130 \(
131 \begin{cases}
132 120 w + 210 b & \leq & 1500\\
133 110 w + 30 b & \leq & 4000\\
134 1 w + 1 w & \leq & 75
135 \end{cases}
136 \)
138 #\(\begin{bmatrix}120&210\\110&30\\1 &
139 #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix}
140 #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\)
142 ** Solution using LP Solve
143 #(LP solve is available at http://www.example.com.)
144 In a new file, =farmer.lp=, we list the variables and constraints
145 of our problem using LP Solve syntax.
147 #+begin_src lpsolve :tangle ../lp/farmer.lp
148 /* Maximize Total Profit */
149 max: +143 wheat +60 barley;
152 /* -------- Constraints --------*/
154 /* the farmer can't spend more money than he has */
155 +120 wheat +210 barley <= 15000;
157 /* the harvest has to fit in his storage space */
158 +110 wheat +30 barley <= 4000;
160 /* he can't use more acres than he owns */
161 +wheat +barley <= 75;
162 #+end_src
165 #This is a set of linear equations ideal for solving using a program like
166 #=lp_solve=. In Linear Algebra terms we are maximizing the linear function
168 #\(\text{profit} = 143\text{ wheat} + 60\text{ barley}\), subject to the constraints
170 #Ax <= b,
172 #where A is [120 210 110 30 1 1], x is [wheat barley] and b is [15000
173 #4000 75].
175 Running the =lp_solve= program on =farmer.lp= yields the following output.
177 #+begin_src sh :exports both :results scalar
178 ~/roBin/lpsolve/lp_solve ~/aurellem/src/pokemon/farmer.lp
179 #+end_src
181 #+results:
182 :
183 : Value of objective function: 6315.62500000
184 :
185 : Actual values of the variables:
186 : wheat 21.875
187 : barley 53.125
189 This shows that the farmer can maximize his profit by planting 21.875
190 of the available acres with wheat and the remaining 53.125 acres with
191 barley; by doing so, he will make $6315.62(5) in profit.
194 #The farmer can make a profit of $6315.62 by planting 21.875 acres of
195 #his land with wheat and the other 53.125 acres of his land with barley.
197 * Incorporating =lp_solve= into Clojure
199 There is a Java API available which enables Java programs to use Lp
200 Solve. Although Clojure can use this Java API directly, the
201 interaction between Java, C, and Clojure is clumsy:
204 The Java API for LP Solve makes it possible to use Lp Solve algorithms
205 within Java. Although Clojure can use this Java API directly,
208 ** The Farmer's Problem in Clojure
209 We are going to solve the same problem involving wheat and barley,
210 that we did above, but this time using clojure and the lpsolve API.
212 #Because we ultimately intend to use Lp Solve to find optimal Pokemon type combinations.
213 # we want to solve linear optimization problems within Clojure, the language
215 ** Setup
216 =lp_solve= is a crufty =C= program which already comes with a JNI
217 interface written by Juergen Ebert. It's API is not
218 particularly friendly from a functional/immutable perspective, but
219 with a little work, we can build something that works great with
220 clojure.
222 #+srcname: intro
223 #+begin_src clojure :results silent
224 (ns pokemon.lpsolve
225 (:use rlm.ns-rlm))
226 (rlm.ns-rlm/ns-clone rlm.light-base)
227 (use 'clojure.set)
228 (import 'lpsolve.LpSolve)
229 (use '[clojure [pprint :only [pprint]]])
230 #+end_src
232 The LpSolve Java interface is available from the same site as
233 =lp_solve= itself, http://lpsolve.sourceforge.net/
234 Using it is the same as many other =C=
235 programs. There are excellent instructions to get set
236 up. The short version is that you must call Java with
237 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the
238 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For
239 example, in my =.bashrc= file, I have the line
240 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=.
241 If everything is set-up correctly,
243 #+srcname: body
244 #+begin_src clojure :results verbatim :exports both
245 (import 'lpsolve.LpSolve)
246 #+end_src
248 #+results: body
249 : lpsolve.LpSolve
251 should run with no problems.
253 ** Making a DSL to talk with LpSolve
254 *** Problems
255 Since we are using a =C= wrapper, we have to deal with manual memory
256 management for the =C= structures which are wrapped by the =LpSolve=
257 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's
258 very important to get it right. Also, the Java wrapper follows the
259 =C= tradition closely and defines many =static final int= constants
260 for the different states of the =LpSolve= instance instead of using Java
261 enums. The calling convention for adding rows and columns to
262 the constraint matrix is rather complicated and must be done column by
263 column or row by row, which can be error prone. Finally, I'd like to
264 gather all the important output information from the =LpSolve= instance
265 into a final, immutable structure.
267 In summary, the issues I'd like to address are:
269 - reliable memory management
270 - functional interface to =LpSolve=
271 - intelligible, immutable output
273 To deal with these issues I'll create four functions for interfacing
274 with =LpSolve=
276 #+srcname: declares
277 #+begin_src clojure :results silent
278 (in-ns 'pokemon.lpsolve)
280 ;; deal with automatic memory management for LpSolve instance.
281 (declare linear-program)
283 ;; functional interface to LpSolve
284 (declare lp-solve)
286 ;; immutable output from lp-solve
287 (declare solve get-results)
288 #+end_src
290 *** Memory Management
292 Every instance of =LpSolve= must be manually garbage collected via a
293 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=
294 to ensure that =deleteLP= is always called.
296 #+srcname: memory-management
297 #+begin_src clojure :results silent
298 (in-ns 'pokemon.lpsolve)
299 (defmacro linear-program
300 "solve a linear programming problem using LpSolve syntax.
301 within the macro, the variable =lps= is bound to the LpSolve instance."
302 [& statements]
303 (list 'let '[lps (LpSolve/makeLp 0 0)]
304 (concat '(try)
305 statements
306 ;; always free the =C= data structures.
307 '((finally (.deleteLp lps))))))
308 #+end_src
311 The macro captures the variable =lps= within its body, providing for a
312 convenient way to access the object using any of the methods of the
313 =LpSolve= API without having to worry about when to call
314 =deleteLP=.
316 *** Sensible Results
317 The =linear-program= macro deletes the actual =lps= object once it is
318 done working, so it's important to collect the important results and
319 add return them in an immutable structure at the end.
321 #+srcname: get-results
322 #+begin_src clojure :results silent
323 (in-ns 'pokemon.lpsolve)
325 (defrecord LpSolution
326 [objective-value
327 optimal-values
328 variable-names
329 solution
330 status
331 model])
333 (defn model
334 "Returns a textual representation of the problem suitable for
335 direct input to the =lp_solve= program (lps format)"
336 [#^LpSolve lps]
337 (let [target (java.io.File/createTempFile "lps" ".lp")]
338 (.writeLp lps (.getPath target))
339 (slurp target)))
341 (defn results
342 "given an LpSolve object, solves the object and returns a map of the
343 essential values which compose the solution."
344 [#^LpSolve lps]
345 (locking lps
346 (let [status (solve lps)
347 number-of-variables (.getNcolumns lps)
348 optimal-values (double-array number-of-variables)
349 optimal-values (do (.getVariables lps optimal-values)
350 (seq optimal-values))
351 variable-names
352 (doall ;; the doall is necessary since the lps object might
353 ;; soon be deleted
354 (map
355 #(.getColName lps (inc %))
356 (range number-of-variables)))
357 model (model lps)]
358 (LpSolution.
359 (.getObjective lps)
360 optimal-values
361 variable-names
362 (zipmap variable-names optimal-values)
363 status
364 model))))
366 #+end_src
368 Here I've created an object called =LpSolution= which stores the
369 important results from a session with =lp_solve=. Of note is the
370 =model= function which returns the problem in a form that can be
371 solved by other linear programming packages.
373 *** Solution Status of an LpSolve Object
375 #+srcname: solve
376 #+begin_src clojure :results silent
377 (in-ns 'pokemon.lpsolve)
379 (defn static-integer?
380 "does the field represent a static integer constant?"
381 [#^java.lang.reflect.Field field]
382 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))
383 (integer? (.get field nil))))
385 (defn integer-constants [class]
386 (filter static-integer? (.getFields class)))
388 (defn-memo constant-map
389 "Takes a class and creates a map of the static constant integer
390 fields with their names. This helps with C wrappers where they have
391 just defined a bunch of integer constants instead of enums"
392 [class]
393 (let [integer-fields (integer-constants class)]
394 (into (sorted-map)
395 (zipmap (map #(.get % nil) integer-fields)
396 (map #(.getName %) integer-fields)))))
398 (defn solve
399 "Solve an instance of LpSolve and return a string representing the
400 status of the computation. Will only solve a particular LpSolve
401 instance once."
402 [#^LpSolve lps]
403 ((constant-map LpSolve)
404 (.solve lps)))
406 #+end_src
408 The =.solve= method of an =LpSolve= object only returns an integer code
409 to specify the status of the computation. The =solve= method here
410 uses reflection to look up the actual name of the status code and
411 returns a more helpful status message that is also resistant to
412 changes in the meanings of the code numbers.
414 *** The Farmer Example in Clojure, Pass 1
416 Now we can implement a nicer version of the examples from the
417 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less
418 line-by-line translation of the Java code from that example.
420 #+srcname: farmer-example
421 #+begin_src clojure :results silent
422 (in-ns 'pokemon.lpsolve)
423 (defn farmer-example []
424 (linear-program
425 (results
426 (doto lps
427 ;; name the columns
428 (.setColName 1 "wheat")
429 (.setColName 2 "barley")
430 (.setAddRowmode true)
431 ;; row 1 : 120x + 210y <= 15000
432 (.addConstraintex 2
433 (double-array [120 210])
434 (int-array [1 2])
435 LpSolve/LE
436 15e3)
437 ;; row 2 : 110x + 30y <= 4000
438 (.addConstraintex 2
439 (double-array [110 30])
440 (int-array [1 2])
441 LpSolve/LE
442 4e3)
443 ;; ;; row 3 : x + y <= 75
444 (.addConstraintex 2
445 (double-array [1 1])
446 (int-array [1 2])
447 LpSolve/LE
448 75)
449 (.setAddRowmode false)
451 ;; add constraints
452 (.setObjFnex 2
453 (double-array [143 60])
454 (int-array [1 2]))
456 ;; set this as a maximization problem
457 (.setMaxim)))))
459 #+end_src
461 #+begin_src clojure :results output :exports both
462 (clojure.pprint/pprint
463 (:solution (pokemon.lpsolve/farmer-example)))
464 #+end_src
466 #+results:
467 : {"barley" 53.12499999999999, "wheat" 21.875}
469 And it works as expected!
471 *** The Farmer Example in Clojure, Pass 2
472 We don't have to worry about memory management anymore, and the farmer
473 example is about half as long as the example from the =LpSolve=
474 website, but we can still do better. Solving linear problems is all
475 about the constraint matrix $A$ , the objective function $c$, and the
476 right-hand-side $b$, plus whatever other options one cares to set for
477 the particular instance of =lp_solve=. Why not make a version of
478 =linear-program= that takes care of initialization?
482 #+srcname: lp-solve
483 #+begin_src clojure :results silent
484 (in-ns 'pokemon.lpsolve)
485 (defn initialize-lpsolve-row-oriented
486 "fill in an lpsolve instance using a constraint matrix =A=, the
487 objective function =c=, and the right-hand-side =b="
488 [#^LpSolve lps A b c]
489 ;; set the name of the last column to _something_
490 ;; this appears to be necessary to ensure proper initialization.
491 (.setColName lps (count c) (str "C" (count c)))
493 ;; This is the recommended way to "fill-in" an lps instance from the
494 ;; documentation. First, set row mode, then set the objective
495 ;; function, then set each row of the problem, and then turn off row
496 ;; mode.
497 (.setAddRowmode lps true)
498 (.setObjFnex lps (count c)
499 (double-array c)
500 (int-array (range 1 (inc (count c)))))
501 (dorun
502 (for [n (range (count A))]
503 (let [row (nth A n)
504 row-length (int (count row))]
505 (.addConstraintex lps
506 row-length
507 (double-array row)
508 (int-array (range 1 (inc row-length)))
509 LpSolve/LE
510 (double (nth b n))))))
511 (.setAddRowmode lps false)
512 lps)
515 (defmacro lp-solve
516 "by default:,
517 minimize (* c x), subject to (<= (* A x) b),
518 using continuous variables. You may set any number of
519 other options as in the LpSolve API."
520 [A b c & lp-solve-forms]
521 ;; assume that A is a vector of vectors
522 (concat
523 (list 'linear-program
524 (list 'initialize-lpsolve-row-oriented 'lps A b c))
525 `~lp-solve-forms))
526 #+end_src
528 Now, we can use a much more functional approach to solving the
529 farmer's problem:
531 #+srcname: better-farmer
532 #+begin_src clojure :results silent
533 (in-ns 'pokemon.lpsolve)
534 (defn better-farmer-example []
535 (lp-solve [[120 210]
536 [110 30]
537 [1 1]]
538 [15000
539 4000
540 75]
541 [143 60]
542 (.setColName lps 1 "wheat")
543 (.setColName lps 2 "barley")
544 (.setMaxim lps)
545 (results lps)))
546 #+end_src
548 #+begin_src clojure :exports both :results verbatim
549 (vec (:solution (pokemon.lpsolve/better-farmer-example)))
550 #+end_src
552 #+results:
553 : [["barley" 53.12499999999999] ["wheat" 21.875]]
555 Notice that both the inputs to =better-farmer-example= and the results
556 are immutable.
558 * Using LpSolve to find Immortal Types
559 ** Converting the Pokemon problem into a linear form
560 How can the original question about pok\eacute{}mon types be converted
561 into a linear problem?
563 Pokemon types can be considered to be vectors of numbers representing
564 their susceptances to various attacking types, so Water might look
565 something like this.
567 #+begin_src clojure :results scalar :exports both
568 (:water (pokemon.types/defense-strengths))
569 #+end_src
571 #+results:
572 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]
574 Where the numbers represent the susceptibility of Water to the
575 attacking types in the following order:
577 #+begin_src clojure :results output :exports both
578 (clojure.pprint/pprint
579 (pokemon.types/type-names))
580 #+end_src
582 #+results:
583 #+begin_example
584 [:normal
585 :fire
586 :water
587 :electric
588 :grass
589 :ice
590 :fighting
591 :poison
592 :ground
593 :flying
594 :psychic
595 :bug
596 :rock
597 :ghost
598 :dragon
599 :dark
600 :steel]
601 #+end_example
604 So, for example, Water is /resistant/ (x0.5) against Fire, which is
605 the second element in the list.
607 To combine types, these sorts of vectors are multiplied together
608 pair-wise to yield the resulting combination.
610 Unfortunately, we need some way to add two type vectors together
611 instead of multiplying them if we want to solve the problem with
612 =lp_solve=. Taking the log of the vector does just the trick.
614 If we make a matrix with each column being the log (base 2) of the
615 susceptance of each type, then finding an immortal type corresponds to
616 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)
617 and setting the constraint vector $c$ to all ones, which means that we
618 want to find the immortal type which uses the least amount of types.
620 #+srcname: pokemon-lp
621 #+begin_src clojure :results silent
622 (in-ns 'pokemon.lpsolve)
624 (require 'pokemon.types)
625 (require 'incanter.core)
627 (defn log-clamp-matrix [matrix]
628 ;; we have to clamp the Infinities to a more reasonable negative
629 ;; value because lp_solve does not play well with infinities in its
630 ;; constraint matrix.
631 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))
632 (incanter.core/log2
633 (incanter.core/trans
634 matrix))))
636 ;; constraint matrices
637 (defn log-defense-matrix []
638 (log-clamp-matrix
639 (doall (map (pokemon.types/defense-strengths)
640 (pokemon.types/type-names)))))
642 (defn log-attack-matrix []
643 (incanter.core/trans (log-defense-matrix)))
645 ;; target vectors
646 (defn all-resistant []
647 (doall (map (constantly -1) (pokemon.types/type-names))))
649 (defn all-weak []
650 (doall (map (constantly 1) (pokemon.types/type-names))))
652 (defn all-neutral []
653 (doall (map (constantly 0) (pokemon.types/type-names))))
656 ;; objective functions
657 (defn number-of-types []
658 (doall (map (constantly 1) (pokemon.types/type-names))))
660 (defn set-constraints
661 "sets all the constraints for an lpsolve instance to the given
662 constraint. =constraint= here is one of the LpSolve constants such
663 as LpSolve/EQ."
664 [#^LpSolve lps constraint]
665 (dorun (map (fn [index] (.setConstrType lps index constraint))
666 ;; ONE based indexing!!!
667 (range 1 (inc (.getNrows lps))))))
670 (defn set-discrete
671 "sets every variable in an lps problem to be a discrete rather than
672 continuous variable"
673 [#^LpSolve lps]
674 (dorun (map (fn [index] (.setInt lps index true))
675 ;; ONE based indexing!!!
676 (range 1 (inc (.getNcolumns lps))))))
678 (defn set-variable-names
679 "sets the variable names of the problem given a vector of names"
680 [#^LpSolve lps names]
681 (dorun
682 (map (fn [[index name]]
683 (.setColName lps (inc index) (str name)))
684 ;; ONE based indexing!!!
685 (indexed names))))
687 (defn poke-solve
688 ([poke-matrix target objective-function constraint min-num-types]
689 ;; must have at least one type
690 (let [poke-matrix
691 (concat poke-matrix
692 [(map (constantly 1)
693 (range (count (first poke-matrix))))])
694 target (concat target [min-num-types])]
695 (lp-solve poke-matrix target objective-function
696 (set-constraints lps constraint)
697 ;; must have more than min-num-types
698 (.setConstrType lps (count target) LpSolve/GE)
699 (set-discrete lps)
700 (set-variable-names lps (pokemon.types/type-names))
701 (results lps))))
702 ([poke-matrix target objective-function constraint]
703 ;; at least one type
704 (poke-solve poke-matrix target objective-function constraint 1)))
706 (defn solution
707 "If the results of an lpsolve operation are feasible, returns the
708 results. Otherwise, returns the error."
709 [results]
710 (if (not (= (:status results) "OPTIMAL"))
711 (:status results)
712 (:solution results)))
714 #+end_src
716 With this, we are finally able to get some results.
718 ** Results
719 #+srcname: results
720 #+begin_src clojure :results silent
721 (in-ns 'pokemon.lpsolve)
723 (defn best-defense-type
724 "finds a type combination which is resistant to all attacks."
725 []
726 (poke-solve
727 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))
729 (defn worst-attack-type
730 "finds the attack type which is not-very-effective against all pure
731 defending types (each single defending type is resistant to this
732 attack combination"
733 []
734 (poke-solve
735 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))
737 (defn worst-defense-type
738 "finds a defending type that is weak to all single attacking types."
739 []
740 (poke-solve
741 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))
743 (defn best-attack-type
744 "finds an attack type which is super effective against all single
745 defending types"
746 []
747 (poke-solve
748 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))
750 (defn solid-defense-type
751 "finds a defense type which is either neutral or resistant to all
752 single attacking types"
753 []
754 (poke-solve
755 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))
757 (defn solid-attack-type
758 "finds an attack type which is either neutral or super-effective to
759 all single attacking types."
760 []
761 (poke-solve
762 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))
764 (defn weak-defense-type
765 "finds a defense type which is either neutral or weak to all single
766 attacking types"
767 []
768 (poke-solve
769 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))
771 (defn neutral-defense-type
772 "finds a defense type which is perfectly neutral to all attacking
773 types."
774 []
775 (poke-solve
776 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))
778 #+end_src
780 *** Strongest Attack/Defense Combinations
782 #+begin_src clojure :results output :exports both
783 (clojure.pprint/pprint
784 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))
785 #+end_src
787 #+results:
788 #+begin_example
789 {":normal" 0.0,
790 ":ground" 1.0,
791 ":poison" 2.0,
792 ":flying" 1.0,
793 ":fighting" 0.0,
794 ":dragon" 0.0,
795 ":fire" 0.0,
796 ":dark" 1.0,
797 ":ice" 0.0,
798 ":steel" 1.0,
799 ":ghost" 0.0,
800 ":electric" 0.0,
801 ":bug" 0.0,
802 ":psychic" 0.0,
803 ":grass" 0.0,
804 ":water" 2.0,
805 ":rock" 0.0}
806 #+end_example
808 # #+results-old:
809 # : [[":normal" 0.0] [":ground" 1.0] [":poison" 0.0] [":flying" 1.0] [":fighting" 0.0] [":dragon" 1.0] [":fire" 0.0] [":dark" 0.0] [":ice" 0.0] [":steel" 2.0] [":ghost" 1.0] [":electric" 0.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 2.0] [":rock" 0.0]]
812 This is the immortal type combination we've been looking for. By
813 combining Steel, Water, Poison, and three types which each have complete
814 immunities to various other types, we've created a type that is resistant to
815 all attacking types.
817 #+begin_src clojure :results output :exports both
818 (clojure.pprint/pprint
819 (pokemon.types/susceptibility
820 [:poison :poison :water :water :steel :ground :flying :dark]))
821 #+end_src
823 #+results:
824 #+begin_example
825 {:water 1/2,
826 :psychic 0,
827 :dragon 1/2,
828 :fire 1/2,
829 :ice 1/2,
830 :grass 1/2,
831 :ghost 1/4,
832 :poison 0,
833 :flying 1/2,
834 :normal 1/2,
835 :rock 1/2,
836 :electric 0,
837 :ground 0,
838 :fighting 1/2,
839 :dark 1/4,
840 :steel 1/8,
841 :bug 1/8}
842 #+end_example
844 # #+results-old:
845 # : {:water 1/4, :psychic 1/4, :dragon 1/2, :fire 1/2, :ice 1/2, :grass 1/2, :ghost 1/2, :poison 0, :flying 1/4, :normal 0, :rock 1/4, :electric 0, :ground 0, :fighting 0, :dark 1/2, :steel 1/16, :bug 1/16}
848 Cool!
850 #+begin_src clojure :results output :exports both
851 (clojure.pprint/pprint
852 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))
853 #+end_src
855 #+results:
856 #+begin_example
857 {":normal" 0.0,
858 ":ground" 0.0,
859 ":poison" 0.0,
860 ":flying" 0.0,
861 ":fighting" 0.0,
862 ":dragon" 0.0,
863 ":fire" 0.0,
864 ":dark" 1.0,
865 ":ice" 0.0,
866 ":steel" 0.0,
867 ":ghost" 1.0,
868 ":electric" 0.0,
869 ":bug" 0.0,
870 ":psychic" 0.0,
871 ":grass" 0.0,
872 ":water" 0.0,
873 ":rock" 0.0}
874 #+end_example
876 Dark and Ghost are the best dual-type combo, and are resistant or
877 neutral to all types.
879 #+begin_src clojure :results output :exports both
880 (clojure.pprint/pprint
881 (pokemon.types/old-school
882 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))
883 #+end_src
885 #+results:
886 #+begin_example
887 {":normal" 0.0,
888 ":ground" 0.0,
889 ":poison" 0.0,
890 ":flying" 0.0,
891 ":fighting" 0.0,
892 ":dragon" 0.0,
893 ":fire" 0.0,
894 ":ice" 0.0,
895 ":ghost" 1.0,
896 ":electric" 0.0,
897 ":bug" 0.0,
898 ":psychic" 1.0,
899 ":grass" 0.0,
900 ":water" 0.0,
901 ":rock" 0.0}
902 #+end_example
904 Ghost and Psychic are a powerful dual type combo in the original games,
905 due to a glitch which made Psychic immune to Ghost type attacks, even
906 though the game claims that Ghost is strong to Psychic.
908 #+begin_src clojure :results verbatim :exports both
909 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))
910 #+end_src
912 #+results:
913 : INFEASIBLE
915 #+begin_src clojure :results verbatim :exports both
916 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))
917 #+end_src
919 #+results:
920 : INFEASIBLE
923 #+begin_src clojure :results verbatim :exports both
924 (pokemon.types/old-school
925 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))
926 #+end_src
928 #+results:
929 : INFEASIBLE
932 #+begin_src clojure :results output :exports both
933 (clojure.pprint/pprint
934 (pokemon.types/old-school
935 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))
936 #+end_src
938 #+results:
939 #+begin_example
940 {":normal" 0.0,
941 ":ground" 0.0,
942 ":poison" 0.0,
943 ":flying" 0.0,
944 ":fighting" 0.0,
945 ":dragon" 1.0,
946 ":fire" 0.0,
947 ":ice" 0.0,
948 ":ghost" 0.0,
949 ":electric" 0.0,
950 ":bug" 0.0,
951 ":psychic" 0.0,
952 ":grass" 0.0,
953 ":water" 0.0,
954 ":rock" 0.0}
955 #+end_example
957 The best attacking type combination is strangely Dragon from the
958 original games. It is neutral against all the original types except
959 for Dragon, which it is strong against. There is no way to make an
960 attacking type that is strong against every type, or even one that is
961 strong or neutral against every type, in the new games.
964 *** Weakest Attack/Defense Combinations
966 #+begin_src clojure :results output :exports both
967 (clojure.pprint/pprint
968 (pokemon.types/old-school
969 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))
970 #+end_src
972 #+results:
973 #+begin_example
974 {":normal" 5.0,
975 ":ground" 0.0,
976 ":poison" 0.0,
977 ":flying" 0.0,
978 ":fighting" 0.0,
979 ":dragon" 0.0,
980 ":fire" 1.0,
981 ":ice" 2.0,
982 ":ghost" 1.0,
983 ":electric" 1.0,
984 ":bug" 1.0,
985 ":psychic" 0.0,
986 ":grass" 3.0,
987 ":water" 2.0,
988 ":rock" 0.0}
989 #+end_example
991 # #+results-old:
992 # : [[":normal" 5.0] [":ground" 1.0] [":poison" 0.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":ice" 4.0] [":ghost" 1.0] [":electric" 4.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 1.0] [":rock" 1.0]]
994 #+begin_src clojure :results output :exports both
995 (clojure.pprint/pprint
996 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))
997 #+end_src
999 #+results:
1000 #+begin_example
1001 {":normal" 4.0,
1002 ":ground" 1.0,
1003 ":poison" 1.0,
1004 ":flying" 0.0,
1005 ":fighting" 1.0,
1006 ":dragon" 0.0,
1007 ":fire" 0.0,
1008 ":dark" 0.0,
1009 ":ice" 4.0,
1010 ":steel" 0.0,
1011 ":ghost" 1.0,
1012 ":electric" 3.0,
1013 ":bug" 0.0,
1014 ":psychic" 1.0,
1015 ":grass" 1.0,
1016 ":water" 1.0,
1017 ":rock" 2.0}
1018 #+end_example
1020 # #+results-old:
1021 # : [[":normal" 4.0] [":ground" 1.0] [":poison" 1.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":dark" 0.0] [":ice" 5.0] [":steel" 0.0] [":ghost" 1.0] [":electric" 5.0] [":bug" 0.0] [":psychic" 1.0] [":grass" 0.0] [":water" 1.0] [":rock" 2.0]]
1024 This is an extremely interesting type combination, in that it uses
1025 quite a few types.
1027 #+begin_src clojure :results verbatim :exports both
1028 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))
1029 #+end_src
1031 #+results:
1032 : 20.0
1034 20 types is the /minimum/ number of types before the attacking
1035 combination is not-very-effective or worse against all defending
1036 types. This would probably have been impossible to discover using
1037 best-first search, since it involves such an intricate type
1038 combination.
1040 It's so interesting that it takes 20 types to make an attack type that
1041 is weak to all types that the combination merits further investigation.
1043 Unfortunately, all of the tools that we've written so far are focused
1044 on defense type combinations. However, it is possible to make every
1045 tool attack-oriented via a simple macro.
1047 #+srcname: attack-oriented
1048 #+begin_src clojure :results silent
1049 (in-ns 'pokemon.lpsolve)
1051 (defmacro attack-mode [& forms]
1052 `(let [attack-strengths# pokemon.types/attack-strengths
1053 defense-strengths# pokemon.types/defense-strengths]
1054 (binding [pokemon.types/attack-strengths
1055 defense-strengths#
1056 pokemon.types/defense-strengths
1057 attack-strengths#]
1058 ~@forms)))
1059 #+end_src
1061 Now all the tools from =pokemon.types= will work for attack
1062 combinations.
1064 #+begin_src clojure :results output :exports both
1065 (clojure.pprint/pprint
1066 (pokemon.types/susceptibility [:water]))
1067 #+end_src
1069 #+results:
1070 #+begin_example
1071 {:water 1/2,
1072 :psychic 1,
1073 :dragon 1,
1074 :fire 1/2,
1075 :ice 1/2,
1076 :grass 2,
1077 :ghost 1,
1078 :poison 1,
1079 :flying 1,
1080 :normal 1,
1081 :rock 1,
1082 :electric 2,
1083 :ground 1,
1084 :fighting 1,
1085 :dark 1,
1086 :steel 1/2,
1087 :bug 1}
1088 #+end_example
1091 #+begin_src clojure :results output :exports both
1092 (clojure.pprint/pprint
1093 (pokemon.lpsolve/attack-mode
1094 (pokemon.types/susceptibility [:water])))
1095 #+end_src
1097 #+results:
1098 #+begin_example
1099 {:water 1/2,
1100 :psychic 1,
1101 :dragon 1/2,
1102 :fire 2,
1103 :ice 1,
1104 :grass 1/2,
1105 :ghost 1,
1106 :poison 1,
1107 :flying 1,
1108 :normal 1,
1109 :rock 2,
1110 :electric 1,
1111 :ground 2,
1112 :fighting 1,
1113 :dark 1,
1114 :steel 1,
1115 :bug 1}
1116 #+end_example
1118 Now =pokemon.types/susceptibility= reports the /attack-type/
1119 combination's effectiveness against other types.
1121 The 20 type combo achieves its goal in a very clever way.
1123 First, it weakens its effectiveness to other types at the expense of
1124 making it very strong against flying.
1126 #+begin_src clojure :results output :exports both
1127 (clojure.pprint/pprint
1128 (pokemon.lpsolve/attack-mode
1129 (pokemon.types/susceptibility
1130 [:normal :normal :normal :normal
1131 :ice :ice :ice :ice
1132 :electric :electric :electric
1133 :rock :rock])))
1134 #+end_src
1136 #+results:
1137 #+begin_example
1138 {:water 1/2,
1139 :psychic 1,
1140 :dragon 2,
1141 :fire 1/4,
1142 :ice 1/4,
1143 :grass 2,
1144 :ghost 0,
1145 :poison 1,
1146 :flying 512,
1147 :normal 1,
1148 :rock 1/16,
1149 :electric 1/8,
1150 :ground 0,
1151 :fighting 1/4,
1152 :dark 1,
1153 :steel 1/1024,
1154 :bug 4}
1155 #+end_example
1157 Then, it removes it's strengths against Flying, Normal, and Fighting
1158 by adding Ghost and Ground.
1160 #+begin_src clojure :results output :exports both
1161 (clojure.pprint/pprint
1162 (pokemon.lpsolve/attack-mode
1163 (pokemon.types/susceptibility
1164 [:normal :normal :normal :normal
1165 :ice :ice :ice :ice
1166 :electric :electric :electric
1167 :rock :rock
1168 ;; Spot resistances
1169 :ghost :ground])))
1170 #+end_src
1172 #+results:
1173 #+begin_example
1174 {:water 1/2,
1175 :psychic 2,
1176 :dragon 2,
1177 :fire 1/2,
1178 :ice 1/4,
1179 :grass 1,
1180 :ghost 0,
1181 :poison 2,
1182 :flying 0,
1183 :normal 0,
1184 :rock 1/8,
1185 :electric 1/4,
1186 :ground 0,
1187 :fighting 1/4,
1188 :dark 1/2,
1189 :steel 1/1024,
1190 :bug 2}
1191 #+end_example
1193 Adding the pair Psychic and Fighting takes care of its strength
1194 against Psychic and makes it ineffective against Dark, which is immune
1195 to Psychic.
1197 Adding the pair Grass and Poison makes takes care of its strength
1198 against poison and makes it ineffective against Steel, which is immune
1199 to poison.
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 #+end_src
1217 #+results:
1218 #+begin_example
1219 {:water 1,
1220 :psychic 1/2,
1221 :dragon 1,
1222 :fire 1/4,
1223 :ice 1/2,
1224 :grass 1,
1225 :ghost 0,
1226 :poison 1/2,
1227 :flying 0,
1228 :normal 0,
1229 :rock 1/4,
1230 :electric 1/4,
1231 :ground 0,
1232 :fighting 1/2,
1233 :dark 0,
1234 :steel 0,
1235 :bug 1/2}
1236 #+end_example
1238 Can you see the final step?
1240 It's adding the Water type, which is weak against Water and Dragon and
1241 strong against Rock and Fire.
1243 #+begin_src clojure :results output :exports both
1244 (clojure.pprint/pprint
1245 (pokemon.lpsolve/attack-mode
1246 (pokemon.types/susceptibility
1247 [;; setup
1248 :normal :normal :normal :normal
1249 :ice :ice :ice :ice
1250 :electric :electric :electric
1251 :rock :rock
1252 ;; Spot resistances
1253 :ghost :ground
1254 ;; Pair resistances
1255 :psychic :fighting
1256 :grass :poison
1257 ;; completion
1258 :water])))
1259 #+end_src
1261 #+results:
1262 #+begin_example
1263 {:water 1/2,
1264 :psychic 1/2,
1265 :dragon 1/2,
1266 :fire 1/2,
1267 :ice 1/2,
1268 :grass 1/2,
1269 :ghost 0,
1270 :poison 1/2,
1271 :flying 0,
1272 :normal 0,
1273 :rock 1/2,
1274 :electric 1/4,
1275 :ground 0,
1276 :fighting 1/2,
1277 :dark 0,
1278 :steel 0,
1279 :bug 1/2}
1280 #+end_example
1282 Which makes a particularly beautiful combination which is ineffective
1283 against all defending types.
1286 # #+begin_src clojure :results scalar :exports both
1287 # (with-out-str (clojure.contrib.pprint/pprint (seq (attack-mode (pokemon.types/susceptibility [:normal :normal :normal :normal :ice :ice :ice :ice :electric :electric :electric :rock :rock :ground :ghost :psychic :fighting :grass :poison])))))
1288 # #+end_src
1290 # #+results:
1291 # | [:water 1] | [:psychic 1/2] | [:dragon 1] | [:fire 1/4] | [:ice 1/2] | [:grass 1] | [:ghost 0] | [:poison 1/2] | [:flying 0] | [:normal 0] | [:rock 1/4] | [:electric 1/4] | [:ground 0] | [:fighting 1/2] | [:dark 0] | [:steel 0] | [:bug 1/2] |
1294 Is there anything else that's interesting?
1296 #+begin_src clojure :exports both
1297 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))
1298 #+end_src
1300 #+results:
1301 : INFEASIBLE
1303 #+begin_src clojure :exports both
1304 (pokemon.types/old-school
1305 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))
1306 #+end_src
1308 #+results:
1309 : INFEASIBLE
1311 #+begin_src clojure :exports both
1312 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))
1313 #+end_src
1315 #+results:
1316 : INFEASIBLE
1318 #+begin_src clojure :exports both
1319 (pokemon.types/old-school
1320 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))
1321 #+end_src
1323 #+results:
1324 : INFEASIBLE
1326 #+begin_src clojure :exports both
1327 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))
1328 #+end_src
1330 #+results:
1331 : INFEASIBLE
1333 #+begin_src clojure :exports both
1334 (pokemon.types/old-school
1335 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))
1336 #+end_src
1338 #+results:
1339 : INFEASIBLE
1341 There is no way to produce a defense-type that is weak to all types.
1342 This is probably because there are many types that are completely
1343 immune to some types, such as Flying, which is immune to Ground. A
1344 perfectly weak type could not use any of these types.
1346 * Summary
1348 Overall, the pok\eacute{}mon type system is slanted more towards defense
1349 rather than offense. While it is possible to create superior
1350 defensive types and exceptionally weak attack types, it is not possible to
1351 create exceptionally weak defensive types or very powerful attack
1352 types.
1354 Using the =lp_solve= library was more complicated than the best-first
1355 search, but yielded results quickly and efficiently. Expressing the
1356 problem in a linear form does have its drawbacks, however --- it's
1357 hard to ask questions such as "what is the best 3-type defensive combo
1358 in terms of susceptibility?", since susceptibility is not a linear
1359 function of a combo's types. It is also hard to get all the solutions
1360 to a particular problem, such as all the pokemon type combinations of
1361 length 8 which are immortal defense types.
1364 * COMMENT main-program
1365 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none
1366 <<intro>>
1367 <<body>>
1368 <<declares>>
1369 <<memory-management>>
1370 <<get-results>>
1371 <<solve>>
1372 <<farmer-example>>
1373 <<lp-solve>>
1374 <<better-farmer>>
1375 <<pokemon-lp>>
1376 <<results>>
1377 <<attack-oriented>>
1378 #+end_src
1381 * COMMENT Stuff to do.
1382 ** TODO fix namespaces to not use rlm.light-base