0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonで〇×ゲームのAIを一から作成する その75 循環インポートとDropdownの項目の自動登録

0
Last updated at Posted at 2024-04-25

目次と前回の記事

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

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

ルールベースの AI の一覧

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

循環インポートのエラーの意味とその対処方法

前回の記事で 作成 したプログラムを marubatsu.py に 記述 し、下記 のプログラムを実行すると 実行結果 のような エラーが発生 します。今回の記事では、最初にこの エラーの意味 と 対処方法 について 説明 します。

%matplotlib widget
from marubatsu import Marubatsu

mb = Marubatsu()
mb.play(ai=[None, None], gui=True)

実行結果

略
File c:\Users\ys\ai\marubatsu\075\ai.py:3
      1 # 3.7 ~ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
      2 from __future__ import annotations
----> 3 from marubatsu import Marubatsu, Markpat
      4 from random import choice
      5 from collections import defaultdict

ImportError: cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import) (c:\Users\ys\ai\marubatsu\075\marubatsu.py)

上記のエラーメッセージは、以下のような意味を持ちます。

  • ImportError
    モジュールのインポート(import)に関するエラー
  • cannot import name 'Marubatsu' from partially initialized module 'marubatsu' (most likely due to a circular import)
    部分的に(partially)初期化された(initialized)marubatsu というモジュール(module)から(from)Marubatsu という名前(name)をインポート(import)できない(can not)。循環インポート(circular import)が最も可能性が高い(most likely)原因(due to)である

循環インポートとは何か

上記 の エラーメッセージ から、エラーの原因 が、循環インポート(circular import)である 可能性が最も高い ことが わかります。

循環インポート がどのようなものであるかを説明するために、先程実行した プログラム の 処理の流れ を、順を追って説明 します。

marubatsu モジュールのインポート

下記 の 先程のプログラム は、2 行目 で marubatsu モジュールから Marubatsu という クラス を インポート します。

%matplotlib widget
from marubatsu import Marubatsu
以下略

以前の記事で説明したように、モジュール を インポート する際には、その モジュール のファイルの中に 記述 された プログラムが すべて 実行 されます。それは、import marubatsu のように、モジュールそのもの を インポート する場合でも、from marubatsu import Marubatsu のように、モジュールの中 で 定義 された 関数 や クラス などを インポート する場合でも 変わりません。そのため、上記 のプログラムの 2 行目 を 実行 すると、marubatsu.py の中に 記述 された プログラム が すべて実行 されます。

python では、モジュール を インポート する処理が行われると、関数 や メソッド などの 呼び出し と 同様 に、モジュール に 記述 された プログラム を すべて実行 し、その 処理 が 完了してから プログラムの 続きが実行 されます。従って、上記 の marubatsu.py の 2 行目 の from marubatsu import Marubatsu が 実行 されると marubatsu.py に 記述 された プログラムが実行 され、その 処理が完了 してから、3 行目以降 の プログラムが 実行 されます。

marubatsu モジュールの実行

下記は、marubatsu.py の 先頭に記述 された、他のモジュール を インポート するプログラムで、marubatsu.py の中 で、ai モジュールを インポート していることが わかります。

# 3.7 ~ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
from __future__ import annotations
略
import ai as ai_module
以下略

先程と同様に、上記 の marubatsu.py の 4 行目 の import ai as ai_module が 実行 されると ai.py に 記述 された プログラムが実行 され、その 処理が完了 してから、marubatsu.py の 5 行目以降 の プログラムが 実行 されます。

ai モジュールの実行

下記は、ai.py の 先頭に記述 された、他のモジュール を インポート するプログラムで、ai.py の中 で、marubatsu モジュールを インポート していることが わかります。

# 3.7 ~ 3.9 の Python のバージョンでエラーが発生しないようにするためのインポート
from __future__ import annotations
from marubatsu import Marubatsu, Markpat
以下略

以前の記事で、Python では、インポートしたモジュール を もう一度インポート しようとした場合は、その モジュールのプログラム が 実行されることはない と説明しましたが、それは、インポート した モジュールのプログラム が 完全に実行された場合 です。

最初に実行 した from marubatsu import Marubatsu によって 実行 された marubatsu.py のプログラムは、実行 の 途中 で ai モジュールを インポート する 処理を行った ため、上記 のプログラムの 3 行目 の from marubatsu import Marubatsu, Markpat を 実行する時点 では 処理 が 完了していません。そのため、上記の 3 行目 を 実行 すると、marubatsu モジュールを インポート するために、marubatsu.py のプログラムが 実行する必要 が 生じます。

インポートの無限の繰り返し

