pygameでリバーシを作ってみた

Python
この記事は約18分で読めます。

GUIリバーシ

今回はPython+pygameでリバーシを作ってみました。実は『リバーシの盤って8×8だっけ?』というレベルでルールうろ覚えなんですが(そして確認もしてないw)、まぁそれっぽいものが数時間でも作れました。

いきなり全ソースコード

import sys
import pygame
import pygame.locals

# 定数・グローバル変数
# ゲームの状態
STARTUP = 0     # タイトル画面
PLAYING = 1     # 対戦中
GAMEOVER= 2     # ゲーム終了
mode = STARTUP

# 盤上の優先順位
cellPriorityList = [[1,1], [8,1], [1,8], [8,8],                             #1
                    [3,1], [6,1], [1,3], [8,3], [1,6], [8,6], [3,8], [6,8], #2
                    [3,3], [6,3], [3,6], [6,6],                             #3
                    [4,1], [5,1], [3,2], [7,2], [2,3], [4,3], [5,3], [7,3], #4
                    [1,4], [3,4], [6,4], [8,4], [1,5], [3,5], [7,5], [8,5],
                    [2,6], [4,6], [5,6], [7,6], [3,7], [6,7], [4,8], [5,8],
                    [4,2], [5,2], [2,4], [7,4], [2,5], [7,5], [4,7], [5,7], #5
                    [2,1], [7,1], [1,2], [8,2], [1,7], [8,7], [2,8], [7,8], #6
                    [2,2], [7,2], [2,7], [7,7]]

# 探索する方向
dlist=[ [-1,-1], [ 0,-1], [ 1,-1],
        [-1, 0],          [ 1, 0],
        [-1, 1], [ 0, 1], [ 1, 1] ]

# 盤面の状態
PLAYER  = 1
CPU     =-1
BLANK   = 0
WALL    = 255

# boardの初期状態を返す
def initBoard():
    return [[WALL,   WALL,   WALL,   WALL,   WALL,   WALL,  WALL,   WALL,    WALL,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK, PLAYER,    CPU,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,    CPU, PLAYER,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,  BLANK,   WALL],
            [WALL,   WALL,   WALL,   WALL,   WALL,   WALL,   WALL,   WALL,   WALL,   WALL]]

# boardを表示
def displayBoard(board):
    surface = pygame.display.get_surface()

    # 盤面の表示
    surface.fill( (0,128,0) )
    for i in range(1, 8, 1):
        pygame.draw.line(surface, (0,0,0), (i*60,0), (i*60,480))
        pygame.draw.line(surface, (0,0,0), (0,i*60), (480,i*60))

    # コマの表示
    for y in range(1, 9, 1):
        for x in range(1, 9, 1):
            if board[y][x] == PLAYER:
                pygame.draw.ellipse(surface, (255,255,255), ((x-1)*60+10, (y-1)*60+10,40,40))
            elif board[y][x] == CPU:
                pygame.draw.ellipse(surface, (0,0,0), ((x-1)*60+10, (y-1)*60+10,40,40))

    # スコアの表示
    scorePlayer, scoreCPU = countPieces(board)
    font = pygame.font.SysFont(None,30)
    text = font.render("Player", True, (255,255,255))
    surface.blit(text, (550,50))
    text = font.render(("%02d" % (scorePlayer,)), True, (255,255,255))
    surface.blit(text, (600,80))
    text = font.render("CPU", True, (255,255,255))
    surface.blit(text, (550,150))
    text = font.render(("%02d" % (scoreCPU,)), True, (255,255,255))
    surface.blit(text, (600,180))

    if mode == STARTUP:
        pygame.draw.rect(surface, (255,255,255), (70,70,340,340))
        font = pygame.font.SysFont(None, 50)
        text = font.render("CLICK TO START", True, (0,0,0))
        surface.blit(text, (int((340-text.get_rect().width)/2+70),215))

    if mode == GAMEOVER:
        pygame.draw.rect(surface, (255,255,255,128), (70,70,340,340))
        font = pygame.font.SysFont(None, 50)
        text = font.render("GAME OVER", True, (255,80,80))
        surface.blit(text, (int((340-text.get_rect().width)/2+70),180))
        if scorePlayer > scoreCPU:
            text = font.render("PLAYER WIN", True, (80,80,255))
        elif scorePlayer < scoreCPU:
            text = font.render("CPU WIN", True, (255,80,80))
        else:
            text = font.render("DRAW", True, (0,0,0))
        surface.blit(text, (int((340-text.get_rect().width)/2+70),250))

    pygame.display.update()

