【Python】2人プレイ用じゃんけんアプリの作り方

2人プレイ用じゃんけんアプリの作り方の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは Python での2人プレイ用じゃんけんアプリの作り方について解説していきます!

各プレイヤーが異なる PC でプレイできるように、アプリには通信機能を搭載します。この通信にはソケット通信を利用します。さらに、GUI は Tkinter で開発していきます。

今回開発するアプリの概要図

1人プレイ用のじゃんけんアプリの作り方に関しては、以前に公開した下記ページでを解説しています。これを拡張して2人プレイ可能なじゃんけんアプリに仕立てていきます。

ソケット通信を利用した一人プレイ用じゃんけんアプリの開発方法解説ページアイキャッチ 【Python/Tkinter】ソケット通信を利用してじゃんけんアプリを開発(一人プレイ編)

開発する「じゃんけんアプリ」

まず、今回どんな「じゃんけんアプリ」を開発していくのか?という点を整理しておきましょう。

ここでは、下記ページで開発した1人プレイ用のじゃんけんアプリと比較する形で整理を行っていきたいと思います。

ソケット通信を利用した一人プレイ用じゃんけんアプリの開発方法解説ページアイキャッチ 【Python/Tkinter】ソケット通信を利用してじゃんけんアプリを開発(一人プレイ編)

1人プレイ用じゃんけんアプリのおさらい

まずは、上記ページで作り方を解説した1人プレイ用のじゃんけんアプリについて簡単に説明しておきたいと思います。

1人プレイ用じゃんけんアプリの全体構成

この1人プレイ用のじゃんけんアプリでは、通信のモデルとしてクライアントサーバーモデルを適用し、ユーザーが操作するクライアントを Tkinter を利用して GUI として開発しました。そして、このクライアントがサーバーと通信を行うことで「じゃんけん」というゲームが成立するようにしていました。

一人プレイ用のじゃんけんアプリの構成を示す図

1人プレイ用じゃんけんアプリの処理フロー

もう少し詳しく1人プレイ用のじゃんけんの処理の流れを説明しておきます。具体的に通信時に利用するソケット通信のメソッド名も記載していきますので、これらのメソッドの詳細を知りたい方は下記ページをご参照いただければと思います。

Pythonでのソケット通信(ポート番号・プロトコル・サーバー / クライアント)

まず、クライアントは起動時に、ソケット通信の connect メソッドを実行してサーバーに接続要求を送信し、サーバーは accept メソッドを実行してクライアントとの接続を確立します。

クライアントとサーバーの間で接続を確立する流れを説明する図

その後、クライアントはグー選択用のボタン、チョキ選択用のボタン、パー選択用のボタンを表示し、ユーザーからのじゃんけんの手の選択を受け付けます。そして、ユーザーがいずれかのボタンをクリックした際に、ユーザーが選択した手を示すデータを sendall メソッドでサーバーに送信します。そのデータを recv メソッドで受信したサーバーは、ユーザーが選択した手とサーバーがランダムに選択した手に基づいてじゃんけんの結果を判定します。

サーバーでじゃんけんの結果を判定する流れを示す図

そして、サーバーが選択した手とじゃんけんの結果を示すデータをクライアントに sendall メソッドで送信し、それを recv メソッドで受信したクライアントが、それらの情報を GUI 上に表示します。

クライアントのGUIにじゃんけんの結果が表示される流れを示す図

このような動作を、じゃんけんの結果が引き分け以外(あいこ以外)になるまで繰り返すことで、1人プレイ用のじゃんけんアプリを実現していました。

サーバーをユーザー操作可能にすれば良いだけ?

ここまでの説明を聞いて、2人プレイ用じゃんけんアプリは「1人プレイ用じゃんけんアプリを、サーバー側もユーザーが操作できるようにするだけで完成するのでは…?」と考えた方もおられるのではないかと思います。

2人プレイ用じゃんけんアプリの構成案

確かに、サーバーが自動的にじゃんけんの手を選択するようになっているので1人プレイ用のじゃんけんアプリになっていましたが、サーバー側でもユーザーがじゃんけんの手を選択できるようになれば2人プレイのじゃんけんアプリになりますね。

なんですが、これって実はアプリの作りとしてはイマイチです。その理由が分かるでしょうか?

その理由は、ユーザーがクライアントとサーバーを意識してアプリを使い分ける必要があるからになります。

たとえば、開発したじゃんけんアプリを PC のアプリとして配布するような場合、クライアント用のアプリとサーバー用のアプリを配布し、サーバーが起動されているのであればクライアント側のアプリを、サーバーが起動されていないのであればサーバー側のアプリを起動するような使い分けがユーザーに求められることになります。そして、起動するアプリを間違うと通信が成立せずにゲームはプレイをすることはできません。こんなアプリはかなり使いにくいですよね…。

