【40行で実装】Pyxelでタイピングゲームを作りたい！

## はじめに
今回はタイトルの通り、Pyxelでタイピングゲームを作ろうと思います。特に以下の三点について取り上げます。
- キー入力
- 正誤判定
- 日本語フォント

↓解説に使っているコードとは別ですが、筆者が試しに作ってみたものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3922095/efb09e73-4a6b-e0ec-7412-780680e40058.png)


## キー入力について
　~~`pygame`や`tkinter`と比べたときに、Pyxelでキーの名前を取得しようと思うと、癖があるな～と思ったので、詳しく取り上げようと思いますこのキーが押されたら～するみたいな処理だったら、`pygame`や`tkinter`と同じような感覚で出来るので、気にしたことがない人が大半だと思います。~~

### ~~違いについて~~
　~~「キーを押された」というイベントが、「どのキーを押されたか」という情報も持っているのが、`pygame`や`tkinter`です。例えば`tkinter`では、バインドされている関数の第一引数に`event`と書いて受け取るのが一般的だと思いますが、`event.char`とすると押されたキーの名前が取得できます。
 それに対して、今回Pyxelで実装した方法は、「このキーを押された」という個別のイベントを、まとめて判定しています。~~

コメントで教えていただいた`input_keys`というのを使うことで、もっと楽に扱えました。これにより、使用感としては`pygame`や`tkinter`と変わらなくなったので消しておきます。

### キーの名前の取得方法
　キーが押されているかの判定と、あらかじめ決められた数字からキーの名前を取得するという事ができるので、それらを組み合わせれば実装できます。

　キーに割り振られている定数名の一覧は以下のページにありました。数字に関しては、45～122辺りの数字に使いそうな文字がまとまっていますが、今回は諸事情により0～122を使います。

https://github.com/kitao/pyxel/blob/main/python/pyxel/__init__.pyi

早速コードを載せます。キー入力を検知して、押されたキーの名前を`print`します。

```python
import pyxel

shifted = {
    'a':'A', 'b':'B', 'c':'C', 'd':'D', 'e':'E', 'f':'F', 'g':'G', 'h':'H', 'i':'I', 'j':'J', 'k':'K', 'l':'L', 'm':'M', 'n':'N', 'o':'O', 'p':'P', 'q':'Q', 'r':'R', 's':'S', 't':'T', 'u':'U', 'v':'V', 'w':'W', 'x':'X', 'y':'Y', 'z':'Z',
    '1':'!', '2':'"', '3':'#', '4':'$', '5':'%', '6':'&', '7':"'", '8':'(', '9':')', '0':'0',
    '-':'=', '^':'~', '\\':'|', '@':'`', '[':'{', ']':'}', ';':'+', ':':'*', ',':'<', '.':'>', '/':'?',' ':' '
    }

class TypingGame:
    def __init__(self):
        pyxel.init(160, 120, fps=50)
        pyxel.run(self.update, self.draw)
    def update(self):
        for key in pyxel.input_keys:
            if key not in range(pyxel.KEY_UNKNOWN, pyxel.KEY_Z+1):    #0～122でなければ無視
                continue
            char = chr(key)    #キーの名前を取得
            if pyxel.btnp(pyxel.KEY_UNKNOWN):    #Windowsでは\が空文字になるので、空文字を\として扱う  
                char = '\\'
            if pyxel.btn(pyxel.KEY_SHIFT):    #シフト入力時
                if key == pyxel.KEY_UNKNOWN:    
                    char = '_'
                else:
                    char = shifted[char]
            print(char)

    def draw(self):
        pyxel.cls(0)
        pyxel.text(50, 60, "Press any key...", pyxel.frame_count % 16)

