Cookie Clicker is a game built around a simulation in which your goal is to bake as many cookies as fast as possible. The main strategy component of the game is choosing how to allocate the cookies that you have produced to upgrade your ability to produce even more cookies faster. You can play Cookie Clicker here. Before you start work on this mini-project, we strongly recommend that you complete the Practice Activity “The Case of the Greedy Boss” which is designed to walk you through the steps of building a simulation similar to Cookie Clicker.
In Cookie Clicker, you have many options for upgrading your ability to produce cookies. Originally, you can only produce cookies by clicking your mouse. However, you can use the cookies you earn to buy other methods of producing cookies (Grandmas, farms, factories, etc.). Each production method increases the number of “cookies per second” (CPS) you produce. Further, each time you buy one of the production methods, its price goes up. So, you must carefully consider the cost and benefits of purchasing a production method, and the trade-offs change as the game goes on.
For this assignment, you will implement a simplified simulation of the Cookie Clicker game. You will implement different strategies and see how they fare over a given period of time. In our version of the game, there is no graphical interface and therefore no actual “clicking”. Instead, you will start with a CPS of 1.0 and may start purchasing automatic production methods once you have enough cookies to do so. You will implement both the simulation engine for the game and your own strategies for selecting what production methods to buy.
We have provided the following template that contains an outline of the code you will write, including a ClickerState
class, which will keep track of the state of the simulation, and a simulate_clicker
function, which will run the simulation. The signature (name and parameters) of the functions, classes, and methods in this file must remain unchanged, but you may add any additional functions, methods, or other code that you need to.
We have provided a BuildInfo
class for you to use. This class keeps track of the cost (in cookies) and value (in CPS) of each item (production method) that you can buy. When you create a new BuildInfo
object, it is initialized by default with the default parameters for our game. Keep in mind that if you pass a BuildInfo
object around in your program and it is modified anywhere, then you will see the changes everywhere. If you do not want that, we have provided a clone
method that will create an identical copy of the object. You can see a description of the class and its methods here.
class BuildInfo:
"""
Class to track build information.
"""
def __init__(self, build_info = None, growth_factor = BUILD_GROWTH):
"""
Initialize the BuildInfo object. Use default arguments for the game.
"""
def build_items(self):
"""
Get a list of buildable items sorted by name.
"""
def get_cost(self, item):
"""
Get the current cost of an item.
Will throw a KeyError exception if item is not in the build info.
"""
def get_cps(self, item):
"""
Get the current CPS of an item
Will throw a KeyError exception if item is not in the build info.
"""
def update_item(self, item):
"""
Update the cost of an item by the growth factor
Will throw a KeyError exception if item is not in the build info.
"""
def clone(self):
"""
Return a clone of this BuildInfo
"""
It has methods to allow you to find out what all of the items’ names are, the cost per CPS of each item, and to update the cost of a particular item appropriately.
We have also provided a run
function to run your simulator. Note that the run
function simply calls run_strategy
, which runs the simulator once with a given strategy. You can add more calls to run_strategy
inside of run
once you develop your own strategies. The run_strategy
function runs the simulation, prints out the final state of the game after the given time and then plots the total number of cookies over time. You can replace our SimplePlot
plotting code with your own plotting code if you wish to use IDLE or another Python IDE. If you do leave our plots in, make sure that your web browser is configured to allow popup windows from CodeSkulptor. The plots will help you understand how the number of cookies grow over time, which is the point of this week in the class, so we recommend you do look at the plots, even if you comment them out while you are debugging.
We have provided a simple strategy, called strategy_cursor_broken
. Note the signature of the function: strategy_cursor_broken(cookies, cps, history, time_left, build_info)
. All strategy functions take the current number of cookies, the current CPS, the history of purchases in the simulation, the amount of time left in the simulation, and a BuildInfo
object (even if they don’t use these parameters). You’ll note that this simple strategy just always picks "Cursor"
no matter what the state of the game is. This is obviously not a good strategy (and it violates the requirements of a strategy function given below), but rather is a placeholder so you can see the signature of a strategy function looks like and use it while you are debugging other parts of your code.
You should first implement the ClickerState
class. This class will keep track of the state of the game during a simulation. This various fields in this class should roughly correspond to the local variables used in implementing the function greedy_boss
in the Practice Activity. (Cookies correspond to money and upgrades to CPS correspond to bribes.) By encapsulating the game state in this class, the logic for running a simulation of the game will be greatly simplified. The ClickerState
class must keep track of four things:
0.0
).0.0
).0.0
).1.0
).Note that you should use float
to keep track of all state properties. You will have fractional values for cookies and CPS throughout.
During a simulation, upgrades are only allowed at an integral number of seconds as required in Cookie Clicker. However, the CPS value is a floating point number. In addition to this information, your ClickerState
class must also keep track of the history of the game. We will track the history as a list of tuples. Each tuple in the list will contain 4 values: a time, an item that was bought at that time (or None
), the cost of the item, and the total number of cookies produced by that time. This history list should therefore be initialized as [(0.0, None, 0.0, 0.0)]
.
The methods of the ClickerState
class interact with this state as follows:
__str__
: This method should return the state (possibly without the history list) as a string in a human readable format. This is primarily to help you develop and debug your program. It will also be used by OwlTest in error messages to show you the state of your ClickerState
object after you fail a test.get_cookies
, get_cps
, get_time
, get_history
: These methods should simply return the current number of cookies, the current CPS, the current time, and the history, respectively. Note that get_history
should return a copy of the history list so that you are not returning a reference to an internal data structure. This will prevent a broken strategy function from inadvertently messing up the history, for instance.time_until
: This method should return the number of seconds you must wait until you will have the given number of cookies. Remember that you cannot wait for fractional seconds, so while you should return a float
it should not have a fractional part.wait
: This method should “wait” for the given amount of time. This means you should appropriately increase the time, the current number of cookies, and the total number of cookies.buy_item
: This method should “buy” the given item. This means you should appropriately adjust the current number of cookies, the CPS, and add an entry into the history.If you are passed an argument that is invalid (such as an attempt to buy an item for which you do not have enough cookies), you should just return from the method without doing anything.
Once you have a complete ClickerState
class, you are ready to implement simulate_clicker
. The simulate_clicker
function should take a BuildInfo
class, the number of seconds to run the simulation for, and a strategy function. Note that simulate_clicker
is a higher-order function: it takes a strategy function as an argument!
The first thing you should do in this function is to make a clone of the build_info
object and create a new ClickerState
object. The function should then loop (in the same manner in the function greedy_boss
from the Practice Activity) until the time in the ClickerState
object reaches the duration of the simulation.
For each iteration of the loop, your simulate_clicker
function should do the following things:
Note that the ClickerState
class already implements methods that will greatly simplify steps 3???6. Use them!
Also note that the time is only incremented when you wait until you can purchase the item selected by the strategy function. You should not try to create a loop that ticks through each second of the simulation. This will not work effectively because it will be incredibly slow. Most times during the simulation you are just waiting until you have accrued enough cookies (this is the boring part of the actual Cookie Clicker game), so the process described above just skips over those times.
For correctness, you should not allow the simulation to run past the duration. This means that you should not allow an item to be purchased if you would have to wait until after the duration of the simulation to have enough cookies. Further, after you have exited the loop, if there is time left, you should allow cookies to accumulate for the remainder of the time left. Note that you should allow the purchase of items at the final duration time. Also, if you have enough cookies, it is possible to purchase multiple items at the same time step. (Note that this differs from the actual Cookie Clicker game, where it is not possible to buy multiple items at the same time.) This is most likely to happen exactly at the final duration time, when a strategy might choose to buy as many items as it can, given that there is no more time left.
Finally, you should return the ClickerState
object that contains the state of the game.
If you have implemented things correctly, with the provided strategy_cursor_broke
n function, the given SIM_TIME
, and default BuildInfo
, the final state of the game should be:
Finally, you should implement some strategies to select items for you game. You are required to implement the following strategies:
strategy_cheap
: this strategy should always select the cheapest item that you can afford in the time left.strategy_expensive
: this strategy should always select the most expensive item you can afford in the time left.strategy_best
: this is the best strategy that you can come up with.If there is not enough time left for you to buy any more items (or your strategy chooses not to), your strategy function should return None
, otherwise your strategy functions should return a valid name of an item as a string. As described above, if your strategy function returns None
this should cause your simulate_clicker
function to exit the loop and finish the simulation.
For strategy_best
, you will be graded on how many total cookies you are able to earn with the default SIM_TIME
and BuildInfo
. To receive full credit, you must get at least \(1.30 \times 10^{18}\) total cookies. In addition, you may implement as many other strategies as you like. We will have a forum thread dedicated to showing off your favorite/best strategies!
"""
Cookie Clicker Simulator
"""
http://www.codeskulptor.org/#user39_CCxO5JObUb_2.py
import simpleplot
import math
# Used to increase the timeout, if necessary
import codeskulptor
codeskulptor.set_timeout(20)
import poc_clicker_provided as provided
# Constants
SIM_TIME = 10000000000.0
class ClickerState:
"""
Simple class to keep track of the game state.
"""
def __init__(self):
self._total_cookies = 0.0
self._current_cookies = 0.0
self._current_time = 0.0
self._current_cps = 1.0
self._history = [(0.0, None, 0.0, 0.0)]
def __str__(self):
"""
Return human readable state
"""
return " ".join(["\ntotal cookies:", str(self._total_cookies),
"\ncurrent cookies:", str(self._current_cookies),
"\ncurrent time:", str(self._current_time),
"\nCurrent CPS:", str(self._current_cps)])
def get_cookies(self):
"""
Return current number of cookies
(not total number of cookies)
Should return a float
"""
return self._current_cookies
def get_cps(self):
"""
Get current CPS
Should return a float
"""
return self._current_cps
def get_time(self):
"""
Get current time
Should return a float
"""
return self._current_time
def get_history(self):
"""
Return history list
History list should be a list of tuples of the form:
(time, item, cost of item, total cookies)
For example: [(0.0, None, 0.0, 0.0)]
Should return a copy of any internal data structures,
so that they will not be modified outside of the class.
"""
return self._history
def time_until(self, cookies):
"""
Return time until you have the given number of cookies
(could be 0.0 if you already have enough cookies)
Should return a float with no fractional part
"""
waittime = math.ceil((cookies - self._current_cookies) / self._current_cps)
if waittime > 0.0:
return waittime
else:
return 0.0
def wait(self, time):
"""
Wait for given amount of time and update state
Should do nothing if time <= 0.0
"""
if time > 0:
self._total_cookies += self._current_cps * time
self._current_cookies += self._current_cps * time
self._current_time += time
def buy_item(self, item_name, cost, additional_cps):
"""
Buy an item and update state
Should do nothing if you cannot afford the item
"""
if cost <= self._current_cookies:
self._history.append((self._current_time,
item_name,
cost,
self._total_cookies))
self._current_cookies -= cost
self._current_cps += additional_cps
def simulate_clicker(build_info, duration, strategy):
"""
Function to run a Cookie Clicker game for the given
duration with the given strategy. Returns a ClickerState
object corresponding to the final state of the game.
"""
build_info_clone = build_info.clone()
clickerstate = ClickerState()
while clickerstate.get_time() < duration:
choice = strategy(clickerstate.get_cookies(),
clickerstate.get_cps(),
clickerstate.get_history(),
duration - clickerstate.get_time(),
build_info_clone)
if choice == None:
clickerstate.wait(duration - clickerstate.get_time())
else:
if clickerstate.get_time() + clickerstate.time_until(build_info_clone.get_cost(choice)) <= duration:
clickerstate.wait(clickerstate.time_until(build_info_clone.get_cost(choice)))
clickerstate.buy_item(choice, build_info_clone.get_cost(choice), build_info_clone.get_cps(choice))
build_info_clone.update_item(choice)
else:
clickerstate.wait(duration - clickerstate.get_time())
while clickerstate.get_time() == duration:
choice = strategy(clickerstate.get_cookies(),
clickerstate.get_cps(),
clickerstate.get_history(),
0,
build_info_clone)
if choice == None:
break
else:
if build_info_clone.get_cost(choice) <= clickerstate.get_cookies():
clickerstate.buy_item(choice, build_info_clone.get_cost(choice), build_info_clone.get_cps(choice))
build_info_clone.update_item(choice)
else:
break
return clickerstate
def strategy_cursor_broken(cookies, cps, history, time_left, build_info):
"""
Always pick Cursor!
Note that this simplistic (and broken) strategy does not properly
check whether it can actually buy a Cursor in the time left. Your
simulate_clicker function must be able to deal with such broken
strategies. Further, your strategy functions must correctly check
if you can buy the item in the time left and return None if you
can't.
"""
return "Cursor"
def strategy_none(cookies, cps, history, time_left, build_info):
"""
Always return None
This is a pointless strategy that will never buy anything, but
that you can use to help debug your simulate_clicker function.
"""
return None
def strategy_cheap(cookies, cps, history, time_left, build_info):
"""
Always buy the cheapest item you can afford in the time left.
"""
pricelist = {}
for item in build_info.build_items():
pricelist[build_info.get_cost(item)] = item
if build_info.get_cost(pricelist[min(pricelist)]) <= cookies + cps * time_left:
return pricelist[min(pricelist)]
else:
return None
def strategy_expensive(cookies, cps, history, time_left, build_info):
"""
Always buy the most expensive item you can afford in the time left.
"""
pricelist = {}
fund = cookies + cps * time_left
for item in build_info.build_items():
if build_info.get_cost(item) <= fund:
pricelist[build_info.get_cost(item)] = item
if len(pricelist) > 0:
return pricelist[max(pricelist)]
else:
return None
def strategy_best(cookies, cps, history, time_left, build_info):
"""
The best strategy that you are able to implement.
"""
pricelist = {}
cpslist = {}
for item in build_info.build_items():
pricelist[build_info.get_cost(item)] = item
cpslist[build_info.get_cps(item) / build_info.get_cost(item)] = item
if build_info.get_cost(pricelist[min(pricelist)]) <= cookies + cps * time_left:
# in order to get as many cookies as possible, at the beginning 1.5% and
# at the end 30% of simulation time, choose the item with lowest price,
# other time choose the item with highest cps/cost
if time_left > SIM_TIME / 100 * 98.5 or time_left < SIM_TIME / 100 * 30:
return pricelist[min(pricelist)]
else:
return cpslist[max(cpslist)]
else:
return None
def run_strategy(strategy_name, time, strategy):
"""
Run a simulation for the given time with one strategy.
"""
state = simulate_clicker(provided.BuildInfo(), time, strategy)
print strategy_name, ":", state
# Plot total cookies over time
# Uncomment out the lines below to see a plot of total cookies vs. time
# Be sure to allow popups, if you do want to see it
# history = state.get_history()
# history = [(item[0], item[3]) for item in history]
# simpleplot.plot_lines(strategy_name, 1000, 400, 'Time', 'Total Cookies', [history], True)
def run():
"""
Run the simulator.
"""
run_strategy("Cursor", SIM_TIME, strategy_cursor_broken)
# Add calls to run_strategy to run additional strategies
run_strategy("Cheap", SIM_TIME, strategy_cheap)
run_strategy("Expensive", SIM_TIME, strategy_expensive)
run_strategy("Best", SIM_TIME, strategy_best)
run()