前書き
タイピングゲームのライブラリを作ったのでタイピングゲームを作りたいと思う。
成果物
雑な設計
・日本語のテキストとローマ字を表示する。
・入力に応じてローマ字の色を変える。
・複数のローマ字入力に対応する。
・ミス数、タイムを計測する。
作る
ライブラリの読み込み
ありがたいことにCDNが使えるので使わせてもらう。
(皆さんは最新バージョンを使ってください!)
<script src="https://cdn.jsdelivr.net/npm/@mogamoga1024/typing-jp@1.0.2/dist/cdn/typing-jp.js"></script>
問題
問題は適当にChatGPT君に考えてもらう。

表示用の文字列とTypingTextオブジェクトをオブジェクトに突っ込んで配列に格納する。
question.js
const allQuestionList = [
{ text: "りんご", typingText: new TypingText("りんご") },
{ text: "バナナ", typingText: new TypingText("バナナ") },
{ text: "オレンジ", typingText: new TypingText("オレンジ") },
{ text: "いちご", typingText: new TypingText("いちご") },
{ text: "ブドウ", typingText: new TypingText("ブドウ") },
{ text: "パイナップル", typingText: new TypingText("パイナップル") },
{ text: "みかん", typingText: new TypingText("みかん") },
{ text: "レモン", typingText: new TypingText("レモン") },
{ text: "メロン", typingText: new TypingText("メロン") },
{ text: "スイカ", typingText: new TypingText("スイカ") },
{ text: "トマト", typingText: new TypingText("トマト") },
{ text: "きゅうり", typingText: new TypingText("きゅうり") },
{ text: "キャベツ", typingText: new TypingText("キャベツ") },
{ text: "レタス", typingText: new TypingText("レタス") },
{ text: "ほうれん草", typingText: new TypingText("ほうれんそう") },
{ text: "かぼちゃ", typingText: new TypingText("かぼちゃ") },
{ text: "じゃがいも", typingText: new TypingText("じゃがいも") },
{ text: "さつまいも", typingText: new TypingText("さつまいも") },
{ text: "玉ねぎ", typingText: new TypingText("たまねぎ") },
{ text: "にんじん", typingText: new TypingText("にんじん") },
{ text: "大根", typingText: new TypingText("だいこん") },
{ text: "しいたけ", typingText: new TypingText("しいたけ") },
{ text: "まいたけ", typingText: new TypingText("まいたけ") },
{ text: "しめじ", typingText: new TypingText("しめじ") },
{ text: "えのき", typingText: new TypingText("えのき") },
{ text: "なす", typingText: new TypingText("なす") },
{ text: "ピーマン", typingText: new TypingText("ピーマン") },
{ text: "トウモロコシ", typingText: new TypingText("トウモロコシ") },
{ text: "ブロッコリー", typingText: new TypingText("ブロッコリー") },
{ text: "カリフラワー", typingText: new TypingText("カリフラワー") },
{ text: "もやし", typingText: new TypingText("もやし") },
{ text: "にら", typingText: new TypingText("にら") },
{ text: "ねぎ", typingText: new TypingText("ねぎ") },
{ text: "セロリ", typingText: new TypingText("セロリ") },
{ text: "パセリ", typingText: new TypingText("パセリ") },
{ text: "アスパラガス", typingText: new TypingText("アスパラガス") },
{ text: "オクラ", typingText: new TypingText("オクラ") },
{ text: "ズッキーニ", typingText: new TypingText("ズッキーニ") },
{ text: "ごぼう", typingText: new TypingText("ごぼう") },
{ text: "れんこん", typingText: new TypingText("れんこん") },
{ text: "枝豆", typingText: new TypingText("えだまめ") },
{ text: "小松菜", typingText: new TypingText("こまつな") },
{ text: "三つ葉", typingText: new TypingText("みつば") },
{ text: "春菊", typingText: new TypingText("しゅんぎく") },
{ text: "山芋", typingText: new TypingText("やまいも") },
{ text: "とうがん", typingText: new TypingText("とうがん") },
{ text: "鯖(サバ)", typingText: new TypingText("さば") },
{ text: "鰯(イワシ)", typingText: new TypingText("いわし") },
{ text: "鮭(サケ)", typingText: new TypingText("さけ") },
{ text: "鯛(タイ)", typingText: new TypingText("たい") },
{ text: "ブリ", typingText: new TypingText("ブリ") },
{ text: "タコ", typingText: new TypingText("タコ") },
{ text: "イカ", typingText: new TypingText("イカ") },
{ text: "アサリ", typingText: new TypingText("アサリ") },
{ text: "ハマグリ", typingText: new TypingText("ハマグリ") },
{ text: "シジミ", typingText: new TypingText("シジミ") },
{ text: "カニ", typingText: new TypingText("カニ") },
{ text: "エビ", typingText: new TypingText("エビ") },
{ text: "ウニ", typingText: new TypingText("ウニ") },
{ text: "カキ", typingText: new TypingText("カキ") },
{ text: "ホタテ", typingText: new TypingText("ホタテ") },
{ text: "鶏肉", typingText: new TypingText("とりにく") },
{ text: "牛肉", typingText: new TypingText("ぎゅうにく") },
{ text: "豚肉", typingText: new TypingText("ぶたにく") },
{ text: "羊肉", typingText: new TypingText("ようにく") },
{ text: "馬肉", typingText: new TypingText("ばにく") },
{ text: "鹿肉", typingText: new TypingText("しかにく") },
{ text: "カモ肉", typingText: new TypingText("カモにく") },
{ text: "鶉(ウズラ)", typingText: new TypingText("うずら") },
{ text: "納豆", typingText: new TypingText("なっとう") },
{ text: "うなぎ", typingText: new TypingText("うなぎ") },
{ text: "たらこ", typingText: new TypingText("たらこ") },
{ text: "いくら", typingText: new TypingText("いくら") },
{ text: "数の子", typingText: new TypingText("かずのこ") },
{ text: "筍(タケノコ)", typingText: new TypingText("たけのこ") },
{ text: "ワカメ", typingText: new TypingText("ワカメ") },
{ text: "昆布", typingText: new TypingText("こんぶ") },
{ text: "のり", typingText: new TypingText("のり") },
{ text: "ひじき", typingText: new TypingText("ひじき") },
{ text: "サバ缶", typingText: new TypingText("サバかん") },
{ text: "ツナ缶", typingText: new TypingText("ツナかん") },
{ text: "うずらの卵", typingText: new TypingText("うずらのたまご") },
{ text: "鳥の唐揚げ", typingText: new TypingText("とりのからあげ") },
{ text: "天ぷら", typingText: new TypingText("てんぷら") },
{ text: "すき焼き", typingText: new TypingText("すきやき") },
{ text: "鍋物", typingText: new TypingText("なべもの") },
{ text: "味噌汁", typingText: new TypingText("みそしる") },
{ text: "煮物", typingText: new TypingText("にもの") },
{ text: "焼き魚", typingText: new TypingText("やきざかな") },
{ text: "焼肉", typingText: new TypingText("やきにく") },
{ text: "牛丼", typingText: new TypingText("ぎゅうどん") },
{ text: "寿司", typingText: new TypingText("すし") },
{ text: "さしみ", typingText: new TypingText("さしみ") },
{ text: "ラーメン", typingText: new TypingText("ラーメン") },
{ text: "うどん", typingText: new TypingText("うどん") },
{ text: "そば", typingText: new TypingText("そば") },
{ text: "パスタ", typingText: new TypingText("パスタ") },
{ text: "カレーライス", typingText: new TypingText("カレーライス") },
{ text: "ピザ", typingText: new TypingText("ピザ") },
{ text: "ハンバーガー", typingText: new TypingText("ハンバーガー") },
];
function createRandomQuestionList(count) {
const tmpQuestionList = [...allQuestionList];
const randomQuestionList = [];
for (let i = 0; i < count; i++) {
const randomIndex = Math.floor(Math.random() * tmpQuestionList.length);
const question = tmpQuestionList.splice(randomIndex, 1)[0];
randomQuestionList.push(question);
}
return randomQuestionList;
}
タイピングの処理
100行程度のコードなんで特に言うことなし。
gameMain関数だけ読めばいいです。
const domText = document.querySelector("#text");
const domRomanContainer = document.querySelector("#roman-container");
const domRoman1 = document.querySelector("#roman1");
const domRoman2 = document.querySelector("#roman2");
const domResult = document.querySelector("#result");
domResult.style.display = "none";
let questionList = createRandomQuestionList(10);
let questionIndex = 0;
let typingText = questionList[questionIndex].typingText;
let missCount = 0;
let typeCount = 0;
let startTime = 0;
let elapsedTime = 0;
gameStart();
function gameStart() {
domText.innerText = "Spaceで開始";
window.onkeydown = e => {
if (e.key === " ") {
gameMain();
}
}
}
function gameMain() {
domText.innerText = `${questionIndex + 1}:${questionList[questionIndex].text}`;
domRoman2.innerText = typingText.remainingRoman;
startTime = performance.now();
window.onkeydown = e => {
if (e.repeat) {
return;
}
// ShiftやF11のような入力を弾く
if (!TypingText.isValidInputKey(e.key)) {
return;
}
typeCount++;
const isCapsLock = e.getModifierState("CapsLock");
// キー入力の更新
const result = typingText.inputKey(e.key, isCapsLock);
switch (result) {
// 不一致の場合
case "unmatch":
missCount++;
return;
// 一致しているが文章が未完成の場合
case "incomplete":
domRoman1.innerText += e.key;
domRoman2.innerText = typingText.remainingRoman;
return;
// 文章が完成した場合
case "complete":
// クリアしたか
if (++questionIndex >= questionList.length) {
elapsedTime = performance.now() - startTime;
domRoman1.innerText += e.key;
domRoman2.innerText = "";
gameEnd();
return;
}
// 次の文章へ
domText.innerText = `${questionIndex + 1}:${questionList[questionIndex].text}`;
typingText = questionList[questionIndex].typingText;
domRoman1.innerText = "";
domRoman2.innerText = typingText.remainingRoman;
return;
}
}
}
function floor(num, decimalPlaces = 0) {
const factor = Math.pow(10, decimalPlaces);
return Math.floor(num * factor) / factor;
}
function gameEnd() {
window.onkeydown = null;
domText.innerText = "リザルト";
domRomanContainer.style.display = "none";
domResult.style.display = "";
const epm = missCount / (elapsedTime / (1000 * 60));
const kpm = typeCount / (elapsedTime / (1000 * 60));
domResult.innerText = `スコア:${floor(kpm * Math.pow(1 - missCount / typeCount, 3), 2)}\n`;
domResult.innerText += `クリアタイム:${floor(elapsedTime / 1000, 2)}秒\n`;
domResult.innerText += `誤タイプ率:${floor(missCount / typeCount * 100, 2)}%\n`;
domResult.innerText += `1分毎のタイプ数:${floor(kpm, 2)}\n`;
domResult.innerText += `1分毎の誤タイプ数:${floor(epm, 2)}\n`;
}
HTML & CSS
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>タイピングゲーム</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/@mogamoga1024/typing-jp@1.0.5/dist/cdn/typing-jp.js"></script>
</head>
<body>
<div id="text"></div>
<div id="roman-container">
<span id="roman1"></span><span id="roman2"></span>
</div>
<div id="result"></div>
<script src="question.js"></script>
<script src="main.js"></script>
</body>
</html>
style.css
* {
font-variant-ligatures: none;
}
#text {
font-size: 2rem;
}
#roman-container, #result {
font-size: 1.5rem;
}
#roman1 {
color: red;
}
最後に
使ってね~☆
ライブラリを作ろうと思ったきっかけの記事
昔書いたやつ