shortdeck1.1
This commit is contained in:
Binary file not shown.
@@ -5,35 +5,43 @@ 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
|
||||
|
||||
|
||||
class ArenaGame:
|
||||
def __init__(self, starting_stack: int = 1000, max_players: int = 6):
|
||||
def __init__(self, starting_stack: int = 1000, max_players: int = 6,
|
||||
small_blind: int = 1, big_blind: int = 2):
|
||||
self.agents = []
|
||||
self.player_names: List[str] = []
|
||||
self.starting_stack = starting_stack
|
||||
self.max_players = max_players
|
||||
self.sim: Optional[Simulation] = None
|
||||
|
||||
# 筹码管理
|
||||
self.stacks: List[int] = []
|
||||
self.current_turn: int = 0
|
||||
self.pot: int = 0
|
||||
self.game_id = [str(name) for name in self.player_names]
|
||||
|
||||
# 盲注配置
|
||||
self.blind_config = BlindConfig(small_blind, big_blind, ante=0)
|
||||
|
||||
# 游戏标识
|
||||
self.game_id = str(uuid.uuid4())
|
||||
|
||||
def join_game(self, name: str) -> int:
|
||||
if len(self.player_names) >= self.max_players:
|
||||
raise ValueError("table full")
|
||||
|
||||
pid = len(self.player_names)
|
||||
self.player_names.append(name)
|
||||
agent = HumanAgent(pid)
|
||||
self.agents.append(agent)
|
||||
self.stacks.append(self.starting_stack)
|
||||
|
||||
self.sim = Simulation(self.agents)
|
||||
self.sim = Simulation(self.agents, self.blind_config)
|
||||
return pid
|
||||
|
||||
def info(self, player_id) -> Dict:
|
||||
|
||||
def info(self, player_id: Optional[int] = None) -> Dict:
|
||||
"""获取游戏状态信息"""
|
||||
if not self.sim:
|
||||
return {
|
||||
"game_id": self.game_id,
|
||||
@@ -48,62 +56,123 @@ class ArenaGame:
|
||||
"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:
|
||||
|
||||
board_cards = [str(c) for c in self.sim.board_cards("river")]
|
||||
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": list(self.stacks),
|
||||
"dealer_index": 0,
|
||||
"current_turn": self.current_turn,
|
||||
"pot": self.pot,
|
||||
"stage": "preflop",
|
||||
"actions": {"bet_min": 1, "bet_max": 100},
|
||||
"stacks": updated_stacks,
|
||||
"dealer_index": 0, # 简化:固定庄家位置, (优化轮询)
|
||||
"current_turn": self.sim.current_turn,
|
||||
"pot": self.sim.total_pot,
|
||||
"stage": self.sim.current_stage.value,
|
||||
"actions": actions,
|
||||
"player_cards": player_cards,
|
||||
"board_cards": board_cards,
|
||||
"player_states": [state.value for state in self.sim.player_states],
|
||||
}
|
||||
|
||||
def apply_action(self, pid: int, action: str, amount: Optional[int] = None):
|
||||
if not self.sim:
|
||||
raise ValueError("no game")
|
||||
if pid != self.current_turn:
|
||||
raise ValueError("not your turn")
|
||||
|
||||
|
||||
# 验证动作合法性
|
||||
if pid != self.sim.current_turn:
|
||||
raise ValueError(f"not your turn, current turn: {self.sim.current_turn}")
|
||||
|
||||
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 == "check":
|
||||
pass
|
||||
elif action == "bet":
|
||||
if amount is None:
|
||||
raise ValueError("bet requires amount")
|
||||
if amount < 0:
|
||||
raise ValueError("invalid amount")
|
||||
if amount > self.stacks[pid]:
|
||||
|
||||
amount = self.stacks[pid]
|
||||
self.stacks[pid] -= amount
|
||||
self.pot += amount
|
||||
elif action == "fold":
|
||||
self.stacks[pid] = 0
|
||||
else:
|
||||
raise ValueError(f"unknown action: {action}")
|
||||
|
||||
self.sim.apply_action(pid, action, amount)
|
||||
|
||||
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:
|
||||
self.sim.apply_action(pid, action, amount)
|
||||
except ValueError as e:
|
||||
raise ValueError(f"invalid action: {e}")
|
||||
|
||||
self.sim.dump_data(Path.cwd() / "shortdeck_arena_history.jsonl")
|
||||
if len(self.agents) > 0:
|
||||
self.current_turn = (self.current_turn + 1) % len(self.agents)
|
||||
|
||||
@property
|
||||
def current_turn(self) -> int:
|
||||
if not self.sim:
|
||||
return 0
|
||||
return self.sim.current_turn
|
||||
|
||||
@property
|
||||
def pot(self) -> int:
|
||||
if not self.sim:
|
||||
return 0
|
||||
return self.sim.total_pot
|
||||
|
||||
@property
|
||||
def history(self) -> List[Dict]:
|
||||
if not self.sim:
|
||||
|
||||
42
shortdeck_server/game_stage.py
Normal file
42
shortdeck_server/game_stage.py
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
|
||||
class GameStage(Enum):
|
||||
PREFLOP = "preflop"
|
||||
FLOP = "flop"
|
||||
TURN = "turn"
|
||||
RIVER = "river"
|
||||
SHOWDOWN = "showdown"
|
||||
|
||||
# def setup_preflop(self):
|
||||
# # 自动扣除小盲/大盲
|
||||
# # 设置首个行动玩家
|
||||
# pass
|
||||
|
||||
def advance_street(self):
|
||||
# 检查下注轮是否结束
|
||||
# 发放公共牌
|
||||
# 重置行动顺序
|
||||
pass
|
||||
|
||||
# def get_call_amount(self, pid):
|
||||
# # 计算跟注所需金额
|
||||
# pass
|
||||
|
||||
# def get_min_raise(self, pid):
|
||||
# # 计算最小加注金额
|
||||
# pass
|
||||
|
||||
def handle_all_in(self, pid, amount):
|
||||
# 创建边池
|
||||
# 标记玩家状态
|
||||
pass
|
||||
|
||||
def evaluate_hand(self, hole_cards, board_cards):
|
||||
# 短牌手牌强度排序
|
||||
# A-6 低顺特殊处理
|
||||
pass
|
||||
|
||||
def determine_winners(self, active_players):
|
||||
# 边池分配
|
||||
# 短牌规则下的比牌
|
||||
pass
|
||||
@@ -18,7 +18,7 @@ class JoinResponse(BaseModel):
|
||||
|
||||
class ActionRequest(BaseModel):
|
||||
player_id: int
|
||||
action: str
|
||||
action: str # "fold", "call", "raise", "check", "bet"
|
||||
amount: int | None = None
|
||||
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ BASE_PATH = "/get_game_state"
|
||||
APPLY_PATH = "/apply_action"
|
||||
|
||||
|
||||
def fetch_game_state(base_url: str, player_id: int) -> Dict[str, Any]:
|
||||
def fetch_game_state(base_url, player_id) -> Dict[str, Any]:
|
||||
resp = requests.get(f"{base_url.rstrip('/')}{BASE_PATH}", params={"player_id": player_id}, timeout=5)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def post_action(base_url: str, player_id: int, action: str, amount: Optional[int]):
|
||||
def post_action(base_url, player_id, action, amount):
|
||||
payload = {"player_id": player_id, "action": action}
|
||||
if amount is not None:
|
||||
payload["amount"] = amount
|
||||
@@ -27,7 +27,7 @@ def post_action(base_url: str, player_id: int, action: str, amount: Optional[int
|
||||
return resp.json()
|
||||
|
||||
|
||||
def choose_random_action(info: Dict[str, Any]) -> Optional[tuple[str, Optional[int]]]:
|
||||
def choose_random_action(info) -> Optional[tuple[str, Optional[int]]]:
|
||||
actions = info.get("actions")
|
||||
if not actions:
|
||||
return None
|
||||
@@ -48,7 +48,7 @@ def choose_random_action(info: Dict[str, Any]) -> Optional[tuple[str, Optional[i
|
||||
return random.choice(choices)
|
||||
|
||||
|
||||
def run_loop(base_url: str, player_id: int, interval: float = 2.0, seed: Optional[int] = None):
|
||||
def run_loop(base_url, player_id, interval, seed):
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
@@ -77,7 +77,7 @@ def run_loop(base_url: str, player_id: int, interval: float = 2.0, seed: Optiona
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
def main(argv) -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--server", default="http://localhost:8000")
|
||||
parser.add_argument("--player_id", type=int, default=1)
|
||||
|
||||
@@ -4,23 +4,31 @@ from shortdeck_server.arena_adapter import ArenaGame
|
||||
|
||||
|
||||
def test_join_and_actions():
|
||||
g = ArenaGame(starting_stack=100, max_players=3)
|
||||
g = ArenaGame(starting_stack=100, max_players=3, small_blind=1, big_blind=2)
|
||||
pid0 = g.join_game("aa")
|
||||
pid1 = g.join_game("bb")
|
||||
assert pid0 == 0
|
||||
assert pid1 == 1
|
||||
|
||||
state = g.info()
|
||||
assert state["stacks"] == [100, 100]
|
||||
# 在短牌扑克中,玩家加入后盲注已自动扣除
|
||||
# 小盲(pid0): 100-1=99, 大盲(pid1): 100-2=98
|
||||
assert state["stacks"] == [99, 98]
|
||||
|
||||
# 验证轮次管理:heads-up时小盲先行动
|
||||
assert g.current_turn == 0
|
||||
|
||||
# 测试错误的玩家尝试行动
|
||||
try:
|
||||
g.apply_action(1, "fold")
|
||||
except ValueError as e:
|
||||
assert "not your turn" in str(e)
|
||||
|
||||
g.apply_action(0, "check")
|
||||
# 小盲玩家call (跟注到大盲)
|
||||
g.apply_action(0, "call")
|
||||
assert g.current_turn == 1
|
||||
|
||||
g.apply_action(1, "bet", 10)
|
||||
assert g.pot == 10
|
||||
assert g.stacks[1] == 90
|
||||
# 大盲玩家加注
|
||||
g.apply_action(1, "bet", 10)
|
||||
assert g.pot >= 10 # 底池至少包含加注金额
|
||||
assert g.history[-1]["action"] == "bet"
|
||||
|
||||
Reference in New Issue
Block a user