[Python]五目並べを作ってみた

Python

Pythonとパッケージ「PySImpleGUI」を使用して「五目並べ」を作りました。
その制作の記録です。

コンピュータ対戦は実装しておらず、プレイヤーが交互に石を打ち、5つ揃うと勝利判定をする、というものです。

前提条件としてPython3.4以上、PySimpleGUIのインストールが必要です。

完成したときの表示は次のようなものになります。

五目並べの起動
ゲーム終了時

これから機能ごとにまとめていきます。

完成したコードの全文は「まとめ」にあります。

使用した環境
Windows10
Python 3.10.0
PySimpleGUI 4.56.0

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

外観の作成

パッケージのインポートと、アプリの表示部を作っていきます。

import PySimpleGUI as sg


sg.theme('DarkBrown7')

def create_layout():
    layout = [[sg.Push(), sg.Text('白のターン', font=(None, 10), key='-TURN-')]]
    for y in range(15):
        inner = []
        for x in range(15):
            inner.append(sg.Button('', size=(4, 2), font=(None, 10), pad=(0, 0), key=str(y)+','+str(x)))
        layout.append(inner.copy())
    return layout

def main():
    layout = create_layout()
    window = sg.Window('五目並べ', layout)
    while True:
        event, _ = window.read()
        if event == sg.WINDOW_CLOSED:
            break

    window.close()

if __name__ == '__main__':
    main()

レイアウトの1行目には誰の番(白か黒)なのか表示させますが、最初は「白のターン」としておきます。

続いてのfor文ではボタンを一つずつ配置し、その座標をkeyに設定――y軸とx軸をカンマでつなぎました。画面更新するときにこのkeyを使い、yとxは石の判定等に利用します。

これで15×15のマス目が並びました。

最初は石を番線の交点に置くようにしたかったのですが、かなり面倒になりそうだったので、ボタンに変更しました。

石を打つ

クラスを作って必要な変数、メソッドを定義していきます。

# 上部省略
class Gobang():
    def __init__(self):
        self.board = [[0 for _ in range(15)] for _ in range(15)]
        self.player = '○'
        self.player_color = '白'
        self.count = 1
    
    def turn_change(self):
        if self.player == '○':
            self.player = '●'
            self.player_color = '黒'
        else:
            self.player = '○'
            self.player_color = '白'
            
    def set_stone(self, y, x):
        if self.board[y][x] == 0:
            self.board[y][x] = self.player
            return True
        else:
            return False


def main():
    game = Gobang()
    layout = create_layout()
    window = sg.Window('五目並べ', layout)
    while True:
        event, _ = window.read()
        if event == sg.WINDOW_CLOSED:
            break

        y, x = map(int, event.split(','))
        setted = game.set_stone(y, x)
        if setted == False:
            continue
        
        window[event].update(game.player)
        game.turn_change()
        window['-TURN-'].update(f'{game.player_color}のターン')
    window.close()

if __name__ == '__main__':
    main()

Gobangクラスを作成し、プレイヤーを変えるメソッド、石を置くメソッドを作りました。

メインループでボタンがクリックされると、event.split(',')でy軸とx軸の座標を取り出します。
それからGobang内boardの対応する箇所をメソッドで更新し、「○」か「●」に更新します。

更新ができたら、押されたボタンの表示をwindow[event].updateで変更、Gobangのメソッドでプレイヤーを変更、updateで画面右上のプレイヤーターン表示を変更します。

石の判定

石が5つ揃っているかの判定をクラス内に記述します。

