Extra Credit: Displaying the top score in a popular word game

The New York Times Company shocked the casual gaming world in January 2022 with its surprise purchase of Wordle, the trendy daily word puzzle that had recently taken the internet by storm, for an undisclosed but reportedly quite large sum of money.

While the deal—for a barely three-month-old piece of intellectual property—undoubtedly turned plenty of heads, Wordle cannot be deemed the most buzz-worthy word game in the New York Times lineup. That title, naturally, will forever belong to Spelling Bee.

A quick run-down of the rules for the uninitiated: Each day, the game features a new set of seven unique letters arranged in a honeycomb-like grid. Players must use these letters—and only these letters—to create as many words as possible, earning points based on their length. Every word has to be at least four letters long and must include the central, highlighted letter at least once. Other than that, individual letters may be used as many (or as few) times as needed.

One other interesting quirk: The solution to each Spelling Bee puzzle includes at least one word that incorporates all seven letters. These bonus words, known as “pangrams,” earn the player extra points. This is significant because the game assigns its players a series of ranks each day based on the number of points they have earned, culminating in the coveted status of “Genius.”

The Problem

Assuming the posts on this Reddit board are indicative of the typical Spelling Bee enthusiast, most players are happy to achieve Genius and move on. However, there exists one more, hidden rank reserved for the intrepid few who manage to find every word on a given day: Queen Bee.

These days, my fiancée and I play the Bee together most mornings during breakfast, trying to earn as many points as we can before heading off to work. (We’re just so wholesome!) Because we really enjoy word games and are almost guaranteed to reach Genius if we work together, our goal (though we rarely achieve it) is always Queen Bee.

The thing about pushing for Queen Bee, however, is knowing when to quit. Although the Times does not officially tell you this, we figured out a long time ago that the daily threshold for Genius is always 70% of the highest possible score.

So we’ll usually do the math at some point and then use that number as either a motivator or a reason to cut our losses before the game stops being fun anymore.

The math, of course, is easy. We just open the calculator app on one of our phones, put in the value for Genius, and divide it by 0.7. But given that this is a well-defined task with well-defined inputs that we end up having to do every single time we play, doesn’t this sound like yet another perfect opportunity for some automation?

The Objective

I understand why The New York Times does not openly publish the number of points necessary to reach Queen Bee on any given puzzle. That would make perfection seem like the goal of the game—a goal that is too hard to reach on a daily basis to keep the game fun and casual.

But because of the way my partner and I play, it would be rather useful to have this number available to us at a glance. (This is especially true because we’re always forgetting the max score and having to find it again.)

My goal for a Spelling Bee automation project would therefore be to devise a way to have the Queen Bee score injected directly into the game’s user interface without our ever having to leave the browser—and ideally without any user intervention required.

The Solution

Arbitrarily modifying the appearance of a relatively static webpage is not, in principle, very hard to accomplish.

In fact, thanks to the demise of proprietary plugins like Adobe Flash, even the sleekest looking pages are typically rendered using plain text for, well, text; HTML tags for structure; and human-readable CSS rules for appearance/presentation.

And that means there’s very little that website owners can do to stop you from altering (on your own computer) a page’s information, design, or layout.

Things get a little harder if the page uses certain components of the HTML5 standard—particularly those designed for fancy graphics, multimedia, or interactive content—that are usually controlled via JavaScript. But otherwise, go to your favorite website, open up your browser’s developer panel, and you can tweak the text, HTML, and CSS to your heart’s content!

Spelling Bee does use plenty of JavaScript code behind the scenes to do things like configure the game board or judge whether a guessed word is a valid answer. However, it turns out that a lot of the basic layout—including the scoring legend—is still handled via plain old, easily manipulatable HTML and CSS.

That’s great, but the only way to actually do this manipulation (without having to hand-edit the source code every single time) would be to write some JavaScript of my own.

With that in mind, to achieve the goals of this project, I would ultimately have to do the following:

  • Figure out where Spelling Bee stores its scoring information.
  • Write some JavaScript to either (a) grab the Queen Bee score from wherever it might be hiding or (b) grab the Genius score and do the math to convert it to Queen Bee.
  • Write some more JavaScript to format the Queen Bee score to look like all the other ranks in the scoring legend and then insert it into the list.
  • Figure out an easy/reliable way to execute all this JavaScript—preferably without user intervention.

The Outcome

The actual solution was pretty simple—around 70 lines of code, including comments and extra spaces for readability. However, there were a few interesting challenges.

For starters, even though I was able to figure out where Spelling Bee stores its answer key (something I definitely did not want to know 🫣), I could not find the maximum score encoded anywhere. So I would have to resort to having my code grab the Genius score and do the math. No biggie.

The second challenge, however, was that the scoring legend appears to be built dynamically whenever the user requests it. In other words, when the page is first loaded, neither the legend nor its data is anywhere to be found. And when the legend is dismissed, rather than simply making the box invisible, the underlying web app completely removes it from the page. This meant I would have to come up with a way to grab the Genius score, calculate Queen Bee, and then insert that new score each time the user opens up the legend—since then and only then is the content available for manipulation.

After doing some research, I learned that most browsers released in the past 10 years support a JavaScript feature called MutationObserver, which can trigger a block of code as soon as something about a particular element in the page structure changes. One such change is the addition of a new “child” to a container. In the case of Spelling Bee, I just had to configure a MutationObserver to watch a specific <div> element and call a function containing my code as soon as the legend appeared inside it.

