@sodiumlabs/gamecord 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2614 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ Connect4: () => Connect4,
35
+ FastType: () => FastType,
36
+ Flood: () => Flood,
37
+ Game2048: () => Game2048,
38
+ Memory: () => Memory,
39
+ Minesweeper: () => Minesweeper,
40
+ RockPaperScissors: () => RockPaperScissors,
41
+ TicTacToe: () => TicTacToe,
42
+ Trivia: () => Trivia,
43
+ Wordle: () => Wordle,
44
+ connect4Options: () => connect4Options,
45
+ fastTypeOptions: () => fastTypeOptions,
46
+ floodOptions: () => floodOptions,
47
+ game2048Options: () => game2048Options,
48
+ jokerEmoji: () => jokerEmoji,
49
+ memoryOptions: () => memoryOptions,
50
+ minesweeperOptions: () => minesweeperOptions,
51
+ rockPaperScissorsOptions: () => rockPaperScissorsOptions,
52
+ ticTacToeOptions: () => ticTacToeOptions,
53
+ triviaOptions: () => triviaOptions,
54
+ wordleOptions: () => wordleOptions
55
+ });
56
+ module.exports = __toCommonJS(index_exports);
57
+
58
+ // src/games/2048.ts
59
+ var import_v42 = __toESM(require("zod/v4"));
60
+ var import_canvas = require("@napi-rs/canvas");
61
+ var import_discord2 = require("discord.js");
62
+
63
+ // src/core/Game.ts
64
+ var import_node_events = require("events");
65
+ var import_discord = require("discord.js");
66
+ var Game = class extends import_node_events.EventEmitter {
67
+ static {
68
+ __name(this, "Game");
69
+ }
70
+ /**
71
+ * The initial context that started this game, either an interaction or a message.
72
+ */
73
+ context;
74
+ /**
75
+ * If the {@link Game#context} is a `Message` or not.
76
+ */
77
+ isMessage;
78
+ /**
79
+ * The player that started the game.
80
+ */
81
+ player;
82
+ /**
83
+ * The timestamp at which the class was instantiated. Not when {@link Game#start} was called.
84
+ */
85
+ gameStartedAt;
86
+ constructor(context) {
87
+ super();
88
+ this.context = context;
89
+ const isMessage = context instanceof import_discord.Message;
90
+ this.isMessage = isMessage;
91
+ this.player = isMessage ? context.author : context.user;
92
+ this.gameStartedAt = Date.now();
93
+ if (isMessage && !context.channel.isSendable()) {
94
+ throw new Error("The context channel must be a sendable channel");
95
+ }
96
+ }
97
+ /**
98
+ * Returns the current duration of the game.
99
+ */
100
+ getGameDuration() {
101
+ return Date.now() - this.gameStartedAt;
102
+ }
103
+ async sendMessage(options) {
104
+ if (this.isMessage) {
105
+ const message = this.context;
106
+ const channel = message.channel;
107
+ return await channel.send(options);
108
+ } else {
109
+ const interaction = this.context;
110
+ if (interaction.deferred || interaction.replied) {
111
+ return await interaction.editReply(options);
112
+ } else {
113
+ const result = await interaction.reply({ ...options, withResponse: true });
114
+ return result.resource.message;
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Edit the context message if it was an interaction,
120
+ * or edit the given message.
121
+ *
122
+ * Since the original context is only valid 15 minutes,
123
+ * we need the message reference if the interaction died.
124
+ */
125
+ async editContextOrMessage(message, options) {
126
+ if (!this.isMessage) {
127
+ const interaction = this.context;
128
+ if (Date.now() - interaction.createdTimestamp < 6e4 * 15) {
129
+ return await interaction.editReply(options);
130
+ }
131
+ }
132
+ return await message.edit(options);
133
+ }
134
+ /**
135
+ * Utility to build an embed from the options.
136
+ */
137
+ async buildEmbed(embed, props) {
138
+ return typeof embed === "function" ? await embed(this) : {
139
+ author: {
140
+ name: this.player.displayName,
141
+ icon_url: this.player.displayAvatarURL()
142
+ },
143
+ ...props,
144
+ ...embed
145
+ };
146
+ }
147
+ /**
148
+ * Utility to build an end embed from the options.
149
+ */
150
+ async buildEndEmbed(embed, endEmbed, result, props) {
151
+ let built;
152
+ if (endEmbed) {
153
+ built = typeof endEmbed === "function" ? await endEmbed(this, result) : endEmbed;
154
+ } else {
155
+ built = typeof embed === "function" ? await embed(this) : {
156
+ author: {
157
+ name: this.player.displayName,
158
+ icon_url: this.player.displayAvatarURL()
159
+ },
160
+ ...embed,
161
+ ...props
162
+ };
163
+ }
164
+ return built;
165
+ }
166
+ /**
167
+ * Utility to build the result of a game.
168
+ */
169
+ result(data) {
170
+ return {
171
+ player: this.player,
172
+ gameStartedAt: this.gameStartedAt,
173
+ gameDuration: this.getGameDuration(),
174
+ ...data
175
+ };
176
+ }
177
+ };
178
+
179
+ // src/utils/games.ts
180
+ var import_node_assert = __toESM(require("assert"));
181
+ var numberEmojis = ["0\uFE0F\u20E3", "1\uFE0F\u20E3", "2\uFE0F\u20E3", "3\uFE0F\u20E3", "4\uFE0F\u20E3", "5\uFE0F\u20E3", "6\uFE0F\u20E3", "7\uFE0F\u20E3", "8\uFE0F\u20E3", "9\uFE0F\u20E3", "\u{1F51F}"];
182
+ var getNumberEmoji = /* @__PURE__ */ __name((number) => {
183
+ (0, import_node_assert.default)(number >= 0 && number <= 9);
184
+ return numberEmojis[number];
185
+ }, "getNumberEmoji");
186
+ var removeEmoji = /* @__PURE__ */ __name((button) => {
187
+ if ("emoji" in button.data) {
188
+ button.data.emoji = void 0;
189
+ }
190
+ return button;
191
+ }, "removeEmoji");
192
+ var moveInDirection = /* @__PURE__ */ __name((pos, direction) => {
193
+ if (direction === "up") return { x: pos.x, y: pos.y - 1 };
194
+ if (direction === "down") return { x: pos.x, y: pos.y + 1 };
195
+ if (direction === "left") return { x: pos.x - 1, y: pos.y };
196
+ if (direction === "right") return { x: pos.x + 1, y: pos.y };
197
+ return pos;
198
+ }, "moveInDirection");
199
+ var getOppositeDirection = /* @__PURE__ */ __name((direction) => {
200
+ if (direction === "up") return "down";
201
+ if (direction === "down") return "up";
202
+ if (direction === "left") return "right";
203
+ return "left";
204
+ }, "getOppositeDirection");
205
+
206
+ // src/utils/schemas.ts
207
+ var import_v4 = __toESM(require("zod/v4"));
208
+ var apiEmbed = /* @__PURE__ */ __name(() => import_v4.default.object({
209
+ author: import_v4.default.object({
210
+ name: import_v4.default.string(),
211
+ url: import_v4.default.string().optional(),
212
+ icon_url: import_v4.default.string().optional()
213
+ }),
214
+ thumbnail: import_v4.default.object({
215
+ url: import_v4.default.string()
216
+ }),
217
+ title: import_v4.default.string(),
218
+ color: import_v4.default.union([
219
+ import_v4.default.string().transform((val) => {
220
+ return parseInt(val.slice(1), 16);
221
+ }),
222
+ import_v4.default.number().int()
223
+ ]),
224
+ description: import_v4.default.string(),
225
+ image: import_v4.default.object({
226
+ url: import_v4.default.string()
227
+ }),
228
+ footer: import_v4.default.object({
229
+ text: import_v4.default.string(),
230
+ icon_url: import_v4.default.string().optional()
231
+ })
232
+ }).partial(), "apiEmbed");
233
+ var functionable = /* @__PURE__ */ __name((schema) => () => import_v4.default.union([schema, import_v4.default.custom()]), "functionable");
234
+ var embedBuilder = /* @__PURE__ */ __name(() => functionable(apiEmbed())(), "embedBuilder");
235
+ var gameMessage = /* @__PURE__ */ __name(() => import_v4.default.union([import_v4.default.string().transform((val) => () => val), import_v4.default.custom()]), "gameMessage");
236
+ var resultMessage = /* @__PURE__ */ __name(() => import_v4.default.union([import_v4.default.string().transform((val) => () => val), import_v4.default.custom()]), "resultMessage");
237
+ var var2Message = /* @__PURE__ */ __name(() => import_v4.default.union([import_v4.default.string().transform((val) => () => val), import_v4.default.custom()]), "var2Message");
238
+
239
+ // src/utils/constants.ts
240
+ var colors = {
241
+ green: 2600544,
242
+ red: 15158332,
243
+ yellow: 16236389,
244
+ blue: 2719929,
245
+ blurple: 5793266
246
+ };
247
+
248
+ // src/utils/random.ts
249
+ var getRandomInt = /* @__PURE__ */ __name((max, min = 0) => {
250
+ if (min >= max) throw new RangeError("`min` should be less than `max`");
251
+ return Math.floor(Math.random() * (min ? max + 1 - min : max + 1)) + min;
252
+ }, "getRandomInt");
253
+ var getRandomElement = /* @__PURE__ */ __name((array) => {
254
+ if (!array.length) throw new RangeError("The array should not be empty");
255
+ return array[Math.floor(Math.random() * array.length)];
256
+ }, "getRandomElement");
257
+ var shuffleArray = /* @__PURE__ */ __name((array) => {
258
+ for (let i = array.length - 1; i > 0; i--) {
259
+ const j = Math.floor(Math.random() * (i + 1));
260
+ [array[i], array[j]] = [array[j], array[i]];
261
+ }
262
+ return array;
263
+ }, "shuffleArray");
264
+
265
+ // src/games/2048.ts
266
+ var defaultOptions = {
267
+ embed: {
268
+ title: "2048",
269
+ color: colors.blurple
270
+ },
271
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use this menu.`, "notPlayerMessage"),
272
+ timeout: 6e4,
273
+ buttonStyle: import_discord2.ButtonStyle.Secondary,
274
+ emojis: {
275
+ up: "\u{1F53C}",
276
+ down: "\u{1F53D}",
277
+ right: "\u25B6\uFE0F",
278
+ left: "\u25C0\uFE0F"
279
+ }
280
+ };
281
+ var game2048Options = import_v42.default.object({
282
+ embed: embedBuilder().optional().default(defaultOptions.embed),
283
+ endEmbed: embedBuilder().optional(),
284
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions.notPlayerMessage),
285
+ timeout: import_v42.default.number().int().optional().default(defaultOptions.timeout),
286
+ buttonStyle: import_v42.default.number().int().optional().default(import_discord2.ButtonStyle.Secondary),
287
+ emojis: import_v42.default.object({
288
+ up: import_v42.default.string().optional().default(defaultOptions.emojis.up),
289
+ down: import_v42.default.string().optional().default(defaultOptions.emojis.down),
290
+ right: import_v42.default.string().optional().default(defaultOptions.emojis.right),
291
+ left: import_v42.default.string().optional().default(defaultOptions.emojis.left)
292
+ }).optional().default(defaultOptions.emojis)
293
+ });
294
+ var Game2048 = class extends Game {
295
+ static {
296
+ __name(this, "Game2048");
297
+ }
298
+ options;
299
+ /**
300
+ * The numbers are stored as exponents (1 => 2, 2 => 4, etc.).
301
+ */
302
+ gameboard = [];
303
+ length = 4;
304
+ score = 0;
305
+ message = null;
306
+ mergedPos = [];
307
+ constructor(context, options) {
308
+ super(context);
309
+ this.options = game2048Options.parse(options || {});
310
+ for (let y = 0; y < this.length; y++) {
311
+ for (let x = 0; x < this.length; x++) {
312
+ this.gameboard[y * this.length + x] = 0;
313
+ }
314
+ }
315
+ }
316
+ async start() {
317
+ this.placeRandomTile();
318
+ this.placeRandomTile();
319
+ const embed = await this.buildEmbed(this.options.embed, {
320
+ image: {
321
+ url: "attachment://board.png"
322
+ },
323
+ fields: [
324
+ {
325
+ name: "Current Score",
326
+ value: this.score.toString()
327
+ }
328
+ ]
329
+ });
330
+ this.message = await this.sendMessage({
331
+ embeds: [embed],
332
+ components: this.getComponents(),
333
+ files: [await this.getBoardAttachment()]
334
+ });
335
+ this.handleButtons(this.message);
336
+ }
337
+ handleButtons(message) {
338
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
339
+ collector.on("collect", async (i) => {
340
+ if (!i.customId.startsWith("$gamecord")) return;
341
+ if (i.user.id !== this.player.id) {
342
+ if (this.options.notPlayerMessage) {
343
+ try {
344
+ await i.reply({
345
+ content: this.options.notPlayerMessage(this),
346
+ flags: import_discord2.MessageFlags.Ephemeral
347
+ });
348
+ } catch (err) {
349
+ this.emit("error", err);
350
+ }
351
+ }
352
+ return;
353
+ }
354
+ let moved = false;
355
+ this.mergedPos = [];
356
+ const direction = i.customId.split("-").at(-1);
357
+ try {
358
+ await i.deferUpdate();
359
+ } catch (err) {
360
+ this.emit("error", err);
361
+ return;
362
+ }
363
+ if (direction === "up" || direction === "down") moved = this.shiftVertical(direction);
364
+ if (direction === "left" || direction === "right") moved = this.shiftHorizontal(direction);
365
+ if (moved) this.placeRandomTile();
366
+ if (this.isGameOver()) return collector.stop("$over");
367
+ const embed = await this.buildEmbed(this.options.embed, {
368
+ image: {
369
+ url: "attachment://board.png"
370
+ },
371
+ fields: [
372
+ {
373
+ name: "Current Score",
374
+ value: this.score.toString()
375
+ }
376
+ ]
377
+ });
378
+ try {
379
+ await i.editReply({ embeds: [embed], files: [await this.getBoardAttachment()] });
380
+ } catch (err) {
381
+ this.emit("error", err);
382
+ }
383
+ });
384
+ collector.on("end", async (_, reason) => {
385
+ this.emit("end");
386
+ if (reason === "idle" || reason === "$over") {
387
+ await this.gameOver(message, reason === "$over" ? "over" : "timeout");
388
+ }
389
+ });
390
+ }
391
+ async gameOver(message, outcome) {
392
+ const result = this.result({ outcome, score: this.score, hasWon: !!this.gameboard.find((n) => n >= 11) });
393
+ this.emit("gameOver", result);
394
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
395
+ image: {
396
+ url: "attachment://board.png"
397
+ },
398
+ fields: [
399
+ {
400
+ name: "Total Score",
401
+ value: this.score.toString()
402
+ }
403
+ ]
404
+ });
405
+ try {
406
+ await this.editContextOrMessage(message, {
407
+ embeds: [embed],
408
+ components: this.getComponents(true),
409
+ files: [await this.getBoardAttachment()]
410
+ });
411
+ } catch (err) {
412
+ this.emit("error", err);
413
+ }
414
+ }
415
+ isGameOver() {
416
+ let boardFull = true;
417
+ let numMoves = 0;
418
+ for (let y = 0; y < this.length; y++) {
419
+ for (let x = 0; x < this.length; x++) {
420
+ if (this.gameboard[y * this.length + x] === 0) boardFull = false;
421
+ const posNum = this.gameboard[y * this.length + x];
422
+ for (const dir of ["down", "left", "right", "up"]) {
423
+ const newPos = moveInDirection({ x, y }, dir);
424
+ if (this.isInsideBlock(newPos) && (this.gameboard[newPos.y * this.length + newPos.x] === 0 || this.gameboard[newPos.y * this.length + newPos.x] === posNum)) {
425
+ numMoves++;
426
+ }
427
+ }
428
+ }
429
+ }
430
+ return boardFull && numMoves === 0;
431
+ }
432
+ placeRandomTile() {
433
+ let tilePos = { x: 0, y: 0 };
434
+ do {
435
+ tilePos = { x: getRandomInt(this.length), y: getRandomInt(this.length) };
436
+ } while (this.gameboard[tilePos.y * this.length + tilePos.x] != 0);
437
+ this.gameboard[tilePos.y * this.length + tilePos.x] = Math.random() > 0.8 ? 2 : 1;
438
+ }
439
+ /**
440
+ * @returns Has moved
441
+ */
442
+ shiftVertical(dir) {
443
+ let moved = false;
444
+ for (let x = 0; x < this.length; x++) {
445
+ if (dir === "up") {
446
+ for (let y = 1; y < this.length; y++) moved = this.shift({ x, y }, "up") || moved;
447
+ } else {
448
+ for (let y = this.length - 2; y >= 0; y--) moved = this.shift({ x, y }, "down") || moved;
449
+ }
450
+ }
451
+ return moved;
452
+ }
453
+ /**
454
+ * @returns Has moved
455
+ */
456
+ shiftHorizontal(dir) {
457
+ let moved = false;
458
+ for (let y = 0; y < this.length; y++) {
459
+ if (dir === "left") {
460
+ for (let x = 1; x < this.length; x++) {
461
+ moved = this.shift({ x, y }, "left") || moved;
462
+ }
463
+ } else {
464
+ for (let x = this.length - 2; x >= 0; x--) {
465
+ moved = this.shift({ x, y }, "right") || moved;
466
+ }
467
+ }
468
+ }
469
+ return moved;
470
+ }
471
+ isInsideBlock(pos) {
472
+ return pos.x >= 0 && pos.y >= 0 && pos.x < this.length && pos.y < this.length;
473
+ }
474
+ /**
475
+ * @returns Has moved
476
+ */
477
+ shift(pos, dir) {
478
+ let moved = false;
479
+ const movingTile = this.gameboard[pos.y * this.length + pos.x];
480
+ if (movingTile === 0) return false;
481
+ let set = false;
482
+ let moveTo = pos;
483
+ while (!set) {
484
+ moveTo = moveInDirection(moveTo, dir);
485
+ const moveToTile = this.gameboard[moveTo.y * this.length + moveTo.x];
486
+ if (!this.isInsideBlock(moveTo) || moveToTile !== 0 && moveToTile !== movingTile || !!this.mergedPos.find((p) => p.x === moveTo.x && p.y === moveTo.y)) {
487
+ const moveBack = moveInDirection(moveTo, getOppositeDirection(dir));
488
+ if (!(moveBack.x === pos.x && moveBack.y === pos.y)) {
489
+ this.gameboard[pos.y * this.length + pos.x] = 0;
490
+ this.gameboard[moveBack.y * this.length + moveBack.x] = movingTile;
491
+ moved = true;
492
+ }
493
+ set = true;
494
+ } else if (moveToTile === movingTile) {
495
+ moved = true;
496
+ this.gameboard[moveTo.y * this.length + moveTo.x] += 1;
497
+ this.score += Math.floor(Math.pow(this.gameboard[moveTo.y * this.length + moveTo.x], 2));
498
+ this.gameboard[pos.y * this.length + pos.x] = 0;
499
+ this.mergedPos.push(moveTo);
500
+ set = true;
501
+ }
502
+ }
503
+ return moved;
504
+ }
505
+ getComponents(disabled = false) {
506
+ const row1 = new import_discord2.ActionRowBuilder().setComponents(
507
+ new import_discord2.ButtonBuilder().setDisabled(true).setLabel("\u200B").setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-none1"),
508
+ new import_discord2.ButtonBuilder().setDisabled(disabled).setEmoji(this.options.emojis.up).setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-up"),
509
+ new import_discord2.ButtonBuilder().setDisabled(true).setLabel("\u200B").setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-none2")
510
+ );
511
+ const row2 = new import_discord2.ActionRowBuilder().setComponents(
512
+ new import_discord2.ButtonBuilder().setDisabled(disabled).setEmoji(this.options.emojis.left).setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-left"),
513
+ new import_discord2.ButtonBuilder().setDisabled(disabled).setEmoji(this.options.emojis.down).setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-down"),
514
+ new import_discord2.ButtonBuilder().setDisabled(disabled).setEmoji(this.options.emojis.right).setStyle(this.options.buttonStyle).setCustomId("$gamecord-2048-right")
515
+ );
516
+ return [row1, row2];
517
+ }
518
+ async getBoardAttachment() {
519
+ return new import_discord2.AttachmentBuilder(await this.getBoardImage(), { name: "board.png" });
520
+ }
521
+ async getBoardImage() {
522
+ const rows = this.length;
523
+ const cols = this.length;
524
+ const tile = 88;
525
+ const gap = 12;
526
+ const padding = 20;
527
+ const width = padding * 2 + cols * tile + (cols - 1) * gap;
528
+ const height = padding * 2 + rows * tile + (rows - 1) * gap;
529
+ const canvas = (0, import_canvas.createCanvas)(width, height);
530
+ const ctx = canvas.getContext("2d");
531
+ const PALETTE = {
532
+ bg: "#faf8ef",
533
+ board: "#bbada0",
534
+ empty: "#cdc1b4",
535
+ textDark: "#776e65",
536
+ textLight: "#f9f6f2",
537
+ tiles: /* @__PURE__ */ new Map([
538
+ [2, "#eee4da"],
539
+ [4, "#ede0c8"],
540
+ [8, "#f2b179"],
541
+ [16, "#f59563"],
542
+ [32, "#f67c5f"],
543
+ [64, "#f65e3b"],
544
+ [128, "#edcf72"],
545
+ [256, "#edcc61"],
546
+ [512, "#edc850"],
547
+ [1024, "#edc53f"],
548
+ [2048, "#edc22e"]
549
+ ])
550
+ };
551
+ ctx.fillStyle = PALETTE.bg;
552
+ ctx.fillRect(0, 0, width, height);
553
+ const boardRadius = 8;
554
+ const boardX = padding - 8;
555
+ const boardY = padding - 8;
556
+ const boardWidth = cols * tile + (cols - 1) * gap + 16;
557
+ const boardHeight = rows * tile + (rows - 1) * gap + 16;
558
+ function roundRect(x, y, w, h, r) {
559
+ ctx.beginPath();
560
+ ctx.moveTo(x + r, y);
561
+ ctx.lineTo(x + w - r, y);
562
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
563
+ ctx.lineTo(x + w, y + h - r);
564
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
565
+ ctx.lineTo(x + r, y + h);
566
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
567
+ ctx.lineTo(x, y + r);
568
+ ctx.quadraticCurveTo(x, y, x + r, y);
569
+ ctx.closePath();
570
+ }
571
+ __name(roundRect, "roundRect");
572
+ ctx.fillStyle = PALETTE.board;
573
+ roundRect(boardX, boardY, boardWidth, boardHeight, boardRadius);
574
+ ctx.fill();
575
+ ctx.textAlign = "center";
576
+ ctx.textBaseline = "middle";
577
+ for (let r = 0; r < rows; r++) {
578
+ for (let c = 0; c < cols; c++) {
579
+ const x = padding + c * (tile + gap);
580
+ const y = padding + r * (tile + gap);
581
+ const exponent = this.gameboard[r * this.length + c] ?? 0;
582
+ const value = exponent > 0 ? 2 ** exponent : 0;
583
+ const bgColor = value === 0 ? PALETTE.empty : PALETTE.tiles.get(value) ?? "#3c3a32";
584
+ const radius = 6;
585
+ ctx.fillStyle = bgColor;
586
+ roundRect(x, y, tile, tile, radius);
587
+ ctx.fill();
588
+ ctx.lineWidth = 2;
589
+ ctx.strokeStyle = value === 0 ? "rgba(0,0,0,0.06)" : "rgba(0,0,0,0.08)";
590
+ ctx.stroke();
591
+ if (value !== 0) {
592
+ const digits = value.toString().length;
593
+ let fontSize = Math.floor(tile * 0.54);
594
+ if (digits >= 3) fontSize = Math.floor(tile * 0.46);
595
+ if (digits >= 4) fontSize = Math.floor(tile * 0.36);
596
+ ctx.font = `700 ${fontSize}px sans-serif`;
597
+ ctx.fillStyle = value <= 4 ? PALETTE.textDark : PALETTE.textLight;
598
+ ctx.fillText(value.toString(), x + tile / 2, y + tile / 2 + 2);
599
+ }
600
+ }
601
+ }
602
+ return canvas.toBuffer("image/png");
603
+ }
604
+ };
605
+
606
+ // src/games/Connect4.ts
607
+ var import_v43 = __toESM(require("zod/v4"));
608
+ var import_discord4 = require("discord.js");
609
+
610
+ // src/core/VersusGame.ts
611
+ var import_zod = __toESM(require("zod"));
612
+ var import_discord3 = require("discord.js");
613
+ var defaultOptions2 = {
614
+ timeout: 6e4,
615
+ requestEmbed: /* @__PURE__ */ __name((name) => ({ player }) => ({
616
+ color: colors.blue,
617
+ description: `${player} has invited you for a round of **${name}**.`
618
+ }), "requestEmbed"),
619
+ rejectEmbed: /* @__PURE__ */ __name((name) => ({ opponent }) => ({
620
+ color: colors.red,
621
+ description: `The player ${opponent} denied your request for a round of **${name}**.`
622
+ }), "rejectEmbed"),
623
+ timeoutEmbed: /* @__PURE__ */ __name(({ opponent }) => ({
624
+ color: colors.yellow,
625
+ description: `Dropped the game as the player ${opponent} did not respond.`
626
+ }), "timeoutEmbed"),
627
+ buttons: {
628
+ accept: "Accept",
629
+ reject: "Reject"
630
+ }
631
+ };
632
+ var versusOptions = /* @__PURE__ */ __name((name) => import_zod.default.object({
633
+ opponent: import_zod.default.custom((val) => val instanceof import_discord3.User, {
634
+ error: "The opponent must be an instance of a discord.js User"
635
+ }),
636
+ timeout: import_zod.default.number().int().optional().default(defaultOptions2.timeout),
637
+ requestEmbed: import_zod.default.custom().optional().default(() => defaultOptions2.requestEmbed(name)),
638
+ rejectEmbed: import_zod.default.custom().optional().default(() => defaultOptions2.rejectEmbed(name)),
639
+ timeoutEmbed: import_zod.default.custom().optional().default(() => defaultOptions2.timeoutEmbed),
640
+ buttons: import_zod.default.object({
641
+ accept: import_zod.default.string().optional().default(defaultOptions2.buttons.accept),
642
+ reject: import_zod.default.string().optional().default(defaultOptions2.buttons.reject)
643
+ }).optional().default(defaultOptions2.buttons)
644
+ }), "versusOptions");
645
+ var VersusGame = class extends Game {
646
+ static {
647
+ __name(this, "VersusGame");
648
+ }
649
+ versusOptions;
650
+ /**
651
+ * The opponent user.
652
+ */
653
+ opponent;
654
+ constructor(context, options) {
655
+ super(context);
656
+ this.versusOptions = options;
657
+ this.opponent = options.opponent;
658
+ }
659
+ async requestVersus() {
660
+ const players = { player: this.player, opponent: this.opponent };
661
+ return new Promise(async (resolve) => {
662
+ const row = new import_discord3.ActionRowBuilder().setComponents(
663
+ new import_discord3.ButtonBuilder().setLabel(this.versusOptions.buttons.accept).setCustomId("$gamecord-versus-accept").setStyle(import_discord3.ButtonStyle.Success),
664
+ new import_discord3.ButtonBuilder().setLabel(this.versusOptions.buttons.reject).setCustomId("$gamecord-versus-reject").setStyle(import_discord3.ButtonStyle.Danger)
665
+ );
666
+ const msg = await this.sendMessage({
667
+ content: this.opponent.toString(),
668
+ embeds: [this.versusOptions.requestEmbed(players)],
669
+ components: [row],
670
+ allowedMentions: { parse: ["users"] }
671
+ });
672
+ const collector = msg.createMessageComponentCollector({ time: this.versusOptions.timeout });
673
+ collector.on("collect", async (i) => {
674
+ try {
675
+ await i.deferUpdate();
676
+ } catch (err) {
677
+ this.emit("error", err);
678
+ }
679
+ if (i.user.id === this.opponent.id) {
680
+ collector.stop(i.customId.split("-").at(-1));
681
+ }
682
+ });
683
+ collector.on("end", async (_, reason) => {
684
+ if (reason === "accept") return resolve(msg);
685
+ const isTime = reason === "time";
686
+ const embed = isTime ? this.versusOptions.timeoutEmbed : this.versusOptions.rejectEmbed;
687
+ this.emit("versusReject", isTime);
688
+ this.emit("end");
689
+ try {
690
+ for (const btn of row.components) {
691
+ btn.setDisabled(true);
692
+ }
693
+ await this.editContextOrMessage(msg, {
694
+ embeds: [embed(players)],
695
+ components: [row]
696
+ });
697
+ } catch (err) {
698
+ this.emit("error", err);
699
+ }
700
+ return resolve(null);
701
+ });
702
+ });
703
+ }
704
+ /**
705
+ * Utility to build an embed from the options.
706
+ */
707
+ async buildEmbed(embed, props) {
708
+ return await super.buildEmbed(embed, {
709
+ author: void 0,
710
+ footer: {
711
+ text: `${this.player.displayName} vs ${this.opponent.displayName}`
712
+ },
713
+ ...props
714
+ });
715
+ }
716
+ /**
717
+ * Utility to build an end embed from the options.
718
+ */
719
+ async buildEndEmbed(embed, endEmbed, result, props) {
720
+ return await super.buildEndEmbed(embed, endEmbed, result, {
721
+ author: void 0,
722
+ footer: {
723
+ text: `${this.player.displayName} vs ${this.opponent.displayName}`
724
+ },
725
+ ...props
726
+ });
727
+ }
728
+ };
729
+
730
+ // src/games/Connect4.ts
731
+ var defaultOptions3 = {
732
+ embed: {
733
+ title: "Connect 4",
734
+ color: colors.blurple
735
+ },
736
+ winMessage: /* @__PURE__ */ __name((res) => `${res.winnerEmoji} | **${res.winner}** won the TicTacToe Game.`, "winMessage"),
737
+ tieMessage: /* @__PURE__ */ __name(() => "The Game tied! No one won the Game!", "tieMessage"),
738
+ timeoutMessage: /* @__PURE__ */ __name(() => "The Game went unfinished! No one won the Game!", "timeoutMessage"),
739
+ turnMessage: /* @__PURE__ */ __name((turn) => `${turn.emoji} | It's player **${turn.player.displayName}**'s turn.`, "turnMessage"),
740
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} and ${game.opponent} can use this menu.`, "notPlayerMessage"),
741
+ statusText: "Game Status",
742
+ emojis: {
743
+ board: "\u26AB",
744
+ player1: "\u{1F534}",
745
+ player2: "\u{1F7E1}"
746
+ },
747
+ buttonStyle: import_discord4.ButtonStyle.Secondary,
748
+ timeout: 6e4
749
+ };
750
+ var connect4Options = import_v43.default.object({
751
+ versus: versusOptions("Connect 4"),
752
+ embed: embedBuilder().optional().default(defaultOptions3.embed),
753
+ endEmbed: embedBuilder().optional(),
754
+ winMessage: resultMessage().optional().default(() => defaultOptions3.winMessage),
755
+ tieMessage: resultMessage().optional().default(() => defaultOptions3.tieMessage),
756
+ timeoutMessage: resultMessage().optional().default(() => defaultOptions3.timeoutMessage),
757
+ turnMessage: var2Message().optional().default(() => defaultOptions3.turnMessage),
758
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions3.notPlayerMessage),
759
+ statusText: import_v43.default.string().optional().default(defaultOptions3.statusText),
760
+ emojis: import_v43.default.object({
761
+ board: import_v43.default.string().optional().default(defaultOptions3.emojis.board),
762
+ player1: import_v43.default.string().optional().default(defaultOptions3.emojis.player1),
763
+ player2: import_v43.default.string().optional().default(defaultOptions3.emojis.player2)
764
+ }).optional().default(defaultOptions3.emojis),
765
+ buttonStyle: import_v43.default.number().int().optional().default(defaultOptions3.buttonStyle),
766
+ timeout: import_v43.default.number().int().optional().default(defaultOptions3.timeout)
767
+ });
768
+ var Connect4 = class extends VersusGame {
769
+ static {
770
+ __name(this, "Connect4");
771
+ }
772
+ options;
773
+ /**
774
+ * A 6*7 elements array.
775
+ *
776
+ * - `0` : empty
777
+ * - `1` : player 1
778
+ * - `2` : player 2
779
+ */
780
+ gameboard = [];
781
+ isPlayer1Turn = true;
782
+ message = null;
783
+ locked = false;
784
+ constructor(context, options) {
785
+ const parsed = connect4Options.parse(options || {});
786
+ super(context, parsed.versus);
787
+ this.options = parsed;
788
+ for (let y = 0; y < 6; y++) {
789
+ for (let x = 0; x < 7; x++) {
790
+ this.gameboard[y * 7 + x] = 0;
791
+ }
792
+ }
793
+ }
794
+ async start() {
795
+ this.message = await this.requestVersus();
796
+ if (this.message) this.runGame(this.message);
797
+ }
798
+ async runGame(message) {
799
+ try {
800
+ const embed = await this.buildEmbed(this.options.embed, {
801
+ description: this.getBoardContent(),
802
+ fields: [
803
+ {
804
+ name: this.options.statusText,
805
+ value: this.options.turnMessage(this.getPlayerTurnData(), this)
806
+ }
807
+ ]
808
+ });
809
+ await this.editContextOrMessage(message, {
810
+ content: null,
811
+ embeds: [embed],
812
+ components: this.getComponents()
813
+ });
814
+ } catch (err) {
815
+ this.emit("fatalError", err);
816
+ this.emit("end");
817
+ return;
818
+ }
819
+ this.handleButtons(message);
820
+ }
821
+ handleButtons(message) {
822
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
823
+ collector.on("collect", async (i) => {
824
+ if (!i.customId.startsWith("$gamecord")) return;
825
+ if (i.user.id !== this.player.id && i.user.id !== this.opponent.id) {
826
+ if (this.options.notPlayerMessage) {
827
+ try {
828
+ await i.reply({
829
+ content: this.options.notPlayerMessage(this),
830
+ flags: import_discord4.MessageFlags.Ephemeral
831
+ });
832
+ } catch (err) {
833
+ this.emit("error", err);
834
+ }
835
+ }
836
+ return;
837
+ }
838
+ try {
839
+ await i.deferUpdate();
840
+ } catch (err) {
841
+ this.emit("error", err);
842
+ return;
843
+ }
844
+ if (this.locked) return;
845
+ this.locked = true;
846
+ try {
847
+ if (i.user.id !== (this.isPlayer1Turn ? this.player : this.opponent).id) return;
848
+ const column = Number(i.customId.split("-").at(-1));
849
+ const block = { x: -1, y: -1 };
850
+ for (let y = 6 - 1; y >= 0; y--) {
851
+ const number = this.gameboard[column + y * 7];
852
+ if (number === 0) {
853
+ this.gameboard[column + y * 7] = this.getPlayerNumber();
854
+ block.x = column;
855
+ block.y = y;
856
+ break;
857
+ }
858
+ }
859
+ if (this.isWinAt(block.x, block.y)) return collector.stop("$win");
860
+ if (this.isBoardFull()) return collector.stop("$tie");
861
+ this.isPlayer1Turn = !this.isPlayer1Turn;
862
+ const embed = await this.buildEmbed(this.options.embed, {
863
+ description: this.getBoardContent(),
864
+ fields: [
865
+ {
866
+ name: this.options.statusText,
867
+ value: this.options.turnMessage(this.getPlayerTurnData(), this)
868
+ }
869
+ ]
870
+ });
871
+ try {
872
+ await i.editReply({ embeds: [embed], components: this.getComponents() });
873
+ } catch (err) {
874
+ this.emit("error", err);
875
+ }
876
+ } finally {
877
+ this.locked = false;
878
+ }
879
+ });
880
+ collector.on("end", async (_, reason) => {
881
+ this.emit("end");
882
+ if (reason === "$win" || reason === "$tie") {
883
+ return await this.gameOver(message, reason === "$win" ? "win" : "tie");
884
+ }
885
+ if (reason === "idle") {
886
+ return await this.gameOver(message, "timeout");
887
+ }
888
+ });
889
+ }
890
+ async gameOver(message, outcome) {
891
+ const winner = this.isPlayer1Turn ? this.player : this.opponent;
892
+ const winnerEmoji = this.isPlayer1Turn ? this.options.emojis.player1 : this.options.emojis.player2;
893
+ const result = this.result({
894
+ opponent: this.opponent,
895
+ outcome,
896
+ winner: outcome === "win" ? winner : null,
897
+ winnerEmoji: outcome === "win" ? winnerEmoji : null
898
+ });
899
+ this.emit("gameOver", result);
900
+ let getMessage;
901
+ switch (outcome) {
902
+ case "win": {
903
+ getMessage = this.options.winMessage;
904
+ break;
905
+ }
906
+ case "tie": {
907
+ getMessage = this.options.tieMessage;
908
+ break;
909
+ }
910
+ case "timeout": {
911
+ getMessage = this.options.timeoutMessage;
912
+ break;
913
+ }
914
+ }
915
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
916
+ description: this.getBoardContent(),
917
+ fields: [
918
+ {
919
+ name: this.options.statusText,
920
+ value: getMessage(result, this)
921
+ }
922
+ ]
923
+ });
924
+ try {
925
+ await this.editContextOrMessage(message, {
926
+ embeds: [embed],
927
+ components: this.getComponents(true)
928
+ });
929
+ } catch (err) {
930
+ this.emit("error", err);
931
+ }
932
+ }
933
+ getPlayerTurnData() {
934
+ return this.isPlayer1Turn ? { player: this.player, emoji: this.options.emojis.player1 } : { player: this.opponent, emoji: this.options.emojis.player2 };
935
+ }
936
+ getBoardContent() {
937
+ let board = "";
938
+ for (let y = 0; y < 6; y++) {
939
+ for (let x = 0; x < 7; x++) {
940
+ board += this.getEmoji(this.gameboard[y * 7 + x]);
941
+ }
942
+ board += "\n";
943
+ }
944
+ board += "1\uFE0F\u20E32\uFE0F\u20E33\uFE0F\u20E34\uFE0F\u20E35\uFE0F\u20E36\uFE0F\u20E37\uFE0F\u20E3";
945
+ return board;
946
+ }
947
+ /**
948
+ * Get the current player number (`1` or `2`).
949
+ */
950
+ getPlayerNumber() {
951
+ return this.isPlayer1Turn ? 1 : 2;
952
+ }
953
+ isBoardFull() {
954
+ for (let y = 0; y < 6; y++) {
955
+ for (let x = 0; x < 7; x++) {
956
+ if (this.gameboard[y * 7 + x] === 0) return false;
957
+ }
958
+ }
959
+ return true;
960
+ }
961
+ /**
962
+ * Get the emoji of a number of the game board (`0`, `1` or `2`).
963
+ */
964
+ getEmoji(number) {
965
+ if (number === 1) return this.options.emojis.player1;
966
+ if (number === 2) return this.options.emojis.player2;
967
+ return this.options.emojis.board;
968
+ }
969
+ isWinAt(x, y) {
970
+ const number = this.getPlayerNumber();
971
+ for (let i = Math.max(0, x - 3); i <= x; i++) {
972
+ const adj = i + y * 7;
973
+ if (i + 3 < 7) {
974
+ if (this.gameboard[adj] === number && this.gameboard[adj + 1] === number && this.gameboard[adj + 2] === number && this.gameboard[adj + 3] === number)
975
+ return true;
976
+ }
977
+ }
978
+ for (let i = Math.max(0, y - 3); i <= y; i++) {
979
+ const adj = x + i * 7;
980
+ if (i + 3 < 6) {
981
+ if (this.gameboard[adj] === number && this.gameboard[adj + 7] === number && this.gameboard[adj + 2 * 7] === number && this.gameboard[adj + 3 * 7] === number)
982
+ return true;
983
+ }
984
+ }
985
+ for (let i = -3; i <= 0; i++) {
986
+ const block = { x: x + i, y: y + i };
987
+ const adj = block.x + block.y * 7;
988
+ if (block.x + 3 < 7 && block.y + 3 < 6) {
989
+ if (this.gameboard[adj] === number && this.gameboard[adj + 7 + 1] === number && this.gameboard[adj + 2 * 7 + 2] === number && this.gameboard[adj + 3 * 7 + 3] === number)
990
+ return true;
991
+ }
992
+ }
993
+ for (let i = -3; i <= 0; i++) {
994
+ const block = { x: x + i, y: y - i };
995
+ const adj = block.x + block.y * 7;
996
+ if (block.x + 3 < 7 && block.y - 3 >= 0 && block.x >= 0) {
997
+ if (this.gameboard[adj] === number && this.gameboard[adj - 7 + 1] === number && this.gameboard[adj - 2 * 7 + 2] === number && this.gameboard[adj - 3 * 7 + 3] === number)
998
+ return true;
999
+ }
1000
+ }
1001
+ return false;
1002
+ }
1003
+ isColumnFilled(x) {
1004
+ return this.gameboard[x] !== 0;
1005
+ }
1006
+ getComponents(disabled = false) {
1007
+ const row1 = new import_discord4.ActionRowBuilder().setComponents(
1008
+ ["1\uFE0F\u20E3", "2\uFE0F\u20E3", "3\uFE0F\u20E3", "4\uFE0F\u20E3"].map(
1009
+ (e, i) => new import_discord4.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(e).setCustomId(`$gamecord-connect4-${i}`).setDisabled(disabled || this.isColumnFilled(i))
1010
+ )
1011
+ );
1012
+ const row2 = new import_discord4.ActionRowBuilder().setComponents(
1013
+ ["5\uFE0F\u20E3", "6\uFE0F\u20E3", "7\uFE0F\u20E3"].map(
1014
+ (e, i) => new import_discord4.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(e).setCustomId(`$gamecord-connect4-${i + 4}`).setDisabled(disabled || this.isColumnFilled(i + 4))
1015
+ )
1016
+ );
1017
+ return [row1, row2];
1018
+ }
1019
+ };
1020
+
1021
+ // src/games/FastType.ts
1022
+ var import_v44 = __toESM(require("zod/v4"));
1023
+ var defaultOptions4 = {
1024
+ embed: {
1025
+ title: "Fast Type",
1026
+ color: colors.blurple,
1027
+ description: `You have 30 seconds to type the sentence below.`
1028
+ },
1029
+ winMessage: /* @__PURE__ */ __name((res) => `You won! You finished the type race in ${res.secondsTaken} seconds with word per minute of ${res.wpm}.`, "winMessage"),
1030
+ loseMessage: /* @__PURE__ */ __name(() => "You lost! You didn't type the correct sentence in time.", "loseMessage"),
1031
+ sentence: "Some really cool sentence to fast type.",
1032
+ timeout: 3e4
1033
+ };
1034
+ var fastTypeOptions = import_v44.default.object({
1035
+ embed: embedBuilder().optional().default(() => defaultOptions4.embed),
1036
+ endEmbed: embedBuilder().optional(),
1037
+ winMessage: resultMessage().optional().default(() => defaultOptions4.winMessage),
1038
+ loseMessage: resultMessage().optional().default(() => defaultOptions4.loseMessage),
1039
+ sentence: import_v44.default.string().optional().default(defaultOptions4.sentence),
1040
+ timeout: import_v44.default.number().int().optional().default(defaultOptions4.timeout)
1041
+ });
1042
+ var FastType = class extends Game {
1043
+ static {
1044
+ __name(this, "FastType");
1045
+ }
1046
+ options;
1047
+ timeTaken = 0;
1048
+ wpm = 0;
1049
+ constructor(context, options) {
1050
+ super(context);
1051
+ this.options = fastTypeOptions.parse(options || {});
1052
+ }
1053
+ async start() {
1054
+ const embed = typeof this.options.embed === "function" ? await this.options.embed(this) : {
1055
+ author: {
1056
+ name: this.player.displayName,
1057
+ icon_url: this.player.displayAvatarURL()
1058
+ },
1059
+ fields: [
1060
+ {
1061
+ name: "Sentence",
1062
+ value: this.options.sentence.split(" ").map((e) => "`" + e.split("").join(" ") + "`").join(" ")
1063
+ }
1064
+ ],
1065
+ ...this.options.embed
1066
+ };
1067
+ const message = await this.sendMessage({ embeds: [embed] });
1068
+ if (!message.channel.isSendable()) return;
1069
+ const startTime = Date.now();
1070
+ const collector = message.channel.createMessageCollector({
1071
+ time: this.options.timeout,
1072
+ filter: /* @__PURE__ */ __name((m) => m.author.id === this.player.id, "filter")
1073
+ });
1074
+ collector.on("collect", async (msg) => {
1075
+ this.timeTaken = Math.floor(Date.now() - startTime);
1076
+ this.wpm = Math.floor(msg.content.trim().length / (this.timeTaken / 6e4 % 60) / 5);
1077
+ const hasWon = msg.content?.toLowerCase().trim() === this.options.sentence.toLowerCase();
1078
+ collector.stop(hasWon ? "$win" : "$lose");
1079
+ });
1080
+ collector.on("end", async (_, reason) => {
1081
+ this.emit("end");
1082
+ if (!this.timeTaken) {
1083
+ this.timeTaken = Math.floor(Date.now() - startTime);
1084
+ }
1085
+ if (reason === "$win" || reason === "$lose" || reason === "time") {
1086
+ await this.gameOver(message, reason === "$win" ? "win" : reason === "$lose" ? "lose" : "timeout");
1087
+ }
1088
+ });
1089
+ }
1090
+ async gameOver(message, outcome) {
1091
+ const result = this.result({
1092
+ outcome,
1093
+ timeTaken: this.timeTaken,
1094
+ secondsTaken: Math.floor(this.timeTaken / 1e3),
1095
+ wpm: this.wpm
1096
+ });
1097
+ this.emit("gameOver", result);
1098
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
1099
+ description: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
1100
+ });
1101
+ try {
1102
+ await this.editContextOrMessage(message, { embeds: [embed] });
1103
+ } catch (err) {
1104
+ this.emit("error", err);
1105
+ }
1106
+ }
1107
+ };
1108
+
1109
+ // src/games/Flood.ts
1110
+ var import_v45 = __toESM(require("zod/v4"));
1111
+ var import_discord5 = require("discord.js");
1112
+ var defaultOptions5 = {
1113
+ embed: {
1114
+ title: "Flood",
1115
+ color: colors.blurple
1116
+ },
1117
+ winMessage: /* @__PURE__ */ __name((res) => `You won! You took **${res.turns}** turns.`, "winMessage"),
1118
+ loseMessage: /* @__PURE__ */ __name((res) => `You lost! You took **${res.turns}** turns.`, "loseMessage"),
1119
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use these buttons.`, "notPlayerMessage"),
1120
+ size: 13,
1121
+ maxTurns: 25,
1122
+ timeout: 12e4,
1123
+ buttonStyle: import_discord5.ButtonStyle.Primary,
1124
+ emojis: ["\u{1F7E5}", "\u{1F7E6}", "\u{1F7E7}", "\u{1F7EA}", "\u{1F7E9}"]
1125
+ };
1126
+ var floodOptions = import_v45.default.object({
1127
+ embed: embedBuilder().optional().default(defaultOptions5.embed),
1128
+ endEmbed: embedBuilder().optional(),
1129
+ winMessage: resultMessage().optional().default(() => defaultOptions5.winMessage),
1130
+ loseMessage: resultMessage().optional().default(() => defaultOptions5.loseMessage),
1131
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions5.notPlayerMessage),
1132
+ size: import_v45.default.number().int().optional().default(defaultOptions5.size),
1133
+ maxTurns: import_v45.default.number().int().optional().default(defaultOptions5.maxTurns),
1134
+ timeout: import_v45.default.number().int().optional().default(defaultOptions5.timeout),
1135
+ buttonStyle: import_v45.default.number().int().optional().default(defaultOptions5.buttonStyle),
1136
+ emojis: import_v45.default.array(import_v45.default.string()).length(5).optional().default(defaultOptions5.emojis)
1137
+ });
1138
+ var Flood = class extends Game {
1139
+ static {
1140
+ __name(this, "Flood");
1141
+ }
1142
+ options;
1143
+ board = [];
1144
+ turns = 0;
1145
+ message = null;
1146
+ constructor(context, options) {
1147
+ super(context);
1148
+ this.options = floodOptions.parse(options || {});
1149
+ for (let y = 0; y < this.options.size; y++) {
1150
+ for (let x = 0; x < this.options.size; x++) {
1151
+ this.board[y * this.options.size + x] = getRandomInt(this.options.emojis.length - 1);
1152
+ }
1153
+ }
1154
+ }
1155
+ async start() {
1156
+ this.message = await this.sendMessage({ embeds: [await this.getEmbed()], components: [this.getActionRow()] });
1157
+ this.handleButtons(this.message);
1158
+ }
1159
+ handleButtons(message) {
1160
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
1161
+ collector.on("collect", async (i) => {
1162
+ if (!i.customId.startsWith("$gamecord")) return;
1163
+ if (i.user.id !== this.player.id) {
1164
+ if (this.options.notPlayerMessage) {
1165
+ try {
1166
+ await i.reply({
1167
+ content: this.options.notPlayerMessage(this),
1168
+ flags: import_discord5.MessageFlags.Ephemeral
1169
+ });
1170
+ } catch (err) {
1171
+ this.emit("error", err);
1172
+ }
1173
+ }
1174
+ return;
1175
+ }
1176
+ try {
1177
+ await i.deferUpdate();
1178
+ } catch (err) {
1179
+ this.emit("error", err);
1180
+ return;
1181
+ }
1182
+ const index = Number(i.customId.split("-").at(-1));
1183
+ const updateResult = this.updateGame(index);
1184
+ if (updateResult === "win") return collector.stop("$win");
1185
+ if (updateResult === "lose") return collector.stop("$lose");
1186
+ try {
1187
+ await i.editReply({ embeds: [await this.getEmbed()], components: [this.getActionRow()] });
1188
+ } catch (err) {
1189
+ this.emit("error", err);
1190
+ }
1191
+ });
1192
+ collector.on("end", async (_, reason) => {
1193
+ this.emit("end");
1194
+ if (reason === "$win" || reason == "$lose" || reason === "idle") {
1195
+ await this.gameOver(message, reason === "$win" ? "win" : reason === "$lose" ? "lose" : "timeout");
1196
+ }
1197
+ });
1198
+ }
1199
+ getBoardContent() {
1200
+ let board = "";
1201
+ for (let y = 0; y < this.options.size; y++) {
1202
+ for (let x = 0; x < this.options.size; x++) {
1203
+ const index = this.board[y * this.options.size + x];
1204
+ board += this.options.emojis[index];
1205
+ }
1206
+ board += "\n";
1207
+ }
1208
+ return board;
1209
+ }
1210
+ getActionRow(disabled = false) {
1211
+ const row = new import_discord5.ActionRowBuilder();
1212
+ for (let i = 0; i < 5; i++) {
1213
+ row.addComponents(
1214
+ new import_discord5.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(this.options.emojis[i]).setCustomId(`$gamecord-flood-${i}`).setDisabled(disabled)
1215
+ );
1216
+ }
1217
+ return row;
1218
+ }
1219
+ async getEmbed() {
1220
+ return typeof this.options.embed === "function" ? await this.options.embed(this) : {
1221
+ author: {
1222
+ name: this.player.displayName,
1223
+ icon_url: this.player.displayAvatarURL()
1224
+ },
1225
+ fields: [{ name: "Turns", value: `${this.turns}/${this.options.maxTurns}` }],
1226
+ ...this.options.embed,
1227
+ description: this.getBoardContent()
1228
+ };
1229
+ }
1230
+ async gameOver(message, outcome) {
1231
+ const result = this.result({
1232
+ outcome,
1233
+ turns: this.turns,
1234
+ maxTurns: this.options.maxTurns,
1235
+ boardEmojiIndex: this.board[0]
1236
+ });
1237
+ this.emit("gameOver", result);
1238
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
1239
+ description: this.getBoardContent(),
1240
+ fields: [
1241
+ {
1242
+ name: "Game Over",
1243
+ value: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
1244
+ }
1245
+ ]
1246
+ });
1247
+ try {
1248
+ await this.editContextOrMessage(message, { embeds: [embed], components: [this.getActionRow(true)] });
1249
+ } catch (err) {
1250
+ this.emit("error", err);
1251
+ }
1252
+ }
1253
+ updateGame(selected) {
1254
+ if (selected === this.board[0]) return "none";
1255
+ const firstBlock = this.board[0];
1256
+ const queue = [{ x: 0, y: 0 }];
1257
+ const visited = [];
1258
+ this.turns += 1;
1259
+ while (queue.length > 0) {
1260
+ const block = queue.shift();
1261
+ if (!block || visited.some((v) => v.x === block.x && v.y === block.y)) continue;
1262
+ const index = block.y * this.options.size + block.x;
1263
+ visited.push(block);
1264
+ if (this.board[index] === firstBlock) {
1265
+ this.board[index] = selected;
1266
+ const up = { x: block.x, y: block.y - 1 };
1267
+ if (!visited.some((v) => v.x === up.x && v.y === up.y) && up.y >= 0) queue.push(up);
1268
+ const down = { x: block.x, y: block.y + 1 };
1269
+ if (!visited.some((v) => v.x === down.x && v.y === down.y) && down.y < this.options.size)
1270
+ queue.push(down);
1271
+ const left = { x: block.x - 1, y: block.y };
1272
+ if (!visited.some((v) => v.x === left.x && v.y === left.y) && left.x >= 0) queue.push(left);
1273
+ const right = { x: block.x + 1, y: block.y };
1274
+ if (!visited.some((v) => v.x === right.x && v.y === right.y) && right.x < this.options.size)
1275
+ queue.push(right);
1276
+ }
1277
+ }
1278
+ let gameOver = true;
1279
+ for (let y = 0; y < this.options.size; y++) {
1280
+ for (let x = 0; x < this.options.size; x++) {
1281
+ if (this.board[y * this.options.size + x] !== selected) gameOver = false;
1282
+ }
1283
+ }
1284
+ if (this.turns >= this.options.maxTurns && !gameOver) return "lose";
1285
+ if (gameOver) return "win";
1286
+ return "none";
1287
+ }
1288
+ };
1289
+
1290
+ // src/games/Memory.ts
1291
+ var import_v46 = __toESM(require("zod/v4"));
1292
+ var import_discord6 = require("discord.js");
1293
+ var jokerEmoji = "\u{1F0CF}";
1294
+ var defaultOptions6 = {
1295
+ embed: {
1296
+ title: "Memory",
1297
+ color: colors.blurple,
1298
+ description: "**Click on the buttons to match emojis with their pairs.**"
1299
+ },
1300
+ winMessage: /* @__PURE__ */ __name((res) => `**You won the Game! You turned a total of \`${res.tilesTurned}\` tiles.**`, "winMessage"),
1301
+ loseMessage: /* @__PURE__ */ __name((res) => `**You lost the Game! You turned a total of \`${res.tilesTurned}\` tiles.**`, "loseMessage"),
1302
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use this menu.`, "notPlayerMessage"),
1303
+ timeout: 12e4,
1304
+ emojis: [
1305
+ "\u{1F349}",
1306
+ "\u{1F347}",
1307
+ "\u{1F34A}",
1308
+ "\u{1F34B}",
1309
+ "\u{1F96D}",
1310
+ "\u{1F34E}",
1311
+ "\u{1F34F}",
1312
+ "\u{1F95D}",
1313
+ "\u{1F965}",
1314
+ "\u{1F353}",
1315
+ "\u{1F352}",
1316
+ "\u{1FAD0}",
1317
+ "\u{1F34D}",
1318
+ "\u{1F345}",
1319
+ "\u{1F350}",
1320
+ "\u{1F954}",
1321
+ "\u{1F33D}",
1322
+ "\u{1F955}",
1323
+ "\u{1F96C}",
1324
+ "\u{1F966}"
1325
+ ]
1326
+ };
1327
+ var memoryOptions = import_v46.default.object({
1328
+ embed: embedBuilder().optional().default(defaultOptions6.embed),
1329
+ endEmbed: embedBuilder().optional(),
1330
+ winMessage: resultMessage().optional().default(() => defaultOptions6.winMessage),
1331
+ loseMessage: resultMessage().optional().default(() => defaultOptions6.loseMessage),
1332
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions6.notPlayerMessage),
1333
+ timeout: import_v46.default.number().int().optional().default(defaultOptions6.timeout),
1334
+ emojis: import_v46.default.array(import_v46.default.string()).length(20).optional().default(defaultOptions6.emojis)
1335
+ });
1336
+ var Memory = class extends Game {
1337
+ static {
1338
+ __name(this, "Memory");
1339
+ }
1340
+ options;
1341
+ emojis;
1342
+ components = [];
1343
+ selected = null;
1344
+ remainingPairs = 12;
1345
+ tilesTurned = 0;
1346
+ size = 5;
1347
+ message = null;
1348
+ constructor(context, options) {
1349
+ super(context);
1350
+ this.options = memoryOptions.parse(options || {});
1351
+ this.emojis = [...this.options.emojis];
1352
+ this.emojis = shuffleArray(this.emojis).slice(0, 12);
1353
+ this.emojis.push(...this.emojis, jokerEmoji);
1354
+ this.emojis = shuffleArray(this.emojis);
1355
+ }
1356
+ async start() {
1357
+ this.components = this.getComponents();
1358
+ const embed = await this.buildEmbed(this.options.embed);
1359
+ this.message = await this.sendMessage({ embeds: [embed], components: this.components });
1360
+ this.handleButtons(this.message);
1361
+ }
1362
+ /**
1363
+ * Get the positions of this emoji.
1364
+ */
1365
+ getPairEmojiPositions(emoji) {
1366
+ const emojis = [];
1367
+ for (let y = 0; y < this.size; y++) {
1368
+ for (let x = 0; x < this.size; x++) {
1369
+ const index = y * this.size + x;
1370
+ if (this.emojis[index] === emoji) emojis.push({ x, y, index });
1371
+ }
1372
+ }
1373
+ return emojis;
1374
+ }
1375
+ getComponents() {
1376
+ const components = [];
1377
+ for (let y = 0; y < this.size; y++) {
1378
+ const row = new import_discord6.ActionRowBuilder();
1379
+ for (let x = 0; x < this.size; x++) {
1380
+ row.addComponents(
1381
+ new import_discord6.ButtonBuilder().setStyle(import_discord6.ButtonStyle.Secondary).setLabel("\u200B").setCustomId(`$gamecord-memory-${x}-${y}`)
1382
+ );
1383
+ }
1384
+ components.push(row);
1385
+ }
1386
+ return components;
1387
+ }
1388
+ async gameOver(message, hasTimedOut) {
1389
+ const result = this.result({
1390
+ outcome: hasTimedOut ? "timeout" : "win",
1391
+ tilesTurned: this.tilesTurned,
1392
+ remainingPairs: this.remainingPairs
1393
+ });
1394
+ this.emit("gameOver", result);
1395
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
1396
+ description: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
1397
+ });
1398
+ try {
1399
+ for (const btn of this.components[0].components) {
1400
+ btn.setDisabled(true);
1401
+ }
1402
+ await this.editContextOrMessage(message, {
1403
+ embeds: [embed],
1404
+ components: this.components
1405
+ });
1406
+ } catch (err) {
1407
+ this.emit("error", err);
1408
+ }
1409
+ }
1410
+ async handleButtons(message) {
1411
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
1412
+ collector.on("collect", async (i) => {
1413
+ if (!i.customId.startsWith("$gamecord")) return;
1414
+ if (i.user.id !== this.player.id) {
1415
+ if (this.options.notPlayerMessage) {
1416
+ try {
1417
+ await i.reply({
1418
+ content: this.options.notPlayerMessage(this),
1419
+ flags: import_discord6.MessageFlags.Ephemeral
1420
+ });
1421
+ } catch (err) {
1422
+ this.emit("error", err);
1423
+ }
1424
+ }
1425
+ return;
1426
+ }
1427
+ const x = Number(i.customId.split("-").at(-2));
1428
+ const y = Number(i.customId.split("-").at(-1));
1429
+ const index = y * this.size + x;
1430
+ try {
1431
+ await i.deferUpdate();
1432
+ } catch (err) {
1433
+ this.emit("error", err);
1434
+ return;
1435
+ }
1436
+ const emoji = this.emojis[index];
1437
+ const emojiBtn = this.components[y].components[x];
1438
+ this.tilesTurned += 1;
1439
+ if (!this.selected) {
1440
+ this.selected = { x, y, index };
1441
+ emojiBtn.setEmoji(emoji).setStyle(import_discord6.ButtonStyle.Primary);
1442
+ } else if (this.selected.index === index) {
1443
+ this.selected = null;
1444
+ removeEmoji(emojiBtn).setStyle(import_discord6.ButtonStyle.Secondary);
1445
+ } else {
1446
+ const selectedEmoji = this.emojis[this.selected.index];
1447
+ const selectedBtn = this.components[this.selected.y].components[this.selected.x];
1448
+ const matched = emoji === selectedEmoji || selectedEmoji === jokerEmoji || emoji === jokerEmoji;
1449
+ if (selectedEmoji === jokerEmoji || emoji === jokerEmoji) {
1450
+ const joker = emoji === jokerEmoji ? this.selected : { x, y, index };
1451
+ const pair = this.getPairEmojiPositions(this.emojis[joker.index]).filter(
1452
+ (b) => b.index !== joker.index
1453
+ )[0];
1454
+ const pairBtn = this.components[pair.y].components[pair.x];
1455
+ pairBtn.setEmoji(this.emojis[pair.index]).setStyle(import_discord6.ButtonStyle.Success).setDisabled(true);
1456
+ }
1457
+ emojiBtn.setEmoji(emoji).setStyle(matched ? import_discord6.ButtonStyle.Success : import_discord6.ButtonStyle.Danger).setDisabled(matched);
1458
+ selectedBtn.setEmoji(selectedEmoji).setStyle(matched ? import_discord6.ButtonStyle.Success : import_discord6.ButtonStyle.Danger).setDisabled(matched);
1459
+ if (!matched) {
1460
+ try {
1461
+ await i.editReply({ components: this.components });
1462
+ } catch (err) {
1463
+ this.emit("error", err);
1464
+ }
1465
+ removeEmoji(emojiBtn).setStyle(import_discord6.ButtonStyle.Secondary);
1466
+ removeEmoji(selectedBtn).setStyle(import_discord6.ButtonStyle.Secondary);
1467
+ this.selected = null;
1468
+ return;
1469
+ }
1470
+ this.remainingPairs -= 1;
1471
+ this.selected = null;
1472
+ }
1473
+ if (this.remainingPairs === 0) return collector.stop("$win");
1474
+ try {
1475
+ await i.editReply({ components: this.components });
1476
+ } catch (err) {
1477
+ this.emit("error", err);
1478
+ }
1479
+ });
1480
+ collector.on("end", async (_, reason) => {
1481
+ this.emit("end");
1482
+ if (reason === "$win" || reason === "idle") {
1483
+ await this.gameOver(message, reason === "idle");
1484
+ }
1485
+ });
1486
+ }
1487
+ };
1488
+
1489
+ // src/games/Minesweeper.ts
1490
+ var import_v47 = __toESM(require("zod/v4"));
1491
+ var import_discord7 = require("discord.js");
1492
+ var defaultOptions7 = {
1493
+ embed: {
1494
+ title: "Minesweeper",
1495
+ color: colors.blurple,
1496
+ description: "Click on the buttons to reveal the blocks except mines."
1497
+ },
1498
+ timeout: 12e4,
1499
+ winMessage: /* @__PURE__ */ __name(() => "You won the Game! You successfully avoided all the mines.", "winMessage"),
1500
+ loseMessage: /* @__PURE__ */ __name(() => "You lost the Game! Beaware of the mines next time.", "loseMessage"),
1501
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use this menu.`, "notPlayerMessage"),
1502
+ mines: 5,
1503
+ emojis: {
1504
+ flag: "\u{1F6A9}",
1505
+ mine: "\u{1F4A3}"
1506
+ }
1507
+ };
1508
+ var minesweeperOptions = import_v47.default.object({
1509
+ embed: embedBuilder().optional().default(defaultOptions7.embed),
1510
+ endEmbed: embedBuilder().optional(),
1511
+ winMessage: resultMessage().optional().default(() => defaultOptions7.winMessage),
1512
+ loseMessage: resultMessage().optional().default(() => defaultOptions7.loseMessage),
1513
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions7.notPlayerMessage),
1514
+ timeout: import_v47.default.number().int().optional().default(defaultOptions7.timeout),
1515
+ mines: import_v47.default.number().int().min(1).max(24).optional().default(defaultOptions7.mines),
1516
+ emojis: import_v47.default.object({
1517
+ flag: import_v47.default.string().optional().default(defaultOptions7.emojis.flag),
1518
+ mine: import_v47.default.string().optional().default(defaultOptions7.emojis.mine)
1519
+ }).optional().default(defaultOptions7.emojis)
1520
+ });
1521
+ var Minesweeper = class extends Game {
1522
+ static {
1523
+ __name(this, "Minesweeper");
1524
+ }
1525
+ options;
1526
+ /**
1527
+ * The board of the game.
1528
+ *
1529
+ * - `true` : empty
1530
+ * - `false` : mine
1531
+ * - `number` : empty, with `n` mines around it
1532
+ */
1533
+ board = [];
1534
+ /**
1535
+ * The width/height of the board
1536
+ */
1537
+ size = 5;
1538
+ message = null;
1539
+ constructor(context, options) {
1540
+ super(context);
1541
+ this.options = minesweeperOptions.parse(options || {});
1542
+ for (let y = 0; y < this.size; y++) {
1543
+ for (let x = 0; x < this.size; x++) {
1544
+ this.board[y * this.size + x] = false;
1545
+ }
1546
+ }
1547
+ }
1548
+ async start() {
1549
+ this.plantMines();
1550
+ this.showFirstBlock();
1551
+ const embed = await this.buildEmbed(this.options.embed);
1552
+ this.message = await this.sendMessage({ embeds: [embed], components: this.getComponents() });
1553
+ this.handleButtons(this.message);
1554
+ }
1555
+ handleButtons(message) {
1556
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
1557
+ collector.on("collect", async (i) => {
1558
+ if (!i.customId.startsWith("$gamecord")) return;
1559
+ if (i.user.id !== this.player.id) {
1560
+ if (this.options.notPlayerMessage) {
1561
+ try {
1562
+ await i.reply({
1563
+ content: this.options.notPlayerMessage(this),
1564
+ flags: import_discord7.MessageFlags.Ephemeral
1565
+ });
1566
+ } catch (err) {
1567
+ this.emit("error", err);
1568
+ }
1569
+ }
1570
+ return;
1571
+ }
1572
+ const x = Number(i.customId.split("-").at(-2));
1573
+ const y = Number(i.customId.split("-").at(-1));
1574
+ const index = y * this.size + x;
1575
+ try {
1576
+ await i.deferUpdate();
1577
+ } catch (err) {
1578
+ this.emit("error", err);
1579
+ return;
1580
+ }
1581
+ if (this.board[index] === true) return collector.stop("$end");
1582
+ const mines = this.getMinesAround(x, y);
1583
+ this.board[index] = mines;
1584
+ if (this.hasFoundAllMines()) return collector.stop("$end");
1585
+ try {
1586
+ await i.editReply({ components: this.getComponents() });
1587
+ } catch (err) {
1588
+ this.emit("error", err);
1589
+ }
1590
+ });
1591
+ collector.on("end", async (_, reason) => {
1592
+ this.emit("end");
1593
+ if (reason === "$end" || reason === "idle") {
1594
+ await this.gameOver(message, reason === "idle");
1595
+ }
1596
+ });
1597
+ }
1598
+ async gameOver(message, hasTimedOut) {
1599
+ const result = this.result({
1600
+ outcome: hasTimedOut ? "timeout" : this.hasFoundAllMines() ? "win" : "lose",
1601
+ tilesTurned: this.board.filter(Number.isInteger).length
1602
+ });
1603
+ this.emit("gameOver", result);
1604
+ for (let y = 0; y < this.size; y++) {
1605
+ for (let x = 0; x < this.size; x++) {
1606
+ const index = y * this.size + x;
1607
+ if (this.board[index] !== true) this.board[index] = this.getMinesAround(x, y);
1608
+ }
1609
+ }
1610
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
1611
+ description: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
1612
+ });
1613
+ try {
1614
+ await this.editContextOrMessage(message, {
1615
+ embeds: [embed],
1616
+ components: this.getComponents(true, result.outcome === "win", true)
1617
+ });
1618
+ } catch (err) {
1619
+ this.emit("error", err);
1620
+ }
1621
+ }
1622
+ plantMines() {
1623
+ for (let i = 0; i <= this.options.mines; i++) {
1624
+ const x = Math.floor(Math.random() * 5);
1625
+ const y = Math.floor(Math.random() * 5);
1626
+ const index = y * this.size + x;
1627
+ if (this.board[index] !== true) {
1628
+ this.board[index] = true;
1629
+ } else {
1630
+ i -= 1;
1631
+ }
1632
+ }
1633
+ }
1634
+ getMinesAround(x, y) {
1635
+ let minesAround = 0;
1636
+ for (let row = -1; row < 2; row++) {
1637
+ for (let col = -1; col < 2; col++) {
1638
+ const block = { x: x + col, y: y + row };
1639
+ if (block.x < 0 || block.x >= 5 || block.y < 0 || block.y >= 5) continue;
1640
+ if (row === 0 && col === 0) continue;
1641
+ if (this.board[block.y * this.size + block.x] === true) minesAround += 1;
1642
+ }
1643
+ }
1644
+ return minesAround;
1645
+ }
1646
+ showFirstBlock() {
1647
+ const emptyBlocks = [];
1648
+ for (let y = 0; y < this.size; y++) {
1649
+ for (let x = 0; x < this.size; x++) {
1650
+ if (this.board[y * this.size + x] === true) emptyBlocks.push({ x, y });
1651
+ }
1652
+ }
1653
+ const safeBlocks = emptyBlocks.filter((b) => !this.getMinesAround(b.x, b.y));
1654
+ const blocks = safeBlocks.length ? safeBlocks : emptyBlocks;
1655
+ const rBlock = blocks[Math.floor(Math.random() * blocks.length)];
1656
+ this.board[rBlock.y * this.size + rBlock.x] = this.getMinesAround(rBlock.x, rBlock.y);
1657
+ }
1658
+ hasFoundAllMines() {
1659
+ let found = true;
1660
+ for (let y = 0; y < this.size; y++) {
1661
+ for (let x = 0; x < this.size; x++) {
1662
+ if (this.board[y * this.size + x] === false) found = false;
1663
+ }
1664
+ }
1665
+ return found;
1666
+ }
1667
+ /**
1668
+ * Build the minesweeper grid using buttons.
1669
+ */
1670
+ getComponents(showMines = false, found = false, disabled = false) {
1671
+ const components = [];
1672
+ for (let y = 0; y < this.size; y++) {
1673
+ const row = new import_discord7.ActionRowBuilder();
1674
+ for (let x = 0; x < this.size; x++) {
1675
+ const block = this.board[y * this.size + x];
1676
+ const numberEmoji = typeof block === "number" && block > 0 ? getNumberEmoji(block) : null;
1677
+ const displayMine = block === true && showMines;
1678
+ const btn = new import_discord7.ButtonBuilder().setStyle(
1679
+ displayMine ? found ? import_discord7.ButtonStyle.Success : import_discord7.ButtonStyle.Danger : typeof block === "number" ? import_discord7.ButtonStyle.Secondary : import_discord7.ButtonStyle.Primary
1680
+ ).setCustomId(`$gamecord-minesweeper-${x}-${y}`).setDisabled(disabled);
1681
+ if (displayMine || numberEmoji)
1682
+ btn.setEmoji(
1683
+ displayMine ? found ? this.options.emojis.flag : this.options.emojis.mine : numberEmoji
1684
+ );
1685
+ else {
1686
+ btn.setLabel("\u200B");
1687
+ }
1688
+ row.addComponents(btn);
1689
+ }
1690
+ components.push(row);
1691
+ }
1692
+ return components;
1693
+ }
1694
+ };
1695
+
1696
+ // src/games/RockPaperScissors.ts
1697
+ var import_v48 = __toESM(require("zod/v4"));
1698
+ var import_discord8 = require("discord.js");
1699
+ var defaultOptions8 = {
1700
+ embed: {
1701
+ title: "Rock Paper Scissors",
1702
+ color: colors.blurple,
1703
+ description: "Press a button below to make a choice."
1704
+ },
1705
+ winMessage: /* @__PURE__ */ __name((res) => `**${res.winner}** won the Game! Congratulations!`, "winMessage"),
1706
+ tieMessage: /* @__PURE__ */ __name(() => "The Game tied! No one won the Game!", "tieMessage"),
1707
+ timeoutMessage: /* @__PURE__ */ __name(() => "The Game went unfinished! No one won the Game!", "timeoutMessage"),
1708
+ choiceMessage: /* @__PURE__ */ __name((emoji) => `You choose ${emoji}.`, "choiceMessage"),
1709
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} and ${game.opponent} can use this menu.`, "notPlayerMessage"),
1710
+ buttons: {
1711
+ rock: "Rock",
1712
+ paper: "Paper",
1713
+ scissors: "Scissors"
1714
+ },
1715
+ emojis: {
1716
+ rock: "\u{1F311}",
1717
+ paper: "\u{1F4F0}",
1718
+ scissors: "\u2702\uFE0F"
1719
+ },
1720
+ timeout: 6e4
1721
+ };
1722
+ var rockPaperScissorsOptions = import_v48.default.object({
1723
+ versus: versusOptions("Rock Paper Scissors"),
1724
+ embed: embedBuilder().optional().default(defaultOptions8.embed),
1725
+ endEmbed: embedBuilder().optional(),
1726
+ winMessage: resultMessage().optional().default(() => defaultOptions8.winMessage),
1727
+ tieMessage: resultMessage().optional().default(() => defaultOptions8.tieMessage),
1728
+ timeoutMessage: resultMessage().optional().default(() => defaultOptions8.timeoutMessage),
1729
+ choiceMessage: var2Message().optional().default(() => defaultOptions8.choiceMessage),
1730
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions8.notPlayerMessage),
1731
+ buttonStyle: import_v48.default.number().int().optional().default(import_discord8.ButtonStyle.Primary),
1732
+ buttons: import_v48.default.object({
1733
+ rock: import_v48.default.string().optional().default(defaultOptions8.buttons.rock),
1734
+ paper: import_v48.default.string().optional().default(defaultOptions8.buttons.paper),
1735
+ scissors: import_v48.default.string().optional().default(defaultOptions8.buttons.scissors)
1736
+ }).optional().default(defaultOptions8.buttons),
1737
+ emojis: import_v48.default.object({
1738
+ rock: import_v48.default.string().optional().default(defaultOptions8.emojis.rock),
1739
+ paper: import_v48.default.string().optional().default(defaultOptions8.emojis.paper),
1740
+ scissors: import_v48.default.string().optional().default(defaultOptions8.emojis.scissors)
1741
+ }).optional().default(defaultOptions8.emojis),
1742
+ timeout: import_v48.default.number().int().optional().default(defaultOptions8.timeout)
1743
+ });
1744
+ var RockPaperScissors = class extends VersusGame {
1745
+ static {
1746
+ __name(this, "RockPaperScissors");
1747
+ }
1748
+ options;
1749
+ playerChoice = null;
1750
+ opponentChoice = null;
1751
+ message = null;
1752
+ constructor(context, options) {
1753
+ const parsed = rockPaperScissorsOptions.parse(options || {});
1754
+ super(context, parsed.versus);
1755
+ this.options = parsed;
1756
+ }
1757
+ async start() {
1758
+ this.message = await this.requestVersus();
1759
+ if (this.message) this.runGame(this.message);
1760
+ }
1761
+ async runGame(message) {
1762
+ try {
1763
+ const embed = await this.buildEmbed(this.options.embed);
1764
+ await this.editContextOrMessage(message, {
1765
+ content: null,
1766
+ embeds: [embed],
1767
+ components: [this.getActionRow()]
1768
+ });
1769
+ } catch (err) {
1770
+ this.emit("fatalError", err);
1771
+ this.emit("end");
1772
+ return;
1773
+ }
1774
+ this.handleButtons(message);
1775
+ }
1776
+ handleButtons(message) {
1777
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
1778
+ const emojis = this.options.emojis;
1779
+ const choices = { r: emojis.rock, p: emojis.paper, s: emojis.scissors };
1780
+ collector.on("collect", async (i) => {
1781
+ if (!i.customId.startsWith("$gamecord")) return;
1782
+ if (i.user.id !== this.player.id && i.user.id !== this.opponent.id) {
1783
+ if (this.options.notPlayerMessage) {
1784
+ try {
1785
+ await i.reply({
1786
+ content: this.options.notPlayerMessage(this),
1787
+ flags: import_discord8.MessageFlags.Ephemeral
1788
+ });
1789
+ } catch (err) {
1790
+ this.emit("error", err);
1791
+ }
1792
+ }
1793
+ return;
1794
+ }
1795
+ const choice = choices[i.customId.split("-").at(-1)];
1796
+ let replyChoice = false;
1797
+ if (i.user.id === this.player.id && !this.playerChoice) {
1798
+ this.playerChoice = choice;
1799
+ replyChoice = true;
1800
+ } else if (i.user.id === this.opponent.id && !this.opponentChoice) {
1801
+ this.opponentChoice = choice;
1802
+ replyChoice = true;
1803
+ }
1804
+ if (replyChoice) {
1805
+ try {
1806
+ await i.reply({
1807
+ content: this.options.choiceMessage(choice, this),
1808
+ flags: import_discord8.MessageFlags.Ephemeral
1809
+ });
1810
+ } catch (err) {
1811
+ this.emit("error", err);
1812
+ }
1813
+ }
1814
+ if (!i.replied) {
1815
+ try {
1816
+ await i.deferUpdate();
1817
+ } catch (err) {
1818
+ this.emit("error", err);
1819
+ return;
1820
+ }
1821
+ }
1822
+ if (this.playerChoice && this.opponentChoice) return collector.stop("$end");
1823
+ });
1824
+ collector.on("end", async (_, reason) => {
1825
+ this.emit("end");
1826
+ if (reason === "idle" || reason === "$end") {
1827
+ await this.gameOver(message);
1828
+ }
1829
+ });
1830
+ }
1831
+ getResult() {
1832
+ if (!this.playerChoice && !this.opponentChoice) return "timeout";
1833
+ else if (this.playerChoice === this.opponentChoice) return "tie";
1834
+ else return "win";
1835
+ }
1836
+ hasPlayer1Won() {
1837
+ const { rock: r, paper: p, scissors: s } = this.options.emojis;
1838
+ return this.playerChoice === s && this.opponentChoice === p || this.playerChoice === r && this.opponentChoice === s || this.playerChoice === p && this.opponentChoice === r;
1839
+ }
1840
+ getActionRow(disabled = false) {
1841
+ return new import_discord8.ActionRowBuilder().setComponents(
1842
+ new import_discord8.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(this.options.emojis.rock).setCustomId("$gamecord-rps-r").setLabel(this.options.buttons.rock).setDisabled(disabled),
1843
+ new import_discord8.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(this.options.emojis.paper).setCustomId("$gamecord-rps-p").setLabel(this.options.buttons.paper).setDisabled(disabled),
1844
+ new import_discord8.ButtonBuilder().setStyle(this.options.buttonStyle).setEmoji(this.options.emojis.scissors).setCustomId("$gamecord-rps-s").setLabel(this.options.buttons.scissors).setDisabled(disabled)
1845
+ );
1846
+ }
1847
+ async gameOver(message) {
1848
+ const outcome = this.getResult();
1849
+ const result = this.result({
1850
+ opponent: this.opponent,
1851
+ outcome,
1852
+ winner: outcome === "win" ? this.hasPlayer1Won() ? this.player : this.opponent : null,
1853
+ playerChoice: this.playerChoice,
1854
+ opponentChoice: this.opponentChoice
1855
+ });
1856
+ this.emit("gameOver", result);
1857
+ let getMessage;
1858
+ switch (outcome) {
1859
+ case "win": {
1860
+ getMessage = this.options.winMessage;
1861
+ break;
1862
+ }
1863
+ case "tie": {
1864
+ getMessage = this.options.tieMessage;
1865
+ break;
1866
+ }
1867
+ case "timeout": {
1868
+ getMessage = this.options.timeoutMessage;
1869
+ break;
1870
+ }
1871
+ }
1872
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
1873
+ description: getMessage(result, this),
1874
+ fields: [
1875
+ {
1876
+ name: this.player.displayName,
1877
+ value: this.playerChoice ?? "\u2754",
1878
+ inline: true
1879
+ },
1880
+ {
1881
+ name: "VS",
1882
+ value: "\u26A1",
1883
+ inline: true
1884
+ },
1885
+ {
1886
+ name: this.opponent.displayName,
1887
+ value: this.opponentChoice ?? "\u2754",
1888
+ inline: true
1889
+ }
1890
+ ]
1891
+ });
1892
+ try {
1893
+ await this.editContextOrMessage(message, {
1894
+ embeds: [embed],
1895
+ components: [this.getActionRow(true)]
1896
+ });
1897
+ } catch (err) {
1898
+ this.emit("error", err);
1899
+ }
1900
+ }
1901
+ };
1902
+
1903
+ // src/games/TicTacToe.ts
1904
+ var import_v49 = __toESM(require("zod/v4"));
1905
+ var import_discord9 = require("discord.js");
1906
+ var defaultOptions9 = {
1907
+ embed: {
1908
+ title: "Tic Tac Toe",
1909
+ color: colors.blurple
1910
+ },
1911
+ winMessage: /* @__PURE__ */ __name((res) => `${res.winnerEmoji} | **${res.winner}** won the TicTacToe Game.`, "winMessage"),
1912
+ tieMessage: /* @__PURE__ */ __name(() => "The Game tied! No one won the Game!", "tieMessage"),
1913
+ timeoutMessage: /* @__PURE__ */ __name(() => "The Game went unfinished! No one won the Game!", "timeoutMessage"),
1914
+ turnMessage: /* @__PURE__ */ __name((turn) => `${turn.emoji} | It's player **${turn.player.displayName}**'s turn.`, "turnMessage"),
1915
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} and ${game.opponent} can use this menu.`, "notPlayerMessage"),
1916
+ emojis: {
1917
+ xButton: "\u274C",
1918
+ oButton: "\u{1F535}"
1919
+ },
1920
+ styles: {
1921
+ xButton: import_discord9.ButtonStyle.Secondary,
1922
+ oButton: import_discord9.ButtonStyle.Secondary
1923
+ },
1924
+ timeout: 6e4
1925
+ };
1926
+ var ticTacToeOptions = import_v49.default.object({
1927
+ versus: versusOptions("Tic Tac Toe"),
1928
+ embed: embedBuilder().optional().default(defaultOptions9.embed),
1929
+ endEmbed: embedBuilder().optional(),
1930
+ winMessage: resultMessage().optional().default(() => defaultOptions9.winMessage),
1931
+ tieMessage: resultMessage().optional().default(() => defaultOptions9.tieMessage),
1932
+ timeoutMessage: resultMessage().optional().default(() => defaultOptions9.timeoutMessage),
1933
+ turnMessage: var2Message().optional().default(() => defaultOptions9.turnMessage),
1934
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions9.notPlayerMessage),
1935
+ emojis: import_v49.default.object({
1936
+ xButton: import_v49.default.string().optional().default(defaultOptions9.emojis.xButton),
1937
+ oButton: import_v49.default.string().optional().default(defaultOptions9.emojis.oButton)
1938
+ }).optional().default(defaultOptions9.emojis),
1939
+ styles: import_v49.default.object({
1940
+ xButton: import_v49.default.number().int().optional().default(defaultOptions9.styles.xButton),
1941
+ oButton: import_v49.default.number().int().optional().default(defaultOptions9.styles.oButton)
1942
+ }).optional().default(defaultOptions9.styles),
1943
+ timeout: import_v49.default.number().int().optional().default(defaultOptions9.timeout)
1944
+ });
1945
+ var TicTacToe = class extends VersusGame {
1946
+ static {
1947
+ __name(this, "TicTacToe");
1948
+ }
1949
+ options;
1950
+ /**
1951
+ * A 9-elements array.
1952
+ *
1953
+ * - `0` : empty
1954
+ * - `1` : X (player 1)
1955
+ * - `2` : O (player 2)
1956
+ */
1957
+ gameboard = [0, 0, 0, 0, 0, 0, 0, 0, 0];
1958
+ isPlayer1Turn = true;
1959
+ message = null;
1960
+ locked = false;
1961
+ constructor(context, options) {
1962
+ const parsed = ticTacToeOptions.parse(options || {});
1963
+ super(context, parsed.versus);
1964
+ this.options = parsed;
1965
+ }
1966
+ async start() {
1967
+ this.message = await this.requestVersus();
1968
+ if (this.message) this.runGame(this.message);
1969
+ }
1970
+ async runGame(message) {
1971
+ try {
1972
+ const embed = await this.buildEmbed(this.options.embed, {
1973
+ description: this.options.turnMessage(this.getPlayerTurnData(), this)
1974
+ });
1975
+ await this.editContextOrMessage(message, {
1976
+ content: null,
1977
+ embeds: [embed],
1978
+ components: this.getComponents()
1979
+ });
1980
+ } catch (err) {
1981
+ this.emit("fatalError", err);
1982
+ this.emit("end");
1983
+ return;
1984
+ }
1985
+ this.handleButtons(message);
1986
+ }
1987
+ handleButtons(message) {
1988
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
1989
+ collector.on("collect", async (i) => {
1990
+ if (!i.customId.startsWith("$gamecord")) return;
1991
+ if (i.user.id !== this.player.id && i.user.id !== this.opponent.id) {
1992
+ if (this.options.notPlayerMessage) {
1993
+ try {
1994
+ await i.reply({
1995
+ content: this.options.notPlayerMessage(this),
1996
+ flags: import_discord9.MessageFlags.Ephemeral
1997
+ });
1998
+ } catch (err) {
1999
+ this.emit("error", err);
2000
+ }
2001
+ }
2002
+ return;
2003
+ }
2004
+ try {
2005
+ await i.deferUpdate();
2006
+ } catch (err) {
2007
+ this.emit("error", err);
2008
+ return;
2009
+ }
2010
+ if (this.locked) return;
2011
+ this.locked = true;
2012
+ try {
2013
+ if (i.user.id !== (this.isPlayer1Turn ? this.player : this.opponent).id) return;
2014
+ const index = Number(i.customId.split("-").at(-1));
2015
+ if (this.gameboard[index] !== 0) return;
2016
+ this.gameboard[index] = this.isPlayer1Turn ? 1 : 2;
2017
+ if (this.hasWonGame(1) || this.hasWonGame(2)) return collector.stop("$win");
2018
+ if (!this.gameboard.includes(0)) return collector.stop("$tie");
2019
+ this.isPlayer1Turn = !this.isPlayer1Turn;
2020
+ const embed = await this.buildEmbed(this.options.embed, {
2021
+ description: this.options.turnMessage(this.getPlayerTurnData(), this)
2022
+ });
2023
+ try {
2024
+ await i.editReply({ embeds: [embed], components: this.getComponents() });
2025
+ } catch (err) {
2026
+ this.emit("error", err);
2027
+ }
2028
+ } finally {
2029
+ this.locked = false;
2030
+ }
2031
+ });
2032
+ collector.on("end", async (_, reason) => {
2033
+ this.emit("end");
2034
+ if (reason === "$win" || reason === "$tie") {
2035
+ return await this.gameOver(message, reason === "$win" ? "win" : "tie");
2036
+ }
2037
+ if (reason === "idle") {
2038
+ return await this.gameOver(message, "timeout");
2039
+ }
2040
+ });
2041
+ }
2042
+ async gameOver(message, outcome) {
2043
+ const winner = this.hasWonGame(1) ? this.player : this.opponent;
2044
+ const winnerEmoji = this.hasWonGame(1) ? this.options.emojis.xButton : this.options.emojis.oButton;
2045
+ const result = this.result({
2046
+ opponent: this.opponent,
2047
+ outcome,
2048
+ winner: outcome === "win" ? winner : null,
2049
+ winnerEmoji: outcome === "win" ? winnerEmoji : null
2050
+ });
2051
+ this.emit("gameOver", result);
2052
+ let getMessage;
2053
+ switch (outcome) {
2054
+ case "win": {
2055
+ getMessage = this.options.winMessage;
2056
+ break;
2057
+ }
2058
+ case "tie": {
2059
+ getMessage = this.options.tieMessage;
2060
+ break;
2061
+ }
2062
+ case "timeout": {
2063
+ getMessage = this.options.timeoutMessage;
2064
+ break;
2065
+ }
2066
+ }
2067
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
2068
+ description: getMessage(result, this)
2069
+ });
2070
+ try {
2071
+ await this.editContextOrMessage(message, {
2072
+ embeds: [embed],
2073
+ components: this.getComponents(true)
2074
+ });
2075
+ } catch (err) {
2076
+ this.emit("error", err);
2077
+ }
2078
+ }
2079
+ hasWonGame(player) {
2080
+ if (this.gameboard[0] === this.gameboard[4] && this.gameboard[0] === this.gameboard[8] && this.gameboard[0] === player) {
2081
+ return true;
2082
+ } else if (this.gameboard[6] === this.gameboard[4] && this.gameboard[6] === this.gameboard[2] && this.gameboard[6] === player) {
2083
+ return true;
2084
+ }
2085
+ for (let i = 0; i < 3; ++i) {
2086
+ if (this.gameboard[i * 3] === this.gameboard[i * 3 + 1] && this.gameboard[i * 3] === this.gameboard[i * 3 + 2] && this.gameboard[i * 3] === player) {
2087
+ return true;
2088
+ }
2089
+ if (this.gameboard[i] === this.gameboard[i + 3] && this.gameboard[i] === this.gameboard[i + 6] && this.gameboard[i] === player) {
2090
+ return true;
2091
+ }
2092
+ }
2093
+ return false;
2094
+ }
2095
+ getPlayerTurnData() {
2096
+ return this.isPlayer1Turn ? { player: this.player, emoji: this.options.emojis.xButton } : { player: this.opponent, emoji: this.options.emojis.oButton };
2097
+ }
2098
+ getButtonData(player) {
2099
+ if (player === 1) return { emoji: this.options.emojis.xButton, style: this.options.styles.xButton };
2100
+ else if (player === 2) return { emoji: this.options.emojis.oButton, style: this.options.styles.oButton };
2101
+ else return { emoji: null, style: import_discord9.ButtonStyle.Secondary };
2102
+ }
2103
+ getComponents(disabled = false) {
2104
+ const components = [];
2105
+ for (let x = 0; x < 3; x++) {
2106
+ const row = new import_discord9.ActionRowBuilder();
2107
+ for (let y = 0; y < 3; y++) {
2108
+ const index = y * 3 + x;
2109
+ const data = this.getButtonData(this.gameboard[index]);
2110
+ const btn = new import_discord9.ButtonBuilder().setStyle(data.style).setCustomId(`$gamecord-tictactoe-${index}`);
2111
+ if (data.emoji) {
2112
+ btn.setEmoji(data.emoji);
2113
+ } else {
2114
+ btn.setLabel("\u200B");
2115
+ }
2116
+ if (this.gameboard[y * 3 + x] !== 0) btn.setDisabled(true);
2117
+ if (disabled) btn.setDisabled(true);
2118
+ row.addComponents(btn);
2119
+ }
2120
+ components.push(row);
2121
+ }
2122
+ return components;
2123
+ }
2124
+ };
2125
+
2126
+ // src/games/Trivia.ts
2127
+ var import_v410 = __toESM(require("zod/v4"));
2128
+ var import_discord10 = require("discord.js");
2129
+ var triviaAPISchema = import_v410.default.object({
2130
+ results: import_v410.default.array(
2131
+ import_v410.default.object({
2132
+ difficulty: import_v410.default.string(),
2133
+ category: import_v410.default.string(),
2134
+ question: import_v410.default.string(),
2135
+ correct_answer: import_v410.default.string(),
2136
+ incorrect_answers: import_v410.default.array(import_v410.default.string())
2137
+ })
2138
+ )
2139
+ });
2140
+ var difficulties = ["easy", "medium", "hard"];
2141
+ var defaultOptions10 = {
2142
+ embed: {
2143
+ title: "Trivia",
2144
+ color: colors.blurple,
2145
+ fields: [
2146
+ {
2147
+ name: "\u200B",
2148
+ value: "You have 60 seconds to guess the answer."
2149
+ }
2150
+ ]
2151
+ },
2152
+ winMessage: /* @__PURE__ */ __name((res) => `You won! The correct answer was ${res.trivia.answer}.`, "winMessage"),
2153
+ loseMessage: /* @__PURE__ */ __name((res) => `You lost! The correct answer was ${res.trivia.answer}.`, "loseMessage"),
2154
+ errorMessage: /* @__PURE__ */ __name(() => "Unable to fetch question data! Please try again.", "errorMessage"),
2155
+ notPlayerMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use this menu.`, "notPlayerMessage"),
2156
+ mode: "multiple",
2157
+ timeout: 6e4,
2158
+ trueText: "True",
2159
+ falseText: "False"
2160
+ };
2161
+ var triviaOptions = import_v410.default.object({
2162
+ embed: embedBuilder().optional().default(defaultOptions10.embed),
2163
+ endEmbed: embedBuilder().optional(),
2164
+ winMessage: resultMessage().optional().default(() => defaultOptions10.winMessage),
2165
+ loseMessage: resultMessage().optional().default(() => defaultOptions10.loseMessage),
2166
+ errorMessage: gameMessage().optional().default(() => defaultOptions10.errorMessage),
2167
+ notPlayerMessage: gameMessage().optional().default(() => defaultOptions10.notPlayerMessage),
2168
+ mode: import_v410.default.enum(["multiple", "single"]).optional().default(defaultOptions10.mode),
2169
+ difficulty: import_v410.default.enum(difficulties).optional(),
2170
+ trivia: import_v410.default.custom().optional(),
2171
+ timeout: import_v410.default.number().int().optional().default(defaultOptions10.timeout),
2172
+ trueText: import_v410.default.string().optional().default(defaultOptions10.trueText),
2173
+ falseText: import_v410.default.string().optional().default(defaultOptions10.falseText)
2174
+ });
2175
+ var Trivia = class extends Game {
2176
+ static {
2177
+ __name(this, "Trivia");
2178
+ }
2179
+ options;
2180
+ difficulty;
2181
+ selected = null;
2182
+ trivia = null;
2183
+ message = null;
2184
+ constructor(context, options) {
2185
+ super(context);
2186
+ this.options = triviaOptions.parse(options || {});
2187
+ this.difficulty = this.options.difficulty || getRandomElement(difficulties);
2188
+ }
2189
+ async start() {
2190
+ this.trivia = await this.getTriviaQuestion();
2191
+ if (!this.trivia) {
2192
+ try {
2193
+ await this.sendMessage({
2194
+ content: this.options.errorMessage(this)
2195
+ });
2196
+ } catch (err) {
2197
+ this.emit("error", err);
2198
+ }
2199
+ return;
2200
+ }
2201
+ const embed = await this.buildEmbed(this.options.embed, {
2202
+ description: `**${this.trivia.question}**
2203
+
2204
+ **Difficulty:** ${this.trivia.difficulty}
2205
+ **Category:** ${this.trivia.category}`
2206
+ });
2207
+ this.message = await this.sendMessage({ embeds: [embed], components: this.getComponents() });
2208
+ this.handleButtons(this.message);
2209
+ }
2210
+ handleButtons(message) {
2211
+ const trivia = this.trivia;
2212
+ const collector = message.createMessageComponentCollector({ idle: this.options.timeout });
2213
+ collector.on("collect", async (i) => {
2214
+ if (!i.customId.startsWith("$gamecord")) return;
2215
+ if (i.user.id !== this.player.id) {
2216
+ if (this.options.notPlayerMessage) {
2217
+ try {
2218
+ await i.reply({
2219
+ content: this.options.notPlayerMessage(this),
2220
+ flags: import_discord10.MessageFlags.Ephemeral
2221
+ });
2222
+ } catch (err) {
2223
+ this.emit("error", err);
2224
+ }
2225
+ }
2226
+ return;
2227
+ }
2228
+ collector.stop();
2229
+ this.selected = Number(i.customId.split("-").at(-1));
2230
+ try {
2231
+ await i.deferUpdate();
2232
+ } catch (err) {
2233
+ this.emit("error", err);
2234
+ }
2235
+ await this.gameOver(message, trivia.options[this.selected] === trivia.answer);
2236
+ });
2237
+ collector.on("end", async (_, reason) => {
2238
+ this.emit("end");
2239
+ if (reason === "idle") {
2240
+ await this.gameOver(message, false, true);
2241
+ }
2242
+ });
2243
+ }
2244
+ async gameOver(message, hasWon, hasTimedOut = false) {
2245
+ const trivia = this.trivia;
2246
+ const result = this.result({
2247
+ outcome: hasTimedOut ? "timeout" : hasWon ? "win" : "lose",
2248
+ trivia,
2249
+ selected: this.selected
2250
+ });
2251
+ this.emit("gameOver", result);
2252
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
2253
+ description: `**${trivia.question}**
2254
+
2255
+ **Difficulty:** ${trivia.difficulty}
2256
+ **Category:** ${trivia.category}`,
2257
+ fields: [
2258
+ {
2259
+ name: "Game Over",
2260
+ value: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
2261
+ }
2262
+ ]
2263
+ });
2264
+ try {
2265
+ await this.editContextOrMessage(message, {
2266
+ embeds: [embed],
2267
+ components: this.getComponents(true)
2268
+ });
2269
+ } catch (err) {
2270
+ this.emit("error", err);
2271
+ }
2272
+ }
2273
+ /**
2274
+ * Build the selection buttons.
2275
+ */
2276
+ getComponents(ended = false) {
2277
+ const trivia = this.trivia;
2278
+ const row = new import_discord10.ActionRowBuilder();
2279
+ if (ended && this.selected === null) {
2280
+ this.selected = trivia.options.indexOf(trivia.answer);
2281
+ }
2282
+ if (this.options.mode === "multiple") {
2283
+ for (let i = 0; i < 4; i++) {
2284
+ row.addComponents(
2285
+ new import_discord10.ButtonBuilder().setStyle(import_discord10.ButtonStyle.Secondary).setCustomId(`$gamecord-trivia-${i}`).setLabel(trivia.options[i]).setDisabled(ended)
2286
+ );
2287
+ }
2288
+ if (this.selected !== null) {
2289
+ if (trivia.answer !== trivia.options[this.selected]) {
2290
+ row.components[this.selected].setStyle(import_discord10.ButtonStyle.Danger);
2291
+ } else {
2292
+ row.components[this.selected].setStyle(import_discord10.ButtonStyle.Success);
2293
+ }
2294
+ }
2295
+ } else {
2296
+ row.setComponents(
2297
+ new import_discord10.ButtonBuilder().setStyle(
2298
+ this.selected === 0 ? trivia.answer === "True" ? import_discord10.ButtonStyle.Success : import_discord10.ButtonStyle.Danger : import_discord10.ButtonStyle.Secondary
2299
+ ).setCustomId("$gamecord-trivia-0").setLabel(this.options.trueText).setDisabled(ended),
2300
+ new import_discord10.ButtonBuilder().setStyle(
2301
+ this.selected === 1 ? trivia.answer === "False" ? import_discord10.ButtonStyle.Success : import_discord10.ButtonStyle.Danger : import_discord10.ButtonStyle.Secondary
2302
+ ).setCustomId("$gamecord-trivia-1").setLabel(this.options.falseText).setDisabled(ended)
2303
+ );
2304
+ }
2305
+ return [row];
2306
+ }
2307
+ async getTriviaQuestion() {
2308
+ if (this.options.trivia) {
2309
+ return await this.options.trivia(this);
2310
+ }
2311
+ const mode = this.options.mode.replace("single", "boolean");
2312
+ let data;
2313
+ try {
2314
+ const res = await fetch(
2315
+ `https://opentdb.com/api.php?amount=1&type=${mode}&difficulty=${this.difficulty}&encode=url3986`
2316
+ );
2317
+ const json = await res.json();
2318
+ if (json.response_code === 5) {
2319
+ throw new Error("Ratelimited by opentdb");
2320
+ }
2321
+ data = triviaAPISchema.parse(json);
2322
+ data = data.results[0];
2323
+ } catch (err) {
2324
+ this.emit("error", err);
2325
+ return null;
2326
+ }
2327
+ if (!data) {
2328
+ return null;
2329
+ }
2330
+ const trivia = {
2331
+ question: decodeURIComponent(data.question),
2332
+ difficulty: decodeURIComponent(data.difficulty),
2333
+ category: decodeURIComponent(data.category),
2334
+ answer: decodeURIComponent(data.correct_answer),
2335
+ options: []
2336
+ };
2337
+ if (mode === "multiple") {
2338
+ data.incorrect_answers.push(data.correct_answer);
2339
+ trivia.options = shuffleArray(data.incorrect_answers).map((a) => decodeURIComponent(a));
2340
+ } else {
2341
+ trivia.options = ["True", "False"];
2342
+ }
2343
+ return trivia;
2344
+ }
2345
+ };
2346
+
2347
+ // src/games/Wordle.ts
2348
+ var import_v411 = __toESM(require("zod/v4"));
2349
+ var import_canvas2 = require("@napi-rs/canvas");
2350
+ var import_discord11 = require("discord.js");
2351
+
2352
+ // src/data/wordle.json
2353
+ var wordle_default = [
2354
+ "APPLE",
2355
+ "BEACH",
2356
+ "CHAIR",
2357
+ "DREAM",
2358
+ "EAGLE",
2359
+ "FIGHT",
2360
+ "GRADE",
2361
+ "HEART",
2362
+ "IDEAS",
2363
+ "JELLY",
2364
+ "KNIFE",
2365
+ "LIGHT",
2366
+ "MOUSE",
2367
+ "NOVEL",
2368
+ "OCEAN",
2369
+ "PLANT",
2370
+ "QUICK",
2371
+ "READY",
2372
+ "SHEEP",
2373
+ "TIGER",
2374
+ "UNION",
2375
+ "VALUE",
2376
+ "WHALE",
2377
+ "YOUNG",
2378
+ "ZEBRA",
2379
+ "ABOVE",
2380
+ "BLANK",
2381
+ "CRISP",
2382
+ "DIRTY",
2383
+ "ERROR",
2384
+ "FLUID",
2385
+ "GIANT",
2386
+ "HAPPY",
2387
+ "INDEX",
2388
+ "JOKER",
2389
+ "KNEEL",
2390
+ "LEMON",
2391
+ "MAGIC",
2392
+ "NIGHT",
2393
+ "ORDER",
2394
+ "POWER",
2395
+ "RIVER",
2396
+ "SCOPE",
2397
+ "SNAKE",
2398
+ "TABLE",
2399
+ "ULTRA",
2400
+ "VOICE",
2401
+ "WATCH",
2402
+ "YIELD",
2403
+ "ZONAL"
2404
+ ];
2405
+
2406
+ // src/games/Wordle.ts
2407
+ var defaultOptions11 = {
2408
+ embed: {
2409
+ title: "Wordle",
2410
+ color: colors.blurple
2411
+ },
2412
+ winMessage: /* @__PURE__ */ __name((res) => `You won! The word was **${res.word}**.`, "winMessage"),
2413
+ loseMessage: /* @__PURE__ */ __name((res) => `You lost! The word was **${res.word}**.`, "loseMessage"),
2414
+ notOwnedMessage: /* @__PURE__ */ __name((game) => `Only ${game.player} can use this menu.`, "notOwnedMessage"),
2415
+ timeout: 12e4
2416
+ };
2417
+ var wordleOptions = import_v411.default.object({
2418
+ embed: embedBuilder().optional().default(defaultOptions11.embed),
2419
+ endEmbed: embedBuilder().optional(),
2420
+ winMessage: resultMessage().optional().default(() => defaultOptions11.winMessage),
2421
+ loseMessage: resultMessage().optional().default(() => defaultOptions11.loseMessage),
2422
+ timeout: import_v411.default.number().int().optional().default(defaultOptions11.timeout),
2423
+ word: import_v411.default.string().length(5).optional()
2424
+ });
2425
+ var Wordle = class extends Game {
2426
+ static {
2427
+ __name(this, "Wordle");
2428
+ }
2429
+ options;
2430
+ /**
2431
+ * The word to guess, in lowercase. Either the one specified or a random one.
2432
+ */
2433
+ word;
2434
+ guesses = [];
2435
+ message = null;
2436
+ collector = null;
2437
+ constructor(context, options) {
2438
+ super(context);
2439
+ this.options = wordleOptions.parse(options || {});
2440
+ this.word = (this.options.word || getRandomElement(wordle_default)).toLowerCase();
2441
+ }
2442
+ async start() {
2443
+ const embed = await this.buildEmbed(this.options.embed, {
2444
+ image: { url: "attachment://board.png" }
2445
+ });
2446
+ this.message = await this.sendMessage({ embeds: [embed], files: [await this.getBoardAttachment()] });
2447
+ const message = this.message;
2448
+ if (!message.channel.isSendable()) return;
2449
+ const collector = message.channel.createMessageCollector({
2450
+ idle: this.options.timeout,
2451
+ filter: /* @__PURE__ */ __name((msg) => msg.author.id === this.player.id && msg.content.length === 5, "filter")
2452
+ });
2453
+ this.collector = collector;
2454
+ collector.on("collect", async (msg) => {
2455
+ const guess = msg.content.toLowerCase();
2456
+ if (msg.deletable) {
2457
+ try {
2458
+ await msg.delete();
2459
+ } catch (err) {
2460
+ this.emit("error", err);
2461
+ }
2462
+ }
2463
+ this.guesses.push(guess);
2464
+ if (this.word === guess || this.guesses.length > 5) return collector.stop("$end");
2465
+ try {
2466
+ await this.editContextOrMessage(message, { embeds: [embed], files: [await this.getBoardAttachment()] });
2467
+ } catch (err) {
2468
+ this.emit("error", err);
2469
+ }
2470
+ });
2471
+ collector.on("end", async (_, reason) => {
2472
+ this.emit("end");
2473
+ if (reason === "$end" || reason === "idle") {
2474
+ await this.gameOver(message, reason === "idle");
2475
+ }
2476
+ });
2477
+ }
2478
+ async gameOver(message, isTimeout) {
2479
+ const result = this.result({
2480
+ outcome: isTimeout ? "timeout" : this.guesses.includes(this.word) ? "win" : "lose",
2481
+ word: this.word,
2482
+ guesses: this.guesses
2483
+ });
2484
+ this.emit("gameOver", result);
2485
+ const embed = await this.buildEndEmbed(this.options.embed, this.options.endEmbed, result, {
2486
+ image: {
2487
+ url: "attachment://board.png"
2488
+ },
2489
+ fields: [
2490
+ {
2491
+ name: "Game Over",
2492
+ value: result.outcome === "win" ? this.options.winMessage(result, this) : this.options.loseMessage(result, this)
2493
+ }
2494
+ ]
2495
+ });
2496
+ try {
2497
+ await this.editContextOrMessage(message, {
2498
+ embeds: [embed],
2499
+ files: [await this.getBoardAttachment()]
2500
+ });
2501
+ } catch (err) {
2502
+ this.emit("error", err);
2503
+ }
2504
+ }
2505
+ async getBoardAttachment() {
2506
+ return new import_discord11.AttachmentBuilder(await this.getBoardImage(), { name: "board.png" });
2507
+ }
2508
+ async getBoardImage() {
2509
+ const rows = 6;
2510
+ const cols = 5;
2511
+ const tile = 88;
2512
+ const gap = 12;
2513
+ const padding = 20;
2514
+ const width = padding * 2 + cols * tile + (cols - 1) * gap;
2515
+ const height = padding * 2 + rows * tile + (rows - 1) * gap;
2516
+ const canvas = (0, import_canvas2.createCanvas)(width, height);
2517
+ const ctx = canvas.getContext("2d");
2518
+ const PALETTE = {
2519
+ green: "#6aaa64",
2520
+ yellow: "#c9b458",
2521
+ gray: "#3a3a3c",
2522
+ empty: "#121213",
2523
+ border: "#3a3a3c",
2524
+ bg: "#121213",
2525
+ textLight: "#ffffff"
2526
+ };
2527
+ ctx.fillStyle = PALETTE.bg;
2528
+ ctx.fillRect(0, 0, width, height);
2529
+ ctx.textAlign = "center";
2530
+ ctx.textBaseline = "middle";
2531
+ ctx.font = `700 ${Math.floor(tile * 0.54)}px sans-serif`;
2532
+ function scoreGuess(guess, answer) {
2533
+ const res = Array(cols).fill(PALETTE.gray);
2534
+ const a = answer.split("");
2535
+ const g = guess.split("");
2536
+ const counts = {};
2537
+ for (let i = 0; i < a.length; i++) counts[a[i]] = (counts[a[i]] || 0) + 1;
2538
+ for (let i = 0; i < cols; i++) {
2539
+ if (g[i] === a[i]) {
2540
+ res[i] = PALETTE.green;
2541
+ counts[g[i]] = Math.max(0, counts[g[i]] - 1);
2542
+ }
2543
+ }
2544
+ for (let i = 0; i < cols; i++) {
2545
+ if (res[i] === PALETTE.green) continue;
2546
+ const ch = g[i];
2547
+ if (counts[ch] && counts[ch] > 0) {
2548
+ res[i] = PALETTE.yellow;
2549
+ counts[ch]--;
2550
+ } else {
2551
+ res[i] = PALETTE.gray;
2552
+ }
2553
+ }
2554
+ return res;
2555
+ }
2556
+ __name(scoreGuess, "scoreGuess");
2557
+ for (let r = 0; r < rows; r++) {
2558
+ const guess = this.guesses[r];
2559
+ const colors2 = guess ? scoreGuess(guess.toLowerCase(), this.word.toLowerCase()) : Array(cols).fill(PALETTE.empty);
2560
+ for (let c = 0; c < cols; c++) {
2561
+ const x = padding + c * (tile + gap);
2562
+ const y = padding + r * (tile + gap);
2563
+ ctx.fillStyle = colors2[c] === PALETTE.empty ? PALETTE.empty : colors2[c];
2564
+ const radius = 8;
2565
+ ctx.beginPath();
2566
+ ctx.moveTo(x + radius, y);
2567
+ ctx.lineTo(x + tile - radius, y);
2568
+ ctx.quadraticCurveTo(x + tile, y, x + tile, y + radius);
2569
+ ctx.lineTo(x + tile, y + tile - radius);
2570
+ ctx.quadraticCurveTo(x + tile, y + tile, x + tile - radius, y + tile);
2571
+ ctx.lineTo(x + radius, y + tile);
2572
+ ctx.quadraticCurveTo(x, y + tile, x, y + tile - radius);
2573
+ ctx.lineTo(x, y + radius);
2574
+ ctx.quadraticCurveTo(x, y, x + radius, y);
2575
+ ctx.closePath();
2576
+ ctx.fill();
2577
+ ctx.lineWidth = 4;
2578
+ ctx.strokeStyle = colors2[c] === PALETTE.empty ? PALETTE.border : colors2[c];
2579
+ ctx.stroke();
2580
+ const ch = guess ? (guess[c] || "").toUpperCase() : "";
2581
+ if (ch) {
2582
+ ctx.fillStyle = PALETTE.textLight;
2583
+ ctx.fillText(ch, x + tile / 2, y + tile / 2 + 2);
2584
+ }
2585
+ }
2586
+ }
2587
+ return canvas.toBuffer("image/png");
2588
+ }
2589
+ };
2590
+ // Annotate the CommonJS export names for ESM import in node:
2591
+ 0 && (module.exports = {
2592
+ Connect4,
2593
+ FastType,
2594
+ Flood,
2595
+ Game2048,
2596
+ Memory,
2597
+ Minesweeper,
2598
+ RockPaperScissors,
2599
+ TicTacToe,
2600
+ Trivia,
2601
+ Wordle,
2602
+ connect4Options,
2603
+ fastTypeOptions,
2604
+ floodOptions,
2605
+ game2048Options,
2606
+ jokerEmoji,
2607
+ memoryOptions,
2608
+ minesweeperOptions,
2609
+ rockPaperScissorsOptions,
2610
+ ticTacToeOptions,
2611
+ triviaOptions,
2612
+ wordleOptions
2613
+ });
2614
+ //# sourceMappingURL=index.js.map