0
0

Pythonで〇×ゲームのAIを一から作成する その84 IntSliderとキー入力によるGUIの実装

Last updated at Posted at 2024-05-26

目次と前回の記事

これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

リプレイ中の背景色の変更

前回の記事で、〇×ゲームの GUI の基本的な機能の実装が完了しましたが、まだ改良の余地があるので、今回の記事では引き続きいくつかの改良を行うことにします。

現状では、リプレイ機能を使って 過去の局面を表示 している場合と、最後の着手が行われた 現在の局面を表示 している場合の 区別がつきづらい という問題があります。実際には、> と >> ボタンが灰色になっているかどうか で区別することができますが、わかりやすいとは言えない でしょう。そこで、リプレイ中の局面を表示している場合に、以下のように表示することで、リプレイ中であるかどうかを簡単に区別をできる ようにすることにします。

  • 背景色を水色で表示する
  • 手番の後に "Replay" というメッセージを表示する

下記は、そのように ゲーム盤の描画を行う draw_board メソッドを修正したプログラムです。なお、背景色やメッセージを別のものに設定したい人は自由に変更して下さい。

  • 5 行目:リプレイ中かどうかを判定し、is_replay に代入する
  • 6 ~ 9 行目:ゲーム盤の背景色を表す facecolor に適切な色を代入する
  • 22、23 行目:リプレイ中の場合に、手番の後に "Replay" を表示するようにする
 1  from marubatsu import Marubatsu, Marubatsu_GUI
 2  
 3  def draw_board(self):   
元と同じなので省略
 4      # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
 5      is_replay =  self.mb.move_count < len(self.mb.records) - 1 
 6      if self.mb.status == Marubatsu.PLAYING:
 7          facecolor = "lightcyan" if is_replay else "white"
 8      else:
 9          facecolor = "lightyellow"
10
11      ax.figure.set_facecolor(facecolor)
元と同じなので省略
12      # ゲームの決着がついていない場合は、手番を表示する
13      if self.mb.status == Marubatsu.PLAYING:
14          text = "Turn " + self.mb.turn
15      # 引き分けの場合
16      elif self.mb.status == Marubatsu.DRAW:
17          text = "Draw game"
18      # 決着がついていれば勝者を表示する
19      else:
20          text = "Winner " + self.mb.status
21      # リプレイ中の場合は "Replay" を表示する
22      if is_replay:
23          text += " Replay"
24      ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略
25
26  Marubatsu_GUI.draw_board = draw_board
行番号のないプログラム
from marubatsu import Marubatsu, Marubatsu_GUI

def draw_board(self):   
    ax = self.ax
    ai = self.mb.ai
    
    # Axes の内容をクリアして、これまでの描画内容を削除する
    ax.clear()
    
    # y 軸を反転させる
    ax.invert_yaxis()
    
    # 枠と目盛りを表示しないようにする
    ax.axis("off")   
    
    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
    is_replay =  self.mb.move_count < len(self.mb.records) - 1 
    if self.mb.status == Marubatsu.PLAYING:
        facecolor = "lightcyan" if is_replay else "white"
    else:
        facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
        
    # 上部のメッセージを描画する
    # 対戦カードの文字列を計算する
    names = []
    for i in range(2):
        names.append("人間" if ai[i] is None else ai[i].__name__)
    ax.text(0, 3.5, f"{names[0]} VS {names[1]}", fontsize=20)   
    
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
    if is_replay:
        text += " Replay"
    ax.text(0, -0.2, text, fontsize=20)
    
    # ゲーム盤の枠を描画する
    for i in range(1, self.mb.BOARD_SIZE):
        ax.plot([0, self.mb.BOARD_SIZE], [i, i], c="k") # 横方向の枠線
        ax.plot([i, i], [0, self.mb.BOARD_SIZE], c="k") # 縦方向の枠線   

    # ゲーム盤のマークを描画する
    for y in range(self.mb.BOARD_SIZE):
        for x in range(self.mb.BOARD_SIZE):
            color = "red" if (x, y) == self.mb.last_move else "black"
            self.draw_mark(ax, x, y, self.mb.board[x][y], color)            

    self.update_widgets_status()  
    
Marubatsu_GUI.draw_board = draw_board
修正箇所
from marubatsu import Marubatsu, Marubatsu_GUI

def draw_board(self):   
元と同じなので省略
    # リプレイ中、ゲームの決着がついていた場合は背景色を変更する
-   facecolor = "white" if self.mb.status == Marubatsu.PLAYING else "lightyellow"
+   is_replay =  self.mb.move_count < len(self.mb.records) - 1 
+   if self.mb.status == Marubatsu.PLAYING:
+       facecolor = "white"
+       facecolor = "lightcyan" if is_replay else "white"
+   else:
+       facecolor = "lightyellow"

    ax.figure.set_facecolor(facecolor)
