Skip to content

Add auto-tutorial, hard mode, and colorblind mode#152

Merged
Hugo0 merged 11 commits into
mainfrom
feature/quick-wins
Mar 14, 2026
Merged

Add auto-tutorial, hard mode, and colorblind mode#152
Hugo0 merged 11 commits into
mainfrom
feature/quick-wins

Conversation

@Hugo0

@Hugo0 Hugo0 commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Summary

Three quick-win features that every major Wordle competitor has and we didn't:

1. Auto-show tutorial on first visit

  • Checks localStorage for existing game state
  • Shows the help modal automatically for new users (once per language)
  • Addresses the ~20% landing page bounce from confused first-time visitors

2. Hard mode

  • Revealed green letters must stay in position, yellow letters must appear somewhere
  • Toggle in settings (disabled once game starts, like NYT)
  • Descriptive error messages ("Hard mode: S must be in position 3")
  • Share text appends * when hard mode active (NYT convention)

3. Colorblind / high contrast mode

  • Swaps green → blue (#85c0f9), yellow → orange (#f5793a)
  • Applies to tiles, keyboard keys, and emoji share board
  • Toggle in settings modal
  • ~8% of men are colorblind — this was an accessibility gap

Why

Market research showed these 3 features are table-stakes that NYT, wordleunlimited (10M/mo), Sanuli (77K/mo), and wordly.org (3M/mo) all have. We were the only major Wordle site missing all three.

Files changed

  • frontend/src/game.ts — tutorial logic, hard mode validation, high contrast toggle (+139 lines)
  • frontend/src/style.css — high contrast color overrides (+19 lines)
  • webapp/templates/game.html — settings toggles for hard mode + high contrast (+38 lines)

Test plan

  • First visit (clear localStorage) — tutorial modal should auto-show
  • Second visit — tutorial should NOT show again
  • Enable hard mode in settings before playing
  • Play with hard mode — verify green/yellow constraints enforced
  • Try toggling hard mode mid-game — should be disabled/grayed out
  • Check share text includes * suffix in hard mode
  • Enable high contrast — tiles should be blue/orange instead of green/yellow
  • Verify keyboard keys also change to blue/orange
  • Share emoji grid should use 🟦/🟧 instead of 🟩/🟨 in high contrast
  • Test both light and dark mode with high contrast
  • Build passes, 81 tests pass

Summary by CodeRabbit

  • New Features
    • Hard Mode: optional stricter gameplay that validates guesses against revealed hints, blocks invalid submissions, shows warnings, and prevents switching to hard mid-game.
    • Difficulty Selector: choose Easy/Normal/Hard from options modal with related messaging and feedback.
    • High Contrast: accessibility theme with adjusted colors for tiles/keyboard and alternative emoji variants.
    • Preferences & Tutorial: settings persist between visits and a language-aware tutorial shows on first visit; shared results include a hard-mode indicator.

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Hugo0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 8 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5186cb83-6a76-4bd8-be3c-bc76fc297121

📥 Commits

Reviewing files that changed from the base of the PR and between c4ef6fd and 30eb971.

📒 Files selected for processing (2)
  • frontend/src/game.ts
  • webapp/templates/game.html
📝 Walkthrough

Walkthrough

Adds Hard Mode (guess-validation and difficulty control) and High Contrast (accessibility color/theme) features with UI toggles, localStorage persistence, CSS overrides, analytics events, and a first-visit tutorial hook.

Changes

Cohort / File(s) Summary
Analytics type
frontend/src/analytics.ts
Expanded SettingsChangeParams.setting union to include 'hard_mode' and 'high_contrast'.
Game logic & public API
frontend/src/game.ts
Added hardMode and highContrast to GameData; added preference load/toggle methods, checkHardMode validation, setDifficulty, maybeShowTutorial, persistence via localStorage, and integrated hard-mode checks into submission and share text.
UI template
webapp/templates/game.html
Added Difficulty selector (Easy/Normal/Hard) and High Contrast toggle in options modal; removed the previous inline "Allow any word" toggle UI and adjusted layout.
Styling
frontend/src/style.css
Added .high-contrast overrides for tile and keyboard key states with explicit high-contrast background colors.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant SettingsUI
    participant GameState
    participant LocalStorage
    participant Analytics

    rect rgba(100,150,200,0.5)
    Note over User,Analytics: Enable High Contrast
    User->>SettingsUI: Click High Contrast toggle
    SettingsUI->>GameState: toggleHighContrast()
    GameState->>LocalStorage: save preference
    GameState->>Analytics: emit 'high_contrast' setting change
    GameState->>SettingsUI: apply CSS class / update UI
    SettingsUI->>User: reflect new style
    end

    rect rgba(150,100,200,0.5)
    Note over User,GameState: Submit Guess under Hard Mode
    User->>SettingsUI: Submit guess
    SettingsUI->>GameState: submitWord()
    GameState->>GameState: checkHardMode(guess)
    alt violates revealed hints
        GameState->>SettingsUI: return validation error
        SettingsUI->>User: show error notification
    else valid
        GameState->>GameState: process guess normally
        GameState->>SettingsUI: update board
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped to toggle contrast bright,
I guarded guesses through the night,
Saved prefs close in local store,
Hard mode rules and colors more,
Hooray — the rabbit plays just right! 🎮✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main features added: auto-tutorial, hard mode, and colorblind mode (via high contrast), matching the changeset scope across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/quick-wins
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
webapp/templates/game.html (1)

371-407: Consider adding i18n support for new toggle labels.

The new toggle labels ("Hard Mode", "High Contrast", "Revealed hints must be used", "Colorblind-friendly colors") are hardcoded in English, while existing toggles use language.config.ui.* keys for localization.

This is fine for an initial implementation, but consider adding translation keys (e.g., language.config.ui.hard_mode, language.config.ui.high_contrast) for consistency with the existing i18n pattern.

Example i18n pattern
-                                   <p :class="{ 'opacity-50': active_row > 0 && !game_over }">Hard Mode</p>
-                                   <p class="text-xs text-neutral-500 dark:text-neutral-400"
-                                      :class="{ 'opacity-50': active_row > 0 && !game_over }">Revealed hints must be used</p>
+                                   <p :class="{ 'opacity-50': active_row > 0 && !game_over }">{{ language.config.ui.hard_mode or "Hard Mode" }}</p>
+                                   <p class="text-xs text-neutral-500 dark:text-neutral-400"
+                                      :class="{ 'opacity-50': active_row > 0 && !game_over }">{{ language.config.ui.hard_mode_desc or "Revealed hints must be used" }}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/templates/game.html` around lines 371 - 407, Replace the hard-coded
English labels in the template with i18n keys consistent with the existing
pattern: use language.config.ui.hard_mode and language.config.ui.hard_mode_hint
for the "Hard Mode" title and hint (referencing the block that reads "Hard Mode"
and "Revealed hints must be used" and the reactive bindings using hardMode,
active_row and game_over), and use language.config.ui.high_contrast and
language.config.ui.high_contrast_hint for the "High Contrast" title and hint
(referencing the block that currently calls toggle_switch('highContrast',
'toggleHighContrast()', 'High Contrast')). Update the template to output those
language.* keys instead of literals and ensure the aria-label on the hardMode
input also uses the i18n key; no logic changes to hardMode,
toggleHighContrast(), toggle_switch, active_row or game_over are required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/game.ts`:
- Around line 1494-1524: The hard-mode check in checkHardMode currently compares
raw characters with toLowerCase(), which fails when diacritics differ; modify
checkHardMode to normalize both the stored tile letter and the guess string
using the same fullNormalize helper used by updateColors (e.g., compute
normalizedGuess = fullNormalize(guess).toLowerCase() and compare against
fullNormalize(letter).toLowerCase()), then use normalizedGuess[c] for position
checks and normalizedGuess.includes(fullNormalize(letter)) for yellow checks;
ensure fullNormalize is imported/available in this module and use the normalized
forms everywhere you currently call guess.toLowerCase() or letter.toLowerCase().

---

Nitpick comments:
In `@webapp/templates/game.html`:
- Around line 371-407: Replace the hard-coded English labels in the template
with i18n keys consistent with the existing pattern: use
language.config.ui.hard_mode and language.config.ui.hard_mode_hint for the "Hard
Mode" title and hint (referencing the block that reads "Hard Mode" and "Revealed
hints must be used" and the reactive bindings using hardMode, active_row and
game_over), and use language.config.ui.high_contrast and
language.config.ui.high_contrast_hint for the "High Contrast" title and hint
(referencing the block that currently calls toggle_switch('highContrast',
'toggleHighContrast()', 'High Contrast')). Update the template to output those
language.* keys instead of literals and ensure the aria-label on the hardMode
input also uses the i18n key; no logic changes to hardMode,
toggleHighContrast(), toggle_switch, active_row or game_over are required.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7e20f38f-d9b9-4546-91ce-80a6d90144bd

