Python制作斗地主纸牌游戏

Python制作斗地主纸牌游戏

这是一个完整的斗地主纸牌游戏实现,使用 Python 开发,提供命令行和图形界面两种版本。游戏包含完整的规则实现、AI 对手和友好的用户界面。


## 核心模块说明

### 1. card.py - 卡牌系统

**主要类:**

- **CardSuit**: 卡牌花色定义
- **CardRank**: 卡牌点数枚举(3-2,大小王)
- **Card**: 扑克牌类
  - 包含花色、点数属性
  - 支持比较和排序
  - 提供字符串显示

- **CardPattern**: 牌型枚举
  - 单张、对子、三张
  - 三带一、三带二
  - 顺子、连对、飞机
  - 四带二、四带两对
  - 炸弹、王炸

- **PlayHand**: 出牌手类
  - 识别牌型
  - 验证牌型有效性
  - 比较牌型大小

**功能特性:**
- 完整的 54 张扑克牌
- 支持所有标准斗地主牌型
- 智能牌型识别和比较

### 2. player.py - 玩家系统

**主要类:**

- **Player**: 基础玩家类
  - 手牌管理(添加、移除、排序)
  - 牌数查询
  - 角色标识(地主/农民)

- **AIPlayer**: AI 玩家类(继承自 Player)
  - 智能叫地主决策
  - 手牌强度评估
  - 自动出牌策略
  - 压牌判断

**AI 策略:**
- 根据手牌强度决定叫分
- 优先出组合牌型(顺子、连对、飞机)
- 保留大牌用于压牌
- 智能选择压牌方案

### 3. game.py - 游戏逻辑

**主要类:**

- **DouDiZhuGame**: 游戏主控类
  - 游戏初始化
  - 发牌流程
  - 叫地主流程
  - 出牌回合管理
  - 胜负判定

**核心功能:**
- 自动洗牌和发牌
- 叫地主竞价机制
- 出牌规则验证
- 回合轮转控制
- 游戏状态管理

### 4. main.py - 命令行界面

**功能:**
- 清晰的命令行交互
- 游戏规则说明
- 实时状态显示
- 玩家输入处理
- 游戏结果展示

**操作方式:**
- 输入牌索引选择出牌(如:0,2,4)
- 直接回车表示不出
- 支持重新开局

### 5. gui.py - 图形界面(推荐)

**主要组件:**

- **CardButton**: 卡牌按钮组件
  - 可视化卡牌显示
  - 点击选中/取消
  - 鼠标悬停效果
  - 选中状态高亮

- **PlayerPanel**: 玩家信息面板
  - 显示玩家名称
  - 显示角色(地主/农民)
  - 显示剩余牌数

- **GameTable**: 游戏桌面
  - 显示当前出牌
  - 显示玩家名和牌型
  - 重叠显示多张牌

- **HistoryPanel**: 出牌历史记录
  - 记录所有出牌
  - 区分地主/农民颜色
  - 回合分隔线
  - 支持滚动查看

- **DouDiZhuGUI**: 主界面类
  - 完整的游戏流程控制
  - 响应式布局
  - 按钮交互
  - 状态提示

**界面特性:**
- 1400x900 大窗口
- 三栏布局(玩家-游戏区-历史)
- 手牌水平滚动
- 实时状态更新
- 美观的配色方案

## 游戏规则

### 基本规则
1. 游戏共 3 人,1 个地主,2 个农民
2. 每人初始 17 张牌,地主额外获得 3 张底牌
3. 地主先出牌,按顺时针轮流出牌
4. 后出的牌必须比前一家大,或选择不出
5. 先出完牌的一方获胜

### 牌型大小

**基本牌型:**
- 单张、对子、三张:按点数比较
- 三带一、三带二:三张部分比较

**组合牌型:**
- 顺子:5 张及以上连续点数(不能含 2 和王)
- 连对:3 对及以上连续对子(不能含 2 和王)
- 飞机:2 组及以上连续三张
- 飞机带单/带对

**特殊牌型:**
- 炸弹:4 张相同点数,可压任何非炸弹牌
- 王炸:大王 + 小王,最大

### 点数大小
3 < 4 < 5 < 6 < 7 < 8 < 9 < 10 < J < Q < K < A < 2 < 小王 < 大王

## 运行方式

### 图形界面版本(推荐)
```bash
python gui.py
```

### 命令行版本
```bash
python main.py
```

## 技术特点

### 代码质量
- 模块化设计,职责清晰
- 类型注解完善
- 面向对象编程
- 代码结构清晰

### 功能完整性
- 支持所有标准牌型
- AI 对手具备基本策略
- 双界面支持
- 完整的游戏流程

### 用户体验
- 图形界面美观易用
- 命令行界面清晰直观
- 历史记录方便查看
- 实时状态反馈

## 依赖库

- **tkinter**: 图形界面(Python 标准库)
- 无其他第三方依赖

## 代码统计

- **总文件数**: 5 个 Python 文件
- **总类数**: 13 个类
- **总函数/方法数**: 约 60 个
- **代码行数**: 约 2000+ 行

## 扩展建议

1. **AI 增强**: 改进 AI 策略,增加难度等级
2. **网络对战**: 实现多人在线对战
3. **音效动画**: 添加出牌音效和动画
4. **记分系统**: 添加积分和排行榜
5. **牌型提示**: 智能提示可出的牌型组合
6. **回放功能**: 游戏过程回放

## 作者说明

本项目为学习和演示目的开发,展示了完整的游戏开发流程,包括数据结构设计、算法实现、AI 策略和用户界面开发。