元と同じなので省略
    # ゲームの決着がついていない場合は、手番を表示する
    if self.mb.status == Marubatsu.PLAYING:
        text = "Turn " + self.mb.turn
    # 引き分けの場合
    elif self.mb.status == Marubatsu.DRAW:
        text = "Draw game"
    # 決着がついていれば勝者を表示する
    else:
        text = "Winner " + self.mb.status
    # リプレイ中の場合は "Replay" を表示する
+   if is_replay:
+       text += " Replay"
    ax.text(0, -0.2, text, fontsize=20)
元と同じなので省略

Marubatsu_GUI.draw_board = draw_board

上記の修正後に、下記のプログラムで gui_play を実行し、< ボタンなどをクリックしてリプレイ中の局面を表示すると、実行結果のように背景色が水色のゲーム盤と、手番の後に "Replay" が表示されるようになります。なお、下記では、すぐにリプレイを行えるように、AI どうしの対戦を行いました。

from ai import ai1

gui_play(ai=[ai1, ai1])

実行結果(下図は、画像なので操作することはできません)

IntSlider による任意の手数への移動

現状のリプレイ機能のボタンでは、一度のクリック操作で 任意の手数の局面に移動することはできません。そこで、任意の手数の局面に移動するための GUI を実装することにします。

IntSlider の属性と表示

ipywidgets には、IntSlider という、整数の値を選択 できるスライダーのウィジェットが用意されています。IntSlider には、主に下記の属性があります。

属性 意味 デフォルト値
value 現在の値 0
min 最小値 0
max 最大値 100
step 選択できる数値の間隔 1
description 左に表示される説明 ""
disabled True の場合に操作できなくなる False

下記は、IntSlider を作成して表示するプログラムの例です。実行結果のように、IntSlider は、ドラッグして整数を選択できる丸いつまみと、その右に現在の値が表示されます。

import ipywidgets as widgets

display(widgets.IntSlider())

実行結果(下図は、画像なので操作することはできません)

IntSlider の詳細については、下記のリンク先を参照して下さい。

他にも、整数以外の数値を扱える FloatSlider などのスライダーがあります。

ウィジェットのイベントハンドラと observe メソッド

ipywidgets のウィジェット は、その 属性の値が変更された際に実行 する イベントハンドラを定義 し、ウィジェットの observe 1というメソッドで 結びつける ことができます。

ウィジェットのイベントの処理に関する詳細は、下記のリンク先を参照して下さい。

イベントハンドラの定義の方法

observe メソッドで結びつけるイベントハンドラには、下記のプログラムのように、ウィジェットの操作によって 変更されたデータを表すオブジェクト を代入するための 仮引数を記述 します。下記のイベントハンドラは、仮引数の値を print で表示する処理を行います。なお、下記のプログラムでは、ipywidgets のドキュメントのプログラムに倣って、この仮引数の名前を changed にしました。

def on_slider_changed(changed):
    print(changed)

observe メソッドの記述方法

ウィジェットの observe メソッドには、下記の実引数を記述します。

  • 最初の実引数:ウィジェットに結びつけるイベントハンドラ
  • キーワード引数 names:ウィジェットのどの属性の値が変更された場合にイベントハンドラが実行されるかを指定する文字列型のデータ2

例えば、下記のプログラムでは、IntSlider に、IntSlider の現在の値 を表す value 属性の値が変更 された際に実行するイベントハンドラを 結び付けています

slider = widgets.IntSlider()
slider.observe(on_slider_changed, names="value")
display(slider)

他の例として、names="max" を記述することで、IntSlider の max 属性に値を代入して最大値が変更された場合に呼び出されるイベントハンドラを定義できます。

イベントハンドラの仮引数の値の意味

上記のプログラムを実行後に、表示された スライダーの値をドラッグして変更 すると、スライダーの value 属性の値が変更される ため on_slider_changed が呼び出され、下記のような changed の値が表示されます。

{'name': 'value', 'old': 20, 'new': 1, 'owner': IntSlider(value=1), 'type': 'change'}

changed には、下記の表のようなキーを持つ dict が代入されます。上記の場合は、value 属性の値が 20 から 1 に変更されたことを意味します。

キー キーの値の意味
name 変更された属性の名前
old 変更前の属性の値
new 変更後の属性の値
owner 属性が変更されたウィジェット
type イベントの種類。change は属性の値が変更されたことを意味する

IntSlider の作成と表示

IntSlider の使い方がわかったので、次は どこにどのような大きさで IntSlider を配置するか を決める必要があります。本記事では、4 つのリプレイボタンの幅を半分にし、空いた右のスペースに IntSlider を配置することにします。下記はそのように create_widgets メソッドを修正するプログラムです。なお、下記のプログラムでは、IntSlider の表示幅のみを設定して作成しています。IntSlider の values 属性などの値の設定は別の所で行います。

  • 3 ~ 6 行目:リプレイボタンの幅を 50 ピクセルに変更する
  • 7 行目:IntSlider を 200 ピクセルの幅で作成し、slider 属性に代入する
 1  def create_widgets(self):
