目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
ルールベースの AI の一覧
ルールベースの AI の一覧については、下記の記事を参照して下さい。
リプレイ機能の問題点(再掲)
前回の記事では、下記のリプレイ機能の問題点のうち、最初の 2 つを修正しました。今回の記事では残りの問題点について修正します。
- 直前の着手 が 赤で表示されない
- ゲーム開始時 の局面で < ボタンをクリックすると おかしな挙動 が発生する
- 最後の着手 が行われた局面で > ボタンをクリックすると エラーが発生 する
- 最後の着手 が行われた 以外 の局面で着手を行うと おかしな挙動 が発生する
最後の着手が行われた局面で >ボタンをクリックするとエラーが発生する問題点の修正
下記のプログラムで gui_play
を実行し、最後の着手が行われた局面 で > ボタンをクリックする と下記のような エラーが発生 します。このエラーを発生させるには、ゲーム開始時に > ボタンをクリックするのが簡単です。このエラーの原因ついて少し考えてみて下さい。
from util import gui_play
gui_play()
実行結果
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
File c:\Users\ys\ai\marubatsu\080\marubatsu.py:420, in Marubatsu.play.<locals>.on_next_button_clicked(b)
419 def on_next_button_clicked(b):
--> 420 change_step(self.move_count + 1)
File c:\Users\ys\ai\marubatsu\080\marubatsu.py:399, in Marubatsu.play.<locals>.change_step(step)
397 step = max(0, step)
398 # step 手目のゲーム盤のデータをコピーし、board に代入する
--> 399 self.board = deepcopy(self.board_records[step])
400 # 手数を表す step を move_count に代入する
401 self.move_count = step
IndexError: list index out of range
エラーの原因の検証
このエラーの原因は、上記のエラーメッセージから、self.board_records[step]
で、step
が board_records
に代入された list の範囲外のインデックス であることです。実際に、最後の着手が行われた局面で > ボタンをクリックすると step
には 最後の着手の局面の次の要素のインデックス が代入されますが、board_records
には そのような要素は存在しません。
このエラーを修正するためには、前回の記事で < ボタンに対して同じ list index out of range のエラーが発生した際に行った修正と同様に、下記のプログラムの 4 行目のように、step
が self.board_records
の要素の数以上になった場合 に、step
にself.board_records
の 最後のインデックスの値を代入 するように修正する必要があります。
1 def change_step(step):
2 # step がインデックスの範囲外の場合は、範囲内に収める
3 step = max(0, step)
4 step = min(len(self.board_records) - 1, step)
元と同じなので略
なお、上記のプログラムの 3、4 行目は、下記のプログラムの 3 行目のように 1 行でまとめることもできますが、わかりづらいと思った方は上記のプログラムを採用して下さい。
1 def change_step(step):
2 # step がインデックスの範囲外の場合は、範囲内に収める
3 step = max(0, min(len(self.board_records) - 1, step))
元と同じなので略
下記は上記のように play
メソッドを修正したプログラムです。
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
# step がインデックスの範囲外の場合は、範囲内に収める
step = max(0, min(len(self.board_records) - 1, step))
元と同じなので省略
Marubatsu.play = play
プログラム全体
from marubatsu import Marubatsu
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math
from copy import deepcopy
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目以下の場合に、<< と < ボタンを操作できないようにする
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
style={"button_color": "lightgreen"},
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
update_widgets_status()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step がインデックスの範囲外の場合は、範囲内に収める
step = max(0, min(len(self.board_records) - 1, step))
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 直前の着手を計算する
self.last_move = None if step == 0 else self.records[step - 1]
# 描画を更新する
self.draw_board(ax, ai)
# ウィジェットの状態を更新する
update_widgets_status()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.move_count - 1)
def on_next_button_clicked(b):
change_step(self.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.board_records) - 1)
first_button.on_click(on_first_button_clicked)
prev_button.on_click(on_prev_button_clicked)
next_button.on_click(on_next_button_clicked)
last_button.on_click(on_last_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
update_widgets_status()
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
update_widgets_status()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
# step がインデックスの範囲外の場合は、範囲内に収める
- step = max(0, step)
+ step = max(0, min(len(self.board_records) - 1, step))
元と同じなので省略
Marubatsu.play = play
実行結果は省略しますが、上記の修正後に、下記のプログラムで gui_play
を実行すると、> ボタンをクリックしてもエラーが発生しなくなることが確認できます。
gui_play()
> と >> ボタンの表示の変更
<< ボタンや < ボタンと同様に、> と >> ボタン も、最後の着手が行われた局面 で 操作をできないように修正する ことにします。それらのボタンが 操作を行えない条件 は、move_count
属性が self.board_records
に代入された list の要素の数 - 1 以上 の場合なので、set_button_status
を使って下記のプログラムのように play
メソッドを修正します。
-
7、8 行目:> と >> ボタンに対して、
move_count
属性が最後の着手が行われた手数以上の場合に、操作できないようにする
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 # ウィジェットの状態を更新する
3 def update_widgets_status():
4 # 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
5 set_button_status(first_button, self.move_count <= 0)
6 set_button_status(prev_button, self.move_count <= 0)
7 set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
8 set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
元と同じなので省略
9
10 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
style={"button_color": "lightgreen"},
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
update_widgets_status()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step がインデックスの範囲外の場合は、範囲内に収める
step = max(0, min(len(self.board_records) - 1, step))
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 直前の着手を計算する
self.last_move = None if step == 0 else self.records[step - 1]
# 描画を更新する
self.draw_board(ax, ai)
# ウィジェットの状態を更新する
update_widgets_status()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.move_count - 1)
def on_next_button_clicked(b):
change_step(self.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.board_records) - 1)
first_button.on_click(on_first_button_clicked)
prev_button.on_click(on_prev_button_clicked)
next_button.on_click(on_next_button_clicked)
last_button.on_click(on_last_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
update_widgets_status()
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
update_widgets_status()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
+ set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
+ set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
元と同じなので省略
Marubatsu.play = play
上記の修正後に、下記のプログラムで gui_play
を実行すると、実行結果のように、ゲーム開始時の局面ではすべてのリプレイのボタンが灰色になり、クリックしても何も起きなくなることが確認できます。また、着手を行い、リプレイのボタンをクリックすると、手数に応じて適切にリプレイのボタンの表示が変化することを確認して下さい。
gui_play()
実行結果(下図は、画像なので操作することはできません)
最後の着手が行われた以外の局面で着手を行うとおかしな挙動が発生する問題点の修正
最後の着手が行われた以外の局面 で空いているマスをクリックして 着手を行う と おかしな挙動が発生 します。わかりづらいと思いますので、具体例を示します。
操作 | 表示 | |
---|---|---|
1 | (0, 0)、(1, 0)、(2, 0) の順で着手を行う | |
2 | < ボタンをクリックして 2 手目の局面に戻す | |
3 | (0, 1) のマスをクリックして着手を行う | |
4 | > ボタンをクリックして次の手数の局面を表示する | |
5 | < ボタンをクリックして前の手数の局面を表示する |
上記で行われたおかしな挙動は以下の通りです。
- 操作 4 で、次の手数の局面を表示 した際に、表示内容が変化しない
- 操作 5 で、前の手数の局面を表示 した場合に、操作 3 の 3 手目の局面ではなく、操作 1 の 3 手目の局面が表示される
何故このようなことが起きるかについて少し考えてみて下さい。
問題の検証
問題を検証するために、それぞれの操作 で、board
、board_records
、move_count
のそれぞれに代入された データを検証 することにします。
ゲーム開始時の処理の検証
ゲーム開始時 には、下記の restart
メソッドの処理が行われます。
1 def restart(self):
2 self.initialize_board()
3 self.turn = Marubatsu.CIRCLE
4 self.move_count = 0
5 self.status = Marubatsu.PLAYING
6 self.last_move = -1, -1
7 self.last_turn = None
8 self.records = []
9 self.board_records = [deepcopy(self.board)]
上記の 2、4、9 行目の処理によって、それぞれの変数には下記の表のデータが代入されます。なお、わかりやすさを重視して、board
と board_records
の 要素のデータ には、ゲーム盤の画像を表記 します。
変数 | 値 |
---|---|
board |
|
board_records |
[ ] |
move_count |
0 |
本当にそれぞれの変数の値が上記のような値になっているかどうかを確認したい人は、restart
メソッド内に print
を記述して変数の値を表示すると良いでしょう。
操作 1 の検証
操作 1 では、3 手分の着手 が下記の move
メソッドで行われます。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.last_turn = self.turn
4 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
5 self.move_count += 1
6 self.status = self.judge()
7 self.last_move = x, y
8 self.records.append(self.last_move)
9 self.board_records.append(deepcopy(self.board))
move
メソッドでは以下の処理が行われます。
- 2 行目の
place_mark
メソッドの呼び出しで着手をboard
属性のデータに反映させる - 5 行目で
move_count
に 1 を足す - 9 行目で
board_records
の要素にboard
属性のデータをコピーしたデータを追加する
従って、操作 1 によって それぞれの変数には下記のデータが代入されます。
変数 | 値 |
---|---|
board (3 手目の局面になる) |
|
board_records (0 ~ 3 手目の局面が代入される) |
[ , , , ] |
move_count |
3 |
操作 2 の検証
< ボタンをクリック すると、下記の on_prev_button_clicked
が呼び出されます。
1 def on_prev_button_clicked(b):
2 change_step(self.move_count - 1)
3
4 def change_step(step):
5 step = max(0, step)
6 self.board = deepcopy(self.board_records[step])
7 self.move_count = step
8 self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
9 self.status = self.judge()
10 self.last_move = None if step == 0 else self.records[step - 1]
11 self.draw_board(ax, ai)
12 update_widgets_status()
上記の on_prev_button_clicked
では以下の処理が行われます。下記の処理では、board_records
属性の値 は 変化しない 点に注意して下さい。
- 4 行目で 仮引数
step
にself.move_count - 1
、すなわち 3 - 1 = 2 が代入される - 6 行目で
board
属性にself.board_records[2]
をコピーしたものが代入される - 7 行目で
move_count
属性に 2 が代入される
従って、操作 2 によってそれぞれの変数には下記のデータが代入されます。
変数 | 値 |
---|---|
board (2 手目の局面になる) |
|
board_records (変化しない) |
[ , , , ] |
move_count |
2 |
操作 3 の検証
(0, 1) に着手を行う 操作 3 では、下記の move
メソッドで下記の処理が行われます。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.last_turn = self.turn
4 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
5 self.move_count += 1
6 self.status = self.judge()
7 self.last_move = x, y
8 self.records.append(self.last_move)
9 self.board_records.append(deepcopy(self.board))
- 2 行目で (0, 1) への着手を
board
属性のデータに反映させる - 5 行目で
move_count
に 1 を足して、2 + 1 = 3 を代入する - 9 行目で
board_records
の要素にboard
属性のデータをコピーしたデータを追加する
従って、操作 3 によってそれぞれの変数には下記のデータが代入されます。
変数 | 値 |
---|---|
board |
|
board_records |
[ , , , , ] |
move_count |
3 |
操作 3 によって、board
属性には 2 手目の局面 で、(0, 1) に着手を行ったデータ が代入されるので、正しい画像が描画 されます。
しかし、board_records
の 内容 は元の 0 ~ 3 手目の局面のデータに 新しい局面のデータを追加 した、おかしなデータ になります。本当 は、board_records
は下記のように、元の 3 手目のデータ を 新しい局面のデータで入れ替えたもの になってほしいのですが、move
メソッドが board_records
に対して行う処理 は、新しい局面のデータを追加 するという処理なので、下記のようなデータにはなりません。
また、board_records[3]
には、下図左の 操作 1 による 3 手目 の 古い ゲーム盤のデータが代入されており、実際に画面に描画 される board
属性の、下図右の 操作 3 による 3 手目 の 新しい ゲーム盤のデータとは 異なる という、食い違った状況が発生 しています。
このように、この後の操作 4、5 で行われるおかしな挙動は、実は 操作 3 の時点 で board_records
が おかしなデータになっている ことが原因です。
操作 4 の検証
プログラムの表記は省略しますが、操作 4 では、on_next_button_clicked
が呼び出され、以下の処理が行われます。操作 2 と同様 に board_records
属性の値は 変化しません。
-
step
にself.move_count + 1
、すなわち 3 + 1 = 4 が代入される -
board
属性にself.board_records[4]
をコピーしたものが代入される -
move_count
属性に 4 が代入される
従って、操作 4 によってそれぞれの変数には下記のデータが代入されます。
変数 | 値 |
---|---|
board |
|
board_records (変化しない) |
[ , , , , ] |
move_count |
4 |
操作 4 によって、描画される ゲーム盤の画像が変化しない のは、先程説明したように、board_records[4]
に代入されている ゲーム盤のデータ が、操作 3 によって board_records
に 追加されたゲーム盤のデータと同じ だからです。
操作 5 の検証
プログラムの表記は省略しますが、操作 5 では、on_prev_button_clicked
が呼び出され、以下の処理が行われます。
-
step
にself.move_count - 1
、すなわち 4 - 1 = 3 が代入される -
board
属性にself.board_records[3]
をコピーしたものが代入される -
move_count
属性に 3 が代入される
従って、操作 5 によってそれぞれの変数には下記のデータが代入されます。
変数 | 値 |
---|---|
board |
|
board_records (変化しない) |
[ , , , , ] |
move_count |
3 |
操作 3 の後 で、> と < の順 でボタンをクリックした際に、操作 3 の時とは 異なるゲーム盤が描画される のは、以下のような理由からです。
- 操作 3 では、操作 2 の局面に対して (0, 1) に着手を行ったゲーム盤が描画される
- 操作 5 では、操作 1 で行った 3 手目の局面を表す
board_records[3]
のゲーム盤のデータが描画される
おかしな挙動が行われる原因がわかったので、問題の修正方法を少し考えてみて下さい。
問題の修正
この問題が発生する原因は、move
メソッドで 行う処理 が、常に board_records
に board
属性の値をコピーしたデータを 追加する処理 だからです。最後の着手を行った局面 であれば その処理で問題はありません が、それ以外の局面 の場合は、board_records
の中の、行った着手に対応する要素とは 異なる要素に board
属性の値が代入 されてしまいます。
move
メソッドで行う 着手に対応 する board_records
の 要素のインデックス は、手数を表す move_count
属性の値に等しい ので、move
メソッドで board_records
に対して行う処理を、下記のプログラムの 9 行目のように、board_records[self.move_count]
に board
属性の値をコピーしたデータを代入するように修正することで、この問題を解決できると思った人がいるかもしれません。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
3 self.last_turn = self.turn
4 self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
5 self.move_count += 1
6 self.status = self.judge()
7 self.last_move = x, y
8 self.records.append(self.last_move)
9 self.board_records[self.move_count] = deepcopy(self.board)
10
11 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
self.records.append(self.last_move)
self.board_records[self.move_count] = deepcopy(self.board)
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
self.records.append(self.last_move)
- self.board_records.append(deepcopy(self.board))
+ self.board_records[self.move_count] = deepcopy(self.board)
Marubatsu.move = move
しかし、上記の修正方法 には いくつかの問題があります。どのような問題があるかについて少し考えてみて下さい。
問題の検証と修正
上記の修正後に、下記のプログラムで gui_play
を実行後に、好きなマスをクリックして着手を行うと、実行結果のようなエラーが発生します。
gui_play()
実行結果
略
Cell In[6], line 9
7 self.last_move = x, y
8 self.records.append(self.last_move)
----> 9 self.board_records[self.move_count] = deepcopy(self.board)
IndexError: list assignment index out of range
実行結果から、self.board_records[self.move_count] = deepcopy(self.board)
の処理で、範囲外のインデックスを指定 したことによって エラーが発生する ことがわかります。
1 手目の着手を行う と、move_count
属性には 1
が代入 されますが、board_records
にはゲーム開始時の局面を表す 0 手目のゲーム盤のデータしか代入されておらず、1 番の要素に対応するデータは存在しません。そのためこのエラーが発生します。
従って、このエラーは、下記のプログラムのように、board_records
の要素の数 が手数を表す move_count
未満の場合 は元のプログラムと同様に append
を使って要素を追加 し、そうでなければ 上記のように 代入を行う ようにすることで修正することができます。
1 def move(self, x, y):
元と同じなので省略
2 self.records.append(self.last_move)
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 else:
6 self.board_records[self.move_count] = deepcopy(self.board)
7
8 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
self.records.append(self.last_move)
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
else:
self.board_records[self.move_count] = deepcopy(self.board)
Marubatsu.move = move
修正箇所
def move(self, x, y):
元と同じなので省略
self.records.append(self.last_move)
- self.board_records[self.move_count] = deepcopy(self.board)
+ if len(self.board_records) <= self.move_count:
+ self.board_records.append(deepcopy(self.board))
+ else:
+ self.board_records[self.move_count] = deepcopy(self.board)
Marubatsu.move = move
上記の修正を行った後で、下記のプログラムを実行して先程の操作 1 ~ 5 を行って下さい。
gui_play()
操作 3 で、(0, 1) に着手を行うと、下図のように、> と >> ボタンが 灰色で表示 され、操作できなくなります。3 手目が最後の着手 なので、これは 正しい動作 です。
これで問題は解決できたと思う人がいるかもしれませんが、実は別のバグが潜んでいます。それは、上記の後に <、> の順でボタンをクリック すると、下図のように 赤いマークが表示されなくなる というものです。その原因について少し考えてみて下さい。
直前の着手が赤色で表示されなくなる原因の検証
赤いマーク は 直前の着手を表す ので、原因 は直前の着手の座標を表す last_move
属性と、直前の着手を記録した records
属性に あるはず です。先程と同様の方法で、それぞれの操作によって、last_move
と records
属性の値がどのように変化するかを検証することで問題の原因を見つけることができますが、同じような検証をもう一度詳しく記述するのは冗長なので、結論だけを記します。興味がある方は先ほどと同様の方法で検証してみて下さい。
records
属性は、board_records
属性と同様 に、move
メソッドで 着手を行うたび にその 着手のデータを list で記録 します。従って、move
メソッドでは records
属性に対して board_records
と同様の処理 を行う必要がありますが、先程 board_records
属性に対する処理を、下記のプログラムの 3 ~ 6 行目のように修正した際 に、records
属性に対する 2 行目の処理を修正しませんでした。これがバグの原因です。
1 def move(self, x, y):
元と同じなので省略
2 self.records.append(self.last_move)
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 else:
6 self.board_records[self.move_count] = deepcopy(self.board)
7
8 Marubatsu.move = move
直前の着手が赤色で表示されなくなる問題の修正
下記は、records
属性に対しても同様の処理を行うように修正したプログラムです。なお、このような修正のし忘れはよくあることです。実際に筆者もこの修正を行うのをうっかり忘れてしまい、後でおかしな挙動が発生した際に気づいて修正しました。
-
4、7 行目:
board_records
と同じ方法で、最後に行われた着手のデータを記録する
1 def move(self, x, y):
元と同じなので省略
2 if len(self.board_records) <= self.move_count:
3 self.board_records.append(deepcopy(self.board))
4 self.records.append(self.last_move)
5 else:
6 self.board_records[self.move_count] = deepcopy(self.board)
7 self.records[self.move_count] = self.last_move
8
9 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count] = self.last_move
Marubatsu.move = move
修正箇所
def move(self, x, y):
元と同じなので省略
- self.records.append(self.last_move)
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
+ self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
+ self.records[self.move_count] = self.last_move
Marubatsu.move = move
上記の修正後に、下記のプログラムで gui_play
を実行し、先程の手順で操作を行うと、手順 2 で < ボタンをクリックした後の 手順 3 で (0, 1) のマスに着手を行う と実行結果のような エラーが発生 してしまいます。このエラーの原因について少し考えててみて下さい。
gui_play()
実行結果
略
Cell In[10], line 13
11 else:
12 self.board_records[self.move_count] = deepcopy(self.board)
---> 13 self.records[self.move_count] = self.last_move
IndexError: list assignment index out of range
新たなエラーの原因の検証と修正
エラーメッセージから、self.records[self.move_count]
の self.move_count
が、list のインデックスの範囲外 であることがわかります。初心者の方にはこの原因を見つけづらいのではないかと思いますが、このエラーの原因は、board_records
属性が 0 手目 のゲーム開始時の局面から のゲーム盤のデータを 記録している のに対し、records
属性は、1 手目から の着手のデータを 記録している からです。そのため、records
属性の list の要素の数 は、board_records
属性の list の要素の数より も 1 つだけ小さくなります。
このことに気づく方法の一つは、先程行ったように、操作ごとの records
属性の値を検証するというものですが、紙に書いて検証するのが面倒な場合は、move
メソッドや、change_step
の中に print(self.records)
を記述して、操作を行うたびに records
属性の値を表示すると良いでしょう。
また、print
を書いたり消したりするのが面倒な場合は、test_judge
で実装したような、デバッグ表示の機能を実装するという方法もあります。
従って、下記のプログラムの 8 行目のように、records
のインデックス に self.move_count - 1
を記述する ことで、この問題を修正することができます。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
元と同じなので省略
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 self.records.append(self.last_move)
6 else:
7 self.board_records[self.move_count] = deepcopy(self.board)
8 self.records[self.move_count - 1] = self.last_move
9
10 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count - 1] = self.last_move
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
元と同じなので省略
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
- self.records[self.move_count] = self.last_move
+ self.records[self.move_count - 1] = self.last_move
Marubatsu.move = move
実行結果は省略しますが、上記の修正を行った後で、下記のプログラムを実行して先程の操作を行い、問題が修正されたことを確認して下さい。
gui_play()
データの記録方法の統一による修正方法
上記の修正では、records
属性と、board_records
属性で、インデックスの意味が異なる ため、同じ手数を表すの要素 のデータを 更新する際 に、下記のプログラムのように 異なるインデックスを計算する必要 が生じています。このようなプログラムは わかりづらく、先程のような バグの原因となる可能性が高い ため 避けたほうが良い でしょう。
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count - 1] = self.last_move
そこで、、records
属性と、board_records
属性の インデックスの意味を統一する ことにします。具体的には records
属性の インデックス を、board_records
属性と同様に、ゲーム開始時の 0 手目から数えた手数に対応させる ことにします。下記は、そのように restart
メソッドを修正したプログラムです。
-
2 行目:
records
属性を、0 手目のゲーム開始時の局面の直前の着手を表すself.last_turn
を要素として持つ list で初期化するように修正する
1 def restart(self):
元と同じなので省略
2 self.records = [self.last_turn]
3 self.board_records = [deepcopy(self.board)]
4
5 Marubatsu.restart = restart
行番号のないプログラム
def restart(self):
self.initialize_board()
self.turn = Marubatsu.CIRCLE
self.move_count = 0
self.status = Marubatsu.PLAYING
self.last_move = -1, -1
self.last_turn = None
self.records = [self.last_turn]
self.board_records = [deepcopy(self.board)]
Marubatsu.restart = restart
修正箇所
def restart(self):
元と同じなので省略
- self.records = []
+ self.records = [self.last_turn]
self.board_records = [deepcopy(self.board)]
Marubatsu.restart = restart
次に、move
メソッドを、下記のプログラムの 8 行目のように修正します。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
元と同じなので省略
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 self.records.append(self.last_move)
6 else:
7 self.board_records[self.move_count] = deepcopy(self.board)
8 self.records[self.move_count] = self.last_move
9
10 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count] = self.last_move
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
元と同じなので省略
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
- self.records[self.move_count - 1] = self.last_move
+ self.records[self.move_count] = self.last_move
Marubatsu.move = move
これで修正は完了したと思っている人がいるかもしれませんが、もう一か所修正する必要があります。それが何かを少し考えてみて下さい。
last_move
属性の値は、リプレイ機能のボタンをクリックした際 に呼び出される change_step
内で変更される ので、その部分を下記のプログラムのように修正する必要があります。なお、0 手目 の直前の着手のデータが records
属性内に記録される ようになったので、下記のプログラムの 4 行目のように last_move
を変更する処理を簡潔に記述できる ようになります。修正前のプログラムと比較してみて下さい。
1 def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
2 def change_step(step):
元と同じなので省略
3 # 直前の着手を計算する
4 self.last_move = self.records[step]
元と同じなので省略
5
6 Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
# ai_dict が None の場合は、空の list で置き換える
if ai_dict is None:
ai_dict = {}
# params が None の場合のデフォルト値を設定する
if params is None:
params = [{}, {}]
# seed が None でない場合は、seed を乱数の種として設定する
if seed is not None:
random.seed(seed)
# gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
if gui:
# %matplotlib widget のマジックコマンドを実行する
get_ipython().run_line_magic('matplotlib', 'widget')
# ボタンのウィジェットの状態を設定する
def set_button_status(button, disabled):
button.disabled = disabled
button.style.button_color = "lightgray" if disabled else "lightgreen"
# ウィジェットの状態を更新する
def update_widgets_status():
# 0 手目と最後の着手を行った局面で、特定のリプレイに関するボタンを操作できないようにする
set_button_status(first_button, self.move_count <= 0)
set_button_status(prev_button, self.move_count <= 0)
set_button_status(next_button, self.move_count >= len(self.board_records) - 1)
set_button_status(last_button, self.move_count >= len(self.board_records) - 1)
# 1 行目の UI を作成する
# それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
select_values = []
# 〇 と × の Dropdown を格納する list
dropdown_list = []
# ai に代入されている内容を ai_dict に追加する
for i in range(2):
# ラベルと項目の値を計算する
if ai[i] is None:
label = "人間"
value = "人間"
else:
label = ai[i].__name__
value = ai[i]
# value を select_values に常に登録する
select_values.append(value)
# value が ai_values に登録済かどうかを判定する
if value not in ai_dict.values():
# 項目を登録する
ai_dict[label] = value
# Dropdown の description を計算する
description = "〇" if i == 0 else "×"
dropdown_list.append(
widgets.Dropdown(
options=ai_dict,
description=description,
layout=widgets.Layout(width="100px"),
style={"description_width": "20px"},
value=select_values[i],
)
)
# ボタンを作成するローカル関数を定義する
def create_button(description, width):
return widgets.Button(
description=description,
layout=widgets.Layout(width=f"{width}px"),
style={"button_color": "lightgreen"},
)
# 変更、リセットボタンを作成する
change_button = create_button("変更", 100)
reset_button = create_button("リセット", 100)
# 変更ボタンのイベントハンドラを定義する
def on_change_button_clicked(b):
for i in range(2):
ai[i] = None if dropdown_list[i].value == "人間" else dropdown_list[i].value
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# リセットボタンのイベントハンドラを定義する
def on_reset_button_clicked(b):
self.restart()
update_widgets_status()
on_change_button_clicked(b)
# イベントハンドラをボタンに結びつける
change_button.on_click(on_change_button_clicked)
reset_button.on_click(on_reset_button_clicked)
# 2 行目の UI を作成する
# リプレイのボタンを作成する
first_button = create_button("<<", 100)
prev_button = create_button("<", 100)
next_button = create_button(">", 100)
last_button = create_button(">>", 100)
def change_step(step):
# step がインデックスの範囲外の場合は、範囲内に収める
step = max(0, min(len(self.board_records) - 1, step))
# step 手目のゲーム盤のデータをコピーし、board に代入する
self.board = deepcopy(self.board_records[step])
# 手数を表す step を move_count に代入する
self.move_count = step
# 手番を計算する。step が偶数の場合は 〇 の 手番
self.turn = Marubatsu.CIRCLE if step % 2 == 0 else Marubatsu.CROSS
# status 属性を judget を使って計算する
self.status = self.judge()
# 直前の着手を計算する
self.last_move = self.records[step]
# 描画を更新する
self.draw_board(ax, ai)
# ウィジェットの状態を更新する
update_widgets_status()
def on_first_button_clicked(b):
change_step(0)
def on_prev_button_clicked(b):
change_step(self.move_count - 1)
def on_next_button_clicked(b):
change_step(self.move_count + 1)
def on_last_button_clicked(b):
change_step(len(self.board_records) - 1)
first_button.on_click(on_first_button_clicked)
prev_button.on_click(on_prev_button_clicked)
next_button.on_click(on_next_button_clicked)
last_button.on_click(on_last_button_clicked)
# 〇 と × の dropdown とボタンを横に配置した HBox を作成する
hbox1 = widgets.HBox([dropdown_list[0], dropdown_list[1], change_button, reset_button])
# リプレイ機能のボタンを横に配置した HBox を作成する
hbox2 = widgets.HBox([first_button, prev_button, next_button, last_button])
# hbox1 と hbox2 を縦に配置した VBox を作成し、表示する
display(widgets.VBox([hbox1, hbox2]))
fig, ax = plt.subplots(figsize=[size, size])
fig.canvas.toolbar_visible = False
fig.canvas.header_visible = False
fig.canvas.footer_visible = False
fig.canvas.resizable = False
# ローカル関数としてイベントハンドラを定義する
def on_mouse_down(event):
# Axes の上でマウスを押していた場合のみ処理を行う
if event.inaxes and self.status == Marubatsu.PLAYING:
x = math.floor(event.xdata)
y = math.floor(event.ydata)
self.move(x, y)
self.draw_board(ax, ai)
update_widgets_status()
# 次の手番の処理を行うメソッドを呼び出す
self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
# fig の画像にマウスを押した際のイベントハンドラを結び付ける
fig.canvas.mpl_connect("button_press_event", on_mouse_down)
else:
ax = None
self.restart()
update_widgets_status()
return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
Marubatsu.play = play
修正箇所
def play(self, ai, ai_dict=None, params=None, verbose=True, seed=None, gui=False, size=3):
元と同じなので省略
def change_step(step):
元と同じなので省略
# 直前の着手を計算する
- self.last_move = None if step == 0 else self.records[step - 1]
+ self.last_move = self.records[step]
元と同じなので省略
Marubatsu.play = play
実行結果は省略しますが、上記の修正を行った後で、下記のプログラムを実行して先程の操作を行い、正しい処理が行われることを確認して下さい。
gui_play()
別の問題の検証と修正
リプレイ機能には、別の問題がまだ潜んでいます。ゲームをリセットし、下記の手順で操作を行ってください。
- (0, 0)、(1, 0)、(2, 0) の順で着手を行う
- << ボタンをクリックして最初の局面に戻す
- (1, 1) に着手を行う
上記の操作を行うと、下記の表示が行われます。一見すると問題はなさそうに思えるかもしれませんが、下記の画像にはおかしな点があります。それが何かを少し考えてみて下さい。
おかしな点は、> や >> のボタンが 緑色で表示 されて、操作を行うことができる点 です。リプレイ中に他の着手を行った場合 は、そこから 元のリプレイとは異なるゲームが行われる ことを意味します。そのため、リプレイ中に他の着手を行った場合は、その局面が最後の着手を行った局面になる ので、> や >> ボタンを 灰色で表示して操作できないようにする 必要があります。また、上記 で > をクリックすると、下図のように、元の 2 手目の局面が表示される という問題が発生します。図は省略しますが、さらに > をクリックするか、>> ボタンをクリックすると、元の 3 手目の局面が表示されます。
問題の検証
この問題は、リプレイ中 に、最後の着手が行われた以外の局面で着手を行った際に発生します。その場合は、下記の move
メソッドの 7 行目で self.board_records[self.move_count] = deepcopy(self.board)
が実行されますが、その処理だけ では、それ以降の手番のデータ が そのまま残ってしまいます。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
省略
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 self.records.append(self.last_move)
6 else:
7 self.board_records[self.move_count] = deepcopy(self.board)
8 self.records[self.move_count] = self.last_move
9
10 Marubatsu.move = move
例えば、(0, 0)、(1, 0)、(2, 0) の順で着手を行い、<< ボタンをクリックすると、それぞれの変数の値 は以下のようになります。
変数 | 値 |
---|---|
board |
|
board_records (変化しない) |
[ , , , ] |
move_count |
0 |
この状態で、(1, 1) に着手を行うと、下記の表のように board_records[1]
に board
属性を コピーしたデータが代入 されますが、board_records[2]
と board_records[3]
のデータは 元のまま残ってしまいます。
変数 | 値 |
---|---|
board |
|
board_records (変化しない) |
[ , , , ] |
move_count |
0 |
問題の修正
この問題は、self.board_records[self.move_count] = deepcopy(self.board)
の処理を行った後で、それ以降の要素を削除する ことで修正することができます。list の特定の範囲の要素 は、下記のプログラムの 9、10 行目のように、以前の記事で説明した スライス表記 を使って 取り出すことができます。なお、先程と同様に、records
属性に対しても 同様の処理を行う必要がある ことを忘れないようにして下さい。
スライス表記で 0 ~ x 番の要素を取り出したい場合は、[0:x+1]
のように記述する必要がある点に注意して下さい。
1 def move(self, x, y):
2 if self.place_mark(x, y, self.turn):
元と同じなので省略
3 if len(self.board_records) <= self.move_count:
4 self.board_records.append(deepcopy(self.board))
5 self.records.append(self.last_move)
6 else:
7 self.board_records[self.move_count] = deepcopy(self.board)
8 self.records[self.move_count] = self.last_move
9 self.records = self.records[0:self.move_count + 1]
10 self.board_records = self.board_records[0:self.move_count + 1]
11
12 Marubatsu.move = move
行番号のないプログラム
def move(self, x, y):
if self.place_mark(x, y, self.turn):
self.last_turn = self.turn
self.turn = Marubatsu.CROSS if self.turn == Marubatsu.CIRCLE else Marubatsu.CIRCLE
self.move_count += 1
self.status = self.judge()
self.last_move = x, y
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
self.board_records = self.board_records[0:self.move_count + 1]
Marubatsu.move = move
修正箇所
def move(self, x, y):
if self.place_mark(x, y, self.turn):
元と同じなので省略
if len(self.board_records) <= self.move_count:
self.board_records.append(deepcopy(self.board))
self.records.append(self.last_move)
else:
self.board_records[self.move_count] = deepcopy(self.board)
self.records[self.move_count] = self.last_move
self.records = self.records[0:self.move_count + 1]
self.board_records = self.board_records[0:self.move_count + 1]
Marubatsu.move = move
実行結果は省略しますが、上記の修正を行った後で、下記のプログラムを実行して先程の操作を行い、この問題が解消されたことを確認して下さい。
gui_play()
今回の記事のまとめ
今回の記事では、リプレイ機能の問題を修正 しましたが、実は今回のプログラムには AI が手番を担当した場合 に うまく動作しない という問題があります。次回の記事ではその問題を修正しますが、余裕がある方は、AI に手番を担当させた場合に、リプレイ機能に関するどのような問題が発生するかを調べておいてください。
本記事で入力したプログラム
以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。
以下のリンクは、今回の記事で更新した marubatsu.py です。
次回の記事