PythonでMinesweeperアルゴリズムを考える
- 2020.07.30
- プログラミング
- Minesweeper, Python, ゲーム, マインスイーパー
情報系に進んでいる大学生なら一度は組んだことがあるであろう(組ませたことがある)プログラムの1つ、マインスイーパーについて、今回はPythonで組んでゆきたいと思います。
私も大学1年のとき(? だったと思う)講義で課題として出されたので、C言語で組んだのですが、「ちょっとアルゴリズムが違くないか?」と思っていました(実際に講義で言われた手法は厳密な意味でのマインスイーパーではなかった)
なので今回は、独自に組んでゆきたいと思います。
アルゴリズム
マインスイーパーのアルゴリズムといえば以下のようになっています。
- マインスイーパーのオブジェクトを生成する
- ゲームボード(地雷と周辺情報が埋めこまれているもの)とプレイボード(開いているか、閉じているか、フラグが立っているか)を構築する
- インデックスを指定する
- ゲームボードで指定したインデックスの値が
MINE
ならばゲーム終了 - ゲームボードで指定したインデックスの値が
1
以上ならばプレイボードのインデックスの値をOPEN
にする - ゲームボードで指定したインデックスの値が
SPACE
ならば周辺インデックスについても調べる。全ての周辺が1
以上になるまでプレイボードの周辺インデックスの値をOPEN
にし続ける
- ゲームボードで指定したインデックスの値が
- ゲームが終わるまで3に戻る
- プレイボードで開ける部分を全部開いたら終了
さらに、「フラグを立てる」や「すでに開いている部分を指定すると適宜開く」などを含めて改良した場合はこのようになります。
- マインスイーパーのオブジェクトを生成する
- ゲームボード(地雷と周辺情報が埋めこまれているもの)とプレイボード(開いているか、閉じているか、フラグが立っているか)を構築する
- インデックスを指定する
- フラグを立てることにした場合は以下のようにする
- プレイボードで指定したインデックスの値が
FLAG
の場合は、CLOSE
- プレイボードで指定したインデックスの値が
CLOSE
の場合は、FLAG
にする
- プレイボードで指定したインデックスの値が
- フラグを立てないことにした場合は以下のようにする
- ゲームボードで指定したインデックスの値が
MINE
ならばゲーム終了 - ゲームボードで指定したインデックスの値が
1
以上ならばプレイボードのインデックスの値をOPEN
にする - ゲームボードで指定したインデックスの値が
SPACE
ならば周辺インデックスについても調べる。FLAG
となっていない全ての周辺が1
以上になるまでプレイボードの周辺インデックスの値をOPEN
にし続ける - プレイボードで指定したインデックスの値が
FLAG
ならばプレイボードの値はそのままにする - プレイボードで指定したインデックスの値が
OPEN
で周辺インデックスのFLAG
の数がゲームボードで指定したインデックスの値以上になっているなら周辺インデックスについても調べる。FLAG
となっていない全ての周辺が1
以上になるまでプレイボードの周辺インデックスの値をOPEN
にし続ける
- ゲームボードで指定したインデックスの値が
- ゲームが終わるまで3に戻る
- プレイボードで開ける部分を全部開いたら終了
プログラムの組む順番
- 表示するボードとゲームで見せるボード
- 地雷を埋め込む部分
- ボードを開く部分
- 終了条件
- (おまけ)フラグを立てる
- (おまけ)すでに開いている部分を指定すると適宜開くように改良
数値を指定しなきゃいけないのに文字を指定するなどの例外処理を適宜書くのはちょっと面倒だったので、そこら辺は省いています。
環境
- numpy 1.18.1
- Python 3.8.0
1. 表示するボードとゲームで見せるボード
ここはウェイト軽めです笑
とりあえずプログラムを組んでみましょう!
import numpy as np
import itertools
class Minesweeper(object):
def __init__(self, shape: (int, int)=(9, 9)):
self.set_board(shape)
def set_board(self, shape: (int, int)):
self.shape = shape # あとで使う
self.size = np.prod(shape) # あとで使う
self.gboard = np.zeros(shape, dtype=np.int8)
self.pboard = np.zeros(shape, dtype=np.int8)
if __name__ == '__main__':
g = Minesweeper()
ここでは地雷と周辺情報を埋め込んであるゲームボード用にgboard
、フラグを立てたとか開いたとかを保存するプレイボード用にpboard
という変数を用意しました。
また、shape
はそのまま、ボードの行数と列数を表し、size
はその積です。あとで使うので書いておきます。
でも、このままだと「何がどうなってるのかわからない!」と思うので、実際に表示しながら確認してゆきましょう!
...
if __name__ == '__main__':
g = Minesweeper()
print(g.gboard)
print(p.board)
んで、実際に実行!
[[0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0]
...
[0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0]]
[[0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0]
...
[0 0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0 0]]
こんな感じでゼロ行列が2つ表示されたら正解です。
2. 地雷を埋め込む部分
次は地雷を埋め込む部分を書いてゆきます。
ここでは主に
- 地雷を埋め込む
- 周辺情報を作り出す
- ゲームボードをもっと見やすくするように改良する
ということをやってゆきます。
1. 地雷を埋め込む
ここは地雷を埋め込むだけなのでウェイト軽め。
import numpy as np
import itertools
MINE = -1
class Minesweeper(object):
def __init__(self, shape: (int, int)=(9, 9), mines: int=10):
self.set_board(shape)
self.set_mines(mines)
def set_board(self, shape: (int, int)):
self.shape = shape
self.size = np.prod(shape)
self.gboard = np.zeros(shape, dtype=np.int8)
self.pboard = np.zeros(shape, dtype=np.int8)
def set_mines(self, mines: int):
r, c = shape = self.shape
if mines >= self.size:
raise Exception(f'mines must be less than product of shape.<{mines=}, {shape=}>')
self.mines = mines
for ix in np.random.choice(range(self.size), mines, replace=False):
self.gboard[divmod(ix, shape[1])] = MINE
def main():
g = Minesweeper()
print(g.gboard)
if __name__ == '__main__':
main()
とりあえず、MINE = -1
と無難に設定しました。
また、size
よりも大きな値で地雷は設定できないのでこの部分の例外を規定しておきました。
numpyはとても便利ですね! choice
とrange
、さらにdivmod
を組み合わせればこのように簡単に「被らない形で地雷を埋め込むこと」ができるのですから笑
実行してみるとこんな感じになります(なお、random
を使っているので若干表示は異なります)
[[ 0 0 0 0 -1 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0 -1]
[ 0 -1 0 0 0 0 0 0 -1]
[ 0 0 -1 0 -1 0 -1 -1 0]
[ 0 0 0 0 0 0 0 0 0]
[ 0 0 0 0 -1 0 0 0 0]
[ 0 0 0 0 0 0 0 0 -1]]
2. 周辺情報を作り出す
次に、周辺情報を作り出します。具体的なフローは以下のようになります。
gboard
に類似したゼロ行列tmp
を宣言tmp
に\(\displaystyle \left[ \begin{array}{ccc}1 & 1 & 1\\1 & 0 & 1\\1 & 1 & 1 \end{array} \right] \)を畳み込み演算するgboard == MINE
をtmp
にブロードキャストgboard = tmp
とする
とりあえず、プログラムを組んでみましょう!
...
def set_mines(self, mines: int):
r, c = shape = self.shape
if mines >= self.size:
raise Exception(f'mines must be less than product of shape.<{mines=}, {shape=}>')
self.mines = mines
for ix in np.random.choice(range(self.size), mines, replace=False):
self.gboard[divmod(ix, shape[1])] = MINE
tmp = np.zeros(shape, dtype=np.int8)
# scipyはノロいので、畳み込み部分は勝手に高速化
for r_, c_ in itertools.product(range(-1, 2), range(-1, 2)):
if r_ == c_ == 0:
continue
tmp[max(0, r_):min(r, r+r_), max(0, c_):min(c, c+c_)] -= self.gboard[max(0, -r_):min(r, r-r_), max(0, -c_):min(c, c-c_)]
tmp[self.gboard == MINE] = MINE
self.gboard = tmp
del tmp
...
本当はscipyのconvolve2d
を使っても悪くはないんだけど、私は個人的にノロいのは使いたくないので勝手に改良しています。
んで、実際に実行してみるこうなります。
[[ 1 1 0 0 1 1 1 1 -1]
[-1 2 1 1 2 -1 2 1 1]
[ 1 2 -1 1 2 -1 2 1 1]
[ 0 1 1 1 1 1 1 1 -1]
[ 1 1 0 0 0 0 0 2 2]
[-1 1 0 0 0 0 0 1 -1]
[ 1 1 0 0 0 0 0 1 1]
[ 0 0 1 1 2 1 1 0 0]
[ 0 0 1 -1 2 -1 1 0 0]]
3. ゲームボードをもっと見やすくするように改良する
このままだとゲームボードが読みにくいので、改良してデバッグのときに見やすい状態にしたいと思います。
とりあえず、こんな感じにしました。
import numpy as np
import itertools
MINE = -1
SPACE = 0
class Minesweeper(object):
...
def get_gboard(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
print(tmp, end='')
print()
return self.gboard
def main():
g = Minesweeper()
g.get_gboard()
if __name__ == '__main__':
main()
んで、出力。
|012345678
--+---------
0|XX1 1X1
1|3431 111
2|1XX211
3|2322X1
4|X1 111
5|1211
6|12X1
7|X321
8|2X1
おお、見やすい……。
ここでは、何もないところをSPACE
、地雷をMINE
として、SPACE
の場所では␣
が出力されるように、MINE
の場所ではX
が出力されるようにしています。
3. ボードを開く部分
次にプレイボードを開く改良をしてゆきます。
import numpy as np
import itertools
OPEN = 1
CLOSE = 0
SPACE = 0
MINE = -1
class Minesweeper(object):
...
def open_pboard(self, index: (int, int)):
r, c = self.shape
r_, c_ = index
if 0 <= r_ < r and 0 <= c_ < c:
pboard, gboard = self.pboard, self.gboard
if gboard[index] == MINE:
pass # ゲームが終わる部分をあとで書く
elif pboard[index] == CLOSE:
open_index_li = [index]
while len(open_index_li) > 0:
# index周辺8セルにSPACEがないか調べる
r_, c_ = index = open_index_li[0]
if gboard[index] == SPACE:
for index_ in itertools.product(range(r_-1, r_+2), range(c_-1, c_+2)):
if 0 <= index_[0] < r and 0 <= index_[1] < c and pboard[index_] == CLOSE:
if gboard[index_] != MINE:
pboard[index_] = OPEN
if gboard[index_] == SPACE:
open_index_li.append(index_)
elif gboard[index] != MINE and pboard[index] == CLOSE:
pboard[index] = OPEN
# 最後に調べたindexを削除
open_index_li.remove(index)
def main():
g = Minesweeper()
g.get_gboard()
print()
index = tuple(map(int, input('input index("row col [mode]"): ').split(' ')))
g.open_pboard(index)
print(g.pboard)
if __name__ == '__main__':
main()
これを実行すればようやくちゃんと開くプレイボードが実現できます!
|012345678
--+---------
0|1221 1X
1|1XX1 11
2|1221
3| 111
4| 2X31
5| 2XX1
6|12332
7|X12X2 11
8|112X2 1X
input index("row col [mode]"): 0 4
[[0 0 0 1 1 1 1 1 0]
[0 0 0 1 1 1 1 1 1]
[0 0 0 1 1 1 1 1 1]
[0 0 0 1 1 1 1 1 1]
[0 0 0 1 1 1 1 1 1]
[0 0 0 0 1 1 1 1 1]
[0 0 0 0 1 1 1 1 1]
[0 0 0 0 1 1 1 1 1]
[0 0 0 0 1 1 1 1 0]]
こんな感じで出力されれば成功です。
ただ、これではプレイボードが非常に見づらいので改良します。
というか、プレイボードがちゃんと表示されなかったらゲームにならないですよね笑
...
def display_board(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.pboard[r_, c_]
if tmp == OPEN:
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
else:
tmp = '+'
print(tmp, end='')
print()
...
これを実行すればようやくマインスイーパーらしくなります。
|012345678
--+---------
0|1X223X1
1|112XX2111
2| 1221 1X
3|1221 11
4|1XX1
5|1221
6| 11211
7| 1X3X1
8| 12X21
|012345678
--+---------
0|+++++++++
1|+++++++++
2|+++++++++
3|+++++++++
4|+++++++++
5|+++++++++
6|+++++++++
7|+++++++++
8|+++++++++
input index("row col [mode]"): 8 0
|012345678
--+---------
0|+++++++++
1|+++++++++
2|+++++++++
3|+++++++++
4|+++++++++
5|1221+++++
6| 1+++++
7| 1+++++
8| 1+++++
だいたいはgboard
を表示するところと似たように書いています。
まだ開いていない部分は+
にしました(別に何の文字でも良い)
4.終了条件
ついに終了条件です。
終了条件がなければ、上のゲームでif __name__ =='__main__'
のなかでwhile True
とした場合に自分でKeyboardInterrupt
を発生させたりしなければなりません。
これではちゃんとしたマインスイーパーにはならないので、終了条件は必須ですね笑
...
class Minesweeper(object):
...
def set_board(self, shape: (int, int)):
self.shape = shape
self.size = np.prod(shape)
self.gboard = np.zeros(shape, dtype=np.int8)
self.pboard = np.zeros(shape, dtype=np.int8)
self.yet = True
...
def open_pboard(self, index: (int, int)):
r, c = self.shape
r_, c_ = index
if 0 <= r_ < r and 0 <= c_ < c:
pboard, gboard = self.pboard, self.gboard
if gboard[index] == MINE:
self.yet = False
elif pboard[index] == CLOSE:
open_index_li = [index]
while len(open_index_li) > 0:
# index周辺8セルにSPACEがないか調べる
r_, c_ = index = open_index_li[0]
if gboard[index] == SPACE:
for index_ in itertools.product(range(r_-1, r_+2), range(c_-1, c_+2)):
if 0 <= index_[0] < r and 0 <= index_[1] < c and pboard[index_] == CLOSE:
if gboard[index_] != MINE:
pboard[index_] = OPEN
if gboard[index_] == SPACE:
open_index_li.append(index_)
elif gboard[index] != MINE and pboard[index] == CLOSE:
pboard[index] = OPEN
# 最後に調べたindexを削除
open_index_li.remove(index)
self.yet = self.mines != np.sum(self.pboard != OPEN)
def end(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
ptmp, gtmp = self.pboard[r_, c_], self.gboard[r_, c_]
if gtmp == MINE:
tmp = 'X'
else:
if ptmp == CLOSE:
tmp = '+'
elif ptmp == OPEN and gtmp == SPACE:
tmp = ' '
else:
tmp = gtmp
print(tmp, end='')
print()
print('END')
...
def main():
g = Minesweeper()
g.get_gboard()
print()
while g.yet:
g.display_board()
index = tuple(map(int, input('input index("row col [mode]"): ').split(' ')))
g.open_pboard(index)
g.end()
if __name__ == '__main__':
main()
以上のようにyet
を宣言することで、実際にゲームが終わったかどうかを判断させます。
このプログラムでは「勝ったか/負けたか」を判断せず、「終わったか/終わっていないか」というところで判断します。
end()
関数では最終的なプレイボードの出力をします。「勝った」場合はちゃんとしたプレイボードが表示されますが、「負けた」場合は地雷がどこにあったか教えてくれるようにプログラムを書きました。
ここで実際に実行してみると、ちゃんとマインスイーパーができると思います。試してみましょう(ここでゲームしてみる時間が流れる)
5. (おまけ)フラグを立てる
ここからはおまけ編です!
多くのマインスイーパーではフラグを立てるが実装されています。
これがあるとマインスイーパーはさっきよりも遊びやすくなりますよね?
それでは実装してゆきます。
import numpy as np
import itertools
FLAG = 2
OPEN = 1
CLOSE = 0
SPACE = 0
MINE = -1
class Minesweeper(object):
...
def set_flag(self, index: (int, int)):
if self.pboard[index] == FLAG:
self.pboard[index] = CLOSE
elif self.pboard[index] == CLOSE:
self.pboard[index] = FLAG
...
def end(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
ptmp, gtmp = self.pboard[r_, c_], self.gboard[r_, c_]
if gtmp == MINE:
if ptmp == FLAG:
tmp = 'F'
else:
tmp = 'X'
else:
if ptmp == CLOSE:
tmp = '+'
elif ptmp == OPEN and gtmp == SPACE:
tmp = ' '
elif ptmp == FLAG:
tmp = 'L'
else:
tmp = gtmp
print(tmp, end='')
print()
print('END')
...
def display_board(self):
r, c = self.shape
print(f'flag(s): {self.mines - np.sum(self.pboard == FLAG)}')
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.pboard[r_, c_]
if tmp == OPEN:
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
elif tmp == FLAG:
tmp = 'F'
else:
tmp = '+'
print(tmp, end='')
print()
def main():
g = Minesweeper()
g.get_gboard()
print()
while g.yet:
g.display_board()
index_li = input('input index("row col [mode]"): ').split(' ')
index = tuple(map(int, index_li[:2]))
if len(index_li) == 3 and (index_li[-1] == 'f' or index_li[-1] == 'F'):
g.set_flag(index)
else:
g.open_pboard(index)
g.end()
if __name__ == '__main__':
main()
実際にプログラムを書き換えた部分はそんなにないです。
FLAG
はpboard
で操作するように書くととても楽です。
実際にプログラムを動かすとこうなります。
|012345678
--+---------
0|12321 11
1|1XXX1 12X
2|12321 1X2
3| 1121211
4| 1X2X111
5| 123211X
6| 2X2 11
7| 2X2
8| 111
flag(s): 10
|012345678
--+---------
0|+++++++++
1|+++++++++
2|+++++++++
3|+++++++++
4|+++++++++
5|+++++++++
6|+++++++++
7|+++++++++
8|+++++++++
input index("row col [mode]"): 0 0 f
flag(s): 9
|012345678
--+---------
0|F++++++++
1|+++++++++
2|+++++++++
3|+++++++++
4|+++++++++
5|+++++++++
6|+++++++++
7|+++++++++
8|+++++++++
input index("row col [mode]"):
フラグはフラグっぽくF
が出力されるようにしました。
6. (おまけ)すでに開いている部分を指定すると適宜開くように改良
アプリとかウェブ上で遊べるマインスイーパーには実装されていないこともあるこの機能。
数字をタッチすると周辺のフラグの数に応じてセルが開いてくれたりするあの機能、あると便利ですよね!
それでは実装してゆきましょう!
...
def open_pboard(self, index: (int, int)):
r, c = self.shape
r_, c_ = index
if 0 <= r_ < r and 0 <= c_ < c:
pboard, gboard = self.pboard, self.gboard
def inr_fn(open_index_li: [(int,int), ...]):
while len(open_index_li) > 0:
# index周辺8セルにSPACEがないか調べる
r_, c_ = index = open_index_li[0]
if gboard[index] == SPACE:
for index_ in itertools.product(range(r_-1, r_+2), range(c_-1, c_+2)):
if 0 <= index_[0] < r and 0 <= index_[1] < c and pboard[index_] == CLOSE:
if gboard[index_] != MINE:
pboard[index_] = OPEN
if gboard[index_] == SPACE:
open_index_li.append(index_)
elif gboard[index] != MINE and pboard[index] == CLOSE:
pboard[index] = OPEN
# 最後に調べたindexを削除
open_index_li.remove(index)
if gboard[index] == MINE:
self.yet = False
elif pboard[index] == CLOSE:
open_index_li = [index]
inr_fn(open_index_li)
self.yet = self.mines != np.sum(self.pboard != OPEN)
elif pboard[index] == OPEN and gboard[index] <= np.sum(pboard[max(0, r_-1):min(r, r_+2), max(0, c_-1):min(c, c_+2)] == FLAG):
open_index_li = [index for index in itertools.product(range(max(0, r_-1), min(r, r_+2)), range(max(0, c_-1), min(c, c_+2))) if pboard[index] != FLAG]
_yet = True
for index in open_index_li:
if gboard[index] == MINE:
pboard[index] = OPEN
_yet = False
inr_fn(open_index_li)
self.yet &= _yet
...
ここは結構書き換えました。
まず、セルを開く部分をinr_fn()
関数として内部関数で再実現し、プレイボードの指定されたインデックスが開いている場合、閉じている場合で場合わけして開き方を改良しました。
これを実行すると、周辺のフラグの数によってセルが開くようになります。
さいごに
いかがでしたか?
このようにいろいろと新しい機能を追加するとそのぶんコードは複雑になります。
それでもしやすいゲームを実現したかったので、まあ良い経験になりました。
ソースコード全体
おまけなしバージョンのソースコードの全体としては、このようになります。
import numpy as np
import itertools
OPEN = 1
CLOSE = 0
SPACE = 0
MINE = -1
class Minesweeper(object):
def __init__(self, shape: (int, int)=(9, 9), mines: int=10):
self.set_board(shape)
self.set_mines(mines)
def set_board(self, shape: (int, int)):
self.shape = shape
self.size = np.prod(shape)
self.gboard = np.zeros(shape, dtype=np.int8)
self.pboard = np.zeros(shape, dtype=np.int8)
self.yet = True
def set_mines(self, mines: int):
r, c = shape = self.shape
if mines >= self.size:
raise Exception(f'mines must be less than product of shape.<{mines=}, {shape=}>')
self.mines = mines
for ix in np.random.choice(range(self.size), mines, replace=False):
self.gboard[divmod(ix, shape[1])] = MINE
tmp = np.zeros(shape, dtype=np.int8)
# scipyはノロいので、畳み込み部分は勝手に高速化
for r_, c_ in itertools.product(range(-1, 2), range(-1, 2)):
if r_ == c_ == 0:
continue
tmp[max(0, r_):min(r, r+r_), max(0, c_):min(c, c+c_)] -= self.gboard[max(0, -r_):min(r, r-r_), max(0, -c_):min(c, c-c_)]
tmp[self.gboard == MINE] = MINE
self.gboard = tmp
del tmp
def open_pboard(self, index: (int, int)):
r, c = self.shape
r_, c_ = index
if 0 <= r_ < r and 0 <= c_ < c:
pboard, gboard = self.pboard, self.gboard
if gboard[index] == MINE:
self.yet = False
elif pboard[index] == CLOSE:
open_index_li = [index]
while len(open_index_li) > 0:
# index周辺8セルにSPACEがないか調べる
r_, c_ = index = open_index_li[0]
if gboard[index] == SPACE:
for index_ in itertools.product(range(r_-1, r_+2), range(c_-1, c_+2)):
if 0 <= index_[0] < r and 0 <= index_[1] < c and pboard[index_] == CLOSE:
if gboard[index_] != MINE:
pboard[index_] = OPEN
if gboard[index_] == SPACE:
open_index_li.append(index_)
elif gboard[index] != MINE and pboard[index] == CLOSE:
pboard[index] = OPEN
# 最後に調べたindexを削除
open_index_li.remove(index)
self.yet = self.mines != np.sum(self.pboard != OPEN)
def end(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
ptmp, gtmp = self.pboard[r_, c_], self.gboard[r_, c_]
if gtmp == MINE:
tmp = 'X'
else:
if ptmp == CLOSE:
tmp = '+'
elif ptmp == OPEN and gtmp == SPACE:
tmp = ' '
else:
tmp = gtmp
print(tmp, end='')
print()
print('END')
def get_gboard(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
print(tmp, end='')
print()
return self.gboard
def display_board(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.pboard[r_, c_]
if tmp == OPEN:
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
else:
tmp = '+'
print(tmp, end='')
print()
def main():
g = Minesweeper()
g.get_gboard()
print()
while g.yet:
g.display_board()
index_li = input('input index("row col [mode]"): ').split(' ')
g.open_pboard(tuple(map(int, index_li[:2])))
g.end()
if __name__ == '__main__':
main()
おまけありバージョンはこうなります。
import numpy as np
import itertools
FLAG = 2
OPEN = 1
CLOSE = 0
SPACE = 0
MINE = -1
class Minesweeper(object):
def __init__(self, shape: (int, int)=(9, 9), mines: int=10):
self.set_board(shape)
self.set_mines(mines)
def set_board(self, shape: (int, int)):
self.shape = shape
self.size = np.prod(shape)
self.gboard = np.zeros(shape, dtype=np.int8)
self.pboard = np.zeros(shape, dtype=np.int8)
self.yet = True
def set_mines(self, mines: int):
r, c = shape = self.shape
if mines >= self.size:
raise Exception(f'mines must be less than product of shape.<{mines=}, {shape=}>')
self.mines = mines
for ix in np.random.choice(range(self.size), mines, replace=False):
self.gboard[divmod(ix, shape[1])] = MINE
tmp = np.zeros(shape, dtype=np.int8)
# scipyはノロいので、畳み込み部分は勝手に高速化
for r_, c_ in itertools.product(range(-1, 2), range(-1, 2)):
if r_ == c_ == 0:
continue
tmp[max(0, r_):min(r, r+r_), max(0, c_):min(c, c+c_)] -= self.gboard[max(0, -r_):min(r, r-r_), max(0, -c_):min(c, c-c_)]
tmp[self.gboard == MINE] = MINE
self.gboard = tmp
del tmp
def set_flag(self, index: (int, int)):
if self.pboard[index] == FLAG:
self.pboard[index] = CLOSE
elif self.pboard[index] == CLOSE:
self.pboard[index] = FLAG
def open_pboard(self, index: (int, int)):
r, c = self.shape
r_, c_ = index
if 0 <= r_ < r and 0 <= c_ < c:
pboard, gboard = self.pboard, self.gboard
def inr_fn(open_index_li: [(int,int), ...]):
while len(open_index_li) > 0:
# index周辺8セルにSPACEがないか調べる
r_, c_ = index = open_index_li[0]
if gboard[index] == SPACE:
for index_ in itertools.product(range(r_-1, r_+2), range(c_-1, c_+2)):
if 0 <= index_[0] < r and 0 <= index_[1] < c and pboard[index_] == CLOSE:
if gboard[index_] != MINE:
pboard[index_] = OPEN
if gboard[index_] == SPACE:
open_index_li.append(index_)
elif gboard[index] != MINE and pboard[index] == CLOSE:
pboard[index] = OPEN
# 最後に調べたindexを削除
open_index_li.remove(index)
if gboard[index] == MINE:
self.yet = False
elif pboard[index] == CLOSE:
open_index_li = [index]
inr_fn(open_index_li)
self.yet = self.mines != np.sum(self.pboard != OPEN)
elif pboard[index] == OPEN and gboard[index] <= np.sum(pboard[max(0, r_-1):min(r, r_+2), max(0, c_-1):min(c, c_+2)] == FLAG):
open_index_li = [index for index in itertools.product(range(max(0, r_-1), min(r, r_+2)), range(max(0, c_-1), min(c, c_+2))) if pboard[index] != FLAG]
_yet = True
for index in open_index_li:
if gboard[index] == MINE:
pboard[index] = OPEN
_yet = False
inr_fn(open_index_li)
self.yet &= _yet
def end(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
ptmp, gtmp = self.pboard[r_, c_], self.gboard[r_, c_]
if gtmp == MINE:
if ptmp == FLAG:
tmp = 'F'
else:
tmp = 'X'
else:
if ptmp == CLOSE:
tmp = '+'
elif ptmp == OPEN and gtmp == SPACE:
tmp = ' '
elif ptmp == FLAG:
tmp = 'L'
else:
tmp = gtmp
print(tmp, end='')
print()
print('END')
def get_gboard(self):
r, c = self.shape
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
print(tmp, end='')
print()
return self.gboard
def display_board(self):
r, c = self.shape
print(f'flag(s): {self.mines - np.sum(self.pboard == FLAG)}')
print(' |', end='')
for c_ in range(c):
print(f'{c_%10:1}', end='')
print('\n--+' + '-' * c)
for r_ in range(r):
print(f'{r_:2}|', end='')
for c_ in range(c):
tmp = self.pboard[r_, c_]
if tmp == OPEN:
tmp = self.gboard[r_, c_]
if tmp == SPACE:
tmp = ' '
elif tmp == MINE:
tmp = 'X'
elif tmp == FLAG:
tmp = 'F'
else:
tmp = '+'
print(tmp, end='')
print()
def main():
g = Minesweeper()
g.get_gboard()
print()
while g.yet:
g.display_board()
index_li = input('input index("row col [mode]"): ').split(' ')
index = tuple(map(int, index_li[:2]))
if len(index_li) == 3 and (index_li[-1] == 'f' or index_li[-1] == 'F'):
g.set_flag(index)
else:
g.open_pboard(index)
g.end()
if __name__ == '__main__':
main()
- 前の記事
フォロワー20人を21人にするラクな方法 2020.07.27
- 次の記事
OCRでレポート作成をサボる 2020.08.03