# Gobang内に追記
   def _count_color_stone(self, yi, xi, before_stone):
        stone = self.board[yi][xi]
        if stone == 0:
            self.count = 1
        elif stone == before_stone:
            self.count += 1
        else:
            self.count = 1

    def stone_judgement(self, y, x):
        # 横方向の判定
        for xi in range(15):
            if xi == 0:
                before_stone = 0
            else:
                before_stone = self.board[y][xi-1]
            
            self._count_color_stone(y, xi, before_stone)

            if self.count == 5:
                stones_coordinates = [(y, xj) for xj in range(xi, xi-5, -1)]
                return stones_coordinates

        # 縦方向の判定
        self.count = 1
        for yi in range(15):
            if yi == 0:
                before_stone = 0
            else:
                before_stone = self.board[yi-1][x]

            self._count_color_stone(yi, x, before_stone)
            
            if self.count == 5:
                stone_coordinates = [(yj, x) for yj in range(yi, yi-5, -1)]
                return stone_coordinates

        # 斜め(\方向)の判定
        if y > x:
            start_y = y - x
            start_x = 0
            end_y = 15
            end_x = x + 15 - y
        elif x > y:
            start_y = 0
            start_x = x - y
            end_y = y + 15 - x
            end_x = 15
        else:
            start_y, start_x = 0, 0
            end_y, end_x = 15, 15

        self.count = 1
        for yi, xi in zip(range(start_y, end_y), range(start_x, end_x)):
            if yi == 0 or xi == 0:
                before_stone = 0
            else:
                before_stone = self.board[yi-1][xi-1]

            self._count_color_stone(yi, xi, before_stone)

            if self.count == 5:
                stone_coordinates = [(yj, xj) for yj, xj in zip(range(yi, yi-5, -1), range(xi, xi-5, -1))]
                return stone_coordinates

        # 斜め(/方向)の判定
        if y + x <= 14:
            start_y = 0
            start_x = y + x
            end_y = y + x + 1
            end_x = -1
        else:
            start_y = y + x -14
            start_x = 14
            end_y =  15
            end_x = start_y - 1

        self.count = 1
        for yi, xi in zip(range(start_y, end_y), range(start_x, end_x, -1)):
            if yi == start_y and xi == start_x:
                before_stone = 0
            else:
                before_stone = self.board[yi-1][xi+1]

            self._count_color_stone(yi, xi, before_stone)
            
            if self.count == 5:
                stone_coordinates = [(yj, xj) for yj, xj in zip(range(yi, yi-5, -1), range(xi, xi+5))]
                return stone_coordinates
        return []

stone_judgementというメソッドに判定機能をまとめました。

ボタンが押された所の横・縦・斜めのマスが揃っているか判定していきます。

その際はいちばん端の座標を洗い出し、端から端まで判定していきますが、同じ色かどうかは_count_color_stoneメソッドに投げ、石が前の座標と同じならカウントがプラスされていきます。

判定の向き

カウントが5になった段階で、5つの石の座標が入ったリストを呼び出し元に返します。

# main()内に追記
        stones_coordinates = game.stone_judgement(y, x)
        if stones_coordinates:
            for key_y, key_x in stones_coordinates:
                key = str(key_y) + ',' + str(key_x)
                window[key].update(button_color='red')
            break

石の入ったリストが空でなかったら、そのリストをfor文にかけ、yとxを取り出し、keyを復元して、該当箇所のボタンを赤色に変更します。

まとめ

クラス内の一部の変数をプライベートな物へと表現しなおし、ポップアップでリスタートできるようにして完成です。

※225マスあるため、「引き分け」は実装しませんでした。

import PySimpleGUI as sg


sg.theme('DarkBrown7')