TypingGame()
```

　コメントに書いたのですが、筆者がWindowsを使っているので、`\`が空文字になってしまっていました。他に空文字で認識されるキーがあるかもしれませんが、タイピングゲームとかを作るなら`_`が打てることを優先したいので、空文字を`\`として扱っています。この空文字が`pyxel.KEY_UNKNOWN`で数字では`0`に当たるので、0～122としました。（使わなくてもいいなってだけで、0～44の間にもTabとかあるので、ついでに含めました）
 
　~~簡単なコードですが、Pyxel向けの記事のため、シフト入力について初心者向けに補足です。左右どちらかのシフトキーが押されたり離されたりすると、`self.shift`が`True`や`False`に切り替わるようにしてあります。このフラグが`True`の時だけ辞書を参照して、シフト後の文字を取ってくるという処理を追加することで、シフト入力に対応しています。`btnp`というのがキーを押したときで、`btnr`がキーを離したときです。（多分pressとreleaseかな）~~

 もっといい方法をコメントで教えていただいたので、コードの方は修正後のものにしてありますが、元の解説は残しておきます。（pygameでマウスのドラッグを実装するときによく使うから、それに引っ張られたのかな...）

## 正誤判定について
　正誤判定の部分は、`typehandler`というモジュールがあるので、これに任せれば実装できます。（差分表示の下にコピー用のコードも置いておきます）
```sh
python -m pip install typehandler
```

```diff_python
import pyxel
+ from typehandler import Process

shifted = {
    'a':'A', 'b':'B', 'c':'C', 'd':'D', 'e':'E', 'f':'F', 'g':'G', 'h':'H', 'i':'I', 'j':'J', 'k':'K', 'l':'L', 'm':'M', 'n':'N', 'o':'O', 'p':'P', 'q':'Q', 'r':'R', 's':'S', 't':'T', 'u':'U', 'v':'V', 'w':'W', 'x':'X', 'y':'Y', 'z':'Z',
    '1':'!', '2':'"', '3':'#', '4':'$', '5':'%', '6':'&', '7':"'", '8':'(', '9':')', '0':'0',
    '-':'=', '^':'~', '\\':'|', '@':'`', '[':'{', ']':'}', ';':'+', ':':'*', ',':'<', '.':'>', '/':'?',' ':' '
    }

+ words = {
+     'Pyxel':'Pyxel',
+     'typing':'typing',
+     'Python':'Python'
+     }

+ process = Process(words)    #お題とフリガナの辞書を渡しておく

class TypingGame:
    def __init__(self):
        pyxel.init(160, 120, fps=50)
        pyxel.run(self.update, self.draw)
    def update(self):
+         process.update_show_roman()    #おまじない
        #キー入力の判定部分
        for key in pyxel.input_keys:
            if key not in range(pyxel.KEY_UNKNOWN, pyxel.KEY_Z+1):    #0～122でなければ無視
                continue
            char = chr(key)    #キーの名前を取得
            if pyxel.btnp(pyxel.KEY_UNKNOWN):    #Windowsでは\が空文字になるので、空文字を\として扱う  
                char = '\\'
            if pyxel.btn(pyxel.KEY_SHIFT):    #シフト入力時
                if key == pyxel.KEY_UNKNOWN:    
                    char = '_'
                else:
                    char = shifted[char]
-             print(char)
+             process.main(char)    #これ一行で正誤判定から文章の更新まで行う

    def draw(self):
        pyxel.cls(0)
-         pyxel.text(50, 60, "Press any key...", pyxel.frame_count % 16)
+         pyxel.text(20, 40, process.show_roman, 13)    #入力パターンの一例
+         pyxel.text(20, 40, process.input, 14)         #現在の入力
+         pyxel.text(20, 60, process.sentence, 7)       #お題

TypingGame()
```

<details><summary>コピー用</summary>

```python
import pyxel
from typehandler import Process

shifted = {
    'a':'A', 'b':'B', 'c':'C', 'd':'D', 'e':'E', 'f':'F', 'g':'G', 'h':'H', 'i':'I', 'j':'J', 'k':'K', 'l':'L', 'm':'M', 'n':'N', 'o':'O', 'p':'P', 'q':'Q', 'r':'R', 's':'S', 't':'T', 'u':'U', 'v':'V', 'w':'W', 'x':'X', 'y':'Y', 'z':'Z',
    '1':'!', '2':'"', '3':'#', '4':'$', '5':'%', '6':'&', '7':"'", '8':'(', '9':')', '0':'0',
    '-':'=', '^':'~', '\\':'|', '@':'`', '[':'{', ']':'}', ';':'+', ':':'*', ',':'<', '.':'>', '/':'?',' ':' '
    }

words = {
    'Pyxel':'Pyxel',
    'typing':'typing',
    'Python':'Python'
    }

process = Process(words)    #お題とフリガナの辞書を渡しておく

