Skip to content

Latest commit

 

History

History
606 lines (514 loc) · 22.5 KB

File metadata and controls

606 lines (514 loc) · 22.5 KB

Wordle Battle Game Functionality Specification

Overview

A minimalistic real-time competitive Wordle game built with Phoenix LiveView, utilizing PubSub and GenServers for state management with session persistence, player reconnection, and automated cleanup. Players compete to guess the most 5-letter words within a timed session.

Core Features

1. Session Management

  • Create Session: Simple button to create new game session with configurable settings
  • Unique Links: Each session gets a unique hash-based URL (/:session_id)
  • Session Duration: Configurable time limits (1, 3, 5 minutes)
  • Attempts Per Word: Fixed at 6 attempts per word (classic Wordle rules)
  • No Authentication: No logins, registrations, or persistent user accounts
  • Session Persistence: Sessions survive server restarts and player disconnections
  • Automatic Cleanup: Sessions automatically cleaned after 1 hour of inactivity

2. Player Connection & Persistence

  • Unique User IDs: Each player gets a persistent UUID stored in localStorage
  • Automatic Reconnection: Players automatically rejoin their session on page refresh
  • Nickname Reclamation: Disconnected players' nicknames can be reclaimed after timeout
  • Connection Status: Players marked as connected/disconnected with timestamps
  • Graceful Degradation: Disconnected players' progress preserved; game continues for others

3. Lobby System

  • Join via Link: Users join by visiting the unique session URL
  • Nickname Entry: Each user can set their display name (must be unique within session)
  • Individual Play: Each player competes individually (no teams)
  • Ready State: All connected players must press "Ready" to start game
  • Flexible Start: Can start with 1+ players (solo practice supported)
  • Settings Display: Show session duration in lobby

4. Game Flow

  • Session-based Gameplay: All players compete simultaneously during timed session
  • Timed Session: Overall timer counting down (1/3/5 minutes) with real-time display
  • Timer Audio: Sound notification when session time expires (/sounds/timer_end.mp3)
  • Available Words Pool: Full list of valid 5-letter answer words loaded at app startup
  • Individual Word Assignment: Each player gets random word from pool (tracked to avoid repeats within session)
  • Classic Wordle Mechanics:
    • 5-letter word guess input
    • 6 attempts per word
    • Color feedback: 🟩 Green (correct position), 🟨 Yellow (wrong position), ⬜ Gray (not in word)
    • Virtual keyboard with color-coded used letters
  • Word Progression: Upon guessing word correctly or exhausting 6 attempts, player immediately gets new random word from remaining pool
  • Real-time Leaderboard: Live updates of player scores visible to all
  • Auto Session End: Session automatically ends when timer reaches zero

5. Scoring System

  • Base Score: 1 point per word guessed correctly
  • Attempt Bonuses:
    • Guessed in 1 attempt: +5 bonus points (total: 6 points)
    • Guessed in 2 attempts: +3 bonus points (total: 4 points)
    • Guessed in 3 attempts: +2 bonus points (total: 3 points)
    • Guessed in 4-6 attempts: +1 bonus point (total: 2 points)
  • Failed Word: 0 points (exhausted 6 attempts without guessing)
  • Speed Tracking: Average attempts per word displayed
  • Total Words: Count of words attempted and words guessed

6. Victory & Game End

  • Score Tracking: Real-time score updates during session
  • Victory Condition: Player with highest score when timer expires wins
  • Tie Breaking: If tied on score, player with lower average attempts wins
  • Game Over Screen:
    • Final scores with winner highlighting
    • Individual statistics: words guessed, total attempts, average attempts
    • Full word history for each player
  • New Game Option: Reset to lobby while preserving players

7. Technical Architecture

  • LiveView + GenServer: Game logic in WordleServer, UI in WordleLive
  • Dynamic Supervision: Each session runs as supervised GenServer process
  • Registry-based Lookup: Sessions registered for efficient process discovery
  • PubSub Integration: Real-time updates to all session participants
  • Cleanup Workers: Automated disconnected player and session cleanup
  • State Persistence: Game state survives player disconnections and reconnections
  • Word Management: Simple in-memory list tracking with per-session used words

State Machine