元と同じなので省略
 2      # リプレイのボタンとスライダーを作成する
 3      self.first_button = self.create_button("<<", 50)
 4      self.prev_button = self.create_button("<", 50)
 5      self.next_button = self.create_button(">", 50)
 6      self.last_button = self.create_button(">>", 50)     
 7      self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
 8      # ゲーム盤の画像を表す figure を作成する
 9      self.create_figure()       
10    
11  Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセットボタンを作成する
    self.change_button = self.create_button("変更", 100)
    self.reset_button = self.create_button("リセット", 100)
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()       
    
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
元と同じなので省略
-   self.first_button = self.create_button("<<", 100)
+   self.first_button = self.create_button("<<", 50)
-   self.prev_button = self.create_button("<", 100)
+   self.prev_button = self.create_button("<", 50)
-   self.next_button = self.create_button(">", 100)
+   self.next_button = self.create_button(">", 50)
-   self.last_button = self.create_button(">>", 100) 
+   self.last_button = self.create_button(">>", 50) 
+   self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()       
    
Marubatsu_GUI.create_widgets = create_widgets

下記は display_widgets を修正するプログラムです。

  • 5 行目hbox2 に登録するウィジェットの最後に self.slider を追加する
1  def display_widgets(self):
2      # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
3      hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
4      # リプレイ機能のボタンを横に配置した HBox を作成する
5      hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
6      # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
7      display(widgets.VBox([hbox1, hbox2])) 
8    
9  Marubatsu_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2])) 
    
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
-   hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button]) 
+   hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2])) 
    
Marubatsu_GUI.display_widgets = display_widgets

上記の修正後に、下記のプログラムで gui_play を実行すると、実行結果のようにリプレイボタンの右に IntSlider が表示されるようになります。

gui_play()

実行結果(下図は、画像なので操作することはできません)

IntSlider の設定の変更

IntSlider の表示ができたので、次は IntSlider の設定を行うプログラム を記述する必要があります。その処理は、他のリプレイボタンの設定の処理と同時に行えば良いので、update_widgets_status メソッドの中で、下記のプログラムのように記述します。なお、最小値を表す min 属性と、選択できる数値の間隔を表す step 属性の値は、デフォルト値のままで良いので設定する必要はありません。

  • 3 行目:IntSlider の現在値を、現在の手数に設定する
  • 4 行目:IntSlider の最大値を、最後の手数を表す records の要素の数 - 1 に設定する3
1  def update_widgets_status(self):
元と同じなので省略
2      self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
3      self.slider.value = self.mb.move_count
4      self.slider.max = len(self.mb.records) - 1
5    
6  Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
    
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
元と同じなので省略
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
+   self.slider.value = self.mb.move_count
+   self.slider.max = len(self.mb.records) - 1
    
Marubatsu_GUI.update_widgets_status = update_widgets_status

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、着手を行ったり、IntSlider 以外のリプレイボタンをクリックして手数を変更すると、IntSlider の値が正しく変更されることを確認して下さい。

gui_play()

IntSlider のイベントハンドラの定義と結び付け

IntSlider の イベントハンドラが行う処理はゲームの手数 を IntSlider の現在値を表す value 属性の値に変更する ことです。従って、create_event_handler の中で、その処理を行う on_slider_changed を下記のプログラムのように定義し、結び付けます。

  • 4、5 行目:IntSlider の値が変更された際に、変更された値を表す changed["new"] 手目の局面に移動する処理を行うイベントハンドラを定義する
  • 11 行目:上記のイベントハンドラを IntSlider に結びつける
 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略
 4      def on_slider_changed(changed):
 5         change_step(changed["new"])
 6
 7      self.first_button.on_click(on_first_button_clicked)
 8      self.prev_button.on_click(on_prev_button_clicked)
 9      self.next_button.on_click(on_next_button_clicked)
10      self.last_button.on_click(on_last_button_clicked)
11      self.slider.observe(on_slider_changed, names="value")
元と同じなので省略              
12    
13  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
import math

def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

    def on_prev_button_clicked(b):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
import math

def create_event_handler(self):
元と同じなので省略
+   def on_slider_changed(changed):
+       change_step(changed["new"])

    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
+   self.slider.observe(on_slider_changed, names="value")
元と同じなので省略              
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、いくつかの着手を行った後で、IntSlider をドラッグして変更すると、それに合わせて表示される手数が正しく変更されることを確認して下さい。

gui_play()

これで、任意の手数の局面に移動する GUI の実装は完了です。

待った機能の実装

自分が行った着手を取り消す「待った」 は、リプレイ機能の < ボタンを 2 回クリックすることで行うことができますが、以下のような問題があります

  • < を 2 回クリックするのは面倒
  • < を 2 回クリックした場合は、リプレイの機能によって 過去の局面が表示 されるので、ゲーム盤の背景色が水色 になり、上部に Replay が表示 される
  • 待ったで行いたい のは、一つ前に行った自分の着手の取り消し なので、リプレイ中の画面になって欲しくない

