4 連目ゲーム - ヒューマン対 AI

PythonBeginner
オンラインで実践に進む

はじめに

このプロジェクトは、プレイヤーが AI と対戦できる、古典的なコネクト・フォーゲームの Python 実装です。ゲームのインターフェイスと制御には Pygame ライブラリを使用しています。AI の意思決定はモンテカルロ木探索アルゴリズムに基づいており、難易度レベルを調整可能で、より賢い AI オポーネントと対戦して自分自身を試すことができます。

キーコンセプト

  • Pygame を使用したゲーム開発
  • AI の意思決定にモンテカルロ木探索アルゴリズムを実装する

👀 プレビュー

Connect Four Game

🎯 タスク

このプロジェクトでは、次のことを学びます。

  • Pygame を使ってゲームを構築する方法
  • AI の意思決定にモンテカルロ木探索アルゴリズムを実装する方法
  • AI の難易度レベルをカスタマイズして向上させる方法
  • 人間対 AI の対戦用に面白くて対話性のあるコネクト・フォーゲームを作成する方法

🏆 成果

このプロジェクトを完了すると、次のことができるようになります。

  • Python と Pygame を使ってゲームを開発する
  • モンテカルロ木探索アルゴリズムの原理を理解する
  • 挑戦的なゲーム体験を作り出すために AI オポーネントの難易度を調整する
  • ユーザーインターフェイスを向上させて、ゲーム体験をより魅力的にする

開発準備

四子棋ゲームは、サイズ 7*6 のグリッド上で行われます。プレイヤーは交互に列の上部からピースを落とします。ピースはその列の最下部の空きスペースに落ちます。横、縦、または斜めに 4 つのピースを一直線に並べたプレイヤーが勝ちます。

Four In A Row game grid

このプロジェクトのコードを保存するために、~/project ディレクトリに fourinrow.py という名前のファイルを作成します。また、ゲームのインターフェイスとサポート操作を実装するために、Pygame ライブラリをインストールする必要があります。

cd ~/project
touch fourinrow.py
sudo pip install pygame

このプロジェクトで必要な画像リソースは、~/project/images ディレクトリにあります。

このプロジェクトのコードをよりよく理解するために、完全なソリューションのコードと一緒に学習することをお勧めします。

✨ 解答を確認して練習

変数の初期化

使用する変数には、チェス盤の幅と高さ(異なるサイズのチェス盤を設計するために変更可能)、難易度レベル、チェスピースのサイズ、およびいくつかの座標変数の設定が含まれます。

fourinrow.py ファイルに、次のコードを入力します。

import random, copy, sys, pygame
from pygame.locals import *

BOARDWIDTH = 7  ## ゲーム盤の列数
BOARDHEIGHT = 6 ## ゲーム盤の行数
assert BOARDWIDTH >= 4 and BOARDHEIGHT >= 4, 'Board must be at least 4x4.'

## python の assert 文は、与えられたブール式が必ず真であることを宣言するために使用されます。
## 式が偽の場合、例外を発生させます。

DIFFICULTY = 2 ## 難易度レベル、コンピュータが考慮できる手数
               ## ここでは 2 は、相手の 7 通りの手を考慮し、それらの 7 通りの手にどのように対応するかを意味します

SPACESIZE = 50 ## チェスピースのサイズ

FPS = 30 ## 画面の更新レート、1 秒あたり 30 回
WINDOWWIDTH = 640  ## ゲーム画面の幅(ピクセル)
WINDOWHEIGHT = 480 ## ゲーム画面の高さ(ピクセル)

XMARGIN = int((WINDOWWIDTH - BOARDWIDTH * SPACESIZE) / 2)  ## グリッドの左辺の X 座標
YMARGIN = int((WINDOWHEIGHT - BOARDHEIGHT * SPACESIZE) / 2) ## グリッドの上辺の Y 座標
BRIGHTBLUE = (0, 50, 255) ## 青い色
WHITE = (255, 255, 255) ## 白い色