サーバーも操作可能なアプリにするとユーザーが状況に応じて起動するアプリ(サーバーorクライアント)を選択する必要があることを示す図

なので、今回は上記のような構成ではなく、下図のような、ユーザーが必ずクライアントを利用する構成のアプリを開発していきたいと思います。そして、サーバーも別途用意し、各ユーザーが操作するクライアント同士はサーバーを介して通信を行うことで「2人プレイ用じゃんけん」を実現していきたいと思います。

今回開発するじゃんけんアプリにおけるサーバーとクライアントの構成を示す図

先ほど示した、ユーザーがサーバーを扱い、各アプリが直接通信を行う構成の方がシンプルに見えるかもしれませんが、使い勝手を考えると上図のようなクライアント同士がサーバーを介して通信を行う構成の方が良いです。

スポンサーリンク

2人プレイ用じゃんけんアプリ

ということで、今回は下図のような構成で通信を行うことで2人プレイ用じゃんけんアプリを開発していきます。

今回開発するじゃんけんアプリにおけるサーバーとクライアントの構成を示す図

この構成でじゃんけんアプリを開発する上でポイントになる点を説明していきます。

2人プレイ用じゃんけんアプリのクライアント

まずは、クライアント側について説明していきます。

クライアント側は1人プレイ用のじゃんけんアプリがそこまで大きく変更する必要はありません。

まず、クライアントの通信相手がサーバーであることは1人プレイ用でも2人プレイ用でも同様になります。また、じゃんけんプレイ時のサーバーとのデータの送受信部分も変更不要です。

MEMO

後述で解説しますが、じゃんけんをプレイする前段階の通信に関しては変更が必要となります

具体的には、サーバーに対してユーザーが選択した手を示すデータを送信し、さらにサーバーから、”じゃんけんの手” とじゃんけんの結果を示すデータを受信する、そして受信したデータの情報を GUI に表示するという通信や処理の流れは変更不要です。

じゃんけんプレイ時のクライアント・サーバー間のデータの送受信の流れの説明図

サーバーとやり取りする通信データに関しても1人プレイ用じゃんけんアプリと同様になりますが、サーバーから受信する “じゃんけんの手” は、サーバーがランダムに選んだ手ではなく “対戦相手のユーザーが選んだ手” になります。ですが、クライアントからすればサーバーが選ぼうがユーザーが選ぼうが受信するのは同じ “じゃんけんの手” を示すデータであり、データの形式等は変わらないので、結局はサーバーとやり取りする通信データも変更不要になります。

ここまで説明してきたように、じゃんけんプレイ時の通信に関しては変更不要なのですが、じゃんけんプレイ前の通信に関しては変更が必要となります。

具体的には、対戦相手のクライアントの起動を待つ処理が追加で必要となります。今回開発するじゃんけんアプリは “2人プレイ用” であるため、もう1つのクライアントが起動するまで先に起動した方のクライアントでは “じゃんけんのプレイ開始” を待たせる必要があります。ネット対戦ゲームをプレイすると「対戦相手を探しています…」みたいなメッセージが表示されて対戦相手が見つかるのを待たされた経験のある方もおられると思いますが、じゃんけんアプリでも複数人でプレイ可能にする場合は、こういった “待たせるための処理” が必要となります。

対戦相手となるクライアントが起動していない場合は、既に起動しているクライアント側を待たせる必要があることを示す図

そのため、2人プレイ用のクライアントの起動時の処理を次のように作成していきます。

まず、クライアントは、起動した直後に、サーバーに対して接続要求を送信し、さらにボタンやラベルを設置した GUI の表示を行います。ここまでは1人プレイ用のクライアントと同じになります。ただし、起動直後はボタンを無効化し、さらにラベルに '対戦相手を待っています...' と表示するようにします。ボタンが無効化されているので、この状態ではユーザーはゲームプレイ不可となります。

クライアント起動後に表示するGUIウィンドウ

さらに、GUI の表示を行った後に、サーバーに対して “ゲームをプレイしたい” ことを示すデータを送信するようにします。まぁ、ここのデータは何でもよいのですが、ここではてきとうに b'join' というデータを送信するようにしましょう。データを送信した後はサーバーからの受信待ちを行います。

起動してサーバーと接続確立後にb'join'をサーバーへ送信してゲームへの参加を伝える様子

そして、サーバーから応答となるデータを受信したタイミングでボタンを有効化し、ラベルに表示するメッセージも変化させてユーザーがゲームをプレイできるような状態にします。ここではサーバーから受信するデータは b'start' としたいと思います。つまり、このサーバーから送信される b'start' はゲームプレイの準備が整ったことを示すデータであり、後述で説明するサーバーは、2つのクライアントが起動したタイミング(b'join' を2つ受信したタイミング)で b'start' を両方のクライアントに送信するようにする必要があります。