そこで、一つ前の自分の手番に戻し、その局面を 最後の着手が行われた局面にする という処理を行う 待ったボタンを実装 することにします。

待ったボタンの配置と表示

まず、待ったボタンを どこに配置するかを検討する 必要があります。本記事では、変更とリセットボタンの幅を小さく し、その右に待ったボタンを配置 することにします。他の配置が良いと思った人は自由に変更して下さい。

下記は、そのように create_widgets メソッドを修正したプログラムです。

  • 5、6 行目:変更ボタンとリセットボタンの幅を小さくする。なお、新しく設定したボタンの幅は、試行錯誤して筆者がちょうどよいと感じたものである
  • 7 行目:待ったボタンを 60 ピクセルの幅で作成し、undo_button 属性に代入する。なお、undo とは、元に戻す という意味の英単語である
1  def create_widgets(self):
2      # AI を選択する Dropdown を作成する
3      self.create_dropdown()
4      # 変更、リセット、待ったボタンを作成する
5      self.change_button = self.create_button("変更", 50)
6      self.reset_button = self.create_button("リセット", 80)
7      self.undo_button = self.create_button("待った", 60)    
元と同じなので省略    
8    
9  Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()       
    
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
-   self.change_button = self.create_button("変更", 100)
+   self.change_button = self.create_button("変更", 50)
-   self.reset_button = self.create_button("リセット", 100)
+   self.reset_button = self.create_button("リセット", 80)
+   self.undo_button = self.create_button("待った", 60)    
元と同じなので省略    
    
Marubatsu_GUI.create_widgets = create_widgets

下記は、そのように display_widgets メソッドを修正したプログラムです。

  • 3 行目hbox1 に登録するウィジェットの最後に self.undo_button を追加する
1  def display_widgets(self):
2      # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
3      hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
4      # リプレイ機能のボタンを横に配置した HBox を作成する
5      hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
6      # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
7      display(widgets.VBox([hbox1, hbox2])) 
8    
9  Marubatsu_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2])) 
    
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
-   hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button])
+   hbox1 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2])) 
    
Marubatsu_GUI.display_widgets = display_widgets

上記の修正後に、下記のプログラムで gui_play を実行すると、実行結果のように「待った」ボタンが表示されるようになります。

gui_play()

待ったボタンのイベントハンドラの定義と結び付け

待ったボタンが行う処理は以下の処理です。最後の 5 行目の条件 は、過去の局面で待ったができるのは変 だと考えて加えた条件ですが、過去の局面でも待ったを行えてよいと思った方はその条件は入れなくても構いません。

  • 2 手前の局面に移動する
  • 局面を最後の着手が行われた局面にする
  • ただし、以下の場合は待ったをかけることはできない
    • 自分が着手を一度も行っていない 0 手目または 1 手目の局面
    • リプレイ中(最後の着手が行われた以外の局面)の場合

従って、イベントハンドラの定義と結び付けは以下のプログラムのように記述します。

  • 3 ~ 8 行目:待ったボタンを押した際に実行するイベントハンドラを定義する
  • 4 行目:2 手目以降で、リプレイ中でないことを判定する
  • 5 ~ 8 行目move_count 属性を 2 減らし、records 属性を修正し、change_step メソッドを呼び出して 2 手前の局面に戻る処理を行う。なお、8 行目の draw_board を呼び出さないと、画面の描画が更新されない点に注意 すること(筆者は記述し忘れました)
  • 13 行目:上記のイベントハンドラを、待ったボタンに結びつける
 1  def create_event_handler(self):
元と同じなので省略
 2      # 待ったボタンのイベントハンドラを定義する
 3      def on_undo_button_clicked(b):
 4          if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
 5              self.mb.move_count -= 2
 6              self.mb.records = self.mb.records[0:self.mb.move_count+1]
 7              self.mb.change_step(self.mb.move_count)
 8              self.draw_board()
 9       
10      # イベントハンドラをボタンに結びつける
11      self.change_button.on_click(on_change_button_clicked)
12      self.reset_button.on_click(on_reset_button_clicked)   
13      self.undo_button.on_click(on_undo_button_clicked)   
元と同じなので省略
14    
15  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

    def on_prev_button_clicked(b):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
            
    # fig の画像にマウスを押した際のイベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    # 待ったボタンのイベントハンドラを定義する
+   def on_undo_button_clicked(b):
+       if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
+           self.mb.move_count -= 2
+           self.mb.records = self.mb.records[0:self.mb.move_count+1]
+           self.mb.change_step(self.mb.move_count)
+           self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
+   self.undo_button.on_click(on_undo_button_clicked)   
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、「待った」ボタンが正しく機能することを確認して下さい。

