在本文中,我们将介绍如何使用 Python 语言创建自己的基于终端的扫雷程序。
关于游戏
1992年4月6日,扫雷和纸牌、空当接龙等小游戏搭载在Windows 3.1系统中与用户见面,主要目的是让用户训练使用鼠标。扫雷是一款单人游戏,这个游戏的玩法很简单,有初级、中级、高级和自定义等模式,雷区中随机布置一定数量的地雷,玩家需要清除一个包含地雷和数字的正方形网格。玩家需要借助相邻方格中的数字来防止自己落在地雷上,但不许踩到地雷。
使用 Python 设计扫雷游戏
在创建游戏逻辑之前,我们需要设计游戏的基本布局。使用 Python 创建正方形网格非常容易:
# Printing the Minesweeper Layout
def print_mines_layout():
global mine_values
global n
print()
print("\t\t\tMINESWEEPER\n")
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()
每次迭代显示的网格如下图所示:
M "符号表示该单元格中存在 “地雷”。我们可以清楚地看到,网格上的任何数字都表示相邻 "8 "单元格中存在的地雷数量。
本教程将进一步解释如何使用 mine_values 等变量。
输入系统
任何游戏最重要的部分之一就是输入法。在我们的扫雷版本中,我们将使用行数和列数作为输入技术。
在开始游戏之前,脚本必须为玩家提供一组指令。我们的游戏会打印以下内容。
与网格一起显示的行数和列数对我们的输入系统很有帮助。我们知道,在没有任何指示器的情况下追踪地雷是很困难的。因此,扫雷游戏提供了一种使用 "标志 "来标记我们已知含有地雷的单元格的方法。
数据存储
对于一局扫雷游戏,我们需要记录以下信息:
- 网格大小
- 地雷数量
- 实际 "网格值——在游戏开始时,我们需要一个容器来存储玩家未知的游戏实际值。例如,地雷的位置。
- 表面 "网格值 - 每次移动后,我们都需要更新所有必须显示给玩家的值。
- 标记位置 - 已标记的单元格。
这些值通过以下数据结构存储
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 = []
扫雷的游戏逻辑并不复杂。所有的努力都是为了设置扫雷布局。
设置地雷
我们需要随机设置地雷的位置,这样玩家就无法预测它们的位置。这可以通过以下方法实现
# 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,而为显示而存储的值则表示地雷为 “M”。
注意:"randint "函数只能在导入随机库后使用。在程序开始时写入 "import random "即可。
设置网格编号
对于网格中的每个单元格,我们必须检查所有相邻单元格是否存在地雷。具体方法如下
# 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()
在循环的每次迭代中,都必须显示扫雷网格并处理玩家的移动。
处理玩家输入
正如我们之前提到的,有两种玩家输入:
# Input from the user
inp = input("Enter row number followed by space and column number = ").split()
标准输入
在普通移动中,会提到行和列的编号。玩家此举的动机是解锁一个没有地雷的单元格。
# 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
旗子输入
在插旗动作中,游戏者会输入三个数值。前两个值表示小区位置,最后一个值表示插旗。
# 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
输入过程完成后,行号和列号将被提取并存储在 "r "和 "c "中。
处理标志输入
管理标记输入并不是一个大问题。在标记单元格为地雷之前,需要检查一些先决条件。
必须进行以下检查:
- 单元格是否已被标记。
- 要标记的单元是否已经显示给玩家。
- 标记数量不超过地雷数量。
处理完这些问题后,该单元就会被标记为地雷。
# 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
当我们降落到有地雷的单元格后,我们需要显示游戏中的所有地雷,并改变游戏循环后面的变量。
函数 "show_mines() "负责执行此操作。
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'
访问 "0 "值单元格。
创建游戏最棘手的部分就是管理这种情况。每当游戏者访问一个 "0 "值单元格时,所有相邻的元素都必须显示出来,直到访问到一个非零值单元格为止。
# 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)
这一目标可以通过递归来实现。递归是一种编程工具,其中的函数会调用自身,直到基本情况得到满足。相邻函数就是一个递归函数,它解决了我们的问题。
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]
针对游戏的这一特殊概念,我们使用了一种新的数据结构,即 vis。vis 的作用是在递归过程中跟踪已访问过的单元格。如果没有这些信息,递归将永远持续下去。
在显示所有零值单元格及其相邻单元格后,我们就可以进入最后一个场景了。
选择非零值单元格
处理这种情况无需费力,因为我们只需更改显示值即可。
# 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
函数 check_over()负责检查游戏是否结束。
# 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")
完整代码
以下是扫雷游戏的完整代码:
# Importing packages
import random
import os
# Printing the Minesweeper Layout
def print_mines_layout():
global mine_values
global n
print()
print("\t\t\tMINESWEEPER\n")
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()
结论
我们希望本教程能让大家明白如何创建自己的扫雷游戏,并从中获得乐趣。如有任何疑问,欢迎在下方评论。