shortdeck1.1

This commit is contained in:
2025-09-30 18:26:06 +08:00
parent ee95b8e049
commit 7071eaa12b
14 changed files with 665 additions and 59 deletions

Binary file not shown.

View File

@@ -50,3 +50,5 @@ class Card:
for s in Suit:
cards.append(Card(r, s))
return cards

View File

@@ -0,0 +1,91 @@
from __future__ import annotations
from enum import Enum
from typing import List, Optional
class GameStage(Enum):
PREFLOP = "preflop"
FLOP = "flop"
TURN = "turn"
RIVER = "river"
SHOWDOWN = "showdown"
FINISHED = "finished"
def __str__(self) -> str:
return self.value
@classmethod
def get_next_stage(cls, current: 'GameStage') -> Optional['GameStage']:
stage_order = [cls.PREFLOP, cls.FLOP, cls.TURN, cls.RIVER, cls.SHOWDOWN, cls.FINISHED]
current_idx = stage_order.index(current)
if current_idx < len(stage_order) - 1:
return stage_order[current_idx + 1]
return None
def get_board_card_count(self) -> int:
if self == GameStage.PREFLOP:
return 0
elif self == GameStage.FLOP:
return 3
elif self == GameStage.TURN:
return 4
elif self in (GameStage.RIVER, GameStage.SHOWDOWN):
return 5
return 0
class PlayerState(Enum):
ACTIVE = "active" # 可行动
FOLDED = "folded" # 已弃牌
ALLIN = "allin" # 全下
CALLED = "called" # 已跟注
RAISE = "raised" # 已加注
WAITING = "waiting" # 等待其他玩家
OUT = "out" # 出局
def __str__(self) -> str:
return self.value
class BlindConfig:
# 对抗玩法是否需要前注?
def __init__(self, small_blind: int = 1, big_blind: int = 2, ante: int = 0):
self.small_blind = small_blind
self.big_blind = big_blind
self.ante = ante
def get_sb_position(self, num_players, dealer_position) -> int:
"""
获取小盲位置
heads-up时小盲为庄位
"""
if num_players == 2:
return dealer_position # heads-up: 小盲 = 庄位
else:
return (dealer_position + 1) % num_players
def get_bb_position(self, num_players, dealer_position) -> int:
"""
获取大盲位置
"""
if num_players == 2:
return (dealer_position + 1) % 2 # heads-up: 大盲 = 庄位
return (dealer_position + 2) % num_players # 多人: 大盲为庄位后两位
def get_first_to_act(self, stage: GameStage, num_players) -> int:
"""
获取首个行动玩家位置
区分preflop和postflop
1. preflop: 大盲后第一位 (UTG)
2. postflop: 小盲位先行动
"""
if stage == GameStage.PREFLOP:
if num_players == 2:
return self.get_sb_position(num_players) # heads-up: 小盲先行动
else: # preflop: 大盲后第一位
return (self.get_bb_position(num_players) + 1) % num_players
else:
# flop/river/turn: 小盲位先行动
return self.get_sb_position(num_players)

View File

