ChatGPT-4に相談しながらモジュール単位でマインスイーパを作る1

　ChatGPT-4が公開されてから、個人的にいろんな質問を試してみています。ワンライナーから50行程度?くらいまでで済むようなプログラムですと、ChatGPT-4は素晴らしい回答をしてくれました。しかし、ChatGPT-4が一回の回答で答えられる範囲を超えるような複雑な要求をすると、タイムアウトになったり、極端に応答が遅くなってしまいなかなか思うような回答が得られない状況に陥ってしまいました。そこで一回の回答でChatGPT-4が答える範囲を狭め、モジュール単位の会話を繰り返すことで比較的大きなアプリケーションも開発できるのではないかと考え、ウォータフォール風味な手法でマインスイーパゲームを作ってみることにしました。

　注)アクセス集中等による短期的なものだったり偶然だったのかもしれないですが、今回の試みを始めたときに、日本語で質問するとタイムアウトエラーになってしまうことが続いてしまい、英語で質問すると回答が返ってくるという状況だったため、一連の会話はすべて英語で行われており、ここに掲載するChatGPT-4の回答は訳文です。

## 要件定義

　いずれにせよ、まずは要件定義からです。マインスイーパに必要な要件を聞いたところこのように返ってきました。

1. グリッドの大きさ。グリッドのサイズ：グリッドの寸法を決定します。グリッドは通常、長方形のマスのフィールドです。一般的なグリッドサイズは、9x9、16x16、30x16などです。
2. 地雷の数。地雷の数：グリッド内に隠された地雷の総数を指定します。ゲームの難易度は、地雷の密度によって決まることが多い。例えば、初級者は10個、中級者は40個、上級者は99個という具合です。
3. セルの状態 マス目の状態：マス目には、「覆われている」「覆われていない」「旗がある」の3つの状態を設定します。覆われているマスはその内容を隠すことができ、覆われていないマスは空か、地雷があるか、隣接する地雷の数を表示することができます。旗のあるマスは、プレイヤーが地雷の疑いのある場所をマークすることができる。
4. ゲームロジック
a. プレイヤーが地雷のないセルを発見した場合、ゲームは隣接する地雷の数（0～8）を表示する必要があります。隣接する地雷がない場合、ゲームは自動的にすべての隣接するセルを発掘し、再帰的につながっている空のセルを明らかにする。
b. プレーヤーが地雷のあるセルを発見した場合、ゲームは終了し、すべての地雷の位置を表示する。
c. プレイヤーは地雷の疑いがあるセルに旗を立てることができる。プレイヤーは必要に応じて旗を消すことができるべきである。
d. 地雷のないセルをすべて発見した時点でゲームに勝利する。
5. タイマー：各ゲームの時間を計測するタイマーを設置することで、プレイヤーは自分の進捗状況を把握し、より早い完了時間を競うことができる。
6. 難易度設定 グリッドサイズや地雷の密度が異なる複数の難易度を用意し、プレイヤーのスキルレベルに対応します。
7. ユーザーインターフェース（UI）。グリッド、タイマー、地雷カウンター、関連するボタン（リセット、難易度変更など）を表示する、クリーンで直感的、かつレスポンシブなUIを設計します。
8. スコアリングと統計 難易度別のベストスコアなど、プレイヤーのスコアを記録し、ゲームインターフェイスに表示します。
9. エラーの処理 ユーザー入力のエラーに適切に対処し、必要な場合はプレイヤーに有益なフィードバックを提供します。
10. ファーストクリックの安全性 ゲームの最初のクリックで地雷が発見されないようにし、プレイヤーが公平にスタートできるようにします。これは、最初のクリックの後に地雷原を生成し、最初にクリックされたセルやそのすぐ隣のセルに地雷を配置しないようにすることで実現できます。

　マインスイーパが慣れ親しまれてるゲームだからなのか、詳細な回答が返ってきました。正直、要件定義でつまづいてこの試みは終わってしまうんじゃないかと思いながら試してみたのに一回でこれが返ってきてしまいました。

## 基本設計

　単に基本設計をしてくださいとだけお願いしたところ、次のような回答が返ってきました。