Game Phases

  1. :lobby - Players joining, setting ready status, viewing settings
  2. :playing - Active session with word guessing and timer
  3. :game_over - Final scores display and restart options

State Transitions

:lobby -> :playing
  Conditions:
    - At least 1 player
    - All players ready
  Trigger: start_game action
  Effects:
    - Start session timer
    - Assign first random word to each player
    - Track assigned words in used_words list
    - Enable word input for all players

:playing -> :game_over
  Conditions:
    - Session timer expires (reaches 0:00)
  Trigger: Automatic (timer expiration)
  Effects:
    - Lock all player inputs
    - Calculate final scores and rankings
    - Determine winner (highest score, or lowest avg attempts if tied)
    - Display game over screen

:game_over -> :lobby
  Trigger: start_new_game action
  Effects:
    - Complete game reset
    - Preserve players and nicknames
    - Reset all scores to 0
    - Clear used_words list
    - Reset all players to not ready

Player State Transitions

nil -> Player{connected: true, ready: false, score: 0}
  Trigger: join_session with valid nickname

Player{connected: true} -> Player{connected: false, disconnected_at: timestamp}
  Trigger: leave_session or browser disconnect

Player{connected: false} -> Player{connected: true, disconnected_at: nil}
  Trigger: rejoin via mark_connected or restore_session

Player{ready: false} -> Player{ready: true}
  Trigger: set_ready(true)

Player{ready: true} -> Player{ready: false}
  Trigger: set_ready(false) or session transition to :lobby

Player{current_word: "CRANE"} -> Player{current_word: "SLATE"}
  Trigger: Word guessed correctly OR 6 attempts exhausted
  Effects:
    - Add score for previous word
    - Clear current attempts
    - Pick new random word from available pool (excluding used_words)
    - Add new word to session's used_words list

Rule System

Session Rules

  • Minimum Players: 1+ players (solo practice mode supported)
  • Duration Options: 1, 3, or 5 minutes
  • Word Pool: All valid answer words available, randomly selected per player
  • Word Tracking: Session tracks all assigned words to prevent repeats
  • Independent Words: Each player can be on different word at any time

Player Rules

  • Nickname Uniqueness: Within session scope only
  • Ready Requirement: Must set ready status to enable game start
  • Reconnection Grace: 10-minute window for automatic reconnection
  • Progress Preservation: Current word, attempts, and score maintained during disconnect

Game Start Rules

  • Ready Check: All connected players must be ready
  • Minimum Players: 1+ players
  • Word Assignment: Each player gets random word from available pool on start

Gameplay Rules

  • Word Length: Fixed at 5 letters
  • Attempts Per Word: Fixed at 6 attempts
  • Input Validation:
    • Must be exactly 5 letters
    • Must be valid English word (any valid 5-letter word accepted)
    • Case insensitive (normalized to uppercase)
  • Letter Feedback Rules:
    • 🟩 Green: Letter in correct position
    • 🟨 Yellow: Letter in word but wrong position
    • ⬜ Gray: Letter not in word
    • Duplicate letter handling: If word has 1 'E' and guess has 2 'E's, only first instance gets color
  • Keyboard Tracking: Used letters colored on virtual keyboard (green/yellow/gray)
  • Word Progression: Automatic - player gets new random word immediately after completing current one
  • No Skipping: Players cannot skip words; must either guess or exhaust attempts
  • Timer Enforcement: When session timer hits 0:00, all inputs immediately locked

Scoring Rules

  • Success Bonus Scale:
    • 1 attempt: 6 points (1 base + 5 bonus)
    • 2 attempts: 4 points (1 base + 3 bonus)
    • 3 attempts: 3 points (1 base + 2 bonus)
    • 4-6 attempts: 2 points (1 base + 1 bonus)
  • Failure Penalty: 0 points for unguessed words
  • Score Accumulation: Total score is sum of all word scores
  • Statistics Tracking:
    • Total words attempted
    • Total words guessed correctly
    • Total attempts made
    • Average attempts per word (for guessed words only)
  • Winner Determination:
    • Primary: Highest total score
    • Tiebreaker: Lowest average attempts per guessed word
    • Secondary tiebreaker: Fewest total attempts

