3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Anthropic Claude で出来るもん!Advent Calendar 2024

Day 16

Claudeとオセロで対戦するゲームをClaudeと作る

Posted at

こんな感じでClaudeとオセロをするゲームを作ります。

image.png

Claude 3.5 Sonnetのマルチモーダル機能を使ってClaudeに次の手を考えさせる作戦です。

image.png

もう一つポイントがあります。「自分でコーディングはしない」です。
コードはすべてClaudeに作らせます。

Anthropicのコンソールを使って行いました。
シンプルな見た目ですが、プロンプトジェネレーターがあったりするので、一度使ってみることをおすすめします。

まずはオセロをコーディング

まずはオセロのコードを作ります。

Webで少し検索すると、Pythonのnumpyやmatplotlibをつかって盤面を再現している方がいたのでその方法を取りたいと思います。

以下のプロンプトを実行しました。

Pythonのnumpyやmatplotlibを使ってオセロの盤面を作ってください。

- 横軸にA~Hのラベルを付けてください
- 縦軸に1~8のラベルを付けてください

- 石(disc)を置く関数 put_disc を作ってください。
    - put_dics関数の引数は x y color board の4つです。xとyは新しく石を置く位置を示します。xはa~hのみ(大文字小文字は区別しない)を受け付けます。yは1~8のみを受け付けます。colorはBLACKまたはWHITEのみを受け付けます。boardは現在の盤面の状態です。
    - 置けない位置に置かれた場合はエラーを返却してください。
    - 石を置くことで変化する部分の色を変えてください。
    - 色を変えたあとの盤面の状態をreturnしてください。

- 盤面の状態をチェックし勝敗がついたかどうかを判定する関数 judge を作ってください。
- 勝敗がついた場合は、勝った側の値(BLACKまたはWHITE)を返却してください。まだ勝敗がついていない場合は、Noneを返却してください。
- 盤面の状態をpng画像に出力する関数を作ってください。

Pythonのコードを作ってくれました。

生成されたPythonのコード
import numpy as np
import matplotlib.pyplot as plt

# 定数定義
EMPTY = 0
BLACK = 1
WHITE = 2

def create_board():
    """初期盤面を作成する"""
    board = np.zeros((8, 8), dtype=int)
    # 初期配置
    board[3:5, 3:5] = np.array([[WHITE, BLACK],
                               [BLACK, WHITE]])
    return board

