]> git.scottworley.com Git - planeteer/blame - planeteer.go
Follow the reverse energy gradient on Eden-selling worlds.
[planeteer] / planeteer.go
CommitLineData
d07f3caa
SW
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
18package main
19
20import "flag"
c45c1bca 21import "fmt"
d07f3caa
SW
22import "json"
23import "os"
c45c1bca
SW
24import "strings"
25
330093c1
SW
26var funds = flag.Int("funds", 0,
27 "Starting funds")
28
c45c1bca
SW
29var start = flag.String("start", "",
30 "The planet to start at")
d07f3caa 31
e346cb37 32var flight_plan_string = flag.String("flight_plan", "",
63b4dbbc 33 "Your hyper-holes for the day, comma-separated.")
544108c4 34
1c1ede68 35var end_string = flag.String("end", "",
e9ff66cf 36 "A comma-separated list of acceptable ending planets.")
c45c1bca
SW
37
38var planet_data_file = flag.String("planet_data_file", "planet-data",
d07f3caa
SW
39 "The file to read planet data from")
40
63b4dbbc 41var fuel = flag.Int("fuel", 16, "Hyper Jump power left")
e9ff66cf
SW
42
43var hold = flag.Int("hold", 300, "Size of your cargo hold")
c45c1bca
SW
44
45var start_edens = flag.Int("start_edens", 0,
46 "How many Eden Warp Units are you starting with?")
47
48var end_edens = flag.Int("end_edens", 0,
49 "How many Eden Warp Units would you like to keep (not use)?")
50
51var cloak = flag.Bool("cloak", false,
52 "Make sure to end with a Device of Cloaking")
53
e9ff66cf 54var drones = flag.Int("drones", 0, "Buy this many Fighter Drones")
c45c1bca 55
e9ff66cf 56var batteries = flag.Int("batteries", 0, "Buy this many Shield Batterys")
c45c1bca 57
76db1b3c
SW
58var drone_price = flag.Int("drone_price", 0, "Today's Fighter Drone price")
59
60var battery_price = flag.Int("battery_price", 0, "Today's Shield Battery price")
61
c45c1bca
SW
62var visit_string = flag.String("visit", "",
63 "A comma-separated list of planets to make sure to visit")
64
65func visit() []string {
e346cb37 66 if *visit_string == "" {
1c1ede68 67 return nil
e346cb37 68 }
c45c1bca
SW
69 return strings.Split(*visit_string, ",")
70}
71
e346cb37
SW
72func flight_plan() []string {
73 if *flight_plan_string == "" {
1c1ede68 74 return nil
e346cb37
SW
75 }
76 return strings.Split(*flight_plan_string, ",")
77}
78
1c1ede68
SW
79func end() map[string]bool {
80 if *end_string == "" {
81 return nil
82 }
83 m := make(map[string]bool)
809e65f4 84 for _, p := range strings.Split(*end_string, ",") {
1c1ede68
SW
85 m[p] = true
86 }
87 return m
88}
89
9b3b3d9a 90type Commodity struct {
9b3b3d9a
SW
91 BasePrice int
92 CanSell bool
93 Limit int
94}
12bc2cd7 95type Planet struct {
12bc2cd7
SW
96 BeaconOn bool
97 /* Use relative prices rather than absolute prices because you
98 can get relative prices without traveling to each planet. */
0e94bdac 99 RelativePrices map[string]int
12bc2cd7 100}
d07f3caa 101type planet_data struct {
0e94bdac
SW
102 Commodities map[string]Commodity
103 Planets map[string]Planet
e7e4bc13
SW
104 p2i, c2i map[string]int // Generated; not read from file
105 i2p, i2c []string // Generated; not read from file
d07f3caa
SW
106}
107
108func ReadData() (data planet_data) {
c45c1bca 109 f, err := os.Open(*planet_data_file)
d07f3caa
SW
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
c45c1bca
SW
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.
e7e4bc13
SW
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.
e346cb37
SW
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 *
c45c1bca
SW
159 */
160
161// The official list of dimensions:
162const (
e9ff66cf 163 // Name Num Size Description
0e94bdac
SW
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)
63b4dbbc 167 Fuel // 4 17 Hyper jump power left (0 - 16)
0e94bdac
SW
168 Location // 5 26 Location (which planet)
169 Hold // 6 15 Cargo bay contents (a *Commodity or nil)
76db1b3c
SW
170 BuyFighters // 7 2 Errand: Buy fighter drones
171 BuyShields // 8 2 Errand: Buy shield batteries
0e94bdac 172 Visit // 9 2**N Visit: Stop by these N planets in the route
c45c1bca
SW
173
174 NumDimensions
175)
176
177func bint(b bool) int {
0e94bdac
SW
178 if b {
179 return 1
180 }
c45c1bca
SW
181 return 0
182}
183
184func DimensionSizes(data planet_data) []int {
185 eden_capacity := data.Commodities["Eden Warp Units"].Limit
330093c1
SW
186 if *start_edens > eden_capacity {
187 eden_capacity = *start_edens
188 }
c45c1bca 189 cloak_capacity := bint(*cloak)
64d87250
SW
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)
c67c206a 196 dims[Hold] = len(data.Commodities) + 1
76db1b3c
SW
197 dims[BuyFighters] = bint(*drones > 0) + 1
198 dims[BuyShields] = bint(*batteries > 0) + 1
64d87250 199 dims[Visit] = 1 << uint(len(visit()))
2f4ed5ca
SW
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 }
c45c1bca
SW
207 return dims
208}
209
210func StateTableSize(dims []int) int {
e346cb37 211 product := 1
c45c1bca 212 for _, size := range dims {
e346cb37 213 product *= size
c45c1bca 214 }
e346cb37 215 return product
c45c1bca
SW
216}
217
218type State struct {
544108c4 219 value, from int
c45c1bca
SW
220}
221
c45c1bca
SW
222func EncodeIndex(dims, addr []int) int {
223 index := addr[0]
e346cb37
SW
224 if addr[0] > dims[0] {
225 panic(0)
226 }
330093c1 227 for i := 1; i < NumDimensions; i++ {
d1ad6058 228 if addr[i] < 0 || addr[i] > dims[i] {
e346cb37
SW
229 panic(i)
230 }
0e94bdac 231 index = index*dims[i] + addr[i]
c45c1bca
SW
232 }
233 return index
234}
235
236func DecodeIndex(dims []int, index int) []int {
330093c1
SW
237 addr := make([]int, NumDimensions)
238 for i := NumDimensions - 1; i > 0; i-- {
c45c1bca
SW
239 addr[i] = index % dims[i]
240 index /= dims[i]
241 }
242 addr[0] = index
243 return addr
244}
245
330093c1
SW
246func 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
e346cb37
SW
256}
257
330093c1
SW
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.
544108c4
SW
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.
544108c4 266 */
330093c1
SW
267
268func 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
276func FillCellByArriving(data planet_data, dims []int, table []State, addr []int) {
e346cb37
SW
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 */
330093c1 282 if addr[Fuel]+2 < dims[Fuel] {
e346cb37 283 other[Fuel] = addr[Fuel] + 2
330093c1 284 for other[Location] = 0; other[Location] < dims[Location]; other[Location]++ {
beb45aca
SW
285 if data.Planets[data.i2p[addr[Location]]].BeaconOn {
286 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
287 }
e346cb37
SW
288 }
289 other[Location] = addr[Location]
290 other[Fuel] = addr[Fuel]
291 }
292
63b4dbbc 293 /* Travel here via a hyper hole */
330093c1 294 if addr[Fuel]+1 < dims[Fuel] {
e346cb37 295 hole_index := (dims[Fuel] - 1) - (addr[Fuel] + 1)
7b5d9d13 296 if hole_index < len(flight_plan()) && addr[Location] == data.p2i[flight_plan()[hole_index]] {
e346cb37 297 other[Fuel] = addr[Fuel] + 1
7b5d9d13
SW
298 for other[Location] = 0; other[Location] < dims[Location]; other[Location]++ {
299 UpdateCell(table, my_index, EncodeIndex(dims, other), 0)
300 }
330093c1 301 other[Location] = addr[Location]
e346cb37
SW
302 other[Fuel] = addr[Fuel]
303 }
304 }
305
544108c4 306 /* Travel here via Eden Warp Unit */
d1ad6058 307 if addr[Edens]+1 < dims[Edens] && addr[UnusedCargo] > 1 {
0c27c344
SW
308 _, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Eden Warp Units"]
309 if !available {
310 other[Edens] = addr[Edens] + 1
d1ad6058 311 other[UnusedCargo] = addr[UnusedCargo] - 1
0c27c344
SW
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]
d1ad6058 316 other[UnusedCargo] = addr[UnusedCargo]
0c27c344 317 other[Edens] = addr[Edens]
330093c1
SW
318 }
319 }
330093c1
SW
320}
321
322func 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
cbf01f59
SW
346 absolute_price := float64(base_price) * float64(relative_price) / 100.0
347 sell_price := int(absolute_price * 0.9)
330093c1
SW
348
349 for other[UnusedCargo] = 0; other[UnusedCargo] < dims[UnusedCargo]; other[UnusedCargo]++ {
350
ada59973 351 quantity := *hold - (other[UnusedCargo] + other[Cloaks] + other[Edens])
330093c1
SW
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
359func 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
cbf01f59 377 absolute_price := int(float64(base_price) * float64(relative_price) / 100.0)
ada59973 378 quantity := *hold - (addr[UnusedCargo] + addr[Cloaks] + addr[Edens])
330093c1
SW
379 total_price := quantity * absolute_price
380 other[Hold] = 0
797391f8 381 other[UnusedCargo] = 0
330093c1 382 UpdateCell(table, my_index, EncodeIndex(dims, other), -total_price)
797391f8
SW
383 other[UnusedCargo] = addr[UnusedCargo]
384 other[Hold] = addr[Hold]
330093c1
SW
385}
386
387func FillCellByMisc(data planet_data, dims []int, table []State, addr []int) {
ada59973
SW
388 my_index := EncodeIndex(dims, addr)
389 other := make([]int, NumDimensions)
390 copy(other, addr)
d16f3322 391
544108c4 392 /* Buy a Device of Cloaking */
ada59973
SW
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
f800f732
SW
398 if other[Hold] != 0 {
399 other[UnusedCargo] = addr[UnusedCargo] + 1
400 }
ada59973
SW
401 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price)
402 other[UnusedCargo] = addr[UnusedCargo]
403 other[Cloaks] = addr[Cloaks]
404 }
405 }
76db1b3c 406
544108c4 407 /* Silly: Dump a Device of Cloaking */
76db1b3c 408
544108c4 409 /* Buy Fighter Drones */
76db1b3c
SW
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
544108c4 420 /* Buy Shield Batteries */
76db1b3c
SW
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
544108c4 431 /* Visit this planet */
76db1b3c 432
e7e4bc13
SW
433}
434
d16f3322
SW
435func FillCellByBuyingEdens(data planet_data, dims []int, table []State, addr []int) {
436 my_index := EncodeIndex(dims, addr)
437 other := make([]int, NumDimensions)
438 copy(other, addr)
439
440 /* Buy Eden warp units */
441 eden_limit := data.Commodities["Eden Warp Units"].Limit
442 if addr[Edens] > 0 && addr[Edens] <= eden_limit {
443 relative_price, available := data.Planets[data.i2p[addr[Location]]].RelativePrices["Eden Warp Units"]
444 if available {
445 absolute_price := int(float64(data.Commodities["Eden Warp Units"].BasePrice) * float64(relative_price) / 100.0)
446 for quantity := addr[Edens]; quantity > 0; quantity-- {
447 other[Edens] = addr[Edens] - quantity
448 if addr[Hold] != 0 {
449 other[UnusedCargo] = addr[UnusedCargo] + quantity
450 }
451 if other[UnusedCargo] < dims[UnusedCargo] {
452 UpdateCell(table, my_index, EncodeIndex(dims, other), -absolute_price * quantity)
453 }
454 }
455 other[Edens] = addr[Edens]
456 other[UnusedCargo] = addr[UnusedCargo]
457 }
458 }
459}
460
330093c1
SW
461func FillStateTable2Iteration(data planet_data, dims []int, table []State,
462addr []int, f func(planet_data, []int, []State, []int)) {
463 /* TODO: Justify the safety of the combination of this dimension
464 * iteration and the various phases f. */
e7e4bc13
SW
465 for addr[Hold] = 0; addr[Hold] < dims[Hold]; addr[Hold]++ {
466 for addr[Cloaks] = 0; addr[Cloaks] < dims[Cloaks]; addr[Cloaks]++ {
a1f10151 467 for addr[UnusedCargo] = 0; addr[UnusedCargo] < dims[UnusedCargo]; addr[UnusedCargo]++ {
76db1b3c
SW
468 for addr[BuyFighters] = 0; addr[BuyFighters] < dims[BuyFighters]; addr[BuyFighters]++ {
469 for addr[BuyShields] = 0; addr[BuyShields] < dims[BuyShields]; addr[BuyShields]++ {
330093c1
SW
470 for addr[Visit] = 0; addr[Visit] < dims[Visit]; addr[Visit]++ {
471 f(data, dims, table, addr)
e7e4bc13
SW
472 }
473 }
474 }
475 }
476 }
477 }
330093c1
SW
478}
479
480func FillStateTable2(data planet_data, dims []int, table []State,
d16f3322 481addr []int, barrier chan<- bool) {
330093c1
SW
482 FillStateTable2Iteration(data, dims, table, addr, FillCellByArriving)
483 FillStateTable2Iteration(data, dims, table, addr, FillCellBySelling)
484 FillStateTable2Iteration(data, dims, table, addr, FillCellByBuying)
485 FillStateTable2Iteration(data, dims, table, addr, FillCellByMisc)
fcdc120b
SW
486 FillStateTable2Iteration(data, dims, table, addr, FillCellByBuyingEdens)
487 if barrier != nil {
488 barrier <- true
489 }
e7e4bc13
SW
490}
491
492/* Filling the state table is a set of nested for loops NumDimensions deep.
493 * We split this into two procedures: 1 and 2. #1 is the outer, slowest-
494 * changing indexes. #1 fires off many calls to #2 that run in parallel.
495 * The order of the nesting of the dimensions, the order of iteration within
496 * each dimension, and where the 1 / 2 split is placed are carefully chosen
497 * to make this arrangement safe.
498 *
499 * Outermost two layers: Go from high-energy states (lots of fuel, edens) to
500 * low-energy state. These must be processed sequentially and in this order
501 * because you travel through high-energy states to get to the low-energy
502 * states.
503 *
504 * Third layer: Planet. This is a good layer to parallelize on. There's
505 * high enough cardinality that we don't have to mess with parallelizing
506 * multiple layers for good utilization (on 2011 machines). Each thread
507 * works on one planet's states and need not synchronize with peer threads.
508 */
e346cb37 509func FillStateTable1(data planet_data, dims []int, table []State) {
e7e4bc13
SW
510 barrier := make(chan bool, len(data.Planets))
511 eden_capacity := data.Commodities["Eden Warp Units"].Limit
512 work_units := (float64(*fuel) + 1) * (float64(eden_capacity) + 1)
513 work_done := 0.0
514 for fuel_remaining := *fuel; fuel_remaining >= 0; fuel_remaining-- {
fcdc120b
SW
515 /* Make an Eden-buying pass (Eden vendors' energy gradient
516 * along the Edens dimension runs backwards) */
517 for edens_remaining := 0; edens_remaining <= eden_capacity; edens_remaining++ {
518 for planet := range data.Planets {
519 if _, available := data.Planets[planet].RelativePrices["Eden Warp Units"]; available {
520 addr := make([]int, len(dims))
521 addr[Edens] = edens_remaining
522 addr[Fuel] = fuel_remaining
523 addr[Location] = data.p2i[planet]
524 FillStateTable2(data, dims, table, addr, nil)
525 }
526 }
527 }
a1f10151 528 for edens_remaining := eden_capacity; edens_remaining >= 0; edens_remaining-- {
d16f3322 529 /* Do the brunt of the work */
e7e4bc13 530 for planet := range data.Planets {
d16f3322
SW
531 addr := make([]int, len(dims))
532 addr[Edens] = edens_remaining
533 addr[Fuel] = fuel_remaining
534 addr[Location] = data.p2i[planet]
535 go FillStateTable2(data, dims, table, addr, barrier)
e7e4bc13
SW
536 }
537 for _ = range data.Planets {
538 <-barrier
539 }
540 work_done++
9eafb7a4 541 print(fmt.Sprintf("\r%3.0f%%", 100*work_done/work_units))
e7e4bc13
SW
542 }
543 }
e346cb37 544 print("\n")
e7e4bc13
SW
545}
546
ad4de13f
SW
547func FindBestState(data planet_data, dims []int, table []State) int {
548 addr := make([]int, NumDimensions)
549 addr[Edens] = *end_edens
550 addr[Cloaks] = dims[Cloaks] - 1
76db1b3c
SW
551 addr[BuyFighters] = dims[BuyFighters] - 1
552 addr[BuyShields] = dims[BuyShields] - 1
ad4de13f
SW
553 addr[Visit] = dims[Visit] - 1
554 // Fuel, Hold, UnusedCargo left at 0
ada59973 555 max_index := -1
ad4de13f
SW
556 max_value := 0
557 for addr[Location] = 0; addr[Location] < dims[Location]; addr[Location]++ {
809e65f4
SW
558 if len(end()) == 0 || end()[data.i2p[addr[Location]]] {
559 index := EncodeIndex(dims, addr)
560 if table[index].value > max_value {
561 max_value = table[index].value
562 max_index = index
563 }
ad4de13f
SW
564 }
565 }
566 return max_index
567}
568
9eafb7a4
SW
569func Commas(n int) (s string) {
570 r := n % 1000
571 n /= 1000
572 for n > 0 {
573 s = fmt.Sprintf(",%03d", r) + s
574 r = n % 1000
575 n /= 1000
576 }
577 s = fmt.Sprint(r) + s
578 return
579}
580
2f4a9ae8
SW
581func DescribePath(data planet_data, dims []int, table []State, start int) (description []string) {
582 for index := start; index > 0 && table[index].from > 0; index = table[index].from {
e4a1b48f 583 var line string
2f4a9ae8
SW
584 addr := DecodeIndex(dims, index)
585 prev := DecodeIndex(dims, table[index].from)
e4a1b48f 586 if addr[Fuel] != prev[Fuel] {
2f4a9ae8
SW
587 from := data.i2p[prev[Location]]
588 to := data.i2p[addr[Location]]
63b4dbbc 589 line += fmt.Sprintf("Jump from %v to %v (%v hyper jump units)", from, to, prev[Fuel]-addr[Fuel])
e4a1b48f 590 }
d16f3322 591 if addr[Edens] == prev[Edens] - 1 {
e4a1b48f
SW
592 from := data.i2p[prev[Location]]
593 to := data.i2p[addr[Location]]
594 line += fmt.Sprintf("Eden warp from %v to %v", from, to)
2f4a9ae8
SW
595 }
596 if addr[Hold] != prev[Hold] {
597 if addr[Hold] == 0 {
598 quantity := *hold - (prev[UnusedCargo] + prev[Edens] + prev[Cloaks])
e4a1b48f 599 line += fmt.Sprintf("Sell %v %v", quantity, data.i2c[prev[Hold]])
2f4a9ae8
SW
600 } else if prev[Hold] == 0 {
601 quantity := *hold - (addr[UnusedCargo] + addr[Edens] + addr[Cloaks])
e4a1b48f 602 line += fmt.Sprintf("Buy %v %v", quantity, data.i2c[addr[Hold]])
2f4a9ae8
SW
603 } else {
604 panic("Switched cargo?")
605 }
606
607 }
f800f732
SW
608 if addr[Cloaks] == 1 && prev[Cloaks] == 0 {
609 // TODO: Dump cloaks, convert from cargo?
e4a1b48f
SW
610 line += "Buy a Cloak"
611 }
d16f3322 612 if addr[Edens] > prev[Edens] {
e4a1b48f
SW
613 line += fmt.Sprint("Buy ", addr[Edens] - prev[Edens], " Eden Warp Units")
614 }
76db1b3c
SW
615 if addr[BuyShields] == 1 && prev[BuyShields] == 0 {
616 line += fmt.Sprint("Buy ", *batteries, " Shield Batterys")
617 }
618 if addr[BuyFighters] == 1 && prev[BuyFighters] == 0 {
619 line += fmt.Sprint("Buy ", *drones, " Fighter Drones")
620 }
e4a1b48f
SW
621 if line == "" {
622 line = fmt.Sprint(prev, " -> ", addr)
f800f732 623 }
e4a1b48f 624 description = append(description, fmt.Sprintf("%13v ", Commas(table[index].value)) + line)
2f4a9ae8
SW
625 }
626 return
627}
628
c45c1bca 629// (Example of a use case for generics in Go)
e7e4bc13 630func IndexPlanets(m *map[string]Planet, start_at int) (map[string]int, []string) {
a1f10151
SW
631 e2i := make(map[string]int, len(*m)+start_at)
632 i2e := make([]string, len(*m)+start_at)
e7e4bc13 633 i := start_at
c45c1bca 634 for e := range *m {
e7e4bc13
SW
635 e2i[e] = i
636 i2e[i] = e
c45c1bca
SW
637 i++
638 }
e7e4bc13 639 return e2i, i2e
c45c1bca 640}
e7e4bc13 641func IndexCommodities(m *map[string]Commodity, start_at int) (map[string]int, []string) {
a1f10151
SW
642 e2i := make(map[string]int, len(*m)+start_at)
643 i2e := make([]string, len(*m)+start_at)
e7e4bc13 644 i := start_at
c45c1bca 645 for e := range *m {
e7e4bc13
SW
646 e2i[e] = i
647 i2e[i] = e
c45c1bca
SW
648 i++
649 }
e7e4bc13 650 return e2i, i2e
c45c1bca
SW
651}
652
d07f3caa
SW
653func main() {
654 flag.Parse()
655 data := ReadData()
76db1b3c
SW
656 if *drone_price > 0 {
657 temp := data.Commodities["Fighter Drones"]
658 temp.BasePrice = *drone_price
659 data.Commodities["Fighter Drones"] = temp
660 }
661 if *battery_price > 0 {
662 temp := data.Commodities["Shield Batterys"]
663 temp.BasePrice = *battery_price
664 data.Commodities["Shield Batterys"] = temp
665 }
e7e4bc13
SW
666 data.p2i, data.i2p = IndexPlanets(&data.Planets, 0)
667 data.c2i, data.i2c = IndexCommodities(&data.Commodities, 1)
c45c1bca 668 dims := DimensionSizes(data)
330093c1 669 table := InitializeStateTable(data, dims)
e346cb37 670 FillStateTable1(data, dims, table)
ad4de13f 671 best := FindBestState(data, dims, table)
ada59973
SW
672 if best == -1 {
673 print("Cannot acheive success criteria\n")
674 } else {
ada59973
SW
675 description := DescribePath(data, dims, table, best)
676 for i := len(description) - 1; i >= 0; i-- {
677 fmt.Println(description[i])
678 }
2f4a9ae8 679 }
d07f3caa 680}