BGCOLOR = BRIGHTBLUE
TEXTCOLOR = WHITE

RED = 'red'
BLACK = 'black'
EMPTY = None
HUMAN = 'human'
COMPUTER = 'computer'

また、pygame のいくつかのグローバル変数を定義する必要があります。これらのグローバル変数は、後でさまざまなモジュールで何度も呼び出されます。その多くは読み込まれた画像を格納する変数であるため、準備作業は少し時間がかかりますので、しばらくお待ちください。

## pygame モジュールを初期化する
pygame.init()

## Clock オブジェクトを作成する
FPSCLOCK = pygame.time.Clock()

## ゲームウィンドウを作成する
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))

## ゲームウィンドウのタイトルを設定する
pygame.display.set_caption(u'four in row')

## Rect(left, top, width, height) は、位置とサイズを定義するために使用されます
REDPILERECT = pygame.Rect(int(SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

## ウィンドウの左下と右下にチェスピースを作成する
BLACKPILERECT = pygame.Rect(WINDOWWIDTH - int(3 * SPACESIZE / 2), WINDOWHEIGHT - int(3 * SPACESIZE / 2), SPACESIZE, SPACESIZE)

## 赤いチェスピースの画像を読み込む
REDTOKENIMG = pygame.image.load('images/4rowred.png')

## 赤いチェスピースの画像を SPACESIZE にスケールする
REDTOKENIMG = pygame.transform.smoothscale(REDTOKENIMG, (SPACESIZE, SPACESIZE))

## 黒いチェスピースの画像を読み込む
BLACKTOKENIMG = pygame.image.load('images/4rowblack.png')

## 黒いチェスピースの画像を SPACESIZE にスケールする
BLACKTOKENIMG = pygame.transform.smoothscale(BLACKTOKENIMG, (SPACESIZE, SPACESIZE))

## チェス盤の画像を読み込む
BOARDIMG = pygame.image.load('images/4rowboard.png')

## チェス盤の画像を SPACESIZE にスケールする
BOARDIMG = pygame.transform.smoothscale(BOARDIMG, (SPACESIZE, SPACESIZE))

## 人間の勝者の画像を読み込む
HUMANWINNERIMG = pygame.image.load('images/4rowhumanwinner.png')

## AI の勝者の画像を読み込む
COMPUTERWINNERIMG = pygame.image.load('images/4rowcomputerwinner.png')

## 引き分けの画像を読み込む
TIEWINNERIMG = pygame.image.load('images/4rowtie.png')

## Rect オブジェクトを返す
WINNERRECT = HUMANWINNERIMG.get_rect()

## 勝者の画像をゲームウィンドウの中央に配置する
WINNERRECT.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))

## ユーザー指示用の矢印画像を読み込む
ARROWIMG = pygame.image.load('images/4rowarrow.png')

## Rect オブジェクトを返す
ARROWRECT = ARROWIMG.get_rect()

## 矢印画像の左位置を設定する
ARROWRECT.left = REDPILERECT.right + 10

## 矢印画像をその下の赤いチェスピースと垂直に整列させる
ARROWRECT.centery = REDPILERECT.centery

このプロジェクトのコードをよりよく理解するために、完全なソリューションのコードと一緒に学習することをお勧めします。

✨ 解答を確認して練習

盤面の設計

最初に、ボードを表す二次元リストをクリアし、その後、プレイヤーと AI の手に基づいて、ボード上の対応する位置に色を設定します。

