npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wynham-games

v1.0.0-beta.15

Published

Create and manage interactive mini-games (Connect Four, Tic-Tac-Toe, Rock Paper Scissors, Hangman) inside your Discord bot, built on top of Discord.js.

Readme

wynham-games

Mini-games for Discord bots, built on discord.js. You get Connect Four, Tic-Tac-Toe, Rock Paper Scissors, Hangman and Snake, each started with one call.

The library handles the boring parts for you: the invite your opponent accepts or declines, turn timers, anti-spam cooldowns, a Tic-Tac-Toe bot with four difficulty levels, and translations in English, French, Spanish and Italian.

Requirements

  • Node.js 22 or later (22, 24 and 26 are the versions Node.js still maintains)
  • discord.js ^14.23.0, installed in your bot (it is a peer dependency)

The boards use Components V2 and the modal LabelBuilder. Older discord.js versions don't ship them, so 14.23.0 is the minimum.

Installation

npm install wynham-games@beta discord.js

Quick start

Create one GameManager next to your client, then call createGame from your slash commands.

import { Client, GatewayIntentBits } from "discord.js";
import { GameManager, GameNameType } from "wynham-games";

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const games = new GameManager(client);

client.on("interactionCreate", async interaction => {
    if (!interaction.isChatInputCommand()) return;

    if (interaction.commandName === "connect4") {
        await games.createGame(GameNameType.Connect4, interaction, {
            players: [interaction.user, interaction.options.getUser("opponent", true)],
        });
    }
});

client.login(process.env.TOKEN);

createGame defers the reply if you haven't, sends the invite, waits for the answer and runs the game until it ends.

GameManager

new GameManager(client: Client, options?: GameManagerOptions)

| Option | Type | Default | Description | | ----------------- | --------- | ------------------ | ----------------------------------------------------------- | | antiSpam.enable | boolean | true | Blocks the same players from starting games back to back. | | antiSpam.time | number | 5000 | Anti-spam cooldown, in milliseconds. | | locale | Locale | Locale.EnglishUS | Language of every game. A game's own locale overrides it. | | colors.play | number | 0x5865f2 | Board color while a game runs. | | colors.win | number | 0xffd700 | Board color when someone wins. | | colors.end | number | 0x808080 | Board color on a draw, a loss or a timeout. |

The colors apply to every game and menu of the manager. A game's theme.colors still wins over them, so you can set your brand colors once and tweak a single game when you need to.

import { Locale } from "discord.js";

const games = new GameManager(client, {
    antiSpam: { time: 10_000 },
    locale: Locale.French,
    colors: { play: 0x2b2d31, win: 0x57f287 },
});

createGame(gameName, interaction, options)

createGame<G extends GameNameType>(gameName: G, interaction: Interaction, options: GameOptionsMap[G]): Promise<void>
  • gameName: a GameNameType value.
  • interaction: a repliable interaction, usually a slash command.
  • options: the options of that game. TypeScript picks the right type from gameName, so passing players to Snake or lifeCount to Connect Four fails to compile.

Who can play against whom

  • Connect Four and Hangman need two humans. The second player gets an Accept / Decline invite.
  • Rock Paper Scissors accepts a bot as player 2. The game starts at once and the bot picks at random.
  • Tic-Tac-Toe accepts a bot as player 2. The challenger picks a difficulty first, unless you set difficulty.
  • Snake is single-player. It takes a player instead of players and starts without an invite.

A player can't challenge themselves, and the challenger can't be a bot.

Timers

Each game ends after turnTimeout seconds without a move. Tic-Tac-Toe, Hangman, Rock Paper Scissors and Snake show a live countdown under the board ("The game ends in 25 seconds if nobody plays."). In Connect Four, a missed turn passes to the other player, and the game is cancelled once both players miss in a row.

Connect Four and Hangman mention the player whose turn it is, so the message lights up for them. Discord doesn't send a notification for a mention added by an edit, though.

Game options

Shared options

| Option | Type | Default | Description | | ------------------ | -------------- | --------------- | ------------------------------------------------------------ | | players | [User, User] | required | Challenger first, opponent second. | | turnTimeout | number | 30 | Seconds without a move before the game ends, from 15 to 360. | | locale | Locale | manager locale | Language of this game only. | | theme.colors | object | manager colors | Board colors for this game only. | | lang.title | string | game name | Title on the board. | | lang.inviteTitle | string | "… Challenge" | Title of the invite. |

Connect Four: Connect4Options

| Option | Type | Default | Description | | ---------------------- | ------------- | --------- | ------------------------------------------------- | | theme.emojis.player1 | string | 🔴 | Player 1 pieces. | | theme.emojis.player2 | string | 🟡 | Player 2 pieces. | | theme.emojis.empty | string | ⬛ | Empty cells. | | theme.emojis.win | string | ⚪ | Pieces of the winning line. | | theme.inputType | InputType | Buttons | Pick a column with buttons or with a select menu. | | theme.buttonStyle | ButtonStyle | Primary | Primary, Secondary, Success or Danger. |

import { GameNameType, InputType } from "wynham-games";
import { ButtonStyle } from "discord.js";

await games.createGame(GameNameType.Connect4, interaction, {
    players: [interaction.user, opponent],
    turnTimeout: 45,
    theme: {
        emojis: { player1: "🔵", player2: "🔴" },
        inputType: InputType.SelectMenu,
        buttonStyle: ButtonStyle.Success,
    },
});

Tic-Tac-Toe: TicTacToeOptions

