こんな感じでClaudeとオセロをするゲームを作ります。
Claude 3.5 Sonnetのマルチモーダル機能を使ってClaudeに次の手を考えさせる作戦です。
もう一つポイントがあります。「自分でコーディングはしない」です。
コードはすべてClaudeに作らせます。
まずはオセロをコーディング
まずはオセロのコードを作ります。
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")
実行してみたところ、以下の画像が生成されました。
とても良さげです!!
マス目の呼び名が分かりづらいと感じたので、以下のプロンプトでラベルを表示するように修正してもらいました。
マス目の中央に「A1」や「B2」など、そのマス目の座標を示すラベルを追加してください。文字のサイズは大きめにしてください。
文字色は黒にしてください。
ラベルは左上がA8で右下がH1になるはずです。
かなり期待ができます。
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を使ってください。
と、なんやかんやプロンプトの指示だけでコードを再生成し続け、できました!!
最終的なコード
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()
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を使うと「プログラム未経験者でもソフトウェアを作る楽しさを実感できるのでは」と感じました。