feat: add human http agent
This commit is contained in:
+106
-1
@@ -193,7 +193,112 @@ def _emit_turn_separator(writer: Writer) -> None:
|
||||
Centralised so the exact glyph/length of the separator can be changed
|
||||
in one place if the visual style ever needs tweaking.
|
||||
"""
|
||||
writer("=====\n\n")
|
||||
line = "~" * 60
|
||||
writer(line + "\n\n")
|
||||
|
||||
|
||||
# ANSI control sequence: ``ESC[2J`` clears the entire screen and ``ESC[H``
|
||||
# moves the cursor back to the top-left. Kept as a module constant so any
|
||||
# caller can reuse the exact same sequence and tests can monkey-patch it.
|
||||
CLEAR_SCREEN_SEQUENCE = "\x1b[2J\x1b[H"
|
||||
|
||||
|
||||
def clear_screen(writer: Writer) -> None:
|
||||
"""Wipe the terminal via ANSI control sequences.
|
||||
|
||||
Implemented as a tiny helper rather than each caller inlining the escape
|
||||
code so we have a single location to swap in alternative strategies
|
||||
(e.g. printing many newlines on terminals that ignore ANSI).
|
||||
"""
|
||||
writer(CLEAR_SCREEN_SEQUENCE)
|
||||
|
||||
|
||||
def render_game_state(game_state: dict[str, Any]) -> str:
|
||||
"""Render a full ``GameManager.to_dict()`` snapshot for terminal display.
|
||||
|
||||
The resulting block is intended for the standalone HTTP human client's
|
||||
``POST /game`` callback so the operator sees the up-to-date table state
|
||||
plus a per-hand digest (winners, awards, showdown hole cards).
|
||||
"""
|
||||
lines: list[str] = []
|
||||
lines.append("#" * 60)
|
||||
lines.append(
|
||||
f"GAME UPDATE game_id={game_state.get('game_id')} "
|
||||
f"status={game_state.get('status')} hand={game_state.get('hand_number')}"
|
||||
)
|
||||
lines.append(
|
||||
f"Blinds {game_state.get('small_blind')}/{game_state.get('big_blind')} "
|
||||
f"| Button seat: {game_state.get('button_seat')} "
|
||||
f"| Starting stack: {game_state.get('starting_stack')}"
|
||||
)
|
||||
|
||||
lines.append("-" * 60)
|
||||
lines.append("Stacks:")
|
||||
for player in game_state.get("players", []):
|
||||
flags = _player_flags(player)
|
||||
lines.append(
|
||||
f" seat {int(player.get('seat', 0)):>2} "
|
||||
f"| {str(player.get('name', '')):<16} "
|
||||
f"| stack {int(player.get('stack', 0)):>6} "
|
||||
f"| {flags}"
|
||||
)
|
||||
|
||||
hands = game_state.get("hands") or []
|
||||
lines.append("-" * 60)
|
||||
lines.append(f"Hands played: {len(hands)}")
|
||||
for hand in hands:
|
||||
lines.extend(_render_hand_digest(hand))
|
||||
|
||||
lines.append("#" * 60)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _player_flags(player: dict[str, Any]) -> str:
|
||||
"""Render the boolean state of a player as a compact tag list."""
|
||||
tags: list[str] = []
|
||||
if player.get("folded"):
|
||||
tags.append("folded")
|
||||
if player.get("all_in"):
|
||||
tags.append("all_in")
|
||||
if not player.get("in_hand"):
|
||||
tags.append("out")
|
||||
return ",".join(tags) if tags else "active"
|
||||
|
||||
|
||||
def _render_hand_digest(hand: dict[str, Any]) -> list[str]:
|
||||
"""Render a single hand summary as a compact, multi-line digest.
|
||||
|
||||
Kept separate from :func:`render_game_state` so the per-hand format can
|
||||
be reused or extended (e.g. detailed action log) without entangling
|
||||
with the table-level header layout.
|
||||
"""
|
||||
lines: list[str] = []
|
||||
lines.append(
|
||||
f" Hand #{hand.get('hand_number')} "
|
||||
f"| button_seat={hand.get('button_seat')} "
|
||||
f"| board: {_format_cards(hand.get('board') or [], '(folded out)')}"
|
||||
)
|
||||
|
||||
awards = hand.get("awards") or []
|
||||
if not awards:
|
||||
lines.append(" (no awards recorded)")
|
||||
for award in awards:
|
||||
winners = ", ".join(str(w) for w in award.get("winners") or [])
|
||||
hand_value = award.get("hand_value") or {}
|
||||
value_label = hand_value.get("name") or "-"
|
||||
lines.append(
|
||||
f" pot {int(award.get('amount', 0)):>6} -> "
|
||||
f"{winners or '(no winner)'} ({value_label})"
|
||||
)
|
||||
|
||||
showdown = hand.get("showdown_hands") or {}
|
||||
if showdown:
|
||||
lines.append(" showdown:")
|
||||
for player_id, cards in showdown.items():
|
||||
lines.append(
|
||||
f" {player_id}: {_format_cards(cards, '(empty)')}"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def format_legal_action(action: dict[str, Any]) -> str:
|
||||
|
||||
Reference in New Issue
Block a user