pythonでterminal上にマインスイーパー実装してみた

# きっかけ
terminal上にマインスイーパーを実装したくなったので，実装しました．実はプログラミング始めたての頃に，Cで一度実装したが完成してなかったこともあり，復習も兼ねて．
＊備忘録用のブログのため，細かい話はありません！（え）

# マインスイーパー概要
>マインスイーパは1980年代に発明された、一人用のコンピュータゲームである。ゲームの目的は地雷原から地雷を取り除くことである。

筆者も小学生くらいの頃に遊んでました．（歳がバレる）
ところで，今の小学生だとどれくらい知ってるんだろう．

ちなみに，今だとChromeで遊べるので，[ぜひ](https://www.google.com/search?q=Minesweeper&oq=min&aqs=chrome.1.69i57j69i59l2j0l2j69i61j69i60l2.2095j0j7&sourceid=chrome&ie=UTF-8)

# 実装概要
大枠は下記サイトと同じです（多分）．
[ターミナル上で遊べるマインスイーパ実装](https://qiita.com/ta-ka/items/40b56722da43d1ba2e26#open%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%86%8D%E5%B8%B0])

# アプリ概要
1. ファイルと，マス目の大きさ，ボムの比率を指定し，実行．


2. 初期位置を指定．マスが表示される．
(*初期位置，および隣接マスには爆弾が配置されないように，設定しています．)
3. 以下，open位置を指定していく．
4. クリアすると，openマス数と経過時間を表示して終了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368826/feba6007-b5ef-3dae-560c-bb8a2bcfef8a.png)
5. 指定していない入力を受け取った際には，下図のように無限ループする．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/368826/c4c8c33d-b9aa-cb66-8cfd-c59766c15046.png)

# ハマリポイント

## 二次元配列の生成
実装時に必要となった二次元配列を生成するときに，下記のコードでは，一つのリストを更新するとすべてのリストが更新されてしまうため注意．

```python:失敗例
    #爆弾の位置，周囲の爆弾数を保持する二次元配列
    mine_list = [["N"] * args.n]

    #マスがopen済みかどうかを保持する二次元配列
    opened_ls = [[False] * args.n]

```

下記のように，リスト内包表記で初期化して事なきを得た．

