It’s big brain time.

Getting a Perfect Score in Kahoot! With the Power of Node.js

Kevin Feng

--

Ever wonder how that one that classmate of yours keeps getting a perfect score in your class’s Kahoot! games? Well, it certainly isn’t because of a good work ethic, natural intellect, or time spent studying. They’re obviously cheating. Today, I’m going to show you how to fight fire with fire and attain a perfect score in Kahoot! every single time by creating a JavaScript bot. Let’s start by getting familiar with some of the tools that we’ll be utilizing in this project.

Source

Node.js

Node.js is a popular backend runtime environment for JavaScript that runs on the V8 engine and enables JS to run outside of a web browser. Node.js is open-source and cross-platform and is often used for developing backend API services or command-line interfaces (CLIs) — which is exactly what we’re going to be doing today.

Ordinarily, JavaScript cannot be run without a web browser, which is why you’ll often see programming tutorials demonstrating basic functionalities of the language in the console of Google Chrome, for example. However, Node.js transforms JavaScript into more of a “traditional” programming language — one that runs free of a browser and in your computer’s terminal instead.

Source

npm

If you are familiar with Python, then you have undoubtedly utilized pip, or the Package Installer for Python. pip is connected to an online repository of Python packages called the Python Package Index and enables programmers to install and manage software packages for the serpentine language.

npm is essentially the pip of Node.js. It is the default package manager for Node.js, connecting programmers to the npm registry, a massive online database of JavaScript packages. When you install Node.js, you’ll gain access to this powerful package manager.

Now let’s start creating our Kahoot! bot.

1. Scraping the correct answers to a quiz

The first function that we want to implement to our bot is the ability to get the correct answers to a Kahoot! quiz. There are limitations to this, as only public Kahoot! quizzes have their answers exposed via play.kahoot URLs. Here’s an example of a public quiz that has its answers openly available to web scraping:

https://play.kahoot.it/rest/kahoots/46b23442-df17-4e78-a685-92fcf98622f7

{"uuid":"46b23442-df17-4e78-a685-92fcf98622f7","language":"English","creator":"6f48929c-97a8-4047-8b1b-5d5c81fb1837","creator_username":"spongebob5656tes34","compatibilityLevel":6,"creator_primary_usage":"student","folderId":"6f48929c-97a8-4047-8b1b-5d5c81fb1837","visibility":1,"audience":"School","title":"public cokhoot","description":"","quizType":"quiz","cover":"https://media.kahoot.it/d9b9946b-9906-4450-a2f9-31ece659ecbe","coverMetadata":{"id":"d9b9946b-9906-4450-a2f9-31ece659ecbe","contentType":"image/jpeg","width":596,"height":333,"resources":""},"lobby_video":{"youtube":{"id":"","startTime":0.0,"endTime":0.0,"service":"youtube","fullUrl":""}},"questions":[{"type":"quiz","question":"but-cheec","time":20000,"points":true,"pointsMultiplier":1,"choices":[{"answer":"buit","correct":false},{"answer":"asdf","correct":false},{"answer":"penoisn","correct":true},{"answer":"gegock","correct":false}],"layout":"CLASSIC","resources":"","video":{"id":"16vjHU8BOgk","startTime":0.0,"endTime":20.0,"service":"youtube","fullUrl":"https://www.youtube.com/watch?v\u003d16vjHU8BOgk"},"questionFormat":1,"media":[]}],"metadata":{"access":{"groupRead":[],"folderGroupIds":[]},"duplicationProtection":false,"lastEdit":{"editorUserId":"6f48929c-97a8-4047-8b1b-5d5c81fb1837","editorUsername":"spongebob5656tes34","editTimestamp":1657294358719}},"resources":"","slug":"public-cokhoot","inventoryItemIds":[],"isValid":true,"type":"quiz","created":1657294358717,"modified":1657294358720}