実際 には、先程 の エラーが発生 するので、marubatsu モジュールが もう一度実行 されることは ありません が、仮に 2 度目 の marubatsu モジュールが 実行された場合 に どのようなことが起きるか について 説明 します。

2 度目 の marubatsu モジュールが 実行 された場合は、1 度目 と 同様 に、import ai as ai_module によって ai モジュールを インポート する 必要 が 生じます が、1 度目 の ai モジュールの ai.py の 実行処理 も、途中 で marubatsu モジュールを インポートする処理 が 行われた ため 完了していません。そのため、ai モジュールを インポート するために、もう一度 の ai.py を 実行 する 必要 が 生じます。以後 は 同様の処理 が 繰り返し 行われます。

モジュール A から モジュール B を インポートする処理 を 「A → B」のように 表記 した場合、上記の処理 は、 「marubatsu → ai → marubatsu → ai → marubatsu → ・・・」のように、インポートの処理 が 循環 するため、無限 に 繰り返される ことになります。これが 循環インポート で、Python では 循環インポート が起きた場合は ImportError を 発生 させて、プログラム を 強制的に終了 するという 仕組み になっています。

循環インポートのような、参照が循環 する 状況 のことを 循環参照 と呼びます。

循環インポート は、以下 のような場合に 発生 する。

モジュール A が 別のモジュール B を インポートする という 処理 を「A → B」と 表記 した場合に、「A → B →・・・→ A」のように、インポート した モジュール を 辿っていく と、元のモジュール に 戻る という 循環が存在する。

Python では、モジュール を インポートする際 に行われる 処理 を、モジュール を 初期化する(initalize)と呼びます。先程 の プログラム を 実行 した際に 表示 された partially initialized module という エラーメッセージ は、モジュール が 部分的(partially)に 初期化 されている(initialized)、すなわち、モジュール の インポート の 処理 が 完了していない という 意味 を表します。

このノート は、細かい話 なので、意味がわからない 場合は 無視して下さい。

循環インポート が あっても、インポート した モジュール の 名前空間 の 名前 を 利用しなければ、エラー は 発生しません。例えば、下記 のような、お互い を インポート する test.py と test2.py があった場合に、import test で test モジュールを インポート しても、それらの モジュールの中 で、インポートしたモジュール の 名前空間 の 名前 を 参照していない ので エラー は 発生しません。

test.py
import test2
test2.py
import test
import test

実行結果(エラーは発生せず、何も表示されない)

一方、下記 の お互い を インポート する test3.py と test4.py は、その中で インポート した モジュール の 名前空間 の 名前を参照する ので、import test3 を 実行 すると 循環インポート を表す ImportError の エラーが発生 します。このようなことが起きるのは、モジュール内 で 定義 された 名前を利用 するための 名前空間 が 完成 するのは、モジュール の 実行の処理 が 完了した時点 だからです。

test3.py
import test4
a = 1
print(test4.a)
test4.py
import test3
a = 2
print(test3.a)
import test3

実行結果

略
File c:\Users\ys\ai\marubatsu\075\test4.py:3
      1 import test3
      2 a = 2
----> 3 print(test3.a)

AttributeError: partially initialized module 'test3' has no attribute 'a' (most likely due to a circular import)

循環インポートの解決方法

循環インポート の エラーを修正 するためには、循環インポート が 起きないよう に プログラム を 記述する必要 が あります。今回の場合は、 「marubatsu → ai → marubatsu → ・・・」という 循環インポート が 発生 してるので、以下 の いずれかの修正 を行うことで、循環インポート が 発生しなくなります。

  • marubatsu モジュールから ai モジュールを インポートしない
  • ai モジュールから marubatsu モジュールを インポートしない

ai モジュール内のプログラムは、marubatsu モジュールで 定義 された Marubatsu クラスを 利用する必要 が ある ので、どうしても marubatsu モジュールを インポート する 必要 が あります。そのため、循環インポート を 解消 するためには、marubatsu モジュールから、ai モジュールを インポートしない ようにする 必要 が あります。

marubatsu モジュール内で ai モジュールを 利用 しているのは、前回の記事で ai モジュール内で 定義 された AI を Dropdown で 選択できる ように 修正 した play メソッドです。下記 は、play メソッド内で ai モジュールを 利用する処理 の部分のプログラムです。具体的には、5 行目 の ai_module が インポート した ai モジュールを 表します。

        # Dropdown の作成の際に必要となる、AI のリストを作成する    
        ai_list = []
        for i in range(1, 15):
            ai_name = f"ai{i}s"  
            ai_list.append((ai_name, getattr(ai_module, ai_name)))    

