VISHAL18for4 commited on
Commit
6a74d1e
·
verified ·
1 Parent(s): 9849a30

Delete server.js

Browse files
Files changed (1) hide show
  1. server.js +0 -770
server.js DELETED
@@ -1,770 +0,0 @@
1
- const express = require('express');
2
- const http = require('http');
3
- const { Server } = require('socket.io');
4
- const path = require('path');
5
- const wordLists = require('./words');
6
-
7
- const app = express();
8
- const server = http.createServer(app);
9
- const io = new Server(server, { cors: { origin: '*' } });
10
- const PORT = process.env.PORT || 7860;
11
-
12
- app.use(express.static(path.join(__dirname, 'public')));
13
- // /game now served by index.html (single page app)
14
- app.get('*', (req, res) => res.sendFile(path.join(__dirname, 'public', 'index.html')));
15
-
16
- // ─── In-memory game state ───────────────────────────────────────────────────
17
- const rooms = {};
18
-
19
- function genCode() {
20
- let code;
21
- do { code = Math.random().toString(36).substring(2, 8).toUpperCase(); } while (rooms[code]);
22
- return code;
23
- }
24
-
25
- function getWords(settings, count = 3) {
26
- let pool = [];
27
- if (settings.wordMode === 'custom' && settings.customWords.length >= count) {
28
- pool = [...settings.customWords];
29
- } else if (settings.wordMode === 'mixed') {
30
- const cats = settings.categories || Object.keys(wordLists);
31
- cats.forEach(c => { if (wordLists[c]) pool = pool.concat(wordLists[c]); });
32
- pool = pool.concat(settings.customWords || []);
33
- } else {
34
- const cats = settings.categories || Object.keys(wordLists);
35
- cats.forEach(c => { if (wordLists[c]) pool = pool.concat(wordLists[c]); });
36
- }
37
- pool = [...new Set(pool)];
38
- const choices = [];
39
- while (choices.length < count && pool.length > 0) {
40
- const i = Math.floor(Math.random() * pool.length);
41
- choices.push(pool.splice(i, 1)[0]);
42
- }
43
- return choices;
44
- }
45
-
46
- function hintMask(word, revealed) {
47
- return word.split('').map((c, i) => c === ' ' ? ' ' : revealed.includes(i) ? c : '_').join('');
48
- }
49
-
50
- function levenshtein(a, b) {
51
- const dp = Array.from({ length: a.length + 1 }, (_, i) =>
52
- Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
53
- for (let i = 1; i <= a.length; i++)
54
- for (let j = 1; j <= b.length; j++)
55
- dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
56
- return dp[a.length][b.length];
57
- }
58
-
59
- function createRoom(settings = {}) {
60
- const id = genCode();
61
- rooms[id] = {
62
- id,
63
- host: null,
64
- players: [],
65
- spectators: [],
66
- state: 'waiting',
67
- settings: {
68
- maxPlayers: 10,
69
- rounds: 3,
70
- drawTime: 80,
71
- wordChoices: 3,
72
- customWords: [],
73
- wordMode: 'normal',
74
- categories: Object.keys(wordLists),
75
- teamMode: false,
76
- suddenDeath: false,
77
- quickDraw: false,
78
- hintsEnabled: true,
79
- ...settings
80
- },
81
- currentRound: 0,
82
- drawerOrder: [],
83
- currentDrawerIndex: 0,
84
- currentWord: null,
85
- wordChoices: [],
86
- drawStrokes: [],
87
- timer: null,
88
- chooseTimer: null,
89
- hintTimer1: null,
90
- hintTimer2: null,
91
- timeLeft: 0,
92
- totalTime: 80,
93
- hintsRevealed: [],
94
- roundStartTime: null,
95
- roundScores: [],
96
- usedWords: [],
97
- teams: { red: [], blue: [] },
98
- scoreHistory: [], // [{round, scores:[{id,score}]}] for graph
99
- };
100
- return id;
101
- }
102
-
103
- function addPlayer(room, sid, name, avatar) {
104
- const player = {
105
- id: sid,
106
- name: (name || 'Player').substring(0, 20),
107
- score: 0,
108
- avatar: avatar || { color: `hsl(${Math.floor(Math.random()*360)},70%,60%)`, icon: '😀' },
109
- team: null,
110
- streak: 0,
111
- powerUps: { revealLetter: 1, timeWarp: 1, bomb: 1 },
112
- achievements: [],
113
- isDrawing: false,
114
- hasGuessed: false,
115
- guessTime: null,
116
- isSpectator: false,
117
- wordsDrawn: 0,
118
- };
119
- room.players.push(player);
120
- return player;
121
- }
122
-
123
- function getPlayer(room, sid) {
124
- return room.players.find(p => p.id === sid) || room.spectators.find(p => p.id === sid);
125
- }
126
-
127
- function clearAllTimers(room) {
128
- if (room.timer) { clearInterval(room.timer); room.timer = null; }
129
- if (room.chooseTimer) { clearTimeout(room.chooseTimer); room.chooseTimer = null; }
130
- if (room.hintTimer1) { clearTimeout(room.hintTimer1); room.hintTimer1 = null; }
131
- if (room.hintTimer2) { clearTimeout(room.hintTimer2); room.hintTimer2 = null; }
132
- }
133
-
134
- function shuffle(arr) {
135
- for (let i = arr.length - 1; i > 0; i--) {
136
- const j = Math.floor(Math.random() * (i + 1));
137
- [arr[i], arr[j]] = [arr[j], arr[i]];
138
- }
139
- return arr;
140
- }
141
-
142
- function getRoomData(room) {
143
- return {
144
- id: room.id,
145
- host: room.host,
146
- state: room.state,
147
- settings: room.settings,
148
- players: room.players,
149
- spectators: room.spectators,
150
- currentRound: room.currentRound,
151
- totalRounds: room.settings.rounds,
152
- teams: room.teams,
153
- };
154
- }
155
-
156
- function broadcastPlayers(room) {
157
- io.to(room.id).emit('playerUpdate', { players: room.players });
158
- }
159
-
160
- // ─── Game Flow ───────────────────────────────────────────────────────────────
161
- function startGame(room) {
162
- room.state = 'playing';
163
- room.currentRound = 1;
164
- room.scoreHistory = [];
165
- room.usedWords = [];
166
- room.players.forEach(p => {
167
- p.score = 0; p.streak = 0; p.achievements = [];
168
- p.powerUps = { revealLetter: 1, timeWarp: 1, bomb: 1 };
169
- });
170
-
171
- if (room.settings.teamMode) assignTeams(room);
172
-
173
- room.drawerOrder = shuffle(room.players.map(p => p.id));
174
- room.currentDrawerIndex = 0;
175
- startRound(room);
176
- }
177
-
178
- function assignTeams(room) {
179
- room.teams = { red: [], blue: [] };
180
- room.players.forEach((p, i) => {
181
- p.team = i % 2 === 0 ? 'red' : 'blue';
182
- room.teams[p.team].push(p.id);
183
- });
184
- }
185
-
186
- function startRound(room) {
187
- clearAllTimers(room);
188
- room.players.forEach(p => { p.hasGuessed = false; p.isDrawing = false; p.guessTime = null; });
189
- room.drawStrokes = [];
190
- room.hintsRevealed = [];
191
- room.roundScores = [];
192
- room.currentWord = null;
193
-
194
- // Find drawer, skip if missing
195
- let drawerFound = false;
196
- while (!drawerFound && room.currentDrawerIndex < room.drawerOrder.length) {
197
- const drawerId = room.drawerOrder[room.currentDrawerIndex];
198
- const drawer = room.players.find(p => p.id === drawerId);
199
- if (drawer) { drawer.isDrawing = true; drawerFound = true; }
200
- else room.currentDrawerIndex++;
201
- }
202
-
203
- if (!drawerFound) { nextTurn(room); return; }
204
-
205
- const drawer = room.players.find(p => p.isDrawing);
206
- const choices = getWords(room.settings, room.settings.wordChoices);
207
- room.wordChoices = choices;
208
- room.state = 'choosing';
209
-
210
- io.to(room.id).emit('roundStart', {
211
- round: room.currentRound,
212
- totalRounds: room.settings.rounds,
213
- drawer: { id: drawer.id, name: drawer.name, avatar: drawer.avatar },
214
- players: room.players,
215
- });
216
-
217
- io.to(drawer.id).emit('chooseWord', { choices });
218
-
219
- room.chooseTimer = setTimeout(() => {
220
- if (room.state === 'choosing') {
221
- wordChosen(room, choices[Math.floor(Math.random() * choices.length)]);
222
- }
223
- }, 15000);
224
- }
225
-
226
- function wordChosen(room, word) {
227
- clearAllTimers(room);
228
- room.currentWord = word;
229
- room.usedWords.push(word);
230
- room.state = 'drawing';
231
-
232
- const drawTime = room.settings.quickDraw ? 20 : room.settings.drawTime;
233
- room.timeLeft = drawTime;
234
- room.totalTime = drawTime;
235
- room.roundStartTime = Date.now();
236
-
237
- const drawer = room.players.find(p => p.isDrawing);
238
- const mask = hintMask(word, []);
239
- const lengths = word.split(' ').map(w => w.length);
240
-
241
- io.to(drawer.id).emit('wordChosen', { word, hint: mask, lengths, isDrawer: true });
242
- socket_to_room_except(room.id, drawer.id, 'wordChosen', { word: null, hint: mask, lengths, isDrawer: false });
243
-
244
- // Auto hints
245
- if (room.settings.hintsEnabled) {
246
- room.hintTimer1 = setTimeout(() => { if (room.state === 'drawing') revealHint(room); }, drawTime * 500);
247
- room.hintTimer2 = setTimeout(() => { if (room.state === 'drawing') revealHint(room); }, drawTime * 750);
248
- }
249
-
250
- // Countdown
251
- room.timer = setInterval(() => {
252
- room.timeLeft--;
253
- io.to(room.id).emit('timerUpdate', { timeLeft: room.timeLeft, totalTime: room.totalTime });
254
- if (room.timeLeft <= 0) endRound(room);
255
- }, 1000);
256
- }
257
-
258
- function socket_to_room_except(roomId, exceptId, event, data) {
259
- io.to(roomId).except(exceptId).emit(event, data);
260
- }
261
-
262
- function revealHint(room) {
263
- const word = room.currentWord;
264
- if (!word) return;
265
- const unrevealed = [];
266
- word.split('').forEach((c, i) => { if (c !== ' ' && !room.hintsRevealed.includes(i)) unrevealed.push(i); });
267
- if (unrevealed.length === 0) return;
268
- const n = Math.max(1, Math.floor(unrevealed.length * 0.3));
269
- const picks = shuffle([...unrevealed]).slice(0, n);
270
- room.hintsRevealed.push(...picks);
271
- const drawer = room.players.find(p => p.isDrawing);
272
- const hint = hintMask(word, room.hintsRevealed);
273
- socket_to_room_except(room.id, drawer?.id || '', 'hintUpdate', { hint, revealed: room.hintsRevealed });
274
- }
275
-
276
- function checkGuess(room, sid, text) {
277
- if (!room.currentWord || room.state !== 'drawing') return 'ignore';
278
- const player = room.players.find(p => p.id === sid);
279
- if (!player || player.hasGuessed || player.isDrawing) return 'ignore';
280
-
281
- const guess = text.trim().toLowerCase();
282
- const answer = room.currentWord.toLowerCase();
283
-
284
- if (guess === answer) {
285
- const elapsed = (Date.now() - room.roundStartTime) / 1000;
286
- const timeRatio = Math.max(0, 1 - elapsed / room.totalTime);
287
- const baseScore = 200;
288
- const timeBonus = Math.round(timeRatio * 300);
289
- const firstBonus = room.roundScores.length === 0 ? 50 : 0;
290
- const streakBonus = player.streak * 25;
291
- const total = baseScore + timeBonus + firstBonus + streakBonus;
292
-
293
- player.hasGuessed = true;
294
- player.guessTime = elapsed;
295
- player.score += total;
296
- player.streak++;
297
- player.wordsDrawn;
298
-
299
- const drawer = room.players.find(p => p.isDrawing);
300
- if (drawer) {
301
- const drawerGain = Math.round(50 + (room.roundScores.length === 0 ? 0 : 10));
302
- drawer.score += drawerGain;
303
- }
304
-
305
- room.roundScores.push({ id: sid, score: total, time: elapsed });
306
- checkAchievements(room, player);
307
-
308
- const nonDrawers = room.players.filter(p => !p.isDrawing && !p.isSpectator);
309
- if (nonDrawers.every(p => p.hasGuessed)) setTimeout(() => endRound(room), 1500);
310
-
311
- return { correct: true, score: total, streak: player.streak, firstBonus };
312
- }
313
-
314
- const dist = levenshtein(guess, answer);
315
- if (dist <= 2 && guess.length > 2) return 'close';
316
-
317
- if (room.settings.suddenDeath) {
318
- player.score = Math.max(0, player.score - 15);
319
- broadcastPlayers(room);
320
- }
321
- return 'wrong';
322
- }
323
-
324
- function checkAchievements(room, player) {
325
- const unlocked = [];
326
- const add = (id, name, desc) => {
327
- if (!player.achievements.includes(id)) {
328
- player.achievements.push(id);
329
- unlocked.push({ id, name, desc });
330
- }
331
- };
332
- if (player.guessTime < 8) add('speed_demon', '⚡ Speed Demon', 'Guessed in under 8 seconds!');
333
- if (player.streak >= 3) add('on_fire', '🔥 On Fire', '3 correct guesses in a row!');
334
- if (player.streak >= 5) add('unstoppable', '💎 Unstoppable', '5 in a row!');
335
- if (room.roundScores.length === 1) add('first_blood', '🩸 First Blood', 'First to guess the word!');
336
- if (room.hintsRevealed.length === 0 && player.guessTime < room.totalTime * 0.5)
337
- add('no_hints', '🕵️ Sherlock', 'Guessed with zero hints revealed!');
338
- if (unlocked.length > 0) io.to(player.id).emit('achievementUnlocked', unlocked);
339
- }
340
-
341
- function endRound(room) {
342
- clearAllTimers(room);
343
- if (room.state === 'intermission' || room.state === 'ended') return;
344
- room.state = 'intermission';
345
-
346
- const drawer = room.players.find(p => p.isDrawing);
347
-
348
- // Picasso achievement
349
- const nonDrawers = room.players.filter(p => !p.isDrawing && !p.isSpectator);
350
- if (drawer && nonDrawers.length > 0 && nonDrawers.every(p => p.hasGuessed)) {
351
- if (!drawer.achievements.includes('picasso')) {
352
- drawer.achievements.push('picasso');
353
- io.to(drawer.id).emit('achievementUnlocked', [{ id: 'picasso', name: '🎨 Picasso', desc: 'Everyone guessed your drawing!' }]);
354
- }
355
- drawer.score += 100; // bonus
356
- }
357
-
358
- // Record score history
359
- room.scoreHistory.push({
360
- round: room.currentRound,
361
- drawerTurn: room.currentDrawerIndex,
362
- scores: room.players.map(p => ({ id: p.id, name: p.name, score: p.score }))
363
- });
364
-
365
- io.to(room.id).emit('roundEnd', {
366
- word: room.currentWord,
367
- scores: room.players.map(p => ({
368
- id: p.id, name: p.name, score: p.score, avatar: p.avatar,
369
- roundGain: room.roundScores.find(r => r.id === p.id)?.score || 0,
370
- guessTime: p.guessTime,
371
- })),
372
- drawStrokes: room.drawStrokes,
373
- drawer: drawer ? { id: drawer.id, name: drawer.name } : null,
374
- scoreHistory: room.scoreHistory,
375
- });
376
-
377
- room.timer = setTimeout(() => {
378
- if (!rooms[room.id]) return;
379
- nextTurn(room);
380
- }, 7000);
381
- }
382
-
383
- function nextTurn(room) {
384
- clearAllTimers(room);
385
- room.currentDrawerIndex++;
386
-
387
- if (room.currentDrawerIndex >= room.drawerOrder.length) {
388
- room.currentRound++;
389
- if (room.currentRound > room.settings.rounds) { endGame(room); return; }
390
- room.drawerOrder = shuffle(room.players.map(p => p.id));
391
- room.currentDrawerIndex = 0;
392
- }
393
-
394
- if (room.players.length < 2) {
395
- room.state = 'waiting';
396
- io.to(room.id).emit('gameAborted', { reason: 'Not enough players' });
397
- return;
398
- }
399
- startRound(room);
400
- }
401
-
402
- function endGame(room) {
403
- clearAllTimers(room);
404
- room.state = 'ended';
405
-
406
- const sorted = [...room.players].sort((a, b) => b.score - a.score);
407
-
408
- // Victory achievement
409
- if (sorted[0]) {
410
- const winner = sorted[0];
411
- if (!winner.achievements.includes('champion')) {
412
- winner.achievements.push('champion');
413
- io.to(winner.id).emit('achievementUnlocked', [{ id: 'champion', name: '👑 Champion', desc: 'You won the game!' }]);
414
- }
415
- }
416
-
417
- io.to(room.id).emit('gameEnd', {
418
- players: sorted.map((p, i) => ({
419
- id: p.id, name: p.name, score: p.score,
420
- rank: i + 1, achievements: p.achievements, avatar: p.avatar, streak: p.streak
421
- })),
422
- scoreHistory: room.scoreHistory,
423
- });
424
-
425
- setTimeout(() => { if (rooms[room.id]) delete rooms[room.id]; }, 60000);
426
- }
427
-
428
- // ─── Socket.IO ───────────────────────────────────────────────────────────────
429
- io.on('connection', (socket) => {
430
-
431
- socket.on('createRoom', ({ name, avatar, settings }) => {
432
- const id = createRoom(settings || {});
433
- const room = rooms[id];
434
- room.host = socket.id;
435
- const player = addPlayer(room, socket.id, name, avatar);
436
- socket.join(id);
437
- socket.roomId = id;
438
- socket.emit('roomCreated', { roomId: id, player, room: getRoomData(room) });
439
- });
440
-
441
- socket.on('joinRoom', ({ roomId, name, avatar, asSpectator }) => {
442
- const code = (roomId || '').toUpperCase().trim();
443
- const room = rooms[code];
444
- if (!room) { socket.emit('joinError', { message: 'Room not found. Check the code!' }); return; }
445
- if (room.players.length >= room.settings.maxPlayers) {
446
- socket.emit('joinError', { message: 'Room is full! (max ' + room.settings.maxPlayers + ')' }); return;
447
- }
448
-
449
- socket.join(code);
450
- socket.roomId = code;
451
-
452
- let player;
453
- if (asSpectator) {
454
- player = addPlayer(room, socket.id, name, avatar);
455
- player.isSpectator = true;
456
- room.players = room.players.filter(p => p.id !== socket.id);
457
- room.spectators.push(player);
458
- } else {
459
- player = addPlayer(room, socket.id, name, avatar);
460
- // If joining mid-game, mark as late joiner (they wait for next round to draw)
461
- if (room.state !== 'waiting') {
462
- player.lateJoin = true;
463
- }
464
- }
465
-
466
- socket.emit('roomJoined', { player, room: getRoomData(room) });
467
- socket.to(code).emit('playerJoined', { player, players: room.players, spectators: room.spectators });
468
- });
469
-
470
- socket.on('joinRandom', ({ name, avatar }) => {
471
- // Find existing matchmaking room
472
- let room = Object.values(rooms).find(r =>
473
- r.state === 'waiting' &&
474
- r.isMatchmaking === true &&
475
- r.players.length < r.settings.maxPlayers
476
- );
477
-
478
- if (!room) {
479
- // Create a new matchmaking room
480
- const id = createRoom();
481
- room = rooms[id];
482
- room.isMatchmaking = true;
483
- room.host = socket.id;
484
- }
485
-
486
- const player = addPlayer(room, socket.id, name, avatar);
487
- socket.join(room.id);
488
- socket.roomId = room.id;
489
-
490
- // If first player, set as host
491
- if (room.players.length === 1) room.host = socket.id;
492
-
493
- socket.emit('roomJoined', { player, room: getRoomData(room) });
494
- socket.to(room.id).emit('playerJoined', { player, players: room.players });
495
-
496
- // Broadcast updated matchmaking count
497
- io.to(room.id).emit('matchmakingUpdate', {
498
- players: room.players.length,
499
- needed: 2,
500
- countdown: null
501
- });
502
-
503
- // Cancel any existing auto-start timer
504
- if (room.matchmakingTimer) { clearTimeout(room.matchmakingTimer); room.matchmakingTimer = null; }
505
- if (room.matchmakingCountdown) { clearInterval(room.matchmakingCountdown); room.matchmakingCountdown = null; }
506
-
507
- if (room.players.length >= 2) {
508
- // Start countdown (10s) to allow more players to join
509
- let secs = 10;
510
- io.to(room.id).emit('matchmakingUpdate', { players: room.players.length, needed: 2, countdown: secs });
511
-
512
- room.matchmakingCountdown = setInterval(() => {
513
- secs--;
514
- io.to(room.id).emit('matchmakingUpdate', { players: room.players.length, needed: 2, countdown: secs });
515
- if (secs <= 0) {
516
- clearInterval(room.matchmakingCountdown);
517
- room.matchmakingCountdown = null;
518
- }
519
- }, 1000);
520
-
521
- room.matchmakingTimer = setTimeout(() => {
522
- if (rooms[room.id] && room.state === 'waiting' && room.players.length >= 2) {
523
- room.isMatchmaking = false;
524
- startGame(room);
525
- }
526
- }, 10000);
527
-
528
- } else {
529
- // Only 1 player — wait up to 30s then start solo (practice) mode
530
- io.to(room.id).emit('matchmakingUpdate', { players: 1, needed: 2, countdown: null, waiting: true });
531
-
532
- room.matchmakingTimer = setTimeout(() => {
533
- if (rooms[room.id] && room.state === 'waiting') {
534
- if (room.players.length >= 2) {
535
- room.isMatchmaking = false;
536
- startGame(room);
537
- } else {
538
- // Solo mode - start with 1 player (they draw, no one guesses but they can practice)
539
- room.isMatchmaking = false;
540
- room.settings.rounds = 2;
541
- startGame(room);
542
- }
543
- }
544
- }, 30000);
545
- }
546
- });
547
-
548
- // CRITICAL: When page reloads after game starts, new socket must rejoin room
549
- socket.on('rejoinGame', ({ roomId, name, avatar }) => {
550
- const code = (roomId || '').toUpperCase().trim();
551
- const room = rooms[code];
552
- if (!room) { socket.emit('rejoinError', { message: 'Room expired or not found' }); return; }
553
-
554
- socket.join(code);
555
- socket.roomId = code;
556
-
557
- // Check if player with same name already exists (reconnect)
558
- let player = room.players.find(p => p.name === name);
559
- if (player) {
560
- // Update their socket id to the new one
561
- const oldId = player.id;
562
- player.id = socket.id;
563
- // Update host if needed
564
- if (room.host === oldId) room.host = socket.id;
565
- // Update drawerOrder
566
- const di = room.drawerOrder.indexOf(oldId);
567
- if (di !== -1) room.drawerOrder[di] = socket.id;
568
- } else {
569
- // New player joining mid-game
570
- if (room.players.length >= room.settings.maxPlayers) {
571
- socket.emit('rejoinError', { message: 'Room is full!' }); return;
572
- }
573
- player = addPlayer(room, socket.id, name, avatar);
574
- player.lateJoin = true;
575
- }
576
-
577
- // Send current game state so they can sync up
578
- const drawer = room.players.find(p => p.isDrawing);
579
- socket.emit('gameRejoined', {
580
- player,
581
- room: getRoomData(room),
582
- state: room.state,
583
- round: room.currentRound,
584
- totalRounds: room.settings.rounds,
585
- drawer: drawer ? { id: drawer.id, name: drawer.name, avatar: drawer.avatar } : null,
586
- players: room.players,
587
- hint: room.currentWord ? hintMask(room.currentWord, room.hintsRevealed) : null,
588
- timeLeft: room.timeLeft,
589
- totalTime: room.totalTime,
590
- drawStrokes: room.drawStrokes,
591
- isDrawer: drawer?.id === socket.id,
592
- word: drawer?.id === socket.id ? room.currentWord : null,
593
- });
594
-
595
- // Tell others this player is back
596
- socket.to(code).emit('playerJoined', { player, players: room.players });
597
- });
598
-
599
- socket.on('startGame', () => {
600
- const room = rooms[socket.roomId];
601
- if (!room) return;
602
- if (room.host !== socket.id) { socket.emit('gameError', { message: 'Only the host can start' }); return; }
603
- if (room.players.length < 2) { socket.emit('gameError', { message: 'Need at least 2 players' }); return; }
604
- startGame(room);
605
- });
606
-
607
- socket.on('chooseWord', ({ word }) => {
608
- const room = rooms[socket.roomId];
609
- if (!room || room.state !== 'choosing') return;
610
- const player = room.players.find(p => p.id === socket.id);
611
- if (!player?.isDrawing || !room.wordChoices.includes(word)) return;
612
- wordChosen(room, word);
613
- });
614
-
615
- socket.on('draw', (data) => {
616
- const room = rooms[socket.roomId];
617
- if (!room || room.state !== 'drawing') return;
618
- const player = room.players.find(p => p.id === socket.id);
619
- if (!player?.isDrawing) return;
620
- const stamped = { ...data, t: Date.now() - (room.roundStartTime || Date.now()) };
621
- room.drawStrokes.push(stamped);
622
- socket.to(socket.roomId).emit('draw', data);
623
- });
624
-
625
- socket.on('clearCanvas', () => {
626
- const room = rooms[socket.roomId];
627
- if (!room || room.state !== 'drawing') return;
628
- const player = room.players.find(p => p.id === socket.id);
629
- if (!player?.isDrawing) return;
630
- room.drawStrokes = [{ type: 'clear', t: 0 }];
631
- io.to(socket.roomId).emit('clearCanvas');
632
- });
633
-
634
- socket.on('guess', ({ text }) => {
635
- const room = rooms[socket.roomId];
636
- if (!room || !text) return;
637
- const player = getPlayer(room, socket.id);
638
- if (!player) return;
639
-
640
- // Block drawer from revealing word in chat
641
- if (player.isDrawing) {
642
- socket.emit('chatMessage', { system: true, text: '🤫 You cannot chat while drawing!', type: 'warn' });
643
- return;
644
- }
645
-
646
- const result = checkGuess(room, socket.id, text);
647
-
648
- if (result === 'ignore') return;
649
-
650
- if (result?.correct) {
651
- socket.emit('correctGuess', { score: result.score, streak: result.streak, firstBonus: result.firstBonus });
652
- io.to(socket.roomId).emit('chatMessage', {
653
- system: true,
654
- text: `🎉 ${player.name} guessed it! (+${result.score}pts${result.streak > 1 ? ` 🔥x${result.streak}` : ''})`,
655
- type: 'correct'
656
- });
657
- broadcastPlayers(room);
658
- } else if (result === 'close') {
659
- socket.emit('chatMessage', { system: true, text: `🌡️ So close! Keep trying...`, type: 'close' });
660
- io.to(socket.roomId).emit('chatMessage', { sender: player.name, text: '...', playerId: socket.id, type: 'guess' });
661
- } else {
662
- io.to(socket.roomId).emit('chatMessage', { sender: player.name, text, playerId: socket.id, type: 'guess' });
663
- }
664
- });
665
-
666
- socket.on('chatMessage', ({ text }) => {
667
- const room = rooms[socket.roomId];
668
- if (!room || !text) return;
669
- const player = getPlayer(room, socket.id);
670
- if (!player) return;
671
- // Only allow chat in non-drawing state or for spectators
672
- if (room.state === 'drawing' && !player.isSpectator) {
673
- // This goes through guess flow - ignore raw chat
674
- return;
675
- }
676
- io.to(socket.roomId).emit('chatMessage', { sender: player.name, text: text.substring(0, 120), playerId: socket.id, type: 'chat' });
677
- });
678
-
679
- socket.on('usePowerUp', ({ type }) => {
680
- const room = rooms[socket.roomId];
681
- if (!room || room.state !== 'drawing') return;
682
- const player = room.players.find(p => p.id === socket.id);
683
- if (!player || player.isDrawing || !player.powerUps[type] || player.powerUps[type] <= 0) return;
684
-
685
- player.powerUps[type]--;
686
-
687
- if (type === 'revealLetter') {
688
- revealHint(room);
689
- io.to(socket.roomId).emit('chatMessage', { system: true, text: `🔍 ${player.name} revealed a letter!`, type: 'powerup' });
690
- } else if (type === 'timeWarp') {
691
- room.timeLeft = Math.min(room.timeLeft + 15, room.totalTime);
692
- io.to(socket.roomId).emit('timerUpdate', { timeLeft: room.timeLeft, totalTime: room.totalTime });
693
- io.to(socket.roomId).emit('chatMessage', { system: true, text: `⏰ ${player.name} warped time! +15s`, type: 'powerup' });
694
- } else if (type === 'bomb') {
695
- room.drawStrokes = [{ type: 'clear', t: Date.now() - room.roundStartTime }];
696
- io.to(socket.roomId).emit('clearCanvas');
697
- io.to(socket.roomId).emit('chatMessage', { system: true, text: `💣 ${player.name} dropped a BOMB! Canvas cleared!`, type: 'powerup' });
698
- }
699
-
700
- socket.emit('powerUpUsed', { type, remaining: player.powerUps });
701
- });
702
-
703
- socket.on('reaction', ({ emoji }) => {
704
- const room = rooms[socket.roomId];
705
- if (!room) return;
706
- const player = getPlayer(room, socket.id);
707
- io.to(socket.roomId).emit('reaction', { emoji, name: player?.name || 'Someone' });
708
- });
709
-
710
- socket.on('updateSettings', (settings) => {
711
- const room = rooms[socket.roomId];
712
- if (!room || room.host !== socket.id || room.state !== 'waiting') return;
713
- Object.assign(room.settings, settings);
714
- io.to(socket.roomId).emit('settingsUpdated', room.settings);
715
- });
716
-
717
- socket.on('kickPlayer', ({ playerId }) => {
718
- const room = rooms[socket.roomId];
719
- if (!room || room.host !== socket.id) return;
720
- const target = io.sockets.sockets.get(playerId);
721
- if (target) {
722
- target.emit('kicked', { reason: 'You were kicked by the host' });
723
- target.leave(socket.roomId);
724
- }
725
- room.players = room.players.filter(p => p.id !== playerId);
726
- io.to(socket.roomId).emit('playerLeft', { playerId, players: room.players });
727
- });
728
-
729
- socket.on('requestReplay', () => {
730
- // Allow replay of last round strokes - sent on roundEnd already
731
- });
732
-
733
- socket.on('disconnect', () => {
734
- const room = rooms[socket.roomId];
735
- if (!room) return;
736
-
737
- const wasDrawing = room.players.find(p => p.id === socket.id)?.isDrawing;
738
- room.players = room.players.filter(p => p.id !== socket.id);
739
- room.spectators = room.spectators.filter(p => p.id !== socket.id);
740
-
741
- if (room.players.length === 0 && room.spectators.length === 0) {
742
- clearAllTimers(room);
743
- delete rooms[socket.roomId];
744
- if (room.matchmakingTimer) clearTimeout(room.matchmakingTimer);
745
- if (room.matchmakingCountdown) clearInterval(room.matchmakingCountdown);
746
- return;
747
- }
748
-
749
- if (room.host === socket.id && room.players.length > 0) {
750
- room.host = room.players[0].id;
751
- io.to(room.host).emit('youAreHost');
752
- }
753
-
754
- io.to(socket.roomId).emit('playerLeft', { playerId: socket.id, players: room.players });
755
-
756
- if (wasDrawing && (room.state === 'drawing' || room.state === 'choosing')) {
757
- endRound(room);
758
- }
759
-
760
- if (room.players.length < 2 && room.state !== 'waiting' && room.state !== 'ended') {
761
- clearAllTimers(room);
762
- room.state = 'waiting';
763
- io.to(socket.roomId).emit('gameAborted', { reason: 'Not enough players to continue' });
764
- }
765
- });
766
- });
767
-
768
- server.listen(PORT, '0.0.0.0', () => {
769
- console.log(`🎨 Drawable running at http://localhost:${PORT}`);
770
- });