# board(x,y) に side がコマをおけるかをチェック
def canPutPieceXY(board, x, y, side):
    if board[y][x] != BLANK:                                        # そもそも(x,y)が空欄じゃなかったらFalse
        return False
    for dx,dy in dlist:                                             # 周囲8方向を順番に探索
        xn = x+dx
        yn = y+dy
        while board[yn][xn] == -side:                               # 注目している方向に相手のコマがあるあいだ進む
            xn += dx
            yn += dy
            if board[yn][xn] == side:
                return True
    return False

# board(x,y) にside がコマをおいたことでひっくり返るマス目のリストを返す
def getReversePiecesList(board, x, y, side):
    if canPutPieceXY(board, x, y, side) != True:                    # そもそも(x,y)におけるかチェック
        return False
    reversePicesesList = [[x,y]]                                    # 最初に駒を置く場所
    for dx,dy in dlist:                                             # 周囲8方向を順番に探索
        xn = x+dx
        yn = y+dy
        tmp=[]
        while board[yn][xn] == -side:                               # 注目している方向に相手のコマがあるあいだ進む
            tmp.append([xn,yn])                                     # ひっくり返すコマの候補として追加
            xn += dx
            yn += dy
            if board[yn][xn] == side:
                reversePicesesList += tmp                           # 辿った先に自分のコマがあったときだけひっくり返すリストに追加
    return reversePicesesList

# リストにあるコマを実際にひっくり返す(アニメーションや効果音などの拡張用に独立させた)
def reversePieces(board, reversePiecesList, side):
    for px,py in reversePiecesList:                                 # リストに入っている座標を取り出しながら、
        board[py][px] = side                                        # sideにしていく。
        displayBoard(board)                                         # 画面上の演出のため、1つ置き換える毎に画面を更新
        pygame.time.delay(100)                                      # 置き換えているところが目で追えるようにあえて100ms待ち

# side がコマをおける場所を探す。なければ False を返す
def think(board, side):
    for p in cellPriorityList:                                      # 優先度の高い増すから順に探索
        if canPutPieceXY(board, p[0], p[1], side):                  # そこに置けたら置く
            return p
    return False

# 勝敗が決したかを調べる
def isGameOver(board):                                              # PLAYER、CPU 双方とも置けなかったらゲームオーバー
    if think(board, PLAYER) == False and think(board, CPU) == False:    
        return True
    return False

# コマの数を調べる
def countPieces(board):
    scorePlayer = 0
    scoreCPU = 0
    for y in range(1, 9, 1):
        for x in range(1, 9, 1):                                    # 二重ループで盤面の全てのコマを探索
            if board[y][x] == PLAYER:                               # PLAYERのコマを数える
                scorePlayer += 1
            elif board[y][x] == CPU:                                # CPUのコマを数える
                scoreCPU += 1
    return scorePlayer, scoreCPU

# ゲームオーバー処理
def gameover(board):
    global mode
    mode = GAMEOVER
    displayBoard(board)