---

**最后更新**: 2026-03-01
**版本**: 1.0

card.py

""" 斗地主游戏 - 卡牌模块 """ from enum import IntEnum from typing import List, Tuple, Optional class CardSuit: """卡牌花色""" JOKER = "JOKER" class CardRank(IntEnum): """卡牌点数""" THREE = 3 FOUR = 4 FIVE = 5 SIX = 6 SEVEN = 7 EIGHT = 8 NINE = 9 TEN = 10 JACK = 11 QUEEN = 12 KING = 13 ACE = 14 TWO = 15 BLACK_JOKER = 16 RED_JOKER = 17 class Card: """扑克牌类""" RANK_NAMES = { CardRank.THREE: "3", CardRank.FOUR: "4", CardRank.FIVE: "5", CardRank.SIX: "6", CardRank.SEVEN: "7", CardRank.EIGHT: "8", CardRank.NINE: "9", CardRank.TEN: "10", CardRank.JACK: "J", CardRank.QUEEN: "Q", CardRank.KING: "K", CardRank.ACE: "A", CardRank.TWO: "2", CardRank.BLACK_JOKER: "小王", CardRank.RED_JOKER: "大王", } SUIT_SYMBOLS = { "♠": "黑桃", "♥": "红桃", "♣": "梅花", "♦": "方块", } def __init__(self, rank: CardRank, suit:): self.rank = rank self.suit = suit def __repr__(self): if self.rank in (CardRank.BLACK_JOKER, CardRank.RED_JOKER): return self.RANK_NAMES[self.rank] return f"{self.SUIT_SYMBOLS.get(self.suit, self.suit)}{self.RANK_NAMES[self.rank]}" def __str__(self): return self.__repr__() def __eq__(self, other): if not isinstance(other, Card): return False return self.rank == other.rank and self.suit == other.suit def __hash__(self): return hash((self.rank, self.suit)) def __lt__(self, other): if not isinstance(other, Card): return NotImplemented return self.rank < other.rank def __gt__(self, other): if not isinstance(other, Card): return NotImplemented return self.rank > other.rank @property def value(self) -> int: """返回卡牌的数值,用于比较大小""" return int(self.rank) @classmethod def create_deck(cls) -> List['Card']: """创建一副完整的扑克牌(54张)""" deck = [] suits = ["♠", "♥", "♣", "♦"] # 添加普通牌(3到2) for rank in range(CardRank.THREE, CardRank.TWO + 1): for suit in suits: deck.append(cls(CardRank(rank), suit)) # 添加大小王 deck.append(cls(CardRank.BLACK_JOKER)) deck.append(cls(CardRank.RED_JOKER)) return deck class CardPattern(IntEnum): """出牌类型""" INVALID = 0 # 无效 SINGLE = 1 # 单张 PAIR = 2 # 对子 TRIPLE = 3 # 三张 TRIPLE_WITH_SINGLE = 4 # 三带一 TRIPLE_WITH_PAIR = 5 # 三带二 STRAIGHT = 6 # 顺子 DOUBLE_STRAIGHT = 7 # 连对 TRIPLE_STRAIGHT = 8 # 飞机(三连张) TRIPLE_STRAIGHT_WITH_SINGLES = 9 # 飞机带单 TRIPLE_STRAIGHT_WITH_PAIRS = 10 # 飞机带对 FOUR_WITH_TWO_SINGLES = 11 # 四带二 FOUR_WITH_TWO_PAIRS = 12 # 四带两对 BOMB = 13 # 炸弹 ROCKET = 14 # 王炸 class PlayHand: """出牌手""" def __init__(self, cards: List[Card]): self.cards = sorted(cards, key=lambda c: c.value) self.pattern = self._identify_pattern() self.main_value = self._get_main_value() def _identify_pattern(self) -> CardPattern: """识别出牌类型""" if not self.cards: return CardPattern.INVALID n = len(self.cards) # 王炸 if n == 2 and self._is_rocket(): return CardPattern.ROCKET # 单张 if n == 1: return CardPattern.SINGLE # 对子 if n == 2 and self._is_pair(self.cards): return CardPattern.PAIR # 三张 if n == 3 and self._is_triple(self.cards): return CardPattern.TRIPLE # 三带一 if n == 4 and self._is_triple_with_single(): return CardPattern.TRIPLE_WITH_SINGLE # 炸弹 if n == 4 and self._is_bomb(): return CardPattern.BOMB # 三带二 if n == 5 and self._is_triple_with_pair(): return CardPattern.TRIPLE_WITH_PAIR # 四带二 if n == 6 and self._is_four_with_two_singles(): return CardPattern.FOUR_WITH_TWO_SINGLES # 四带两对 if n == 8 and self._is_four_with_two_pairs(): return CardPattern.FOUR_WITH_TWO_PAIRS # 顺子 if n >= 5 and self._is_straight(): return CardPattern.STRAIGHT # 连对 if n >= 6 and n % 2 == 0 and self._is_double_straight(): return CardPattern.DOUBLE_STRAIGHT # 飞机(三连张) if n >= 6 and n % 3 == 0 and self._is_triple_straight(): return CardPattern.TRIPLE_STRAIGHT # 飞机带单 if n >= 8 and self._is_triple_straight_with_singles(): return CardPattern.TRIPLE_STRAIGHT_WITH_SINGLES # 飞机带对 if n >= 10 and self._is_triple_straight_with_pairs(): return CardPattern.TRIPLE_STRAIGHT_WITH_PAIRS return CardPattern.INVALID def _is_rocket(self) -> bool: """检查是否是王炸""" ranks = sorted([c.rank for c in self.cards]) return ranks == [CardRank.BLACK_JOKER, CardRank.RED_JOKER] def _is_pair(self, cards: List[Card]) -> bool: """检查是否是对子""" return len(cards) == 2 and cards[0].rank == cards[1].rank def _is_triple(self, cards: List[Card]) -> bool: """检查是否是三张""" return len(cards) == 3 and len(set(c.rank for c in cards)) == 1 def _is_bomb(self) -> bool: """检查是否是炸弹""" return len(self.cards) == 4 and len(set(c.rank for c in self.cards)) == 1 def _is_triple_with_single(self) -> bool: """检查是否是三带一""" ranks = [c.rank for c in self.cards] rank_counts = {} for r in ranks: rank_counts[r] = rank_counts.get(r, 0) + 1 return sorted(rank_counts.values()) == [1, 3] def _is_triple_with_pair(self) -> bool: """检查是否是三带二""" ranks = [c.rank for c in self.cards] rank_counts = {} for r in ranks: rank_counts[r] = rank_counts.get(r, 0) + 1 return sorted(rank_counts.values()) == [2, 3] def _is_four_with_two_singles(self) -> bool: """检查是否是四带二""" ranks = [c.rank for c in self.cards] rank_counts = {} for r in ranks: rank_counts[r] = rank_counts.get(r, 0) + 1 return sorted(rank_counts.values()) == [1, 1, 4] def _is_four_with_two_pairs(self) -> bool: """检查是否是四带两对""" ranks = [c.rank for c in self.cards] rank_counts = {} for r in ranks: rank_counts[r] = rank_counts.get(r, 0) + 1 return sorted(rank_counts.values()) == [2, 2, 4] def _is_straight(self) -> bool: """检查是否是顺子""" if len(self.cards) < 5: return False ranks = sorted([c.rank for c in self.cards]) # 顺子不能包含2和王 if any(r >= CardRank.TWO for r in ranks): return False # 检查是否连续 for i in range(len(ranks) - 1): if ranks[i + 1] - ranks[i] != 1: return False return True def _is_double_straight(self) -> bool: """检查是否是连对""" if len(self.cards) < 6 or len(self.cards) % 2 != 0: return False ranks = sorted([c.rank for c in self.cards]) # 连对不能包含2和王 if any(r >= CardRank.TWO for r in ranks): return False # 检查是否成对且连续 for i in range(0, len(ranks), 2): if ranks[i] != ranks[i + 1]: return False unique_ranks = list(dict.fromkeys(ranks)) # 保持顺序去重 for i in range(len(unique_ranks) - 1): if unique_ranks[i + 1] - unique_ranks[i] != 1: return False return True def _is_triple_straight(self) -> bool: """检查是否是飞机(三连张)""" if len(self.cards) < 6 or len(self.cards) % 3 != 0: return False ranks = sorted([c.rank for c in self.cards]) # 飞机不能包含2和王 if any(r >= CardRank.TWO for r in ranks): return False # 检查是否三张且连续 for i in range(0, len(ranks), 3): if ranks[i] != ranks[i + 1] or ranks[i] != ranks[i + 2]: return False unique_ranks = list(dict.fromkeys(ranks)) for i in range(len(unique_ranks) - 1): if unique_ranks[i + 1] - unique_ranks[i] != 1: return False return True def _is_triple_straight_with_singles(self) -> bool: """检查是否是飞机带单""" # 简化处理:检查是否可以分成飞机和单张 rank_counts = {} for c in self.cards: rank_counts[c.rank] = rank_counts.get(c.rank, 0) + 1 # 找出三张的部分 triple_ranks = [r for r, count in rank_counts.items() if count == 3] if len(triple_ranks) < 2: return False # 检查三张是否连续 triple_ranks.sort() if any(r >= CardRank.TWO for r in triple_ranks): return False for i in range(len(triple_ranks) - 1): if triple_ranks[i + 1] - triple_ranks[i] != 1: return False # 检查剩余牌数是否等于飞机长度 single_count = sum(1 for r, count in rank_counts.items() if count == 1) pair_count = sum(1 for r, count in rank_counts.items() if count == 2) return single_count + pair_count * 2 == len(triple_ranks) def _is_triple_straight_with_pairs(self) -> bool: """检查是否是飞机带对""" rank_counts = {} for c in self.cards: rank_counts[c.rank] = rank_counts.get(c.rank, 0) + 1 # 找出三张的部分 triple_ranks = [r for r, count in rank_counts.items() if count == 3] if len(triple_ranks) < 2: return False # 检查三张是否连续 triple_ranks.sort() if any(r >= CardRank.TWO for r in triple_ranks): return False for i in range(len(triple_ranks) - 1): if triple_ranks[i + 1] - triple_ranks[i] != 1: return False # 检查对子数量是否等于飞机长度 pair_count = sum(1 for r, count in rank_counts.items() if count == 2) return pair_count == len(triple_ranks) def _get_main_value(self) -> int: """获取主牌值(用于比较大小)""" if self.pattern == CardPattern.INVALID: return 0 if self.pattern == CardPattern.ROCKET: return 100 if self.pattern == CardPattern.BOMB: return self.cards[0].value rank_counts = {} for c in self.cards: rank_counts[c.rank] = rank_counts.get(c.rank, 0) + 1 # 对于带牌的类型,找出主牌(数量最多的) if self.pattern in (CardPattern.TRIPLE_WITH_SINGLE, CardPattern.TRIPLE_WITH_PAIR, CardPattern.TRIPLE_STRAIGHT, CardPattern.TRIPLE_STRAIGHT_WITH_SINGLES, CardPattern.TRIPLE_STRAIGHT_WITH_PAIRS): # 找出三张的牌 triple_ranks = [r for r, count in rank_counts.items() if count >= 3] return max(triple_ranks) if self.pattern in (CardPattern.FOUR_WITH_TWO_SINGLES, CardPattern.FOUR_WITH_TWO_PAIRS): for r, count in rank_counts.items(): if count == 4: return r # 默认返回最小值 return min(c.value for c in self.cards) def can_beat(self, other: 'PlayHand') -> bool: """判断是否能压过对方的牌""" if self.pattern == CardPattern.INVALID: return False # 王炸最大 if self.pattern == CardPattern.ROCKET: return True # 炸弹可以压非炸弹 if self.pattern == CardPattern.BOMB: if other.pattern != CardPattern.BOMB: return True return self.main_value > other.main_value # 非炸弹不能压炸弹 if other.pattern == CardPattern.BOMB or other.pattern == CardPattern.ROCKET: return False # 类型必须相同 if self.pattern != other.pattern: return False # 张数必须相同 if len(self.cards) != len(other.cards): return False # 比较主牌值 return self.main_value > other.main_value def __repr__(self): return f"{self.cards} ({self.pattern.name})" 

