2023.8.18更新:有人给我建议把它放到github上,于是我就这么做了。项目地址在 https://github.com/Kide-Lee/Bash-Minesweeper ,欢迎各位给我提issue或给我星星哦。

怎么玩

载入脚本后,用WASD键控制光标移动,按空格挖开地块,挖到的数字是地块周围的地雷数量,挖到地雷后游戏失败;
按F标记有地雷的地块,按E表示可能有地雷。已挖开的地块无法被标记。将所有地雷标记完毕后游戏胜利。
按Q键退出游戏。无论如何退出游戏,脚本都会总结扫到雷的数量和本局游戏的时间。

在CentOS 7上运行脚本

CentOS 7上的bash版本太低,无法解释脚本中的某些语法。因此我们需要升级CenOS 7上的bash解释器。具体而言,我们需要依次执行如下命令:

1
2
3
4
wget http://ftp.gnu.org/gnu/bash/bash-5.2.15.tar.gz
tar zxvf bash-5.2.15.tar.gz
cd bash-5.2.15
./configure && make && make install

假如电脑上没有C语言编译器,最后一条命令会报错;此时我们只要先yum install gcc,再去执行那条命令就好。

编译结束后,重启电脑以使新bash生效。

最后我们还要在/bin目录下添加新bash的软链接,然后重启,才能使我们的bash命令焕然一新:

1
2
3
mv /bin/bash /bin/bash.bak
ln -s /usr/local/bin/bash /bin/bash
reboot

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
#!/bin/bash
# Author: Kide_Lee
# Date: 2023.8.15
# Blog: https://www.cnblogs.com/-zyyz-/

# ========================================
# 用户设置
# 扫雷游戏中常见的难度设置:
# * 容易:9×9大小,10颗地雷;
# * 中等:16×16大小,40颗地雷;
# * 困难:30×16大小,99颗地雷。
# ========================================
width=9 # 界面的宽度
height=9 # 界面的高度
mineCount=10 # 地雷数量
mine="#" # 地雷样式
title="MineSweeper"

# ========================================
# 方法
# 用一维数组模拟一张二维表;
# 下面提供若干方法,使脚本能够通过查询二维坐标的方式访问数组;
# 其中横纵坐标按书写方向,从零开始计数。
# ========================================
function ReadTable() {
local x=$2
local y=$3

# matrix和field是脚本维护的两张表,
# 其分别记录了雷区信息和用户界面。
case "$1" in
matrix) local info=("${matrix[@]}") ;;
field) local info=("${field[@]}") ;;
esac

# 若读取的坐标越界或没有值,则返回0;否则返回坐标对应值。
local result=${info[$(("$y" * "$width" + "$x"))]}
if [ -z "$result" ]; then return 0;
elif [ "$x" -lt 0 ]; then return 0;
elif [ "$y" -lt 0 ]; then return 0;
elif [ "$x" -ge $width ]; then return 0;
elif [ "$y" -ge $height ]; then return 0;
else return "$result";
fi
}
function WriteTable() {
local x=$2
local y=$3
local new=$4

# 若读取的坐标越界,则拒绝执行函数。
if [ "$x" -lt 0 ]; then return 0;
elif [ "$y" -lt 0 ]; then return 0;
elif [ "$x" -ge $width ]; then return 0;
elif [ "$y" -ge $height ]; then return 0;
fi

if [ "$1" == "matrix" ]; then
matrix["$y" * "$width" + "$x"]=$new
elif [ "$1" == "field" ]; then
field["$y" * "$width" + "$x"]=$new
fi
}

