-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
433 lines (391 loc) · 14.7 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
import { Hono } from "https://deno.land/x/[email protected]/mod.ts";
import { serve } from "https://deno.land/[email protected]/http/server.ts";
import { serveStatic } from "https://deno.land/x/[email protected]/middleware.ts";
import "https://deno.land/x/[email protected]/load.ts";
import {
defaultSetupRoles,
defaultSpecialRoles,
images,
} from "./lib/defaults.ts";
import { privateMessages, roleMessges } from "./lib/messages.ts";
import { logJson, sendSlackMessage } from "./lib/helpers.ts";
import shuffle from "./lib/shuffle.ts";
declare global {
interface Array<T> {
random(): T;
}
}
Array.prototype.random = function () {
return this[Math.floor(Math.random() * this.length)];
};
const app = new Hono();
/*
Our index route, a simple hello world.
*/
app.get(
"/",
(c) => c.text("Hello, world! This is Avalon slack bot for K9 house. v1.6.2"),
);
app.post("/slack/slash", async (c) => {
const contentType = c.req.header("content-type") || "";
// Slack send data in a "form"
if (contentType.includes("form")) {
const payload = await c.req.parseBody();
logJson(payload, "payload");
// "text" field is what the user wrote after the slash command (in this case anything after /avalon)
const text = payload.text;
if (typeof text !== "string") {
return c.text("You need to write something after the slash command!");
}
// Regex to retrieve all the mentioned users, they start with @
let userFromText: string[] = [];
const rePattern = new RegExp(/@\S+/gm);
userFromText = text.match(rePattern) || [];
const matchedUsers = [`@${payload.user_name}`, ...userFromText]; // payload.user_name is the user who issued the slack command
logJson(matchedUsers, "matchedUsers");
// Remove duplicate users
const users = matchedUsers.filter((value, index) => {
return matchedUsers.indexOf(value) === index;
});
logJson(users, "users");
// Game validation, Avalon has min 5 players and 10 max
const minPlayers = parseInt(Deno.env.get("MIN_PLAYERS") || "5");
const maxPlayers = parseInt(Deno.env.get("MAX_PLAYERS") || "10");
if (users.length < minPlayers) {
return c.text(
`You need to be at least ${minPlayers} players!`,
);
}
if (users.length > maxPlayers) {
return c.text(
`You cannot be more than ${maxPlayers} players!`,
);
}
// in case players don't want to the default roles, they can write after the slash which special roles to include
const statedRoles: string[] = [];
// if they want to play with Mordred they can write: mordred, +md or +mrd
if (text.search(/mordred|\+md|\+mrd/i) > -1) {
statedRoles.push("mordred");
}
// if they want to play with Morgana they can write: morgana, +mg or +mrg
if (text.search(/morgana|\+mg|\+mrg/i) > -1) {
statedRoles.push("morgana");
}
// if they want to play with Percival they can write: percival, +p or +pr
if (text.search(/percival|\+p|\+pr/i) > -1) {
statedRoles.push("percival");
}
// if they want to play with Oberon they can write: oberon, +o or +ob
if (text.search(/oberon|\+o|\+ob/i) > -1) {
statedRoles.push("oberon");
}
// if they want to play with no special roles they can write: basic, no special or --
if (text.search(/basic|no special|\-\-/i) > -1) {
statedRoles.push("basic");
}
// We can't have morgana without percival
if (
statedRoles.indexOf("morgana") > -1 &&
statedRoles.indexOf("percival") === -1
) {
statedRoles.push("percival");
}
logJson(statedRoles, "statedRoles");
const currentDate = new Date();
const dateString = currentDate.toLocaleDateString("en-us", {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
});
const numberOfPlayers = users.length;
// deep copying these values so we don't mutate the defualts
const setup: string[] = JSON.parse(
JSON.stringify(defaultSetupRoles[numberOfPlayers]),
);
let specialRoles = defaultSpecialRoles[numberOfPlayers];
if (statedRoles.length > 0) {
// if players wants no special roles, we empty the whole array
if (statedRoles.indexOf("basic") > -1) {
statedRoles.splice(0, statedRoles.length);
}
// Every game must have a Merlin and Assassin
statedRoles.push("merlin");
statedRoles.push("assassin");
specialRoles = statedRoles;
}
const numberOfEvil = setup.filter((x: string) => x === "evil").length;
// Replacing "evil" & "good" with specialRoles (if needed)
const goodRoles: string[] = [];
const evilRoles: string[] = [];
specialRoles.forEach((role) => {
switch (role) {
case "merlin":
case "percival":
setup.splice(setup.indexOf("good"), 1, role);
goodRoles.push(roleMessges[role]);
break;
case "assassin":
case "mordred":
case "morgana":
case "oberon":
if (setup.indexOf("evil") > -1) {
setup.splice(setup.indexOf("evil"), 1, role);
evilRoles.push(roleMessges[role]);
}
break;
case "mordred-or-morgana":
// randomize which special role will be played this round
if (setup.indexOf("evil") > -1) {
const random = ["mordred", "morgana"].random();
setup.splice(setup.indexOf("evil"), 1, random);
evilRoles.push(roleMessges[random]);
// We can't have morgana without percival
if (random === "morgana") {
setup.splice(setup.indexOf("good"), 1, "percival");
goodRoles.push(roleMessges["percival"]);
}
}
break;
}
});
// Shuffling the users order
const shuffledUsers = shuffle(users);
// Pick a random king
const kingIndex = Math.floor(Math.random() * users.length);
interface Player {
role: string;
user: string;
isKing: boolean;
}
const players: Player[] = [];
// Giving each user a role
setup.forEach((role, index) => {
const user = shuffledUsers.pop();
const isKing = index === kingIndex;
players.push({
role,
user,
isKing,
});
console.log(`LOG user: ${user} is ${role}`);
});
// Setting some variables to be used later
const mordred = players.find((e) => e.role === "mordred");
const oberon = players.find((e) => e.role === "oberon");
const merlin = players.find((e) => e.role === "merlin");
const percival = players.find((e) => e.role === "percival");
const morgana = players.find((e) => e.role === "morgana");
const evilPlayers = players.filter((e) =>
["evil", "assassin", "mordred", "morgana", "oberon"].includes(
e.role,
)
);
const evilsButMordred = evilPlayers
.filter((e) => e.role !== "mordred")
.map((e) => e.user);
const evilsButOberon = evilPlayers
.filter((e) => e.role !== "oberon")
.map((e) => e.user);
logJson(evilPlayers, "evilPlayers");
logJson(evilsButMordred, "evilsButMordred");
logJson(evilsButOberon, "evilsButOberon");
// Sending private messages to each player based on the role (and other players roles)
const numberOfEmptySpaces = Deno.env.get("NUMBER_OF_EMPTY_SPACES") || "50";
for (const player of players) {
let premessage =
`\nScroll down to see your role :point_down: :point_down: :point_down: :point_down: \n`;
for (
let i = 0;
i < parseInt(numberOfEmptySpaces);
i++
) {
premessage += ` \n`;
}
premessage += `:point_down: \n`;
await sendSlackMessage(player.user, premessage);
// posting an image of the role
await sendSlackMessage(
player.user,
roleMessges[player.role],
images[player.role],
);
let message = `\nYou are ${privateMessages[player.role]}\n`;
if (player.isKing) {
message += "\nYou are the :crown: *KING* for this round\n";
}
switch (player.role) {
case "merlin":
// MERLIN can see all evil players, but not MORDRED
message += "- *Evils* are: ";
message += evilsButMordred
.filter((e) => e !== player.user)
.join(" ");
message += " \n";
if (mordred) {
message += "- *MORDERED* is with the evils, but *hidden*. \n";
}
if (percival && morgana) {
message +=
"- *PERCIVAL* is *confused* between you and *MORGANA*. \n";
} else if (percival && !morgana) {
message += "- *PERCIVAL* knows you are *MERLIN*. \n";
}
break;
case "percival":
// PERCIVAL can see who MERLIN is, if MORGANA playing then will see both
if (merlin && !morgana) {
message += "- *MERLIN* is " + merlin.user + " \n";
}
if (merlin && morgana) {
// Shuffling the 2 roles so they won't have a pattern
message += "- *MERLIN* is either ";
message += shuffle([merlin.user, morgana.user]).join(
" or ",
);
message += " \n";
message +=
"- One of them is *MORGANA* (evil) pretending to be *MERLIN* to confuse you \n";
}
message += "- *MERLIN* knows who the *evils* are \n";
break;
case "assassin":
case "morgana":
case "mordred":
case "evil":
// All evils (exluding OBERON) see each others, except the evil OBERON sees no one
message += "- *Evils* are: " +
evilsButOberon
.filter((e) => e !== player.user)
.join(" ") +
" \n";
if (percival && player.role === "morgana") {
message +=
"- *PERCIVAL* is *confused* between you and *MERLIN*. \n";
}
if (oberon) {
message += "- *OBERON* is in your team, but *hidden*. \n";
}
if (merlin) {
message += "- *MERLIN* knows who the *evils* are ";
if (player.role !== "mordred") {
message += "(including *you*) ";
if (mordred) {
message += "except *MORDRED*. ";
}
} else {
message += "*except you* ";
}
message += "\n";
}
break;
case "oberon":
if (merlin) {
message += "- *MERLIN* knows who the *evils* are (including you) ";
if (mordred) message += ", except *MORDRED*. ";
}
message += "\n";
break;
}
for (
let i = 0;
i < parseInt(numberOfEmptySpaces);
i++
) {
message += ` \n`;
}
message +=
`\nScroll up to see your role :point_up: :point_up: :point_up: :point_up: \n`;
console.log(`LOG message: ${player.user} -> ${message}`);
// Sending a private message to each user with their specific role message
await sendSlackMessage(player.user, message);
}
console.log("Done sending private messages");
let broadcastMessage = "-----------------------------";
for (let i = 0; i < 4; i++) {
broadcastMessage += ` \n`;
}
broadcastMessage +=
`:crossed_swords: *Starting a new Avalon Game* (${dateString}) :crossed_swords:\n\n`;
broadcastMessage +=
`*${numberOfEvil}* out of *${numberOfPlayers}* players are evil.\n\n`;
broadcastMessage += `:red_circle: Special Evil characters: `;
broadcastMessage += evilRoles.join(", ");
broadcastMessage += `. \n`;
broadcastMessage += `:large_blue_circle: Special Good characters: `;
broadcastMessage += goodRoles.join(", ");
broadcastMessage += `. \n\n`;
// Making a list of who's playing this round (shuffling them again)
broadcastMessage += `Players this round: `;
const shuffledPlayers = shuffle(players);
shuffledPlayers.forEach((player) => {
broadcastMessage += ` ${player.user} `;
});
broadcastMessage += ` \n `;
const king = shuffledPlayers.find((p) => p.isKing);
broadcastMessage += `:crown: *KING* for this round is: ${king.user} \n\n`;
let story = "\n\n\n:star: :star: :star: \n\n";
story += `As the night falls on *Avalon*, \n\n`;
story +=
`Hidden among *Arthur*'s brave warriors are *MORDRED*'s unscrupulous minions.\n`;
story +=
`These forces of evil are few in number (:red_circle: only *${numberOfEvil}*) but have knowledge of each other and remain hidden from all`;
if (merlin) {
story += ` but one of *Arthur*'s servants.\n\n`;
story += `${
roleMessges["merlin"]
} alone knows the agents of evil, but he must speak of this only in riddles. If his true identity is discovered :dagger_knife: *all will be lost*.\n\n`;
if (mordred) {
story +=
`*MERLIN* is powerful, but his powers fall short when it comes to ${
roleMessges["mordred"]
} himself. Only *MORDRED* stays hidden in the shadow, never revealing his evil intentions.\n\n`;
}
} else {
story += `.\n\n`;
}
if (percival) {
story += `Fear not, as one of *Arthur*'s loyal servants is ${
roleMessges["percival"]
}, with his knowledge of the identity of *MERLIN*. Using that knowledge is *key* to protecting *MERLIN*.\n\n`;
if (morgana) {
story +=
`Although *MORDRED*'s minions are not to be underestimated, as they also have ${
roleMessges["morgana"]
}, with her unique power to reveal herself as *MERLIN*, making it difficult for *PERCIVAL* to know which is which.\n\n`;
}
}
if (oberon) {
story += `${
roleMessges["oberon"]
} has sided with the force of evil, but his loyalty is invisible to other *MORDRED*'s minions, nor does he gain knowledge of them either.\n\n`;
}
story +=
`\n- Will goodness prevail? Or will Avalon fall under *MORDRED*'s dark shadow?`;
broadcastMessage += story;
// Broadcasting the message in the main channel
const slackChannel = Deno.env.get("BROADCAST_SLACK_CHANNEL");
if (slackChannel) {
console.log(
"Sending Broadcast message to channel " +
Deno.env.get("BROADCAST_SLACK_CHANNEL"),
);
await sendSlackMessage(slackChannel, broadcastMessage);
}
return c.text(broadcastMessage);
}
// If we didn't get a "form", respond with an error
return c.text("Error! No form received", 400);
});
// Static route for serving images
app.use("/images/*", serveStatic({ root: "./" }));
// 404 handler
app.notFound((c) => {
return c.text("404 Message, You lost buddy?", 404);
});
// Error handler
app.onError((err, c) => {
console.error(`${err}`);
return c.text("Error! Something went very wrong", 500);
});
serve(app.fetch);