I’ve also pasted the raw JSON content above, and if you take a quick glance at the middle, there are four lines that start with {"answer":, indicating that these are the four answer options to a question. Only one of these answer options, “penoisn” is correct, as indicated by "correct":true.

So what can we do with this information? Obviously, this information is quite easy to read through for us humans (although this becomes more difficult with larger JSON documents), but a computer needs to parse through the bits of information that matter: the correct answers.

Consider this Python script, which is a derivation of a short project by DaniellTa. gameid is the only input that this script requires. If the quiz is a public one, then this script parses through the JSON data of the quiz’s public data and returns a list of answers, based on the colors of the answer choices. I’ve modified this script to print out the answers, as we’ll have to pass this “Python” information to our program in Node.js.

The process of obtaining the gameid of a Kahoot! quiz is simple enough:

Also from DaniellTa’s GitHub

2. Building the CLI

The next step is to actually build the command-line interface using Node.js. You’ll want to install some packages using npm as such:

npm i puppeteer puppeteer-extra puppeteer-extra-plugin-stealth kahoot.js-updated@2.4.0 prompt-sync

Puppeteer is a headless Chromium tool that allows programmers to automate browser actions; we’ll be using this package to enter bots into Kahoot! quizzes and answer questions perfectly as fast as possible.

Kahoot.js-updated is a deprecated package from the npm registry, meaning it is no longer maintained. This doesn’t mean that the package won’t work, but any existing error/problems with it are not actively being fixed. We can optionally use this package to spam bots into Kahoot! quizzes and answer questions randomly to draw attention away from the Puppeteer bot.

Lastly, prompt-sync will allow us to get input from the user — the fundamental function of any CLI.

Here’s an example of an initial framework for this project:

const prompt = require("prompt-sync")({ sigint: true });
console.log('----------------------------------------');
console.log('Welcome to Kahoot Monkey!');
console.log('----------------------------------------');
const quizId = prompt('Enter the quiz ID of the Kahoot: ');
const gamePIN = prompt('Enter the game PIN of the Kahoot: ');
const realName = prompt('Enter your nickname: ');

The first line calls on the prompt-sync package so that we can prompt the user for input whenever necessary; we assign this to a variable called prompt. Following that are just a few console.logs lines that introduce the program to the user. After that come the actual prompts themselves. These will prompt the user for input one-by-one (not all at once).

Congratulations, you’ve just built a CLI! Although it technically doesn’t do anything right now… Let’s change that by adding a bot that perfectly answers every question as fast as possible.

3. Implementing mainBot

I’ve dubbed this bot “mainBot,” as it is the primary bot of the application and will allow your own name to appear under a perfect score. This bot will answer every single question within 0.1s (time to answer is a metric that Kahoot! actually tracks), meaning it will lose absolutely 0 points to time.

To do this, we’re going to use the Puppeteer package, correlating the correct answers that we’ve scraped from step 1 to the answer options that the bot encounters during any Kahoot! quiz.

First, let’s format the answers from the Python script that we put together earlier:

Now I know this looks like a lot of code, but it’s formatted quite vertically due to the switch-case. Additionally, the last two chunks of code are just to catch standard errors; they’re not necessary. The first few lines are also just essentially import statements, allowing us to create a Puppeteer object that is harder to detect (stealth plugin).

const spawn = require("child_process").spawn;
const pythonProcess = spawn('python',["./kahootparse.py", quizId]);

With the above two lines of code, we get the data scraped from the Python script (notice how we’re feeding the quiz ID that the user inputs to the parsing script).

The rest of this code is essentially the switch statement; it only looks like a lot of code because of how many lines it takes up. This bit of logic is actually much more straightforward than it may initially seem.

From lines 12 to 31, we are simply looping through every single character that we’ve gotten from the Python script, and by default, the character is pushed to the curr object. However, if the character signifies the beginning of an array (opening bracket), the end of an array (closing bracket), a new item (comma and/or space), then we have to handle it differently. In the case that the array ends, then we push the current string to the answers array, and reset curr to an empty string. We also have to break from the switch statement. The same logic applies when we encounter an empty character, or a space — the current string is pushed to the answers array, curr is reset to an empty string, and we break. In all other cases (besides the default), we immediately break, as there is nothing to add to the current string.

After those lines of code, we can have the program print out a little cheatsheet for the user, which just indicates the correct answers for each question, based on color. We also remind the user that red and blue are swapped for true/false questions in Kahoot!.

So now that we have an array with all the correct answers, we need to have the bot perform browser actions to input all of these answers properly.

Here is the asynchronous function for mainBot, which is arguably more straightforward than the last chunk of code — it’s just browser automation that waits on webpage elements appearing so it can then do mouse/keyboard inputs.

In the first four lines of the function, we’re just setting up the bot. We create a Puppeteer instance, have it open a new browser page (although we won’t see anything because Puppeteer is headless by default), make sure that it doesn’t time out, and send it to the URL passed in the function’s header. All of the await statements ensure that the program is asynchronously executing these lines of code. As we’ll see later on, this will become particularly important when inputting answers to questions (the bot can’t click on an HTML element if it doesn’t exist yet).

const browser = await puppeteer.launch(); // {headless: false}    const page = await browser.newPage();
await page.setDefaultTimeout(0);
await page.goto(url);

Here’s how calling the mainBot function would look like:

mainBot('https://kahoot.it/');

In the next section, we want the bot to actually join the game, which also only takes a few lines of code.

await page.focus('input#game-input');
await page.keyboard.type(gamePIN);
await page.keyboard.press('Enter'); page.waitForSelector('input#nickname').then(async function(){
await page.focus('input#nickname'); await page.keyboard.type(realName);
await page.keyboard.press('Enter'); });
console.log('mainBot successfully joined game');

We use a lot more await statements here because now the bot is actually providing input to the browser. First, we focus on an HTML element with the ID “game-input.” When focusing on elements with Puppeteer, we pass a string formatted as such: [HTML-element-type][HTML-element-identifier]. Here, the text box that the player uses to enter the game PIN is of type “input,” and when checking out its other HTML attributes, it actually has a unique ID of “game-input.” IDs always make everything easier during web scraping/browser automation, as they are, as mentioned earlier, unique. Thus, calling on an HTML element’s ID will always be successful, whereas HTML classes can be applied to multiple elements on the page.

Well, how do we figure out these details about the site’s HTML structure. All you have to do is perform the infamous “inspect element,” by right-clicking on the element of interest, selecting “inspect element,” and taking a look at the page’s structure. Here’s what it looks like:

Notice how the HTML element is of type “input,” as specified by the opening HTML bracket follows by “input,” and that the element’s unique ID is listed as “game-input.”

Once we’ve focused on the element, we tell the bot to type the game PIN (provided by the user) and then hit the <ENTER> key. At this point, the bot is not yet in the game, but needs to input their nickname to get in the lobby. The page will look like this:

If we look inside the asynchronous wrapper function, we see three lines of code that are very similar to what was just executed. Here, we’re just performing the same thing again, just one function deeper, as the bot needs to wait for the nickname input box to show up. Once the bot hits <ENTER> after entering the name provided by the user, we can log a message to the console to indicate that the bot successfully joined the game.

Now all we need to do is make the bot answer everything correctly as fast as possible. This part of the code is the most complex, and although I’ll explain it, you’ll definitely have to play around with it to see how everything works when it comes together.

The bot’s answering behavior is entirely wrapped in a for-loop, as we need the bot to iterate through the answers array that we created earlier. For each item, the bot awaits for an XPath, which is just another tool we can use to navigate through a page’s structure. Here’s the exact XPath that we want the bot to wait for:

//*[@id="root"]/div[1]/main/div[2]/div/div/button[1]

As denoted by the very end of this XPath, we’re waiting for a button — one of the answer choice buttons to be precise. Using XPaths can be powerful because like HTML IDs, they are also unique. To find an XPath, you’ll want to perform an “inspect element,” find the element of interest, right-click on it, hover over the “copy” option, and copy the element’s XPath.

So as soon as the bot detects button[1], or the first answer choice button to a question, it executes an asynchronous function to answer it properly. For each question, we actually have to determine whether or not the question is a regular multiple choice with four options, or if it’s a true or false. We have to check this because the XPaths are actually inverted depending on the question type. In other words, “true” does not correspond to button[1] as you would expect. Instead, button[2] corresponds to answering “true” and button[1] corresponds to answering “false.” Since this hasn’t been adjusted when we created the answers array, we just have to perform a quick check right now. To do this, we find out the question’s type with four lines of code:

let option = '';
const [el] = await page.$x('//*[@id="root"]/div[1]/main/div[1]/div/div[2]/div/div/span'); const txt = await el.getProperty('textContent');
const questionType = await txt.jsonValue();

We use another XPath here; in this case, it’s the header at the top of the page that indicates what type of question it is. We retrieve the text content of the element and store the JSON data of that property into a variable called questionType. Now we’re ready to answer questions perfectly in under 0.1s each. The code also gets pretty straightforward from here on out.

Using some basic control flow, we have two switch statements, one for if the question type is “Quiz,” meaning it’s multiple choice, and one for if the question type is “True or false,” which is self-explanatory. Depending on the color of the current iterated item in the answers array, we set the variable option to the correct XPath that we want the bot to click on. The default console logs that I’ve included should never be encountered, even if the quiz includes a third question type that is not “Quiz” or “True or false.” The switch statements only occur under these two question types after all.

Lastly, after correctly determining the XPath of the correct answer, the bot awaits for the element and promptly clicks on it when it does appear.

4. Do whatever you want!

And that’s pretty much it! If you want to check out the full CLI, take a look at this GitHub link; I’ve also included a few more bot types for you to explore. There’s even an unfinished “humanBot” that should emulate a real player by occasionally getting questions wrong or answering a bit slowly. If you want to clone the repo and make your own modifications, please feel free to do so. I would definitely recommend implementing the diversion bots using the deprecated Kahoot! package from the npm registry. Have fun!

--

--