{"id":50568257,"url":"https://github.com/sito8943/wordle-clone","last_synced_at":"2026-06-04T16:30:56.586Z","repository":{"id":342042811,"uuid":"1170809202","full_name":"sito8943/wordle-clone","owner":"sito8943","description":"Module 8 exercise","archived":false,"fork":false,"pushed_at":"2026-05-06T22:31:34.000Z","size":11100,"stargazers_count":11,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-07T00:26:54.482Z","etag":null,"topics":["harbourspace","react","taildwindcss"],"latest_commit_sha":null,"homepage":"https://wordle.sito8943.com/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sito8943.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["Sito8943"],"custom":["https://www.paypal.com/donate/?hosted_button_id=TGFCRW9NBTJZY"]}},"created_at":"2026-03-02T14:46:54.000Z","updated_at":"2026-05-06T22:31:37.000Z","dependencies_parsed_at":"2026-03-19T20:04:17.439Z","dependency_job_id":null,"html_url":"https://github.com/sito8943/wordle-clone","commit_stats":null,"previous_names":["sito8943/wordle-clone"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sito8943/wordle-clone","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sito8943%2Fwordle-clone","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sito8943%2Fwordle-clone/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sito8943%2Fwordle-clone/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sito8943%2Fwordle-clone/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sito8943","download_url":"https://codeload.github.com/sito8943/wordle-clone/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sito8943%2Fwordle-clone/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33914543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["harbourspace","react","taildwindcss"],"created_at":"2026-06-04T16:30:55.791Z","updated_at":"2026-06-04T16:30:56.572Z","avatar_url":"https://github.com/sito8943.png","language":"TypeScript","funding_links":["https://github.com/sponsors/Sito8943","https://www.paypal.com/donate/?hosted_button_id=TGFCRW9NBTJZY"],"categories":[],"sub_categories":[],"readme":"# Wordle Clone\n\nThis project is an original word game inspired by popular word-guessing mechanics.\nIt is not affiliated with or endorsed by The New York Times.\n\nReact + TypeScript Wordle clone with:\n\n- Difficulty modes and scoring/streak system\n- Profile and scoreboard routes\n- Convex-backed online scoreboard and dictionary\n- Local offline fallbacks and resume flow\n- PWA support (installable app)\n\n## Requirements\n\n- Node.js `22.12.0` (see `.nvmrc`)\n- npm\n\n## Quick Start\n\n1. Install dependencies:\n\n```bash\nnpm install\n```\n\n2. (Optional) Start Convex backend:\n\n```bash\nnpm run convex:dev\n```\n\n3. Create `.env.local` (or copy `.env.example`) and set:\n\n```bash\nVITE_CONVEX_URL=https://YOUR-DEPLOYMENT.convex.cloud\nDAILY_PROXY_TARGET=http://localhost:8787\nVITE_DAILY_WORD_API_URL=/api/daily\nVITE_WORD_LIST_BUTTON_ENABLED=true\nVITE_SETTINGS_DRAWER_ENABLED=true\n```\n\n4. (Recommended for Daily mode in local dev) start the sibling daily backend:\n\n```bash\ncd ../wordle-daily-backend\nnpm install\nnpm run dev\n```\n\n5. Run the app:\n\n```bash\nnpm run dev\n```\n\nIf `VITE_CONVEX_URL` is not set, the app still works with local-only behavior for scoreboard and dictionary cache.\n\n## Scoring\n\n- Base points are the remaining attempts after a win.\n- Difficulty multiplies base points: `easy x1`, `normal x2`, `hard x5`, `insane x7`.\n- `insane` also adds `+1` point for every `4` seconds left.\n- `normal` adds a `+0.4` dictionary bonus per valid non-answer guess row.\n- Streak acts as a multiplier (capped at `100`) instead of a flat addition:\n\n```ts\nconst scoreBase = basePoints * difficultyMultiplier + timeBonus;\nconst scoreFinal = Math.round(scoreBase * (1 + 0.3 * Math.sqrt(streak)));\n```\n\n## Gameplay Constants Reference\n\nThis list centralizes the main game-related constants (difficulty, timer, board, dictionary, and persistence keys).\n\n### Core Gameplay\n\n| Constant                                 | Value                                        | Source                                 |\n| ---------------------------------------- | -------------------------------------------- | -------------------------------------- |\n| `WORD_LENGTH`                            | `5`                                          | `src/domain/wordle/constants.ts`       |\n| `MAX_GUESSES`                            | `6`                                          | `src/domain/wordle/constants.ts`       |\n| `BOARD_ROWS`                             | `6`                                          | `src/domain/wordle/board/constants.ts` |\n| `BOARD_COLUMNS`                          | `5`                                          | `src/domain/wordle/board/constants.ts` |\n| `DIFFICULTY_SCORE_MULTIPLIERS`           | `{ easy: 1, normal: 2, hard: 5, insane: 7 }` | `src/domain/wordle/constants.ts`       |\n| `STREAK_MODIFIER`                        | `0.3`                                        | `src/domain/wordle/constants.ts`       |\n| `MAX_STREAK_FOR_SCORE_MULTIPLIER`        | `100`                                        | `src/domain/wordle/constants.ts`       |\n| `LIGHTNING_SECONDS_BONUS`                | `4`                                          | `src/domain/wordle/constants.ts`       |\n| `NORMAL_DICTIONARY_ROW_BONUS`            | `0.4`                                        | `src/domain/wordle/constants.ts`       |\n| `SCORE_DECIMAL_FACTOR`                   | `10`                                         | `src/domain/wordle/constants.ts`       |\n| `MIN_ROUND_DURATION_FOR_SCORE_COMMIT_MS` | `4000`                                       | `src/domain/wordle/constants.ts`       |\n| `PlayerDifficulty`                       | `\"easy\" \\| \"normal\" \\| \"hard\" \\| \"insane\"`   | `src/domain/wordle/player.ts`          |\n\n### Timer and Hints\n\n| Constant                           | Value                    | Source                                                |\n| ---------------------------------- | ------------------------ | ----------------------------------------------------- |\n| `HARD_MODE_TOTAL_SECONDS`          | `60`                     | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `HARD_MODE_FINAL_STRETCH_SECONDS`  | `15`                     | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `HARD_MODE_CLOCK_BOOST_SCALES`     | `[0.28, 0.2, 0.14, 0.1]` | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `HARD_MODE_CLOCK_BOOST_THRESHOLDS` | `[30, 45]`               | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `EASY_MODE_HINT_LIMIT`             | `2`                      | `src/views/Play/hooks/useHintController/constants.ts` |\n| `NORMAL_MODE_HINT_LIMIT`           | `1`                      | `src/views/Play/hooks/useHintController/constants.ts` |\n| `HARD_MODE_HINT_LIMIT`             | `0`                      | `src/views/Play/hooks/useHintController/constants.ts` |\n\n### Player Defaults\n\n| Constant                             | Value                    | Source                              |\n| ------------------------------------ | ------------------------ | ----------------------------------- |\n| `DEFAULT_PLAYER_DIFFICULTY`          | `\"normal\"`               | `src/providers/Player/constants.ts` |\n| `DEFAULT_PLAYER_KEYBOARD_PREFERENCE` | `\"onscreen\"`             | `src/providers/Player/constants.ts` |\n| `DEFAULT_PLAYER_LANGUAGE`            | `\"en\"`                   | `src/providers/Player/constants.ts` |\n| `PlayerKeyboardPreference`           | `\"onscreen\" \\| \"native\"` | `src/domain/wordle/player.ts`       |\n| `PlayerLanguage`                     | `\"en\" \\| \"es\"`           | `src/domain/wordle/player.ts`       |\n\n### Dictionary and Language\n\n| Constant                    | Value                          | Source                       |\n| --------------------------- | ------------------------------ | ---------------------------- |\n| `WORDS_DEFAULT_LANGUAGE`    | `\"es\"`                         | `src/api/words/constants.ts` |\n| `WORDS_SUPPORTED_LANGUAGES` | `[\"es\"]`                       | `src/api/words/constants.ts` |\n| `WORDS_CACHE_KEY_PREFIX`    | `\"wordle:dictionary\"`          | `src/api/words/constants.ts` |\n| `WORDS_CHECKSUM_KEY_PREFIX` | `\"wordle:dictionary:checksum\"` | `src/api/words/constants.ts` |\n\n### Persistence and Session Keys\n\n| Constant                                      | Value                                    | Source                                                |\n| --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------- |\n| `WORDLE_GAME_STORAGE_KEY`                     | `\"wordle:game\"`                          | `src/config/constants.ts`                             |\n| `WORDLE_SESSION_STORAGE_KEY`                  | `\"wordle:session-id\"`                    | `src/domain/wordle/constants.ts`                      |\n| `HINT_USAGE_STORAGE_KEY`                      | `\"wordle:hint-usage\"`                    | `src/views/Home/hooks/useHintController/constants.ts` |\n| `WORDLE_SYNC_EVENTS_KEY`                      | `\"wordle:sync-events\"`                   | `src/api/score/constants.ts`                          |\n| `END_OF_GAME_DIALOG_SEEN_SESSION_STORAGE_KEY` | `\"wordle:end-of-game-dialog-seen\"`       | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `HARD_MODE_TIMER_STORAGE_KEY`                 | `\"wordle:hard-mode-timer\"`               | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `SCOREBOARD_CACHE_KEY`                        | `\"wordle:scoreboard:cache\"`              | `src/api/score/constants.ts`                          |\n| `SCOREBOARD_PENDING_KEY`                      | `\"wordle:scoreboard:pending\"`            | `src/api/score/constants.ts`                          |\n| `SCOREBOARD_CLIENT_ID_KEY`                    | `\"wordle:scoreboard:client-id\"`          | `src/api/score/constants.ts`                          |\n| `SCOREBOARD_PROFILE_IDENTITY_KEY`             | `\"wordle:scoreboard:profile-identity\"`   | `src/api/score/constants.ts`                          |\n| `WORDLE_ANIMATIONS_DISABLED_STORAGE_KEY`      | `\"wordle:disable-start-animations\"`      | `src/domain/wordle/constants.ts`                      |\n| `WORDLE_START_ANIMATION_SESSION_KEY`          | `\"wordle:start-animation-session-seen\"`  | `src/domain/wordle/constants.ts`                      |\n| `WORDLE_KEYBOARD_ENTRY_ANIMATION_SESSION_KEY` | `\"wordle:keyboard-entry-animation-seen\"` | `src/domain/wordle/constants.ts`                      |\n\n### Gameplay UX Timing\n\n| Constant                                            | Value  | Source                                                |\n| --------------------------------------------------- | ------ | ----------------------------------------------------- |\n| `MESSAGE_VISIBILITY_DURATION_MS`                    | `1800` | `src/hooks/useWordle/constants.ts`                    |\n| `GAME_STATE_PERSIST_DEBOUNCE_MS`                    | `150`  | `src/hooks/useWordle/constants.ts`                    |\n| `COMBO_FLASH_VISIBILITY_DURATION_MS`                | `820`  | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `CHALLENGE_COMPLETION_ALERT_VISIBILITY_DURATION_MS` | `4000` | `src/views/Play/hooks/usePlayController/constants.ts` |\n| `SCORE_LIMIT`                                       | `10`   | `src/config/constants.ts`                             |\n\n## Environment Variables\n\nAll flags are read from `src/config/env.ts`.\n\n- `VITE_APP_VERSION` (default `\"0.0.0\"`): app version.\n- `VITE_CONVEX_URL` (optional): Convex deployment URL.\n- `VITE_DAILY_WORD_API_URL` (default `\"/api/daily\"`): base endpoint used by `DailyWordClient` to fetch the daily payload (word + meaning) in a single request.\n- `VITE_WORD_REPORT_PHONE_NUMBER` (optional): WhatsApp target for the invalid-word report link.\n- `VITE_PAYPAL_DONATION_BUTTON_URL` (optional): PayPal donation URL.\n- `DAILY_PROXY_TARGET` (default `\"http://localhost:8787\"`): dev proxy target for `/api/daily` in Vite.\n\nFeature flags (all default `true` unless noted):\n\n- `VITE_WORD_LIST_BUTTON_ENABLED`: enables the **Words** button (shown only in Easy).\n- `VITE_WORD_REPORT_BUTTON_ENABLED`: enables the invalid-word report button.\n- `VITE_PAYPAL_DONATION_BUTTON_ENABLED`: enables the donate button.\n- `VITE_SHARE_BUTTON_ENABLED`: enables the victory share action.\n- `VITE_DEV_CONSOLE_ENABLED`: enables the developer console (only in dev mode at runtime).\n- `VITE_SOUND_ENABLED`: enables procedural sound effects.\n- `VITE_MASTER_AND_MUSIC_CHANNELS_ENABLED` (default `false`): enables the `master` and `music` channels. When `false`, the app runs with SFX-only channel controls and disables music playback.\n- `VITE_HINTS_ENABLED`: enables the hint system.\n- `VITE_HELP_BUTTON_ENABLED`: enables the help button.\n- `VITE_CHALLENGES_ENABLED`: enables daily challenges.\n- `VITE_SETTINGS_DRAWER_ENABLED`: enables the in-game settings drawer.\n- `VITE_PLAY_OFFLINE_STATE_ENABLED` (default `false`): shows the offline state in Play view.\n- `VITE_LIGHTNING_MODE_ENABLED`: enables the lightning/insane timer mode.\n- `VITE_LIGHTNING_START_CUE_AND_AUTO_TIMER_ENABLED` (default `false`): enables the lightning start cue (`Begin`) and starts the lightning timer immediately on screen entry. When `false`, cue is hidden and timer starts on first input.\n- `VITE_TIMER_AUTO_PAUSE_ENABLED` (default `false`): pauses the hard-mode timer when Play dialogs are open or the browser tab is hidden.\n- `VITE_DIFFICULTY_EASY_ENABLED` / `VITE_DIFFICULTY_NORMAL_ENABLED` / `VITE_DIFFICULTY_HARD_ENABLED` / `VITE_DIFFICULTY_INSANE_ENABLED`: individual difficulty toggles.\n\nNotes:\n\n- The **Words** button is only shown in **Easy** difficulty.\n- The button is disabled while dictionary data is loading or unavailable.\n\n## Available Scripts\n\n- `npm run dev`: start Vite dev server.\n- `npm run build`: production build.\n- `npm run preview`: preview production build locally.\n- `npm run test`: run tests with Vitest.\n- `npm run coverage`: run tests with coverage.\n- `npm run lint`: run typecheck + eslint + prettier check + depcheck.\n\n## i18n guardrail\n\n- User-facing copy in JSX must go through `i18next` / `react-i18next`.\n- ESLint blocks new literal strings in JSX text nodes and visible attributes such as `aria-label`, `title`, `placeholder`, and `alt`.\n- Technical constants, test files, `src/i18n/**`, and module `constants.ts` files are excluded from that rule.\n- `npm run lint-prettier`: validate formatting with Prettier.\n- `npm run prettier`: format the project with Prettier.\n- `npm run convex:dev`: start Convex dev backend.\n- `npm run convex:deploy`: deploy Convex functions/schema.\n- `npm run convex:backfill-player-codes`: assign missing 4-character recovery codes to existing remote players.\n\n## Persistence Behavior\n\n- `sessionStorage`\n  - `wordle:session-id`: per-tab session id.\n- `localStorage`\n  - `wordle:game`: in-progress game reference `{ sessionId, gameId, seed, guesses, current, gameOver }`.\n    - `answer` is not stored; it is derived locally from `gameId`, `seed`, and the dictionary.\n  - `wordle:hint-usage`: hint usage snapshot keyed by game reference, without storing `answer` in clear.\n  - `wordle:sync-events`: pending offline victory events used to sync score/streak to Convex in order.\n  - `wordle:dictionary:es`: cached dictionary.\n  - `wordle:daily-word:\u003cYYYY-MM-DD\u003e`: cached remote daily word.\n  - `wordle:daily-meaning:\u003cYYYY-MM-DD\u003e:\u003cWORD\u003e`: cached meaning for the daily word fetched from the daily payload.\n  - `player`: player profile and score/streak metadata, including recovery `code`.\n    - local `score`/`streak` act as UI cache; confirmed remote values take precedence after sync.\n  - `wordle:scoreboard:*`: scoreboard cache/pending/client metadata.\n  - `wordle:scoreboard:profile-identity`: adopted remote profile identity (`clientRecordId`) after profile creation or recovery.\n\nResume rules:\n\n- Same tab + refresh restores the current board automatically.\n- New tab + in-progress board from another tab session prompts to continue or start fresh.\n\n## Player Recovery\n\n- A named profile gets a unique 4-character recovery code (`A-Z`, `0-9`) from Convex.\n- The code is shown in `Profile` and is read-only.\n- The initial dialog now supports two explicit identity actions:\n  - create a unique player name\n  - recover an existing profile with its code\n- Identity operations are remote-first. If Convex is unavailable, profile creation/recovery fails without mutating the local player into an unrecognized state.\n\nBackfill existing remote players:\n\n```bash\nnpm run convex:backfill-player-codes\n```\n\nYou can pass extra Convex CLI flags through the script, for example:\n\n```bash\nnpm run convex:backfill-player-codes -- --prod\n```\n\n## Dictionary + Word List\n\n- Dictionary words are stored in Convex (`words` table) by language.\n- Gameplay dictionary language is fixed to `es`.\n- The app reads cached words first and only fetches from Convex when cache is empty.\n- The fetched dictionary is cached locally in `wordle:dictionary:es`.\n\n## PWA\n\n- Powered by `vite-plugin-pwa`.\n- Production build generates `manifest.webmanifest` and service worker assets.\n- Icons live in `public/` (`pwa-192x192.png`, `pwa-512x512.png`, and maskable variants).\n\nLocal installability check:\n\n```bash\nnpm run build\nnpm run preview\n```\n\nOpen the preview URL in Chrome/Edge and use the install button in the address bar.\n\n## Architecture\n\nFor architecture and layering rules, see:\n\n- `ARCHITECTURE.md`\n- `AGENTS.md`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsito8943%2Fwordle-clone","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsito8943%2Fwordle-clone","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsito8943%2Fwordle-clone/lists"}