OpenSpiel 中的 PokerkitWrapper 与 RepeatedPokerkit:多扑克变体仿真与现金局/锦标赛建模指南
人工智能强化学习深度学习【免费下载链接】open_spielOpenSpiel is a collection of environments and algorithms for research in general reinforcement learning and search/planning in games.项目地址https://gitcode.com/gh_mirrors/op/open_spiel点击查看免费下载导读本文以 open_spiel/python/games/README.pokerkit_wrapper.md 为主线系统讲解 OpenSpiel 中两个基于 PokerKit 构建的扑克游戏适配层PokerkitWrapper与RepeatedPokerkit。读完本文你将掌握如何在 OpenSpiel 中加载多种预定义扑克变体德州扑克、限注、七张梭哈、Razz、底池限注奥马哈、短牌等并配置其全部参数如何通过RepeatedPokerkit在同一 episode 内连打多手牌从而仿真现金局cash game与锦标赛tournament两种赛制以及这两层包装背后的动作编码、观察字符串、手牌历史PHH等实现原理可直接用于强化学习与博弈论算法的环境搭建。一、背景为什么 OpenSpiel 需要两层包装OpenSpiel 原生 C 侧的扑克实现是universal_poker基于 ACPC 协议只覆盖德州扑克等少数变体。为了支持更多预定义的扑克游戏变体OpenSpiel 引入了两层 Python 包装PokerkitWrapper直接包装 PokerKit 库将 PokerKit 的State对象适配为 OpenSpiel 的pyspiel.Game/pyspiel.State接口目前支持 6 种变体RepeatedPokerkit再包装PokerkitWrapper即对 PokerKit 的双重包装让一个 episode 内连续打多手牌通过盲注/底注/限注额度的**升级计划表schedule**与筹码管理同时支持现金局与锦标赛仿真。这种分层设计在源码中的体现为open_spiel/python/games/pokerkit_wrapper.py 定义了基础包装器及其 ACPC 风格子类并在文件末尾通过pyspiel.register_game(_GAME_TYPE, PokerkitWrapper)注册open_spiel/python/games/repeated_pokerkit.py 依赖前者注册名为python_repeated_pokerkit的游戏。两个模块都属于可选依赖加载逻辑见 open_spiel/python/games/init.py当pokerkit库不可导入时两个模块会被置为None而不影响 OpenSpiel 主框架启动。相关论文Juho Kim 所著PokerKit: A Comprehensive Python Library for Fine-Grained Multivariant Poker Game Simulations发表于IEEE Transactions on GamesDOI: 10.1109/TG.2023.3325637。本仓库中的实现即依据该库的设计接口进行包装。二、PokerkitWrapper单局多变体扑克2.1 注册名与支持的变体PokerkitWrapper的注册短名为python_pokerkit_wrapper其 GameType 定义在 pokerkit_wrapper.py#L178-L197dynamics SEQUENTIAL序贯决策、chance_mode EXPLICIT_STOCHASTIC显式随机节点发牌、information IMPERFECT_INFORMATION不完全信息、utility ZERO_SUM零和、reward_model TERMINAL终局结算min_num_players 2、max_num_players 10与 universal_poker 保持一致提供信息状态字符串与观察字符串provides_information_state_stringTrue、provides_observation_stringTrue但暂不提供张量provides_information_state_tensorFalse、provides_observation_tensorFalse使用上需依赖字符串观察。支持的变体由SUPPORTED_VARIANT_MAP定义pokerkit_wrapper.py#L66-L83共 6 种变体名参数variant取值对应 PokerKit 类说明NoLimitTexasHoldempokerkit.NoLimitTexasHoldem无限注德州扑克默认NoLimitShortDeckHoldempokerkit.NoLimitShortDeckHoldem短牌德州扑克36 张牌FixedLimitTexasHoldempokerkit.FixedLimitTexasHoldem限注德州扑克Limit HoldemFixedLimitSevenCardStudpokerkit.FixedLimitSevenCardStud七张梭哈Seven Card StudFixedLimitRazzpokerkit.FixedLimitRazzRazz梭哈类比最小牌PotLimitOmahaHoldempokerkit.PotLimitOmahaHoldem底池限注奥马哈PLO其中NoLimitShortDeckHoldem使用pokerkit.Deck.SHORT_DECK_HOLDEM短牌牌堆其余变体使用pokerkit.Deck.STANDARD标准 52 张牌堆见 pokerkit_wrapper.py#L387-L395。测试中对此也有验证test_deck_size_is_correct_for_nolimit_holdem断言无限注德州为 52 张test_shortdeck_deck断言短牌为 36 张且不含 2~5 点牌pokerkit_wrapper_test.py。2.2 默认参数与完整参数表_DEFAULT_PARAMSpokerkit_wrapper.py#L130-L176给出了所有可配置参数及其默认值加载游戏时缺省参数会自动取默认值构造逻辑见__init__中的_DEFAULT_PARAMS | params参数默认值类型与约束适用变体variantNoLimitTexasHoldem字符串必须命中SUPPORTED_VARIANT_MAP全部num_players2整数2~10全部blinds5 10空格分隔的字符串SB BB要求 SB ≥ 1、BB ≥ 2、SB ≤ BB有盲注的变体Holdem 类num_streets4整数街道下注轮数量全部bring_in5整数公开底牌的强制注必须严格小于small_bet提供small_bet时必须提供七张梭哈、Razzsmall_bet10整数限注变体的小注额必须严格小于big_betFixedLimit 系列big_bet20整数限注变体的大注额FixedLimit 系列antes0 0空格分隔字符串前注目前主要作为预留参数全部预留stack_sizes2000 2000空格分隔的num_players个整数各玩家起始筹码全部几个重要的使用注意点源码注释中明确给出字符串参数不要包含逗号单值整数不要写成字符串否则默认 pickle 序列化/反序列化往返可能出错pokerkit_wrapper.py#L122-L129传入浮点数筹码如2000.0会开启分数分池fractional split pots传整数则按 PokerKit 的默认方式整分blinds中的位置语义遵循 PokerKit 的位置position而非座位号非单挑按钮局中0 号玩家永远是小盲、1 号是大盲、2 号是 UTG……最后一位玩家在按钮位梭哈类游戏没有按钮概念庄家左邻为 0 号位源码引用了 PokerKit 文档 的说明参数存在强耦合对如bring_in/small_bet、small_bet/big_bet若只改动其中一个而另一个保持默认代码会打印警告日志提示可能配置错误pokerkit_wrapper.py#L351-L376。源码还提供了VARIANT_PARAM_USAGEpokerkit_wrapper.py#L109-L118通过反射各 PokerKit 变体类的构造函数签名告诉你每个参数实际被哪些变体使用测试test_bring_in_usage等也印证了这一点bring_in只用于 Razz 与七张梭哈small_bet/big_bet用于 FixedLimit 系列。2.3 加载游戏与基本使用import pyspiel # 默认无限注德州扑克2 人盲注 5/10每人 2000 筹码 game pyspiel.load_game(python_pokerkit_wrapper) # 自定义4 人 PLO盲注 5/10不等筹码 game pyspiel.load_game( python_pokerkit_wrapper, { variant: PotLimitOmahaHoldem, num_players: 4, blinds: 5 10, stack_sizes: 198 189 200 200, }, ) # 七张梭哈示例无盲注需要 bring_in small_bet big_bet game pyspiel.load_game( python_pokerkit_wrapper, { variant: FixedLimitSevenCardStud, num_players: 4, stack_sizes: 200 200 189 164, bring_in: 5, small_bet: 10, big_bet: 20, }, )状态推进与普通 OpenSpiel 游戏一致发牌节点chance node通过chance_outcomes()取牌并用apply_action(card_int)发牌玩家节点通过legal_actions()取动作。每个状态内部包装了一个pokerkit.State对象PokerkitWrapperState见 pokerkit_wrapper.py#L653-L663chance 动作的编号即牌堆中每张牌的下标0~51 或 0~35字符串形式如Deal Hole Card: TEN OF SPADES (Ts)。2.4 动作编码Fold / Check-Call / 下注额度动作空间被刻意设计为与 universal_poker 的kFold/kCall对齐便于从 ACPC 实现移植代码pokerkit_wrapper.py#L223-L2480 FOLD弃牌1 CHECK_OR_CALL同时在特殊局面下复用为 POST_BRING_IN这两个动作在语义上不会同时合法因此共用编号 1 是安全的 2的整数 加注到 XBet/Raise to X其中X 表示当前街street内的累计投入这与 ACPC/universal_poker 的整手牌累计投入语义不同是两类实现最核心的差异。由于动作1被保留下注 1 个筹码在玩家只剩 1 个筹码想全下时与保留动作冲突因此实现将该局面特殊映射为动作2动作字符串为Bet/Raise to 1 [ALL-IN EDGECASE]_ALL_IN_FOR_ONE_CHIP_EDGECASE_STRINGpokerkit_wrapper.py#L120。源码通过强制min_bet/small_bet至少为 2 来避免除全下外出现 1 筹码下注。测试test_side_pot_with_one_chip_bet完整演示了该边界场景P0翻牌全下 1 筹码时合法动作是[ACTION_CHECK_OR_CALL, 2]应用动作2后内部实际执行complete_bet_or_raise_to(1)pokerkit_wrapper_test.py。下注额度的合法范围由 PokerKit 给出限注FIXED_LIMIT下 min/max 相同、只有一个固定额度无限注NO_LIMIT与底池限注POT_LIMIT下枚举min_completion_betting_or_raising_to_amount到max_completion_betting_or_raising_to_amount之间的所有整数pokerkit_wrapper.py#L717-L749。因此num_distinct_actions上界取max(stack_sizes) 10 为 fold1 为 check2 以上为加注额度见_calculate_num_distinct_actions。测试test_action_space_equals_max_stack_size验证了 4 人局最大筹码 400 时num_distinct_actions 401。2.5 自动流程与不发牌PokerkitWrapper用一组**自动流程automations**接管了 PokerKit 中本需显式调用的步骤pokerkit_wrapper.py#L52-L63pokerkit.Automation.ANTE_POSTING # 前注自动缴纳 pokerkit.Automation.BET_COLLECTION # 下注自动收集入池 pokerkit.Automation.BLIND_OR_STRADDLE_POSTING # 盲注/抓注自动缴纳 pokerkit.Automation.HOLE_CARDS_SHOWING_OR_MUCKING # 摊牌/盖牌自动处理 pokerkit.Automation.HAND_KILLING # 死牌自动处理 pokerkit.Automation.CHIPS_PUSHING # 筹码推入 pokerkit.Automation.CHIPS_PULLING # 筹码收回结算源码特别警告不要自动化 CARD_BURNING烧牌。因为烧牌在 OpenSpiel 中应作为 chance 节点/显式动作处理否则无法忠实反映随机性。为此wrapped_state_factory()在构造底层pokerkit.State时显式把每条街的card_burning_status设为Falsepokerkit_wrapper.py#L579-L647测试test_card_burning_is_disabled_everywhere也逐一断言所有 street 的烧牌状态为关闭。发牌、下注则保留为显式 chance/player 节点保证 OpenSpiel 的树形搜索与随机模拟接口语义完整。2.6 观察字符串与手牌历史PHHPokerkitWrapperObserverpokerkit_wrapper.py#L1291-L1470生成按玩家视角的观察字符串字段包括当前街道、该街是否已发牌/抽牌、下一个行动玩家、底池、各玩家当前下注、公共牌、本玩家私有底牌、各玩家公开视角底牌他人底牌以??打码、各玩家起始与当前筹码。例如来自官方 playthrough 文件 python_pokerkit_wrapper(variantNoLimitTexasHoldem).txt.txt)Player: 0 ||Current Street: 0 ||Current Street discard/draw performed: False ||Next Player to act: None ||Pot(s): [] ||Bets: [10, 5] ||Board Cards: [] ||Players Private Hole Cards: [Ts] ||Per-player Hole Cards (public view): [[??], []] ||Per-player Starting Stacks: (2000, 2000) ||Per-player Current Stacks: [1990, 1995]当 observer 以IIGObservationType(perfect_recallTrue)创建时string_from还会把该玩家的 PokerKit 手牌历史动作PHH Actions拼接到观察字符串末尾pokerkit_wrapper.py#L1461-L1470。PHH 动作通过pokerkit.HandHistory.from_game_state生成且自动对其他玩家的底牌进行打码如d dh p2 ????见poker_hand_history_actionspokerkit_wrapper.py#L1317-L1365。测试test_observer_string_contains_phh_for_all_game_variants_iff_perfect_recall验证了完美回忆perfect_recall开关对 PHH 是否出现在字符串中的影响。另外to_struct()返回的pyspiel.pokerkit_wrapper.PokerkitStateStruct还额外提供各玩家视角的 PHH 动作列表、ACPC 协议的per_player_acpc_logs形如[S-, MATCHSTATE:0:0:cc/r300r600:AcKc|/QcJcTc\r\n]、投注历史字符串如cc/r200r500r800、盲注/起始筹码/底池大小等字段并兼容UniversalPokerStateStruct的一部分字段betting_history、player_contributions、pot_size等pokerkit_wrapper.py#L1148-L1271方便与既有 universal_poker 代码对接。2.7 ACPC 风格子类PokerkitWrapperAcpcStyle为更贴近 C 侧 UniversalPoker / ACPC 的动作语义源码提供了子类PokerkitWrapperAcpcStyle注册名python_pokerkit_wrapper_acpc_stylepokerkit_wrapper.py#L1490-L1536但仅支持两种变体NoLimitTexasHoldem与FixedLimitTexasHoldem_VARIANTS_SUPPORTING_ACPC_STYLE。其核心差异在于ACPC 风格中加注动作X表示所有街道的累计投入而基础类是当前街道投入。子类通过ToAcpcActionConverter逐街跟踪每个玩家的投入在合法动作列表与动作字符串之间做双向映射pokerkit_wrapper.py#L1801-L2074。观察字符串与基础类完全一致不匹配 universal_poker但betting_history中的加注额度会换算为 ACPC 总投入口径to_struct()中会断言换算前后仅r后的数字不同pokerkit_wrapper.py#L1779-L1798。与 universal_poker 对照时源码注释建议以 PHH 手牌历史与 returns 数组为准。三、RepeatedPokerkit同一 episode 连打多手牌3.1 设计目标与注册名RepeatedPokerkit注册名python_repeated_pokerkitrepeated_pokerkit.py#L88-L109在PokerkitWrapper之上再包一层让一个 OpenSpiel episode 内连续进行多手牌。每一手牌结束底层pokerkit.State终局后自动开始下一手更新筹码、按规则轮转庄家按钮、按计划表推进盲注/限注额度/底注直到达到max_num_hands或锦标赛产生冠军。每手牌的收益逐手累加作为该 episode 的总收益returns()汇总_hand_returnsrepeated_pokerkit.py#L1019-L1031。GameType 特征与包装器一致序贯、显式随机、不完全信息、零和、终局结算同样只提供观察字符串、不提供张量与信息状态字符串。由于嵌套pokerkit_game_params的 pickle 序列化存在问题源码 TODO b/445192003该游戏的 GameType 序列化测试被暂时禁用repeated_pokerkit.py#L61-L64。3.2 参数表与赛制语义RepeatedPokerkit的全部参数repeated_pokerkit.py#L65-L86及__init__中的校验逻辑repeated_pokerkit.py#L336-L400如下参数默认值类型与说明pokerkit_game_params必填指定底层单局游戏必须是python_pokerkit_wrapper(...)或python_pokerkit_wrapper_acpc_style(...)形式的参数字符串/字典其中的num_players、blinds、stack_sizes等会被本层实时改写以反映重复局当前状态max_num_hands1episode 内最大手牌数必填锦标赛中需与盲注计划表协同设定reset_stacksFalse每手牌是否重置筹码。True 现金局每手重新等额买入False 锦标赛筹码跨手累积、输光出局rotate_dealerTrue每手牌是否轮转庄家对 PokerKit 而言即轮转座位使按钮成为最后一个 stack。设为False时若当前庄家出局仍会强制轮转blind_schedule锦标赛盲注计划表格式见下bet_size_schedule限注变体的注额计划表bring_in_schedule梭哈类变体的底注计划表first_button_player-1第一手牌的按钮位玩家 ID-1表示取最后一位玩家即 PokerKit 默认解释record_full_hand_historiesTrue每手结束时是否记录各玩家视角的完整 PHH 动作_wrapped_state_hand_histories手数/玩家数很大时内存占用高遇内存问题可置False计划表schedule均为分号分隔的等级列表解析函数见 repeated_pokerkit.py#L114-L225盲注计划表blind_schedule盲注等级1;盲注等级2;...每个等级形如手数:小盲/大盲例如1:10/20;2:20/40;1:50/100注额计划表bet_size_schedule每个等级形如手数:小注/大注仅适用于 FIXED_LIMIT 注额结构否则报错底注计划表bring_in_schedule每个等级形如手数:底注同样仅适用于 FIXED_LIMIT 结构。超出最后一个等级的手数沿用最后一档update_blinds_bet_sizes_and_or_bring_in中的schedule[-1]兜底repeated_pokerkit.py#L883-L898。测试test_blind_schedule_progression验证了1:10/20;2:20/40;1:50/100;1:1000/1000在手 0、1、2、3、4 的盲注演进以及最后一档1000/1000的持续沿用。3.3 现金局与锦标赛的加载示例现金局每手重置筹码import pyspiel game pyspiel.load_game( python_repeated_pokerkit, { max_num_hands: 25, reset_stacks: True, # 现金局 rotate_dealer: True, pokerkit_game_params: { name: python_pokerkit_wrapper, variant: NoLimitTexasHoldem, num_players: 4, blinds: 5 10, stack_sizes: 1000 1000 1000 1000, }, }, )锦标赛筹码跨手累积带盲注升级game pyspiel.load_game( python_repeated_pokerkit, { max_num_hands: 100, reset_stacks: False, # 锦标赛 rotate_dealer: True, blind_schedule: 10:10/20;10:15/30;10:25/50;10:50/100;10:100/200;50:200/400, pokerkit_game_params: { name: python_pokerkit_wrapper, num_players: 6, blinds: 10 20, stack_sizes: 2000 2000 2000 2000 2000 2000, }, }, )官方 playthrough 文件也提供了可直接对照的实例repeated_pokerkit_nolimit_holdem_cash_game.txtmax_num_hands25,reset_stacksTrue与 repeated_pokerkit_nolimit_holdem_tournament.txtmax_num_hands200,reset_stacksFalse。从 tournament playthrough 的 GameInfo 可见4 人各 200 筹码时MinUtility() -200、MaxUtility() 800总筹码守恒这正是锦标赛总筹码不流失语义的体现。3.4 实现细节座位轮转、筹码结转与出局判定RepeatedPokerkitStaterepeated_pokerkit.py#L475-L668维护了 OpenSpiel 玩家 ID 与 PokerKit 座位之间的双向映射以及庄家、盲注、注额、底注等跨手状态。每手结束后的流程为_apply_actionrepeated_pokerkit.py#L960-L1000记录本手各座位的收益到_hand_returns按需记录各玩家视角 PHH判断是否达到max_num_hands达到则终局并做最终筹码结算或仅剩 1 名活跃玩家锦标赛冠军产生否则进入下一手update_stacks()将底池收益结转进筹码 →update_seat_assignments_unrotated()按筹码归零与否标记出局座位 →update_dealer()轮转庄家若庄家出局则跳过其 ID 继续轮转→update_seat_rotation()计算旋转量使按钮坐到最后一座位 → 按计划表推进盲注/注额/底注 →update_pokerkit_wrapper()用最新参数重新pyspiel.load_game加载底层包装器并创建新状态。几个值得注意的语义源码注释明确说明使用简化按钮规则而非死按钮dead button规则即当盲注位玩家出局时跳过空位继续轮转这是线上扑克常见做法与 RepeatedPokerC 实现不同本实现支持筹码不足一个大盲的玩家继续参赛而非自动淘汰因此锦标赛总奖励恒等于初始总筹码当rotate_dealerFalse时若庄家出局仍会强制轮转到下一个未出局玩家以保证后续座位旋转可计算无按钮旋转概念的变体如梭哈类若设置rotate_dealerTrue会直接报错repeated_pokerkit.py#L620-L625。相关测试覆盖了筹码结转test_stacks_carried_over、多人轮转test_stack_rotation_multiway、恰好归零淘汰test_eliminates_players_at_exactly_zero_chips、多人同时出局后的分池test_multiway_all_but_one_players_bust_simultaneously、test_6max_3players_bust_then_split_pot、手数上限test_max_num_hands_limit与终局收益test_returns_in_terminal_state等场景repeated_pokerkit_test.py。四、安装前提与测试验证4.1 依赖安装两个游戏均依赖第三方 Python 库pokerkit。OpenSpiel 的依赖脚本 open_spiel/scripts/python_extra_deps.sh 中将其列入可选依赖不同 Python 版本对应pokerkit0.6.3或pokerkit0.7.3可据此执行pip install pokerkit未安装时open_spiel/python/games/init.py 的 try/except 会静默地将两个模块置为NoneOpenSpiel 其余功能不受影响测试文件 pokerkit_wrapper_test.py 与 repeated_pokerkit_test.py 顶部也采用同样的 try/except 导入策略以保证在缺少该依赖的环境下测试可正常跳过。4.2 可验证的运行方式加载游戏后即可用 OpenSpiel 自带工具做随机模拟验证 API 一致性import pyspiel game pyspiel.load_game(python_pokerkit_wrapper) pyspiel.random_sim_test(game, num_sims10, serializeFalse, verboseTrue) game2 pyspiel.load_game( python_repeated_pokerkit(max_num_hands20,reset_stacksTrue, rotate_dealerTrue,pokerkit_game_paramspython_pokerkit_wrapper()) ) pyspiel.random_sim_test(game2, num_sims5, serializeFalse, verboseTrue)random_sim_test会从初始状态随机走完整个树直到终局并校验动作合法性、收益零和性等一致性约束。仓库中的官方 playthrough 文件open_spiel/integration_tests/playthroughs/为每个受支持的变体保留了逐步状态转储例如python_pokerkit_wrapper(variantFixedLimitRazz).txt、python_pokerkit_wrapper(variantPotLimitOmahaHoldem).txt、python_pokerkit_wrapper_acpc_style.txt、repeated_pokerkit_plo_cash_game.txt与repeated_pokerkit_seven_card_stud_tournament.txt等可作为理解各变体行为与调试自定义配置的参考基线。五、总结与扩展方向从源码结构看这两层包装为 OpenSpiel 补齐了多扑克变体 多手牌赛制两块能力PokerkitWrapper解决了变体覆盖问题德州/限注/梭哈/Razz/PLO/短牌并通过 ACPC 风格子类兼容既有 UniversalPoker 动作语义RepeatedPokerkit解决了赛制建模问题现金局/锦标赛以计划表驱动盲注与限注额升级并内置出局、庄家轮转、筹码结转等规则。仓库中的 TODO 也指明了可继续扩展的方向支持任意数量盲注当前限定恰好两个、更紧的收益上界估计、锦标赛 payout 结构、分数分池的多人局支持以及PokerkitWrapperAcpcStyle在特定 all-in 场景下与 UniversalPoker 的完全对齐详见 pokerkit_wrapper.py 与 repeated_pokerkit.py 中的b/437724266、b/434776281、b/444333187等注释。对于需要在不完全信息扑克环境中做 CFR、MCTS、DQN 等算法研究的开发者可以直接以这两个游戏为环境起点通过观察字符串或 PHH 手牌历史构造信息集。赞分享人工智能强化学习深度学习【免费下载链接】open_spielOpenSpiel is a collection of environments and algorithms for research in general reinforcement learning and search/planning in games.项目地址https://gitcode.com/gh_mirrors/op/open_spiel点击查看免费下载相关推荐QEMU 中的 CXL 2.0 设备仿真完全指南拓扑建模、内存交织与命令行实战QEMU 中的 CXL 2.0 设备仿真完全指南拓扑建模、内存交织与命令行实战 本篇技术指南基于 QEMU 官方文档 docs/system/devices/虚拟化硬件仿真MuJoCo柔体仿真弹性变形的物理建模技术MuJoCo柔体仿真弹性变形的物理建模技术 引言柔体仿真的挑战与机遇 在传统的刚体物理仿真中物体被视为不可变形的实体这在许多应用场景中已经足够。然而当物理引擎机器人机器学习图形学创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考