Claude + MCP Pythonサーバーで新作ゲームを作ってみた

#### Claude+MCP Pythonサーバーで新作ゲームを作ってみた
Claude + MCP Pythonサーバーでゲームを作成しました。

![スクリーンショット 2025-06-21 17.37.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430586/8834e39d-6e8f-415c-b2de-a3f5d6846e67.png)


本記事は　qiitaの記事

https://qiita.com/quittardis/items/60b8c703c0c918ae80c6

がベースとなっています。
この記事では、Claude + MCP Pythonサーバーでスタンドアローンで動作する
GUIアプリを作成する方法を紹介していますが、この仕組みで、もちろんゲームも作成できます。

プロンプトで、ゲームの仕様を簡単に定義し、何度か意図通りに動かない点を修正して完成させます。

コードは最後に置いてます。


# 🎱 Othello Pool ゲームマニュアル(Claudeが生成）

## 📖 ゲーム概要

Othello Poolは、白と黒のボールをビリヤードのようにぶつけあって相手のボールを減らし合い、最後にどちらのボールが残るかを競い合うゲームです。プレイヤーは白いボールを操作し、コンピューターの黒いボールと対戦します。

## 🎯 ゲーム目標

**相手のボールをすべて消去して勝利を目指しましょう！**

## 🎮 操作方法

### 基本操作
1. **白いボールの近くでクリック** 
   - マウス左ボタンを押下（この時点ではエイムラインは表示されません）

2. **マウスを動かす**
   - 5ピクセル以上動かすと黄色いエイムラインが表示されます
   - エイムラインはショットの方向を示します

3. **方向と力を調整**
   - エイムラインの長さがショットの強さを表します
   - より長いラインほど強力なショットになります

4. **マウスボタンを離す**
   - ショットが実行されます

### 🎛️ 操作のコツ
- **精密なエイム**: 小さなマウス移動では練習用の線表示が出ません
- **パワー調整**: ドラッグ距離でショットの強さを調節できます
- **最大パワー**: 25パワーが上限です

## 🎲 ゲームルール

### プレイヤー構成
- **プレイヤー**: 白いボール 15個
- **コンピューター**: 黒いボール 15個

### 得点システム
- **異なる色のボール同士が衝突**すると得点チャンス
- **ターン中のプレイヤーが相手のボールを獲得**
  - プレイヤーターン中: 黒いボールを獲得
  - コンピューターターン中: 白いボールを獲得

### 勝利条件
- **相手のボールをすべて消去**したプレイヤーの勝利

## 🔄 ターンシステム

### ターンの流れ
1. **プレイヤーターン**
   - 白いボールでショット
   - すべてのボールが停止するまで待機

2. **コンピューターターン**
   - 黒いボールが自動的にショット
   - 戦略的にプレイヤーのボールを狙います

3. **ターン切り替え**
   - ボールが完全に停止後、次のターンへ

### ターン中の表示
- `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²)アルゴリズム
- **スムーズなアニメーション**: 高精度な物理計算
- **メモリ管理**: 不要なオブジェクトの自動クリーンアップ

## 🔧 トラブルシューティング

### よくある問題
1. **ゲームが起動しない**
   - Pygameライブラリがインストールされているか確認
   - Pythonのバージョンを確認

2. **エイムラインが表示されない**
   - マウスを5ピクセル以上動かしてください
   - ボールの近く（50ピクセル以内）でクリックしてください

3. **ショットが効かない**
   - プレイヤーのターンであることを確認
   - ターン進行中でないことを確認


## 🎈 楽しみ方のコツ

### 初心者向け
- **小さなパワー**から始めて感覚を掴みましょう
- **壁の反射**を利用したトリックショットを試してみましょう
- **相手ボールの配置**を観察して戦略を立てましょう

### 上級者向け
- **連続衝突**を狙って複数のボールを一度に獲得
- **相手の動きを予測**した守備的プレイ
- **物理法則を活用**した高度なショットテクニック

---

## 📝 バージョン情報
- **Version**: 2.0 (エイムライン改善版)
- **Last Updated**: 2025年6月21日
- **Author**: Othello Pool Development Team

楽しいゲームをお楽しみください！🎱✨

🔽クリックでコードが展開します。

<details>

```
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()
```
</details>