Pythonでshellで遊べるマインスイーパーを作ってみた

プログラミングの授業で作ったマインスイーパのコードです。

コード

コード
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
import string
import random
import time
from functools import wraps


##### Field #####
class Field:
"""フィールドクラス
インスタンス変数
-----------------------------------
h :(縦) ~99
w :(横) ~52 (a-z, A-Z)
mine :(地雷の数) ~h * w
field :フィールドを表す二次元配列
answer :答えを表す二次元配列
mask :ゲーム中に表示する二次元配列
mine_xy :地雷の座標、(0~8:周囲の地雷の数, 9:地雷)
AROUND :マスの上下左右、斜めの8方向を示す相対座標
-----------------------------------
"""

def __init__(self, h, w, mine):
if h > 99 or w > 52 or mine > h * w:
raise ValueError("\n h :(縦) ~99\n w :(横) ~52 (a-z, A-Z)\n mine :(地雷の数) ~h * w")

self.h, self.w = h, w # フィールドの縦、横
self.mine = mine # 地雷の数
self.field = [[0 for _ in range(w)] for _ in range(h)] # フィールド
self.answer = [[0 for _ in range(w)] for _ in range(h)] # 正解
self.mask = [[" " for _ in range(w)] for _ in range(h)] # 表示用
self.mine_xy = [] # 地雷の座標を保存

self.AROUND = [(-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1)]

def is_in_field(self, x, y):
"""与えられた(x, y)がフィールドのなかにあるかどうかをチェックする"""
if 0 <= x < self.h and 0 <= y < self.w:
return True
return False

def check_xy(func):
"""与えられた(x, y)が正しい値かどうかをチェック
他の関数をラップすることで、引数をチェックする"""
@wraps(func)
def inner(inst, x, y, *other):
if not inst.is_in_field(x, y):
raise ValueError("Coordinate out of the field.")
return func(inst, x, y, *other)
return inner