game.py

""" 斗地主游戏 - 游戏主逻辑 """ import random from typing import List, Optional, Tuple from card import Card, PlayHand, CardPattern, CardRank from player import Player, AIPlayer class DouDiZhuGame: """斗地主游戏类""" def __init__(self): self.players: List[Player] = [] self.landlord: Optional[Player] = None self.current_player_idx = 0 self.last_play: Optional[PlayHand] = None self.last_player: Optional[Player] = None self.bottom_cards: List[Card] = [] self.deck: List[Card] = [] self.game_over = False self.winner: Optional[Player] = None def init_game(self, player_name: str = "玩家"): """初始化游戏""" # 创建玩家 self.players = [ Player(player_name, is_ai=False), AIPlayer("AI-1"), AIPlayer("AI-2") ] # 创建牌组并洗牌 self.deck = Card.create_deck() random.shuffle(self.deck) # 发牌 self._deal_cards() # 叫地主 self._call_landlord() # 设置从地主开始 for i, player in enumerate(self.players): if player.is_landlord: self.current_player_idx = i break self.last_play = None self.last_player = None self.game_over = False self.winner = None def _deal_cards(self): """发牌""" # 每人17张 for i in range(51): self.players[i % 3].add_cards([self.deck[i]]) # 剩余3张作为底牌 self.bottom_cards = self.deck[51:] def _call_landlord(self): """叫地主流程""" scores = [] current_score = 0 print("\n=== 叫地主阶段 ===") for i, player in enumerate(self.players): if player.is_ai: score = player.decide_call_landlord(current_score) print(f"{player.name} 叫分: {score}") else: # 人类玩家 while True: try: print(f"\n你的手牌: {player.display_cards()}") print(f"当前最高叫分: {current_score}") score = int(input(f"{player.name}, 请输入叫分 (0-{3}, 必须>{current_score}): ")) if 0 <= score <= 3 and (score > current_score or score == 0): break print("输入无效,请重新输入") except ValueError: print("请输入数字") scores.append((i, score)) if score > current_score: current_score = score # 确定地主 if current_score == 0: # 没人叫地主,重新发牌 print("无人叫地主,重新发牌...") self.deck = Card.create_deck() random.shuffle(self.deck) for player in self.players: player.cards = [] self._deal_cards() self._call_landlord() return # 找出叫分最高的玩家作为地主 max_score = max(scores, key=lambda x: x[1]) landlord_idx = max_score[0] self.landlord = self.players[landlord_idx] self.landlord.is_landlord = True print(f"\n{self.landlord.name} 成为地主!") print(f"底牌: {' '.join(str(c) for c in self.bottom_cards)}") # 地主拿底牌 self.landlord.add_cards(self.bottom_cards) print(f"{self.landlord.name} 获得底牌后的手牌: {len(self.landlord.cards)}张") def play_turn(self, selected_indices: Optional[List[int]] = None) -> Tuple[bool, str]: """ 执行当前玩家的回合 返回: (是否成功, 消息) """ if self.game_over: return False, "游戏已结束" player = self.players[self.current_player_idx] is_first_play = self.last_play is None or self.last_player == player if player.is_ai: return self._ai_play(player, is_first_play) else: return self._human_play(player, selected_indices, is_first_play) def _ai_play(self, player: AIPlayer, is_first_play: bool) -> Tuple[bool, str]: """AI玩家出牌""" cards_to_play = player.play_turn(self.last_play, is_first_play) if cards_to_play is None: # AI选择不出 print(f"{player.name}: 不出") self._next_player() return True, f"{player.name} 不出" # 验证出牌 play_hand = PlayHand(cards_to_play) if play_hand.pattern == CardPattern.INVALID: # AI不应该出无效牌,重新选择 cards_to_play = [player.cards[0]] play_hand = PlayHand(cards_to_play) if not is_first_play and self.last_play and not play_hand.can_beat(self.last_play): # AI不应该压不过,选择不出 print(f"{player.name}: 不出") self._next_player() return True, f"{player.name} 不出" # 执行出牌 player.remove_cards(cards_to_play) self.last_play = play_hand self.last_player = player print(f"{player.name}: {' '.join(str(c) for c in cards_to_play)} ({play_hand.pattern.name})") # 检查是否获胜 if player.is_empty(): self.game_over = True self.winner = player return True, f"{player.name} 获胜!" self._next_player() return True, f"{player.name} 出了 {play_hand.pattern.name}" def _human_play(self, player: Player, selected_indices: Optional[List[int]], is_first_play: bool) -> Tuple[bool, str]: """人类玩家出牌""" if selected_indices is None or len(selected_indices) == 0: # 不出 if is_first_play: return False, "第一手必须出牌" print(f"{player.name}: 不出") self._next_player() return True, f"{player.name} 不出" # 获取选中的牌 try: selected_cards = [player.cards[i] for i in selected_indices] except IndexError: return False, "选择的牌索引无效" # 验证是否拥有这些牌 if not player.has_cards(selected_cards): return False, "你没有这些牌" # 验证出牌类型 play_hand = PlayHand(selected_cards) if play_hand.pattern == CardPattern.INVALID: return False, "无效的牌型" # 验证是否能压过上家 if not is_first_play and self.last_play: if not play_hand.can_beat(self.last_play): return False, "无法压过上家的牌" # 执行出牌 player.remove_cards(selected_cards) self.last_play = play_hand self.last_player = player print(f"{player.name}: {' '.join(str(c) for c in selected_cards)} ({play_hand.pattern.name})") # 检查是否获胜 if player.is_empty(): self.game_over = True self.winner = player return True, f"{player.name} 获胜!" self._next_player() return True, f"{player.name} 出了 {play_hand.pattern.name}" def _next_player(self): """切换到下一个玩家""" self.current_player_idx = (self.current_player_idx + 1) % 3 # 如果回到最后出牌的玩家,清空场上牌 if self.last_player and self.players[self.current_player_idx] == self.last_player: self.last_play = None print("\n=== 新一轮开始 ===") def get_current_player(self) -> Player: """获取当前玩家""" return self.players[self.current_player_idx] def get_game_state(self) -> dict: """获取游戏状态""" return { 'current_player': self.get_current_player(), 'last_play': self.last_play, 'last_player': self.last_player, 'game_over': self.game_over, 'winner': self.winner, 'players': self.players, 'landlord': self.landlord } def check_winner(self) -> Optional[Player]: """检查是否有获胜者""" for player in self.players: if player.is_empty(): return player return None def parse_card_indices(input_str: str) -> List[int]: """解析用户输入的牌索引""" if not input_str.strip(): return [] try: indices = [int(x.strip()) for x in input_str.split(',')] return indices except ValueError: return None if __name__ == "__main__": game = DouDiZhuGame() game.init_game() print("\n=== 游戏开始 ===") while not game.game_over: current = game.get_current_player() print(f"\n{'='*30}") print(f"当前玩家: {current}") print(f"你的手牌: {current.display_cards()}") if game.last_play: print(f"场上牌: {' '.join(str(c) for c in game.last_play.cards)} ({game.last_play.pattern.name})") else: print("场上牌: 无") if current.is_ai: game.play_turn() else: while True: user_input = input("请输入要出的牌索引(逗号分隔,直接回车表示不出): ") indices = parse_card_indices(user_input) if indices is None: print("输入格式错误,请重新输入") continue success, msg = game.play_turn(indices) print(msg) if success: break print(f"\n=== 游戏结束 ===") print(f"获胜者: {game.winner}") if game.winner.is_landlord: print("地主获胜!") else: print("农民获胜!") 

