タイピングソフトを作ってみる

## はじめに

特打というソフトがあります。特打はタイピングソフトです。手元にある特打のパッケージには「630万本突破 1番売れているタイピングソフト」とあります。私も特打を使ったことがあります。素晴らしいソフトだと思います。そこで、特打のようなタイピングソフトを目標として、ブラウザで動くタイピングソフトをつくってみました。今回も、どんなものつくったのか、どのようにつくったのか、これらについて書いてみたいと思います。

## どんなものをつくったのか

青いキーを押すと、別のキーが青くなります。
(稀に、同じキーが青くなることがあります。)

![touch_sample.png](https://qiita-image-store.s3.amazonaws.com/0/29884/2f691566-ab6e-fa2e-bdb4-612f897a1417.png)


青いキー以外を押すと、押したキーが赤くなります。

![touch_sample2.png](https://qiita-image-store.s3.amazonaws.com/0/29884/b3110ef9-9311-ed0e-0755-e83241fd71ee.png)


得点や結果のレポートはありません。自分の苦手なキーを分析することもできません。(これをタイピングソフトといってよいものか。。。)

## どうやってつくったのか

作業は次のような流れでした。

- コマンドライン
- ノート１
- 実装(キーイベント)
- ノート２
- プロトタイプ
- 実装(乱数)
- 実装(キーボード)
- ノート１の残り
- 実装(まとめ)

紆余曲折して最終的なカタチとなったので、その辺も記録として残しておきたいという気持ちがあり、今回は作業内容を簡単に紹介しようと思います。

### コマンドライン

まずは、次のようなコマンドを実行します。

```
$ rbenv rails new touchtyping
$ cd touchtyping
$ rbenv rails generate controller Touch sample
```

### ノート１

ここではタイピングソフトのイメージをつかむために思いついたことをノートに書き出します。書き出した内容は次のような内容でした。

- 正しいタイピングなら青く光る
- 間違ったタイピングなら赤く光る
- 乱数を使う
- 遅延リストを使ってみたい
- キーイベントを拾う
- 画面中央に文字を表示する
- 時間を計測する

### 実装(キーイベント)

ノート１で書き出した項目を見比べて、キーイベントを拾うという項目は必須項目のように思えたので、その項目から実装してみることにしました。この辺はインターネットでサンプルコードを見つけることができるので、それらを参考に実装しました。実装は次のようになりました。

```
  window.onkeydown = (e) ->
    keycode = e.keyCode
    if 48 <= keycode && keycode < 58 ||
       65 <= keycode && keycode < 91 ||
       keycode == 186 ||
       keycode == 188 ||
       keycode == 190 ||
       keycode == 191
         alert keycode
```

この動作を確認することで、キーコードを文字に変える必要があると分かり、そのためにマップを用意しました。

### ノート２

キーイベントを拾うことは確認できましたので、次に処理の流れを考えます。まずは簡単なケースを考えます。これもノートに書き出します。次のような処理を書き出しました。

- Aを表示する
- Aを打鍵する
- Bを表示する(AをBに更新する)

当たり前の処理かもしれませんが、次のような処理も書き出しました。

- Aを表示する
- A以外を打鍵する
- Aを更新しない

将来的にはどんな感じにしたいかについても考えます。ひとまず目標を忘れないようにノートに書き出しておきます。

- ランダムな文字を表示する
- 表示された文字を打鍵する
- ランダムな文字に更新する

### プロトタイプ

ビューを実装して(といっても`<p>A</p>`を追加するだけですが)、ノート２を実装します。そして、文字が更新されること、更新されないことを確認します。

### 実装(乱数)

これもインターネットで見つけることができました。`Math.random()`を使えば、乱数を取得することができます。

### 実装(キーボード)

乱数を利用するために、乱数を文字に変える処理を考えました。はじめはマップでその処理を用意しようと考えたのですが、次のようなビューを用意することでビューから文字を取得することにしました。

このアイデアはプロトタイプを動かしていたときにキーボードのようなビューが必要だと痛感したことが大きいと思います。次のように実装してキーボードのようなビューを用意しました。

```
<h1>Touch#sample</h1>
<p>Find me in app/views/touch/sample.html.erb</p>

<table id="board">
  <% key = [[1,2,3,4,5,6,7,8,9,0], ['Q','W','E','R','T','Y','U','I','O','P'], ['A','S','D','F','G','H','J','K','L',';'], ['Z','X','C','V','B','N','M',',','.','/']] %>
  <% (0...4).each do |i| %>
  <tr>
    <% (0...10).each do |j| %>
    <td style="width: 50px; height: 50px; font-size: 50px; background: #FFF0F0"><%= key[i][j] %></td>
    <% end %>
  </tr>
  <% end %>
</table>
```

### ノート１の残り

いろいろ実装していない項目が残っているのですが、最低限この項目は実装しておきたい、この項目は見当違いだ、などの理由から次の項目を追加で実装することにしました。

- 正しいタイピングなら青く光る
- 間違ったタイピングなら赤く光る

インターネットでCSSの`box-shadow`を使えば光ることが分かったのですが、実装してみると思っていたほどよくありませんでした。直感的な見た目でタイピングをアブダクションさせるような感じにしたかったので、キーを光らせる代わりに、キーを青や赤にしてみました。(タイピングソフトの雰囲気をかもしだせていれば嬉しいのです。)

### 実装(まとめ)

あとは黙々と目標に向かって、つじつまを合わせるように実装するだけです。途中、typoではまったりしないように慎重に実装します。最終的な実装は次のようになりました。

まずはビューの実装。

```
<h1>Touch#sample</h1>
<p>Find me in app/views/touch/sample.html.erb</p>

<table id="board">
  <% key = [[1,2,3,4,5,6,7,8,9,0], ['Q','W','E','R','T','Y','U','I','O','P'], ['A','S','D','F','G','H','J','K','L',';'], ['Z','X','C','V','B','N','M',',','.','/']] %>
  <% (0...4).each do |i| %>
  <tr>
    <% (0...10).each do |j| %>
    <td style="width: 50px; height: 50px; font-size: 50px; background: #FFF0F0"><%= key[i][j] %></td>
    <% end %>
  </tr>
  <% end %>
</table>
```

次にスタイルシートの実装。

```
// Place all the styles related to the Touch controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#board td {
  border-radius: 10px;
}
// vim: set sw=2 sts=2 et :
```

そして、動作の実装。

```
# Place all the behaviors and hooks related to the matching controller here.
# All this logic will automatically be available in application.js.
# You can use CoffeeScript in this file: http://coffeescript.org/

class Touch
  $board: null
  _keyname: null
  row: null
  col: null
  shadow: false
  myRow: null
  myCol: null

  _toKeyName: { 48:'0', 49:'1', 50:'2', 51:'3', 52:'4',\
                53:'5', 54:'6', 55:'7', 56:'8', 57:'9',
                65:'A', 66:'B', 67:'C', 68:'D', 69:'E',
                70:'F', 71:'G', 72:'H', 73:'I', 74:'J',
                75:'K', 76:'L', 77:'M', 78:'N', 79:'O',
                80:'P', 81:'Q', 82:'R', 83:'S', 84:'T',
                85:'U', 86:'V', 87:'W', 88:'X', 89:'Y', 90:'Z',
                186:';', 188:',', 190:'.', 191:'/' }

  _toCol:  { '1':0, '2':1, '3':2, '4':3, '5':4, '6':5, '7':6, '8':7, '9':8, '0':9,\
             'Q':0, 'W':1, 'E':2, 'R':3, 'T':4, 'Y':5, 'U':6, 'I':7, 'O':8, 'P':9,
             'A':0, 'S':1, 'D':2, 'F':3, 'G':4, 'H':5, 'J':6, 'K':7, 'L':8, ';':9,
             'Z':0, 'X':1, 'C':2, 'V':3, 'B':4, 'N':5, 'M':6, ',':7, '.':8, '/':9 }

  _toRow:  { '1':0, '2':0, '3':0, '4':0, '5':0, '6':0, '7':0, '8':0, '9':0, '0':0,\
             'Q':1, 'W':1, 'E':1, 'R':1, 'T':1, 'Y':1, 'U':1, 'I':1, 'O':1, 'P':1,
             'A':2, 'S':2, 'D':2, 'F':2, 'G':2, 'H':2, 'J':2, 'K':2, 'L':2, ';':2,
             'Z':3, 'X':3, 'C':3, 'V':3, 'B':3, 'N':3, 'M':3, ',':3, '.':3, '/':3 }

  _noShadow: (row, col) ->
    key = @$board.filter("[data-row = #{row}]").children("[data-column = #{col}]")
    style = key.attr("style")
    ns = style.replace(/^(.+background: )(.+)$/, "$1#FFF0F0")
    key.attr("style", ns)

  _postproc: ->
    if @shadow is true
      @_noShadow(@myRow, @myCol)
      @shadow = false

  _shadow: (row, col) ->
    if @shadow is false
      key = @$board.filter("[data-row = #{row}]").children("[data-column = #{col}]")
      style = key.attr("style")
      ns = style.replace(/^(.+background: )(.+)$/, "$1#F00")
      key.attr("style", ns)
      @shadow = true

  _noNotice: (row, col) ->
    key = @$board.filter("[data-row = #{row}]").children("[data-column = #{col}]")
    style = key.attr("style")
    ns = style.replace(/^(.+background: )(.+)$/, "$1#FFF0F0")
    key.attr("style", ns)

  _equal: (keycode) ->
    keyname = @_toKeyName[keycode]
    if @_keyname is keyname
      @_noNotice(@row, @col)
      @_random()
    else
      @myRow = @_toRow[keyname]
      @myCol = @_toCol[keyname]
      @_shadow(@myRow, @myCol)

  _notice: (row, col) ->
    key = @$board.filter("[data-row = #{row}]").children("[data-column = #{col}]")
    style = key.attr("style")
    ns = style.replace(/^(.+background: )(.+)$/, "$1#0050FF")
    key.attr("style", ns)
    @_keyname = key.text()

  _random: ->
    @row = Math.ceil(Math.random() * 123) % 4
    @col = Math.ceil(Math.random() * 123) % 10
    @_notice(@row, @col)

  _prepare: ->
    @$board = $("#board tr")
    
    j = 0
    @$board.each ->
      $(@).attr("data-row": j)
      j = j + 1
      i = 0
      $(@).children("td").each ->
        $(@).attr("data-column": i)
        i = i + 1

  constructor: ->
    @_prepare()
    @_random()
    @

$ ->
  touch = new Touch()
  window.onkeydown = (e) ->
    keycode = e.keyCode
    if 48 <= keycode && keycode < 58 ||
       65 <= keycode && keycode < 91 ||
       keycode == 186 ||
       keycode == 188 ||
       keycode == 190 ||
       keycode == 191
      touch._equal(keycode)

  window.onkeyup = (e) ->
    keycode = e.keyCode
    if 48 <= keycode && keycode < 58 ||
       65 <= keycode && keycode < 91 ||
       keycode == 186 ||
       keycode == 188 ||
       keycode == 190 ||
       keycode == 191
      touch._postproc()

# vim: set sw=2 sts=2 et :
```

## まとめ

タイピングソフトが作れたと思います。特打には及びませんが、気楽にタイピングソフトを楽しめるかと思います。[パレットを作ってみる](http://qiita.com/t-mochizuki/items/842d312f8b0fa99ccdc5)から始まった流れに乗ってタイピングソフトまで来れました。やはり、手を動かすことは大切なのだろうと思います。

じつは、ひとつ気になっていることがあります。あるキー(例えばA)を押しながら、別のキー(例えばB)を押して離すと、ちょっと期待しない結果になります。