@20syldev/api 4.8.0 → 4.9.0

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.
Files changed (109) hide show
  1. package/README.md +84 -47
  2. package/package.json +2 -1
  3. package/.github/FUNDING.yml +0 -1
  4. package/.prettierrc +0 -7
  5. package/eslint.config.js +0 -24
  6. package/robots.txt +0 -74
  7. package/src/app.ts +0 -53
  8. package/src/config/env.ts +0 -87
  9. package/src/config/plans.ts +0 -61
  10. package/src/config/versions.ts +0 -121
  11. package/src/constants.ts +0 -61
  12. package/src/favicon.ico +0 -0
  13. package/src/middleware/cors.ts +0 -23
  14. package/src/middleware/error.ts +0 -16
  15. package/src/middleware/json.ts +0 -9
  16. package/src/middleware/logger.ts +0 -32
  17. package/src/middleware/ratelimit.ts +0 -89
  18. package/src/middleware/version.ts +0 -40
  19. package/src/modules/v3/algorithms.js +0 -218
  20. package/src/modules/v3/captcha.js +0 -60
  21. package/src/modules/v3/chat.js +0 -111
  22. package/src/modules/v3/color.js +0 -60
  23. package/src/modules/v3/convert.js +0 -39
  24. package/src/modules/v3/domain.js +0 -86
  25. package/src/modules/v3/hash.js +0 -16
  26. package/src/modules/v3/hyperplanning.js +0 -56
  27. package/src/modules/v3/levenshtein.js +0 -46
  28. package/src/modules/v3/personal.js +0 -139
  29. package/src/modules/v3/qrcode.js +0 -20
  30. package/src/modules/v3/tic_tac_toe.js +0 -242
  31. package/src/modules/v3/time.js +0 -98
  32. package/src/modules/v3/token.js +0 -54
  33. package/src/modules/v3/username.js +0 -94
  34. package/src/modules/v3/utils.js +0 -77
  35. package/src/modules/v3.js +0 -15
  36. package/src/modules/v4/address.ts +0 -412
  37. package/src/modules/v4/agent.ts +0 -104
  38. package/src/modules/v4/algorithms.ts +0 -276
  39. package/src/modules/v4/captcha.ts +0 -114
  40. package/src/modules/v4/chat.ts +0 -130
  41. package/src/modules/v4/color.ts +0 -75
  42. package/src/modules/v4/convert.ts +0 -127
  43. package/src/modules/v4/cron.ts +0 -221
  44. package/src/modules/v4/dice.ts +0 -46
  45. package/src/modules/v4/domain.ts +0 -86
  46. package/src/modules/v4/encode.ts +0 -241
  47. package/src/modules/v4/geo.ts +0 -64
  48. package/src/modules/v4/hash.ts +0 -32
  49. package/src/modules/v4/hyperplanning.ts +0 -93
  50. package/src/modules/v4/ip.ts +0 -134
  51. package/src/modules/v4/levenshtein.ts +0 -46
  52. package/src/modules/v4/palette.ts +0 -71
  53. package/src/modules/v4/password.ts +0 -379
  54. package/src/modules/v4/personal.ts +0 -140
  55. package/src/modules/v4/placeholder.ts +0 -197
  56. package/src/modules/v4/qrcode.ts +0 -121
  57. package/src/modules/v4/regex.ts +0 -67
  58. package/src/modules/v4/statistics.ts +0 -62
  59. package/src/modules/v4/text.ts +0 -330
  60. package/src/modules/v4/tic_tac_toe.ts +0 -264
  61. package/src/modules/v4/time.ts +0 -149
  62. package/src/modules/v4/token.ts +0 -53
  63. package/src/modules/v4/username.ts +0 -94
  64. package/src/modules/v4/validate.ts +0 -76
  65. package/src/modules/v4.ts +0 -29
  66. package/src/routes/delete.ts +0 -72
  67. package/src/routes/get.ts +0 -856
  68. package/src/routes/index.ts +0 -59
  69. package/src/routes/patch.ts +0 -45
  70. package/src/routes/post.ts +0 -209
  71. package/src/storage/index.ts +0 -16
  72. package/src/types/express.d.ts +0 -17
  73. package/src/types/storage.ts +0 -48
  74. package/src/utils/colors.ts +0 -91
  75. package/src/utils/helpers.ts +0 -90
  76. package/src/utils/response.ts +0 -28
  77. package/tests/integration/api.test.ts +0 -772
  78. package/tests/tsconfig.json +0 -3
  79. package/tests/unit/address.test.ts +0 -84
  80. package/tests/unit/agent.test.ts +0 -113
  81. package/tests/unit/algorithms.test.ts +0 -121
  82. package/tests/unit/captcha.test.ts +0 -62
  83. package/tests/unit/chat.test.ts +0 -54
  84. package/tests/unit/color.test.ts +0 -54
  85. package/tests/unit/convert.test.ts +0 -68
  86. package/tests/unit/cron.test.ts +0 -110
  87. package/tests/unit/dice.test.ts +0 -58
  88. package/tests/unit/domain.test.ts +0 -48
  89. package/tests/unit/encode.test.ts +0 -109
  90. package/tests/unit/geo.test.ts +0 -46
  91. package/tests/unit/hash.test.ts +0 -45
  92. package/tests/unit/hyperplanning.test.ts +0 -97
  93. package/tests/unit/ip.test.ts +0 -140
  94. package/tests/unit/levenshtein.test.ts +0 -35
  95. package/tests/unit/palette.test.ts +0 -54
  96. package/tests/unit/password.test.ts +0 -86
  97. package/tests/unit/personal.test.ts +0 -66
  98. package/tests/unit/placeholder.test.ts +0 -58
  99. package/tests/unit/qrcode.test.ts +0 -83
  100. package/tests/unit/regex.test.ts +0 -95
  101. package/tests/unit/statistics.test.ts +0 -58
  102. package/tests/unit/text.test.ts +0 -108
  103. package/tests/unit/tic_tac_toe.test.ts +0 -56
  104. package/tests/unit/time.test.ts +0 -115
  105. package/tests/unit/token.test.ts +0 -62
  106. package/tests/unit/username.test.ts +0 -35
  107. package/tests/unit/validate.test.ts +0 -72
  108. package/tsconfig.json +0 -17
  109. package/tsconfig.test.json +0 -9
