🔰

オセロアプリ開発の過程 ~ChatGPTとのやり取り~一日目

に公開

オセロアプリ開発の道のり ~Pythonでオセロゲームを作る過程~

開発の背景と目的

百個アプリでも作ればプログラミングが上手くなるだろうなな考えスタートです、就活中でポートフォリオも作りたいし

どんなアプリを作ろうとしたのか?

普通のオセロ

このアプリでは、コンピュータとの対戦ができる基本的なオセロを目指しました。プレイヤーは盤面に石を置いていき、コンピュータも適当な手を打ってくれる形です。

使用した技術とツール

ゲーム開発に必要な基本的なツールと技術について紹介します。

Python

チャットGPT

ChatGPT(2025年4月23日のGPT-4o)を利用して、ゲーム開発中に出てきた問題を解決しました。具体的には、勝敗表示やプレイヤーの手番の進行などに関する問題を助けてもらいました。
ほぼコイツ任せです。

開発過程

オセロアプリ開発には、一時間ほど、
最終的に動くアプリを作り上げることができました。その過程を簡単に振り返ります。

最初のプロンプトのやり取り

最初にChatGPTにリクエストした内容は以下の通りです:

オセロのプログラミングコードを書きなさい
なるべくシンプルにコードを
tkinterでアプリ化
色をつけること
コンピューターと対戦できるように

このリクエストをもとに、オセロアプリの基本的な部分がスタートしました。

勝敗表示の追加

初期段階では、コメントとゲーム終了時に勝敗が表示されませんでした。そこで、勝敗表示を追加するために以下のようにChatGPTにリクエストしました:

後すべてのコードにおいてコメントでプログラムの説明
勝敗の表示を追加すること
自分の手番と相手の手番を表記すること

これにより、ゲーム終了時に勝者が表示されるようになりました。

自分の手番がスキップされるように修正

次に、置ける場所がない時にプレイヤーの手番でスキップされない問題が発生しました。ChatGPTに以下のように依頼し、問題を解決しました:

自分の手番でスキップされるように修正してほしい

これにより、手番が正しく順番通りに進行するようになりました。

完成したオセロアプリの特徴

ユーザーインターフェース

Tkinterを使ったシンプルなGUIが完成しました。ユーザーは盤面をクリックすることで石を置くことができ、コンピュータの手も自動的に表示されます。

ゲームの流れ

  1. プレイヤーが先攻、後攻を選択
  2. プレイヤーが石を置く
  3. コンピュータが適当な手を打つ
  4. 盤面が埋まる、または両者が置けなくなるまで2, 3を繰り返す
  5. ゲーム終了時に勝敗を表示する
  6. リスタート

ランダムで選択するだけのCPU

CPUは、盤面の中で置ける場所に石を置くシンプルなシステムを採用しました。

今後の改善点

難易度調整

現時点では、CPUは簡単に倒せてしまいます。今後は、アルゴリズムを取り入れて、難易度を調整できるようにしたいと考えています。例えば、ミニマックス法を使った最適手探索などです。

まとめ

AIコーディングでのオセロアプリの開発を通して、Pythonでのゲーム開発の楽しさや難しさをあまり実感はできない。
シンプルなゲームでも、基礎的な学習が足りない作ることができない!
学ぶべきことがたくさんあるがしかし基礎が足りない、コーディングの勉強、練習は必須である

上手くプロンプトで指示を出せる能力も必要だがやはりツールを制御する人間がナンセンスならコードもナンセンスだ

もし興味があれば、コードを下部で公開しているので、ナンセンス具合を確認してください



コード

import tkinter as tk
import random
SIZE = 8
CELL_SIZE = 60
EMPTY, BLACK, WHITE = 0, 1, 2
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1),
              (0, -1),          (0, 1),
              (1, -1), (1, 0), (1, 1)]
