420 lines
18 KiB
Python
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']
|