def drawBoard(board, extraToken=None):
    ## DISPLAYSURF は、変数初期化モジュールで定義された私たちのインターフェイスです。
    DISPLAYSURF.fill(BGCOLOR) ## ゲームウィンドウの背景色を青で塗りつぶします。
    spaceRect = pygame.Rect(0, 0, SPACESIZE, SPACESIZE) ## Rect インスタンスを作成します。
    for x in range(BOARDWIDTH):
        ## 各列の各行の各セルの左上位置座標を決定します。
        for y in range(BOARDHEIGHT):
            spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))

            ## x = 0 かつ y = 0 のとき、1 列目の 1 行目の最初のセルです。
            if board[x][y] == RED: ## セルの値が赤の場合、
                ## ゲームウィンドウの spaceRect 内に赤いトークンを描画します。
                DISPLAYSURF.blit(REDTOKENIMG, spaceRect)
            elif board[x][y] == BLACK: ## それ以外の場合、黒いトークンを描画します。
                DISPLAYSURF.blit(BLACKTOKENIMG, spaceRect)

    ## extraToken は、位置情報と色情報を含む変数です。
    ## 特定のトークンを表示するために使用されます。
    if extraToken!= None:
        if extraToken['color'] == RED:
            DISPLAYSURF.blit(REDTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))
        elif extraToken['color'] == BLACK:
            DISPLAYSURF.blit(BLACKTOKENIMG, (extraToken['x'], extraToken['y'], SPACESIZE, SPACESIZE))

    ## トークンのパネルを描画します。
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            spaceRect.topleft = (XMARGIN + (x * SPACESIZE), YMARGIN + (y * SPACESIZE))
            DISPLAYSURF.blit(BOARDIMG, spaceRect)

    ## ゲームウィンドウの左下と右下にトークンを描画します。
    DISPLAYSURF.blit(REDTOKENIMG, REDPILERECT) ## 左の赤いトークン。
    DISPLAYSURF.blit(BLACKTOKENIMG, BLACKPILERECT) ## 右の黒いトークン。


def getNewBoard():
    board = []
    for x in range(BOARDWIDTH):
        board.append([EMPTY] * BOARDHEIGHT)
    return board ## BOARDHEIGHT 個の None 値を持つボードリストを返します。

上記のコードでは、drawBoard()関数がボードとボード上のトークンを描画します。getNewBoard()関数は新しいボードデータ構造を返します。

✨ 解答を確認して練習

最適な動きのための AI アルゴリズム

モンテカルロ木探索の考え方を簡単に説明すると:

一次元のモンテカルロ法を使って碁盤の局面を評価する。具体的には、特定のチェス盤の局面が与えられたとき、プログラムは現在の局面で利用可能なすべての点からランダムに 1 点を選び、そこにチェスピースを置く。利用可能な点をランダムに選ぶ(ロールポイント)プロセスを、両者に利用可能な点がなくなるまで(ゲーム終了)繰り返し、その最終状態の勝敗結果を、現在の局面を評価する基準としてフィードバックする。

このプロジェクトでは、AI は連続して異なる列を選び、両者の勝利結果を評価する。AI は最終的に評価が高い戦略を選ぶ。

以下の画像と文章を見る前に、最後のコードを見てから、対応する説明を参照してください。

以下の図で AI とプレイヤーの対戦を見る:

AI player move analysis

プロジェクト内のいくつかの変数は、AI のチェスピース操作のプロセスを直感的に反映しています:

PotentialMoves:チェスピースをリスト内の任意の列に動かしたときの AI の勝利の可能性を表すリストを返します。値は -1 から 1 までの乱数です。値が負の場合、次の 2 手でプレイヤーが勝つ可能性があり、値が小さいほど、プレイヤーが勝つ可能性が高くなります。値が 0 の場合、プレイヤーは勝たないし、AI も勝たないことを意味します。値が 1 の場合、AI が勝てることを意味します。

bestMoveFitness:フィットネスは PotentialMoves から選択される最大値です。

bestMoves:PotentialMoves に複数の最大値がある場合、AI がチェスピースをこれらの値がある列に動かしたとき、プレイヤーの勝つ確率が最も小さいことを意味します。したがって、これらの列が bestMoves リストに追加されます。

column:bestMoves に複数の値がある場合、bestMoves からランダムに 1 列を選んで AI の手とします。1 つの値の場合、column はこの唯一の値です。

