dcrey7 commited on
Commit
e30257d
·
verified ·
1 Parent(s): eadea0f

Upload 9 files

Browse files
README.md CHANGED
@@ -1,13 +1 @@
1
- ---
2
- title: Team 16trial
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 5.13.1
8
- app_file: app.py
9
- pinned: false
10
- short_description: trial
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ # trial
 
 
 
 
 
 
 
 
 
 
 
 
app.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ from flask_socketio import SocketIO, emit
3
+ import os
4
+ import requests
5
+ import json
6
+ import uuid
7
+ from datetime import datetime
8
+ from dotenv import load_dotenv
9
+ import logging
10
+ from werkzeug.utils import secure_filename
11
+ import random
12
+ import asyncio
13
+
14
+ # Initialize Flask and configure core settings
15
+ app = Flask(__name__)
16
+ app.config['SECRET_KEY'] = os.urandom(24) # Generate a random secret key for security
17
+ app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # Limit file uploads to 16MB
18
+
19
+ # Initialize SocketIO with CORS support for development
20
+ socketio = SocketIO(app, cors_allowed_origins="*")
21
+
22
+ # Load environment variables from .env file
23
+ load_dotenv()
24
+ MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY')
25
+ ELEVENLABS_API_KEY = os.getenv('ELEVENLABS_API_KEY')
26
+
27
+ # Configure logging to track application behavior
28
+ logging.basicConfig(level=logging.INFO)
29
+ logger = logging.getLogger(__name__)
30
+
31
+ class GameState:
32
+ """Manages the state of all active game sessions."""
33
+
34
+ def __init__(self):
35
+ """Initialize the game state manager."""
36
+ self.games = {} # Dictionary to store all active games
37
+ self.cleanup_interval = 3600 # Cleanup inactive games every hour
38
+
39
+ def create_game(self):
40
+ """Create a new game session with a unique identifier."""
41
+ game_id = str(uuid.uuid4())
42
+ self.games[game_id] = {
43
+ 'players': [],
44
+ 'current_phase': 'setup',
45
+ 'recordings': {},
46
+ 'impostor': None,
47
+ 'votes': {},
48
+ 'question': None,
49
+ 'impostor_answer': None,
50
+ 'modified_recording': None,
51
+ 'round_number': 1,
52
+ 'start_time': datetime.now().isoformat(),
53
+ 'completed_rounds': [],
54
+ 'score': {'impostor_wins': 0, 'player_wins': 0}
55
+ }
56
+ return game_id
57
+
58
+ def cleanup_inactive_games(self):
59
+ """Remove inactive game sessions older than 2 hours."""
60
+ current_time = datetime.now()
61
+ inactive_threshold = 7200 # 2 hours in seconds
62
+
63
+ for game_id, game in list(self.games.items()):
64
+ start_time = datetime.fromisoformat(game['start_time'])
65
+ if (current_time - start_time).total_seconds() > inactive_threshold:
66
+ del self.games[game_id]
67
+
68
+ # Initialize global game state
69
+ game_state = GameState()
70
+
71
+ async def generate_question():
72
+ """Generate an engaging question using Mistral AI."""
73
+ try:
74
+ headers = {
75
+ 'Authorization': f'Bearer {MISTRAL_API_KEY}',
76
+ 'Content-Type': 'application/json'
77
+ }
78
+
79
+ # Craft a prompt that encourages interesting, personal questions
80
+ payload = {
81
+ 'messages': [{
82
+ 'role': 'user',
83
+ 'content': '''Generate an engaging personal question for a social game.
84
+ The question should:
85
+ 1. Encourage creative and unique responses
86
+ 2. Be open-ended but not too philosophical
87
+ 3. Be answerable in 15-30 seconds
88
+ 4. Be appropriate for all ages
89
+ 5. Spark interesting conversation
90
+
91
+ Generate only the question, without any additional text.'''
92
+ }]
93
+ }
94
+
95
+ response = requests.post(
96
+ 'https://api.mistral.ai/v1/chat/completions',
97
+ headers=headers,
98
+ json=payload,
99
+ timeout=10 # Set timeout to handle slow responses
100
+ )
101
+
102
+ if response.status_code == 200:
103
+ question = response.json()['choices'][0]['message']['content'].strip()
104
+ logger.info(f"Generated question: {question}")
105
+ return question
106
+ else:
107
+ logger.error(f"Mistral API error: {response.status_code}")
108
+ # Fallback questions if API fails
109
+ fallback_questions = [
110
+ "What is your favorite childhood memory?",
111
+ "What's the most interesting place you've ever visited?",
112
+ "What's a skill you'd love to master and why?",
113
+ "What's the best piece of advice you've ever received?"
114
+ ]
115
+ return random.choice(fallback_questions)
116
+
117
+ except Exception as e:
118
+ logger.error(f"Error generating question: {str(e)}")
119
+ return "What is your favorite memory from your childhood?"
120
+
121
+ async def generate_impostor_answer(question):
122
+ """Generate a convincing impostor response using Mistral AI."""
123
+ try:
124
+ headers = {
125
+ 'Authorization': f'Bearer {MISTRAL_API_KEY}',
126
+ 'Content-Type': 'application/json'
127
+ }
128
+
129
+ # Craft a detailed prompt for generating a natural response
130
+ prompt = f'''Given the question "{question}", generate a detailed and convincing personal response.
131
+ The response should:
132
+ 1. Sound natural and conversational
133
+ 2. Be 2-3 sentences long
134
+ 3. Include specific details to sound authentic
135
+ 4. Be suitable for text-to-speech conversion
136
+ 5. Avoid complex words or punctuation that might affect voice synthesis
137
+
138
+ Generate only the response, without any additional context.'''
139
+
140
+ payload = {
141
+ 'messages': [{
142
+ 'role': 'user',
143
+ 'content': prompt
144
+ }]
145
+ }
146
+
147
+ response = requests.post(
148
+ 'https://api.mistral.ai/v1/chat/completions',
149
+ headers=headers,
150
+ json=payload,
151
+ timeout=10
152
+ )
153
+
154
+ if response.status_code == 200:
155
+ answer = response.json()['choices'][0]['message']['content'].strip()
156
+ logger.info(f"Generated impostor answer: {answer}")
157
+ return answer
158
+ else:
159
+ logger.error(f"Mistral API error generating answer: {response.status_code}")
160
+ return "I have an interesting story about that, but I'd need more time to explain it properly."
161
+
162
+ except Exception as e:
163
+ logger.error(f"Error generating impostor answer: {str(e)}")
164
+ return "I have an interesting story about that, but I'd need more time to explain it properly."
165
+
166
+ async def clone_voice(audio_file):
167
+ """Clone a voice using ElevenLabs API."""
168
+ try:
169
+ headers = {
170
+ 'xi-api-key': ELEVENLABS_API_KEY
171
+ }
172
+
173
+ with open(audio_file, 'rb') as f:
174
+ files = {
175
+ 'files': ('recording.wav', f, 'audio/wav')
176
+ }
177
+
178
+ response = requests.post(
179
+ 'https://api.elevenlabs.io/v1/voices/add',
180
+ headers=headers,
181
+ files=files,
182
+ timeout=30
183
+ )
184
+
185
+ if response.status_code == 200:
186
+ voice_id = response.json().get('voice_id')
187
+ logger.info(f"Successfully cloned voice: {voice_id}")
188
+ return voice_id
189
+ else:
190
+ logger.error(f"ElevenLabs voice cloning error: {response.status_code}")
191
+ return None
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error cloning voice: {str(e)}")
195
+ return None
196
+
197
+ async def generate_cloned_speech(voice_id, text):
198
+ """Generate speech using ElevenLabs with a cloned voice."""
199
+ try:
200
+ headers = {
201
+ 'xi-api-key': ELEVENLABS_API_KEY,
202
+ 'Content-Type': 'application/json'
203
+ }
204
+
205
+ payload = {
206
+ 'text': text,
207
+ 'voice_settings': {
208
+ 'stability': 0.75,
209
+ 'similarity_boost': 0.75
210
+ }
211
+ }
212
+
213
+ response = requests.post(
214
+ f'https://api.elevenlabs.io/v1/text-to-speech/{voice_id}',
215
+ headers=headers,
216
+ json=payload,
217
+ timeout=30
218
+ )
219
+
220
+ if response.status_code == 200:
221
+ filename = f"temp/impostor_audio_{uuid.uuid4()}.mp3"
222
+ with open(filename, 'wb') as f:
223
+ f.write(response.content)
224
+ logger.info(f"Generated cloned speech: {filename}")
225
+ return filename
226
+ else:
227
+ logger.error(f"ElevenLabs speech generation error: {response.status_code}")
228
+ return None
229
+
230
+ except Exception as e:
231
+ logger.error(f"Error generating cloned speech: {str(e)}")
232
+ return None
233
+
234
+ @app.route('/')
235
+ def home():
236
+ """Serve the main game page."""
237
+ return render_template('index.html')
238
+
239
+ @app.route('/api/start_game', methods=['POST'])
240
+ async def start_game():
241
+ """Initialize a new game session."""
242
+ try:
243
+ data = request.get_json()
244
+ game_id = data.get('game_id')
245
+
246
+ if game_id not in game_state.games:
247
+ return jsonify({'error': 'Game not found'}), 404
248
+
249
+ game = game_state.games[game_id]
250
+
251
+ # Generate question for the round
252
+ question = await generate_question()
253
+ game['question'] = question
254
+ game['current_phase'] = 'recording'
255
+
256
+ return jsonify({
257
+ 'status': 'success',
258
+ 'question': question
259
+ })
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error starting game: {str(e)}")
263
+ return jsonify({'error': 'Internal server error'}), 500
264
+
265
+ @app.route('/api/submit_recording', methods=['POST'])
266
+ async def submit_recording():
267
+ """Handle voice recording submissions."""
268
+ try:
269
+ game_id = request.form.get('game_id')
270
+ player_id = request.form.get('player_id')
271
+ audio_file = request.files.get('audio')
272
+
273
+ if not all([game_id, player_id, audio_file]):
274
+ return jsonify({'error': 'Missing required data'}), 400
275
+
276
+ if game_id not in game_state.games:
277
+ return jsonify({'error': 'Game not found'}), 404
278
+
279
+ # Save the recording
280
+ filename = secure_filename(f"recording_{game_id}_{player_id}.wav")
281
+ filepath = os.path.join('temp', filename)
282
+ audio_file.save(filepath)
283
+
284
+ game_state.games[game_id]['recordings'][player_id] = filepath
285
+
286
+ return jsonify({'status': 'success'})
287
+
288
+ except Exception as e:
289
+ logger.error(f"Error submitting recording: {str(e)}")
290
+ return jsonify({'error': 'Internal server error'}), 500
291
+
292
+ @app.route('/api/process_impostor', methods=['POST'])
293
+ async def process_impostor():
294
+ """Handle the complete impostor voice generation process."""
295
+ try:
296
+ data = request.get_json()
297
+ game_id = data.get('game_id')
298
+ impostor_id = data.get('impostor_id')
299
+
300
+ if game_id not in game_state.games:
301
+ return jsonify({'error': 'Game not found'}), 404
302
+
303
+ game = game_state.games[game_id]
304
+
305
+ # Generate impostor's answer
306
+ impostor_answer = await generate_impostor_answer(game['question'])
307
+ game['impostor_answer'] = impostor_answer
308
+
309
+ # Get impostor's original recording
310
+ original_recording = game['recordings'].get(impostor_id)
311
+ if not original_recording:
312
+ return jsonify({'error': 'Original recording not found'}), 404
313
+
314
+ # Clone voice
315
+ voice_id = await clone_voice(original_recording)
316
+ if not voice_id:
317
+ return jsonify({'error': 'Voice cloning failed'}), 500
318
+
319
+ # Generate modified speech
320
+ audio_file = await generate_cloned_speech(voice_id, impostor_answer)
321
+ if not audio_file:
322
+ return jsonify({'error': 'Speech generation failed'}), 500
323
+
324
+ game['modified_recording'] = audio_file
325
+
326
+ return jsonify({
327
+ 'status': 'success',
328
+ 'audio_url': audio_file
329
+ })
330
+
331
+ except Exception as e:
332
+ logger.error(f"Error processing impostor: {str(e)}")
333
+ return jsonify({'error': 'Internal server error'}), 500
334
+
335
+ @socketio.on('join_game')
336
+ def handle_join_game(data):
337
+ """Handle a player joining the game."""
338
+ game_id = data.get('game_id')
339
+ player_name = data.get('player_name')
340
+
341
+ if game_id in game_state.games:
342
+ game = game_state.games[game_id]
343
+ if len(game['players']) < 5:
344
+ player_id = len(game['players']) + 1
345
+ game['players'].append({
346
+ 'id': player_id,
347
+ 'name': player_name
348
+ })
349
+ emit('player_joined', {
350
+ 'player_id': player_id,
351
+ 'player_name': player_name
352
+ }, broadcast=True, room=game_id)
353
+
354
+ @socketio.on('submit_vote')
355
+ def handle_vote(data):
356
+ """Process player votes and determine round outcome."""
357
+ game_id = data.get('game_id')
358
+ voter_id = data.get('voter_id')
359
+ vote_for = data.get('vote_for')
360
+
361
+ if game_id in game_state.games:
362
+ game = game_state.games[game_id]
363
+ game['votes'][voter_id] = vote_for
364
+
365
+ # Check if all players have voted
366
+ if len(game['votes']) == len(game['players']):
367
+ # Calculate results
368
+ votes_count = {}
369
+ for vote in game['votes'].values():
370
+ votes_count[vote] = votes_count.get(vote, 0) + 1
371
+
372
+ most_voted = max(votes_count.items(), key=lambda x: x[1])[0]
373
+
374
+ # Update scores
375
+ if most_voted == game['impostor']:
376
+ game['score']['player_wins'] += 1
377
+ else:
378
+ game['score']['impostor_wins'] += 1
379
+
380
+ # Store round results
381
+ game['completed_rounds'].append({
382
+ 'round_number': game['round_number'],
383
+ 'impostor': game['impostor'],
384
+ 'votes': game['votes'].copy(),
385
+ 'most_voted': most_voted
386
+ })
387
+
388
+ # Emit results
389
+ emit('round_result', {
390
+ 'impostor': game['impostor'],
391
+ 'most_voted': most_voted,
392
+ 'votes': game['votes'],
393
+ 'score': game['score']
394
+ }, broadcast=True, room=game_id)
395
+
396
+ if __name__ == '__main__':
397
+ # Create temporary directory for recordings if it doesn't exist
398
+ os.makedirs('temp', exist_ok=True)
399
+
400
+ # Start the server
401
+ socketio.run(app, host='0.0.0.0', port=7860, debug=True)
requirements.txt ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Web Framework and Server
2
+ Flask==2.3.3
3
+ Flask-SocketIO==5.3.6
4
+ python-dotenv==1.0.0
5
+ gunicorn==21.2.0
6
+ eventlet==0.33.3
7
+ Werkzeug==2.3.7
8
+
9
+ # Real-time Communication
10
+ websockets==11.0.3
11
+ python-engineio==4.5.1
12
+ python-socketio==5.8.0
13
+
14
+ # API and Network Requests
15
+ requests==2.31.0
16
+ urllib3==2.0.4
17
+ certifi==2023.7.22
18
+
19
+ # Audio Processing
20
+ pydub==0.25.1
21
+ soundfile==0.12.1
22
+ numpy==1.24.3
23
+
24
+ # Error Handling and Logging
25
+ python-json-logger==2.0.7
26
+
27
+ # Security
28
+ PyJWT==2.8.0
29
+
30
+ # CORS support
31
+ Flask-CORS==4.0.0
32
+
33
+ # Time zone support
34
+ pytz==2023.3
35
+
36
+ # File handling
37
+ python-magic==0.4.27
static/css/styles.css ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Base styling and CSS variables for consistent theming */
2
+ :root {
3
+ /* Color palette */
4
+ --bg-primary: #0a0a0a;
5
+ --bg-secondary: #1a1a1a;
6
+ --text-primary: #ffffff;
7
+ --text-secondary: #cccccc;
8
+ --accent-red: #ff0000;
9
+ --accent-green: #00ff00;
10
+
11
+ /* Animation timings */
12
+ --transition-speed: 0.3s;
13
+
14
+ /* Spacing units */
15
+ --spacing-sm: 0.5rem;
16
+ --spacing-md: 1rem;
17
+ --spacing-lg: 2rem;
18
+ }
19
+
20
+ /* Global reset and base styles */
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: 'Pixelify Sans', sans-serif;
29
+ background-color: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ line-height: 1.6;
32
+ overflow-x: hidden;
33
+ }
34
+
35
+ /* WebGL background container */
36
+ #webgl-background {
37
+ position: fixed;
38
+ top: 0;
39
+ left: 0;
40
+ width: 100vw;
41
+ height: 100vh;
42
+ z-index: 1;
43
+ }
44
+
45
+ /* Main game container */
46
+ #game-container {
47
+ position: relative;
48
+ z-index: 2;
49
+ min-height: 100vh;
50
+ padding: var(--spacing-lg);
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ justify-content: center;
55
+ }
56
+
57
+ /* Game pages styling */
58
+ .game-page {
59
+ display: none;
60
+ width: 100%;
61
+ max-width: 800px;
62
+ margin: 0 auto;
63
+ padding: var(--spacing-lg);
64
+ }
65
+
66
+ .game-page.active {
67
+ display: block;
68
+ animation: fadeIn 0.5s ease-in-out;
69
+ }
70
+
71
+ /* Landing page specific styles */
72
+ #landing-page {
73
+ text-align: center;
74
+ }
75
+
76
+ .game-title {
77
+ font-size: 4rem;
78
+ margin-bottom: var(--spacing-lg);
79
+ text-transform: uppercase;
80
+ letter-spacing: 0.2em;
81
+ }
82
+
83
+ .title-emphasis {
84
+ color: var(--accent-red);
85
+ }
86
+
87
+ /* Button styles */
88
+ .button {
89
+ background: transparent;
90
+ border: 2px solid var(--text-primary);
91
+ color: var(--text-primary);
92
+ padding: var(--spacing-md) var(--spacing-lg);
93
+ font-family: 'Pixelify Sans', sans-serif;
94
+ font-size: 1.2rem;
95
+ cursor: pointer;
96
+ transition: all var(--transition-speed) ease;
97
+ }
98
+
99
+ .button:hover {
100
+ background: var(--text-primary);
101
+ color: var(--bg-primary);
102
+ }
103
+
104
+ .button:disabled {
105
+ opacity: 0.5;
106
+ cursor: not-allowed;
107
+ }
108
+
109
+ /* Player setup page styles */
110
+ .player-grid {
111
+ display: grid;
112
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
113
+ gap: var(--spacing-md);
114
+ margin-bottom: var(--spacing-lg);
115
+ }
116
+
117
+ .player-avatar {
118
+ width: 100px;
119
+ height: 100px;
120
+ border-radius: 50%;
121
+ border: 2px solid var(--text-primary);
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ font-size: 2rem;
126
+ margin: 0 auto;
127
+ }
128
+
129
+ /* Recording phase styles */
130
+ .question-display {
131
+ text-align: center;
132
+ font-size: 1.5rem;
133
+ margin-bottom: var(--spacing-lg);
134
+ padding: var(--spacing-md);
135
+ background: var(--bg-secondary);
136
+ border-radius: 8px;
137
+ }
138
+
139
+ .timer {
140
+ position: absolute;
141
+ top: var(--spacing-md);
142
+ left: var(--spacing-md);
143
+ font-size: 2rem;
144
+ color: var(--accent-red);
145
+ }
146
+
147
+ .recording-controls {
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: center;
151
+ gap: var(--spacing-md);
152
+ }
153
+
154
+ /* Listening phase styles */
155
+ .audio-player {
156
+ width: 100%;
157
+ margin-bottom: var(--spacing-md);
158
+ }
159
+
160
+ .player-list {
161
+ display: flex;
162
+ flex-direction: column;
163
+ gap: var(--spacing-md);
164
+ }
165
+
166
+ /* Voting phase styles */
167
+ .vote-options {
168
+ display: grid;
169
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
170
+ gap: var(--spacing-md);
171
+ margin-top: var(--spacing-lg);
172
+ }
173
+
174
+ .vote-button {
175
+ width: 100px;
176
+ height: 100px;
177
+ border-radius: 50%;
178
+ border: 2px solid var(--text-primary);
179
+ background: transparent;
180
+ color: var(--text-primary);
181
+ font-size: 1.5rem;
182
+ cursor: pointer;
183
+ transition: all var(--transition-speed) ease;
184
+ }
185
+
186
+ .vote-button:hover {
187
+ background: var(--text-primary);
188
+ color: var(--bg-primary);
189
+ }
190
+
191
+ /* Results page styles */
192
+ .results-container {
193
+ text-align: center;
194
+ }
195
+
196
+ .result-message {
197
+ font-size: 2rem;
198
+ margin-bottom: var(--spacing-lg);
199
+ }
200
+
201
+ .impostor-reveal {
202
+ color: var(--accent-red);
203
+ font-size: 1.5rem;
204
+ margin-bottom: var(--spacing-lg);
205
+ }
206
+
207
+ /* Animation keyframes */
208
+ @keyframes fadeIn {
209
+ from {
210
+ opacity: 0;
211
+ transform: translateY(20px);
212
+ }
213
+ to {
214
+ opacity: 1;
215
+ transform: translateY(0);
216
+ }
217
+ }
218
+
219
+ /* Settings icon */
220
+ .settings-icon {
221
+ position: absolute;
222
+ top: var(--spacing-md);
223
+ right: var(--spacing-md);
224
+ font-size: 1.5rem;
225
+ cursor: pointer;
226
+ transition: transform var(--transition-speed) ease;
227
+ }
228
+
229
+ .settings-icon:hover {
230
+ transform: rotate(90deg);
231
+ }
232
+
233
+ /* Responsive design */
234
+ @media (max-width: 768px) {
235
+ .game-title {
236
+ font-size: 3rem;
237
+ }
238
+
239
+ .player-grid {
240
+ grid-template-columns: repeat(2, 1fr);
241
+ }
242
+
243
+ .player-avatar {
244
+ width: 80px;
245
+ height: 80px;
246
+ font-size: 1.5rem;
247
+ }
248
+
249
+ .question-display {
250
+ font-size: 1.2rem;
251
+ }
252
+ }
253
+
254
+ @media (max-width: 480px) {
255
+ .game-title {
256
+ font-size: 2rem;
257
+ }
258
+
259
+ .player-grid {
260
+ grid-template-columns: 1fr;
261
+ }
262
+
263
+ .button {
264
+ width: 100%;
265
+ margin-bottom: var(--spacing-sm);
266
+ }
267
+ }
268
+
269
+ /* Loading and transition states */
270
+ .loading {
271
+ position: fixed;
272
+ top: 0;
273
+ left: 0;
274
+ width: 100vw;
275
+ height: 100vh;
276
+ background: rgba(0, 0, 0, 0.8);
277
+ display: flex;
278
+ align-items: center;
279
+ justify-content: center;
280
+ z-index: 1000;
281
+ }
282
+
283
+ .loading-spinner {
284
+ width: 50px;
285
+ height: 50px;
286
+ border: 4px solid var(--text-secondary);
287
+ border-top-color: var(--accent-red);
288
+ border-radius: 50%;
289
+ animation: spin 1s linear infinite;
290
+ }
291
+
292
+ @keyframes spin {
293
+ to {
294
+ transform: rotate(360deg);
295
+ }
296
+ }
297
+
298
+ /* Error message styling */
299
+ .error-message {
300
+ background: rgba(255, 0, 0, 0.2);
301
+ border: 1px solid var(--accent-red);
302
+ padding: var(--spacing-md);
303
+ margin: var(--spacing-md) 0;
304
+ border-radius: 4px;
305
+ text-align: center;
306
+ }
static/js/audio.js ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class AudioManager {
2
+ constructor() {
3
+ // Core audio configuration
4
+ this.config = {
5
+ sampleRate: 44100,
6
+ channels: 1,
7
+ bitDepth: 16,
8
+ maxRecordingTime: 30000, // 30 seconds in milliseconds
9
+ minRecordingTime: 15000 // 15 seconds in milliseconds
10
+ };
11
+
12
+ // Recording state management
13
+ this.state = {
14
+ isRecording: false,
15
+ startTime: null,
16
+ recorder: null,
17
+ stream: null,
18
+ audioChunks: [],
19
+ audioContext: null,
20
+ analyser: null
21
+ };
22
+
23
+ // Audio visualization settings
24
+ this.visualizer = {
25
+ canvasContext: null,
26
+ dataArray: null,
27
+ bufferLength: null,
28
+ width: 0,
29
+ height: 0
30
+ };
31
+
32
+ // Initialize audio context with error handling
33
+ try {
34
+ this.state.audioContext = new (window.AudioContext || window.webkitAudioContext)();
35
+ } catch (error) {
36
+ console.error('AudioContext not supported in this browser');
37
+ }
38
+
39
+ // Bind methods to maintain context
40
+ this.startRecording = this.startRecording.bind(this);
41
+ this.stopRecording = this.stopRecording.bind(this);
42
+ this.processAudio = this.processAudio.bind(this);
43
+ }
44
+
45
+ /**
46
+ * Initialize audio visualization on a canvas element
47
+ * @param {HTMLCanvasElement} canvas - The canvas element for visualization
48
+ */
49
+ initializeVisualizer(canvas) {
50
+ if (!canvas) return;
51
+
52
+ this.visualizer.canvasContext = canvas.getContext('2d');
53
+ this.visualizer.width = canvas.width;
54
+ this.visualizer.height = canvas.height;
55
+
56
+ // Set up audio analyser for visualization
57
+ this.state.analyser = this.state.audioContext.createAnalyser();
58
+ this.state.analyser.fftSize = 2048;
59
+ this.visualizer.bufferLength = this.state.analyser.frequencyBinCount;
60
+ this.visualizer.dataArray = new Uint8Array(this.visualizer.bufferLength);
61
+ }
62
+
63
+ /**
64
+ * Start recording audio with visualization
65
+ * @returns {Promise<void>}
66
+ */
67
+ async startRecording() {
68
+ try {
69
+ // Request microphone access
70
+ this.state.stream = await navigator.mediaDevices.getUserMedia({
71
+ audio: {
72
+ channelCount: this.config.channels,
73
+ sampleRate: this.config.sampleRate
74
+ }
75
+ });
76
+
77
+ // Create and configure MediaRecorder
78
+ this.state.recorder = new MediaRecorder(this.state.stream, {
79
+ mimeType: 'audio/webm;codecs=opus'
80
+ });
81
+
82
+ // Set up recording event handlers
83
+ this.state.recorder.ondataavailable = (event) => {
84
+ if (event.data.size > 0) {
85
+ this.state.audioChunks.push(event.data);
86
+ }
87
+ };
88
+
89
+ // Connect audio nodes for visualization
90
+ const source = this.state.audioContext.createMediaStreamSource(this.state.stream);
91
+ source.connect(this.state.analyser);
92
+
93
+ // Start recording
94
+ this.state.recorder.start(100); // Collect data every 100ms
95
+ this.state.isRecording = true;
96
+ this.state.startTime = Date.now();
97
+
98
+ // Start visualization if canvas is set up
99
+ if (this.visualizer.canvasContext) {
100
+ this.drawVisualization();
101
+ }
102
+
103
+ // Set up automatic recording stop
104
+ setTimeout(() => {
105
+ if (this.state.isRecording) {
106
+ this.stopRecording();
107
+ }
108
+ }, this.config.maxRecordingTime);
109
+
110
+ } catch (error) {
111
+ console.error('Error starting recording:', error);
112
+ throw new Error('Failed to start recording');
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Stop recording and process the audio
118
+ * @returns {Promise<Blob>} The processed audio blob
119
+ */
120
+ async stopRecording() {
121
+ return new Promise((resolve, reject) => {
122
+ try {
123
+ const recordingDuration = Date.now() - this.state.startTime;
124
+
125
+ // Check if recording meets minimum duration
126
+ if (recordingDuration < this.config.minRecordingTime) {
127
+ throw new Error('Recording too short');
128
+ }
129
+
130
+ this.state.recorder.onstop = async () => {
131
+ try {
132
+ const audioBlob = await this.processAudio();
133
+ resolve(audioBlob);
134
+ } catch (error) {
135
+ reject(error);
136
+ }
137
+ };
138
+
139
+ // Stop recording and clean up
140
+ this.state.recorder.stop();
141
+ this.state.stream.getTracks().forEach(track => track.stop());
142
+ this.state.isRecording = false;
143
+
144
+ } catch (error) {
145
+ reject(error);
146
+ }
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Process recorded audio chunks into a single blob
152
+ * @returns {Promise<Blob>}
153
+ */
154
+ async processAudio() {
155
+ try {
156
+ // Combine audio chunks into a single blob
157
+ const audioBlob = new Blob(this.state.audioChunks, { type: 'audio/webm;codecs=opus' });
158
+
159
+ // Convert to proper format for ElevenLabs API
160
+ const arrayBuffer = await audioBlob.arrayBuffer();
161
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
162
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
163
+
164
+ // Create WAV format buffer
165
+ const wavBuffer = this.createWAVBuffer(audioBuffer);
166
+
167
+ return new Blob([wavBuffer], { type: 'audio/wav' });
168
+ } catch (error) {
169
+ console.error('Error processing audio:', error);
170
+ throw new Error('Failed to process audio');
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Create WAV buffer from audio buffer
176
+ * @param {AudioBuffer} audioBuffer
177
+ * @returns {ArrayBuffer}
178
+ */
179
+ createWAVBuffer(audioBuffer) {
180
+ const numChannels = audioBuffer.numberOfChannels;
181
+ const length = audioBuffer.length * numChannels * 2;
182
+ const buffer = new ArrayBuffer(44 + length);
183
+ const view = new DataView(buffer);
184
+
185
+ // Write WAV header
186
+ this.writeWAVHeader(view, length, numChannels, audioBuffer.sampleRate);
187
+
188
+ // Write audio data
189
+ const channels = [];
190
+ for (let i = 0; i < numChannels; i++) {
191
+ channels.push(audioBuffer.getChannelData(i));
192
+ }
193
+
194
+ let offset = 44;
195
+ for (let i = 0; i < audioBuffer.length; i++) {
196
+ for (let channel = 0; channel < numChannels; channel++) {
197
+ const sample = Math.max(-1, Math.min(1, channels[channel][i]));
198
+ view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);
199
+ offset += 2;
200
+ }
201
+ }
202
+
203
+ return buffer;
204
+ }
205
+
206
+ /**
207
+ * Write WAV header to DataView
208
+ * @param {DataView} view
209
+ * @param {number} length
210
+ * @param {number} numChannels
211
+ * @param {number} sampleRate
212
+ */
213
+ writeWAVHeader(view, length, numChannels, sampleRate) {
214
+ // RIFF identifier
215
+ this.writeString(view, 0, 'RIFF');
216
+ // RIFF chunk length
217
+ view.setUint32(4, 36 + length, true);
218
+ // RIFF type
219
+ this.writeString(view, 8, 'WAVE');
220
+ // Format chunk identifier
221
+ this.writeString(view, 12, 'fmt ');
222
+ // Format chunk length
223
+ view.setUint32(16, 16, true);
224
+ // Sample format (raw)
225
+ view.setUint16(20, 1, true);
226
+ // Channel count
227
+ view.setUint16(22, numChannels, true);
228
+ // Sample rate
229
+ view.setUint32(24, sampleRate, true);
230
+ // Byte rate (sample rate * block align)
231
+ view.setUint32(28, sampleRate * numChannels * 2, true);
232
+ // Block align (channel count * bytes per sample)
233
+ view.setUint16(32, numChannels * 2, true);
234
+ // Bits per sample
235
+ view.setUint16(34, 16, true);
236
+ // Data chunk identifier
237
+ this.writeString(view, 36, 'data');
238
+ // Data chunk length
239
+ view.setUint32(40, length, true);
240
+ }
241
+
242
+ /**
243
+ * Write string to DataView
244
+ * @param {DataView} view
245
+ * @param {number} offset
246
+ * @param {string} string
247
+ */
248
+ writeString(view, offset, string) {
249
+ for (let i = 0; i < string.length; i++) {
250
+ view.setUint8(offset + i, string.charCodeAt(i));
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Draw audio visualization
256
+ */
257
+ drawVisualization() {
258
+ if (!this.state.isRecording || !this.visualizer.canvasContext) return;
259
+
260
+ requestAnimationFrame(() => this.drawVisualization());
261
+
262
+ // Get frequency data
263
+ this.state.analyser.getByteFrequencyData(this.visualizer.dataArray);
264
+
265
+ // Clear canvas
266
+ this.visualizer.canvasContext.fillStyle = 'rgb(10, 10, 10)';
267
+ this.visualizer.canvasContext.fillRect(0, 0, this.visualizer.width, this.visualizer.height);
268
+
269
+ // Draw frequency bars
270
+ const barWidth = (this.visualizer.width / this.visualizer.bufferLength) * 2.5;
271
+ let barHeight;
272
+ let x = 0;
273
+
274
+ for (let i = 0; i < this.visualizer.bufferLength; i++) {
275
+ barHeight = (this.visualizer.dataArray[i] / 255) * this.visualizer.height;
276
+
277
+ this.visualizer.canvasContext.fillStyle = `rgb(${barHeight + 100},50,50)`;
278
+ this.visualizer.canvasContext.fillRect(
279
+ x,
280
+ this.visualizer.height - barHeight,
281
+ barWidth,
282
+ barHeight
283
+ );
284
+
285
+ x += barWidth + 1;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Clean up audio resources
291
+ */
292
+ cleanup() {
293
+ if (this.state.stream) {
294
+ this.state.stream.getTracks().forEach(track => track.stop());
295
+ }
296
+ this.state.audioChunks = [];
297
+ this.state.isRecording = false;
298
+ }
299
+ }
300
+
301
+ // Export the AudioManager class
302
+ export default AudioManager;
static/js/background.js ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Import Three.js library (already included in index.html)
2
+ // const THREE = window.THREE;
3
+
4
+ class Background {
5
+ constructor() {
6
+ // Initialize core Three.js components
7
+ this.scene = new THREE.Scene();
8
+ this.camera = new THREE.PerspectiveCamera(
9
+ 75, // Field of view
10
+ window.innerWidth / window.innerHeight, // Aspect ratio
11
+ 0.1, // Near plane
12
+ 1000 // Far plane
13
+ );
14
+
15
+ // Setup renderer with transparency and anti-aliasing
16
+ this.renderer = new THREE.WebGLRenderer({
17
+ canvas: document.querySelector('#webgl-background'),
18
+ alpha: true,
19
+ antialias: true
20
+ });
21
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
22
+ this.renderer.setClearColor(0x0a0a0a, 1); // Dark background color
23
+
24
+ // Initialize particle system properties
25
+ this.particles = [];
26
+ this.particleCount = 100;
27
+ this.particleGeometry = new THREE.BufferGeometry();
28
+ this.particleMaterial = new THREE.PointsMaterial({
29
+ size: 2,
30
+ color: 0xffffff,
31
+ transparent: true,
32
+ opacity: 0.5,
33
+ blending: THREE.AdditiveBlending
34
+ });
35
+
36
+ // Set up camera position
37
+ this.camera.position.z = 100;
38
+
39
+ // Initialize the background
40
+ this.init();
41
+
42
+ // Bind event listeners
43
+ this.bindEvents();
44
+ }
45
+
46
+ init() {
47
+ // Create particle positions array
48
+ const positions = new Float32Array(this.particleCount * 3);
49
+
50
+ // Generate random positions for particles
51
+ for (let i = 0; i < this.particleCount; i++) {
52
+ const i3 = i * 3; // Index for x, y, z coordinates
53
+ positions[i3] = (Math.random() - 0.5) * window.innerWidth; // X coordinate
54
+ positions[i3 + 1] = (Math.random() - 0.5) * window.innerHeight; // Y coordinate
55
+ positions[i3 + 2] = (Math.random() - 0.5) * 500; // Z coordinate
56
+
57
+ // Store particle data for animation
58
+ this.particles.push({
59
+ velocity: (Math.random() - 0.5) * 0.2,
60
+ baseX: positions[i3],
61
+ baseY: positions[i3 + 1]
62
+ });
63
+ }
64
+
65
+ // Set particle positions in geometry
66
+ this.particleGeometry.setAttribute(
67
+ 'position',
68
+ new THREE.BufferAttribute(positions, 3)
69
+ );
70
+
71
+ // Create particle system and add to scene
72
+ this.particleSystem = new THREE.Points(
73
+ this.particleGeometry,
74
+ this.particleMaterial
75
+ );
76
+ this.scene.add(this.particleSystem);
77
+
78
+ // Start animation loop
79
+ this.animate();
80
+ }
81
+
82
+ // Handle window resize events
83
+ bindEvents() {
84
+ window.addEventListener('resize', () => {
85
+ // Update camera aspect ratio and projection matrix
86
+ this.camera.aspect = window.innerWidth / window.innerHeight;
87
+ this.camera.updateProjectionMatrix();
88
+
89
+ // Update renderer size
90
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
91
+ });
92
+ }
93
+
94
+ // Animation loop for particle movement
95
+ animate() {
96
+ requestAnimationFrame(() => this.animate());
97
+
98
+ const positions = this.particleGeometry.attributes.position.array;
99
+ const time = Date.now() * 0.0005;
100
+
101
+ // Update particle positions with wave-like motion
102
+ for (let i = 0; i < this.particleCount; i++) {
103
+ const i3 = i * 3;
104
+ const particle = this.particles[i];
105
+
106
+ // Create wave-like motion using sine waves
107
+ positions[i3] = particle.baseX + Math.sin(time + i) * 2;
108
+ positions[i3 + 1] = particle.baseY + Math.cos(time + i) * 2;
109
+
110
+ // Add slight drift to z-position
111
+ positions[i3 + 2] += particle.velocity;
112
+
113
+ // Reset particles that drift too far
114
+ if (Math.abs(positions[i3 + 2]) > 250) {
115
+ positions[i3 + 2] = -250;
116
+ }
117
+ }
118
+
119
+ // Mark particle positions for update
120
+ this.particleGeometry.attributes.position.needsUpdate = true;
121
+
122
+ // Add subtle camera movement
123
+ this.camera.position.x = Math.sin(time) * 10;
124
+ this.camera.position.y = Math.cos(time) * 10;
125
+ this.camera.lookAt(this.scene.position);
126
+
127
+ // Render the scene
128
+ this.renderer.render(this.scene, this.camera);
129
+ }
130
+
131
+ // Method to add dramatic effects during game events
132
+ addDramaticEffect(type) {
133
+ switch(type) {
134
+ case 'impostor_reveal':
135
+ // Create a dramatic red flash effect
136
+ this.particleMaterial.color.setHex(0xff0000);
137
+ setTimeout(() => {
138
+ this.particleMaterial.color.setHex(0xffffff);
139
+ }, 1000);
140
+ break;
141
+
142
+ case 'round_start':
143
+ // Increase particle movement temporarily
144
+ const originalVelocities = this.particles.map(p => p.velocity);
145
+ this.particles.forEach(p => p.velocity *= 2);
146
+ setTimeout(() => {
147
+ this.particles.forEach((p, i) => p.velocity = originalVelocities[i]);
148
+ }, 2000);
149
+ break;
150
+
151
+ case 'voting':
152
+ // Create a pulsing effect
153
+ const pulseAnimation = () => {
154
+ this.particleMaterial.size = 2 + Math.sin(Date.now() * 0.005) * 1;
155
+ };
156
+ const pulseInterval = setInterval(pulseAnimation, 16);
157
+ setTimeout(() => {
158
+ clearInterval(pulseInterval);
159
+ this.particleMaterial.size = 2;
160
+ }, 3000);
161
+ break;
162
+ }
163
+ }
164
+ }
165
+
166
+ // Initialize the background when the DOM is loaded
167
+ document.addEventListener('DOMContentLoaded', () => {
168
+ const background = new Background();
169
+
170
+ // Expose background instance for game events
171
+ window.gameBackground = background;
172
+ });
173
+
174
+ // Export the Background class for potential module usage
175
+ export default Background;
static/js/game.js ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Initialize Socket.IO connection with automatic reconnection
2
+ const socket = io({
3
+ reconnection: true,
4
+ reconnectionAttempts: 5,
5
+ reconnectionDelay: 1000
6
+ });
7
+
8
+ class Game {
9
+ constructor() {
10
+ // Core game state
11
+ this.state = {
12
+ gameId: null,
13
+ currentPhase: 'landing',
14
+ playerId: null,
15
+ players: [],
16
+ currentQuestion: null,
17
+ isRecording: false,
18
+ recordingTime: 30, // seconds
19
+ listeningTime: 60, // seconds
20
+ votingTime: 60, // seconds
21
+ recordings: new Map(),
22
+ votes: new Map(),
23
+ impostor: null
24
+ };
25
+
26
+ // Audio recording configuration
27
+ this.audioConfig = {
28
+ mediaRecorder: null,
29
+ audioChunks: [],
30
+ stream: null
31
+ };
32
+
33
+ // Initialize the game
34
+ this.initializeEventListeners();
35
+ this.bindUIElements();
36
+ }
37
+
38
+ // Initialize all event listeners for the game
39
+ initializeEventListeners() {
40
+ // Socket event listeners
41
+ socket.on('connect', () => this.handleConnection());
42
+ socket.on('game_created', (data) => this.handleGameCreated(data));
43
+ socket.on('player_joined', (data) => this.handlePlayerJoined(data));
44
+ socket.on('round_start', (data) => this.handleRoundStart(data));
45
+ socket.on('round_result', (data) => this.handleRoundResult(data));
46
+
47
+ // UI event listeners
48
+ document.getElementById('start-button')?.addEventListener('click', () => this.startGame());
49
+ document.getElementById('add-player-button')?.addEventListener('click', () => this.addPlayer());
50
+ }
51
+
52
+ // Bind UI elements and initialize their event handlers
53
+ bindUIElements() {
54
+ // Bind all necessary UI elements
55
+ const uiElements = {
56
+ gameContainer: document.getElementById('game-container'),
57
+ landingPage: document.getElementById('landing-page'),
58
+ setupPage: document.getElementById('setup-page'),
59
+ gamePage: document.getElementById('game-page'),
60
+ questionDisplay: document.getElementById('question-display'),
61
+ timerDisplay: document.getElementById('timer-display'),
62
+ recordButton: document.getElementById('record-button'),
63
+ playerList: document.getElementById('player-list'),
64
+ voteButtons: document.querySelectorAll('.vote-button')
65
+ };
66
+
67
+ // Store UI elements in the class
68
+ this.ui = uiElements;
69
+ }
70
+
71
+ // Handle initial connection to the server
72
+ handleConnection() {
73
+ console.log('Connected to server');
74
+ this.showPage('landing');
75
+ }
76
+
77
+ // Create a new game session
78
+ async createGame() {
79
+ try {
80
+ socket.emit('create_game');
81
+ } catch (error) {
82
+ this.handleError('Failed to create game');
83
+ }
84
+ }
85
+
86
+ // Handle successful game creation
87
+ handleGameCreated(data) {
88
+ this.state.gameId = data.gameId;
89
+ this.showPage('setup');
90
+ }
91
+
92
+ // Add a new player to the game
93
+ async addPlayer() {
94
+ if (this.state.players.length >= 5) {
95
+ this.handleError('Maximum player limit reached');
96
+ return;
97
+ }
98
+
99
+ const playerName = prompt('Enter player name:');
100
+ if (!playerName) return;
101
+
102
+ socket.emit('join_game', {
103
+ gameId: this.state.gameId,
104
+ playerName: playerName
105
+ });
106
+ }
107
+
108
+ // Handle new player joining the game
109
+ handlePlayerJoined(data) {
110
+ this.state.players.push({
111
+ id: data.playerId,
112
+ name: data.playerName
113
+ });
114
+ this.updatePlayerList();
115
+ }
116
+
117
+ // Update the player list in the UI
118
+ updatePlayerList() {
119
+ if (!this.ui.playerList) return;
120
+
121
+ this.ui.playerList.innerHTML = '';
122
+ this.state.players.forEach(player => {
123
+ const playerElement = document.createElement('div');
124
+ playerElement.className = 'player-avatar';
125
+ playerElement.textContent = player.id;
126
+ this.ui.playerList.appendChild(playerElement);
127
+ });
128
+ }
129
+
130
+ // Start the game
131
+ async startGame() {
132
+ if (this.state.players.length < 3) {
133
+ this.handleError('Need at least 3 players to start');
134
+ return;
135
+ }
136
+
137
+ try {
138
+ const response = await fetch('/api/start_game', {
139
+ method: 'POST',
140
+ headers: {
141
+ 'Content-Type': 'application/json'
142
+ },
143
+ body: JSON.stringify({
144
+ gameId: this.state.gameId
145
+ })
146
+ });
147
+
148
+ const data = await response.json();
149
+ if (data.status === 'success') {
150
+ this.handleRoundStart(data);
151
+ }
152
+ } catch (error) {
153
+ this.handleError('Failed to start game');
154
+ }
155
+ }
156
+
157
+ // Handle the start of a new round
158
+ handleRoundStart(data) {
159
+ this.state.currentQuestion = data.question;
160
+ this.state.currentPhase = 'recording';
161
+ this.showPage('game');
162
+ this.updateQuestionDisplay();
163
+ this.startTimer(this.state.recordingTime, () => this.endRecordingPhase());
164
+ }
165
+
166
+ // Start audio recording
167
+ async startRecording() {
168
+ try {
169
+ this.audioConfig.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
170
+ this.audioConfig.mediaRecorder = new MediaRecorder(this.audioConfig.stream);
171
+ this.audioConfig.audioChunks = [];
172
+
173
+ this.audioConfig.mediaRecorder.ondataavailable = (event) => {
174
+ this.audioConfig.audioChunks.push(event.data);
175
+ };
176
+
177
+ this.audioConfig.mediaRecorder.onstop = () => {
178
+ this.submitRecording();
179
+ };
180
+
181
+ this.audioConfig.mediaRecorder.start();
182
+ this.state.isRecording = true;
183
+ this.updateRecordButton();
184
+ } catch (error) {
185
+ this.handleError('Failed to start recording');
186
+ }
187
+ }
188
+
189
+ // Stop audio recording
190
+ stopRecording() {
191
+ if (this.audioConfig.mediaRecorder && this.state.isRecording) {
192
+ this.audioConfig.mediaRecorder.stop();
193
+ this.audioConfig.stream.getTracks().forEach(track => track.stop());
194
+ this.state.isRecording = false;
195
+ this.updateRecordButton();
196
+ }
197
+ }
198
+
199
+ // Submit recording to server
200
+ async submitRecording() {
201
+ const audioBlob = new Blob(this.audioConfig.audioChunks, { type: 'audio/wav' });
202
+ const formData = new FormData();
203
+ formData.append('audio', audioBlob);
204
+ formData.append('gameId', this.state.gameId);
205
+ formData.append('playerId', this.state.playerId);
206
+
207
+ try {
208
+ const response = await fetch('/api/submit_recording', {
209
+ method: 'POST',
210
+ body: formData
211
+ });
212
+
213
+ const data = await response.json();
214
+ if (data.status === 'success') {
215
+ this.state.recordings.set(this.state.playerId, data.recordingUrl);
216
+ }
217
+ } catch (error) {
218
+ this.handleError('Failed to submit recording');
219
+ }
220
+ }
221
+
222
+ // Start the timer for a game phase
223
+ startTimer(duration, callback) {
224
+ let timeLeft = duration;
225
+ this.updateTimerDisplay(timeLeft);
226
+
227
+ this.timer = setInterval(() => {
228
+ timeLeft--;
229
+ this.updateTimerDisplay(timeLeft);
230
+
231
+ if (timeLeft <= 0) {
232
+ clearInterval(this.timer);
233
+ if (callback) callback();
234
+ }
235
+ }, 1000);
236
+ }
237
+
238
+ // Update the timer display
239
+ updateTimerDisplay(timeLeft) {
240
+ if (this.ui.timerDisplay) {
241
+ const minutes = Math.floor(timeLeft / 60);
242
+ const seconds = timeLeft % 60;
243
+ this.ui.timerDisplay.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
244
+ }
245
+ }
246
+
247
+ // End the recording phase and move to listening phase
248
+ endRecordingPhase() {
249
+ if (this.state.isRecording) {
250
+ this.stopRecording();
251
+ }
252
+ this.state.currentPhase = 'listening';
253
+ this.startListeningPhase();
254
+ }
255
+
256
+ // Start the listening phase
257
+ startListeningPhase() {
258
+ this.showPage('listening');
259
+ this.loadRecordings();
260
+ this.startTimer(this.state.listeningTime, () => this.startVotingPhase());
261
+ }
262
+
263
+ // Load all player recordings
264
+ async loadRecordings() {
265
+ // Implementation for loading and playing recordings
266
+ // This would integrate with the audio playback UI
267
+ }
268
+
269
+ // Start the voting phase
270
+ startVotingPhase() {
271
+ this.state.currentPhase = 'voting';
272
+ this.showPage('voting');
273
+ this.startTimer(this.state.votingTime, () => this.endVotingPhase());
274
+ }
275
+
276
+ // Submit a vote
277
+ submitVote(votedPlayerId) {
278
+ socket.emit('submit_vote', {
279
+ gameId: this.state.gameId,
280
+ voterId: this.state.playerId,
281
+ votedPlayerId: votedPlayerId
282
+ });
283
+ }
284
+
285
+ // Handle the round results
286
+ handleRoundResult(data) {
287
+ this.showResults(data);
288
+ // Trigger dramatic background effect
289
+ window.gameBackground?.addDramaticEffect('impostor_reveal');
290
+ }
291
+
292
+ // Show the results page
293
+ showResults(data) {
294
+ this.showPage('results');
295
+ // Implementation for displaying round results
296
+ }
297
+
298
+ // Switch between game pages
299
+ showPage(pageName) {
300
+ const pages = document.querySelectorAll('.game-page');
301
+ pages.forEach(page => {
302
+ page.classList.remove('active');
303
+ if (page.id === `${pageName}-page`) {
304
+ page.classList.add('active');
305
+ }
306
+ });
307
+ }
308
+
309
+ // Handle errors
310
+ handleError(message) {
311
+ const errorElement = document.createElement('div');
312
+ errorElement.className = 'error-message';
313
+ errorElement.textContent = message;
314
+ this.ui.gameContainer.appendChild(errorElement);
315
+ setTimeout(() => errorElement.remove(), 3000);
316
+ }
317
+ }
318
+
319
+ // Initialize the game when the DOM is loaded
320
+ document.addEventListener('DOMContentLoaded', () => {
321
+ const game = new Game();
322
+ // Expose game instance for debugging
323
+ window.game = game;
324
+ });
325
+
326
+ export default Game;
static/js/ui.js ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class GameUI {
2
+ constructor() {
3
+ // Initialize UI state and configurations
4
+ this.state = {
5
+ currentPage: 'landing',
6
+ isAnimating: false,
7
+ darkMode: true,
8
+ animationDuration: 300
9
+ };
10
+
11
+ // Store references to frequently accessed DOM elements
12
+ this.elements = {
13
+ pages: {},
14
+ buttons: {},
15
+ containers: {},
16
+ overlays: {}
17
+ };
18
+
19
+ // Initialize the UI
20
+ this.initialize();
21
+ }
22
+
23
+ async initialize() {
24
+ // Wait for DOM to be fully loaded
25
+ if (document.readyState === 'loading') {
26
+ document.addEventListener('DOMContentLoaded', () => this.setupUI());
27
+ } else {
28
+ this.setupUI();
29
+ }
30
+ }
31
+
32
+ setupUI() {
33
+ // Cache all important DOM elements
34
+ this.cacheElements();
35
+ // Set up event listeners
36
+ this.setupEventListeners();
37
+ // Initialize the landing page
38
+ this.showPage('landing');
39
+ }
40
+
41
+ cacheElements() {
42
+ // Cache all page elements
43
+ ['landing', 'setup', 'game', 'recording', 'listening', 'voting', 'results'].forEach(pageId => {
44
+ this.elements.pages[pageId] = document.getElementById(`${pageId}-page`);
45
+ });
46
+
47
+ // Cache all button elements
48
+ this.elements.buttons = {
49
+ play: document.getElementById('play-button'),
50
+ addPlayer: document.getElementById('add-player-button'),
51
+ start: document.getElementById('start-button'),
52
+ record: document.getElementById('record-button'),
53
+ settings: document.getElementById('settings-button')
54
+ };
55
+
56
+ // Cache container elements
57
+ this.elements.containers = {
58
+ playerList: document.getElementById('player-list'),
59
+ questionDisplay: document.getElementById('question-display'),
60
+ timerDisplay: document.getElementById('timer-display'),
61
+ recordingVisualizer: document.getElementById('recording-visualizer'),
62
+ votingOptions: document.getElementById('voting-options')
63
+ };
64
+
65
+ // Cache overlay elements
66
+ this.elements.overlays = {
67
+ loading: document.getElementById('loading-overlay'),
68
+ error: document.getElementById('error-overlay'),
69
+ settings: document.getElementById('settings-overlay')
70
+ };
71
+ }
72
+
73
+ setupEventListeners() {
74
+ // Set up button click handlers
75
+ Object.entries(this.elements.buttons).forEach(([key, button]) => {
76
+ if (button) {
77
+ button.addEventListener('click', () => this.handleButtonClick(key));
78
+ }
79
+ });
80
+
81
+ // Set up keyboard shortcuts
82
+ document.addEventListener('keydown', (e) => this.handleKeyPress(e));
83
+
84
+ // Set up settings toggle
85
+ if (this.elements.buttons.settings) {
86
+ this.elements.buttons.settings.addEventListener('click', () => this.toggleSettings());
87
+ }
88
+ }
89
+
90
+ handleButtonClick(buttonType) {
91
+ switch (buttonType) {
92
+ case 'play':
93
+ this.transitionToPage('setup');
94
+ break;
95
+ case 'addPlayer':
96
+ window.game.addPlayer();
97
+ break;
98
+ case 'start':
99
+ window.game.startGame();
100
+ break;
101
+ case 'record':
102
+ this.toggleRecording();
103
+ break;
104
+ default:
105
+ console.warn(`Unhandled button type: ${buttonType}`);
106
+ }
107
+ }
108
+
109
+ async transitionToPage(pageName) {
110
+ if (this.state.isAnimating || this.state.currentPage === pageName) return;
111
+
112
+ this.state.isAnimating = true;
113
+
114
+ // Fade out current page
115
+ const currentPage = this.elements.pages[this.state.currentPage];
116
+ if (currentPage) {
117
+ await this.animateElement(currentPage, 'fadeOut');
118
+ currentPage.classList.remove('active');
119
+ }
120
+
121
+ // Update state and fade in new page
122
+ this.state.currentPage = pageName;
123
+ const newPage = this.elements.pages[pageName];
124
+ if (newPage) {
125
+ newPage.classList.add('active');
126
+ await this.animateElement(newPage, 'fadeIn');
127
+ }
128
+
129
+ this.state.isAnimating = false;
130
+ }
131
+
132
+ async animateElement(element, animation) {
133
+ return new Promise(resolve => {
134
+ element.classList.add(animation);
135
+ setTimeout(() => {
136
+ element.classList.remove(animation);
137
+ resolve();
138
+ }, this.state.animationDuration);
139
+ });
140
+ }
141
+
142
+ updatePlayerList(players) {
143
+ if (!this.elements.containers.playerList) return;
144
+
145
+ const playerList = this.elements.containers.playerList;
146
+ playerList.innerHTML = '';
147
+
148
+ players.forEach(player => {
149
+ const playerElement = document.createElement('div');
150
+ playerElement.className = 'player-avatar';
151
+
152
+ // Create avatar circle
153
+ const avatarCircle = document.createElement('div');
154
+ avatarCircle.className = 'avatar-circle';
155
+ avatarCircle.textContent = player.id;
156
+
157
+ // Create player name
158
+ const playerName = document.createElement('div');
159
+ playerName.className = 'player-name';
160
+ playerName.textContent = player.name;
161
+
162
+ playerElement.appendChild(avatarCircle);
163
+ playerElement.appendChild(playerName);
164
+ playerList.appendChild(playerElement);
165
+ });
166
+ }
167
+
168
+ updateTimer(timeLeft) {
169
+ if (!this.elements.containers.timerDisplay) return;
170
+
171
+ const minutes = Math.floor(timeLeft / 60);
172
+ const seconds = timeLeft % 60;
173
+ this.elements.containers.timerDisplay.textContent =
174
+ `${minutes}:${seconds.toString().padStart(2, '0')}`;
175
+
176
+ // Add warning class when time is running low
177
+ if (timeLeft <= 10) {
178
+ this.elements.containers.timerDisplay.classList.add('warning');
179
+ }
180
+ }
181
+
182
+ showQuestion(question) {
183
+ if (!this.elements.containers.questionDisplay) return;
184
+
185
+ const questionElement = this.elements.containers.questionDisplay;
186
+ questionElement.textContent = question;
187
+
188
+ // Animate question appearance
189
+ this.animateElement(questionElement, 'slideIn');
190
+ }
191
+
192
+ toggleRecording(isRecording) {
193
+ if (!this.elements.buttons.record) return;
194
+
195
+ const recordButton = this.elements.buttons.record;
196
+ recordButton.classList.toggle('recording', isRecording);
197
+ recordButton.textContent = isRecording ? 'Stop Recording' : 'Start Recording';
198
+ }
199
+
200
+ showLoadingOverlay(show, message = 'Loading...') {
201
+ if (!this.elements.overlays.loading) return;
202
+
203
+ const overlay = this.elements.overlays.loading;
204
+ if (show) {
205
+ overlay.querySelector('.loading-message').textContent = message;
206
+ overlay.classList.add('active');
207
+ } else {
208
+ overlay.classList.remove('active');
209
+ }
210
+ }
211
+
212
+ showError(message, duration = 3000) {
213
+ if (!this.elements.overlays.error) return;
214
+
215
+ const errorOverlay = this.elements.overlays.error;
216
+ const errorMessage = errorOverlay.querySelector('.error-message');
217
+
218
+ errorMessage.textContent = message;
219
+ errorOverlay.classList.add('active');
220
+
221
+ setTimeout(() => {
222
+ errorOverlay.classList.remove('active');
223
+ }, duration);
224
+ }
225
+
226
+ updateVotingOptions(players, currentPlayer) {
227
+ if (!this.elements.containers.votingOptions) return;
228
+
229
+ const votingContainer = this.elements.containers.votingOptions;
230
+ votingContainer.innerHTML = '';
231
+
232
+ players.forEach(player => {
233
+ if (player.id !== currentPlayer) {
234
+ const voteButton = document.createElement('button');
235
+ voteButton.className = 'vote-button';
236
+ voteButton.dataset.playerId = player.id;
237
+
238
+ const playerCircle = document.createElement('div');
239
+ playerCircle.className = 'player-circle';
240
+ playerCircle.textContent = player.id;
241
+
242
+ voteButton.appendChild(playerCircle);
243
+ votingContainer.appendChild(voteButton);
244
+
245
+ voteButton.addEventListener('click', () => {
246
+ window.game.submitVote(player.id);
247
+ });
248
+ }
249
+ });
250
+ }
251
+
252
+ showResults(results) {
253
+ const resultsPage = this.elements.pages.results;
254
+ if (!resultsPage) return;
255
+
256
+ // Clear previous results
257
+ resultsPage.innerHTML = '';
258
+
259
+ // Create results content
260
+ const content = document.createElement('div');
261
+ content.className = 'results-content';
262
+
263
+ // Add impostor reveal
264
+ const impostorReveal = document.createElement('div');
265
+ impostorReveal.className = 'impostor-reveal';
266
+ impostorReveal.textContent = `The impostor was Player ${results.impostor}!`;
267
+
268
+ // Add voting results
269
+ const votingResults = document.createElement('div');
270
+ votingResults.className = 'voting-results';
271
+ Object.entries(results.votes).forEach(([player, vote]) => {
272
+ const voteEntry = document.createElement('div');
273
+ voteEntry.className = 'vote-entry';
274
+ voteEntry.textContent = `Player ${player} voted for Player ${vote}`;
275
+ votingResults.appendChild(voteEntry);
276
+ });
277
+
278
+ content.appendChild(impostorReveal);
279
+ content.appendChild(votingResults);
280
+ resultsPage.appendChild(content);
281
+
282
+ this.transitionToPage('results');
283
+ }
284
+
285
+ handleKeyPress(event) {
286
+ // Add keyboard shortcuts
287
+ switch (event.key) {
288
+ case 'Escape':
289
+ this.closeAllOverlays();
290
+ break;
291
+ case 'r':
292
+ if (this.state.currentPage === 'recording') {
293
+ this.toggleRecording();
294
+ }
295
+ break;
296
+ }
297
+ }
298
+
299
+ closeAllOverlays() {
300
+ Object.values(this.elements.overlays).forEach(overlay => {
301
+ if (overlay) {
302
+ overlay.classList.remove('active');
303
+ }
304
+ });
305
+ }
306
+
307
+ // Method to clean up UI resources
308
+ cleanup() {
309
+ // Remove event listeners
310
+ Object.values(this.elements.buttons).forEach(button => {
311
+ if (button) {
312
+ button.replaceWith(button.cloneNode(true));
313
+ }
314
+ });
315
+
316
+ // Clear all intervals and timeouts
317
+ this.closeAllOverlays();
318
+ }
319
+ }
320
+
321
+ // Export the GameUI class
322
+ export default GameUI;
templates/index.html ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="NOT ME - A voice deduction game powered by AI">
7
+
8
+ <title>NOT ME - Voice Deduction Game</title>
9
+
10
+ <!-- Import the Pixelify Sans font for our game's unique style -->
11
+ <link href="https://fonts.googleapis.com/css2?family=Pixelify+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- Main stylesheet -->
14
+ <link rel="stylesheet" href="static/css/styles.css">
15
+
16
+ <!-- Socket.IO client for real-time communication -->
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
18
+
19
+ <!-- Three.js for background animations -->
20
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
21
+ </head>
22
+ <body>
23
+ <!-- WebGL background container for particle effects -->
24
+ <canvas id="webgl-background"></canvas>
25
+
26
+ <!-- Settings button that appears after game starts -->
27
+ <button id="settings-button" class="settings-button hidden">
28
+ <span class="settings-icon">⚙️</span>
29
+ </button>
30
+
31
+ <!-- Main game container -->
32
+ <div id="game-container">
33
+ <!-- Landing Page -->
34
+ <div id="landing-page" class="game-page active">
35
+ <h1 class="game-title">
36
+ NOT <span class="title-emphasis">ME</span>
37
+ </h1>
38
+ <button id="play-button" class="button primary-button">PLAY</button>
39
+ </div>
40
+
41
+ <!-- Player Setup Page -->
42
+ <div id="setup-page" class="game-page">
43
+ <h2 class="page-title">Player Setup</h2>
44
+ <div id="player-list" class="player-grid"></div>
45
+ <div class="setup-controls">
46
+ <button id="add-player-button" class="button secondary-button">Add Player</button>
47
+ <button id="start-button" class="button primary-button" disabled>Start Game</button>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Game Page (Recording Phase) -->
52
+ <div id="recording-page" class="game-page">
53
+ <div id="timer-display" class="timer"></div>
54
+ <div id="question-display" class="question-box"></div>
55
+
56
+ <!-- Recording interface -->
57
+ <div class="recording-controls">
58
+ <canvas id="recording-visualizer" class="visualizer"></canvas>
59
+ <button id="record-button" class="button record-button">Start Recording</button>
60
+ <div class="recording-time"></div>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Listening Phase -->
65
+ <div id="listening-page" class="game-page">
66
+ <div id="timer-display" class="timer"></div>
67
+ <h2 class="page-title">Listen to Responses</h2>
68
+ <div id="recordings-list" class="recordings-grid">
69
+ <!-- Recording playback buttons will be dynamically added here -->
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Voting Phase -->
74
+ <div id="voting-page" class="game-page">
75
+ <div id="timer-display" class="timer"></div>
76
+ <h2 class="page-title">Vote for the Impostor</h2>
77
+ <div id="voting-options" class="voting-grid">
78
+ <!-- Voting buttons will be dynamically added here -->
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Results Page -->
83
+ <div id="results-page" class="game-page">
84
+ <h2 class="page-title">Round Results</h2>
85
+ <div id="results-content" class="results-container">
86
+ <!-- Results will be dynamically added here -->
87
+ </div>
88
+ <button id="next-round-button" class="button primary-button">Next Round</button>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Overlay containers -->
93
+ <div id="loading-overlay" class="overlay">
94
+ <div class="loading-spinner"></div>
95
+ <div class="loading-message">Loading...</div>
96
+ </div>
97
+
98
+ <div id="error-overlay" class="overlay">
99
+ <div class="error-message"></div>
100
+ </div>
101
+
102
+ <div id="settings-overlay" class="overlay">
103
+ <div class="settings-panel">
104
+ <h3>Game Settings</h3>
105
+ <div class="settings-options">
106
+ <div class="setting-item">
107
+ <label for="dark-mode">Dark Mode</label>
108
+ <input type="checkbox" id="dark-mode" checked>
109
+ </div>
110
+ <div class="setting-item">
111
+ <label for="sound-effects">Sound Effects</label>
112
+ <input type="checkbox" id="sound-effects" checked>
113
+ </div>
114
+ </div>
115
+ <button id="exit-game" class="button secondary-button">Exit Game</button>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Game scripts -->
120
+ <script type="module" src="static/js/background.js"></script>
121
+ <script type="module" src="static/js/audio.js"></script>
122
+ <script type="module" src="static/js/ui.js"></script>
123
+ <script type="module" src="static/js/game.js"></script>
124
+
125
+ <!-- Audio context initialization script -->
126
+ <script>
127
+ // Initialize audio context on user interaction
128
+ document.addEventListener('click', function initAudioContext() {
129
+ if (!window.audioContext) {
130
+ window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
131
+ }
132
+ document.removeEventListener('click', initAudioContext);
133
+ }, { once: true });
134
+ </script>
135
+ </body>
136
+ </html>