feat(cli): render --transcript output as a table
This commit is contained in:
@@ -10,10 +10,13 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import unicodedata
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from pf1e_simulator.runner import EncounterReport, EncounterSpec
|
||||
|
||||
from pf1e_simulator.loaders import CombatantLoadError, SheetLoadError, load_combatant
|
||||
@@ -26,6 +29,81 @@ class CliError(Exception):
|
||||
"""User-facing CLI error (bad arguments or unusable files)."""
|
||||
|
||||
|
||||
_TRANSCRIPT_COLUMNS = ("Round", "Combattant", "Message")
|
||||
_TRANSCRIPT_GUTTER = " "
|
||||
|
||||
|
||||
def _display_width(text: str) -> int:
|
||||
"""Terminal columns used by ``text``: wide CJK counts 2, combining marks 0."""
|
||||
width = 0
|
||||
for char in text:
|
||||
if unicodedata.combining(char):
|
||||
continue
|
||||
width += 2 if unicodedata.east_asian_width(char) in ("W", "F") else 1
|
||||
return width
|
||||
|
||||
|
||||
def _pad_cell(text: str, width: int, *, right: bool = False) -> str:
|
||||
"""Pad ``text`` with spaces to ``width`` display columns."""
|
||||
filler = " " * (width - _display_width(text))
|
||||
return filler + text if right else text + filler
|
||||
|
||||
|
||||
def _parse_transcript_line(line: str) -> tuple[str, str, str] | None:
|
||||
"""Split a ``round N id: message`` engine line into (round, id, message).
|
||||
|
||||
Returns None for lines that do not follow the pattern (initiative rolls,
|
||||
battle-over notices); the renderer keeps them as raw fallback rows.
|
||||
"""
|
||||
if not line.startswith("round "):
|
||||
return None
|
||||
rest = line.removeprefix("round ")
|
||||
space = rest.find(" ")
|
||||
if space <= 0 or not rest[:space].isdigit():
|
||||
return None
|
||||
remainder = rest[space + 1 :]
|
||||
colon = remainder.find(": ")
|
||||
if colon <= 0 or " " in remainder[:colon]:
|
||||
return None
|
||||
return rest[:space], remainder[:colon], remainder[colon + 2 :]
|
||||
|
||||
|
||||
def render_transcript_table(transcript: Sequence[str]) -> str:
|
||||
"""Render transcript lines as an aligned three-column table (no truncation).
|
||||
|
||||
Entries shaped ``round N id: message`` fill the Round / Combattant /
|
||||
Message columns (computed widths, display-width-aware padding so accents
|
||||
and CJK stay aligned); any other entry is emitted verbatim as a raw
|
||||
fallback row so no transcript line is ever dropped.
|
||||
"""
|
||||
rows: list[tuple[str, str, str] | None] = [_parse_transcript_line(line) for line in transcript]
|
||||
parsed = [row for row in rows if row is not None]
|
||||
widths = [
|
||||
max([_display_width(header)] + [_display_width(row[index]) for row in parsed])
|
||||
for index, header in enumerate(_TRANSCRIPT_COLUMNS)
|
||||
]
|
||||
lines = [
|
||||
_TRANSCRIPT_GUTTER.join(
|
||||
_pad_cell(header, width)
|
||||
for header, width in zip(_TRANSCRIPT_COLUMNS, widths, strict=True)
|
||||
),
|
||||
_TRANSCRIPT_GUTTER.join("-" * width for width in widths),
|
||||
]
|
||||
for row, line in zip(rows, transcript, strict=True):
|
||||
if row is None:
|
||||
lines.append(line)
|
||||
continue
|
||||
cells = [
|
||||
_pad_cell(row[0], widths[0], right=True),
|
||||
*(
|
||||
_pad_cell(cell, width)
|
||||
for cell, width in zip(row[1:], widths[1:], strict=True)
|
||||
),
|
||||
]
|
||||
lines.append(_TRANSCRIPT_GUTTER.join(cells))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _build_spec(args: argparse.Namespace) -> EncounterSpec:
|
||||
map_spec = load_map(args.map)
|
||||
provided = [tuple(raw) for raw in args.side]
|
||||
@@ -103,7 +181,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
parser.add_argument(
|
||||
"--transcript",
|
||||
action="store_true",
|
||||
help="print the turn-by-turn log of the first battle before the report",
|
||||
help="print the turn-by-turn log of the first battle as a table before the report",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--simple-monster-death",
|
||||
@@ -139,7 +217,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 2
|
||||
if args.transcript and report.transcript is not None:
|
||||
print("=== Déroulé du premier combat ===")
|
||||
print("\n".join(report.transcript))
|
||||
print(render_transcript_table(report.transcript))
|
||||
print()
|
||||
print(format_report(report, spec))
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user