和Claude对战黑白棋!一起开发AI对弈游戏

发布于:2025-02-26 ⋅ 阅读:(20) ⋅ 点赞:(0)

序言

为了提升自己的多模态处理能力和API调用技巧,我决定挑战一个有趣的项目——开发一款可以与Claude对战的黑白棋游戏!这个项目不仅涉及游戏逻辑的实现,还需要调用Claude的API,让AI作为对手进行博弈。通过这个过程,我希望能够深入理解多模态交互的实现方式,同时提升自己在AI集成和API开发方面的技能。接下来,就让我来分享这个项目的构思和开发过程吧!

 我的计划是利用 Claude 3.5 Sonnet多模态功能,让 Claude 通过分析棋盘图像来计算下一步最佳落子策略。相比传统的 API 仅处理文本输入,这次的挑战在于如何让 Claude 通过视觉信息理解棋局局势,并给出合理的走法建议。这不仅是对 Claude 多模态能力的测试,也是一次结合图像处理与 AI 交互的实践探索。

让AI编码

整个代码的实现都将交给 Claude 来完成!

这不仅是一次 AI 对战的实验,也是对 AI 代码生成能力的挑战。我会专注于设计思路,而所有的代码,包括棋盘状态的解析、游戏逻辑的实现、以及 Claude 的 API 调用,都会完全由 Claude 生成。通过这个方式,我希望能验证 Claude 在实际编程任务中的表现,同时也能更加专注于优化交互体验和 AI 对弈策略。

我是使用 Anthropic 的控制台 进行的实验。
界面看起来虽然简单,但其实内置了 Prompt 生成器 等实用功能,非常值得一试!

如果你还没有用过,不妨尝试一下,或许会对提升你的 AI 交互体验有所帮助。

 

 

首先,我们来编写黑白棋的代码。


我打算先实现一个基础的黑白棋游戏。

在网上搜索了一下,发现有人使用 Python 的 numpy 和 matplotlib 来还原棋盘,我决定采用这种方法。

接下来,我执行了以下的提示词(Prompt)。

使用 Python 的 numpy 和 matplotlib 创建一个黑白棋(Othello)的棋盘。

- 在横轴上标注 A~H。
- 在纵轴上标注 1~8。

- 创建一个放置棋子(disc)的函数 `put_disc`。
    - `put_disc` 函数的参数有 4 个:`x`、`y`、`color` 和 `board`。
        - `x` 表示放置棋子的位置,接受 a~h(不区分大小写)。
        - `y` 只能接受 1~8 作为合法输入。
        - `color` 只能是 `BLACK` 或 `WHITE`。
        - `board` 代表当前棋盘的状态。
    - 如果尝试在 不可放置的位置 落子,应返回错误。
    - 放置棋子后,应翻转相应的棋子颜色。
    - 返回 更新后的棋盘状态。

- 创建一个检查棋盘状态并 判断游戏是否结束 的函数 `judge`。
    - 若分出胜负,则返回 获胜方(BLACK 或 WHITE)。
    - 若游戏尚未结束,则返回 `None`。

- 创建一个函数,将当前棋盘状态 导出为 PNG 图片。

Claude 为我生成了 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 必须是单个字符")
    
    x = x.upper()
    if x not in 'ABCDEFGH':
        raise ValueError("x 必须在 A 到 H 之间")
    
    if not isinstance(y, int) or y < 1 or y > 8:
        raise ValueError("y 必须在 1 到 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("无效的落子位置")
    
    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"错误: {e}")
    
    # 判断胜负
    result = judge(board)
    if result == BLACK:
        print("Black wins!")
    elif result == WHITE:
        print("White wins!")
    else:
        print("Game is not finished yet")

执行后,生成了以下图像

看起来非常不错!!🎉

不过,我觉得棋盘格子的坐标标识可能不太直观,所以我用以下的提示词让 Claude 进行了调整,使其在棋盘上显示更清晰的标签。

请在棋盘的每个格子中央添加对应的坐标标签(如 A1、B2)。

字体大小 请设置为较大,以确保清晰可见。
字体颜色 设为黑色,以提高可读性。
标签位置 应符合标准棋盘标识方式,左上角为 A8,右下角为 H1。

 

效果非常不错! 

修改后的代码

​
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'])

    # 在网格中添加坐标标签(从 A8 到 H1)
    for i in range(8):
        for j in range(8):
            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 必须是单个字符")

    x = x.upper()
    if x not in "ABCDEFGH":
        raise ValueError("x 必须在 A 到 H 之间")

    if not isinstance(y, int) or y < 1 or y > 8:
        raise ValueError("y 必须在 1 到 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

​

使用 Streamlit 开发 UI

接下来,我计划创建一个 对战界面,让游戏更直观、更容易操作!

我将使用 Streamlit 来构建用户界面,让玩家可以通过网页直接进行黑白棋对战,同时实现棋盘的可视化显示和交互功能。

import json
import base64
import boto3
import httpx

# 创建 Amazon Bedrock 客户端
bedrock_client = boto3.client('bedrock-runtime', region_name='us-east-1')

# 获取棋盘图片并进行 Base64 编码
image_url = "https://your-image-url.com/board.png"  # 请替换为实际的棋盘图片 URL
image_media_type = "image/png"
image_data = base64.standard_b64encode(httpx.get(image_url).content).decode("utf-8")

# 生成请求体
prompt = "请分析当前黑白棋的棋盘状态,并建议下一步的最佳落子位置。"

request_body = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 50,
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": image_media_type,
                        "data": image_data  # 传入棋盘图片的 Base64 数据
                    }
                }
            ]
        }
    ]
}