サーバーから準備が整ったことを示すb'start'を受信したらボタンを有効化したユーザーの操作を受け付けるようにする様子

この b'start' を受信してボタンの有効化等を行なったタイミングで “ユーザーがじゃんけんをプレイ可能な状態” になったことになります。そして、ここからは前述で示した1人プレイ用のじゃんけんアプリと同じ通信フローでサーバーと通信を行いながら処理を実施していけばよいだけになります。

2人プレイ用じゃんけんアプリのクライアント

次はサーバーについて説明していきます。

サーバーに関しては、1人プレイ用のじゃんけんアプリから結構変更が必要になります。

まず、1人プレイ用のじゃんけんアプリでは、accept メソッドを実行してクライアントから接続要求を受信して接続を確立した後はすぐにゲームを開始し、クライアントからじゃんけんの手を受信するようになっていました。

1人プレイ用のじゃんけんアプリにおける接続確立後のサーバーの動作を示す図

ですが、2人プレイ用のじゃんけんアプリでは、2つのクライアントを相手にする必要がありますし、前述のとおりクライアントは接続要求送信後に b'join' というデータを送信してくるようになっています。そのため、ここの処理の流れは変更が必要で、具体的には accept を実行して1つのクライアントと接続を確立した後に、そのクライアントからの b'join' の受信を行い、その後に再度 accept を実行して他方のクライアントとの接続の確立を試みるように変更する必要があります。そして、2つ目のクライアントと接続が確立できた時にも、そのクライアントからの b'join' の受信を行う必要があります。

1つのクライアントからb'join'を受信したら、再度acceptを実行して他方のクライアントとの接続確立を試みる必要があることを示す図

で、2つのクライアントに対して接続の確立と b'join' の受信が完了したということは、2つのクライアントが起動してゲームがプレイできる状態になったことを意味しますので、ここで2つのクライアントに対して b'start' を送信します。このような通信フローにすることで、2つのクライアントが起動するまでクライアントのゲーム開始を待たせるという処理を実現することができます。

2つのクライアントからb'join'を受信したのちにb'start'を両方のクライアントに送信する様子

b'start' を送信することでゲーム開始となりますので、この後は、クライアントからのじゃんけんの手を受信待ちを行い、2つのクライアントからじゃんけんの手が送信されてきたら、多方側の手とじゃんけんの結果をそれぞれのクライアントに送信してやれば良いことになります。

で、ここでポイントになるのが、じゃんけんの手の受信待ちを2つのクライアントのソケットに対して同時に実行する必要があるという点になります。どのユーザーがどのタイミングでボタンをクリックするかが分からないため、どのクライアントが先にじゃんけんの手を送信してくるかが分かりません。そのため、どちらのクライアントが先にじゃんけんの手を送信してきてもデータが受信できるように、2つのソケットに対して同時に受信待ちを実行する必要があります。

サーバーが2つのクライアントに対するデータの受信待ちを同時に行う必要があることを説明する図

ただし、1人プレイ用のじゃんけんアプリ開発時に利用した recv メソッドでは1つのソケットに対してのみにしか受信待ちを行うことができず、複数のソケットに対して同時に受信待ちを行うようなことはできません。

こういった、複数のソケットに対して同時に受信待ちを行うような場合に便利なのが、下記ページで解説している select になります。この select を利用することで複数のソケットに対して同時に受信待ちを行うことが可能となり、今回実現したい “複数のクライアントからのじゃんけんの手の受信待ち” も実現可能となります。

【Python/ソケット通信】select関数での複数ソケットでの同時受信待ちの実現

詳細に関しては上記ページを読んでいただければありがたいのですが、select 関数を実行すれば複数のソケットを受信待ち状態にすることができます。そして、1つのソケットでも “受信に対して Ready” になったときに select 関数が終了することになりますので、select 関数が終了したら、その Ready になったソケットを調べ、そのソケットに対して recv メソッドで受信を行えば即座にデータの受信を行うことが可能となります。

なので、select 関数を実行してクライアントと接続を確立した2つのソケットを受信待ち状態にし、さらに select 関数が終了したときに “受信に対して Ready” になったソケットに対して recv を実行してじゃんけんの手を取得する、そして、まだ Ready になっていないソケットが存在するのであれば、そのソケットのみに対して select 関数を再び実行する、という処理の流れを繰り返し実行してやれば、いずれはじゃんけんの手を2つ受信することができます。

2つのクライアントからじゃんけんの手を受信する流れを示す図

