Pythonでマインスイーパーを作る方法を解説する

スポンサーリンク

今回は、Python言語を使って、端末ベースのマインスイーパーを自作する手順を説明します。

スポンサーリンク

ゲームについて

Minesweeperは、地雷と数字を含む正方形のグリッドをクリアするシングルプレイヤー・ゲームです。

プレイヤーは、隣のタイルにある数字を使って、地雷に着地するのを防がなければなりません。

ゲームプレイデモ

マインスイーパのゲームを数時間かけて作った余韻。

マインスイーパデモ

Pythonを使ったマインスイーパの設計

ゲームロジックを作る前に、ゲームの基本的なレイアウトを設計する必要があります。


正方形のグリッドはPythonで簡単に作成することができます

# Printing the Minesweeper Layout
def print_mines_layout():
    global mine_values
    global n
 
    print()
    print(" MINESWEEPER
"
)
 
    st = "   "
    for i in range(n):
        st = st + "     " + str(i + 1)
    print(st)  
 
    for r in range(n):
        st = "     "
        if r == 0:
            for col in range(n):
                st = st + "______" 
            print(st)
 
        st = "     "
        for col in range(n):
            st = st + "|     "
        print(st + "|")
         
        st = "  " + str(r + 1) + "  "
        for col in range(n):
            st = st + "|  " + str(mine_values[r][col]) + "  "
        print(st + "|")
 
        st = "     "
        for col in range(n):
            st = st + "|_____"
        print(st + '|')
 
    print()

各反復で表示されるグリッドは、次の図のようになります。

if __name__ == "__main__":
 
    # Size of grid
    n = 8
    # Number of mines
    mines_no = 8
 
    # The actual values of the grid
    numbers = [[0 for y in range(n)] for x in range(n)]
    # The apparent values of the grid
    mine_values = [[' ' for y in range(n)] for x in range(n)]
    # The positions that have been flagged
    flags = []

M’`マークは、そのマスに「地雷」があることを示しています。

このように、グリッド上の任意の数値は、隣接する「8」個のセルに存在する地雷の数を表している。

mine_values`のような変数の使用については、チュートリアルの中でさらに説明します。

入力システム

どのようなゲームでも、最も重要な部分の1つは、入力方法を維持することです。

私たちのマインスイーパバージョンでは、入力手法として行と列の番号を使用することにします。

ゲームを始める前に、スクリプトはプレイヤーに一連の指示を与えなければならない。

我々のゲームでは次のようにプリントします。

# Function for setting up Mines
def set_mines():
 
    global numbers
    global mines_no
    global n
 
    # Track of number of mines already set up
    count = 0
    while count < mines_no:
 
        # Random number from all possible grid positions
        val = random.randint(0, n*n-1)
 
        # Generating row and column from the number
        r = val // n
        col = val % n
 
        # Place the mine, if it doesn't already have one
        if numbers[r][col] != -1:
            count = count + 1
            numbers[r][col] = -1

グリッドと一緒に表示される行と列の番号は、入力システムとして有用です。

ご存知のように、何の指標もなく地雷を追跡することは困難です。

そこで、マインスイーパーには、地雷があることが分かっているマスをマークするために「フラグ」を使用する機能がある。

データストレージ

マインスイーパの1回のゲームでは、以下の情報を記録しておく必要がある。

  • グリッドの大きさ
  • 地雷の数。
  • ゲーム開始時に、プレイヤーに知られていないゲームの実際の値を保存するためのコンテナが必要です。例えば、地雷の位置など。
  • 見かけの」グリッド値 – 各手順の後、プレーヤーに表示されなければならないすべての値を更新する必要があります。
  • フラグを立てた位置 – フラグを立てたセル。

これらの値は次のデータ構造を使って格納される。

