テキストファイルをエディタで修正するスピードを競うゲーム edita.py をつくった

edita.py というゲームをつくった。

どんなゲーム?

edita_demo

ゲーム本体は edita.py というスクリプト。

実行すると、edita.py は問題文ファイルをつくった後、(ユーザーの修正を判定するため)監視に入る。

ユーザーはエディタで問題文ファイルを開いて修正する。修正できたと思ったら上書き保存。そしたら edita.py が(タイムスタンプを見てるので)更新を検知して答え合わせをする。正解するまで先に進めない。

正解したら先に進める(ラップタイムが出る)。edita.py が次の問題文をつくるので、ユーザーはエディタ側で再読込をかけて、次の問題文ファイルを修正する……と、こんな感じで遊ぶ。

全ラップを終えたら終了。合計タイムが表示される。

修正とは

問題文ファイルは小文字英単語の集まりに「一部だけ大文字が混ざった」ものだが、ユーザーのミッションは 大文字部分を見つけて、全部小文字に修正する こと。

以下は 32x10 で修正数 10 の問題文サンプル。左が問題文で、右が答え

jay liechtEnstein added shots pa           jay liechtenstein added shots pa
tch isa cAstle pErhaps dl notes            tch isa castle perhaps dl notes 
million retrieved trauma elegant           million retrieved trauma elegant
 engineer pas sellers ShakeSpear            engineer pas sellers shakespear
e hills mild gentleman dIctionar   ===>    e hills mild gentleman dictionar
y conversations heights bP hunti           y conversations heights bp hunti
ng winners bubbLe arkanSas manda           ng winners bubble arkansas manda
tory deputy decorative postage p           tory deputy decorative postage p
rocessor calvin fair combinaTion           rocessor calvin fair combination
s ocean proposal js bat skilled            s ocean proposal js bat skilled 

ちなみに修正箇所は以下のとおり。

jay liecht[E]nstein added shots pa 
tch isa c[A]stle p[E]rhaps dl notes  
million retrieved trauma elegant 
 engineer pas sellers [S]hake[S]pear 
e hills mild gentleman d[I]ctionar 
y conversations heights b[P] hunti 
ng winners bubb[L]e arkan[S]as manda 
tory deputy decorative postage p 
rocessor calvin fair combina[T]ion 
s ocean proposal js bat skilled  

ユーザーに要求される能力

  • エディタの操作スキル(特にカーソル移動系の操作)
  • 正確なタイピング
  • 大文字を素早く探せる動体視力あるいは正確性(焦るとマジで見つかりません)

ちなみにズルすれば秒単位で解ける(たとえば選択範囲を lowercase に変換する機能があれば、全選択→変換→上書き保存で一発)けど、それではゲームにならないので、ぜひカーソル移動で遊んでみてください。

実装の話

工夫したこと

最初はブラウザで遊べるようにすることを考えたが、ブラウザの textarea だとストレスが溜まってゲームどころじゃないと思い直した。愛用のテキストエディタで遊びたい。

しかし私の愛用は秀丸エディタであり、秀丸エディタのユーザー層でこういうゲームを遊ぶ人は相当限られていることが予想された。Qiita の読者層にも届くような手段が欲しい。ふとひらめいたのが、

  • ユーザーはテキストファイルをいじる
  • ゲームシステムはそのテキストファイルを監視して判定する

こんなアイデアだった。試しに書いてみたら、ああ普通に遊べるじゃん、とわかった。

ファイルの監視

無限ループでタイムスタンプ(更新日時)を調べて、前回の値と違っていたら読込をかけて判定するという形にした。

タイムスタンプは st_mtime_ns でナノ秒で取る。

def get_lastmodified_nanotime(filename):
    stat_result = os.stat(filename)
    return stat_result.st_mtime_ns

監視は以下のように無限ループ(いわゆるゲームループ)。ただし sleep しないと CPU 食い切って重くなるので 0.01 秒にしてる。

    cur_ns = get_lastmodified_nanotime(tempfilename)
    while True:
        sleep(0.01)
        latest_ns = get_lastmodified_nanotime(tempfilename)
        if latest_ns == cur_ns:
            continue
        cur_ns = latest_ns
        ...

前回のタイムスタンプ値は cur_ns に保持。今のタイムスタンプ値は latest_ns。latest_ns と cur_ns が違う値になった = タイムスタンプが変わった = ユーザー側で修正(上書き更新か)かけられたと判断している。

タイムの計測

「ゲーム開始時の現在日時」と「ゲーム終了時の現在日時」を記録しておいて、後者から前者を引くことで秒数差分を求めるというやり方にした。

Python だと datetime という素晴らしいライブラリがあるので楽できる。

以下は現在日時の datetime オブジェクト(コードでは dt という名前を付けている)を取得する関数。

def get_now_by_dt():
    """ @return A datetime object """
    return datetime.datetime.now()

以下は datetime オブジェクト二つの秒数差分(ただし精度が欲しいのでマイクロ秒)を求める関数。datetime オブジェクト同士を引くと timedelta オブジェクトになる。これも datetime ライブラリの恩恵。楽すぎる。

def diff_microseconds_between_dt_and_dt(dt_future, dt_past):
    delta = dt_future - dt_past
    microseconds = delta / datetime.timedelta(microseconds=1)
    return microseconds

最後に秒の精度を調整。1秒だと荒いし、1.111秒だと細かいかなと感じたので、1.11秒の精度に。以下のような関数を書いた。リーダブルなコード(意図をコメントで残してる)を意識してみたのですが、どうでしょうか。

def convert_microseconds_to_edita_score_float(microseconds_by_float):
    # dt1 = get_now_by_dt()
    # sleep(1.5)
    # dt2 = get_now_by_dt()
    # ms = diff_microseconds_between_dt_and_dt(dt2, dt1)
    # print(ms) 
    #    -> 1513190.0
    #
    #       1      <- too rough
    #       1.5    <- rough a little
    #       1.51   <= I chooose you!
    #       1.513  <- too grain
    return round(microseconds_by_float/1000000, 2)

問題文の作成

一行一単語が並んだファイルから単語リストを読み込んで、そこからランダムに選ぶ……という愚直なアルゴリズムにしている。解説は面倒なので割愛。

今回一番苦労したのは実は 単語ファイルの入手 だったりする。本当は edita.py にくっつけて配布したかったのだけど、配布 OK な単語ファイルが見つからなかったので「各自ダウンロードしてね」で妥協した。

※唯一見つけたのは SecLists という MIT LICENSE の単語リスト。これは「よく使われてるパスワードベストn」みたいな単語リストを扱ってる……けど、単語が偏りすぎると数字も混ざってて使いづらかったので、今回採用は見送った。