シェルスクリプト:マインスイーパーゲーム

 シェルスクリプトは、システム管理だけでなく、ゲームの作成にも活用できる強力なツールです。ここでは、シェルスクリプトを使って「マインスイーパーゲーム」を作成し、その手順と詳細な解説を行います。この演習を通じて、シェルスクリプトの基本的な構文、配列の操作、条件分岐、ループ、関数の定義と利用などの知識を総合的に学ぶことができます。

マインスイーパーゲームの概要

ゲームの目的

  • 隠された地雷を避けながら、すべての安全なマスを開けること。

ゲームのルール

  • ボードの設定
    ・M×N のグリッドを使用します(今回は 5×5 とします)。
    ・ボード上にはランダムに配置された地雷(M)が存在します(今回は5つの地雷を配置します)。
  • ゲームの進行
    ・プレイヤーは座標を指定してマスを開けます(例:A1, B3)。
    ・マスを開けたときに地雷があればゲームオーバー。
    ・開けたマスに地雷がない場合、周囲8マスにある地雷の数が表示されます。
    ・すべての安全なマスを開ければ勝利。

ボードの例

12345
A
B
C
D
E

minesweeper.shの作成から実行までの手順と解説

ステップ1:スクリプトファイルの作成

ターミナルで以下のコマンドを実行し、新しいスクリプトファイル minesweeper.sh を作成します。

user01@ubuntu:~$ nano minesweeper.sh

ステップ2:スクリプトの内容を記述

エディタが開いたら、以下のコードをコピーして貼り付けます。

#!/bin/bash

# ボードサイズと地雷の数
ROWS=5
COLS=5
MINES=5

# ボードの初期化
initialize_board() {
    for ((i=0; i<ROWS*COLS; i++)); do
        board[$i]="."
        display_board[$i]="#"
    done
}

# 地雷の配置
place_mines() {
    mines_placed=0
    while [ $mines_placed -lt $MINES ]; do
        position=$((RANDOM % (ROWS * COLS)))
        if [ "${board[$position]}" != "M" ]; then
            board[$position]="M"
            mines_placed=$((mines_placed + 1))
        fi
    done
}

# 数字の計算
calculate_numbers() {
    for ((i=0; i<ROWS; i++)); do
        for ((j=0; j<COLS; j++)); do
            index=$((i * COLS + j))
            if [ "${board[$index]}" != "M" ]; then
                count=0
                for ((x=-1; x<=1; x++)); do
                    for ((y=-1; y<=1; y++)); do
                        ni=$((i + x))
                        nj=$((j + y))
                        nindex=$((ni * COLS + nj))
                        if [ $ni -ge 0 ] && [ $ni -lt $ROWS ] && [ $nj -ge 0 ] && [ $nj -lt $COLS ]; then
                            if [ "${board[$nindex]}" == "M" ]; then
                                count=$((count + 1))
                            fi
                        fi
                    done
                done
                board[$index]=$count
            fi
        done
    done
}

# ボードの表示
display() {
    echo "  1 2 3 4 5"
    rows=("A" "B" "C" "D" "E")
    for ((i=0; i<ROWS; i++)); do
        echo -n "${rows[$i]} "
        for ((j=0; j<COLS; j++)); do
            index=$((i * COLS + j))
            echo -n "${display_board[$index]} "
        done
        echo
    done
}

# 座標をインデックスに変換
coordinate_to_index() {
    declare -A row_map=( ["A"]=0 ["B"]=1 ["C"]=2 ["D"]=3 ["E"]=4 )
    row_letter=${1^^}
    col_number=$2
    row=${row_map[$row_letter]}
    col=$((col_number - 1))
    index=$((row * COLS + col))
    echo $index
}

# 安全なマスの数を計算
count_safe_cells() {
    safe_cells=0
    for cell in "${display_board[@]}"; do
        if [ "$cell" == "#" ]; then
            safe_cells=$((safe_cells + 1))
        fi
    done
    echo $safe_cells
}

# ゲームの初期化
initialize_board
place_mines
calculate_numbers

echo "マインスイーパーゲームへようこそ!"