1. ゲーム盤。
    - グリッドを表すデータ構造（例：2次元配列や行列）を作成する。
    - 各セルは、状態（カバー、アンカバー、フラグ）と内容（空、地雷、隣接する地雷の数）を格納する必要があります。
2. ゲームロジック。
    - ユーザーのアクションを処理する関数を実装する（覆われないセル、フラグを立てるセル、フラグを解除するセル）。
    - 地雷原を初期化する関数を作成し、ファーストクリックの安全性を確保する。
    - 各セルの隣接する地雷の数を計算する関数を実装する。
    - 接続された空のセルを覆い隠すための再帰的な関数を開発する。
    - 有効な手を打つたびに、ゲームの勝ち負けをチェックする関数を実装する。
3. ユーザーインターフェース（UI）。
    - グリッド、タイマー、地雷カウンター、関連ボタン（リセット、難易度変更）を含むシンプルなレイアウトを設計する。
    - マスの発見、マスのフラグ立て、ゲームの勝敗など、プレイヤーがアクションを起こすたびにUIが更新されるようにする。
4. タイマー。
    - タイマーを開始、更新、UIに表示する機能を実装する。
    - タイマーは、最初のセルをクリックしたときに開始し、ゲームの勝敗が決まったら停止するようにします。
5. 難易度調整
    - 選択した難易度に応じて、グリッドのサイズと地雷の密度を調整する関数を作成します。
6. スコアリングと統計
    - 難易度別にプレイヤーのスコアやベストコンプリートタイムなどの統計情報を保存・表示するシステムを構築する。
7. エラー処理
    - 適切な入力検証を行い、ゲームプレイ中に発生する可能性のあるエッジケースに対処する。
    - 無効な操作の場合は、ユーザーに適切なフィードバックを行う。

　なぜか項目数が要件定義より少なくなってしまいました。いくつか要件定義で行っていることを忘れてしまっているものもあります。もっとよい聞き方があったのかもしれないですが、ここはこのまま会話を続けることにより、思い出してもらったり、何が必要なのかを会話上で追加していくくことにより基本設計を行いました。基本設計はこのくらいでいいかとChatGPT-4に相談していいですと回答が返ってきたくらいで基本設計を終えました。なお、ここで言語をPythonと指定しています。

## 詳細設計

　これまでの内容でMermaid形式でクラス図を作ってくださいとお願いして出てきたのが次のクラス図でした。基本設計で話していた内容が、結構なくなってしまってしまっていたのですが、話のたたき台にはなります。何もないのとは大違いです。


```mermaid
classDiagram
    class MinesweeperGame {
        +start_game()
        +select_difficulty()
        +display_statistics()
    }
    class Grid {
        +generate_grid()
        +place_mines()
        +reveal_cell()
        +flag_cell()
    }
    class Timer {
        +start_timer()
        +stop_timer()
        +get_time()
    }
    class Scoreboard {
        +update_high_score()
        +update_game_statistics()
        +display_high_score()
    }
    class Interface {
        +render_start_screen()
        +render_game_screen()
        +render_game_statistics_screen()
        +render_menu_bar()
        +render_pop_ups()
    }

    MinesweeperGame --> Grid
    MinesweeperGame --> Timer
    MinesweeperGame --> Scoreboard
    MinesweeperGame --> Interface
```

　ここから、ChatGPT-4との相談を経て以下のようなクラス図ができました。以下はChatGPT-4が出力したものですが、正しさという意味ではよくないです。相談の間に決めたことが一度反映されて、もう一度聞きなおすと忘れていたりなど繰り返すようになってしまったため、一定のところで、今わかっている全てのクラスを出力してみてと言って出してもらったものです。なお、長くなりすぎるので割愛していますが、これらすべての変数、関数にそれがどういうものかを説明する文もつけてくれました(それが正しいかはさておき)。