gui.py

""" 斗地主游戏 - 图形界面(改进版) """ 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:): """显示出的牌""" 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:, is_pass: bool = False): """添加记录""" if is_pass: tag = "pass" text = f"{player_name}: 不出\n" else: tag = "landlord" if is_landlord else "farmer".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) # Canvas + 滚动条 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): """显示游戏规则"""" 斗地主规则: 1. 游戏共3人,1个地主,2个农民 2. 每人17张牌,地主额外获得3张底牌 3. 地主先出牌,按顺时针轮流出牌 4. 后出的牌必须比前一家大,或选择不出 5. 先出完牌的一方获胜 牌型大小: • 单张、对子、三张:按点数比较 • 顺子:5张及以上连续点数 • 连对:3对及以上连续对子 • 飞机:2组及以上连续三张 • 炸弹:4张相同点数,可压任何非炸弹 • 王炸:大王+小王,最大 操作说明: • 点击卡牌选中/取消选中 • 点击"出牌"按钮出选中的牌 • 点击"不出"跳过回合 • 点击"提示"获取建议 """ messagebox.showinfo("游戏规则", rules) 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 = {} # 左侧玩家(AI-1) panel1 = PlayerPanel(self.left_frame, self.game.players[1], bg="#1A252F") panel1.pack(fill=tk.X, pady=10) self.player_panels[1] = panel1 # 右侧玩家(AI-2)- 放在历史面板上方 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 # AI-1 叫分 ai1_score = self.game.players[1].decide_call_landlord(current_max) scores.append(ai1_score) if ai1_score > current_max: current_max = ai1_score # AI-2 叫分 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) # 显示结果.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): """AI出牌""" 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() 

