Today me and my girlfriend were a little bored having just came back from holiday and both decided to play a little bit of Cookie Clicker, seeing who could produce the most cookies in a short period of time. Naturally, I’m a programmer and I immediately thought of a dozen ways I could exploit it by creating a bot to do the clicking for me.
Needless to say, I won our little competition in dramatic fashion. This is what I did.
Why node.js?
For something as trivial as an auto clicker I’d naturally use something like tampermonkey to manage the script. And indeed there are a number of community made scripts for cookie clicker already available.
But I decided I wanted to try and make the most out of this challenge, and try something new.
Enter puppeteer.
Puppeteer
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
The TL;DR being that Puppeteer allows us to interact with the browser from our node app. This means we can leverage the endless, ever expanding world of npm. Anything we might need, somebody, somewhere has already coded it and packaged it up nicely for us.
Connect Puppeteer to an existing browser
See I’ve already started manually clicking, I need some way for Puppeteer to latch onto my existing session, I don’t want to lose my current progression.
In order to do this I need to connect Puppeteer to my browser using a web socket connection. To enable this you need to run chrome with remote debugging enabled, I already had this set up, but for those playing along at home you need to do the following:
- Right click your google chrome shortcut and go to properties
- In the target field you need to add
--remote-debugging-port=9222
at the very end - Restart chrome
Each time you start chrome it creates a new websocket debug url, so you need to go to http://127.0.0.1:9222/json/version
to grab that, mine looks like the following:
{
"Browser": "Chrome/95.0.4638.69",
"Protocol-Version": "1.3",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36",
"V8-Version": "9.5.172.25",
"WebKit-Version": "537.36 (@6a1600eda2fedecd573b6c2b90a22fe6392a410)",
"webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/0aeb09d9-741c-47dd-85ab-db195257b6e8"
}
Note, if this page isn’t showing you may need to run chrome as an administrator.
Then make the connection in code:
const puppeteer = require('puppeteer');
(async () => {
const wsChromeEndpointUrl = 'ws://127.0.0.1:9222/devtools/browser/0aeb09d9-741c-47bd-85ab-bb195257b6e8';
const browser = await puppeteer.connect({
browserWSEndpoint: wsChromeEndpointUrl
});
})();
Now that we’re connected to the browser using the web socket, let’s connect to our existing cookie clicker page:
const puppeteer = require('puppeteer');
(async () => {
const wsChromeEndpointUrl = 'ws://127.0.0.1:9222/devtools/browser/0aeb09d9-741c-47bd-85ab-db195257b6e8';
const browser = await puppeteer.connect({
browserWSEndpoint: wsChromeEndpointUrl
});
const pages = await browser.pages();
let page = await openCookieClicker(pages, browser);
page.setViewport({width: 1920, height: 1080});
})();
async function openCookieClicker(pages, browser) {
for (let page of pages) {
if (page.url() === 'https://orteil.dashnet.org/cookieclicker/') {
return page;
}
}
let page = await browser.newPage();
await page.goto('https://orteil.dashnet.org/cookieclicker');
return page;
}
Here I’m checking to see if an existing cookie clicker page is open, if so then I return that page else I open a new one. I also set the page viewport to the same size as my screen, I at least want the illusion I’m playing the game.
Autoclicking
The next step is to create a bot to do the clicking for me. Let’s add some code to do that:
function startAutoClicker(page) {
botLoop = setInterval(async () => {
await page.click('#bigCookie');
}, 10)
}
And call that in our main method:
const pages = await browser.pages();
let page = await openCookieClicker(pages, browser);
page.setViewport({width: 1920, height: 1080});
startAutoClicker(page); // add this
And we should get something that looks like this:

Hiding my cheating
My girlfriend mustn’t know I’m cheating so I need some way to pause the bot. Pressing any key should pause the bot but pressing spacebar should resume it.
To do this I’m going to need to make use of the Puppeteer function exposeFunction
:
The method adds a function called name on the page’s window object. When called, the function executes puppeteerFunction in node.js and returns a Promise which resolves to the return value of puppeteerFunction.
https://pptr.dev/#?product=Puppeteer&version=v11.0.0&show=api-pageexposefunctionname-puppeteerfunction
So we need to expose a function that gets called when the user hits a key:
await page.exposeFunction('onUserPressedKey', e => {
if (e && e.keyUserPressed) {
clearInterval(botLoop);
if (e.keyUserPressed === ' ') {
startAutoClicker(page);
}
}
});
So when the user presses a key, we immediately delete the botLoop
we created above, I could do a check to see if the value was a spacebar
beforehand but that would lead to issues with having multiple event listeners, and clearing those down is a bit of a pain.
So now we’ve exposed our function, we need to call it when the user enters a key, to do that we need to make use of the Puppeteer function evaluate
. This just allows us to easily enter a JavaScript function that’ll get executed in the browser, such:
await page.evaluate(() => alert('Alert box message'))
Would do exactly what you’d imagine.
So let’s add an event listener that listens for the keypress
event and then executes our function we’ve just added using the exposeFunction
:
await page.evaluate(() => {
window.addEventListener('keypress', e => {
window.onUserPressedKey({type: 'keypress', keyUserPressed: e.key});
});
});
Now we can pause!
Completely automating it
Okay, I’ve won our little competition, but it’s not enough. I still have to manually go in and click the stuff in the store I want to upgrade. I need my bot to buy for me, whether it purchases Grandma, a Farm, a Factory or a bank, the decision should be the bots.
Luckily when inspecting the elements we can see a distinct difference between the items in the shop we can and can’t afford, when we can afford them they’re appended with the .enabled
class, let’s use this in our code:
function startAutoBuyer(page) {
buyLoop = setInterval(async () => {
let products = await page.$$('.product.unlocked.enabled');
products[products.length - 1].click();
}, 15000)
}
Every 15 seconds we’re grabbing all the items we can buy and then choosing the last, most expensive one.
And now, we have a pretty decent bot, and one very sad girlfriend.
[…] I hope you enjoyed this blog, I have plans to do more on Tampermonkey as friends from the newsletter seemed intrigued when I mentioned it in my previous blog. […]