Pyxel 三目並べゲームの作成

Pyxelでプログラミング練習(クラスを使う 第2回)

 Python向けレトロゲームエンジン「Pyxel(ピクセル)」を使ってのゲーム作成を通してプログラミングを学習します。
 オブジェクト指向の基本的な仕組み「クラス」を利用してボードゲームの三目並べ(※)を作成する例を紹介します。

 ○×ゲーム,Tic-Tac-Toeなどと呼ばれるプログラミング練習でよく使われる題材です。京都大学が公開しているテキストには,設計方法や棋譜を再生する仕組みやテスト方法など詳細が書かれた「三目並べで学ぶプログラム開発」という章があります。

 
 【第1回の記事】
 Pyxel クラスを使って複数のキャラクターを表示する - 勉強ボックス管理者ブログ

 

ゲームの内容を考える

 三目並べゲームで思いつく要素を箇条書きにしてみましょう。
 
 盤面は2次元配列で作成して,空きマスを0,先手番のコマが置かれたマスを1,後手番のコマが置かれたマスを2とすれば良さそうです。
 登場するものが,盤面とコマなのでクラスを二種類(Board と Sprite )作ることにします。
 

三目並べ(Tic-Tac-Toe)のコード例

 ※リソースファイルはPyxelのサンプルを使用します。以下の記事を参照してください。 
 Pyxel サンプルリソースを使う - 勉強ボックス管理者ブログ

 ※ソースファイルの内容(list09_01 ~ list09_04)
 Pyxel_クラス - Google ドライブ

 

01 キャラクターを表示するクラス(Sprite)

 前回の記事で,任意の場所にキャラクターを表示するスプライトクラスを作成したのでそのまま使います。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list09_01.py抜粋

class Sprite:
    """ キャラクターを表示するクラス """

    def __init__(self,x,y,idx):
        self.x = x
        self.y = y
        self.u , self.v = character_list[idx]
   
    def draw(self):
        """ 毎フレーム実行する画面描画用メソッド """
        pyxel.blt(self.x,self.y, 0, self.u,self.v, 8,8, 0)
        return

help(Sprite) 	# 確認用コード

実行結果(IDLEのシェル画面)

Help on class Sprite in module __main__:
class Sprite(builtins.object)
| Sprite(x, y, idx)
|
| キャラクターを表示するクラス
|
(省略)

