|
from flask import Flask, render_template, request, jsonify |
|
from flask_socketio import SocketIO, emit, join_room, leave_room |
|
import os |
|
import requests |
|
import json |
|
import uuid |
|
from datetime import datetime |
|
from dotenv import load_dotenv |
|
import logging |
|
from werkzeug.utils import secure_filename |
|
import random |
|
import asyncio |
|
|
|
|
|
app = Flask(__name__) |
|
app.config['SECRET_KEY'] = os.urandom(24) |
|
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 |
|
|
|
|
|
socketio = SocketIO(app, cors_allowed_origins="*", logger=True, engineio_logger=True, async_mode='eventlet') |
|
|
|
|
|
load_dotenv() |
|
MISTRAL_API_KEY = os.getenv('MISTRAL_API_KEY') |
|
ELEVENLABS_API_KEY = os.getenv('ELEVENLABS_API_KEY') |
|
|
|
|
|
logging.basicConfig( |
|
level=logging.INFO, |
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' |
|
) |
|
logger = logging.getLogger(__name__) |
|
|
|
class GameState: |
|
"""Manages the state of all active game sessions.""" |
|
|
|
def __init__(self): |
|
self.games = {} |
|
self.cleanup_interval = 3600 |
|
|
|
def create_game(self): |
|
"""Creates a new game session with proper initialization.""" |
|
try: |
|
game_id = str(uuid.uuid4()) |
|
self.games[game_id] = { |
|
'players': [], |
|
'current_phase': 'setup', |
|
'recordings': {}, |
|
'impostor': None, |
|
'votes': {}, |
|
'question': None, |
|
'impostor_answer': None, |
|
'modified_recording': None, |
|
'round_number': 1, |
|
'start_time': datetime.now().isoformat(), |
|
'completed_rounds': [], |
|
'score': {'impostor_wins': 0, 'player_wins': 0}, |
|
'room': game_id |
|
} |
|
logger.info(f"Successfully created game with ID: {game_id}") |
|
return game_id |
|
except Exception as e: |
|
logger.error(f"Error creating game: {str(e)}") |
|
raise |
|
|
|
def get_game(self, game_id): |
|
"""Safely retrieves a game by ID.""" |
|
game = self.games.get(game_id) |
|
if not game: |
|
logger.error(f"Game not found: {game_id}") |
|
raise ValueError("Game not found") |
|
return game |
|
|
|
def cleanup_inactive_games(self): |
|
"""Removes inactive game sessions.""" |
|
current_time = datetime.now() |
|
for game_id, game in list(self.games.items()): |
|
start_time = datetime.fromisoformat(game['start_time']) |
|
if (current_time - start_time).total_seconds() > 7200: |
|
del self.games[game_id] |
|
logger.info(f"Cleaned up inactive game: {game_id}") |
|
|
|
|
|
game_state = GameState() |
|
|
|
@app.route('/') |
|
def home(): |
|
"""Serves the main game page.""" |
|
return render_template('index.html') |
|
|
|
@socketio.on('connect') |
|
def handle_connect(): |
|
"""Handles client connection.""" |
|
logger.info(f"Client connected: {request.sid}") |
|
emit('connection_success', {'status': 'connected'}) |
|
|
|
@socketio.on('disconnect') |
|
def handle_disconnect(): |
|
"""Handles client disconnection.""" |
|
logger.info(f"Client disconnected: {request.sid}") |
|
|
|
@socketio.on('create_game') |
|
def handle_create_game(): |
|
"""Handles game creation request.""" |
|
try: |
|
game_id = game_state.create_game() |
|
join_room(game_id) |
|
logger.info(f"Created and joined game room: {game_id}") |
|
emit('game_created', { |
|
'gameId': game_id, |
|
'status': 'success' |
|
}) |
|
except Exception as e: |
|
logger.error(f"Error in game creation: {str(e)}") |
|
emit('game_error', { |
|
'error': 'Failed to create game', |
|
'details': str(e) |
|
}) |
|
|
|
@socketio.on('join_game') |
|
def handle_join_game(data): |
|
"""Handles player joining a game.""" |
|
try: |
|
game_id = data.get('gameId') |
|
player_name = data.get('playerName') |
|
|
|
if not game_id or not player_name: |
|
raise ValueError("Missing game ID or player name") |
|
|
|
game = game_state.get_game(game_id) |
|
|
|
|
|
if len(game['players']) >= 5: |
|
raise ValueError("Game is full") |
|
|
|
|
|
player_id = len(game['players']) + 1 |
|
player = { |
|
'id': player_id, |
|
'name': player_name, |
|
'socket_id': request.sid |
|
} |
|
game['players'].append(player) |
|
|
|
|
|
join_room(game_id) |
|
logger.info(f"Player {player_name} (ID: {player_id}) joined game {game_id}") |
|
|
|
|
|
emit('player_joined', { |
|
'playerId': player_id, |
|
'playerName': player_name, |
|
'status': 'success' |
|
}, room=game_id) |
|
|
|
except Exception as e: |
|
error_msg = str(e) |
|
logger.error(f"Error in handle_join_game: {error_msg}") |
|
emit('game_error', {'error': error_msg}) |
|
|
|
@app.route('/api/start_game', methods=['POST']) |
|
async def start_game(): |
|
"""Initializes a new game round.""" |
|
try: |
|
data = request.get_json() |
|
game_id = data.get('gameId') |
|
|
|
if not game_id: |
|
raise ValueError("Missing game ID") |
|
|
|
game = game_state.get_game(game_id) |
|
|
|
|
|
if len(game['players']) < 3: |
|
raise ValueError("Need at least 3 players to start") |
|
|
|
|
|
question = await generate_question() |
|
game['question'] = question |
|
game['current_phase'] = 'recording' |
|
|
|
logger.info(f"Started game {game_id} with question: {question}") |
|
|
|
|
|
socketio.emit('round_started', { |
|
'question': question |
|
}, room=game_id) |
|
|
|
return jsonify({ |
|
'status': 'success', |
|
'question': question |
|
}) |
|
|
|
except Exception as e: |
|
error_msg = str(e) |
|
logger.error(f"Error starting game: {error_msg}") |
|
return jsonify({ |
|
'status': 'error', |
|
'error': error_msg |
|
}), 500 |
|
|
|
async def generate_question(): |
|
"""Generates an engaging question using Mistral AI.""" |
|
try: |
|
headers = { |
|
'Authorization': f'Bearer {MISTRAL_API_KEY}', |
|
'Content-Type': 'application/json' |
|
} |
|
|
|
payload = { |
|
'messages': [{ |
|
'role': 'user', |
|
'content': '''Generate an engaging personal question for a social game. |
|
The question should: |
|
1. Encourage creative and unique responses |
|
2. Be open-ended but not too philosophical |
|
3. Be answerable in 15-30 seconds |
|
4. Be appropriate for all ages |
|
5. Spark interesting conversation |
|
|
|
Generate only the question, without any additional text.''' |
|
}] |
|
} |
|
|
|
response = requests.post( |
|
'https://api.mistral.ai/v1/chat/completions', |
|
headers=headers, |
|
json=payload, |
|
timeout=10 |
|
) |
|
|
|
if response.status_code == 200: |
|
question = response.json()['choices'][0]['message']['content'].strip() |
|
logger.info(f"Generated question: {question}") |
|
return question |
|
|
|
logger.error(f"Mistral API error: {response.status_code}") |
|
return random.choice([ |
|
"What's your favorite childhood memory?", |
|
"What's the most interesting place you've ever visited?", |
|
"What's a skill you'd love to master and why?", |
|
"What's the best piece of advice you've ever received?" |
|
]) |
|
|
|
except Exception as e: |
|
logger.error(f"Error generating question: {str(e)}") |
|
return "What is your favorite memory from your childhood?" |
|
|
|
@app.route('/api/submit_recording', methods=['POST']) |
|
async def submit_recording(): |
|
"""Handles voice recording submissions.""" |
|
try: |
|
game_id = request.form.get('gameId') |
|
player_id = request.form.get('playerId') |
|
audio_file = request.files.get('audio') |
|
|
|
if not all([game_id, player_id, audio_file]): |
|
raise ValueError("Missing required data") |
|
|
|
game = game_state.get_game(game_id) |
|
|
|
|
|
filename = secure_filename(f"recording_{game_id}_{player_id}.wav") |
|
filepath = os.path.join('temp', filename) |
|
audio_file.save(filepath) |
|
|
|
game['recordings'][player_id] = filepath |
|
logger.info(f"Saved recording for player {player_id} in game {game_id}") |
|
|
|
|
|
socketio.emit('recording_submitted', { |
|
'playerId': player_id, |
|
'status': 'success' |
|
}, room=game_id) |
|
|
|
return jsonify({'status': 'success'}) |
|
|
|
except Exception as e: |
|
error_msg = str(e) |
|
logger.error(f"Error submitting recording: {error_msg}") |
|
return jsonify({ |
|
'status': 'error', |
|
'error': error_msg |
|
}), 500 |
|
|
|
@socketio.on('submit_vote') |
|
def handle_vote(data): |
|
"""Processes player votes and determines round outcome.""" |
|
try: |
|
game_id = data.get('gameId') |
|
voter_id = data.get('voterId') |
|
vote_for = data.get('voteFor') |
|
|
|
if not all([game_id, voter_id, vote_for]): |
|
raise ValueError("Missing vote data") |
|
|
|
game = game_state.get_game(game_id) |
|
game['votes'][voter_id] = vote_for |
|
|
|
|
|
if len(game['votes']) == len(game['players']): |
|
|
|
votes_count = {} |
|
for vote in game['votes'].values(): |
|
votes_count[vote] = votes_count.get(vote, 0) + 1 |
|
|
|
most_voted = max(votes_count.items(), key=lambda x: x[1])[0] |
|
|
|
|
|
if most_voted == game['impostor']: |
|
game['score']['player_wins'] += 1 |
|
result = 'players_win' |
|
else: |
|
game['score']['impostor_wins'] += 1 |
|
result = 'impostor_wins' |
|
|
|
|
|
game['completed_rounds'].append({ |
|
'round_number': game['round_number'], |
|
'impostor': game['impostor'], |
|
'votes': game['votes'].copy(), |
|
'most_voted': most_voted, |
|
'result': result |
|
}) |
|
|
|
|
|
emit('round_result', { |
|
'impostor': game['impostor'], |
|
'most_voted': most_voted, |
|
'votes': game['votes'], |
|
'score': game['score'], |
|
'result': result |
|
}, room=game_id) |
|
|
|
logger.info(f"Round completed for game {game_id}. Result: {result}") |
|
|
|
except Exception as e: |
|
error_msg = str(e) |
|
logger.error(f"Error processing vote: {error_msg}") |
|
emit('game_error', {'error': error_msg}) |
|
|
|
if __name__ == '__main__': |
|
|
|
os.makedirs('temp', exist_ok=True) |
|
|
|
|
|
logger.info("Starting server...") |
|
socketio.run(app, host='0.0.0.0', port=7860, debug=True) |