はじめに
何番煎じかはわかりませんがAmazon Q developer CLIを使ってゲームを作成しました。
本記事は、「Build Games with Amazon Q CLI and score a T shirt 」によるものです。
作ったもの
今回作成したのは、「ソリティア(クロンダイク)」になります。
プレイのイメージ動画はXに投稿しました。
なんでソリティアにしたの?
- おそらく、現代社会で一番禁止されているゲーム(Windows標準で搭載されており、)
- ドラックアンドドロップで完結するゲーム性と、運の要素が絡み繰り返しプレイできる
- (おそらく多数いるであろう)ソリティアおじさんがAIを使えたら面白いなという好奇心
前提条件
元記事にあるような、最低限の条件で実行しています。
- WSL上のUbuntu 24.04で実行
- Amazon Q Deeveloper CLI ver.1.9.1(ただアップデート忘れただけです・・・)
- pygameがインストール済み
プロンプトの条件
ということで、制約は次のようなものとしました。
- Windowsの基本動作を行える(クリック・ドラッグ&ドロップ、コピペによるコマンド実行)想定で、コードレビューは一切行わず、出来上がりに対してコメントを行うだけ
- 指示も抽象的なものとして、具体的な指示はできるだけ行わない
初期の指示
シンプルに下記のプロンプトにしました
ソリティアを作ってください。
すると、コマンド入力形式になりました。コマンド入力式ですね。
次は、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でソリティアが禁止されていてもソリティアおじさんになれるぞ!
(職場での使用はお控えください…)