🎬 導入:R60ポンコツ夫婦、七並べAI開発で「脳内活性」に挑む!
私たちR60のポンコツ夫婦は、仲良く老いていく中で、脳内活性(アンチエイジング)のための挑戦を続ける「ポンコツ夫婦のGame Trial Log」シリーズを続けています。
今回はVol.3の難題であった将棋編から一転、Pythonでトランプの「七並べ(Sevens)アプリ」のAI開発に挑みました。
【YouTubeタイトル】 プログラミング無知な筆者Dr.takodemousが、AI(Gemini)と共同開発した七並べアプリ。外郭は「プロンプト一発」で爆速完了したものの、ジョーカーの定義やCM(コンピューター)側の戦略が難関すぎ、全員続行不能の危機に!動画では、このAIの欠陥を回避するため、夫婦二人で戦略を立ててCMに挑む様子をお届けします。
I. Dr.takodemousの戦い:AIとの共同開発ログ
1. 爆速開発と初期の壁:ジョーカーの罠
Vol.3までの経験を活かし、七並べの外郭とルール設定は驚くほどスムーズに進行しました。しかし、トランプゲームならではのジョーカーの定義が最初の難関でした。
【💻難関!ジョーカーの罠】 カードのマークや数字をPythonが正確に読み込めず、任意の場所に置けるというジョーカーの特殊なルールを設定するのに大幅に時間を費やしました。この定義の複雑さが、後のCM戦略の欠陥にも繋がっていきます。
II. 💻 難関!CM戦略とデバッグの苦労話(技術的な核)
1. CMの壁と致命的な欠陥
七並べAIの最大の難関は、CM側に人間のような戦略的な判断をさせることでした。
【💻難関!CMの壁】 CM側は、遠い場所から順に置くことしか設定できず、キングやエースを持っている場合でも、あえて7に近い場所を置いて場の流れを支配するといった戦略が不能でした。この融通の利かないロジックこそが、「全員続行不能」の危機を招く致命的な欠陥となります。
【サイト紹介】プロのコーダーの脳トレアプリとの対比
今回も前回同様、PR TIMESさんのサイトよりプロのコーダーが作成した脳トレWebゲームを紹介します。
素人のAIコーダーである筆者のコードの難解さと、プロのアプリの機能を対比させることで、AIサポートの凄さが浮き彫りになります。これにより、プログラミング初心者でもAIを駆使すれば、コーダーと言えるレベルのプログラムがつくれるという事実を強調します。
III. Redkabagon参戦!夫婦共闘でAIの欠陥を攻略
1. Redkabagon参戦と夫婦共闘のテーマ
前回の将棋編ではギブアップしたRedkabagonですが、今回はこの「融通の利かないAIの弱点」を攻略するパズル要素に興味を示し、夫婦共闘が実現しました。
動画では、このAIの欠陥を回避するため、R60夫婦二人で戦略を立ててCMに挑む様子が収録されています。夫婦の会話を通じて、CMが次に何を置くかを予測する作業は、まさに脳内活性(アンチエイジング)そのものです。
2. Redkabagonのソロ活動と着実な進展
一方で、 Redkabagonは、これまで通りG5 Entertainment提供のアイテム探し&3マッチゲームをクリアする動画を投稿し、ブログにリンクしています。彼女の着実なゲームクリアも、本シリーズの隠れた見どころです。
さらに、 Redkabagonのソロ活動として、「日常にほぼ不要な特殊能力」という脳内活性ゲームの動画リンクも引き続き行います。
IV. YouTube動画公開とコード公開
1. 筆者クリア動画の一般公開と読者への協力依頼
このAIとの対戦の模様は、筆者クリア動画として一般公開されています。
【📣皆さんの知識が必要です!】 この七並べAIは、特にCM側の戦略設定に課題が残っています。CMに人間的な戦略を持たせる改善案や知識をお持ちの方がいらっしゃいましたら、夫婦二人では限界に達しましたので、ぜひコメント欄で共有をお願いいたします!
したがって、プロのアプリの機能と対比しながら、筆者のコードを紹介します。プログラミング未経験以前の無知な筆者でも、AIを駆使すれば、コーダーと言えるレベルのプログラムがつくれるという事実は驚きです。
今回はデバッグの経緯を追体験していただくために、ジョーカー設定時バグコード、GUIの不安定要素のあるコード、不安定な要素は無いがGUIの改善が必要なコードの3つの段階のコードを紹介します。
2. Pythonコードの公開とシリーズの継続
ジョーカーの定義やCM戦略に関する詳細な苦労、そして現在のPythonコードをこちらのブログで公開しています。
プロンプトno1(ジョーカー設定時バグコード)
import random
import tkinter as tk
from tkinter import messagebox
from tkinter import simpledialog
# ======================================================================
# A. カード、デッキ、プレイヤー、フィールド、ゲームロジック (変更なし)
# ======================================================================
class Card:
SUITS = ['Spade', 'Heart', 'Diamond', 'Club']
RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
RANK_VALUES = {rank: i + 1 for i, rank in enumerate(RANKS)}
JOKER_SUIT = 'Joker'
JOKER_RANK = 'JK'
SUIT_SYMBOLS = {'Spade': '♠', 'Heart': '♥', 'Diamond': '♦', 'Club': '♣'}
def __init__(self, suit_index, rank_index, is_joker=False):
self.is_joker = is_joker
if is_joker:
self.suit = self.JOKER_SUIT
self.rank = self.JOKER_RANK
self.value = 0
self.suit_index = 5
else:
self.suit_index = suit_index
self.rank_index = rank_index
self.suit = self.SUITS[suit_index]
self.rank = self.RANKS[rank_index]
self.value = self.RANK_VALUES[self.rank]
self.symbol = self.SUIT_SYMBOLS.get(self.suit, '')
def __str__(self):
if self.is_joker: return "JOKER"
return f"{self.symbol}{self.rank}"
def __repr__(self):
return f"<Card: {self.__str__()}>"
def __eq__(self, other):
if not isinstance(other, Card): return NotImplemented
if self.is_joker and other.is_joker: return True
if self.is_joker or other.is_joker: return False
return self.suit_index == other.suit_index and self.rank_index == other.rank_index
class Deck:
def __init__(self, num_jokers=1):
self.num_jokers = num_jokers
self.cards = []
self.reset()
def reset(self):
self.cards = []
for suit_index, suit in enumerate(Card.SUITS):
for rank_index, rank in enumerate(Card.RANKS):
self.cards.append(Card(suit_index, rank_index))
for _ in range(self.num_jokers):
self.cards.append(Card(0, 0, is_joker=True))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, num_cards):
if len(self.cards) < num_cards:
num_cards = len(self.cards)
dealt_cards = self.cards[:num_cards]
self.cards = self.cards[num_cards:]
return dealt_cards
class Player:
def __init__(self, name, is_human=False):
self.name = name
self.hand = []
self.passes_count = 0
self.is_human = is_human
def receive_cards(self, cards):
self.hand.extend(cards)
self.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
def remove_card(self, card_to_remove):
for i, card in enumerate(self.hand):
if card == card_to_remove:
del self.hand[i]
return True
return False
def can_play(self, field):
for card in self.hand:
if card.is_joker or field.is_playable(card):
return True
return False
def get_playable_cards(self, field):
playable = []
for card in self.hand:
if card.is_joker or field.is_playable(card):
playable.append(card)
return playable
class Field:
def __init__(self):
self.table = {suit: [None] * len(Card.RANKS) for suit in Card.SUITS}
self.center_index = Card.RANK_VALUES['7'] - 1
def initialize_sevens(self):
for suit in Card.SUITS:
self.table[suit][self.center_index] = True
def is_playable(self, card):
if card.is_joker: return True
suit = card.suit
rank_index = card.rank_index
if isinstance(self.table[suit][rank_index], Card):
return False
if card.value < 7:
next_index = rank_index + 1
if next_index <= self.center_index:
return isinstance(self.table[suit][next_index], Card)
if card.value > 7:
prev_index = rank_index - 1
if prev_index >= self.center_index:
return isinstance(self.table[suit][prev_index], Card)
if card.value == 7:
return self.table[suit][rank_index] is True
return False
def place_card(self, card):
self.table[card.suit][card.rank_index] = card
return True
class Game:
def __init__(self, player_names, max_passes=3, num_jokers=1):
self.players = [Player(name, is_human=(name == 'YOU')) for name in player_names]
self.num_players = len(self.players)
self.deck = Deck(num_jokers=num_jokers)
self.field = Field()
self.max_passes = max_passes
self.current_player_index = -1
self.is_running = False
self.h7_card = Card(suit_index=1, rank_index=6)
def start_game(self):
for player in self.players:
player.hand = []
player.passes_count = 0
self.deck.reset()
self.deck.shuffle()
total_cards = len(self.deck.cards)
cards_per_player = total_cards // self.num_players
remainder = total_cards % self.num_players
for i, player in enumerate(self.players):
num_to_deal = cards_per_player + (1 if i < remainder else 0)
player.receive_cards(self.deck.deal(num_to_deal))
self.field = Field()
self.field.initialize_sevens()
starter_index = -1
h7_card_in_hand = None
for i, player in enumerate(self.players):
for card in player.hand:
if card == self.h7_card:
starter_index = i
h7_card_in_hand = card
break
if starter_index != -1: break
if starter_index == -1: return False
self.current_player_index = starter_index
self.is_running = True
starter = self.players[starter_index]
if h7_card_in_hand and starter.remove_card(h7_card_in_hand):
self.field.place_card(h7_card_in_hand)
starter.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
self.current_player_index = (starter_index + 1) % self.num_players
return True
def get_current_player(self):
return self.players[self.current_player_index]
def enforce_seven_play(self, seven_card):
enforced_plays = []
for p in self.players:
if p.name == self.get_current_player().name:
continue
for card in p.hand:
if card == seven_card:
p.remove_card(card)
self.field.place_card(card)
enforced_plays.append(f"{p.name} が {seven_card.__str__()} を強制提示")
break
return enforced_plays
def play_card(self, player, card_to_play, target_card_info=None):
if card_to_play not in player.hand: return False, "手札にありません"
if card_to_play.is_joker:
if target_card_info is None: return False, "ジョーカーは代用するカードの情報を指定してください。"
target_suit, target_rank_str = target_card_info
try:
target_rank_index = Card.RANKS.index(target_rank_str)
target_suit_index = Card.SUITS.index(target_suit)
temp_card = Card(target_suit_index, target_rank_index)
except (ValueError, IndexError):
return False, "無効なスートまたはランクが指定されました。"
suit = temp_card.suit
rank_index = temp_card.rank_index
field = self.field
if isinstance(field.table[suit][rank_index], Card):
return False, f"そのカード({temp_card.__str__()})は既に出ています。"
if temp_card.value == 7:
if field.table[suit][rank_index] is not True:
return False, "7の代用はできません。(H7以外は初期フラグTrueである必要)"
elif not field.is_playable(temp_card):
return False, f"ジョーカーを {temp_card.__str__()} の代わりに出せません。隣接カードがありません。"
player.remove_card(card_to_play)
self.field.table[suit][rank_index] = temp_card
forced_msg_list = []
if temp_card.value == 7:
forced_msg_list = self.enforce_seven_play(temp_card)
base_msg = f"ジョーカーを {temp_card.__str__()} の代わりに出しました。"
if forced_msg_list:
base_msg += " (" + ", ".join(forced_msg_list) + ")"
return True, base_msg
if not self.field.is_playable(card_to_play):
return False, "そのカードは場に出せません。"
player.remove_card(card_to_play)
self.field.place_card(card_to_play)
return True, f"{card_to_play} を出しました。"
def pass_turn(self, player):
if player.can_play(self.field):
return False, "出せるカードがあるため、パスはできません。"
player.passes_count += 1
if player.passes_count > self.max_passes:
return True, f"{player.name} はパス制限回数を超え、脱落しました。"
return True, f"{player.name} はパスしました。"
def check_winner(self):
for player in self.players:
if not player.hand:
self.is_running = False
return player.name
active_players = [p for p in self.players if p.passes_count <= self.max_passes and p.hand]
if not active_players and self.is_running:
self.is_running = False
return "全員がパス制限を超え、ゲーム終了です。"
return None
def next_turn(self):
for _ in range(self.num_players):
self.current_player_index = (self.current_player_index + 1) % self.num_players
next_player = self.get_current_player()
if next_player.passes_count <= self.max_passes and next_player.hand:
return True
if self.check_winner():
return False
return False
# ======================================================================
# B. Tkinter GUI クラス
# ======================================================================
class SevensGUI:
def __init__(self, master):
self.master = master
master.title("七並べ (Sevens)")
self.player_names = ["YOU", "COM-A", "COM-B", "COM-C"]
self.game = Game(self.player_names, max_passes=3, num_jokers=1)
# フィールドフレーム (最上部)
self.field_frame = tk.Frame(master, padx=10, pady=10, bg='#333333')
self.field_frame.pack()
# ステータスラベル (2番目)
self.status_label = tk.Label(master, text="スタートボタンを押してください", font=('Arial', 14))
self.status_label.pack(pady=5)
self.highlight_color = '#FFFFCC'
self.default_bg = master.cget('bg')
# 3. 手札とコントロールを格納するコンテナ (中央寄せ)
self.hand_control_container = tk.Frame(master, padx=10, pady=10)
# 中央寄せ: fill='none'とanchor='center'
self.hand_control_container.pack(fill='none', expand=False, anchor='center')
# 3.1 コントロールフレーム (ボタンを左に)
self.control_frame = tk.Frame(self.hand_control_container, padx=10, pady=0)
self.control_frame.pack(side=tk.LEFT, anchor='n') # LEFTに配置
self.start_button = tk.Button(self.control_frame, text="ゲームスタート", command=self.start_game, font=('Arial', 12), bg='lightgreen', width=12)
self.start_button.pack(pady=5)
self.pass_button = tk.Button(self.control_frame, text="パス", command=self.handle_pass, font=('Arial', 12), bg='salmon', state=tk.DISABLED, width=12)
self.pass_button.pack(pady=5)
# 3.2 手札フレーム (手札を右に)
self.hand_frame = tk.Frame(self.hand_control_container, padx=0, pady=0, bg=self.default_bg)
self.hand_frame.pack(side=tk.RIGHT, fill='none', expand=False, anchor='n') # RIGHTに配置
self.draw_field()
self.draw_hand(self.game.players[0])
def start_game(self):
if self.game.start_game():
self.start_button.config(state=tk.DISABLED)
self.update_gui()
current_player = self.game.get_current_player()
if self.game.is_running and not current_player.is_human:
self.run_turn()
elif self.game.is_running and current_player.is_human:
self.pass_button.config(state=tk.NORMAL)
else:
messagebox.showerror("エラー", "ゲームを開始できませんでした。Heart 7 が見つかりません。")
def reset_and_start_game(self):
self.start_button.config(state=tk.NORMAL, text="ゲームスタート")
self.pass_button.config(state=tk.DISABLED)
self.status_label.config(text="スタートボタンを押してください", fg='black')
self.game = Game(self.player_names, max_passes=3, num_jokers=1)
self.draw_field()
self.draw_hand(self.game.players[0])
def get_card_color(self, card_str):
if '♥' in card_str or '♦' in card_str:
return 'red', 'mistyrose'
elif card_str == 'JOKER':
return 'purple', 'yellow'
return 'black', 'white'
def get_card_highlight_color(self, card):
if card.is_joker:
return 'purple', 'gold'
if card.suit == 'Spade':
return 'white', '#333399'
elif card.suit == 'Heart':
return 'white', '#993333'
elif card.suit == 'Diamond':
return 'black', '#FFD700'
elif card.suit == 'Club':
return 'white', '#339933'
return 'black', 'lightgray'
def draw_field(self):
for widget in self.field_frame.winfo_children(): widget.destroy()
ranks = Card.RANKS
suits = Card.SUITS
for c, rank in enumerate(ranks):
tk.Label(self.field_frame, text=rank, width=6, font=('Arial', 10, 'bold'), bg='#555555', fg='white').grid(row=0, column=c+1)
for r, suit in enumerate(suits):
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'white'
tk.Label(self.field_frame, text=Card.SUIT_SYMBOLS.get(suit, suit[0]), width=2,
font=('Arial', 18, 'bold'), bg='#555555', fg=symbol_fg).grid(row=r+1, column=0)
for c in range(len(ranks)):
card_obj = self.game.field.table[suit][c] if suit in self.game.field.table else None
if isinstance(card_obj, Card):
card_str = card_obj.__str__()
fg, bg = self.get_card_color(card_str)
else:
if Card.RANKS[c] == '7':
card_str = Card.RANKS[c]
fg, bg = ('black', 'yellow')
else:
card_str = ' '
fg, bg = ('black', 'white')
card_label = tk.Label(self.field_frame, text=card_str, width=6, height=3,
bg=bg, fg=fg, font=('Arial', 12, 'bold'), relief=tk.SUNKEN)
card_label.grid(row=r+1, column=c+1, padx=1, pady=1)
def draw_hand(self, player):
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.hand_frame.config(bg=self.default_bg)
tk.Label(self.hand_frame, text=f"あなたの手札 ({len(player.hand)}枚):", font=('Arial', 10, 'bold')).pack(anchor='w', pady=(0, 5))
cards_by_suit = {suit: [] for suit in Card.SUITS}
jokers = []
for card in player.hand:
if card.is_joker:
jokers.append(card)
else:
cards_by_suit[card.suit].append(card)
display_order = ['Spade', 'Heart', 'Diamond', 'Club']
CARDS_PER_ROW = 5
for suit in display_order:
cards = cards_by_suit[suit]
if not cards:
continue
suit_frame = tk.Frame(self.hand_frame, bg=self.hand_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
suit_frame.pack(anchor='w', pady=5, padx=0)
symbol_frame = tk.Frame(suit_frame, bg=suit_frame.cget('bg'))
symbol_frame.pack(side=tk.LEFT, fill='y', padx=(5, 2))
symbol = Card.SUIT_SYMBOLS[suit]
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'black'
tk.Label(symbol_frame, text=f"{symbol}:", fg=symbol_fg, bg=symbol_frame.cget('bg'), font=('Arial', 14, 'bold'), width=2).pack(pady=(0, 0))
cards_container = tk.Frame(suit_frame, bg=suit_frame.cget('bg'))
cards_container.pack(side=tk.LEFT)
cards_row_frame = tk.Frame(cards_container, bg=cards_container.cget('bg'))
cards_row_frame.pack(anchor='w', pady=1)
for i, card in enumerate(cards):
if i > 0 and i % CARDS_PER_ROW == 0:
cards_row_frame = tk.Frame(cards_container, bg=cards_container.cget('bg'))
cards_row_frame.pack(anchor='w', pady=1)
is_playable = self.game.field.is_playable(card)
if is_playable:
fg, bg = self.get_card_highlight_color(card)
else:
fg, bg = self.get_card_color(card.__str__())
card_button = tk.Button(cards_row_frame, text=card.__str__(), width=4, height=2,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
if not is_playable:
card_button.config(state=tk.DISABLED, bg='lightgray', fg='darkgray')
if player.is_human and is_playable:
card_button.config(relief=tk.RAISED)
card_button.pack(side=tk.LEFT, padx=1)
if jokers:
joker_frame = tk.Frame(self.hand_frame, bg=self.hand_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
joker_frame.pack(anchor='w', pady=5, padx=0)
symbol_frame = tk.Frame(joker_frame, bg=joker_frame.cget('bg'))
symbol_frame.pack(side=tk.LEFT, fill='y', padx=(5, 2))
tk.Label(symbol_frame, text="JK:", fg='purple', bg=symbol_frame.cget('bg'), font=('Arial', 14, 'bold'), width=2).pack(pady=(0, 0))
cards_container = tk.Frame(joker_frame, bg=joker_frame.cget('bg'))
cards_container.pack(side=tk.LEFT)
joker_row_frame = tk.Frame(cards_container, bg=cards_container.cget('bg'))
joker_row_frame.pack(anchor='w', pady=1)
for i, card in enumerate(jokers):
if i > 0 and i % CARDS_PER_ROW == 0:
joker_row_frame = tk.Frame(cards_container, bg=cards_container.cget('bg'))
joker_row_frame.pack(anchor='w', pady=1)
fg, bg = self.get_card_highlight_color(card)
card_button = tk.Button(joker_row_frame, text=card.__str__(), width=4, height=2,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
card_button.pack(side=tk.LEFT, padx=1)
def update_hand_frame_highlight(self):
self.hand_frame.config(bg=self.highlight_color)
for widget in self.hand_frame.winfo_children():
if isinstance(widget, tk.Label):
widget.config(bg=self.highlight_color)
elif isinstance(widget, tk.Frame):
widget.config(bg=self.highlight_color)
for sub_frame in widget.winfo_children():
if isinstance(sub_frame, tk.Frame):
sub_frame.config(bg=self.highlight_color)
for row_frame in sub_frame.winfo_children():
if isinstance(row_frame, tk.Frame):
row_frame.config(bg=self.highlight_color)
if isinstance(row_frame, tk.Label):
row_frame.config(bg=self.highlight_color)
elif isinstance(sub_frame, tk.Label):
sub_frame.config(bg=self.highlight_color)
def update_gui(self):
if not self.game.is_running:
self.draw_field()
return
current_player = self.game.get_current_player()
self.draw_field()
status_text = f"ターン: {current_player.name} | パス: {current_player.passes_count}/{self.game.max_passes} | 手札: {len(current_player.hand)}枚"
self.status_label.config(text=status_text)
if current_player.is_human:
self.draw_hand(current_player)
self.pass_button.config(state=tk.NORMAL)
self.update_hand_frame_highlight()
else:
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.pass_button.config(state=tk.DISABLED)
self.hand_frame.config(bg=self.default_bg)
self.master.update()
def show_joker_options(self, joker_card):
playable_targets = []
field = self.game.field
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
if not isinstance(field.table[suit][rank_index], Card):
temp_card = Card(Card.SUITS.index(suit), rank_index)
if field.is_playable(temp_card):
playable_targets.append((suit, Card.RANKS[rank_index], temp_card.__str__()))
if not playable_targets:
messagebox.showinfo("ジョーカー", "現在、ジョーカーを代用して出せるカードがありません。")
return
joker_window = tk.Toplevel(self.master)
joker_window.title("ジョーカーの代用カードを選択")
joker_window.transient(self.master)
joker_window.grab_set()
tk.Label(joker_window, text="ジョーカーをどのカードの代わりに出しますか?", font=('Arial', 10, 'bold')).pack(padx=10, pady=10)
button_frame = tk.Frame(joker_window)
button_frame.pack(padx=10, pady=5)
row = 0
col = 0
for suit, rank, card_str in playable_targets:
fg, bg = self.get_card_color(card_str)
btn = tk.Button(button_frame, text=card_str, width=6, height=2,
bg=bg, fg=fg, font=('Arial', 12, 'bold'),
command=lambda s=suit, r=rank, w=joker_window: self.process_joker_choice(joker_card, s, r, w))
btn.grid(row=row, column=col, padx=5, pady=5)
col += 1
if col > 6:
col = 0
row += 1
joker_window.protocol("WM_DELETE_WINDOW", lambda: self.process_joker_choice(joker_card, None, None, joker_window))
self.master.wait_window(joker_window)
def process_joker_choice(self, joker_card, target_suit, target_rank, joker_window):
try:
joker_window.destroy()
except tk.TclError:
pass
if target_suit is None:
self.status_label.config(text="YOU の移動: ジョーカー使用をキャンセルしました", fg='orange')
return
player = self.game.get_current_player()
target_info = (target_suit, target_rank)
success, msg = self.game.play_card(player, joker_card, target_info)
if success:
if "強制提示" in msg:
self.status_label.config(text=f"YOU の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_card_click(self, card):
player = self.game.get_current_player()
if not player.is_human: return
if card.is_joker:
self.show_joker_options(card)
else:
success, msg = self.game.play_card(player, card)
if success:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_pass(self, event=None):
player = self.game.get_current_player()
if not player.is_human: return
success, msg = self.game.pass_turn(player)
if success:
self.status_label.config(text=f"YOU の移動: {msg}", fg='blue')
self.end_turn()
else:
messagebox.showerror("パス失敗", msg)
def end_turn(self):
winner = self.game.check_winner()
if winner:
self.status_label.config(text=f"🎉 勝者: {winner} 🎉", fg='red')
response = messagebox.askyesno("ゲーム終了", f"勝者は {winner} です!\nもう一度プレイしますか?")
if response:
self.reset_and_start_game()
else:
self.pass_button.config(state=tk.DISABLED)
self.master.quit()
return
if self.game.next_turn():
self.update_gui()
if not self.game.get_current_player().is_human:
self.run_turn()
else:
self.update_gui()
def run_turn(self):
current_player = self.game.get_current_player()
if current_player.is_human: return
self.master.after(500, lambda: self.ai_move(current_player))
def ai_move(self, player):
playable_cards = player.get_playable_cards(self.game.field)
if playable_cards:
non_jokers = [c for c in playable_cards if not c.is_joker]
if non_jokers:
card_to_play = max(non_jokers, key=lambda c: abs(c.value - 7))
success, msg = self.game.play_card(player, card_to_play)
elif playable_cards[0].is_joker:
joker = playable_cards[0]
target_info = None
field = self.game.field
candidates = []
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
if not isinstance(field.table[suit][rank_index], Card):
temp_card = Card(Card.SUITS.index(suit), rank_index)
if field.is_playable(temp_card):
candidates.append((suit, temp_card.rank))
if candidates:
candidates.sort(key=lambda x: abs(Card.RANK_VALUES[x[1]] - 7))
target_suit, target_rank = candidates[0]
target_info = (target_suit, target_rank)
if target_info:
success, msg = self.game.play_card(player, joker, target_info)
else:
success, msg = self.game.pass_turn(player)
else:
success, msg = self.game.pass_turn(player)
else:
success, msg = self.game.pass_turn(player)
if "強制提示" in msg:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='blue')
self.master.update()
self.master.after(500, self.end_turn)
# ======================================================================
# C. メイン実行
# ======================================================================
if __name__ == '__main__':
root = tk.Tk()
app = SevensGUI(root)
root.mainloop()
プロンプトno2(GUIの不安定要素のあるコード)
import random
import tkinter as tk
from tkinter import messagebox
from tkinter import simpledialog
# ======================================================================
# A. カード、デッキ、プレイヤー、フィールド、ゲームロジック
# ======================================================================
class Card:
SUITS = ['Spade', 'Heart', 'Diamond', 'Club']
RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
RANK_VALUES = {rank: i + 1 for i, rank in enumerate(RANKS)}
JOKER_SUIT = 'Joker'
JOKER_RANK = 'JK'
SUIT_SYMBOLS = {'Spade': '♠', 'Heart': '♥', 'Diamond': '♦', 'Club': '♣'}
def __init__(self, suit_index, rank_index, is_joker=False):
self.is_joker = is_joker
if is_joker:
self.suit = self.JOKER_SUIT
self.rank = self.JOKER_RANK
self.value = 0
self.suit_index = 5
else:
self.suit_index = suit_index
self.rank_index = rank_index
self.suit = self.SUITS[suit_index]
self.rank = self.RANKS[rank_index]
self.value = self.RANK_VALUES[self.rank]
self.symbol = self.SUIT_SYMBOLS.get(self.suit, '')
def __str__(self):
if self.is_joker: return "JOKER"
return f"{self.symbol}{self.rank}"
def __repr__(self):
return f"<Card: {self.__str__}>"
def __eq__(self, other):
if not isinstance(other, Card): return NotImplemented
if self.is_joker and other.is_joker: return True
if self.is_joker or other.is_joker: return False
return self.suit_index == other.suit_index and self.rank_index == other.rank_index
class Deck:
def __init__(self, num_jokers=1):
self.num_jokers = num_jokers
self.cards = []
self.reset()
def reset(self):
self.cards = []
for suit_index, suit in enumerate(Card.SUITS):
for rank_index, rank in enumerate(Card.RANKS):
self.cards.append(Card(suit_index, rank_index))
for _ in range(self.num_jokers):
self.cards.append(Card(0, 0, is_joker=True))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, num_cards):
if len(self.cards) < num_cards:
num_cards = len(self.cards)
dealt_cards = self.cards[:num_cards]
self.cards = self.cards[num_cards:]
return dealt_cards
class Player:
def __init__(self, name, is_human=False):
self.name = name
self.hand = []
self.passes_count = 0
self.is_human = is_human
def receive_cards(self, cards):
self.hand.extend(cards)
self.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
def remove_card(self, card_to_remove):
# カードオブジェクトの削除を厳密に行う
for i, card in enumerate(self.hand):
# 1. オブジェクトID(メモリ上のアドレス)が一致する場合を最優先で削除
if card is card_to_remove:
del self.hand[i]
return True
# 2. カード情報が一致する場合 (__eq__ に依存)
if card == card_to_remove:
del self.hand[i]
return True
return False
def can_play(self, field):
for card in self.hand:
if card.is_joker or field.is_playable(card):
return True
return False
def get_playable_cards(self, field):
playable = []
for card in self.hand:
if card.is_joker or field.is_playable(card):
playable.append(card)
return playable
class Field:
def __init__(self):
# self.tableにはカードが出ている場合、Trueが格納される (Noneは空の状態)
self.table = {suit: [None] * len(Card.RANKS) for suit in Card.SUITS}
self.center_index = Card.RANK_VALUES['7'] - 1 # 6
def place_card(self, card):
"""カードを場に出す。"""
if card.is_joker:
return False
suit = card.suit
rank_index = card.rank_index
if self.table[suit][rank_index] is not None:
return False
# Trueを格納し、その場所が使用済みであることを示す
self.table[suit][rank_index] = True
return True
def is_playable(self, card):
if card.is_joker: return True
suit = card.suit
rank_index = card.rank_index
# 1. すでにカードが出ているか? (True/None を判定)
if self.table[suit][rank_index] is not None:
return False
# 2. 7 のカードの場合
if card.value == 7:
# self.table[suit][rank_index] is None が既に確認されているため、常に True
return True
# 3. 7 より小さいカード (A, 2, ..., 6)
if card.value < 7:
next_index = rank_index + 1
if next_index <= self.center_index:
# 隣接カード(next_index)が「出ている」ことを確認する
return self.table[suit][next_index] is not None
# 7までの道筋に隣接していない場合は False
return False
# 4. 7 より大きいカード (8, 9, ..., K)
if card.value > 7:
prev_index = rank_index - 1
if prev_index >= self.center_index:
# 隣接カード(prev_index)が「出ている」ことを確認する
return self.table[suit][prev_index] is not None
# 7までの道筋に隣接していない場合は False
return False
return False
class Game:
def __init__(self, player_names, max_passes=3, num_jokers=1):
self.players = [Player(name, is_human=(name == 'YOU')) for name in player_names]
self.num_players = len(self.players)
self.deck = Deck(num_jokers=num_jokers)
self.field = Field()
self.max_passes = max_passes
self.current_player_index = -1
self.is_running = False
# Heart 7 のカードオブジェクトを作成
self.h7_card = Card(suit_index=1, rank_index=6)
def start_game(self):
for player in self.players:
player.hand = []
player.passes_count = 0
self.deck.reset()
self.deck.shuffle()
random.shuffle(self.players)
total_cards = len(self.deck.cards)
cards_per_player = total_cards // self.num_players
remainder = total_cards % self.num_players
for i, player in enumerate(self.players):
num_to_deal = cards_per_player + (1 if i < remainder else 0)
player.receive_cards(self.deck.deal(num_to_deal))
self.field = Field()
starter_index = -1
h7_card_in_hand = None
for i, player in enumerate(self.players):
for card in player.hand:
if card == self.h7_card:
starter_index = i
h7_card_in_hand = card
break
if starter_index != -1: break
if starter_index == -1: return False
starter = self.players[starter_index]
if h7_card_in_hand and starter.remove_card(h7_card_in_hand):
# Heart 7 を場に出す
self.field.place_card(h7_card_in_hand)
starter.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
self.current_player_index = (starter_index + 1) % self.num_players
self.is_running = True
return True
def get_current_player(self):
return self.players[self.current_player_index]
def enforce_seven_play(self, seven_card):
enforced_plays = []
for p in self.players:
if p.name == self.get_current_player().name:
continue
for card in p.hand:
if card == seven_card:
p.remove_card(card)
self.field.place_card(card)
enforced_plays.append(f"{p.name} が {seven_card.__str__()} を強制提示")
break
return enforced_plays
def play_card(self, player, card_to_play, target_card_info=None):
if card_to_play not in player.hand: return False, "手札にありません"
if card_to_play.is_joker:
if target_card_info is None: return False, "ジョーカーは代用するカードの情報を指定してください。"
target_suit, target_rank_str = target_card_info
try:
target_rank_index = Card.RANKS.index(target_rank_str)
target_suit_index = Card.SUITS.index(target_suit)
temp_card = Card(target_suit_index, target_rank_index)
except (ValueError, IndexError):
return False, "無効なスートまたはランクが指定されました。"
suit = temp_card.suit
rank_index = temp_card.rank_index
field = self.field
# 1. 既に出ているかのチェック
if field.table[suit][rank_index] is not None:
return False, f"そのカード({temp_card.__str__()})は既に出ています。"
# 2. 7 のカードの代用チェック
if temp_card.value == 7:
if temp_card == self.h7_card:
return False, "Heart 7 の代用は禁止されています。"
# 7のカードは、まだ出ていなければ(Noneであれば)代用可能
if field.table[suit][rank_index] is not None:
return False, "その7の代用はできません。"
# 3. 7 以外のカードの代用チェック
elif not field.is_playable(temp_card):
return False, f"ジョーカーを {temp_card.__str__()} の代わりに出せません。隣接カードがありません。"
player.remove_card(card_to_play)
# ジョーカー代用時も True を格納する
self.field.table[suit][rank_index] = True
forced_msg_list = []
if temp_card.value == 7:
# 7のカードを代用で出した場合も強制提示をチェック
forced_msg_list = self.enforce_seven_play(temp_card)
base_msg = f"ジョーカーを {temp_card.__str__()} の代わりに出しました。"
if forced_msg_list:
base_msg += " (" + ", ".join(forced_msg_list) + ")"
return True, base_msg
# ジョーカー以外のカードで、出せるカードがあるかチェック
if not self.field.is_playable(card_to_play):
return False, "そのカードは場に出せません。"
player.remove_card(card_to_play)
self.field.place_card(card_to_play)
# 7のカードを通常で出した場合も強制提示をチェック
forced_msg_list = []
if card_to_play.value == 7:
forced_msg_list = self.enforce_seven_play(card_to_play)
base_msg = f"{card_to_play} を出しました。"
if forced_msg_list:
base_msg += " (" + ", ".join(forced_msg_list) + ")"
return True, base_msg
def pass_turn(self, player):
# ★修正箇所: 出せるカードがあってもパスを可能にする (戦略的パスを許可)★
# if player.can_play(self.field):
# return False, "出せるカードがあるため、パスはできません。"
player.passes_count += 1
if player.passes_count > self.max_passes:
return True, f"{player.name} はパス制限回数を超え、脱落しました。"
return True, f"{player.name} はパスしました。"
def check_winner(self):
for player in self.players:
if not player.hand:
self.is_running = False
return player.name
active_players = [p for p in self.players if p.passes_count <= self.max_passes and p.hand]
if not active_players and self.is_running:
self.is_running = False
return "全員がパス制限を超え、ゲーム終了です。"
return None
def next_turn(self):
for _ in range(self.num_players):
self.current_player_index = (self.current_player_index + 1) % self.num_players
next_player = self.get_current_player()
if next_player.passes_count <= self.max_passes and next_player.hand:
return True
if self.check_winner():
return False
return False
# ======================================================================
# B. Tkinter GUI クラス
# ======================================================================
class SevensGUI:
def __init__(self, master):
self.master = master
master.title("七並べ (Sevens)")
self.player_names = ["YOU", "COM-A", "COM-B", "COM-C"]
self.game = Game(self.player_names, max_passes=3, num_jokers=1)
self.highlight_color = '#FFFFCC'
self.default_bg = master.cget('bg')
# 1. Top Container: Field + Player Status
self.top_container = tk.Frame(master, padx=10, pady=10)
self.top_container.pack(side=tk.TOP, fill='x')
# 1.1 Field Frame: 場
self.field_frame = tk.Frame(self.top_container, padx=10, pady=10, bg='#333333')
self.field_frame.pack(side=tk.TOP, fill='x', expand=False, pady=(0, 5))
# self.status_label を field_frame の中で使用するため、ここでインスタンス化
self.status_label = tk.Label(self.field_frame, text="スタートボタンを押してください",
font=('Arial', 12), justify=tk.LEFT, anchor='w',
bg=self.field_frame.cget('bg'), fg='white')
# 1.2 Player Status Frame: プレイヤーの状態
self.player_status_frame = tk.Frame(self.top_container, padx=0, pady=0)
self.player_status_frame.pack(side=tk.TOP, fill='x', expand=False, pady=(0, 5))
# 3. Bottom Frame: Hand + Control Buttons
self.bottom_container = tk.Frame(master, padx=10, pady=10)
self.bottom_container.pack(side=tk.BOTTOM, fill='both', expand=True)
# 3.2 Control Buttons (右側: 固定幅)
self.control_frame = tk.Frame(self.bottom_container, padx=10, pady=0, width=150)
self.control_frame.grid_propagate(False)
self.control_frame.pack(side=tk.RIGHT, fill='y')
self.start_button = tk.Button(self.control_frame, text="ゲームスタート", command=self.start_game, font=('Arial', 12), bg='lightgreen', width=12)
self.start_button.pack(pady=5, anchor=tk.N)
self.pass_button = tk.Button(self.control_frame, text="パス", command=self.handle_pass, font=('Arial', 12), bg='salmon', state=tk.DISABLED, width=12)
self.pass_button.pack(pady=5, anchor=tk.N)
# 3.1 Hand Frame (左側: 拡張可能) - スクロール可能な領域に変更
self.hand_outer_frame = tk.Frame(self.bottom_container, padx=0, pady=0, bg=self.default_bg)
self.hand_outer_frame.pack(side=tk.LEFT, fill='both', expand=True)
self.hand_scrollbar = tk.Scrollbar(self.hand_outer_frame, orient=tk.VERTICAL)
self.hand_scrollbar.pack(side=tk.RIGHT, fill='y')
self.hand_canvas = tk.Canvas(self.hand_outer_frame, yscrollcommand=self.hand_scrollbar.set, bg=self.default_bg, highlightthickness=0)
self.hand_canvas.pack(side=tk.LEFT, fill='both', expand=True)
self.hand_scrollbar.config(command=self.hand_canvas.yview)
self.hand_frame = tk.Frame(self.hand_canvas, bg=self.default_bg)
self.hand_frame_id = self.hand_canvas.create_window((0, 0), window=self.hand_frame, anchor='nw')
self.hand_frame.bind('<Configure>', lambda e: self.hand_canvas.config(scrollregion=self.hand_canvas.bbox("all")))
self.hand_canvas.bind('<Configure>', self._on_canvas_configure)
# 初期描画
self.draw_field()
self.draw_hand(self.game.players[0])
self.update_status_display()
def _on_canvas_configure(self, event):
canvas_width = event.width
self.hand_canvas.itemconfig(self.hand_frame_id, width=canvas_width)
def start_game(self):
if self.game.start_game():
self.start_button.config(state=tk.DISABLED)
self.status_label.config(text="Heart 7 が場に出されました。ゲーム開始!", fg='white')
self.update_gui()
current_player = self.game.get_current_player()
if self.game.is_running and not current_player.is_human:
self.run_turn()
elif self.game.is_running and current_player.is_human:
self.pass_button.config(state=tk.NORMAL)
else:
messagebox.showerror("エラー", "ゲームを開始できませんでした。Heart 7 の初期配置に失敗しました。")
def reset_and_start_game(self):
player_names = [p.name for p in self.game.players]
self.start_button.config(state=tk.NORMAL, text="ゲームスタート")
self.pass_button.config(state=tk.DISABLED)
self.status_label.config(text="スタートボタンを押してください", fg='white')
self.game = Game(player_names, max_passes=3, num_jokers=1)
self.draw_field()
self.draw_hand(self.game.players[0])
self.update_status_display()
def update_status_display(self):
for widget in self.player_status_frame.winfo_children():
widget.destroy()
if not self.game.players:
return
player_map = {p.name: p for p in self.game.players}
display_players = [player_map[name] for name in self.player_names if name in player_map]
for col, player in enumerate(display_players):
player_frame = tk.Frame(self.player_status_frame, padx=10, pady=5, relief=tk.RIDGE, borderwidth=2)
player_frame.pack(side=tk.LEFT, padx=5, fill=tk.Y)
is_current_player = (player == self.game.get_current_player()) and self.game.is_running
name_bg = 'lightblue' if is_current_player else 'lightgray'
name_fg = 'black' if is_current_player else 'darkslategray'
name_label = tk.Label(player_frame, text=player.name,
font=('Arial', 12, 'bold'),
bg=name_bg, fg=name_fg, width=8)
name_label.pack(pady=(0, 5))
hand_text = f"手札: {len(player.hand)}枚"
tk.Label(player_frame, text=hand_text, font=('Arial', 10)).pack()
pass_color = 'red' if player.passes_count > self.game.max_passes else 'orange' if player.passes_count >= 2 else 'black'
pass_text = f"パス: {player.passes_count}/{self.game.max_passes}"
tk.Label(player_frame, text=pass_text, font=('Arial', 10, 'bold'), fg=pass_color).pack()
if player.passes_count > self.game.max_passes:
status_text = "脱落"
tk.Label(player_frame, text=status_text, font=('Arial', 10, 'bold'), fg='red', bg='white').pack(pady=(5,0))
elif self.game.is_running and not player.hand:
status_text = "上がり"
tk.Label(player_frame, text=status_text, font=('Arial', 10, 'bold'), fg='green', bg='white').pack(pady=(5,0))
def get_card_color(self, card_str):
if '♥' in card_str or '♦' in card_str:
return 'red', 'mistyrose'
elif card_str == 'JOKER':
return 'purple', 'yellow'
return 'black', 'white'
def get_card_highlight_color(self, card):
if card.is_joker:
return 'purple', 'gold'
if card.value == 7:
return 'black', '#FFD700'
if card.suit == 'Spade':
return 'white', '#1E90FF'
elif card.suit == 'Heart':
return 'white', '#DC143C'
elif card.suit == 'Diamond':
return 'black', '#FFA500'
elif card.suit == 'Club':
return 'white', '#3CB371'
return 'black', 'lightgray'
def draw_field(self):
for widget in self.field_frame.winfo_children():
if widget is not self.status_label:
widget.destroy()
ranks = Card.RANKS
suits = Card.SUITS
NUM_RANKS = len(ranks)
NUM_SUITS = len(suits)
# ランク表示 (0行目)
for c, rank in enumerate(ranks):
# Column 1 から Column 13: カードランク
tk.Label(self.field_frame, text=rank, width=6, font=('Arial', 10, 'bold'), bg='#555555', fg='white').grid(row=0, column=c+1)
# スートシンボルとカード本体 (1行目から4行目)
for r, suit in enumerate(suits):
# Column 0: スートシンボル
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'white'
tk.Label(self.field_frame, text=Card.SUIT_SYMBOLS.get(suit, suit[0]), width=2,
font=('Arial', 18, 'bold'), bg='#555555', fg=symbol_fg).grid(row=r+1, column=0)
# Column 1 から Column 13: カード本体
for c in range(NUM_RANKS):
card_obj_or_bool = self.game.field.table[suit][c] if suit in self.game.field.table else None
# Trueの場合もカードが出ていると見なして描画する
if card_obj_or_bool is not None:
# 場に出ているカードの情報を取得
card_obj = Card(Card.SUITS.index(suit), c)
card_str = card_obj.__str__()
fg, bg = self.get_card_color(card_str)
else:
card_str = ' '
fg, bg = ('black', 'white')
card_label = tk.Label(self.field_frame, text=card_str, width=6, height=3,
bg=bg, fg=fg, font=('Arial', 12, 'bold'), relief=tk.SUNKEN)
card_label.grid(row=r+1, column=c+1, padx=1, pady=1)
# メッセージラベルをキング(K)の右に配置 (列インデックス: 1 + NUM_RANKS = 14)
self.status_label.grid(row=1, column=NUM_RANKS + 1, padx=10, sticky='nsew',
rowspan=NUM_SUITS)
# --- Weight の調整 ---
for c in range(1, NUM_RANKS + 1):
self.field_frame.grid_columnconfigure(c, weight=0)
self.field_frame.grid_columnconfigure(NUM_RANKS + 1, weight=1)
for r in range(NUM_SUITS + 1):
self.field_frame.grid_rowconfigure(r, weight=0)
# -------------------------------------------
# ★GUI改善後の draw_hand メソッド★
def draw_hand(self, player):
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.hand_frame.config(bg=self.default_bg)
# タイトルラベル
tk.Label(self.hand_frame, text=f"あなたの手札 ({len(player.hand)}枚):",
font=('Arial', 10, 'bold')).pack(anchor='w', pady=(0, 5))
cards_by_suit = {suit: [] for suit in Card.SUITS}
jokers = []
for card in player.hand:
if card.is_joker:
jokers.append(card)
else:
cards_by_suit[card.suit].append(card)
display_order = ['Spade', 'Heart', 'Diamond', 'Club']
# スートカードの表示
for suit in display_order:
cards = cards_by_suit[suit]
if not cards:
continue
# 各スートのコンテナフレーム
suit_container_frame = tk.Frame(self.hand_frame, bg=self.hand_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
suit_container_frame.pack(anchor='w', pady=3, padx=0, fill='x')
# スートシンボルのラベル (左端)
symbol_frame = tk.Frame(suit_container_frame, bg=suit_container_frame.cget('bg'))
symbol_frame.pack(side=tk.LEFT, fill='y', padx=(5, 5))
symbol = Card.SUIT_SYMBOLS[suit]
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'black'
tk.Label(symbol_frame, text=f"{symbol}:", fg=symbol_fg, bg=symbol_frame.cget('bg'),
font=('Arial', 14, 'bold'), width=2).pack(pady=0)
# カードボタンを配置するための内部フレーム
cards_frame = tk.Frame(suit_container_frame, bg=suit_container_frame.cget('bg'))
cards_frame.pack(side=tk.LEFT, fill='x', expand=True)
# カードの折り返し表示
cards_row_frame = tk.Frame(cards_frame, bg=cards_frame.cget('bg'))
cards_row_frame.pack(anchor='w', pady=1, fill='x')
CARDS_PER_ROW_FIXED = 13
for i, card in enumerate(cards):
if i > 0 and i % CARDS_PER_ROW_FIXED == 0:
# 13枚を超えたら新しい行に
cards_row_frame = tk.Frame(cards_frame, bg=cards_frame.cget('bg'))
cards_row_frame.pack(anchor='w', pady=1, fill='x')
is_playable = self.game.field.is_playable(card)
if is_playable:
fg, bg = self.get_card_highlight_color(card)
else:
fg = 'darkgray'
bg = 'lightgray'
card_button = tk.Button(cards_row_frame, text=card.__str__(), width=4, height=2,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
# ジョーカー以外の出せないカードは無効
if not is_playable and not card.is_joker:
card_button.config(state=tk.DISABLED)
# ジョーカーまたは出せるカードは有効
if player.is_human and (is_playable or card.is_joker):
card_button.config(relief=tk.RAISED)
card_button.pack(side=tk.LEFT, padx=1, pady=1)
# ジョーカーの表示
if jokers:
joker_container_frame = tk.Frame(self.hand_frame, bg=self.hand_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
joker_container_frame.pack(anchor='w', pady=3, padx=0, fill='x')
symbol_frame = tk.Frame(joker_container_frame, bg=joker_container_frame.cget('bg'))
symbol_frame.pack(side=tk.LEFT, fill='y', padx=(5, 5))
tk.Label(symbol_frame, text="JK:", fg='purple', bg=symbol_frame.cget('bg'),
font=('Arial', 14, 'bold'), width=2).pack(pady=0)
cards_frame = tk.Frame(joker_container_frame, bg=joker_container_frame.cget('bg'))
cards_frame.pack(side=tk.LEFT, fill='x', expand=True)
joker_row_frame = tk.Frame(cards_frame, bg=cards_frame.cget('bg'))
joker_row_frame.pack(anchor='w', pady=1, fill='x')
CARDS_PER_ROW_FIXED = 13
for i, card in enumerate(jokers):
if i > 0 and i % CARDS_PER_ROW_FIXED == 0:
joker_row_frame = tk.Frame(cards_frame, bg=cards_frame.cget('bg'))
joker_row_frame.pack(anchor='w', pady=1, fill='x')
fg, bg = self.get_card_highlight_color(card)
card_button = tk.Button(joker_row_frame, text=card.__str__(), width=4, height=2,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
card_button.pack(side=tk.LEFT, padx=1, pady=1)
self.hand_frame.update_idletasks()
self.hand_canvas.config(scrollregion=self.hand_canvas.bbox("all"))
def update_hand_frame_highlight(self):
# ... (中略 - 変更なし)
# 画面ハイライトのロジックは sevens-008.py と同一
for widget in self.hand_frame.winfo_children():
if isinstance(widget, tk.Label):
widget.config(bg=self.highlight_color)
elif isinstance(widget, tk.Frame):
widget.config(bg=self.highlight_color)
for sub_frame in widget.winfo_children():
if isinstance(sub_frame, tk.Frame):
sub_frame.config(bg=self.highlight_color)
for row_frame in sub_frame.winfo_children():
if isinstance(row_frame, tk.Frame):
row_frame.config(bg=self.highlight_color)
if isinstance(row_frame, tk.Label):
row_frame.config(bg=self.highlight_color)
elif isinstance(sub_frame, tk.Label):
sub_frame.config(bg=self.highlight_color)
def update_gui(self):
if not self.game.is_running:
self.draw_field()
return
current_player = self.game.get_current_player()
self.draw_field()
self.update_status_display()
if current_player.is_human:
self.draw_hand(current_player)
# ★修正: 出せるカードがある場合でもパスボタンを有効のままにする (Game.pass_turn が修正されているため)
self.pass_button.config(state=tk.NORMAL)
self.update_hand_frame_highlight()
else:
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.hand_canvas.config(scrollregion=(0, 0, 0, 0))
self.pass_button.config(state=tk.DISABLED)
self.hand_outer_frame.config(bg=self.default_bg)
self.hand_canvas.config(bg=self.default_bg)
self.status_label.config(text=f"{current_player.name} のターンです...", fg='white')
self.master.update()
def show_joker_options(self, joker_card):
# sevens-008.py と同一のバグ対策ロジック (誰の手札にも残っていないカードのみ候補にする)
all_hands_cards = set()
for p in self.game.players:
for card in p.hand:
if not card.is_joker:
all_hands_cards.add((card.suit, card.rank))
playable_targets = []
field = self.game.field
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
temp_card = Card(Card.SUITS.index(suit), rank_index)
# 1. 既に出ているカードはスキップ
if field.table[suit][rank_index] is not None:
continue
# 2. Heart 7 は代用不可
if temp_card == self.game.h7_card:
continue
# 3. 誰かの手札に残っているカードは代用不可
if (temp_card.suit, temp_card.rank) in all_hands_cards:
continue
if temp_card.value == 7:
# 他の 7 は場に出ていなければ常に代用可能
playable_targets.append((suit, Card.RANKS[rank_index], temp_card.__str__()))
elif field.is_playable(temp_card):
# 7 以外のカードは隣接カードが出ていれば代用可能
playable_targets.append((suit, Card.RANKS[rank_index], temp_card.__str__()))
if not playable_targets:
messagebox.showinfo("ジョーカー", "現在、ジョーカーを代用して出せるカードがありません。")
return
joker_window = tk.Toplevel(self.master)
joker_window.title("ジョーカーの代用カードを選択")
joker_window.transient(self.master)
joker_window.grab_set()
tk.Label(joker_window, text="ジョーカーをどのカードの代わりに出しますか?", font=('Arial', 10, 'bold')).pack(padx=10, pady=10)
button_frame = tk.Frame(joker_window)
button_frame.pack(padx=10, pady=5)
row = 0
col = 0
for suit, rank, card_str in playable_targets:
fg, bg = self.get_card_color(card_str)
btn = tk.Button(button_frame, text=card_str, width=6, height=2,
bg=bg, fg=fg, font=('Arial', 12, 'bold'),
command=lambda s=suit, r=rank, w=joker_window: self.process_joker_choice(joker_card, s, r, w))
btn.grid(row=row, column=col, padx=5, pady=5)
col += 1
if col > 6:
col = 0
row += 1
joker_window.protocol("WM_DELETE_WINDOW", lambda: self.process_joker_choice(joker_card, None, None, joker_window))
self.master.wait_window(joker_window)
def process_joker_choice(self, joker_card, target_suit, target_rank, joker_window):
# ... (中略 - 変更なし)
try:
joker_window.destroy()
except tk.TclError:
pass
if target_suit is None:
self.status_label.config(text="YOU の移動: ジョーカー使用をキャンセルしました", fg='orange')
return
player = self.game.get_current_player()
target_info = (target_suit, target_rank)
success, msg = self.game.play_card(player, joker_card, target_info)
if success:
if "強制提示" in msg:
self.status_label.config(text=f"YOU の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_card_click(self, card):
player = self.game.get_current_player()
if not player.is_human: return
# ★修正箇所1: ジョーカーは出せるカードの有無に関わらず常に実行可能とする★
if card.is_joker:
self.show_joker_options(card)
else:
success, msg = self.game.play_card(player, card)
if success:
if "強制提示" in msg:
self.status_label.config(text=f"YOU の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_pass(self, event=None):
player = self.game.get_current_player()
if not player.is_human: return
# ★修正箇所2: can_play() のチェックを削除したため、常にパスを試みる★
# ただし、GUIではcan_playがないとパスボタンを無効にできないため、
# can_play()がTrueを返す場合はGUI側でボタンを無効化せず、Game側で常にパスを許可する。
success, msg = self.game.pass_turn(player)
if success:
self.status_label.config(text=f"YOU の移動: {msg}", fg='blue')
self.end_turn()
else:
# Game.pass_turn から can_play のチェックを削除したため、通常このエラーは出ません。
messagebox.showerror("パス失敗", msg)
def end_turn(self):
# ... (中略 - 変更なし)
winner = self.game.check_winner()
if winner:
self.status_label.config(text=f"🎉 勝者: {winner} 🎉", fg='red')
self.update_status_display()
response = messagebox.askyesno("ゲーム終了", f"勝者は {winner} です!\nもう一度プレイしますか?")
if response:
self.reset_and_start_game()
else:
self.pass_button.config(state=tk.DISABLED)
self.master.quit()
return
if self.game.next_turn():
self.update_gui()
if not self.game.get_current_player().is_human:
self.run_turn()
else:
self.update_gui()
def run_turn(self):
# ... (中略 - 変更なし)
current_player = self.game.get_current_player()
if current_player.is_human: return
self.master.after(500, lambda: self.ai_move(current_player))
def ai_move(self, player):
# ... (後略 - AIのロジックは sevens-008.py と同一)
playable_cards = player.get_playable_cards(self.game.field)
target_info = None
card_to_play = None
if not playable_cards:
# パス
success, msg = self.game.pass_turn(player)
else:
joker = next((c for c in playable_cards if c.is_joker), None)
non_jokers = [c for c in playable_cards if not c.is_joker]
if non_jokers:
# 7から遠いカードを優先して出す (手札を減らす戦略)
card_to_play = max(non_jokers, key=lambda c: abs(c.value - 7))
success, msg = self.game.play_card(player, card_to_play)
elif joker:
# ジョーカーしか出せない場合
field = self.game.field
# 全てのプレイヤーの手札にあるカードのセットを作成
all_hands_cards = set()
for p in self.game.players:
for card in p.hand:
if not card.is_joker:
all_hands_cards.add((card.suit, card.rank))
candidates = []
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
temp_card = Card(Card.SUITS.index(suit), rank_index)
# 1. 既に出ているか?
if field.table[suit][rank_index] is not None: continue
# 2. Heart 7 の代用禁止
if temp_card == self.game.h7_card: continue
# 3. 誰かの手札に残っているか?
if (temp_card.suit, temp_card.rank) in all_hands_cards: continue
# 4. 7または隣接カードが出ているか?
if temp_card.value == 7 or field.is_playable(temp_card):
candidates.append((suit, temp_card.rank))
if candidates:
# 7に近いカードを優先して代用する (場を繋げる戦略)
candidates.sort(key=lambda x: abs(Card.RANK_VALUES[x[1]] - 7))
target_suit, target_rank = candidates[0]
target_info = (target_suit, target_rank)
card_to_play = joker
success, msg = self.game.play_card(player, card_to_play, target_info)
else:
# 代用できるカードがない場合はパス
success, msg = self.game.pass_turn(player)
else:
success, msg = self.game.pass_turn(player)
if "強制提示" in msg:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='white')
self.master.update()
self.master.after(500, self.end_turn)
# ======================================================================
# C. メイン実行
# ======================================================================
if __name__ == '__main__':
root = tk.Tk()
app = SevensGUI(root)
root.mainloop()
プロンプトno3(不安定な要素は無いがGUIの改善が必要なコード)
import random
import tkinter as tk
from tkinter import messagebox
from tkinter import simpledialog
# ======================================================================
# A. カード、デッキ、プレイヤー、フィールド、ゲームロジック
# ======================================================================
class Card:
SUITS = ['Spade', 'Heart', 'Diamond', 'Club']
RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
RANK_VALUES = {rank: i + 1 for i, rank in enumerate(RANKS)}
JOKER_SUIT = 'Joker'
JOKER_RANK = 'JK'
SUIT_SYMBOLS = {'Spade': '♠', 'Heart': '♥', 'Diamond': '♦', 'Club': '♣'}
def __init__(self, suit_index, rank_index, is_joker=False):
self.is_joker = is_joker
if is_joker:
self.suit = self.JOKER_SUIT
self.rank = self.JOKER_RANK
self.value = 0
self.suit_index = 5
else:
self.suit_index = suit_index
self.rank_index = rank_index
self.suit = self.SUITS[suit_index]
self.rank = self.RANKS[rank_index]
self.value = self.RANK_VALUES[self.rank]
self.symbol = self.SUIT_SYMBOLS.get(self.suit, '')
def __str__(self):
if self.is_joker: return "JOKER"
return f"{self.symbol}{self.rank}"
def __repr__(self):
return f"<Card: {self.__str__}>"
def __eq__(self, other):
if not isinstance(other, Card): return NotImplemented
if self.is_joker and other.is_joker: return True
if self.is_joker or other.is_joker: return False
return self.suit_index == other.suit_index and self.rank_index == other.rank_index
class Deck:
def __init__(self, num_jokers=1):
self.num_jokers = num_jokers
self.cards = []
self.reset()
def reset(self):
self.cards = []
for suit_index, suit in enumerate(Card.SUITS):
for rank_index, rank in enumerate(Card.RANKS):
self.cards.append(Card(suit_index, rank_index))
for _ in range(self.num_jokers):
self.cards.append(Card(0, 0, is_joker=True))
def shuffle(self):
random.shuffle(self.cards)
def deal(self, num_cards):
if len(self.cards) < num_cards:
num_cards = len(self.cards)
dealt_cards = self.cards[:num_cards]
self.cards = self.cards[num_cards:]
return dealt_cards
class Player:
def __init__(self, name, is_human=False):
self.name = name
self.hand = []
self.passes_count = 0
self.is_human = is_human
def receive_cards(self, cards):
self.hand.extend(cards)
self.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
def remove_card(self, card_to_remove):
# カードオブジェクトの削除を厳密に行う
for i, card in enumerate(self.hand):
# 1. オブジェクトID(メモリ上のアドレス)が一致する場合を最優先で削除
if card is card_to_remove:
del self.hand[i]
return True
# 2. カード情報が一致する場合 (__eq__ に依存)
if card == card_to_remove:
del self.hand[i]
return True
return False
def can_play(self, field):
for card in self.hand:
if card.is_joker or field.is_playable(card):
return True
return False
def get_playable_cards(self, field):
playable = []
for card in self.hand:
if card.is_joker or field.is_playable(card):
playable.append(card)
return playable
class Field:
def __init__(self):
# self.tableにはカードが出ている場合、Trueが格納される (Noneは空の状態)
self.table = {suit: [None] * len(Card.RANKS) for suit in Card.SUITS}
self.center_index = Card.RANK_VALUES['7'] - 1 # 6
def place_card(self, card):
"""カードを場に出す。"""
if card.is_joker:
return False
suit = card.suit
rank_index = card.rank_index
if self.table[suit][rank_index] is not None:
return False
# Trueを格納し、その場所が使用済みであることを示す
self.table[suit][rank_index] = True
return True
def is_playable(self, card):
if card.is_joker: return True
suit = card.suit
rank_index = card.rank_index
# 1. すでにカードが出ているか? (True/None を判定)
if self.table[suit][rank_index] is not None:
return False
# 2. 7 のカードの場合
if card.value == 7:
# self.table[suit][rank_index] is None が既に確認されているため、常に True
return True
# 3. 7 より小さいカード (A, 2, ..., 6)
if card.value < 7:
next_index = rank_index + 1
if next_index <= self.center_index:
# 隣接カード(next_index)が「出ている」ことを確認する
return self.table[suit][next_index] is not None
# 7までの道筋に隣接していない場合は False
return False
# 4. 7 より大きいカード (8, 9, ..., K)
if card.value > 7:
prev_index = rank_index - 1
if prev_index >= self.center_index:
# 隣接カード(prev_index)が「出ている」ことを確認する
return self.table[suit][prev_index] is not None
# 7までの道筋に隣接していない場合は False
return False
return False
class Game:
def __init__(self, player_names, max_passes=3, num_jokers=1):
self.players = [Player(name, is_human=(name == 'YOU')) for name in player_names]
self.num_players = len(self.players)
self.deck = Deck(num_jokers=num_jokers)
self.field = Field()
self.max_passes = max_passes
self.current_player_index = -1
self.is_running = False
# Heart 7 のカードオブジェクトを作成
self.h7_card = Card(suit_index=1, rank_index=6)
def start_game(self):
for player in self.players:
player.hand = []
player.passes_count = 0
self.deck.reset()
self.deck.shuffle()
random.shuffle(self.players)
total_cards = len(self.deck.cards)
cards_per_player = total_cards // self.num_players
remainder = total_cards % self.num_players
for i, player in enumerate(self.players):
num_to_deal = cards_per_player + (1 if i < remainder else 0)
player.receive_cards(self.deck.deal(num_to_deal))
self.field = Field()
starter_index = -1
h7_card_in_hand = None
for i, player in enumerate(self.players):
for card in player.hand:
if card == self.h7_card:
starter_index = i
h7_card_in_hand = card
break
if starter_index != -1: break
if starter_index == -1: return False
starter = self.players[starter_index]
if h7_card_in_hand and starter.remove_card(h7_card_in_hand):
# Heart 7 を場に出す
self.field.place_card(h7_card_in_hand)
starter.hand.sort(key=lambda card: (card.suit_index if not card.is_joker else 5,
card.value if not card.is_joker else 0))
self.current_player_index = (starter_index + 1) % self.num_players
self.is_running = True
return True
def get_current_player(self):
return self.players[self.current_player_index]
def enforce_seven_play(self, seven_card):
"""7のカードが出た際に、そのカードを誰かの手札から強制的に場に出させる(バグ対策含む)"""
enforced_plays = []
# 7のカードオブジェクトを作成(ジョーカー代用時も正しいカード情報が必要)
enforced_card = Card(Card.SUITS.index(seven_card.suit), seven_card.rank_index)
for p in self.players:
if p.name == self.get_current_player().name:
continue
# 手札に強制提示対象のカードがあるかを確認し、削除する
for card in p.hand:
if card == enforced_card:
p.remove_card(card)
# 既に場に出ているため Field.place_card は不要だが、
# ログを残すため forced_plays に追加
enforced_plays.append(f"{p.name} が {enforced_card.__str__()} を強制提示")
break
return enforced_plays
def play_card(self, player, card_to_play, target_card_info=None):
if card_to_play not in player.hand: return False, "手札にありません"
if card_to_play.is_joker:
if target_card_info is None: return False, "ジョーカーは代用するカードの情報を指定してください。"
target_suit, target_rank_str = target_card_info
try:
target_rank_index = Card.RANKS.index(target_rank_str)
target_suit_index = Card.SUITS.index(target_suit)
temp_card = Card(target_suit_index, target_rank_index)
except (ValueError, IndexError):
return False, "無効なスートまたはランクが指定されました。"
suit = temp_card.suit
rank_index = temp_card.rank_index
field = self.field
# 1. 既に出ているかのチェック
if field.table[suit][rank_index] is not None:
return False, f"そのカード({temp_card.__str__()})は既に出ています。"
# 2. 7 のカードの代用チェック
if temp_card.value == 7:
if temp_card == self.h7_card:
return False, "Heart 7 の代用は禁止されています。"
# 7のカードは、まだ出ていなければ(Noneであれば)代用可能
# 3. 7 以外のカードの代用チェック
elif not field.is_playable(temp_card):
return False, f"ジョーカーを {temp_card.__str__()} の代わりに出せません。隣接カードがありません。"
# ジョーカーを消費
player.remove_card(card_to_play)
# ジョーカー代用時も True を格納する
self.field.table[suit][rank_index] = True
forced_msg_list = []
if temp_card.value == 7:
# 7のカードを代用で出した場合も強制提示をチェック
# このとき、COMの手札に残っていた7のカードはここで削除されるため、整合性が保たれます。
forced_msg_list = self.enforce_seven_play(temp_card)
base_msg = f"ジョーカーを {temp_card.__str__()} の代わりに出しました。"
if forced_msg_list:
base_msg += " (" + ", ".join(forced_msg_list) + ")"
return True, base_msg
# ジョーカー以外のカードで、出せるカードがあるかチェック
if not self.field.is_playable(card_to_play):
return False, "そのカードは場に出せません。"
player.remove_card(card_to_play)
self.field.place_card(card_to_play)
# 7のカードを通常で出した場合も強制提示をチェック
forced_msg_list = []
if card_to_play.value == 7:
forced_msg_list = self.enforce_seven_play(card_to_play)
base_msg = f"{card_to_play} を出しました。"
if forced_msg_list:
base_msg += " (" + ", ".join(forced_msg_list) + ")"
return True, base_msg
def pass_turn(self, player):
# 戦略的パスを許可
player.passes_count += 1
if player.passes_count > self.max_passes:
return True, f"{player.name} はパス制限回数を超え、脱落しました。"
return True, f"{player.name} はパスしました。"
def check_winner(self):
for player in self.players:
if not player.hand:
self.is_running = False
return player.name
active_players = [p for p in self.players if p.passes_count <= self.max_passes and p.hand]
if not active_players and self.is_running:
self.is_running = False
return "全員がパス制限を超え、ゲーム終了です。"
return None
def next_turn(self):
for _ in range(self.num_players):
self.current_player_index = (self.current_player_index + 1) % self.num_players
next_player = self.get_current_player()
if next_player.passes_count <= self.max_passes and next_player.hand:
return True
if self.check_winner():
return False
return False
# ======================================================================
# B. Tkinter GUI クラス
# ======================================================================
class SevensGUI:
def __init__(self, master):
self.master = master
master.title("七並べ (Sevens)")
# GUI修正1: ウィンドウサイズの指定 (1920x1080 に収まるよう設定)
master.geometry("1200x700")
self.player_names = ["YOU", "COM-A", "COM-B", "COM-C"]
self.game = Game(self.player_names, max_passes=3, num_jokers=1)
self.highlight_color = '#FFFFCC'
self.default_bg = master.cget('bg')
# 1. Top Container: Field + Player Status
self.top_container = tk.Frame(master, padx=10, pady=10)
self.top_container.pack(side=tk.TOP, fill='x')
# 1.1 Field Frame: 場
self.field_frame = tk.Frame(self.top_container, padx=10, pady=10, bg='#333333')
self.field_frame.pack(side=tk.TOP, fill='x', expand=False, pady=(0, 5))
# self.status_label を field_frame の中で使用するため、ここでインスタンス化
self.status_label = tk.Label(self.field_frame, text="スタートボタンを押してください",
font=('Arial', 12), justify=tk.LEFT, anchor='w',
bg=self.field_frame.cget('bg'), fg='white')
# 1.2 Player Status Frame: プレイヤーの状態
self.player_status_frame = tk.Frame(self.top_container, padx=0, pady=0)
self.player_status_frame.pack(side=tk.TOP, fill='x', expand=False, pady=(0, 5))
# 3. Bottom Frame: Hand + Control Buttons (画面下部の操作エリア全体)
self.bottom_container = tk.Frame(master, padx=10, pady=10)
self.bottom_container.pack(side=tk.BOTTOM, fill='both', expand=True)
# ★★★ GUI修正4: Control Buttons を手札エリアのすぐ上に配置 ★★★
self.control_frame = tk.Frame(self.bottom_container, padx=10, pady=0)
# 手札の上に水平方向に配置
self.control_frame.pack(side=tk.TOP, fill='x', anchor='w')
self.start_button = tk.Button(self.control_frame, text="ゲームスタート", command=self.start_game, font=('Arial', 12), bg='lightgreen', width=12)
self.start_button.pack(side=tk.LEFT, pady=5, padx=(0, 10)) # 左端に配置
self.pass_button = tk.Button(self.control_frame, text="パス", command=self.handle_pass, font=('Arial', 12), bg='salmon', state=tk.DISABLED, width=12)
self.pass_button.pack(side=tk.LEFT, pady=5) # スタートボタンの隣に配置
# 3.1 Hand Frame (下側: 拡張可能) - 水平スクロール可能な領域
self.hand_outer_frame = tk.Frame(self.bottom_container, padx=0, pady=0, bg=self.default_bg)
# ★★★ GUI修正4: control_frame の下に配置 ★★★
self.hand_outer_frame.pack(side=tk.TOP, fill='both', expand=True)
# スクロールバーとキャンバスの設定はそのまま維持
self.hand_scrollbar = tk.Scrollbar(self.hand_outer_frame, orient=tk.HORIZONTAL)
self.hand_scrollbar.pack(side=tk.BOTTOM, fill='x')
self.hand_canvas = tk.Canvas(self.hand_outer_frame, xscrollcommand=self.hand_scrollbar.set, bg=self.default_bg, highlightthickness=0)
self.hand_canvas.pack(side=tk.TOP, fill='both', expand=True)
self.hand_scrollbar.config(command=self.hand_canvas.xview)
# Frame の配置を 'nw' に設定 (左上基準)
self.hand_frame = tk.Frame(self.hand_canvas, bg=self.default_bg)
# hand_frame はコンテンツサイズに合わせて拡張し、キャンバスに埋め込む
self.hand_canvas.create_window((0, 0), window=self.hand_frame, anchor='nw')
# コンテンツのサイズが変わったらスクロール領域を更新
self.hand_frame.bind('<Configure>', lambda e: self.hand_canvas.config(scrollregion=self.hand_canvas.bbox("all")))
# 初期描画
self.draw_field()
self.draw_hand(self.game.players[0])
self.update_status_display()
def _on_canvas_configure(self, event):
# 水平スクロールに変更したため、このメソッドは不要
pass
def start_game(self):
if self.game.start_game():
self.start_button.config(state=tk.DISABLED)
self.status_label.config(text="Heart 7 が場に出されました。ゲーム開始!", fg='white')
self.update_gui()
current_player = self.game.get_current_player()
if self.game.is_running and not current_player.is_human:
self.run_turn()
elif self.game.is_running and current_player.is_human:
self.pass_button.config(state=tk.NORMAL)
else:
messagebox.showerror("エラー", "ゲームを開始できませんでした。Heart 7 の初期配置に失敗しました。")
def reset_and_start_game(self):
player_names = [p.name for p in self.game.players]
self.start_button.config(state=tk.NORMAL, text="ゲームスタート")
self.pass_button.config(state=tk.DISABLED)
self.status_label.config(text="スタートボタンを押してください", fg='white')
self.game = Game(player_names, max_passes=3, num_jokers=1)
self.draw_field()
self.draw_hand(self.game.players[0])
self.update_status_display()
def update_status_display(self):
for widget in self.player_status_frame.winfo_children():
widget.destroy()
if not self.game.players:
return
player_map = {p.name: p for p in self.game.players}
display_players = [player_map[name] for name in self.player_names if name in player_map]
for col, player in enumerate(display_players):
player_frame = tk.Frame(self.player_status_frame, padx=10, pady=5, relief=tk.RIDGE, borderwidth=2)
player_frame.pack(side=tk.LEFT, padx=5, fill=tk.Y)
is_current_player = (player == self.game.get_current_player()) and self.game.is_running
name_bg = 'lightblue' if is_current_player else 'lightgray'
name_fg = 'black' if is_current_player else 'darkslategray'
name_label = tk.Label(player_frame, text=player.name,
font=('Arial', 12, 'bold'),
bg=name_bg, fg=name_fg, width=8)
name_label.pack(pady=(0, 5))
hand_text = f"手札: {len(player.hand)}枚"
tk.Label(player_frame, text=hand_text, font=('Arial', 10)).pack()
pass_color = 'red' if player.passes_count > self.game.max_passes else 'orange' if player.passes_count >= 2 else 'black'
pass_text = f"パス: {player.passes_count}/{self.game.max_passes}"
tk.Label(player_frame, text=pass_text, font=('Arial', 10, 'bold'), fg=pass_color).pack()
if player.passes_count > self.game.max_passes:
status_text = "脱落"
tk.Label(player_frame, text=status_text, font=('Arial', 10, 'bold'), fg='red', bg='white').pack(pady=(5,0))
elif self.game.is_running and not player.hand:
status_text = "上がり"
tk.Label(player_frame, text=status_text, font=('Arial', 10, 'bold'), fg='green', bg='white').pack(pady=(5,0))
def get_card_color(self, card_str):
if '♥' in card_str or '♦' in card_str:
return 'red', 'mistyrose'
elif card_str == 'JOKER':
return 'purple', 'yellow'
return 'black', 'white'
def get_card_highlight_color(self, card):
if card.is_joker:
return 'purple', 'gold'
if card.value == 7:
return 'black', '#FFD700'
if card.suit == 'Spade':
return 'white', '#1E90FF'
elif card.suit == 'Heart':
return 'white', '#DC143C'
elif card.suit == 'Diamond':
return 'black', '#FFA500'
elif card.suit == 'Club':
return 'white', '#3CB371'
return 'black', 'lightgray'
def draw_field(self):
for widget in self.field_frame.winfo_children():
if widget is not self.status_label:
widget.destroy()
ranks = Card.RANKS
suits = Card.SUITS
NUM_RANKS = len(ranks)
NUM_SUITS = len(suits)
# ランク表示 (0行目)
for c, rank in enumerate(ranks):
# Column 1 から Column 13: カードランク
tk.Label(self.field_frame, text=rank, width=6, font=('Arial', 10, 'bold'), bg='#555555', fg='white').grid(row=0, column=c+1)
# スートシンボルとカード本体 (1行目から4行目)
for r, suit in enumerate(suits):
# Column 0: スートシンボル
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'white'
tk.Label(self.field_frame, text=Card.SUIT_SYMBOLS.get(suit, suit[0]), width=2,
font=('Arial', 18, 'bold'), bg='#555555', fg=symbol_fg).grid(row=r+1, column=0)
# Column 1 から Column 13: カード本体
for c in range(NUM_RANKS):
card_obj_or_bool = self.game.field.table[suit][c] if suit in self.game.field.table else None
# Trueの場合もカードが出ていると見なして描画する
if card_obj_or_bool is not None:
# 場に出ているカードの情報を取得
card_obj = Card(Card.SUITS.index(suit), c)
card_str = card_obj.__str__()
fg, bg = self.get_card_color(card_str)
else:
card_str = ' '
fg, bg = ('black', 'white')
# GUI修正2: height=2 に変更し、フィールドの縦幅を縮小
card_label = tk.Label(self.field_frame, text=card_str, width=6, height=2,
bg=bg, fg=fg, font=('Arial', 12, 'bold'), relief=tk.SUNKEN)
card_label.grid(row=r+1, column=c+1, padx=1, pady=1)
# メッセージラベルをキング(K)の右に配置 (列インデックス: 1 + NUM_RANKS = 14)
self.status_label.grid(row=1, column=NUM_RANKS + 1, padx=10, sticky='nsew',
rowspan=NUM_SUITS)
# --- Weight の調整 ---
for c in range(1, NUM_RANKS + 1):
self.field_frame.grid_columnconfigure(c, weight=0)
self.field_frame.grid_columnconfigure(NUM_RANKS + 1, weight=1)
for r in range(NUM_SUITS + 1):
self.field_frame.grid_rowconfigure(r, weight=0)
# -------------------------------------------
def draw_hand(self, player):
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.hand_frame.config(bg=self.default_bg)
# タイトルラベル
tk.Label(self.hand_frame, text=f"あなたの手札 ({len(player.hand)}枚):",
font=('Arial', 10, 'bold')).pack(anchor='w', pady=(0, 5), padx=5)
cards_by_suit = {suit: [] for suit in Card.SUITS}
jokers = []
for card in player.hand:
if card.is_joker:
jokers.append(card)
else:
cards_by_suit[card.suit].append(card)
display_order = ['Spade', 'Heart', 'Diamond', 'Club']
# GUI修正3: 全スートのカードを格納する単一のフレーム (水平スクロールさせる対象)
self.all_cards_frame = tk.Frame(self.hand_frame, bg=self.hand_frame.cget('bg'))
self.all_cards_frame.pack(anchor='w', fill='x', pady=0)
# スートカードの表示
for suit in display_order:
cards = cards_by_suit[suit]
if not cards:
continue
# 各スートのコンテナフレーム
# GUI修正3: all_cards_frame に横並びで配置 (side=tk.LEFT)
suit_container_frame = tk.Frame(self.all_cards_frame, bg=self.all_cards_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
suit_container_frame.pack(side=tk.LEFT, pady=3, padx=5, fill=tk.Y)
# スートシンボルのラベル (上端)
symbol_frame = tk.Frame(suit_container_frame, bg=suit_container_frame.cget('bg'))
symbol_frame.pack(side=tk.TOP, fill='x', padx=5)
symbol = Card.SUIT_SYMBOLS[suit]
symbol_fg = 'red' if suit in ('Heart', 'Diamond') else 'black'
tk.Label(symbol_frame, text=f"{symbol}:", fg=symbol_fg, bg=symbol_frame.cget('bg'),
font=('Arial', 14, 'bold')).pack(side=tk.LEFT, pady=0)
# カードボタンを配置するための内部フレーム (縦方向にスタック)
cards_frame = tk.Frame(suit_container_frame, bg=suit_container_frame.cget('bg'))
cards_frame.pack(side=tk.TOP, fill='y', expand=True)
for i, card in enumerate(cards):
is_playable = self.game.field.is_playable(card)
if is_playable:
fg, bg = self.get_card_highlight_color(card)
else:
fg = 'darkgray'
bg = 'lightgray'
# GUI修正3: width=3, height=1 に縮小
card_button = tk.Button(cards_frame, text=card.__str__(), width=3, height=1,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
if not is_playable and not card.is_joker:
card_button.config(state=tk.DISABLED)
if player.is_human and (is_playable or card.is_joker):
card_button.config(relief=tk.RAISED)
card_button.pack(side=tk.TOP, padx=1, pady=1) # 縦方向に配置
# ジョーカーの表示
if jokers:
# GUI修正3: all_cards_frame に横並びで配置 (side=tk.LEFT)
joker_container_frame = tk.Frame(self.all_cards_frame, bg=self.all_cards_frame.cget('bg'), relief=tk.RIDGE, borderwidth=2)
joker_container_frame.pack(side=tk.LEFT, pady=3, padx=5, fill=tk.Y)
symbol_frame = tk.Frame(joker_container_frame, bg=joker_container_frame.cget('bg'))
symbol_frame.pack(side=tk.TOP, fill='x', padx=5)
tk.Label(symbol_frame, text="JK:", fg='purple', bg=symbol_frame.cget('bg'),
font=('Arial', 14, 'bold')).pack(side=tk.LEFT, pady=0)
cards_frame = tk.Frame(joker_container_frame, bg=joker_container_frame.cget('bg'))
cards_frame.pack(side=tk.TOP, fill='y', expand=True)
for i, card in enumerate(jokers):
fg, bg = self.get_card_highlight_color(card)
# GUI修正3: width=3, height=1 に縮小
card_button = tk.Button(cards_frame, text=card.__str__(), width=3, height=1,
bg=bg, fg=fg, font=('Arial', 9, 'bold'),
command=lambda c=card: self.handle_card_click(c))
card_button.pack(side=tk.TOP, padx=1, pady=1) # 縦方向に配置
self.hand_frame.update_idletasks()
# GUI修正3: scrollregion を更新して水平スクロールを可能にする
self.hand_canvas.config(scrollregion=self.hand_canvas.bbox("all"))
def update_hand_frame_highlight(self):
# 現在のプレイヤーのターンであることを示すため、手札フレームをハイライト
current_player = self.game.get_current_player()
if not current_player.is_human: return
# self.hand_frame の直下にあるウィジェット(タイトルラベルと self.all_cards_frame)をハイライト
for widget in self.hand_frame.winfo_children():
# Frameの場合、再帰的にハイライトを適用
if isinstance(widget, tk.Frame):
self._highlight_frame_recursively(widget)
elif isinstance(widget, tk.Label): # タイトルラベル
widget.config(bg=self.highlight_color)
def _highlight_frame_recursively(self, frame):
frame.config(bg=self.highlight_color)
for child in frame.winfo_children():
if isinstance(child, tk.Frame):
self._highlight_frame_recursively(child)
elif isinstance(child, tk.Label):
child.config(bg=self.highlight_color)
def update_gui(self):
if not self.game.is_running:
self.draw_field()
return
current_player = self.game.get_current_player()
self.draw_field()
self.update_status_display()
if current_player.is_human:
self.draw_hand(current_player)
self.pass_button.config(state=tk.NORMAL)
self.update_hand_frame_highlight()
else:
# COMのターン中は手札表示を非表示に
for widget in self.hand_frame.winfo_children(): widget.destroy()
self.hand_canvas.config(scrollregion=(0, 0, 0, 0))
self.pass_button.config(state=tk.DISABLED)
self.hand_outer_frame.config(bg=self.default_bg)
self.hand_canvas.config(bg=self.default_bg)
self.status_label.config(text=f"{current_player.name} のターンです...", fg='white')
self.master.update()
def show_joker_options(self, joker_card):
# プレイヤーがジョーカーをクリックした場合に呼ばれます。
playable_targets = []
field = self.game.field
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
temp_card = Card(Card.SUITS.index(suit), rank_index)
# 1. 既に出ているカードはスキップ
if field.table[suit][rank_index] is not None:
continue
# 2. Heart 7 は代用不可
if temp_card == self.game.h7_card:
continue
# 3. 代用可能かどうかのチェック (7のカード、または隣接カード)
if temp_card.value == 7 or field.is_playable(temp_card):
# バグ修正: 誰の手札にあっても代用候補に含める
playable_targets.append((suit, Card.RANKS[rank_index], temp_card.__str__()))
if not playable_targets:
messagebox.showinfo("ジョーカー", "現在、ジョーカーを代用して出せるカードが場にありません。(既に出ているカード、または Heart 7 を除く)")
return
joker_window = tk.Toplevel(self.master)
joker_window.title("ジョーカーの代用カードを選択")
joker_window.transient(self.master)
joker_window.grab_set()
tk.Label(joker_window, text="ジョーカーをどのカードの代わりに出しますか?", font=('Arial', 10, 'bold')).pack(padx=10, pady=10)
button_frame = tk.Frame(joker_window)
button_frame.pack(padx=10, pady=5)
row = 0
col = 0
for suit, rank, card_str in playable_targets:
fg, bg = self.get_card_color(card_str)
btn = tk.Button(button_frame, text=card_str, width=6, height=2,
bg=bg, fg=fg, font=('Arial', 12, 'bold'),
command=lambda s=suit, r=rank, w=joker_window: self.process_joker_choice(joker_card, s, r, w))
btn.grid(row=row, column=col, padx=5, pady=5)
col += 1
if col > 6:
col = 0
row += 1
joker_window.protocol("WM_DELETE_WINDOW", lambda: self.process_joker_choice(joker_card, None, None, joker_window))
self.master.wait_window(joker_window)
def process_joker_choice(self, joker_card, target_suit, target_rank, joker_window):
try:
joker_window.destroy()
except tk.TclError:
pass
if target_suit is None:
self.status_label.config(text="YOU の移動: ジョーカー使用をキャンセルしました", fg='orange')
return
player = self.game.get_current_player()
target_info = (target_suit, target_rank)
# ジョーカーを使って場に出す
success, msg = self.game.play_card(player, joker_card, target_info)
if success:
if "強制提示" in msg:
self.status_label.config(text=f"YOU の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_card_click(self, card):
player = self.game.get_current_player()
if not player.is_human: return
# バグ修正: ジョーカーは出せるカードの有無に関わらず常に実行可能とする
if card.is_joker:
self.show_joker_options(card)
else:
success, msg = self.game.play_card(player, card)
if success:
if "強制提示" in msg:
self.status_label.config(text=f"YOU の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"YOU の移動: {msg}", fg='green')
self.end_turn()
else:
messagebox.showerror("失敗", msg)
def handle_pass(self, event=None):
player = self.game.get_current_player()
if not player.is_human: return
# Game.pass_turn が修正されているため、戦略的パスを許可
success, msg = self.game.pass_turn(player)
if success:
self.status_label.config(text=f"YOU の移動: {msg}", fg='blue')
self.end_turn()
else:
messagebox.showerror("パス失敗", msg)
def end_turn(self):
winner = self.game.check_winner()
if winner:
self.status_label.config(text=f"🎉 勝者: {winner} 🎉", fg='red')
self.update_status_display()
response = messagebox.askyesno("ゲーム終了", f"勝者は {winner} です!\nもう一度プレイしますか?")
if response:
self.reset_and_start_game()
else:
self.pass_button.config(state=tk.DISABLED)
self.master.quit()
return
if self.game.next_turn():
self.update_gui()
if not self.game.get_current_player().is_human:
self.run_turn()
else:
self.update_gui()
def run_turn(self):
current_player = self.game.get_current_player()
if current_player.is_human: return
self.master.after(500, lambda: self.ai_move(current_player))
def ai_move(self, player):
playable_cards = player.get_playable_cards(self.game.field)
target_info = None
card_to_play = None
if not playable_cards:
# パス
success, msg = self.game.pass_turn(player)
else:
joker = next((c for c in playable_cards if c.is_joker), None)
non_jokers = [c for c in playable_cards if not c.is_joker]
if non_jokers:
# 7から遠いカードを優先して出す (手札を減らす戦略)
card_to_play = max(non_jokers, key=lambda c: abs(c.value - 7))
success, msg = self.game.play_card(player, card_to_play)
elif joker:
# ジョーカーしか出せない場合、代用候補を探す
field = self.game.field
# 全てのプレイヤーの手札にあるカードのセットを作成 (COMのジョーカー代用戦略は保守的に制限を残す)
all_hands_cards = set()
for p in self.game.players:
for card in p.hand:
if not card.is_joker:
all_hands_cards.add((card.suit, card.rank))
candidates = []
for suit in Card.SUITS:
for rank_index in range(len(Card.RANKS)):
temp_card = Card(Card.SUITS.index(suit), rank_index)
# 1. 既に出ているか?
if field.table[suit][rank_index] is not None: continue
# 2. Heart 7 の代用禁止
if temp_card == self.game.h7_card: continue
# 3. 誰かの手札に残っているか? (COMはバグ対策のため制限を残す)
if (temp_card.suit, temp_card.rank) in all_hands_cards: continue
# 4. 7または隣接カードが出ているか?
if temp_card.value == 7 or field.is_playable(temp_card):
candidates.append((suit, temp_card.rank))
if candidates:
# 7に近いカードを優先して代用する (場を繋げる戦略)
candidates.sort(key=lambda x: abs(Card.RANK_VALUES[x[1]] - 7))
target_suit, target_rank = candidates[0]
target_info = (target_suit, target_rank)
card_to_play = joker
success, msg = self.game.play_card(player, card_to_play, target_info)
else:
# 代用できるカードがない場合はパス
success, msg = self.game.pass_turn(player)
else:
# ここはロジック的には通らないはずだが、念のためパス
success, msg = self.game.pass_turn(player)
if "強制提示" in msg:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='red')
else:
self.status_label.config(text=f"{player.name} の移動: {msg}", fg='white')
self.master.update()
self.master.after(500, self.end_turn)
# ======================================================================
# C. メイン実行
# ======================================================================
if __name__ == '__main__':
root = tk.Tk()
app = SevensGUI(root)
root.mainloop()
「ポンコツ夫婦のGame Trial Log」シリーズは今後も継続します。AIの力を借りて『AIコーダー』としてどこまでいけるか、R60の挑戦は続きます。どうぞご期待ください。
III. Redkabagonのゲーム進展と次回予告
このAIとの対戦の模様は、筆者クリア動画として一般公開されています。
一方で、Redkabagonは、これまで通りG5 Entertainment提供のアイテム探し&3マッチゲームをクリアする動画を投稿し、ブログにリンクしています。彼女の着実なゲームクリアも、本シリーズの隠れた見どころです。
2. シリーズの継続と次回の挑戦
本シリーズは、今後も継続的に新しいアプリ開発に挑んでいきます。
Redkabagonさんの公道デビューはネタ化していますが、プログラミング初心者の筆者も、AIの力を借りて『AIコーダー』としてデビューできるのか、どうか!?という挑戦は続きます。
次回は別のパズルゲームアプリの作成、またはプロの脳トレアプリの機能との比較検証を予定しています。
どうぞ、Geminiのプロンプトの世界に飛び込んでみてください。
Dr.takodemous(筆者)
「AIコーダー」を目指すR60世代の夫。プログラミング無知ながら、Gemini(AI)と格闘しながらアプリ開発に挑むロジック担当。趣味はAIとの対話と、昔取った杵柄の格闘技。最近は妻と楽しむ近郊への夫婦旅の企画と、インドア・アウトドア活動の経費化を目的とした生活研究に熱中しています。**R60が頑張ってチャレンジしてみた!**というテーマで、新しい発見を共有します。
Redkabagon(ポンコツ夫婦の妻)
夫と共にブログを運営するR60世代の妻。得意分野は、脳内活性(ナンプレ、ゲーム)と、夫婦の活動を支える生活の知恵。G5ゲームや趣味の音楽動画(Remix)の制作にも挑戦中。特に、**特技を活かしたレンタル企画(観葉植物など)の実現に向けて奮闘中です。夫の奇抜なアイデアに振り回されつつ、ポンコツ夫婦が仲良く老いていくための「絆と癒やしのロジック」**を担当しています。
【The Gear】撮影機材のご紹介
本動画の撮影機材や新しい動画編集ソフト「PowerDirector 365」に関する情報、アフィリエイト情報については、「The Gear」カテゴリーで詳しくご紹介しています。