サーバーがじゃんけんの手を2つ取得できれば、後は、これらのじゃんけんの手から各クライアントの勝敗を判定し、さらに、対戦相手のクライアントの手と勝敗の結果を結合したデータをそれぞれのクライアントに送信してやれば良いだけです。ここまで説明してきた処理の流れを、じゃんけんの結果が引き分け以外になるまで繰り返すようにしてやれば、2人プレイ用のじゃんけんアプリにおけるサーバーが実現できることになります。

サーバーが2つのじゃんけんの手を受信し、それらの手から勝敗を判定して、その結果をクライアントに送信する様子

クライアント – サーバー間の通信シーケンス

ここまでのまとめとして、クライアントとサーバーの処理・通信の流れをシーケンス図として示しておきます。青部分がクライアント1で実行するメソッド or サーバーがクライアント1に対して実行するメソッド、緑部分がクライアント2で実行するメソッド o サーバーがクライアント2に対して実行するメソッドを示しています。

クライアント・サーバー間の通信のシーケンス図

2人プレイ用のじゃんけんアプリのサンプルスクリプト

ここまで説明してきたじゃんけんアプリのサンプルスクリプトを紹介していきます。

ここまで説明してきたように、今回紹介するアプリはクライアントサーバーモデルとしており、サーバーとクライアントのスクリプトの2つを紹介していきます。

スポンサーリンク

サーバーのサンプルスクリプト

まず紹介するのがサーバーのサンプルスクリプトになります。

スクリプト

サーバーのサンプルスクリプトは下記となります。以降、このスクリプトのファイル名を server.py として解説を進めていきます。

server.py
import socket
import select

def judge(player1_hand, player2_hand):

    # じゃんけんの結果を判断
    if player1_hand == player2_hand:
        return 'even', 'even'
    elif player1_hand == 'stone' and player2_hand == 'scissors':
        return 'win', 'lose'
    elif player1_hand == 'scissors' and player2_hand == 'paper':
        return 'win', 'lose'
    elif player1_hand == 'paper' and player2_hand == 'stone':
        return 'win', 'lose'
    else:
        return 'lose', 'win'
    
def janken(player1_sock, player2_sock):

    # 勝ち負けが決まるまでじゃんけんを継続
    player1_result = 'even'
    player2_result = 'even'
    while player1_result == 'even' and player2_result == 'even':

        # 接続確立済みのソケットのリストを生成
        rlist = [player1_sock, player2_sock]

        # データ未受信のソケットが無くなるまでループ
        while len(rlist) != 0:

            # rlistに含まれるソケットに対して受信待ち
            r_socks, _, _ = select.select(rlist, [], [])


            if player1_sock in r_socks:
                # player1_sockが受信に対してReadyの場合

                # じゃんけんの手を受信
                recv_data = player1_sock.recv(1024)
                player1_hand = recv_data.decode()

                # データ未受信のリストからソケット削除
                rlist.remove(player1_sock)
            
            if player2_sock in r_socks:
                # player2_sockが受信に対してReadyの場合

                # じゃんけんの手を受信
                recv_data = player2_sock.recv(1024)
                player2_hand = recv_data.decode()

                # データ未受信のリストからソケット削除
                rlist.remove(player2_sock)

        # じゃんけんの結果を判断
        player1_result, player2_result = judge(player1_hand, player2_hand)
        
        # 各々のクライアントに送信するデータを用意して送信
        player1_send_data = f'{player2_hand}:{player1_result}'.encode()
        player2_send_data = f'{player1_hand}:{player2_result}'.encode()
        player1_sock.sendall(player1_send_data)
        player2_sock.sendall(player2_send_data)


# ソケットを生成してバインド・リッスン
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('0.0.0.0', 40001))
sock.listen(5)

# 接続確立済みのソケットを管理するリスト
e_socks = []

# 無限ループで常駐させる
while True:

    # 接続要求の受付 / 接続の確立
    e_sock, addr = sock.accept()

    # b'join'を受信
    recv_data = e_sock.recv(1024)

    # 接続確立済みのソケットをリストに追加
    e_socks.append(e_sock)

    # 接続確率済みのソケットが2つになればじゃんけん開始
    if len(e_socks) == 2:
        # 各クライアントにb'start'を送信してゲーム開始を通知
        for e_sock in e_socks:
            e_sock.sendall(b'start')
        
        # じゃんけんを開始
        janken(e_socks[0], e_socks[1])

        # じゃんけんが終了したので接続切断・ソケットクローズ
        for e_sock in e_socks:
            e_sock.close()

        e_socks = []

sock.close()

スクリプトの説明