# Function for setting up the other grid values
def set_values():
 
    global numbers
    global n
 
    # Loop for counting each cell value
    for r in range(n):
        for col in range(n):
 
            # Skip, if it contains a mine
            if numbers[r][col] == -1:
                continue
 
            # Check up 
            if r > 0 and numbers[r-1][col] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check down   
            if r < n-1  and numbers[r+1][col] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check left
            if col > 0 and numbers[r][col-1] == -1:
                numbers[r][c] = numbers[r][c] + 1
            # Check right
            if col < n-1 and numbers[r][col+1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check top-left   
            if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check top-right
            if r > 0 and col < n-1 and numbers[r-1][col+1]== -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check below-left 
            if r < n-1 and col > 0 and numbers[r+1][col-1]== -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check below-right
            if r < n-1 and col< n-1 and numbers[r+1][col+1]==-1:
                numbers[r][col] = numbers[r][col] + 1

マインスイーパのゲームロジックはそれほど多くはない。

すべての努力はマインスイーパのレイアウトを設定することです。

地雷を設置する

プレイヤーが地雷の位置を予測できないように、地雷の位置をランダムに設定する必要があります。

これは以下の方法で行うことができます。

# Set the mines
set_mines()
 
# Set the values
set_values()
 
# Display the instructions
instructions()
 
# Variable for maintaining Game Loop
over = False
         
# The GAME LOOP
while not over:
    print_mines_layout()

このコードでは、グリッド内のすべての可能なマスから乱数を選択します。

この乱数を、指定された地雷の数が得られるまで続けます。

注:実際の地雷の値は-1であり、表示用に保存された値は'M'と表記される。

>
注意: ‘randint’関数はランダムライブラリをインポートした後でのみ使用可能です。

プログラムの最初に 'import random' と記述することで使用できます。

グリッド番号の設定

グリッド内の各セルについて、地雷が存在するかどうか、隣接するすべてのセルをチェックする必要があります。

これは次のようにして行う。

# Input from the user
inp = input("Enter row number followed by space and column number = ").split()

これらの値はプレイヤーから隠蔽されるため、変数 numbers に格納される。

ゲームループ

ゲームループは、ゲームにおいて非常に重要な部分です。

プレイヤーの一挙手一投足と、ゲームの結末を更新するために必要です。

# Standard Move
if len(inp) == 2:
 
    # Try block to handle errant input
    try:
        val = list(map(int, inp))
    except ValueError:
        clear()
        print("Wrong input!")
        instructions()
        continue

ループの各反復で、マインスイーパーのグリッドが表示され、プレイヤーの動きが処理されなければならない。

プレイヤーの入力を処理する

前述したように、プレーヤーの入力には2種類あります。

# Flag Input
elif len(inp) == 3:
    if inp[2] != 'F' and inp[2] != 'f':
        clear()
        print("Wrong Input!")
        instructions()
        continue
 
    # Try block to handle errant input 
    try:
        val = list(map(int, inp[:2]))
    except ValueError:
        clear()
        print("Wrong input!")
        instructions()
        continue

標準入力

通常の手筋では、行番号と列番号が記載されている。

地雷のないマスのロックを解除するのが目的。

# Sanity checks
if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
    clear()
    print("Wrong Input!")
    instructions()
    continue
 
# Get row and column numbers
r = val[0]-1
col = val[1]-1

フラグ入力

フラッグ技では、ゲーマーから3つの値が送信されます。

最初の2つの値はセルの位置を表し、最後の1つはフラグを立てることを表す。

# If cell already been flagged
if [r, col] in flags:
    clear()
    print("Flag already set")
    continue
 
# If cell already been displayed
if mine_values[r][col] != ' ':
    clear()
    print("Value already known")
    continue
 
# Check the number for flags   
if len(flags) < mines_no:
    clear()
    print("Flag set")
 
    # Adding flag to the list
    flags.append([r, col])
     
    # Set the flag for display
    mine_values[r][col] = 'F'
    continue
else:
    clear()
    print("Flags finished")
    continue    

入力のサニタイズ

入力を保存した後、ゲームをスムーズに動作させるために、いくつかのサニタイズチェックを行う必要があります。

# If landing on a mine --- GAME OVER   
if numbers[r][col] == -1:
    mine_values[r][col] = 'M'
    show_mines()
    print_mines_layout()
    print("Landed on a mine. GAME OVER!!!!!")
    over = True
    continue

入力処理が完了したら、行番号と列番号を抽出して 'r''c' に格納します。

フラグ入力の処理

フラグ入力を管理することは大きな問題ではありません。

地雷のセルにフラグを立てる前に、いくつかの前提条件をチェックする必要があります。


以下のチェックが必要です。

  • そのセルはすでにフラグが立っているかどうか。
  • フラグを立てるべきセルが既にプレイヤーに表示されているかどうか。
  • フラグの数が地雷の数を超えていないか。

フラグの数が地雷の数を超えていないか これらの点を確認した上で、そのセルに地雷フラグを立てます。

def show_mines():
    global mine_values
    global numbers
    global n
 
    for r in range(n):
        for col in range(n):
            if numbers[r][col] == -1:
                mine_values[r][col] = 'M'

標準入力を処理する

標準入力は、ゲーム全体の機能に関わるものです。

3つの異なるシナリオがあります。

地雷にアンカーを打つ

地雷のあるマスを選択した時点でゲーム終了となります。

運が悪かったり、判断が鈍かったりすると発生することがあります。

# If landing on a cell with 0 mines in neighboring cells
elif numbers[r][n] == 0:
    vis = []
    mine_values[r][n] = '0'
    neighbours(r, col) 

地雷のあるマスに着地したら、ゲーム内のすべての地雷を表示し、ゲームループの後ろにある変数を変更する必要があります。

関数 'show_mines()' がその役割を担っています。

def neighbours(r, col):
     
    global mine_values
    global numbers
    global vis
 
    # If the cell already not visited
    if [r,col] not in vis:
 
        # Mark the cell visited
        vis.append([r,col])
 
        # If the cell is zero-valued
        if numbers[r][col] == 0:
 
            # Display it to the user
            mine_values[r][col] = numbers[r][col]
 
            # Recursive calls for the neighbouring cells
            if r > 0:
                neighbours(r-1, col)
            if r < n-1:
                neighbours(r+1, col)
            if col > 0:
                neighbours(r, col-1)
            if col < n-1:
                neighbours(r, col+1)   
            if r > 0 and col > 0:
                neighbours(r-1, col-1)
            if r > 0 and col < n-1:
                neighbours(r-1, col+1)
            if r < n-1 and col > 0:
                neighbours(r+1, col-1)
            if r < n-1 and col < n-1:
                neighbours(r+1, col+1
                 
        # If the cell is not zero-valued           
        if numbers[r][col] != 0:
                mine_values[r][col] = numbers[r][col]

0’値のセルを訪問します。

ゲームを作る上で最も厄介なのは、このシナリオを管理することです。

ゲーマーが「0」値のセルにアクセスするたびに、「0」値でないセルに到達するまで、隣接するすべての要素を表示しなければならない。

# If selecting a cell with atleast 1 mine in neighboring cells 
else:  
    mine_values[r][col] = numbers[r][col]

この目的を達成するために、Recursionを使用します。

再帰は、基本ケースを満たすまで関数が自分自身を呼び出すプログラミングツールです。

Neighbours` 関数は再帰的な関数であり、我々の問題を解決します。

# Check for game completion
if(check_over()):
    show_mines()
    print_mines_layout()
    print("Congratulations!!! YOU WIN")
    over = True
    continue

このゲームの特殊なコンセプトのために、新しいデータ構造、すなわち vis が使用される。

vis` の役割は、再帰の間に既に訪問したセルを追跡することです。

この情報がなければ、再帰は永久に続く。

値がゼロのセルとその近傍がすべて表示されたら、最後のシナリオに進むことができる。

ゼロ値でないセルの選択

この場合は、表示値を変更するだけなので、特に工夫は必要ありません。

# Function to check for completion of the game
def check_over():
    global mine_values
    global n
    global mines_no
 
    # Count of all numbered values
    count = 0
 
    # Loop for checking each cell in the grid
    for r in range(n):
        for col in range(n):
 
            # If cell not empty or flagged
            if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
                count = count + 1
     
    # Count comparison         
    if count == n * n - mines_no:
        return True
    else:
        return False

エンドゲーム

対局が終了したことを確認するために、対局が行われるたびに確認する必要があります。

これは以下の方法で行われる。

# Function for clearing the terminal
def clear():
    os.system("clear")

関数 check_over() は、ゲームの終了をチェックする責任を負う。

# Importing packages
import random
import os
 
# Printing the Minesweeper Layout
def print_mines_layout():
 
    global mine_values
    global n
 
    print()
    print(" MINESWEEPER
"
)
 
    st = "   "
    for i in range(n):
        st = st + "     " + str(i + 1)
    print(st)  
 
    for r in range(n):
        st = "     "
        if r == 0:
            for col in range(n):
                st = st + "______" 
            print(st)
 
        st = "     "
        for col in range(n):
            st = st + "|     "
        print(st + "|")
         
        st = "  " + str(r + 1) + "  "
        for col in range(n):
            st = st + "|  " + str(mine_values[r][col]) + "  "
        print(st + "|")
 
        st = "     "
        for col in range(n):
            st = st + "|_____"
        print(st + '|')
 
    print()
  
# Function for setting up Mines
def set_mines():
 
    global numbers
    global mines_no
    global n
 
    # Track of number of mines already set up
    count = 0
    while count < mines_no:
 
        # Random number from all possible grid positions
        val = random.randint(0, n*n-1)
 
        # Generating row and column from the number
        r = val // n
        col = val % n
 
        # Place the mine, if it doesn't already have one
        if numbers[r][col] != -1:
            count = count + 1
            numbers[r][col] = -1
 
# Function for setting up the other grid values
def set_values():
 
    global numbers
    global n
 
    # Loop for counting each cell value
    for r in range(n):
        for col in range(n):
 
            # Skip, if it contains a mine
            if numbers[r][col] == -1:
                continue
 
            # Check up 
            if r > 0 and numbers[r-1][col] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check down   
            if r < n-1  and numbers[r+1][col] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check left
            if col > 0 and numbers[r][col-1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check right
            if col < n-1 and numbers[r][col+1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check top-left   
            if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check top-right
            if r > 0 and col < n-1 and numbers[r-1][col+1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check below-left 
            if r < n-1 and col > 0 and numbers[r+1][col-1] == -1:
                numbers[r][col] = numbers[r][col] + 1
            # Check below-right
            if r < n-1 and col < n-1 and numbers[r+1][col+1] == -1:
                numbers[r][col] = numbers[r][col] + 1
 
# Recursive function to display all zero-valued neighbours 
def neighbours(r, col):
     
    global mine_values
    global numbers
    global vis
 
    # If the cell already not visited
    if [r,col] not in vis:
 
        # Mark the cell visited
        vis.append([r,col])
 
        # If the cell is zero-valued
        if numbers[r][col] == 0:
 
            # Display it to the user
            mine_values[r][col] = numbers[r][col]
 
            # Recursive calls for the neighbouring cells
            if r > 0:
                neighbours(r-1, col)
            if r < n-1:
                neighbours(r+1, col)
            if col > 0:
                neighbours(r, col-1)
            if col < n-1:
                neighbours(r, col+1)   
            if r > 0 and col > 0:
                neighbours(r-1, col-1)
            if r > 0 and col < n-1:
                neighbours(r-1, col+1)
            if r < n-1 and col > 0:
                neighbours(r+1, col-1)
            if r < n-1 and col < n-1:
                neighbours(r+1, col+1
 
        # If the cell is not zero-valued           
        if numbers[r][col] != 0:
                mine_values[r][col] = numbers[r][col]
 
# Function for clearing the terminal
def clear():
    os.system("clear")     
 
# Function to display the instructions
def instructions():
    print("Instructions:")
    print("1. Enter row and column number to select a cell, Example "2 3"")
    print("2. In order to flag a mine, enter F after row and column numbers, Example "2 3 F"")
 
# Function to check for completion of the game
def check_over():
    global mine_values
    global n
    global mines_no
 
    # Count of all numbered values
    count = 0
 
    # Loop for checking each cell in the grid
    for r in range(n):
        for col in range(n):
 
            # If cell not empty or flagged
            if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
                count = count + 1
     
    # Count comparison         
    if count == n * n - mines_no:
        return True
    else:
        return False
 
# Display all the mine locations                   
def show_mines():
    global mine_values
    global numbers
    global n
 
    for r in range(n):
        for col in range(n):
            if numbers[r][col] == -1:
                mine_values[r][col] = 'M'
 
 
if __name__ == "__main__":
 
    # Size of grid
    n = 8
    # Number of mines
    mines_no = 8
 
    # The actual values of the grid
    numbers = [[0 for y in range(n)] for x in range(n)]
    # The apparent values of the grid
    mine_values = [[' ' for y in range(n)] for x in range(n)]
    # The positions that have been flagged
    flags = []
 
    # Set the mines
    set_mines()
 
    # Set the values
    set_values()
 
    # Display the instructions
    instructions()
 
    # Variable for maintaining Game Loop
    over = False
         
    # The GAME LOOP
    while not over:
        print_mines_layout()
 
        # Input from the user
        inp = input("Enter row number followed by space and column number = ").split()
         
        # Standard input
        if len(inp) == 2:
 
            # Try block to handle errant input
            try:
                val = list(map(int, inp))
            except ValueError:
                clear()
                print("Wrong input!")
                instructions()
                continue
 
        # Flag input
        elif len(inp) == 3:
            if inp[2] != 'F' and inp[2] != 'f':
                clear()
                print("Wrong Input!")
                instructions()
                continue
 
            # Try block to handle errant input 
            try:
                val = list(map(int, inp[:2]))
            except ValueError:
                clear()
                print("Wrong input!")
                instructions()
                continue
 
            # Sanity checks
            if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
                clear()
                print("Wrong input!")
                instructions()
                continue
 
            # Get row and column numbers
            r = val[0]-1
            col = val[1]-1 
 
            # If cell already been flagged
            if [r, col] in flags:
                clear()
                print("Flag already set")
                continue
 
            # If cell already been displayed
            if mine_values[r][col] != ' ':
                clear()
                print("Value already known")
                continue
 
            # Check the number for flags   
            if len(flags) < mines_no:
                clear()
                print("Flag set")
 
                # Adding flag to the list
                flags.append([r, col])
                 
                # Set the flag for display
                mine_values[r][col] = 'F'
                continue
            else:
                clear()
                print("Flags finished")
                continue    
 
        else:
            clear()
            print("Wrong input!")  
            instructions()
            continue
             
 
        # Sanity checks
        if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
            clear()
            print("Wrong Input!")
            instructions()
            continue
             
        # Get row and column number
        r = val[0]-1
        col = val[1]-1
 
        # Unflag the cell if already flagged
        if [r, col] in flags:
            flags.remove([r, col])
 
        # If landing on a mine --- GAME OVER   
        if numbers[r][col] == -1:
            mine_values[r][col] = 'M'
            show_mines()
            print_mines_layout()
            print("Landed on a mine. GAME OVER!!!!!")
            over = True
            continue
 
        # If landing on a cell with 0 mines in neighboring cells
        elif numbers[r][col] == 0:
            vis = []
            mine_values[r][col] = '0'
            neighbours(r, col)
 
        # If selecting a cell with atleast 1 mine in neighboring cells 
        else:  
            mine_values[r][col] = numbers[r][col]
 
        # Check for game completion
        if(check_over()):
            show_mines()
            print_mines_layout()
            print("Congratulations!!! YOU WIN")
            over = True
            continue
        clear()

空でもフラグでもないセルの数を数える。

この数が地雷のないマスの総数と等しいとき、ゲームは終了したとみなされる。

一手ごとに出力をクリアする

端末に印刷し続けると端末が混雑します。

そのため、常に出力をクリアできるようにする必要があります。

これは次のようにして行うことができます。

Minesweeper Layout
Minesweeper Layout


Note: この機能を使用する前に os ライブラリをインポートする必要があります。

これはプログラムの最初に 'import os' とすることで可能です。

完全なコード

以下は、マインスイーパの完全なコードです。

Minesweeper Instructions
Minesweeper Instructions

まとめ

この記事では、マインスイーパの作り方を説明しましたが、いかがだったでしょうか?何か疑問があれば、下のコメント欄からお気軽にどうぞ。

完全なコードは、私のGithubアカウントでも公開されています。

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