从零构建策略骰子游戏:Python实现与概率博弈

从零构建策略骰子游戏:Python实现与概率博弈 1. 项目概述从零构建一个可玩性十足的骰子游戏最近在整理一些经典的小游戏原型发现骰子游戏是一个被严重低估的“宝藏”。它规则简单上手极快但背后蕴含的概率计算、策略博弈和交互反馈设计却非常考验一个开发者的基本功。很多人觉得骰子游戏不就是“扔一下比大小”吗但当你真正动手去实现一个完整的、带点策略和趣味性的骰子游戏时你会发现从随机数生成、游戏状态管理、到UI交互和胜负判定每一个环节都有不少门道。这个项目我们就来深入拆解一个“骰子游戏”的完整实现。它不仅仅是一个简单的随机数模拟器而是一个包含玩家决策、多轮次积分、以及一定策略深度的可交互程序。我们将从最核心的游戏规则设计开始逐步深入到代码架构、关键算法实现最后还会分享如何为这个简单的游戏骨架“注入灵魂”让它变得好玩起来。无论你是想学习游戏开发的基础逻辑还是想找一个轻量级的练手项目这个从零开始的骰子游戏构建过程都能给你带来实实在在的收获。2. 游戏核心规则与玩法设计拆解一个游戏好不好玩规则是灵魂。我们不能真的只做“比大小”那太单调了。我们需要设计一套有层次、有选择、有悬念的规则。2.1 基础规则不止于比大小我设计的核心玩法叫做“风险抉择”。游戏支持两名玩家或一名玩家对战电脑。每轮游戏双方会同时获得5个虚拟骰子。每一轮分为两个阶段第一阶段掷骰与“锁定”玩家掷出手中的所有骰子初始为5个。掷骰后玩家可以查看结果并选择将其中任意数量的、点数相同的骰子“锁定”起来。例如掷出[3, 3, 6, 1, 4]玩家可以选择锁定那两个点数3的骰子。被锁定的骰子在本轮后续的投掷中将保持当前点数不再参与重掷。第二阶段重掷与博弈锁定部分骰子后玩家用剩余未锁定的骰子进行第二次也是最后一次重掷。重掷后将锁定骰子的点数与重掷骰子的新点数合并形成本轮最终的手牌。胜负判定组合为王胜负并非比较总和而是比较最终手牌形成的“牌型”。我们借鉴扑克的思路但简化以适应骰子设定以下牌型从高到低五连顺五个点数连续如[1,2,3,4,5]或[2,3,4,5,6]。四相同四个点数相同。满堂彩三个点数相同外加另外两个点数相同。三相同三个点数相同。两对两个点数相同外加另外两个点数相同共四个骰子形成两对。一对两个点数相同。高牌不符合以上任何牌型则比较最大单颗骰子的点数。设计心路为什么选择“锁定”机制这引入了游戏最核心的策略点——风险与收益的权衡。你是见好就收锁定一个不错的对子或三同还是冒险一搏放弃中等牌型去追求顺子或满堂彩这个简单的选择让游戏从纯运气变成了带有心理博弈和概率计算的轻度策略游戏。2.2 游戏流程与状态机设计一个清晰的游戏流程是代码稳定的基础。我们可以用状态机来管理整个游戏进程初始化状态确定玩家重置积分生成5个骰子对象。回合开始状态清空上一轮锁定状态所有骰子进入“可掷”状态。第一次掷骰状态随机生成所有骰子点数展示结果。锁定选择状态玩家界面提供锁定/解锁交互。这是主要的决策点。第二次掷骰状态仅对未锁定的骰子进行重掷更新点数。牌型判定与计分状态根据双方最终手牌按照牌型规则判定胜负赢家获得积分。回合结束/游戏结束判断判断是否达到预设总局数如5局3胜是则进入游戏结束状态否则回到第2步。用代码中的枚举类来定义这些状态能让逻辑无比清晰from enum import Enum class GameState(Enum): INIT 1 # 初始化 ROUND_START 2 # 回合开始 FIRST_ROLL 3 # 第一次掷骰 LOCK_PHASE 4 # 锁定阶段 SECOND_ROLL 5 # 第二次掷骰 SCORING 6 # 计分阶段 ROUND_END 7 # 回合结束 GAME_OVER 8 # 游戏结束3. 核心技术实现与代码解析有了规则和流程我们进入具体的实现环节。这里我用Python作为示例语言因为它语法清晰适合展示逻辑。3.1 骰子与手牌类的构建首先我们需要一个Die骰子类和一个Hand手牌类。Die类不仅存储点数更重要的是管理其“锁定状态”。import random class Die: def __init__(self): self.value 1 # 骰子当前点数1-6 self.is_locked False # 是否被锁定 def roll(self): 如果骰子未被锁定则随机掷出一个1-6的点数 if not self.is_locked: self.value random.randint(1, 6) return self.value def lock(self): self.is_locked True def unlock(self): self.is_locked False def __str__(self): return f[{self.value}] ( (锁定) if self.is_locked else )接下来是Hand类它管理5个Die对象并核心负责牌型判定。class Hand: def __init__(self): self.dice [Die() for _ in range(5)] # 一手5个骰子 self.final_values [] # 存储最终用于判定的点数列表 def roll_all(self): 掷出手中的所有骰子尊重锁定状态 for die in self.dice: die.roll() self._update_final_values() def roll_unlocked(self): 仅重掷未被锁定的骰子 for die in self.dice: if not die.is_locked: die.roll() self._update_final_values() def _update_final_values(self): 更新最终点数列表并按从小到大排序便于牌型判断 self.final_values sorted([die.value for die in self.dice]) def get_hand_type(self): 判断手牌的牌型返回牌型名称和用于比较的权重值 vals self.final_values counts {i: vals.count(i) for i in set(vals)} # 统计每个点数的出现次数 # 判断五连顺 if vals [1,2,3,4,5] or vals [2,3,4,5,6]: return 五连顺, 7 # 判断四相同 if 4 in counts.values(): for point, count in counts.items(): if count 4: return 四相同, 6 point/10 # 权重6.xx为点数 # 判断满堂彩 (32) if 3 in counts.values() and 2 in counts.values(): three_of [p for p, c in counts.items() if c 3][0] two_of [p for p, c in counts.items() if c 2][0] return 满堂彩, 5 three_of/10 two_of/100 # 判断三相同 if 3 in counts.values(): three_of [p for p, c in counts.items() if c 3][0] # 找出剩余两个骰子中最大的点数作为次级比较 kickers sorted([p for p in vals if p ! three_of], reverseTrue) return 三相同, 4 three_of/10 kickers[0]/100 if kickers else 4 three_of/10 # 判断两对 pairs [p for p, c in counts.items() if c 2] if len(pairs) 2: high_pair, low_pair max(pairs), min(pairs) kicker [p for p in vals if p not in pairs][0] # 单张牌 return 两对, 3 high_pair/10 low_pair/100 kicker/1000 # 判断一对 if 2 in counts.values(): pair_point [p for p, c in counts.items() if c 2][0] kickers sorted([p for p in vals if p ! pair_point], reverseTrue) weight 2 pair_point/10 # 将剩余三张单牌的点数按重要性依次加入权重 for i, k in enumerate(kickers[:3]): # 只取前三个 weight k / (100 * (10 ** i)) return 一对, weight # 高牌直接按点数从大到小构成权重 weight 1 # 基础权重 for i, v in enumerate(reversed(vals)): # 从最大点数开始 weight v / (10 ** (i1)) # 最大点数占十分位次之占百分位以此类推 return 高牌, weight关键点解析get_hand_type方法是核心中的核心。它返回牌型名称和一个浮点数权重。这个权重的设计非常巧妙整数部分代表牌型等级五连顺是7高牌是1小数部分则编码了牌型内部的大小比较信息比如四相同中四个6就比四个5大。这样只需要比较两个手牌返回的权重值就能一次性完成“先比牌型牌型相同再比细节”的完整胜负判定代码简洁高效。3.2 游戏主循环与状态管理游戏主类DiceGame将串联所有模块并管理游戏状态。class DiceGame: def __init__(self, player1_name玩家1, player2_name玩家2, max_rounds5): self.players { A: {name: player1_name, hand: Hand(), score: 0}, B: {name: player2_name, hand: Hand(), score: 0} } self.current_player A # 当前操作玩家在锁定阶段 self.state GameState.INIT self.max_rounds max_rounds self.current_round 1 self.winner None def start_game(self): 启动游戏进入主循环 self.state GameState.ROUND_START self._run_game_loop() def _run_game_loop(self): 核心游戏循环根据状态执行不同操作 while self.state ! GameState.GAME_OVER: if self.state GameState.ROUND_START: self._start_new_round() elif self.state GameState.FIRST_ROLL: self._first_roll() elif self.state GameState.LOCK_PHASE: # 在实际GUI或Web应用中这里会等待玩家输入 # 在命令行演示中我们可以模拟一个简单的锁定策略 self._simulate_lock_phase() elif self.state GameState.SECOND_ROLL: self._second_roll() elif self.state GameState.SCORING: self._determine_winner() elif self.state GameState.ROUND_END: self._check_game_over() def _start_new_round(self): 开始新回合重置骰子锁定状态 print(f\n 第 {self.current_round} 轮 ) for p in self.players.values(): for die in p[hand].dice: die.unlock() # 解锁所有骰子 self.state GameState.FIRST_ROLL def _first_roll(self): 双方进行第一次掷骰 print(--- 第一次掷骰 ---) for pid, pdata in self.players.items(): pdata[hand].roll_all() print(f{pdata[name]}掷出: {self._format_hand(pdata[hand])}) self.state GameState.LOCK_PHASE def _simulate_lock_phase(self): 模拟锁定阶段一个简单的AI策略——锁定所有成对的或以上的骰子 print(--- 锁定阶段 ---) for pid, pdata in self.players.items(): hand pdata[hand] vals hand.final_values # 找出出现次数大于1的点数 from collections import Counter counter Counter(vals) to_lock [point for point, count in counter.items() if count 2] for die in hand.dice: if die.value in to_lock: die.lock() locked_dice [d.value for d in hand.dice if d.is_locked] print(f{pdata[name]}锁定了点数: {locked_dice if locked_dice else 无}) print(f锁定后手牌状态: {self._format_hand(hand)}) self.state GameState.SECOND_ROLL def _second_roll(self): 双方进行第二次掷骰仅未锁定的骰子 print(--- 第二次掷骰 ---) for pid, pdata in self.players.items(): pdata[hand].roll_unlocked() print(f{pdata[name]}最终手牌: {self._format_hand(pdata[hand])}) self.state GameState.SCORING def _determine_winner(self): 判定本轮胜负并计分 print(--- 胜负判定 ---) results {} for pid, pdata in self.players.items(): hand_type, weight pdata[hand].get_hand_type() results[pid] {name: pdata[name], type: hand_type, weight: weight} print(f{pdata[name]}: {hand_type} (权重: {weight:.4f})) # 比较权重 if results[A][weight] results[B][weight]: winner_id A elif results[A][weight] results[B][weight]: winner_id B else: winner_id None # 平局 if winner_id: self.players[winner_id][score] 1 print(f本轮获胜者: {results[winner_id][name]}) else: print(本轮平局) print(f当前积分 - {results[A][name]}: {self.players[A][score]}, {results[B][name]}: {self.players[B][score]}) self.state GameState.ROUND_END def _check_game_over(self): 检查游戏是否结束 if self.current_round self.max_rounds or max(p[score] for p in self.players.values()) (self.max_rounds // 2 1): self.state GameState.GAME_OVER self._declare_final_winner() else: self.current_round 1 self.state GameState.ROUND_START def _declare_final_winner(self): 宣布最终胜利者 scores {pid: pdata[score] for pid, pdata in self.players.items()} max_score max(scores.values()) winners [pid for pid, score in scores.items() if score max_score] if len(winners) 1: winner_name self.players[winners[0]][name] print(f\n 游戏结束最终胜利者是{winner_name}) else: print(f\n 游戏结束双方平局) def _format_hand(self, hand): 格式化显示手牌锁定骰子用*标出 return .join([f*{die.value}* if die.is_locked else str(die.value) for die in hand.dice]) # 运行游戏 if __name__ __main__: game DiceGame(player1_name小明, player2_name电脑, max_rounds3) game.start_game()4. 从命令行到图形界面交互体验升级上面的代码提供了一个完整的、可运行的游戏逻辑核心。但命令行交互体验有限。要让游戏真正“好玩”一个直观的图形界面至关重要。这里我以Python的Pygame库为例讲解几个图形化关键点。4.1 骰子图形绘制与状态反馈在Pygame中我们需要将每个Die对象与一个图形矩形和状态关联起来。import pygame class GraphicalDie(Die): def __init__(self, x, y, size60): super().__init__() self.rect pygame.Rect(x, y, size, size) self.size size self.color (240, 240, 240) # 默认浅灰色 self.locked_color (255, 200, 200) # 锁定后变为浅红色 def draw(self, screen): 在屏幕上绘制骰子 # 绘制骰子主体 current_color self.locked_color if self.is_locked else self.color pygame.draw.rect(screen, current_color, self.rect, border_radius10) pygame.draw.rect(screen, (50, 50, 50), self.rect, 3, border_radius10) # 边框 # 绘制点数用圆点模拟 dot_color (30, 30, 30) dot_radius self.size // 10 dot_positions { 1: [(self.rect.centerx, self.rect.centery)], 2: [(self.rect.centerx - offset, self.rect.centery - offset), (self.rect.centerx offset, self.rect.centery offset)], 3: [(self.rect.centerx - offset, self.rect.centery - offset), (self.rect.centerx, self.rect.centery), (self.rect.centerx offset, self.rect.centery offset)], # ... 为点数4,5,6定义相应的圆点位置这里省略详细坐标计算 } offset self.size // 4 positions dot_positions.get(self.value, []) for pos in positions: pygame.draw.circle(screen, dot_color, pos, dot_radius) def is_clicked(self, pos): 判断鼠标点击是否在骰子范围内 return self.rect.collidepoint(pos)在游戏主循环中你需要处理鼠标事件当点击一个GraphicalDie时调用其lock()或unlock()方法并触发重绘视觉上骰子颜色会立刻改变给予玩家清晰的反馈。4.2 游戏状态与UI的同步图形界面的游戏主循环与命令行不同它是由事件驱动的。你需要将之前的GameState与不同的UI界面绑定。def main_game_loop(): running True clock pygame.time.Clock() game DiceGame() # 使用图形化版本的Game类 game.state GameState.ROUND_START while running: for event in pygame.event.get(): if event.type pygame.QUIT: running False # 根据游戏状态处理不同事件 if game.state GameState.LOCK_PHASE: if event.type pygame.MOUSEBUTTONDOWN: mouse_pos pygame.mouse.get_pos() # 检查点击了哪个骰子切换其锁定状态 for die in game.current_player_hand.dice: if die.is_clicked(mouse_pos): if die.is_locked: die.unlock() else: die.lock() break if event.type pygame.KEYDOWN and event.key pygame.K_SPACE: # 按空格确认锁定进入下一状态 game.state GameState.SECOND_ROLL # 根据状态绘制不同界面 screen.fill((255, 255, 255)) if game.state GameState.LOCK_PHASE: # 绘制所有骰子显示“锁定阶段”提示文字和操作说明 draw_dice(game.current_player_hand.dice) draw_text(请选择要锁定的骰子按空格确认, ...) elif game.state GameState.SCORING: # 绘制双方最终手牌和牌型结果 draw_hand_comparison(game.players[A][hand], game.players[B][hand]) # ... 其他状态 pygame.display.flip() clock.tick(60)交互设计心得在锁定阶段一定要提供明确的视觉反馈和操作引导。比如被锁定的骰子不仅颜色变化还可以轻微放大或加上一个锁形图标。同时在界面角落始终显示当前游戏阶段、剩余操作如“点击骰子锁定/解锁按空格继续”和双方积分。清晰的UI状态提示能极大降低玩家的学习成本。5. 策略浅析与概率计算这个游戏的有趣之处在于简单的规则下藏着概率的博弈。了解基础概率能帮你做出更好的决策。5.1 常见牌型概率与锁定策略第一次掷骰后你面临选择锁定哪些骰子我们分析几个场景场景一掷出[3, 3, 6, 1, 4]保守策略锁定两个3一对。你保底了一个“一对”的牌型。然后用剩下的三个骰子去博一个更大的对子凑成两对或一个三同。激进策略什么都不锁全部重掷。你放弃了现有的一对去追求概率更小的顺子、满堂彩等大牌。这在积分落后时需要搏命时可以考虑。场景二掷出[5, 5, 5, 2, 6]几乎必锁锁定三个5三同。三同已经是一个中等偏上的牌型了。用剩下两个骰子去博另一个对子凑成满堂彩的概率是多少计算一下两个骰子掷出相同点数的概率是6 / 36 1/6。这个概率不低值得一搏。所以最优策略是锁定三个5重掷另外两个。场景三掷出[1, 2, 3, 4, 6]顺子追求你有一个四连顺缺5。这时你应该锁定[1,2,3,4]只重掷那个6。掷出5的概率是1/6。这是追求最大牌型“五连顺”的最优路径。5.2 权重系统的数学合理性验证我们之前设计的权重系统如五连顺7四相同6.x是否真的能保证在所有情况下正确比较我们来验证一个边界情况手牌A四相同四个5。权重 6 5/10 6.5手牌B满堂彩三个6带两个6即五个6但按规则是满堂彩。权重 5 6/10 6/100 5.66比较6.5 5.66因此“四相同四个5”大于“满堂彩三个6带两个6”。这符合扑克规则吗在标准扑克中“四条”确实大于“满堂彩”。我们的权重系统正确反映了这一点。另一个案例同为“一对”一对6带K、Q、J和一对5带A、K、Q如何比较我们的权重算法会将单牌点数按重要性依次编码到小数位后。例如一对6带K(13)、Q(12)、J(11)假设点数为6,6,13,12,11权重约为2 0.6 0.13 0.012 0.0011 ≈ 2.7431。一对5带A(14)、K(13)、Q(12)权重约为2 0.5 0.14 0.013 0.0012 ≈ 2.6542。2.7431 2.6542因此一对6更大。这符合“先比对子点数对子相同再依次比剩余单牌”的规则。避坑指南在实现权重系统时最容易出错的地方是小数精度的处理和比较。确保你的权重层级整数部分间隔足够大避免小数部分累加后跨层级。例如最差的高牌1,2,3,4,5权重约为1.54321而最好的一对一对2带A,K,Q权重约为2.2141312。两者之间仍有明显差距不会因为小数进位产生误判。在实际编码中可以使用Decimal库进行高精度比较或者确保权重的小数部分设计得足够“稀疏”。6. 项目扩展与创意方向一个基础框架完成后你可以从多个方向扩展它让它变成一个更丰满的作品。6.1 玩法扩展加入特殊技能与道具让游戏更具变化性可以引入“技能系统”每局或每隔几局可以使用一次。窥视查看对方一个未锁定骰子的点数。重锁在第二次掷骰后允许你重新调整一次锁定状态但点数不变。强制重掷指定对方一个已锁定的骰子强制其重新掷一次。双倍积分在本轮获胜时积分翻倍。实现上可以在玩家对象中增加一个skills列表在锁定阶段或计分阶段提供额外的按钮或菜单来触发。6.2 平台移植从桌面到网页如果你想分享给朋友网页版是更好的选择。使用HTML5 Canvas和JavaScript几乎可以完全复现Pygame的功能。架构转换将Python的Die,Hand,Game类用JavaScript重写。逻辑代码可以高度复用。绘图用Canvas的drawRect,drawCircle和fillText来绘制骰子和界面。交互监听onclick和onkeydown事件来替代Pygame的事件循环。优势无需安装链接即可分享可以更容易地加入网络对战功能通过WebSocket。6.3 网络对战功能实现雏形为你的骰子游戏加入联机能力乐趣会成倍增长。一个简单的架构是采用“客户端-服务器”模型。服务器端Python/Node.js负责维护游戏房间、转发玩家操作锁定骰子、重掷、验证操作合法性、计算最终结果并广播给所有客户端。游戏的核心逻辑牌型判定应该在服务器端进行防止客户端作弊。客户端可以是你的Pygame程序或网页前端。它只负责向服务器发送玩家操作指令如“锁定第3个骰子”。从服务器接收游戏状态更新如所有骰子最新点数、对手锁定状态、回合阶段。根据接收到的数据渲染本地界面。通信协议使用JSON格式的消息。例如// 客户端 - 服务器锁定动作 {action: lock, player_id: player_abc, die_index: 2, round: 1} // 服务器 - 所有客户端状态同步 {type: state_update, round: 1, phase: lock, player_states: {...}}网络同步要点网络游戏的核心是状态同步。务必设计一个权威的游戏状态存储在服务器客户端只是这个状态的视图。任何玩家操作都必须先发送到服务器由服务器验证并更新权威状态再将新状态广播给所有客户端。这样可以杜绝大部分外挂。对于骰子游戏这种节奏较慢的回合制游戏即使有少量网络延迟体验影响也不大。