在浏览器中运行的打字游戏的罗马字输入判定思路|如何接受「し=shi/si」「ん」「っ」
ブラウザで動くタイピングゲームのローマ字入力判定の考え方|「し=shi/si」「ん」「っ」をどう受け付けるか
译文尚不完整,完整内容请切换到原文。
前言
制作日文打字游戏时,最先遇到的就是罗马字的表记差异。如果题目是"すし",希望 sushi 和 susi 都算正确;"かんた"无论是 kanta 还是 kannta 都可以。另一方面,如果"かんい"被输入为 kani,就无法与"かに"区分开。这里,我们用一个简单的 JavaScript 例子来梳理这种逐键判定的思路。
策略:将假名按"单位"划分,每个单位各持有候选项
- 将给定的假名,以拗音(如「しゃ」)为一个单位进行分割
- 每个单位拥有一份可接受的罗马字候选列表
- 「ん」「っ」通过查看下一个单位来增加候选
- 输入过程中,只要"当前单位的输入缓冲区"与某个候选的前缀一致即可。
虽然也可以先构建整个字符串的模式,但由于组合容易爆炸,按单位逐一判断会更容易处理。
\n 代码示例
const TABLE = {
'す':['su'], 'し':['shi','si','ci'], 'か':['ka','ca'], 'た':['ta'],
'ほ':['ho'], 'い':['i'], 'や':['ya'], 'ん':['nn','xn'],
'しゃ':['sha','sya'], 'っ':['xtu','ltu'],
};
const tokenize = s => s.match(/.[ゃゅょ]?/g); // 拗音は2文字で1単位
function candidates(units, i) {
const [u, next] = [units[i], units[i + 1]];
const list = [...TABLE[u]];
const heads = next ? candidates(units, i + 1).map(r => r[0]) : [];
if (u === 'ん' && next && heads.every(h => !'aiueoyn'.includes(h))) list.push('n');
if (u === 'っ') list.push(...heads.filter(h => !'aiueon'.includes(h)));
return [...new Set(list)];
}
class RomajiJudge {
constructor(kana) { this.units = tokenize(kana); this.i = 0; this.buf = ''; }
get done() { return this.i >= this.units.length; }
input(key) {
if (this.done) return false;
const cands = candidates(this.units, this.i);
const next = this.buf + key;
if (cands.some(c => c.startsWith(next))) {
this.buf = next;
if (!cands.some(c => c !== next && c.startsWith(next))) { this.i++; this.buf = ''; }
return true;
}
if (cands.includes(this.buf)) { this.i++; this.buf = ''; return this.input(key); }
return false; // ミス
}
}
\n 要点说明
\n 确认的时机
像 shi 和 si 这样,当一个候选不是另一个候选的中间部分时,完全一致即可确认。问题在于像"ん"的 n 和 nn 这样,较短的候选是较长候选的前缀的情况。输入 n 时,无法判断用户是否打算输入 nn。
因此,即使在完全匹配的情况下,只要仍有较长的候选存在,就暂缓确认;如果下一个按键无法延续较长的候选,则用较短的候选进行确认,并将该按键转交给下一个单位。input() 中递归调用自身正是为了这个目的。
允许「ん」只按一次 n 的条件
当下一个单位的候选以元音、や行、な行开头时,按 n 一次会导致读音改变(「かんい」和「かに」不同),因此将 nn 设为必需。题目末尾的「ん」也因为不存在下一个单位,所以需要 nn。
「っ」的辅音重叠
如果是「った」,可以重复下一个单位的首辅音 t 来打出 tta。收集下一个单位候选的首字母,将除元音和 n 以外的字符添加为候选。保留 xtu / ltu 的单独输入,可以适应不同的输入偏好。
按键输入的处理方式
ブラウザでは keydown の e.key を使い、1文字のキーだけを input() に渡します。日本語 IME がオンのままだと変換中の文字が届くので、e.isComposing が true のときは無視して「半角英数に切り替えてください」と案内を出すと、問い合わせが減ります。大文字は toLowerCase() でそろえておきましょう。
動作確認
const run = (kana, keys) => {
const j = new RomajiJudge(kana);
return [...keys].every(k => j.input(k)) && j.done;
};
run('すし', 'suci'); // true
run('かんた', 'kanta'); // true
run('かんい', 'kani'); // false(nn が必要)
run('かった', 'katta'); // true
実際のゲームで足りないもの
このサンプルは考え方の確認用なので、実用にするには「ゃ・ゅ・ょ」を含む拗音のテーブルの網羅、tsu / tu のような候補の追加、お手本表示を入力に合わせて切り替える処理などが必要です。ちなみに、ブラウザで遊べる寿司打では、「し」を shi / si / ci のどれで打っても正解になり、打った文字に合わせてお手本も切り替わります。実装の参考に触ってみると、どこで確定しているかがよくわかります。
おわりに
ローマ字判定は「候補の集合」と「確定の保留」の2つを押さえると、ぐっと見通しがよくなります。まずは小さなテーブルで動かしてみて、遊ぶ人の打ち方に合わせて候補を育てていくのがおすすめです。
来源:zenn · 游戏开发社区 · zenn.dev