@@ -0,0 +1,115 @@
from typing import List, Tuple, Dict
from collections import Counter
from itertools import combinations
from .card import Card, Rank, Suit
from .hand_ranking import HandRanking, HandType
class HandEvaluator:
@staticmethod
def evaluateHand(cards) -> HandRanking:
"""
从7张牌中找出最好的5张牌组合
"""
if len(cards) != 7:
raise ValueError(f"Expected 7 cards, got {len(cards)}")
best_ranking = None
best_cards = None
# 所有可能的5张牌组合
for five_cards in combinations(cards, 5):
ranking = HandEvaluator.evaluate5Cards(list(five_cards))
if best_ranking is None or ranking > best_ranking:
best_ranking = ranking
best_cards = list(five_cards)
best_ranking.cards = best_cards
return best_ranking
@staticmethod
def evaluate5Cards(cards) -> HandRanking:
if len(cards) != 5:
raise ValueError(f"Expected 5 cards, got {len(cards)}")
# 按点数排序(降序)
sorted_cards = sorted(cards, key=lambda c: c.rank.numeric_value, reverse=True)
ranks = [card.rank for card in sorted_cards]
suits = [card.suit for card in sorted_cards]
# 统计点数出现次数
rank_counts = Counter(ranks)
count_values = sorted(rank_counts.values(), reverse=True)
# 同花
is_flush = len(set(suits)) == 1
# 顺子
is_straight, straight_high = HandEvaluator._isStraight(ranks)
# 根据牌型返回相应的HandRanking
if is_straight and is_flush:
if straight_high == Rank.ACE and ranks == [Rank.ACE, Rank.KING, Rank.QUEEN, Rank.JACK, Rank.TEN]:
return HandRanking(HandType.ROYAL_FLUSH, [Rank.ACE], sorted_cards) # 皇家同花顺
else:
return HandRanking(HandType.STRAIGHT_FLUSH, [straight_high], sorted_cards) # 同花顺
elif count_values == [4, 1]: # 四条
quad_rank = [rank for rank, count in rank_counts.items() if count == 4][0]
kicker = [rank for rank, count in rank_counts.items() if count == 1][0]
return HandRanking(HandType.FOUR_OF_A_KIND, [quad_rank, kicker], sorted_cards)
elif count_values == [3, 2]: # 三条+一对
trips_rank = [rank for rank, count in rank_counts.items() if count == 3][0]
pair_rank = [rank for rank, count in rank_counts.items() if count == 2][0]
return HandRanking(HandType.FULL_HOUSE, [trips_rank, pair_rank], sorted_cards)
elif is_flush: # 同花
return HandRanking(HandType.FLUSH, ranks, sorted_cards)
elif is_straight: # 顺子
return HandRanking(HandType.STRAIGHT, [straight_high], sorted_cards)
elif count_values == [3, 1, 1]: # 三条
trips_rank = [rank for rank, count in rank_counts.items() if count == 3][0]
kickers = sorted([rank for rank, count in rank_counts.items() if count == 1], reverse=True)
return HandRanking(HandType.THREE_OF_A_KIND, [trips_rank] + kickers, sorted_cards)
elif count_values == [2, 2, 1]: # 两对
pairs = sorted([rank for rank, count in rank_counts.items() if count == 2], reverse=True)
kicker = [rank for rank, count in rank_counts.items() if count == 1][0]
return HandRanking(HandType.TWO_PAIR, pairs + [kicker], sorted_cards)
elif count_values == [2, 1, 1, 1]: # 一对
pair_rank = [rank for rank, count in rank_counts.items() if count == 2][0]
kickers = sorted([rank for rank, count in rank_counts.items() if count == 1], reverse=True)
return HandRanking(HandType.ONE_PAIR, [pair_rank] + kickers, sorted_cards)
else: # 高牌
return HandRanking(HandType.HIGH_CARD, ranks, sorted_cards)
@staticmethod
def _isStraight(ranks: List[Rank]) -> Tuple[bool, Rank]:
values = sorted([rank.numeric_value for rank in ranks], reverse=True)
is_regular_straight = True
for i in range(1, len(values)):
if values[i-1] - values[i] != 1:
is_regular_straight = False
break
if is_regular_straight:
# 返回最高牌
highest_rank = None
for rank in ranks:
if rank.numeric_value == values[0]:
highest_rank = rank
break
return True, highest_rank
if values == [14, 5, 4, 3, 2]: # A, 5, 4, 3, 2
return True, Rank.FIVE
return False, None

View File

@@ -0,0 +1,57 @@
from enum import Enum
from typing import List, Tuple
from .card import Card, Rank
class HandType(Enum):
HIGH_CARD = (1, "High Card")
ONE_PAIR = (2, "Pair")
TWO_PAIR = (3, "Two Pair")
THREE_OF_A_KIND = (4, "Three of a Kind")
STRAIGHT = (5, "Straight")
FLUSH = (6, "Flush")
FULL_HOUSE = (7, "Full House")
FOUR_OF_A_KIND = (8, "Four of a Kind")
STRAIGHT_FLUSH = (9, "Straight Flush")
ROYAL_FLUSH = (10, "Royal Flush")
def __new__(cls, strength, name):
obj = object.__new__(cls)
obj._value_ = strength
obj.strength = strength
obj.type_name = name
return obj
class HandRanking:
def __init__(self, hand_type: HandType, key_ranks: List[Rank], cards: List[Card]):
self.hand_type = hand_type
self.key_ranks = key_ranks # 用于比较的关键点数
self.cards = cards # 组成这个ranking的5张牌
def __str__(self):
if self.hand_type == HandType.FOUR_OF_A_KIND:
return f"Quad({self.key_ranks[0].symbol})"
elif self.hand_type == HandType.FULL_HOUSE:
return f"Full House({self.key_ranks[0].symbol} over {self.key_ranks[1].symbol})"
elif self.hand_type == HandType.FLUSH:
return f"Flush({self.key_ranks[0].symbol} high)"
elif self.hand_type == HandType.STRAIGHT:
return f"Straight({self.key_ranks[0].symbol} high)"
elif self.hand_type == HandType.STRAIGHT_FLUSH:
if self.key_ranks[0] == Rank.ACE:
return "Royal Flush"
else:
return f"Straight Flush({self.key_ranks[0].symbol} high)"
elif self.hand_type == HandType.ROYAL_FLUSH:
return "Royal Flush"
elif self.hand_type == HandType.THREE_OF_A_KIND:
return f"Three of a Kind({self.key_ranks[0].symbol})"
elif self.hand_type == HandType.TWO_PAIR:
return f"Two Pair({self.key_ranks[0].symbol} and {self.key_ranks[1].symbol})"
elif self.hand_type == HandType.ONE_PAIR:
return f"Pair({self.key_ranks[0].symbol})"
else:
return f"High Card({self.key_ranks[0].symbol})"