上記 の 処理 を play メソッドから 削除 することで、marubatsu モジュール内で ai モジュールを インポート する 必要 が なくなります。ただし、この処理 を play メソッドから 削除 してしまうと、Dropdown に 登録 する AI の一覧 の 情報 が なくなってしまう ので、その代わりに 下記 のプログラムのように、play メソッドの 仮引数 に ai_list を追加 し、play メソッドを 実行する際 に、Dropdown に 登録 する AI の一覧 を 実引数 で記述して 指定する ように 修正 することにします。その際に、これまでのプログラムとの 互換性を考慮 して、ai_list は、空の list を デフォルト値 とした デフォルト引数 とします。

  • 1 行目:仮引数 に、デフォルト値 に 空の list を 設定 した ai_list を 追加 する
  • ai_list を 作成する処理 を 削除 する
1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
2      # seed が None でない場合は、seed を乱数の種として設定する
3      if seed is not None:
4          random.seed(seed)
5
6      # この下にあった、ai_list を作成する処理を削除する
元と同じなので省略
行番号のないプログラム
    def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
        # seed が None でない場合は、seed を乱数の種として設定する
        if seed is not None:
            random.seed(seed)
        
        # 〇 と × の Dropdown を作成する   
        dropdown_circle = widgets.Dropdown(
            options=ai_list,
            description="〇",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
        )
        dropdown_cross = widgets.Dropdown(
            options=ai_list,
            description="×",
            layout=widgets.Layout(width="100px"),
            style={"description_width": "20px"},
        )       
        
        # リセットボタンを作成する
        button = widgets.Button(
            description="リセット",
            layout=widgets.Layout(width="100px"),
        )
        
        # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
        hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
        display(hbox)
        
        # リセットボタンのイベントハンドラを定義する
        def on_button_clicked(b):
            self.restart()
            self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
            
        # イベントハンドラをリセットボタンに結びつける
        button.on_click(on_button_clicked)

        # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
        if gui:
            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)

                    # 次の手番の処理を行うメソッドを呼び出す
                    self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                    
            # fig の画像にマウスを押した際のイベントハンドラを結び付ける
            fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

        self.restart()
        return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