@check_xy # wrapper
def set_mines(self, x, y):
"""地雷の座標をセットする
与えられた(x, y)の座標には地雷を配置しない設定"""
first_open = self.w * x + y
flatten = list(range(first_open)) + list(range(first_open+1, self.h * self.w)) # フィールドを一次元に展開
mine_num = random.sample(flatten, self.mine) # ランダムに地雷を配置
self.mine_xy = [(num//self.w, num%self.w) for num in mine_num] # 座標の形に直す

# フィールドに格納
for x, y in self.mine_xy:
self.field[x][y] = 9 # mine: 9
self.answer[x][y] = "*" # mine: *

@check_xy
def around(self, x, y):
"""与えたマスの周りのマスを返す"""
around_cells = []
for diff_x, diff_y in self.AROUND:
x_, y_ = x + diff_x, y + diff_y
if self.is_in_field(x_, y_):
around_cells.append((x_, y_))
return around_cells

@check_xy
def count_adjasent_mines(self, x, y):
"""マスの周囲にある地雷の数をカウント"""
around_cells = self.around(x, y)
count_mines = [self.field[x][y] for x, y in around_cells].count(9) # 9で表された地雷の数を数える
return count_mines

def set_number(self):
"""フィールド全体の数字を初期化"""
for x in range(self.h):
for y in range(self.w):
if self.field[x][y] != 9:
self.field[x][y] = self.answer[x][y] = self.count_adjasent_mines(x, y)

@check_xy
def open(self, x, y):
"""幅優先探索を用いて指定したマスの周囲を展開する

Returns
-------
is_gameover: bool
開いたマスが地雷だった時にTrueを返す、その他の場合はFalseを返す
"""
queue = [(x, y)] # 周囲のマス
already = [] # すでに探索済み

while queue:
pos = x, y = queue.pop(0)
if pos in already:
continue
else:
already.append(pos)

cell = self.field[x][y]
# マスが0だったとき -> 周囲のマスも開く
if cell == 0:
queue += self.around(x, y)
self.mask[x][y] = 0
# マスが数字だったとき -> 終了
elif 1 <= cell <= 8:
self.mask[x][y] = cell
# マスが地雷だったとき -> Trueを返す
else:
self.mask[x][y] = "*"
return True
return False

def is_cleared(self):
"""クリアできたかどうかをチェックする

Returns
-------
is_cleared: bool
開いていないマスが全て地雷(9)だった場合にTrueを返し、その他の場合はFalseを返す
"""
for x in range(self.h):
for y in range(self.w):
mask_status = self.mask[x][y]
field_status = self.field[x][y]
if mask_status == " " and field_status != 9:
return False
return True

@staticmethod
def show(array):
"""二次元配列を整形して表示する"""
w = len(array[0])
print(" ", *string.ascii_letters[0:w], sep=" ")

for i, row in enumerate(array):
# print(f"{i+1:2}|", *row, sep=" ")
print(f"{i+1:2}|", " ".join(map(str, row)), f"|{i+1:2}")

print(" ", *string.ascii_letters[0:w], sep=" ")
###### ######




def start(field):
"""ゲームの初期設定を行う関数"""
field.show(field.mask) # 最初にフィールドを表示

while True:
cmd = input("> ").split()

if cmd == []:
continue
elif cmd[0] == "q":
return

try:
x_ = int(cmd[0]) - 1
y_ = string.ascii_letters.index(cmd[1])
break
except:
continue # エラーの場合は最初の入力を繰り返す

field.set_mines(x_, y_) # 地雷を設定
field.set_number() # 地雷周りの数字を設定
field.open(x_, y_) # 最初の座標を開く

if field.is_cleared():
print("\nHahahahahahaha!")
return

# 表示
print()
field.show(field.mask)

interpret(field) # 入力画面に戻す


def interpret(field):
"""入力を解釈する関数、同時に計測を行う"""
start = time.time() # タイマーをリセット

while True:
cmd = input("> ").split()

if cmd == []:
continue
elif cmd[0] == "q":
return

try:
x = int(cmd[0]) - 1
y = string.ascii_letters.index(cmd[1])

# field.open関数を実行、戻り値を受け取る
is_gameover = field.open(x, y)
is_clear = field.is_cleared()

# フィールドを表示
print()
field.show(field.mask)

except:
continue

# ゲームが終わるかを判定
if is_gameover:
print("\n!!! Game Over !!!\n")
print("[正解]".center(field.w * 2 + 2))
field.show(field.answer)
print()
return

elif is_clear:
print("\nCongratulations!")
seconds = time.time() - start # タイマーをストップ
print(f"記録:{seconds:.2f}秒\n")
return



if __name__ == "__main__":

while True:
# 初期設定
config = input("縦 横 地雷の数\n> ").split()

if config:
try:
HEIGHT, WIDTH, MINES = list( map(int, config) )
break
except:
continue
else:
HEIGHT = WIDTH = MINES = 10
break

print(f"縦: {HEIGHT}, 横: {WIDTH}, 地雷: {MINES}個\n")

myField = Field(HEIGHT, WIDTH, MINES)

start(myField) # ゲームを実行

遊び方

準備

  1. 上のコードをminesweeper.pyとして保存。
  2. ターミナルに、python minesweeper.pyと入力し実行する。

地雷の設定

1
2
縦 横 地雷の数
>

コマンドの例(縦: 15, 横: 15, 地雷の数: 30の場合)

1
2
縦 横 地雷の数
> 15 15 30

何も入力しない場合、デフォルトで縦: 10, 横: 10, 地雷の数: 10と設定される。

マスの指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
縦: 10, 横: 10, 地雷: 10個

a b c d e f g h i j
1| | 1
2| | 2
3| | 3
4| | 4
5| | 5
6| | 6
7| | 7
8| | 8
9| | 9
10| |10
a b c d e f g h i j
>

最初に開くマスを選択(開き方は毎回異なる)

数字 アルファベットの順に指定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
> 3 b

a b c d e f g h i j
1| 0 0 1 | 1
2| 0 0 1 | 2
3| 0 0 1 2 | 3
4| 0 0 0 1 2 | 4
5| 0 0 0 0 1 | 5
6| 0 0 0 0 1 1 1 1 | 6
7| 1 1 1 0 0 0 0 1 | 7
8| 1 0 0 0 0 1 | 8
9| 2 1 0 0 0 0 1 | 9
10| 1 0 0 0 0 0 1 |10
a b c d e f g h i j
>
  1. 3.を繰り返し、地雷を踏まないようにマスを開けていく

ゲーム終了

クリアした場合

クリアタイムが表示される

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    a b c d e f g h i j
1| 0 0 1 1 1 1 2 1 0 | 1
2| 0 0 1 2 2 2 1 0 | 2
3| 0 0 1 2 2 1 1 0 0 | 3
4| 0 0 0 1 2 2 1 0 0 0 | 4
5| 0 0 0 0 1 1 0 0 0 | 5
6| 0 0 0 0 1 1 1 1 1 1 | 6
7| 1 1 1 0 0 0 0 1 2 | 7
8| 2 1 0 0 0 0 1 2 | 8
9| 2 1 0 0 0 0 1 2 2 | 9
10| 1 1 0 0 0 0 0 1 1 |10
a b c d e f g h i j

Congratulations!
記録:323.65秒

ゲームオーバとなった場合

答えが表示される

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
> 2 f

a b c d e f g h i j
1| 0 0 0 0 1 1 2 | 1
2| 0 0 0 0 1 * | 2
3| 1 2 2 1 1 | 3
4| | 4
5| | 5
6| | 6
7| | 7
8| | 8
9| | 9
10| |10
a b c d e f g h i j

!!! Game Over !!!

[正解]
a b c d e f g h i j
1| 0 0 0 0 1 1 2 1 1 0 | 1
2| 0 0 0 0 1 * 3 * 1 0 | 2
3| 1 2 2 1 1 2 * 2 1 0 | 3
4| 2 * * 1 0 1 1 1 0 0 | 4
5| 2 * 3 1 1 1 1 0 0 0 | 5
6| 1 1 1 0 1 * 1 0 0 0 | 6
7| 0 0 0 0 1 1 1 0 0 0 | 7
8| 1 1 0 0 0 1 1 1 1 1 | 8
9| * 1 0 0 0 1 * 1 1 * | 9
10| 1 1 0 0 0 1 1 1 1 1 |10
a b c d e f g h i j

終了する場合はqを入力する


このページはHexoStellarを使用して作成されました。