# メインゲームループ
while true; do
    display
    read -p "マスを選択してください(例:A1): " input
    row_input=${input:0:1}
    col_input=${input:1}
    if [[ ! "$row_input" =~ [A-Ea-e] ]] || [[ ! "$col_input" =~ [1-5] ]]; then
        echo "エラー: A1からE5の範囲で入力してください。"
        continue
    fi
    index=$(coordinate_to_index "$row_input" "$col_input")
    if [ "${display_board[$index]}" != "#" ]; then
        echo "既に開けたマスです。別のマスを選んでください。"
        continue
    fi
    if [ "${board[$index]}" == "M" ]; then
        echo "ゲームオーバー!地雷を踏みました。"
        # 全ての地雷を表示
        for ((i=0; i<ROWS*COLS; i++)); do
            if [ "${board[$i]}" == "M" ]; then
                display_board[$i]="M"
            fi
        done
        display
        break
    else
        display_board[$index]="${board[$index]}"
        remaining_cells=$(count_safe_cells)
        if [ $remaining_cells -eq $MINES ]; then
            echo "おめでとうございます!すべての安全なマスを開けました。"
            display
            break
        fi
    fi
done

ステップ3:スクリプトの内容の解説

1.シェバン行

#!/bin/bash
  • スクリプトが Bash シェルで実行されることを指定します。

2.ボードサイズと地雷の数の設定

ROWS=5
COLS=5
MINES=5
  • 目的:ボードの行数、列数、地雷の数を設定します。

3.ボードの初期化関数 initialize_board

initialize_board() {
    for ((i=0; i<ROWS*COLS; i++)); do
        board[$i]="."
        display_board[$i]="#"
    done
}

目的

  • 内部的なボード board と表示用のボード display_board を初期化します。
  • board:実際の地雷や数字を保持します。
  • display_board:ユーザーに見せるボードで、未開封のマスは # で表示します。

4.地雷の配置関数 place_mines

place_mines() {
    mines_placed=0
    while [ $mines_placed -lt $MINES ]; do
        position=$((RANDOM % (ROWS * COLS)))
        if [ "${board[$position]}" != "M" ]; then
            board[$position]="M"
            mines_placed=$((mines_placed + 1))
        fi
    done
}
  • 目的:ボード上に指定された数の地雷をランダムに配置します。
  • 解説
    RANDOM を使用してボード上の位置をランダムに選択します。
    ・既に地雷が配置されていないか確認し、配置します。

5.数字を計算する関数 calculate_numbers

calculate_numbers() {
    for ((i=0; i<ROWS; i++)); do
        for ((j=0; j<COLS; j++)); do
            index=$((i * COLS + j))
            if [ "${board[$index]}" != "M" ]; then
                count=0
                for ((x=-1; x<=1; x++)); do
                    for ((y=-1; y<=1; y++)); do
                        ni=$((i + x))
                        nj=$((j + y))
                        nindex=$((ni * COLS + nj))
                        if [ $ni -ge 0 ] && [ $ni -lt $ROWS ] && [ $nj -ge 0 ] && [ $nj -lt $COLS ]; then
                            if [ "${board[$nindex]}" == "M" ]; then
                                count=$((count + 1))
                            fi
                        fi
                    done
                done
                board[$index]=$count
            fi
        done
    done
}
  • 目的:地雷でないマスに、周囲の地雷の数を計算して設定します。
  • 解説
    ・各マスについて、周囲8マスをチェックします。
    ・ボードの境界を超えないように条件を設定しています。

6.ボードの表示関数 display

display() {
    echo "  1 2 3 4 5"
    rows=("A" "B" "C" "D" "E")
    for ((i=0; i<ROWS; i++)); do
        echo -n "${rows[$i]} "
        for ((j=0; j<COLS; j++)); do
            index=$((i * COLS + j))
            echo -n "${display_board[$index]} "
        done
        echo
    done
}
  • 目的:ユーザーに見えるボードを表示します。
  • 解説
    ・行と列のラベルを表示。
    display_board 配列を使用してボードを表示。

7.座標をインデックスに変換する関数 coordinate_to_index

coordinate_to_index() {
    declare -A row_map=( ["A"]=0 ["B"]=1 ["C"]=2 ["D"]=3 ["E"]=4 )
    row_letter=${1^^}
    col_number=$2
    row=${row_map[$row_letter]}
    col=$((col_number - 1))
    index=$((row * COLS + col))
    echo $index
}
  • 目的:ユーザーの入力した座標をボードのインデックスに変換します。
  • 解説
    ・大文字小文字を統一するために ${1^^} を使用して大文字に変換。
    ・行の文字を数値にマッピング。

8.安全なマスの数を計算する関数 count_safe_cells