def draw_board(board, filename=None):
    """盤面を描画する"""
    fig, ax = plt.subplots(figsize=(8, 8))
    
    # 背景を緑色に
    ax.set_facecolor('forestgreen')
    
    # グリッドを描画
    for i in range(9):
        ax.axhline(i, color='black', linewidth=1)
        ax.axvline(i, color='black', linewidth=1)
    
    # ラベルを設定
    ax.set_xticks(np.arange(8))
    ax.set_yticks(np.arange(8))
    ax.set_xticklabels(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
    ax.set_yticklabels(['1', '2', '3', '4', '5', '6', '7', '8'])
    
    # 石を描画
    for i in range(8):
        for j in range(8):
            if board[i, j] == BLACK:
                circle = plt.Circle((j + 0.5, 7.5 - i), 0.4, color='black')
                ax.add_patch(circle)
            elif board[i, j] == WHITE:
                circle = plt.Circle((j + 0.5, 7.5 - i), 0.4, color='white')
                ax.add_patch(circle)
    
    ax.set_aspect('equal')
    
    if filename:
        plt.savefig(filename)
    plt.close()

def convert_position(x, y):
    """座標を内部表現に変換する"""
    if not isinstance(x, str) or len(x) != 1:
        raise ValueError("x must be a single character")
    
    x = x.upper()
    if x not in 'ABCDEFGH':
        raise ValueError("x must be between A and H")
    
    if not isinstance(y, int) or y < 1 or y > 8:
        raise ValueError("y must be between 1 and 8")
    
    return 8 - y, ord(x) - ord('A')

def is_valid_move(board, row, col, color):
    """指定位置に石を置けるかチェックする"""
    if board[row, col] != EMPTY:
        return False
    
    directions = [(0,1), (1,0), (0,-1), (-1,0), 
                 (1,1), (-1,-1), (1,-1), (-1,1)]
    
    valid = False
    for dx, dy in directions:
        if check_direction(board, row, col, dx, dy, color):
            valid = True
    
    return valid

def check_direction(board, row, col, dx, dy, color):
    """特定の方向に石を裏返せるかチェックする"""
    opponent = WHITE if color == BLACK else BLACK
    x, y = row + dx, col + dy
    
    if not (0 <= x < 8 and 0 <= y < 8) or board[x, y] != opponent:
        return False
    
    while 0 <= x < 8 and 0 <= y < 8:
        if board[x, y] == EMPTY:
            return False
        if board[x, y] == color:
            return True
        x, y = x + dx, y + dy
    
    return False

def put_disc(x, y, color, board):
    """石を置く"""
    row, col = convert_position(x, y)
    
    if not is_valid_move(board, row, col, color):
        raise ValueError("Invalid move")
    
    new_board = board.copy()
    new_board[row, col] = color
    
    directions = [(0,1), (1,0), (0,-1), (-1,0), 
                 (1,1), (-1,-1), (1,-1), (-1,1)]
    
    for dx, dy in directions:
        if check_direction(board, row, col, dx, dy, color):
            x, y = row + dx, col + dy
            while board[x, y] != color:
                new_board[x, y] = color
                x, y = x + dx, y + dy
    
    return new_board

def judge(board):
    """勝敗を判定する"""
    black_count = np.sum(board == BLACK)
    white_count = np.sum(board == WHITE)
    empty_count = np.sum(board == EMPTY)
    
    if empty_count == 0:
        if black_count > white_count:
            return BLACK
        elif white_count > black_count:
            return WHITE
        else:
            return None
    
    # まだ空きマスがある場合
    return None

# 使用例
if __name__ == "__main__":
    board = create_board()
    draw_board(board, "initial_board.png")
    
    # 例: 石を置く
    try:
        board = put_disc('C', 4, BLACK, board)
        draw_board(board, "after_move.png")
    except ValueError as e:
        print(f"Error: {e}")
    
    # 勝敗判定
    result = judge(board)
    if result == BLACK:
        print("Black wins!")
    elif result == WHITE:
        print("White wins!")
    else:
        print("Game is not finished yet")

実行してみたところ、以下の画像が生成されました。

initial_board.png

とても良さげです!!

マス目の呼び名が分かりづらいと感じたので、以下のプロンプトでラベルを表示するように修正してもらいました。

マス目の中央に「A1」や「B2」など、そのマス目の座標を示すラベルを追加してください。文字のサイズは大きめにしてください。
文字色は黒にしてください。
ラベルは左上がA8で右下がH1になるはずです。

initial_board.png

かなり期待ができます。

StreamlitのUIをコーディング

では、対戦する画面を作りたいと思います。

人と生成AIが対戦するオセロゲームを作りたいです。
UI部分をStreamlitを使って実現したいです。

すでにオセロのロジックはothello.pyとして実装済みです。

```python:othello.py
{{先ほど生成したPythonプログラム}}
```

AIを呼び出す処理は以下の例を参考にしてください。

```python
import base64
import httpx

image1_url = "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg"
image1_media_type = "image/jpeg"
image1_data = base64.standard_b64encode(httpx.get(image1_url).content).decode("utf-8")

image2_url = "https://upload.wikimedia.org/wikipedia/commons/b/b5/Iridescent.green.sweat.bee1.jpg"
image2_media_type = "image/jpeg"
image2_data = base64.standard_b64encode(httpx.get(image2_url).content).decode("utf-8")

import anthropic

client = anthropic.Anthropic()
message = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": image1_media_type,
                        "data": image1_data,
                    },
                },
                {
                    "type": "text",
                    "text": "Describe this image."
                }
            ],
        }
    ],
)
```
生成されたPythonのコード
import streamlit as st
import base64
import io
import anthropic
from othello import *
import matplotlib.pyplot as plt

# Anthropic APIクライアントの初期化
client = anthropic.Anthropic()

def get_board_image(board):
    """盤面画像を取得してbase64エンコードする"""
    buf = io.BytesIO()
    draw_board(board)
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    return base64.b64encode(buf.getvalue()).decode()

def get_ai_move(board):
    """AIの手を取得する"""
    # 盤面画像をbase64エンコード
    board_image = get_board_image(board)
    
    # AIに盤面を送信して次の手を問い合わせ
    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": board_image,
                        },
                    },
                    {
                        "type": "text",
                        "text": "You are playing as White. Please analyze this Othello board and provide your next move in the format 'X#' where X is a letter A-H and # is a number 1-8."
                    }
                ],
            }
        ],
    )
    
    # AIの応答から手を抽出
    ai_move = message.content[0].text.strip()
    return ai_move[0], int(ai_move[1])