The final problem to solve was the question of how to actually run this code. My first idea was something called a bookmarklet, which is essentially a bunch of JavaScript smushed into a hyperlink that you can keep in your browser’s toolbar or Bookmarks menu.

When you click or tap on a bookmarklet, the browser executes the code in the context of the current webpage. This solution worked, but it came with a major downside: I would have to manually run the bookmarklet every single time I opened the Spelling Bee—and then re-run it if the browser decided to refresh the page.

What I really needed was a way to force some code to run every time a page loads, as soon as it loads. It eventually occurred to me that this is exactly one of the problems that browser extensions are intended to solve. These days, extensions are created using HTML, CSS, and JavaScript just like websites. Google created a model API for use in its Chrome browser; Mozilla, Microsoft, and Apple all eventually followed with the same or similar implementations.

I had never tried this before, but since I already had some working JavaScript, I figured it couldn’t be too much more trouble to bundle it into an extension. So I read some of Google’s documentation and then, since I ultimately wanted this to work on my iPhone, I followed Apple’s instructions for creating a Safari Web Extension.

Lo and behold, after installing my newly minted extension and giving it permission to access The New York Times, I finally had a solution that met all of my objectives.

Now, whenever I am playing the Spelling Bee, I can open up the scoring legend and see what it will take to reach Queen Bee right at the top of the list. And in order to preserve some of the magic of the unadulterated game, my extension can even be configured to hide the maximum score until after genius has been achieved!

The Takeaway

Here’s the JavaScript for performing the necessary page manipulation. (Note: The code, as written below, will always show the Queen Bee score once it has been executed.)

// ==UserScript==
// @name        Bee Royal
// @match       *://www.nytimes.com/puzzles/spelling-bee
// @match       *://www.nytimes.com/puzzles/spelling-bee/*
// @grant       none
// @version     1.0
// @author      teacherdan
// @description Adds the number of points needed to reach \"Queen Bee\" status to the scoring legend when playing the Spelling Bee game from the New York Times.
// ==/UserScript==

// Function to add Queen Bee (e.g. total possible points) to the scoring legend
const addQueen = () => {
    // Grab the Genius score from the legend and use it to calculate Queen Bee
    const genius = document.querySelectorAll(".sb-modal-frame.ranks .sb-modal-ranks__rank-points")[1].innerText;
    const queen = Math.round(genius / 0.7);
    
    // Grab the current score from the legend and use it to calculate points-to-go
    const current = document.querySelectorAll(".sb-modal-frame.ranks .current-score")[0].innerText;
    const diff = queen - current;
    
    // Create a new table row containing the Queen Bee value
    let queenRow = document.createElement('tr');
    queenRow.innerHTML = '<td class="sb-modal-ranks__rank-marker"><div class="square"></div></td><td class="sb-modal-ranks__rank-title"><span class="current-rank">Queen Bee</span></td><td class="sb-modal-ranks__spacer hr"></td><td class="sb-modal-ranks__rank-points">' + queen + '</td>';
    
    // Insert the new row at the top of the scoring legend
    let scoreList = document.querySelector(".sb-modal-frame.ranks tbody");
    scoreList.insertBefore(queenRow, scoreList.firstChild);
    
    // Update Genius subtext
    if (playerIsGenius()) {
        let subtext = document.querySelector(".sb-modal-frame.ranks .sub-text");
        subtext.innerText = diff + " points to go!";
        subtext.style.display = "block";
    }
}

// Checks whether current player is a genius
const playerIsGenius = () => {
    return document.querySelector(".current-rank").innerText == "Genius";
}

// MutationObserver callback function that will be executed whenever
// a new modal dialog (such as scoring legend) has been added to the UI
const newModalCallback = (mutationList, observer) => {
    
    // Loop through all the mutations we've received
    for (const mutation of mutationList) {
        
        // Only respond to child list mutations
        if (mutation.type === "childList") {
    
            // Do the manipulation!
            addQueen();
        }
    }
};

// Create a MutationObserver linked to the callback function
const modalObserver = new MutationObserver(newModalCallback);

// Find the DIV used by the game for displaying modal dialogs (including the scoring legend)
// and observe it for changes to its child list (e.g. dialogs appearing/disappearing)
const modalWrapper = document.querySelector(".sb-modal-wrapper");
modalObserver.observe(modalWrapper, {
    childList: true
});

And here’s that same code as a bookmarklet you can drag to your toolbar or add to your Bookmarks menu: Bee Royal. (If you’d prefer, for security reasons, to create your own bookmarklet, you can paste my code into a generator. This is the one that I used.)

Now, I wish I could share my final product—the automatically activating, configurable extension. Unfortunately, Safari extensions can only be installed from Apple’s App Store, and this is just a hobby project that I’m not prepared to officially support. (Although there are workarounds, the other major browsers have similar restrictions these days.)

There is, however, one other option. Instead of using a bookmarklet, you could install something called a user script manager, which is basically an extension that allow you to automatically run arbitrary, third-party code, either on specific webpages (what we need here) or even potentially all pages. Greasy Fork links to a few options you might consider. I can’t vouch for any of these extensions’ safety, privacy, or security practices, but I did confirm that they are able to run my JavaScript solution.

Instructions for using a script manager are beyond the scope of this article, but you are welcome to copy and paste my code into your chosen extension. I’ve even already included the necessary metadata to make sure things go as smoothly as possible!

Wrapping Up

As always, thank you for following along on this journey toward orthographic royalty. I look forward to additional automation and/or scripting projects in the future!