子供のために迷路を自動生成する

# はじめに
娘が迷路に興味を持ち始め、私も強化学習で迷路を自動生成していたので…
折角だからExcelに出力して、印刷して、一緒に遊んでしまおう！という動機です。
また、私のゲーミングPCにはExcelが入っていないので、妻の端末を借りています。
流石に妻の端末をpythonで侵すわけにはいかなかったので、google の colaboratory を使用してみました。

# 結論
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/304167/efefa22d-00e7-3c9d-e609-13bf142ac7b3.png)

# 自動生成のルール
- Excelのセルを部屋と考え、inputで1辺のセル数Nを決める。
- ２つの変数`部屋番号`と`壁`を使用する。
    - 部屋番号
        - 初期値`[0, 1, 2, 3, ...]`のN*Nの連番とする。
    - 壁
        - N個のセルに壁4つを設定する。
        - 壁は`[1, 1, 1, 1] = [上, 右, 下, 左]`として`1:壁/0:壁無し`を設定。
        - データのイメージは`[[1,1,1,1][1,1,1,1]...]`
        - データ数は[N]個（表記方法合ってる？）
- ランダムに部屋を選択
- ランダムに突き破る壁を選択
- 部屋番号を小さい方に統一させる
    - 例えば 3*3 の迷路を想定し、部屋2 の 下向き を突き破る場合
    - 部屋番号 [0, 1, **2**, 3, 4, **2**, 6, 7, 8]
    - 壁 [[略][略][1, 1, **0**, 1][略][略][**0**, 1, 1, 1][略][略][略]]　となる。
- これを繰り返し、部屋番号が全て0になるまで繰り返す。ただし、同じ部屋番号の場合は処理しない。
- 全て0になったら、Excelに罫線で壁を記載して完了とする。