# ========================================
# 初始化
# matrix和field中储存的数字含义是一致的,
# 其中0-8代表这个格子周围的地雷数量;
# 9代表这个格子中埋藏了地雷;
# 10代表未检查过的格子;
# 11代表格子未检查,且被玩家标记,代表他认为这个格子下面有地雷;
# 12代表格子未检查,且被玩家标记,代表他不确定格子下面是否有地雷。
# ========================================
# 主函数
function StartGame() {
# X和Y是模拟光标所在的坐标,即所谓的焦点;
# 同时初始化old_x和old_Y,以供DrawFocus函数使用;
# 并隐藏真实光标。
X=$(("$width" / 2))
Y=$(("$height" / 2))
old_X=$X
old_Y=$Y
size=$(("$width" * "$height"))
tput civis
# 生成雷区
# 此时还未开始布雷,所以雷区用0填充。
for ((i = 0; i < "$size"; i++)); do
matrix+=("0")
done
# 生成界面
# 此时还未开始检查格子,所以用户界面用10填充。
for ((i = 0; i < "$size"; i++)); do
field+=("10")
done
DrawField
# 等待用户翻开第一个格子,然后进行初始化。
while [ -z "$checkFirst" ]; do
DrawFocus $X $Y
# IFS是定义分割符的环境变量,
# 这里暂时将其设为空值,以确保read命令能够读到空格。
IFS_back=$IFS
IFS=
read -srn1 input
IFS=$IFS_back
case "$input" in
w|W) MoveUp ;;
a|A) MoveLeft ;;
s|S) MoveDown ;;
d|D) MoveRight ;;
f|F) MarkMatrix $X $Y 11 ;; # 标记可能埋藏有地雷的格子。
e|E) MarkMatrix $X $Y 12 ;; # 标记不确定是否埋藏了地雷的格子。
q|Q) return 1 ;; # 退出游戏。
" ")
CreateMine
CreateNumber
CheckMatrix $X $Y # 检查格子。
local checkFirst="yes"
startTime=$(date +%s) # 准备就绪后开始计时。
;;
esac
done
}
# 绘制界面
function DrawField() {
# 绘制标题栏
tput clear
local fieldWidth=$(("$width" * 3 + 2))
local titleWidth=${#title}
if [ "$fieldWidth" -gt "$titleWidth" ]; then
tput cup 0 $(("$fieldWidth" / 2 - "$titleWidth" / 2))
echo $title
else
tput cup 0 0
echo $title
fi
# 绘制上边框
echo -n +
for ((i = 0; i < "$width"; i++)); do
echo -n "---"
done
echo +
# 绘制游戏区域
for ((i = 0; i < "$height"; i++)); do
echo -n "|"
for ((j = 0; j < "$width"; j++)); do
echo -n "[ ]"
done
echo "|"
done
# 绘制下边框
echo -n +
for ((i = 0; i < "$width"; i++)); do
echo -n "---"
done
echo +
# 增加游戏提示
echo "move: WASD check: Space"
echo "mark: F question: E quit: Q"
}
# 铺设地雷
function CreateMine() {
# 定义安全区,保证首次排雷安全
local safeFieldList=()
for x in $X-1 $X $X+1; do
for y in $Y-1 $Y $Y+1; do
x=$(("$x"))
y=$(("$y"))
local safeNum=$(("$y" * "$width" + "$x"))
if [ $safeNum -ge 0 ] || [ $safeNum -lt "$size" ]; then
safeFieldList+=("$safeNum")
fi
done
done
# 定义地雷,本质是一串互不重复的随机数
local mineList=()
while [ ${#mineList[*]} -lt $mineCount ]; do
local random=$(("$RANDOM" % "$size"))
local isRepeat=false
for i in "${mineList[@]}" "${safeFieldList[@]}"; do
if [ $random == "$i" ]; then
isRepeat=true
break
fi
done

if [ $isRepeat == false ]; then
mineList+=("$random")
fi
done
# 布置地雷
# 在雷区中,9代表有地雷
for i in "${mineList[@]}"; do
matrix["$i"]=9
done
}
# 生成数字
function CreateNumber() {
function PutOne() {
local x=$1
local y=$2
for i in $x-1 $x $x+1; do
for j in $y-1 $y $y+1; do
local i=$(("$i"))
local j=$(("$j"))
ReadTable matrix $i $j
tmpNum=$?
if [ $tmpNum != 9 ]; then
WriteTable matrix $i $j $(("$tmpNum" + 1))
fi
done
done
}
# 遇到地雷,非地雷邻格的数字+1。
for ((y = 0; y < "$height"; y++)); do
for ((x = 0; x < "$width"; x++)); do
ReadTable matrix $x $y
if [ $? == 9 ]; then
PutOne $x $y
fi
done
done
}

# ========================================
# 游戏过程
# ========================================
# 主函数
function PlayGame() {
while true; do
DrawFocus $X $Y
# IFS是定义分割符的环境变量,
# 这里暂时将其设为空值,以确保read命令能够读取到空格。
IFS_back=$IFS
IFS=
read -srn1 input
IFS=$IFS_back
case "$input" in
w|W) MoveUp ;;
a|A) MoveLeft ;;
s|S) MoveDown ;;
d|D) MoveRight ;;
f|F) MarkMatrix $X $Y 11 ;; # 标记可能埋藏有地雷的格子。
e|E) MarkMatrix $X $Y 12 ;; # 标记不确定是否埋藏了地雷的格子。
q|Q) return 1 ;; # 退出游戏。
" ")
CheckMatrix $X $Y # 检查格子
if [ $? == 9 ]; then # 检查到地雷后,终端响铃,游戏失败。
tput bel
return 2
fi
;;
esac
Judging # 裁决是否赢得本场游戏
if [ $? == 2 ]; then
return 3
fi
done
}
# 检查雷区
function CheckCell() {
local x=$1
local y=$2
ReadTable field "$x" "$y"
local result=$?
# 若格子未被检查,则进行检查,并绘制这个格子检查后的样子;
if [ $result -ge 10 ]; then
ReadTable matrix "$x" "$y"
local result=$?
WriteTable field "$x" "$y" "$result"
Draw "$x" "$y"
fi
return "$result"
}
# 递归检查雷区
function CheckMatrix() {
local x_1=$1
local y_1=$2
# 这个“edge”指空区的边缘
# 边缘数组中边缘格的横纵坐标成对存储,
# edgeLength是边缘数组的长度。
local edgeList=("$x_1" "$y_1")
local edgeLength=2
while [ "$edgeLength" -gt 0 ]; do
# 读取边缘坐标,并从边缘数组中删除掉它。
local x=${edgeList[$edgeLength - 2]}
local y=${edgeList[$edgeLength - 1]}
unset "edgeList[$edgeLength-2]"
unset "edgeList[$edgeLength-1]"
# 检查边缘格,若边缘格四周无地雷,则将边缘格四周未检查的地方归为边缘格。
CheckCell "$x" "$y"
local result=$?
if [ $result == 0 ]; then
for i in $x-1 $x $x+1; do
for j in $y-1 $y $y+1; do
local i=$(("$i"))
local j=$(("$j"))
ReadTable field $i $j
if [ $? -gt 9 ]; then
edgeList+=("$i" "$j")
fi
done
done
fi
# 读取边缘数组的长度,若边缘数组被抽空,则停止循环。
local edgeLength=${#edgeList[@]}
done
# 将第一个格子的值作为返回值
ReadTable field "$x_1" "$y_1"
return $?
}
# 渲染
# 本函数同时仅能渲染一个格子。
function Draw() {
local x=$1
local y=$2
# 读取field表,根据读到的值渲染格子。
# 这里默认玩家终端所用的是等宽字体,并将一个格子设为三个字符的宽度。
ReadTable field "$x" "$y"
local result=$?
case "$result" in
0) display=" " ;; # 格子四周无雷,则不对其渲染;
9) display="$(tput setaf 1) ${mine} $(tput sgr0)" ;; # 地雷标红,以示警告;
10) display="[ ]" ;; # 未检查过的格子用方括号括起来;
11) display="$(tput setaf 2)[F]$(tput sgr0)" ;; # 方括号内显示对格子的标记;
12) display="$(tput setaf 3)[?]$(tput sgr0)" ;; # 对于数字,对tput的颜色代码反序,以免和非数字格的颜色相重复。
*) display="$(tput setaf $((8 - "$result"))) ${result} $(tput sgr0)" ;;
esac
# 光标移到屏幕上的相应位置,并输出合适的内容。
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "$(tput sgr0)${display}"
}
# 渲染焦点
# “焦点”就是游戏过程中的模拟光标。
function DrawFocus() {
local x=$1
local y=$2
# 重新渲染旧焦点,来保证屏幕上只有一个焦点。
Draw "$old_X" "$old_Y"
# 对焦点反色,以突出焦点所在的位置。
Draw "$x" "$y"
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "$(tput rev)${display}"
# 在下个焦点生成之前,定义好旧焦点。
old_X=$1
old_Y=$2
}
# 标记格子
function MarkMatrix() {
local x=$1
local y=$2
local mark=$3
ReadTable field "$x" "$y"
local fieldChar=$?
# 若将要打上的标记和旧标记一致,则取消标记;
if [ $fieldChar == "$mark" ]; then
WriteTable field "$x" "$y" 10
# 对未检查过的格子进行标记。
elif [ $fieldChar -gt 9 ]; then
WriteTable field "$x" "$y" "$mark"
fi
}
# 焦点的移动
# 当焦点越界时,让焦点进入界面的另一侧。
function MoveUp() {
Y=$(("$Y" - 1))
if [ $Y -lt 0 ]; then
Y=$(("$Y" + "$height"))
fi
}
function MoveDown() {
Y=$(("$Y" + 1))
if [ $Y -ge $height ]; then
Y=$(("$Y" - "$height"))
fi
}
function MoveLeft() {
X=$(("$X" - 1))
if [ $X -lt 0 ]; then
X=$(("$X" + "$width"))
fi
}
function MoveRight() {
X=$(("$X" + 1))
if [ $X -ge $width ]; then
X=$(("$X" - "$width"))
fi
}
# 裁判是否胜利
# 当场上所有未检查的格子均被标记,且它们的数量等于地雷数量时,判胜。
function Judging() {
local markNum=0
for i in "${field[@]}"; do
if [ "$i" == 10 ] || [ "$i" == 12 ]; then
return 1
elif [ "$i" == 11 ]; then
markNum=$(("$markNum"+1))
fi
done
if [ "$markNum" == "$mineCount" ]; then
return 2
fi
}