gui_play()

待ったボタンの表示の変更

上記では、待ったができない場合でも待ったボタンが緑色で表示される ので、待ったができない場合 に、待ったボタンを 灰色で表示して操作できない ようにします。下記は、そのように update_widgets_status メソッドを修正したプログラムです。

  • 2 行目:待ったができない場合に待ったボタンを操作できないようにする。なお、待ったができないのは、「2 手目未満」または、「最後の着手の局面でない」場合である
1  def update_widgets_status(self):
2      self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
3      # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
4      self.set_button_status(self.first_button, self.mb.move_count <= 0)
元と同じなので省略
5    
6  Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
    self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
    self.set_button_status(self.prev_button, self.mb.move_count <= 0)
    self.set_button_status(self.next_button, self.mb.move_count >= len(self.mb.records) - 1)
    self.set_button_status(self.last_button, self.mb.move_count >= len(self.mb.records) - 1)    
    self.slider.value = self.mb.move_count
    self.slider.max = len(self.mb.records) - 1
    
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
+   self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
    # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
    self.set_button_status(self.first_button, self.mb.move_count <= 0)
元と同じなので省略
    
Marubatsu_GUI.update_widgets_status = update_widgets_status

上記の修正後に、下記のプログラムで gui_play を実行すると、実行結果のように、ゲーム開始時の局面では「待った」ボタンが灰色で表示され、操作できないようになります。また、着手を行ったり、リプレイボタンをクリックし、待ったを行える状況でのみ、待ったボタンが緑色で表示されることを確認して下さい。

gui_play()

実行結果(下図は、画像なので操作することはできません)

キー入力による操作

ここまでで実装した〇×ゲームの GUI は、マウスによって操作を行うというものでしたが、キー入力による操作も行えると便利 です。そこで、本記事では〇×ゲームの操作を、下記の表のキーで行えるようにすることにします。下記は、テンキーとカーソルキーで操作を行える ようにしたものですが、他のキーの方が良いと思った方は自由に変更して下さい。

キー 行われる操作
1 ~ 9 テンキーをゲーム盤のマスと見立て、対応するマスに着手を行う
ゲーム開始時まで戻す(<< ボタンに対応)
一手戻す(< ボタンに対応)
一手進める(> ボタンに対応)
最後に行われた着手まで進める(>> ボタンに対応)
0 待ったを行う
Enter ゲームをリセットする

キーイベントに対するイベントハンドラの定義と結び付け

Matplotlib の Figure は、以前の記事で説明したように、mpl_connect を使ってイベントハンドラを結び付けることができます。

どのキーが押されたかは、イベントハンドラの仮引数に代入されるオブジェクトの key 属性に代入されます。下記は、キーが押された場合に、どのキーが押されたかを print で表示するイベントハンドラの定義です。

def on_key_press(event):
    print(event.key)

キーが押された場合のイベントハンドラを結び付けるには、下記のプログラムのように、mpl_connect の最初の実引数に "key_press_event" を記述します。

fig.canvas.mpl_connect("key_press_event", on_mouse_down)    

下記は、create_event_handler に上記のイベントハンドラの定義と、Figure への結び付けを記述するプログラムです。

  • 2 行目:キーを押した時に呼び出されるイベントハンドラを定義する
  • 7 行目:Figure と上記のイベントハンドラを結び付ける
1  def create_event_handler(self):
元と同じなので省略
2      def on_key_press(event):
3          print(event.key)            
4            
5      # fig の画像イベントハンドラを結び付ける
6      self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
7      self.fig.canvas.mpl_connect("key_press_event", on_key_press)     
8    
9  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b):
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b):
        change_step(0)

    def on_prev_button_clicked(b):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    def on_key_press(event):
        print(event.key)            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)     
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
+   def on_key_press(event):
+       print(event.key)            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
+   self.fig.canvas.mpl_connect("key_press_event", on_key_press)     
    
Marubatsu_GUI.create_event_handler = create_event_handler

上記の修正後に、下記のプログラムで gui_play を実行し、ゲーム盤の画像の上でマウスをクリックして Figure を選択後にキーを押す と、実行結果のように 押したキーに対応する文字列が表示されます。下記は、↑、0、Enter の順でキーを押した場合の実行結果です。なお、ゲーム盤の画像は前と同じなので省略します。

gui_play()

実行結果

up
0
enter

Figure に対するキー入力の処理の注意点

matplotlib の Figure に対するキー入力処理に関しては、以下の点に注意する必要があります。

Figure を選択状態にする必要がある

Figure に結びつけたイベントハンドラは、Figure が選択状態の場合でしか呼び出されません。従って、下記の場合は、キーを押しても対応する文字列は表示されません。

  • gui_play() の実行直後。JupyterLab のセルが選択状態になるため
  • リセットボタンなどのボタンのクリック後。クリックしたボタンが選択状態になるため