このプロジェクトでは、これらの bestMoveFitness、bestMoves、column、および potentialMoves を印刷することで、上の図での AI の各手のパラメータを推測することができます。

✨ 解答を確認して練習

AI の動き

ステップ potentialMoves bestMoveFitness bestMoves column
1 [0, 0, 0, 0, 0, 0, 0] 0 [0, 1, 2, 3, 4, 5, 6] 0
2 [0, 0, 0, 0, 0, 0, 0] 0 [0, 1, 2, 3, 4, 5, 6] 6
3 [-0.12, -0.12, -0.12, 0, -0.12, -0.12, -0.12] 0 [3] 3
4 [-0.34, -0.22, 0, -0.34, -0.34, -0.22, -0.34] 0 [2] 2
AI move selection flowchart

3 ステップ目の AI の選択を調べることで、アルゴリズムをよりよく理解することができます:

以下の図は、AI のいくつかの手を示しており、AI が 1 列目にピースを置いた場合のプレイヤーの可能な選択と、AI の次の手がプレイヤーの勝利確率に与える影響を示しています。この探索と反復プロセスを通じて、AI は次の 2 手で相手と自分自身の勝利状況を判断し、それに応じて決定を行うことができます。

AI move impact flowchart

以下の図は、AI のフィットネス値を計算するフローチャートです。このプロジェクトでは、難易度係数は 2 であり、7^4 = 2041 ケースを考慮する必要があります:

AI fitness calculation flowchart

上記のフローチャートから、AI が最初のピースを 0 列、1 列、2 列、4 列、5 列、または 6 列に置いた場合、プレイヤーは常に残りの 2 つのピースを 3 列に置いて勝つことができることがわかります。表現を簡単にするために、シーケンスを使ってさまざまな組み合わせを表します。最初の要素は AI の最初の手を表し、2 番目の数字はプレイヤーの応答を表し、3 番目の数字は AI の応答を表します。「X」は任意の有効な手を表します。したがって、[0,0,x]=0 であり、シーケンスが[0,x<>3,x]の場合、プレイヤーは勝てないことが推測できます。プレイヤーの 2 番目のピースが 3 列にあり、AI の 2 番目の手が 3 列にない場合にのみ、AI が勝てます。したがって、[0,x=3,x<>3] = -1 であり、このようなケースは 6 つあります。最終結果は(0+0+...(43回)-1*6)/7/7 = -0.12です。

同様の推論で、他の 4 ケースの結果はすべて -0.12 です。AI の最初の手が 3 列にある場合、プレイヤーは勝てず、AI も勝てないので、値は 0 になります。AI は最も高いフィットネス値の手を選び、これは 3 列にピースを置くことを意味します。

同じ分析を AI の後続の手にも適用できます。要するに、AI の手の後にプレイヤーが勝つ可能性が高ければ高いほど、AI のフィットネス値は低くなり、AI はプレイヤーが勝たないように、より高いフィットネス値の手を選びます。もちろん、AI 自身が勝てる場合、それ自身が勝利につながる手を優先します。

