shortdeck1.2

This commit is contained in:
2025-10-09 15:27:17 +08:00
parent 7071eaa12b
commit 4763f9a630
19 changed files with 615 additions and 250 deletions

Binary file not shown.

View File

@@ -31,6 +31,14 @@ class Rank(IntEnum):
return str(self.value) return str(self.value)
return {10: "T", 11: "J", 12: "Q", 13: "K", 14: "A"}[self.value] return {10: "T", 11: "J", 12: "Q", 13: "K", 14: "A"}[self.value]
@property
def numeric_value(self):
return self.value
@property
def symbol(self):
return str(self)
class Card: class Card:
def __init__(self, rank: Rank, suit: Suit): def __init__(self, rank: Rank, suit: Suit):

View File

@@ -41,8 +41,8 @@ class PlayerState(Enum):
ALLIN = "allin" # 全下 ALLIN = "allin" # 全下
CALLED = "called" # 已跟注 CALLED = "called" # 已跟注
RAISE = "raised" # 已加注 RAISE = "raised" # 已加注
WAITING = "waiting" # 等待其他玩家 # WAITING = "waiting" # 等待其他玩家
OUT = "out" # 出局 # OUT = "out" # 出局
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@@ -74,7 +74,7 @@ class BlindConfig:
return (dealer_position + 1) % 2 # heads-up: 大盲 = 庄位 return (dealer_position + 1) % 2 # heads-up: 大盲 = 庄位
return (dealer_position + 2) % num_players # 多人: 大盲为庄位后两位 return (dealer_position + 2) % num_players # 多人: 大盲为庄位后两位
def get_first_to_act(self, stage: GameStage, num_players) -> int: def get_first_to_act(self, stage: GameStage, num_players, dealer_position=0) -> int:
""" """
获取首个行动玩家位置 获取首个行动玩家位置
区分preflop和postflop 区分preflop和postflop
@@ -83,9 +83,9 @@ class BlindConfig:
""" """
if stage == GameStage.PREFLOP: if stage == GameStage.PREFLOP:
if num_players == 2: if num_players == 2:
return self.get_sb_position(num_players) # heads-up: 小盲先行动 return self.get_sb_position(num_players, dealer_position) # heads-up: 小盲先行动
else: # preflop: 大盲后第一位 else: # preflop: 大盲后第一位
return (self.get_bb_position(num_players) + 1) % num_players return (self.get_bb_position(num_players, dealer_position) + 1) % num_players
else: else:
# flop/river/turn: 小盲位先行动 # flop/river/turn: 小盲位先行动
return self.get_sb_position(num_players) return self.get_sb_position(num_players, dealer_position)

View File

@@ -91,7 +91,6 @@ class HandEvaluator:
@staticmethod @staticmethod
def _isStraight(ranks: List[Rank]) -> Tuple[bool, Rank]: def _isStraight(ranks: List[Rank]) -> Tuple[bool, Rank]:
values = sorted([rank.numeric_value for rank in ranks], reverse=True) values = sorted([rank.numeric_value for rank in ranks], reverse=True)
is_regular_straight = True is_regular_straight = True
@@ -101,7 +100,6 @@ class HandEvaluator:
break break
if is_regular_straight: if is_regular_straight:
# 返回最高牌
highest_rank = None highest_rank = None
for rank in ranks: for rank in ranks:
if rank.numeric_value == values[0]: if rank.numeric_value == values[0]:
@@ -109,7 +107,6 @@ class HandEvaluator:
break break
return True, highest_rank return True, highest_rank
if values == [14, 5, 4, 3, 2]: # A, 5, 4, 3, 2 if values == {14, 9, 8, 7, 6}: # A, T, 9, 8, 7, 6
return True, Rank.FIVE return True, Rank.R9
return False, None return False, None

View File

