DEV Community

SEN LLC
SEN LLC

Posted on

Building a Japanese Typing Game That Accepts Every Valid Romaji Variant

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

Screenshot

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
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

っ (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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)