def getPotentialMoves(board, tile, lookAhead):
    if lookAhead == 0 or isBoardFull(board):
        '''
        難易度係数が 0 またはボードが埋まっている場合、
        すべての値が 0 に設定されたリストを返します。これは、
        フィットネス値が各列の潜在的な手に等しいことを意味します。
        この場合、AI はランダムにピースを落とし、知性を失います。
        '''
        return [0] * BOARDWIDTH

    ## 相手のピースの色を決定する
    if tile == RED:
        enemyTile = BLACK
    else:
        enemyTile = RED
    potentialMoves = [0] * BOARDWIDTH
    ## 潜在的な手のリストを初期化し、すべての値を 0 に設定する
    for firstMove in range(BOARDWIDTH):
        ## 各列を反復し、どちらかの手を最初の手として考慮する
        ## 相手の手はカウンタームーブとして考慮される
        ## ここで、最初の手は AI の手を指し、相手の手はカウンタームーブとして考慮される
        ## ボードの深いコピーを取って、ボードと dupeBoard の相互影響を防ぐ
        dupeBoard = copy.deepcopy(board)
        if not isValidMove(dupeBoard, firstMove):
        ## dupeBoard で最初の手で指定された列に黒いピースを置く手が無効な場合
            continue
            ## 次の最初の手に続ける
        makeMove(dupeBoard, tile, firstMove)
        ## 有効な手の場合、対応するグリッドの色を設定する
        if isWinner(dupeBoard, tile):
        ## AI が勝つ場合
            potentialMoves[firstMove] = 1
            ## 勝利するピースは自動的に高い値を取得して、勝利の確率を示す
            ## 値が大きいほど、勝利の確率が高く、相手の勝利の確率が低くなる
            break
            ## 他の手の計算を妨げない
        else:
            if isBoardFull(dupeBoard):
            ## dupeBoard に空のグリッドがない場合
                potentialMoves[firstMove] = 0
                ## 動かすことはできない
            else:
                for counterMove in range(BOARDWIDTH):
                ## 相手の手を考慮する
                    dupeBoard2 = copy.deepcopy(dupeBoard)
                    if not isValidMove(dupeBoard2, counterMove):
                        continue
                    makeMove(dupeBoard2, enemyTile, counterMove)
                    if isWinner(dupeBoard2, enemyTile):
                        potentialMoves[firstMove] = -1
                        ## プレイヤーが勝つ場合、この列の AI のフィットネス値は最も低くなる
                        break
                    else:
                        ## 再帰的に getPotentialMoves を呼び出す
                        results = getPotentialMoves(dupeBoard2, tile, lookAhead - 1)
                        ## より正確な結果のためにここで浮動小数点数表現を使用する
                        ## これにより、potentialMoves の値が [-1, 1] の範囲内に収まることが保証される
                        potentialMoves[firstMove] += (sum(results)*1.0 / BOARDWIDTH) / BOARDWIDTH
    return potentialMoves
✨ 解答を確認して練習

プレイヤー操作

チェスピースをドラッグし、チェスピースが配置されるマスを決定し、チェスピースを検証し、チェスピースを落とす関数を呼び出して操作を完了します。

def getHumanMove(board, isFirstMove):
    draggingToken = False
    tokenx, tokeny = None, None
    while True:
        ## pygame.event.get() を使用してすべてのイベントを処理する
        for event in pygame.event.get():
            if event.type == QUIT: ## 停止して終了する
                pygame.quit()
                sys.exit()

            elif event.type == MOUSEBUTTONDOWN and not draggingToken and REDPILERECT.collidepoint(event.pos):
                ## イベントタイプがマウスボタン押下で、draggingToken が True で、マウスクリック位置が REDPILERECT 内の場合
                draggingToken = True
                tokenx, tokeny = event.pos

            elif event.type == MOUSEMOTION and draggingToken: ## 赤いピースがドラッグされている場合
                tokenx, tokeny = event.pos ## ドラッグされたピースの位置を更新する

            elif event.type == MOUSEBUTTONUP and draggingToken:
                ## マウスが離され、チェスピースがドラッグされている場合
                ## チェスピースがボードの真上にドラッグされている場合
                if tokeny < YMARGIN and tokenx > XMARGIN and tokenx < WINDOWWIDTH - XMARGIN:
                    column = int((tokenx - XMARGIN) / SPACESIZE) ## チェスピースの x 座標に基づいて、チェスピースが落ちる列を決定する (0,1...6)
                    if isValidMove(board, column): ## チェスピースの移動が有効な場合
                        """
                        対応する空きマスに落とす
                        この関数は、落とす効果のみを表示する
                        この関数なしでも、次のコードによりマスを埋めるチェスピースを実現できる
                        """
                        animateDroppingToken(board, column, RED)

                        ## 空き列の最下部のマスを赤に設定する
                        board[column][getLowestEmptySpace(board, column)] = RED
                        drawBoard(board) ## 落とされたマスに赤いチェスピースを描画する
                        pygame.display.update() ## ウィンドウを更新する
                        return
                tokenx, tokeny = None, None
                draggingToken = False

        if tokenx!= None and tokeny!= None: ## チェスピースがドラッグされている場合、ドラッグされたチェスピースを表示する
            drawBoard(board, {'x':tokenx - int(SPACESIZE / 2), 'y':tokeny - int(SPACESIZE / 2), 'color':RED})
            ## x、y 座標を調整して、ドラッグ中にマウスが常にチェスピースの中央位置になるようにする

        else:
            drawBoard(board) ## 無効な手の場合、マウスが離された後、ボード内のすべての値が None のため
            ## drawBoard を呼び出すときに行われる操作は、下の 2 つのチェスピースを表示することで、ドラッグ開始位置にチェスピースを戻すのと同じ

        if isFirstMove:
            DISPLAYSURF.blit(ARROWIMG, ARROWRECT) ## AI が最初に手を打つ場合、ヒント操作画像を表示する

        pygame.display.update()
        FPSCLOCK.tick()

