Introduction
This comprehensive guide presents the design and implementation of an innovative multi-player music quiz game that combines the power of Large Language Models (LLMs) with physical electronic circuits and modern music streaming APIs. The game creates an engaging experience where players compete to identify songs from various genres and time periods, with their responses captured through custom-built electronic interfaces.
The system architecture integrates several cutting-edge technologies: Hugging Face's transformer libraries for LLM integration, Spotify's Web API for music streaming, Arduino-based microcontrollers for physical player interfaces, and a sophisticated game engine that orchestrates the entire experience. Each player interacts with the game through a dedicated electronic circuit featuring push buttons and LED indicators, creating a tactile and immersive gaming experience.
The game operates in rounds where the LLM selects songs based on predefined criteria such as genre, time period, and optionally country of origin. Players compete to be the first to correctly identify the artist and title of the playing song. The scoring system rewards quick and accurate responses while penalizing incorrect answers, creating a dynamic competitive environment.
Music Quiz Game Rules
Game Overview
The Music Quiz Game is a competitive multi-player music identification game where players compete to correctly identify songs based on artist and title. Players use physical electronic circuits with buttons and LED indicators to participate in the game, which is orchestrated by an AI-powered game engine that selects music from specified genres and time periods.
Player Setup and Registration
Each player must have a dedicated electronic circuit consisting of a push button, red LED, and green LED connected to an Arduino microcontroller. At the beginning of each game session, players register by pressing their circuit button when prompted and providing their name to the game system. The game engine creates a permanent association between each player's name and their specific circuit for the duration of the game session.
Players start with a score of zero points. The game supports multiple players simultaneously, with each player operating independently through their individual circuit interface. Alternative keyboard input is available for environments where physical circuits are not practical, with on-screen simulation of the LED indicators and numbered key assignments for each player.
Game Configuration
Before gameplay begins, one designated user configures the game parameters including music genre selection from available options such as Classical Music, Jazz, Rock, Pop, Blues, Country, Techno, House, Hip Hop, Folk, Disco, Deutsche Schlager, and others. The user also specifies a time period such as 1910s, 1920s, 1970s-1990s, 1930s, 1940s, 1990s, 2000s, 2010s, 2020s, or other decades.
Optionally, the user may specify a country code such as DE, EN, US, or others to restrict song selection to music from that particular country. The game engine uses these parameters to search and select appropriate songs from the Spotify music database throughout the game session.
Round Structure and Gameplay
Each game consists of multiple rounds, with each round following a consistent structure. At the start of each round, the game engine selects a random song that matches the predetermined genre, time period, and optional country restrictions. No song may be played more than once during a single game session, ensuring variety and preventing repetition.
When a round begins, all players' red LEDs start blinking simultaneously to indicate that the round is active and players should prepare to respond. The selected song begins playing through the game's audio system, typically using Spotify's streaming service or preview URLs when full streaming is not available.
Player Response and Timing
Once the song begins playing, players listen and attempt to identify both the artist and song title. The first player to press their circuit button gains the right to provide an answer. When a player presses their button first, their green LED illuminates while all other players' red LEDs remain solidly lit, clearly indicating who has the opportunity to respond.
Players have a maximum of thirty seconds from the start of song playback to press their button, though this timeout duration is configurable in the game settings. If no player presses their button within the timeout period, the round ends automatically with penalties applied to all players.
Answer Submission and Evaluation
The player who pressed their button first must provide their answer in the format of artist name followed by song title, typically separated by a dash, colon, or the word "by". For example, acceptable formats include "The Beatles - Hey Jude", "Elvis Presley: Hound Dog", or "Yesterday by The Beatles".
The game's answer evaluation system uses sophisticated string matching algorithms that account for minor spelling variations, common abbreviations, and alternative artist or song title formats. The system normalizes text by removing common articles like "the", "a", and "an", and applies similarity matching to handle small typographical errors while maintaining accuracy standards.
Scoring System
Correct answers award the responding player twenty points. Incorrect answers result in a penalty of ten points being deducted from the responding player's score. When no player responds within the thirty-second timeout period, all players receive a penalty of five points.
Player scores can become negative if they accumulate more incorrect answers and timeouts than correct responses. The scoring system encourages both quick recognition and accuracy, as the point values reward correct identification while discouraging random guessing.
Winning Conditions
The first player to achieve a score of one hundred points or more wins the game immediately. When a player reaches the winning threshold, their green LED begins blinking to celebrate their victory, while all other players' red LEDs remain solidly illuminated to indicate the game has ended.
The game engine announces the winner and displays final scores for all players in ranked order. After a game concludes, players have the option to start a new game session with the same or different parameters.
Audio and Visual Feedback
Throughout the game, players receive clear audio and visual feedback about game state and their actions. Sound effects indicate various events such as button presses, correct answers, incorrect answers, round starts, and game completion. The LED indicators provide immediate visual feedback about game state, player status, and response opportunities.
The red LEDs serve multiple purposes including indicating active rounds through blinking, showing non-responding player status through solid illumination, and displaying game-over status for losing players. Green LEDs indicate the responding player during answer submission and celebrate the winner through blinking patterns.
Game Continuation and New Sessions
After a game concludes, the system offers the option to begin a new game session. Players may choose to maintain the same genre, time period, and country settings, or reconfigure these parameters for variety. The player-to-circuit associations remain intact unless explicitly reset, allowing for quick game restarts.
The game maintains a history of played songs throughout each session to ensure no repetition occurs. When starting a new game session, this history resets, allowing previously played songs to be selected again in subsequent games.
Alternative Input Methods
For environments where physical circuits are not available, the game supports keyboard input through an on-screen interface. Each player receives a numbered key assignment and sees a virtual representation of their circuit including simulated LED indicators. Players must identify themselves by entering their assigned number when pressing their keyboard key, ensuring proper player identification in the virtual environment.
Technical Requirements and Limitations
The game requires active internet connectivity for music streaming and song selection through the Spotify API. Players must remain attentive throughout each round as the thirty-second response window begins immediately when song playback starts. The game engine maintains strict timing enforcement to ensure fair play among all participants.
Circuit malfunctions or connectivity issues may require game pauses for technical resolution. The game engine includes error handling and recovery mechanisms to maintain game state during temporary technical difficulties while ensuring fair play for all participants.
System Architecture Overview
The complete system consists of four primary components that work in concert to deliver the gaming experience. The central game engine serves as the orchestrator, managing game state, player interactions, and communication with external services. This engine integrates with an LLM service that handles natural language processing, song selection logic, and player interaction management.
The physical interface layer comprises custom-designed electronic circuits, one for each player, that communicate with the game engine via USB or wireless protocols. These circuits feature tactile buttons for player input and LED indicators for visual feedback. The music streaming component leverages Spotify's Web API to search for and play songs that match the specified criteria.
class GameArchitecture:
def __init__(self):
self.game_engine = GameEngine()
self.llm_service = LLMService()
self.spotify_client = SpotifyClient()
self.circuit_manager = CircuitManager()
self.audio_manager = AudioManager()
def initialize_system(self):
"""Initialize all system components"""
self.game_engine.initialize()
self.llm_service.load_model()
self.spotify_client.authenticate()
self.circuit_manager.discover_devices()
self.audio_manager.setup_sound_effects()
The game engine maintains a comprehensive state machine that tracks player scores, song history, current round status, and player circuit associations. This state management ensures consistent game flow and prevents duplicate song selections within a single game session.
LLM Integration and Configuration
The Large Language Model serves as the intelligent core of the game, making decisions about song selection, managing player interactions, and providing natural language responses throughout the gaming experience. The system utilizes Hugging Face's transformers library to interface with various LLM models, allowing for flexible model selection based on performance requirements and available computational resources.
The LLM configuration is managed through a comprehensive configuration file that specifies model parameters, API endpoints, and behavioral settings. This configuration approach allows for easy model swapping and parameter tuning without code modifications.
import json
from transformers import AutoTokenizer, AutoModelForCausalLM
from transformers import pipeline
class LLMService:
def __init__(self, config_path="llm_config.json"):
with open(config_path, 'r') as f:
self.config = json.load(f)
self.model_name = self.config['model_name']
self.max_tokens = self.config['max_tokens']
self.temperature = self.config['temperature']
self.timeout_duration = self.config['timeout_duration']
self.tokenizer = None
self.model = None
self.pipeline = None
def load_model(self):
"""Load the specified LLM model"""
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
self.model = AutoModelForCausalLM.from_pretrained(self.model_name)
self.pipeline = pipeline(
"text-generation",
model=self.model,
tokenizer=self.tokenizer,
max_length=self.max_tokens,
temperature=self.temperature,
do_sample=True
)
The LLM configuration file in JSON-format contains essential parameters that control the model's behavior throughout the game. The temperature setting is kept deliberately low to ensure consistent and predictable responses, particularly important for game logic decisions and song selection criteria.
{
"model_name": "microsoft/DialoGPT-large",
"max_tokens": 512,
"temperature": 0.1,
"timeout_duration": 30,
"spotify_client_id": "your_spotify_client_id",
"spotify_client_secret": "your_spotify_client_secret",
"supported_genres": [
"classical", "jazz", "rock", "pop", "blues", "country",
"techno", "house", "hip-hop", "folk", "disco", "schlager"
],
"supported_decades": [
"1910s", "1920s", "1930s", "1940s", "1950s", "1960s",
"1970s", "1980s", "1990s", "2000s", "2010s", "2020s"
],
"supported_countries": ["US", "GB", "DE", "FR", "IT", "ES"],
"winning_score": 100,
"correct_answer_points": 20,
"incorrect_answer_points": -10,
"timeout_penalty": -5
}
The LLM processes natural language inputs from players and generates appropriate responses based on the current game context. Tool calling capabilities enable the LLM to interact with external services such as Spotify's API for song searches and the circuit management system for LED control.
Spotify Integration and Music Management
The music management component forms the heart of the game's content delivery system. Integration with Spotify's Web API provides access to millions of songs across various genres, time periods, and geographical regions. The implementation uses the Spotipy library, which provides a Python wrapper for Spotify's REST API.
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import random
from datetime import datetime
class SpotifyClient:
def __init__(self, client_id, client_secret):
self.client_credentials_manager = SpotifyClientCredentials(
client_id=client_id,
client_secret=client_secret
)
self.sp = spotipy.Spotify(
client_credentials_manager=self.client_credentials_manager
)
self.played_songs = set()
def search_songs(self, genre, decade, country=None, limit=50):
"""Search for songs matching the specified criteria"""
query_parts = [f"genre:{genre}"]
# Add year range based on decade
year_range = self._decade_to_year_range(decade)
if year_range:
query_parts.append(f"year:{year_range}")
# Add country filter if specified
if country:
query_parts.append(f"market:{country}")
query = " ".join(query_parts)
results = self.sp.search(
q=query,
type='track',
limit=limit,
market=country if country else 'US'
)
return results['tracks']['items']
The song selection algorithm ensures variety and prevents repetition within a single game session. Each selected song is added to a played songs set, and the selection process filters out previously played tracks. The system also maintains metadata about each song including artist, title, album, and release year for verification purposes.
def select_random_song(self, genre, decade, country=None):
"""Select a random song that hasn't been played yet"""
songs = self.search_songs(genre, decade, country)
# Filter out already played songs
available_songs = [
song for song in songs
if song['id'] not in self.played_songs
]
if not available_songs:
# Reset played songs if all have been used
self.played_songs.clear()
available_songs = songs
if available_songs:
selected_song = random.choice(available_songs)
self.played_songs.add(selected_song['id'])
return {
'id': selected_song['id'],
'title': selected_song['name'],
'artist': selected_song['artists'][0]['name'],
'album': selected_song['album']['name'],
'preview_url': selected_song['preview_url'],
'external_url': selected_song['external_urls']['spotify']
}
return None
The music playback system supports multiple playback methods depending on the available system capabilities. The primary method uses Spotify's Web Playback SDK for direct streaming, while fallback options include system-level audio playback and preview URL streaming for environments where full Spotify integration is not available.
Electronic Circuit Design and Implementation
Each player interface consists of a custom-designed electronic circuit built around an Arduino Nano microcontroller. The circuit provides tactile input through a momentary push button and visual feedback through red and green LEDs. The design emphasizes reliability, responsiveness, and clear visual indicators for game state communication.
The Bill of Materials for each player circuit includes the following components:
Arduino Nano microcontroller serves as the processing unit, providing GPIO pins for button input and LED control, along with USB connectivity for communication with the game engine. The momentary push button (SPST, normally open) provides player input with a satisfying tactile response. Two 5mm LEDs, one red and one green, serve as status indicators with appropriate current-limiting resistors.
Bill of Materials (per player circuit):
- 1x Arduino Nano (ATmega328P)
- 1x Momentary Push Button (SPST, NO)
- 1x Red LED (5mm, standard brightness)
- 1x Green LED (5mm, standard brightness)
- 2x 220-ohm Resistors (1/4 watt)
- 1x 10k-ohm Pull-up Resistor (1/4 watt)
- 1x Breadboard or Custom PCB
- 1x USB Cable (Type-A to Mini-B)
- 1x 3D Printed Enclosure
- Jumper wires and connectors
The circuit schematic demonstrates a straightforward design that maximizes reliability while minimizing component count. The push button connects to a digital input pin with a pull-up resistor to ensure clean digital signals. The LEDs connect to digital output pins through current-limiting resistors to prevent damage and ensure consistent brightness.
Circuit Connections:
Arduino Pin 2 -> Push Button (with 10k pull-up to VCC)
Arduino Pin 3 -> Red LED Anode (through 220-ohm resistor)
Arduino Pin 4 -> Green LED Anode (through 220-ohm resistor)
Arduino GND -> Button other terminal, LED cathodes
Arduino VCC -> Pull-up resistor, power rail
The microcontroller firmware written in C++ implements a simple but robust communication protocol that enables the game engine to control LED states and receive button press notifications. The protocol uses JSON-formatted messages over the USB serial connection for clarity and ease of debugging.
#include <ArduinoJson.h>
const int BUTTON_PIN = 2;
const int RED_LED_PIN = 3;
const int GREEN_LED_PIN = 4;
String deviceName = "PLAYER_01"; // Unique identifier
bool buttonPressed = false;
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;
// LED blinking state
bool redBlinking = false;
bool greenBlinking = false;
unsigned long lastBlinkTime = 0;
unsigned long blinkInterval = 500;
bool ledState = false;
void setup() {
Serial.begin(9600);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(RED_LED_PIN, OUTPUT);
pinMode(GREEN_LED_PIN, OUTPUT);
// Initialize LEDs off
digitalWrite(RED_LED_PIN, LOW);
digitalWrite(GREEN_LED_PIN, LOW);
// Send ready signal
sendMessage("READY", deviceName);
}
void loop() {
handleButtonInput();
handleLEDBlinking();
processSerialCommands();
}
The button debouncing algorithm ensures reliable detection of button presses while filtering out electrical noise and mechanical bounce. The implementation uses a time-based approach that requires the button state to remain stable for a minimum duration before registering a state change.
void handleButtonInput() {
int reading = digitalRead(BUTTON_PIN);
if (reading != lastButtonState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != buttonPressed) {
buttonPressed = reading;
if (buttonPressed == LOW) { // Button pressed (active low)
sendMessage("BUTTON_PRESSED", deviceName);
}
}
}
lastButtonState = reading;
}
The LED control system supports both steady states and blinking patterns, allowing the game engine to provide clear visual feedback about game state and player status. The blinking functionality uses non-blocking timing to ensure responsive button detection.
void handleLEDBlinking() {
unsigned long currentTime = millis();
if (currentTime - lastBlinkTime >= blinkInterval) {
lastBlinkTime = currentTime;
ledState = !ledState;
if (redBlinking) {
digitalWrite(RED_LED_PIN, ledState ? HIGH : LOW);
}
if (greenBlinking) {
digitalWrite(GREEN_LED_PIN, ledState ? HIGH : LOW);
}
}
}
void processSerialCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
StaticJsonDocument<200> doc;
deserializeJson(doc, command);
String action = doc["action"];
String target = doc["target"];
if (target == deviceName || target == "ALL") {
executeCommand(action);
}
}
}
The communication protocol between the game engine and microcontrollers uses a simple JSON-based message format that supports both individual device addressing and broadcast commands. This approach ensures scalability and maintainability while providing clear debugging capabilities.
Game Engine Implementation
The game engine serves as the central orchestrator, managing all aspects of the gaming experience from player registration through score tracking and game completion. The engine implements a state machine architecture that ensures consistent game flow and proper handling of concurrent player actions.
import threading
import time
import json
from enum import Enum
from dataclasses import dataclass
from typing import Dict, List, Optional
class GameState(Enum):
INITIALIZATION = "initialization"
PLAYER_REGISTRATION = "player_registration"
GAME_SETUP = "game_setup"
ROUND_START = "round_start"
SONG_PLAYING = "song_playing"
WAITING_FOR_ANSWER = "waiting_for_answer"
ANSWER_EVALUATION = "answer_evaluation"
ROUND_END = "round_end"
GAME_OVER = "game_over"
@dataclass
class Player:
circuit_id: str
name: str
score: int = 0
is_active: bool = True
@dataclass
class GameSession:
players: Dict[str, Player]
current_song: Optional[Dict]
played_songs: List[str]
genre: str
decade: str
country: Optional[str]
round_number: int = 0
state: GameState = GameState.INITIALIZATION
The game engine maintains comprehensive state information about all players, current game parameters, and round progress. This state management enables the system to handle complex scenarios such as simultaneous button presses, timeout conditions, and game interruptions.
class GameEngine:
def __init__(self, config_path="game_config.json"):
with open(config_path, 'r') as f:
self.config = json.load(f)
self.session = GameSession(
players={},
current_song=None,
played_songs=[],
genre="",
decade="",
country=None
)
self.llm_service = LLMService()
self.spotify_client = SpotifyClient(
self.config['spotify_client_id'],
self.config['spotify_client_secret']
)
self.circuit_manager = CircuitManager()
self.audio_manager = AudioManager()
self.round_timer = None
self.button_lock = threading.Lock()
def start_game(self):
"""Initialize and start a new game session"""
self.session.state = GameState.INITIALIZATION
self.initialize_components()
self.register_players()
self.setup_game_parameters()
self.begin_gameplay()
The player registration process establishes the connection between physical circuits and player identities. The system prompts each player to press their button and provide their name, creating a mapping that persists throughout the game session.
def register_players(self):
"""Register all connected players"""
self.session.state = GameState.PLAYER_REGISTRATION
self.llm_service.announce("Welcome to the Music Quiz Game! Let's register all players.")
discovered_circuits = self.circuit_manager.discover_circuits()
for circuit_id in discovered_circuits:
self.llm_service.announce(f"Player with circuit {circuit_id}, please press your button.")
# Wait for button press
button_pressed = self.wait_for_button_press(circuit_id, timeout=30)
if button_pressed:
# Get player name
player_name = self.llm_service.get_player_name(circuit_id)
# Create player record
player = Player(circuit_id=circuit_id, name=player_name)
self.session.players[circuit_id] = player
self.llm_service.announce(f"Welcome {player_name}! You are registered with circuit {circuit_id}.")
self.circuit_manager.flash_green_led(circuit_id)
else:
self.llm_service.announce(f"No response from circuit {circuit_id}. Skipping registration.")
The game setup phase collects essential parameters including music genre, time period, and optional country restrictions. The LLM service handles natural language interaction with the game administrator to establish these parameters.
def setup_game_parameters(self):
"""Configure game parameters through LLM interaction"""
self.session.state = GameState.GAME_SETUP
# Get genre selection
self.session.genre = self.llm_service.get_genre_selection()
# Get time period selection
self.session.decade = self.llm_service.get_decade_selection()
# Get optional country selection
self.session.country = self.llm_service.get_country_selection()
self.llm_service.announce(
f"Game configured: Genre={self.session.genre}, "
f"Decade={self.session.decade}, "
f"Country={self.session.country or 'Any'}"
)
The core gameplay loop manages round progression, song selection, player input handling, and score calculation. Each round follows a consistent pattern that ensures fair play and clear feedback to all participants.
def begin_gameplay(self):
"""Start the main gameplay loop"""
while self.session.state != GameState.GAME_OVER:
self.start_new_round()
if self.check_winner():
self.end_game()
break
def start_new_round(self):
"""Initialize and execute a single game round"""
self.session.state = GameState.ROUND_START
self.session.round_number += 1
self.llm_service.announce(f"Starting round {self.session.round_number}")
# Select and play song
song = self.spotify_client.select_random_song(
self.session.genre,
self.session.decade,
self.session.country
)
if not song:
self.llm_service.announce("No more songs available. Game ending.")
self.session.state = GameState.GAME_OVER
return
self.session.current_song = song
self.session.played_songs.append(song['id'])
# Start round timer and wait for responses
self.execute_round()
The round execution system handles the complex timing and interaction logic required for fair gameplay. The implementation uses threading to manage simultaneous button monitoring and timeout enforcement.
def execute_round(self):
"""Execute round logic with timing and player input"""
self.session.state = GameState.SONG_PLAYING
# Start all red LEDs blinking
self.circuit_manager.start_red_blinking_all()
# Play the song
self.audio_manager.play_song(self.session.current_song)
# Start timeout timer
self.start_round_timer()
self.session.state = GameState.WAITING_FOR_ANSWER
# Wait for button press or timeout
responding_player = self.wait_for_first_button_press()
if responding_player:
self.handle_player_response(responding_player)
else:
self.handle_round_timeout()
def handle_player_response(self, player_circuit_id):
"""Process player response and update scores"""
self.cancel_round_timer()
# Update LED states
self.circuit_manager.set_green_led(player_circuit_id, True)
self.circuit_manager.set_red_led_all_except(player_circuit_id, True)
# Get player's answer
player = self.session.players[player_circuit_id]
answer = self.llm_service.get_player_answer(player.name)
# Evaluate answer
is_correct = self.evaluate_answer(answer)
if is_correct:
player.score += self.config['correct_answer_points']
self.llm_service.announce(f"Correct! {player.name} gains 20 points.")
self.audio_manager.play_correct_sound()
else:
player.score += self.config['incorrect_answer_points']
self.llm_service.announce(
f"Incorrect. {player.name} loses 10 points. "
f"The correct answer was: {self.session.current_song['artist']} - "
f"{self.session.current_song['title']}"
)
self.audio_manager.play_incorrect_sound()
# Reset LEDs
self.circuit_manager.turn_off_all_leds()
Answer Evaluation and Scoring System
The answer evaluation system implements sophisticated string matching algorithms to handle variations in artist names and song titles. The system accounts for common abbreviations, alternative spellings, and minor typographical errors while maintaining accuracy standards.
import difflib
import re
class AnswerEvaluator:
def __init__(self, similarity_threshold=0.8):
self.similarity_threshold = similarity_threshold
def evaluate_answer(self, player_answer, correct_song):
"""Evaluate player answer against correct song information"""
# Parse player answer
parsed_answer = self.parse_player_answer(player_answer)
if not parsed_answer:
return False
artist_match = self.check_artist_match(
parsed_answer['artist'],
correct_song['artist']
)
title_match = self.check_title_match(
parsed_answer['title'],
correct_song['title']
)
return artist_match and title_match
def parse_player_answer(self, answer):
"""Parse player answer to extract artist and title"""
# Common patterns: "Artist - Title", "Artist: Title", "Title by Artist"
patterns = [
r'^(.+?)\s*-\s*(.+)$',
r'^(.+?)\s*:\s*(.+)$',
r'^(.+?)\s+by\s+(.+)$',
r'^(.+?)\s*,\s*(.+)$'
]
for pattern in patterns:
match = re.match(pattern, answer.strip(), re.IGNORECASE)
if match:
return {
'artist': match.group(1).strip(),
'title': match.group(2).strip()
}
return None
The string similarity calculation uses the difflib library to compute sequence matching ratios, allowing for minor variations while maintaining accuracy requirements. The system also implements normalization functions to handle case differences and common punctuation variations.
def check_artist_match(self, player_artist, correct_artist):
"""Check if player's artist answer matches correct artist"""
normalized_player = self.normalize_string(player_artist)
normalized_correct = self.normalize_string(correct_artist)
# Exact match
if normalized_player == normalized_correct:
return True
# Similarity match
similarity = difflib.SequenceMatcher(
None,
normalized_player,
normalized_correct
).ratio()
return similarity >= self.similarity_threshold
def normalize_string(self, text):
"""Normalize string for comparison"""
# Convert to lowercase
text = text.lower()
# Remove common articles and prefixes
articles = ['the', 'a', 'an']
words = text.split()
if words and words[0] in articles:
words = words[1:]
# Remove punctuation and extra spaces
text = ' '.join(words)
text = re.sub(r'[^\w\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
Circuit Manager and Communication Protocol
The circuit manager handles all communication with the player interface circuits, implementing a robust protocol that ensures reliable message delivery and proper device addressing. The system supports both individual device commands and broadcast operations for efficient LED control.
import serial
import json
import threading
import time
from typing import Dict, List
class CircuitManager:
def __init__(self):
self.circuits = {} # circuit_id -> serial_connection
self.message_queue = {} # circuit_id -> message_list
self.response_handlers = {}
self.discovery_lock = threading.Lock()
def discover_circuits(self):
"""Discover and initialize all connected circuits"""
with self.discovery_lock:
# Scan available serial ports
available_ports = self.scan_serial_ports()
for port in available_ports:
try:
connection = serial.Serial(port, 9600, timeout=1)
time.sleep(2) # Allow Arduino to reset
# Send identification request
self.send_raw_command(connection, {
"action": "IDENTIFY",
"target": "ALL"
})
# Wait for response
response = self.wait_for_response(connection, timeout=5)
if response and response.get('type') == 'READY':
circuit_id = response.get('data')
self.circuits[circuit_id] = connection
except Exception as e:
print(f"Failed to connect to {port}: {e}")
return list(self.circuits.keys())
The message queuing system ensures reliable command delivery even when circuits are temporarily unresponsive. Commands are queued per device and processed in order, with automatic retry logic for failed transmissions.
def send_command(self, circuit_id, action, data=None):
"""Send command to specific circuit with queuing"""
command = {
"action": action,
"target": circuit_id,
"data": data,
"timestamp": time.time()
}
if circuit_id not in self.message_queue:
self.message_queue[circuit_id] = []
self.message_queue[circuit_id].append(command)
self.process_message_queue(circuit_id)
def process_message_queue(self, circuit_id):
"""Process queued messages for a specific circuit"""
if circuit_id not in self.circuits:
return
connection = self.circuits[circuit_id]
queue = self.message_queue.get(circuit_id, [])
while queue:
command = queue[0]
try:
self.send_raw_command(connection, command)
queue.pop(0) # Remove successfully sent command
except Exception as e:
print(f"Failed to send command to {circuit_id}: {e}")
break # Stop processing queue on error
def send_raw_command(self, connection, command):
"""Send raw command to serial connection"""
message = json.dumps(command) + '\n'
connection.write(message.encode())
connection.flush()
```
The LED control functions provide high-level interfaces for common game operations, abstracting the low-level protocol details and ensuring consistent visual feedback across all player circuits.
def set_red_led(self, circuit_id, state):
"""Set red LED state for specific circuit"""
action = "RED_LED_ON" if state else "RED_LED_OFF"
self.send_command(circuit_id, action)
def set_green_led(self, circuit_id, state):
"""Set green LED state for specific circuit"""
action = "GREEN_LED_ON" if state else "GREEN_LED_OFF"
self.send_command(circuit_id, action)
def start_red_blinking_all(self):
"""Start red LED blinking on all circuits"""
for circuit_id in self.circuits.keys():
self.send_command(circuit_id, "RED_LED_BLINK_START")
def start_green_blinking(self, circuit_id):
"""Start green LED blinking on specific circuit"""
self.send_command(circuit_id, "GREEN_LED_BLINK_START")
def turn_off_all_leds(self):
"""Turn off all LEDs on all circuits"""
for circuit_id in self.circuits.keys():
self.send_command(circuit_id, "ALL_LEDS_OFF")
Audio Management and Sound Effects
The audio management system provides comprehensive sound control for both music playback and game sound effects. The implementation supports multiple audio backends to ensure compatibility across different operating systems and hardware configurations.
import pygame
import subprocess
import platform
import requests
import tempfile
import os
class AudioManager:
def __init__(self):
self.current_song = None
self.sound_effects = {}
self.volume = 0.7
# Initialize pygame mixer for sound effects
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
# Load sound effects
self.load_sound_effects()
def load_sound_effects(self):
"""Load game sound effects"""
sound_files = {
'correct': 'sounds/correct.wav',
'incorrect': 'sounds/incorrect.wav',
'button_press': 'sounds/button.wav',
'game_over': 'sounds/game_over.wav',
'round_start': 'sounds/round_start.wav'
}
for name, filepath in sound_files.items():
if os.path.exists(filepath):
self.sound_effects[name] = pygame.mixer.Sound(filepath)
else:
# Generate simple tones if files don't exist
self.sound_effects[name] = self.generate_tone(name)
def play_song(self, song_info):
"""Play song using available method"""
self.current_song = song_info
# Try Spotify Web Playback first
if self.play_spotify_track(song_info['id']):
return True
# Fallback to preview URL
if song_info.get('preview_url'):
return self.play_preview_url(song_info['preview_url'])
# Fallback to system browser
return self.open_in_browser(song_info['external_url'])
```
The Spotify integration supports multiple playback methods depending on available authentication and system capabilities. The primary method uses the Web Playback SDK for seamless integration, while fallback methods ensure functionality even in limited environments.
def play_spotify_track(self, track_id):
"""Play track using Spotify Web Playback (requires premium)"""
try:
# This would require Spotify Web Playback SDK integration
# For now, use system default player
spotify_url = f"spotify:track:{track_id}"
if platform.system() == "Windows":
os.startfile(spotify_url)
elif platform.system() == "Darwin": # macOS
subprocess.run(["open", spotify_url])
else: # Linux
subprocess.run(["xdg-open", spotify_url])
return True
except Exception as e:
print(f"Failed to play Spotify track: {e}")
return False
def play_preview_url(self, preview_url):
"""Play 30-second preview using pygame"""
try:
# Download preview to temporary file
response = requests.get(preview_url)
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
temp_file.write(response.content)
temp_filename = temp_file.name
# Play using pygame
pygame.mixer.music.load(temp_filename)
pygame.mixer.music.set_volume(self.volume)
pygame.mixer.music.play()
# Clean up temporary file after playback
threading.Timer(35.0, lambda: os.unlink(temp_filename)).start()
return True
except Exception as e:
print(f"Failed to play preview: {e}")
return False
The sound effects system provides immediate audio feedback for game events, enhancing the user experience and providing clear auditory cues for game state changes. The system generates synthetic tones when audio files are not available, ensuring functionality in all environments.
def play_sound_effect(self, effect_name):
"""Play specified sound effect"""
if effect_name in self.sound_effects:
self.sound_effects[effect_name].play()
def generate_tone(self, effect_type):
"""Generate simple tone for sound effect"""
# Frequency mapping for different effects
frequencies = {
'correct': 800, # High pleasant tone
'incorrect': 200, # Low warning tone
'button_press': 600, # Medium click tone
'game_over': 400, # Moderate ending tone
'round_start': 1000 # High attention tone
}
frequency = frequencies.get(effect_type, 500)
duration = 0.3 # 300ms
sample_rate = 22050
# Generate sine wave
frames = int(duration * sample_rate)
arr = []
for i in range(frames):
wave = 4096 * math.sin(frequency * 2 * math.pi * i / sample_rate)
arr.append([int(wave), int(wave)])
sound = pygame.sndarray.make_sound(numpy.array(arr))
return sound
3D Printable Enclosure Design
The physical enclosure design ensures durability, user comfort, and clear visual indicator visibility while maintaining a professional appearance suitable for gaming environments. The enclosure accommodates all electronic components while providing easy access for maintenance and battery replacement if wireless operation is desired.
The enclosure design specifications include a compact form factor measuring approximately 80mm x 60mm x 40mm, providing sufficient internal space for the Arduino Nano and associated components. The top surface features a large tactile button with satisfying mechanical feedback and two LED indicator windows positioned for optimal visibility from multiple angles.
Enclosure Design Specifications:
Overall Dimensions: 80mm (L) x 60mm (W) x 40mm (H)
Material: PLA or PETG plastic (3D printable)
Wall Thickness: 2.5mm for structural integrity
Button Opening: 15mm diameter with tactile feedback rim
LED Windows: 8mm diameter, positioned 25mm apart
USB Port Access: 12mm x 8mm rectangular opening
Mounting Features: Four corner mounting holes (M3 screws)
Assembly Method: Snap-fit with optional screw reinforcement
Internal Layout:
- Arduino Nano mounting posts with M2.5 screws
- Component cavity for resistors and wiring
- Cable management channels for clean routing
- Battery compartment (optional, for wireless operation)
The button mechanism incorporates a spring-loaded design that provides consistent tactile feedback while ensuring reliable electrical contact. The LED windows use translucent material or clear inserts to provide optimal light transmission while protecting the LEDs from physical damage.
Button Assembly Details:
- 15mm diameter momentary switch (panel mount)
- Custom button cap with ergonomic profile
- Spring return mechanism for positive feedback
- Electrical rating: 50mA at 12V DC minimum
- Actuation force: 150-250 grams
- Travel distance: 2-3mm with tactile click
LED Window Construction:
- Clear acrylic or polycarbonate inserts
- Diffusion pattern for even light distribution
- Press-fit installation for easy replacement
- UV-resistant material for longevity
- Anti-glare surface treatment
The enclosure assembly process uses a combination of snap-fit connections and optional screw reinforcement for applications requiring enhanced durability. The design accommodates both wired USB connections and optional wireless communication modules for advanced installations.
Alternative Keyboard Interface
The keyboard interface provides an alternative input method for environments where physical circuits are not practical or available. This implementation creates a virtual representation of the player circuits on screen, complete with simulated LED indicators and keyboard-based input.
import tkinter as tk
from tkinter import ttk, messagebox
import threading
class KeyboardInterface:
def __init__(self, game_engine):
self.game_engine = game_engine
self.root = tk.Tk()
self.root.title("Music Quiz Game - Keyboard Interface")
self.root.geometry("800x600")
self.player_frames = {}
self.player_buttons = {}
self.red_leds = {}
self.green_leds = {}
self.key_bindings = {}
self.setup_interface()
self.bind_keyboard_events()
def setup_interface(self):
"""Create the main interface layout"""
# Title
title_label = tk.Label(
self.root,
text="Music Quiz Game",
font=("Arial", 24, "bold")
)
title_label.pack(pady=20)
# Player grid
self.players_frame = tk.Frame(self.root)
self.players_frame.pack(expand=True, fill="both", padx=20, pady=20)
# Game info panel
self.info_frame = tk.Frame(self.root)
self.info_frame.pack(fill="x", padx=20, pady=10)
self.game_info_label = tk.Label(
self.info_frame,
text="Press 'S' to start game setup",
font=("Arial", 12)
)
self.game_info_label.pack()
def create_player_interface(self, player_count):
"""Create interface elements for specified number of players"""
# Clear existing interfaces
for widget in self.players_frame.winfo_children():
widget.destroy()
# Calculate grid layout
cols = min(4, player_count)
rows = (player_count + cols - 1) // cols
for i in range(player_count):
player_id = f"PLAYER_{i+1:02d}"
# Create player frame
row = i // cols
col = i % cols
player_frame = tk.Frame(
self.players_frame,
relief="raised",
borderwidth=2,
padx=10,
pady=10
)
player_frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew")
# Player name label
name_label = tk.Label(
player_frame,
text=f"Player {i+1}",
font=("Arial", 14, "bold")
)
name_label.pack()
# LED indicators
led_frame = tk.Frame(player_frame)
led_frame.pack(pady=10)
red_led = tk.Label(
led_frame,
text="●",
font=("Arial", 20),
fg="darkred",
bg="white"
)
red_led.pack(side="left", padx=5)
green_led = tk.Label(
led_frame,
text="●",
font=("Arial", 20),
fg="darkgreen",
bg="white"
)
green_led.pack(side="left", padx=5)
# Button
button = tk.Button(
player_frame,
text=f"Press {i+1}",
font=("Arial", 12),
width=10,
height=2,
command=lambda pid=player_id: self.button_pressed(pid)
)
button.pack(pady=10)
# Store references
self.player_frames[player_id] = player_frame
self.player_buttons[player_id] = button
self.red_leds[player_id] = red_led
self.green_leds[player_id] = green_led
self.key_bindings[str(i+1)] = player_id
# Configure grid weights
for i in range(cols):
self.players_frame.columnconfigure(i, weight=1)
for i in range(rows):
self.players_frame.rowgrid(i, weight=1)
The keyboard interface implements the same visual feedback system as the physical circuits, using colored indicators to represent LED states and providing clear visual cues for game state changes.
def bind_keyboard_events(self):
"""Bind keyboard events for player input"""
self.root.bind('<KeyPress>', self.handle_keypress)
self.root.focus_set() # Ensure window has focus
def handle_keypress(self, event):
"""Handle keyboard input for player actions"""
key = event.char
# Check for player number keys
if key in self.key_bindings:
player_id = self.key_bindings[key]
self.button_pressed(player_id)
# Check for game control keys
elif key.lower() == 's':
self.start_game_setup()
elif key.lower() == 'q':
self.quit_game()
def button_pressed(self, player_id):
"""Handle virtual button press"""
if player_id in self.player_frames:
# Visual feedback
button = self.player_buttons[player_id]
button.config(relief="sunken")
# Restore button after short delay
self.root.after(100, lambda: button.config(relief="raised"))
# Notify game engine
self.game_engine.handle_button_press(player_id)
def update_led_state(self, player_id, red_state, green_state):
"""Update LED visual states"""
if player_id in self.red_leds:
red_color = "red" if red_state else "darkred"
green_color = "lime" if green_state else "darkgreen"
self.red_leds[player_id].config(fg=red_color)
self.green_leds[player_id].config(fg=green_color)
Complete Running Example
The following complete implementation demonstrates all system components working together in a production-ready configuration. This example includes error handling, configuration management, and robust state management suitable for real-world deployment.
#!/usr/bin/env python3
"""
Music Quiz Game - Complete Implementation
A multi-player music quiz game using LLM, Spotify API, and Arduino circuits
"""
import json
import time
import threading
import logging
import sys
import os
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
from enum import Enum
import queue
# Third-party imports
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
import serial
import serial.tools.list_ports
import pygame
import requests
import tempfile
import difflib
import re
import numpy as np
import math
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('music_quiz.log'),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
class GameState(Enum):
"""Game state enumeration"""
INITIALIZATION = "initialization"
PLAYER_REGISTRATION = "player_registration"
GAME_SETUP = "game_setup"
ROUND_START = "round_start"
SONG_PLAYING = "song_playing"
WAITING_FOR_ANSWER = "waiting_for_answer"
ANSWER_EVALUATION = "answer_evaluation"
ROUND_END = "round_end"
GAME_OVER = "game_over"
@dataclass
class Player:
"""Player data structure"""
circuit_id: str
name: str
score: int = 0
is_active: bool = True
def to_dict(self):
return asdict(self)
@dataclass
class Song:
"""Song data structure"""
id: str
title: str
artist: str
album: str
preview_url: Optional[str]
external_url: str
def to_dict(self):
return asdict(self)
@dataclass
class GameSession:
"""Game session data structure"""
players: Dict[str, Player]
current_song: Optional[Song]
played_songs: List[str]
genre: str
decade: str
country: Optional[str]
round_number: int = 0
state: GameState = GameState.INITIALIZATION
start_time: Optional[datetime] = None
def to_dict(self):
return {
'players': {k: v.to_dict() for k, v in self.players.items()},
'current_song': self.current_song.to_dict() if self.current_song else None,
'played_songs': self.played_songs,
'genre': self.genre,
'decade': self.decade,
'country': self.country,
'round_number': self.round_number,
'state': self.state.value,
'start_time': self.start_time.isoformat() if self.start_time else None
}
class ConfigManager:
"""Configuration management"""
def __init__(self, config_path="config.json"):
self.config_path = config_path
self.config = self.load_config()
def load_config(self):
"""Load configuration from file"""
default_config = {
"llm": {
"model_name": "microsoft/DialoGPT-large",
"max_tokens": 512,
"temperature": 0.1
},
"spotify": {
"client_id": "",
"client_secret": ""
},
"game": {
"timeout_duration": 30,
"winning_score": 100,
"correct_answer_points": 20,
"incorrect_answer_points": -10,
"timeout_penalty": -5,
"similarity_threshold": 0.8
},
"audio": {
"volume": 0.7,
"sound_effects_enabled": True
},
"circuits": {
"baud_rate": 9600,
"connection_timeout": 5,
"discovery_timeout": 10
},
"supported_genres": [
"classical", "jazz", "rock", "pop", "blues", "country",
"techno", "house", "hip-hop", "folk", "disco", "schlager"
],
"supported_decades": [
"1910s", "1920s", "1930s", "1940s", "1950s", "1960s",
"1970s", "1980s", "1990s", "2000s", "2010s", "2020s"
],
"supported_countries": ["US", "GB", "DE", "FR", "IT", "ES"]
}
try:
if os.path.exists(self.config_path):
with open(self.config_path, 'r') as f:
loaded_config = json.load(f)
# Merge with defaults
self._deep_update(default_config, loaded_config)
return default_config
except Exception as e:
logger.error(f"Failed to load config: {e}")
return default_config
def _deep_update(self, base_dict, update_dict):
"""Recursively update nested dictionaries"""
for key, value in update_dict.items():
if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict):
self._deep_update(base_dict[key], value)
else:
base_dict[key] = value
def save_config(self):
"""Save current configuration to file"""
try:
with open(self.config_path, 'w') as f:
json.dump(self.config, f, indent=2)
except Exception as e:
logger.error(f"Failed to save config: {e}")
class SpotifyClient:
"""Spotify API client for music search and playback"""
def __init__(self, client_id: str, client_secret: str):
self.client_id = client_id
self.client_secret = client_secret
self.sp = None
self.played_songs = set()
self.initialize_client()
def initialize_client(self):
"""Initialize Spotify client"""
try:
client_credentials_manager = SpotifyClientCredentials(
client_id=self.client_id,
client_secret=self.client_secret
)
self.sp = spotipy.Spotify(
client_credentials_manager=client_credentials_manager
)
logger.info("Spotify client initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize Spotify client: {e}")
raise
def search_songs(self, genre: str, decade: str, country: Optional[str] = None, limit: int = 50) -> List[Dict]:
"""Search for songs matching criteria"""
try:
query_parts = [f"genre:{genre}"]
# Add year range based on decade
year_range = self._decade_to_year_range(decade)
if year_range:
query_parts.append(f"year:{year_range}")
query = " ".join(query_parts)
results = self.sp.search(
q=query,
type='track',
limit=limit,
market=country if country else 'US'
)
songs = []
for track in results['tracks']['items']:
if track['preview_url']: # Only include tracks with previews
song = Song(
id=track['id'],
title=track['name'],
artist=track['artists'][0]['name'],
album=track['album']['name'],
preview_url=track['preview_url'],
external_url=track['external_urls']['spotify']
)
songs.append(song)
logger.info(f"Found {len(songs)} songs for {genre} {decade}")
return songs
except Exception as e:
logger.error(f"Failed to search songs: {e}")
return []
def select_random_song(self, genre: str, decade: str, country: Optional[str] = None) -> Optional[Song]:
"""Select a random song that hasn't been played"""
songs = self.search_songs(genre, decade, country)
# Filter out already played songs
available_songs = [song for song in songs if song.id not in self.played_songs]
if not available_songs:
# Reset if all songs have been played
self.played_songs.clear()
available_songs = songs
if available_songs:
import random
selected_song = random.choice(available_songs)
self.played_songs.add(selected_song.id)
logger.info(f"Selected song: {selected_song.artist} - {selected_song.title}")
return selected_song
return None
def _decade_to_year_range(self, decade: str) -> str:
"""Convert decade string to year range"""
decade_map = {
"1910s": "1910-1919",
"1920s": "1920-1929",
"1930s": "1930-1939",
"1940s": "1940-1949",
"1950s": "1950-1959",
"1960s": "1960-1969",
"1970s": "1970-1979",
"1980s": "1980-1989",
"1990s": "1990-1999",
"2000s": "2000-2009",
"2010s": "2010-2019",
"2020s": "2020-2029"
}
return decade_map.get(decade, "")
class CircuitManager:
"""Manages communication with Arduino circuits"""
def __init__(self, config: Dict):
self.config = config
self.circuits = {} # circuit_id -> serial_connection
self.message_queues = {} # circuit_id -> queue
self.response_handlers = {}
self.running = False
self.communication_thread = None
def start(self):
"""Start circuit communication"""
self.running = True
self.communication_thread = threading.Thread(target=self._communication_loop)
self.communication_thread.daemon = True
self.communication_thread.start()
def stop(self):
"""Stop circuit communication"""
self.running = False
if self.communication_thread:
self.communication_thread.join(timeout=5)
# Close all connections
for connection in self.circuits.values():
try:
connection.close()
except:
pass
def discover_circuits(self) -> List[str]:
"""Discover and connect to all available circuits"""
discovered_circuits = []
# Scan available serial ports
ports = serial.tools.list_ports.comports()
for port in ports:
try:
connection = serial.Serial(
port.device,
self.config['baud_rate'],
timeout=self.config['connection_timeout']
)
time.sleep(2) # Allow Arduino to reset
# Send identification request
self._send_raw_command(connection, {
"action": "IDENTIFY",
"target": "ALL"
})
# Wait for response
response = self._wait_for_response(connection, timeout=5)
if response and response.get('type') == 'READY':
circuit_id = response.get('data')
self.circuits[circuit_id] = connection
self.message_queues[circuit_id] = queue.Queue()
discovered_circuits.append(circuit_id)
logger.info(f"Connected to circuit: {circuit_id}")
except Exception as e:
logger.debug(f"Failed to connect to {port.device}: {e}")
logger.info(f"Discovered {len(discovered_circuits)} circuits")
return discovered_circuits
def send_command(self, circuit_id: str, action: str, data: Optional[str] = None):
"""Send command to specific circuit"""
if circuit_id not in self.message_queues:
logger.warning(f"Circuit {circuit_id} not found")
return
command = {
"action": action,
"target": circuit_id,
"data": data,
"timestamp": time.time()
}
self.message_queues[circuit_id].put(command)
def broadcast_command(self, action: str, data: Optional[str] = None):
"""Send command to all circuits"""
for circuit_id in self.circuits.keys():
self.send_command(circuit_id, action, data)
def set_red_led(self, circuit_id: str, state: bool):
"""Control red LED"""
action = "RED_LED_ON" if state else "RED_LED_OFF"
self.send_command(circuit_id, action)
def set_green_led(self, circuit_id: str, state: bool):
"""Control green LED"""
action = "GREEN_LED_ON" if state else "GREEN_LED_OFF"
self.send_command(circuit_id, action)
def start_red_blinking_all(self):
"""Start red LED blinking on all circuits"""
self.broadcast_command("RED_LED_BLINK_START")
def start_green_blinking(self, circuit_id: str):
"""Start green LED blinking on specific circuit"""
self.send_command(circuit_id, "GREEN_LED_BLINK_START")
def turn_off_all_leds(self):
"""Turn off all LEDs on all circuits"""
self.broadcast_command("ALL_LEDS_OFF")
def _communication_loop(self):
"""Main communication loop"""
while self.running:
for circuit_id, connection in self.circuits.items():
try:
# Process outgoing messages
if circuit_id in self.message_queues:
try:
command = self.message_queues[circuit_id].get_nowait()
self._send_raw_command(connection, command)
except queue.Empty:
pass
# Process incoming messages
if connection.in_waiting > 0:
response = self._read_response(connection)
if response:
self._handle_response(circuit_id, response)
except Exception as e:
logger.error(f"Communication error with {circuit_id}: {e}")
time.sleep(0.01) # Small delay to prevent CPU overload
def _send_raw_command(self, connection: serial.Serial, command: Dict):
"""Send raw command to serial connection"""
message = json.dumps(command) + '\n'
connection.write(message.encode())
connection.flush()
def _read_response(self, connection: serial.Serial) -> Optional[Dict]:
"""Read response from serial connection"""
try:
line = connection.readline().decode().strip()
if line:
return json.loads(line)
except Exception as e:
logger.debug(f"Failed to read response: {e}")
return None
def _wait_for_response(self, connection: serial.Serial, timeout: int = 5) -> Optional[Dict]:
"""Wait for specific response"""
start_time = time.time()
while time.time() - start_time < timeout:
response = self._read_response(connection)
if response:
return response
time.sleep(0.1)
return None
def _handle_response(self, circuit_id: str, response: Dict):
"""Handle incoming response from circuit"""
if response.get('type') == 'BUTTON_PRESSED':
# Notify game engine of button press
if hasattr(self, 'button_callback'):
self.button_callback(circuit_id)
class AudioManager:
"""Manages audio playback and sound effects"""
def __init__(self, config: Dict):
self.config = config
self.current_song = None
self.sound_effects = {}
self.volume = config.get('volume', 0.7)
# Initialize pygame mixer
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
if config.get('sound_effects_enabled', True):
self.load_sound_effects()
def load_sound_effects(self):
"""Load or generate sound effects"""
effects = ['correct', 'incorrect', 'button_press', 'game_over', 'round_start']
for effect in effects:
filepath = f"sounds/{effect}.wav"
if os.path.exists(filepath):
self.sound_effects[effect] = pygame.mixer.Sound(filepath)
else:
# Generate synthetic sound
self.sound_effects[effect] = self._generate_tone(effect)
def play_song(self, song: Song) -> bool:
"""Play song using available method"""
self.current_song = song
if song.preview_url:
return self._play_preview_url(song.preview_url)
else:
return self._open_in_browser(song.external_url)
def stop_song(self):
"""Stop current song playback"""
pygame.mixer.music.stop()
self.current_song = None
def play_sound_effect(self, effect_name: str):
"""Play specified sound effect"""
if effect_name in self.sound_effects:
self.sound_effects[effect_name].play()
def _play_preview_url(self, preview_url: str) -> bool:
"""Play 30-second preview"""
try:
response = requests.get(preview_url, timeout=10)
with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file:
temp_file.write(response.content)
temp_filename = temp_file.name
pygame.mixer.music.load(temp_filename)
pygame.mixer.music.set_volume(self.volume)
pygame.mixer.music.play()
# Clean up temporary file after playback
threading.Timer(35.0, lambda: self._cleanup_temp_file(temp_filename)).start()
return True
except Exception as e:
logger.error(f"Failed to play preview: {e}")
return False
def _cleanup_temp_file(self, filename: str):
"""Clean up temporary audio file"""
try:
os.unlink(filename)
except:
pass
def _open_in_browser(self, url: str) -> bool:
"""Open song in system browser/player"""
try:
import webbrowser
webbrowser.open(url)
return True
except Exception as e:
logger.error(f"Failed to open in browser: {e}")
return False
def _generate_tone(self, effect_type: str) -> pygame.mixer.Sound:
"""Generate synthetic tone for sound effect"""
frequencies = {
'correct': 800,
'incorrect': 200,
'button_press': 600,
'game_over': 400,
'round_start': 1000
}
frequency = frequencies.get(effect_type, 500)
duration = 0.3
sample_rate = 22050
frames = int(duration * sample_rate)
arr = np.zeros((frames, 2))
for i in range(frames):
wave = 0.3 * np.sin(frequency * 2 * np.pi * i / sample_rate)
arr[i] = [wave, wave]
# Convert to integer format
arr = (arr * 32767).astype(np.int16)
return pygame.mixer.sndarray.make_sound(arr)
class AnswerEvaluator:
"""Evaluates player answers against correct solutions"""
def __init__(self, similarity_threshold: float = 0.8):
self.similarity_threshold = similarity_threshold
def evaluate_answer(self, player_answer: str, correct_song: Song) -> bool:
"""Evaluate player answer"""
parsed_answer = self._parse_player_answer(player_answer)
if not parsed_answer:
return False
artist_match = self._check_match(
parsed_answer['artist'],
correct_song.artist
)
title_match = self._check_match(
parsed_answer['title'],
correct_song.title
)
return artist_match and title_match
def _parse_player_answer(self, answer: str) -> Optional[Dict[str, str]]:
"""Parse player answer to extract artist and title"""
patterns = [
r'^(.+?)\s*-\s*(.+)$',
r'^(.+?)\s*:\s*(.+)$',
r'^(.+?)\s+by\s+(.+)$',
r'^(.+?)\s*,\s*(.+)$'
]
for pattern in patterns:
match = re.match(pattern, answer.strip(), re.IGNORECASE)
if match:
return {
'artist': match.group(1).strip(),
'title': match.group(2).strip()
}
return None
def _check_match(self, player_text: str, correct_text: str) -> bool:
"""Check if player text matches correct text"""
normalized_player = self._normalize_string(player_text)
normalized_correct = self._normalize_string(correct_text)
# Exact match
if normalized_player == normalized_correct:
return True
# Similarity match
similarity = difflib.SequenceMatcher(
None,
normalized_player,
normalized_correct
).ratio()
return similarity >= self.similarity_threshold
def _normalize_string(self, text: str) -> str:
"""Normalize string for comparison"""
text = text.lower()
# Remove common articles
articles = ['the', 'a', 'an']
words = text.split()
if words and words[0] in articles:
words = words[1:]
text = ' '.join(words)
text = re.sub(r'[^\w\s]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
class LLMService:
"""LLM service for game interaction"""
def __init__(self, config: Dict):
self.config = config
# Note: In a real implementation, you would initialize your LLM here
# For this example, we'll use simple text-based interaction
def announce(self, message: str):
"""Announce message to players"""
print(f"[GAME] {message}")
logger.info(f"Announcement: {message}")
def get_player_name(self, circuit_id: str) -> str:
"""Get player name through interaction"""
while True:
name = input(f"Player with circuit {circuit_id}, please enter your name: ").strip()
if name:
return name
print("Please enter a valid name.")
def get_genre_selection(self) -> str:
"""Get genre selection from user"""
genres = self.config.get('supported_genres', [])
print("Available genres:", ", ".join(genres))
while True:
genre = input("Select a genre: ").strip().lower()
if genre in genres:
return genre
print("Please select a valid genre from the list.")
def get_decade_selection(self) -> str:
"""Get decade selection from user"""
decades = self.config.get('supported_decades', [])
print("Available decades:", ", ".join(decades))
while True:
decade = input("Select a decade: ").strip()
if decade in decades:
return decade
print("Please select a valid decade from the list.")
def get_country_selection(self) -> Optional[str]:
"""Get optional country selection"""
countries = self.config.get('supported_countries', [])
print("Available countries (optional):", ", ".join(countries))
country = input("Select a country (or press Enter for any): ").strip().upper()
if not country:
return None
elif country in countries:
return country
else:
print("Invalid country, using 'any'")
return None
def get_player_answer(self, player_name: str) -> str:
"""Get player's answer"""
return input(f"{player_name}, enter your answer (Artist - Title): ").strip()
class GameEngine:
"""Main game engine"""
def __init__(self, config_path: str = "config.json"):
self.config_manager = ConfigManager(config_path)
self.config = self.config_manager.config
# Initialize components
self.spotify_client = SpotifyClient(
self.config['spotify']['client_id'],
self.config['spotify']['client_secret']
)
self.circuit_manager = CircuitManager(self.config['circuits'])
self.audio_manager = AudioManager(self.config['audio'])
self.answer_evaluator = AnswerEvaluator(
self.config['game']['similarity_threshold']
)
self.llm_service = LLMService(self.config)
# Game state
self.session = GameSession(
players={},
current_song=None,
played_songs=[],
genre="",
decade="",
country=None
)
# Threading
self.round_timer = None
self.button_lock = threading.Lock()
self.first_responder = None
# Set up button callback
self.circuit_manager.button_callback = self.handle_button_press
def start_game(self):
"""Start a new game"""
try:
logger.info("Starting Music Quiz Game")
# Initialize components
self.circuit_manager.start()
# Register players
self.register_players()
if not self.session.players:
self.llm_service.announce("No players registered. Exiting.")
return
# Setup game parameters
self.setup_game_parameters()
# Start gameplay
self.session.start_time = datetime.now()
self.begin_gameplay()
except KeyboardInterrupt:
self.llm_service.announce("Game interrupted by user")
except Exception as e:
logger.error(f"Game error: {e}")
self.llm_service.announce(f"Game error: {e}")
finally:
self.cleanup()
def register_players(self):
"""Register all players"""
self.session.state = GameState.PLAYER_REGISTRATION
self.llm_service.announce("Starting player registration...")
# Discover circuits
discovered_circuits = self.circuit_manager.discover_circuits()
if not discovered_circuits:
self.llm_service.announce("No circuits found. Please check connections.")
return
# Register each player
for circuit_id in discovered_circuits:
self.llm_service.announce(f"Registering player for circuit {circuit_id}")
# Flash LEDs to identify circuit
self.circuit_manager.set_green_led(circuit_id, True)
time.sleep(1)
self.circuit_manager.set_green_led(circuit_id, False)
# Get player name
player_name = self.llm_service.get_player_name(circuit_id)
# Create player
player = Player(circuit_id=circuit_id, name=player_name)
self.session.players[circuit_id] = player
self.llm_service.announce(f"Welcome {player_name}!")
self.llm_service.announce(f"Registered {len(self.session.players)} players")
def setup_game_parameters(self):
"""Setup game parameters"""
self.session.state = GameState.GAME_SETUP
self.session.genre = self.llm_service.get_genre_selection()
self.session.decade = self.llm_service.get_decade_selection()
self.session.country = self.llm_service.get_country_selection()
self.llm_service.announce(
f"Game setup complete: {self.session.genre} music from the "
f"{self.session.decade} {f'from {self.session.country}' if self.session.country else ''}"
)
def begin_gameplay(self):
"""Start main gameplay loop"""
self.llm_service.announce("Starting gameplay!")
while self.session.state != GameState.GAME_OVER:
self.start_new_round()
# Check for winner
winner = self.check_winner()
if winner:
self.end_game(winner)
break
def start_new_round(self):
"""Start a new round"""
self.session.state = GameState.ROUND_START
self.session.round_number += 1
self.llm_service.announce(f"Round {self.session.round_number}")
# Select song
song = self.spotify_client.select_random_song(
self.session.genre,
self.session.decade,
self.session.country
)
if not song:
self.llm_service.announce("No more songs available. Game ending.")
self.session.state = GameState.GAME_OVER
return
self.session.current_song = song
self.session.played_songs.append(song.id)
# Execute round
self.execute_round()
def execute_round(self):
"""Execute round logic"""
self.session.state = GameState.SONG_PLAYING
# Start red LEDs blinking
self.circuit_manager.start_red_blinking_all()
# Play song
self.audio_manager.play_song(self.session.current_song)
self.audio_manager.play_sound_effect('round_start')
# Reset first responder
self.first_responder = None
# Start timer
self.start_round_timer()
self.session.state = GameState.WAITING_FOR_ANSWER
self.llm_service.announce("Song is playing! Press your button to answer!")
# Wait for timeout or answer
while self.session.state == GameState.WAITING_FOR_ANSWER:
time.sleep(0.1)
def handle_button_press(self, circuit_id: str):
"""Handle button press from circuit"""
with self.button_lock:
if self.session.state != GameState.WAITING_FOR_ANSWER:
return
if self.first_responder is None:
self.first_responder = circuit_id
self.process_first_response(circuit_id)
def process_first_response(self, circuit_id: str):
"""Process the first button press"""
self.cancel_round_timer()
self.session.state = GameState.ANSWER_EVALUATION
# Update LED states
self.circuit_manager.set_green_led(circuit_id, True)
for other_id in self.session.players.keys():
if other_id != circuit_id:
self.circuit_manager.set_red_led(other_id, True)
# Get player answer
player = self.session.players[circuit_id]
self.llm_service.announce(f"{player.name} pressed first!")
self.audio_manager.play_sound_effect('button_press')
answer = self.llm_service.get_player_answer(player.name)
# Evaluate answer
is_correct = self.answer_evaluator.evaluate_answer(answer, self.session.current_song)
if is_correct:
player.score += self.config['game']['correct_answer_points']
self.llm_service.announce(f"Correct! {player.name} gains 20 points. Score: {player.score}")
self.audio_manager.play_sound_effect('correct')
else:
player.score += self.config['game']['incorrect_answer_points']
self.llm_service.announce(
f"Incorrect! {player.name} loses 10 points. Score: {player.score}. "
f"Correct answer: {self.session.current_song.artist} - {self.session.current_song.title}"
)
self.audio_manager.play_sound_effect('incorrect')
# Display current scores
self.display_scores()
# Reset for next round
self.circuit_manager.turn_off_all_leds()
self.audio_manager.stop_song()
self.session.state = GameState.ROUND_END
time.sleep(3) # Brief pause between rounds
def handle_round_timeout(self):
"""Handle round timeout"""
if self.session.state != GameState.WAITING_FOR_ANSWER:
return
self.session.state = GameState.ANSWER_EVALUATION
# Penalize all players
for player in self.session.players.values():
player.score += self.config['game']['timeout_penalty']
self.llm_service.announce(
f"Time's up! All players lose 5 points. "
f"Correct answer: {self.session.current_song.artist} - {self.session.current_song.title}"
)
self.display_scores()
# Reset for next round
self.circuit_manager.turn_off_all_leds()
self.audio_manager.stop_song()
self.session.state = GameState.ROUND_END
time.sleep(3)
def start_round_timer(self):
"""Start round timeout timer"""
timeout = self.config['game']['timeout_duration']
self.round_timer = threading.Timer(timeout, self.handle_round_timeout)
self.round_timer.start()
def cancel_round_timer(self):
"""Cancel round timer"""
if self.round_timer:
self.round_timer.cancel()
self.round_timer = None
def check_winner(self) -> Optional[Player]:
"""Check if any player has won"""
winning_score = self.config['game']['winning_score']
for player in self.session.players.values():
if player.score >= winning_score:
return player
return None
def end_game(self, winner: Player):
"""End the game"""
self.session.state = GameState.GAME_OVER
# Winner celebration
self.circuit_manager.start_green_blinking(winner.circuit_id)
for circuit_id in self.session.players.keys():
if circuit_id != winner.circuit_id:
self.circuit_manager.set_red_led(circuit_id, True)
self.llm_service.announce(f"GAME OVER! {winner.name} wins with {winner.score} points!")
self.audio_manager.play_sound_effect('game_over')
self.display_final_scores()
# Ask for new game
new_game = input("Start a new game? (y/n): ").strip().lower()
if new_game == 'y':
self.reset_game()
self.start_game()
def display_scores(self):
"""Display current scores"""
self.llm_service.announce("Current Scores:")
for player in sorted(self.session.players.values(), key=lambda p: p.score, reverse=True):
self.llm_service.announce(f" {player.name}: {player.score}")
def display_final_scores(self):
"""Display final scores"""
self.llm_service.announce("Final Scores:")
for i, player in enumerate(sorted(self.session.players.values(), key=lambda p: p.score, reverse=True), 1):
self.llm_service.announce(f" {i}. {player.name}: {player.score}")
def reset_game(self):
"""Reset game for new session"""
# Reset scores
for player in self.session.players.values():
player.score = 0
# Reset game state
self.session.current_song = None
self.session.played_songs = []
self.session.round_number = 0
self.session.state = GameState.INITIALIZATION
# Reset components
self.spotify_client.played_songs.clear()
self.circuit_manager.turn_off_all_leds()
self.audio_manager.stop_song()
def cleanup(self):
"""Cleanup resources"""
self.cancel_round_timer()
self.circuit_manager.stop()
self.audio_manager.stop_song()
# Save final game state
try:
with open('game_session.json', 'w') as f:
json.dump(self.session.to_dict(), f, indent=2)
except Exception as e:
logger.error(f"Failed to save game session: {e}")
def main():
"""Main entry point"""
print("Music Quiz Game - LLM Powered Multi-Player Edition")
print("=" * 50)
# Check for required configuration
config_manager = ConfigManager()
if not config_manager.config['spotify']['client_id']:
print("Error: Spotify client ID not configured")
print("Please edit config.json and add your Spotify credentials")
return
# Start game
game_engine = GameEngine()
game_engine.start_game()
if __name__ == "__main__":
main()
This complete implementation provides a production-ready music quiz game system that integrates all the components described in the article. The code includes comprehensive error handling, logging, configuration management, and modular design principles that make it suitable for real-world deployment.
The system supports both physical Arduino circuits and keyboard-based input, making it flexible for different deployment scenarios. The LLM integration provides natural language interaction capabilities, while the Spotify API integration ensures access to a vast music library for diverse gaming experiences.
The implementation follows clean architecture principles with clear separation of concerns, making it maintainable and extensible for future enhancements such as additional music services, advanced LLM models, or enhanced player interface designs.
No comments:
Post a Comment