📥 Commits

Reviewing files that changed from the base of the PR and between e7cb425 and 1a57a99.

📒 Files selected for processing (4)
  • frontend/src/analytics.ts
  • frontend/src/game.ts
  • frontend/src/style.css
  • webapp/templates/game.html

Comment thread frontend/src/game.ts
Hugo0 added 6 commits March 14, 2026 18:48
…lector

- Unified Easy/Normal/Hard as a segmented button group
- Removed standalone "Allow any word" checkbox
- Hard button grayed out mid-game with hint: "can be enabled before
  your first guess tomorrow"
- Contextual descriptions under each difficulty level
Instead of toast notification, tapping Hard mid-game now:
- Shakes the button group (reuses existing shake animation)
- Shows amber warning: "Hard mode can only be enabled before your first guess"
- Stays on current difficulty (no visual change)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
webapp/templates/game.html (1)

371-410: Difficulty descriptions are not localized.

The difficulty label uses the localization pattern ({{ language.config.ui.difficulty or "Difficulty" }}), but the mode descriptions (lines 403-409) are hardcoded in English. For consistency with other UI strings, consider using localization keys.

♻️ Suggested localization pattern
 <p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1"
-   v-if="allow_any_word && !hardMode">Any word accepted, even if not in the dictionary</p>
+   v-if="allow_any_word && !hardMode">{{ language.config.ui.easy_mode_desc or "Any word accepted, even if not in the dictionary" }}</p>
 <p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1"
