Skip to content

feat: channel points redemption triggers semantic hint reveal #78

Description

@FlorentPoinsaut

User Story

As a viewer, I want to spend channel points to receive a semantic hint — a word semantically close to the secret word — so that I can progress in the game when I am stuck, without the hint being free or spammable.

As a streamer, I want to configure a single Twitch custom reward and have the bot handle all logic automatically, so that I can enable hints during a live stream with minimal setup and no risk of breaking the game mid-broadcast.


Description

Streamantix currently has no channel points integration. This feature adds a hint system driven by Twitch Channel Points redemptions:

  • The streamer creates a custom reward in the Twitch dashboard (e.g. Streamantix Hint) and sets HINT_REWARD_NAME in .env
  • When a viewer redeems the reward, the bot reveals the next hint from a pre-computed pool of Word2Vec neighbors of the target word
  • Hints are revealed from farthest to closest (hint 1 = moderately related, hint 5 = very close), building progressive tension
  • Maximum HINTS_PER_GAME (default: 5) hints per game; redemptions beyond the limit are automatically cancelled/refunded

Hint reveal behaviour

  • Pool size: top-50 Word2Vec neighbors (HINT_POOL_SIZE=50), reversed (farthest first)
  • Words already guessed by players are silently skipped; the next unguessed word in the pool is used
  • Redemption is auto-cancelled (points refunded) if: no game is running, limit is reached, or pool is exhausted
  • Chat message: @user A hint: "lumière" (hint 1/5)
  • All revealed hints are displayed persistently in the overlay (not just the last one)

New configuration variables

Variable Default Description
HINT_REWARD_NAME `` (disabled) Exact name of the Twitch custom reward to listen for
HINTS_PER_GAME 5 Maximum hints per game
HINT_POOL_SIZE 50 Number of Word2Vec neighbors pre-computed at game start

Acceptance Criteria

  • Viewer redeems the configured reward → bot posts hint in chat with index (e.g. hint 1/5)
  • Hints are revealed farthest-to-closest (hint 1 least useful, hint 5 most useful)
  • Already-guessed words are skipped silently; the next pool word is used
  • Redemption is auto-cancelled (points refunded) when no game is running or limit is reached
  • All revealed hints are shown in the overlay (persistent list, not just last)
  • HINT_REWARD_NAME comparison is case-insensitive and whitespace-trimmed
  • If HINT_REWARD_NAME is empty at startup, EventSub subscription is skipped with a warning log
  • Startup validates that the stored OAuth token includes channel:read:redemptions and channel:manage:redemptions; forces re-login if scopes are missing

Technical Specification

Architecture notes (from [Tech] Architect review)

EventSub WebSocket — not PubSub. Twitch deprecated PubSub; use twitchio.ext.eventsub with WebSocket transport. Same user token, no additional infrastructure.

Scorer protocol boundary. GameState must not call the engine directly. bot.py pre-computes the hint pool via engine.get_hints(target), reverses it, and passes it to start_new_game(hint_pool=...). GameState receives plain list[str] data.

Discriminated hint result. next_hint() returns a (HintResult, str | None) tuple where HintResult is an enum: OK, LIMIT_REACHED, POOL_EXHAUSTED. The bot posts a different chat message for each case.

Async safety. next_hint() must stay synchronous (no await); asyncio cooperative scheduling makes synchronous GameState mutations atomic.

Channel ID resolution. Use twitchio.Client.fetch_users() (async Helix) to resolve channel_id at startup — not a blocking urllib call.

Implementation phases

Phase 1 — Semantic engine

  • game/engine.py: add SemanticEngine.get_hints(word: str, n: int = 50) -> list[str]
    • Calls model.most_similar(vocab_key, topn=n + buffer)
    • Filters results through _cleaned_key_map
    • Returns up to n cleaned words

Phase 2 — Game state

  • game/state.py:
    • New HintResult enum: OK / LIMIT_REACHED / POOL_EXHAUSTED
    • New private fields: _hint_pool: list[str], _hints_revealed: list[str]
    • Updated signature: start_new_game(target, difficulty, hint_pool: list[str] | None = None)
    • New method: next_hint() -> tuple[HintResult, str | None]
    • New property: hints_revealed: list[str]
    • start_new_game() resets all hint state

Phase 3 — Twitch integration

  • config.py: add HINT_REWARD_NAME, HINTS_PER_GAME, HINT_POOL_SIZE
  • auth/twitch_auth.py: add channel:read:redemptions and channel:manage:redemptions to default TWITCH_SCOPES
  • main.py:
    • Resolve channel_id via async Helix client
    • Subscribe to EventSub WebSocket for channel point redemptions
    • Add scope validation in _resolve_token(): compare validated token scopes against required set; force re-login on mismatch
    • Skip subscription and log warning if HINT_REWARD_NAME == ""
  • bot/bot.py:
    • EventSub redemption handler: normalise reward title (.strip().lower()), call next_hint(), dispatch chat message
    • Auto-cancel redemption via Helix on LIMIT_REACHED / POOL_EXHAUSTED / no game running
    • Rename hint command → top; update _HELP_TEXT
    • Add new hint command (broadcaster only): calls next_hint() directly
    • Update start_game to call engine.get_hints() and pass hint_pool to start_new_game()

Phase 4 — Overlay

  • overlay/state.py: add "hints": state.hints_revealed to serialize_game_state()
  • overlay/static/index.html: persistent hints panel showing all revealed hint words

Phase 5 — Tests

  • tests/unit/test_engine.py: get_hints() with mocked KeyedVectors
  • tests/unit/test_game_state.py: next_hint() — OK path, LIMIT_REACHED, POOL_EXHAUSTED, skip guessed words, reset on new game
  • tests/unit/test_commands.py: EventSub handler mock (reward match / no match), broadcaster hint command, renamed top command

Files to modify

config.py · game/engine.py · game/state.py · bot/bot.py · main.py · auth/twitch_auth.py · overlay/state.py · overlay/static/index.html · tests/unit/test_engine.py · tests/unit/test_game_state.py · tests/unit/test_commands.py


Identified by

  • 👤 [User] Viewer — engagement mechanic, fairness (refund), hint numbering UX
  • 👤 [User] Streamer — setup simplicity, live reliability, broadcaster fallback command
  • 🏗️ [Tech] Architect — EventSub over PubSub, Scorer protocol boundary, discriminated result, async safety

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium priority — noticeable but not blockingarchitectureCode structure and design concernsenhancementNew feature or requestpythonPull requests that update python code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions