【令和最新版】ソリティアおじさんになろう！(Amazon Q developer CLIでゲーム作った)

# はじめに
何番煎じかはわかりませんがAmazon Q developer CLIを使ってゲームを作成しました。
本記事は、「[Build Games with Amazon Q CLI and score a T shirt ](https://community.aws/content/2xIoduO0xhkhUApQpVUIqBFGmAc/build-games-with-amazon-q-cli-and-score-a-t-shirt)」によるものです。

# 作ったもの
今回作成したのは、「ソリティア（クロンダイク）」になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308706/7a3c25a4-be82-4e13-bb14-a124d7613b25.png)

プレイのイメージ動画はXに投稿しました。

https://x.com/whitebird_sp/status/1934124344223101044

### なんでソリティアにしたの？
+ おそらく、現代社会で一番禁止されているゲーム（Windows標準で搭載されており、）
+ ドラックアンドドロップで完結するゲーム性と、運の要素が絡み繰り返しプレイできる
+ （おそらく多数いるであろう）ソリティアおじさんがAIを使えたら面白いなという好奇心

### 前提条件
元記事にあるような、最低限の条件で実行しています。
+ WSL上のUbuntu 24.04で実行
+ Amazon Q Deeveloper CLI　ver.1.9.1(ただアップデート忘れただけです・・・)
+ pygameがインストール済み

### プロンプトの条件
ということで、制約は次のようなものとしました。
+ Windowsの基本動作を行える（クリック・ドラッグ＆ドロップ、コピペによるコマンド実行）想定で、コードレビューは一切行わず、出来上がりに対してコメントを行うだけ
+ 指示も抽象的なものとして、具体的な指示はできるだけ行わない


### 初期の指示
シンプルに下記のプロンプトにしました

```
ソリティアを作ってください。
```

すると、コマンド入力形式になりました。コマンド入力式ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/308706/523ca550-e54b-42bb-9173-29240d2b10c5.png)

次は、pygameを使用して少しだけ具体的にしてみました。
```
pygameを使用して、ソリティアを作成してください。ドラッグ＆ドロップで実行できるようにしてください
```

すると、グラフィックは今のような形になりました。
ドラッグするとカードが下に流れて行ってしまったので、修正指示を出して、最終版としました。

### 最終的に出来上がったコード
今回作成されたコードは、こんな形になりました。

```
#!/usr/bin/env python3
# Pygame Solitaire Game with mouse drag and drop functionality

import pygame
import random
import os
import json
import sys
from typing import List, Dict, Tuple, Optional

# Initialize pygame
pygame.init()

# Constants
SCREEN_WIDTH = 1000
SCREEN_HEIGHT = 700
CARD_WIDTH = 80
CARD_HEIGHT = 120
CARD_SPACING = 30
MARGIN = 20
FPS = 60

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GREEN = (0, 128, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)

# Card suits and values
SUITS = ['S', 'H', 'D', 'C']  # Spades, Hearts, Diamonds, Clubs
VALUES = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']

# Set up the display
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Solitaire")
clock = pygame.time.Clock()

# Load card images or create them
font = pygame.font.SysFont('arial', 24, bold=True)

class Card:
    def __init__(self, suit: str, value: str, face_up: bool = False):
        self.suit = suit
        self.value = value
        self.face_up = face_up
        self.rect = pygame.Rect(0, 0, CARD_WIDTH, CARD_HEIGHT)
        self.dragging = False
        self.drag_offset = (0, 0)
        self.original_position = (0, 0)
        
        # Create card surface
        self.surface = self.create_card_surface()
        self.back_surface = self.create_card_back()
    
    def create_card_surface(self) -> pygame.Surface:
        """Create a surface for the card front"""
        surface = pygame.Surface((CARD_WIDTH, CARD_HEIGHT))
        surface.fill(WHITE)
        
        # Draw border
        pygame.draw.rect(surface, BLACK, (0, 0, CARD_WIDTH, CARD_HEIGHT), 2)
        
        # Draw value and suit
        color = RED if self.suit in ['H', 'D'] else BLACK
        
        # Draw value at top-left
        value_text = font.render(self.value, True, color)
        surface.blit(value_text, (5, 5))
        
        # Draw suit symbol
        suit_symbol = self.get_suit_symbol()
        suit_text = font.render(suit_symbol, True, color)
        surface.blit(suit_text, (5, 30))
        
        # Draw value and suit at bottom-right (upside down)
        value_text = font.render(self.value, True, color)
        surface.blit(pygame.transform.rotate(value_text, 180), 
                    (CARD_WIDTH - value_text.get_width() - 5, 
                     CARD_HEIGHT - value_text.get_height() - 5))
        
        suit_text = font.render(suit_symbol, True, color)
        surface.blit(pygame.transform.rotate(suit_text, 180), 
                    (CARD_WIDTH - suit_text.get_width() - 5, 
                     CARD_HEIGHT - suit_text.get_height() - 35))
        
        # Draw center symbol (larger)
        big_font = pygame.font.SysFont('arial', 48, bold=True)
        center_text = big_font.render(suit_symbol, True, color)
        surface.blit(center_text, 
                    (CARD_WIDTH // 2 - center_text.get_width() // 2, 
                     CARD_HEIGHT // 2 - center_text.get_height() // 2))
        
        return surface
    
    def create_card_back(self) -> pygame.Surface:
        """Create a surface for the card back"""
        surface = pygame.Surface((CARD_WIDTH, CARD_HEIGHT))
        surface.fill(BLUE)
        
        # Draw border
        pygame.draw.rect(surface, BLACK, (0, 0, CARD_WIDTH, CARD_HEIGHT), 2)
        
        # Draw pattern
        for i in range(0, CARD_WIDTH, 10):
            pygame.draw.line(surface, BLACK, (i, 0), (i, CARD_HEIGHT), 1)
        for i in range(0, CARD_HEIGHT, 10):
            pygame.draw.line(surface, BLACK, (0, i), (CARD_WIDTH, i), 1)
        
        return surface
    
    def get_suit_symbol(self) -> str:
        """Get the symbol for the suit"""
        if self.suit == 'S':
            return '♠'
        elif self.suit == 'H':
            return '♥'
        elif self.suit == 'D':
            return '♦'
        elif self.suit == 'C':
            return '♣'
        return self.suit
    
    def draw(self, surface: pygame.Surface, position: Tuple[int, int]):
        """Draw the card at the specified position"""
        self.rect.topleft = position
        if not self.dragging:
            self.original_position = position
            
        if self.face_up:
            surface.blit(self.surface, self.rect.topleft)
        else:
            surface.blit(self.back_surface, self.rect.topleft)
    
    def flip(self):
        """Flip the card over"""
        self.face_up = not self.face_up
    
    def start_drag(self, mouse_pos: Tuple[int, int]):
        """Start dragging the card"""
        self.dragging = True
        self.drag_offset = (self.rect.x - mouse_pos[0], self.rect.y - mouse_pos[1])
    
    def update_drag(self, mouse_pos: Tuple[int, int]):
        """Update the card position while dragging"""
        if self.dragging:
            self.rect.x = mouse_pos[0] + self.drag_offset[0]
            self.rect.y = mouse_pos[1] + self.drag_offset[1]
    
    def stop_drag(self):
        """Stop dragging the card"""
        self.dragging = False
    
    def reset_position(self):
        """Reset the card to its original position"""
        self.rect.topleft = self.original_position
    
    def to_dict(self):
        """Convert card to dictionary for saving"""
        return {
            'suit': self.suit,
            'value': self.value,
            'face_up': self.face_up
        }
    
    @classmethod
    def from_dict(cls, data):
        """Create card from dictionary"""
        return cls(data['suit'], data['value'], data['face_up'])

class Deck:
    def __init__(self):
        self.cards = []
        self.reset()
    
    def reset(self):
        """Reset and create a new deck of cards"""
        self.cards = []
        for suit in SUITS:
            for value in VALUES:
                self.cards.append(Card(suit, value))
        self.shuffle()
    
    def shuffle(self):
        """Shuffle the deck"""
        random.shuffle(self.cards)
    
    def deal(self) -> Optional[Card]:
        """Deal a card from the deck"""
        if not self.cards:
            return None
        return self.cards.pop()

class Solitaire:
    def __init__(self):
        self.deck = Deck()
        self.tableau = [[] for _ in range(7)]  # 7 tableau piles
        self.foundations = {suit: [] for suit in SUITS}  # 4 foundation piles
        self.stock = []  # Cards in the stock
        self.waste = []  # Cards in the waste pile
        
        # Positions
        self.stock_pos = (MARGIN, MARGIN)
        self.waste_pos = (MARGIN + CARD_WIDTH + 20, MARGIN)
        self.foundation_pos = {
            suit: (MARGIN + (CARD_WIDTH + 20) * (i + 3), MARGIN)
            for i, suit in enumerate(SUITS)
        }
        self.tableau_pos = [
            (MARGIN + (CARD_WIDTH + 20) * i, MARGIN * 2 + CARD_HEIGHT)
            for i in range(7)
        ]
        
        # Dragging state
        self.dragging = False
        self.drag_cards = []
        self.drag_source = None
        self.drag_source_index = -1
        
        self.setup_game()
    
    def setup_game(self):
        """Set up the initial game state"""
        # Deal cards to tableau
        for i in range(7):
            for j in range(i, 7):
                card = self.deck.deal()
                if card:
                    # Only the top card is face up
                    if j == i:
                        card.face_up = True
                    self.tableau[j].append(card)
        
        # Remaining cards go to stock
        self.stock = self.deck.cards
        self.deck.cards = []
    
    def draw_from_stock(self):
        """Draw a card from the stock to the waste pile"""
        if not self.stock:
            # If stock is empty, flip waste back to stock
            self.stock = list(reversed(self.waste))
            self.waste = []
            for card in self.stock:
                card.face_up = False
            return
        
        card = self.stock.pop()
        card.face_up = True
        self.waste.append(card)
    
    def can_move_to_foundation(self, card: Card) -> bool:
        """Check if a card can be moved to its foundation pile"""
        if not card or not card.face_up:
            return False
        
        foundation = self.foundations[card.suit]
        
        # If foundation is empty, only Ace can be placed
        if not foundation:
            return card.value == 'A'
        
        # Otherwise, check if card is next in sequence
        top_card_value = foundation[-1].value
        top_card_index = VALUES.index(top_card_value)
        return VALUES.index(card.value) == top_card_index + 1
    
    def can_move_to_tableau(self, card: Card, tableau_pile: List[Card]) -> bool:
        """Check if a card can be moved to a tableau pile"""
        if not card or not card.face_up:
            return False
        
        # If tableau pile is empty, only King can be placed
        if not tableau_pile:
            return card.value == 'K'
        
        # Check if card can be placed on the tableau pile
        top_card = tableau_pile[-1]
        if not top_card.face_up:
            return False
        
        # Check if card is opposite color and one value lower
        top_card_is_red = top_card.suit in ['H', 'D']
        card_is_red = card.suit in ['H', 'D']
        
        if top_card_is_red == card_is_red:
            return False  # Must be opposite colors
        
        top_card_index = VALUES.index(top_card.value)
        card_index = VALUES.index(card.value)
        
        return card_index == top_card_index - 1
    
    def move_to_foundation(self, card: Card, source: str, source_index: int) -> bool:
        """Move a card to its foundation pile"""
        if not card or not card.face_up:
            return False
            
        if not self.can_move_to_foundation(card):
            return False
            
        # Remove card from source
        if source == 'waste':
            if self.waste and self.waste[-1] == card:
                self.waste.pop()
            else:
                return False
        elif source == 'tableau':
            if 0 <= source_index < 7 and self.tableau[source_index] and self.tableau[source_index][-1] == card:
                self.tableau[source_index].pop()
                # Flip the new top card if needed
                if self.tableau[source_index] and not self.tableau[source_index][-1].face_up:
                    self.tableau[source_index][-1].face_up = True
            else:
                return False
        else:
            return False
            
        # Add card to foundation
        self.foundations[card.suit].append(card)
        return True
    
    def move_to_tableau(self, cards: List[Card], source: str, source_index: int, dest_index: int) -> bool:
        """Move card(s) to a tableau pile"""
        if not cards:
            return False
            
        # Check if we can move the bottom card to the destination
        if not self.can_move_to_tableau(cards[0], self.tableau[dest_index]):
            return False
            
        # Remove cards from source
        if source == 'waste':
            if len(cards) == 1 and self.waste and self.waste[-1] == cards[0]:
                self.waste.pop()
            else:
                return False
        elif source == 'tableau':
            if 0 <= source_index < 7 and source_index != dest_index:
                # Find the index of the first card in the source pile
                try:
                    card_index = self.tableau[source_index].index(cards[0])
                    # Check that we're moving a sequence of cards from the source
                    if self.tableau[source_index][card_index:] == cards:
                        self.tableau[source_index] = self.tableau[source_index][:card_index]
                        # Flip the new top card if needed
                        if self.tableau[source_index] and not self.tableau[source_index][-1].face_up:
                            self.tableau[source_index][-1].face_up = True
                    else:
                        return False
                except ValueError:
                    return False
            else:
                return False
        else:
            return False
            
        # Add cards to destination
        self.tableau[dest_index].extend(cards)
        return True
    
    def check_win(self) -> bool:
        """Check if the game has been won"""
        # Check if all foundations have 13 cards (A through K)
        return all(len(pile) == 13 for pile in self.foundations.values())
    
    def draw(self, surface: pygame.Surface):
        """Draw the game state"""
        # Draw background
        surface.fill(GREEN)
        
        # Draw stock
        if self.stock:
            self.stock[-1].draw(surface, self.stock_pos)
        else:
            # Draw empty stock outline
            pygame.draw.rect(surface, BLACK, 
                            (self.stock_pos[0], self.stock_pos[1], CARD_WIDTH, CARD_HEIGHT), 2)
        
        # Draw waste
        if self.waste:
            self.waste[-1].draw(surface, self.waste_pos)
        else:
            # Draw empty waste outline
            pygame.draw.rect(surface, BLACK, 
                            (self.waste_pos[0], self.waste_pos[1], CARD_WIDTH, CARD_HEIGHT), 2)
        
        # Draw foundations
        for suit, pile in self.foundations.items():
            pos = self.foundation_pos[suit]
            if pile:
                pile[-1].draw(surface, pos)
            else:
                # Draw empty foundation outline with suit symbol
                pygame.draw.rect(surface, BLACK, (pos[0], pos[1], CARD_WIDTH, CARD_HEIGHT), 2)
                suit_symbol = Card(suit, 'A').get_suit_symbol()
                color = RED if suit in ['H', 'D'] else BLACK
                suit_text = font.render(suit_symbol, True, color)
                surface.blit(suit_text, (pos[0] + CARD_WIDTH // 2 - suit_text.get_width() // 2, 
                                        pos[1] + CARD_HEIGHT // 2 - suit_text.get_height() // 2))
        
        # Draw tableau
        for i, pile in enumerate(self.tableau):
            pos = self.tableau_pos[i]
            if not pile:
                # Draw empty tableau outline
                pygame.draw.rect(surface, BLACK, (pos[0], pos[1], CARD_WIDTH, CARD_HEIGHT), 2)
            else:
                # Draw cards in cascade
                for j, card in enumerate(pile):
                    # Skip cards that are being dragged
                    if card in self.drag_cards:
                        continue
                    card_pos = (pos[0], pos[1] + j * CARD_SPACING)
                    card.draw(surface, card_pos)
        
        # Draw cards being dragged last (on top)
        if self.dragging and self.drag_cards:
            for i, card in enumerate(self.drag_cards):
                # Draw cards in cascade while dragging
                card_pos = (card.rect.x, card.rect.y + i * CARD_SPACING)
                card.draw(surface, card_pos)
    
    def handle_click(self, pos: Tuple[int, int]) -> bool:
        """Handle mouse click at the given position"""
        # Check if stock was clicked
        stock_rect = pygame.Rect(self.stock_pos[0], self.stock_pos[1], CARD_WIDTH, CARD_HEIGHT)
        if stock_rect.collidepoint(pos):
            self.draw_from_stock()
            return True
        
        # Check if waste was clicked for dragging
        if self.waste:
            waste_rect = pygame.Rect(self.waste_pos[0], self.waste_pos[1], CARD_WIDTH, CARD_HEIGHT)
            if waste_rect.collidepoint(pos):
                self.start_drag('waste', -1, [self.waste[-1]], pos)
                return True
        
        # Check if tableau was clicked for dragging
        for i, pile in enumerate(self.tableau):
            if not pile:
                continue
                
            # Check each card in the pile
            for j, card in enumerate(pile):
                card_pos = (self.tableau_pos[i][0], self.tableau_pos[i][1] + j * CARD_SPACING)
                card_rect = pygame.Rect(card_pos[0], card_pos[1], CARD_WIDTH, CARD_HEIGHT)
                
                # If card is face up and clicked
                if card.face_up and card_rect.collidepoint(pos):
                    # Start dragging this card and all cards on top of it
                    self.start_drag('tableau', i, pile[j:], pos)
                    return True
        
        return False
    
    def start_drag(self, source: str, source_index: int, cards: List[Card], pos: Tuple[int, int]):
        """Start dragging cards"""
        if not cards:
            return
            
        self.dragging = True
        self.drag_source = source
        self.drag_source_index = source_index
        self.drag_cards = cards
        
        # Set up dragging for the first card
        cards[0].start_drag(pos)
        
        # For subsequent cards, position them in a cascade below the first card
        for i in range(1, len(cards)):
            cards[i].rect.x = cards[0].rect.x
            cards[i].rect.y = cards[0].rect.y + i * CARD_SPACING
            cards[i].dragging = True
    
    def update_drag(self, pos: Tuple[int, int]):
        """Update position of dragged cards"""
        if not self.dragging or not self.drag_cards:
            return
            
        # Update position of first card
        self.drag_cards[0].update_drag(pos)
        
        # Update position of subsequent cards to maintain cascade
        for i in range(1, len(self.drag_cards)):
            self.drag_cards[i].rect.x = self.drag_cards[0].rect.x
            self.drag_cards[i].rect.y = self.drag_cards[0].rect.y + i * CARD_SPACING
    
    def stop_drag(self, pos: Tuple[int, int]) -> bool:
        """Stop dragging and try to place cards"""
        if not self.dragging or not self.drag_cards:
            return False
            
        # Try to place on foundation (only single cards)
        if len(self.drag_cards) == 1:
            for suit, foundation_pos in self.foundation_pos.items():
                foundation_rect = pygame.Rect(foundation_pos[0], foundation_pos[1], CARD_WIDTH, CARD_HEIGHT)
                if foundation_rect.collidepoint(pos):
                    if self.move_to_foundation(self.drag_cards[0], self.drag_source, self.drag_source_index):
                        self.dragging = False
                        self.drag_cards = []
                        return True
        
        # Try to place on tableau
        for i, tableau_pos in enumerate(self.tableau_pos):
            tableau_rect = pygame.Rect(tableau_pos[0], tableau_pos[1], CARD_WIDTH, CARD_HEIGHT * 3)  # Larger target area
            if tableau_rect.collidepoint(pos):
                if self.move_to_tableau(self.drag_cards, self.drag_source, self.drag_source_index, i):
                    self.dragging = False
                    self.drag_cards = []
                    return True
        
        # If we couldn't place the cards, reset their positions
        for card in self.drag_cards:
            card.reset_position()
            
        self.dragging = False
        self.drag_cards = []
        return False
    
    def save_game(self, filename: str = "solitaire_save.json"):
        """Save the game state to a file"""
        game_state = {
            'tableau': [[card.to_dict() for card in pile] for pile in self.tableau],
            'foundations': {suit: [card.to_dict() for card in pile] for suit, pile in self.foundations.items()},
            'stock': [card.to_dict() for card in self.stock],
            'waste': [card.to_dict() for card in self.waste]
        }
        
        with open(filename, 'w') as f:
            json.dump(game_state, f)
        
        print(f"Game saved to {filename}")
    
    def load_game(self, filename: str = "solitaire_save.json"):
        """Load the game state from a file"""
        try:
            with open(filename, 'r') as f:
                game_state = json.load(f)
            
            self.tableau = [[Card.from_dict(card_data) for card_data in pile] for pile in game_state['tableau']]
            self.foundations = {suit: [Card.from_dict(card_data) for card_data in pile] for suit, pile in game_state['foundations'].items()}
            self.stock = [Card.from_dict(card_data) for card_data in game_state['stock']]
            self.waste = [Card.from_dict(card_data) for card_data in game_state['waste']]
            
            print(f"Game loaded from {filename}")
            return True
        except (FileNotFoundError, json.JSONDecodeError):
            print("No saved game found or save file is corrupted.")
            return False

def main():
    game = Solitaire()
    
    # Ask if user wants to load a saved game
    if os.path.exists("solitaire_save.json"):
        font = pygame.font.SysFont('arial', 32)
        screen.fill(GREEN)
        text = font.render("Load saved game? (Y/N)", True, BLACK)
        screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, SCREEN_HEIGHT // 2))
        pygame.display.flip()
        
        waiting_for_input = True
        while waiting_for_input:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_y:
                        game.load_game()
                        waiting_for_input = False
                    elif event.key == pygame.K_n:
                        waiting_for_input = False
    
    # Main game loop
    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_s:
                    game.save_game()
                elif event.key == pygame.K_q:
                    running = False
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:  # Left mouse button
                    game.handle_click(event.pos)
            elif event.type == pygame.MOUSEMOTION:
                game.update_drag(event.pos)
            elif event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:  # Left mouse button
                    game.stop_drag(event.pos)
        
        # Draw everything
        game.draw(screen)
        
        # Check for win
        if game.check_win():
            font = pygame.font.SysFont('arial', 48)
            text = font.render("You Win!", True, BLACK)
            screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, SCREEN_HEIGHT // 2))
        
        # Update the display
        pygame.display.flip()
        clock.tick(FPS)
    
    # Ask to save before quitting
    if not game.check_win():
        screen.fill(GREEN)
        font = pygame.font.SysFont('arial', 32)
        text = font.render("Save game before quitting? (Y/N)", True, BLACK)
        screen.blit(text, (SCREEN_WIDTH // 2 - text.get_width() // 2, SCREEN_HEIGHT // 2))
        pygame.display.flip()
        
        waiting_for_input = True
        while waiting_for_input:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    waiting_for_input = False
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_y:
                        game.save_game()
                        waiting_for_input = False
                    elif event.key == pygame.K_n:
                        waiting_for_input = False
    
    pygame.quit()

if __name__ == "__main__":
    main()
```

# 所感・追加機能
セーブ機能やドラッグアンドドロップでの操作などは特に細かい指示をしなくても導入してくれました。
後は、こんな便利機能があるともっとよさそうですね。
+ Undo/Redo機能
+ ヒント機能
+ おける場所をビジュアルで表示
+ 解くのにかかった時間や移動回数の表示

ということで、これをClaude CodeやClaude Opus 4に修正させたバージョンもあるのですが、本題とずれるので省略します。

# さいごに
これでActive DirectoryのGPOでソリティアが禁止されていてもソリティアおじさんになれるぞ！
（職場での使用はお控えください…）