なお、Figure をクリックして選択状態にしても Figure の表示は変化しませんが、その状態でキーを押すと、下図のように Figure の枠がオレンジ色で表示されるようになるようです。

条件がよくわからないのですが、Figure をクリックすることでも上図のように Figure の枠がオレンジ色で表示される場合もあるようです。条件についてご存じの方がいればコメントで教えて頂けると嬉しいです。

なお、gui_play() を実行した直後 は、ゲーム盤の画像の Figure が選択状態になっていない ため、キーで操作を行うため には、一度 Figure をクリックして選択する必要があります。JupyterLab のセルで gui_play() を実行後に、自動的に Figure を選択状態にする方法を探してみたのですが今の所見つけられていません。ご存じの方がいればコメントで知らせて頂ければうれしいです。

初期設定で登録されているショートカットキーによる操作

Figure には、初期設定でいくつかのキーショートカットキーとして特定の操作に割り当てられています。例えば、Figure を選択中に s キーを押すと、Figure を画像として保存するパネルが表示されます。従って、ショートカットキーに割り当てられているキーを〇×ゲームの操作に割り当ててしまう と、意図しない処理が行われる ことになります。どのようなキーがどのような操作に対応しているかについては、下記のリンク先を参照して下さい。

先程〇×ゲームの操作に割り当てたキーのうち、← と → は Back と Forward という操作に割り当てられています。Back と Forward の操作は、Figure に対して行った Zoom(拡大・縮小)や Pan(移動)などの操作を前後に移動するという操作なので、それらの操作が行われていない場合は何も行われません。また、それ以外のキーはショートカットキーに割り当てられていない ので、先程の表のキーしか押さなければ問題は発生しません。そのため、ショートカットキーに対する対策を行わなくても問題はありませんが、間違ってショートカットキーに割り当てられたキーを押してしまうと、下記のような予期せぬ動作が行われる場合があるので、ショートカットキーの操作を禁止することにします。

例えば、Figure をクリックして選択後に p キーを押すと、下図のように、Figure の内容をマウスでドラッグして移動することができるようになります。また、下図の状態で ← キーを押すと、Figure の表示が移動が行われる前の状態に戻ります。

matplotlib の設定の変更

先程のショートカットキーの操作は、matplotlib の設定を変更することで禁止できます。matplotlib の設定を変更する方法はいくつかありますが、本記事ではその中で プログラムで設定を変更する方法 を紹介します。他の方法については下記のリンク先を参照して下さい。

matplotlib の設定は、インポートした matplotlib の rcParams 属性に dict の形式で保存 されており、設定できる属性は上記のリンク先のページの下部に一覧で示されています。

