LVPWB/lvpwb/card_flows.py

420 lines
18 KiB
Python

from __future__ import annotations
import json
import re
from datetime import date
from pathlib import Path
from typing import Any
import lvpwb as app
def show_name_for_brand(brand: str, shows: dict[str, str] | None = None) -> str:
shows = shows or {}
if brand == "Raw":
return shows.get("Raw") or "Monday Night Raw"
if brand == "SmackDown":
return shows.get("SmackDown") or "Thursday Night SmackDown!"
return shows.get("PPV") or "WWF PPV"
def game_show_names(game_id: str) -> dict[str, str]:
"""Resolved show names for a registered game (WWE names for built-ins)."""
config = app.CUSTOM_GAME_CONFIGS.get(game_id, {})
shows = config.get("shows") if isinstance(config, dict) else None
return {brand: app.show_name_for_brand(brand, shows if isinstance(shows, dict) else None)
for brand in app.SHOW_BRANDS}
def is_ppv_card(card: dict[str, Any]) -> bool:
if card.get("season_show") == "PPV":
return True
show = str(card.get("show", ""))
folded = show.casefold()
if "ppv" in folded or "pay-per-view" in folded:
return True
game_id = str(card.get("game_id", ""))
if game_id:
ppv_name = app.game_show_names(game_id).get("PPV", "")
if ppv_name and folded == ppv_name.casefold():
return True
return False
def random_attendance(city: str) -> int:
"""Pick an attendance within the city's typical venue range."""
low, high = app.CITY_ATTENDANCE_RANGES.get(city, app.DEFAULT_ATTENDANCE_RANGE)
return app.random.randint(low, high)
def random_location() -> tuple[str, int]:
city = app.random.choice(sorted(app.CITY_ATTENDANCE_RANGES))
return city, app.random_attendance(city)
def parse_attendance(raw: str) -> int | None:
cleaned = re.sub(r"[,\s_]", "", raw)
if not cleaned.isdigit():
return None
value = int(cleaned)
return value if value > 0 else None
def ask_location() -> tuple[str, int]:
"""Prompt for city and attendance; 'r' randomizes both, or just attendance."""
while True:
city = app.ask("City (r randomizes city and attendance)").strip()
if city.casefold() == "r":
city, attendance = app.random_location()
print(f"Random location: {city} · {attendance:,} fans")
return city, attendance
if city:
break
print("Enter a city, or 'r' to randomize.")
while True:
raw = app.ask(f"Attendance in {city} (r to randomize)").strip()
if raw.casefold() == "r":
attendance = app.random_attendance(city)
print(f"Random attendance: {attendance:,} fans")
return city, attendance
attendance = app.parse_attendance(raw)
if attendance is not None:
return city, attendance
print("Enter a whole number greater than zero, or 'r' to randomize.")
def show_location_text(card: dict[str, Any]) -> str:
"""Show name with its city and attendance, when the card has them."""
parts = [card["show"]]
city = str(card.get("city") or "").strip()
attendance = card.get("attendance")
if city:
parts.append(city)
if attendance:
parts.append(f"{attendance:,} fans")
return " · ".join(parts)
def choose_game() -> str:
app.refresh_custom_games()
ids = list(app.GAMES)
labels = [app.game_display_name(game_id) for game_id in ids]
return ids[app.ask_choice("Choose a game", ids, labels=labels)]
def choose_match_type(game_id: str) -> tuple[str, tuple[int, ...]]:
options = app.match_type_definitions(game_id)
labels = [f"{item['name']} ({' vs '.join(map(str, item['sides']))})"
for item in options]
selected = app.ask_choice("Choose match type", [item["name"] for item in options], labels=labels)
definition = options[selected]
return definition["name"], tuple(definition["sides"])
def choose_sides(side_sizes: tuple[int, ...], roster: list[str]) -> list[list[str]]:
total = sum(side_sizes)
if total > len(roster):
raise ValueError("This match needs more unique wrestlers than the roster provides.")
roster = sorted(roster, key=str.casefold)
print("\nParticipant selection: enter a roster number, 'r' for a random pick, or 'a' to "
"randomly fill the remaining spots.")
for index, name in enumerate(roster, 1):
print(f" {index:>2}. {name}")
selected: list[str] = []
sides: list[list[str]] = []
for side_number, size in enumerate(side_sizes, 1):
side: list[str] = []
for slot in range(1, size + 1):
while True:
choice = app.ask(f"Side {side_number}, wrestler {slot}/{size}")
if choice.lower() == "a":
remaining = [name for name in roster if name not in selected]
needed = sum(side_sizes[side_number - 1:]) - len(side)
picks = app.random.sample(remaining, needed)
# Fill this side and all remaining sides, then return.
take = size - len(side)
side.extend(picks[:take])
sides.append(side)
rest = picks[take:]
cursor = 0
for remaining_size in side_sizes[side_number:]:
sides.append(rest[cursor:cursor + remaining_size])
cursor += remaining_size
return sides
if choice.lower() == "r":
available = [name for name in roster if name not in selected]
if available:
pick = app.random.choice(available)
break
else:
try:
index = int(choice)
pick = roster[index - 1]
if index < 1 or index > len(roster):
raise IndexError
if pick in selected:
print("Each wrestler can only be selected once in a match.")
continue
break
except (ValueError, IndexError):
print("Choose an available roster number, 'r', or 'a'.")
side.append(pick)
selected.append(pick)
sides.append(side)
return sides
def create_match(number: int, game_id: str, roster: list[str]) -> dict[str, Any]:
print(f"\n{'=' * 14} MATCH {number} {'=' * 14}")
match_name, side_sizes = app.choose_match_type(game_id)
title = app.ask("Optional match title (leave blank for none)")
sides = app.choose_sides(side_sizes, roster)
definition = app.match_type_definition(game_id, match_name)
image = ""
if app.game_has_screenshots(game_id) and app.ask("Attach a screenshot? (y/N)").lower() == "y":
chosen = app.choose_screenshot(app.screenshot_directory_for_game(game_id))
if chosen is not None:
image = str(chosen)
return {"title": title, "type": match_name, "sides": sides,
"winner_side": None, "finish": "", "promo": None, "image": image,
"ranking_division": None, "title_defense": False, "rivalry_payoff": False,
"match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None,
"base_rating": definition["base_rating"],
"valid_outcomes": list(definition["outcomes"])}
def select_card_path() -> Path | None:
candidates = sorted(app.CARD_DIR.glob("*.md")) if app.CARD_DIR.is_dir() else []
if candidates:
print("\nSaved cards:")
for index, path in enumerate(candidates, 1):
print(f" {index}. {app.card_summary(path)} [{path.name}]")
print(" Enter a number to load a listed card, or type a path.")
raw = app.ask("Card path (blank to cancel)")
if not raw:
return None
if raw.isdigit() and candidates and 1 <= int(raw) <= len(candidates):
return candidates[int(raw) - 1]
path = Path(raw).expanduser()
if not path.is_absolute():
path = app.ROOT / path
return path
def create_card() -> dict[str, Any]:
game_id = app.choose_game()
roster = app.load_roster(game_id)
show = app.ask("Show name", app.game_show_names(game_id)["SmackDown"])
while True:
show_date = app.ask("Show date (YYYY-MM-DD)", date.today().isoformat())
try:
show_date = date.fromisoformat(show_date).isoformat()
break
except ValueError:
print("Enter a valid date in YYYY-MM-DD format.")
city, attendance = app.ask_location()
card: dict[str, Any] = {
"game_id": game_id, "game_title": app.game_title(game_id),
"federation_name": app.federation_name(game_id),
"title_definitions": app.title_definitions_for_game(game_id),
"show": show, "date": show_date,
"city": city, "attendance": attendance,
"matches": [{"title": "", "type": "Singles", "sides": [], "winner_side": None,
"finish": "", "promo": None, "image": "", "ranking_division": None,
"title_defense": False, "rivalry_payoff": False,
"match_time_minutes": None, "near_falls": 0, "finishers": 0,
"rating": None} for _ in range(3)],
"path": str(app.default_card_path(show, show_date)),
}
app.save_card(card)
print(f"\nCard started: {card['path']}")
for number in range(1, 4):
card["matches"][number - 1] = app.create_match(number, game_id, roster)
app.save_card(card)
print(f"\nCard saved to {card['path']}")
return card
def random_match(game_id: str, roster: list[str], used: set[str],
profile: dict[str, Any] | None = None) -> dict[str, Any]:
definition = app.random.choice(app.match_type_definitions(game_id, profile))
match_name, side_sizes = definition["name"], tuple(definition["sides"])
used_folded = {name.casefold() for name in used}
available = [name for name in roster if name.casefold() not in used_folded]
if len(available) < sum(side_sizes):
available = list(roster)
picks = app.random.sample(available, sum(side_sizes))
sides: list[list[str]] = []
cursor = 0
for size in side_sizes:
sides.append(picks[cursor:cursor + size])
cursor += size
used.update(name.casefold() for name in picks)
return {
"title": app.random.choice(["", "Opening Match", "Grudge Match", "Main Event"]),
"type": match_name, "sides": sides, "winner_side": None, "finish": "",
"promo": None, "image": "", "ranking_division": None,
"title_defense": False, "rivalry_payoff": False,
"match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None,
"base_rating": definition["base_rating"],
"valid_outcomes": list(definition["outcomes"]),
}
def create_random_card() -> dict[str, Any]:
"""Create a randomized three-match show after the user selects a game."""
game_id = app.choose_game()
roster = app.load_roster(game_id)
show = app.random.choice(list(app.game_show_names(game_id).values()))
city, attendance = app.random_location()
show_date = date.today().isoformat()
used: set[str] = set()
card: dict[str, Any] = {
"game_id": game_id, "game_title": app.game_title(game_id),
"federation_name": app.federation_name(game_id),
"title_definitions": app.title_definitions_for_game(game_id),
"show": show, "date": show_date,
"city": city, "attendance": attendance,
"matches": [app.random_match(game_id, roster, used) for _ in range(3)],
"path": str(app.default_card_path(show, show_date)),
}
app.save_card(card)
print(f"\nRandom card saved to {card['path']}")
return card
def pending_indices(card: dict[str, Any]) -> list[int]:
return [index for index, match in enumerate(card["matches"])
if match.get("sides") and match.get("winner_side") is None]
def editable_indices(card: dict[str, Any]) -> list[int]:
return [index for index, match in enumerate(card["matches"])
if match.get("winner_side") is None]
def choose_match_index(card: dict[str, Any], indices: list[int], prompt: str) -> int:
labels: list[str] = []
for index in indices:
match = card["matches"][index]
title = f" — {match['title']}" if match.get("title") else ""
state = " · not configured" if not match.get("sides") else ""
labels.append(f"Match {index + 1}: {match['type']}{title}{state}")
selected = app.ask_choice(prompt, indices, labels=labels)
return indices[selected]
def season_card_exit_label(card: dict[str, Any]) -> str:
if not card.get("season_id"):
return "Back to main menu"
complete = all(match.get("winner_side") is not None for match in card["matches"])
return "Advance to Next Show" if complete else "Return to Season Menu"
def choose_ranking_division(game_id: str, current: str | None = None,
allow_unranked: bool = True,
title_definitions: list[dict[str, str]] | None = None) -> str | None:
titles = title_definitions or app.title_definitions_for_game(game_id)
options: list[str | None] = ([None] if allow_unranked else []) + [title["name"] for title in titles]
labels = ["Unranked / Others pool" if item is None else item for item in options]
default = options.index(current) + 1 if current in options else None
return options[app.ask_choice("Assign ranking division", options,
default=default, labels=labels)]
def edit_match(card: dict[str, Any]) -> None:
if card.get("season_id"):
try:
season = app.load_season(str(card["season_id"]))
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(f"Cannot verify season control mode: {exc}")
season = None
if season and season.get("control_mode") == "wrestler":
print("Matches cannot be edited in Wrestler control.")
return
editable = app.editable_indices(card)
if not editable:
print("There are no unfinished matches to edit.")
return
index = app.choose_match_index(card, editable, "Choose a match to edit")
match = card["matches"][index]
roster = app.load_roster(card["game_id"])
options = app.match_type_definitions(card["game_id"])
names = [item["name"] for item in options] + ["Keep current"]
labels = [f"{item['name']} ({' vs '.join(map(str, item['sides']))})"
for item in options]
labels.append(f"Keep current: {match['type']}")
selected = app.ask_choice("Choose match type", names, labels=labels)
old_sizes = tuple(len(side) for side in match.get("sides", []))
if selected < len(options):
match["type"] = options[selected]["name"]
side_sizes = tuple(options[selected]["sides"])
else:
found = next((entry for entry in options if entry["name"] == match["type"]), options[0])
side_sizes = tuple(found["sides"])
needs_participants = old_sizes != side_sizes
if needs_participants or app.ask("Change participants too? (y/N)").lower() == "y":
match["sides"] = app.choose_sides(side_sizes, roster)
profile = app.match_type_definition(card["game_id"], match["type"])
match["base_rating"] = profile["base_rating"]
match["valid_outcomes"] = list(profile["outcomes"])
current_title = match.get("title", "")
match["title"] = app.ask(f"Match title (blank removes; current: {current_title or 'none'})")
if card.get("season_id"):
match["ranking_division"] = app.choose_ranking_division(
card["game_id"], match.get("ranking_division"), allow_unranked=True,
title_definitions=card.get("title_definitions"),
)
if match["ranking_division"]:
match["title_defense"] = app.ask_yes_no(
"Is this a title defense?", default=bool(match.get("title_defense"))
)
else:
match["title_defense"] = False
app.save_card(card)
print(f"Match updated and saved to {card['path']}.")
def resume_card(card: dict[str, Any]) -> bool:
season = None
if card.get("season_id"):
try:
season = app.load_season(str(card["season_id"]))
except (OSError, ValueError, json.JSONDecodeError):
season = None
if season and season.get("control_mode") == "wrestler":
app.simulate_non_player_matches(card, season)
app.sync_season_card_results(card)
while True:
print(f"\n{app.show_location_text(card)} · {card.get('federation_name', app.federation_name(card['game_id']))}"
f" · {card.get('game_title', app.game_title(card['game_id']))} · {card['date']}")
if card.get("ppv_rating") is not None:
print(f"PPV Rating: {float(card['ppv_rating']):.1f}%")
print(f"Card: {card['path']}")
wrestler_mode = bool(season and season.get("control_mode") == "wrestler")
if wrestler_mode:
actions = ["view", "result", "promo", "image", "exit"]
labels = ["View card", "Record your match result",
"Generate a promo before the next match", "Attach/edit a match image",
app.season_card_exit_label(card)]
else:
actions = ["view", "result", "edit", "promo", "image", "simulate all", "exit"]
labels = ["View card", "Record a match result", "Edit an unfinished match",
"Generate a promo before the next match", "Attach/edit a match image",
"Auto-simulate all remaining matches", app.season_card_exit_label(card)]
choice = app.ask_choice("Card action", actions, labels=labels)
if choice == 0:
app.view_card(card)
elif choice == 1:
app.record_result(card)
elif choice == 2 and not wrestler_mode:
app.edit_match(card)
elif choice == (2 if wrestler_mode else 3):
app.generate_promo(card)
elif choice == (3 if wrestler_mode else 4):
app.attach_match_image(card)
elif choice == 5 and not wrestler_mode:
app.simulate_pending_matches(card)
else:
return bool(card.get("season_id"))
__all__ = ['ask_location', 'choose_game', 'choose_match_index', 'choose_match_type', 'choose_ranking_division', 'choose_sides', 'create_card', 'create_match', 'create_random_card', 'edit_match', 'editable_indices', 'game_show_names', 'is_ppv_card', 'parse_attendance', 'pending_indices', 'random_attendance', 'random_location', 'random_match', 'resume_card', 'season_card_exit_label', 'select_card_path', 'show_location_text', 'show_name_for_brand']