DEV Community

SEN LLC
SEN LLC

Posted on

Japanese Wordle — Why Duplicate-Character Handling Is Trickier Than It Looks

Japanese Wordle — Why Duplicate-Character Handling Is Trickier Than It Looks

Wordle's color feedback rule has a subtlety most implementations get wrong: if the answer has one and your guess has two, only one of your s should be highlighted. Doing this correctly requires a two-pass algorithm with a running frequency map of unmatched answer characters.

Wordle is simple to describe but surprisingly easy to implement wrong. The edge case that trips people up: when the guess has more copies of a character than the answer does, how do you decide which ones are "present" and which are "absent"?

🔗 Live demo: https://sen.ltd/portfolio/wordle-jp/
📦 GitHub: https://github.com/sen-ltd/wordle-jp

Screenshot

Features:

  • 5-character hiragana word guessing in 6 attempts
  • 164 curated words
  • Daily puzzle (same word for everyone per day)
  • Practice mode with random words
  • On-screen 50音 hiragana keyboard
  • Emoji share grid (🟩🟨⬜)
  • Stats tracking (win rate, streak, distribution)
  • Japanese / English UI
  • Zero dependencies, 40 tests

The two-pass duplicate-handling algorithm

Naive implementation:

// WRONG
for (let i = 0; i < 5; i++) {
  if (guess[i] === answer[i]) status[i] = 'correct';
  else if (answer.includes(guess[i])) status[i] = 'present';
  else status[i] = 'absent';
}
Enter fullscreen mode Exit fullscreen mode

This fails when the guess has more copies than the answer. Example: answer あいうえお, guess ああああい. The naive algorithm would mark all four s as 'present', but only one copy of exists in the answer.

The correct algorithm is two passes:

export function checkGuess(guess, answer) {
  const result = new Array(5);
  const answerRemaining = {};

  // Pass 1: mark exact matches, record remaining answer chars
  for (let i = 0; i < 5; i++) {
    if (guess[i] === answer[i]) {
      result[i] = { char: guess[i], status: 'correct' };
    } else {
      answerRemaining[answer[i]] = (answerRemaining[answer[i]] || 0) + 1;
    }
  }

  // Pass 2: mark 'present' only if there's an unmatched answer char
  for (let i = 0; i < 5; i++) {
    if (result[i]) continue;
    if (answerRemaining[guess[i]] > 0) {
      result[i] = { char: guess[i], status: 'present' };
      answerRemaining[guess[i]]--;
    } else {
      result[i] = { char: guess[i], status: 'absent' };
    }
  }

  return result;
}
Enter fullscreen mode Exit fullscreen mode

Worked example with answer あいうあい, guess ああああい:

  • Pass 1: position 0 (=) → correct, position 3 (=) → correct. Positions 1, 2, 4 go to remaining: {い: 2, う: 1}.
  • Pass 2: position 1 () → not in remaining → absent. Position 2 () → absent. Position 4 () → remaining has い → present.

Daily puzzles with a date seed

The daily word uses a simple hash of the date string:

export function getDailyWord(date, wordList) {
  const dateStr = date.toISOString().slice(0, 10);
  let hash = 5381;
  for (let i = 0; i < dateStr.length; i++) {
    hash = ((hash << 5) + hash) + dateStr.charCodeAt(i);
  }
  return wordList[Math.abs(hash) % wordList.length];
}
Enter fullscreen mode Exit fullscreen mode

djb2 hash seeded with the date. Same date = same word, different dates = different words. No server needed.

Hiragana keyboard

The on-screen keyboard follows 50音 layout:

  • あ行: あいうえお
  • か行: かきくけこ
  • さ行: さしすせそ
  • ... etc.
  • Voiced: が〜, ざ〜, だ〜, ば〜
  • Semi-voiced: ぱ〜
  • Small: っ, ゃ, ゅ, ょ

Each key gets colored by its best known status across all guesses (correct > present > absent).

Series

This is entry #44 in my 100+ public portfolio series.

Top comments (0)