どうしても漢文テクニカルクイズがやりたかったので自分で作った話


概要

SEGAが提供していたゲーム"Answer×Answer"でプレイできた"漢文テクニカルクイズ"がどうしてもプレイしたかったので自作しました
簡単なプログラムですが、自分の覚書も兼ねてちょっとした知識を共有します
主な内容:漢字の抽出、漢文表示の仕組み

制作物

こんな感じです

実際に作ったソフトはこちら(github)
ゲームのプレイ方法などはリンク先を参照にしてください

情報系の学生をしています。Qiitaへの投稿は初です
クイズはガチ勢の入り口レベルです

漢文テクニカルクイズって??

ルール

漢文テクニカルクイズは、早押しクイズの形式の1つです
このルールではまず、問題文中の漢字のみが出力され、そこから徐々に問題が表示されていきます
回答者は、漢字のみの状態からクイズを推測し、答えを導き出します

例題

(初期状態)
山登質問対山答知登山家誰

「山登質問対山答知登山家誰

「な山登質問対山答知登山家誰

「なぜ山登質問対山答知登山家誰

「なぜ山に登質問対山答知登山家誰

「なぜ山に登る質問対山答知登山家誰



(出題終了時)
「なぜ山に登るのか」という質問に対し、「そこに山があるからだ」と答えたことで知られる登山家は誰でしょう?(答:ジョージ・マロリー)

wxPython

GUIアプリケーションを作るに当たって今回はwxPythonと呼ばれるフレームワークを使用しました。
文字列の表示・ボタン進行などの比較的単純な機能があれば十分だと判断したことと、普段使いしているpythonが使用できることが採用理由です
今にして思うと他のフレームワークの方がよかったかも
wxPythonの基本的な使い方は本記事の主題とはそれるので割愛します。必要な内容のみ随時補足していきます

漢文表示

本記事のメインディッシュです。漢文テクニカルクイズを実現するにあたり、以下の仕組みが必要な要件として挙げられます。

  • 問題文から漢字のみの文章(漢文)を作る仕組み
  • 漢字の文章を表示した後、徐々に全文を表示する仕組み

この2つの仕組みを実現した方法をまとめていきます

漢字を抽出する仕組み

kanbun = ""
for i in row[1]:
    # unicodeから漢字を判定
    if("CJK UNIFIED" in unicodedata.name(i)):
         kanbun += i

row[1]は問題ファイルから読み込んできた問題が入っているstr型のデータです。
pythonでは、for文のオブジェクトにstr型を与えると、変数として1文字ずつ返却します
例えば実行したコードが

foo = "quiz"
for i in foo:
    print(quiz)

なら、出力は

q
u
i
z

となります。
これで問題文を頭から1文字ずつ確認していきます。
漢字の判別を行うのはunicodedataモジュールです。unicodedataはpythonに標準で付属するため、インストールは不要です。
unicodedataモジュールのname関数は、引数として与えた文字の名前(内部値)をstr型で返します。
文字の名前ってなんぞや?具体的には、以下のようなものです。

  • あ: HIRAGANA LETTER A
  • ア: KATAKANA LETTER A
  • A : LATIN CAPITAL LETTER A
  • 亜: CJK UNIFIED IDEOGRAPH-4E9C
  • 0 : DIGIT ZERO

見てもらえますとわかる通り、この名前には「HIRAGANA」「LATIN CAPITAL」など、文字の形態を表す部分が含まれています。
この部分は、漢字だと「CJK UNIFIED」となるので、「CJK UNIFIED」が文字の名前に入っていれば漢字、と判定できるわけです。
この判定を全文字に対して繰り返し、抽出した文字を繋ぎ合わせれば、漢文の完成です。

全文を表示する仕組み

全文を表示する際、問題文の先頭から少しずつ漢字以外の文字を表示していくのですが、ある時点で表示されてる文章は、全文の先頭部分と漢文の末尾部分を組み合わせたものと見て取れます。
例えば"「なぜ山に登るのか質問対山答知登山家誰"という文章は、"「なぜ〜のか"までが全文の先頭部分、"質問〜誰"が漢文の末尾部分です。
すなわち、全文の表示を少しずつ増やし、適切なところから漢文を表示すれば、基本的な問題文の出力が可能になります。