上記のコードでは、getHumanMove()関数がプレイヤーの手を処理します。animateDroppingToken()関数がチェスピースの落下をアニメーション化します。getLowestEmptySpace()関数が列内の最下部の空きスペースを返します。

✨ 解答を確認して練習

AI の操作

AI のピースがそれぞれの位置に移動して着地するアニメーションを実現する関数を実装する。

def animateComputerMoving(board, column):
    x = BLACKPILERECT.left ## 下の黒いピースの左座標
    y = BLACKPILERECT.top ## 下の黒いピースの上座標
    speed = 1.0
    while y > (YMARGIN - SPACESIZE): ## y が大きな値を持つとき、つまりピースがウィンドウの下にあるとき
        y -= int(speed) ## y を連続的に減らす、つまりピースが上に移動する
        speed += 0.5 ## y が減少する速度を上げる
        drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
        ## y が変化し続け、黒いピースを連続的に描画し、連続的な上昇の効果を作り出す
        pygame.display.update()
        FPSCLOCK.tick()
    ## ピースがボードの上部に上昇したとき
    y = YMARGIN - SPACESIZE ## y をリセットし、ピースの下端をボードの上端に合わせる
    speed = 1.0
    while x > (XMARGIN + column * SPACESIZE): ## x が目的の列の x 座標より大きいとき
        x -= int(speed) ## x を連続的に減らす、つまりピースが左に移動する
        speed += 0.5
        drawBoard(board, {'x':x, 'y':y, 'color':BLACK})
        ## このとき、y 座標は変化しない、つまりピースが水平方向に列に移動する
        pygame.display.update()
        FPSCLOCK.tick()
    ## 黒いピースが計算された空きスペースに着地する
    animateDroppingToken(board, column, BLACK)

返された potentialMoves のリストから最も高い数を選び、フィットネス値として、そして高いフィットネス値を持つ列の中からランダムに選んで最終的な移動目標とする。

def getComputerMove(board):
    potentialMoves = getPotentialMoves(board, BLACK, DIFFICULTY) ## 潜在的な手、BOARDWIDTH 個の値を持つリスト
               ## リスト内の値は設定された難易度レベルに関連付けられている
    bestMoves = [] ## 空の bestMoves リストを作成する
    bestMoveFitness = -1 ## potentialMoves の最小値が -1 なので、下限として機能する
    print(bestMoveFitness)
    for i in range(len(potentialMoves)):
        if potentialMoves[i] > bestMoveFitness and isValidMove(board, i):
            bestMoveFitness = potentialMoves[i] ## bestMoves を連続的に更新し、bestMoves の各値が最大値になるようにする
            ## 同時に、移動が有効であることを確認する。

    for i in range(len(potentialMoves)):
        if potentialMoves[i] == bestMoveFitness and isValidMove(board, i):
            bestMoves.append(i) ## ピースを移動できるすべての列をリストに追加する。このリストは空でも、1 つの値のみを含んでも、複数の値を含んでもよい。
    print(bestMoves)
    return random.choice(bestMoves) ## ピースを移動できる列の中からランダムに 1 つ選んで目標とする移動とする。
