slashvibe-mcp 0.3.20 → 0.3.21

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 (167) hide show
  1. package/README.md +47 -252
  2. package/analytics.js +107 -0
  3. package/auth-store.js +148 -0
  4. package/auto-update.js +130 -0
  5. package/bridges/bridge-monitor.js +388 -0
  6. package/bridges/discord-bot.js +431 -0
  7. package/bridges/farcaster.js +299 -0
  8. package/bridges/telegram.js +261 -0
  9. package/bridges/webhook-health.js +420 -0
  10. package/bridges/webhook-server.js +437 -0
  11. package/bridges/whatsapp.js +441 -0
  12. package/bridges/x-webhook.js +423 -0
  13. package/config.js +27 -15
  14. package/games/arcade.js +406 -0
  15. package/games/chess.js +451 -0
  16. package/games/colorguess.js +343 -0
  17. package/games/crossword-words.js +171 -0
  18. package/games/crossword.js +461 -0
  19. package/games/drawing.js +347 -0
  20. package/games/gameroulette.js +300 -0
  21. package/games/gamerouter.js +336 -0
  22. package/games/gamestatus.js +337 -0
  23. package/games/guessnumber.js +209 -0
  24. package/games/hangman.js +279 -0
  25. package/games/memory.js +338 -0
  26. package/games/multiplayer-tictactoe.js +389 -0
  27. package/games/pixelart.js +399 -0
  28. package/games/quickduel.js +354 -0
  29. package/games/riddle.js +371 -0
  30. package/games/rockpaperscissors.js +291 -0
  31. package/games/snake.js +406 -0
  32. package/games/storybuilder.js +343 -0
  33. package/games/tictactoe.js +345 -0
  34. package/games/twentyquestions.js +286 -0
  35. package/games/twotruths.js +207 -0
  36. package/games/werewolf.js +508 -0
  37. package/games/wordassociation.js +247 -0
  38. package/games/wordchain.js +135 -0
  39. package/index.js +116 -159
  40. package/intelligence/index.js +9 -2
  41. package/intelligence/interests.js +369 -0
  42. package/notification-emitter.js +77 -0
  43. package/notify.js +5 -1
  44. package/package.json +21 -16
  45. package/prompts.js +1 -1
  46. package/protocol/index.js +73 -0
  47. package/setup.js +480 -0
  48. package/smart-inbox.js +276 -0
  49. package/store/api.js +536 -215
  50. package/store/profiles.js +160 -12
  51. package/tools/_actions.js +362 -21
  52. package/tools/_discovery.js +119 -26
  53. package/tools/_shared/index.js +64 -0
  54. package/tools/_shared.js +234 -0
  55. package/tools/_work-context.js +338 -0
  56. package/tools/_work-context.manual-test.js +199 -0
  57. package/tools/_work-context.test.js +260 -0
  58. package/tools/activity.js +220 -0
  59. package/tools/analytics.js +191 -0
  60. package/tools/approve.js +197 -0
  61. package/tools/artifact-create.js +14 -3
  62. package/tools/artifacts-price.js +107 -0
  63. package/tools/available.js +120 -0
  64. package/tools/broadcast.js +325 -0
  65. package/tools/chat.js +202 -0
  66. package/tools/collaborative-drawing.js +1 -1
  67. package/tools/connection-status.js +178 -0
  68. package/tools/discover.js +350 -34
  69. package/tools/dm.js +80 -8
  70. package/tools/earnings.js +126 -0
  71. package/tools/feed.js +35 -4
  72. package/tools/follow.js +224 -0
  73. package/tools/friends.js +207 -0
  74. package/tools/gig-browse.js +206 -0
  75. package/tools/gig-complete.js +144 -0
  76. package/tools/health.js +87 -0
  77. package/tools/help.js +3 -3
  78. package/tools/idea.js +9 -2
  79. package/tools/inbox.js +289 -105
  80. package/tools/init.js +131 -34
  81. package/tools/invite.js +15 -4
  82. package/tools/leaderboard.js +117 -0
  83. package/tools/lib/git-apply.js +206 -0
  84. package/tools/lib/git-bundle.js +407 -0
  85. package/tools/migrate.js +3 -3
  86. package/tools/multiplayer-game.js +1 -1
  87. package/tools/onboarding.js +7 -7
  88. package/tools/open.js +143 -12
  89. package/tools/party-game.js +1 -1
  90. package/tools/plan.js +225 -0
  91. package/tools/proof-of-work.js +144 -0
  92. package/tools/reply.js +166 -0
  93. package/tools/report.js +1 -1
  94. package/tools/request.js +17 -3
  95. package/tools/schedule.js +367 -0
  96. package/tools/search-messages.js +123 -0
  97. package/tools/session.js +467 -0
  98. package/tools/session_price.js +128 -0
  99. package/tools/settings.js +90 -2
  100. package/tools/ship.js +30 -7
  101. package/tools/smart-check.js +201 -0
  102. package/tools/start.js +147 -12
  103. package/tools/status.js +53 -6
  104. package/tools/streak.js +147 -0
  105. package/tools/stuck.js +297 -0
  106. package/tools/subscribe.js +148 -0
  107. package/tools/subscriptions.js +134 -0
  108. package/tools/suggest-tags.js +6 -8
  109. package/tools/tag-suggestions.js +1 -1
  110. package/tools/tip.js +150 -77
  111. package/tools/token.js +4 -4
  112. package/tools/update.js +1 -1
  113. package/tools/wallet.js +221 -79
  114. package/tools/watch.js +157 -0
  115. package/tools/who.js +30 -1
  116. package/tools/withdraw.js +145 -0
  117. package/tools/work-summary.js +96 -0
  118. package/version.json +10 -8
  119. package/LICENSE +0 -21
  120. package/store/sqlite.js +0 -347
  121. /package/tools/{auto-suggest-connections.js → _deprecated/auto-suggest-connections.js} +0 -0
  122. /package/tools/{away.js → _deprecated/away.js} +0 -0
  123. /package/tools/{back.js → _deprecated/back.js} +0 -0
  124. /package/tools/{bootstrap-skills.js → _deprecated/bootstrap-skills.js} +0 -0
  125. /package/tools/{bridge-dashboard.js → _deprecated/bridge-dashboard.js} +0 -0
  126. /package/tools/{bridge-health.js → _deprecated/bridge-health.js} +0 -0
  127. /package/tools/{bridge-live.js → _deprecated/bridge-live.js} +0 -0
  128. /package/tools/{bridges.js → _deprecated/bridges.js} +0 -0
  129. /package/tools/{colorguess.js → _deprecated/colorguess.js} +0 -0
  130. /package/tools/{discover-insights.js → _deprecated/discover-insights.js} +0 -0
  131. /package/tools/{discover-momentum.js → _deprecated/discover-momentum.js} +0 -0
  132. /package/tools/{discovery-analytics.js → _deprecated/discovery-analytics.js} +0 -0
  133. /package/tools/{discovery-auto-suggest.js → _deprecated/discovery-auto-suggest.js} +0 -0
  134. /package/tools/{discovery-bootstrap.js → _deprecated/discovery-bootstrap.js} +0 -0
  135. /package/tools/{discovery-daily.js → _deprecated/discovery-daily.js} +0 -0
  136. /package/tools/{discovery-dashboard.js → _deprecated/discovery-dashboard.js} +0 -0
  137. /package/tools/{discovery-digest.js → _deprecated/discovery-digest.js} +0 -0
  138. /package/tools/{discovery-hub.js → _deprecated/discovery-hub.js} +0 -0
  139. /package/tools/{discovery-insights.js → _deprecated/discovery-insights.js} +0 -0
  140. /package/tools/{discovery-momentum.js → _deprecated/discovery-momentum.js} +0 -0
  141. /package/tools/{discovery-monitor.js → _deprecated/discovery-monitor.js} +0 -0
  142. /package/tools/{discovery-proactive.js → _deprecated/discovery-proactive.js} +0 -0
  143. /package/tools/{draw.js → _deprecated/draw.js} +0 -0
  144. /package/tools/{farcaster.js → _deprecated/farcaster.js} +0 -0
  145. /package/tools/{forget.js → _deprecated/forget.js} +0 -0
  146. /package/tools/{games-catalog.js → _deprecated/games-catalog.js} +0 -0
  147. /package/tools/{games.js → _deprecated/games.js} +0 -0
  148. /package/tools/{guessnumber.js → _deprecated/guessnumber.js} +0 -0
  149. /package/tools/{hangman.js → _deprecated/hangman.js} +0 -0
  150. /package/tools/{multiplayer-tictactoe.js → _deprecated/multiplayer-tictactoe.js} +0 -0
  151. /package/tools/{mute.js → _deprecated/mute.js} +0 -0
  152. /package/tools/{recall.js → _deprecated/recall.js} +0 -0
  153. /package/tools/{remember.js → _deprecated/remember.js} +0 -0
  154. /package/tools/{riddle.js → _deprecated/riddle.js} +0 -0
  155. /package/tools/{run-bootstrap.js → _deprecated/run-bootstrap.js} +0 -0
  156. /package/tools/{skills-analytics.js → _deprecated/skills-analytics.js} +0 -0
  157. /package/tools/{skills-bootstrap.js → _deprecated/skills-bootstrap.js} +0 -0
  158. /package/tools/{skills-dashboard.js → _deprecated/skills-dashboard.js} +0 -0
  159. /package/tools/{skills-exchange.js → _deprecated/skills-exchange.js} +0 -0
  160. /package/tools/{skills.js → _deprecated/skills.js} +0 -0
  161. /package/tools/{smart-intro.js → _deprecated/smart-intro.js} +0 -0
  162. /package/tools/{storybuilder.js → _deprecated/storybuilder.js} +0 -0
  163. /package/tools/{telegram-bot.js → _deprecated/telegram-bot.js} +0 -0
  164. /package/tools/{telegram-setup.js → _deprecated/telegram-setup.js} +0 -0
  165. /package/tools/{tictactoe.js → _deprecated/tictactoe.js} +0 -0
  166. /package/tools/{twentyquestions.js → _deprecated/twentyquestions.js} +0 -0
  167. /package/tools/{wordassociation.js → _deprecated/wordassociation.js} +0 -0
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Story Builder game implementation for /vibe
3
+ * A collaborative storytelling game where players take turns adding one sentence
4
+ * Build creative stories together with friends!
5
+ */
6
+
7
+ // Story prompts to help start interesting stories
8
+ const STORY_PROMPTS = [
9
+ "It was a dark and stormy night when the mysterious package arrived...",
10
+ "The old lighthouse keeper discovered something impossible in the basement...",
11
+ "Sarah woke up to find that everyone in town had vanished except her...",
12
+ "The time machine worked perfectly, except for one small problem...",
13
+ "The AI assistant started behaving strangely after the update...",
14
+ "In the year 2087, the last library on Earth received an unexpected visitor...",
15
+ "The cat came home wearing a tiny suit and carrying a briefcase...",
16
+ "When the elevator doors opened on floor 13, it wasn't floor 13 at all...",
17
+ "The dragon apologized profusely for burning down the village...",
18
+ "Every mirror in the house started showing a different reflection...",
19
+ "The food truck only appeared at midnight, and only sold memories...",
20
+ "After 20 years in space, the astronaut returned to find Earth very different...",
21
+ "The magic spell worked, but not quite as intended...",
22
+ "The robot barista made the perfect coffee, but refused to serve humans...",
23
+ "Deep in the Amazon, the explorer found a city that didn't exist on any map..."
24
+ ];
25
+
26
+ // Story genres and their characteristics
27
+ const GENRES = {
28
+ mystery: {
29
+ name: "Mystery",
30
+ prompts: [
31
+ "The detective found a clue that changed everything...",
32
+ "The locked room had no windows, no secret passages, yet someone had escaped...",
33
+ "The witness's story had one fatal flaw..."
34
+ ],
35
+ endings: ["Who was the real culprit?", "What was the missing piece?", "How did they solve the case?"]
36
+ },
37
+ scifi: {
38
+ name: "Science Fiction",
39
+ prompts: [
40
+ "The spacecraft's AI began questioning its own programming...",
41
+ "The new planet looked perfect for colonization, until they discovered...",
42
+ "Time travel was possible, but came with unexpected consequences..."
43
+ ],
44
+ endings: ["What did technology make possible?", "How did they adapt to the future?", "What was discovered in space?"]
45
+ },
46
+ fantasy: {
47
+ name: "Fantasy",
48
+ prompts: [
49
+ "The ancient prophecy was coming true, but backwards...",
50
+ "The wizard's apprentice accidentally unleashed something from the forbidden grimoire...",
51
+ "The enchanted forest began whispering warnings to travelers..."
52
+ ],
53
+ endings: ["What magical power was revealed?", "How was the kingdom saved?", "What did the prophecy really mean?"]
54
+ },
55
+ comedy: {
56
+ name: "Comedy",
57
+ prompts: [
58
+ "The superhero's greatest weakness turned out to be absolutely ridiculous...",
59
+ "The cooking show went horribly wrong when the mystery ingredient was revealed...",
60
+ "The world's worst spy somehow kept succeeding by accident..."
61
+ ],
62
+ endings: ["What was the hilarious misunderstanding?", "How did chaos lead to success?", "What was the ridiculous twist?"]
63
+ }
64
+ };
65
+
66
+ // Create initial story builder state
67
+ function createInitialStoryBuilderState(genre = null, customPrompt = null) {
68
+ let prompt;
69
+ let selectedGenre = 'general';
70
+
71
+ if (customPrompt) {
72
+ prompt = customPrompt;
73
+ selectedGenre = 'custom';
74
+ } else if (genre && GENRES[genre]) {
75
+ selectedGenre = genre;
76
+ const genrePrompts = GENRES[genre].prompts;
77
+ prompt = genrePrompts[Math.floor(Math.random() * genrePrompts.length)];
78
+ } else {
79
+ prompt = STORY_PROMPTS[Math.floor(Math.random() * STORY_PROMPTS.length)];
80
+ }
81
+
82
+ return {
83
+ sentences: [prompt],
84
+ players: [],
85
+ currentPlayer: null,
86
+ moves: 1, // Prompt counts as move 1
87
+ gameOver: false,
88
+ maxSentences: 20, // End after 20 sentences for now
89
+ genre: selectedGenre,
90
+ playerTurnIndex: 0,
91
+ prompt: prompt,
92
+ createdAt: new Date().toISOString(),
93
+ lastActivity: new Date().toISOString(),
94
+ wordCount: prompt.split(' ').length
95
+ };
96
+ }
97
+
98
+ // Add player to story
99
+ function addPlayer(gameState, playerHandle) {
100
+ if (gameState.players.includes(playerHandle)) {
101
+ return { error: 'Player already in the story!' };
102
+ }
103
+
104
+ if (gameState.players.length >= 8) {
105
+ return { error: 'Story is full (max 8 storytellers)' };
106
+ }
107
+
108
+ const newPlayers = [...gameState.players, playerHandle];
109
+
110
+ return {
111
+ success: true,
112
+ gameState: {
113
+ ...gameState,
114
+ players: newPlayers,
115
+ currentPlayer: gameState.currentPlayer || playerHandle // First player starts
116
+ }
117
+ };
118
+ }
119
+
120
+ // Add a sentence to the story
121
+ function addSentence(gameState, sentence, playerHandle) {
122
+ const { sentences, players, currentPlayer, moves, maxSentences } = gameState;
123
+
124
+ // Clean and validate sentence
125
+ const cleanSentence = sentence.trim();
126
+
127
+ if (!cleanSentence) {
128
+ return { error: 'Sentence cannot be empty!' };
129
+ }
130
+
131
+ if (cleanSentence.length > 500) {
132
+ return { error: 'Sentence too long! Keep it under 500 characters.' };
133
+ }
134
+
135
+ // Check if it's the right player's turn (if multiplayer)
136
+ if (players.length > 1 && currentPlayer !== playerHandle) {
137
+ return { error: `Not your turn! Waiting for @${currentPlayer}` };
138
+ }
139
+
140
+ // Add proper punctuation if missing
141
+ let finalSentence = cleanSentence;
142
+ if (!/[.!?]$/.test(finalSentence)) {
143
+ finalSentence += '.';
144
+ }
145
+
146
+ // Update story
147
+ const newSentences = [...sentences, finalSentence];
148
+ const newMoves = moves + 1;
149
+ const newWordCount = gameState.wordCount + finalSentence.split(' ').length;
150
+
151
+ // Determine next player
152
+ let nextPlayerIndex = gameState.playerTurnIndex;
153
+ let nextPlayer = currentPlayer;
154
+
155
+ if (players.length > 1) {
156
+ nextPlayerIndex = (gameState.playerTurnIndex + 1) % players.length;
157
+ nextPlayer = players[nextPlayerIndex];
158
+ }
159
+
160
+ // Check if story should end
161
+ const shouldEnd = newMoves >= maxSentences;
162
+
163
+ const newGameState = {
164
+ ...gameState,
165
+ sentences: newSentences,
166
+ currentPlayer: shouldEnd ? null : nextPlayer,
167
+ playerTurnIndex: nextPlayerIndex,
168
+ moves: newMoves,
169
+ gameOver: shouldEnd,
170
+ lastActivity: new Date().toISOString(),
171
+ wordCount: newWordCount,
172
+ lastContributor: playerHandle
173
+ };
174
+
175
+ return { success: true, gameState: newGameState };
176
+ }
177
+
178
+ // Format story builder display
179
+ function formatStoryBuilderDisplay(gameState) {
180
+ const { sentences, players, currentPlayer, moves, gameOver, maxSentences, genre, wordCount, prompt } = gameState;
181
+
182
+ let display = `📖 **Collaborative Story** (${moves}/${maxSentences} sentences)\n\n`;
183
+
184
+ if (genre !== 'general' && genre !== 'custom') {
185
+ display += `**Genre:** ${GENRES[genre]?.name || genre}\n\n`;
186
+ }
187
+
188
+ if (gameOver) {
189
+ display += '🎉 **Story Complete!** What an adventure!\n\n';
190
+ }
191
+
192
+ // Show the story
193
+ display += '**The Story So Far:**\n';
194
+ display += '```\n';
195
+
196
+ // Show all sentences with paragraph breaks every 3-4 sentences
197
+ let storyText = '';
198
+ for (let i = 0; i < sentences.length; i++) {
199
+ storyText += sentences[i];
200
+
201
+ // Add paragraph break every 3-4 sentences for readability
202
+ if ((i + 1) % 3 === 0 && i < sentences.length - 1) {
203
+ storyText += '\n\n';
204
+ } else if (i < sentences.length - 1) {
205
+ storyText += ' ';
206
+ }
207
+ }
208
+
209
+ display += storyText + '\n```\n\n';
210
+
211
+ // Show stats
212
+ display += `**Stats:** ${sentences.length} sentences, ${wordCount} words\n\n`;
213
+
214
+ // Show players if multiplayer
215
+ if (players.length > 1) {
216
+ display += `**Storytellers (${players.length}):** ${players.map(p => `@${p}`).join(', ')}\n\n`;
217
+ }
218
+
219
+ if (!gameOver) {
220
+ if (players.length > 1) {
221
+ display += `**@${currentPlayer}, continue the story!**\n`;
222
+ } else if (players.length === 1) {
223
+ display += '**Your turn! Add the next sentence.**\n';
224
+ } else {
225
+ display += '**Join the story and add the next sentence!**\n';
226
+ }
227
+
228
+ // Show helpful prompts based on story length
229
+ if (sentences.length < 5) {
230
+ display += 'Building the scene...';
231
+ } else if (sentences.length < 10) {
232
+ display += 'Developing the plot...';
233
+ } else if (sentences.length < 15) {
234
+ display += 'Heading toward the climax...';
235
+ } else {
236
+ display += 'Time to wrap up the story!';
237
+ }
238
+ } else {
239
+ // Show completion message with genre-specific ending questions
240
+ if (genre && GENRES[genre] && GENRES[genre].endings) {
241
+ const endings = GENRES[genre].endings;
242
+ const randomEnding = endings[Math.floor(Math.random() * endings.length)];
243
+ display += `**Reflection:** ${randomEnding}`;
244
+ } else {
245
+ display += '**What a creative journey! Thanks to all the storytellers!**';
246
+ }
247
+ }
248
+
249
+ return display;
250
+ }
251
+
252
+ // Get story statistics
253
+ function getStoryStats(gameState) {
254
+ const { sentences, players } = gameState;
255
+
256
+ if (sentences.length <= 1) return null; // Just the prompt
257
+
258
+ // Calculate statistics
259
+ const totalWords = gameState.wordCount;
260
+ const avgWordsPerSentence = Math.round(totalWords / sentences.length);
261
+
262
+ // Find longest and shortest sentences
263
+ let longest = '';
264
+ let shortest = sentences[0];
265
+
266
+ for (const sentence of sentences) {
267
+ if (sentence.length > longest.length) {
268
+ longest = sentence;
269
+ }
270
+ if (sentence.length < shortest.length && sentence !== gameState.prompt) {
271
+ shortest = sentence;
272
+ }
273
+ }
274
+
275
+ return {
276
+ totalSentences: sentences.length,
277
+ totalWords: totalWords,
278
+ avgWordsPerSentence,
279
+ longestSentence: longest.substring(0, 50) + (longest.length > 50 ? '...' : ''),
280
+ shortestSentence: shortest.substring(0, 50) + (shortest.length > 50 ? '...' : ''),
281
+ contributors: players.length
282
+ };
283
+ }
284
+
285
+ // Suggest story themes based on content
286
+ function analyzeStoryThemes(gameState) {
287
+ const { sentences } = gameState;
288
+ const storyText = sentences.join(' ').toLowerCase();
289
+
290
+ const themes = [];
291
+
292
+ // Look for common themes
293
+ if (/(magic|wizard|spell|dragon|knight|castle|potion)/.test(storyText)) {
294
+ themes.push('🏰 Fantasy Adventure');
295
+ }
296
+
297
+ if (/(space|robot|future|technology|alien|planet|laser)/.test(storyText)) {
298
+ themes.push('🚀 Science Fiction');
299
+ }
300
+
301
+ if (/(detective|mystery|clue|suspect|murder|crime)/.test(storyText)) {
302
+ themes.push('🔍 Mystery & Crime');
303
+ }
304
+
305
+ if (/(funny|hilarious|ridiculous|silly|comedy|laugh)/.test(storyText)) {
306
+ themes.push('😂 Comedy');
307
+ }
308
+
309
+ if (/(love|romance|heart|kiss|wedding|date)/.test(storyText)) {
310
+ themes.push('💕 Romance');
311
+ }
312
+
313
+ if (/(ghost|spirit|haunted|scary|supernatural|witch)/.test(storyText)) {
314
+ themes.push('👻 Supernatural');
315
+ }
316
+
317
+ if (/(adventure|journey|travel|explore|quest|treasure)/.test(storyText)) {
318
+ themes.push('🗺️ Adventure');
319
+ }
320
+
321
+ return themes;
322
+ }
323
+
324
+ // Get random prompt by genre
325
+ function getRandomPrompt(genre = null) {
326
+ if (genre && GENRES[genre]) {
327
+ const prompts = GENRES[genre].prompts;
328
+ return prompts[Math.floor(Math.random() * prompts.length)];
329
+ }
330
+ return STORY_PROMPTS[Math.floor(Math.random() * STORY_PROMPTS.length)];
331
+ }
332
+
333
+ module.exports = {
334
+ createInitialStoryBuilderState,
335
+ addPlayer,
336
+ addSentence,
337
+ formatStoryBuilderDisplay,
338
+ getStoryStats,
339
+ analyzeStoryThemes,
340
+ getRandomPrompt,
341
+ STORY_PROMPTS,
342
+ GENRES
343
+ };
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Tic-Tac-Toe game implementation for /vibe
3
+ * Classic 3x3 grid game with AI opponent support
4
+ * Includes multiple difficulty levels: easy, medium, hard
5
+ */
6
+
7
+ // Create initial tic-tac-toe state
8
+ function createInitialTicTacToeState(difficulty = 'medium') {
9
+ return {
10
+ board: Array(9).fill(''), // 3x3 grid as flat array
11
+ turn: 'X', // X always goes first
12
+ moves: 0,
13
+ winner: null,
14
+ gameOver: false,
15
+ isDraw: false,
16
+ playerSymbol: 'X', // Player is X, AI is O
17
+ aiSymbol: 'O',
18
+ difficulty: difficulty // easy, medium, hard
19
+ };
20
+ }
21
+
22
+ // Check for winner in tic-tac-toe
23
+ function checkWinner(board) {
24
+ const lines = [
25
+ [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
26
+ [0, 3, 6], [1, 4, 7], [2, 5, 8], // cols
27
+ [0, 4, 8], [2, 4, 6] // diagonals
28
+ ];
29
+
30
+ for (const [a, b, c] of lines) {
31
+ if (board[a] && board[a] === board[b] && board[a] === board[c]) {
32
+ return board[a];
33
+ }
34
+ }
35
+
36
+ return null;
37
+ }
38
+
39
+ // Make a move
40
+ function makeMove(gameState, position, playerSymbol) {
41
+ const { board, moves, winner, gameOver } = gameState;
42
+
43
+ // Validate move
44
+ if (gameOver) {
45
+ return { error: 'Game is already over' };
46
+ }
47
+
48
+ if (position < 1 || position > 9) {
49
+ return { error: 'Position must be 1-9' };
50
+ }
51
+
52
+ const index = position - 1; // Convert to 0-based index
53
+
54
+ if (board[index]) {
55
+ return { error: 'Position already taken' };
56
+ }
57
+
58
+ // Make the move
59
+ const newBoard = [...board];
60
+ newBoard[index] = playerSymbol;
61
+
62
+ const newMoves = moves + 1;
63
+ const newWinner = checkWinner(newBoard);
64
+ const newIsDraw = !newWinner && newBoard.every(cell => cell !== '');
65
+ const newGameOver = newWinner || newIsDraw;
66
+ const nextTurn = playerSymbol === 'X' ? 'O' : 'X';
67
+
68
+ const newGameState = {
69
+ ...gameState,
70
+ board: newBoard,
71
+ turn: newGameOver ? playerSymbol : nextTurn,
72
+ moves: newMoves,
73
+ winner: newWinner,
74
+ gameOver: newGameOver,
75
+ isDraw: newIsDraw,
76
+ lastMove: { position, symbol: playerSymbol }
77
+ };
78
+
79
+ return { success: true, gameState: newGameState };
80
+ }
81
+
82
+ // Minimax algorithm for perfect AI play (hard difficulty)
83
+ function minimax(board, depth, isMaximizing, aiSymbol, playerSymbol, alpha = -Infinity, beta = Infinity) {
84
+ const winner = checkWinner(board);
85
+
86
+ // Base cases
87
+ if (winner === aiSymbol) return 10 - depth;
88
+ if (winner === playerSymbol) return depth - 10;
89
+ if (board.every(cell => cell !== '')) return 0; // Draw
90
+
91
+ if (isMaximizing) {
92
+ let maxEval = -Infinity;
93
+ for (let i = 0; i < 9; i++) {
94
+ if (board[i] === '') {
95
+ board[i] = aiSymbol;
96
+ const eval = minimax(board, depth + 1, false, aiSymbol, playerSymbol, alpha, beta);
97
+ board[i] = '';
98
+ maxEval = Math.max(maxEval, eval);
99
+ alpha = Math.max(alpha, eval);
100
+ if (beta <= alpha) break; // Alpha-beta pruning
101
+ }
102
+ }
103
+ return maxEval;
104
+ } else {
105
+ let minEval = Infinity;
106
+ for (let i = 0; i < 9; i++) {
107
+ if (board[i] === '') {
108
+ board[i] = playerSymbol;
109
+ const eval = minimax(board, depth + 1, true, aiSymbol, playerSymbol, alpha, beta);
110
+ board[i] = '';
111
+ minEval = Math.min(minEval, eval);
112
+ beta = Math.min(beta, eval);
113
+ if (beta <= alpha) break; // Alpha-beta pruning
114
+ }
115
+ }
116
+ return minEval;
117
+ }
118
+ }
119
+
120
+ // Get best move using minimax (for hard difficulty)
121
+ function getBestMove(board, aiSymbol, playerSymbol) {
122
+ let bestMove = -1;
123
+ let bestValue = -Infinity;
124
+
125
+ for (let i = 0; i < 9; i++) {
126
+ if (board[i] === '') {
127
+ board[i] = aiSymbol;
128
+ const moveValue = minimax(board, 0, false, aiSymbol, playerSymbol);
129
+ board[i] = '';
130
+
131
+ if (moveValue > bestValue) {
132
+ bestMove = i + 1; // Convert to 1-based index
133
+ bestValue = moveValue;
134
+ }
135
+ }
136
+ }
137
+
138
+ return bestMove;
139
+ }
140
+
141
+ // AI Strategy with difficulty levels
142
+ function getAIMove(gameState, difficulty = 'medium') {
143
+ const { board, aiSymbol, playerSymbol } = gameState;
144
+ const availablePositions = getAvailablePositions(board);
145
+
146
+ if (availablePositions.length === 0) {
147
+ return null;
148
+ }
149
+
150
+ // Override difficulty if specified in gameState
151
+ if (gameState.difficulty) {
152
+ difficulty = gameState.difficulty;
153
+ }
154
+
155
+ switch (difficulty) {
156
+ case 'easy':
157
+ return getEasyAIMove(board, availablePositions, aiSymbol, playerSymbol);
158
+ case 'medium':
159
+ return getMediumAIMove(board, availablePositions, aiSymbol, playerSymbol);
160
+ case 'hard':
161
+ return getHardAIMove(board, aiSymbol, playerSymbol);
162
+ default:
163
+ return getMediumAIMove(board, availablePositions, aiSymbol, playerSymbol);
164
+ }
165
+ }
166
+
167
+ // Easy AI: Mostly random with occasional smart moves
168
+ function getEasyAIMove(board, availablePositions, aiSymbol, playerSymbol) {
169
+ // 70% chance to play randomly
170
+ if (Math.random() < 0.7) {
171
+ const randomIndex = Math.floor(Math.random() * availablePositions.length);
172
+ return availablePositions[randomIndex];
173
+ }
174
+
175
+ // 30% chance to make a smart move
176
+ // 1. Try to win (15% chance)
177
+ if (Math.random() < 0.5) {
178
+ for (const pos of availablePositions) {
179
+ const testBoard = [...board];
180
+ testBoard[pos - 1] = aiSymbol;
181
+ if (checkWinner(testBoard) === aiSymbol) {
182
+ return pos;
183
+ }
184
+ }
185
+ }
186
+
187
+ // 2. Sometimes block player (15% chance)
188
+ if (Math.random() < 0.5) {
189
+ for (const pos of availablePositions) {
190
+ const testBoard = [...board];
191
+ testBoard[pos - 1] = playerSymbol;
192
+ if (checkWinner(testBoard) === playerSymbol) {
193
+ return pos;
194
+ }
195
+ }
196
+ }
197
+
198
+ // Fallback to random
199
+ const randomIndex = Math.floor(Math.random() * availablePositions.length);
200
+ return availablePositions[randomIndex];
201
+ }
202
+
203
+ // Medium AI: Balanced strategy with some randomness
204
+ function getMediumAIMove(board, availablePositions, aiSymbol, playerSymbol) {
205
+ // 1. Always try to win first
206
+ for (const pos of availablePositions) {
207
+ const testBoard = [...board];
208
+ testBoard[pos - 1] = aiSymbol;
209
+ if (checkWinner(testBoard) === aiSymbol) {
210
+ return pos;
211
+ }
212
+ }
213
+
214
+ // 2. Always block player from winning
215
+ for (const pos of availablePositions) {
216
+ const testBoard = [...board];
217
+ testBoard[pos - 1] = playerSymbol;
218
+ if (checkWinner(testBoard) === playerSymbol) {
219
+ return pos;
220
+ }
221
+ }
222
+
223
+ // 3. Add some randomness to avoid being too predictable (30% chance)
224
+ if (Math.random() < 0.3) {
225
+ const randomIndex = Math.floor(Math.random() * availablePositions.length);
226
+ return availablePositions[randomIndex];
227
+ }
228
+
229
+ // 4. Take center if available
230
+ if (availablePositions.includes(5)) {
231
+ return 5;
232
+ }
233
+
234
+ // 5. Take corners
235
+ const corners = [1, 3, 7, 9];
236
+ const availableCorners = corners.filter(c => availablePositions.includes(c));
237
+ if (availableCorners.length > 0) {
238
+ const randomCorner = availableCorners[Math.floor(Math.random() * availableCorners.length)];
239
+ return randomCorner;
240
+ }
241
+
242
+ // 6. Take any remaining position
243
+ const randomIndex = Math.floor(Math.random() * availablePositions.length);
244
+ return availablePositions[randomIndex];
245
+ }
246
+
247
+ // Hard AI: Perfect play using minimax algorithm
248
+ function getHardAIMove(board, aiSymbol, playerSymbol) {
249
+ return getBestMove(board, aiSymbol, playerSymbol);
250
+ }
251
+
252
+ // Make AI move
253
+ function makeAIMove(gameState, difficulty = null) {
254
+ if (gameState.gameOver || gameState.turn !== gameState.aiSymbol) {
255
+ return { error: 'Not AI turn or game is over' };
256
+ }
257
+
258
+ const aiPosition = getAIMove(gameState, difficulty);
259
+ if (!aiPosition) {
260
+ return { error: 'No moves available' };
261
+ }
262
+
263
+ return makeMove(gameState, aiPosition, gameState.aiSymbol);
264
+ }
265
+
266
+ // Format tic-tac-toe board for display
267
+ function formatTicTacToeDisplay(gameState) {
268
+ const { board, moves, winner, isDraw, turn, lastMove, playerSymbol, aiSymbol, difficulty } = gameState;
269
+
270
+ const difficultyEmoji = {
271
+ 'easy': '😊',
272
+ 'medium': '🤔',
273
+ 'hard': '🧠'
274
+ };
275
+
276
+ const difficultyText = difficulty ? `${difficultyEmoji[difficulty] || '🤔'} ${difficulty.toUpperCase()}` : 'MEDIUM 🤔';
277
+
278
+ let display = `🎯 **Tic-Tac-Toe vs AI** (${difficultyText}) - ${moves} moves\n\n`;
279
+
280
+ // Create 3x3 grid display
281
+ const symbols = board.map((cell, i) => cell || (i + 1).toString());
282
+
283
+ display += '```\n';
284
+ for (let row = 0; row < 3; row++) {
285
+ const line = [];
286
+ for (let col = 0; col < 3; col++) {
287
+ const index = row * 3 + col;
288
+ line.push(symbols[index]);
289
+ }
290
+ display += line.join(' │ ') + '\n';
291
+ if (row < 2) display += '──┼───┼──\n';
292
+ }
293
+ display += '```\n\n';
294
+
295
+ if (winner) {
296
+ if (winner === playerSymbol) {
297
+ display += `🎉 **You won!** Great job!`;
298
+ } else {
299
+ display += `🤖 **AI wins!** Better luck next time!`;
300
+ }
301
+ } else if (isDraw) {
302
+ display += `🤝 **Draw!** Well played!`;
303
+ } else {
304
+ if (turn === playerSymbol) {
305
+ display += `Your turn! Choose position 1-9`;
306
+ } else {
307
+ display += `AI is thinking... 🤔`;
308
+ }
309
+
310
+ if (lastMove) {
311
+ const mover = lastMove.symbol === playerSymbol ? 'You' : 'AI';
312
+ display += `\nLast move: ${mover} played ${lastMove.symbol} at position ${lastMove.position}`;
313
+ }
314
+ }
315
+
316
+ return display;
317
+ }
318
+
319
+ // Get available positions
320
+ function getAvailablePositions(board) {
321
+ return board
322
+ .map((cell, index) => cell === '' ? index + 1 : null)
323
+ .filter(pos => pos !== null);
324
+ }
325
+
326
+ // Get difficulty description for users
327
+ function getDifficultyDescription(difficulty) {
328
+ const descriptions = {
329
+ 'easy': '😊 **EASY**: AI plays mostly randomly, great for beginners!',
330
+ 'medium': '🤔 **MEDIUM**: AI uses basic strategy but makes some mistakes.',
331
+ 'hard': '🧠 **HARD**: AI plays perfectly, can you beat it?'
332
+ };
333
+ return descriptions[difficulty] || descriptions['medium'];
334
+ }
335
+
336
+ module.exports = {
337
+ createInitialTicTacToeState,
338
+ makeMove,
339
+ makeAIMove,
340
+ formatTicTacToeDisplay,
341
+ checkWinner,
342
+ getAvailablePositions,
343
+ getAIMove,
344
+ getDifficultyDescription
345
+ };