player.py

""" 斗地主游戏 - 玩家模块 """ import random from typing import List, Optional, Tuple from card import Card, PlayHand, CardPattern class Player: """玩家类""" def __init__(self, name: str, is_ai: bool = False): self.name = name self.is_ai = is_ai self.cards: List[Card] = [] self.is_landlord = False def add_cards(self, cards: List[Card]): """添加手牌""" self.cards.extend(cards) self.sort_cards() def sort_cards(self): """整理手牌""" self.cards.sort(key=lambda c: (c.value, c.suit)) def remove_cards(self, cards: List[Card]) -> bool: """移除手牌""" for card in cards: if card in self.cards: self.cards.remove(card) else: return False return True def has_cards(self, cards: List[Card]) -> bool: """检查是否拥有指定牌""" temp_cards = self.cards.copy() for card in cards: if card in temp_cards: temp_cards.remove(card) else: return False return True def get_card_count(self) -> int: """获取手牌数量""" return len(self.cards) def is_empty(self) -> bool: """检查手牌是否出完""" return len(self.cards) == 0 def display_cards(self) -> str: """显示手牌""" return " ".join([f"[{i}]{card}" for i, card in enumerate(self.cards)]) def __repr__(self): return f"{self.name}({'地主' if self.is_landlord else '农民'}, {len(self.cards)}张)" class AIPlayer(Player): """AI玩家""" def __init__(self, name: str): super().__init__(name, is_ai=True) def decide_call_landlord(self, score: int) -> int: """决定是否叫地主,返回0-3的分数""" # 简单策略:根据手牌强度决定是否叫地主 strength = self._evaluate_hand_strength() if score == 0: # 第一个叫分 if strength > 0.7: return 3 elif strength > 0.5: return 2 elif strength > 0.3: return 1 return 0 else: # 后面叫分,必须比前面高 if strength > 0.8 and score < 3: return 3 elif strength > 0.6 and score < 2: return 2 elif strength > 0.4 and score < 1: return 1 return 0 def _evaluate_hand_strength(self) -> float: """评估手牌强度""" strength = 0.0 # 统计各种牌型 rank_counts = {} for card in self.cards: rank_counts[card.rank] = rank_counts.get(card.rank, 0) + 1 # 有大王或小王加分 if CardRank.RED_JOKER in rank_counts: strength += 0.2 if CardRank.BLACK_JOKER in rank_counts: strength += 0.15 # 有2加分 if CardRank.TWO in rank_counts: strength += 0.1 * rank_counts[CardRank.TWO] # 有炸弹加分 for count in rank_counts.values(): if count == 4: strength += 0.2 # 有连牌加分 ranks = sorted(rank_counts.keys()) consecutive = 1 for i in range(len(ranks) - 1): if ranks[i + 1] - ranks[i] == 1 and ranks[i] < CardRank.TWO: consecutive += 1 else: if consecutive >= 5: strength += 0.1 consecutive = 1 return min(strength, 1.0) def play_turn(self, last_play: Optional[PlayHand], is_first_play: bool) -> Optional[List[Card]]: """ AI出牌决策 返回要出的牌列表,如果不出则返回None """ if is_first_play: # 第一手出牌,出最小的单张 return [self.cards[0]] if last_play is None: # 上家不要,可以自由出牌 return self._choose_best_play(None) # 需要压过上家的牌 return self._find_beat_play(last_play) def _choose_best_play(self, last_play: Optional[PlayHand]) -> List[Card]: """选择最佳出牌""" # 优先出顺子、连对等组合 plays = self._find_all_valid_plays() if not plays: return [self.cards[0]] # 优先出组合牌型,保留单张 # 按牌型优先级排序 priority_order = [ CardPattern.ROCKET, CardPattern.BOMB, CardPattern.TRIPLE_STRAIGHT, CardPattern.TRIPLE_STRAIGHT_WITH_PAIRS, CardPattern.TRIPLE_STRAIGHT_WITH_SINGLES, CardPattern.DOUBLE_STRAIGHT, CardPattern.STRAIGHT, CardPattern.TRIPLE_WITH_PAIR, CardPattern.TRIPLE_WITH_SINGLE, CardPattern.TRIPLE, CardPattern.PAIR, CardPattern.SINGLE, ] # 找出非炸弹、非王炸的组合 normal_plays = [p for p in plays if p.pattern not in (CardPattern.ROCKET, CardPattern.BOMB)] if normal_plays: # 优先出张数多的组合(快速走牌) normal_plays.sort(key=lambda p: (-len(p.cards), priority_order.index(p.pattern) if p.pattern in priority_order else 99)) return normal_plays[0].cards # 没有组合,出最小的单张 return [self.cards[0]] def _find_beat_play(self, last_play: PlayHand) -> Optional[List[Card]]: """找出能压过上家的出牌""" plays = self._find_all_valid_plays() # 按牌型分组 same_pattern_plays = [p for p in plays if p.pattern == last_play.pattern and len(p.cards) == len(last_play.cards)] bombs = [p for p in plays if p.pattern == CardPattern.BOMB] rockets = [p for p in plays if p.pattern == CardPattern.ROCKET] # 先找同类型的牌 for play in same_pattern_plays: if play.can_beat(last_play): return play.cards # 如果上家不是炸弹,可以用炸弹压 if last_play.pattern != CardPattern.BOMB and last_play.pattern != CardPattern.ROCKET: # 优先用小炸弹 if bombs: bombs.sort(key=lambda p: p.main_value) return bombs[0].cards # 用王炸 if rockets: return rockets[0].cards # 压不过,不出 return None def _find_all_valid_plays(self) -> List[PlayHand]: """找出所有可能的出牌组合""" plays = [] # 单张 for card in self.cards: plays.append(PlayHand([card])) # 对子 rank_cards = {} for card in self.cards: rank_cards.setdefault(card.rank, []).append(card) for cards in rank_cards.values(): if len(cards) >= 2: plays.append(PlayHand(cards[:2])) # 三张 for cards in rank_cards.values(): if len(cards) >= 3: plays.append(PlayHand(cards[:3])) # 炸弹 for cards in rank_cards.values(): if len(cards) == 4: plays.append(PlayHand(cards)) # 王炸 jokers = [c for c in self.cards if c.rank in (CardRank.BLACK_JOKER, CardRank.RED_JOKER)] if len(jokers) == 2: plays.append(PlayHand(jokers)) # 三带一、三带二 for rank, cards in rank_cards.items(): if len(cards) >= 3: # 三带一 for other_card in self.cards: if other_card.rank != rank: plays.append(PlayHand(cards[:3] + [other_card])) # 三带二 for other_rank, other_cards in rank_cards.items(): if other_rank != rank and len(other_cards) >= 2: plays.append(PlayHand(cards[:3] + other_cards[:2])) # 顺子(5张及以上) unique_ranks = sorted(set(c.rank for c in self.cards if c.rank < CardRank.TWO)) for length in range(5, len(unique_ranks) + 1): for start in range(len(unique_ranks) - length + 1): straight_ranks = unique_ranks[start:start + length] if all(straight_ranks[i+1] - straight_ranks[i] == 1 for i in range(length - 1)): # 找到顺子,每种点数选一张 straight_cards = [] for r in straight_ranks: for c in self.cards: if c.rank == r: straight_cards.append(c) break if len(straight_cards) == length: plays.append(PlayHand(straight_cards)) # 连对 pair_ranks = [r for r, cards in rank_cards.items() if len(cards) >= 2 and r < CardRank.TWO] pair_ranks.sort() for length in range(3, len(pair_ranks) + 1): for start in range(len(pair_ranks) - length + 1): straight_pairs_ranks = pair_ranks[start:start + length] if all(straight_pairs_ranks[i+1] - straight_pairs_ranks[i] == 1 for i in range(length - 1)): straight_pairs_cards = [] for r in straight_pairs_ranks: for c in self.cards: if c.rank == r: straight_pairs_cards.append(c) if len([x for x in straight_pairs_cards if x.rank == r]) == 2: break if len(straight_pairs_cards) == length * 2: plays.append(PlayHand(straight_pairs_cards)) # 过滤掉无效的牌型 valid_plays = [p for p in plays if p.pattern != CardPattern.INVALID] return valid_plays from card import CardRank 