class TypingGame:
    def __init__(self):
        pyxel.init(160, 120, fps=50)
        pyxel.run(self.update, self.draw)
    def update(self):
        process.update_show_roman()    #おまじない
        #キー入力の判定部分
        for key in pyxel.input_keys:
            if key not in range(pyxel.KEY_UNKNOWN, pyxel.KEY_Z+1):    #0～122でなければ無視
                continue
            char = chr(key)    #キーの名前を取得
            if pyxel.btnp(pyxel.KEY_UNKNOWN):    #Windowsでは\が空文字になるので、空文字を\として扱う  
                char = '\\'
            if pyxel.btn(pyxel.KEY_SHIFT):    #シフト入力時
                if key == pyxel.KEY_UNKNOWN:    
                    char = '_'
                else:
                    char = shifted[char]

            process.main(char)    #これ一行で正誤判定から文章の更新まで行う

    def draw(self):
        pyxel.cls(0)
        pyxel.text(20, 40, process.show_roman, 13)    #入力パターンの一例
        pyxel.text(20, 40, process.input, 14)         #現在の入力
        pyxel.text(20, 60, process.sentence, 7)       #お題

TypingGame()
```

</details>

## 日本語フォントについて
　とりあえず簡単な英字で説明しましたが、`typehandler`は日本語にも対応しているので、せっかくなら日本語のタイピングゲームを作っていきましょう。それにあたって、Pyxel側も日本語に対応させる必要があるので、その方法を説明していきます。

まずは`bdf`形式のフォントを用意します。以下のサイトのダウンロードリンクから、`k8x12.bdf`をダウンロードしてください。（他にフォントがある方は自分の好みのものでOK）

https://littlelimit.net/k8x12.htm

次に、先ほどのコードに以下の行を追加します

```diff_python
process = Process(words)
+ font = pyxel.Font("k8x12.bdf")
```

```diff_python
    def draw(self):
        pyxel.cls(0)
-         pyxel.text(20, 40, process.show_roman, 13)    #入力パターンの一例
-         pyxel.text(20, 40, process.input, 14)         #現在の入力
-         pyxel.text(20, 60, process.sentence, 7)       #お題
+         pyxel.text(20, 40, process.show_roman, 13, font)    #入力パターンの一例
+         pyxel.text(20, 40, process.input, 14, font)         #現在の入力
+         pyxel.text(20, 60, process.sentence, 7, font)       #お題
```

これで日本語が表示できるようになったので、`words`のお題を日本語のものに変えてみましょう。好きなもので構いませんが、以下に例を置いておきます（ついでにコード全体）

```python
import pyxel
from typehandler import Process

shifted = {
    'a':'A', 'b':'B', 'c':'C', 'd':'D', 'e':'E', 'f':'F', 'g':'G', 'h':'H', 'i':'I', 'j':'J', 'k':'K', 'l':'L', 'm':'M', 'n':'N', 'o':'O', 'p':'P', 'q':'Q', 'r':'R', 's':'S', 't':'T', 'u':'U', 'v':'V', 'w':'W', 'x':'X', 'y':'Y', 'z':'Z',
    '1':'!', '2':'"', '3':'#', '4':'$', '5':'%', '6':'&', '7':"'", '8':'(', '9':')', '0':'0',
    '-':'=', '^':'~', '\\':'|', '@':'`', '[':'{', ']':'}', ';':'+', ':':'*', ',':'<', '.':'>', '/':'?',' ':' '
    }

words = {
    'タイピングゲーム':'たいぴんぐげーむ',
    'Pyxelでも':'Pyxelでも',
    '作ってみたよ':'つくってみたよ'
    }

process = Process(words)    #お題とフリガナの辞書を渡しておく
font = pyxel.Font("k8x12.bdf")

class TypingGame:
    def __init__(self):
        pyxel.init(160, 120, fps=50)
        pyxel.run(self.update, self.draw)
    def update(self):
        process.update_show_roman()    #おまじない
        #キー入力の判定部分
        for key in pyxel.input_keys:
            if key not in range(pyxel.KEY_UNKNOWN, pyxel.KEY_Z+1):    #0～122でなければ無視
                continue
            char = chr(key)    #キーの名前を取得
            if pyxel.btnp(pyxel.KEY_UNKNOWN):    #Windowsでは\が空文字になるので、空文字を\として扱う  
                char = '\\'
            if pyxel.btn(pyxel.KEY_SHIFT):    #シフト入力時
                if key == pyxel.KEY_UNKNOWN:    
                    char = '_'
                else:
                    char = shifted[char]

            process.main(char)    #これ一行で正誤判定から文章の更新まで行う

    def draw(self):
        pyxel.cls(0)
        pyxel.text(20, 40, process.show_roman, 13, font)    #入力パターンの一例
        pyxel.text(20, 40, process.input, 14, font)         #現在の入力
        pyxel.text(20, 60, process.sentence, 7, font)       #お題