def reloadquestion(self, event):
    while True:
        if(self.counter == self.call_num):
            self.btn_start.Disable()
            self.btn_stop.Disable()
            self.timer.Stop()

        if(Quiz.zenbun_now[self.counter + self.pointer] == Quiz.kanbun_now[self.pointer] and self.pointer < len(Quiz.kanbun_now) - 1):
            self.pointer += 1
        elif(Quiz.zenbun_now[self.counter + self.pointer] == Quiz.kanbun_now[self.pointer] and self.pointer == len(Quiz.kanbun_now) - 1 and self.last_flg):
            self.last_flg = False
            break
        else:
            break

    zenbun_output = Quiz.zenbun_now[:self.counter + self.pointer + 1]
    if(self.pointer != len(Quiz.kanbun_now) - 1 or self.last_flg):
        zenbun_output += Quiz.kanbun_now[self.pointer:]

    for i in range(int(len(Quiz.zenbun_now) / 20)):
        zenbun_output = zenbun_output[:20 *
                                      (i + 1) + i] + "\n" + zenbun_output[20 * (i + 1) + i:]

    self.text.SetLabel(zenbun_output)
    self.counter += 1

問題の表示を司るreloadquestion関数です。
漢文の表示にはcounterとpointerという2つの値を用いています。
counterはreloadquestionを呼び出した回数です。性質上、全文の先頭部分で、漢字以外の文字を何文字出力するかを示す値と一致します。pointerは表示中の問題文の後ろにつく漢文が何文字目からスタートするかを示す値です。pointerは、全文の(counter+pointer)文字目と漢文のpointer文字目が一致した時に増加するようになっています。
counterとpointerが確定したら"全文の1~(counter+pointer)文字+漢文のpointer~最終文字"をその時点の問題文として出力します。これを繰り返すことで、漢字以外の文字が1文字ずつ現れる様子を再現します。
ちなみに、20文字ごとに"\n"を挟んでいるのは改行のためです。改行しないとウィンドウ上に問題が表示されなくなります。
前述のunicodedataを上手く使えば、文字数でなく文字の形状(半角/全角)を意識した綺麗な改行ができそうですね
問題文の表示終了はcounterの値とcall_num(=問題文の文字数-漢字の文字数)の一致で判定しています。

ちなみに、reloadquestionは一定間隔で繰り返し呼び出す必要があるのですが、これはwxPythonのTimerオブジェクトを使用します
Timerオブジェクトは、以下のように宣言しておきます

    self.timer = wx.Timer(self)
    self.Bind(wx.EVT_TIMER, self.reloadquestion)

まずwx.Timerで、Timerオブジェクトを生成しておきます。
Bindでは第一引数としてwx.EVT_TIMERを与えます。wx.EVT_TIMERを与えることで「一定時間間隔で関数を呼び出す」ことを明示します。第二引数がTimerオブジェクトで呼び出される関数です。準備はこれだけです。
実行開始と終了は以下の通りです。

    self.timer.Start(self.flame)
    self.timer.Stop()

Startでは引数として実行間隔をミリ秒で指定します。これにより、Bindで指定しておいた関数を指定の時間間隔で立て続けに実行します。実行を停止したい場合はStop()を呼び出せばOKです。
たったこれだけで一定間隔で任意の関数を繰り返し呼び出すことが可能になります。
Start,Stopをボタンオブジェクトに紐づければ「Startボタンで処理を開始/Stopボタンで処理を停止」みたいな処理が簡単に実装できます。
便利なもんですね…

終わりに

いかがでしたでしょうか。大した話はかけていませんが、たった一人にでも、役にたつ内容であれたら嬉しいです
wxPythonは初めて触りましたが、ほどほどのUIでソフトウェアを作るのには向いていると感じました。今回は触れていませんが、文字列の表示とかもうちょっと柔軟にできたら嬉しかったですね
なかなか力技なプログラムになってしまったので、もう少し賢いプログラムがかけるようになりたいものです

余談

あわよくばQuizKnockさんとかにプレイしてもらえないかな…と思っているうちに似たような内容のクイズがプレイされていました。
問題文も凝ってて面白いので是非視聴してみてください。