server.py の1つ目のポイントが、2つのクライアントが準備完了になるまでクライアントを待たせるという点になります。後述の client.py を確認していただければ分かる通り、クライアント側ではゲームがプレイ可能になるのはサーバーから b'start' を受信した後になっています。その一方で、server.py では、この b'start' の送信を “accept メソッドによる接続の確立”と “b'join' の受信” のそれぞれが2回行われないと実行されないようにしており、このような制御を行うことで2つのクライアントが準備完了になるまでクライアントを待たせることができるようになっています。

また、2つのクライアントからのデータの受信待ちを同時に実行する必要があるという点も server.py のポイントになります。これを行なっているのが janken 関数の中で実行している select になります。この select の実行によって引数 rlist のリストに含まれる “クライアントと接続確立済みのソケット” 全てに対して受信待ちを行い、select 終了後に “受信に対して Ready” になったソケットを調べ、そのソケットに対して recv でじゃんけんの手を示すデータの受信を行なっています。そして、一度受信を行なったソケットは rlist から取り除き、再度 select を実行するという処理の流れを rlist が空になるまで繰り返すことで、全クライアントからのじゃんけんの手の受信を実現しています。

あとは、じゃんけんの結果を判定し、相手側のクライアントから送信されてきたじゃんけんの手と結果を結合したデータをクライアントに送信してやれば良いだけです。2つのクライアント両方に対してデータの送信が必要であるという点に注意してください。

クライアントのサンプルスクリプト

続いてクライアント側のサンプルスクリプトを紹介していきます。

スクリプト

クライアントのサンプルスクリプトは下記となります。以降、このスクリプトのファイル名を client.py として解説を進めていきます。

この client.py では connect メソッド実行時に引数に '127.0.0.1' を指定しているので同じ PC 上で動作するサーバーと通信を行うことになります。他の PC 上で動作するサーバーと通信を行うようにしたいのであれば、'127.0.0.1' の部分を、その PC の IP アドレスに変更してください。

client.py
import socket
import tkinter

# 文字のサイズ調整用
font = ('', 40)

class Janken:
    def __init__(self, master, sock):
        '''初期化を行う'''

        self.master = master
        self.create_widgets()
        self.sock = sock

        self.master.after(1000, self.wait)

    def wait(self):
        '''他方のクライアントが起動するまで待ち'''
        
        # サーバーにデータを送信
        self.sock.sendall(b'join')

        # サーバーからのゲーム開始通知 b'start' を待つ
        self.sock.recv(1024)

        # ゲームがプレイ可能になったのでウィジェットの状態を変化
        self.stone_button.config(state=tkinter.NORMAL)
        self.scissors_button.config(state=tkinter.NORMAL)
        self.paper_button.config(state=tkinter.NORMAL)

        self.game_message.config(text='じゃんけんの手を選んでください')
        self.server_hand_message.config(text=f'相手の手:???')


    def create_widgets(self):
        '''ウィジェットの作成と配置を行う'''

        # グー選択用のボタン
        self.stone_button = tkinter.Button(
            self.master,
            text='グー',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_stone
        )
        self.stone_button.grid(row=0, column=0, padx=10, pady=10)

        # チョキ選択用のボタン
        self.scissors_button = tkinter.Button(
            self.master,
            text='チョキ',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_scissors
        )
        self.scissors_button.grid(row=0, column=1, padx=10, pady=10)

        # パー選択用のボタン
        self.paper_button = tkinter.Button(
            self.master,
            text='パー',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_paper
        )
        self.paper_button.grid(row=0, column=2, padx=10, pady=10)

        # 相手の手を表示するためのラベル
        self.server_hand_message = tkinter.Label(
            self.master,
            text='',
            font=font
        )
        self.server_hand_message.grid(row=1, column=0, columnspan=3, padx=10, pady=10)

        # ゲームの説明を表示するためのラベル
        self.game_message = tkinter.Label(
            self.master,
            text='対戦相手を待っています...',
            font=font
        )
        self.game_message.grid(row=2, column=0, columnspan=3, padx=10, pady=10)

    def start(self, hand):
        '''他のユーザーとじゃんけんを行う'''

        # 連打防止のためボタンを無効化
        self.stone_button.config(state=tkinter.DISABLED)
        self.scissors_button.config(state=tkinter.DISABLED)
        self.paper_button.config(state=tkinter.DISABLED)

        # ユーザーが選択した手を示すデータを送信
        send_data = hand.encode()
        self.sock.sendall(send_data)

        # サーバーからサーバーの手とじゃんけんの結果を受信
        recv_data = self.sock.recv(1024)
        server_hand_result = recv_data.decode()

        # サーバーの手とじゃんけんの結果を分離
        server_hand = server_hand_result.split(':')[0]
        result = server_hand_result.split(':')[1]

        # 相手の手をラベルに表示
        server_hand_str = {'stone': 'グー', 'scissors': 'チョキ', 'paper': 'パー'}[server_hand]
        self.server_hand_message.config(text=f'相手の手:{server_hand_str}')

        if result == 'even':
            # 再度じゃんけんの手が選択できるようにボタンを有効化
            self.stone_button.config(state=tkinter.NORMAL)
            self.scissors_button.config(state=tkinter.NORMAL)
            self.paper_button.config(state=tkinter.NORMAL)

            # 再度じゃんけんの手を選ぶ必要があることを表示
            self.game_message.config(text='再度じゃんけんの手を選んでください')

        elif result == 'win':
            # ユーザーの勝ちであることをを表示
            self.game_message.config(text='あなたの勝ちです!')
            
            # ゲームは終了したのでソケットはクローズ
            self.sock.close()

        else:
            # ユーザーの負負けあることを表示
            self.game_message.config(text='あなたの負けです...')

            # ゲームは終了したのでソケットはクローズ
            self.sock.close()


    def choice_stone(self):
        self.start('stone')

    def choice_scissors(self):
        self.start('scissors')

    def choice_paper(self):
        self.start('paper')


