PoC-Project 5

Zifan Lin

Cookie Clicker

Overview

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.

Provided Code

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.

Phase One

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:

  1. The total number of cookies produced throughout the entire game (this should be initialized to 0.0).
  2. The current number of cookies you have (this should be initialized to 0.0).
  3. The current time (in seconds) of the game (this should be initialized to 0.0).
  4. The current CPS (this should be initialized to 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.

Phase Two

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:

  1. Check the current time and break out of the loop if the duration has been passed.
  2. Call the strategy function with the appropriate arguments to determine which item to purchase next. If the strategy function returns None, you should break out of the loop, as that means no more items will be purchased.
  3. Determine how much time must elapse until it is possible to purchase the item. If you would have to wait past the duration of the simulation to purchase the item, you should end the simulation.
  4. Wait until that time.
  5. Buy the item.
  6. Update the build information.

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_broken function, the given SIM_TIME, and default BuildInfo, the final state of the game should be:

  • Time: 10000000000.0
  • Current Cookies: 6965195661.5
  • CPS: 16.1
  • Total Cookies: 153308849166.0

Phase Three

Finally, you should implement some strategies to select items for you game. You are required to implement the following strategies:

  1. strategy_cheap: this strategy should always select the cheapest item that you can afford in the time left.
  2. strategy_expensive: this strategy should always select the most expensive item you can afford in the time left.
  3. 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!

Submission

Owltest page

"""
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()