下記は、上記のリンク先に示されている属性(先頭の # は名前ではありません)の中から ショートカットキーの設定に関する属性 の部分を抜き出したものです。これらの属性には、対応するショートカットキーを表す文字列を list の形式で保存します。従って、空の list を代入することで、対応するショートカットキーの操作が禁止 されます。

#keymap.fullscreen: f, ctrl+f   # toggling
#keymap.home: h, r, home        # home or reset mnemonic
#keymap.back: left, c, backspace, MouseButton.BACK  # forward / backward keys
#keymap.forward: right, v, MouseButton.FORWARD      # for quick navigation
#keymap.pan: p                  # pan mnemonic
#keymap.zoom: o                 # zoom mnemonic
#keymap.save: s, ctrl+s         # saving current figure
#keymap.help: f1                # display help about active tools
#keymap.quit: ctrl+w, cmd+w, q  # close the current figure
#keymap.quit_all:               # close all figures
#keymap.grid: g                 # switching on/off major grids in current axes
#keymap.grid_minor: G           # switching on/off minor grids in current axes
#keymap.yscale: l               # toggle scaling of y-axes ('log'/'linear')
#keymap.xscale: k, L            # toggle scaling of x-axes ('log'/'linear')
#keymap.copy: ctrl+c, cmd+c     # copy figure to clipboard

例えば、下記のプログラムを実行すると、Figure を保存するパネルを表示する ショートカットキーが無くなる ため、禁止されたことになります

import matplotlib as mlp

mlp.rcParams["keymap.save"] = []

上記のプログラムを実行後に、ゲーム盤の画像の上をクリックして Figure を選択し、s キーを押しても何も起きなくなることを確認して下さい。

上記のすべての属性に空の list を代入する処理を、一つずつ記述するのは大変です。上記の属性はすべて keymap. で始まるので、下記のプログラムのように、その後ろに続く属性の名前を要素とする list を作成 し、繰り返し処理 を使って すべてのショートカットキー操作を禁止 することができます。

attrs = [ "fullscreen", "home", "back", "forward", "pan", "zoom", "save", "help",
         "quit", "quit_all", "grid", "grid_minor", "yscale", "xscale", "copy"]
for attr in attrs:
    mlp.rcParams[f"keymap.{attr}"] = []

そこで、Marubatsu_GUI クラスに上記の処理を行う disable_shortcutkeys というメソッドを下記のプログラムのように定義します。なお、このメソッドは Marubatsu_GUI クラスのインスタンスの情報を利用しないので、静的メソッドとして定義する事にします。

@staticmethod
def disable_shortcutkeys(self):
    attrs = [ "fullscreen", "home", "back", "forward", "pan", "zoom", "save", "help",
             "quit", "quit_all", "grid", "grid_minor", "yscale", "xscale", "copy"]
    for attr in attrs:
        mlp.rcParams[f"keymap.{attr}"] = []

Marubatsu_GUI.disable_shortcutkeys = disable_shortcutkeys

次に、下記のプログラムの 2 行目ように、__init__ メソッドに上記の関数を呼び出す処理を追加します。

1  def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
2      self.disable_shortcutkeys()
3      self.create_widgets()
4      self.create_event_handler()
5      self.display_widgets() 
6        
7  Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
def __init__(self, mb, ai_dict=None, size=3):
    # ai_dict が None の場合は、空の list で置き換える
    if ai_dict is None:
        ai_dict = {}

    self.mb = mb
    self.ai_dict = ai_dict
    self.size = size
    
    # %matplotlib widget のマジックコマンドを実行する
    get_ipython().run_line_magic('matplotlib', 'widget')
        
    self.disable_shortcutkeys()
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__
修正箇所
def __init__(self, mb, ai_dict=None, size=3):
元と同じなので省略
    self.disable_shortcutkeys()
    self.create_widgets()
    self.create_event_handler()
    self.display_widgets() 
        
Marubatsu_GUI.__init__ = __init__

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、ゲーム盤の画像の上でマウスをクリックして Figure を選択後にキーを押す と、どのキーを押してもそのキーに対応する文字が表示され、その際に余計な処理が実行されなくなります。

gui_play()

なお、rcParams の属性に値を代入 する方法で変更した matplotlib の設定 は、JupyterLab を再起動すると無効になります。永久に matplotlib の設定を変更したい場合は、先程紹介したリンク先の記事に記されている、設定ファイルを変更方法で行うことができます。

押されたキーに対応する処理の記述

押されたキーに対応する処理を記述するためには、先程の表の それぞれのキーが押された時 に、どのような文字列が event.key に代入されるかを知る 必要がありますが、それは 実際にそれぞれのキーを押して表示される文字列を見る ことで確認できます。

それぞれのキーに対応する event.key の値

下記はそれぞれのキーに対応する文字列です。

キー event.key の値
1 ~ 9 "1" ~ "9"
"up"
"left"
"right"
"down"
0 "0"
Enter "enter"

← キーに対応する処理の記述方法

1 手前に戻る ← キーの処理 は、on_prev_button_clicked のイベントハンドラと同じ です。そのため、その処理は on_key_press 内で下記のプログラムのように記述できます。

def on_key_press(event):
    if event.key == "left":
        on_prev_button_clicked(None)

なお、ボタンをクリックした際に実行するイベントハンドラ には、クリックされたボタンのウィジェットが代入される 仮引数が必要 なので、on_prev_button_clicked を呼び出す際 にはその仮引数に対応する 実引数を記述する必要がありますon_prev_button_clicked は、下記のプログラムのように 仮引数 b の情報を利用しない ので、実引数には何を記述しても構いません。そこで、上記のプログラムでは実引数に None を記述しました。

def on_prev_button_clicked(b):
    change_step(self.mb.move_count - 1)

また、on_prev_button_clicked の仮引数 を下記のプログラムのように、何らかのデフォルト値を持つ デフォルト引数として定義 する事で、on_prev_button_clicked を呼び出す際に実引数を省略できる ようになるので、本記事では下記のように修正することにします。

def on_prev_button_clicked(b=None):
    change_step(self.mb.move_count - 1)
修正箇所
-def on_prev_button_clicked(b):
+def on_prev_button_clicked(b=None):
    change_step(self.mb.move_count - 1)

このように、イベントハンドラとして定義した関数 を、通常の関数と同様の方法で呼び出して利用 することができますが、その際には、イベントハンドラの 仮引数の扱いに注意 して呼び出す必要があります。

リセット、待った、リプレイ機能に対するキー入力の処理の記述

上記の方法で、下記のプログラムのように、リセット、待った、リプレイ機能に対するキー入力の処理を記述することができます。

  • 3、5、6、9、12、15 行目:ボタンをクリックした時に実行するイベントハンドラの仮引数を None をデフォルト値とするデフォルト引数にする
  • 18 ~ 29 行目:それぞれのキーに対応する処理を行うイベントハンドラを呼び出す
 1  def create_event_handler(self):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_reset_button_clicked(b=None):
元と同じなので省略
 4      # 待ったボタンのイベントハンドラを定義する
 5      def on_undo_button_clicked(b=None):
元と同じなので省略
 6      def on_first_button_clicked(b=None):
 7          change_step(0)
 8
 9      def on_prev_button_clicked(b=None):
10          change_step(self.mb.move_count - 1)
11
12      def on_next_button_clicked(b=None):
13          change_step(self.mb.move_count + 1)
14        
15      def on_last_button_clicked(b=None):
16          change_step(len(self.mb.records) - 1)
元と同じなので省略
17      def on_key_press(event):
18          if event.key == "up":
19              on_first_button_clicked()
20          elif event.key == "left":
21              on_prev_button_clicked()
22          elif event.key == "right":
23              on_next_button_clicked()
24          elif event.key == "down":
25              on_last_button_clicked()
26          elif event.key == "0":
27              on_undo_button_clicked()
28          elif event.key == "enter":
29              on_reset_button_clicked()
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    def on_key_press(event):
        if event.key == "up":
            on_first_button_clicked()
        elif event.key == "left":
            on_prev_button_clicked()
        elif event.key == "right":
            on_next_button_clicked()
        elif event.key == "down":
            on_last_button_clicked()
        elif event.key == "0":
            on_undo_button_clicked()
        elif event.key == "enter":
            on_reset_button_clicked()
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)     
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
-   def on_reset_button_clicked(b):
+   def on_reset_button_clicked(b=None):
元と同じなので省略
    # 待ったボタンのイベントハンドラを定義する