def main():
    st.title("Othello vs AI")
    
    # セッションステートの初期化
    if 'board' not in st.session_state:
        st.session_state.board = create_board()
        st.session_state.game_over = False
    
    # 盤面の表示
    board_image = get_board_image(st.session_state.board)
    st.image(f"data:image/png;base64,{board_image}")
    
    # プレイヤーの手の入力
    if not st.session_state.game_over:
        col1, col2 = st.columns(2)
        with col1:
            x = st.selectbox("Column (A-H)", list("ABCDEFGH"))
        with col2:
            y = st.number_input("Row (1-8)", min_value=1, max_value=8)
        
        if st.button("Place Stone"):
            try:
                # プレイヤーの手を実行
                st.session_state.board = put_disc(x, y, BLACK, st.session_state.board)
                
                # 勝敗判定
                result = judge(st.session_state.board)
                if result is not None:
                    st.session_state.game_over = True
                else:
                    # AIの手を取得して実行
                    ai_x, ai_y = get_ai_move(st.session_state.board)
                    st.session_state.board = put_disc(ai_x, ai_y, WHITE, st.session_state.board)
                    
                    # 再度勝敗判定
                    result = judge(st.session_state.board)
                    if result is not None:
                        st.session_state.game_over = True
                
                st.experimental_rerun()
                
            except ValueError as e:
                st.error(f"Invalid move: {e}")
    
    # ゲーム終了時の表示
    if st.session_state.game_over:
        result = judge(st.session_state.board)
        if result == BLACK:
            st.success("You win!")
        elif result == WHITE:
            st.error("AI wins!")
        else:
            st.info("Draw!")
        
        if st.button("New Game"):
            st.session_state.board = create_board()
            st.session_state.game_over = False
            st.experimental_rerun()

if __name__ == "__main__":
    main()

実行してみましょう。

環境変数にANTHROPIC_API_KEYを指定しています。

streamlit run app.py

でました。構文エラーとかは出る気配が全く無いですね。

真っ白の部分に盤面の画像を出そうとしているのですが、実装がおかしいのか表示されません。

以下のプロンプトで修正してもらいます。

盤面の画像が出ていないので、改善してください。st.imageは、画像ファイルのパスを渡す事もできるようです。

無事、盤面の画像が表示されました。

C5においてみるとエラーになりました。

Claudeに次の手を考えさせた際のレスポンスが文章になっていることが原因のようです。

I'm seeing an empty Othello/Reversi board without the standard initial setup of four pieces in the center (which normally has two black and two white discs arranged diagonally at D4, E4, D5, and E5). Without the initial setup or any pieces on the board, I cannot suggest a valid move. Would you like me to explain the standard starting position and rules of Othello instead?

プロンプトを改善してもらいました。

生成AIのレスポンスが以下のような文字列のため、次のコマの位置を正しく取得できていません。プロンプトを改善してください。

I'm seeing an empty Othello/Reversi board without the standard initial setup of four pieces in the center (which normally has two black and two white discs arranged diagonally at D4, E4, D5, and E5). Without the initial setup or any pieces on the board, I cannot suggest a valid move. Would you like me to explain the standard starting position and rules of Othello instead?

一部の関数だけではなく、app.pyの全体を出力してください。

使えない関数を呼び出すようになってしまったので、以下のプロンプトを追加で実行しました。

st.experimental_rerunという関数は使えなくなっているので、代わりにst.rerunを使ってください。

と、なんやかんやプロンプトの指示だけでコードを再生成し続け、できました!!

Animation.gif

最終的なコード
app.py
import streamlit as st
import base64
import io
import anthropic
from othello import *
import matplotlib.pyplot as plt
import tempfile
import os

# Anthropic APIクライアントの初期化
client = anthropic.Anthropic()

def save_board_image(board):
    """盤面画像を一時ファイルとして保存し、そのパスを返す"""
    temp_dir = tempfile.gettempdir()
    temp_path = os.path.join(temp_dir, 'othello_board.png')
    draw_board(board, temp_path)
    return temp_path

def get_board_image_base64(board):
    """AIに送信用の盤面画像をbase64エンコードする"""
    buf = io.BytesIO()
    draw_board(board)
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    return base64.b64encode(buf.getvalue()).decode()

def get_valid_moves(board, color):
    """有効な手のリストを取得する"""
    valid_moves = []
    for i in range(8):
        for j in range(8):
            if is_valid_move(board, i, j, color):
                # 内部座標を変換して追加
                col = chr(j + ord('A'))
                row = 8 - i
                valid_moves.append(f"{col}{row}")
    return valid_moves

