4/14(日) 足・靴・木型研究会「第2回研究集会」を開催します☆彡

16-25. Pythonでマインスイーパーを手短に書いてみた

やること

Pythonでマインスイーパーがどれくらい短く書けるかずっと気になっていたので、重い腰を上げてやってみました。3時間くらいかかったと思います。人によって様々な実装が考えられるのでプログラミング教室にも良いかと思いました。

実行環境

WinPython3.6をおすすめしています。

WinPython - Browse /WinPython_3.6/3.6.7.0 at SourceForge.net
Portable Scientific Python 2/3 32/64bit Distribution for Windows

なお、Google Colabはウィンドウを立ち上げることができないため非対応です。

仕様

以下の最低限の機能は付けました。

  • 左クリックでマスを開く
  • 右クリックで爆弾フラグを立てる
  • “0”マスは連鎖的に開く
  • 全爆弾にフラグを立ててもクリア
  • クリア/ゲームオーバー時に爆弾の場所を開示

コードの簡便さを優先し、妥協した機能は次のとおりです。

  • リセットボタンなし(ウィンドウを閉じてプログラムを再実行すること)
  • 初手爆発の可能性あり
  • セーフマスに旗を立てるとアウト(なのでフラグキャンセルもなし)
  • タイマー表示なし
  • 残り爆弾数の表示なし

赤字はなんとかしたかったなと思いますが、自分がやりたい「プログラムによる自動探索」には不要なため割愛しました。

コード

冒頭に盤面サイズ、爆弾比率、ウィンドウサイズのパラメータがあります。

import cv2
import numpy as np
import numpy.random as nr
from scipy import signal
import itertools

"""
注意事項
・初手爆発の可能性あり
・セーフマスに旗を立てるとアウト
"""

#パラメータ
w = 6
h = 5 #初級:9*9, 中級:16*16, 上級:30*16
bomb_rate = 0.12 #初級:0.12, 中級:0.16, 上級:0.20くらい
cell_size = 60 #PCの画面サイズによってお好みで
nr.seed() #問題を固定する場合
number_c = [(255, 0, 0), (0, 128, 0), (0, 0, 255), (128, 0, 0), (0, 0, 128), (128, 128, 0), (0, 0, 0), (128, 128, 128)] #数字に対応する色 #BGR




#bomb : 爆弾(bool)
bomb_num = int(w * h * bomb_rate)
safe_num = w * h - bomb_num
bomb = nr.permutation([1]*bomb_num + [0]*safe_num).reshape(h, w).astype(bool)
print('bomb:\n{}'.format(bomb))

#number : 8近傍の爆弾の数(int)
f = np.array([[1, 1, 1],
              [1, 0, 1],
              [1, 1, 1]], int)
number = signal.convolve2d(bomb, f, mode='same', boundary='fill', fillvalue=0)
print('number:\n{}'.format(number))

#clear : 開けたセル(bool)
clear = np.zeros((h, w), bool)
print('clear:\n{}'.format(clear))

#flag : 爆弾フラグ(bool)
flag = np.zeros((h, w), bool)
print('flag:\n{}'.format(flag))


#表示用の画像作成
def make_img(extratext=''):
    #画像
    img = np.zeros((h, w, 3), 'uint8')
    img[~clear] = [190]*3
    img[clear] = [160]*3
    img = img.repeat(cell_size, axis=0).repeat(cell_size, axis=1)
    img[::cell_size, :] = [50]*3
    img[:, ::cell_size] = [50]*3
    #数字表示
    font_size = cell_size * 0.02
    x_shift, y_shift = cell_size // 4, cell_size - (cell_size // 4)
    for i, j in itertools.product(range(h), range(w)):
        if flag[i, j]:
            cv2.putText(img, '+', (cell_size*j+x_shift, cell_size*i+y_shift), cv2.FONT_HERSHEY_TRIPLEX, font_size, [0]*3, 2)
            continue
        if not clear[i, j] or number[i, j] == 0:
            continue
        cv2.putText(img, str(number[i, j]), (cell_size*j+x_shift, cell_size*i+y_shift), cv2.FONT_HERSHEY_TRIPLEX, font_size, number_c[number[i, j]-1], 2)
    #ゲーム終了時の特別テキスト
    if len(extratext) > 0:
        for i, j in itertools.product(range(h), range(w)):
            if bomb[i, j]:
                cv2.putText(img, '@', (cell_size*j+x_shift, cell_size*i+y_shift), cv2.FONT_HERSHEY_TRIPLEX, font_size, (0, 0, 255), 2)
        cv2.putText(img, extratext, (cell_size*0+x_shift, cell_size*0+y_shift), cv2.FONT_HERSHEY_TRIPLEX, font_size, (0, 0, 0), 2)
    return img
    
