Over Engineering a Cookie: Part 1

A few months ago, my desk motherboard fried itself. So during the period while I was waiting for a new desktop, I decided to start a new project which had a lot of nostalgia for me.

Introducing: Cookie Clicker

If you haven’t had the privilige of being introduced to Cookie Clicker, let me summarise it quickly for you.

Have you ever wanted to own your own Cookie Empire?

Controlling the universes economy, while also enslaving all Grandma’s to bake these cookies for you?

Well, cookie clicker may be the game for you!

Released in 2013 by French Programmer “Orteil”, Cookie Clicker is an Idle Clicker game where you can click to collect cookies. With these cookies that you collect, you can upgrade your click to provide you more cookies. Eventually, you’ll collect enough cookies to hire a Grandma. Grandma’s will passively bake cookies for you, reducing the need to click. The cycle continues from there:

The more cookies you collect, the better the upgrades, the faster you collect cookies.

I played this game a lot when I was younger, idle games have always been an appeal to me and Cookie Clicker was one of the first in the genre. But why am I writing this blog? Well, while I only had my Macbook at my disposal, I thought this would be a great opportunity to play around with game automation and see how quickly I could 100% Cookie Clicker. It had been a while since I had used Python, so I thought this would be a great opportunity to brush up on those skills.

But before we get started, a couple of rules:

  1. As the title suggests: where possible, we’re going to over engineer this script,

  2. We can’t loose control of our mouse (I want to do other things while this is running),

  3. No hard-coded lists (make decisions dynamically),

  4. and we can’t do anything that normal players can’t do (e.g. spawn infinite cookies)


Clicking the Cookie

So, where do we start?

Considering the core of this game is built around clicking the cookie, I figured we’d start there. The more we click the cookie, the more cookies we have to spend, so ideally we’ll want to click this cookie as fast as possible.

To do this, I used the web app testing framework, Selenium. Selenium allows us to load a driver into Chrome to give us code developer access (essentially the JavaScript console).

https://www.selenium.dev/

The first goal was just to open the web browser, and navigate to the Cookie Clicker website.

from selenium import webdriver
import time

# Load the chrome driver and the load the webpage.
PATH = "chromedriver"
driver = webdriver.Chrome(PATH)
driver.get("https://orteil.dashnet.org/cookieclicker/")

# Wait for the webpage to load
time.sleep(5)

Selenium opening Cookie Clicker.

Now that we’ve opened the game, we need to be locate the cookie before we can click it. We can achieve that by opening up the game in Chrome, right clicking the cookie and inspecting it. Highlighting the cookie, we can see that it’s ID is “bigCookie”.

Initially I tried clicking the cookie through a feature called “Action Chains”.

Action chains essentially allow you to setup a list of actions that you want to perform, and then when you’re ready you can perform those actions.

Initially this worked, but it was way slower than I was hoping. We were managing about 2.5cps (cookies per second).

Playing around in the JavaScript console, I was able to find the “Click Cookie” function.

This function was called whenever you click on the cookie.

The Selenium web driver allows us to use “execute_script()” to execute raw JavaScript within the developers console. Combine that with the “setInternal()” function, and we can set an infinite loop to call the “Game.ClickCookie()”.

driver.execute_script("setInterval(function() {Game.ClickCookie; Game.lastClick = 0;}, 1);")

With this change, we were getting ~170 cps, which is a pretty huge increase.

But this still wasn’t the “Game Breaking” amount of clicks that I was hoping for.

To try and improve it, I threw the command in a for loop to execute it 100 times.

Using this loop, we were able to sky rocket our cps to over 5,250.

That’s it right?

We’ve clicked the cookie?

That’s all there is to do?

Not quite.. We still have to:

  • Save the game

  • Load the game

  • Golden Cookies

  • Buy buildings

  • Buy upgrades

  • Collect Achievements

  • Ascend and Resurrect

  • Manage the talent tree.

  • Manage seasons

  • Stop the GrandmaPocalypse

  • Kill the “wrinklers”.

  • Raise our dragon, and pet it regularly.


Saving/Loading the Game

For the most part, the concept of saving and loading the game is pretty straight forward. It essentially follows the same steps that you would normally follow.

Loading a save.

Exporting a save.

"""
Function to import existing save. 
"""
def ImportSave(driver):
    if not os.path.exists("CookieClicker.save"):
        return

    driver.execute_script("Game.ShowMenu();")
           
    with open("CookieClicker.save", "r") as save:
        driver.execute_script("Game.ImportSave();")
        savedata = save.readlines()
        save.close()

    driver.find_element_by_id("textareaPrompt").send_keys(savedata)
    driver.execute_script("Game.ImportSaveCode(l('textareaPrompt').value);")
    driver.execute_script("Game.ClosePrompt();")
    driver.execute_script("Game.ShowMenu();")

To load the game, our code needs the following steps:

  1. Check if there’s a save to load.

  2. Show the menu.

  3. Call “ImportSave()”

  4. Open the file and copy the save.

  5. Find the “TextAreaPrompt”.

  6. Type save into the “TextAreaPrompt”.

  7. Run “ImportSaveCode”.

  8. Close the prompt.

  9. Close the menu.

"""
Function to export data into a save file. 
"""
def ExportSave(driver):
    driver.execute_script("Game.ExportSave();")
    savedata = driver.find_element_by_id("textareaPrompt").text

    with open("CookieClicker.save", "w") as save:
        save.write(savedata)
        save.close()
    
    driver.execute_script("Game.ClosePrompt();")

To save the game, our code needs the following steps:

  1. Call ExportSave()

  2. Find the “TextAreaPrompt”

  3. Copy data within the “TextAreaPrompt”

  4. Write it to a file.

  5. Close the prompt.


