Building a Japanese Typing Game That Accepts Every Valid Romaji Variant
The hardest part of a Japanese typing game isn't the game loop — it's the input mapping. し can be typed as "si" or "shi". っち can be "cchi" or "tti". ん before a vowel requires "nn" but before a consonant allows just "n". The ROMAJI_MAP and tokenizer handle all these variants so the player is never penalized for a correct-but-alternative spelling.
Japanese typing practice is different from English typing. You see hiragana (あいうえお) and type the romaji equivalent. But unlike English where each letter has one key, many Japanese characters have multiple valid romaji inputs. A typing game that only accepts one variant is frustrating and teaches bad habits.
🔗 Live demo: https://sen.ltd/portfolio/typing-jp/
📦 GitHub: https://github.com/sen-ltd/typing-jp
Features:
- Accepts all valid romaji variants (shi/si, tsu/tu, chi/ti, fu/hu, etc.)
- 3 modes: Practice, Time Attack (60s), Endurance (3 mistakes)
- 3 word sets: Basic hiragana, Common words (30), Sentences (10)
- っ (small tsu) consonant doubling
- ん disambiguation before vowels
- Real-time character highlighting
- WPM and accuracy stats
- Japanese / English UI
- Zero dependencies, 39 tests
The romaji map
Each hiragana maps to an array of valid inputs:
export const ROMAJI_MAP = {
'し': ['si', 'shi'],
'ち': ['ti', 'chi'],
'つ': ['tu', 'tsu'],
'ふ': ['hu', 'fu'],
'じ': ['zi', 'ji'],
'しゃ': ['sya', 'sha'],
'ちゃ': ['tya', 'cha'],
'じゃ': ['zya', 'ja', 'jya'],
// ... 80+ entries
};
This covers basic hiragana (46), dakuten/handakuten (25), and yōon combinations (33). Every entry was verified against standard Japanese IME behavior.
Tokenizing: yōon before singles
The tokenizer must check two-character combinations before single characters. きゃ is one token (kya), not き+や (ki+ya):
export function tokenize(str) {
const tokens = [];
let i = 0;
while (i < str.length) {
if (i + 1 < str.length) {
const pair = str[i] + str[i + 1];
if (ROMAJI_MAP[pair]) {
tokens.push({ kana: pair, romaji: [...ROMAJI_MAP[pair]] });
i += 2;
continue;
}
}
// Single character fallback
tokens.push({ kana: str[i], romaji: [...(ROMAJI_MAP[str[i]] || [str[i]])] });
i += 1;
}
return tokens;
}
っ (small tsu) consonant doubling
っ before a consonant doubles that consonant. っか becomes "kka", っし becomes "sshi" or "ssi":
The tokenizer peeks ahead at the next token's romaji options and prepends the first consonant of each option. This produces all valid doubled forms automatically.
ん disambiguation
ん is tricky: before a vowel (あいうえお) or や/ゆ/よ, you must type "nn" to avoid ambiguity. Before consonants or at word-end, both "n" and "nn" work.
Example: かんい (simplicity) = ka + nn + i. If "n" alone were accepted, the parser couldn't distinguish かに (crab) from かんい.
Validation with prefix matching
validateInput works token-by-token. For each token, it checks if the typed input matches a prefix of any valid romaji variant:
// If input fully matches a romaji variant, advance to next token
// If input is a prefix of a variant, keep accepting
// If input matches no prefix, mark as wrong
This gives instant green feedback as soon as a character is resolved, even mid-word.
Tests
39 tests covering:
- ROMAJI_MAP has entries for all basic hiragana
- Tokenize produces correct tokens for single chars, yōon, っ doubling
- ん requires "nn" before vowels
- validateInput with correct, partial, and wrong inputs
- WPM and accuracy calculations
- Edge cases (empty string, unknown characters)
Series
This is entry #38 in my 100+ public portfolio series.
- 📦 Repo: https://github.com/sen-ltd/typing-jp
- 🌐 Live: https://sen.ltd/portfolio/typing-jp/
- 🏢 Company: https://sen.ltd/

Top comments (0)