Work

NYTimes Bot

Typescript
Discord
Regex

A Discord bot to track and crown the best entries in common NYTimes Daily Games!

Iridescent ripples of a bright blue and pink liquid

Since Wordle first dropped on the seen during covid, I have been an avid daily player of NYTimes daily games. I have created a channel on my personal Discord where my friends and I can paste our daily scores to congratulate, talk-smack, and otherwise engage in a very small but passionate community of folks. When we first started this, we took it upon ourselves to “crown” a winner with an emoji every day, corresponding to the lowest score. And this was fine. But sometimes folks forgot to crown you when you had the best score, and the empty hollow feeling of a well-earned but unrecognized victory would be sure to ruin your day. So I decided we could solve this social problem with a bit of technology.

I created a Discord bot that would read all entries for the day and automatically crown a winner for the best score of a day, notifying leaders when their submission is in the lead, and notifying/removing crowns from those who had previous… ahem… inferior entries. I have since extended this to several other games that have shareable messages.

I host this bot in my homelab! More to come on that soon.

Technical Stuff

The Game Service

I created a standard GameService interface that allowed me to quickly add new games. Games needed an understanding of what they were called, what the emoji they reacted with would be (a 🪢 for strands, 🎲 for pips, etc.), and how to gauge an entry on a numerical scale that could be compared with other entries. In order to grade a message, I created a regex that would have capture groups for the important information from a day- what day was this completed on, what was the days “score” (number of tries for wordle, time for pips, etc.), and then any additional info from the message (i.e. did someone embarassingly solve the Wordle on easy mode instead of hard mode?). Once a message could be dated and scored from a regex, it was easy to write a GameRunner that would grab all of the relevant messages for a day and compare them to determine a winner.

// How do we respond to the user's message?
export interface GameReaction {
    replyMessage?: string;
    reaction?: string;
    privateMessage?: string;
}

// The grade of the user's message (+ an extra reaction if we determine we need to trash talk their easy Wordle... I am strongly opinionated on this)
export interface ParsedMessage {
    day: number;
    solvedIn: Nullable<number>;
    extraReaction?: GameReaction;
}

// And finally the GameService that could be implemented for various games. If the message wasn't a valid matching of the regex, parseMessage will return null
export interface GameService {
    name: string;
    emoji: string;

    parseMessage: (message: string) => Nullable<ParsedMessage>;

    getLeadMessage: () => string;
    getNoLongerLeadMessage: () => string;
}

I wrote comprehensive test suites for all of the different games to ensure that I covered all edge cases.

test('rejects unsolved', async () => {
  const msg = readFile(__dirname + '/testMessagesFail.txt').toString();
  const res = wordleService.parseMessage(msg);
  expect(res).toBe(null);
})

// Lots more tests not included for each game type

test('reads message solved in 5', async () => {
    const msg = readFile(__dirname + '/testMessages5.txt').toString();
    const { solvedIn, extraReaction } = wordleService.parseMessage(msg)!;
    expect(solvedIn).toBe(5);
    expect(extraReaction!.replyMessage).toBeDefined(); // Easy button
});

Discord Integration

I applied for a bot token on Discord, which was easy enough to get for a small bot. You have to request specific permissions, of which this bot needs very little: the ability to read and react to messages in channels, and send DMs. On channels that it is enabled, it listens for messages, and then determines if it is a NYT games submission before choosing whether to take any additional actions.

client.on('messageCreate', async message => {
    registeredGames.forEach(gameService => {
        const parsedMessage = gameService.parseMessage(message.content);
        if (parsedMessage === null) return;
        handleGame(message, gameService, parsedMessage);
    });
});

The handleGame method compares all of the tracked messages for the day, informing the winners and removing invalid reactions. All in all, this has been a very fun way to track and give validation to a lot of these daily games!

Picture of NYTimes bot profile tag