Cleanup Rules

  • Player Cleanup: Disconnected players removed after 10 minutes
  • Session Cleanup: Entire session terminated after 1 hour of inactivity
  • Activity Tracking: Any player action updates session activity timestamp
  • Cleanup Frequency: Automated cleanup runs every 5 minutes

Data Structure

%WordleState{
  session_id: string(),
  session_duration: integer(), # minutes: 1, 3, or 5
  phase: :lobby | :playing | :game_over,

  players: %{user_id => %{
    nickname: string(),
    ready: boolean(),
    connected: boolean(),
    disconnected_at: DateTime.t() | nil,

    # Current game progress
    current_word: string() | nil, # The word player is currently guessing
    current_attempts: [%{
      guess: string(),
      result: [:correct | :present | :wrong, ...] # 5-element list
    }],
    words_completed: integer(), # Count of words attempted (guessed or failed)
    words_guessed: integer(), # Count successfully guessed
    total_attempts: integer(), # Sum of all attempts across all words
    score: integer(), # Total score

    # Per-word history for final display
    word_history: [%{
      word: string(),
      guessed: boolean(),
      attempts: integer(),
      points: integer()
    }]
  }},

  # Word management
  used_words: [string()], # List of all words assigned during this session

  # Session timing
  session_start_time: DateTime.t() | nil,
  session_end_time: DateTime.t() | nil, # start_time + duration
  timer_ref: reference() | nil,
  time_remaining: integer() | nil, # Seconds

  # Game metadata
  game_ever_started: boolean(),
  cleanup_timer_ref: reference(),
  created_at: DateTime.t(),
  last_activity: DateTime.t(),

  # Winner tracking
  winner_id: user_id | nil,
  final_rankings: [%{user_id: string(), nickname: string(), score: integer(), avg_attempts: float()}]
}

Word Management

Dictionary Structure

Store words as plain Elixir lists loaded at application startup:

# In application.ex or dedicated module
defmodule WordleBattle.Dictionary do
  @answer_words_file "priv/dictionary/answer_words.txt"
  @valid_guesses_file "priv/dictionary/valid_guesses.txt"

  def answer_words do
    # Lazy load and cache in module attribute
    @answer_words_file
    |> File.read!()
    |> String.split("\n", trim: true)
    |> Enum.map(&String.upcase/1)
  end

  def valid_guesses do
    # All valid 5-letter words for validation
    @valid_guesses_file
    |> File.read!()
    |> String.split("\n", trim: true)
    |> Enum.map(&String.upcase/1)
    |> MapSet.new()
  end

  def random_word(exclude_list \\ []) do
    answer_words()
    |> Enum.reject(&(&1 in exclude_list))
    |> Enum.random()
  end

  def valid_guess?(word) do
    MapSet.member?(valid_guesses(), String.upcase(word))
  end
end

Word Assignment Logic

# In WordleServer
def assign_new_word(state, player_id) do
  new_word = Dictionary.random_word(state.used_words)

  updated_state =
    state
    |> put_in([:players, player_id, :current_word], new_word)
    |> put_in([:players, player_id, :current_attempts], [])
    |> update_in([:used_words], &[new_word | &1])

  {new_word, updated_state}
end

Dictionary Files

Answer Words (priv/dictionary/answer_words.txt):

  • ~2,300 common, guessable 5-letter words
  • One word per line, uppercase
  • Examples: CRANE, SLATE, AUDIO, PRIDE, HOUSE, TRACK, FRAME, CHESS, BLEND, GHOST, CRISP, HUMOR, LIGHT, TRADE, SMART

Valid Guesses (priv/dictionary/valid_guesses.txt):

  • ~12,000 valid 5-letter English words (includes all answer words)
  • Allows obscure but valid words like ZAXES, QAJAQ
  • Used only for validation, not for assignment

Guess Validation Algorithm

def check_guess(guess, target) do
  guess = String.upcase(guess)
  target = String.upcase(target)

  guess_letters = String.graphemes(guess)
  target_letters = String.graphemes(target)

  # First pass: mark exact matches
  exact_matches = Enum.zip(guess_letters, target_letters)
  |> Enum.map(fn {g, t} -> if g == t, do: :correct, else: nil end)

  # Second pass: mark present letters (avoiding double-counting)
  target_remaining = target_letters
  |> Enum.zip(exact_matches)
  |> Enum.reject(fn {_, match} -> match == :correct end)
  |> Enum.map(fn {letter, _} -> letter end)

  Enum.zip(guess_letters, exact_matches)
  |> Enum.map_reduce(target_remaining, fn {g, match}, remaining ->
    case match do
      :correct ->
        {:correct, remaining}
      nil ->
        if g in remaining do
          {:present, List.delete(remaining, g)}
        else
          {:wrong, remaining}
        end
    end
  end)
  |> elem(0)
