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?

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

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.py2 行目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.py4 行目import ai as ai_module実行 されると ai.py記述 された プログラムが実行 され、その 処理が完了 してから、marubatsu.py5 行目以降 の プログラムが 実行 されます。

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」のように 表記 した場合、上記の処理 は、 「marubatsuaimarubatsuaimarubatsu → ・・・」のように、インポートの処理循環 するため、無限繰り返される ことになります。これが 循環インポート で、Python では 循環インポート が起きた場合は ImportError発生 させて、プログラム強制的に終了 するという 仕組み になっています。

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

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

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

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

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

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

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

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

一方、下記お互いインポート する test3.pytest4.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)

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

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

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

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

marubatsu モジュール内で ai モジュールを 利用 しているのは、前回の記事ai モジュール内で 定義 された AIDropdown選択できる ように 修正 した 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 ~ ai14s14 種類AI選択できる ように 実装 しましたが、一方で それ以外の AI選択できない という 欠点 があります。

それに対し、先程修正 した play メソッドは、好きな AI のリスト作成 して 実引数に記述 することで、任意の AI選択できる ようになったという 利点 があります。しかし、その一方 で、下記 のプログラムのように ai_list対応 する 実引数記述せずplay メソッドを呼び出すと、Dropdownoptions 属性に 空の 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 行目条件式 に「Noneai_values登録されていない」という 条件を追加 する
  • 12 行目ai_values に、None追加 する

ai_listai_values異なる list なので、12 行目ai_valuesNone追加 する 処理忘れないよう注意 して下さい。

 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_listai_valuesAI のデータ登録 する
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 行目Noneai[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_valuesTrue になる 場合のみ実行 されます。従って、上記 のプログラムは、下記 のように 記述 できます。

  • 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 NoneFalse の場合に 実行 されるので、その場合必ず 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_listai_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作成時valueindexlabelいずれの属性設定しない 場合は、先頭の項目選択状態になる からです。

そこで、下記 のプログラムのように、項目の値 を表す 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_listai_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 を使って、Dropdownvalue 属性の 値を設定 する
 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?