1ファイルでタイピングゲームを作る

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/137d709c-1b10-60e2-1e62-a7358dff2d44.png">
1つのhtmlファイルで、タイピングゲームを作成します。

+ タイピングする文字は、preタグに入れておきます。
+ javascriptでこれから打つ文字、打ち終わった文字をそれぞれのpre内のspanタグに入れます。
+ これから打つ文字、打ち終わった文字は、CSSで見た目を変えます。
+ 残り時間は、setIntervalで減らしていきます。
+ ゲーム開始前に、初期の残り時間を増やしたり、減らしたりできます。

ちょっと制限としては次のような感じです。

+ プログラムのタイプ練習用としているので、字下げ部分は、タイプしなくても先に進むようになっています。
+ その字下げは、ゲーム内ではスペース2つ以上が連続するところ、と判断しているので、字下げ以外でスペース2つ以上を作ると、タイプせずに飛ばされてしまいます。

また別記事の[1つのjsファイルで効果音を鳴らす](https://qiita.com/toyohisa/items/051823bb5c3e7dfb8832)を利用すると、タイプ音を付けることもできます。

# ソースコードです。

```html:typing.js.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
    <style>
        /* innerを画面中央に配置するために、外側の要素をposition:relativeにする。*/
        .outer {
            position: relative;
        }
        /* leftで要素の左側を画面中央に、transformで要素の半分だけ左側に移動して、中央配置にする。*/
        .inner {
            position: absolute;
            left: 50%;
            transform: translateX(-50%);
        }
        /* ゲーム開始前の「スペースキーで開始」を表示する要素 */
        #overlay {
            position:absolute;
            left: 50%;
            top: 50%;
            transform: translateX(-50%) ;
            background-color: blanchedalmond;
            font-size:small;
            padding: 20px;
        }
        /* 要素の消去 */
        .none {
            display: none;
        }
        /* ゲーム実行中に表示される説明部分の要素 */
        #message {
            position: absolute;
            font-size: large;
        }
        /* タイピングする文字の全体（これから打つもの、打ったもの） */
        #panel {
            font-size:xx-large;
        }
        /* 打ち終わった文字 */
        #done {
            background-color:grey;
        }
        /* 記録などを表示するヘッダー部分 */
        #header {
            font-size:medium;
            position:absolute;
            transform: translateX(-10%);
            width: 600px;
        }
        /* 時間表示部分 */
        #time {
            font-size: large;
        }
    </style>
</head>
<body class="outer">
    <div id="panel" class="inner">
        <div id="header">
			残り時間: <span id="time">0</span>
			自分の記録: <span id="hist"></span>
		</div>

        <!-- #textにタイピングする文字を入れる。 #done, #typeは、それぞれ打ち終わったもの、これから打つものが入る。 -->
        <pre id="text"><span id="done"></span><span id="type">int i=0;
scanf("%d",&i);
printf("i=%d\n",i);
int values[5] = {4,6,1,4,2};
int cnt=0;
while(cnt<5){
    if(values[cnt]>i){
        printf("v=%d\n",values[cnt]);
    }
    cnt+=1;
}</span></pre>

        <div id="overlay">スペースキーで開始</div>
        <div id="message"></div>
    </div>

    <script type="text/javascript">
        // 最初にタイピングする文字を取得しておく。
        const text = $("#text").text();
        // タイピングする文字のどこまでを打ったかを記録する変数
        let text_i = 0;
        // 残り時間を管理する lm の値を変更すると、初期の残り時間を変更できる。
        let tm = {
            id:undefined, // setIntervalのid
            st:undefined, // タイピングの開始時間
            lm:150*1000, // タイピングのリミット時間（ミリ秒）
            tm:()=>(new Date()).getTime()-tm.st, // 経過時間
            rs:()=>tm.lm - tm.tm(), // 残り時間
            fm:(t)=>(Number(t)/1000).toFixed(1),
            clear:()=>{clearInterval(tm.id);},
        };
        // タイピングの記録を、履歴として記録する。
        let data = [];
        // ゲーム開始前に、+文字、-文字を打つと、初期の残り時間を増やす/減らすことができる。
		const tm_ctrl = {"+":10*1000,"-":-10*1000};

        // 時間を表示
        $("#time").text(tm.fm(tm.lm));
        // 現在のタイピングしている文字（打ち終わったもの、これから打つもの）を表示するラムダ関数
        const disp = ()=>{
            $("#done").text(text.slice(0,text_i));
            $("#type").text(text.slice(text_i));
        }
        // キーが押されたときの処理
        $(window).on("keydown",(e)=>{
            // #overlayの要素がある場合は、ゲーム開始前と判断する。
            if($("#overlay").hasClass("none")===false){
                // ゲーム開始のスペース以外が押された場合は、初期の残り時間を増減するキー以外は無視する。
                if(e.key !== " "){
                    tm.lm += e.key in tm_ctrl ? tm_ctrl[e.key] : 0;
                    $("#time").text(tm.fm(tm.lm));
                    return;
                }
                // #overlayの要素を消去して、時間計測を開始する。
                $("#overlay").toggleClass("none");
                tm.st = (new Date()).getTime();
                tm.id = setInterval(()=>{
                    $("#time").text(tm.fm(tm.rs()));
                },100);
            }
            // 入力されたキーを、タイピングする文字と一致しているか比較するために、Enterキーの変換等をする。
            const c = {
                key:e.key,
                code:Number(e.key.replace("Enter","10")),
            };
            // 入力がタイピングするキーと一致している場合、
            if(text[text_i]===c.key || text[text_i].charCodeAt()===c.code){
                // これから打つタイピング文字が、2つ以上連続するスペースである場合は、その先までこれから打つ文字を移動させる。
                text_i += 1;
				let a = 0;
				while(text[text_i]===" " && text[text_i+1]===" "){
					text_i += 1;
					a = 1;
				}
				text_i += a;
                // 現在のタイピング文字を表示する。
                disp();
            }
            // ゲーム中に表示する説明文を定義する。
            const msgs = {
                "10":"Enterを押してください。",
                "32":"スペースキーを押してください。",
            };
            // これから打つ文字によって、説明文を画面に表示する。
            const n = text.charCodeAt(text_i);
            $("#message").text(n in msgs ? msgs[n] : "");

            // すべての文字をタイプし終わったとき、
            if(text.length===text_i){
                // ゲームをリセットして、時間を履歴に追加する。
                text_i = 0;
                disp();
                $("#overlay").toggleClass("none");
                data.push(tm.rs());
                tm.clear();
                $("#hist").text(data.map(d=>tm.fm(d)).join(","));
            }
        });
    </script>
</body>
</html>
```

# タイプ音を付ける。

[1つのjsファイルで効果音を鳴らす](https://qiita.com/toyohisa/items/051823bb5c3e7dfb8832)を利用して、正解をタイプしたときにタイプ音を鳴らします。

sound.jsファイルを読み込んでおいて、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/33392976-4108-0334-7bd7-ebe1838f32f2.png)

javascriptの最初の方でsound.init()で初期化して、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/eb12429f-a859-14b1-1c65-a2440299304f.png)

正解をタイプしたときに、sound.play()でタイプ音を鳴らします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/275020/afa85343-adba-a0c2-c1cc-7215336d0b33.png)