修正箇所
-   def play(self, ai, params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
+   def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
        # seed が None でない場合は、seed を乱数の種として設定する
        if seed is not None:
            random.seed(seed)

        # Dropdown の作成の際に必要となる、AI のリストを作成する    
-       ai_list = []
-       for i in range(1, 15):
-           ai_name = f"ai{i}s"  
-           ai_list.append((ai_name, getattr(ai_module, ai_name)))    
元と同じなので省略

marubatsu モジュールを インポート すると エラーが発生 するため、これまで のように、上記 のプログラムを 記述した後 で、Marubatsu.play = play を 実行 するという方法で play メソッドを 修正 することは できません。そこで、今回の記事では、marubatsu.py から ai モジュールを インポート する import ai as ai_module を 削除 し、play メソッドを 上記 のように 修正 した marubatsu_correct.py という ファイルを保存 することにします。次回の記事 からは、これまでと同様 に marubatsu.py というファイルに 保存 します。

下記 は、そのファイル を 保存後 に、ai_list を 作成する処理 を 記述 し、play メソッドの 実引数に記述 して 実行 するプログラムです。実行結果 から、正しく プログラムが 動作する ことが 確認できます。

  • 1 行目:marubatsu_correct から Marubatsu クラスを インポート する
  • 2 行目:ai モジュールを インポート する。なお、play メソッドの 外 で ai_list を作成 するため、ai モジュールと、play メソッドの 仮引数 ai の 名前 が かぶらなくなった ので、import ai as ai_module のように 名前を変えてインポート する 必要はない
  • 5 ~ 8 行目:play メソッド内で 記述 していたのと 同じ方法 で ai_list を 作成 する
  • 11 行目:play メソッドの 実引数 に ai_list=ai_list を 記述 して 呼び出す
 1  from marubatsu_correct import Marubatsu
 2  import ai
 3
 4  # Dropdown の作成の際に必要となる、AI のリストを作成する    
 5  ai_list = []
 6  for i in range(1, 15):
 7      ai_name = f"ai{i}s"  
 8      ai_list.append((ai_name, getattr(ai, ai_name)))    
 9   
10  mb = Marubatsu()
11  mb.play(ai=[None, None], ai_list=ai_list, gui=True)
行番号のないプログラム
from marubatsu_correct import Marubatsu
import ai

# Dropdown の作成の際に必要となる、AI のリストを作成する    
ai_list = []
for i in range(1, 15):
    ai_name = f"ai{i}s"  
    ai_list.append((ai_name, getattr(ai, ai_name)))    
    
mb = Marubatsu()
mb.play(ai=[None, None], ai_list=ai_list, gui=True)

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

play メソッドの改良その 1(ai の内容の自動登録)

前回の記事では play メソッドを、ai1s ~ ai14s の 14 種類 の AI を 選択できる ように 実装 しましたが、一方で それ以外の AI を 選択できない という 欠点 があります。

それに対し、先程修正 した play メソッドは、好きな AI のリスト を 作成 して 実引数に記述 することで、任意の AI を 選択できる ようになったという 利点 があります。しかし、その一方 で、下記 のプログラムのように ai_list に 対応 する 実引数 を 記述せず に play メソッドを呼び出すと、Dropdown の options 属性に 空の list が 代入 されるため、下図左 の 実行結果 のように、Dropdown に 何も表示されなく なります。また、Dropdown の上 で マウスを押す と、下図右 のように、選択できる項目がない メニューが 表示 されます。

mb.play(ai=[None, None],  gui=True)

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

 

Dropdown の 項目 に 何も表示されない のは 変 なので、仮引数 ai に 代入 された 内容 を Dropdown に 自動的に登録 するように play メソッドを 修正 することにします。

人間の登録

先程 の 下記 のプログラムは、人間どうし の 対戦 を行うものですが、前回の記事 で 作成 した Dropdown には AI のみ が 登録 されていました。

mb.play(ai=[None, None],  gui=True)

人間を表す Dropdown の項目の設定

そのため、Dropdown に 人間を表す項目 を どのように登録するか について 決める必要 が あります。Dropdown の項目 には、メニューに 表示する文字 を表す ラベル と、項目の値 を 決める必要 が あります。本記事では ラベル は "人間" とすることにします。他の文字が良いと思った人は自由に変更して下さい。項目の値 は、play メソッドの 仮引数 ai では 人間 の担当を None と 表していた ので、そのまま None を設定 することにします。

実は、Dropdown の 項目の値 に None を 設定するべき では ありません が、筆者はそのことに気づかずに最初は None を設定し、後からおかしいことに気づきました。Dropdown の 仕組み を 理解する 上で、この 間違いの体験 は 役に立つ と 思いました ので、あえて最初 は Dropdown の 項目の値 に None を 設定 しました。

play メソッドの修正

下記 は、仮引数 ai の 要素 に 人間を表す None が 代入 されていた場合に、ai_list に 人間を表す項目 を 追加 するように play メソッドを 修正 したプログラムです。

  • 11 ~ 13 行目:繰り返し処理 によって、ai の 各要素 に None が代入 されている場合に、ai_list に 人間の項目 を 追加 する
 1  import matplotlib.pyplot as plt
 2  import ipywidgets as widgets
 3  import math
 4
 5  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
 6      # seed が None でない場合は、seed を乱数の種として設定する
 7      if seed is not None:
 8          random.seed(seed)
 9   
10      # ai に代入されている内容を ai_list に追加する
11      for i in range(2):
12          if ai[i] is None:
13              ai_list.append(("人間", None))
元と同じなので省略
14
15  Marubatsu.play = play
行番号のないプログラム
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None:
            ai_list.append(("人間", None))
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
import matplotlib.pyplot as plt
import ipywidgets as widgets
import math

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai に代入されている内容を ai_list に追加する
+   for i in range(2):
+       if ai[i] is None:
+           ai_list.append(("人間", None))
元と同じなので省略

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 の 下図左 のように、Dropdown に 人間が表示 されるようになります。しかし、Dropdown の上 で マウスを押す と、下図右 のように 人間 が 2 つ登録 されるという 問題が発生 します。このようなことがおきる原因について少し考えてみて下さい。

mb.play(ai=[None, None], gui=True);

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

 

項目の重複登録の原因と修正

先程 のプログラムは、繰り返し処理 によって、ai の 各要素 が None の場合に 人間の項目 を 登録 する処理を行います。そのため、人間 VS 人間 の場合は、2 回分 の 人間の項目 が 登録 されてしまいます。この 問題を解決 するためには、ai_list に既に 登録済の項目 を 重複 して 登録しない ようにする 必要 が あります。

list の 要素の中 に、特定の値 が 含まれているか どうかは、in という 演算子 で 判定 することが できます が、ai_list の 要素 に 人間を表す項目 が 含まれているか どうかを None in ai_list で 判定 することは残念ながら できません。その 理由 は、ai_list の 要素 が、(ラベル, 項目の値) という tuple だからです。ai_list の中から、項目の値 だけを 取り出した list を 作成 することで、in 演算子を使って 判定できる ようになります。

下記は、play メソッドで 人間の項目 を 重複 して 登録しないよう にするプログラムです。

  • 7 行目:list 内包表記 を使って、ai_list の 各要素 から 項目の値 を 取り出した list を 作成 し、ai_values に 代入 する
  • 10 行目:条件式 に「None が ai_values に 登録されていない」という 条件を追加 する
  • 12 行目:ai_values に、None を 追加 する

ai_list と ai_values は 異なる list なので、12 行目 で ai_values に None を 追加 する 処理 を 忘れないよう に 注意 して下さい。

 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
 2      # seed が None でない場合は、seed を乱数の種として設定する
 3      if seed is not None:
 4          random.seed(seed)
 5   
 6      # ai_list から、項目だけを取り出した list を作成する
 7      ai_values = [value for label, value in ai_list]
 8      # ai に代入されている内容を ai_list に追加する
 9      for i in range(2):
10          if ai[i] is None and None not in ai_values:
11              ai_list.append(("人間", None))
12              ai_values.append(None)
元と同じなので省略
13
14  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None and None not in ai_values:
            ai_list.append(("人間", None))
            ai_values.append(None)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
+   ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None:
+       if ai[i] is None and None not in ai_values:
            ai_list.append(("人間", None))
            ai_values.append(None)
元と同じなので省略

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 し、Dropdown の上 で マウスを押す と、下図右 のように 人間 が 1 つだけ表示 されることが 確認 できます。

mb.play(ai=[None, None], gui=True);

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

 

AI の登録

次は、AI を 自動的に登録 することにします。AI の ラベル は、AI の 関数名 を 設定 すれば良く、関数の名前 は 以前の記事で説明したように、__name__ という 特殊属性 に 代入 されています。従って、play メソッドの 該当する部分 を 下記 のように 修正 します。

  • 6 行目:人間でなく、ai[i] が ai_values に 登録されていない ことを 判定 する
  • 7、8 行目:ai_list と ai_values に AI のデータ を 登録 する
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and None not in ai_values:
4          ai_list.append(("人間", None))
5          ai_values.append(None)
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        

上記 のプログラムは、3 ~ 5 行目 と 6 ~ 8 行目 の 重複する処理 を まとめる ことが できます。どのようにすれば良いかについて少し考えてみて下さい。

重複する処理の統合

上記 のプログラムの 3 行目 の 条件式 が True になる 場合 は、ai[i] の値が None になります。従って、上記 のプログラムは、下記 のように 記述 することができます。

  • 3 ~ 5 行目:None を ai[i] に 修正 する
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and ai[i] not in ai_values:
4          ai_list.append(("人間", ai[i]))
5          ai_values.append(ai[i])
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        
行番号のないプログラム
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] is None and ai[i] not in ai_values:
            ai_list.append(("人間", ai[i]))
            ai_values.append(ai[i])
        elif ai[i] is not None and ai[i] not in ai_values:
            ai_list.append((ai[i].__name__, ai[i]))
            ai_values.append(ai[i])       