main.py

""" 斗地主游戏 - 主程序入口 """ import os import sys from game import DouDiZhuGame, parse_card_indices def clear_screen(): """清屏""" os.system('cls' if os.name == 'nt' else 'clear') def print_header(): """打印游戏标题""" print("=" * 50) print(" 🎴 斗地主游戏 🎴") print("=" * 50) def print_rules(): """打印游戏规则""" print(""" 游戏规则: 1. 游戏共3人,1个地主,2个农民 2. 每人初始17张牌,地主额外获得3张底牌 3. 地主先出牌,然后按顺时针轮流出牌 4. 后出的牌必须比前一家大,或选择不出 5. 先出完牌的一方获胜 牌型大小: - 单张、对子、三张:按点数比较 - 顺子:5张及以上连续点数 - 连对:3对及以上连续对子 - 飞机:2组及以上连续三张 - 炸弹:4张相同点数,可压任何非炸弹牌 - 王炸:大王+小王,最大 出牌方式: - 输入牌索引(如:0,2,4)选择要出的牌 - 直接回车表示不出 """) def print_game_status(game: DouDiZhuGame): """打印游戏状态""" print("\n" + "-" * 50) # 显示所有玩家信息 print("玩家状态:") for player in game.players: role = "👑地主" if player.is_landlord else "🧑‍🌾农民" status = "✅" if player.is_empty() else f"{player.get_card_count()}张" current = "👉" if player == game.get_current_player() else " " print(f" {current} {player.name} {role}: {status}") print("-" * 50) # 显示场上牌 if game.last_play and game.last_player: print(f"场上: {' '.join(str(c) for c in game.last_play.cards)} ({game.last_play.pattern.name})") print(f"出牌者: {game.last_player.name}") else: print("场上: 无(新一轮)") print("-" * 50) def get_player_action(player, game: DouDiZhuGame) -> list: """获取玩家操作""" print(f"\n>>> 轮到你了,{player.name}!") print(f"你的手牌(共{len(player.cards)}张):") print(player.display_cards()) # 检查是否必须出牌 is_first = game.last_play is None or game.last_player == player while True: if is_first: user_input = input("\n请输入要出的牌索引(逗号分隔): ").strip() else: user_input = input("\n请输入要出的牌索引(逗号分隔,回车=不出): ").strip() if not user_input: if is_first: print("第一手必须出牌!") continue return [] # 不出 indices = parse_card_indices(user_input) if indices is None: print("输入格式错误!请使用逗号分隔数字(如:0,1,2)") continue # 验证索引范围 if any(i < 0 or i >= len(player.cards) for i in indices): print(f"索引超出范围!请输入 0 到 {len(player.cards)-1} 之间的数字") continue return indices def play_game(): """主游戏循环""" clear_screen() print_header() print_rules() # 获取玩家名字 player_name = input("\n请输入你的名字: ").strip() if not player_name: player_name = "玩家" # 初始化游戏 game = DouDiZhuGame() game.init_game(player_name) input("\n按回车开始游戏...") # 游戏主循环 while not game.game_over: clear_screen() print_header() print_game_status(game) current_player = game.get_current_player() if current_player.is_ai: # AI回合 print(f"\n{current_player.name} 正在思考...") import time time.sleep(1) success, msg = game.play_turn() print(msg) input("\n按回车继续...") else: # 玩家回合 indices = get_player_action(current_player, game) success, msg = game.play_turn(indices) if not success: print(f"\n❌ 出牌失败: {msg}") input("按回车重新选择...") else: print(f"\n✅ {msg}") if not game.game_over: input("按回车继续...") # 游戏结束 clear_screen() print_header() print("\n" + "=" * 50) print(" 🎉 游戏结束 🎉") print("=" * 50) winner = game.winner if winner.is_landlord: print(f"\n🏆 获胜者: {winner.name} (地主)") print("地主获胜!") else: print(f"\n🏆 获胜者: {winner.name} (农民)") print("农民获胜!") print("\n最终手牌:") for player in game.players: cards_str = "无" if player.is_empty() else " ".join(str(c) for c in player.cards) print(f" {player.name}: {cards_str}") print("\n" + "=" * 50) def main(): """程序入口""" while True: play_game() print("\n") again = input("是否再来一局? (y/n): ").strip().lower() if again != 'y': print("\n感谢游玩,再见!") break if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\n游戏已退出") sys.exit(0) 

