尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python实现斗牛游戏核心算法:从规则解析到代码模拟

Python实现斗牛游戏核心算法:从规则解析到代码模拟 1. 项目概述与核心思路最近在整理一些Python的趣味小项目发现用代码模拟经典纸牌游戏是个特别好的练手方式。它不仅能巩固基础语法还能让你接触到随机、逻辑判断、数据结构这些核心概念。这次我选的是“斗牛”也就是大家常说的“牛牛”。这个游戏规则清晰但计算胜负的逻辑又有点小复杂非常适合用来练习Python中的列表操作、函数封装和面向对象设计。我的目标不是做一个带图形界面的完整游戏而是先实现一个在命令行里运行的核心逻辑模拟器把发牌、算牛、比大小的流程跑通。这样我们就能清晰地看到每一局牌的运行过程理解算法是如何工作的。这个模拟器的核心价值在于它剥离了花哨的界面直击游戏规则的算法本质。对于初学者你可以通过它理解如何用代码描述现实规则对于有经验的开发者可以思考如何优化算法效率或者为后续的图形界面、网络对战版本打下坚实的数据逻辑基础。整个过程我们会从一副牌的创建开始一步步走到最终胜负的判定我会把其中容易踩坑的细节和我的调试心得都分享出来。2. 游戏规则与算法设计解析2.1 “牛牛”规则精讲在动手写代码之前我们必须把规则吃透任何一点模糊都可能让后面的逻辑判断出错。一副扑克牌去掉大小王共52张。游戏通常支持2-6人每人发5张牌。目标是用手中的5张牌组合出尽可能大的“牛数”。核心计算流程任选3张牌从5张牌中选出3张牌计算它们的点数之和。判断是否为10的倍数如果这3张牌的点数之和是10的倍数10 20 30那么剩下的2张牌的点数之和的个位数就是你的“牛数”。特殊牌型牛牛如果步骤1中找到的3张牌之和为10的倍数并且剩下的2张牌之和也为10的倍数即个位数为0那就是最大的“牛牛”。五花牛如果5张牌全是J、Q、K在算点时记为10点直接视为“五花牛”通常大于“牛牛”。五小牛如果5张牌的点数均小于5即A,2,3,4且点数之和小于等于10则为“五小牛”通常大于“五花牛”。炸弹牛如果有4张牌的点数相同则为“炸弹牛”其大小介于“牛牛”和普通有牛牌型之间。无牛如果在5张牌中找不到任意3张牌的点数之和为10的倍数则为“无牛”。点数计算A 计为1点。2-10 按其牌面数字计点。J, Q, K 均计为10点。比大小规则从大到小 五小牛 五花牛 炸弹牛 牛牛 牛九 牛八 ... 牛一 无牛。 当牛数相同时需要比较牌中最大单张牌的点数考虑花色但通常先比点数再比花色我们模拟时可以先忽略花色或自定义花色顺序。注意不同地区的规则可能有细微差别例如对“五花牛”、“炸弹牛”的认定和大小排序。我们这里采用一套较为通用的规则进行实现你完全可以根据自己了解的规则调整后续的判定函数。2.2 核心算法设计思路规则清楚了怎么用代码来实现呢关键在于如何高效地从5张牌中找出那3张和为10倍数的牌。这是一个典型的组合问题。5张牌中选3张一共有C(5,3)10种组合。对于计算机来说直接遍历这10种组合是最高效、最不容易出错的方法远比我们去想什么巧妙的数学方法要可靠。因此我们的核心算法find_cattle函数可以这样设计使用itertools.combinations生成5张牌中所有3张牌的组合。遍历每个组合计算3张牌的点数之和。如果和是10的倍数记录这个组合并计算剩余2张牌的点数之和的个位数即牛数。遍历完所有组合后如果找到了符合条件的组合就返回牛数0-90代表牛牛以及用于构成牛的那三张牌的索引或牌面方便后续展示或调试。如果没找到返回一个特殊值如 -1代表无牛。这个“暴力遍历”的方法虽然看起来不“聪明”但对于仅5张牌的场景其时间复杂度是常数级的代码简单易懂是绝对的最佳实践。在编程中清晰的逻辑往往比看似高级的技巧更重要。3. 代码实现与模块拆解接下来我们把整个项目拆分成几个模块逐个实现。我会先给出代码然后解释关键点。3.1 牌与牌组的类设计首先我们需要用面向对象的思想来定义“一张牌”和“一副牌”。import random import itertools from collections import Counter class Card: 一张扑克牌 # 花色和点数的映射关系 SUITS [♠, ♥, ♦, ♣] # 黑桃、红心、方块、梅花 RANKS [A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K] # 点数映射A为1JQK为10 RANK_VALUES {A: 1, J: 10, Q: 10, K: 10} for i in range(2, 11): RANK_VALUES[str(i)] i def __init__(self, suit, rank): if suit not in self.SUITS or rank not in self.RANKS: raise ValueError(fInvalid card: suit{suit}, rank{rank}) self.suit suit self.rank rank self.value self.RANK_VALUES[rank] # 牌的点数值 def __repr__(self): # 打印时显示为例如♠A return f{self.suit}{self.rank} def __str__(self): return self.__repr__() class Deck: 一副牌不含大小王 def __init__(self): self.cards [Card(suit, rank) for suit in Card.SUITS for rank in Card.RANKS] self.shuffle() def shuffle(self): 洗牌 random.shuffle(self.cards) def deal(self, num_cards): 发指定张数的牌返回牌列表并从牌堆移除 if num_cards len(self.cards): raise ValueError(Not enough cards in the deck!) dealt_cards self.cards[:num_cards] self.cards self.cards[num_cards:] return dealt_cards关键点解析Card类将花色和点数封装为属性并预先计算好value点数。这样在后续算牛时直接取card.value即可避免反复查字典提升效率。RANK_VALUES字典的构建方式先处理特殊牌AJQK再用循环处理数字牌2-10代码更简洁。Deck类的deal方法模拟发牌直接使用列表切片发牌后更新牌堆。这是模拟抽牌最直观的方式。3.2 核心算牛算法实现这是整个项目的灵魂我们按照之前的设计思路来实现。def calculate_cattle(cards): 计算一手牌5张的牛数。 返回一个字典包含 - type: 牌型如五小牛无牛等 - cattle_num: 牛数0-9-1表示无牛 - max_card: 用于比大小的最大单张牌Card对象 - cattle_cards: 构成牛的3张牌列表仅当有牛时有效 # 0. 预处理获取5张牌的点数列表和牌列表 values [card.value for card in cards] cards_sorted sorted(cards, keylambda x: x.value, reverseTrue) # 按点数从大到小排序方便找最大牌 # 1. 检查特殊牌型优先级最高 # 五小牛所有牌点数5且总和10 if all(v 5 for v in values) and sum(values) 10: return {type: 五小牛, cattle_num: 10, max_card: cards_sorted[0], cattle_cards: None} # 五花牛所有牌都是J,Q,K (value10) if all(v 10 for v in values): return {type: 五花牛, cattle_num: 9, max_card: cards_sorted[0], cattle_cards: None} # 炸弹牛有4张牌点数相同 value_counter Counter(values) most_common value_counter.most_common(1)[0] # 例如 (4, 4) 表示点数为4的牌出现了4次 if most_common[1] 4: # 找到构成炸弹的4张牌中点数最大的那张作为最大牌 bomb_value most_common[0] bomb_cards [card for card in cards if card.value bomb_value] max_bomb_card max(bomb_cards, keylambda x: Card.SUITS.index(x.suit)) # 按花色顺序比大小 return {type: 炸弹牛, cattle_num: 8, max_card: max_bomb_card, cattle_cards: None} # 2. 计算普通牛型 # 遍历所有3张牌的组合 found False cattle_num -1 best_cattle_cards None # 记录构成牛的3张牌 for three_cards in itertools.combinations(cards, 3): if sum(card.value for card in three_cards) % 10 0: found True # 找到构成牛的3张牌 best_cattle_cards list(three_cards) # 计算剩余2张牌的牛数 remaining_cards [card for card in cards if card not in three_cards] remainder sum(card.value for card in remaining_cards) % 10 cattle_num 0 if remainder 0 else remainder # 一旦找到一种组合就可以跳出循环因为任意一种都行不影响牛数计算。 # 但注意可能存在多种组合都能构成牛但牛数相同。我们取找到的第一种即可。 break if found: type_name 牛牛 if cattle_num 0 else f牛{cattle_num} return {type: type_name, cattle_num: cattle_num, max_card: cards_sorted[0], cattle_cards: best_cattle_cards} else: # 无牛 return {type: 无牛, cattle_num: -1, max_card: cards_sorted[0], cattle_cards: None}算法细节与避坑指南特殊牌型优先判断必须按照“五小牛 五花牛 炸弹牛 普通牛”的顺序判断。因为一副牌可能同时满足多个条件比如全是10点的牌也满足“任意三张和为10的倍数”我们必须先捕获最高级的牌型。itertools.combinations的使用它返回的是元组且元素是原始列表中的对象引用。所以if card not in three_cards的判断是有效的。找到即停止在遍历3张牌组合时一旦找到一组和为10倍数的组合就可以break跳出循环。因为规则只关心“是否存在”这样的组合以及由此算出的牛数。多找几种组合不会改变结果反而浪费计算资源。max_card的处理在比大小时我们预先将5张牌按点数降序排序cards_sorted[0]就是点数最大的牌。对于炸弹牛我们需要在4张相同点数的牌里按花色再比一次找出“最大”的那张作为该手牌的代表。这里我简单用花色列表的索引来比较♠♥♦♣。3.3 多局游戏模拟与胜负判定单局游戏的逻辑通了我们就可以模拟多局并让多个“玩家”进行对战统计胜负。class Player: 模拟一个玩家 def __init__(self, name): self.name name self.hand [] # 当前手牌 self.result {} # 当前牌型结果 def receive_cards(self, cards): 接收发的牌 self.hand cards self.result calculate_cattle(self.hand) def show_hand(self): 展示手牌和结果 hand_str .join(str(card) for card in self.hand) return f{self.name}: 手牌[{hand_str}] - {self.result[type]} (最大牌:{self.result[max_card]}) def compare_players(players): 比较多个玩家的牌返回获胜的玩家列表可能有多人并列。 比较规则按牌型优先级和牛数比较。 # 排序规则先按牛数cattle_num降序再按最大牌的点数值降序最后按最大牌的花色索引降序 def player_sort_key(player): res player.result # 将特殊牌型的cattle_num映射为更高的数字以便排序 # 无牛为-1牛1-9为1-9牛牛为0但我们希望牛牛牛九...牛一无牛 # 因此我们创建一个排序用的分数 score_map {五小牛: 12, 五花牛: 11, 炸弹牛: 10} if res[type] in score_map: type_score score_map[res[type]] else: # 普通牛型或无牛 type_score res[cattle_num] if res[cattle_num] ! -1 else -1 # 将牛牛0映射到比牛九9大的数例如9.5这里需要调整逻辑。 # 更清晰的比较方式直接在两两比较时使用复杂的规则。 # 更健壮的方式实现一个两两比较的函数用于sorted的key return player # 由于比较规则复杂使用sorted的key函数可能不够灵活我们实现一个自定义比较函数 sorted_players sorted(players, keylambda p: ( -p.result[cattle_num] if p.result[cattle_num] ! -1 else -10, # 无牛放最后 -p.result[max_card].value, -Card.SUITS.index(p.result[max_card].suit) )) # 但上述排序对特殊牌型处理不对。我们需要一个更通用的方法。 # 方法先按牌型优先级排序再按同牌型内的规则排序 type_priority {五小牛: 5, 五花牛: 4, 炸弹牛: 3, 牛牛: 2, 无牛: 0} # 对于普通牛型优先级为1但牛数越高越好 def get_priority(player_result): t player_result[type] if t in type_priority: return type_priority[t] elif t.startswith(牛): # 牛一至牛九 return 1 else: return 0 sorted_players sorted(players, keylambda p: ( -get_priority(p.result), # 牌型优先级降序 -p.result[cattle_num] if p.result[cattle_num] ! -1 else -10, # 牛数降序 -p.result[max_card].value, # 最大牌点数降序 -Card.SUITS.index(p.result[max_card].suit) # 花色降序 )) # 找出赢家可能并列 winner sorted_players[0] winners [winner] for player in sorted_players[1:]: # 比较winner和player是否完全相等 if (get_priority(winner.result) get_priority(player.result) and winner.result[cattle_num] player.result[cattle_num] and winner.result[max_card].value player.result[max_card].value and Card.SUITS.index(winner.result[max_card].suit) Card.SUITS.index(player.result[max_card].suit)): winners.append(player) else: break return winners def simulate_one_round(player_names): 模拟一局游戏 deck Deck() players [Player(name) for name in player_names] # 发牌 for player in players: cards deck.deal(5) player.receive_cards(cards) # 展示手牌 print(\n 第1轮发牌结果 ) for player in players: print(player.show_hand()) # 比大小决出赢家 winners compare_players(players) if len(winners) 1: print(f\n 本轮赢家是{winners[0].name} ) else: winner_names , .join([w.name for w in winners]) print(f\n 本轮赢家并列{winner_names} ) return winners实现要点与心得Player类是一个简单的数据容器保存玩家名、手牌和计算结果。receive_cards方法接收牌并立即调用calculate_cattle计算结果这样玩家的状态就完整了。胜负判定的复杂性这是整个模拟中最容易出错的部分。我最初想用一个简单的key函数对玩家排序但发现牌型优先级五小牛五花牛...无牛和同牌型内的比较比牛数、再比最大牌交织在一起逻辑很难用一个元组表达清楚。最终的比较策略我定义了一个get_priority函数将牌型映射为数字优先级。然后在排序key中依次比较牌型优先级降序、牛数降序、最大牌点数降序、最大牌花色降序。这样就能得到一个正确的排序。处理并列第一排序后我们需要检查排在后面的玩家是否和第一名完全相等所有比较条件都相同。如果是则加入赢家列表。这里比较花色索引是必要的因为点数最大的牌可能点数相同需要靠花色决出胜负。3.4 主程序与模拟运行最后我们写一个主函数来串联一切并可以方便地运行多局模拟。def main(): 主函数运行多局模拟 print(欢迎来到Python斗牛(牛牛)模拟器) player_names [玩家1, 玩家2, 玩家3, 玩家4] # 可以自定义玩家数量和名字 total_rounds 3 # 模拟的局数 win_count {name: 0 for name in player_names} for round_num in range(1, total_rounds 1): print(f\n{*30}) print(f开始第 {round_num} 局) print(f{*30}) winners simulate_one_round(player_names) for winner in winners: win_count[winner.name] 1 # 统计最终胜率 print(f\n{*40}) print(模拟结束最终胜场统计) for name, count in win_count.items(): print(f{name}: 获胜 {count} 局) if __name__ __main__: main()运行这个main()函数你会在控制台看到类似下面的输出欢迎来到Python斗牛(牛牛)模拟器 开始第 1 局 第1轮发牌结果 玩家1: 手牌[♠6 ♥9 ♦Q ♣3 ♠10] - 牛8 (最大牌:♦Q) 玩家2: 手牌[♦J ♣7 ♥2 ♠A ♣K] - 无牛 (最大牌:♦J) 玩家3: 手牌[♥Q ♠5 ♦8 ♣4 ♠2] - 牛9 (最大牌:♥Q) 玩家4: 手牌[♣J ♦5 ♥7 ♠3 ♣9] - 无牛 (最大牌:♣J) 本轮赢家是玩家3 通过多局模拟你就能看到不同牌型的出现以及胜负是如何判定的。4. 常见问题与扩展思考在实现和测试这个模拟器的过程中我遇到了一些典型问题也思考了可以进一步优化的方向。4.1 调试与验证技巧当你觉得计算结果不对时可以按以下步骤排查单元测试核心函数单独测试calculate_cattle函数。准备几副已知结果的牌比如[♠A, ♥2, ♦3, ♣4, ♠5](五小牛)或者[♠J, ♥Q, ♦K, ♣10, ♠10](五花牛)看输出是否符合预期。test_cards [Card(♠, A), Card(♥, 2), Card(♦, 3), Card(♣, 4), Card(♠, 5)] result calculate_cattle(test_cards) print(result) # 应该输出 {type: 五小牛, ...}打印中间结果在calculate_cattle函数的关键步骤如遍历组合时打印出当前组合和其点数之和确认遍历逻辑是否正确。for three_cards in itertools.combinations(cards, 3): sum_val sum(card.value for card in three_cards) print(f尝试组合: {three_cards}, 和: {sum_val}) if sum_val % 10 0: ...验证比较逻辑手动创建两个玩家的手牌调用compare_players看返回的赢家是否符合你的直觉。特别注意边界情况如两个都是“牛牛”时如何比最大牌。4.2 性能与优化思考虽然当前代码对于单局游戏已经足够快但如果我们想模拟上百万局来统计牌型概率就需要考虑优化。算法层面算牛部分遍历10种组合是常数时间已是最优。主要优化点可能在特殊牌型判断。例如检查“五小牛”时all(v 5 for v in values)会遍历5次sum(values)又会遍历5次。可以合并遍历但鉴于只有5张牌优化收益微乎其微代码清晰更重要。大规模模拟如果要跑百万局应避免频繁创建和销毁对象。可以考虑复用Deck和Card对象池。使用numpy数组来存储牌的点数利用向量化运算加速组合求和与判断。但这会大大增加代码复杂度属于“过度优化”除非有极端的性能需求。内存与速度的权衡当前实现中每张牌都是一个Card对象。如果纯粹为了速度可以只用数字0-51来表示一张牌通过预计算的映射表来获取点数和花色。但这会牺牲代码的可读性和面向对象的设计美感。对于教学和一般模拟当前方式更合适。4.3 功能扩展方向这个命令行模拟器是一个完美的起点你可以基于它扩展出更多有趣的功能图形化界面 (GUI)使用Pygame、Tkinter或PyQt库将牌面可视化。发牌、亮牌的过程用动画展示体验会好很多。核心逻辑calculate_cattle完全不用变只需将Card对象与图片资源绑定即可。网络对战使用socket或websocket库将当前代码改造成一个服务器端逻辑。多个客户端连接后服务器负责发牌、计算、广播结果。客户端只负责展示和接收用户操作如准备、亮牌。智能提示编写一个函数在发完牌后不仅算出牛数还能提示用户“如何组合”。例如输出“用 ♠A、♥9、♦10 组成牛剩下 ♣2、♠3 得牛五”。这对于新手学习游戏很有帮助。概率统计运行数十万局统计各种牌型无牛、牛一...牛牛、五花牛等出现的概率。你会发现“无牛”的概率其实不低而“五花牛”、“五小牛”则是可遇不可求的极品牌型。引入下注与积分系统模拟一个完整的游戏房间玩家有初始筹码每局可以下注赢家通吃池底。这需要增加游戏状态管理和更复杂的回合逻辑。这个项目麻雀虽小五脏俱全。从数据建模、算法实现到模块封装最后到调试优化走完整个流程你对Python解决实际问题的能力会有更实在的体会。最关键的是它很有趣不是吗下次当你和朋友玩真牌时脑海里或许会不由自主地开始运行这段算牛的代码。
返回列表