# 调用 Amazon Bedrock 运行 Claude 3.5 Sonnet
response = bedrock_client.invoke_model(
    modelId="anthropic.claude-3-5-sonnet-20240620-v1:0",
    body=json.dumps(request_body),
    contentType="application/json",
    accept="application/json"
)

# 解析 AI 返回的结果
response_data = json.loads(response['body'].read())
ai_reply = response_data.get("content", [{}])[0].get("text", "")

print("Claude's move suggestion:", ai_reply)

生成的代码

import streamlit as st
import base64
import io
import boto3
import numpy as np
from othello import *  # Assuming this contains your Othello game logic
import matplotlib.pyplot as plt
import tempfile
import os
import json
import uuid

# Initialize Amazon Bedrock client
bedrock_client = boto3.client('bedrock-runtime', region_name='us-east-1')

# Store all active game sessions
if 'game_sessions' not in st.session_state:
    st.session_state.game_sessions = {}


def save_board_image(board):
    """Save board image to temporary file and return path"""
    temp_dir = tempfile.gettempdir()
    temp_path = os.path.join(temp_dir, f'othello_board_{uuid.uuid4().hex}.png')
    draw_board(board, temp_path)
    return temp_path


def get_board_image_bytes(board):
    """Convert board image to bytes and encode as base64"""
    buf = io.BytesIO()
    draw_board(board)
    plt.savefig(buf, format='png', bbox_inches='tight')
    buf.seek(0)
    return base64.b64encode(buf.getvalue()).decode('utf-8')


def get_valid_moves(board, color):
    """Get list of valid moves"""
    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, valid_moves):
    """Get AI's next move"""
    if not valid_moves:
        return None, None

    board_image_base64 = get_board_image_bytes(board)

    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."""

    request_body = {
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 50,
        "messages": [
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": "image/png",
                            "data": board_image_base64
                        }
                    }
                ]
            }
        ]
    }

    response = bedrock_client.invoke_model(
        modelId="anthropic.claude-3-5-sonnet-20240620-v1:0",
        body=json.dumps(request_body),
        contentType="application/json",
        accept="application/json"
    )

    response_body = json.loads(response['body'].read().decode('utf-8'))
    ai_response = response_body['content'][0]['text'].strip()

    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 get_session_id():
    """Get or create a session ID for the current player"""
    if 'session_id' not in st.session_state:
        st.session_state.session_id = str(uuid.uuid4())

        # Initialize new game state for this session
        st.session_state.game_sessions[st.session_state.session_id] = {
            'board': create_board(),
            'game_over': False
        }
    return st.session_state.session_id


def main():
    st.title("Othello vs AI")

    # Get current player's session
    session_id = get_session_id()
    session = st.session_state.game_sessions[session_id]

    # Display board
    board_path = save_board_image(session['board'])
    st.image(board_path)

    # Show stone counts
    black_count = np.sum(session['board'] == BLACK)
    white_count = np.sum(session['board'] == WHITE)
    st.write(f"Black (You): {black_count} stones")
    st.write(f"White (AI): {white_count} stones")

    if not session['game_over']:
        valid_moves = get_valid_moves(session['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:
                    session['board'] = put_disc(x, y, BLACK, session['board'])
                    print(f"\nPlayer's move: {x}{y}")

                    result = judge(session['board'])
                    if result is not None:
                        session['game_over'] = True
                    else:
                        ai_valid_moves = get_valid_moves(
                            session['board'], WHITE)
                        if ai_valid_moves:
                            ai_x, ai_y = get_ai_move(
                                session['board'], ai_valid_moves)
                            if ai_x and ai_y:
                                session['board'] = put_disc(
                                    ai_x, ai_y, WHITE, session['board'])
                                print(f"AI's move: {ai_x}{ai_y}")

                        result = judge(session['board'])
                        if result is not None:
                            session['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_valid_moves = get_valid_moves(session['board'], WHITE)
            if ai_valid_moves:
                ai_x, ai_y = get_ai_move(session['board'], ai_valid_moves)
                if ai_x and ai_y:
                    session['board'] = put_disc(
                        ai_x, ai_y, WHITE, session['board'])
                    st.rerun()
            else:
                session['game_over'] = True

    if session['game_over']:
        result = judge(session['board'])
        if result == BLACK:
            st.success("You win!")
        elif result == WHITE:
            st.error("AI wins!")
        else:
            st.info("Draw!")

        if st.button("New Game"):
            session['board'] = create_board()
            session['game_over'] = False
            st.rerun()


if __name__ == "__main__":
    main()

让我们运行看看吧!

streamlit run app.py

正确的表示出了棋盘!

 ​​​​​​​

AI也能正确的做出响应! 

总结

我们成功完成了 黑白棋(Othello) 的开发~~

在过程中,虽然 多次想要自己修改代码,但最终还是完全依靠 生成式 AI 完成了整个编程工作。

对于 严格的业务逻辑,可能仍然存在一定的挑战,但通过 生成式 AI,即使是 零编程经验 的人,也能体验到 创造软件的乐趣,这确实是一件很棒的事情!