Summary
Add an optional "Guess the Artist" challenge during gameplay where players can earn bonus points by identifying the correct artist from a list of three candidates within the 30-second round.
Game Mechanic
How It Works
- During a round, after the song starts playing, an Artist Challenge appears
- Players see 3 artist options (1 correct + 2 decoys)
- First player to select the correct artist earns 10 bonus points
- Challenge runs parallel to year guessing (doesn't replace it)
- Players can still earn full year-guessing points regardless of artist guess
Strategic Elements
- Speed matters: Only first correct answer wins the bonus
- Risk-free: Wrong artist guess has no penalty
- Additive: Bonus points stack with year accuracy + speed + streak bonuses
- Optional engagement: Players can ignore artist challenge and focus on year
Implementation Details
Backend Changes
-
const.py - Add artist challenge constants:
ARTIST_CHALLENGE_ENABLED = True
ARTIST_BONUS_POINTS = 10
ARTIST_DECOY_COUNT = 2 # Number of wrong options shown
-
game/state.py - Extend round state:
@dataclass
class RoundState:
# ... existing fields ...
artist_challenge: ArtistChallenge | None = None
artist_winner: str | None = None # First correct guesser
@dataclass
class ArtistChallenge:
correct_artist: str
options: list[str] # Shuffled: correct + decoys
winner: str | None = None
winner_time: float | None = None
-
game/state.py - Add artist guess handler:
def submit_artist_guess(self, player_name: str, artist: str) -> tuple[bool, int]:
"""
Process artist guess. Returns (is_correct, points_earned).
Only first correct guess wins points.
"""
if self.artist_challenge.winner is not None:
return (False, 0) # Someone already won
if artist == self.artist_challenge.correct_artist:
self.artist_challenge.winner = player_name
self.artist_challenge.winner_time = time.time()
return (True, ARTIST_BONUS_POINTS)
return (False, 0)
-
game/artist_decoys.py - New module for generating plausible decoys:
class ArtistDecoyGenerator:
"""Generate plausible wrong artist options."""
def __init__(self, playlist_artists: list[str]):
self.artist_pool = playlist_artists
def get_decoys(self, correct_artist: str, count: int = 2) -> list[str]:
"""Return similar-era artists as decoys."""
# Filter out correct artist
# Prefer artists from same decade/genre if metadata available
# Fallback to random selection from playlist
-
game/scoring.py - Include artist bonus in score calculation:
def calculate_round_score(
self,
year_guess: int,
correct_year: int,
submission_time: float,
artist_bonus: int = 0, # New parameter
# ... other params
) -> RoundScore:
base = self.calculate_base_points(year_guess, correct_year)
speed = self.calculate_speed_multiplier(submission_time)
# ... existing logic ...
return RoundScore(
base_points=base,
speed_multiplier=speed,
artist_bonus=artist_bonus, # Add to total
total=int(base * speed) + streak_bonus + artist_bonus
)
-
playlists/*.json - Ensure artist field exists (already present):
{
"year": 1975,
"artist": "Queen",
"uri": "spotify:track:xxx",
"fun_fact": "..."
}
WebSocket Protocol
New client → server message:
{
"type": "artist_guess",
"artist": "Queen"
}
Server → client acknowledgment:
{
"type": "artist_guess_ack",
"correct": true,
"points": 10,
"message": "You got it first!"
}
State broadcast includes:
{
"artist_challenge": {
"options": ["Queen", "Led Zeppelin", "Pink Floyd"],
"winner": "Alice",
"winner_time": 1705123456789
}
}
Frontend Changes
-
player.js - Add artist challenge UI:
function renderArtistChallenge(challenge) {
if (challenge.winner) {
showArtistWinner(challenge.winner);
return;
}
const options = challenge.options.map(artist =>
`<button class="artist-option" onclick="submitArtistGuess('${artist}')">${artist}</button>`
);
artistChallengeEl.innerHTML = `
<div class="artist-challenge">
<h3>${i18n.t('artist.who_sings')}</h3>
<div class="artist-options">${options.join('')}</div>
</div>
`;
}
-
css/styles.css - Artist challenge styling:
.artist-challenge {
background: var(--surface-elevated);
border-radius: 12px;
padding: 16px;
margin: 16px 0;
}
.artist-option {
display: block;
width: 100%;
padding: 12px;
margin: 8px 0;
background: var(--primary-dim);
border: 2px solid var(--primary);
border-radius: 8px;
color: var(--text-primary);
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.artist-option:hover {
background: var(--primary);
transform: scale(1.02);
}
.artist-option.correct {
background: var(--success);
border-color: var(--success);
}
.artist-option.wrong {
opacity: 0.5;
}
.artist-winner {
text-align: center;
color: var(--success);
font-weight: bold;
}
-
i18n/en.json - English translations:
"artist": {
"who_sings": "Who sings this song?",
"you_got_it": "You got it! +10 points",
"too_slow": "{winner} got it first!",
"wrong": "Not quite...",
"bonus": "+{points} Artist Bonus"
}
-
i18n/de.json - German translations:
"artist": {
"who_sings": "Wer singt diesen Song?",
"you_got_it": "Richtig! +10 Punkte",
"too_slow": "{winner} war schneller!",
"wrong": "Leider falsch...",
"bonus": "+{points} Künstler-Bonus"
}
Admin Configuration (Optional)
Allow admin to enable/disable artist challenge per game:
┌─────────────────────────────────┐
│ Game Options │
│ ☑ Enable Artist Challenge │
│ First correct guess: +10 pts │
└─────────────────────────────────┘
UI Mockup
During Round (Artist Challenge Active)
┌─────────────────────────────────────────┐
│ ♪ Playing... │
│ 0:18 │
├─────────────────────────────────────────┤
│ │
│ 🎤 Who sings this song? │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Queen │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Led Zeppelin │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ Pink Floyd │ │
│ └─────────────────────────────────┘ │
│ │
├─────────────────────────────────────────┤
│ │
│ What year was this released? │
│ │
│ ◄━━━━━━━━━━━●━━━━━━━━━━━► │
│ 1975 │
│ │
│ [ SUBMIT GUESS ] │
│ │
└─────────────────────────────────────────┘
After Someone Wins Artist Challenge
┌─────────────────────────────────────────┐
│ 🎤 Who sings this song? │
│ │
│ ┌─────────────────────────────────┐ │
│ │ ✓ Queen │ ←── Highlighted green
│ └─────────────────────────────────┘ │
│ │
│ 🏆 Alice got it first! (+10 pts) │
│ │
└─────────────────────────────────────────┘
Reveal Phase (Score Breakdown)
┌─────────────────────────────────────────┐
│ 🎵 Bohemian Rhapsody │
│ 👤 Queen • 1975 │
├─────────────────────────────────────────┤
│ │
│ Your guess: 1976 │
│ │
│ Points earned: │
│ • Year accuracy: +5 │
│ • Speed bonus: ×1.4 │
│ • Artist bonus: +10 ← NEW │
│ ───────────────────────── │
│ Round total: +17 │
│ │
└─────────────────────────────────────────┘
Decoy Generation Strategy
To make the challenge interesting but fair:
- Same-era artists: Prefer artists active in the same decade
- Same-genre artists: If genre metadata available, match genre
- Playlist diversity: Pull from other songs in the same playlist
- Avoid obvious wrong answers: Don't show "Mozart" for a 1980s pop song
- Fallback: Random selection from artist pool if constraints can't be met
def get_decoys(correct_artist: str, song_year: int, playlist: Playlist) -> list[str]:
candidates = []
# Prefer same-decade artists from playlist
decade = (song_year // 10) * 10
for song in playlist.songs:
if song.artist != correct_artist:
song_decade = (song.year // 10) * 10
if song_decade == decade:
candidates.append(song.artist)
# Deduplicate and shuffle
candidates = list(set(candidates))
random.shuffle(candidates)
# Return top 2 (or fill with random if not enough)
return candidates[:2]
Acceptance Criteria
Core Functionality
UI/UX
Configuration
Edge Cases
Testing
Translations
Future Enhancements (Out of Scope)
- Artist challenge difficulty modes (more decoys)
- Genre-based decoy matching with external API
- "Artist Streak" bonus for consecutive correct artist guesses
- Artist leaderboard separate from year-guessing leaderboard
Summary
Add an optional "Guess the Artist" challenge during gameplay where players can earn bonus points by identifying the correct artist from a list of three candidates within the 30-second round.
Game Mechanic
How It Works
Strategic Elements
Implementation Details
Backend Changes
const.py- Add artist challenge constants:game/state.py- Extend round state:game/state.py- Add artist guess handler:game/artist_decoys.py- New module for generating plausible decoys:game/scoring.py- Include artist bonus in score calculation:playlists/*.json- Ensure artist field exists (already present):{ "year": 1975, "artist": "Queen", "uri": "spotify:track:xxx", "fun_fact": "..." }WebSocket Protocol
New client → server message:
{ "type": "artist_guess", "artist": "Queen" }Server → client acknowledgment:
{ "type": "artist_guess_ack", "correct": true, "points": 10, "message": "You got it first!" }State broadcast includes:
{ "artist_challenge": { "options": ["Queen", "Led Zeppelin", "Pink Floyd"], "winner": "Alice", "winner_time": 1705123456789 } }Frontend Changes
player.js- Add artist challenge UI:css/styles.css- Artist challenge styling:i18n/en.json- English translations:i18n/de.json- German translations:Admin Configuration (Optional)
Allow admin to enable/disable artist challenge per game:
UI Mockup
During Round (Artist Challenge Active)
After Someone Wins Artist Challenge
Reveal Phase (Score Breakdown)
Decoy Generation Strategy
To make the challenge interesting but fair:
Acceptance Criteria
Core Functionality
UI/UX
Configuration
Edge Cases
Testing
Translations
Future Enhancements (Out of Scope)