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.
Maintainers
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.jsQuick 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: aGameNameTypevalue.interaction: a repliable interaction, usually a slash command.options: the options of that game. TypeScript picks the right type fromgameName, so passingplayersto Snake orlifeCountto 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
playerinstead ofplayersand 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.
