← Files QuestforgeARCHIVED FILE
scripts/check_resolution.py
3.83 KB · Sep 30, 2026 · 23:13 UTC
"""Resolve Questforge checks without letting failed rolls stall play."""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict, dataclass
from typing import Iterable
@dataclass(frozen=True)
class CheckResolution:
"""Structured result for one uncertain action."""
total: int
dc: int
outcome: str
margin: int
must_move_scene: bool
summary: str
def resolve_check(
total: int,
dc: int,
action: str,
success: str,
failure_cost: str,
new_option: str,
failure_count: int = 0,
) -> CheckResolution:
"""Classify a check and require a failure-forward consequence."""
if dc <= 0:
raise ValueError("DC must be positive.")
if not action.strip():
raise ValueError("Action is required.")
if not success.strip():
raise ValueError("Success result is required.")
if not failure_cost.strip() or not new_option.strip():
raise ValueError("Failure requires both failure_cost and new_option.")
if failure_count < 0:
raise ValueError("Failure count cannot be negative.")
margin = total - dc
must_move_scene = failure_count >= 2
if total >= dc:
outcome = "success"
summary = f"{action}: success. {sentence(success)}"
elif total >= dc - 3:
outcome = "success_with_cost"
summary = (
f"{action}: partial success. {sentence(success)} "
f"Cost: {sentence(failure_cost)}"
)
else:
outcome = "failure_forward"
summary = (
f"{action}: failure, but play moves. "
f"Cost: {sentence(failure_cost)} "
f"New option: {sentence(new_option)}"
)
if must_move_scene and outcome == "failure_forward":
summary += " Do not ask for another roll against the same obstacle."
return CheckResolution(
total=total,
dc=dc,
outcome=outcome,
margin=margin,
must_move_scene=must_move_scene,
summary=summary,
)
def sentence(value: str) -> str:
stripped_value = value.strip()
if stripped_value.endswith((".", "!", "?")):
return stripped_value
return stripped_value + "."
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Resolve a Questforge check with failure-forward output."
)
parser.add_argument("--total", required=True, type=int)
parser.add_argument("--dc", required=True, type=int)
parser.add_argument("--action", required=True)
parser.add_argument("--success", required=True)
parser.add_argument("--failure-cost", required=True)
parser.add_argument("--new-option", required=True)
parser.add_argument("--failure-count", type=int, default=0)
parser.add_argument(
"--format", choices=("markdown", "json"), default="markdown"
)
return parser
def main(arguments: Iterable[str] | None = None) -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parsed_arguments = build_parser().parse_args(arguments)
resolution = resolve_check(
total=parsed_arguments.total,
dc=parsed_arguments.dc,
action=parsed_arguments.action,
success=parsed_arguments.success,
failure_cost=parsed_arguments.failure_cost,
new_option=parsed_arguments.new_option,
failure_count=parsed_arguments.failure_count,
)
if parsed_arguments.format == "json":
print(json.dumps(asdict(resolution), indent=2, ensure_ascii=False))
else:
print(f"- Outcome: `{resolution.outcome}`")
print(f"- Margin: `{resolution.margin}`")
print(
f"- Must move scene: `{str(resolution.must_move_scene).lower()}`"
)
print(f"- Summary: {resolution.summary}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: fd126800f9d03403909749007f31f1d03e0813b349ed729349ce1e90d8b1c9f6