# ソケットを生成してサーバーに接続要求を送信
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 40001))


# GUIの表示
master = tkinter.Tk()
app = Janken(master, sock)

# メインループでイベント待ち
master.mainloop()

スクリプトの説明

client.py のポイントは、対戦相手となるクライアントが起動するまでユーザーからの操作を待たせるという点になります。これは、client.py が起動した直後にボタンを無効化し、サーバーから b'start' を受信するまで、すなわち b'join' 送信後に実行する recv メソッドが終了するまでボタンを無効化したままにしておくことで実現可能です。

あとは、b'start' 受信後にボタンを有効化してやれば、1人プレイ用じゃんけんと同じ処理の流れで2人プレイ用じゃんけんのクライアントが出来上がります。

また、Janken クラスの __init__ の最後で after メソッドを実行する必要があるという点にも注意してください。下記の self.master.after(1000, self.wait) を単に self.wait() に変更して直接 wait メソッドを実行するようにしてしまうと、クライアントが2つ起動するまで最初に起動した方のクライアントの GUI ウィンドウが表示されないことになります。

afterメソッドの実行
def __init__(self, master, sock):
    # 略
    self.master.after(1000, self.wait)

これは、直接 wait メソッドを実行すると Tkinter の mainloop が実行される前に wait メソッド内で recv メソッドが実行されることになることが原因となります。Tkinter では mainloop が実行されないとウィンドウがの表示やボタンクリック等の受付が行われません。

この mainloop に関しては下記ページで解説していますし、

tkinterのmainloopの解説ページのアイキャッチ 【Python】tkinterのmainloopについて解説

上記で用いた after に関しても下記ページで解説していますので、この辺りの詳細を知りたい方はこれらのページを参照していただければと思います。

Tkinterの使い方:after で処理を「遅らせて」or 処理を「定期的」に実行する

動作確認

続いて、ここまで説明およびスクリプトの紹介を行ってきた「2人プレイ用じゃんけんアプリ」の動作確認を行っていきます!

まず、server.pyclient.py を同じフォルダ内に保存し、さらにターミナルやコマンドプロンプト・PowerShell 等の CLI アプリを3つ起動し、全ての CLI アプリでスクリプトを保存したフォルダに移動してください。

続いて、1つの CLI アプリで下記を実行してください。これにより、じゃんけんアプリのサーバーが起動することになります

python server.py

次に、2つ目の CLI アプリで下記を実行してください。

python client.py

これにより、クライアントが1つのみ起動したことになり、下の図のような GUI ウィンドウが表示されるはずです。ちなみに、下の図のは Mac で client.py を実行した場合に起動するウィンドウで、WIndows の場合は多少見た目が異なると思います。

クライアントを1つのみ起動したときのクライアントのウィンドウ

ここでポイントになるのが、GUI ウィンドウは表示されるものの、まだゲームがプレイ不可であるという点になります。ボタンが無効化されており、クリックしても反応しないことが確認できるはずです。また、ラベルに表示される文字列からも、他のクライアントの起動を待っている状態であることが確認できると思います。

ということで、次は3つ目の CLI アプリで下記を実行して2つ目のクライアントを起動させましょう!

python client.py

これにより、1つ目のクライアントの GUI ウィンドウの表示が下の図のように変化するはずです。これは、2つ目のクライアントが起動することで、サーバーから b'start' が送信されてきてボタンの有効化やラベルの表示文字列の変更が行われたからになります。また、2つ目のクライアントの GUI ウィンドウも同様のものになっているはずです。

クライアントを2つ起動したときのクライアントのウィンドウ