修正箇所
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None and None not in ai_values:
+       if ai[i] is None and ai[i] not in ai_values:
-           ai_list.append(("人間", None))
+           ai_list.append(("人間", ai[i]))
-           ai_values.append(None)
+           ai_values.append(ai[i])
        elif ai[i] is not None and ai[i] not in ai_values:
            ai_list.append((ai[i].__name__, ai[i]))
            ai_values.append(ai[i])       

上記 の 3 行目 と 6 行目 の 条件式 は、いずれ も and の後ろ に ai[i] not in ai_values が 記述 されているので、どちらのブロック も ai[i] not in ai_values が True になる 場合のみ実行 されます。従って、上記 のプログラムは、下記 のように 記述 できます。

  • 3 行目:ai[i] が ai_values に 含まれているか どうかを 判定 する
  • 4 ~ 6 行目:人間が担当 する場合に、人間の項目 を 登録 する
  • 7 ~ 9 行目:AI が担当 する場合に、AI の項目 を 登録 する
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              ai_list.append(("人間", None))
6              ai_values.append(ai[i])
7          elif ai[i] is not None:
8              ai_list.append((ai[i].__name__, ai[i]))
9              ai_values.append(ai[i])        
行番号のないプログラム
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", None))
                ai_values.append(ai[i])
            elif ai[i] is not None:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i])    
修正箇所
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
-       if ai[i] is None and ai[i] not in ai_values:
-           ai_list.append(("人間", ai[i]))
-           ai_values.append(ai[i])
+       if ai[i] not in ai_values:
+           if ai[i] is None:
+               ai_list.append(("人間", None))
+               ai_values.append(ai[i])
-       elif ai[i] is not None and ai[i] not in ai_values:
-           ai_list.append((ai[i].__name__, ai[i]))
-           ai_values.append(ai[i])  
+           elif ai[i] is not None:
+              ai_list.append((ai[i].__name__, ai[i]))
+               ai_values.append(ai[i])    