✨ 解答を確認して練習

駒の移動操作

ピースの対応する座標を連続的に変更することで、落下するアニメーション効果を実現する。

def getLowestEmptySpace(board, column):
    ## 列内の最下部の空きスペースを返す
    for y in range(BOARDHEIGHT-1, -1, -1):
        if board[column][y] == EMPTY:
            return y
    return -1

def makeMove(board, player, column):
    lowest = getLowestEmptySpace(board, column)
    if lowest!= -1:
        board[column][lowest] = player
        '''
        列内の最下部の空きスペースにプレイヤー(赤/黒)を割り当てる。
        ピースは列内の最下部の空きスペースに落とされるため、
        そのスペースの色として考えられる。
        '''

def animateDroppingToken(board, column, color):
    x = XMARGIN + column * SPACESIZE
    y = YMARGIN - SPACESIZE
    dropSpeed = 1.0
    lowestEmptySpace = getLowestEmptySpace(board, column)

    while True:
        y += int(dropSpeed)
        dropSpeed += 0.5
        if int((y - YMARGIN) / SPACESIZE) >= lowestEmptySpace:
            return
        drawBoard(board, {'x':x, 'y':y, 'color':color})
        pygame.display.update()
        FPSCLOCK.tick()

上記のコードでは、makeMove()関数がボード上で手を打ちます。animateDroppingToken()関数がチェスピースの落下をアニメーション化します。getLowestEmptySpace()関数が列内の最下部の空きスペースを返します。

✨ 解答を確認して練習

いくつかの判定関数

チェスピースの移動の妥当性を判定し、チェス盤上に空きスペースが残っているかどうかを判定する。

def isValidMove(board, column):
    ## チェスピースの移動の妥当性を判定する
    if column < 0 or column >= (BOARDWIDTH) or board[column][0]!= EMPTY:
    ## 列が 0 未満または BOARDWIDTH より大きい場合、または列に空きスペースがない場合
        return False
        ## それは無効な手であり、それ以外の場合は有効である
    return True


def isBoardFull(board):
    ## グリッドに空きスペースがない場合、True を返す
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT):
            if board[x][y] == EMPTY:
                return False
    return True

上記のコードでは、isValidMove()関数は移動が有効な場合に True を返します。isBoardFull()関数はボードが埋まっている場合に True を返します。

✨ 解答を確認して練習

勝利条件判定

4 つの勝利条件を分かりやすく理解するためにいくつかの図を用意します。図に示されている位置は x と y の極端な値に対応しています。