Golden Cookies

Golden cookies are special cookies that randomly appear on the screen and provide a huge bonus when clicked.

Essentially we want to click them as quickly as possible.

Depending on your progress, there’s only a handle full of buffs that you can get.

Due to our clicks per second, Click Frezy sky rockets our progress.

The selection of Golden Cookie buffs we can get.

To solve this, I used a pretty simple solution and just repeated the Cookie Clicking loop. Rather than click the cookie, I search for whether a “shimmer” (Golden Cookie) is on screen and if so, I pop it.

"""
Function to click da Golden cookies.
"""
def ClickGoldenCookie(driver):
    driver.execute_script("setInterval(function() {for (var i in Game.shimmers) { Game.shimmers[i].pop(); }}, 1000);")

Funnily enough, there’s actually an achievement for clicking a Golden Cookie within a second of it spawning.

Due to the sheer increase in cps due to Click Frenzy, we’ll come back to Golden Cookies later when we discuss a way that we force spwawn them.


Buildings

Buildings provide passive cps, allowing you to still increase your cookie empire while you’re not clicking. (Not that it’s really a problem for us!)

There are 18 different buildings, with the progression scaling in a linear pattern. From hiring Grandma’s to help you bake your cookies to spawning idleverses for your idleverse, causing a very meta reference. This is additionally exciting for us, because our building cps will also increase our clicking cps exponentially.

I had a friend help me with the mathematical side of our logic, so I initally started with a pretty simple algorithm:

  1. Move to the bottom of the list,

  2. Click on each building once,

  3. Move up the list.

  4. Repeat

Usually the higher the tier, the better the building.

# Setup action chain
buyTheBuildings = ActionChains(driver)

for x in range(sys.maxsize):
    for i in range(17, -1, -1):
        # Get how many cookies we have and reset our action chains. 
        cookie = int(driver.find_element_by_id("cookies").text.split(" ")[0].replace(",", "")
        buyTheBuilding.reset_actions()

        # Find the building price, and convert it to an int. 
        element = driver.find_element_by_id(f"productPrice{i}")
        try:
            cost = int(element.text)
        except:
            # If we can't buy that building yet, continue.
            continue

        # If cost is less than cookies, buy the building
        if cost <= cookies:
            buyTheBuildings.move_to_element(element)
            buyTheBuildings.click()
            try:
                buyTheBuildings.perform()
            except:
                continue

  • There’s only 18 items, so we loop through bottom to top.

  • We get our current cookie count and store it as an int.

  • We can find the current price of the building, but providing it’s ID to productPrice()

  • If the value is null, that was because we couldn’t buy it yet.

  • If we have enough cookies to buy it, we do!

  • Sometimes that’ll cause an exception because another thread has already spent our cookies.

But I noticed pretty quickly that this was not an efficient way to do this.

  • It was long,

  • It was convoluted

  • It was buggy.

We don’t always receive ints!

At a million cookies, we get strings rather than ints.

Through the developer console, I was able to find the class structure for the buildings.

Looking through the class, there is a buy function which we can provide an amount for buildings we want to buy. If we try and buy something that we don’t have the cookies for, it just continues.

There is also an enabled field, that allows us to quickly check if we’ve unlocked that building.

Using these new findings, I quickly updated the function.

"""
Function for our thread to loop through available buildings,
and spend our hard earned cookies. 
"""
def BuyBuildingsThread(driver):
    for x in range(sys.maxsize):
        # Loop through buildings. 
        for i in range(17, -1, -1):
            # Find the object. 
            element = driver.find_element_by_id(f"product{i}")

            if "enabled" in element.get_attribute("class"):
                try:
                    # Buy as many as we can. 
                    driver.execute_script(f"Game.ObjectsById[{i}].buy(1000);")
                except:
                    pass

(Excuse the formatting errors).

The same concept as the previous version, but we simplified it a lot. Now we just check if we can buy the building, then we buy as many as possible.

On the right, you can see the script running through buying the buildings.

But it’s still not perfect, as there’s no buy order. We just loop through from bottom to top.

We could see that a building will increase its price by 15% each time that you buy a building.

Because of this, it made it quite easy to predict which building is the most efficient to buy.

This is where rule three comes in. It would have been quite easy for us to calculate the most efficient buy order for buildings, but in my mind that kind of takes the joy out of it. So, how do we do this dynamically?

We built a simple algorithm around finding the best return on investment when buying a buying. The basis of the algorithm is:

  1. Get the cost of each building,

  2. Get the cps of each building,

  3. Divide cps by the cost

  4. Higher the value, the better.

# Calculate the return in investment. 
def calculate_roi():

    roi = []
    for i in range[0, 18, 1]:
        # for each product, calculate cps / cost
        cost = (int(driver.execute_script(f"return Game.ObjectsById[{i}].price")))
        cps = (int(driver.execute_script(f"return Game.ObjectsById[{i}].storedCps")))
        roi.append(float(cps)/float(cost))
   
    return roi

My friend ended up updating the algorithm later on. I’ll be honest, I have no idea what this means, but apparently it’s right? ¯\_(ツ)_/¯


Wrapping up Part 1

In all honesty, we’re about half way through this script so far. But I realised this is already a bloody long blog post so I figured I’d leave it here for now, so I can get back to kernel development.

Next time, we’ll follow up the script with:

  • Buy upgrades

  • Collect Achievements

  • Ascend and Resurrect

  • Manage the talent tree.

  • Manage seasons

  • Stop the GrandmaPocalypse

  • Kill the “wrinklers”.

Previous
Previous

W3WProtect: Writing a minifilter

Next
Next

W3WProtect – A look into preventing IIS Exploitation