```python:成功例
    #爆弾の位置，周囲の爆弾数を保持する二次元配列
    mine_list = [["N"] * args.n for i in range(args.n)]

    #マスがopen済みかどうかを保持する二次元配列
    opened_ls = [[False] * args.n for i in range(args.n)]

```
# 感想
単純なゲームであるが，プログラムの基本を理解していないと実装できない．そのため，基礎復習の良いきっかけとなった．
## 課題，改善点
* 再帰処理が含まれるため，マスの大きさによっては最大再起回数を超える可能性あり．[対応策](https://qiita.com/wifecooky/items/348da636f456a00120db)
* フラグ設置機能の実装

# Code
コードの全容は以下です．[Github](https://github.com/hirogithu/minsweeper_on_terminal)でも公開しました．

```python

import argparse
import random
import copy
import itertools
import time

def main(args):
    def chk():
        if args.n > 99:
            args.n = 99
        if args.bomb_rate >= 1 or args.bomb_rate <= 0:
            args.bomb_rate = 0.5
        return args

    def create_mine_map(init_w, init_h):
        def num_bomb(mine_list, iw, ih):
            num_bomb = 0
            for i in range(-1, 2):
                for j in range(-1, 2):
                    if iw+i < 0 or iw+i >= args.n or ih+j < 0 or ih+j >= args.n:
                        continue
                    elif mine_list[iw+i][ih+j] == "B":
                        num_bomb += 1
            return num_bomb

        mine_list = [["N"] * args.n for i in range(args.n)]
        # add bomb
        n_bomb = int((args.n ** 2) * args.bomb_rate)
        bomb_count = 0
        for bomb_w in range(args.n):
            for bomb_h in range(args.n):
                # bomb設置
                if bomb_count >= n_bomb:
                    break
                if random.randint(0, 100) > 100 * (1 - args.bomb_rate):
                    # 初期入力位置と周辺は除外
                    if bomb_w != init_w and bomb_h != init_h and \
                        bomb_w != init_w - 1 and bomb_h != init_h - 1 and \
                            bomb_w != init_w + 1 and bomb_h != init_h + 1:
                                mine_list[bomb_w][bomb_h] = "B"
                                bomb_count += 1

        # increment around bomb
        for i in range(args.n):
            for j in range(args.n):
                if mine_list[i][j] == "N":
                    mine_list[i][j] = num_bomb(mine_list, i, j)
        return mine_list, bomb_count

    def open_map(mine_list, open_w, open_h, opened_ls):
        if mine_list[open_w][open_h] == "B":
            opened_ls = [[True] * args.n for i in range(args.n)]
            return opened_ls

        opened_ls[open_w][open_h] = True

        if mine_list[open_w][open_h] == 0:
            for i in range(-1, 2):
                for j in range(-1, 2):
                    if open_w + i < 0 or open_w + i >= args.n or open_h + j < 0 or open_h + j >= args.n:
                        continue
                    elif not opened_ls[open_w + i][open_h + j]:
                        opened_ls = open_map(mine_list, open_w + i, open_h + j, opened_ls)
        return opened_ls

    def plt_mine(mine_list, opened_ls, play_mode=True):
        h = args.n
        mine_list_cp = copy.deepcopy(mine_list)
        print(*["="]*(args.n+2))
        if play_mode:
            for i in range(h):
                for j in range(h):
                    if not opened_ls[i][j]:
                        mine_list_cp[i][j] = "-"
            print("PLOT MAP")
        else:
            print("PLOT MAP (All Opened)")

        print(" ", " ", *list(range(0, args.n)))
        print(*["="]*(args.n + 2))

        for i in range(h):
            print(i, ":", *mine_list_cp[:][i])

    "chk args"
    args = chk()

    "while wait input(w, h)"
    while True:
        try:
            init_w, init_h = map(int, input("input w h ({} ~ {}) >> ".format(0, args.n - 1)).split())        

            if init_w >= 0 and init_w < args.n and init_h >= 0 and init_h < args.n:
                break
            else:
                print("Over" + str(args.n))

        except ValueError:
            print("input 2 numbers. 0 0")

    "create mine"
    opened_ls = [[False] * args.n for i in range(args.n)]
    mine_list, n_bomb = create_mine_map(init_w, init_h)
    opened_ls = open_map(mine_list, init_w, init_h, opened_ls)

    "plot mine"
    plt_mine(mine_list, opened_ls, play_mode=args.debug)

    "while wait input(w, h)"
    init_time = time.time()
    init_opend_num = sum(list(itertools.chain.from_iterable(opened_ls)))

    while True:

        if all(list(itertools.chain.from_iterable(opened_ls))):
            print("!!!!!!!!BOMBED!!!!!!!!")
            break

        elif sum(list(itertools.chain.from_iterable(opened_ls))) == args.n**2 - n_bomb:
            end_time = time.time()
            print("!!!!!!!!CLEARD!!!!!!!!")
            print("YOUR TIME:{:0=3.2f}".format(end_time - init_time))
            print("OPEND:{}".format(args.n**2 - init_opend_num - n_bomb))
            break

        try:
            open_w, open_h = map(int, input("input w h ({} ~ {}) >> ".format(0, args.n - 1)).split())
    
            if open_w >= 0 and open_w < args.n and open_h >= 0 and open_h < args.n:
                "update mine"
                opened_ls = open_map(mine_list, open_w, open_h, opened_ls)

                "plot mine"
                plt_mine(mine_list, opened_ls, play_mode=args.debug)

            else:
                print("Over " + str(args.n))

        except ValueError:
            print("input 2 numbers. 0 0")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("-n", type=int, default=8, help="create (n, n) size")
    parser.add_argument("-b", "--bomb_rate", type=float, default=0.1, help="how many bomb in the mine.")
    parser.add_argument("-d", "--debug", action="store_false")
    args = parser.parse_args()
    main(args)
```

## 参考
[ターミナル上で遊べるマインスイーパ実装](https://qiita.com/ta-ka/items/40b56722da43d1ba2e26#open%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AE%E5%86%8D%E5%B8%B0])