Claude+MCP Pythonサーバーで新作ゲームを作ってみた
Claude + MCP Pythonサーバーでゲームを作成しました。
本記事は qiitaの記事
がベースとなっています。
この記事では、Claude + MCP Pythonサーバーでスタンドアローンで動作する
GUIアプリを作成する方法を紹介していますが、この仕組みで、もちろんゲームも作成できます。
プロンプトで、ゲームの仕様を簡単に定義し、何度か意図通りに動かない点を修正して完成させます。
コードは最後に置いてます。
🎱 Othello Pool ゲームマニュアル(Claudeが生成)
📖 ゲーム概要
Othello Poolは、白と黒のボールをビリヤードのようにぶつけあって相手のボールを減らし合い、最後にどちらのボールが残るかを競い合うゲームです。プレイヤーは白いボールを操作し、コンピューターの黒いボールと対戦します。
🎯 ゲーム目標
相手のボールをすべて消去して勝利を目指しましょう!
🎮 操作方法
基本操作
-
白いボールの近くでクリック
- マウス左ボタンを押下(この時点ではエイムラインは表示されません)
-
マウスを動かす
- 5ピクセル以上動かすと黄色いエイムラインが表示されます
- エイムラインはショットの方向を示します
-
方向と力を調整
- エイムラインの長さがショットの強さを表します
- より長いラインほど強力なショットになります
-
マウスボタンを離す
- ショットが実行されます
🎛️ 操作のコツ
- 精密なエイム: 小さなマウス移動では練習用の線表示が出ません
- パワー調整: ドラッグ距離でショットの強さを調節できます
- 最大パワー: 25パワーが上限です
🎲 ゲームルール
プレイヤー構成
- プレイヤー: 白いボール 15個
- コンピューター: 黒いボール 15個
得点システム
- 異なる色のボール同士が衝突すると得点チャンス
-
ターン中のプレイヤーが相手のボールを獲得
- プレイヤーターン中: 黒いボールを獲得
- コンピューターターン中: 白いボールを獲得
勝利条件
- 相手のボールをすべて消去したプレイヤーの勝利
🔄 ターンシステム
ターンの流れ
-
プレイヤーターン
- 白いボールでショット
- すべてのボールが停止するまで待機
-
コンピューターターン
- 黒いボールが自動的にショット
- 戦略的にプレイヤーのボールを狙います
-
ターン切り替え
- ボールが完全に停止後、次のターンへ
ターン中の表示
-
Player Turn
: プレイヤーのターン -
Computer Turn
: コンピューターのターン -
Turn in progress...
: ターン実行中(操作不可)
🎱 物理システム
ボールの動き
- リアルな物理演算: 衝突、反射、摩擦を再現
- 壁との反射: ボールはテーブルの壁で跳ね返ります
- 摩擦による減速: ボールは徐々に減速して停止します
衝突システム
- 弾性衝突: ボール同士の衝突でエネルギーが伝達
- 分離処理: 重なったボールは自動的に分離されます
🎭 コンピューターAI
AI戦略
- 70%の確率: プレイヤーのボールを狙い撃ち
- 30%の確率: ランダムな方向にショット
- 適応的パワー: 距離と戦略に応じてパワーを調整
📊 ゲーム画面
表示情報
- Player (White): X balls: プレイヤーの残りボール数
- Computer (Black): X balls: コンピューターの残りボール数
- ターン表示: 現在のターン状況
- 操作ガイド: 画面下部に表示
エイムライン表示
- 黄色い線: ショットの方向と強さ
- Power: X.X: ショットパワーの数値表示
🎊 ゲーム終了
勝利表示
- "Player Wins!": プレイヤーの勝利
- "Computer Wins!": コンピューターの勝利
リスタート
- Rキーを押す: 新しいゲームを開始
- ボール配置が再ランダム化されます
⚡ パフォーマンス
システム要件
- フレームレート: 60 FPS
- 解像度: 1000×700ピクセル
- Python: 3.7以上推奨
- Pygame: 2.0以上推奨
最適化機能
- 効率的な衝突検出: O(n²)アルゴリズム
- スムーズなアニメーション: 高精度な物理計算
- メモリ管理: 不要なオブジェクトの自動クリーンアップ
🔧 トラブルシューティング
よくある問題
-
ゲームが起動しない
- Pygameライブラリがインストールされているか確認
- Pythonのバージョンを確認
-
エイムラインが表示されない
- マウスを5ピクセル以上動かしてください
- ボールの近く(50ピクセル以内)でクリックしてください
-
ショットが効かない
- プレイヤーのターンであることを確認
- ターン進行中でないことを確認
🎈 楽しみ方のコツ
初心者向け
- 小さなパワーから始めて感覚を掴みましょう
- 壁の反射を利用したトリックショットを試してみましょう
- 相手ボールの配置を観察して戦略を立てましょう
上級者向け
- 連続衝突を狙って複数のボールを一度に獲得
- 相手の動きを予測した守備的プレイ
- 物理法則を活用した高度なショットテクニック
📝 バージョン情報
- Version: 2.0 (エイムライン改善版)
- Last Updated: 2025年6月21日
- Author: Othello Pool Development Team
楽しいゲームをお楽しみください!🎱✨
🔽クリックでコードが展開します。
import pygame
import random
import math
import sys
# 初期化
pygame.init()
# 定数
WINDOW_WIDTH = 1000
WINDOW_HEIGHT = 700
POOL_WIDTH = 900
POOL_HEIGHT = 600
POOL_X = (WINDOW_WIDTH - POOL_WIDTH) // 2
POOL_Y = (WINDOW_HEIGHT - POOL_HEIGHT) // 2
WALL_THICKNESS = 20
# 色定義
GREEN = (0, 100, 0)
BROWN = (139, 69, 19)
WHITE = (255, 255, 255)
BLACK = (30, 30, 30)
YELLOW = (255, 255, 0)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GRAY = (128, 128, 128)
# ボールクラス
class Ball:
def __init__(self, x, y, radius=15, is_player_ball=True):
self.x = x
self.y = y
self.vx = 0
self.vy = 0
self.radius = radius
self.is_player_ball = is_player_ball
self.active = True
self.friction = 0.985 # 摩擦係数を減らして移動距離を2倍に
def update(self):
if not self.active:
return
# 摩擦を適用
self.vx *= self.friction
self.vy *= self.friction
# 位置更新
self.x += self.vx
self.y += self.vy
# 壁との衝突
left_wall = POOL_X + WALL_THICKNESS + self.radius
right_wall = POOL_X + POOL_WIDTH - WALL_THICKNESS - self.radius
top_wall = POOL_Y + WALL_THICKNESS + self.radius
bottom_wall = POOL_Y + POOL_HEIGHT - WALL_THICKNESS - self.radius
if self.x <= left_wall or self.x >= right_wall:
self.vx = -self.vx
self.x = max(left_wall, min(right_wall, self.x))
if self.y <= top_wall or self.y >= bottom_wall:
self.vy = -self.vy
self.y = max(top_wall, min(bottom_wall, self.y))
def draw(self, screen):
if self.active:
color = WHITE if self.is_player_ball else BLACK
pygame.draw.circle(screen, color, (int(self.x), int(self.y)), self.radius)
pygame.draw.circle(screen, GRAY, (int(self.x), int(self.y)), self.radius, 2)
def get_speed(self):
return math.sqrt(self.vx**2 + self.vy**2)
def is_moving(self):
return self.get_speed() > 0.1
# ゲームクラス
class OthelloPoolGame:
def __init__(self):
self.screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Othello Pool")
self.clock = pygame.time.Clock()
# フォントを英語対応に変更
try:
self.font = pygame.font.Font(None, 36)
self.small_font = pygame.font.Font(None, 24)
except:
# フォールバック
self.font = pygame.font.SysFont('arial', 36)
self.small_font = pygame.font.SysFont('arial', 24)
self.player_turn = True
self.player_score = 0
self.computer_score = 0
self.game_over = False
self.winner = None
self.aiming = False
self.aim_start = None
self.aim_end = None
self.mouse_moved = False # マウスが動いたかどうかを追跡
# ターン管理用の変数を追加
self.turn_in_progress = False
self.computer_move_delay = 0
self.init_balls()
def init_balls(self):
self.balls = []
# プレイヤーボール(白)15個をランダム配置
for i in range(15):
while True:
x = random.randint(POOL_X + WALL_THICKNESS + 20, POOL_X + POOL_WIDTH - WALL_THICKNESS - 20)
y = random.randint(POOL_Y + WALL_THICKNESS + 20, POOL_Y + POOL_HEIGHT - WALL_THICKNESS - 20)
# 他のボールと重ならないかチェック
valid = True
for ball in self.balls:
distance = math.sqrt((x - ball.x)**2 + (y - ball.y)**2)
if distance < 35: # ボール間の最小距離
valid = False
break
if valid:
self.balls.append(Ball(x, y, is_player_ball=True))
break
# コンピューターボール(黒)15個をランダム配置
for i in range(15):
while True:
x = random.randint(POOL_X + WALL_THICKNESS + 20, POOL_X + POOL_WIDTH - WALL_THICKNESS - 20)
y = random.randint(POOL_Y + WALL_THICKNESS + 20, POOL_Y + POOL_HEIGHT - WALL_THICKNESS - 20)
# 他のボールと重ならないかチェック
valid = True
for ball in self.balls:
distance = math.sqrt((x - ball.x)**2 + (y - ball.y)**2)
if distance < 35: # ボール間の最小距離
valid = False
break
if valid:
self.balls.append(Ball(x, y, is_player_ball=False))
break
def check_collisions(self):
for i in range(len(self.balls)):
for j in range(i + 1, len(self.balls)):
ball1 = self.balls[i]
ball2 = self.balls[j]
if not ball1.active or not ball2.active:
continue
# 距離計算
dx = ball2.x - ball1.x
dy = ball2.y - ball1.y
distance = math.sqrt(dx**2 + dy**2)
if distance < ball1.radius + ball2.radius:
# 衝突発生
print(f"Collision: {'White' if ball1.is_player_ball else 'Black'} vs {'White' if ball2.is_player_ball else 'Black'}")
# ターンベースの消去ルール
if ball1.is_player_ball != ball2.is_player_ball: # 異なる色のボール同士
if self.player_turn:
# プレイヤーターン: 相手のボール(黒)を消去
if not ball1.is_player_ball:
ball1.active = False
self.player_score += 1
print(f"Player scores! Black ball removed")
elif not ball2.is_player_ball:
ball2.active = False
self.player_score += 1
print(f"Player scores! Black ball removed")
else:
# コンピューターターン: 相手のボール(白)を消去
if ball1.is_player_ball:
ball1.active = False
self.computer_score += 1
print(f"Computer scores! White ball removed")
elif ball2.is_player_ball:
ball2.active = False
self.computer_score += 1
print(f"Computer scores! White ball removed")
# 物理的な衝突反応(両方とも生きている場合)
if ball1.active and ball2.active:
# 衝突の物理計算
angle = math.atan2(dy, dx)
# 衝突による速度変化
v1 = math.sqrt(ball1.vx**2 + ball1.vy**2)
v2 = math.sqrt(ball2.vx**2 + ball2.vy**2)
# 簡易的な弾性衝突
ball1.vx = -ball1.vx * 0.8 + math.cos(angle + math.pi) * v2 * 0.3
ball1.vy = -ball1.vy * 0.8 + math.sin(angle + math.pi) * v2 * 0.3
ball2.vx = -ball2.vx * 0.8 + math.cos(angle) * v1 * 0.3
ball2.vy = -ball2.vy * 0.8 + math.sin(angle) * v1 * 0.3
# ボールを離す
overlap = ball1.radius + ball2.radius - distance
ball1.x -= math.cos(angle) * overlap * 0.5
ball1.y -= math.sin(angle) * overlap * 0.5
ball2.x += math.cos(angle) * overlap * 0.5
ball2.y += math.sin(angle) * overlap * 0.5
def all_balls_stopped(self):
for ball in self.balls:
if ball.active and ball.is_moving():
return False
return True
def count_active_balls(self, is_player):
count = 0
for ball in self.balls:
if ball.active and ball.is_player_ball == is_player:
count += 1
return count
def computer_turn(self):
# コンピューターの黒ボールを探す
computer_balls = [ball for ball in self.balls if ball.active and not ball.is_player_ball]
player_balls = [ball for ball in self.balls if ball.active and ball.is_player_ball]
if not computer_balls:
return
# ランダムにコンピューターのボールを選択
shooter = random.choice(computer_balls)
# 戦略決定(70%で相手ボールを狙う、30%で壁を狙う)
if player_balls and random.random() < 0.7:
# 相手ボール(白)を狙う
target = random.choice(player_balls)
dx = target.x - shooter.x
dy = target.y - shooter.y
distance = math.sqrt(dx**2 + dy**2)
if distance > 0:
# ターゲットに向けてショット
power = random.uniform(10, 20) # パワーを増加
shooter.vx = (dx / distance) * power
shooter.vy = (dy / distance) * power
print(f"Computer: Black ball targets white ball!")
else:
# ランダムな方向にショット
angle = random.uniform(0, 2 * math.pi)
power = random.uniform(8, 16) # パワーを増加
shooter.vx = math.cos(angle) * power
shooter.vy = math.sin(angle) * power
print(f"Computer: Random shot")
# コンピューターがショットを打ったら、ターンが進行中であることを示す
self.turn_in_progress = True
def handle_mouse_events(self, event):
# プレイヤーのターンかつゲームが終了していない、かつターンが進行中でない場合のみ入力を受け付ける
if not self.player_turn or self.game_over or self.turn_in_progress:
return
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1: # 左クリック
self.aiming = True
self.aim_start = pygame.mouse.get_pos()
self.mouse_moved = False # マウス移動フラグをリセット
self.aim_end = None # エイムエンドをリセット
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1 and self.aiming:
if self.mouse_moved and self.aim_end: # マウスが動いた場合のみショット
self.shoot()
self.aiming = False
self.mouse_moved = False
self.aim_start = None
self.aim_end = None
elif event.type == pygame.MOUSEMOTION:
if self.aiming:
current_pos = pygame.mouse.get_pos()
# マウスが最初のクリック位置から一定距離以上動いた場合のみエイムラインを表示
if self.aim_start:
dx = current_pos[0] - self.aim_start[0]
dy = current_pos[1] - self.aim_start[1]
distance = math.sqrt(dx**2 + dy**2)
if distance > 5: # 5ピクセル以上動いた場合
self.mouse_moved = True
self.aim_end = current_pos
else:
self.mouse_moved = False
self.aim_end = None
def shoot(self):
if not self.aim_start or not self.aim_end:
return
# プレイヤーのボールを探す
player_balls = [ball for ball in self.balls if ball.active and ball.is_player_ball]
if not player_balls:
return
# 最も近いプレイヤーボールを選択
mouse_x, mouse_y = self.aim_start
closest_ball = None
min_distance = float('inf')
for ball in player_balls:
distance = math.sqrt((ball.x - mouse_x)**2 + (ball.y - mouse_y)**2)
if distance < min_distance:
min_distance = distance
closest_ball = ball
if closest_ball and min_distance < 50: # ボールの近くでクリックした場合
dx = self.aim_end[0] - self.aim_start[0]
dy = self.aim_end[1] - self.aim_start[1]
power = min(math.sqrt(dx**2 + dy**2) * 0.4, 25) # パワーを増加
if power > 0:
angle = math.atan2(dy, dx)
closest_ball.vx = math.cos(angle) * power
closest_ball.vy = math.sin(angle) * power
print(f"Player: White ball shot! Power: {power:.1f}")
# プレイヤーがショットを打ったら、ターンが進行中であることを示す
self.turn_in_progress = True
def draw_aim_line(self):
# マウスが動いた場合のみエイムラインを描画
if (self.aiming and self.mouse_moved and self.aim_start and self.aim_end and
self.player_turn and not self.turn_in_progress):
pygame.draw.line(self.screen, YELLOW, self.aim_start, self.aim_end, 3)
# パワー表示
dx = self.aim_end[0] - self.aim_start[0]
dy = self.aim_end[1] - self.aim_start[1]
power = min(math.sqrt(dx**2 + dy**2) * 0.4, 25)
power_text = self.small_font.render(f"Power: {power:.1f}", True, YELLOW)
self.screen.blit(power_text, (self.aim_end[0] + 10, self.aim_end[1] - 10))
def draw_table(self):
# テーブル背景
pygame.draw.rect(self.screen, BROWN, (POOL_X - WALL_THICKNESS, POOL_Y - WALL_THICKNESS,
POOL_WIDTH + 2*WALL_THICKNESS, POOL_HEIGHT + 2*WALL_THICKNESS))
# プレイエリア
pygame.draw.rect(self.screen, GREEN, (POOL_X, POOL_Y, POOL_WIDTH, POOL_HEIGHT))
# 境界線
pygame.draw.rect(self.screen, BROWN, (POOL_X, POOL_Y, POOL_WIDTH, POOL_HEIGHT), WALL_THICKNESS)
def draw_ui(self):
# スコア表示
player_balls_left = self.count_active_balls(True)
computer_balls_left = self.count_active_balls(False)
score_text = self.font.render(f"Player (White): {player_balls_left} balls", True, WHITE)
self.screen.blit(score_text, (10, 10))
computer_text = self.font.render(f"Computer (Black): {computer_balls_left} balls", True, WHITE)
self.screen.blit(computer_text, (10, 50))
# ターン表示
if self.turn_in_progress:
turn_text = "Turn in progress..."
turn_color = YELLOW
else:
turn_text = "Player Turn" if self.player_turn else "Computer Turn"
turn_color = WHITE if self.player_turn else YELLOW
turn_surface = self.font.render(turn_text, True, turn_color)
self.screen.blit(turn_surface, (10, 100))
# 操作説明
if self.player_turn and not self.game_over and not self.turn_in_progress:
help_text = self.small_font.render("Click & drag near white ball to shoot", True, WHITE)
self.screen.blit(help_text, (10, 140))
elif self.turn_in_progress:
help_text = self.small_font.render("Please wait for turn to complete...", True, YELLOW)
self.screen.blit(help_text, (10, 140))
# ゲーム終了時の表示
if self.game_over:
game_over_text = self.font.render(f"{self.winner} Wins!", True, RED)
text_rect = game_over_text.get_rect(center=(WINDOW_WIDTH//2, WINDOW_HEIGHT//2))
self.screen.blit(game_over_text, text_rect)
restart_text = self.small_font.render("Press R to restart", True, WHITE)
restart_rect = restart_text.get_rect(center=(WINDOW_WIDTH//2, WINDOW_HEIGHT//2 + 40))
self.screen.blit(restart_text, restart_rect)
def check_game_over(self):
player_balls = self.count_active_balls(True)
computer_balls = self.count_active_balls(False)
if player_balls == 0:
self.game_over = True
self.winner = "Computer"
elif computer_balls == 0:
self.game_over = True
self.winner = "Player"
def restart_game(self):
self.player_turn = True
self.player_score = 0
self.computer_score = 0
self.game_over = False
self.winner = None
self.aiming = False
self.aim_start = None
self.aim_end = None
self.mouse_moved = False
self.turn_in_progress = False
self.computer_move_delay = 0
self.init_balls()
def run(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_r and self.game_over:
self.restart_game()
self.handle_mouse_events(event)
# ゲーム更新
if not self.game_over:
# ボール更新
for ball in self.balls:
ball.update()
# 衝突チェック
self.check_collisions()
# ターン切り替えロジック
if self.turn_in_progress and self.all_balls_stopped():
# 現在のターンが終了
self.turn_in_progress = False
# ターンを切り替え
if self.player_turn:
self.player_turn = False
self.computer_move_delay = 60 # 1秒のディレイ(60fps想定)
print("Turn switched to Computer")
else:
self.player_turn = True
print("Turn switched to Player")
# コンピューターのターンでディレイ後に行動
if not self.player_turn and not self.turn_in_progress and self.computer_move_delay > 0:
self.computer_move_delay -= 1
if self.computer_move_delay == 0:
self.computer_turn()
# ゲーム終了チェック
self.check_game_over()
# 描画
self.screen.fill(BLACK)
self.draw_table()
for ball in self.balls:
ball.draw(self.screen)
self.draw_aim_line()
self.draw_ui()
pygame.display.flip()
self.clock.tick(60)
pygame.quit()
sys.exit()
# メイン実行
if __name__ == "__main__":
game = OthelloPoolGame()
game.run()