今回は、Python言語を使って、端末ベースのマインスイーパーを自作する手順を説明します。
ゲームについて
Minesweeperは、地雷と数字を含む正方形のグリッドをクリアするシングルプレイヤー・ゲームです。
プレイヤーは、隣のタイルにある数字を使って、地雷に着地するのを防がなければなりません。
ゲームプレイデモ
マインスイーパのゲームを数時間かけて作った余韻。
マインスイーパデモ
Pythonを使ったマインスイーパの設計
ゲームロジックを作る前に、ゲームの基本的なレイアウトを設計する必要があります。
正方形のグリッドはPythonで簡単に作成することができます。
# Printing the Minesweeper Layoutdef 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 Minesdef 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 valuesdef 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 minesset_mines()# Set the valuesset_values()# Display the instructionsinstructions()# Variable for maintaining Game Loopover = False
# The GAME LOOP while not over:
print_mines_layout()
|
このコードでは、グリッド内のすべての可能なマスから乱数を選択します。
この乱数を、指定された地雷の数が得られるまで続けます。
注:実際の地雷の値は-1であり、表示用に保存された値は'M'と表記される。
>
注意: ‘randint’関数はランダムライブラリをインポートした後でのみ使用可能です。
プログラムの最初に 'import random' と記述することで使用できます。
この記事もチェック:Pythonのrandint関数の使い方|引数や範囲指定のやり方を解説
グリッド番号の設定
グリッド内の各セルについて、地雷が存在するかどうか、隣接するすべてのセルをチェックする必要があります。
これは次のようにして行う。
# Input from the userinp = input("Enter row number followed by space and column number = ").split()
|
これらの値はプレイヤーから隠蔽されるため、変数 numbers に格納される。
ゲームループ
ゲームループは、ゲームにおいて非常に重要な部分です。
プレイヤーの一挙手一投足と、ゲームの結末を更新するために必要です。
# Standard Moveif 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 Inputelif 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 checksif 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 numbersr = val[0]-1
col = val[1]-1
|
フラグ入力
フラッグ技では、ゲーマーから3つの値が送信されます。
最初の2つの値はセルの位置を表し、最後の1つはフラグを立てることを表す。
# If cell already been flaggedif [r, col] in flags:
clear()
print("Flag already set")
continue
# If cell already been displayedif 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 cellselif 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 gamedef 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 terminaldef clear():
os.system("clear")
|
関数 check_over() は、ゲームの終了をチェックする責任を負う。
# Importing packagesimport random
import os
# Printing the Minesweeper Layoutdef 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 Minesdef 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 valuesdef 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 terminaldef clear():
os.system("clear")
# Function to display the instructionsdef 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 gamedef 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()
|
空でもフラグでもないセルの数を数える。
この数が地雷のないマスの総数と等しいとき、ゲームは終了したとみなされる。
一手ごとに出力をクリアする
端末に印刷し続けると端末が混雑します。
そのため、常に出力をクリアできるようにする必要があります。
これは次のようにして行うことができます。

Note: この機能を使用する前に os ライブラリをインポートする必要があります。
これはプログラムの最初に 'import os' とすることで可能です。
完全なコード
以下は、マインスイーパの完全なコードです。

まとめ
この記事では、マインスイーパの作り方を説明しましたが、いかがだったでしょうか?何か疑問があれば、下のコメント欄からお気軽にどうぞ。
完全なコードは、私のGithubアカウントでも公開されています。