Add attendance count and city listing for shows
This commit is contained in:
parent
971b347b99
commit
a0e8575ab2
3 changed files with 207 additions and 5 deletions
|
|
@ -11,6 +11,12 @@ Cards are saved as Markdown in `cards/`. Load a card to view it, record results,
|
|||
edit unfinished matches, or add a randomized promo before the next match. A card
|
||||
is saved after each edit or result, so you can stop and resume later.
|
||||
|
||||
When creating a show, enter a city and attendance or press `r` to randomize
|
||||
both; entering `r` at the attendance prompt after typing a city randomizes only
|
||||
the attendance. Venues span the US, Canada, Europe, and Japan. The location is
|
||||
stored in the card and listed next to the show name in the card header and the
|
||||
saved-card listing.
|
||||
|
||||
The promo generator gives selected wrestlers distinct voices and uses a generic
|
||||
style for wrestlers without a dedicated profile. Roster files share the schema
|
||||
`{"game_id": "sd1", "game": "...", "wrestlers": ["..."]}`.
|
||||
|
|
|
|||
|
|
@ -70,6 +70,47 @@ MATCH_TYPES: dict[str, list[tuple[str, tuple[int, ...]]]] = {
|
|||
],
|
||||
}
|
||||
|
||||
# Typical live-event attendance per city; randomization picks a city then an
|
||||
# attendance within its venue range. Unknown cities use the default range.
|
||||
CITY_ATTENDANCE_RANGES: dict[str, tuple[int, int]] = {
|
||||
# United States
|
||||
"New York, NY": (17000, 19800),
|
||||
"Boston, MA": (14500, 17500),
|
||||
"Philadelphia, PA": (15000, 19500),
|
||||
"Chicago, IL": (18000, 22000),
|
||||
"Detroit, MI": (15000, 20000),
|
||||
"Atlanta, GA": (12000, 17000),
|
||||
"Dallas, TX": (14000, 18000),
|
||||
"Los Angeles, CA": (15000, 19000),
|
||||
"San Francisco, CA": (13000, 16000),
|
||||
"Seattle, WA": (12000, 15500),
|
||||
"Minneapolis, MN": (12000, 16500),
|
||||
"Nashville, TN": (10000, 15000),
|
||||
"Miami, FL": (12500, 15500),
|
||||
# Canada
|
||||
"Toronto, ON": (15000, 19000),
|
||||
"Montreal, QC": (16000, 20000),
|
||||
"Vancouver, BC": (14000, 17000),
|
||||
"Calgary, AB": (9500, 13000),
|
||||
# Europe
|
||||
"London, England": (12000, 17000),
|
||||
"Manchester, England": (9000, 12500),
|
||||
"Dublin, Ireland": (9000, 13000),
|
||||
"Paris, France": (13000, 17000),
|
||||
"Berlin, Germany": (11000, 14500),
|
||||
"Amsterdam, Netherlands": (9000, 12000),
|
||||
"Madrid, Spain": (10000, 13500),
|
||||
"Milan, Italy": (9000, 12500),
|
||||
# Japan
|
||||
"Tokyo, Japan": (15000, 45000),
|
||||
"Yokohama, Japan": (12000, 18000),
|
||||
"Osaka, Japan": (11000, 17000),
|
||||
"Nagoya, Japan": (9000, 12000),
|
||||
"Sapporo, Japan": (8000, 11000),
|
||||
"Fukuoka, Japan": (8000, 11000),
|
||||
}
|
||||
DEFAULT_ATTENDANCE_RANGE = (7000, 15000)
|
||||
|
||||
RESET = "\033[0m"
|
||||
RED = "\033[31m"
|
||||
YELLOW = "\033[33m"
|
||||
|
|
@ -137,6 +178,8 @@ def save_card(card: dict[str, Any]) -> None:
|
|||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lines = [
|
||||
f"# Show: {json.dumps(card['show'], ensure_ascii=False)}",
|
||||
f"- City: {json.dumps(card.get('city', ''), ensure_ascii=False)}",
|
||||
f"- Attendance: {json.dumps(card.get('attendance'))}",
|
||||
f"- Game ID: {json.dumps(card['game_id'])}",
|
||||
f"- Game: {json.dumps(GAMES[card['game_id']]['title'], ensure_ascii=False)}",
|
||||
f"- Date: {json.dumps(card['date'])}",
|
||||
|
|
@ -179,7 +222,7 @@ def load_card(path: Path) -> dict[str, Any]:
|
|||
value = json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"Invalid Markdown card field '{key}' in {path}") from exc
|
||||
if key in ("game id", "date"):
|
||||
if key in ("game id", "date", "city", "attendance"):
|
||||
card[key.replace(" ", "_")] = value
|
||||
elif current is not None:
|
||||
mapping = {"title": "title", "type": "type", "sides": "sides",
|
||||
|
|
@ -195,6 +238,12 @@ def load_card(path: Path) -> dict[str, Any]:
|
|||
"winner_side": None, "finish": "", "promo": None})
|
||||
if not card.get("show") or not card.get("date"):
|
||||
raise ValueError(f"Card is missing its show name or date: {path}")
|
||||
card.setdefault("city", "")
|
||||
card.setdefault("attendance", None)
|
||||
if card["attendance"] is not None and (
|
||||
not isinstance(card["attendance"], int) or card["attendance"] <= 0
|
||||
):
|
||||
raise ValueError(f"Card has an invalid attendance: {path}")
|
||||
for match in card["matches"]:
|
||||
if any(not side for side in match.get("sides", [])):
|
||||
raise ValueError(f"A match is missing participants: {path}")
|
||||
|
|
@ -204,6 +253,68 @@ def load_card(path: Path) -> dict[str, Any]:
|
|||
return card
|
||||
|
||||
|
||||
def random_attendance(city: str) -> int:
|
||||
"""Pick an attendance within the city's typical venue range."""
|
||||
low, high = CITY_ATTENDANCE_RANGES.get(city, DEFAULT_ATTENDANCE_RANGE)
|
||||
return random.randint(low, high)
|
||||
|
||||
|
||||
def random_location() -> tuple[str, int]:
|
||||
city = random.choice(sorted(CITY_ATTENDANCE_RANGES))
|
||||
return city, 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 = ask("City (r randomizes city and attendance)").strip()
|
||||
if city.casefold() == "r":
|
||||
city, attendance = 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 = ask(f"Attendance in {city} (r to randomize)").strip()
|
||||
if raw.casefold() == "r":
|
||||
attendance = random_attendance(city)
|
||||
print(f"Random attendance: {attendance:,} fans")
|
||||
return city, attendance
|
||||
attendance = 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 card_summary(path: Path) -> str:
|
||||
"""One-line listing for a saved card; falls back to the file stem."""
|
||||
try:
|
||||
return show_location_text(load_card(path))
|
||||
except (OSError, ValueError, json.JSONDecodeError):
|
||||
return path.stem
|
||||
|
||||
|
||||
def choose_game() -> str:
|
||||
ids = list(GAMES)
|
||||
print("\nChoose a game:")
|
||||
|
|
@ -287,7 +398,7 @@ def select_card_path() -> Path | None:
|
|||
if candidates:
|
||||
print("\nSaved cards:")
|
||||
for index, path in enumerate(candidates, 1):
|
||||
print(f" {index}. {path.name}")
|
||||
print(f" {index}. {card_summary(path)} [{path.name}]")
|
||||
print(" Enter a number to load a listed card, or type a path.")
|
||||
raw = ask("Card path (blank to cancel)")
|
||||
if not raw:
|
||||
|
|
@ -311,8 +422,10 @@ def create_card() -> dict[str, Any]:
|
|||
break
|
||||
except ValueError:
|
||||
print("Enter a valid date in YYYY-MM-DD format.")
|
||||
city, attendance = ask_location()
|
||||
card: dict[str, Any] = {
|
||||
"game_id": game_id, "show": show, "date": show_date,
|
||||
"city": city, "attendance": attendance,
|
||||
"matches": [{"title": "", "type": "Singles", "sides": [], "winner_side": None,
|
||||
"finish": "", "promo": None} for _ in range(3)],
|
||||
"path": str(default_card_path(show, show_date)),
|
||||
|
|
@ -505,7 +618,7 @@ def header_lines(card: dict[str, Any], width: int) -> list[str]:
|
|||
if width < 62:
|
||||
title = GAMES[card["game_id"]]["title"]
|
||||
return [f"{color('WWF.com', RED)} {color(title, CYAN)}",
|
||||
f"{card['date']} · {card['show']}", ""]
|
||||
f"{card['date']} · {show_location_text(card)}", ""]
|
||||
left_x = 1
|
||||
center_x = max(left_x + left_width + 3, (width - center_width) // 2)
|
||||
date_text = card["date"]
|
||||
|
|
@ -525,7 +638,7 @@ def header_lines(card: dict[str, Any], width: int) -> list[str]:
|
|||
if index == 0 and date_x > cursor:
|
||||
segments.append(" " * (date_x - cursor) + color(date_text, DIM))
|
||||
output.append("".join(segments))
|
||||
output.append(color(short(card["show"], width - 2), CYAN))
|
||||
output.append(color(short(show_location_text(card), width - 2), CYAN))
|
||||
return output
|
||||
|
||||
|
||||
|
|
@ -1023,7 +1136,7 @@ def generate_promo(card: dict[str, Any]) -> None:
|
|||
|
||||
def resume_card(card: dict[str, Any]) -> None:
|
||||
while True:
|
||||
print(f"\n{card['show']} · {GAMES[card['game_id']]['title']} · {card['date']}")
|
||||
print(f"\n{show_location_text(card)} · {GAMES[card['game_id']]['title']} · {card['date']}")
|
||||
print(f"Card: {card['path']}")
|
||||
print(" 1. View card")
|
||||
print(" 2. Record a match result")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
|
@ -113,6 +114,88 @@ class PromoVoiceTests(unittest.TestCase):
|
|||
self.assertNotIn(":", line)
|
||||
|
||||
|
||||
class LocationTests(unittest.TestCase):
|
||||
def ask_location(self, answers: list[str]) -> tuple[str, int]:
|
||||
with contextlib.redirect_stdout(io.StringIO()), \
|
||||
patch("builtins.input", side_effect=answers):
|
||||
return app.ask_location()
|
||||
|
||||
def test_random_location_uses_known_city_range(self):
|
||||
city, attendance = app.random_location()
|
||||
self.assertIn(city, app.CITY_ATTENDANCE_RANGES)
|
||||
low, high = app.CITY_ATTENDANCE_RANGES[city]
|
||||
self.assertGreaterEqual(attendance, low)
|
||||
self.assertLessEqual(attendance, high)
|
||||
|
||||
def test_manual_city_and_attendance_are_used_as_entered(self):
|
||||
self.assertEqual(self.ask_location(["Boston, MA", "15,450"]), ("Boston, MA", 15450))
|
||||
|
||||
def test_random_city_prompt_skips_attendance_question(self):
|
||||
with contextlib.redirect_stdout(io.StringIO()), \
|
||||
patch("builtins.input", side_effect=["r"]):
|
||||
city, attendance = app.ask_location()
|
||||
self.assertIn(city, app.CITY_ATTENDANCE_RANGES)
|
||||
low, high = app.CITY_ATTENDANCE_RANGES[city]
|
||||
self.assertTrue(low <= attendance <= high)
|
||||
|
||||
def test_random_attendance_follows_entered_city_range(self):
|
||||
city, attendance = self.ask_location(["Osaka, Japan", "r"])
|
||||
self.assertEqual(city, "Osaka, Japan")
|
||||
low, high = app.CITY_ATTENDANCE_RANGES["Osaka, Japan"]
|
||||
self.assertTrue(low <= attendance <= high)
|
||||
|
||||
def test_invalid_attendance_reprompts(self):
|
||||
self.assertEqual(self.ask_location(["Tokyo, Japan", "abc", "0", "18000"]),
|
||||
("Tokyo, Japan", 18000))
|
||||
|
||||
def test_unknown_city_random_attendance_uses_default_range(self):
|
||||
low, high = app.DEFAULT_ATTENDANCE_RANGE
|
||||
for _ in range(30):
|
||||
self.assertTrue(low <= app.random_attendance("Springfield, OR") <= high)
|
||||
|
||||
def test_markdown_preserves_city_and_attendance(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "card.md"
|
||||
card = MarkdownTests().make_card(path)
|
||||
card["city"] = "Toronto, ON"
|
||||
card["attendance"] = 17500
|
||||
app.save_card(card)
|
||||
loaded = app.load_card(path)
|
||||
self.assertEqual(loaded["city"], "Toronto, ON")
|
||||
self.assertEqual(loaded["attendance"], 17500)
|
||||
|
||||
def test_legacy_card_without_location_still_loads(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "legacy.md"
|
||||
card = MarkdownTests().make_card(path)
|
||||
app.save_card(card)
|
||||
text = path.read_text(encoding="utf-8")
|
||||
path.write_text("\n".join(
|
||||
line for line in text.splitlines()
|
||||
if not line.startswith(("- City:", "- Attendance:"))
|
||||
) + "\n", encoding="utf-8")
|
||||
loaded = app.load_card(path)
|
||||
self.assertEqual(loaded["city"], "")
|
||||
self.assertIsNone(loaded["attendance"])
|
||||
|
||||
def test_header_and_listing_show_location_next_to_show_name(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
path = Path(folder) / "card.md"
|
||||
card = MarkdownTests().make_card(path)
|
||||
card["city"] = "Osaka, Japan"
|
||||
card["attendance"] = 12000
|
||||
app.save_card(card)
|
||||
with patch.object(app, "_render_image", return_value=([], 0)):
|
||||
header = "\n".join(app.header_lines(card, 100))
|
||||
self.assertIn('The "Big" Show', header)
|
||||
self.assertIn("Osaka, Japan", header)
|
||||
self.assertIn("12,000 fans", header)
|
||||
summary = app.card_summary(path)
|
||||
self.assertIn('The "Big" Show', summary)
|
||||
self.assertIn("Osaka, Japan", summary)
|
||||
self.assertIn("12,000 fans", summary)
|
||||
|
||||
|
||||
class FlashRenderingTests(unittest.TestCase):
|
||||
def test_flash_updates_only_changed_rows_not_static_header(self):
|
||||
card = MarkdownTests().make_card(Path("card.md"))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue