質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.51%

  • Python

    11744questions

    Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

  • Python 3.x

    9832questions

    Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

  • Tkinter

    272questions

    Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

  • 非同期処理

    134questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。

Pythonで実装しているリバーシプログラムの非同期処理を実現したい

解決済

回答 2

投稿

  • 評価
  • クリップ 0
  • VIEW 1,135
退会済みユーザー

退会済みユーザー

 前提・実現したいこと

Python3.6.4でリバーシのプログラムを作成しています。GUI部分はtkinterで実装し、マウスの左クリックで石を打てるようにしています。
後々重い処理を行うAIの作成を考えているため、AIの思考部分(?)を非同期で処理を行えるような設計にしておきたいのですが、
コードの書き方が分からず困っております。

 該当のソースコード

#!/usr/bin/python3
# -*- coding: utf8 -*-
import tkinter as tk
import sys
import random


class PlayerObject(object):

    myTurn = None
    def __init__(self, myTurn):
        self.myTurn = myTurn

    def placeDisc(self, board, global_turn, b_index,):

        list_canplace = None

        # 自分のターンの時、着手可能場所を取得
        if(self.myTurn == global_turn):
            list_canplace = board.getCanPlace(self.myTurn)

        # 自分のターンじゃないときは何もしない
        if(self.myTurn != global_turn or b_index is None):
            return "None"

        # 着手できる場所が無いときはパス
        if(len(list_canplace) == 0):
            return "Pass"

        # クリックした場所に着手可能なとき
        if(board.discs[b_index].type == "CanPlace"):
            board.resetNewDisc()
            board.reverseDisc(self.myTurn, b_index)
            return "Done"

        # クリックした場所に着手できないとき
        elif(board.discs[b_index].type != "CanPlace"):
            return "None"


class Player(PlayerObject):

    def __init__(self, myTurn):
        super().__init__(myTurn)


class AI(PlayerObject):

    def __init__(self, myTurn):
        super().__init__(myTurn)

    def placeDisc(self, board, global_turn, b_index,):
        # 自分のターンじゃないときは何もしない
        if(self.myTurn != global_turn):
            return "None"

        list_canplace = board.getCanPlace(self.myTurn)
        # 着手できる場所が無いときはパス
        if(len(list_canplace) == 0):
            return "Pass"

        # 着手可能な場所からランダムに選択して打つ
        random.shuffle(list_canplace) # 着手可能場所のリストをランダムにシャッフル
        board.resetNewDisc()
        board.reverseDisc(self.myTurn, list_canplace[0])
        return "Done"


class GameManager(object):

    root = None
    frame = None
    canvas = None
    Board = None
    Player1 = None
    Player2 = None
    Turn = None
    clicked_board_index = None
    pass_count = None

    def __init__(self, root, Board, Player1, Player2):
        self.root = root
        self.frame = tk.Frame(width=960, height=720)
        self.frame.place(x=0, y=0)
        self.canvas = tk.Canvas(self.frame, width=720, height=720)
        self.canvas.place(x=0, y=0)

        self.Board = Board
        self.Player1 = Player1
        self.Player2 = Player2

        self.Turn = "Black"
        self.pass_count = 0

        # 左クリック時にコールバック関数self.clickを呼び出す
        self.root.bind("<Button-1>", self.click)

    # ボードの着手可能場所を全て"空き"にする
    def clean_board(self, ):
        for index, disc in enumerate(self.Board.discs):
            if(disc.type == "CanPlace"):
                self.Board.discs[index].type = "Space"

    def click(self, mouse):

        # clicked out of the board
        if(mouse.x > 720):
            return

        index = int(mouse.y / 90 + 1) * 10 + int(mouse.x / 90 + 1)
        self.clicked_board_index = index

    def draw(self, ):

        self.canvas.delete("board") # ボードを消す
        self.canvas.delete("disc")  # ボード上の石を消す

        # ボードを描画
        self.canvas.create_rectangle(0, 0, 720, 720, fill='#1E824C', tag="board")

        # ボードのマスを描画
        for i in range(9):
            self.canvas.create_line(
                i * 90, 0, i * 90, 720, width=1.2, fill="Black", tag="board")
            self.canvas.create_line(
                0, i * 90, 720, i * 90, width=1.2, fill="Black", tag="board")

        # ボードの丸印を描画
        self.canvas.create_oval(180 - 4, 180 - 4, 180 + 4, 180 + 4, fill="Black", outline="Black", tag="board")
        self.canvas.create_oval(180 - 4, 540 - 4, 180 + 4, 540 + 4, fill="Black", outline="Black", tag="board")
        self.canvas.create_oval(540 - 4, 180 - 4, 540 + 4, 180 + 4, fill="Black", outline="Black", tag="board")
        self.canvas.create_oval(540 - 4, 540 - 4, 540 + 4, 540 + 4, fill="Black", outline="Black", tag="board")

        # 石を描画
        for index, disc in enumerate(self.Board.discs):
            center_x = 45 + int((index-1) % 10) * 90
            center_y = 45 + int((index-10) / 10) * 90
            if(disc.type == "Black"):
                self.canvas.create_oval(center_x - 43, center_y - 43, center_x + 43, center_y + 43, fill="Black", outline="Black", tag="disc")
            elif(disc.type == "White"):
                self.canvas.create_oval(center_x - 44, center_y - 44, center_x + 44, center_y + 44, fill="White", outline="Black", tag="disc")
            elif(disc.type == "CanPlace"):
                self.canvas.create_oval(center_x - 5, center_y - 5, center_x + 5, center_y + 5, fill="OliveDrab1", outline="OliveDrab1", tag="disc")
            # 最後に打たれた石の場合、マークを付ける
            if(disc.newest_place):
                self.canvas.create_oval(center_x - 8, center_y - 8, center_x + 8, center_y + 8, fill="Red", outline="Red", tag="disc")

        self.canvas.pack()

        # コンソール出力テスト
        '''
        print(len(self.Board.discs))
        for index, disc in enumerate(self.Board.discs):
            str = " " if index % 10 != 9 else '\n'
            if(disc.type == "Ban"):
                print("@" + str, end="")
            elif(disc.type == "Space"):
                print("*" + str, end="")
            elif(disc.type == "Black"):
                print("o" + str, end="")
            elif(disc.type == "White"):
                print("x" + str, end="")
            else:
                print("?" + str, end="")
        '''

    def play(self, ):

        p1_status = self.Player1.placeDisc(self.Board, self.Turn, self.clicked_board_index)
        p2_status = self.Player2.placeDisc(self.Board, self.Turn, self.clicked_board_index)

        if(p1_status == "Pass"):
            self.pass_count += 1
            self.Turn = "Black" if (self.Turn == "White") else "White"
        elif(p1_status == "Done"):
            self.pass_count = 0
            self.Turn = "Black" if (self.Turn == "White") else "White"

        if(p2_status == "Pass"):
            self.pass_count += 1
            self.Turn = "Black" if (self.Turn == "White") else "White"
        elif(p2_status == "Done"):
            self.pass_count = 0
            self.Turn = "Black" if (self.Turn == "White") else "White"

        self.draw()

        if(self.pass_count < 2):
            self.root.after(10, self.play)


