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/





![Defluffer - reduce token usage π by 45% using this one simple trick! [Earthday challenge]](https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiekbgepcutl4jse0sfs0.png)