このように GUI ウィンドウが表示されればゲームプレイ可能状態となっているため、2つの GUI ウィンドウから好きな手を選んでじゃんけんをプレイしてみてください!一方の GUI ウィンドウのボタンをクリックしただけでは画面に変化はありませんが、2つともの GUI ウィンドウからボタンをクリックしてやれば、対戦相手のクライアントで選択された手とじゃんけんの結果が表示されるはずです。

じゃんけんプレイ後のクライアントのウィンドウ

ここでは1人で2人分の操作を行いましたが、各ウィンドウを別のユーザーが操作することで2人でじゃんけんをプレイすることが可能となります。

また、今回はサーバーおよび、2つのクライアントを同じ PC 上で動作させましたが、これらは他の PC 上で動作させることも可能です。その場合は client.pyconnect の引数に指定している 127.0.0.1 の部分を、サーバーを起動している PC の IP アドレスに変更してください。これにより、2つのクライアントが別の PC 上で動作していてもサーバーを介して通信が行われ、別の PC からじゃんけんをプレイすることができるようになります。

異なるPC上で各クライアントやサーバーが動作する様子

以上で動作確認は一旦終了となります。サーバー側を終了させたい場合は、server.py を実行している方の CLI アプリで control + c を入力して強制終了させてください。クライアントに関してはウィンドウの閉じるボタンをクリックして終了させてください。

スポンサーリンク

(参考)アプリの反応を良くする方法

最後に、参考として Tkinter 使用時の注意点について補足をしておきます。

先ほどの動作確認時に、現状の client.py では “アプリが反応しなくなるタイミング” が存在することに気づいたでしょうか?

具体的には、”クライアントが1つのみ起動している”、さらに、”対戦相手のクライアントのボタンのクリックを待っている (一方のクライアントのボタンのみをクリックした状態)” の2つのタイミングでアプリが反応しなくなってしまっています。実際に、これらのタイミングでウィンドウの最大化ボタンや最小化ボタンをクリックしてみてもアプリは反応しないはずです。

下記ページでも解説しているのですが、Tkinter の mainloop が実行されていないタイミングではボタンのクリック等の操作を行なってもアプリが反応してくれません。

Tkinterと通信併用時の注意点の解説ページアイキャッチ 【Tkinterの使い方】”通信処理”実行時の注意点と解決策(アプリが反応しない)

client.py のコードを確認していただければ分かると思いますが、client.py では recv での受信待ちを2箇所で行なっており、これらが実行されるタイミングでは mainloop が実行されていない状態となるためアプリが反応しなくなってしまっています

mainloopが実行されていない間アプリが反応しない様子

この解決方法に関しても上記ページで解説しているので詳細に関しては上記ページを参照していただきたいのですが、結論としてはマルチスレッドを導入して recvmainloop を並行して実行させるようにしてやれば、アプリが反応しない問題は解決可能です。

具体的には、下記のように client.py を変更することでアプリが反応しなくなる問題を解決できます。

マルチスレッド導入後のclient.py
import socket
import tkinter
import threading

# 文字のサイズ調整用
font = ('', 40)