def get_ai_move(board):
    """AIの手を取得する"""
    # 有効な手のリストを取得
    valid_moves = get_valid_moves(board, WHITE)
    if not valid_moves:
        return None, None

    # 盤面画像をbase64エンコード
    board_image = get_board_image_base64(board)
    
    # AIに盤面を送信して次の手を問い合わせ
    prompt = f"""You are playing as White in this Othello/Reversi game.
Valid moves available to you are: {', '.join(valid_moves)}
Analyze the board and choose one move from the valid moves listed above.
Respond with ONLY the move coordinate (e.g., 'E6') - no other text."""

    message = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": board_image,
                        },
                    },
                    {
                        "type": "text",
                        "text": prompt
                    }
                ],
            }
        ],
    )
    
    # AIの応答を出力
    ai_response = message.content[0].text.strip()
    print("AI's response:", ai_response)
    
    # 応答が有効な手かチェック
    if ai_response in valid_moves:
        return ai_response[0], int(ai_response[1])
    else:
        # 有効な手でない場合は、最初の有効な手を使用
        print(f"Invalid AI response. Using first valid move: {valid_moves[0]}")
        return valid_moves[0][0], int(valid_moves[0][1])

def main():
    st.title("Othello vs AI")
    
    # セッションステートの初期化
    if 'board' not in st.session_state:
        st.session_state.board = create_board()
        st.session_state.game_over = False
    
    # 盤面の画像を保存して表示
    board_path = save_board_image(st.session_state.board)
    st.image(board_path)
    
    # 現在の石の数を表示
    black_count = np.sum(st.session_state.board == BLACK)
    white_count = np.sum(st.session_state.board == WHITE)
    st.write(f"Black (You): {black_count} stones")
    st.write(f"White (AI): {white_count} stones")
    
    # プレイヤーの手の入力
    if not st.session_state.game_over:
        # 有効な手のリストを取得
        valid_moves = get_valid_moves(st.session_state.board, BLACK)
        
        if valid_moves:
            col1, col2 = st.columns(2)
            with col1:
                x = st.selectbox("Column (A-H)", list("ABCDEFGH"))
            with col2:
                y = st.number_input("Row (1-8)", min_value=1, max_value=8)
            
            st.write("Valid moves:", ", ".join(valid_moves))
            
            if st.button("Place Stone"):
                try:
                    # プレイヤーの手を実行
                    st.session_state.board = put_disc(x, y, BLACK, st.session_state.board)
                    print(f"\nPlayer's move: {x}{y}")
                    
                    # 勝敗判定
                    result = judge(st.session_state.board)
                    if result is not None:
                        st.session_state.game_over = True
                    else:
                        # AIの手を取得して実行
                        ai_valid_moves = get_valid_moves(st.session_state.board, WHITE)
                        if ai_valid_moves:
                            ai_x, ai_y = get_ai_move(st.session_state.board)
                            if ai_x and ai_y:
                                st.session_state.board = put_disc(ai_x, ai_y, WHITE, st.session_state.board)
                                print(f"AI's move: {ai_x}{ai_y}")
                        
                        # 再度勝敗判定
                        result = judge(st.session_state.board)
                        if result is not None:
                            st.session_state.game_over = True
                    
                    st.rerun()
                    
                except ValueError as e:
                    st.error(f"Invalid move: {e}")
                    print(f"Error: {e}")
        else:
            st.write("No valid moves available for you. Skipping turn...")
            # AIの手番へ
            ai_valid_moves = get_valid_moves(st.session_state.board, WHITE)
            if ai_valid_moves:
                ai_x, ai_y = get_ai_move(st.session_state.board)
                if ai_x and ai_y:
                    st.session_state.board = put_disc(ai_x, ai_y, WHITE, st.session_state.board)
                    st.rerun()
            else:
                st.session_state.game_over = True
    
    # ゲーム終了時の表示
    if st.session_state.game_over:
        result = judge(st.session_state.board)
        if result == BLACK:
            st.success("You win!")
        elif result == WHITE:
            st.error("AI wins!")
        else:
            st.info("Draw!")
        
        if st.button("New Game"):
            st.session_state.board = create_board()
            st.session_state.game_over = False
            st.rerun()

if __name__ == "__main__":
    main()
othello.py
import matplotlib.pyplot as plt
import numpy as np

# 定数定義
EMPTY = 0
BLACK = 1
WHITE = 2


def create_board():
    """初期盤面を作成する"""
    board = np.zeros((8, 8), dtype=int)
    # 初期配置
    board[3:5, 3:5] = np.array([[WHITE, BLACK], [BLACK, WHITE]])
    return board