end

Scoring Calculation

def calculate_word_score(attempts_used, guessed_correctly) do
  if guessed_correctly do
    case attempts_used do
      1 -> 6  # 1 base + 5 bonus
      2 -> 4  # 1 base + 3 bonus
      3 -> 3  # 1 base + 2 bonus
      _ -> 2  # 1 base + 1 bonus (4-6 attempts)
    end
  else
    0
  end
end

User Interface

Lobby Screen

  • Header: "Wordle Battle" title
  • Session Info:
    • Session link display with copy button
    • Settings summary: Duration (1/3/5 min)
  • Player List: Connected players with ready indicators (✓ checkmark or ⏳ waiting)
  • Controls:
    • Ready/Not Ready toggle button (large, prominent)
    • Start Game button (enabled when all ready, visible to all)

Playing Screen Layout

┌────────────────────────────────────────────────┐
│  WORDLE BATTLE        Time: 02:47              │
├────────────────────────────────────────────────┤
│                                                │
│  Current Word (Attempt 3/6):                   │
│                                                │
│  [C][R][A][N][E]  🟩⬜🟨⬜⬜                    │
│  [S][L][A][T][E]  ⬜⬜🟩⬜🟩                    │
│  [_][_][_][_][_]  ← Current input             │
│  [_][_][_][_][_]                              │
│  [_][_][_][_][_]                              │
│  [_][_][_][_][_]                              │
│                                                │
│  [Virtual Keyboard - color coded letters]      │
│                                                │
├────────────────────────────────────────────────┤
│  📊 LEADERBOARD                                │
│  🥇 Player2    5 words (18 pts)   avg: 3.4    │
│  🥈 You        3 words (10 pts)   avg: 3.0    │
│  🥉 Player3    2 words (6 pts)    avg: 4.0    │
└────────────────────────────────────────────────┘

Game Screen Components

Main Game Area:

  • Timer Display: Large, prominent countdown with color coding:
    • Green: >60 seconds remaining
    • Yellow: 30-60 seconds
    • Red: <30 seconds (with pulsing animation)
  • Word Grid: 6 rows × 5 columns for attempts
    • Completed attempts show letters with color feedback
    • Current attempt shows live typing
    • Unused rows show empty boxes
  • Input Field: 5-letter input with live validation
    • Auto-uppercase transformation
    • Visual feedback: green border if valid word, red if invalid
    • Submit on Enter key or button click
  • Virtual Keyboard:
    • QWERTY layout with letter buttons
    • Color coding: Green (correct position used), Yellow (wrong position used), Gray (not in word), White (unused)
    • Backspace and Enter buttons

Leaderboard Sidebar (Collapsible on mobile):

  • Real-time rankings
  • Player nickname
  • Words guessed count
  • Total score
  • Average attempts (for completed words)
  • Visual indicators for 1st/2nd/3rd place

Status Messages:

  • "Word not in dictionary" (when invalid guess)
  • "🎉 Correct! +X points" (when word guessed, shows bonus)
  • "❌ Word not guessed. Moving to next word." (after 6 failed attempts)
  • "⚡ Player X just guessed a word in 2 attempts!" (activity feed, optional)

Game Over Screen

┌────────────────────────────────────────────────┐
│  🏆 GAME OVER                                  │
│                                                │
│  🥇 WINNER: Player2                            │
│     Score: 24 points                           │
│     Words: 8 guessed                           │
│     Avg Attempts: 3.25                         │
│                                                │
├────────────────────────────────────────────────┤
│  Final Rankings:                               │
│                                                │
│  🥇 Player2  -  24 pts  (8 words, 3.25 avg)   │
│  🥈 You      -  18 pts  (6 words, 3.50 avg)   │
│  🥉 Player3  -  12 pts  (4 words, 4.25 avg)   │
│                                                │
├────────────────────────────────────────────────┤
│  Your Game Stats:                              │
│  ├─ Words Attempted: 7                         │
│  ├─ Words Guessed: 6                           │
│  ├─ Success Rate: 85.7%                        │
│  ├─ Total Attempts: 21                         │
│  └─ Average Attempts: 3.50                     │
│                                                │
│  [View Word History] [Start New Game] [Home]   │
└────────────────────────────────────────────────┘