#爆弾フラグを立てて画像取得
def put_flag(i, j):
    global flag
    print('put_flag:({}, {})'.format(j, i))
    
    #無効チェック、すでに空いているマスかフラグ済みマスなら何もしない
    if clear[i, j] or flag[i, j]:
        print(' -> Cannot flag here')
        return make_img()
    
    #終了チェック、セーフマスに立てたら終わり
    if not bomb[i, j]:
        print(' -> Game over')
        return make_img(extratext='Game over')
    
    #フラグ立てる
    flag[i, j] = True
    
    #終了チェック、ゲームクリアか?
    if (flag == bomb).all():
        print(' -> Success')
        return make_img(extratext='Success!!')
    
    return make_img()

#開けて画像取得
def put_clear(i, j):
    global clear
    print('put_clear:({}, {})'.format(j, i))
    
    #無効チェック、すでに空いているマスかフラグ済みマスなら何もしない
    if clear[i, j] or flag[i, j]:
        print(' -> Cannot open here')
        return make_img()
    
    #終了チェック、爆弾を開けたら終わり
    if bomb[i, j]:
        print(' -> Game over')
        return make_img(extratext='Game over')
    
    #開ける
    clear[i, j] = True
        
    #数字0を開いた場合は8近傍を再帰的に開く
    if number[i, j] == 0:
        put_clear(np.clip(i - 1, 0, h - 1), np.clip(j - 1, 0, w - 1))
        put_clear(np.clip(i - 1, 0, h - 1), np.clip(j, 0, w - 1))
        put_clear(np.clip(i - 1, 0, h - 1), np.clip(j + 1, 0, w - 1))
        put_clear(np.clip(i, 0, h - 1), np.clip(j - 1, 0, w - 1))
        put_clear(np.clip(i, 0, h - 1), np.clip(j + 1, 0, w - 1))
        put_clear(np.clip(i + 1, 0, h - 1), np.clip(j - 1, 0, w - 1))
        put_clear(np.clip(i + 1, 0, h - 1), np.clip(j, 0, w - 1))
        put_clear(np.clip(i + 1, 0, h - 1), np.clip(j + 1, 0, w - 1))
    
    #終了チェック
    if (clear == ~bomb).all():
        print(' -> Success')
        return make_img(extratext='Success!!')
    
    return make_img()

#ウィンドウ関数
def onMouse(event, x, y, flags, param):
    #左クリック
    if event == cv2.EVENT_LBUTTONDOWN:
        #マス特定
        i, j = y//cell_size, x//cell_size
        #開けて画像取得
        img = put_clear(i, j)
        #表示
        cv2.imshow(wname, img)
        
    #右クリック
    if event == cv2.EVENT_RBUTTONDOWN:
        #マス特定
        i, j = y//cell_size, x//cell_size
        #旗を立てて画像取得
        img = put_flag(i, j)
        #表示
        cv2.imshow(wname, img)
    

#初期画像
img = make_img()

#ウィンドウ準備
wname = 'Minesweeper'
cv2.namedWindow(wname)
cv2.setMouseCallback(wname, onMouse)

#ウインドウ開始
cv2.imshow(wname, img)
cv2.waitKey()

これくらいコメントを入れても170行弱でした。他の言語と比べてどうなのでしょうか?

6×5サイズのプレイです。

30×16サイズのプレイはYoutubeにアップしました。

Pythonでマインスイーパーを手短に書いてみた

コードの補足

マスを開ける put_clear() 関数の中に「”0”のマスを開いた場合に連鎖的に開く」機能があり、これは put_clear() を再帰的に呼び出しています。この部分は scipy.signal.convolve2d() で畳み込む方法も書いたのですが、よりシンプルな今の方法にしました。反面、実行速度は遅いため、上級者モードで大きめの領域が開くのに1~2秒かかります。

ウィンドウを cv2.imshow() で出しているのですが、画像の色をBGRで指定する必要があるのは相変わらず面倒です。毎回 cv2.COLOR_RGB2BGR を挟むという手もありますが、なんかこう、冒頭に1行入れておけば以降はRGB順で良いよみたいな裏技はないものでしょうか。。

おわりに

これで100×100サイズでも遊べますし、自動探索もできそうです。

タイトルとURLをコピーしました