def draw_board(board, filename=None):
    """盤面を描画する"""
    fig, ax = plt.subplots(figsize=(8, 8))
    
    # 背景を緑色に
    ax.set_facecolor('forestgreen')
    
    # グリッドを描画
    for i in range(9):
        ax.axhline(i, color='black', linewidth=1)
        ax.axvline(i, color='black', linewidth=1)
    
    # ラベルを設定
    ax.set_xticks(np.arange(8))
    ax.set_yticks(np.arange(8))
    ax.set_xticklabels(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
    ax.set_yticklabels(['1', '2', '3', '4', '5', '6', '7', '8'])
    
    # マス目の座標ラベルを追加
    for i in range(8):
        for j in range(8):
            # 座標ラベルを作成 (A8からH1)
            label = f"{chr(65+j)}{8-i}"
            ax.text(j + 0.5, 7.5 - i, label,
                   horizontalalignment='center',
                   verticalalignment='center',
                   color='black',
                   fontsize=12,
                   alpha=0.3)  # 透明度を設定
    
    # 石を描画
    for i in range(8):
        for j in range(8):
            if board[i, j] == BLACK:
                circle = plt.Circle((j + 0.5, 7.5 - i), 0.4, color='black')
                ax.add_patch(circle)
            elif board[i, j] == WHITE:
                circle = plt.Circle((j + 0.5, 7.5 - i), 0.4, color='white')
                ax.add_patch(circle)
    
    ax.set_aspect('equal')
    
    if filename:
        plt.savefig(filename)
    plt.close()


def convert_position(x, y):
    """座標を内部表現に変換する"""
    if not isinstance(x, str) or len(x) != 1:
        raise ValueError("x must be a single character")

    x = x.upper()
    if x not in "ABCDEFGH":
        raise ValueError("x must be between A and H")

    if not isinstance(y, int) or y < 1 or y > 8:
        raise ValueError("y must be between 1 and 8")

    return 8 - y, ord(x) - ord("A")


def is_valid_move(board, row, col, color):
    """指定位置に石を置けるかチェックする"""
    if board[row, col] != EMPTY:
        return False

    directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]

    valid = False
    for dx, dy in directions:
        if check_direction(board, row, col, dx, dy, color):
            valid = True

    return valid


def check_direction(board, row, col, dx, dy, color):
    """特定の方向に石を裏返せるかチェックする"""
    opponent = WHITE if color == BLACK else BLACK
    x, y = row + dx, col + dy

    if not (0 <= x < 8 and 0 <= y < 8) or board[x, y] != opponent:
        return False

    while 0 <= x < 8 and 0 <= y < 8:
        if board[x, y] == EMPTY:
            return False
        if board[x, y] == color:
            return True
        x, y = x + dx, y + dy

    return False


def put_disc(x, y, color, board):
    """石を置く"""
    row, col = convert_position(x, y)

    if not is_valid_move(board, row, col, color):
        raise ValueError("Invalid move")

    new_board = board.copy()
    new_board[row, col] = color

    directions = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)]

    for dx, dy in directions:
        if check_direction(board, row, col, dx, dy, color):
            x, y = row + dx, col + dy
            while board[x, y] != color:
                new_board[x, y] = color
                x, y = x + dx, y + dy

    return new_board


def judge(board):
    """勝敗を判定する"""
    black_count = np.sum(board == BLACK)
    white_count = np.sum(board == WHITE)
    empty_count = np.sum(board == EMPTY)

    if empty_count == 0:
        if black_count > white_count:
            return BLACK
        elif white_count > black_count:
            return WHITE
        else:
            return None

    # まだ空きマスがある場合
    return None


# 使用例
if __name__ == "__main__":
    board = create_board()
    draw_board(board, "initial_board.png")

    # 例: 石を置く
    try:
        board = put_disc("C", 4, BLACK, board)
        draw_board(board, "after_move.png")
    except ValueError as e:
        print(f"Error: {e}")

    # 勝敗判定
    result = judge(board)
    if result == BLACK:
        print("Black wins!")
    elif result == WHITE:
        print("White wins!")
    else:
        print("Game is not finished yet")

まとめ

無事、オセロを作ることができました~~🎉🎉

途中、自分でコードを修正したくなることが何度もありましたが、生成AIだけでもコーディングができました。

厳密なビジネスロジックがあると難しいかもしれませんが、生成AIを使うと「プログラム未経験者でもソフトウェアを作る楽しさを実感できるのでは」と感じました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?