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
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';
}
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;
}
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];
}
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.
- 📦 Repo: https://github.com/sen-ltd/wordle-jp
- 🌐 Live: https://sen.ltd/portfolio/wordle-jp/
- 🏢 Company: https://sen.ltd/

Top comments (0)