上記 の 7 行目 の 条件式 の ai[i] is not None は、4 行目 の ai[i] is None が False の場合に 実行 されるので、その場合 は 必ず True になります。従って、上記 のプログラムは 下記 のように 記述 できます。

  • 7 行目:else に 修正 する
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              ai_list.append(("人間", ai[i]))
6              ai_values.append(ai[i])
7          else:
8              ai_list.append((ai[i].__name__, ai[i]))
9              ai_values.append(ai[i])        
行番号のないプログラム
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", ai[i]))
                ai_values.append(ai[i])
            else:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i]) 
修正箇所
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                ai_list.append(("人間", ai[i]))
                ai_values.append(ai[i])
-           elif ai[i] is not None:
+           else:
                ai_list.append((ai[i].__name__, ai[i]))
                ai_values.append(ai[i]) 

上記 の 5、6 行目 の ブロック と、8、9 行目 の ブロック の処理で 異なる のは、項目 の ラベルだけ なので、上記 のプログラムは、下記 のように 記述 できます。

  • 5、7 行目:条件式 の ブロック内 で行う 処理 を、ラベルの計算だけ にする
  • 8、9 行目:ai_list と ai_values の 要素の追加 を、ブロックの外 で 行う ようにする
1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] not in ai_values:
4          if ai[i] is None:
5              label = "人間"
6          else:
7              label = ai[i].__name__        
8          ai_list.append((label, ai[i]))
9          ai_values.append(ai[i])
行番号のないプログラム
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
修正箇所
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        if ai[i] not in ai_values:
+           if ai[i] is None:
-               ai_list.append(("人間", ai[i]))
-               ai_values.append(ai[i])
+               label = "人間"
+           else:
-               ai_list.append((ai[i].__name__, ai[i]))
-               ai_values.append(ai[i]) 
+               label = ai[i].__name__        
+           ai_list.append((label, ai[i]))
+           ai_values.append(ai[i])

上記が 重複する処理 を まとめた プログラムです。下記 の まとめる前 のプログラムと 比べる と、行数 が 1 行増えています が、「条件式 が 簡潔 になっている」、「重複する処理 が まとめられている」という点で 元のプログラム より わかりやすく なっています。

1  # ai に代入されている内容を ai_list に追加する
2  for i in range(2):
3      if ai[i] is None and None not in ai_values:
4          ai_list.append(("人間", None))
5          ai_values.append(None)
6      elif ai[i] is not None and ai[i] not in ai_values:
7          ai_list.append((ai[i].__name__, ai[i]))
8          ai_values.append(ai[i])        

play メソッドの修正

下記 は、play メソッドを 修正 したプログラムです。

def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
元と同じなので省略

Marubatsu.play = play
プログラム全体
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
-       if ai[i] is None and None not in ai_values:
-           ai_list.append(("人間", None))
-           ai_values.append(None)
-       elif ai[i] is not None and ai[i] not in ai_values:
-           ai_list.append((ai[i].__name__, ai[i]))
-           ai_values.append(ai[i])       
+       if ai[i] not in ai_values:
            # ラベルを計算する
+           if ai[i] is None:
+               label = "人間"
+           else:
+               label = ai[i].__name__        
            # 項目を登録する
+           ai_list.append((label, ai[i]))
+           ai_values.append(ai[i])
元と同じなので省略

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 の 下図左 のように、Dropdown に 人間のみが表示 されますが、Dropdown の上 で マウスを押す と、下図右 のように 人間 と ai1s が 表示 されることが 確認 できます。

from ai import ai1s

mb.play(ai=[None, ai1s],  gui=True)

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

 

play メソッドの改良その 2(Dropdown の初期選択)

上記 のプログラムでは、人間 VS ai1s の 対戦 を行っていますが、実行結果 からわかるように、両方 の Dropdown に "人間" が 表示される という 問題 があります。これは、前回の記事で説明したように、Dropdown の 作成時 に value、index、label の いずれの属性 も 設定しない 場合は、先頭の項目 が 選択状態になる からです。

そこで、下記 のプログラムのように、項目の値 を表す value 属性に、それぞれ の ai の要素 を 代入 することで、適切な項目 が Dropdown に 選択される ようにします。

  • 8、15 行目:Dropdown の キーワード引数 value に それぞれの手番 の ai の要素 を 記述 することで、Dropdown の 作成時 に value 属性に 値を代入 する
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # 〇 と × の Dropdown を作成する   
 3      dropdown_circle = widgets.Dropdown(
 4          options=ai_list,
 5          description="〇",
 6          layout=widgets.Layout(width="100px"),
 7          style={"description_width": "20px"},
 8          value=ai[0],
 9      )