@@ -4,13 +4,14 @@ from .card import Card, Rank
class HandType(Enum): class HandType(Enum):
# 短牌规则:同花 > 葫芦
HIGH_CARD = (1, "High Card") HIGH_CARD = (1, "High Card")
ONE_PAIR = (2, "Pair") ONE_PAIR = (2, "Pair")
TWO_PAIR = (3, "Two Pair") TWO_PAIR = (3, "Two Pair")
THREE_OF_A_KIND = (4, "Three of a Kind") THREE_OF_A_KIND = (4, "Three of a Kind")
STRAIGHT = (5, "Straight") STRAIGHT = (5, "Straight")
FLUSH = (6, "Flush") FULL_HOUSE = (6, "Full House")
FULL_HOUSE = (7, "Full House") FLUSH = (7, "Flush")
FOUR_OF_A_KIND = (8, "Four of a Kind") FOUR_OF_A_KIND = (8, "Four of a Kind")
STRAIGHT_FLUSH = (9, "Straight Flush") STRAIGHT_FLUSH = (9, "Straight Flush")
ROYAL_FLUSH = (10, "Royal Flush") ROYAL_FLUSH = (10, "Royal Flush")
@@ -55,3 +56,26 @@ class HandRanking:
return f"Pair({self.key_ranks[0].symbol})" return f"Pair({self.key_ranks[0].symbol})"
else: else:
return f"High Card({self.key_ranks[0].symbol})" return f"High Card({self.key_ranks[0].symbol})"
def __lt__(self, other):
"""比较牌力,用于排序"""
if not isinstance(other, HandRanking):
return NotImplemented
if self.hand_type.strength != other.hand_type.strength:
return self.hand_type.strength < other.hand_type.strength
for my_rank, other_rank in zip(self.key_ranks, other.key_ranks):
if my_rank.numeric_value != other_rank.numeric_value:
return my_rank.numeric_value < other_rank.numeric_value
return False
def get_strength(self) -> int:
# 返回牌力 还是 牌型+点数
# 基础强度 = 牌型强度 * 1000000
strength = self.hand_type.strength * 1000000
for i, rank in enumerate(self.key_ranks):
strength += rank.numeric_value * (100 ** (4 - i))
return strength

104
shortdeck_arena/side_pot.py Normal file
View File

@@ -0,0 +1,104 @@
from __future__ import annotations
from typing import List, Set, Dict
from dataclasses import dataclass
@dataclass
class SidePot:
amount: int
eligible_players: Set[int]
is_main_pot: bool = False
def __post_init__(self):
if not self.eligible_players:
self.eligible_players = set()
class SidePotManager:
def __init__(self):
self.pots: List[SidePot] = []
self.player_total_investment: Dict[int, int] = {}
def add_investment(self, player_id: int, amount: int):
if player_id not in self.player_total_investment:
self.player_total_investment[player_id] = 0
self.player_total_investment[player_id] += amount
def create_side_pots(self, active_players: List[int]) -> List[SidePot]:
if not self.player_total_investment:
return []
# 按投入金额排序
sorted_investments = sorted(
[(pid, amount) for pid, amount in self.player_total_investment.items()
if pid in active_players and amount > 0],
key=lambda x: x[1]
)
if not sorted_investments:
return []
pots = []
prev_level = 0
for i, (player_id, investment) in enumerate(sorted_investments):
if investment > prev_level:
# 计算本层级的贡献
level_contribution = investment - prev_level
# 找到有资格竞争本层级的玩家
eligible_players = {
pid for pid, inv in sorted_investments[i:]
if inv >= investment
}
pot_amount = level_contribution * len(eligible_players)
side_pot = SidePot(
amount=pot_amount,
eligible_players=eligible_players,
is_main_pot=(len(pots) == 0)
)
pots.append(side_pot)
prev_level = investment
self.pots = pots
return pots
def get_total_pot(self) -> int:
return sum(pot.amount for pot in self.pots)
def distribute_winnings(self, hand_rankings: Dict[int, int]) -> Dict[int, int]:
winnings = {}
for pot in self.pots:
eligible = [pid for pid in pot.eligible_players if pid in hand_rankings]
if not eligible:
continue
# 找到最强手牌
best_strength = max(hand_rankings[pid] for pid in eligible)
winners = [pid for pid in eligible
if hand_rankings[pid] == best_strength]
# 平分底池
winnings_per_winner = pot.amount // len(winners)
remainder = pot.amount % len(winners)
for i, winner in enumerate(winners):
if winner not in winnings:
winnings[winner] = 0
winnings[winner] += winnings_per_winner
# 余数给前面的获胜者
if i < remainder:
winnings[winner] += 1
return winnings
def reset(self):
"""重置边池管理器"""
self.pots.clear()
self.player_total_investment.clear()

View File

@@ -10,6 +10,9 @@ if TYPE_CHECKING:
from .agent import Agent from .agent import Agent
from .card import Card from .card import Card
from .game_stage import GameStage, PlayerState, BlindConfig from .game_stage import GameStage, PlayerState, BlindConfig
from .side_pot import SidePotManager
from .hand_evaluator import HandEvaluator
from .hand_ranking import HandRanking
class Simulation: class Simulation:
@@ -35,6 +38,10 @@ class Simulation:
self.min_raise = self.blind_config.big_blind self.min_raise = self.blind_config.big_blind
self.dealer_position = -1 self.dealer_position = -1
# 边池管理和筹码
self.side_pot_manager = SidePotManager()
self.stacks: List[int] = [1000] * len(agents) # 默认筹码
self.new_round() self.new_round()
def new_round(self): def new_round(self):
@@ -54,6 +61,9 @@ class Simulation:
self.last_raise_amount = 0 self.last_raise_amount = 0
self.min_raise = self.blind_config.big_blind self.min_raise = self.blind_config.big_blind
# 重置边池管理器
self.side_pot_manager.reset()
# 设置盲注 # 设置盲注
self._setup_blinds() self._setup_blinds()
@@ -61,7 +71,6 @@ class Simulation:
self.dealer_position = random.choice(range(len(self.agents))) self.dealer_position = random.choice(range(len(self.agents)))
def _setup_blinds(self): def _setup_blinds(self):
"""设置盲注"""
num_players = len(self.agents) num_players = len(self.agents)
# 至少需要2个玩家才能设置盲注 # 至少需要2个玩家才能设置盲注
@@ -78,25 +87,31 @@ class Simulation:
return return
# 扣除小盲 # 扣除小盲
self.pot[sb_pos] = self.blind_config.small_blind sb_amount = min(self.blind_config.small_blind, self.stacks[sb_pos])
self.total_pot += self.blind_config.small_blind self.pot[sb_pos] = sb_amount
self.stacks[sb_pos] -= sb_amount
self.total_pot += sb_amount
self.side_pot_manager.add_investment(sb_pos, sb_amount)
self.history.append({ self.history.append({
"pid": sb_pos, "pid": sb_pos,
"action": "small_blind", "action": "small_blind",
"amount": self.blind_config.small_blind "amount": sb_amount
}) })
# 扣除大盲 # 扣除大盲
self.pot[bb_pos] = self.blind_config.big_blind bb_amount = min(self.blind_config.big_blind, self.stacks[bb_pos])
self.total_pot += self.blind_config.big_blind self.pot[bb_pos] = bb_amount
self.stacks[bb_pos] -= bb_amount
self.total_pot += bb_amount
self.side_pot_manager.add_investment(bb_pos, bb_amount)
self.history.append({ self.history.append({
"pid": bb_pos, "pid": bb_pos,
"action": "big_blind", "action": "big_blind",
"amount": self.blind_config.big_blind "amount": bb_amount
}) })
# 首个行动玩家 # 首个行动玩家
self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players) self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players, self.dealer_position)
self.last_raise_amount = self.blind_config.big_blind self.last_raise_amount = self.blind_config.big_blind
def player_cards(self, pid) -> List[Card]: def player_cards(self, pid) -> List[Card]:
@@ -114,20 +129,114 @@ class Simulation:
return [] return []
def get_current_max_bet(self) -> int: def get_current_max_bet(self) -> int:
"""
获取当前最高下注额, 用于计算跟注金额
"""
return max(self.pot) if self.pot else 0 return max(self.pot) if self.pot else 0
def get_call_amount(self, pid) -> int: def get_call_amount(self, pid) -> int:
""" """
计算玩家跟注所需金额 跟注金额
""" """
if pid >= len(self.pot): if pid >= len(self.pot):
return 0 return 0
max_pot = self.get_current_max_bet() max_pot = self.get_current_max_bet()
return max(0, max_pot - self.pot[pid]) 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
available_stack = self.stacks[pid]
call_amount = self.get_call_amount(pid)
if action == "fold":
return True, "", 0
elif action == "check":
if call_amount > 0:
return False, "不能过牌,需跟注或弃牌", 0
return True, "", 0
elif action == "call":
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 in ["bet", "raise"]:
if amount <= 0:
return False, "无效下注金额", amount
# allin
if amount >= available_stack:
return True, "", available_stack
if action == "raise":
min_raise = self.get_min_raise_amount(pid)
if amount < min_raise:
return False, f"最小加注金额为 {min_raise}", amount
if action == "bet" and max(self.pot) == 0:
if amount < self.blind_config.big_blind:
return False, f"最小下注金额为 {self.blind_config.big_blind}", amount
return True, "", amount
return False, "无效行为", amount
def get_available_actions(self, pid: int) -> dict:
if pid != self.current_turn:
return {"can_act": False, "reason": "不是你的回合"}
if pid >= len(self.player_states):
return {"can_act": False, "reason": "无效玩家"}
state = self.player_states[pid]
if state in [PlayerState.FOLDED, PlayerState.ALLIN, PlayerState.OUT]:
return {"can_act": False, "reason": f"Player state: {state}"}
call_amount = self.get_call_amount(pid)
available_stack = self.stacks[pid]
actions = {
"can_act": True,
"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_raise": call_amount > 0 and available_stack > call_amount,
"can_allin": available_stack > 0,
"call_amount": call_amount,
"min_bet": self.blind_config.big_blind if max(self.pot) == 0 else 0,
"min_raise": self.get_min_raise_amount(pid) if call_amount > 0 else 0,
"max_bet": available_stack,
"stack": available_stack
}
return actions
def is_betting_round_complete(self) -> bool: def is_betting_round_complete(self) -> bool:
""" """
检查当前下注轮是否完成 检查当前下注轮是否完成
@@ -138,12 +247,29 @@ class Simulation:
if len(active_players) <= 1: if len(active_players) <= 1:
return True return True
# 检查所有active玩家是否都已投入相同金额 # 检查所有active玩家是否都已投入相同金额,且所有人都已经行动过
max_pot = self.get_current_max_bet() max_pot = self.get_current_max_bet()
for i in active_players: # allin玩家不要求跟注
if self.pot[i] < max_pot and self.player_states[i] != PlayerState.ALLIN: # 统计还需要行动的玩家
return False players_need_action = []
return True for i in active_players:
# allin
if self.player_states[i] == PlayerState.ALLIN:
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)):
# 检查大盲是否已经行动过(除了盲注)
bb_actions = [h for h in self.history if h.get('pid') == i and h.get('action') not in ['big_blind']]
if not bb_actions:
players_need_action.append(i)
return len(players_need_action) == 0
def advance_to_next_street(self): def advance_to_next_street(self):
if self.current_stage == GameStage.FINISHED: if self.current_stage == GameStage.FINISHED:
@@ -152,23 +278,28 @@ class Simulation:
next_stage = GameStage.get_next_stage(self.current_stage) next_stage = GameStage.get_next_stage(self.current_stage)
if next_stage is None: if next_stage is None:
self.current_stage = GameStage.FINISHED self.current_stage = GameStage.FINISHED
self.complete_hand()
return return
self.current_stage = next_stage self.current_stage = next_stage
active_players = self.get_active_players()
if len(active_players) <= 1:
self.current_stage = GameStage.FINISHED
self.complete_hand()
return
# 重置下注轮状态 # 重置下注轮状态
self.betting_round_complete = False self.betting_round_complete = False
#### 重置pot为累计投入 (不清零,为了计算边池)
# 重置行动状态 # 重置行动状态
for i, state in enumerate(self.player_states): for i, state in enumerate(self.player_states):
if state == PlayerState.CALLED: if state == PlayerState.CALLED:
self.player_states[i] = PlayerState.ACTIVE self.player_states[i] = PlayerState.ACTIVE
# 设置首个行动玩家 # 首个行动玩家
num_players = len(self.agents) num_players = len(self.agents)
self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players) self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players, self.dealer_position)
self.last_raise_amount = 0 self.last_raise_amount = 0
self.min_raise = self.blind_config.big_blind self.min_raise = self.blind_config.big_blind
@@ -179,21 +310,25 @@ class Simulation:
return pos return pos
return None return None
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]
]
return self.side_pot_manager.create_side_pots(active_players)
def node_info(self) -> Dict: def node_info(self) -> Dict:
if self.current_turn >= len(self.pot): if self.current_turn >= len(self.pot):
return {"bet_min": self.min_raise, "bet_max": 0, "call_amount": 0} return {"bet_min": self.min_raise, "bet_max": 0, "call_amount": 0}
# 跟注
call_amount = self.get_call_amount(self.current_turn) actions = self.get_available_actions(self.current_turn)
return { return {
"bet_min": max(self.min_raise, call_amount + self.min_raise), "bet_min": actions.get("min_bet", self.min_raise),
"bet_max": 100, ########## 需要从ArenaGame获取实际大小 "bet_max": actions.get("max_bet", 100),
"call_amount": call_amount "call_amount": actions.get("call_amount", 0)
} }
def apply_action(self, pid, action, amount): def apply_action(self, pid, action, amount):
"""
应用玩家动作
"""
if pid != self.current_turn: if pid != self.current_turn:
raise ValueError(f"不是玩家 {pid} 的回合") raise ValueError(f"不是玩家 {pid} 的回合")
@@ -202,6 +337,14 @@ class Simulation:
action = action.lower() action = action.lower()
# 验证动作合法性
is_valid, error_msg, adjusted_amount = self.validate_bet_amount(pid, action, amount or 0)
if not is_valid:
raise ValueError(error_msg)
# 使用调整后的金额
amount = adjusted_amount
self.history.append({"pid": pid, "action": action, "amount": amount}) self.history.append({"pid": pid, "action": action, "amount": amount})
if action == "fold": if action == "fold":
@@ -212,10 +355,19 @@ class Simulation:
if call_amount == 0: if call_amount == 0:
# check # check
self.history[-1]["action"] = "check" self.history[-1]["action"] = "check"
else:
self.pot[pid] += call_amount
self.total_pot += call_amount
self.player_states[pid] = PlayerState.CALLED self.player_states[pid] = PlayerState.CALLED
else:
# 检查是否all-in
actual_amount = min(call_amount, self.stacks[pid])
if actual_amount >= self.stacks[pid]:
self.player_states[pid] = PlayerState.ALLIN
else:
self.player_states[pid] = PlayerState.CALLED
self.pot[pid] += actual_amount
self.stacks[pid] -= actual_amount
self.total_pot += actual_amount
self.side_pot_manager.add_investment(pid, actual_amount)
elif action == "check": elif action == "check":
call_amount = self.get_call_amount(pid) call_amount = self.get_call_amount(pid)
@@ -226,19 +378,25 @@ class Simulation:
elif action in ("bet", "raise"): elif action in ("bet", "raise"):
if amount is None: if amount is None:
raise ValueError(f"{action} 需要指定金额") raise ValueError(f"{action} 需要指定金额")
# 加注需要补齐跟注金额
call_amount = self.get_call_amount(pid)
total_amount = call_amount + (amount or 0)
self.pot[pid] += total_amount # 检查是否all-in
self.total_pot += total_amount actual_amount = min(amount, self.stacks[pid])
if actual_amount >= self.stacks[pid]:
if amount and amount > 0: self.player_states[pid] = PlayerState.ALLIN
self.last_raise_amount = amount else:
self.min_raise = amount
self.player_states[pid] = PlayerState.CALLED self.player_states[pid] = PlayerState.CALLED
# 其他跟注的玩家 self.pot[pid] += actual_amount
self.stacks[pid] -= actual_amount
self.total_pot += actual_amount
self.side_pot_manager.add_investment(pid, actual_amount)
# 更新最后加注金额
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): for i, state in enumerate(self.player_states):
if i != pid and state == PlayerState.CALLED: if i != pid and state == PlayerState.CALLED:
self.player_states[i] = PlayerState.ACTIVE self.player_states[i] = PlayerState.ACTIVE
@@ -285,3 +443,128 @@ class Simulation:
f.write(json.dumps(self.to_save_data())) f.write(json.dumps(self.to_save_data()))
f.write("\n") f.write("\n")
self.saved = True self.saved = True
def evaluate_player_hand(self, pid: int) -> Optional[HandRanking]:
"""评估玩家手牌强度"""
if pid >= len(self.agents):
return None
if self.player_states[pid] == PlayerState.FOLDED:
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:
print(f"评估玩家 {pid} 手牌时出错: {e}")
return None
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]]
def is_hand_complete(self) -> bool:
active_players = self.get_active_players()
if len(active_players) <= 1:
return True
# 到达河牌且所有下注完成
if (self.current_stage == GameStage.FINISHED or
(self.current_stage == GameStage.RIVER and self.betting_round_complete)):
return True
return False
def determine_winners(self) -> Dict[int, HandRanking]:
active_players = self.get_active_players()
if not active_players:
return {}
if len(active_players) == 1:
return {active_players[0]: None} # 不需要摊牌
# 多人摊牌
hand_rankings = {}
for pid in active_players:
ranking = self.evaluate_player_hand(pid)
if ranking is not None:
hand_rankings[pid] = ranking
return hand_rankings
def distribute_pot(self) -> Dict[int, int]:
winners = self.determine_winners()
if not winners:
return {}
# 只有一人获胜(其他人弃牌)
if len(winners) == 1 and list(winners.values())[0] is None:
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:
hand_strengths[pid] = ranking.get_strength()
else:
hand_strengths[pid] = 0 # 弃牌玩家
return self.side_pot_manager.distribute_winnings(hand_strengths)
return {}
def complete_hand(self) -> Dict:
if not self.is_hand_complete():
return {"complete": False, "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.current_stage = GameStage.FINISHED
result = {
"complete": True,
"winners": list(winners.keys()),
"winnings": winnings,
"final_stacks": self.stacks.copy(),
"showdown_hands": {}
}
# 摊牌信息
for pid, ranking in winners.items():
if ranking is not None:
result["showdown_hands"][pid] = {
"cards": [str(card) for card in self.player_cards(pid)],
"hand_type": ranking.hand_type.type_name,
"description": str(ranking)
}
return result

View File

@@ -1,12 +1,8 @@
from __future__ import annotations
from typing import List, Dict, Optional
from pathlib import Path
from shortdeck_arena.simulation import Simulation
from shortdeck_arena.agent import HumanAgent
from shortdeck_arena.game_stage import BlindConfig, GameStage, PlayerState
import uuid import uuid
from typing import List, Optional, Dict
from shortdeck_arena.simulation import Simulation
from shortdeck_arena.agent import Agent, HumanAgent
from shortdeck_arena.game_stage import BlindConfig
class ArenaGame: class ArenaGame:
@@ -18,163 +14,153 @@ class ArenaGame:
self.max_players = max_players self.max_players = max_players
self.sim: Optional[Simulation] = None self.sim: Optional[Simulation] = None
# 筹码管理
self.stacks: List[int] = []
# 盲注配置
self.blind_config = BlindConfig(small_blind, big_blind, ante=0) self.blind_config = BlindConfig(small_blind, big_blind, ante=0)
# 游戏标识
self.game_id = str(uuid.uuid4()) self.game_id = str(uuid.uuid4())
def join_game(self, name: str) -> int: def join_game(self, name) -> int:
if len(self.player_names) >= self.max_players: if len(self.agents) >= self.max_players:
raise ValueError("table full") raise ValueError("Game is full")
pid = len(self.player_names) player_id = len(self.agents)
self.player_names.append(name) agent = HumanAgent(player_id)
agent = HumanAgent(pid)
self.agents.append(agent) self.agents.append(agent)
self.stacks.append(self.starting_stack) self.player_names.append(name)
self.sim = Simulation(self.agents, self.blind_config) if len(self.agents) == 1:
return pid self.sim = Simulation(self.agents, blind_config=self.blind_config)
# 筹码默认1000
self.sim.stacks = [self.starting_stack] * len(self.agents)
elif self.sim:
self.sim.agents = self.agents
self.sim.player_states.append(self.sim.player_states[0].__class__.ACTIVE)
self.sim.pot.append(0)
self.sim.stacks.append(self.starting_stack)
# 当有至少2个玩家时触发新一轮
if len(self.agents) >= 2 and self.sim:
self.sim.stacks = [self.starting_stack] * len(self.agents)
self.sim.new_round()
return player_id
def apply_action(self, player_id, action, amount: Optional[int] = None) -> dict:
if not self.sim:
return {"success": False, "message": "游戏未开始"}
try:
self.sim.apply_action(player_id, action, amount)
self.sim.dump_data()
return {"success": True, "message": f"Applied {action}"}
except Exception as e:
return {"success": False, "message": str(e)}
def info(self, player_id: Optional[int] = None) -> Dict: def info(self, player_id: Optional[int] = None) -> Dict:
"""获取游戏状态信息"""
if not self.sim: if not self.sim:
return { return {"error": "游戏未初始化"}
info_data = {
"game_id": self.game_id, "game_id": self.game_id,
"players": self.player_names, "players": self.player_names,
"stacks": [], "dealer_index": self.sim.dealer_position,
"dealer_index": 0,
"current_turn": 0,
"pot": 0,
"stage": "preflop",
"actions": {},
"player_cards": [],
"board_cards": [],
}
# 更新栈大小 (扣除已投入底池的金额)
updated_stacks = []
for i, base_stack in enumerate(self.stacks):
pot_contribution = self.sim.pot[i] if i < len(self.sim.pot) else 0
updated_stacks.append(max(0, base_stack - pot_contribution))
for i in range(len(self.stacks)):
if i < len(updated_stacks):
self.stacks[i] = updated_stacks[i]
player_cards = []
board_cards = []
# 获取玩家手牌
try:
if player_id is not None and 0 <= player_id < len(self.agents):
player_cards = [str(c) for c in self.sim.player_cards(player_id)]
except Exception:
player_cards = []
# 获取公共牌 (根据当前阶段)
try:
current_stage = self.sim.current_stage.value
board_cards = [str(c) for c in self.sim.board_cards(current_stage)]
except Exception:
board_cards = []
# 获取可用动作信息
actions = {}
if (player_id is not None and player_id == self.sim.current_turn and
self.sim.current_stage != GameStage.FINISHED):
call_amount = self.sim.get_call_amount(player_id)
available_stack = updated_stacks[player_id] if player_id < len(updated_stacks) else 0
# 更新node_info以包含实际栈信息
node_info = self.sim.node_info()
node_info["bet_max"] = available_stack
actions = {
"call_amount": call_amount,
"bet_min": node_info["bet_min"],
"bet_max": node_info["bet_max"],
"can_check": call_amount == 0,
"can_fold": True,
"can_call": call_amount > 0 and call_amount <= available_stack,
"can_bet": available_stack > call_amount,
}
return {
"game_id": self.game_id,
"players": self.player_names,
"stacks": updated_stacks,
"dealer_index": 0, # 简化:固定庄家位置, (优化轮询)
"current_turn": self.sim.current_turn, "current_turn": self.sim.current_turn,
"pot": self.sim.total_pot,
"stage": self.sim.current_stage.value, "stage": self.sim.current_stage.value,
"actions": actions, "total_pot": self.sim.total_pot,
"player_cards": player_cards, "side_pots": [{"amount": pot.amount, "eligible_players": list(pot.eligible_players)}
"board_cards": board_cards, for pot in self.sim.get_side_pots()],
"player_states": [state.value for state in self.sim.player_states],
} }
def apply_action(self, pid: int, action: str, amount: Optional[int] = None): if player_id is not None and 0 <= player_id < len(self.sim.stacks):
if not self.sim: try:
raise ValueError("no game") player_cards = self.sim.player_cards(player_id)
info_data["player_cards"] = [str(card) for card in player_cards]
except Exception:
info_data["player_cards"] = []
# 验证动作合法性 info_data["actions"] = self.sim.get_available_actions(player_id)
if pid != self.sim.current_turn: else:
raise ValueError(f"not your turn, current turn: {self.sim.current_turn}") info_data["player_cards"] = []
info_data["actions"] = {"can_act": False, "reason": "Invalid player"}
if self.sim.current_stage == GameStage.FINISHED:
raise ValueError("game already finished")
# 获取玩家可用筹码
pot_contribution = self.sim.pot[pid] if pid < len(self.sim.pot) else 0
available_stack = self.stacks[pid] - pot_contribution
# 预处理动作和金额
action = action.lower()
if action in ("bet", "raise") and amount is not None:
# 限制下注金额不超过可用筹码
if amount > available_stack:
amount = available_stack
# 如果全下可能需要标记为allin
if amount == available_stack and available_stack > 0:
self.sim.player_states[pid] = PlayerState.ALLIN
elif action == "call":
# call动作的金额验证
call_amount = self.sim.get_call_amount(pid)
if call_amount > available_stack:
# 不够跟注自动allin
amount = available_stack
if available_stack > 0:
self.sim.player_states[pid] = PlayerState.ALLIN
try: try:
self.sim.apply_action(pid, action, amount) board_cards = self.sim.board_cards(self.sim.current_stage.value)
except ValueError as e: info_data["board_cards"] = [str(card) for card in board_cards]
raise ValueError(f"invalid action: {e}") except Exception:
info_data["board_cards"] = []
self.sim.dump_data(Path.cwd() / "shortdeck_arena_history.jsonl") info_data["stacks"] = self.sim.stacks.copy()
info_data["player_states"] = [state.value for state in self.sim.player_states]
info_data["current_pot"] = self.sim.pot.copy()
return info_data
def get_hand_strength(self, player_id: int) -> Dict:
ranking = self.sim.evaluate_player_hand(player_id)
if ranking is None:
return {"error": "无法评估手牌"}
return {
"hand_type": ranking.hand_type.type_name,
"description": str(ranking),
"strength": ranking.get_strength(),
"cards": [str(card) for card in ranking.cards]
}
def check_hand_complete(self) -> bool:
return self.sim.is_hand_complete() if self.sim else False
def get_winners(self) -> Dict:
@property
def current_turn(self) -> int:
if not self.sim: if not self.sim:
return 0 return {"error": "游戏未初始化"}
return self.sim.current_turn
if not self.sim.is_hand_complete():
return {"error": "手牌未完成"}
return self.sim.complete_hand()
def showdown(self) -> Dict:
if not self.sim:
return {"error": "游戏未初始化"}
winners = self.sim.determine_winners()
showdown_info = {}
for pid, ranking in winners.items():
if ranking is not None:
showdown_info[pid] = {
"cards": [str(card) for card in self.sim.player_cards(pid)],
"hand_type": ranking.hand_type.type_name,
"description": str(ranking),
"strength": ranking.get_strength()
}
else:
showdown_info[pid] = {
"cards": [str(card) for card in self.sim.player_cards(pid)],
"hand_type": "Winner by default",
"description": "Other players folded",
"strength": float('inf')
}
return {
"showdown": showdown_info,
"pot_distribution": self.sim.distribute_pot()
}
@property @property
def pot(self) -> int: def pot(self) -> int:
if not self.sim: return self.sim.total_pot if self.sim else 0
return 0
return self.sim.total_pot @property
def stacks(self) -> List[int]:
return self.sim.stacks if self.sim else []
@property
def current_turn(self) -> int:
return self.sim.current_turn if self.sim else -1
@property @property
def history(self) -> List[Dict]: def history(self) -> List[Dict]:
if not self.sim: return self.sim.history if self.sim else []
return []
return self.sim.history

View File

@@ -12,11 +12,11 @@ class GameStage(Enum):
# # 设置首个行动玩家 # # 设置首个行动玩家
# pass # pass
def advance_street(self): # def advance_street(self):
# 检查下注轮是否结束 # # 检查下注轮是否结束
# 发放公共牌 # # 发放公共牌
# 重置行动顺序 # # 重置行动顺序
pass # pass
# def get_call_amount(self, pid): # def get_call_amount(self, pid):
# # 计算跟注所需金额 # # 计算跟注所需金额
@@ -31,10 +31,10 @@ class GameStage(Enum):
# 标记玩家状态 # 标记玩家状态
pass pass
def evaluate_hand(self, hole_cards, board_cards): # def evaluate_hand(self, hole_cards, board_cards):
# 短牌手牌强度排序 # # 短牌手牌强度排序
# A-6 低顺特殊处理 # # A-6 低顺特殊处理
pass # pass
def determine_winners(self, active_players): def determine_winners(self, active_players):
# 边池分配 # 边池分配

View File

@@ -19,10 +19,9 @@ def test_join_and_actions():
assert g.current_turn == 0 assert g.current_turn == 0
# 测试错误的玩家尝试行动 # 测试错误的玩家尝试行动
try: result = g.apply_action(1, "fold")
g.apply_action(1, "fold") assert result["success"] == False
except ValueError as e: assert "不是玩家" in result["message"] or "turn" in result["message"].lower()
assert "not your turn" in str(e)
# 小盲玩家call (跟注到大盲) # 小盲玩家call (跟注到大盲)
g.apply_action(0, "call") g.apply_action(0, "call")

View File

@@ -1,36 +0,0 @@
from __future__ import annotations
import json
from fastapi.testclient import TestClient
from shortdeck_server.main import app
client = TestClient(app)
def pretty(o):
print(json.dumps(o, ensure_ascii=False, indent=2))
def run():
print('POST /join aa')
r = client.post('/join', json={'name':'aa'})
pretty(r.json())
print('POST /join bb')
r = client.post('/join', json={'name':'bb'})
pretty(r.json())
print('GET /get_game_state?player_id=0')
r = client.get('/get_game_state', params={'player_id':0})
pretty(r.json())
print('POST /apply_action check by pid=0')
r = client.post('/apply_action', json={'player_id':0, 'action':'check'})
pretty(r.json())
print('POST /apply_action bet 10 by pid=1')
r = client.post('/apply_action', json={'player_id':1, 'action':'bet', 'amount':10})
pretty(r.json())
if __name__ == '__main__':
run()