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:
As the title suggests: where possible, we’re going to over engineer this script,
We can’t loose control of our mouse (I want to do other things while this is running),
No hard-coded lists (make decisions dynamically),
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).
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)
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).
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.
""" 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:
Check if there’s a save to load.
Show the menu.
Call “ImportSave()”
Open the file and copy the save.
Find the “TextAreaPrompt”.
Type save into the “TextAreaPrompt”.
Run “ImportSaveCode”.
Close the prompt.
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:
Call ExportSave()
Find the “TextAreaPrompt”
Copy data within the “TextAreaPrompt”
Write it to a file.
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.
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:
Move to the bottom of the list,
Click on each building once,
Move up the list.
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.
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:
Get the cost of each building,
Get the cps of each building,
Divide cps by the cost
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”.