# コーディング
## colaboratoryからgoogleドライブを使えるようにする。
[External data: Drive, Sheets, and Cloud Storage](https://colab.research.google.com/notebooks/io.ipynb) にアクセスし、`Mounting Google Drive locally`を処理する。

```python
from google.colab import drive
drive.mount('/content/gdrive')
```

を実行すると、`authorization code`を聞いてくる。
`Go to this URL in a browser:`後ろのURLにアクセスし、`authorization code`を取得し、コピペする。
colaboratory の左メニューで`ファイル`を見ると`gdrive`がある。
あとは通常のファイルのようにアクセスする。
![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/304167/e303d0d8-bc10-b748-f802-4306826ac691.png)



## ライブラリ

```python
import openpyxl as xl
import numpy as np
from datetime import datetime
```

## 進行方向を取得
```python
def direction():
  '''
  進行方向を選択。
  戻り値は順に「現在地の破壊壁方向」「進行部屋の壁破壊方向」「進行部屋の部屋番号」
  '''
  rand = np.random.randint(0, high=4)
  if rand == 0: return 0, 2, -1 * INT_MAZE_SIZE  # 上
  if rand == 1: return 1, -1, 1  # 右
  if rand == 2: return 2, 0, INT_MAZE_SIZE  # 下
  return -1, 1, -1  # 左
```

## 進めるかチェック
```python
def check_cell(cells, c_cell, d_cell, dire):
  ''' 進めるかチェック '''
  # 進んだ部屋が迷路外かどうか
  # 上下へのはみ出し
  if d_cell < 0 or INT_MAZE_CELL <= d_cell: return False
  # 部屋番号+1が迷路サイズで割り切れる場合、右端の部屋。右には進めない。
  if (c_cell+1) % INT_MAZE_SIZE == 0 and dire == 1: return False
  # 部屋番号+1が迷路サイズで割り、余り１の場合、左端の部屋。左には進めない。
  if (c_cell+1) % INT_MAZE_SIZE == 1 and dire == -1: return False
  # 現在地部屋番号 = 進んだ部屋番号。同じ部屋の場合は処理しない
  if cells[c_cell] == cells[d_cell]: return False
  # それ以外は進める
  return True
```

## 小さい部屋番号を選択
```python
def choice_cell_no(cells, c_cell, d_cell):
  ''' 小さい方の部屋番号を取得 '''
  if cells[c_cell] < cells[d_cell]:
    return cells[c_cell], cells[d_cell]
  return cells[d_cell], cells[c_cell]
```

## 迷路を生成する
```python
def create_maze():
  ''' 迷路を生成 '''
  # 初期状態を生成
  # [上, 右, 下, 左] = 1:壁 / 0:通路
  lst_maze = np.ones([INT_MAZE_CELL, 4])
  # 部屋のナンバリング、最終的に全部0にする。
  lst_cells = np.array([i for i in range(0, INT_MAZE_CELL)])
  
  while True:
    # すべての部屋が番号0の時は処理終了
    if np.sum(lst_cells) == 0: break
    
    # 壁を破る元のセルを選択（現在地）
    int_choice_cell = np.random.randint(0, high=INT_MAZE_CELL)
    # 既にスタート地点とつながっている（0）時は処理しない。
    if lst_cells[int_choice_cell] == 0: continue
    
    # 進行方向を選択
    int_now_wall, int_direction_wall, int_direction = direction()
    # 進んだときのセル
    int_direction_cell = int_choice_cell + int_direction
    
    # 値のチェック
    if not check_cell(lst_cells, int_choice_cell, int_direction_cell, int_direction):
      continue
    
    # 部屋番号を確保
    int_min_value, int_max_value = choice_cell_no(lst_cells, int_choice_cell, int_direction_cell)
    
    # 小さい方の部屋番号に統一する
    lst_cells[lst_cells == int_max_value] = int_min_value
    
    # 壁も更新する
    lst_maze[int_choice_cell][int_now_wall] = 0
    lst_maze[int_direction_cell][int_direction_wall] = 0

  return lst_maze 
```

## Excelに出力
```python
def out_excel(maze):
  # 現在時刻をファイル名に使う
  str_now = datetime.now().strftime("%Y%m%d%H%M%S")

  # Excelを開く
  file_pass = '/content/gdrive/My Drive/Colab Notebooks/'
  xlsx_workbook = xl.load_workbook(file_pass+'maze.xlsx')
  xlsx_worksheet = xlsx_workbook.active

  # スタートとゴールを記載する
  cell_font = xl.styles.Font(name='Meiryo UI')
  cell_alig =  xl.styles.Alignment(horizontal='center', \
                                   vertical = 'center', \
                                   wrap_text=True)
  # スタート
  start_cell = xlsx_worksheet.cell(row=1, column=1)
  start_cell.value = 'S'
  start_cell.font = cell_font
  start_cell.alignment = cell_alig
  # ゴール
  end_cell = xlsx_worksheet.cell(row=INT_MAZE_SIZE, column=INT_MAZE_SIZE)
  end_cell.value = 'G'
  end_cell.font = cell_font
  end_cell.alignment = cell_alig

  # Excelに描画する。
  # セル数ループ
  r = 0
  for c, cell in enumerate(maze):
    # 罫線の変数
    border = xl.styles.Border()
    # 壁の数だけループ
    for d, wall in enumerate(cell):
      # 壁だけ処理
      if wall == 1:
        cell_style = xl.styles.Side(border_style='thin', color='000000')
        if d == 0:  # 上
          border.top = cell_style
        elif d == 1:  # 右
          border.right = cell_style
        elif d == 2:  # 下
          border.bottom = cell_style
        else:  # 左
          border.left = cell_style
    # 罫線を引くセルを選択
    xlsx_cell = xlsx_worksheet.cell(row=r+1, column=c-r*INT_MAZE_SIZE+1)
    xlsx_cell.border = border
    if (c+1) % INT_MAZE_SIZE == 0: r += 1
  
  xlsx_workbook.save(file_pass+'maze_'+str_now+'.xlsx')
```

## いざ実行
```python
INT_MAZE_SIZE = int(input('maze size->'))  # 迷路の1辺の長さ
INT_MAZE_CELL = INT_MAZE_SIZE * INT_MAZE_SIZE  # 迷路のセル数

# 迷路を生成
out_maze = create_maze()

out_excel(out_maze)
```

# 補足
- なぜかgoogle driveで開くと罫線が欠ける
- colaboratoryのファイルをダブルクリックで開くと、ちゃんと罫線が引けている。謎。
- `壁` が強化学習で使用できる。
- A4でいい感じに印刷できるのはN=33ぐらい？