勝利条件の図
def isWinner(board, tile):
    ## ピースの水平方向の状況をチェックする
    for x in range(BOARDWIDTH - 3): ## x は 0、1、2、3 の値をとる
        for y in range(BOARDHEIGHT): ## すべての行を反復する
            ## x = 0 の場合、y 行目の最初の 4 つのピースがすべて同じタイルであるかどうかをチェックします。これは、4 つ並んだピースのすべての水平方向の状況を横断するために使用できます。どんな x、y が真であれば、勝利と判定できます
            if board[x][y] == tile and board[x+1][y] == tile and board[x+2][y] == tile and board[x+3][y] == tile:
                return True

    ## ピースの垂直方向の状況をチェックする、水平方向の状況と同様
    for x in range(BOARDWIDTH):
        for y in range(BOARDHEIGHT - 3):
            if board[x][y] == tile and board[x][y+1] == tile and board[x][y+2] == tile and board[x][y+3] == tile:
                return True

    ## ピースの左傾きの斜め方向の状況をチェックする
    for x in range(BOARDWIDTH - 3): ## x は 0、1、2、3 の値をとる
        for y in range(3, BOARDHEIGHT): ## 左傾きの斜めに 4 つ並んだ場合、最下部のピースは最上部から少なくとも 4 マス離れていなければならないため、つまり y >= 3
            if board[x][y] == tile and board[x+1][y-1] == tile and board[x+2][y-2] == tile and board[x+3][y-3] == tile: ## 左傾きの斜めの 4 つのピースが同じ色であるかどうかを判定する
                return True

    ## ピースの右傾きの斜め方向の状況をチェックする、左傾きの斜め方向の状況と同様
    for x in range(BOARDWIDTH - 3):
        for y in range(BOARDHEIGHT - 3):
            if board[x][y] == tile and board[x+1][y+1] == tile and board[x+2][y+2] == tile and board[x+3][y+3] == tile:
                return True
    return False
✨ 解答を確認して練習

ゲームのメインループの作成

最後に、ゲームを継続的に実行するためのゲームのメインループを作成します。

def main():

    ## 省略された既存のコード

    isFirstGame = True ## isFirstGame を初期化する

    while True: ## ゲームを継続的に実行する
        runGame(isFirstGame)
        isFirstGame = False


def runGame(isFirstGame):
    if isFirstGame:
        ## 最初のゲームの開始時
        ## AI に最初の手番をさせて、プレイヤーがゲームの進行を見ることができるようにする
        turn = COMPUTER
        showHelp = True
    else:
        ## 2 回目以降のゲームでは、手番をランダムに割り当てる
        if random.randint(0, 1) == 0:
            turn = COMPUTER
        else:
            turn = HUMAN
        showHelp = False
    mainBoard = getNewBoard() ## 初期の空の盤の構造を設定する
    while True: ## ゲームのメインループ
        if turn == HUMAN: ## プレイヤーの手番の場合

            getHumanMove(mainBoard, showHelp) ## プレイヤーの手番のメソッドを呼び出す、詳細は getHumanMove メソッドを参照のこと
            if showHelp:
                ## ヒント画像がある場合、AI が最初の手番を行った後にヒントを消す
                showHelp = False
            if isWinner(mainBoard, RED): ## 赤いチップ(プレイヤー)が勝利した場合
                winnerImg = HUMANWINNERIMG ## プレイヤーの勝利画像を読み込む
                break ## ループを抜ける
            turn = COMPUTER ## 最初の手番を AI に渡す
        else:
            ## AI の手番の場合
            column = getComputerMove(mainBoard) ## AI の手番のメソッドを呼び出す、詳細は getComputerMove メソッドを参照のこと
            print(column)
            animateComputerMoving(mainBoard, column) ## 黒いチップを動かす
            makeMove(mainBoard, BLACK, column) ## 列内の最下部の空きスロットを黒に設定する
            if isWinner(mainBoard, BLACK):
                winnerImg = COMPUTERWINNERIMG
                break
            turn = HUMAN ## プレイヤーの手番に切り替える

        if isBoardFull(mainBoard):
            ## 盤が埋まっている場合、引き分け
            winnerImg = TIEWINNERIMG
            break
✨ 解答を確認して練習

実行とテスト

次に、プログラムを実行して、その動作を確認します。

cd ~/project
python fourinrow.py
プログラム実行のデモ
✨ 解答を確認して練習

まとめ

このプロジェクトは、モンテカルロアルゴリズムに基づき、Python と Pygame モジュールを使ってヒューマン対 AI のチェスゲームを実装しました。このプロジェクトを通じて、Pygame におけるインスタンスの作成とオブジェクトの移動の基本に慣れることができ、またモンテカルロアルゴリズムの具体的な応用についても初步的な理解を得ることができました。