-   v-if="!allow_any_word && !hardMode">Guesses must be valid words from the dictionary</p>
+   v-if="!allow_any_word && !hardMode">{{ language.config.ui.normal_mode_desc or "Guesses must be valid words from the dictionary" }}</p>
 <p class="text-xs text-neutral-500 dark:text-neutral-400 mt-1"
-   v-if="hardMode">Revealed hints must be used in subsequent guesses</p>
+   v-if="hardMode">{{ language.config.ui.hard_mode_desc or "Revealed hints must be used in subsequent guesses" }}</p>
 <p class="text-xs text-amber-500 mt-1"
-   v-if="difficultyWarning">Hard mode can only be enabled before your first guess</p>
+   v-if="difficultyWarning">{{ language.config.ui.hard_mode_warning or "Hard mode can only be enabled before your first guess" }}</p>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/templates/game.html` around lines 371 - 410, Replace the three
hardcoded mode description strings and the three explanatory lines with
localized strings from language.config.ui (similar to the existing difficulty
label) — update the button labels and the paragraphs that use allow_any_word,
!allow_any_word, hardMode, and difficultyWarning to reference keys like
language.config.ui.difficulty_easy / difficulty_normal / difficulty_hard and
language.config.ui.difficulty_any_word / difficulty_valid_guesses /
language.config.ui.difficulty_hard_hint / language.config.ui.difficulty_warning
(or your chosen key names) so the UI uses {{ language.config.ui.<key> or
"<fallback>" }} instead of literal English; keep the existing bindings
(v-on:click="setDifficulty('...')", :class conditions and v-if expressions)
intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/game.ts`:
- Around line 1494-1516: The setDifficulty method only persists hardMode to
localStorage, so selecting Easy (allow_any_word = true) is lost on reload;
update setDifficulty to also persist the easy mode flag (e.g., write
allow_any_word or an "easyMode"/"allowAnyWord" key via localStorage.setItem) and
update loadHardModePreference (or add a new loadEasyModePreference) to read that
key and restore this.allow_any_word accordingly so the Easy setting survives
page refresh; ensure the same keys are used and handle localStorage exceptions
the same way as existing hardMode persistence.

---

Nitpick comments:
In `@webapp/templates/game.html`:
- Around line 371-410: Replace the three hardcoded mode description strings and
the three explanatory lines with localized strings from language.config.ui
(similar to the existing difficulty label) — update the button labels and the
paragraphs that use allow_any_word, !allow_any_word, hardMode, and
difficultyWarning to reference keys like language.config.ui.difficulty_easy /
difficulty_normal / difficulty_hard and language.config.ui.difficulty_any_word /
difficulty_valid_guesses / language.config.ui.difficulty_hard_hint /
language.config.ui.difficulty_warning (or your chosen key names) so the UI uses
{{ language.config.ui.<key> or "<fallback>" }} instead of literal English; keep
the existing bindings (v-on:click="setDifficulty('...')", :class conditions and
v-if expressions) intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41c0312e-2caf-4851-b897-f6f5dc47ef47

📥 Commits

Reviewing files that changed from the base of the PR and between 1a57a99 and c4ef6fd.

📒 Files selected for processing (2)
  • frontend/src/game.ts
  • webapp/templates/game.html

Comment thread frontend/src/game.ts
Hugo0 added 3 commits March 14, 2026 19:10
- Tracks the highest difficulty any guess was submitted at
- Can always go back up to that level (e.g., Normal→Easy→Normal is fine)
- Can't go higher than max used (e.g., Easy→Normal blocked if first
  guess was on Easy)
- Fix: persist allow_any_word to localStorage (easy mode was reverting on reload)
- Fix: remove dead toggleHardMode() (replaced by setDifficulty)
- Fix: remove unused diffLevels variable
- DRY: extract currentDifficultyLevel() helper (was inline ternary 3x)
- Rename loadHardModePreference → loadDifficultyPreference (loads both)
- Guard against hardMode + allow_any_word both true
@Hugo0
Hugo0 merged commit f8d4a1b into main Mar 14, 2026
4 checks passed
@Hugo0
Hugo0 deleted the feature/quick-wins branch April 15, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant