Skip to content

Guess artist functionality #1

@mholzi

Description

@mholzi

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

  1. During a round, after the song starts playing, an Artist Challenge appears
  2. Players see 3 artist options (1 correct + 2 decoys)
  3. First player to select the correct artist earns 10 bonus points
  4. Challenge runs parallel to year guessing (doesn't replace it)
  5. 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:

  1. Same-era artists: Prefer artists active in the same decade
  2. Same-genre artists: If genre metadata available, match genre
  3. Playlist diversity: Pull from other songs in the same playlist
  4. Avoid obvious wrong answers: Don't show "Mozart" for a 1980s pop song
  5. 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

  • Artist challenge appears during round with 3 options (1 correct, 2 decoys)
  • Only first correct guess earns the 10-point bonus
  • Wrong guesses have no penalty
  • Challenge result shown to all players in real-time
  • Artist bonus included in score breakdown during reveal

UI/UX

  • Artist options are clearly tappable buttons
  • Visual feedback on selection (correct = green, wrong = dim)
  • Winner announcement shown to all players
  • Artist challenge doesn't block year guessing UI

Configuration

  • Admin can enable/disable artist challenge per game
  • Works correctly when disabled (no challenge shown)

Edge Cases

  • Handle missing artist metadata gracefully (skip challenge for that round)
  • Handle playlist with few unique artists (reduce decoy count if needed)
  • Simultaneous correct guesses: first by timestamp wins

Testing

  • Unit tests for decoy generation
  • Unit tests for artist guess scoring
  • Integration tests for WebSocket artist_guess message
  • E2E test for full artist challenge flow

Translations

  • English translations complete
  • German translations complete

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions