shortdeck1.3:ui and fix

This commit is contained in:
2025-10-11 18:24:24 +08:00
parent 4763f9a630
commit 8f30e75e1a
69 changed files with 2753 additions and 97 deletions

View File

@@ -0,0 +1,19 @@
from .simulation import Simulation
from .agent import Agent, HumanAgent
from .game_stage import GameStage, BlindConfig
from .hand_evaluator import HandEvaluator
from .hand_ranking import HandRanking
from .card import Card
from .side_pot import SidePot
__all__ = [
"Simulation",
"Agent",
"HumanAgent",
"GameStage",
"BlindConfig",
"HandEvaluator",
"HandRanking",
"Card",
"SidePot"
]

Binary file not shown.

View File

@@ -1,4 +1,3 @@
"""ShortDeck card model (6-A, 36 cards)."""
from __future__ import annotations
from enum import IntEnum

View File

@@ -73,6 +73,7 @@ class HandRanking:
def get_strength(self) -> int:
# 返回牌力 还是 牌型+点数
# 基础强度 = 牌型强度 * 1000000
# todo
strength = self.hand_type.strength * 1000000
for i, rank in enumerate(self.key_ranks):

View File

@@ -99,6 +99,5 @@ class SidePotManager:
return winnings
def reset(self):
"""重置边池管理器"""
self.pots.clear()
self.player_total_investment.clear()

View File

@@ -42,6 +42,9 @@ class Simulation:
self.side_pot_manager = SidePotManager()
self.stacks: List[int] = [1000] * len(agents) # 默认筹码
# 用于结算
self.hand_completed = False
self.new_round()
def new_round(self):
@@ -54,6 +57,7 @@ class Simulation:
self.current_stage = GameStage.PREFLOP
self.player_states = [PlayerState.ACTIVE] * len(self.agents)
self.betting_round_complete = False
self.hand_completed = False # 重置完成标志
# 重置下注状态
self.pot = [0] * len(self.agents)
@@ -141,25 +145,19 @@ class Simulation:
return max(0, max_pot - self.pot[pid])
def get_min_raise_amount(self, pid) -> int:
"""最小加注金额"""
call_amount = self.get_call_amount(pid)
min_raise = call_amount + max(self.last_raise_amount, self.blind_config.big_blind)
return min_raise
def get_max_bet_amount(self, pid) -> int:
"""最大下注金额(剩余筹码)"""
if pid >= len(self.stacks):
return 0
return self.stacks[pid]
def is_all_in_amount(self, pid, amount) -> bool:
"""检查是否为allin"""
return amount >= self.stacks[pid]
def validate_bet_amount(self, pid, action, amount) -> tuple[bool, str, int]:
"""
验证下注金额合法性
"""
if pid >= len(self.stacks):
return False, "无效玩家", amount
@@ -178,12 +176,16 @@ class Simulation:
if call_amount == 0:
return False, "不需要跟注", 0
# All-in call
if call_amount >= available_stack:
return True, "", available_stack
return True, "", call_amount
elif action == "allin":
if available_stack <= 0:
return False, "没有筹码进行 all-in", 0
return True, "", available_stack
elif action in ["bet", "raise"]:
if amount <= 0:
return False, "无效下注金额", amount
@@ -207,6 +209,9 @@ class Simulation:
return False, "无效行为", amount
def get_available_actions(self, pid: int) -> dict:
if self.current_stage in [GameStage.FINISHED, GameStage.SHOWDOWN]:
return {"can_act": False, "reason": "游戏已结束" if self.current_stage == GameStage.FINISHED else "摊牌阶段"}
if pid != self.current_turn:
return {"can_act": False, "reason": "不是你的回合"}
@@ -214,7 +219,7 @@ class Simulation:
return {"can_act": False, "reason": "无效玩家"}
state = self.player_states[pid]
if state in [PlayerState.FOLDED, PlayerState.ALLIN, PlayerState.OUT]:
if state in [PlayerState.FOLDED, PlayerState.ALLIN]:
return {"can_act": False, "reason": f"Player state: {state}"}
call_amount = self.get_call_amount(pid)
@@ -225,7 +230,7 @@ class Simulation:
"can_fold": True,
"can_check": call_amount == 0,
"can_call": call_amount > 0 and call_amount < available_stack,
"can_bet": max(self.pot) == 0 and available_stack > 0,
"can_bet": call_amount == 0 and available_stack > 0,
"can_raise": call_amount > 0 and available_stack > call_amount,
"can_allin": available_stack > 0,
"call_amount": call_amount,
@@ -241,27 +246,33 @@ class Simulation:
"""
检查当前下注轮是否完成
"""
active_players = [i for i, state in enumerate(self.player_states)
if state in (PlayerState.ACTIVE, PlayerState.CALLED)]
# 首先检查是否只剩一个未弃牌的玩家
non_folded_players = [i for i, state in enumerate(self.player_states)
if state != PlayerState.FOLDED]
if len(active_players) <= 1:
if len(non_folded_players) <= 1:
return True
active_or_allin_players = [i for i, state in enumerate(self.player_states)
if state in (PlayerState.ACTIVE, PlayerState.CALLED, PlayerState.ALLIN)]
all_allin_or_folded = all(state in (PlayerState.ALLIN, PlayerState.FOLDED)
for state in self.player_states)
if all_allin_or_folded:
return True
# 检查所有active玩家是否都已投入相同金额且所有人都已经行动过
max_pot = self.get_current_max_bet()
# 统计还需要行动的玩家
players_need_action = []
for i in active_players:
for i in active_or_allin_players:
# allin
if self.player_states[i] == PlayerState.ALLIN:
continue
# 投入金额不足的玩家需要行动
continue
if self.pot[i] < max_pot:
players_need_action.append(i)
# Active状态的玩家如果还没有在本轮行动过也需要行动
elif self.player_states[i] == PlayerState.ACTIVE:
# 在翻前,大盲玩家即使投入了足够金额,也有权行动一次
if (self.current_stage == GameStage.PREFLOP and
i == self.blind_config.get_bb_position(len(self.agents), self.dealer_position)):
# 检查大盲是否已经行动过(除了盲注)
@@ -289,6 +300,12 @@ class Simulation:
self.complete_hand()
return
if self.current_stage == GameStage.SHOWDOWN:
self.current_stage = GameStage.FINISHED
self.complete_hand()
return
# 重置下注轮状态
self.betting_round_complete = False
@@ -306,6 +323,7 @@ class Simulation:
def get_next_active_player(self, start_pos) -> Optional[int]:
for i in range(len(self.agents)):
pos = (start_pos + i) % len(self.agents)
# 只有ACTIVE状态的玩家可以行动ALLIN和FOLDED的玩家不能行动
if self.player_states[pos] == PlayerState.ACTIVE:
return pos
return None
@@ -313,7 +331,7 @@ class Simulation:
def get_side_pots(self) -> List:
active_players = [
i for i, state in enumerate(self.player_states)
if state not in [PlayerState.FOLDED, PlayerState.OUT]
if state not in [PlayerState.FOLDED]
]
return self.side_pot_manager.create_side_pots(active_players)
@@ -375,6 +393,29 @@ class Simulation:
raise ValueError("跟注金额>0, 无法过牌,需要跟注或弃牌")
self.player_states[pid] = PlayerState.CALLED
elif action == "allin":
# all-in
actual_amount = self.stacks[pid]
if actual_amount <= 0:
raise ValueError("没有可用筹码进行 all-in")
self.player_states[pid] = PlayerState.ALLIN
self.pot[pid] += actual_amount
self.stacks[pid] = 0
self.total_pot += actual_amount
self.side_pot_manager.add_investment(pid, actual_amount)
# 更新最后加注金额(如果 all-in 金额超过跟注金额)
call_amount = self.get_call_amount(pid)
raise_amount = actual_amount - call_amount
if raise_amount > 0:
self.last_raise_amount = raise_amount
self.min_raise = raise_amount
for i, state in enumerate(self.player_states):
if i != pid and state == PlayerState.CALLED:
self.player_states[i] = PlayerState.ACTIVE
elif action in ("bet", "raise"):
if amount is None:
raise ValueError(f"{action} 需要指定金额")
@@ -446,7 +487,6 @@ class Simulation:
def evaluate_player_hand(self, pid: int) -> Optional[HandRanking]:
"""评估玩家手牌强度"""
if pid >= len(self.agents):
return None
@@ -454,22 +494,17 @@ class Simulation:
return None
try:
# 获取玩家手牌
player_cards = self.player_cards(pid)
# 获取公共牌
board_cards = self.board_cards(self.current_stage.value)
# 至少需要5张牌才能评估
all_cards = player_cards + board_cards
if len(all_cards) < 5:
return None
# 如果正好5张牌直接评估
if len(all_cards) == 5:
return HandEvaluator.evaluate5Cards(all_cards)
# 如果超过5张牌找最佳组合
return HandEvaluator.evaluateHand(all_cards)
except Exception as e:
@@ -478,7 +513,7 @@ class Simulation:
def get_active_players(self) -> List[int]:
return [i for i, state in enumerate(self.player_states)
if state not in [PlayerState.FOLDED, PlayerState.OUT]]
if state not in [PlayerState.FOLDED]]
def is_hand_complete(self) -> bool:
active_players = self.get_active_players()
@@ -522,9 +557,8 @@ class Simulation:
winner_id = list(winners.keys())[0]
return {winner_id: self.total_pot}
# 多人摊牌,使用边池分配
# 多人摊牌
if len(winners) > 1:
#转换HandRanking为数值强度
hand_strengths = {}
for pid, ranking in winners.items():
if ranking is not None:
@@ -539,16 +573,31 @@ class Simulation:
def complete_hand(self) -> Dict:
if not self.is_hand_complete():
return {"complete": False, "message": "牌局未结束"}
if self.hand_completed:
return {
"complete": True,
"winners": [],
"winnings": {},
"final_stacks": self.stacks.copy(),
"showdown_hands": {},
"message": "手牌已完成"
}
winners = self.determine_winners()
winnings = self.distribute_pot()
# 更新筹码
for pid, amount in winnings.items():
if pid < len(self.stacks):
self.stacks[pid] += amount
self.total_pot = 0
self.pot = [0] * len(self.agents)
self.side_pot_manager.reset()
self.current_stage = GameStage.FINISHED
self.hand_completed = True
result = {
"complete": True,
@@ -557,14 +606,22 @@ class Simulation:
"final_stacks": self.stacks.copy(),
"showdown_hands": {}
}
active_players = [i for i, state in enumerate(self.player_states)
if state != PlayerState.FOLDED]
# 摊牌信息
for pid, ranking in winners.items():
if ranking is not None:
for pid in active_players:
player_hand = self.player_cards(pid)
if len(player_hand) >= 2:
evaluator = HandEvaluator()
board_cards = self.board_cards()
ranking = evaluator.evaluate(player_hand, board_cards)
result["showdown_hands"][pid] = {
"cards": [str(card) for card in self.player_cards(pid)],
"hand_type": ranking.hand_type.type_name,
"description": str(ranking)
"cards": [str(card) for card in player_hand],
"hand_type": ranking.hand_type.type_name if ranking else "无牌型",
"description": str(ranking) if ranking else "无效手牌",
"is_winner": pid in winners
}
return result