・「"""」三重引用符で囲んだ docstring
 class文の次の行や,draw()メソッドのdef文の次の行にダブルクォーテーション3つで囲まれた文字列だけの行を書きました。
 クラスやメソッドの説明を記述しておく docstring 呼ばれる使い方で,ルールに従って書いておくとhelp()関数で内容を表示したり統合開発環境でヒントに表示されたりします。
 「Python docstring」で検索すると書き方が調べられます。(クラス説明とメソッドの間は1行開けよう等)

 

02 盤面を管理するクラス(Board)

 盤面を管理するクラスとしてBoardクラスを新しく作ります。3×3のマスの状態を二次元配列(リストのリスト)で管理して画面にも表示します。Spriteクラスのインスタンスを保持するリスト(配列)と手番を管理する変数も持たせます。
【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list09_02.py抜粋

class Board:
    """ 盤面を管理するクラス """

    def __init__(self):
        self.grids = [[0,0,0],
                      [0,0,0],
                      [0,0,0]] # 状態 0:空き 1:先手 2:後手
        self.markers = []   # コマのインスタンスを格納する配列
        self.turn = 0       # 手番 0:先手 1:後手

    def update(self):
        """ 毎フレーム実行する状態更新メソッド """
        return
    
    def draw(self):
        """ 毎フレーム実行する画面描画用メソッド """
        # 枠線を描く
        for i in range(4):
            pyxel.line(0,9*i, 27,9*i, 1)
            pyxel.line(9*i,0, 9*i,27, 1)
        return

・・・

# ここからメインの処理
b = Board()
def update():
    b.update()
    return

def draw():
    pyxel.cls(0)
    b.draw()
    return

実行結果
 

・枠線を引くときの座標が0,0ですが,list09_02.pyの初めの方の pyxel.camera(-2,-2) 命令により表示位置をずらしています。

 

03 マウスクリック時の処理の追加

 「空きマスをクリックしたら手番のコマを表示する」ためには,以下のように処理できればよいでしょう。
 マウスクリック検出
  ↓
 マウスのxy座標が,盤面の範囲内か調べる
  ↓
 xy座標のマスが空いているか二次元配列を調べる
  ↓
 空きマスならば,Spriteのインスタンスをマスの位置に作成

 ※本コード例のような二次元配列の参照時は,縦 y 方向と横 x 方向の指定に注意が必要です。
 

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list09_03.py抜粋

    def check_grids(self,x,y):
        """ クリック位置の判定処理 """
        ret = False
        xi,yi = points_to_idx(x,y)
        if 0 <= xi and xi < 3 and 0 <= yi and yi < 3:
            if 0 == self.grids[yi][xi]:
                # 空きマスの場合は盤面の状態を更新する
                ret = True
                self.grids[yi][xi] = self.turn + 1 # 先手を1,後手を2
        return ret,xi,yi

    def update(self):
        """ 毎フレーム実行する状態更新メソッド """
        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT):
            ret,xi,yi = self.check_grids(pyxel.mouse_x-MARGIN,pyxel.mouse_y-MARGIN)
            if ret :
                x,y = idx_to_points(xi,yi)
                self.markers.append(Sprite(x,y,self.turn))

                # 手番交代(0と1の繰り返し)
                self.turn = 1 - self.turn

実行結果
 

・メソッドの呼び出し
 update()の中で同じクラス内のメソッド check_grids() を呼び出すには「self.check_grids(・・・)」とselfを付けて記述します。外部で定義された関数 idx_to_points(xi,yi) はそのままの記述で呼び出せます。

・手番とマスの状態
 「手番 0:先手 1:後手」「盤面の状態 0:空き 1:先手 2:後手」としたので,二次元配列に値を入れるときには手番の値に+1しました。手番を1,2の繰り返しにすれば,盤面の状態の値と一致させることもできますので,好みで作成しましょう。

・範囲判定
 「 if 0 <= xi and xi < 3 and 0 <= yi and yi < 3:」は,Pythonでは「if 0 <= xi < 3 and 0 <= yi < 3:」のように短く記述することができます。「Python 比較演算子 連結」等で調べてみてください。

 

04 結果判定処理の追加

 コマを置いたあとの処理分岐を作ります。
 「3つ並んでいれば勝ち」を判定するメソッド追加(並んだコマの上にエフェクト表示)
 「空きマスがあるか」を判定するメソッド追加(空きマスなしなら引き分け表示)
 3つ並んでいなくて空きマスがあれば,手番の交代

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)
list09_04.py抜粋

    def update(self):
        """ 毎フレーム実行する状態更新メソッド """
        if pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT) and 0 == self.status:
            ret,xi,yi = self.check_grids(pyxel.mouse_x-MARGIN,pyxel.mouse_y-MARGIN)
            if ret :
                x,y = idx_to_points(xi,yi)
                self.markers.append(Sprite(x,y,self.turn))
                if self.is_win(xi,yi) :
                    # 手番プレーヤーの勝利
                    self.status = self.turn + 1
                elif self.is_full():
                    # 引き分け
                    self.status = 3
                else:
                    # 手番交代(0と1の繰り返し)
                    self.turn = 1 - self.turn
        elif pyxel.btnp(pyxel.MOUSE_BUTTON_LEFT) and 0 != self.status:
            # 最初に戻す
            self.markers.clear()
            self.effect.clear()
            self.__init__()

・self.status
 Boardクラスの変数にゲームの状態を管理する変数を追加して,繰り返しゲームができるようにしました。
 draw()メソッドでもこの変数を参照して,表示を切り替えています。

・最初に戻す
 self.markers.clear()とself.effect.clear()はおそらく不要な処理です。リストにappend()で要素を追加しているので,要素の削除を明示的に記述しておこうかなという意図で入れてあります。(プログラミング言語によってはメモリの解放を手動でやる必要があるものもあると頭の片隅に入れておいてください)

 
・二次元配列全体を調べる二重ループ

    def is_full(self):
        """ 全て埋まっているか判定 空き無しの場合True """
        ret = True
        for row in self.grids:
            if 0 in row:
                ret = False
                break
        return ret

 コード例中の「for row in self.grids:」はリストの要素を先頭から取得するループ処理です。
 if文の条件式の「0 in row」は,指定した値がリストの要素にあるかどうか調べるin演算子です。

 一般的なプログラミングの知識としては,二次元配列は次のような二重ループで全体を調べることを知っておいた方が良いでしょう。(他言語ではfor文の記述が異なっていたり,インデックスの変数は x,y ではなく i,j が使われることもあります)

grids = [[1,2,1],
         [2,1,0],
         [0,1,2]]
for y in range( len(grids) ):
    for x in range( len(grids[y]) ):
        if 0 == grids[y][x]:
            print("0!")

Webブラウザでのデモ

 Pyxel demo Tic-Tac-Toe


 三目並べゲームの作成は以上です。
 盤面を大きく6×6マスなどに広げて先に三つ並んだ方が”負け”というルールにするなど,別のゲームに改造することもできると思います。
 クリック位置の座標をマス目に変換する仕組みで,神経衰弱や15パズルなどのゲームも考えられそうです。

 クラスを使う次の記事kinutani.hateblo.jp

関連記事

 kinutani.hateblo.jp