wynham-games
v1.0.0-beta.12
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
game
An open-source Node.js library built on top of Discord.js that lets you create and manage interactive mini-games inside your Discord bot with a single function call.
It ships with player invitations, anti-spam cooldowns, bot opponents (with difficulty levels), full theming, and built-in translations.
Features
- 🎮 5 ready-to-play games — Connect Four, Tic-Tac-Toe, Rock Paper Scissors, Hangman and Snake.
- 🤖 Play against the bot — set player 2 to your client user and pick a difficulty (
Easy→Impossible). - 👥 Player-vs-player invites — the challenged user gets an Accept / Decline prompt before the game starts.
- 🛡️ Anti-spam & cooldowns — built-in protection so the same users can't flood your bot with games.
- 🌍 i18n out of the box — English, French, Spanish and Italian, selectable per manager or per game.
- 🎨 Theming — customise embed colors, emojis, titles and input mode (buttons or select menu).
Requirements
- Node.js
discord.js^14.26.4(peer dependency — install it in your bot project).
Installation
npm install wynham-games@beta discord.jsQuick start
The whole library revolves around a single class: GameManager. You instantiate it once with your Discord client, then call createGame(...) from a slash command interaction.
import { Client, GatewayIntentBits } from "discord.js";
import { GameManager, GameNameType } from "wynham-games";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// Create the manager once, ideally next to your client setup.
const games = new GameManager(client);
client.on("interactionCreate", async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName === "connect4") {
const opponent = interaction.options.getUser("opponent", true);
await games.createGame(GameNameType.Connect4, interaction, {
players: [interaction.user, opponent],
});
}
});
client.login(process.env.TOKEN);That's it — createGame automatically defers the reply, shows the invite, waits for the opponent, and runs the game to completion.
Core concept: GameManager
Constructor
new GameManager(client: Client, options?: GameManagerOptions)| Option | Type | Default | Description |
| ----------------- | --------- | ------------------ | ------------------------------------------------------------------------------------ |
| antiSpam.enable | boolean | true | Prevents the same pair of players from starting back-to-back games. |
| antiSpam.time | number | 5000 | Cooldown duration in milliseconds. |
| locale | Locale | Locale.EnglishUS | Default language for every game created by this manager. Can be overridden per game. |
import { Locale } from "discord.js";
const games = new GameManager(client, {
antiSpam: { enable: true, time: 10_000 },
locale: Locale.French,
});createGame(gameName, interaction, options)
Validates the players, displays the invitation/difficulty menu when needed, and launches the game.
createGame(gameName: GameNameType, interaction: Interaction, options): Promise<void>gameName— one of theGameNameTypeenum values.interaction— a repliable slash-command interaction. It is deferred automatically if it hasn't been already.options— the per-game options object (always contains theplayerstuple, see below).
The method is overloaded, so TypeScript infers the correct options type from the gameName you pass.
Bot opponents
Only RPS and Tic-Tac-Toe support a bot opponent (players[1].bot === true):
- RPS runs immediately against the bot (the bot plays a random choice).
- Tic-Tac-Toe first shows a difficulty-selection menu, then runs against the bot.
Connect Four and Hangman are player-vs-player only — passing a bot as the second player is rejected.
For human-vs-human, the second player is sent an Accept / Decline invite and the game only starts once they accept.
Snake is single-player: it takes a single player instead of a players tuple, and starts immediately with no invite.
Games & options
Every option object shares the players field and most share turnTimeout, locale, theme and lang. The table below lists what is specific to each game.
Shared options
| Option | Type | Default | Description |
| ------------------ | -------------- | ---------------- | -------------------------------------------------------------- |
| players | [User, User] | — | Required. The two participants. Player 2 may be the bot. |
| turnTimeout | number | 30 | Max seconds a player has to act before the game ends (15–360). |
| locale | Locale | manager's locale | Overrides the manager language for this game only. |
| theme.colors | object | see below | Embed colors for play / win / draw states. |
| lang.title | string | game name | Title shown on the game board. |
| lang.inviteTitle | string | "… Challenge" | Title shown in the invite message. |
GameNameType.Connect4 — 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 | ⚪ | Winning pieces. |
| theme.inputType | InputType | Buttons | Column input mode: Buttons or SelectMenu. |
| theme.buttonStyle | ButtonStyle | Primary | Style of the column input buttons: 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.Buttons,
buttonStyle: ButtonStyle.Success,
},
});GameNameType.TicTacToe — TicTacToeOptions
| Option | Type | Default | Description |
| ---------------------- | --------------- | ------- | ------------------------------------------------------ |
| difficulty | BotDifficulty | Hard | Bot strength when player 2 is a bot — see table below. |
| theme.emojis.player1 | string | ❌ | Player 1 marks. |
| theme.emojis.player2 | string | ⭕ | Player 2 marks. |
| theme.emojis.empty | string | ⬜ | Empty cells. |
The bot opponent behaves differently per difficulty:
| Difficulty | Behaviour |
| ------------ | ---------------------------------------------------------- |
| Easy | Always plays a random move. |
| Normal | Plays the optimal move ~50% of the time, random otherwise. |
| Hard | Plays the optimal move ~80% of the time. |
| Impossible | Always optimal (minimax, unbeatable). |
GameNameType.RPS — RPSOptions
| Option | Type | Default | Description |
| ----------------------- | -------- | ------- | --------------- |
| theme.emojis.rock | string | ✊ | Rock emoji. |
| theme.emojis.paper | string | 🤚 | Paper emoji. |
| theme.emojis.scissors | string | ✌️ | Scissors emoji. |
GameNameType.Hangman — HangmanOptions
| Option | Type | Default | Description |
| ----------- | ------- | ------- | -------------------------------- |
| lifeCount | 1–9 | 6 | Number of wrong guesses allowed. |
The Hangman word list is taken from the active locale, so changing
localealso changes the language of the words to guess.
GameNameType.Snake — SnakeOptions
Single-player. Snake does not use the shared
players/lang.inviteTitlefields. It takes a singleplayerand starts immediately — no invite is sent.
| Option | Type | Default | Description |
| -------------------- | -------- | ------- | ------------------------------------------------------------------- |
| player | User | — | Required. The player (must not be a bot). |
| turnTimeout | number | 30 | Max seconds the player can stay idle between two moves (15–360). |
| boardWidth | number | 10 | Board width in cells (clamped 5–12). |
| boardHeight | number | 10 | Board height in cells (clamped 5–12). |
| theme.colors.lose | number | 0x808080 | Color on game over / timeout (replaces the shared draw color). |
| theme.emojis.head | string | 🟢 | Snake head. |
| theme.emojis.body | string | 🟩 | Snake body. |
| theme.emojis.food | string | 🍎 | Food. |
| theme.emojis.empty | string | ⬛ | Empty cells. |
| lang.title | string | "Snake" | Title shown on the game board. |
The player moves the snake one cell per button press (⬅️ ⬆️ ⬇️ ➡️) and can quit with ⏹️. Eating the 🍎 grows the snake and increases the score; hitting a wall or the snake's own body ends the game.
import { GameNameType } from "wynham-games";
await games.createGame(GameNameType.Snake, interaction, {
player: interaction.user,
boardWidth: 10,
boardHeight: 10,
});Internationalisation
Built-in locales are keyed by the Discord.js Locale enum:
| Locale | Enum value |
| ------------ | ------------------ |
| English (US) | Locale.EnglishUS |
| French | Locale.French |
| Spanish (ES) | Locale.SpanishES |
| Italian | Locale.Italian |
You can resolve a player's own language and fall back to English:
import { locales, Locale } from "wynham-games";
const locale = locales[interaction.locale] ?? locales[Locale.EnglishUS];Or simply pass interaction.locale (or interaction.guildLocale) as the locale option when creating a game.
Enums reference
enum GameNameType {
Connect4,
RPS,
TicTacToe,
Hangman,
Snake,
}
enum BotDifficulty {
Easy,
Normal,
Hard,
Impossible,
}
enum InputType {
SelectMenu,
Buttons,
}
enum Choice {
Rock,
Paper,
Scissors,
}
enum InviteResult {
Accepted,
Declined,
Timeout,
}Building from source
npm run build # tsc && tsc-alias
npm run format # prettier --write .License
MIT — Yanis Hamburger & Stefanoz.