class Janken:
    def __init__(self, master, sock):
        '''初期化を行う'''

        self.master = master
        self.create_widgets()
        self.sock = sock

        # waitメソッドを別スレッドで実行
        thread = threading.Thread(target=self.wait)
        thread.start()

    def wait(self):
        '''他方のクライアントが起動するまで待ち'''
        
        # サーバーにデータを送信
        self.sock.sendall(b'join')

        # サーバーからのゲーム開始通知 b'start' を待つ
        self.sock.recv(1024)

        # ゲームがプレイ可能になったのでウィジェットの状態を変化
        self.stone_button.config(state=tkinter.NORMAL)
        self.scissors_button.config(state=tkinter.NORMAL)
        self.paper_button.config(state=tkinter.NORMAL)

        self.game_message.config(text='じゃんけんの手を選んでください')
        self.server_hand_message.config(text=f'相手の手:???')


    def create_widgets(self):
        '''ウィジェットの作成と配置を行う'''

        # グー選択用のボタン
        self.stone_button = tkinter.Button(
            self.master,
            text='グー',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_stone
        )
        self.stone_button.grid(row=0, column=0, padx=10, pady=10)

        # チョキ選択用のボタン
        self.scissors_button = tkinter.Button(
            self.master,
            text='チョキ',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_scissors
        )
        self.scissors_button.grid(row=0, column=1, padx=10, pady=10)

        # パー選択用のボタン
        self.paper_button = tkinter.Button(
            self.master,
            text='パー',
            width=10,
            font=font,
            state=tkinter.DISABLED,
            command=self.choice_paper
        )
        self.paper_button.grid(row=0, column=2, padx=10, pady=10)

        # 相手の手を表示するためのラベル
        self.server_hand_message = tkinter.Label(
            self.master,
            text='',
            font=font
        )
        self.server_hand_message.grid(row=1, column=0, columnspan=3, padx=10, pady=10)

        # ゲームの説明を表示するためのラベル
        self.game_message = tkinter.Label(
            self.master,
            text='対戦相手を待っています...',
            font=font
        )
        self.game_message.grid(row=2, column=0, columnspan=3, padx=10, pady=10)

    def result(self):
        '''結果の表示を行う'''

        # サーバーからサーバーの手とじゃんけんの結果を受信
        recv_data = self.sock.recv(1024)
        server_hand_result = recv_data.decode()

        # サーバーの手とじゃんけんの結果を分離
        server_hand = server_hand_result.split(':')[0]
        result = server_hand_result.split(':')[1]

        # 相手の手をラベルに表示
        server_hand_str = {'stone': 'グー', 'scissors': 'チョキ', 'paper': 'パー'}[server_hand]
        self.server_hand_message.config(text=f'相手の手:{server_hand_str}')

        if result == 'even':
            # 再度じゃんけんの手が選択できるようにボタンを有効化
            self.stone_button.config(state=tkinter.NORMAL)
            self.scissors_button.config(state=tkinter.NORMAL)
            self.paper_button.config(state=tkinter.NORMAL)

            # 再度じゃんけんの手を選ぶ必要があることを表示
            self.game_message.config(text='再度じゃんけんの手を選んでください')

        elif result == 'win':
            # ユーザーの勝ちであることをを表示
            self.game_message.config(text='あなたの勝ちです!')
            
            # ゲームは終了したのでソケットはクローズ
            self.sock.close()

        else:
            # ユーザーの負負けあることを表示
            self.game_message.config(text='あなたの負けです...')

            # ゲームは終了したのでソケットはクローズ
            self.sock.close()
    
    def start(self, hand):
        '''他のユーザーとじゃんけんを行う'''

        # 連打防止のためボタンを無効化
        self.stone_button.config(state=tkinter.DISABLED)
        self.scissors_button.config(state=tkinter.DISABLED)
        self.paper_button.config(state=tkinter.DISABLED)

        # ユーザーが選択した手を示すデータを送信
        send_data = hand.encode()
        self.sock.sendall(send_data)

        # resultメソッドを別スレッドで実行
        thread = threading.Thread(target=self.result)
        thread.start()


    def choice_stone(self):
        self.start('stone')

    def choice_scissors(self):
        self.start('scissors')

    def choice_paper(self):
        self.start('paper')


# ソケットを生成してサーバーに接続要求を送信
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 40001))


# GUIの表示
master = tkinter.Tk()
app = Janken(master, sock)

# メインループでイベント待ち
master.mainloop()

先ほど紹介したページの解説内容と中身が重複するため、ここでの詳細な解説は省略させていただきますが、まずは Tkinter で開発したアプリは mainloop 実行中でないと反応しないこと、および、マルチスレッド等を利用して mainloop と他の処理を並行・並列に実行させることで問題が解決できることは覚えておいていただければと思います!

まとめ

このページでは、Python での「2人プレイ用じゃんけんアプリ」の作り方について解説しました!

2人プレイ用じゃんけんアプリを開発するポイントは「2つのクライアントが起動するまでクライアントを待たせる」と「2つのクライアントからの受信待ちを行う」の2点になると思います。

前者に関しては、要はクライアントとサーバー間の通信フローを工夫することで実現可能で、例えば下記のように通信フローを組んでやることで実現可能です。

  • クライアントが起動時に特定のデータをサーバーに送信する
    • (クライアントが起動したことをサーバーに伝える)
  • サーバーが2つのクライアントからデータを受信した時に特定のデータをクライアントに送信する
    • (2つのクライアントが起動済みであることをクライアントに伝える)

また、後者に関しては、select 関数を利用することで実現可能となります。

今回は2人プレイ用のじゃんけんアプリの作り方について解説しましたが、これを発展させることで3人プレイ用、4人プレイ用のじゃんけんアプリも開発可能になると思います。ぜひ、これらの開発にも挑戦してみてください!

ちなみに、3人以上のじゃんけんの勝敗判定方法については下記ページで解説していますので、3人以上でプレイ可能なじゃんけんアプリを開発する際に参考にしていただければと思います!

プレイヤーが3人以上の場合のじゃんけんの勝敗判定方法解説ページアイキャッチ 【Python】プレイヤー3人以上のじゃんけんの勝敗判定

また、今回解説した内容を踏まえれば、じゃんけん以外のゲームを複数人でプレイするアプリも開発できると思いますので、是非こちらも挑戦してみていただければと思います!

同じカテゴリのページ一覧を表示