View File

@@ -9,26 +9,100 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .agent import Agent
from .card import Card
from .game_stage import GameStage, PlayerState, BlindConfig
class Simulation:
def __init__(self, agents: List[Agent]):
def __init__(self, agents: List[Agent], blind_config: Optional[BlindConfig] = None):
self.agents = agents
self.history: List[Dict] = []
self.cards: List[Card] = []
self.saved = False
# 游戏状态管理
self.current_stage = GameStage.PREFLOP
self.player_states: List[PlayerState] = [PlayerState.ACTIVE] * len(agents)
self.current_turn = 0
self.betting_round_complete = False
# 盲注配置
self.blind_config = blind_config or BlindConfig()
# 筹码和底池管理
self.pot: List[int] = [0] * len(agents) # 每个玩家在当前轮的投入
self.total_pot = 0
self.last_raise_amount = 0
self.min_raise = self.blind_config.big_blind
self.dealer_position = -1
self.new_round()
def new_round(self):
self.history = []
self.cards = Card.all_short()
random.shuffle(self.cards)
random.shuffle(self.cards) # 洗牌
self.saved = False
# 重置游戏状态
self.current_stage = GameStage.PREFLOP
self.player_states = [PlayerState.ACTIVE] * len(self.agents)
self.betting_round_complete = False
# 重置下注状态
self.pot = [0] * len(self.agents)
self.total_pot = 0
self.last_raise_amount = 0
self.min_raise = self.blind_config.big_blind
# 设置盲注
self._setup_blinds()
def player_cards(self, pid: int) -> List[Card]:
return self.cards[pid * 2 : pid * 2 + 2]
# 庄家位置
self.dealer_position = random.choice(range(len(self.agents)))
def _setup_blinds(self):
"""设置盲注"""
num_players = len(self.agents)
# 至少需要2个玩家才能设置盲注
if num_players < 2:
self.current_turn = 0 if num_players > 0 else 0
return
def board_cards(self, street: str) -> List[Card]:
sb_pos = self.blind_config.get_sb_position(num_players,self.dealer_position)
bb_pos = self.blind_config.get_bb_position(num_players,self.dealer_position)
# 确保位置有效
if sb_pos >= num_players or bb_pos >= num_players:
self.current_turn = 0
return
# 扣除小盲
self.pot[sb_pos] = self.blind_config.small_blind
self.total_pot += self.blind_config.small_blind
self.history.append({
"pid": sb_pos,
"action": "small_blind",
"amount": self.blind_config.small_blind
})
# 扣除大盲
self.pot[bb_pos] = self.blind_config.big_blind
self.total_pot += self.blind_config.big_blind
self.history.append({
"pid": bb_pos,
"action": "big_blind",
"amount": self.blind_config.big_blind
})
# 首个行动玩家
self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players)
self.last_raise_amount = self.blind_config.big_blind
def player_cards(self, pid) -> List[Card]:
return self.cards[pid * 2 : (pid * 2 + 2)]
def board_cards(self, street) -> List[Card]:
nplayers = len(self.agents)
idx_start = nplayers * 2
if street == "flop":
@@ -39,11 +113,159 @@ class Simulation:
return self.cards[idx_start: idx_start + 5]
return []
def node_info(self) -> Dict:
return {"bet_min": 1, "bet_max": 100}
def get_current_max_bet(self) -> int:
"""
获取当前最高下注额, 用于计算跟注金额
"""
return max(self.pot) if self.pot else 0
def get_call_amount(self, pid) -> int:
"""
计算玩家跟注所需金额
"""
if pid >= len(self.pot):
return 0
max_pot = self.get_current_max_bet()
return max(0, max_pot - self.pot[pid])
def is_betting_round_complete(self) -> bool:
"""
检查当前下注轮是否完成
"""
active_players = [i for i, state in enumerate(self.player_states)
if state in (PlayerState.ACTIVE, PlayerState.CALLED)]
if len(active_players) <= 1:
return True
# 检查所有active玩家是否都已投入相同金额
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
return True
def advance_to_next_street(self):
if self.current_stage == GameStage.FINISHED:
return
next_stage = GameStage.get_next_stage(self.current_stage)
if next_stage is None:
self.current_stage = GameStage.FINISHED
return
self.current_stage = next_stage
# 重置下注轮状态
self.betting_round_complete = False
#### 重置pot为累计投入 (不清零,为了计算边池)
def apply_action(self, pid: int, action: str, amount: Optional[int] = None):
# 重置行动状态
for i, state in enumerate(self.player_states):
if state == PlayerState.CALLED:
self.player_states[i] = PlayerState.ACTIVE
# 设置首个行动玩家
num_players = len(self.agents)
self.current_turn = self.blind_config.get_first_to_act(self.current_stage, num_players)
self.last_raise_amount = 0
self.min_raise = self.blind_config.big_blind
def get_next_active_player(self, start_pos) -> Optional[int]:
for i in range(len(self.agents)):
pos = (start_pos + i) % len(self.agents)
if self.player_states[pos] == PlayerState.ACTIVE:
return pos
return None
def node_info(self) -> Dict:
if self.current_turn >= len(self.pot):
return {"bet_min": self.min_raise, "bet_max": 0, "call_amount": 0}
# 跟注
call_amount = self.get_call_amount(self.current_turn)
return {
"bet_min": max(self.min_raise, call_amount + self.min_raise),
"bet_max": 100, ########## 需要从ArenaGame获取实际大小
"call_amount": call_amount
}
def apply_action(self, pid, action, amount):
"""
应用玩家动作
"""
if pid != self.current_turn:
raise ValueError(f"不是玩家 {pid} 的回合")
if self.player_states[pid] not in (PlayerState.ACTIVE,):
raise ValueError(f"玩家 {pid} 无法行动,当前状态: {self.player_states[pid]}")
action = action.lower()
self.history.append({"pid": pid, "action": action, "amount": amount})
if action == "fold":
self.player_states[pid] = PlayerState.FOLDED
elif action == "call":
call_amount = self.get_call_amount(pid)
if call_amount == 0:
# check
self.history[-1]["action"] = "check"
else:
self.pot[pid] += call_amount
self.total_pot += call_amount
self.player_states[pid] = PlayerState.CALLED
elif action == "check":
call_amount = self.get_call_amount(pid)
if call_amount > 0:
raise ValueError("跟注金额>0, 无法过牌,需要跟注或弃牌")
self.player_states[pid] = PlayerState.CALLED
elif action in ("bet", "raise"):
if amount is None:
raise ValueError(f"{action} 需要指定金额")
# 加注需要补齐跟注金额
call_amount = self.get_call_amount(pid)
total_amount = call_amount + (amount or 0)
self.pot[pid] += total_amount
self.total_pot += total_amount
if amount and amount > 0:
self.last_raise_amount = amount
self.min_raise = amount
self.player_states[pid] = PlayerState.CALLED
# 其他跟注的玩家
for i, state in enumerate(self.player_states):
if i != pid and state == PlayerState.CALLED:
self.player_states[i] = PlayerState.ACTIVE
else:
raise ValueError(f"未知动作: {action}")
# 下一个玩家
self._advance_turn()
def _advance_turn(self):
"""
推进回合
"""
# 检查下注轮是否完成
if self.is_betting_round_complete():
self.betting_round_complete = True
self.advance_to_next_street()
else:
# 找到下一个可行动玩家
next_player = self.get_next_active_player(self.current_turn + 1)
if next_player is not None:
self.current_turn = next_player
else:
# 没有玩家需要行动,结束下注轮
self.betting_round_complete = True
self.advance_to_next_street()
def to_save_data(self) -> Dict:
players = [f"Agent{a.pid}" for a in self.agents]
@@ -60,6 +282,6 @@ class Simulation:
if path is None:
path = Path.cwd() / "shortdeck_arena_history.jsonl"
with path.open("a", encoding="utf-8") as f:
f.write(json.dumps(self.to_save_data(), ensure_ascii=False))
f.write(json.dumps(self.to_save_data()))
f.write("\n")
self.saved = True