| Option | Type | Default | Description | | ---------------------- | --------------- | ------- | ------------------------------------------ | | difficulty | BotDifficulty | menu | Bot strength, used when player 2 is a bot. | | theme.emojis.player1 | string | ❌ | Player 1 marks. | | theme.emojis.player2 | string | ⭕ | Player 2 marks. | | theme.emojis.empty | string | ⬜ | Empty cells. |

| Difficulty | Bot behaviour | | ------------ | ---------------------------------------------- | | Easy | Random moves. | | Normal | Best move half the time, random otherwise. | | Hard | Best move 80% of the time. | | Impossible | Best move every turn (minimax). You can't win. |

Leave difficulty out and the challenger picks it from a menu before the game. Set it and the game starts straight away.

Rock Paper Scissors: RPSOptions

| Option | Type | Default | | ----------------------- | -------- | ------- | | theme.emojis.rock | string | ✊ | | theme.emojis.paper | string | 🤚 | | theme.emojis.scissors | string | ✌️ |

If only one player picks before the timer runs out, that player wins by default.

Hangman: HangmanOptions

| Option | Type | Default | Description | | ----------- | ---------- | ------- | ------------------------------ | | lifeCount | 1 to 9 | 6 | Wrong guesses before you lose. |

Players take turns picking a letter or guessing the whole word. A wrong word costs two lives. The word list comes from the locale, so a French game gives French words.

Snake: SnakeOptions

| Option | Type | Default | Description | | -------------------- | -------- | ------------- | ------------------------------------------ | | player | User | required | The player. Bots are refused. | | boardWidth | number | 10 | Width in cells, clamped between 5 and 12. | | boardHeight | number | 10 | Height in cells, clamped between 5 and 12. | | theme.colors.lose | number | manager end | Color on game over or timeout. | | theme.emojis.head | string | 🟢 | Head. | | theme.emojis.body | string | 🟩 | Body. | | theme.emojis.food | string | 🍎 | Food. | | theme.emojis.empty | string | ⬛ | Empty cells. |

Snake also takes turnTimeout, locale and lang.title. Each arrow button moves the snake one cell and ⏹️ quits. Eating an apple adds a point. Hitting a wall or your own tail ends the game, and filling the whole board wins it.

await games.createGame(GameNameType.Snake, interaction, {
    player: interaction.user,
    boardWidth: 8,
    boardHeight: 8,
});

Languages

| Language | Value | | ------------ | ------------------ | | English (US) | Locale.EnglishUS | | French | Locale.French | | Spanish | Locale.SpanishES | | Italian | Locale.Italian |

To speak each player's language, pass interaction.locale (or interaction.guildLocale) as the game's locale. An unsupported locale falls back to English.

await games.createGame(GameNameType.Hangman, interaction, {
    players: [interaction.user, opponent],
    locale: interaction.locale,
});

The GameManager constructor rejects an unsupported locale with a LocaleNotSupported error.

Errors

The library wraps every Discord call it makes. Raw DiscordAPIErrors come back as a WynhamGamesError with a stable code, so you never compare Discord error numbers yourself.

Some failures only mean the game can't go on: the message was deleted (10008), the interaction expired (10062, 50027), the channel is gone (10003), the interaction was already answered (40060) or the thread got archived (50083). The game then stops quietly and nothing is thrown.

Anything else throws. That covers missing permissions (50013), missing access (50001), an invalid payload (50035), rate limits and network errors. createGame() rejects, and your command handler can catch it:

import { MessageFlags } from "discord.js";
import { GameNameType, WynhamGamesError, WynhamErrorCode } from "wynham-games";

try {
    await games.createGame(GameNameType.Snake, interaction, { player: interaction.user });
} catch (error) {
    if (error instanceof WynhamGamesError && error.code === WynhamErrorCode.MissingPermissions) {
        await interaction.followUp({ content: "I need permission to edit my messages here.", flags: MessageFlags.Ephemeral });
    }
}

A WynhamGamesError carries code, discordCode, status, action (the discord.js call that failed), game and cause (the original error).

Once createGame() has resolved, errors come from collectors and timers that your code can't await. The library logs them with console.error instead of crashing your bot. Send them to your own logger with setErrorHandler:

import { setErrorHandler } from "wynham-games";

setErrorHandler(error => logger.error({ code: error.code, game: error.game }, error.message));

Bad options throw the same error type before anything reaches Discord: ClientMissing, LocaleNotSupported, InvalidAntiSpamTime, InvalidColor, InvalidTurnTimeout, InvalidLifeCount and InteractionNotRepliable.

The guarded helpers are exported if you want the same behaviour in your own code: safeDiscordCall, safeEdit, safeFollowUp, safeReply, safeNotify, safeUpdate, safeDeferUpdate, safeMessageEdit, toWynhamError and isWynhamGamesError.

Enums

| Enum | Values | | ----------------- | ------------------------------------------------------------------------ | | GameNameType | Connect4, RPS, TicTacToe, Hangman, Snake | | BotDifficulty | Easy, Normal, Hard, Impossible | | InputType | Buttons, SelectMenu | | Choice | Rock, Paper, Scissors | | DiscordAction | EditReply, FollowUp, Reply, Update, DeferUpdate, EditMessage | | WynhamErrorCode | See Errors and src/structures/Types.ts. |

Building from source

npm run build    # tsc && tsc-alias
npm run format   # prettier --write .

License

MIT, by Yanis Hamburger and Stefanoz.