]> git.scottworley.com Git - planeteer/blob - planeteer.go
a2406ab30cf9a2ec6a0a79ab2afdcbc03e1232f0
[planeteer] / planeteer.go
1 /* Planeteer: Give trade route advice for Planets: The Exploration of Space
2 * Copyright (C) 2011 Scott Worley <sworley@chkno.net>
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation, either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 package main
19
20 import "flag"
21 import "fmt"
22 import "json"
23 import "os"
24 import "strings"
25
26 var funds = flag.Int("funds", 0,
27 "Starting funds")
28
29 var start = flag.String("start", "",
30 "The planet to start at")
31
32 var flight_plan_string = flag.String("flight_plan", "",
33 "Your hyper-holes for the day, comma-separated.")
34
35 var end_string = flag.String("end", "",
36 "A comma-separated list of acceptable ending planets.")
37
38 var planet_data_file = flag.String("planet_data_file", "planet-data",
39 "The file to read planet data from")
40
41 var fuel = flag.Int("fuel", 16, "Hyper Jump power left")
42
43 var hold = flag.Int("hold", 300, "Size of your cargo hold")
44
45 var start_edens = flag.Int("start_edens", 0,
46 "How many Eden Warp Units are you starting with?")
47
48 var end_edens = flag.Int("end_edens", 0,
49 "How many Eden Warp Units would you like to keep (not use)?")
50
51 var cloak = flag.Bool("cloak", false,
52 "Make sure to end with a Device of Cloaking")
53
54 var drones = flag.Int("drones", 0, "Buy this many Fighter Drones")
55
56 var batteries = flag.Int("batteries", 0, "Buy this many Shield Batterys")
57
58 var drone_price = flag.Int("drone_price", 0, "Today's Fighter Drone price")
59
60 var battery_price = flag.Int("battery_price", 0, "Today's Shield Battery price")
61
62 var visit_string = flag.String("visit", "",
63 "A comma-separated list of planets to make sure to visit")
64
65 func visit() []string {
66 if *visit_string == "" {
67 return nil
68 }
69 return strings.Split(*visit_string, ",")
70 }
71
72 func flight_plan() []string {
73 if *flight_plan_string == "" {
74 return nil
75 }
76 return strings.Split(*flight_plan_string, ",")
77 }
78
79 func end() map[string]bool {
80 if *end_string == "" {
81 return nil
82 }
83 m := make(map[string]bool)
84 for _, p := range strings.Split(*end_string, ",") {
85 m[p] = true
86 }
87 return m
88 }
89
90 type Commodity struct {
91 BasePrice int
92 CanSell bool
93 Limit int
94 }
95 type Planet struct {
96 BeaconOn bool
97 /* Use relative prices rather than absolute prices because you
98 can get relative prices without traveling to each planet. */
99 RelativePrices map[string]int
100 }
101 type planet_data struct {
102 Commodities map[string]Commodity
103 Planets map[string]Planet
104 p2i, c2i map[string]int // Generated; not read from file
105 i2p, i2c []string // Generated; not read from file
106 }
107
108 func ReadData() (data planet_data) {
109 f, err := os.Open(*planet_data_file)
110 if err != nil {
111 panic(err)
112 }
113 defer f.Close()
114 err = json.NewDecoder(f).Decode(&data)
115 if err != nil {
116 panic(err)
117 }
118 return
119 }
120
121 /* This program operates by filling in a state table representing the best
122 * possible trips you could make; the ones that makes you the most money.
123 * This is feasible because we don't look at all the possible trips.
124 * We define a list of things that are germane to this game and then only
125 * consider the best outcome in each possible game state.
126 *
127 * Each cell in the table represents a state in the game. In each cell,
128 * we track two things: 1. the most money you could possibly have while in
129 * that state and 2. one possible way to get into that state with that
130 * amount of money.
131 *
132 * A basic analysis can be done with a two-dimensional table: location and
133 * fuel. planeteer-1.0 used this two-dimensional table. This version
134 * adds features mostly by adding dimensions to this table.
135 *
136 * Note that the sizes of each dimension are data driven. Many dimensions
137 * collapse to one possible value (ie, disappear) if the corresponding
138 * feature is not enabled.
139 *
140 * The order of the dimensions in the list of constants below determines
141 * their layout in RAM. The cargo-based 'dimensions' are not completely
142 * independent -- some combinations are illegal and not used. They are
143 * handled as three dimensions rather than one for simplicity. Placing
144 * these dimensions first causes the unused cells in the table to be
145 * grouped together in large blocks. This keeps them from polluting
146 * cache lines, and if they are large enough, prevent the memory manager
147 * from allocating pages for these areas at all.
148 *
149 * If the table gets too big to fit in RAM:
150 * * Combine the Edens, Cloaks, and UnusedCargo dimensions. Of the
151 * 24 combinations, only 15 are legal: a 38% savings.
152 * * Reduce the size of the Fuel dimension to 3. We only ever look
153 * backwards 2 units, so just rotate the logical values through
154 * the same 3 physical addresses. This is good for an 82% savings.
155 * * Reduce the size of the Edens dimension from 3 to 2, for the
156 * same reasons as Fuel above. 33% savings.
157 * * Buy more ram. (Just sayin'. It's cheaper than you think.)
158 *
159 */
160
161 // The official list of dimensions:
162 const (
163 // Name Num Size Description
164 Edens = iota // 1 3 # of Eden warp units (0 - 2 typically)
165 Cloaks // 2 2 # of Devices of Cloaking (0 or 1)
166 UnusedCargo // 3 4 # of unused cargo spaces (0 - 3 typically)
167 Fuel // 4 17 Hyper jump power left (0 - 16)
168 Location // 5 26 Location (which planet)
169 Hold // 6 15 Cargo bay contents (a *Commodity or nil)
170 BuyFighters // 7 2 Errand: Buy fighter drones
171 BuyShields // 8 2 Errand: Buy shield batteries
172 Visit // 9 2**N Visit: Stop by these N planets in the route
173
174 NumDimensions
175 )
176
177 func bint(b bool) int {
178 if b {
179 return 1
180 }
181 return 0
182 }
183
184 func DimensionSizes(data planet_data) []int {
185 eden_capacity := data.Commodities["Eden Warp Units"].Limit
186 if *start_edens > eden_capacity {
187 eden_capacity = *start_edens
188 }
189 cloak_capacity := bint(*cloak)
190 dims := make([]int, NumDimensions)
191 dims[Edens] = eden_capacity + 1
192 dims[Cloaks] = cloak_capacity + 1
193 dims[UnusedCargo] = eden_capacity + cloak_capacity + 1
194 dims[Fuel] = *fuel + 1
195 dims[Location] = len(data.Planets)
196 dims[Hold] = len(data.Commodities) + 1
197 dims[BuyFighters] = bint(*drones > 0) + 1
198 dims[BuyShields] = bint(*batteries > 0) + 1
199 dims[Visit] = 1 << uint(len(visit()))
200
201 // Remind myself to add a line above when adding new dimensions
202 for i, dim := range dims {
203 if dim < 1 {
204 panic(i)
205 }
206 }
207 return dims
208 }
209
210 func StateTableSize(dims []int) int {
211 product := 1
212 for _, size := range dims {
213 product *= size
214 }
215 return product
216 }
217
218 type State struct {
219 value, from int
220 }
221
222 func EncodeIndex(dims, addr []int) int {
223 index := addr[0]
224 if addr[0] > dims[0] {
225 panic(0)
226 }
227 for i := 1; i < NumDimensions; i++ {
228 if addr[i] < 0 || addr[i] > dims[i] {
229 panic(i)
230 }
231 index = index*dims[i] + addr[i]
232 }
233 return index
234 }
235
236 func DecodeIndex(dims []int, index int) []int {
237 addr := make([]int, NumDimensions)
238 for i := NumDimensions - 1; i > 0; i-- {
239 addr[i] = index % dims[i]
240 index /= dims[i]
241 }
242 addr[0] = index
243 return addr
244 }
245
246 func InitializeStateTable(data planet_data, dims []int) []State {
247 table := make([]State, StateTableSize(dims))
248
249 addr := make([]int, NumDimensions)
250 addr[Fuel] = *fuel
251 addr[Edens] = *start_edens
252 addr[Location] = data.p2i[*start]
253 table[EncodeIndex(dims, addr)].value = *funds
254
255 return table
256 }
257
258 /* These four fill procedures fill in the cell at address addr by
259 * looking at all the possible ways to reach this cell and selecting
260 * the best one.
261 *
262 * The other obvious implementation choice is to do this the other way
263 * around -- for each cell, conditionally overwrite all the other cells
264 * that are reachable *from* the considered cell. We choose gathering
265 * reads over scattering writes to avoid having to take a bunch of locks.
266 */
267
268 func UpdateCell(table []State, here, there, value_difference int) {
269 possible_value := table[there].value + value_difference
270 if table[there].value > 0 && possible_value > table[here].value {
271 table[here].value = possible_value
272 table[here].from = there
273 }
274 }
275
276 func FillCellByArriving(data planet_data, dims []int, table []State, addr []int) {
277 my_index := EncodeIndex(dims, addr)
278 other := make([]int, NumDimensions)
279 copy(other, addr)
280
281 /* Travel here via a 2-fuel unit jump */
282 if addr[Fuel]+2 < dims[Fuel] {
283 other[Fuel] = addr[Fuel] + 2
284 for other[Location] = 0; other[Location] < dims[Location]; other[Location]++ {
285 if data.Planets[data.i2p[addr[Location]]].BeaconOn {
286 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
287 }
288 }
289 other[Location] = addr[Location]
290 other[Fuel] = addr[Fuel]
291 }
292
293 /* Travel here via a hyper hole */
294 if addr[Fuel]+1 < dims[Fuel] {
295 hole_index := (dims[Fuel] - 1) - (addr[Fuel] + 1)
296 if hole_index < len(flight_plan()) && addr[Location] == data.p2i[flight_plan()[hole_index]] {
297 other[Fuel] = addr[Fuel] + 1
298 for other[Location] = 0; other[Location] < dims[Location]; other[Location]++ {
299 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
300 }
301 other[Location] = addr[Location]
302 other[Fuel] = addr[Fuel]
303 }
304 }
305
306 /* Travel here via Eden Warp Unit */
307 if addr[Edens]+1 < dims[Edens] && addr[UnusedCargo] > 1 {
308 _, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Eden Warp Units"]
309 if !available {
310 other[Edens] = addr[Edens] + 1
311 other[UnusedCargo] = addr[UnusedCargo] - 1
312 for other[Location] = 0; other[Location] < dims[Location]; other[Location]++ {
313 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
314 }
315 other[Location] = addr[Location]
316 other[UnusedCargo] = addr[UnusedCargo]
317 other[Edens] = addr[Edens]
318 }
319 }
320 }
321
322 func FillCellBySelling(data planet_data, dims []int, table []State, addr []int) {
323 if addr[Hold] > 0 {
324 // Can't sell and still have cargo
325 return
326 }
327 if addr[UnusedCargo] > 0 {
328 // Can't sell everything and still have 'unused' holds
329 return
330 }
331 my_index := EncodeIndex(dims, addr)
332 other := make([]int, NumDimensions)
333 copy(other, addr)
334 planet := data.i2p[addr[Location]]
335 for other[Hold] = 0; other[Hold] < dims[Hold]; other[Hold]++ {
336 commodity := data.i2c[other[Hold]]
337 if !data.Commodities[commodity].CanSell {
338 // TODO: Dump cargo
339 continue
340 }
341 relative_price, available := data.Planets[planet].RelativePrices[commodity]
342 if !available {
343 continue
344 }
345 base_price := data.Commodities[commodity].BasePrice
346 absolute_price := float64(base_price) * float64(relative_price) / 100.0
347 sell_price := int(absolute_price * 0.9)
348
349 for other[UnusedCargo] = 0; other[UnusedCargo] < dims[UnusedCargo]; other[UnusedCargo]++ {
350
351 quantity := *hold - (other[UnusedCargo] + other[Cloaks] + other[Edens])
352 sale_value := quantity * sell_price
353 UpdateCell(table, my_index, EncodeIndex(dims, other), sale_value)
354 }
355 }
356 other[UnusedCargo] = addr[UnusedCargo]
357 }
358
359 func FillCellByBuying(data planet_data, dims []int, table []State, addr []int) {
360 if addr[Hold] == 0 {
361 // Can't buy and then have nothing
362 return
363 }
364 my_index := EncodeIndex(dims, addr)
365 other := make([]int, NumDimensions)
366 copy(other, addr)
367 planet := data.i2p[addr[Location]]
368 commodity := data.i2c[addr[Hold]]
369 if !data.Commodities[commodity].CanSell {
370 return
371 }
372 relative_price, available := data.Planets[planet].RelativePrices[commodity]
373 if !available {
374 return
375 }
376 base_price := data.Commodities[commodity].BasePrice
377 absolute_price := int(float64(base_price) * float64(relative_price) / 100.0)
378 quantity := *hold - (addr[UnusedCargo] + addr[Cloaks] + addr[Edens])
379 total_price := quantity * absolute_price
380 other[Hold] = 0
381 other[UnusedCargo] = 0
382 UpdateCell(table, my_index, EncodeIndex(dims, other), -total_price)
383 other[UnusedCargo] = addr[UnusedCargo]
384 other[Hold] = addr[Hold]
385 }
386
387 func FillCellByMisc(data planet_data, dims []int, table []State, addr []int) {
388 my_index := EncodeIndex(dims, addr)
389 other := make([]int, NumDimensions)
390 copy(other, addr)
391
392 /* Buy a Device of Cloaking */
393 if addr[Cloaks] == 1 && addr[UnusedCargo] < dims[UnusedCargo]-1 {
394 relative_price, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Device Of Cloakings"]
395 if available {
396 absolute_price := int(float64(data.Commodities["Device Of Cloakings"].BasePrice) * float64(relative_price) / 100.0)
397 other[Cloaks] = 0
398 if other[Hold] != 0 {
399 other[UnusedCargo] = addr[UnusedCargo] + 1
400 }
401 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price)
402 other[UnusedCargo] = addr[UnusedCargo]
403 other[Cloaks] = addr[Cloaks]
404 }
405 }
406
407 /* Silly: Dump a Device of Cloaking */
408
409 /* Buy Fighter Drones */
410 if addr[BuyFighters] == 1 {
411 relative_price, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Fighter Drones"]
412 if available {
413 absolute_price := int(float64(data.Commodities["Fighter Drones"].BasePrice) * float64(relative_price) / 100.0)
414 other[BuyFighters] = 0
415 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price * *drones)
416 other[BuyFighters] = addr[BuyFighters]
417 }
418 }
419
420 /* Buy Shield Batteries */
421 if addr[BuyShields] == 1 {
422 relative_price, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Shield Batterys"]
423 if available {
424 absolute_price := int(float64(data.Commodities["Shield Batterys"].BasePrice) * float64(relative_price) / 100.0)
425 other[BuyShields] = 0
426 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price * *batteries)
427 other[BuyShields] = addr[BuyShields]
428 }
429 }
430
431 /* Visit this planet */
432 var i uint
433 for i = 0; i < uint(len(visit())); i++ {
434 if addr[Visit] & (1 << i) != 0 && visit()[i] == data.i2p[addr[Location]] {
435 other[Visit] = addr[Visit] & ^(1 << i)
436 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
437 }
438 }
439 other[Visit] = addr[Visit]
440
441 }
442
443 func FillCellByBuyingEdens(data planet_data, dims []int, table []State, addr []int) {
444 my_index := EncodeIndex(dims, addr)
445 other := make([]int, NumDimensions)
446 copy(other, addr)
447
448 /* Buy Eden warp units */
449 eden_limit := data.Commodities["Eden Warp Units"].Limit
450 if addr[Edens] > 0 && addr[Edens] <= eden_limit {
451 relative_price, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Eden Warp Units"]
452 if available {
453 absolute_price := int(float64(data.Commodities["Eden Warp Units"].BasePrice) * float64(relative_price) / 100.0)
454 for quantity := addr[Edens]; quantity > 0; quantity-- {
455 other[Edens] = addr[Edens] - quantity
456 if addr[Hold] != 0 {
457 other[UnusedCargo] = addr[UnusedCargo] + quantity
458 }
459 if other[UnusedCargo] < dims[UnusedCargo] {
460 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price * quantity)
461 }
462 }
463 other[Edens] = addr[Edens]
464 other[UnusedCargo] = addr[UnusedCargo]
465 }
466 }
467 }
468
469 func FillStateTable2Iteration(data planet_data, dims []int, table []State,
470 addr []int, f func(planet_data, []int, []State, []int)) {
471 /* TODO: Justify the safety of the combination of this dimension
472 * iteration and the various phases f. */
473 for addr[Hold] = 0; addr[Hold] < dims[Hold]; addr[Hold]++ {
474 for addr[Cloaks] = 0; addr[Cloaks] < dims[Cloaks]; addr[Cloaks]++ {
475 for addr[UnusedCargo] = 0; addr[UnusedCargo] < dims[UnusedCargo]; addr[UnusedCargo]++ {
476 for addr[BuyFighters] = 0; addr[BuyFighters] < dims[BuyFighters]; addr[BuyFighters]++ {
477 for addr[BuyShields] = 0; addr[BuyShields] < dims[BuyShields]; addr[BuyShields]++ {
478 for addr[Visit] = 0; addr[Visit] < dims[Visit]; addr[Visit]++ {
479 f(data, dims, table, addr)
480 }
481 }
482 }
483 }
484 }
485 }
486 }
487
488 func FillStateTable2(data planet_data, dims []int, table []State,
489 addr []int, barrier chan<- bool) {
490 FillStateTable2Iteration(data, dims, table, addr, FillCellByArriving)
491 FillStateTable2Iteration(data, dims, table, addr, FillCellBySelling)
492 FillStateTable2Iteration(data, dims, table, addr, FillCellByBuying)
493 FillStateTable2Iteration(data, dims, table, addr, FillCellByMisc)
494 FillStateTable2Iteration(data, dims, table, addr, FillCellByBuyingEdens)
495 if barrier != nil {
496 barrier <- true
497 }
498 }
499
500 /* Filling the state table is a set of nested for loops NumDimensions deep.
501 * We split this into two procedures: 1 and 2. #1 is the outer, slowest-
502 * changing indexes. #1 fires off many calls to #2 that run in parallel.
503 * The order of the nesting of the dimensions, the order of iteration within
504 * each dimension, and where the 1 / 2 split is placed are carefully chosen
505 * to make this arrangement safe.
506 *
507 * Outermost two layers: Go from high-energy states (lots of fuel, edens) to
508 * low-energy state. These must be processed sequentially and in this order
509 * because you travel through high-energy states to get to the low-energy
510 * states.
511 *
512 * Third layer: Planet. This is a good layer to parallelize on. There's
513 * high enough cardinality that we don't have to mess with parallelizing
514 * multiple layers for good utilization (on 2011 machines). Each thread
515 * works on one planet's states and need not synchronize with peer threads.
516 */
517 func FillStateTable1(data planet_data, dims []int, table []State) {
518 barrier := make(chan bool, len(data.Planets))
519 eden_capacity := data.Commodities["Eden Warp Units"].Limit
520 work_units := (float64(*fuel) + 1) * (float64(eden_capacity) + 1)
521 work_done := 0.0
522 for fuel_remaining := *fuel; fuel_remaining >= 0; fuel_remaining-- {
523 /* Make an Eden-buying pass (Eden vendors' energy gradient
524 * along the Edens dimension runs backwards) */
525 for edens_remaining := 0; edens_remaining <= eden_capacity; edens_remaining++ {
526 for planet := range data.Planets {
527 if _, available := data.Planets[planet].RelativePrices["Eden Warp Units"]; available {
528 addr := make([]int, len(dims))
529 addr[Edens] = edens_remaining
530 addr[Fuel] = fuel_remaining
531 addr[Location] = data.p2i[planet]
532 FillStateTable2(data, dims, table, addr, nil)
533 }
534 }
535 }
536 for edens_remaining := eden_capacity; edens_remaining >= 0; edens_remaining-- {
537 /* Do the brunt of the work */
538 for planet := range data.Planets {
539 addr := make([]int, len(dims))
540 addr[Edens] = edens_remaining
541 addr[Fuel] = fuel_remaining
542 addr[Location] = data.p2i[planet]
543 go FillStateTable2(data, dims, table, addr, barrier)
544 }
545 for _ = range data.Planets {
546 <-barrier
547 }
548 work_done++
549 print(fmt.Sprintf("\r%3.0f%%", 100*work_done/work_units))
550 }
551 }
552 print("\n")
553 }
554
555 func FindBestState(data planet_data, dims []int, table []State) int {
556 addr := make([]int, NumDimensions)
557 addr[Edens] = *end_edens
558 addr[Cloaks] = dims[Cloaks] - 1
559 addr[BuyFighters] = dims[BuyFighters] - 1
560 addr[BuyShields] = dims[BuyShields] - 1
561 addr[Visit] = dims[Visit] - 1
562 // Fuel, Hold, UnusedCargo left at 0
563 max_index := -1
564 max_value := 0
565 for addr[Location] = 0; addr[Location] < dims[Location]; addr[Location]++ {
566 if len(end()) == 0 || end()[data.i2p[addr[Location]]] {
567 index := EncodeIndex(dims, addr)
568 if table[index].value > max_value {
569 max_value = table[index].value
570 max_index = index
571 }
572 }
573 }
574 return max_index
575 }
576
577 func Commas(n int) (s string) {
578 r := n % 1000
579 n /= 1000
580 for n > 0 {
581 s = fmt.Sprintf(",%03d", r) + s
582 r = n % 1000
583 n /= 1000
584 }
585 s = fmt.Sprint(r) + s
586 return
587 }
588
589 func DescribePath(data planet_data, dims []int, table []State, start int) (description []string) {
590 for index := start; index > 0 && table[index].from > 0; index = table[index].from {
591 var line string
592 addr := DecodeIndex(dims, index)
593 prev := DecodeIndex(dims, table[index].from)
594 if addr[Fuel] != prev[Fuel] {
595 from := data.i2p[prev[Location]]
596 to := data.i2p[addr[Location]]
597 line += fmt.Sprintf("Jump from %v to %v (%v hyper jump units)", from, to, prev[Fuel]-addr[Fuel])
598 }
599 if addr[Edens] == prev[Edens] - 1 {
600 from := data.i2p[prev[Location]]
601 to := data.i2p[addr[Location]]
602 line += fmt.Sprintf("Eden warp from %v to %v", from, to)
603 }
604 if addr[Hold] != prev[Hold] {
605 if addr[Hold] == 0 {
606 quantity := *hold - (prev[UnusedCargo] + prev[Edens] + prev[Cloaks])
607 line += fmt.Sprintf("Sell %v %v", quantity, data.i2c[prev[Hold]])
608 } else if prev[Hold] == 0 {
609 quantity := *hold - (addr[UnusedCargo] + addr[Edens] + addr[Cloaks])
610 line += fmt.Sprintf("Buy %v %v", quantity, data.i2c[addr[Hold]])
611 } else {
612 panic("Switched cargo?")
613 }
614
615 }
616 if addr[Cloaks] == 1 && prev[Cloaks] == 0 {
617 // TODO: Dump cloaks, convert from cargo?
618 line += "Buy a Cloak"
619 }
620 if addr[Edens] > prev[Edens] {
621 line += fmt.Sprint("Buy ", addr[Edens] - prev[Edens], " Eden Warp Units")
622 }
623 if addr[BuyShields] == 1 && prev[BuyShields] == 0 {
624 line += fmt.Sprint("Buy ", *batteries, " Shield Batterys")
625 }
626 if addr[BuyFighters] == 1 && prev[BuyFighters] == 0 {
627 line += fmt.Sprint("Buy ", *drones, " Fighter Drones")
628 }
629 if addr[Visit] != prev[Visit] {
630 // TODO: verify that the bit chat changed is addr[Location]
631 line += fmt.Sprint("Visit ", data.i2p[addr[Location]])
632 }
633 if line == "" {
634 line = fmt.Sprint(prev, " -> ", addr)
635 }
636 description = append(description, fmt.Sprintf("%13v ", Commas(table[index].value)) + line)
637 }
638 return
639 }
640
641 // (Example of a use case for generics in Go)
642 func IndexPlanets(m *map[string]Planet, start_at int) (map[string]int, []string) {
643 e2i := make(map[string]int, len(*m)+start_at)
644 i2e := make([]string, len(*m)+start_at)
645 i := start_at
646 for e := range *m {
647 e2i[e] = i
648 i2e[i] = e
649 i++
650 }
651 return e2i, i2e
652 }
653 func IndexCommodities(m *map[string]Commodity, start_at int) (map[string]int, []string) {
654 e2i := make(map[string]int, len(*m)+start_at)
655 i2e := make([]string, len(*m)+start_at)
656 i := start_at
657 for e := range *m {
658 e2i[e] = i
659 i2e[i] = e
660 i++
661 }
662 return e2i, i2e
663 }
664
665 func main() {
666 flag.Parse()
667 data := ReadData()
668 if *drone_price > 0 {
669 temp := data.Commodities["Fighter Drones"]
670 temp.BasePrice = *drone_price
671 data.Commodities["Fighter Drones"] = temp
672 }
673 if *battery_price > 0 {
674 temp := data.Commodities["Shield Batterys"]
675 temp.BasePrice = *battery_price
676 data.Commodities["Shield Batterys"] = temp
677 }
678 data.p2i, data.i2p = IndexPlanets(&data.Planets, 0)
679 data.c2i, data.i2c = IndexCommodities(&data.Commodities, 1)
680 dims := DimensionSizes(data)
681 table := InitializeStateTable(data, dims)
682 FillStateTable1(data, dims, table)
683 best := FindBestState(data, dims, table)
684 if best == -1 {
685 print("Cannot acheive success criteria\n")
686 } else {
687 description := DescribePath(data, dims, table, best)
688 for i := len(description) - 1; i >= 0; i-- {
689 fmt.Println(description[i])
690 }
691 }
692 }