Read more

Git BASH安装教程

什么是 Git Bash? 简单来说,Git Bash 是为 Windows 系统提供的模拟 Linux 风格的 Bash 命令行环境,主要用于运行 Git 命令。Bash 是 Linux 和 macOS 用户常用的命令行工具,而 Windows 自带的命令提示符与它不兼容。因此,Git for Windows 软件包中包含了 Git Bash,让你可以在 Windows 上使用熟悉的 Bash 语法来操作 Git 和进行文件管理 第一步:下载 Git for Windows Git Bash 是 Git for Windows

By Ne0inhk
{宇}柯南同款, 全自研高速电动滑板开源! STM32项目|宇的DIY#1

{宇}柯南同款, 全自研高速电动滑板开源! STM32项目|宇的DIY#1

开源文档原文:https://shouchenyu.feishu.cn/wiki/PxnSwaRRRixkaokr8oBcpYotnyb B站视频项目介绍链接:https://www.bilibili.com/video/BV1cr2sBsEsF/ 立创开源硬件平台:https://oshwhub.com/shouchenyu/conan-with-the-full-self-develop 现已开始更新电动滑板各个模块详细教学,移步守辰宇B站主页即可查看最新视频! 立创开源硬件平台官方公众号推文:https://mp.weixin.qq.com/s/RsQGZqsOtaHpAqjfm9CHLQ * 建议复刻此项目前,先学习基础的嵌入式入门知识: * 编译下载代码:STM32课程https://www.bilibili.com/video/BV11C411h79W/ * PCB电路板导出与下单:嘉立创PCB课程https://www.bilibili.com/video/BV1bj4bzEEQQ/ * PCB元件焊接:插件焊接https://www.b