-   def on_undo_button_clicked(b):
+   def on_undo_button_clicked(b=None):
元と同じなので省略
-   def on_first_button_clicked(b):
+   def on_first_button_clicked(b=None):
        change_step(0)

-   def on_prev_button_clicked(b):
+   def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

-   def on_next_button_clicked(b):
+   def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
-   def on_last_button_clicked(b):
+   def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)
元と同じなので省略
    def on_key_press(event):
-       print(event.key)
+       if event.key == "up":
+           on_first_button_clicked()
+       elif event.key == "left":
+           on_prev_button_clicked()
+       elif event.key == "right":
+           on_next_button_clicked()
+       elif event.key == "down":
+           on_last_button_clicked()
+       elif event.key == "0":
+           on_undo_button_clicked()
+       elif event.key == "enter":
+           on_reset_button_clicked()
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、ゲーム盤の画像の上でマウスをクリックして Figure を選択する と、キー操作によってリセット、待った、リプレイ機能を呼び出すことができるようになります。

gui_play()

プログラムの改良

上記のプログラムで、それぞれのキーに対応する処理に対して、if 文の条件式を書くのが面倒だと思った人はいないでしょうか。上記のプログラムは、下記のような dict を使って 下記のプログラムのように 簡潔に記述 することができます。

dict のキー:〇×ゲームの操作を行うキー4を表す文字列
dict のキーの値:押されたキー4に対応する処理を行う関数

  • 3 ~ 10:上記の dict を keymap5 に代入する
  • 11 行目押されたキーを表す文字列 が、keymap のキーに存在するか どうかを判定する
  • 12 行目:存在する場合は、keymap[event.key] そのキーに 対応する処理を行う関数が代入 されているので、後ろに () を記述 することで その関数を呼び出す
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
 3          keymap = {
 4              "up": on_first_button_clicked,
 5              "left": on_prev_button_clicked,
 6              "right": on_next_button_clicked,
 7              "down": on_last_button_clicked,
 8              "0": on_undo_button_clicked,
 9              "enter": on_reset_button_clicked,            
10           }
11           if event.key in keymap:
12               keymap[event.key]()
元と同じなので省略
13    
14  Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            self.draw_board()
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)     
    
Marubatsu_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play を実行し、正しい処理が行われることを確認して下さい。

gui_play()

まだ着手を行うキーの処理の実装を行っていませんが、長くなりましたので今回の記事はここまでにします。余裕がある方は、着手を行うキーの実装方法について考えてみて下さい。

今回の記事のまとめ

今回の記事では、以下の実装を行いました。

  • リプレイ中の局面であることを明確にする
  • IntSlider を使って、任意の手数の局面に移動する機能の実装
  • 待った機能の実装
  • キー操作による GUI の機能の実装の一部

次回の記事では、キー操作による GUI の機能の実装を完了した後でリプレイ機能以外の改良について紹介し、GUI の実装を完了する予定です。

本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

以下のリンクは、今回の記事で更新した marubatsu.py です。

次回の記事

  1. observe は、監視する、観察するという意味の英単語です

  2. キーワード引数 names を省略 すると、ウィジェットの すべての属性が対象になります。また、list で指定することで、複数の属性の名前を指定することもできます

  3. 1 を引くのは、records 属性の要素 に、0 手目の着手のデータが代入されている ためです

  4. このキーは キーボードのキー の事です 2

  5. map は一般的には地図という意味を表しますが、図表 という意味も持ちます。keymap は、キーとそのキーに対応する関数を表す 対応表 であることからそのような名前を付けました

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0