TypingGame()
```

たった４０行ほどで本格的なタイピングゲームが実装できました！

## 最後に
　ここまで読んで下さりありがとうございました。Pyxelってデフォルトだと日本語対応してないんだーと思っていたんですが、ビットマップフォントを読み込むだけなら、ほぼ手間０で導入できますね。
　キー入力の方も簡単に実装できたので、この記事を役立てて、タイピングゲームを作る人が増えればいいなと思います！

　最後まで読んで下さった方へ、最初に見せた画像のコードも置いておきます（70行くらい）。ちなみにmainの第二引数に`True`を渡すと、シフト入力を勝手に実装してくれるので、その機能を使っています。`|`だけは使えませんが、マニアックな文章でなければ、この機能で事足りるでしょう。


<details><summary>コードを見る</summary>

```python
import pyxel
from typehandler import Process

words = {
    '画竜点睛を欠く':'がりょうてんせいをかく',
    '十把一絡げ':'じっぱひとからげ',
    'ヘキサクロロシクロヘキサン':'へきさくろろしくろへきさん',
    '部分分数分解':'ぶぶんぶんすうぶんかい',
    'for i in range(100):':'for i in range(100):',
    'random.randint(0, 10):':'random.randint(0, 10):',
    'I wanna eat salmon.':'I wanna eat salmon.',
    "Shall we dance?":"Shall we dance?"
    }

process = Process(words)

def draw_text_with_border(x, y, s, col, bcol, font):
    for dx in range(-1, 2):
        for dy in range(-1, 2):
            if dx != 0 or dy != 0:
                pyxel.text(
                    x + dx,
                    y + dy,
                    s,
                    bcol,
                    font,
                )
    pyxel.text(x, y, s, col, font)

font = pyxel.Font("k8x12.bdf")

class TypingGame:
    def __init__(self):
        self.miss = False
        self.sentence_count = 0
        pyxel.init(160, 120, fps=50)
        pyxel.run(self.update, self.draw)
    def update(self):
        process.update_show_roman()
        if pyxel.btnp(pyxel.KEY_LSHIFT)  or pyxel.btnp(pyxel.KEY_RSHIFT):
            self.shift = True
        if pyxel.btnr(pyxel.KEY_LSHIFT)  or pyxel.btnr(pyxel.KEY_RSHIFT):
            self.shift = False
        #キー入力の判定部分
        for key in pyxel.input_keys:
            if key not in range(pyxel.KEY_UNKNOWN, pyxel.KEY_Z+1):    #0～122でなければ無視
                continue
            char = chr(key)    #キーの名前を取得
            if pyxel.btnp(pyxel.KEY_UNKNOWN):    #Windowsでは\が空文字になるので、空文字を\として扱う  
                char = '\\'
                #正誤判定部分
            result = process.main(char, pyxel.btn(pyxel.KEY_SHIFT))
            if result == process.SENTENCE_COMPLETE:
                self.sentence_count += 1
            elif result == process.MISS:
                self.miss = True

    def draw(self):
        pyxel.cls(0)
        if self.miss:
            pyxel.cls(4)
            self.miss = False
            
        pyxel.text(10, 10, f'打ち終わった数：{self.sentence_count}', 7, font)
        pyxel.text(20, 40, process.show_roman, 7, font)
        draw_text_with_border(20, 40, process.input, 7, 10, font)
        pyxel.text(20, 60, process.sentence, 7, font)
        pyxel.text(20, 100, 'next=>'+process.next, 13, font)

TypingGame()
```

</details>

## 参考
Pyxelについてはこちら

https://github.com/kitao/pyxel/blob/main/docs/README.ja.md

キー入力のところで紹介したリンク（再掲）

https://github.com/kitao/pyxel/blob/main/python/pyxel/__init__.pyi

`typehandler`についてはこちら

https://github.com/prosamo/typehandler