By Ne0inhk

璀璨星河使用技巧:如何优化AI绘画提示词

璀璨星河使用技巧:如何优化AI绘画提示词 "我梦见了画,然后画下了梦。" —— 文森特 · 梵高 1. 引言:为什么提示词如此重要? 在AI绘画的世界里,提示词就是你的画笔和颜料。璀璨星河(Starry Night)作为一款高端AI艺术生成工具,虽然拥有强大的Kook Zimage Turbo幻想引擎,但最终作品的惊艳程度很大程度上取决于你如何用文字描述心中的画面。 很多用户在使用璀璨星河时都有一个共同的困惑:为什么同样的模型,别人能生成惊艳的艺术作品,而我的结果却平平无奇?答案往往就藏在提示词的优化技巧中。本文将带你深入了解如何通过优化提示词,让璀璨星河真正成为你手中的魔法画笔。 2. 理解璀璨星河的提示词处理机制 2.1 自动翻译功能的妙用 璀璨星河内置了Deep Translator模块,这是一个非常重要的特性。当你输入中文描述时,系统会自动将其转换为专业级的艺术英文提示词。这个功能极大降低了创作门槛,但同时也需要你了解其工作原理: * 中文到英文的精准转换:系统会将你的中文描述转化为AI模型更容易理解的英文艺术术语 * 艺术术语优化:自动添加合适的风格描

By Ne0inhk

FPGA中XDMA多通道传输架构:全面讲解

FPGA中XDMA多通道传输架构:实战解析与工程优化 从一个真实问题说起:为什么我的FPGA数据传不快? 你有没有遇到过这样的场景: FPGA采集了一路4K视频流,每秒要往主机内存送超过1.5GB的数据;同时还要接收来自CPU的控制指令,比如调整曝光、切换模式。结果发现—— 视频帧延迟越来越高,控制命令还经常丢包 。 查PCIe带宽?没问题,Gen3 x8理论有7.8 GB/s,远超需求。 看CPU负载?也不高,不到20%。 那问题出在哪? 答案往往是: 数据通路设计不合理,没有用好XDMA的多通道能力 。 很多工程师把所有数据都塞进一个H2C或C2H通道里,导致高优先级的控制流被大块数据“堵”在后面。这就像让救护车和货车挤同一条车道,再宽的马路也会瘫痪。 本文将带你深入Xilinx XDMA(Xilinx Direct Memory Access)IP核的多通道机制,不仅讲清楚“它是怎么工作的”,更聚焦于 如何在实际项目中高效使用它 ——从寄存器配置到软件编程,从性能调优到常见坑点,全部基于一线开发经验展开。 XDMA是什么?

By Ne0inhk