@@ -1,330 +0,0 @@
1
- import { MAX_STRING_LENGTH } from '../../constants.js';
2
-
3
- const LOREM_WORDS = [
4
- 'lorem',
5
- 'ipsum',
6
- 'dolor',
7
- 'sit',
8
- 'amet',
9
- 'consectetur',
10
- 'adipiscing',
11
- 'elit',
12
- 'sed',
13
- 'do',
14
- 'eiusmod',
15
- 'tempor',
16
- 'incididunt',
17
- 'ut',
18
- 'labore',
19
- 'et',
20
- 'dolore',
21
- 'magna',
22
- 'aliqua',
23
- 'enim',
24
- 'ad',
25
- 'minim',
26
- 'veniam',
27
- 'quis',
28
- 'nostrud',
29
- 'exercitation',
30
- 'ullamco',
31
- 'laboris',
32
- 'nisi',
33
- 'aliquip',
34
- 'ex',
35
- 'ea',
36
- 'commodo',
37
- 'consequat',
38
- 'duis',
39
- 'aute',
40
- 'irure',
41
- 'in',
42
- 'reprehenderit',
43
- 'voluptate',
44
- 'velit',
45
- 'esse',
46
- 'cillum',
47
- 'eu',
48
- 'fugiat',
49
- 'nulla',
50
- 'pariatur',
51
- 'excepteur',
52
- 'sint',
53
- 'occaecat',
54
- 'cupidatat',
55
- 'non',
56
- 'proident',
57
- 'sunt',
58
- 'culpa',
59
- 'qui',
60
- 'officia',
61
- 'deserunt',
62
- 'mollit',
63
- 'anim',
64
- 'id',
65
- 'est',
66
- 'laborum',
67
- ];
68
-
69
- const FR_UNITS = [
70
- 'zéro',
71
- 'un',
72
- 'deux',
73
- 'trois',
74
- 'quatre',
75
- 'cinq',
76
- 'six',
77
- 'sept',
78
- 'huit',
79
- 'neuf',
80
- 'dix',
81
- 'onze',
82
- 'douze',
83
- 'treize',
84
- 'quatorze',
85
- 'quinze',
86
- 'seize',
87
- 'dix-sept',
88
- 'dix-huit',
89
- 'dix-neuf',
90
- ];
91
- const FR_TENS = [
92
- '',
93
- '',
94
- 'vingt',
95
- 'trente',
96
- 'quarante',
97
- 'cinquante',
98
- 'soixante',
99
- 'soixante',
100
- 'quatre-vingt',
101
- 'quatre-vingt',
102
- ];
103
-
104
- const EN_UNITS = [
105
- 'zero',
106
- 'one',
107
- 'two',
108
- 'three',
109
- 'four',
110
- 'five',
111
- 'six',
112
- 'seven',
113
- 'eight',
114
- 'nine',
115
- 'ten',
116
- 'eleven',
117
- 'twelve',
118
- 'thirteen',
119
- 'fourteen',
120
- 'fifteen',
121
- 'sixteen',
122
- 'seventeen',
123
- 'eighteen',
124
- 'nineteen',
125
- ];
126
- const EN_TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
127
-
128
- function checkText(value: string): void {
129
- if (!value) throw new Error('A value is required');
130
- if (typeof value !== 'string') throw new Error('Value must be a string');
131
- if (value.length > MAX_STRING_LENGTH)
132
- throw new Error(`Value must be less than ${MAX_STRING_LENGTH} characters long`);
133
- }
134
-
135
- export interface TextStats {
136
- characters: number;
137
- charactersNoSpaces: number;
138
- words: number;
139
- sentences: number;
140
- paragraphs: number;
141
- readingTime: string;
142
- mostFrequentChar: string;
143
- }
144
-
145
- /**
146
- * Analyzes a text string and returns character, word, sentence, and reading time statistics.
147
- *
148
- * @param value - The text to analyze
149
- * @returns Object containing character counts, word/sentence/paragraph counts, reading time, and most frequent character
150
- * @throws Error if value is missing, not a string, or too long
151
- */
152
- export function stats(value: string): TextStats {
153
- checkText(value);
154
-
155
- const characters = value.length;
156
- const charactersNoSpaces = value.replace(/\s/g, '').length;
157
- const words = value.trim().split(/\s+/).filter(Boolean).length;
158
- const sentences = value.split(/[.!?]+/).filter((s) => s.trim().length > 0).length;
159
- const paragraphs = value.split(/\n\s*\n/).filter((p) => p.trim().length > 0).length || 1;
160
-
161
- const minutes = words / 200;
162
- const readingTime = minutes < 1 ? `${Math.ceil(minutes * 60)}s` : `${Math.round(minutes)}min`;
163
-
164
- const counts = new Map<string, number>();
165
- for (const c of value.replace(/\s/g, '').toLowerCase()) counts.set(c, (counts.get(c) ?? 0) + 1);
166
- let mostFrequentChar = '';
167
- let maxCount = 0;
168
- for (const [char, count] of counts) {
169
- if (count > maxCount) {
170
- mostFrequentChar = char;
171
- maxCount = count;
172
- }
173
- }
174
-
175
- return { characters, charactersNoSpaces, words, sentences, paragraphs, readingTime, mostFrequentChar };
176
- }
177
-
178
- /**
179
- * Converts a string to a URL-friendly slug.
180
- *
181
- * @param value - The string to slugify
182
- * @returns Lowercase hyphenated slug with diacritics and special characters removed
183
- * @throws Error if value is missing, not a string, or too long
184
- */
185
- export function slug(value: string): string {
186
- checkText(value);
187
- return value
188
- .normalize('NFD')
189
- .replace(/[\u0300-\u036f]/g, '')
190
- .toLowerCase()
191
- .replace(/[^a-z0-9\s-]/g, '')
192
- .trim()
193
- .replace(/[\s-]+/g, '-');
194
- }
195
-
196
- /**
197
- * Generates Lorem Ipsum placeholder text.
198
- *
199
- * @param type - Content type: "words", "sentences", or "paragraphs"
200
- * @param count - Number of words, sentences, or paragraphs to generate
201
- * @returns Generated Lorem Ipsum text
202
- * @throws Error if count is out of range or type is not one of the accepted values
203
- */
204
- export function lorem(type: string, count: string): string {
205
- const n = Number(count) || 5;
206
- if (n < 1 || n > 500) throw new Error('Count must be between 1 and 500');
207
-
208
- const randomWord = (): string => LOREM_WORDS[Math.floor(Math.random() * LOREM_WORDS.length)]!;
209
- const randomSentence = (): string => {
210
- const length = 5 + Math.floor(Math.random() * 10);
211
- const words = Array.from({ length }, randomWord);
212
- words[0] = words[0]!.charAt(0).toUpperCase() + words[0]!.slice(1);
213
- return words.join(' ') + '.';
214
- };
215
- const randomParagraph = (): string => {
216
- const length = 3 + Math.floor(Math.random() * 5);
217
- return Array.from({ length }, randomSentence).join(' ');
218
- };
219
-
220
- switch (type) {
221
- case 'words':
222
- return Array.from({ length: n }, randomWord).join(' ');
223
- case 'sentences':
224
- return Array.from({ length: n }, randomSentence).join(' ');
225
- case 'paragraphs':
226
- return Array.from({ length: n }, randomParagraph).join('\n\n');
227
- default:
228
- throw new Error('Type must be one of: words, sentences, paragraphs');
229
- }
230
- }
231
-
232
- function frBelowHundred(n: number): string {
233
- if (n < 20) return FR_UNITS[n]!;
234
- const t = Math.floor(n / 10);
235
- const u = n % 10;
236
- if (t === 7 || t === 9) {
237
- const base = FR_TENS[t]!;
238
- const sub = 10 + u;
239
- return base + (base.endsWith('-') ? '' : '-') + FR_UNITS[sub];
240
- }
241
- if (t === 8 && u === 0) return 'quatre-vingts';
242
- if (u === 0) return FR_TENS[t]!;
243
- if (u === 1 && t < 8) return FR_TENS[t] + ' et un';
244
- return FR_TENS[t] + '-' + FR_UNITS[u];
245
- }
246
-
247
- function frBelowThousand(n: number): string {
248
- if (n < 100) return frBelowHundred(n);
249
- const h = Math.floor(n / 100);
250
- const r = n % 100;
251
- let result = '';
252
- if (h === 1) result = 'cent';
253
- else result = FR_UNITS[h] + ' cent' + (r === 0 ? 's' : '');
254
- if (r > 0) result += ' ' + frBelowHundred(r);
255
- return result;
256
- }
257
-
258
- function numberToFrench(n: number): string {
259
- if (n === 0) return 'zéro';
260
- if (n < 0) return 'moins ' + numberToFrench(-n);
261
- if (n < 1000) return frBelowThousand(n);
262
- if (n < 1_000_000) {
263
- const t = Math.floor(n / 1000);
264
- const r = n % 1000;
265
- const thousands = t === 1 ? 'mille' : frBelowThousand(t) + ' mille';
266
- return r === 0 ? thousands : thousands + ' ' + frBelowThousand(r);
267
- }
268
- if (n < 1_000_000_000) {
269
- const m = Math.floor(n / 1_000_000);
270
- const r = n % 1_000_000;
271
- const millions = m === 1 ? 'un million' : frBelowThousand(m) + ' millions';
272
- return r === 0 ? millions : millions + ' ' + numberToFrench(r);
273
- }
274
- throw new Error('Number must be less than 1 billion');
275
- }
276
-
277
- function enBelowHundred(n: number): string {
278
- if (n < 20) return EN_UNITS[n]!;
279
- const t = Math.floor(n / 10);
280
- const u = n % 10;
281
- return u === 0 ? EN_TENS[t]! : EN_TENS[t] + '-' + EN_UNITS[u];
282
- }
283
-
284
- function enBelowThousand(n: number): string {
285
- if (n < 100) return enBelowHundred(n);
286
- const h = Math.floor(n / 100);
287
- const r = n % 100;
288
- return r === 0 ? EN_UNITS[h] + ' hundred' : EN_UNITS[h] + ' hundred ' + enBelowHundred(r);
289
- }
290
-
291
- function numberToEnglish(n: number): string {
292
- if (n === 0) return 'zero';
293
- if (n < 0) return 'minus ' + numberToEnglish(-n);
294
- if (n < 1000) return enBelowThousand(n);
295
- if (n < 1_000_000) {
296
- const t = Math.floor(n / 1000);
297
- const r = n % 1000;
298
- return r === 0 ? enBelowThousand(t) + ' thousand' : enBelowThousand(t) + ' thousand ' + enBelowThousand(r);
299
- }
300
- if (n < 1_000_000_000) {
301
- const m = Math.floor(n / 1_000_000);
302
- const r = n % 1_000_000;
303
- return r === 0 ? enBelowThousand(m) + ' million' : enBelowThousand(m) + ' million ' + numberToEnglish(r);
304
- }
305
- throw new Error('Number must be less than 1 billion');
306
- }
307
-
308
- /**
309
- * Converts an integer to its written-out word form in French or English.
310
- *
311
- * @param value - The integer to convert (must be less than 1 billion)
312
- * @param lang - Language: "fr" for French or "en" for English
313
- * @returns The number written out in words
314
- * @throws Error if value is not an integer, exceeds the maximum, or lang is not supported
315
- */
316
- export function number(value: string, lang: string): string {
317
- const n = Number(value);
318
- if (isNaN(n)) throw new Error('Value must be a number');
319
- if (!Number.isInteger(n)) throw new Error('Value must be an integer');
320
- if (Math.abs(n) >= 1_000_000_000) throw new Error('Number must be less than 1 billion');
321
-
322
- switch (lang) {
323
- case 'fr':
324
- return numberToFrench(n);
325
- case 'en':
326
- return numberToEnglish(n);
327
- default:
328
- throw new Error('Lang must be one of: fr, en');
329
- }
330
- }
@@ -1,264 +0,0 @@
1
- import { randomBytes } from 'crypto';
2
-
3
- import { GAME_CLEANUP_TTL, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, SESSION_TTL } from '../../constants.js';
4
- import type { TicTacToeGame, TicTacToeMove, TicTacToeStorage } from '../../types/storage.js';
5
-
6
- interface TicTacToeParams {
7
- username?: string;
8
- move?: string;
9
- session?: string;
10
- game?: string;
11
- private?: boolean;
12
- storage: TicTacToeStorage;
13
- }
14
-
15
- interface GameResult {
16
- winner?: string | null;
17
- loser?: string | null;
18
- tie?: boolean;
19
- }
20
-
21
- /**
22
- * Handles Tic-Tac-Toe game actions including playing a move, fetching game state, listing games, and forfeiting.
23
- *
24
- * @param action - The action to perform: "play", "fetch", "list", or "forfeit"
25
- * @param params - Game parameters including username, move, session, game ID, and shared storage
26
- * @returns Game state or action result depending on the action
27
- * @throws Error if a required parameter is missing, the action is invalid, or a rate limit is exceeded
28
- */
29
- export default function tic_tac_toe(action: string, params: TicTacToeParams): Record<string, unknown> {
30
- const storage = params.storage;
31
-
32
- storage.games ??= {};
33
- storage.sessions ??= {};
34
- storage.rateLimits ??= {};
35
-
36
- const { games, sessions, rateLimits } = storage;
37
-
38
- if (action === 'list') return listGames(games);
39
-
40
- if (!params.username) throw new Error('Please provide a username');
41
-
42
- const u = params.username.toLowerCase();
43
- const now = Date.now();
44
-
45
- rateLimits[u] = (rateLimits[u] ?? []).filter((ts) => now - ts < RATE_LIMIT_WINDOW);
46
- if (rateLimits[u]!.length > RATE_LIMIT_MAX) {
47
- const remainingTime = Math.ceil((rateLimits[u]![0]! + RATE_LIMIT_WINDOW - now) / 1000);
48
- throw new Error(`Rate limit exceeded. Try again in ${remainingTime} seconds.`);
49
- }
50
- rateLimits[u]!.push(now);
51
-
52
- if (action === 'play') {
53
- return playMove(params, games, sessions, u, now);
54
- } else if (action === 'fetch') {
55
- return fetchGame(params, games, u);
56
- } else if (action === 'forfeit') {
57
- return forfeitGame(params, games, sessions);
58
- } else {
59
- throw new Error('Invalid action. Use "play", "fetch", "list", or "forfeit"');
60
- }
61
- }
62
-
63
- function forfeitGame(
64
- params: TicTacToeParams,
65
- games: Record<string, TicTacToeGame>,
66
- sessions: Record<string, { user: string; last: number }>,
67
- ): Record<string, unknown> {
68
- const { game, session } = params;
69
-
70
- if (!game) throw new Error('Please provide a valid game ID');
71
- if (!session) throw new Error('Please provide a valid session ID');
72
- if (!games[game]) throw new Error('Game not found');
73
-
74
- const u = params.username!.toLowerCase();
75
- if (sessions[u] && sessions[u].user !== session) {
76
- throw new Error('Session ID mismatch');
77
- }
78
-
79
- const players = games[game]!.players;
80
- if (!players.includes(u)) throw new Error('You are not a player in this game');
81
-
82
- const winner = players.find((p) => p !== u) ?? null;
83
- delete games[game];
84
- delete sessions[u];
85
-
86
- return {
87
- message: `${params.username} forfeited the game.${winner ? ` ${winner} wins.` : ''}`,
88
- winner,
89
- loser: params.username,
90
- };
91
- }
92
-
93
- function playMove(
94
- params: TicTacToeParams,
95
- games: Record<string, TicTacToeGame>,
96
- sessions: Record<string, { user: string; last: number }>,
97
- u: string,
98
- now: number,
99
- ): Record<string, unknown> {
100
- const { move, session, game } = params;
101
- const validMoves = ['1-1', '1-2', '1-3', '2-1', '2-2', '2-3', '3-1', '3-2', '3-3'];
102
-
103
- if (!move) throw new Error('Please provide a valid move');
104
- if (!session) throw new Error('Please provide a valid session ID');
105
- if (!game) throw new Error('Please provide a valid game ID');
106
- if (!validMoves.includes(move)) throw new Error('Invalid move. Please provide a valid move (e.g., 1-1, 2-2, 3-3).');
107
-
108
- if (sessions[u] && sessions[u].user !== session) {
109
- throw new Error('Session ID mismatch');
110
- }
111
-
112
- games[game] = games[game] ?? {
113
- moves: [],
114
- players: [],
115
- private: params.private || false,
116
- creation: Date.now(),
117
- };
118
- const moves = games[game]!.moves;
119
-
120
- const players = [...new Set(moves.map((play) => play.username))];
121
- if (players.length >= 2 && !players.includes(params.username!)) {
122
- throw new Error('Game is full, you can only watch.');
123
- }
124
-
125
- if (moves.length > 0 && moves[moves.length - 1]!.username === params.username) {
126
- throw new Error('Please wait for the other player to make a move.');
127
- }
128
-
129
- if (moves.some((play) => play.move === move)) {
130
- throw new Error('Move already made. Please choose a different move.');
131
- }
132
-
133
- const play: TicTacToeMove = { username: params.username!, move, session };
134
- moves.push(play);
135
-
136
- const result = checkGame(moves);
137
- if (result.winner || result.tie) {
138
- setTimeout(() => delete games[game!], GAME_CLEANUP_TTL);
139
- return {
140
- message: `Move sent successfully. ${result.winner ? result.winner + ' wins. ' + result.loser + ' loses.' : "It's a tie."}`,
141
- ...result,
142
- };
143
- }
144
-
145
- setTimeout(() => delete games[game!], SESSION_TTL);
146
-
147
- sessions[u] = sessions[u] ?? { user: session, last: now };
148
- sessions[u]!.last = now;
149
- setTimeout(() => {
150
- if (sessions[u] && now - sessions[u].last >= SESSION_TTL) delete sessions[u];
151
- }, SESSION_TTL);
152
-
153
- return { message: 'Move sent successfully' };
154
- }
155
-
156
- function fetchGame(params: TicTacToeParams, games: Record<string, TicTacToeGame>, u: string): Record<string, unknown> {
157
- const id = params.game || generateGameId();
158
- if (!games[id]) {
159
- games[id] = {
160
- moves: [],
161
- players: [],
162
- private: params.private || false,
163
- creation: Date.now(),
164
- };
165
- }
166
- if (!games[id]!.players.includes(u)) {
167
- games[id]!.players.push(u);
168
- }
169
-
170
- const moves = games[id]!.moves;
171
- const players = games[id]!.players;
172
- const privateGame = games[id]!.private;
173
- const lastPlayer = moves.length ? moves[moves.length - 1]!.username : null;
174
- const turn = players.find((p) => p !== lastPlayer) || players[0];
175
- const status = players.length >= 2 ? 'ready' : 'waiting';
176
- const result = moves.length ? checkGame(moves) : {};
177
-
178
- return {
179
- id,
180
- moves,
181
- players,
182
- turn,
183
- status,
184
- private: privateGame,
185
- ...result,
186
- };
187
- }
188
-
189
- function listGames(games: Record<string, TicTacToeGame>): Record<string, unknown> {
190
- const publicGames: Record<string, unknown>[] = [];
191
- const now = Date.now();
192
-
193
- for (const [gameId, game] of Object.entries(games)) {
194
- if (!game.private) {
195
- const result = game.moves.length ? checkGame(game.moves) : {};
196
- const isFinished = result.winner || result.tie;
197
-
198
- if (!isFinished) {
199
- const lastPlayer = game.moves.length ? game.moves[game.moves.length - 1]!.username : null;
200
- const turn = game.players.find((p) => p !== lastPlayer) || game.players[0];
201
- const status = game.players.length >= 2 ? 'ready' : 'waiting';
202
-
203
- publicGames.push({
204
- id: gameId,
205
- players: game.players,
206
- playersCount: game.players.length,
207
- moves: game.moves.length,
208
- turn,
209
- status,
210
- creation: game.creation || now,
211
- });
212
- }
213
- }
214
- }
215
-
216
- publicGames.sort((a, b) => (b.creation as number) - (a.creation as number));
217
-
218
- return {
219
- message: 'Public games available',
220
- count: publicGames.length,
221
- games: publicGames,
222
- };
223
- }
224
-
225
- function generateGameId(): string {
226
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
227
- return Array.from(randomBytes(5))
228
- .map((b) => chars[b % chars.length])
229
- .join('');
230
- }
231
-
232
- function checkGame(moves: TicTacToeMove[]): GameResult {
233
- const board: (string | null)[][] = Array(3)
234
- .fill(null)
235
- .map(() => Array(3).fill(null) as (string | null)[]);
236
- const playerSymbols: Record<string, string> = {};
237
- const playersOrder: string[] = [];
238
-
239
- moves.forEach(({ username, move }) => {
240
- if (!playerSymbols[username]) {
241
- playersOrder.push(username);
242
- playerSymbols[username] = playersOrder.length === 1 ? 'X' : 'O';
243
- }
244
- const [row, col] = move.split('-').map(Number) as [number, number];
245
- board[row - 1]![col - 1] = playerSymbols[username]!;
246
- });
247
-
248
- const checkWinner = (symbol: string): boolean => {
249
- for (let i = 0; i < 3; i++) {
250
- if (board[i]![0] === symbol && board[i]![1] === symbol && board[i]![2] === symbol) return true;
251
- if (board[0]![i] === symbol && board[1]![i] === symbol && board[2]![i] === symbol) return true;
252
- }
253
- if (board[0]![0] === symbol && board[1]![1] === symbol && board[2]![2] === symbol) return true;
254
- if (board[0]![2] === symbol && board[1]![1] === symbol && board[2]![0] === symbol) return true;
255
- return false;
256
- };
257
-
258
- const winner = Object.keys(playerSymbols).find((player) => checkWinner(playerSymbols[player]!)) ?? null;
259
- const isTie = !winner && moves.length === 9;
260
- const loser =
261
- winner && playersOrder.length === 2 ? (playersOrder.find((player) => player !== winner) ?? null) : null;
262
-
263
- return { winner, loser, tie: isTie };
264
- }