class Othello:
    def __init__(self, master):
        self.master = master
        self.master.title("Othello")

        # 先攻・後攻ボタンの表示
        self.start_frame = tk.Frame(master)
        self.start_frame.pack()
        self.black_button = tk.Button(self.start_frame, text="黒(先攻)で開始", command=lambda: self.start_game(BLACK))
        self.white_button = tk.Button(self.start_frame, text="白(後攻)で開始", command=lambda: self.start_game(WHITE))
        self.black_button.pack(side="left")
        self.white_button.pack(side="left")

        self.info_label = tk.Label(master, text="先攻を選んでください")
        self.info_label.pack()

        self.canvas = tk.Canvas(master, width=SIZE*CELL_SIZE, height=SIZE*CELL_SIZE)
        self.canvas.pack()
        self.canvas.bind("<Button-1>", self.click)

        self.restart_button = tk.Button(master, text="リスタート", command=self.reset_game)
        self.restart_button.pack()
        self.restart_button.pack_forget()

        self.turn = None
        self.board = None
        self.game_active = False # ゲームが進行中かどうかのフラグを追加

    def start_game(self, player_color):
        self.player_color = player_color
        self.computer_color = WHITE if player_color == BLACK else BLACK
        self.turn = BLACK
        self.game_active = True # ゲーム開始状態にする

        self.start_frame.pack_forget()
        self.reset_board()
        self.draw_board()
        self.update_info() # 最初の手番情報を表示
        # コンピュータが先攻の場合、少し待ってから手を打つ
        if self.turn != self.player_color:
            self.master.after(500, self.computer_move)

    def reset_board(self):
        self.board = [[EMPTY]*SIZE for _ in range(SIZE)]
        self.board[3][3], self.board[4][4] = WHITE, WHITE
        self.board[3][4], self.board[4][3] = BLACK, BLACK

    def reset_game(self):
        self.game_active = False # ゲームを非アクティブに
        self.restart_button.pack_forget()
        self.start_frame.pack()
        self.info_label.config(text="先攻を選んでください")
        self.canvas.delete("all") # リスタート時に盤面をクリア

    def draw_board(self):
        self.canvas.delete("all")
        for i in range(SIZE):
            for j in range(SIZE):
                x0, y0 = j*CELL_SIZE, i*CELL_SIZE
                x1, y1 = x0 + CELL_SIZE, y0 + CELL_SIZE
                self.canvas.create_rectangle(x0, y0, x1, y1, fill="green", outline="black") # 枠線を追加
                if self.board[i][j] == BLACK:
                    self.canvas.create_oval(x0+5, y0+5, x1-5, y1-5, fill="black")
                elif self.board[i][j] == WHITE:
                    self.canvas.create_oval(x0+5, y0+5, x1-5, y1-5, fill="white")

    def click(self, event):
        # ゲームがアクティブでない、またはプレイヤーのターンでない場合は無視
        if not self.game_active or self.turn != self.player_color:
            return

        row, col = event.y // CELL_SIZE, event.x // CELL_SIZE

        # 有効な手かどうかを確認
        if self.valid_move(row, col, self.player_color):
            self.make_move(row, col, self.player_color)
            self.draw_board()
            self.turn = self.computer_color
            self.update_info() # コンピュータの手番表示(スコア含む)
            # 少し待ってからコンピュータの手を実行
            self.master.after(500, self.computer_move)
        else:
            # クリックした場所が無効な場合、他に打てる手があるか確認
            player_can_move = any(self.valid_move(i, j, self.player_color) for i in range(SIZE) for j in range(SIZE))
            if not player_can_move:
                # --- パス処理 ---
                # プレイヤーが打てる手がない場合 -> パス
                self.info_label.config(text="あなたはパスしました。コンピュータの番です。")
                self.turn = self.computer_color
                # 少し待ってからコンピュータの手を実行(パスの後なので少し長めに待つ)
                self.master.after(1000, self.computer_move)
            else:
                # 打てる手はあるが、クリックした場所が悪いだけ
                # 必要であれば一時的なフィードバックを表示
                # self.info_label.config(text="そこには置けません。")
                pass # 特にメッセージは出さない(クリックしても何も起こらない)

    def valid_move(self, row, col, color):
        # 最初に範囲外チェックと空きマスチェックを行う
        if not (0 <= row < SIZE and 0 <= col < SIZE and self.board[row][col] == EMPTY):
             return False

        opponent_color = 3 - color # 相手の色を計算
        for dr, dc in DIRECTIONS:
            r, c = row + dr, col + dc
            flipped = []
            # 相手の石が連続している間、リストに追加
            while 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == opponent_color:
                flipped.append((r, c))
                r += dr
                c += dc
            # 連続が終わった先が自分の石であれば、有効な手
            if flipped and 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == color:
                return True
        # どの方向にも返せる石がなければ無効
        return False

    def make_move(self, row, col, color):
        self.board[row][col] = color
        opponent_color = 3 - color # 相手の色
        stones_to_flip = [] # この手で反転する石のリスト

        for dr, dc in DIRECTIONS:
            r, c = row + dr, col + dc
            flipped_in_direction = []
            while 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == opponent_color:
                flipped_in_direction.append((r, c))
                r += dr
                c += dc
            # 自分の石で挟めていれば、反転リストに追加
            if flipped_in_direction and 0 <= r < SIZE and 0 <= c < SIZE and self.board[r][c] == color:
                stones_to_flip.extend(flipped_in_direction)

        # リストにある石をすべて反転させる
        for fr, fc in stones_to_flip:
            self.board[fr][fc] = color

    def computer_move(self):
        # ゲームがアクティブでない場合は何もしない
        if not self.game_active:
            return

        # コンピュータが打てる手があるか確認
        valid_moves = [(i, j) for i in range(SIZE) for j in range(SIZE) if self.valid_move(i, j, self.computer_color)]

        if valid_moves:
            # 打てる手がある場合、ランダムに選択して打つ
            move = random.choice(valid_moves)
            self.make_move(*move, self.computer_color)
            self.draw_board()
            self.turn = self.player_color
            self.update_info() # プレイヤーの手番表示(スコア含む)
        else:
            # --- パス処理 ---
            # コンピュータが打てる手がない場合 -> パス
            self.info_label.config(text="コンピュータはパスしました。あなたの番です。")
            self.turn = self.player_color
            # 少し待ってからプレイヤーの手番表示を更新(ゲーム終了チェックも兼ねる)
            # ここで update_info を呼ぶことで、プレイヤーも打てない場合にゲーム終了判定が正しく行われる
            self.master.after(1000, self.update_info)

    def update_info(self):
        # ゲームがアクティブでない場合は何もしない
        if not self.game_active:
             # ゲーム終了後やリセット後に呼ばれた場合は何もしないか、最終結果を維持
             # (現状では game_active フラグで制御しているので、ここに来る場合はほぼないはず)
             return

        black_count = sum(row.count(BLACK) for row in self.board)
        white_count = sum(row.count(WHITE) for row in self.board)
        empty_count = sum(row.count(EMPTY) for row in self.board)

        # 現在の手番のプレイヤーが打てるか確認
        current_player_can_move = any(self.valid_move(i, j, self.turn) for i in range(SIZE) for j in range(SIZE))
        # 相手の手番のプレイヤーが打てるか確認 (次のターンでパスが必要かどうかの判定用)
        opponent_color = 3 - self.turn
        opponent_can_move = any(self.valid_move(i, j, opponent_color) for i in range(SIZE) for j in range(SIZE))

        # ゲーム終了条件: 盤面が埋まった or 両者とも打てない
        if empty_count == 0 or (not current_player_can_move and not opponent_can_move):
            self.game_active = False # ゲーム終了状態にする
            if black_count > white_count:
                result = "黒の勝ち!"
            elif white_count > black_count:
                result = "白の勝ち!"
            else:
                result = "引き分け"

            # プレイヤー視点での勝敗メッセージ
            if (self.player_color == BLACK and black_count > white_count) or \
               (self.player_color == WHITE and white_count > black_count):
                final_message = f"あなたの勝ち! ({result})"
            elif (self.player_color == BLACK and white_count > black_count) or \
                 (self.player_color == WHITE and black_count > white_count):
                final_message = f"コンピュータの勝ち ({result})"
            else: # 引き分け
                 final_message = result

            self.info_label.config(text=f"ゲーム終了:黒 {black_count} - 白 {white_count}{final_message}")
            self.restart_button.pack() # リスタートボタン表示
        else:
            # ゲーム続行
            # 通常の手番表示 (スコアも表示)
            if self.turn == self.player_color:
                turn_text = "あなたの番です(黒)" if self.player_color == BLACK else "あなたの番です(白)"
            else: # コンピュータのターン
                turn_text = "コンピューターの番です(白)" if self.computer_color == WHITE else "コンピューターの番です(黒)"

            # パスが発生した直後(computer_moveからafterで呼ばれた場合など)でも、
            # この時点で打てる手がない場合はパス処理が必要。
            # ただし、パス処理は click と computer_move で行うため、ここでは通常表示のみ。
            # もし現在の手番のプレイヤーが打てない場合(相手がパスしてきた直後など)、
            # そのプレイヤーはクリックしても無効となり、click内のパス処理が起動するか、
            # コンピュータなら computer_move 内のパス処理が起動する。
            self.info_label.config(text=f"{turn_text} (黒:{black_count} 白:{white_count})")

            # --- 重要:update_info内での自動パス処理は削除 ---
            # 以前のコードにあった、ここで現在の手番プレイヤーが打てない場合に自動でターンを交代させる処理は、
            # click と computer_move での明示的なパス処理に置き換えたため不要。
if __name__ == "__main__":
    root = tk.Tk()
    game = Othello(root)
    root.mainloop()


Discussion