def main():

    Player1 = None # プレーヤー1
    Player2 = None # プレーヤー2

    args = sys.argv # コマンドライン引数
    if(len(args) >= 3):
        Player1 = Player("Black") if args[1] == '-player' else AI("Black") if args[1] == '-ai' else None
        Player2 = Player("White") if args[2] == '-player' else AI("White") if args[2] == '-ai' else None
    elif(len(args) == 2):
        Player1 = AI("Black") if args[1] == '-training' else None
        Player2 = AI("White") if args[1] == '-training' else None
    if(Player1 is None or Player2 is None):
            args = []

    if(len(args) < 2):
        print("error!!")
        print("If you want to play Player vs Player mode, execute below command")
        print(" $python main.py -player -player")
        print("")
        print("If you want to play Player vs AI mode, execute below command")
        print(" $python main.py -player -ai")
        print("")
        print("If you want to play AI vs AI mode, execute below command")
        print(" $python main.py -ai -ai")
        print("")
        print("If you want to play AI training mode, execute below command")
        print(" $python main.py -training")
        print("")
        sys.exit()

    root = tk.Tk()  # rootウィンドウを作成
    root.title("VimRev")  # rootウィンドウのタイトルを変える
    root.geometry("960x720")  # rootウィンドウの大きさを960x720に
    root.resizable(0, 0)  # 縦、横共に画面サイズの変更を禁止

    board = Board() # ボードクラス
    gameManager = GameManager(root, board, Player1, Player2) # ゲームマネージャークラス
    gameManager.play()

    root.mainloop()  # メインループ


if __name__ == '__main__':
    main()

 試したこと

キューやパイプを用いてプロセス間でデータのやり取りを行う方法など調べ、見様見真似で実装してみたのですが、スレッドやプロセスなどについて詳しくないため実装が困難でした。

現状ですと、例えばAIクラス中のplaceDisc関数の最初の方で10000回ループを回すと処理が奪われてしまい、その間マウスのクリックやウィンドウ移動などが出来なくなってしまいます。

やりたいこととしてはAI部分は別プロセス(?)で処理させておき、処理が終わったタイミングで結果を反映するといったことです。
AIの処理が行われている間も、マウスのクリックや他のイベント処理は呼び出せるようにしたいです。

もし宜しければ、Pythonでこのような処理を実現するために、どういった内容(プロセスやスレッド、通信?)を勉強すればいいかということも併せて教えていただければ幸いです。

プログラム全体の設計などにつきましても、今より良い方法やアドバイス等がありましたらご教示頂けると有難いです。

以上、宜しくお願い致します。

 補足情報(FW/ツールのバージョンなど)

プログラムはPython3.6.4で実装しております。
このソースコードを公開している私のGithubのページ

Player vs Playerを行うには以下のコマンドを実行してください。

$Python source.py -player -player


Player vs AIでは以下のコマンドを実行してください。
(-player, -aiは先に書いた方が先手となります。)

