This is the second iteration of the code I originally posted here: Calculate optimal game upgrades. Based on the feedback there, I chose to change the code to use a dynamic programming approach with memoization. I also made some general improvements to the code. I'm looking for ways to increase the performance of the code further, as well as general code improvements. I think the get_max_dollars
function can be improved, since there's a trivial closed form solution for the case where dollars and gold can be real numbers, although I'm not sure how to apply that knowledge to make the function faster.
(Note: I chose not to use the heuristic solution from @user286929 just because I would like the code to find the provably optimal solution. )
(Note 2: A few people were confused about the strange choice of costs and bonus formula in my last question, the upgrade costs and bonuses are just from a random idle game. )
'''
This program calculates the optimal number of upgrades to purchase
given a certain number of points to spend on upgrades
Upgrades cost:
- pennies: 1
- dimes: 3
- dollars: 6
- gold: 15
The upgrade bonus can be calculated as:
pennies + pennies * dimes + pennies * dimes * dollars
+ pennies * dimes * dollars * gold
or alternatively:
pennies * (1 + dimes * (1 + dollars * (1 + gold)))
'''
from functools import cache
import time
'''
Input: points is the number of points to spend on pennies, dimes, dollars, and gold combined
Output: returns a nested tuple containing
((pennies, dimes, dollars, gold), total bonus)
'''
def get_max_score(points):
last_max_score = -1
last_max_combo = (0, 0, 0, 0)
# we decrement by 3 instead of 1, because the cost of all
# upgrades other than pennies is a multiple of 3.
# Therefore, the number of upgrades other than pennies that
# we can buy only changes every 3 pennies.
for pennies in range(points, -1, -3):
points_left = points - pennies
dimes, dollars, gold = get_max_dimes(points_left)
score = pennies * (1 + dimes * (1 + dollars * (1 + gold)))
if score >= last_max_score:
last_max_combo = (pennies, dimes, dollars, gold)
last_max_score = score
else:
# It seems like this function has one global maximum
# although I haven't proved this
pass # break
return last_max_combo, last_max_score
'''
Input: points is the number of points to spend on dimes, dollars, and gold combined
Output: returns a tuple containing
(dimes, dollars, gold)
'''
@cache
def get_max_dimes(points):
# the cost of dimes, dollars, and gold are all divisible by 3
# hence, the max number of coins with x points will be the same
# as the max number of coins with y points, where y is x rounded
# to the nearest multiple of 3
point_remainder = points % 3
if point_remainder != 0:
return get_max_dimes(points - point_remainder)
max_dimes = points // 3
last_max_score = -1
last_max_combo = (0, 0, 0)
for dimes in range(max_dimes, -1, -1):
points_left = points - dimes * 3
dollars, gold = get_max_dollars(points_left)
partial_score = dimes * (1 + dollars * (1 + gold))
if partial_score >= last_max_score:
last_max_combo = (dimes, dollars, gold)
last_max_score = partial_score
return last_max_combo
'''
Input: points is the number of points to spend on dollars and gold combined
Output: returns a tuple containing
(dollars, gold)
'''
@cache
def get_max_dollars(points):
max_dollars = points // 6
last_max_score = -1
last_max_combo = (0, 0)
for dollars in range(max_dollars, -1, -1):
points_left = points - dollars * 6
gold = points_left // 15
partial_score = dollars * (1 + gold)
if partial_score >= last_max_score:
last_max_combo = (dollars, gold)
last_max_score = partial_score
return last_max_combo
while True:
start_points = int(input("Start points: "))
end_points = input("End points (leave blank for single value): ")
if end_points == '':
end_points = start_points
end_points = int(end_points)
if end_points < start_points:
start_points, end_points = end_points, start_points
print()
time_start = time.perf_counter()
for y in range(end_points, start_points - 1, -1):
combo = get_max_score(y)
print(f"score: {y}, {combo}")
time_end = time.perf_counter()
print(f"\nComputation took {time_end - time_start:.5f}s")
if input("\nRun again? [y]/[n] ") != "y":
break
print()
get_max_score
does not have 1 global maximum. Considerx = 6
; either do 6 pennies or 3 pennies and 1 dime. The code above seems to run >5x faster than my previous answer - I'm quite impressed with what you've done with it. \$\endgroup\$get_max_score
either has one global maximum, or it has several local maximums with equivalent values (so once we've found a local maximum, we've found the final solution). Of course this is just speculation based on plotting the values and graphing in Desmos. \$\endgroup\$get_max_dollars
function? It seems like the simplest function to optimize, and it feels like there should be a faster algorithm to find the optimal value than looping over all possible values. \$\endgroup\$