count_safe_cells() {
    safe_cells=0
    for cell in "${display_board[@]}"; do
        if [ "$cell" == "#" ]; then
            safe_cells=$((safe_cells + 1))
        fi
    done
    echo $safe_cells
}
  • 目的:まだ開けていない安全なマスの数を計算します。
  • 解説
    display_board 配列を走査し、# の数をカウント。

9.ゲームの初期化

initialize_board
place_mines
calculate_numbers

echo "マインスイーパーゲームへようこそ!"

解説

  • ボードを初期化し、地雷を配置し、数字を計算します。
  • ゲーム開始のメッセージを表示。

10.メインゲームループ

while true; do
    display
    read -p "マスを選択してください(例:A1): " input
    row_input=${input:0:1}
    col_input=${input:1}
    if [[ ! "$row_input" =~ [A-Ea-e] ]] || [[ ! "$col_input" =~ [1-5] ]]; then
        echo "エラー: A1からE5の範囲で入力してください。"
        continue
    fi
    index=$(coordinate_to_index "$row_input" "$col_input")
    if [ "${display_board[$index]}" != "#" ]; then
        echo "既に開けたマスです。別のマスを選んでください。"
        continue
    fi
    if [ "${board[$index]}" == "M" ]; then
        echo "ゲームオーバー!地雷を踏みました。"
        # 全ての地雷を表示
        for ((i=0; i<ROWS*COLS; i++)); do
            if [ "${board[$i]}" == "M" ]; then
                display_board[$i]="M"
            fi
        done
        display
        break
    else
        display_board[$index]="${board[$index]}"
        remaining_cells=$(count_safe_cells)
        if [ $remaining_cells -eq $MINES ]; then
            echo "おめでとうございます!すべての安全なマスを開けました。"
            display
            break
        fi
    fi
done

解説

  • ユーザーから座標を入力してもらいます。
  • 入力のバリデーションを行い、正しい範囲の座標か確認します。
  • 既に開けたマスでないかをチェックします。
  • 地雷を踏んだ場合、ゲームオーバーとして全ての地雷を表示します。
  • 安全なマスを開けた場合、表示ボードを更新します。
  • 残りの安全なマスが地雷の数と同じになったら、ゲームクリアです。

ステップ4:スクリプトに実行権限を付与

スクリプトを保存してエディタを終了したら、実行権限を付与します。

user01@ubuntu:~$ chmod +x minesweeper.sh

ステップ5:スクリプトの実行

スクリプトを実行して、マインスイーパーゲームを開始します。

user01@ubuntu:~$ ./minesweeper.sh

実行例

マインスイーパーゲームへようこそ!
  1 2 3 4 5
A # # # # #
B # # # # #
C # # # # #
D # # # # #
E # # # # #
マスを選択してください(例:A1): B2
  1 2 3 4 5
A # # # # #
B # 1 # # #
C # # # # #
D # # # # #
E # # # # #
マスを選択してください(例:A1): A1
(中略)
おめでとうございます!すべての安全なマスを開けました。

解説

  • ユーザーは座標を入力してマスを開けます。
  • 開けたマスが地雷でない場合、周囲の地雷の数が表示されます。
  • 地雷を踏んだ場合、ゲームオーバーとなり、全ての地雷が表示されます。
  • すべての安全なマスを開けるとゲームクリアとなります。

ここで作成したファイルの削除

ゲームのテストが終了したら、作成した minesweeper.sh ファイルを削除します。

user01@ubuntu:~$ rm minesweeper.sh

まとめ

  • シェルスクリプトの応用:マインスイーパーゲームを通じて、シェルスクリプトでのゲーム作成方法を学びました。
  • 配列の操作:ボードや表示用ボードを配列で管理し、効率的にデータを扱いました。
  • 関数の定義と利用:再利用可能なコードを関数として定義し、コードの可読性と保守性を向上させました。
  • ランダム性の導入$RANDOM 変数を使用して、地雷の配置にランダム性を持たせました。
  • 条件分岐とループif 文や while ループを使って、ゲームの進行とロジックを制御しました。
  • 文字列操作:ユーザーの入力を解析し、座標をインデックスに変換するなどの文字列操作を行いました。

 シェルスクリプトは、システム管理以外にも多くの用途で活用できます。今回のマインスイーパーゲームを基に、ボードサイズの拡大や地雷の数の変更、フラグ機能の追加など、さらなる機能拡張に挑戦してみてください。