""" 斗地主游戏 - 图形界面(改进版) """
import tkinter as tk
from tkinter import messagebox, ttk
from typing import List, Optional, Callable
import random
from card import Card, PlayHand, CardPattern, CardRank
from player import Player, AIPlayer
from game import DouDiZhuGame
class CardButton(tk.Canvas):
"""卡牌按钮组件 - 缩小版"""
SUIT_COLORS = {"♠": "black", "♥": "red", "♣": "black", "♦": "red"}
BG_NORMAL = "white"
BG_SELECTED = "#FFE4B5"
BG_HOVER = "#E8E8E8"
CARD_WIDTH = 60
CARD_HEIGHT = 90
def __init__(self, parent, card: Card, index: int, click_callback: Callable, **kwargs):
super().__init__(parent, width=self.CARD_WIDTH, height=self.CARD_HEIGHT, bg=self.BG_NORMAL,
highlightthickness=1, highlightbackground="#333", cursor="hand2", **kwargs)
self.card = card
self.index = index
self.click_callback = click_callback
self.selected = False
self._draw_card()
self.bind("<Button-1>", self._on_click)
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
def _draw_card(self):
self.delete("all")
bg_color = self.BG_SELECTED if self.selected else self.BG_NORMAL
self.create_rectangle(2, 2, self.CARD_WIDTH-2, self.CARD_HEIGHT-2, fill=bg_color, outline="#333", width=2)
is_joker = self.card.rank in (CardRank.BLACK_JOKER, CardRank.RED_JOKER)
if is_joker:
color = "red" if self.card.rank == CardRank.RED_JOKER else "black"
text = "大王" if self.card.rank == CardRank.RED_JOKER else "小王"
self.create_text(self.CARD_WIDTH/2, self.CARD_HEIGHT/2-10, text=text, font=("SimHei", 10, "bold"), fill=color)
self.create_text(self.CARD_WIDTH/2, self.CARD_HEIGHT/2+15, text="🃏", font=("Arial", 18))
else:
color = self.SUIT_COLORS.get(self.card.suit, "black")
rank_text = Card.RANK_NAMES[self.card.rank]
suit_text = self.card.suit
self.create_text(10, 12, text=rank_text, font=("Arial", 11, "bold"), fill=color)
self.create_text(10, 26, text=suit_text, font=("Arial", 10), fill=color)
self.create_text(self.CARD_WIDTH/2, self.CARD_HEIGHT/2+5, text=suit_text, font=("Arial", 28), fill=color)
self.create_text(self.CARD_WIDTH-10, self.CARD_HEIGHT-26, text=rank_text, font=("Arial", 11, "bold"), fill=color)
self.create_text(self.CARD_WIDTH-10, self.CARD_HEIGHT-12, text=suit_text, font=("Arial", 10), fill=color)
def _on_click(self, event):
self.selected = not self.selected
self._draw_card()
self.click_callback(self.index, self.selected)
def _on_enter(self, event):
if not self.selected:
self.config(bg=self.BG_HOVER)
def _on_leave(self, event):
self.config(bg=self.BG_NORMAL)
def set_selected(self, selected: bool):
self.selected = selected
self._draw_card()
def deselect(self):
self.selected = False
self._draw_card()
class PlayerPanel(tk.Frame):
"""玩家面板"""
def __init__(self, parent, player: Player, **kwargs):
super().__init__(parent, **kwargs)
self.player = player
self._setup_ui()
def _setup_ui(self):
self.info_label = tk.Label(self, font=("SimHei", 11), bg="#34495E", fg="white", justify="center", padx=15, pady=10, relief="ridge", bd=2)
self.info_label.pack()
def update_info(self):
role_text = "👑 地主" if self.player.is_landlord else "🧑🌾 农民"
count = len(self.player.cards)
status = "✓ 出完" if count == 0 else f"{count} 张"
self.info_label.config(text=f"{self.player.name}\n{role_text}\n{status}")
class GameTable(tk.Canvas):
"""游戏桌面 - 显示当前出牌"""
def __init__(self, parent, **kwargs):
super().__init__(parent, width=500, height=200, bg="#1E8449", highlightthickness=2, highlightbackground="#145A32", **kwargs)
self.current_cards: List[Card] = []
self.current_player = ""
self._draw_table()
def _draw_table(self):
self.delete("all")
self.create_oval(100, 30, 400, 170, fill="#27AE60", outline="#1E8449", width=2)
self.create_text(250, 15, text="当前出牌", font=("SimHei", 12, "bold"), fill="white")
def show_play(self, cards: List[Card], player_name: str, pattern_name: str):
self.current_cards = cards
self.current_player = player_name
self._draw_table()
if not cards:
self.create_text(250, 100, text="等待出牌...", font=("SimHei", 14), fill="#CCCCCC", tags="cards")
return
pattern_text = f" ({pattern_name})" if pattern_name else ""
self.create_text(250, 50, text=f"{player_name}{pattern_text}", font=("SimHei", 11), fill="white", tags="cards")
card_width = 40
card_height = 56
overlap = 25
total_width = len(cards) * overlap + (card_width - overlap)
start_x = 250 - total_width / 2
y = 115
for i, card in enumerate(cards):
x = start_x + i * overlap
self._draw_mini_card(x, y, card)
def _draw_mini_card(self, x: float, y: float, card: Card):
card_width = 40
card_height = 56
is_joker = card.rank in (CardRank.BLACK_JOKER, CardRank.RED_JOKER)
color = "red" if is_joker or card.suit in ("♥", "♦") else "black"
self.create_rectangle(x+2, y+2, x+card_width+2, y+card_height+2, fill="#000000", stipple="gray50", tags="cards")
self.create_rectangle(x, y, x+card_width, y+card_height, fill="white", outline="#333", tags="cards")
if is_joker:
text = "大王" if card.rank == CardRank.RED_JOKER else "小王"
self.create_text(x+card_width/2, y+card_height/2, text=text, font=("SimHei", 8), fill=color, tags="cards")
else:
rank_text = Card.RANK_NAMES[card.rank]
suit_text = card.suit
self.create_text(x+8, y+12, text=rank_text, font=("Arial", 9, "bold"), fill=color, tags="cards")
self.create_text(x+8, y+24, text=suit_text, font=("Arial", 8), fill=color, tags="cards")
def clear(self):
self.show_play([], "")
class HistoryPanel(tk.Frame):
"""出牌历史记录面板"""
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
tk.Label(self, text="📜 出牌记录", font=("SimHei", 12, "bold"), bg="#2C3E50", fg="white").pack(fill=tk.X, pady=5)
self.text_widget = tk.Text(self, width=25, height=20, font=("SimHei", 10), bg="#ECF0F1", fg="#2C3E50", relief="sunken", bd=2, wrap=tk.WORD)
self.text_widget.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(self, command=self.text_widget.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.text_widget.config(yscrollcommand=scrollbar.set)
self.text_widget.tag_config("landlord", foreground="#E74C3C", font=("SimHei", 10, "bold"))
self.text_widget.tag_config("farmer", foreground="#3498DB", font=("SimHei", 10, "bold"))
self.text_widget.tag_config("pass", foreground="#7F8C8D", font=("SimHei", 10, "italic"))
self.text_widget.tag_config("round", foreground="#27AE60", font=("SimHei", 10, "bold"))
def add_record(self, player_name: str, is_landlord: bool, cards: List[Card], pattern_name: str, is_pass: bool = False):
if is_pass:
tag = "pass"
text = f"{player_name}: 不出\n"
else:
tag = "landlord" if is_landlord else "farmer"
cards_str = ''.join(str(c) for c in cards)
pattern_text = f" [{pattern_name}]" if pattern_name else ""
text = f"{player_name}{pattern_text}:\n{cards_str}\n"
self.text_widget.insert(tk.END, text, tag)
self.text_widget.see(tk.END)
def add_round_separator(self, round_num: int):
self.text_widget.insert(tk.END, f"\n=== 第{round_num}轮 ===\n", "round")
self.text_widget.see(tk.END)
def clear(self):
self.text_widget.delete(1.0, tk.END)
class DouDiZhuGUI:
"""斗地主图形界面主类"""
def __init__(self):
self.root = tk.Tk()
self.root.title("斗地主 - 图形版")
self.root.geometry("1400x900")
self.root.config(bg="#1A252F")
self.root.minsize(1200, 800)
self.game: Optional[DouDiZhuGame] = None
self.selected_indices: set = set()
self.card_buttons: List[CardButton] = []
self.player_panels: dict = {}
self.is_player_turn = False
self.round_count = 1
self._create_widgets()
def _create_widgets(self):
self.main_frame = tk.Frame(self.root, bg="#1A252F")
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self._create_top_bar()
self.center_area = tk.Frame(self.main_frame, bg="#1A252F")
self.center_area.pack(fill=tk.BOTH, expand=True, pady=10)
self.left_frame = tk.Frame(self.center_area, bg="#1A252F", width=120)
self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5)
self.left_frame.pack_propagate(False)
self.middle_frame = tk.Frame(self.center_area, bg="#1A252F")
self.middle_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
self.right_frame = tk.Frame(self.center_area, bg="#2C3E50", width=250)
self.right_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=5)
self.right_frame.pack_propagate(False)
self.history_panel = HistoryPanel(self.right_frame, bg="#2C3E50")
self.history_panel.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.table = GameTable(self.middle_frame)
self.table.pack(pady=10)
self._create_hand_area()
self._create_control_bar()
self.show_start_screen()
def _create_top_bar(self):
self.top_bar = tk.Frame(self.main_frame, bg="#2C3E50", height=50)
self.top_bar.pack(fill=tk.X, pady=(0, 5))
self.status_label = tk.Label(self.top_bar, text="🎴 欢迎来到斗地主!", font=("SimHei", 14, "bold"), bg="#2C3E50", fg="#F39C12")
self.status_label.pack(side=tk.LEFT, padx=20, pady=10)
self.round_label = tk.Label(self.top_bar, font=("SimHei", 12), bg="#2C3E50", fg="white")
self.round_label.pack(side=tk.RIGHT, padx=20, pady=10)
def _create_hand_area(self):
self.hand_frame_outer = tk.Frame(self.middle_frame, bg="#1A252F")
self.hand_frame_outer.pack(fill=tk.X, side=tk.BOTTOM, pady=10)
tk.Label(self.hand_frame_outer, text="你的手牌", font=("SimHei", 11), bg="#1A252F", fg="white").pack(anchor=tk.W, padx=5)
self.hand_canvas = tk.Canvas(self.hand_frame_outer, bg="#2C3E50", height=110, highlightthickness=0)
self.hand_canvas.pack(fill=tk.X, expand=True)
self.hand_scrollbar = ttk.Scrollbar(self.hand_frame_outer, orient=tk.HORIZONTAL, command=self.hand_canvas.xview)
self.hand_scrollbar.pack(fill=tk.X)
self.hand_canvas.configure(xscrollcommand=self.hand_scrollbar.set)
self.cards_inner_frame = tk.Frame(self.hand_canvas, bg="#2C3E50")
self.canvas_window = self.hand_canvas.create_window((0, 0), window=self.cards_inner_frame, anchor=tk.NW)
self.cards_inner_frame.bind("<Configure>", self._on_cards_configure)
def _on_cards_configure(self, event):
self.hand_canvas.configure(scrollregion=self.hand_canvas.bbox("all"))
def _create_control_bar(self):
self.control_bar = tk.Frame(self.main_frame, bg="#2C3E50", height=70)
self.control_bar.pack(fill=tk.X, pady=(5, 0))
def clear_control_bar(self):
for widget in self.control_bar.winfo_children():
widget.destroy()
def show_start_screen(self):
self.clear_control_bar()
btn_frame = tk.Frame(self.control_bar, bg="#2C3E50")
btn_frame.pack(expand=True)
tk.Button(btn_frame, text="🎮 开始游戏", font=("SimHei", 14, "bold"), bg="#27AE60", fg="white", padx=30, pady=8, command=self.start_game).pack(side=tk.LEFT, padx=10)
tk.Button(btn_frame, text="📖 游戏规则", font=("SimHei", 12), bg="#3498DB", fg="white", padx=20, pady=8, command=self.show_rules).pack(side=tk.LEFT, padx=10)
def show_rules(self):
rules_text = "斗地主规则:\n1. 游戏共 3 人,1 个地主,2 个农民\n2. 每人 17 张牌,地主额外获得 3 张底牌\n3. 地主先出牌,按顺时针轮流出牌\n4. 后出的牌必须比前一家大,或选择不出\n5. 先出完牌的一方获胜\n牌型大小:\n• 单张、对子、三张:按点数比较\n• 顺子:5 张及以上连续点数\n• 连对:3 对及以上连续对子\n• 飞机:2 组及以上连续三张\n• 炸弹:4 张相同点数,可压任何非炸弹\n• 王炸:大王 + 小王,最大\n操作说明:\n• 点击卡牌选中/取消选中\n• 点击'出牌'按钮出选中的牌\n• 点击'不出'跳过回合\n• 点击'提示'获取建议"
messagebox.showinfo("游戏规则", rules_text)
def start_game(self):
self.game = DouDiZhuGame()
self.round_count = 1
self.game.players = [
Player("玩家", is_ai=False),
AIPlayer("AI-1"),
AIPlayer("AI-2")
]
self.game.deck = Card.create_deck()
random.shuffle(self.game.deck)
for i in range(51):
self.game.players[i % 3].add_cards([self.game.deck[i]])
self.game.bottom_cards = self.game.deck[51:]
self.history_panel.clear()
self.history_panel.add_round_separator(self.round_count)
self._create_player_panels()
self.show_call_landlord_screen()
def _create_player_panels(self):
for widget in self.left_frame.winfo_children():
widget.destroy()
self.player_panels = {}
panel1 = PlayerPanel(self.left_frame, self.game.players[1], bg="#1A252F")
panel1.pack(fill=tk.X, pady=10)
self.player_panels[1] = panel1
panel2 = PlayerPanel(self.right_frame, self.game.players[2], bg="#2C3E50")
panel2.pack(fill=tk.X, pady=5, before=self.history_panel)
self.player_panels[2] = panel2
def show_call_landlord_screen(self):
self.clear_control_bar()
self.status_label.config(text="叫地主阶段 - 请选择叫分")
self.round_label.config(text="底牌:??? ??? ???")
self.update_hand_display()
self.table.clear()
btn_frame = tk.Frame(self.control_bar, bg="#2C3E50")
btn_frame.pack(expand=True)
tk.Label(btn_frame, text="请叫分:", font=("SimHei", 12), bg="#2C3E50", fg="white").pack(side=tk.LEFT, padx=10)
for score in range(4):
color = "#E74C3C" if score == 3 else "#F39C12" if score == 2 else "#3498DB" if score == 1 else "#95A5A6"
btn = tk.Button(btn_frame, text=f"{score}分" if score > 0 else "不叫", font=("SimHei", 12, "bold"), bg=color, fg="white", width=8, command=lambda s=score: self.call_landlord(s))
btn.pack(side=tk.LEFT, padx=5)
def update_hand_display(self):
for widget in self.cards_inner_frame.winfo_children():
widget.destroy()
self.card_buttons = []
self.selected_indices.clear()
player = self.game.players[0]
for i, card in enumerate(player.cards):
btn = CardButton(self.cards_inner_frame, card, i, self.on_card_click)
btn.pack(side=tk.LEFT, padx=2, pady=5)
self.card_buttons.append(btn)
self.cards_inner_frame.update_idletasks()
self.hand_canvas.configure(scrollregion=self.hand_canvas.bbox("all"))
def on_card_click(self, index: int, selected: bool):
if selected:
self.selected_indices.add(index)
else:
self.selected_indices.discard(index)
def call_landlord(self, score: int):
scores = [score]
current_max = score
ai1_score = self.game.players[1].decide_call_landlord(current_max)
scores.append(ai1_score)
if ai1_score > current_max:
current_max = ai1_score
ai2_score = self.game.players[2].decide_call_landlord(current_max)
scores.append(ai2_score)
if ai2_score > current_max:
current_max = ai2_score
if current_max == 0:
messagebox.showinfo("叫地主", "无人叫地主,重新发牌!")
self.start_game()
return
max_idx = scores.index(max(scores))
landlord = self.game.players[max_idx]
landlord.is_landlord = True
self.game.landlord = landlord
landlord.add_cards(self.game.bottom_cards)
bottom_str = ''.join(str(c) for c in self.game.bottom_cards)
self.round_label.config(text=f"地主:{landlord.name} | 底牌:{bottom_str}")
messagebox.showinfo("叫地主结果", f"{landlord.name} 成为地主!\n底牌:{bottom_str}")
for i, panel in self.player_panels.items():
panel.player = self.game.players[i]
panel.update_info()
if max_idx == 0:
self.update_hand_display()
self.game.current_player_idx = max_idx
self.start_play_phase()
def start_play_phase(self):
self.clear_control_bar()
btn_frame = tk.Frame(self.control_bar, bg="#2C3E50")
btn_frame.pack(expand=True)
self.play_btn = tk.Button(btn_frame, text="✓ 出牌", font=("SimHei", 12, "bold"), bg="#27AE60", fg="white", padx=25, pady=6, command=self.play_cards)
self.play_btn.pack(side=tk.LEFT, padx=8)
self.pass_btn = tk.Button(btn_frame, text="✗ 不出", font=("SimHei", 12), bg="#95A5A6", fg="white", padx=25, pady=6, command=self.pass_turn)
self.pass_btn.pack(side=tk.LEFT, padx=8)
self.hint_btn = tk.Button(btn_frame, text="💡 提示", font=("SimHei", 12), bg="#3498DB", fg="white", padx=25, pady=6, command=self.show_hint)
self.hint_btn.pack(side=tk.LEFT, padx=8)
self.reset_btn = tk.Button(btn_frame, text="↺ 重置选择", font=("SimHei", 11), bg="#E67E22", fg="white", padx=20, pady=6, command=self.deselect_all_cards)
self.reset_btn.pack(side=tk.LEFT, padx=8)
self.game_loop()
def game_loop(self):
if self.game.game_over:
self.show_game_over()
return
current = self.game.get_current_player()
is_first = self.game.last_play is None or self.game.last_player == current
if current.is_ai:
self.is_player_turn = False
self.status_label.config(text=f"⏳ {current.name} 正在思考...")
self.play_btn.config(state=tk.DISABLED)
self.pass_btn.config(state=tk.DISABLED if is_first else tk.NORMAL)
self.hint_btn.config(state=tk.DISABLED)
self.reset_btn.config(state=tk.DISABLED)
self.root.after(1500, self.ai_play)
else:
self.is_player_turn = True
turn_text = "新的一轮,请出牌!" if is_first else "请出牌压过上家!"
self.status_label.config(text=f"🎯 {turn_text}")
self.play_btn.config(state=tk.NORMAL)
self.pass_btn.config(state=tk.DISABLED if is_first else tk.NORMAL)
self.hint_btn.config(state=tk.NORMAL)
self.reset_btn.config(state=tk.NORMAL)
self.deselect_all_cards()
def ai_play(self):
if not self.game or self.game.game_over:
return
current = self.game.get_current_player()
is_first = self.game.last_play is None or self.game.last_player == current
cards_to_play = current.play_turn(self.game.last_play, is_first)
if cards_to_play is None:
self.status_label.config(text=f"{current.name} 选择不出")
self.history_panel.add_record(current.name, current.is_landlord, [], is_pass=True)
self.game._next_player()
else:
play_hand = PlayHand(cards_to_play)
current.remove_cards(cards_to_play)
self.game.last_play = play_hand
self.game.last_player = current
self.table.show_play(cards_to_play, current.name, play_hand.pattern.name)
self.history_panel.add_record(current.name, current.is_landlord, cards_to_play, play_hand.pattern.name)
self.status_label.config(text=f"{current.name} 出了 {play_hand.pattern.name}")
for i, panel in self.player_panels.items():
if panel.player == current:
panel.update_info()
break
if current.is_empty():
self.game.game_over = True
self.game.winner = current
self.show_game_over()
return
self.game._next_player()
if self.game.last_player == self.game.get_current_player():
self.round_count += 1
self.history_panel.add_round_separator(self.round_count)
self.table.clear()
self.root.after(500, self.game_loop)
def play_cards(self):
if not self.is_player_turn:
return
if not self.selected_indices:
messagebox.showwarning("提示", "请先选择要出的牌!")
return
player = self.game.players[0]
selected_cards = [player.cards[i] for i in sorted(self.selected_indices)]
play_hand = PlayHand(selected_cards)
if play_hand.pattern == CardPattern.INVALID:
messagebox.showerror("错误", "无效的牌型!")
return
is_first = self.game.last_play is None or self.game.last_player == player
if not is_first and self.game.last_play:
if not play_hand.can_beat(self.game.last_play):
messagebox.showerror("错误", "无法压过上家的牌!")
return
player.remove_cards(selected_cards)
self.game.last_play = play_hand
self.game.last_player = player
self.table.show_play(selected_cards, player.name, play_hand.pattern.name)
self.history_panel.add_record(player.name, player.is_landlord, selected_cards, play_hand.pattern.name)
self.update_hand_display()
if player.is_empty():
self.game.game_over = True
self.game.winner = player
self.show_game_over()
return
self.game._next_player()
self.game_loop()
def pass_turn(self):
if not self.is_player_turn:
return
is_first = self.game.last_play is None or self.game.last_player == self.game.players[0]
if is_first:
messagebox.showwarning("提示", "第一手必须出牌!")
return
self.history_panel.add_record(self.game.players[0].name, self.game.players[0].is_landlord, [], is_pass=True)
self.deselect_all_cards()
self.game._next_player()
self.game_loop()
def deselect_all_cards(self):
self.selected_indices.clear()
for btn in self.card_buttons:
btn.deselect()
def show_hint(self):
if not self.is_player_turn or not self.game:
return
player = self.game.players[0]
is_first = self.game.last_play is None or self.game.last_player == player
if is_first:
if self.card_buttons:
self.deselect_all_cards()
self.card_buttons[0].set_selected(True)
self.selected_indices.add(0)
messagebox.showinfo("提示", "建议出最小的单张")
else:
messagebox.showinfo("提示", f"上家出了 {self.game.last_play.pattern.name},请选择能压过的牌")
def show_game_over(self):
winner = self.game.winner
if winner.is_landlord:
result_text = f"{winner.name} (地主) 获胜!"
is_player_win = winner == self.game.players[0]
else:
result_text = f"{winner.name} (农民) 获胜!"
is_player_win = winner == self.game.players[0]
self.status_label.config(text=f"游戏结束 - {result_text}")
if is_player_win:
messagebox.showinfo("🎉 游戏结束", f"恭喜你获胜!\n{result_text}")
else:
messagebox.showinfo("😢 游戏结束", f"你输了!\n{result_text}")
self.clear_control_bar()
btn_frame = tk.Frame(self.control_bar, bg="#2C3E50")
btn_frame.pack(expand=True)
tk.Button(btn_frame, text="🔄 重新开始", font=("SimHei", 14, "bold"), bg="#27AE60", fg="white", padx=30, pady=10, command=self.start_game).pack(side=tk.LEFT, padx=10)
tk.Button(btn_frame, text="📊 查看记录", font=("SimHei", 12), bg="#3498DB", fg="white", padx=20, pady=10, command=lambda: self.history_panel.text_widget.see(tk.END)).pack(side=tk.LEFT, padx=10)
def run(self):
self.root.mainloop()
if __name__ == "__main__":
game = DouDiZhuGUI()
game.run()