目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
marubatsu.py | Marubatsu、Marubatsu_GUI クラスの定義 |
ai.py | AI に関する関数 |
test.py | テストに関する関数 |
util.py | ユーティリティ関数の定義。現在は gui_play のみ定義されている |
tree.py | ゲーム木に関する Node、Mbtree クラスの定義 |
gui.py | GUI に関する処理を行う基底クラスとなる GUI クラスの定義 |
AI の一覧とこれまでに作成したデータファイルについては、下記の記事を参照して下さい。
Marubatsu_GUI クラスの改良の続き
前回の記事で、Marubatsu_GUI クラスの改良を行いました。今回の記事ではその続きで、下記の改良のうちの最初の 2 つを実装します。
- 現在の局面の状況がわかるようにする
- ゲーム盤のマスに、そのマスに着手を行った場合の局面の状況を表示する
- ゲーム盤のマスに、そのマスに着手を行った際の AI の評価値を表示できるようにする
現在の局面の状況がわかるようにする改良
現在の局面の状況 は、下部の GUI の部分木 の赤枠の選択された 局面の色を見ればわかる ようになっていますが、ゲーム盤にもその情報が表示されると便利 です。
ただし、局面の状況を常に表示してしまうと、真剣勝負の邪魔になる ので、ボタンでその表示を切り替える ことができるようにします。
現在の局面の状況の表示方法の検討
まず、局面の状況をどのように表示するかを決める必要があります。本記事では、局面の状況を表す下記表の 1 文字を表示 するという、シンプルな表示を行うことにします。別の方法で表示を行いたい人は自由に変更して下さい。
局面の状況 | 表示する文字 |
---|---|
〇 の必勝の局面 | 〇 |
引き分けの局面 | △ |
× の必勝の局面 | × |
また、局面の状況を表す文字は、ゲーム盤の上の 手番の右に表示 することにします。別の場所に表示したい人は自由に変更して下さい。
Marubatsu_GUI クラスの __init__
メソッドの修正
局面の状況の表示を行うようにするための 修正の流れ は、前回の記事で Marubatsu_GUI クラスの改良を行った場合のものと ほぼ同じ です。
まず、下記のプログラムのように、最初に局面の状況(status)を表示するかどうかを表す 仮引数 show_status
を Marubatsu_GUI クラスの __init__
メソッドに追加 します。
また、局面の状況を計算して表示するため には、局面と最善手・評価値の対応表のデータが必要 なので、13 行目でそのデータをファイルから読み込んで score_table
属性に代入しています。
-
6 行目:仮引数
show_status
を追加する -
8 行目:
show_status
を同名の属性に代入する -
12、14 行目:局面と最善手・評価値の対応表のデータをファイルから読み込んで
score_table
属性に代入する。なお、12 行目でload_bestmoves
を ローカルなインポートを行っているのは、循環参照を防ぐためである
1 from marubatsu import Marubatsu_GUI
2 from tkinter import Tk
3 import os
4
5 def __init__(self, mb, params, names, ai_dict, scoretable_dict, show_subtree,
6 show_status, seed, size):
元と同じなので省略
7 self.show_subtree = show_subtree
8 self.show_status = show_status
9 self.seed = seed
10 self.size = size
11
12 from util import load_bestmoves
13
14 self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
元と同じなので省略
15
16 Marubatsu_GUI.__init__ = __init__
行番号のないプログラム
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
def __init__(self, mb, params, names, ai_dict, scoretable_dict, show_subtree,
show_status, seed, size):
if params is None:
params = [{}, {}]
if ai_dict is None:
ai_dict = {}
if names is None:
names = [None, None]
for i in range(2):
if names[i] is None:
if mb.ai[i] is None:
names[i] = "人間"
else:
names[i] = mb.ai[i].__name__
# JupyterLab からファイルダイアログを開く際に必要な前処理
root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)
# save フォルダが存在しない場合は作成する
if not os.path.exists("save"):
os.mkdir("save")
self.mb = mb
self.ai_dict = ai_dict
self.params = params
self.names = names
self.show_subtree = show_subtree
self.show_status = show_status
self.seed = seed
self.size = size
from util import load_bestmoves
self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
super(Marubatsu_GUI, self).__init__()
from tree import Mbtree_GUI
self.mbtree_gui = Mbtree_GUI(scoretable_dict, size=0.1)
Marubatsu_GUI.__init__ = __init__
修正箇所
from marubatsu import Marubatsu_GUI
from tkinter import Tk
import os
def __init__(self, mb, params, names, ai_dict, scoretable_dict, show_subtree,
- seed, size):
+ show_status, seed, size):
元と同じなので省略
self.show_subtree = show_subtree
+ self.show_status = show_status
self.seed = seed
self.size = size
+ from util import load_bestmoves
+ self.score_table = load_bestmoves("../data/bestmoves_and_score_by_board.dat")
元と同じなので省略
Marubatsu_GUI.__init__ = __init__
これまでの記事で、局面と最善手・評価値の対応表のデータは 通常の方法で評価値を計算したもの と、最短の勝利を優先して評価値を計算したもの の 2 つを作成しましたが、どちらも 〇 の必勝の局面の評価値を正の値、引き分けの局面の評価値を 0、× の必勝の曲年の評価値を負の値 として計算している ので、局面の状況を調べて表示する場合は どちらの対応表のデータを読み込んでもかまいません。
また、局面と最善手・評価値の対応表のデータは Mbtree_GUI クラスでもファイルから読みんでいますが、ファイルサイズが小さく、ほぼ一瞬で読み込むことができる ので 重複して読み込む ことにしました。
Marubatsu クラスの play
メソッドの修正
次に、Marubatsu_GUI クラスのインスタンスを作成する Marubatsu クラスの play
メソッドを以下のプログラムのように修正します。
-
4 行目:デフォルト値を
False
1 とする仮引数show_status
を追加する -
6 行目:Marubatsu_GUI クラスのインスタンスを作成する際に、実引数
show_status=show_status
を追加する
1 from marubatsu import Marubatsu
2
3 def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
4 show_subtree=True, show_status=False, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
5 if gui:
6 mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
7 show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
元と同じなので省略
8
9 Marubatsu.play = play
行番号のないプログラム
from marubatsu import Marubatsu
def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
show_subtree=True, show_status=False, verbose=True, seed=None, gui=False, size=3):
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# 一部の仮引数をインスタンスの属性に代入する
self.ai = ai
self.verbose = verbose
self.gui = gui
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、GUI の処理を行う Marubatsu_GUI のインスタンスを作成する
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
else:
mb_gui = None
self.restart()
return self.play_loop(mb_gui, params=params)
Marubatsu.play = play
修正箇所
from marubatsu import Marubatsu
def play(self, ai:list, ai_dict=None, params=None, names=None, scoretable_dict=None,
- show_subtree=True, verbose=True, seed=None, gui=False, size=3):
+ show_subtree=True, show_status=False, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
if gui:
mb_gui = Marubatsu_GUI(self, params=params, names=names, ai_dict=ai_dict, scoretable_dict=scoretable_dict,
- show_subtree=show_subtree, seed=seed, size=size)
+ show_subtree=show_subtree, show_status=show_status, seed=seed, size=size)
元と同じなので省略
Marubatsu.play = play
Marubatsu_GUI クラスの create_widgets
メソッドの修正
2024/11/16 修正
create_widgets
の最後に余計な処理が記述されていたので修正しました。
次に、局面の状況の 表示の有無を選択するボタンを作成 する必要があります。本記事では、ボタンに表示する文字 を「状況」としました。下記は、そのように create_widgets
を修正したプログラムです。
- 5 行目:「状況」と表示するボタンを作成する
1 import ipywidgets as widgets
2
3 def create_widgets(self):
元と同じなので省略
4 # 状況ボタン、大きさを変更する FloatSlider を作成する
5 self.show_status_button = self.create_button("状況", 50)
6 self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
7 description="size", value=self.size)
元と同じなので省略
8
9 Marubatsu_GUI.create_widgets = create_widgets
行番号のないプログラム
import ipywidgets as widgets
def create_widgets(self):
# 乱数の種の Checkbox と IntText を作成する
self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
indent=False, layout=widgets.Layout(width="100px"))
self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
layout=widgets.Layout(width="80px"))
# 読み書き、ヘルプのボタンを作成する
self.load_button = self.create_button("開く", 50)
self.save_button = self.create_button("保存", 50)
self.show_tree_button = self.create_button("木", 34)
self.reset_tree_button = self.create_button("リ", 34)
self.help_button = self.create_button("?", 34)
# 状況ボタン、大きさを変更する FloatSlider を作成する
self.show_status_button = self.create_button("状況", 50)
self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
description="size", value=self.size)
# 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()
# print による文字列を表示する Output を作成する
self.output = widgets.Output()
# ヘルプを表示する Output を作成し、表示の設定を行う
self.help = widgets.Output()
self.print_helpmessage()
self.help.layout.display = "none"
self.output = widgets.Output()
self.print_helpmessage()
self.output.layout.display = "none"
Marubatsu_GUI.create_widgets = create_widgets
修正箇所
import ipywidgets as widgets
def create_widgets(self):
元と同じなので省略
# 状況ボタン、大きさを変更する FloatSlider を作成する
+ self.show_status_button = self.create_button("状況", 50)
self.size_slider = widgets.FloatSlider(min=1.0, max=5.0, step=0.1,
description="size", value=self.size)
元と同じなので省略
Marubatsu_GUI.create_widgets = create_widgets
Marubatsu_GUI クラスの display_widgets
メソッドの修正
「状況」ボタンは、FloatSlider の左に配置 することにします。下記はそのように display_widgets
を修正したプログラムです。
- 3 行目:FloatSlider の左に「状況」ボタンのウィジェットを配置するようにする
1 def display_widgets(self):
元と同じなので省略
2 # 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
3 hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
元と同じなので省略
4
5 Marubatsu_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):
# 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button,
self.show_tree_button, self.reset_tree_button, self.help_button])
# 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox3 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox4 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider])
# hbox1 ~ hbox4、Figure、Output を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2, hbox3, hbox4, self.fig.canvas, self.output, self.help]))
Marubatsu_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):
元と同じなので省略
# 状況ボタンとゲーム盤のサイズの変更の FloatSlider を配置した HBox を作成する
- hbox2 = widgets.HBox([self.size_slider])
+ hbox2 = widgets.HBox([self.show_status_button, self.size_slider])
元と同じなので省略
Marubatsu_GUI.display_widgets = display_widgets
Marubatsu_GUI クラスの create_event_handler
メソッドの修正
次に、create_event_handler
以下のプログラムのように修正します。
-
4 ~ 6 行目:「状況」ボタンをクリックした際に実行されるイベントハンドラを定義する。行う処理は、
show_status
属性の値を反転し、画面の表示を更新する処理である - 14 行目:「状況」ボタンとイベントハンドラを結び付ける
1 import math
2
3 def create_event_handler(self):
元と同じなので省略
4 def on_show_status_button_clicked(b=None):
5 self.show_status = not self.show_status
6 self.update_gui()
7
8 def on_size_slider_changed(changed):
9 self.size = changed["new"]
10 self.fig.set_figwidth(self.size)
11 self.fig.set_figheight(self.size)
12 self.update_gui()
13
14 self.show_status_button.on_click(on_show_status_button_clicked)
15 self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
行番号のないプログラム
import math
def create_event_handler(self):
# 乱数の種のチェックボックスのイベントハンドラを定義する
def on_checkbox_changed(changed):
self.update_widgets_status()
self.checkbox.observe(on_checkbox_changed, names="value")
# 開く、保存ボタンのイベントハンドラを定義する
def on_load_button_clicked(b=None):
path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save")
if path != "":
with open(path, "rb") as f:
data = pickle.load(f)
self.mb.records = data["records"]
self.mb.ai = data["ai"]
self.params = data["params"] if "params" in data else [ {}, {} ]
if "names" in data:
names = data["names"]
else:
names = [ "人間" if mb.ai[i] is None else mb.ai[i].__name__ for i in range(2)]
options = self.dropdown_list[0].options.copy()
for i in range(2):
value = (self.mb.ai[i], self.params[i])
if not value in options.values():
options[names[i]] = value
for i in range(2):
self.dropdown_list[i].options = options
self.dropdown_list[i].value = (self.mb.ai[i], self.params[i])
change_step(data["move_count"])
if data["seed"] is not None:
self.checkbox.value = True
self.inttext.value = data["seed"]
else:
self.checkbox.value = False
def on_save_button_clicked(b=None):
names = [ self.dropdown_list[i].label for i in range(2) ]
timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
fname = f"{names[0]} VS {names[1]} {timestr}"
path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
initialdir="save", initialfile=fname,
defaultextension="mbsav")
if path != "":
with open(path, "wb") as f:
data = {
"records": self.mb.records,
"move_count": self.mb.move_count,
"ai": self.mb.ai,
"params": self.params,
"names": names,
"seed": self.inttext.value if self.checkbox.value else None
}
pickle.dump(data, f)
def on_show_tree_button_clicked(b=None):
self.show_subtree = not self.show_subtree
self.mbtree_gui.vbox.layout.display = None if self.show_subtree else "none"
self.update_gui()
def on_reset_tree_button_clicked(b=None):
self.update_gui()
def on_help_button_clicked(b=None):
self.help.layout.display = "none" if self.help.layout.display is None else None
self.load_button.on_click(on_load_button_clicked)
self.save_button.on_click(on_save_button_clicked)
self.show_tree_button.on_click(on_show_tree_button_clicked)
self.reset_tree_button.on_click(on_reset_tree_button_clicked)
self.help_button.on_click(on_help_button_clicked)
def on_show_status_button_clicked(b=None):
self.show_status = not self.show_status
self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
self.show_status_button.on_click(on_show_status_button_clicked)
self.size_slider.observe(on_size_slider_changed, names="value")
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
self.mb.ai[i], self.params[i] = self.dropdown_list[i].value
self.mb.play_loop(self, self.params)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b=None):
# 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
if self.checkbox.value:
random.seed(self.inttext.value)
self.mb.restart()
self.output.clear_output()
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.update_gui()
# イベントハンドラをボタンに結びつける
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)
# step 手目の局面に移動する
def change_step(step):
self.mb.change_step(step)
# 描画を更新する
self.update_gui()
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):
if self.mb.move_count != changed["new"]:
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)
with self.output:
self.mb.move(x, y)
# 次の手番の処理を行うメソッドを呼び出す
self.mb.play_loop(self, self.params)
# ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
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,
"-": on_load_button_clicked,
"l": on_load_button_clicked,
"+": on_save_button_clicked,
"s": on_save_button_clicked,
"*": on_help_button_clicked,
"h": on_help_button_clicked,
}
if event.key in keymap:
keymap[event.key]()
else:
try:
num = int(event.key) - 1
event.inaxes = True
event.xdata = num % 3
event.ydata = 2 - (num // 3)
on_mouse_down(event)
except:
pass
# 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
修正箇所
import math
def create_event_handler(self):
元と同じなので省略
+ def on_show_status_button_clicked(b=None):
+ self.show_status = not self.show_status
+ self.update_gui()
def on_size_slider_changed(changed):
self.size = changed["new"]
self.fig.set_figwidth(self.size)
self.fig.set_figheight(self.size)
self.update_gui()
+ self.show_status_button.on_click(on_show_status_button_clicked)
self.size_slider.observe(on_size_slider_changed, names="value")
元と同じなので省略
Marubatsu_GUI.create_event_handler = create_event_handler
Marubastu_GUI クラスの update_gui
メソッドの修正
次に、局面の状況を表示 する処理を update_gui
に記述します。決着がついている場合は o win のような表示が行われるので、局面の状況は 決着がついていない場合 に行えばよいでしょう。下記は、そのように update_gui
を修正したプログラムです。
-
3、5 行目:ゲームの決着がついておらず、局面の状況を表示するかどうかを表す
self.show_status
がTrue
であることを判定する -
6 行目:
score_table
属性に代入された局面と最善手・評価値の対応表のデータと、局面を表す文字列を利用して、現在の局面の評価値を計算する -
7 行目:手番を表す文字列と局面の状況を表す文字列の 間を空ける ために、半角の空白を
text
の後に追加 する -
8 ~ 13 行目:評価値の値に応じて、手番を表す
text
の後に局面の状況を表す文字列を計算して追加する
1 def update_gui(self):
元と同じなので省略
2 # ゲームの決着がついていない場合は、手番を表示する
3 if self.mb.status == Marubatsu.PLAYING:
4 text = "Turn " + self.mb.turn
5 if self.show_status:
6 score = self.score_table[self.mb.board_to_str()]["score"]
7 text += " "
8 if score > 0:
9 text += "〇"
10 elif score == 0:
11 text += "△"
12 else:
13 text += "×"
元と同じなので省略
14
15 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(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)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
if self.show_status:
score = self.score_table[self.mb.board_to_str()]["score"]
text += " "
if score > 0:
text += "〇"
elif score == 0:
text += "△"
else:
text += "×"
# 引き分けの場合
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=7*self.size)
self.draw_board(ax, self.mb, lw=0.7*self.size)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
元と同じなので省略
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
+ if self.show_status:
+ score = self.score_table[self.mb.board_to_str()]["score"]
+ text += " "
+ if score > 0:
+ text += "〇"
+ elif score == 0:
+ text += "△"
+ else:
+ text += "×"
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
Marubastu_GUI クラスの update_widgets_status
メソッドの修正
最後に、show_status
属性の値に応じて 「状況」ボタンの表示の色を変更する ために、下記のプログラムのように update_widgets_status
の 5 行目を追加します。
1 def update_widgets_status(self):
2 self.inttext.disabled = not self.checkbox.value
3 self.set_button_color(self.show_tree_button, self.show_subtree)
4 self.set_button_status(self.reset_tree_button, not self.show_subtree)
5 self.set_button_color(self.show_status_button, self.show_status)
元と同じなので省略
6
7 Marubatsu_GUI.update_widgets_status = update_widgets_status
行番号のないプログラム
def update_widgets_status(self):
self.inttext.disabled = not self.checkbox.value
self.set_button_color(self.show_tree_button, self.show_subtree)
self.set_button_status(self.reset_tree_button, not self.show_subtree)
self.set_button_color(self.show_status_button, self.show_status)
self.set_button_status(self.undo_button, self.mb.move_count < 2 or self.mb.move_count != len(self.mb.records) - 1)
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)
# value 属性よりも先に max 属性に値を代入する必要がある点に注意!
self.slider.max = len(self.mb.records) - 1
self.slider.value = self.mb.move_count
Marubatsu_GUI.update_widgets_status = update_widgets_status
修正箇所
def update_widgets_status(self):
self.inttext.disabled = not self.checkbox.value
self.set_button_color(self.show_tree_button, self.show_subtree)
self.set_button_status(self.reset_tree_button, not self.show_subtree)
+ self.set_button_color(self.show_status_button, self.show_status)
元と同じなので省略
Marubatsu_GUI.update_widgets_status = update_widgets_status
上記の修正後に下記のプログラムを実行すると、初期設定では「状況」ボタンが OFF になっているので実行結果の左図のように局面の状況は表示されません。
次に「状況」ボタンをクリックすると、実行結果の真ん中の図のように手番を表す文字の右に 局面の状況を表す文字列が表示される ようになります。実行結果の右図は 〇 の必勝の局面の場合 の図です。表示がわかりづらいと思った方は、自由に変更して下さい。
from util import gui_play
gui_play()
着手を行った場合の局面の状況の表示
現状では、それぞれの合法手に 着手を行った局面の状況 は、下部の GUI の部分木の選択された局面の 子ノードの局面の色を見ればわかる ようになっていますが、直観的に わかりやすいとは言えない でしょう。そこで、ゲーム盤のマス に着手を行った場合の 局面の状況を直接表示 するという改良を行うことにします。
本記事では、その 表示の切り替え を、先程の 「状況」ボタンで行う ことにします。「状況」ボタンとは別のボタンを配置して表示の切り替えを別々に行いたい人は、そのように実装してみて下さい。
着手を行った場合の局面の状況の表示方法の検討
まず、ゲーム盤のマスに 着手を行った場合の局面の状況 を どのように表示するかを決める 必要があります。本記事ではゲーム盤の マスの中の左上 に、そのマスに 着手した場合の局面の状況 を、先程と同様の 〇、△、× の一文字を 小さく表示する ことにします。左上に小さく表示するのは、マスに 配置されたマークと区別できるようにするため です。
また、別の工夫として 最善手であるかどうかを区別できる ように、最善手の場合 は 文字を赤く表示 することにします。その際に、最短の勝利を優先 して計算された局面と最善手・評価値の対応表を利用すると、一部の最善手が赤く表示されなくなる ので、局面と最善手・評価値の対応表は、先程のプログラムで score_table
属性に代入した bestmoves_and_score_by_board.dat のファイルのデータを利用することにします。
着手を行った場合の局面の状況を表示する処理の記述場所
新しいボタンを配置しないことにしたので、__init__
メソッドや create_widgets
メソッド などの修正を行う必要はありません。修正する必要があるのは ゲーム盤の表示を行う処理 で、その処理は update_gui
メソッドの下記のプログラムで行っています。
def update_gui(self):
略
self.draw_board(ax, self.mb, lw=0.7*self.size)
略
着手を行った場合の局面の状況を 表示する処理 は、以下の 2 種類の方法で実装することができます。
-
draw_board
メソッドの中に記述する -
update_gui
メソッドの中で、上記のdraw_board
メソッドを呼び出した後に記述する
どちらの方法で実装しても構いませんが、draw_board
メソッドの中に記述すると、draw_board
に仮引数を追加するなどの修正が少々面倒なので、本記事では後者の方法で実装することにします。
Marubatsu_GUI クラスの update_gui
メソッドの修正
着手を行った場合の局面の状況を表示する処理は、下記の手順行います。
show_status
属性が True
の場合に それぞれの合法手 に対して下記の処理を行う。
- 合法手を着手した場合の 局面の状況を計算 する
- 文字の色 を合法手が最善手の場合は赤、そうでない場合は黒とする
- 合法手のマスの左上に、手順 1 で計算した局面の状況を、手順 2 で計算した文字の色で表示する
下記は、そのように update_gui
メソッドを修正したプログラムです。
-
6 行目:
show_status
属性がTrue
の場合に処理を行うようにする -
7 行目:局面と最善手・評価値の対応表から、現在の局面の最善手の一覧を計算して
bestmoves
に代入する - 8 行目:現在の局面のそれぞれの合法手に対する繰り返しの処理を行う
-
9、10 行目:現在の局面を表す
self.mb
に対する深いコピーを計算してmb
に代入し、mb
に対して合法手の着手を行う - 11 行目:局面と最善手・評価値の対応表から、合法手を着手した局面の評価値を計算する
- 12 行目:合法手が最善手であるかどうかによって、表示する文字の色を計算する
- 13 ~ 18 行目:評価値から局面の状況を表す文字列を計算する
-
19 行目:合法手のマスの左上に、上記で計算した局面の状況を表す文字を表示する。表示する文字の 座標 と、文字の大きさ は 試行錯誤して決めた ものである。また、文字の大きさ は前回の記事で説明したように、ゲーム盤の大きさを表す
self.size
に比例した値にする必要がある 点に注意する事
1 from copy import deepcopy
2
3 def update_gui(self):
元と同じなので省略
4 self.draw_board(ax, self.mb, lw=0.7*self.size)
5
6 if self.show_status:
7 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
8 for x, y in self.mb.calc_legal_moves():
9 mb = deepcopy(self.mb)
10 mb.move(x, y)
11 score = self.score_table[mb.board_to_str()]["score"]
12 color = "red" if (x, y) in bestmoves else "black"
13 if score > 0:
14 text = "〇"
15 elif score == 0:
16 text = "△"
17 else:
18 text = "×"
19 ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
20
21 Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
from copy import deepcopy
def update_gui(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)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
text += " "
if score > 0:
text += "〇"
elif score == 0:
text += "△"
else:
text += "×"
# 引き分けの場合
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=7*self.size)
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
for x, y in self.mb.calc_legal_moves():
mb = deepcopy(self.mb)
mb.move(x, y)
score = self.score_table[mb.board_to_str()]["score"]
color = "red" if (x, y) in bestmoves else "black"
if score > 0:
text = "〇"
elif score == 0:
text = "△"
else:
text = "×"
ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
from copy import deepcopy
def update_gui(self):
元と同じなので省略
self.draw_board(ax, self.mb, lw=0.7*self.size)
+ if self.show_status:
+ bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
+ for x, y in self.mb.calc_legal_moves():
+ mb = deepcopy(self.mb)
+ mb.move(x, y)
+ score = self.score_table[mb.board_to_str()]["score"]
+ color = "red" if (x, y) in bestmoves else "black"
+ if score > 0:
+ text = "〇"
+ elif score == 0:
+ text = "△"
+ else:
+ text = "×"
+ ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
上記の修正後に下記のプログラムを実行し、「状況」ボタンをクリックすると、実行結果のように 空いているマス に 局面の状況を表す文字が表示される ようになります。
ゲーム開始時の局面はすべての合法手が最善手で、どのマスに着手を行っても引き分けの局面になるので、左図のようにすべてのマスに赤い △ が表示されます。
右図の (1, 1) に着手した局面は、四隅の合法手が最善手で引き分けの局面になるので赤い △ が、辺の合法手に着手すると 〇 の必勝の局面になるので黒い 〇 が表示されます。
他にも様々な着手を行い、下部の GUI の部分木の表示と比較して正しい表示が行われることを確認してみて下さい。
gui_play()
実行結果
update_gui
メソッドの修正
上記のプログラムでは、update_gui
の中で、評価値を表す score
から局面の状況を表す文字列を計算するという、ほとんど同じ内容の処理 を 2 箇所で記述している という問題点があるので、修正することにします。
具体的には、下記のような関数 を update_gui
メソッドの中にローカル関数として定義 し、その関数を呼び出すようにします。なお、calc_status_txt
をローカル関数として定義したのは、この関数を update_gui
以外で利用することがなさそうだったからです。他の場所から利用する可能性がある場合はメソッドとして定義したほうが良いでしょう。
名前:局面の状況を表す文字列を計算するので calc_status_txt
とする
処理:評価値から局面の状況を表す文字列を計算する
入力:仮引数 score
に評価値を代入する
出力:局面の状況を表す文字列を返り値として返す
下記は、そのように update_gui
を修正したプログラムです。なお、10 行目の下に記述されていた ai = self.mb.ai
は、ローカル変数 ai
が update_gui
の中で使われていないことに気づいたので削除しました。
-
2 ~ 8 行目:上記の
calc_status_txt
をupdate_gui
のローカル関数として定義する -
16、24 行目:
calc_status_txt
を利用して局面の状況を表す文字列を計算するように修正する
1 def update_gui(self):
2 def calc_status_txt(score):
3 if score > 0:
4 return "〇"
5 elif score == 0:
6 return "△"
7 else:
8 return "×"
9
10 ax = self.ax
元と同じなので省略
11 # ゲームの決着がついていない場合は、手番を表示する
12 if self.mb.status == Marubatsu.PLAYING:
13 text = "Turn " + self.mb.turn
14 score = self.score_table[self.mb.board_to_str()]["score"]
15 if self.show_status:
16 text += " " + calc_status_txt(score)
元と同じなので省略
17 if self.show_status:
18 bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
19 for x, y in self.mb.calc_legal_moves():
20 mb = deepcopy(self.mb)
21 mb.move(x, y)
22 score = self.score_table[mb.board_to_str()]["score"]
23 color = "red" if (x, y) in bestmoves else "black"
24 text = calc_status_txt(score)
25 ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
行番号のないプログラム
def update_gui(self):
def calc_status_txt(score):
if score > 0:
return "〇"
elif score == 0:
return "△"
else:
return "×"
ax = self.ax
# 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)
# 上部のメッセージを描画する
# 対戦カードの文字列を計算する
ax.text(1.5, 3.5, f"{self.dropdown_list[0].label} VS {self.dropdown_list[1].label}",
fontsize=7*self.size, ha="center")
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
text += " " + calc_status_txt(score)
# 引き分けの場合
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=7*self.size)
self.draw_board(ax, self.mb, lw=0.7*self.size)
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
for x, y in self.mb.calc_legal_moves():
mb = deepcopy(self.mb)
mb.move(x, y)
score = self.score_table[mb.board_to_str()]["score"]
color = "red" if (x, y) in bestmoves else "black"
text = calc_status_txt(score)
ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
self.update_widgets_status()
if hasattr(self, "mbtree_gui"):
from tree import Node
self.mbtree_gui.selectednode = Node(self.mb, depth=self.mb.move_count)
self.mbtree_gui.update_gui()
Marubatsu_GUI.update_gui = update_gui
修正箇所
def update_gui(self):
+ def calc_status_txt(score):
+ if score > 0:
+ return "〇"
+ elif score == 0:
+ return "△"
+ else:
+ return "×"
ax = self.ax
- ai = self.mb.ai
元と同じなので省略
# ゲームの決着がついていない場合は、手番を表示する
if self.mb.status == Marubatsu.PLAYING:
text = "Turn " + self.mb.turn
score = self.score_table[self.mb.board_to_str()]["score"]
if self.show_status:
- text += " "
- if score > 0:
- text += "〇"
- elif score == 0:
- text += "△"
- else:
- text += "×"
+ text += " " + calc_status_txt(score)
元と同じなので省略
if self.show_status:
bestmoves = self.score_table[self.mb.board_to_str()]["bestmoves"]
for x, y in self.mb.calc_legal_moves():
mb = deepcopy(self.mb)
mb.move(x, y)
score = self.score_table[mb.board_to_str()]["score"]
color = "red" if (x, y) in bestmoves else "black"
- if score > 0:
- text = "〇"
- elif score == 0:
- text = "△"
- else:
- text = "×"
+ text = calc_status_txt(score)
ax.text(x + 0.1, y + 0.35, text, fontsize=5*self.size, c=color)
元と同じなので省略
Marubatsu_GUI.update_gui = update_gui
実行結果は先ほどと同じなので省略しますが、下記のプログラムを実行して様々な操作を行った際に、正しい表示が行われることを確認して下さい。
gui_play()
今回の記事のまとめ
今回の記事では、「状況」ボタンを作成し、現在の局面の状況と、それぞれの合法手を着手した際の局面の状況を表示できるように修正しました。次回の記事では、現在の局面に対するそれぞれの AI が計算した最善手と評価値を表示する処理を実装します。
本記事で入力したプログラム
リンク | 説明 |
---|---|
marubatsu.ipynb | 本記事で入力して実行した JupyterLab のファイル |
marubatsu_new.py | 今回の記事で更新した marubatsu.py |
次回の記事
更新日時 | 更新内容 |
---|---|
2024/11/16 |
create_widgets の最後に余計な処理が記述されていたので削除しました |
-
本記事ではデフォルト値を
False
としましたが、True
にしたい人は自由に変更して下さい。 ↩