$Python source.py -player -ai
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • tachikoma

    2018/03/20 18:10

    非同期だと、aiの計算が終わるのを待たずにゲームを進める、、、ということですか?

    キャンセル

  • 退会済みユーザー

    退会済みユーザー

    2018/03/20 18:20

    連絡有難う御座います。今の実装のままだと、AIが計算を行っている間はマウスのクリックやウィンドウの移動などのイベント処理ができず、固まってしまいます。そのためゲーム自体を進めるのでは無く、AIの計算は非同期で行わせておき、その間もマウス入力などを受け付けるようにしたいです。

    キャンセル

  • mkgrei

    2018/03/20 18:43

    処理しているからといって、tkinterはマウスをキャプチャするのですか?

    キャンセル

  • 退会済みユーザー

    退会済みユーザー

    2018/03/20 18:50

    連絡有難う御座います。self.root.bind("<Button-1>", self.click)という一行をGameManagerクラスのコンストラクタに書いているのですが、ウィンドウ内を左クリックした時にそのx, y座標がself.click関数に渡されます。今後、それらをAIの処理中も使用したいと考えております。また、現状のままですと、ウィンドウを移動する時なども固まってしまうため非同期の処理を行いたいと考えております。

    キャンセル

回答 2

checkベストアンサー

+2

確かめてみました。
tkinterのウィンドーに関わることはレインボーが回るんですね。

非同期処理ということですが、multiprocessingやthreadingを使うと、クラスをシリアライズするところで苦労します。


一番簡単なのは別プロセスで走らせることです。

ソケットか何かを通して、盤面データと打ち手をやり取りします。

この時にゲームAIをプロセスとして維持するセッション系、にするか
番が回ってくるたびにプロセスを走らせるステートレス系、にするか決める必要があります。

後者のほうが実装が簡単で、前者は相手が考えている間評価し続けることができるので強くなります。

socketやsubprocessを検討するとよいと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/03/20 19:13

    回答有難うございます。調べたところ、難しそうですがソケット通信を実装すれば私のやりたいことが実現できそうです。
    「python セッション」や「python ステートレス」で検索してもあまりそれらしい記事がヒットしなかったのですが、subprocessでコマンドを実行するのはステートレス系という認識で合っているでしょうか?

    キャンセル

  • 2018/03/20 19:27

    セッションやステートレスはWebサイトを作る時によく出てくる概念です。
    ここで言っている意味としては、
    セッションの場合ずっと手を探索していて、自分の番になるたびにそれまでの探索でわかった情報を踏まえつつ最善手を打ちます。
    ステートレスの場合、自分の番だよと知らされて初めて盤面を確認して最善と思われる手を打ちます。

    ---

    subprocessの使い方次第でどちらにもできます。

    先程コードを読んでみましたが、
    見通しを良くするために、

    画面⇔ドライバ⇔プレーヤー

    に分解する必要があるように思います。

    ---

    どのような方法であれThreadingを使うことから逃れられないのですね…
    https://medium.com/swlh/lets-write-a-chat-app-in-python-f6783a9ac170

    ソケットにものが来ていないか逐一確認しながらもブロックしない画面プロセス
    全体のルールの整合性を管理しながら、画面を更新する情報を送ったり、プレーヤーからの指示を聞き入れたりするドライバプロセス
    ゲームを遊ぶだけのプレーヤープロセス

    を別々に実装する必要があるように思います。

    キャンセル

  • 2018/03/20 21:13

    ご説明有難うございます。だんだんと実装のイメージが湧いてきました。
    今後のことを考えると、セッションを頑張って実装した方がより強いAIを作成できそうですね。

    私も設計方法に少し違和感を感じながらコーディングを行っていました。
    一度、mkgrei様のような設計方法で、ソケットを使って一から作り直してみたいと思います。
    ご回答、丁寧な説明有難うございました。

    キャンセル

+2

非同期で処理を行いたいとのことなので、
asyncioか 17.4. concurrent.futures – 並列タスク実行が使えるかと。

Webサーバー(tornado/django)を裏で立ち上げて、JSONで処理を投げるという手もあります。


GitHubを見ました。
3点ほど。

  1. コマンドライン引数の処理はargparse
  2. Diseクラスのtypeにenum型
  3. GameManagerクラスのコンソール出力テストはBoardクラスの__str__関数をオーバーライドして、pprintで出力すると楽です。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/03/20 21:02

    Githubを拝見いただきありがとうございます。

    argparseやpprintの存在を知りませんでした。
    スッキリしたコードに書き直してみようと思います。

    webサーバーは私にとってはかなり難易度が高いですが、勉強して頑張ってみようと思います。
    JSONでデータをやり取りできるのはかなり便利ですね。皆様の意見を聞いて検討しようと思います。

    キャンセル

同じタグがついた質問を見る

  • Python

    11744questions

    Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

  • Python 3.x

    9832questions

    Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

  • Tkinter

    272questions

    Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

  • 非同期処理

    134questions

    非同期処理とは一部のコードを別々のスレッドで実行させる手法です。アプリケーションのパフォーマンスを向上させる目的でこの手法を用います。