# ========================================
# 游戏清算
# ========================================
function ClearGame() {
# 结束计时。若游戏未曾初始化,则将时间计为0秒。
endTime=$(date +%s)
if [ "$startTime" ]; then
time=$(("$endTime" - "$startTime"))
else
time=0
fi
# 清除焦点
Draw $X $Y
# 将标记正确的格子数量mineSweeper初始化为0。
mineSweeper=0
if [ $result == 1 ]; then #若退出游戏
for ((i = 0; i < ${#matrix[@]}; i++)); do
x=$(("$i" % "$width"))
y=$(("$i" / "$height"))
if [ "${matrix[$i]}" == 9 ]; then
# 将标记正确的格子染成绿色,并计数。
if [ "${field[$i]}" == 11 ]; then
mineSweeper=$(("$mineSweeper" + 1))
Draw "$x" "$y"
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "$(tput rev)$(tput setaf 2)[F]$(tput sgr0)"
# 显示未找到的地雷
else
Draw "$x" "$y"
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "[${mine}]"
fi
fi
done
clearInfo="Game Exited.\nYou swept $mineSweeper mines in $time seconds."
elif [ $result == 2 ]; then # 若输掉游戏
for ((i = 0; i < ${#matrix[@]}; i++)); do
x=$(("$i" % "$width"))
y=$(("$i" / "$height"))
if [ "${matrix[$i]}" == 9 ]; then
# 将标记正确的格子染成绿色,并去掉方括号以示地雷已爆炸,同时计数。
if [ "${field[$i]}" == 11 ]; then
mineSweeper=$(("$mineSweeper" + 1))
Draw "$x" "$y"
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "$(tput rev)$(tput setaf 2) ${mine} $(tput sgr0)"
# 将标记错误或未标记的格子染成红色,并去掉方括号以示地雷爆炸
else
Draw "$x" "$y"
tput cup $(("$y" + 2)) $(("$x" * 3 + 1))
echo -n "$(tput rev)$(tput setaf 1) ${mine} $(tput sgr0)"
fi
fi
done
clearInfo="$(tput setaf 1)The Game Failed. \nYou swept $mineSweeper mines in $time seconds."
elif [ $result == 3 ]; then # 若赢得游戏
clearInfo="$(tput setaf 2)You Win!\nYou swept all mines in $time seconds."
fi
# 输出结语,并恢复光标。
tput cnorm
tput cup $(("$height" + 3)) 0
tput el
echo -e "$(tput rev)${clearInfo}$(tput sgr0)"
}

# ========================================
# 游戏运行
# 游戏初始化成功后再进行游戏。
# ========================================
StartGame
result=$?
if [ $result == 0 ]; then
PlayGame
result=$?
fi
ClearGame