```mermaid
classDiagram
    class Interface {
        +root: tk.Tk
        +start_screen: StartScreen
        +game_screen: GameScreen
        +game_statistics_window: GameStatisticsWindow
        +grid: Grid
        +timer: Timer
        +scoreboard: Scoreboard

        +__init__(): None
        +initialize_game(difficulty: str): None
        +start(): None
        +show_game_over(result: str, time: int): None
    }
```
```mermaid
classDiagram
    class GameScreen {
        +root: tk.Tk
        +master: tk.Toplevel
        +canvas: tk.Canvas
        +score_label: tk.Label
        +timer_label: tk.Label
        +interface: Interface

        +__init__(root: tk.Tk, interface: Interface): None
        +create_ui_elements(): None
        +update_score_label(score: int): None
        +update_timer_label(time: int): None
        +draw_cell(x: int, y: int, color: str, text: str): None
        +handle_left_click(event): None
        +handle_right_click(event): None
    }
```
```mermaid
classDiagram
    class StartScreen {
        +root: tk.Tk
        +frame: tk.Frame
        +title_label: tk.Label
        +difficulty_var: tk.StringVar
        +difficulty_menu: ttk.Combobox
        +start_button: tk.Button
        +interface: Interface

        +__init__(root: tk.Tk, interface: Interface): None
        +setup_ui(): None
        +start_game(): None
    }
```
```mermaid
classDiagram
    class GameStatisticsWindow {
        -root: tk.Tk
        -scoreboard: Scoreboard
        +__init__(root: tk.Tk, scoreboard: Scoreboard): None
        +show(): None
        -create_widgets(): None
        -populate_statistics(): None
    }
```
```mermaid
classDiagram
    class Timer {
        +start_time: float
        +is_running: bool

        +__init__(): None
        +start_timer(): None
        +get_elapsed_time() -> float
    }
```
```mermaid
classDiagram
    class Grid {
        -rows: int
        -cols: int
        -mines: int
        -cells: List[List[Cell]]
        -mine_opened: bool

        +__init__(rows: int, cols: int, mines: int): None
        -initialize_grid(first_click_row: int, first_click_col: int): None
        -place_mines(exclude_row: int, exclude_col: int): None
        -calculate_adjacent_mines(): None
        +open_cell(row: int, col: int) -> Tuple[bool, List[Tuple[int, int]]]
        +flag_cell(row: int, col: int) -> Tuple[bool, bool]
        +check_game_status() -> str
    }
```
```mermaid
classDiagram
    class Scoreboard {
        -high_scores: Dict[str, List[Tuple[str, int]]]

        +__init__(): None
        +update_game_statistics(difficulty: str, won: bool, time: int): None
        +update_high_score(difficulty: str, name: str, time: int) -> Optional[int]
        +get_high_scores(difficulty: str) -> List[Tuple[str, int]]
    }
```

　ここから、ChatGPT-4と会話を続けて正しいクラス図にするのはちょっと難しいなと思いました。話が深まってきたせいか結構な頻度で前に話してたことが忘れられてしまうようになってしまったからです。ただ、正しくはないとはいえ、まるでだめでもないので、ここまでやってくれたら後はここを修正の土台にすることもできるのでないかと思います。次はこのクラス図を自分で修正したところから始めてChatGPT-4と一緒にコードにするところに挑戦してみたいと思います。

## 感想

　この試みを通じて、ChatGPT-4のすごいところと難しいところを体験できました。ただ、うまく使えていないだけの可能性も大いにあるので、いろんな方の使い方を見て学ばなくてはいけないなと思います。一度決めたことをこちらが指令するまで覆さないような工夫ができれば規模の大きなプログラムでもChatGPT-4との会話だけでウォータフォール開発できるかもしれません。(こちら側で常に設計項目やクラス図を保持して、一度の会話ごとにそれを確認し続け、何か変わっていたら自動的に戻させるような機能を用意するなど？一つ戻させたら連鎖的に他のものも変え続けて収集つかなくなるかもしれないですが)、ChatGPT-4は画像に対する入力もできるとのことで、紙の上に書いたデザインからウェブサイトを作るデモがありました。一般にはこの機能まだ使えないですが、使えたら基本設計の段階でUI設計もできるのではないかと期待します。

　また、ChatGPT-4は「これで抜けはないか？」みたいな聞き方をすると、かなりの確率でこれで十分ですと言ってくれることが多いです。その場の気分はとても良いですが、後から足りないとわかることが結構あるので、こういった聞き方はよくなかったです。もっと情報を引き出すように、ここで考えるべきことを列挙してくれなど話を絞り出す感じの聞き方の方がよさそうでした。