# メイン
def main():
    global mode

    pygame.init()
    pygame.display.set_mode((640,480))
    pygame.display.set_caption("Reversi")

    board = initBoard()                                             # 盤面の初期化
    displayBoard(board)

    mode = STARTUP
    turn = PLAYER

    while True:
        for event in pygame.event.get():
            if event.type == pygame.locals.QUIT:                    # ×ボタンをクリックしたらプログラム終了
                pygame.quit()
                sys.exit()
            if event.type == pygame.locals.MOUSEBUTTONDOWN:         # マウスボタンがクリックされたとき

                # PLAYER の手番
                if mode == PLAYING and turn == PLAYER:
                    mx,my = event.pos                               # マウスカーソルの座標を取得
                    x=int(mx/60)+1                                  # マス目を求める(単純に60で割るだけ)
                    y=int(my/60)+1
                    if canPutPieceXY(board, x, y, PLAYER) == True:  # クリックした場所にコマを置けるなら
                        reversePiecesList = getReversePiecesList(board, x, y, PLAYER)   # それでひっくり返るマスのリストを取得
                        reversePieces(board, reversePiecesList, PLAYER)     # 実際にひっくり返す
                        if isGameOver(board):
                            gameover(board)
                        turn = CPU

                # タイトル画面やゲームオーバー画面ではない場合
                if mode == STARTUP:                                 # スタート画面でクリックされたとき
                    board = initBoard()
                    turn = PLAYER                                   # PLAYERの手番から開始
                    mode = PLAYING                                  # 対戦モードに移動
                    displayBoard(board)
                if mode == GAMEOVER:                                # ゲーム終了画面でクリックされたとき
                    mode = STARTUP                                  # スタート画面に移動
                    displayBoard(board)

        # CPU の手番
        if mode == PLAYING and turn == CPU:
            t = think(board, CPU)                                   # 次に置く場所を決める
            if t != False:                                          # 置く場所がなければCPUはパス
                reversePiecesList = getReversePiecesList(board, t[0], t[1], CPU) # そこに置いた場合にひっくり返るコマのリスト
                reversePieces(board, reversePiecesList, CPU)        # 実際にひっくり返す
            if isGameOver(board):                                   # ゲーム終了になっているか確認
                gameover(board)
            if think(board, PLAYER):                                # PLAYER がどこにもおけない場合は強制パス
                turn = PLAYER

        pygame.time.wait(100)                                       # 100msのウェイト

if __name__=="__main__":
    main()

コード解説

今回は、後でちょっとしたことを試すために全面的に関数にしました。関数の外ではグローバル変数の初期化以外のことはやっていません。コメントを多めに入れたので読めば判るかな…?

プログラムで使っているテクニックでは、新しいものは1つだけ。

一番最後の2行です。

if __name__=="__main__":
    main()

__name__ は、システムが自動的に値をセットするグローバル変数です。このプログラムがコマンドラインから直接呼ばれた場合、__name__ には”__main__”という値がセットされます。また、このプログラムが別のプログラムからimportされた場合は、このプログラムのファイル名がセットされます。

つまり、if __name__==”__main__”: という判定によって、このプログラムがコマンドラインから直接呼ばれた場合のみ、ゲームが起動されるようになっています。

アルゴリズム解説

いちおう思考ゲームなのでCPUの手番では盤面のどこにコマを置くかを『考えて』いるわけですが…

大したことはやっていません。実はCPUの思考部分は、僕が小学生の時に作ったプログラムをPythonに移植しただけです。正直言って激弱です。

原理は、盤面のどこに置くと有利か(まず四隅、次はそこから2マス離れたところ…四隅のすぐ隣は最悪、下のリストを参照)、という優先順位のリストを作って、リストの順に探索して自分のコマを置ける場所があったら置く、というだけです。盤面全体の評価も、先読みもしていません。…まぁ小学生が思いつくレベルなんで。

1 6 2 4 4 2 6 1
6 7 4 5 5 4 7 6
2 4 3 4 4 3 4 2
4 5 4     4 5 4
4 5 4     4 5 4
2 4 3 4 4 3 4 2
6 7 4 5 5 4 7 6
1 6 2 4 4 2 6 1

それでもまぁ、暇つぶし程度にはなります。

そのうち真面目な思考ルーチンのゲームも作ってみます…。

コメント

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