Word History Modal (Optional):

  • Expandable list of all words attempted
  • Per word: Target word, attempts used, guessed/failed, points earned
  • Filter by player

Technical Requirements

  • Phoenix LiveView: Real-time UI with minimal JavaScript
  • GenServer Supervision: Fault-tolerant game state management
  • PubSub Broadcasting: Session-wide real-time updates
  • Registry: Efficient session process lookup
  • DaisyUI + Tailwind: Modern component-based styling
  • Heroicons: Consistent iconography
  • LocalStorage Integration: Client-side persistence via JS hooks for user_id
  • Audio Support: Timer notification sounds with autoplay
  • Dictionary Files: Text files with word lists loaded at startup
  • Keyboard Event Handling: JavaScript hooks for physical keyboard input

Implementation Notes

Word Dictionary Loading

Load dictionaries at application startup in application.ex:

defmodule WordleBattle.Application do
  use Application

  def start(_type, _args) do
    # Preload dictionaries into memory
    _ = WordleBattle.Dictionary.answer_words()
    _ = WordleBattle.Dictionary.valid_guesses()

    children = [
      # ... other children
    ]

    opts = [strategy: :one_for_one, name: WordleBattle.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Word Selection Strategy

# Pick random word avoiding already used words in session
def get_next_word(used_words) do
  case Dictionary.answer_words() -- used_words do
    [] ->
      # All words used, start over (rare in normal gameplay)
      Dictionary.random_word()
    available_words ->
      Enum.random(available_words)
  end
end

Memory Usage

  • Answer words list: ~2,300 words × ~5 bytes = ~12KB
  • Valid guesses MapSet: ~12,000 words × ~10 bytes = ~120KB
  • Per session tracking: ~50 words × 5 bytes = ~250 bytes per session
  • Total: ~132KB baseline + minimal per-session overhead

Duplicate Letter Handling

Ensure color feedback correctly handles duplicate letters using the two-pass algorithm shown above.

Real-time Timer

Use GenServer with :timer.send_interval/2 to broadcast time updates every second:

def handle_info(:tick, state) do
  time_remaining = state.time_remaining - 1

  if time_remaining <= 0 do
    # End game
    {:noreply, end_game(state)}
  else
    # Broadcast time update
    Phoenix.PubSub.broadcast(
      WordleBattle.PubSub,
      "session:#{state.session_id}",
      {:time_update, time_remaining}
    )

    {:noreply, %{state | time_remaining: time_remaining}}
  end
end

Keyboard State Management

Track used letters per player and broadcast updates after each guess. Virtual keyboard component updates colors based on player's letter usage history.

Reconnection Handling

When player reconnects during :playing:

  • Restore current word and all attempts
  • Restore score and statistics
  • Resume from current state
  • Sync timer display

Mobile Optimization

  • Virtual keyboard essential for mobile (touch-friendly buttons)
  • Collapsible leaderboard sidebar to maximize game area
  • Larger touch targets for buttons (minimum 44×44px)
  • Responsive grid layout

Accessibility

  • ARIA labels for all interactive elements
  • Keyboard navigation support (Tab, Arrow keys, Enter, Backspace)
  • Color-blind friendly mode option (patterns/symbols in addition to colors)
  • Screen reader announcements for timer, score changes, game phase transitions

Performance Optimization

  • Word lists loaded once at startup (no per-request loading)
  • Random selection using Enum.random/1 is O(n) but with small n (~2300)
  • Consider MapSet for used_words if sessions last very long (>100 words used)
  • Throttle PubSub broadcasts for high-frequency updates

Security Considerations

  • Rate limiting on guess submissions (prevent spam/cheating)
  • Word assignment server-side only (prevent client inspection)
  • Validate all player actions server-side (no trust in client)
  • Current word never sent to non-owning players (prevent cheating)