class Gobang():
    def __init__(self):
        self._board = [[0 for _ in range(15)] for _ in range(15)]
        self._count = 1
        self.player = '○'
        self.player_color = '白'
    
    def turn_change(self):
        if self.player == '○':
            self.player = '●'
            self.player_color = '黒'
        else:
            self.player = '○'
            self.player_color = '白'
            
    def set_stone(self, y, x):
        if self._board[y][x] == 0:
            self._board[y][x] = self.player
            return True
        else:
            return False

    def _count_color_stone(self, yi, xi, before_stone):
        stone = self._board[yi][xi]
        if stone == 0:
            self._count = 1
        elif stone == before_stone:
            self._count += 1
        else:
            self._count = 1

    def stone_judgement(self, y, x):
        # 横方向の判定
        for xi in range(15):
            if xi == 0:
                before_stone = 0
            else:
                before_stone = self._board[y][xi-1]
            
            self._count_color_stone(y, xi, before_stone)

            if self._count == 5:
                stones_coordinates = [(y, xj) for xj in range(xi, xi-5, -1)]
                return stones_coordinates

        # 縦方向の判定
        self._count = 1
        for yi in range(15):
            if yi == 0:
                before_stone = 0
            else:
                before_stone = self._board[yi-1][x]

            self._count_color_stone(yi, x, before_stone)
            
            if self._count == 5:
                stone_coordinates = [(yj, x) for yj in range(yi, yi-5, -1)]
                return stone_coordinates

        # 斜め(\方向)の判定
        if y > x:
            start_y = y - x
            start_x = 0
            end_y = 15
            end_x = x + 15 - y
        elif x > y:
            start_y = 0
            start_x = x - y
            end_y = y + 15 - x
            end_x = 15
        else:
            start_y, start_x = 0, 0
            end_y, end_x = 15, 15

        self._count = 1
        for yi, xi in zip(range(start_y, end_y), range(start_x, end_x)):
            if yi == 0 or xi == 0:
                before_stone = 0
            else:
                before_stone = self._board[yi-1][xi-1]

            self._count_color_stone(yi, xi, before_stone)

            if self._count == 5:
                stone_coordinates = [(yj, xj) for yj, xj in zip(range(yi, yi-5, -1), range(xi, xi-5, -1))]
                return stone_coordinates

        # 斜め(/方向)の判定
        if y + x <= 14:
            start_y = 0
            start_x = y + x
            end_y = y + x + 1
            end_x = -1
        else:
            start_y = y + x -14
            start_x = 14
            end_y =  15
            end_x = start_y - 1

        self._count =1
        for yi, xi in zip(range(start_y, end_y), range(start_x, end_x, -1)):
            if yi == start_y and xi == start_x:
                before_stone = 0
            else:
                before_stone = self._board[yi-1][xi+1]

            self._count_color_stone(yi, xi, before_stone)
            
            if self._count == 5:
                stone_coordinates = [(yj, xj) for yj, xj in zip(range(yi, yi-5, -1), range(xi, xi+5))]
                return stone_coordinates
        return []


def create_layout():
    layout = [[sg.Push(), sg.Text('白のターン', font=(None, 10), key='-TURN-')]]
    for y in range(15):
        inner = []
        for x in range(15):
            inner.append(sg.Button('', size=(4, 2), font=(None, 10), pad=(0, 0), key=str(y)+','+str(x)))
        layout.append(inner.copy())
    return layout

def main():
    game = Gobang()
    layout = create_layout()
    window = sg.Window('五目並べ', layout)
    restart = 'No'
    while True:
        event, _ = window.read()
        if event == sg.WINDOW_CLOSED:
            break

        y, x = map(int, event.split(','))
        setted = game.set_stone(y, x)
        if setted == False:
            continue
        
        window[event].update(game.player)
        winner = game.player_color
        game.turn_change()
        window['-TURN-'].update(f'{game.player_color}のターン')

        stones_coordinates = game.stone_judgement(y, x)
        if stones_coordinates:
            for key_y, key_x in stones_coordinates:
                key = str(key_y) + ',' + str(key_x)
                window[key].update(button_color='red')
            restart = sg.popup_yes_no(f'{winner}の勝利!\nリスタートしますか?', no_titlebar=True, grab_anywhere=True)
            break

    window.close()
    if restart == 'Yes':
        main()

if __name__ == '__main__':
    main()

斜めの判定がかなり苦戦しました。
思いつかなかっただけで、もっとスマートな判定方法がありそうですが。

この「五目並べ」はちょっと前に一度完成していたのですが、ごちゃごちゃしているように思え、現在のクラスを使った書き方に直しました。

クラスを使わないバージョンのファイルの履歴は上げていませんが、ファイル自体はGitHubのリポジトリ内に記録として残しています。

プログラミングは実際に自分で作ってみないとわからないことが多く、かなり勉強になりました。

このページが少しでもお役に立てたのなら幸いです。

タイトルとURLをコピーしました