10      dropdown_cross = widgets.Dropdown(
11          options=ai_list,
12          description="×",
13          layout=widgets.Layout(width="100px"),
14          style={"description_width": "20px"},
15          value=ai[1],
16      )          
元と同じなので省略
17
18  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルを計算する
            if ai[i] is None:
                label = "人間"
            else:
                label = ai[i].__name__        
            # 項目を登録する
            ai_list.append((label, ai[i]))
            ai_values.append(ai[i])
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=ai[1],
    )          
元と同じなので省略

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 の 下図左 のように、× の Dropdown には ai1s が 正しく表示 されますが、〇 の Dropdown には 空白 が 表示 されてしまいます。ただし、〇 の Dropdown の上 で マウスを押す と、下図右 のように 人間 と ai1s が 正しく表示 されることが 確認 できます。

mb.play(ai=[None, ai1s],  gui=True)

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

 

Dropdown の value 属性の None の意味

このようなことが起きる 原因 は、value 属性に None が 代入 された場合は、どの項目 も 選択されていない という 意味を表す ように Dropdown が 設計されている からです。

そのため、上記 のプログラムで 人間 が 手番を担当 する場合の 項目の値 を None に 設定 した場合のように、Dropdown の 項目の値 に None が設定 されている場合では、value 属性に None を代入 すると、None が 設定された項目 が 選択状態 になるの ではなく、先程の実行結果のように、どの項目 も 選択されない という 状態 に なります。

Dropdown では、項目の値 に None を 設定するべきではない。

問題の解決方法

この問題 を 修正 する 方法 の 一つ に、play メソッドの 仮引数 ai の要素が 人間の手番 を 表す値 を None 以外 のデータに 変更する という方法がありますが、その方法 で play メソッドを 修正 すると、これまで に play メソッドを 利用するプログラム が 動作しなくなる という、互換性 の 問題が発生 します。

play メソッドの 互換性を保つ 別の 解決方法 として、人間を表す Dropdown の 項目の値 を None 以外の値 に 変更 するという 方法 があります。None 以外 の 値 であれば 何でも構わない ので、本記事では "人間" という 文字列 で 表現 することにします。

他の候補 として、AI ではない という 意図 の False や、何もない という 意図 の 0 や "" などが挙げられるでしょう。変更したい人は自由に変更して下さい。

play メソッドの修正

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

  • 9、12 行目:value に それぞれ の場合の 適切 な 項目の値 を 代入 する
  • 14、15 行目:value を使って ai_list と ai_values の 要素 を 追加 する
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # ai に代入されている内容を ai_list に追加する
 3      for i in range(2):
 4          # ai[i] が ai_values に登録済かどうかを判定する
 5          if ai[i] not in ai_values:
 6              # ラベルと項目の値を計算する
 7              if ai[i] is None:
 8                  label = "人間"
 9                  value = "人間"
