]> git.scottworley.com Git - planeteer/blame - planeteer.go
Implement --visit
[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 */
4d7362df
SW
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]
76db1b3c 440
e7e4bc13
SW
441}
442
d16f3322
SW
443func 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
330093c1
SW
469func FillStateTable2Iteration(data planet_data, dims []int, table []State,
470addr []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. */
e7e4bc13
SW
473 for addr[Hold] = 0; addr[Hold] < dims[Hold]; addr[Hold]++ {
474 for addr[Cloaks] = 0; addr[Cloaks] < dims[Cloaks]; addr[Cloaks]++ {
a1f10151 475 for addr[UnusedCargo] = 0; addr[UnusedCargo] < dims[UnusedCargo]; addr[UnusedCargo]++ {
76db1b3c
SW
476 for addr[BuyFighters] = 0; addr[BuyFighters] < dims[BuyFighters]; addr[BuyFighters]++ {
477 for addr[BuyShields] = 0; addr[BuyShields] < dims[BuyShields]; addr[BuyShields]++ {
330093c1
SW
478 for addr[Visit] = 0; addr[Visit] < dims[Visit]; addr[Visit]++ {
479 f(data, dims, table, addr)
e7e4bc13
SW
480 }
481 }
482 }
483 }
484 }
485 }
330093c1
SW
486}
487
488func FillStateTable2(data planet_data, dims []int, table []State,
d16f3322 489addr []int, barrier chan<- bool) {
330093c1
SW
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)
fcdc120b
SW
494 FillStateTable2Iteration(data, dims, table, addr, FillCellByBuyingEdens)
495 if barrier != nil {
496 barrier <- true
497 }
e7e4bc13
SW
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 */
e346cb37 517func FillStateTable1(data planet_data, dims []int, table []State) {
e7e4bc13
SW
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-- {
fcdc120b
SW
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 }
a1f10151 536 for edens_remaining := eden_capacity; edens_remaining >= 0; edens_remaining-- {
d16f3322 537 /* Do the brunt of the work */
e7e4bc13 538 for planet := range data.Planets {
d16f3322
SW
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)
e7e4bc13
SW
544 }
545 for _ = range data.Planets {
546 <-barrier
547 }
548 work_done++
9eafb7a4 549 print(fmt.Sprintf("\r%3.0f%%", 100*work_done/work_units))
e7e4bc13
SW
550 }
551 }
e346cb37 552 print("\n")
e7e4bc13
SW
553}
554
ad4de13f
SW
555func 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
76db1b3c
SW
559 addr[BuyFighters] = dims[BuyFighters] - 1
560 addr[BuyShields] = dims[BuyShields] - 1
ad4de13f
SW
561 addr[Visit] = dims[Visit] - 1
562 // Fuel, Hold, UnusedCargo left at 0
ada59973 563 max_index := -1
ad4de13f
SW
564 max_value := 0
565 for addr[Location] = 0; addr[Location] < dims[Location]; addr[Location]++ {
809e65f4
SW
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 }
ad4de13f
SW
572 }
573 }
574 return max_index
575}
576
9eafb7a4
SW
577func 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
2f4a9ae8
SW
589func 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 {
e4a1b48f 591 var line string
2f4a9ae8
SW
592 addr := DecodeIndex(dims, index)
593 prev := DecodeIndex(dims, table[index].from)
e4a1b48f 594 if addr[Fuel] != prev[Fuel] {
2f4a9ae8
SW
595 from := data.i2p[prev[Location]]
596 to := data.i2p[addr[Location]]
63b4dbbc 597 line += fmt.Sprintf("Jump from %v to %v (%v hyper jump units)", from, to, prev[Fuel]-addr[Fuel])
e4a1b48f 598 }
d16f3322 599 if addr[Edens] == prev[Edens] - 1 {
e4a1b48f
SW
600 from := data.i2p[prev[Location]]
601 to := data.i2p[addr[Location]]
602 line += fmt.Sprintf("Eden warp from %v to %v", from, to)
2f4a9ae8
SW
603 }
604 if addr[Hold] != prev[Hold] {
605 if addr[Hold] == 0 {
606 quantity := *hold - (prev[UnusedCargo] + prev[Edens] + prev[Cloaks])
e4a1b48f 607 line += fmt.Sprintf("Sell %v %v", quantity, data.i2c[prev[Hold]])
2f4a9ae8
SW
608 } else if prev[Hold] == 0 {
609 quantity := *hold - (addr[UnusedCargo] + addr[Edens] + addr[Cloaks])
e4a1b48f 610 line += fmt.Sprintf("Buy %v %v", quantity, data.i2c[addr[Hold]])
2f4a9ae8
SW
611 } else {
612 panic("Switched cargo?")
613 }
614
615 }
f800f732
SW
616 if addr[Cloaks] == 1 && prev[Cloaks] == 0 {
617 // TODO: Dump cloaks, convert from cargo?
e4a1b48f
SW
618 line += "Buy a Cloak"
619 }
d16f3322 620 if addr[Edens] > prev[Edens] {
e4a1b48f
SW
621 line += fmt.Sprint("Buy ", addr[Edens] - prev[Edens], " Eden Warp Units")
622 }
76db1b3c
SW
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 }
4d7362df
SW
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 }
e4a1b48f
SW
633 if line == "" {
634 line = fmt.Sprint(prev, " -> ", addr)
f800f732 635 }
e4a1b48f 636 description = append(description, fmt.Sprintf("%13v ", Commas(table[index].value)) + line)
2f4a9ae8
SW
637 }
638 return
639}
640
c45c1bca 641// (Example of a use case for generics in Go)
e7e4bc13 642func IndexPlanets(m *map[string]Planet, start_at int) (map[string]int, []string) {
a1f10151
SW
643 e2i := make(map[string]int, len(*m)+start_at)
644 i2e := make([]string, len(*m)+start_at)
e7e4bc13 645 i := start_at
c45c1bca 646 for e := range *m {
e7e4bc13
SW
647 e2i[e] = i
648 i2e[i] = e
c45c1bca
SW
649 i++
650 }
e7e4bc13 651 return e2i, i2e
c45c1bca 652}
e7e4bc13 653func IndexCommodities(m *map[string]Commodity, start_at int) (map[string]int, []string) {
a1f10151
SW
654 e2i := make(map[string]int, len(*m)+start_at)
655 i2e := make([]string, len(*m)+start_at)
e7e4bc13 656 i := start_at
c45c1bca 657 for e := range *m {
e7e4bc13
SW
658 e2i[e] = i
659 i2e[i] = e
c45c1bca
SW
660 i++
661 }
e7e4bc13 662 return e2i, i2e
c45c1bca
SW
663}
664
d07f3caa
SW
665func main() {
666 flag.Parse()
667 data := ReadData()
76db1b3c
SW
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 }
e7e4bc13
SW
678 data.p2i, data.i2p = IndexPlanets(&data.Planets, 0)
679 data.c2i, data.i2c = IndexCommodities(&data.Commodities, 1)
c45c1bca 680 dims := DimensionSizes(data)
330093c1 681 table := InitializeStateTable(data, dims)
e346cb37 682 FillStateTable1(data, dims, table)
ad4de13f 683 best := FindBestState(data, dims, table)
ada59973
SW
684 if best == -1 {
685 print("Cannot acheive success criteria\n")
686 } else {
ada59973
SW
687 description := DescribePath(data, dims, table, best)
688 for i := len(description) - 1; i >= 0; i-- {
689 fmt.Println(description[i])
690 }
2f4a9ae8 691 }
d07f3caa 692}