10              else:
11                  label = ai[i].__name__        
12                  value = ai[i]
13              # 項目を登録する
14              ai_list.append((label, value))
15              ai_values.append(value)
元と同じなので省略
16
17  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=ai[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
+               value = "人間"
            else:
                label = ai[i].__name__        
+               value = ai[i]
            # 項目を登録する
-           ai_list.append((label, ai[i]))
+           ai_list.append((label, value))
-           ai_values.append(ai[i])
+           ai_values.append(value)
元と同じなので省略

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、残念ながら 実行結果 の 下図左 のように、〇 の Dropdown には相変わらず 空白が表示 されてしまいます。このようなことが起きる原因について少し考えてみて下さい。

mb.play(ai=[None, ai1s],  gui=True)

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

 

問題の原因と修正

上記 の 問題 は、人間 を表す 項目の値 を 変更 したにも 関わらず、Dropdown を 作成する際 の、キーワード引数 value の 値 をそれに合わせて 変更していない からです。

ただし、現状のプログラム では、ai_values に 登録 した 項目の値 を 記録していない ので、Dropdown の 作成時 に その値 を 利用 することが できません。そこで、下記 のプログラムのように、それぞれ の 手番 の Dropdown の 項目の値 を select_values という list に記録 することで、Dropdown の 作成時 に 適切な項目 が 選択される ように 修正 します。

  • 3 行目:それぞれ の 手番の担当 を表す Dropdown の 項目の値 を 記録 する select_values の値を 空の list で 初期化 する
  • 18 行目:それぞれ の 手番の担当 を表す 項目の値 を select_values に 追加 する
  • 26、33 行目:select_values を使って、Dropdown の value 属性の 値を設定 する
 1  def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
 2      # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
 3      select_values = []
 4      # ai に代入されている内容を ai_list に追加する
 5      for i in range(2):
 6          # ai[i] が ai_values に登録済かどうかを判定する
 7          if ai[i] not in ai_values:
 8              # ラベルと項目の値を計算する
 9              if ai[i] is None:
10                  label = "人間"
11                  value = "人間"
12              else:
13                  label = ai[i].__name__        
14                  value = ai[i]
15              # 項目を登録する
16              ai_list.append((label, value))
17              ai_values.append(value)
18              select_values.append(value)
19    
20      # 〇 と × の Dropdown を作成する   
21      dropdown_circle = widgets.Dropdown(
22          options=ai_list,
23          description="〇",
24          layout=widgets.Layout(width="100px"),
25          style={"description_width": "20px"},
26          value=select_values[0],
27      )
28      dropdown_cross = widgets.Dropdown(
29          options=ai_list,
30          description="×",
31          layout=widgets.Layout(width="100px"),
32          style={"description_width": "20px"},
33          value=select_values[1],
34      )       
元と同じなので省略    
35
36  Marubatsu.play = play
行番号のないプログラム
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
    # seed が None でない場合は、seed を乱数の種として設定する
    if seed is not None:
        random.seed(seed)
    
    # ai_list から、項目だけを取り出した list を作成する
    ai_values = [value for label, value in ai_list]
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
    select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
            select_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
        value=select_values[1],
    )       
    
    # リセットボタンを作成する
    button = widgets.Button(
        description="リセット",
        layout=widgets.Layout(width="100px"),
    )
    
    # 〇 と × の dropdown と リセットボタンを横に配置した HBox を作成し、表示する
    hbox = widgets.HBox([dropdown_circle, dropdown_cross, button])
    display(hbox)
    
    # リセットボタンのイベントハンドラを定義する
    def on_button_clicked(b):
        self.restart()
        self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)           
        
    # イベントハンドラをリセットボタンに結びつける
    button.on_click(on_button_clicked)

    # gui が True の場合に、ゲーム盤を描画する画像を作成し、イベントハンドラに結びつける
    if gui:
        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)

                # 次の手番の処理を行うメソッドを呼び出す
                self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)
                
        # fig の画像にマウスを押した際のイベントハンドラを結び付ける
        fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

    self.restart()
    return self.play_loop(ai=ai, ax=ax, params=params, verbose=verbose, gui=gui)

Marubatsu.play = play
修正箇所
def play(self, ai, ai_list=[], params=[{}, {}], verbose=True, seed=None, gui=False, size=3):      
元と同じなので省略
    # それぞれの手番の担当を表す Dropdown の項目の値を記録する list を初期化する
+   select_values = []
    # ai に代入されている内容を ai_list に追加する
    for i in range(2):
        # ai[i] が ai_values に登録済かどうかを判定する
        if ai[i] not in ai_values:
            # ラベルと項目の値を計算する
            if ai[i] is None:
                label = "人間"
                value = "人間"
            else:
                label = ai[i].__name__        
                value = ai[i]
            # 項目を登録する
            ai_list.append((label, value))
            ai_values.append(value)
+           select_values.append(value)
    
    # 〇 と × の Dropdown を作成する   
    dropdown_circle = widgets.Dropdown(
        options=ai_list,
        description="〇",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=select_values[0],
    )
    dropdown_cross = widgets.Dropdown(
        options=ai_list,
        description="×",
        layout=widgets.Layout(width="100px"),
        style={"description_width": "20px"},
+       value=select_values[1],
    )       
元と同じなので省略    

Marubatsu.play = play

上記 の 修正後 に、下記 のプログラムで play メソッドを 実行 すると、実行結果 のように、それぞれ の Dropdown が 正しい項目 を 選択 することが 確認 できます。

mb.play(ai=[None, ai1s], gui=True)

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

 

実は、上記 の play メソッドには 重大なバグ が いくつかあります。余裕がある方は、play メソッドで 様々な対戦 を行ってみて、どのような バグ があるかを 探してみて下さい。

今回の記事のまとめ

今回の記事では、循環インポート の エラーの原因 と 修正方法 について 説明 しました。

また、play メソッドの 仮引数 ai に 代入 された値に 応じて、Dropdown の 項目 の 自動登録 と、Dropdown の 選択項目 を 設定 する方法について紹介しました。

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

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

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

次回の記事

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?