1672 lines
86 KiB
Python
1672 lines
86 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import lvpwb as app
|
|
|
|
|
|
class RosterTests(unittest.TestCase):
|
|
def test_rosters_load_from_uniform_json_payloads(self):
|
|
self.assertEqual(len(app.load_roster("sd1")), 36)
|
|
self.assertEqual(len(app.load_roster("sd2")), 65)
|
|
self.assertEqual(len(app.load_roster("sd3")), 43)
|
|
for game_id, game in app.GAMES.items():
|
|
payload = json.loads((app.ROOT / game["roster"]).read_text(encoding="utf-8"))
|
|
self.assertEqual(set(payload), {"game_id", "game", "wrestlers"})
|
|
self.assertEqual(payload["game_id"], game_id)
|
|
self.assertEqual(payload["game"], game["title"])
|
|
|
|
|
|
class MarkdownTests(unittest.TestCase):
|
|
def make_card(self, path: Path) -> dict:
|
|
def profile_fields(match_type: str) -> dict:
|
|
definition = app.match_type_definition("sd2", match_type)
|
|
return {"base_rating": definition["base_rating"],
|
|
"valid_outcomes": list(definition["outcomes"])}
|
|
|
|
return {
|
|
"game_id": "sd2",
|
|
"show": 'The "Big" Show',
|
|
"date": "2026-10-25",
|
|
"path": str(path),
|
|
"matches": [
|
|
{"title": "Opener", "type": "Singles", "sides": [["The Rock"], ["Kane"]],
|
|
"winner_side": 0, "finish": "pinfall", "promo": None, "image": "",
|
|
"ranking_division": None, "title_defense": False, "rivalry_payoff": False,
|
|
"match_time_minutes": None, "near_falls": 0, "finishers": 0, "rating": None,
|
|
**profile_fields("Singles")},
|
|
{"title": "", "type": "Tag Team", "sides": [["Edge", "Christian"],
|
|
["The Hardy Boyz", "Tazz"]], "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, **profile_fields("Tag Team")},
|
|
{"title": "", "type": "Triple Threat", "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,
|
|
**profile_fields("Triple Threat")},
|
|
],
|
|
}
|
|
|
|
def test_markdown_round_trip_including_incomplete_match(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "card.md"
|
|
card = self.make_card(path)
|
|
app.save_card(card)
|
|
loaded = app.load_card(path)
|
|
self.assertEqual(loaded["show"], card["show"])
|
|
self.assertEqual(loaded["game_id"], card["game_id"])
|
|
self.assertEqual(loaded["matches"], card["matches"])
|
|
|
|
def test_load_pads_short_card_to_three_matches(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "short.md"
|
|
card = self.make_card(path)
|
|
card["matches"] = card["matches"][:1]
|
|
app.save_card(card)
|
|
loaded = app.load_card(path)
|
|
self.assertEqual(len(loaded["matches"]), 3)
|
|
self.assertEqual(loaded["matches"][1]["sides"], [])
|
|
|
|
def test_render_shows_match_names_and_recorded_winner(self):
|
|
card = self.make_card(Path("card.md"))
|
|
with patch.object(app, "_render_image", return_value=([], 0)):
|
|
rendered = app.render_card(card)
|
|
flashing = app.render_card(card, flash=True)
|
|
self.assertIn("Match 1 — Opener", rendered)
|
|
self.assertIn("The Rock", rendered)
|
|
self.assertIn("WIN!", flashing)
|
|
self.assertIn("UP NEXT", rendered)
|
|
main_event_position = rendered.index("Match 3")
|
|
second_match_position = rendered.index("Match 2")
|
|
opener_position = rendered.index("Match 1 — Opener")
|
|
self.assertLess(main_event_position, second_match_position)
|
|
self.assertLess(second_match_position, opener_position)
|
|
|
|
|
|
class RatingTests(unittest.TestCase):
|
|
def test_fixed_match_type_bases(self):
|
|
self.assertEqual(app.base_match_rating("Singles"), 4.5)
|
|
self.assertEqual(app.base_match_rating("TLC"), 7.5)
|
|
self.assertEqual(app.base_match_rating("Hell in a Cell"), 9.5)
|
|
|
|
def test_time_near_falls_and_finishers_drive_match_rating(self):
|
|
match = {"type": "Singles", "match_time_minutes": 12.9,
|
|
"near_falls": 2, "finishers": 3, "title_defense": False}
|
|
self.assertEqual(app.calculate_match_rating(match), 7.6)
|
|
match["title_defense"] = True
|
|
self.assertEqual(app.calculate_match_rating(match), 11.4)
|
|
|
|
def test_combined_live_bonus_is_capped_before_title_multiplier(self):
|
|
match = {"type": "Singles", "match_time_minutes": 200,
|
|
"near_falls": 100, "finishers": 100, "title_defense": True}
|
|
self.assertEqual(app.calculate_match_rating(match), 21.8)
|
|
|
|
def test_match_duration_and_counts_validate(self):
|
|
self.assertEqual(app.parse_match_minutes("12.5"), 12.5)
|
|
self.assertEqual(app.parse_match_minutes("0"), 0)
|
|
for invalid in ("-1", "nan", "inf", "abc"):
|
|
self.assertIsNone(app.parse_match_minutes(invalid))
|
|
self.assertEqual(app.parse_action_count("3"), 3)
|
|
self.assertIsNone(app.parse_action_count("-1"))
|
|
self.assertIsNone(app.parse_action_count("2.5"))
|
|
|
|
def test_ppv_uses_weighted_three_match_score_only_when_all_are_rated(self):
|
|
card = {"show": "WWF PPV", "matches": [{"rating": value}
|
|
for value in (10.0, 20.0, 30.0)]}
|
|
self.assertEqual(app.calculate_ppv_rating(card), 23.0)
|
|
card["matches"][1]["rating"] = None
|
|
self.assertIsNone(app.calculate_ppv_rating(card))
|
|
card["show"] = "Monday Night Raw"
|
|
self.assertIsNone(app.calculate_ppv_rating(card))
|
|
|
|
def test_match_and_ppv_ratings_round_trip_in_markdown(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "ppv.md"
|
|
card = MarkdownTests().make_card(path)
|
|
card["show"] = "WWF PPV"
|
|
for match, rating in zip(card["matches"], (10.0, 20.0, 30.0)):
|
|
match.update(match_time_minutes=12.5, near_falls=2, finishers=1, rating=rating)
|
|
app.save_card(card)
|
|
loaded = app.load_card(path)
|
|
self.assertEqual(loaded["matches"][1]["match_time_minutes"], 12.5)
|
|
self.assertEqual(loaded["matches"][1]["near_falls"], 2)
|
|
self.assertEqual(loaded["matches"][1]["finishers"], 1)
|
|
self.assertEqual(loaded["matches"][1]["rating"], 20.0)
|
|
self.assertEqual(loaded["ppv_rating"], 23.0)
|
|
with patch.object(app, "_render_image", return_value=([], 0)):
|
|
rendered = app.render_card(loaded)
|
|
self.assertIn("WWF PPV Rating: 23.0%", rendered)
|
|
|
|
def test_historical_brand_names_use_thursday_and_generic_ppv(self):
|
|
self.assertEqual(app.show_name_for_brand("SmackDown"), "Thursday Night SmackDown!")
|
|
self.assertEqual(app.show_name_for_brand("PPV"), "WWF PPV")
|
|
|
|
def test_result_prompt_records_duration_near_falls_finishers_and_rating(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
card = MarkdownTests().make_card(Path(folder) / "card.md")
|
|
answers = ["1", "1", "1", "1", "12.5", "3", "2"]
|
|
with patch("builtins.input", side_effect=answers), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.record_result(card)
|
|
match = card["matches"][1]
|
|
self.assertEqual(match["winner_side"], 0)
|
|
self.assertEqual(match["match_time_minutes"], 12.5)
|
|
self.assertEqual(match["near_falls"], 3)
|
|
self.assertEqual(match["finishers"], 2)
|
|
self.assertEqual(match["rating"], 7.3)
|
|
|
|
|
|
class MatchLabelTests(unittest.TestCase):
|
|
def test_ranking_division_alone_is_labeled_non_title(self):
|
|
match = {"title": "", "type": "Singles", "sides": [["The Undertaker"], ["Viscera"]],
|
|
"winner_side": None, "ranking_division": "European", "title_defense": False}
|
|
rendered = "\n".join(app.match_lines(match, 1, 100))
|
|
self.assertIn("NON-TITLE MATCH", rendered)
|
|
self.assertNotIn("EUROPEAN", rendered)
|
|
|
|
def test_only_explicit_title_match_displays_championship(self):
|
|
match = {"title": "European Championship", "type": "Singles",
|
|
"sides": [["The Undertaker"], ["Viscera"]], "winner_side": None,
|
|
"ranking_division": "European", "title_defense": True}
|
|
rendered = "\n".join(app.match_lines(match, 1, 100))
|
|
self.assertIn("EUROPEAN TITLE MATCH", rendered)
|
|
self.assertNotIn("NON-TITLE MATCH", rendered)
|
|
|
|
|
|
class PromoVoiceTests(unittest.TestCase):
|
|
def test_rock_austin_and_triple_h_have_signature_voices(self):
|
|
with patch.object(app.random, "choice", side_effect=lambda options: options[0]):
|
|
rock = app.promo_line_for("The Rock", "Mankind", "openers")
|
|
austin = app.promo_line_for("Stone Cold Steve Austin", "The Rock", "challenges")
|
|
triple_h = app.promo_line_for("Triple H", "Kane", "openers")
|
|
self.assertIn("jabroni", rock.lower())
|
|
with patch.object(app.random, "choice", side_effect=lambda options: options[0]):
|
|
rock_with_opponent = app.promo_line_for("The Rock", "Jeff Hardy", "openers")
|
|
self.assertIn("jabroni Jeff Hardy—", rock_with_opponent)
|
|
self.assertIn("bottom line", austin.lower())
|
|
self.assertIn("I am the Game", triple_h)
|
|
|
|
def test_mankind_has_persecuted_little_guy_twisted_idiom_and_pain(self):
|
|
with patch.object(app.random, "choice", side_effect=lambda options: options[0]):
|
|
line = app.promo_line_for("Mick Foley", "The Rock", "challenges")
|
|
self.assertIn("little guy", line)
|
|
self.assertIn("worm gets the bird", line)
|
|
self.assertIn("bricks", line)
|
|
|
|
def test_unprofiled_wrestler_uses_generic_fallback(self):
|
|
with patch.object(app.random, "choice", side_effect=lambda options: options[0]):
|
|
line = app.promo_line_for("Debra", "The Rock", "openers")
|
|
self.assertIn(app.PROMO_OPENERS[0], line)
|
|
|
|
def test_every_profile_formats_for_each_promo_turn(self):
|
|
with patch.object(app.random, "choice", side_effect=lambda options: options[0]):
|
|
for alias in app.PROMO_PROFILE_ALIASES:
|
|
for moment in ("openers", "challenges", "retorts"):
|
|
line = app.promo_line_for(alias, "Jeff Hardy", moment)
|
|
self.assertTrue(line)
|
|
self.assertNotIn(":", line, f"Unexpected speaker-like colon for {alias}: {line}")
|
|
|
|
for profile_key, profile in app.PROMO_PROFILES.items():
|
|
for moment in ("openers", "challenges", "retorts"):
|
|
for template in profile[moment]:
|
|
line = template.format(opponent="Jeff Hardy", wrestler="Test Wrestler")
|
|
self.assertNotIn(":", line, f"Unexpected colon in {profile_key}: {line}")
|
|
|
|
for bank in (app.PROMO_OPENERS, app.PROMO_CHALLENGES, app.PROMO_RETORTS):
|
|
for line in bank:
|
|
self.assertNotIn(":", line)
|
|
|
|
|
|
class MatchOutcomeTests(unittest.TestCase):
|
|
def test_match_types_only_offer_legal_win_conditions(self):
|
|
self.assertEqual(app.allowed_match_outcomes("Ladder"), ("Item retrieval",))
|
|
self.assertEqual(app.allowed_match_outcomes("TLC"), ("Item retrieval",))
|
|
self.assertEqual(app.allowed_match_outcomes("Table"), ("Table break",))
|
|
self.assertEqual(app.allowed_match_outcomes("Battle Royal"),
|
|
("Last wrestler standing",))
|
|
self.assertEqual(app.allowed_match_outcomes("Cage"), ("Escape the cage",))
|
|
self.assertEqual(app.allowed_match_outcomes("Singles"), ("Pinfall", "Submission"))
|
|
|
|
def test_auto_sim_uses_only_valid_finish_and_randomizes_match_stats(self):
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
card["matches"][1].update(type="Ladder", sides=[["Edge"], ["Kane"]])
|
|
with patch.object(app.random, "choices", return_value=[0]), \
|
|
patch.object(app.random, "choice", side_effect=lambda options: options[0]), \
|
|
patch.object(app.random, "randint", side_effect=lambda low, high: low):
|
|
result = app.simulate_match_result(card, 1)
|
|
self.assertEqual(result["finish"], "Item retrieval")
|
|
self.assertIn(result["winner_side"], (0, 1))
|
|
self.assertIsNotNone(result["match_time_minutes"])
|
|
self.assertIsNotNone(result["rating"])
|
|
|
|
def test_auto_simulate_all_completes_and_persists_pending_matches(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "card.md"
|
|
card = MarkdownTests().make_card(path)
|
|
card["matches"][1]["type"] = "Ladder"
|
|
with patch.object(app.random, "choices", return_value=[1]), \
|
|
patch.object(app.random, "choice", side_effect=lambda options: options[0]), \
|
|
patch.object(app.random, "randint", side_effect=lambda low, _high: low), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.simulate_pending_matches(card)
|
|
match = app.load_card(path)["matches"][1]
|
|
self.assertEqual(match["winner_side"], 1)
|
|
self.assertEqual(match["finish"], "Item retrieval")
|
|
self.assertIsNotNone(match["rating"])
|
|
|
|
def test_season_simulation_weights_higher_ranked_sides_but_one_off_is_even(self):
|
|
division = "European"
|
|
season = app.new_season_state("Sim Test", "sd1", season_id="sim-test")
|
|
for wrestler, wins, losses in (("Kane", 5, 0), ("Mankind", 0, 5)):
|
|
season["rankings"][division][app.side_identity([wrestler])] = {
|
|
"members": [wrestler], "wins": wins, "losses": losses,
|
|
"recent": ["W"] * wins + ["L"] * losses,
|
|
}
|
|
match = {"ranking_division": division, "title_defense": False,
|
|
"sides": [["Kane"], ["Mankind"]]}
|
|
weights = app.season_simulation_weights(season, match)
|
|
self.assertGreater(weights[0], weights[1])
|
|
self.assertEqual(app.season_simulation_weights(None, match), [1.0, 1.0])
|
|
|
|
def test_ranked_non_title_bouts_are_not_labeled_contender_matches(self):
|
|
season = app.new_season_state("Test Season", "sd1", season_id="rank-label-test")
|
|
candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set())
|
|
ranked = [candidate["match"] for candidate in candidates if candidate["kind"] == "ranked"]
|
|
self.assertTrue(ranked)
|
|
self.assertTrue(all(match["title"] == "" for match in ranked))
|
|
self.assertTrue(all(not match["title_defense"] for match in ranked))
|
|
|
|
def test_season_card_exit_label_changes_after_all_matches_complete(self):
|
|
card = MarkdownTests().make_card(Path("season.md"))
|
|
card["season_id"] = "season-test"
|
|
self.assertEqual(app.season_card_exit_label(card), "Return to Season Menu")
|
|
for match in card["matches"]:
|
|
match["winner_side"] = 0
|
|
self.assertEqual(app.season_card_exit_label(card), "Advance to Next Show")
|
|
card.pop("season_id")
|
|
self.assertEqual(app.season_card_exit_label(card), "Back to main menu")
|
|
|
|
|
|
class YesNoPromptTests(unittest.TestCase):
|
|
def test_yes_no_prompt_accepts_short_and_long_answers_case_insensitively(self):
|
|
with patch("builtins.input", side_effect=["YES", "n"]):
|
|
self.assertTrue(app.ask_yes_no("Title defense?"))
|
|
self.assertFalse(app.ask_yes_no("Title defense?"))
|
|
|
|
def test_yes_no_prompt_reprompts_invalid_input_and_keeps_default_on_blank(self):
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch("builtins.input", side_effect=["maybe", "y", ""]):
|
|
self.assertTrue(app.ask_yes_no("Title defense?", default=False))
|
|
self.assertTrue(app.ask_yes_no("Title defense?", default=True))
|
|
|
|
|
|
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 ScreenshotTests(unittest.TestCase):
|
|
def make_screenshots(self, folder: Path) -> dict[str, Path]:
|
|
names = {
|
|
"old": "WWF SmackDown 2026-01-01-10-00-00.png",
|
|
"newest": "WWF SmackDown 2026-10-26-21-30-00.png",
|
|
"newer": "WWF SmackDown 2026-10-26-09-15-00.png",
|
|
"dated": "No stamp in this name.png",
|
|
}
|
|
paths = {key: folder / value for key, value in names.items()}
|
|
for path in paths.values():
|
|
path.write_bytes(b"png")
|
|
return paths
|
|
|
|
def test_parse_screenshot_stamp_reads_filename(self):
|
|
stamp = app.parse_screenshot_stamp("WWF SmackDown 2026-10-26-21-30-00.png")
|
|
self.assertEqual(stamp, datetime(2026, 10, 26, 21, 30, 0).astimezone())
|
|
self.assertIsNone(app.parse_screenshot_stamp("No stamp in this name.png"))
|
|
|
|
def test_session_filter_keeps_only_shots_after_since_with_mtime_fallback(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
directory = Path(folder)
|
|
paths = self.make_screenshots(directory)
|
|
fall_back = datetime(2026, 10, 26, 8, 0, 0).astimezone()
|
|
os.utime(paths["dated"], (fall_back.timestamp(), fall_back.timestamp()))
|
|
since = datetime(2026, 10, 26, 7, 0, 0).astimezone()
|
|
found = app.session_screenshots(since=since, directory=directory)
|
|
self.assertEqual(found, [paths["newest"], paths["newer"], paths["dated"]])
|
|
self.assertNotIn(paths["old"], app.session_screenshots(
|
|
since=datetime(2026, 10, 26, 9, 0, 0).astimezone(), directory=directory))
|
|
|
|
def test_missing_screenshot_directory_lists_nothing(self):
|
|
self.assertEqual(app.session_screenshots(directory=Path("/nonexistent-dir")), [])
|
|
|
|
def test_builtin_games_use_their_emulator_screenshot_directories(self):
|
|
self.assertEqual(app.screenshot_directory_for_game("sd1"),
|
|
app.DUCKSTATION_SCREENSHOT_DIR)
|
|
self.assertEqual(app.screenshot_directory_for_game("sd2"),
|
|
app.DUCKSTATION_SCREENSHOT_DIR)
|
|
self.assertEqual(app.screenshot_directory_for_game("sd3"),
|
|
app.PCSX2_SCREENSHOT_DIR)
|
|
self.assertTrue(app.game_has_screenshots("sd3"))
|
|
|
|
def test_attach_uses_the_games_screenshot_directory(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
card = MarkdownTests().make_card(Path(folder) / "card.md")
|
|
card["game_id"] = "sd3"
|
|
with patch.object(app, "choose_match_index", return_value=0), \
|
|
patch.object(app, "choose_screenshot", return_value=Path(folder) / "shot.png") as choose, \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.attach_match_image(card)
|
|
choose.assert_called_once_with(app.PCSX2_SCREENSHOT_DIR)
|
|
self.assertEqual(card["matches"][0]["image"], str(Path(folder) / "shot.png"))
|
|
|
|
def test_attach_flow_prompts_yes_and_stores_chosen_image(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "card.md"
|
|
card = MarkdownTests().make_card(path)
|
|
chosen = Path(folder) / "WWF SmackDown 2026-10-26-21-30-00.png"
|
|
chosen.write_bytes(b"png")
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch("builtins.input", side_effect=["2", "1"]), \
|
|
patch.object(app, "session_screenshots", return_value=[chosen]), \
|
|
patch.object(app, "DUCKSTATION_SCREENSHOT_DIR", Path(folder)):
|
|
app.attach_match_image(card)
|
|
self.assertEqual(card["matches"][1]["image"], str(chosen))
|
|
self.assertIn(f'- Image: "{chosen}"', path.read_text(encoding="utf-8"))
|
|
self.assertEqual(app.load_card(path)["matches"][1]["image"], str(chosen))
|
|
|
|
def test_attach_declines_when_game_has_no_screenshot_directory(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "card.md"
|
|
card = MarkdownTests().make_card(path)
|
|
card["game_id"] = "unknown-other-game"
|
|
before = json.dumps(card["matches"])
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch("builtins.input", side_effect=RuntimeError("unexpected prompt")):
|
|
app.attach_match_image(card)
|
|
self.assertEqual(json.dumps(card["matches"]), before)
|
|
|
|
|
|
class RandomShowTests(unittest.TestCase):
|
|
def test_randomized_card_uses_selected_games_roster_without_repeats(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
with patch.object(app, "CARD_DIR", Path(folder)), \
|
|
patch.object(app, "choose_game", return_value="sd2"), \
|
|
patch.object(app.random, "seed", 42), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
card = app.create_random_card()
|
|
roster = set(app.load_roster("sd2"))
|
|
self.assertEqual(len(card["matches"]), 3)
|
|
self.assertIn(card["show"], {"Monday Night Raw", "Thursday Night SmackDown!",
|
|
"WWF PPV"})
|
|
for match in card["matches"]:
|
|
wrestlers = [name for side in match["sides"] for name in side]
|
|
self.assertTrue(set(wrestlers) <= roster)
|
|
self.assertEqual(len(wrestlers), len(set(wrestlers)))
|
|
self.assertIsNone(match["winner_side"])
|
|
|
|
|
|
class SelectionPromptTests(unittest.TestCase):
|
|
def test_numeric_choice_shows_exact_labels_and_repeats_them_on_invalid_input(self):
|
|
with patch("builtins.input", side_effect=["3", "1"]), \
|
|
contextlib.redirect_stdout(io.StringIO()) as output:
|
|
selected = app.ask_choice("Winning method", ["Pinfall", "Submission"])
|
|
self.assertEqual(selected, 0)
|
|
self.assertEqual(output.getvalue().count("1. Pinfall"), 2)
|
|
self.assertIn("2. Submission", output.getvalue())
|
|
self.assertIn("Enter a number from 1 to 2.", output.getvalue())
|
|
|
|
def test_manual_wrestler_selection_is_alphabetical(self):
|
|
roster = app.load_roster("sd2")
|
|
ordered = sorted(roster, key=str.casefold)
|
|
with patch("builtins.input", side_effect=["1", "2"]), \
|
|
contextlib.redirect_stdout(io.StringIO()) as output:
|
|
sides = app.choose_sides((1, 1), roster)
|
|
self.assertEqual(sides, [[ordered[0]], [ordered[1]]])
|
|
listing = output.getvalue()
|
|
self.assertLess(listing.index(ordered[0]), listing.index(ordered[1]))
|
|
|
|
def test_result_mode_displays_manual_and_auto_sim_options(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
card = MarkdownTests().make_card(Path(folder) / "card.md")
|
|
with patch("builtins.input", side_effect=["1", "2"]), \
|
|
patch.object(app.random, "choices", return_value=[0]), \
|
|
patch.object(app.random, "choice", side_effect=lambda options: options[0]), \
|
|
patch.object(app.random, "randint", side_effect=lambda low, _high: low), \
|
|
contextlib.redirect_stdout(io.StringIO()) as output:
|
|
app.record_result(card)
|
|
self.assertIn("1. Enter result manually", output.getvalue())
|
|
self.assertIn("2. Auto-simulate result", output.getvalue())
|
|
self.assertIsNotNone(card["matches"][1]["rating"])
|
|
|
|
|
|
class SeasonModeTests(unittest.TestCase):
|
|
def season(self, **kwargs) -> dict:
|
|
return app.new_season_state(
|
|
kwargs.get("name", "Test Season"), "sd1",
|
|
kwargs.get("champions", {}), kwargs.get("season_id", "test-season"),
|
|
)
|
|
|
|
def test_initial_single_title_champion_options_match_displayed_indices(self):
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=0), \
|
|
patch.object(app, "choose_roster_member", return_value="Al Snow") as choose:
|
|
self.assertEqual(app.choose_initial_champion("European", "sd1", set()), ["Al Snow"])
|
|
choose.assert_called_once()
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=1), \
|
|
patch.object(app.random, "choice", return_value="The Rock"), \
|
|
patch.object(app, "choose_roster_member") as choose:
|
|
self.assertEqual(app.choose_initial_champion("European", "sd1", set()), ["The Rock"])
|
|
choose.assert_not_called()
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=2), \
|
|
patch.object(app, "choose_roster_member") as choose:
|
|
self.assertIsNone(app.choose_initial_champion("European", "sd1", set()))
|
|
choose.assert_not_called()
|
|
|
|
def test_initial_tag_team_champion_options_match_displayed_indices(self):
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=0), \
|
|
patch.object(app, "choose_roster_member", side_effect=["Al Snow", "The Rock"]):
|
|
self.assertEqual(app.choose_initial_champion("Tag Team", "sd1", set()),
|
|
["Al Snow", "The Rock"])
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=1), \
|
|
patch.object(app.random, "sample", return_value=["Al Snow", "The Rock"]), \
|
|
patch.object(app, "choose_roster_member") as choose:
|
|
self.assertEqual(app.choose_initial_champion("Tag Team", "sd1", set()),
|
|
["Al Snow", "The Rock"])
|
|
choose.assert_not_called()
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=2), \
|
|
patch.object(app, "choose_roster_member") as choose:
|
|
self.assertIsNone(app.choose_initial_champion("Tag Team", "sd1", set()))
|
|
choose.assert_not_called()
|
|
|
|
def test_raw_smackdown_ppv_cycle_repeats(self):
|
|
self.assertEqual([app.season_show_brand(n) for n in range(1, 7)],
|
|
["Raw", "SmackDown", "PPV", "Raw", "SmackDown", "PPV"])
|
|
|
|
def test_wins_and_losses_update_division_rankings(self):
|
|
season = self.season()
|
|
match = {"ranking_division": "Intercontinental", "winner_side": 0,
|
|
"sides": [["The Rock"], ["Kane"]], "title_defense": False,
|
|
"rivalry_payoff": False}
|
|
app.record_season_match(season, {"season_show": "Raw"}, match, 0)
|
|
entries = app.season_ranking_entries(season, "Intercontinental")
|
|
self.assertEqual(entries[0]["members"], ["The Rock"])
|
|
self.assertEqual(entries[0]["wins"], 1)
|
|
self.assertEqual(entries[1]["losses"], 1)
|
|
|
|
def test_title_defenses_increment_and_title_changes_reset_current_reign(self):
|
|
division = "World Wrestling Federation"
|
|
season = self.season(champions={division: ["The Rock"]})
|
|
successful = {"ranking_division": division, "title_defense": True,
|
|
"rivalry_payoff": False, "sides": [["The Rock"], ["Kane"]],
|
|
"winner_side": 0}
|
|
app.record_season_match(season, {"season_show": "Raw"}, successful, 0)
|
|
belt = season["champions"][division]
|
|
self.assertEqual(belt["successful_defenses"], 1)
|
|
self.assertEqual(belt["defense_history"][app.side_identity(["The Rock"])]
|
|
["successful_defenses"], 1)
|
|
|
|
new_champion = dict(successful, winner_side=1)
|
|
app.record_season_match(season, {"season_show": "PPV"}, new_champion, 1)
|
|
self.assertEqual(belt["holder"], ["Kane"])
|
|
self.assertEqual(belt["successful_defenses"], 0)
|
|
self.assertEqual(belt["defense_history"][app.side_identity(["The Rock"])]
|
|
["successful_defenses"], 1)
|
|
|
|
def test_tag_team_champion_is_ranked_and_defenses_count_for_team(self):
|
|
division = "Tag Team"
|
|
team = ["Edge", "Christian"]
|
|
season = self.season(champions={division: team})
|
|
match = {"ranking_division": division, "title_defense": True,
|
|
"rivalry_payoff": False, "sides": [team, ["The Hardy Boyz", "Tazz"]],
|
|
"winner_side": 0}
|
|
app.record_season_match(season, {"season_show": "SmackDown"}, match, 0)
|
|
self.assertEqual(season["champions"][division]["successful_defenses"], 1)
|
|
self.assertEqual(app.season_ranking_entries(season, division)[0]["members"], team)
|
|
|
|
def test_others_pool_is_unranked_and_excludes_title_division_participants(self):
|
|
season = self.season(champions={"European": ["Kane"]})
|
|
season["rankings"]["European"][app.side_identity(["The Rock"])] = {
|
|
"members": ["The Rock"], "wins": 1, "losses": 0, "recent": ["W"],
|
|
}
|
|
pool = app.season_other_pool(season)
|
|
self.assertNotIn("Kane", pool)
|
|
self.assertNotIn("The Rock", pool)
|
|
self.assertIn("Val Venis", pool)
|
|
self.assertNotIn("Others", app.CHAMPIONSHIPS)
|
|
|
|
def test_ppv_modestly_boosts_title_and_rivalry_booking(self):
|
|
self.assertGreater(app.title_defense_weight("title", "PPV"),
|
|
app.title_defense_weight("title", "Raw"))
|
|
self.assertGreater(app.title_defense_weight("rivalry", "PPV"),
|
|
app.title_defense_weight("rivalry", "SmackDown"))
|
|
self.assertEqual(app.title_defense_weight("pool", "PPV"),
|
|
app.title_defense_weight("pool", "Raw"))
|
|
|
|
def test_title_feud_uses_highest_ranked_eligible_challenger(self):
|
|
division = "World Wrestling Federation"
|
|
season = self.season(champions={division: ["The Rock"]})
|
|
table = season["rankings"][division]
|
|
table[app.side_identity(["Kane"])] = {
|
|
"members": ["Kane"], "wins": 5, "losses": 1, "recent": ["W", "W"],
|
|
}
|
|
table[app.side_identity(["Mankind"])] = {
|
|
"members": ["Mankind"], "wins": 2, "losses": 2, "recent": ["L", "W"],
|
|
}
|
|
candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set())
|
|
title_match = next(candidate["match"] for candidate in candidates
|
|
if candidate["kind"] == "title"
|
|
and candidate["match"]["ranking_division"] == division)
|
|
self.assertEqual(title_match["sides"], [["The Rock"], ["Kane"]])
|
|
self.assertTrue(title_match["title_defense"])
|
|
|
|
def test_vacant_championship_uses_top_two_ranked_contenders(self):
|
|
division = "European"
|
|
season = self.season()
|
|
table = season["rankings"][division]
|
|
for wrestler, wins in (("Kane", 5), ("Mankind", 3), ("The Rock", 1)):
|
|
table[app.side_identity([wrestler])] = {
|
|
"members": [wrestler], "wins": wins, "losses": 0, "recent": ["W"],
|
|
}
|
|
candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "Raw", set())
|
|
title = next(item["match"] for item in candidates
|
|
if item["kind"] == "title" and item["match"]["ranking_division"] == division)
|
|
self.assertEqual(title["sides"], [["Kane"], ["Mankind"]])
|
|
self.assertTrue(title["title"].startswith("Vacant "))
|
|
self.assertTrue(title["title_defense"])
|
|
|
|
def test_ppv_rivalry_match_is_marked_as_storyline_payoff(self):
|
|
season = self.season()
|
|
sides = [["The Rock"], ["Stone Cold Steve Austin"]]
|
|
rivalry_id = app.season_rivalry_id(sides)
|
|
season["rivalries"][rivalry_id] = {
|
|
"sides": sides, "meetings": 3, "winner_counts": {}, "resolved": False,
|
|
"ranking_division": "World Wrestling Federation",
|
|
}
|
|
candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "PPV", set())
|
|
feud = next(candidate["match"] for candidate in candidates
|
|
if candidate["kind"] == "rivalry")
|
|
self.assertTrue(feud["rivalry_payoff"])
|
|
self.assertEqual(feud["title"], "Rivalry Finale")
|
|
|
|
def test_ppv_title_match_can_resolve_an_existing_title_feud(self):
|
|
division = "World Wrestling Federation"
|
|
season = self.season(champions={division: ["The Rock"]})
|
|
challenger = ["Kane"]
|
|
season["rankings"][division][app.side_identity(challenger)] = {
|
|
"members": challenger, "wins": 3, "losses": 1, "recent": ["W", "W"],
|
|
}
|
|
sides = [["The Rock"], challenger]
|
|
season["rivalries"][app.season_rivalry_id(sides)] = {
|
|
"sides": sides, "meetings": 1, "winner_counts": {}, "resolved": False,
|
|
"ranking_division": division,
|
|
}
|
|
candidates = app.season_booking_candidates(season, app.load_roster("sd1"), "PPV", set())
|
|
title = next(item["match"] for item in candidates if item["kind"] == "title")
|
|
self.assertTrue(title["title_defense"])
|
|
self.assertTrue(title["rivalry_payoff"])
|
|
self.assertIn("Feud Finale", title["title"])
|
|
|
|
def test_booking_caps_title_defenses_at_one_weekly_and_two_on_ppv(self):
|
|
champions = {
|
|
"Tag Team": ["Road Dogg", "Billy Gunn"],
|
|
"World Wrestling Federation": ["The Rock"],
|
|
"Intercontinental": ["The Undertaker"],
|
|
"European": ["Kane"],
|
|
"Hardcore": ["Mankind"],
|
|
"Light-Heavyweight": ["X-Pac"],
|
|
"Women's": ["Chyna"],
|
|
}
|
|
season = self.season(champions=champions)
|
|
for division, holder in champions.items():
|
|
if division == "Tag Team":
|
|
challenger = ["Edge", "Christian"]
|
|
elif division == "Women's":
|
|
challenger = ["Tori"]
|
|
else:
|
|
challenger = ["Val Venis"]
|
|
season["rankings"][division][app.side_identity(challenger)] = {
|
|
"members": challenger, "wins": 3, "losses": 1, "recent": ["W", "W"],
|
|
}
|
|
with patch.object(app.random, "seed", 6):
|
|
raw = app.build_season_matches(season, "Raw")
|
|
ppv = app.build_season_matches(season, "PPV")
|
|
self.assertLessEqual(sum(bool(match["title_defense"]) for match in raw), 1)
|
|
self.assertLessEqual(sum(bool(match["title_defense"]) for match in ppv), 2)
|
|
|
|
def test_booking_a_show_persists_current_card_and_cycle_brand(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root = Path(folder)
|
|
season = self.season(season_id="booking-test")
|
|
with patch.object(app, "SEASON_DIR", root / "seasons"), \
|
|
patch.object(app, "CARD_DIR", root / "cards"), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
card = app.book_next_season_show(season)
|
|
saved = app.load_season(season["season_id"])
|
|
loaded_card = app.load_card(Path(card["path"]))
|
|
self.assertEqual(card["season_show"], "Raw")
|
|
self.assertEqual(len(card["matches"]), 3)
|
|
self.assertEqual(saved["current_card"], card["path"])
|
|
self.assertEqual(loaded_card["season_show_number"], 1)
|
|
|
|
def test_season_save_round_trip_and_season_linked_card_metadata(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
season_dir, card_dir = Path(folder) / "seasons", Path(folder) / "cards"
|
|
season = self.season(champions={"European": ["Kane"]})
|
|
card = MarkdownTests().make_card(card_dir / "Raw.md")
|
|
card.update(season_id=season["season_id"], season_show="Raw", season_show_number=1)
|
|
with patch.object(app, "SEASON_DIR", season_dir), patch.object(app, "CARD_DIR", card_dir):
|
|
app.save_season(season)
|
|
app.save_card(card)
|
|
loaded_season = app.load_season(season["season_id"])
|
|
loaded_card = app.load_card(Path(card["path"]))
|
|
self.assertEqual(loaded_season["champions"]["European"]["holder"], ["Kane"])
|
|
self.assertEqual(loaded_card["season_id"], season["season_id"])
|
|
self.assertEqual(loaded_card["season_show"], "Raw")
|
|
self.assertEqual(loaded_card["season_show_number"], 1)
|
|
|
|
def test_syncing_completed_season_card_updates_results_only_once(self):
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
root = Path(folder)
|
|
season = self.season(season_id="sync-test")
|
|
card = MarkdownTests().make_card(root / "Raw.md")
|
|
card.update(season_id=season["season_id"], season_show="Raw",
|
|
season_show_number=1)
|
|
for match in card["matches"]:
|
|
match["winner_side"] = 0
|
|
match["ranking_division"] = "European"
|
|
season["current_card"] = str(root / "Raw.md")
|
|
with patch.object(app, "SEASON_DIR", root / "seasons"):
|
|
app.save_season(season)
|
|
app.sync_season_card_results(card)
|
|
after_first = app.load_season(season["season_id"])
|
|
app.sync_season_card_results(card)
|
|
after_second = app.load_season(season["season_id"])
|
|
self.assertEqual(after_first["next_show_number"], 2)
|
|
self.assertEqual(after_first["current_card"], "")
|
|
self.assertEqual(len(after_first["processed_results"]), 3)
|
|
self.assertEqual(after_second["rankings"], after_first["rankings"])
|
|
self.assertEqual(len(after_second["processed_results"]), 3)
|
|
|
|
|
|
class SeasonControlTests(unittest.TestCase):
|
|
def season(self, season_id="control-test"):
|
|
return app.new_season_state("Control Test", "sd2", season_id=season_id)
|
|
|
|
def test_new_and_legacy_seasons_default_to_booker_control(self):
|
|
season = self.season()
|
|
self.assertEqual(season["control_mode"], "booker")
|
|
self.assertIsNone(season["player_wrestler"])
|
|
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)):
|
|
season.pop("control_mode")
|
|
season.pop("player_wrestler")
|
|
app.save_season(season)
|
|
loaded = app.load_season(season["season_id"])
|
|
self.assertEqual(loaded["control_mode"], "booker")
|
|
self.assertIsNone(loaded["player_wrestler"])
|
|
|
|
def test_startup_choice_and_toggle_persist_wrestler_then_booker(self):
|
|
season = self.season()
|
|
expected_wrestler = sorted(app.load_roster("sd2"), key=str.casefold)[0]
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)), \
|
|
patch.object(app, "ask_choice", side_effect=[1, 0]):
|
|
app.choose_season_control_mode(season)
|
|
self.assertEqual(season["control_mode"], "wrestler")
|
|
self.assertEqual(season["player_wrestler"], expected_wrestler)
|
|
self.assertEqual(app.load_season(season["season_id"])["player_wrestler"],
|
|
expected_wrestler)
|
|
app.toggle_season_control_mode(season)
|
|
self.assertEqual(app.load_season(season["season_id"])["control_mode"], "booker")
|
|
with patch.object(app, "ask_choice", return_value=0):
|
|
app.toggle_season_control_mode(season)
|
|
saved = app.load_season(season["season_id"])
|
|
self.assertEqual(saved["control_mode"], "wrestler")
|
|
self.assertEqual(saved["player_wrestler"], expected_wrestler)
|
|
|
|
def test_season_menu_displays_current_control_mode_and_toggles_back(self):
|
|
season = self.season()
|
|
season["control_mode"] = "wrestler"
|
|
season["player_wrestler"] = "The Rock"
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)), \
|
|
patch.object(app, "ask_choice", side_effect=[2, 3]) as choose, \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.save_season(season)
|
|
app.run_season(season)
|
|
saved = app.load_season(season["season_id"])
|
|
self.assertIn("Toggle Control Mode — Current: Wrestler (The Rock)",
|
|
choose.call_args_list[0].kwargs["labels"])
|
|
self.assertEqual(season["control_mode"], "booker")
|
|
self.assertEqual(saved["control_mode"], "booker")
|
|
|
|
def test_wrestler_control_simulates_non_player_matches_and_preserves_player_match(self):
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder) / "seasons"):
|
|
season = self.season("wrestler-control-results")
|
|
season["control_mode"] = "wrestler"
|
|
season["player_wrestler"] = "The Rock"
|
|
card = MarkdownTests().make_card(Path(folder) / "card.md")
|
|
card["season_id"] = season["season_id"]
|
|
card["season_show_number"] = 1
|
|
card["title_definitions"] = season["title_definitions"]
|
|
card["matches"][0].update({
|
|
"sides": [["The Rock"], ["Kane"]], "winner_side": None,
|
|
})
|
|
card["matches"][1].update({
|
|
"sides": [["Edge"], ["Christian"]], "winner_side": None,
|
|
})
|
|
card["matches"][2].update({
|
|
"sides": [["Tazz"], ["Undertaker"]], "winner_side": None,
|
|
})
|
|
season["current_card"] = card["path"]
|
|
app.save_season(season)
|
|
app.save_card(card)
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertFalse(app.record_match_result(card, 1, auto_simulate=False))
|
|
app.simulate_non_player_matches(card, season)
|
|
|
|
self.assertIsNone(card["matches"][0]["winner_side"])
|
|
self.assertIsNotNone(card["matches"][1]["winner_side"])
|
|
self.assertIsNotNone(card["matches"][2]["winner_side"])
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertFalse(app.record_match_result(card, 0, auto_simulate=True))
|
|
captured = {}
|
|
|
|
def exit_card(_prompt, choices, labels=None):
|
|
captured["choices"] = choices
|
|
captured["labels"] = labels
|
|
return 4
|
|
|
|
with patch.object(app, "ask_choice", side_effect=exit_card), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertTrue(app.resume_card(card))
|
|
self.assertNotIn("edit", captured["choices"])
|
|
self.assertNotIn("simulate all", captured["choices"])
|
|
self.assertIn("Record your match result", captured["labels"])
|
|
|
|
with patch.object(app, "ask_choice", return_value=0), \
|
|
patch.object(app, "choose_match_outcome", return_value="Pinfall"), \
|
|
patch.object(app, "prompt_match_minutes", return_value=10), \
|
|
patch.object(app, "prompt_action_count", return_value=1), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.record_result(card, match_index=0)
|
|
self.assertEqual(card["matches"][0]["winner_side"], 0)
|
|
|
|
def test_absent_wrestler_causes_all_pending_matches_to_be_simulated(self):
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder) / "seasons"):
|
|
season = self.season("wrestler-absent")
|
|
season["control_mode"] = "wrestler"
|
|
season["player_wrestler"] = "The Rock"
|
|
card = MarkdownTests().make_card(Path(folder) / "card.md")
|
|
card["season_id"] = season["season_id"]
|
|
card["season_show_number"] = 1
|
|
card["title_definitions"] = season["title_definitions"]
|
|
for match, sides in zip(card["matches"], [
|
|
[["Kane"], ["Undertaker"]],
|
|
[["Edge"], ["Christian"]],
|
|
[["Tazz"], ["Big Show"]],
|
|
]):
|
|
match.update({"sides": sides, "winner_side": None})
|
|
season["current_card"] = card["path"]
|
|
app.save_season(season)
|
|
app.save_card(card)
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
app.simulate_non_player_matches(card, season)
|
|
|
|
self.assertTrue(all(match["winner_side"] is not None for match in card["matches"]))
|
|
updated = app.load_season(season["season_id"])
|
|
self.assertEqual(updated["current_card"], "")
|
|
self.assertEqual(updated["next_show_number"], 2)
|
|
|
|
|
|
class LightHeavyweightTests(unittest.TestCase):
|
|
def test_builtin_season_selects_and_snapshots_eligible_wrestlers(self):
|
|
def select_eligible(profile):
|
|
profile["light_heavyweight_roster"] = ["X-Pac", "Jeff Hardy"]
|
|
|
|
def initial_champion(division, _game_id, _used, _profile):
|
|
return ["X-Pac"] if division == "Light-Heavyweight" else None
|
|
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)), \
|
|
patch.object(app, "choose_game", return_value="sd1"), \
|
|
patch.object(app, "ask", return_value="Lightweight Season"), \
|
|
patch.object(app, "edit_light_heavyweight_roster", side_effect=select_eligible), \
|
|
patch.object(app, "choose_initial_champion", side_effect=initial_champion), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
season = app.create_season()
|
|
loaded = app.load_season(season["season_id"])
|
|
|
|
self.assertEqual(season["game_config"]["light_heavyweight_roster"],
|
|
["X-Pac", "Jeff Hardy"])
|
|
self.assertEqual(
|
|
app.eligible_roster_for_division(
|
|
"sd1", "Light-Heavyweight", profile=loaded["game_config"]
|
|
),
|
|
["X-Pac", "Jeff Hardy"],
|
|
)
|
|
self.assertEqual(loaded["champions"]["Light-Heavyweight"]["holder"], ["X-Pac"])
|
|
defense = {
|
|
"ranking_division": "Light-Heavyweight", "title_defense": True,
|
|
"sides": [["X-Pac"], ["Jeff Hardy"]], "winner_side": 1,
|
|
}
|
|
self.assertTrue(app.validate_title_defense(loaded, defense))
|
|
app.record_season_match(loaded, {"season_show": "Raw"}, defense, 0)
|
|
self.assertEqual(loaded["champions"]["Light-Heavyweight"]["holder"], ["Jeff Hardy"])
|
|
|
|
def test_initial_champion_random_choice_uses_only_eligible_wrestlers(self):
|
|
profile = app.game_profile("sd1")
|
|
profile["light_heavyweight_roster"] = ["X-Pac"]
|
|
choices_seen = []
|
|
|
|
def first_choice(options):
|
|
choices_seen.append(list(options))
|
|
return options[0]
|
|
|
|
with patch.object(app, "ask_choice", return_value=1), \
|
|
patch.object(app.random, "choice", side_effect=first_choice), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
champion = app.choose_initial_champion(
|
|
"Light-Heavyweight", "sd1", set(), profile
|
|
)
|
|
self.assertEqual(champion, ["X-Pac"])
|
|
self.assertEqual(choices_seen, [["X-Pac"]])
|
|
|
|
def test_ineligible_wrestlers_cannot_record_or_sync_division_results(self):
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)):
|
|
season = app.new_season_state(
|
|
"Lightweight Season", "sd1",
|
|
champions={"Light-Heavyweight": ["X-Pac"]},
|
|
season_id="lightweight-results",
|
|
)
|
|
season["game_config"]["light_heavyweight_roster"] = ["X-Pac"]
|
|
card_path = Path(folder) / "card.md"
|
|
season["current_card"] = str(card_path)
|
|
app.save_season(season)
|
|
match = {
|
|
"type": "Singles", "sides": [["X-Pac"], ["The Rock"]],
|
|
"winner_side": 1, "ranking_division": "Light-Heavyweight",
|
|
"title_defense": True,
|
|
}
|
|
card = {
|
|
"season_id": season["season_id"], "season_show_number": 1,
|
|
"season_show": "Raw", "path": str(card_path),
|
|
"matches": [match],
|
|
}
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertFalse(app.validate_title_defense(season, match))
|
|
self.assertFalse(app.record_match_result(card, 0, auto_simulate=True))
|
|
app.sync_season_card_results(card)
|
|
|
|
updated = app.load_season(season["season_id"])
|
|
self.assertEqual(updated["champions"]["Light-Heavyweight"]["holder"], ["X-Pac"])
|
|
self.assertEqual(updated["rankings"]["Light-Heavyweight"], {})
|
|
self.assertEqual(updated["processed_results"], [])
|
|
|
|
def test_legacy_season_without_eligibility_snapshot_remains_unrestricted(self):
|
|
season = app.new_season_state("Legacy Season", "sd1", season_id="legacy-lightweight")
|
|
season["game_config"].pop("light_heavyweight_roster", None)
|
|
self.assertEqual(
|
|
app.eligible_roster_for_division(
|
|
"sd1", "Light-Heavyweight", profile=season["game_config"]
|
|
),
|
|
season["game_config"]["roster"],
|
|
)
|
|
|
|
def test_legacy_builtin_light_heavyweight_kind_migrates_without_restricting_roster(self):
|
|
season = app.new_season_state(
|
|
"Legacy Lightweight", "sd1", season_id="legacy-lightweight-kind"
|
|
)
|
|
for definitions in (season["title_definitions"],
|
|
season["game_config"]["championships"]):
|
|
next(item for item in definitions if item["name"] == "Light-Heavyweight")["kind"] = "singles"
|
|
season["game_config"].pop("light_heavyweight_roster", None)
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder)):
|
|
app.save_season(season)
|
|
loaded = app.load_season(season["season_id"])
|
|
self.assertEqual(
|
|
app.season_championship_kind(loaded, "Light-Heavyweight"),
|
|
"light_heavyweight",
|
|
)
|
|
self.assertEqual(
|
|
app.eligible_roster_for_division(
|
|
"sd1", "Light-Heavyweight", profile=loaded["game_config"]
|
|
),
|
|
loaded["game_config"]["roster"],
|
|
)
|
|
|
|
|
|
class BYOGTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self._configs = app.CUSTOM_GAME_CONFIGS
|
|
self._games = dict(app.GAMES)
|
|
self._match_types = dict(app.MATCH_TYPES)
|
|
# Point the BYOG directory at a session-local temp dir so custom
|
|
# registrations survive refreshes and never touch the real edit/ folder.
|
|
self.game_dir = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.game_dir.cleanup)
|
|
patcher = patch.object(app, "CUSTOM_GAME_DIR", Path(self.game_dir.name) / "games")
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
|
|
def tearDown(self):
|
|
app.CUSTOM_GAME_CONFIGS = self._configs
|
|
app.GAMES.clear()
|
|
app.GAMES.update(self._games)
|
|
app.MATCH_TYPES.clear()
|
|
app.MATCH_TYPES.update(self._match_types)
|
|
|
|
def config(self, **overrides) -> dict:
|
|
data = {
|
|
"game_id": "custom-fantasy-fed-rumble-royale",
|
|
"game_title": "Rumble Royale",
|
|
"federation_name": "Fantasy Fed",
|
|
"platform": "ps1",
|
|
"roster": ["Ava Crusher", "Ben Brawler", "Cara Storm", "Dash Vance"],
|
|
"match_types": [
|
|
{"name": "Scaffold", "sides": [1, 1], "base_rating": 8.5},
|
|
{"name": "Tag Tornado", "sides": [2, 2],
|
|
"outcomes": ["pinfall", "submission"]},
|
|
],
|
|
"championships": [
|
|
{"name": "Rumble Belt", "kind": "singles"},
|
|
{"name": "Fantasy Tag", "kind": "tag"},
|
|
{"name": "Fantasy Women", "kind": "women"},
|
|
],
|
|
"women_roster": ["Cara Storm"],
|
|
"light_heavyweight_roster": [],
|
|
}
|
|
data.update(overrides)
|
|
return data
|
|
|
|
def register(self, data: dict) -> dict:
|
|
saved = app.save_custom_game(data, app.CUSTOM_GAME_DIR)
|
|
app.refresh_custom_games()
|
|
return saved
|
|
|
|
def filler_match(self, match_type: str, base_rating: float | None = None) -> dict:
|
|
sized = {}
|
|
for item in app.match_type_definitions(self.config()["game_id"]):
|
|
if item["name"] == match_type:
|
|
sized = item
|
|
break
|
|
return {"title": "", "type": match_type, "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": base_rating if base_rating is not None
|
|
else (sized.get("base_rating") if sized else app.base_match_rating(match_type)),
|
|
"valid_outcomes": list(sized["outcomes"]) if sized
|
|
else list(app.STANDARD_MATCH_OUTCOMES)}
|
|
|
|
def test_validate_custom_game_accepts_and_normalizes_profiles(self):
|
|
saved = app.validate_custom_game(self.config())
|
|
self.assertEqual(saved["game_id"], "custom-fantasy-fed-rumble-royale")
|
|
self.assertEqual(saved["schema_version"], 1)
|
|
self.assertEqual(saved["match_types"][0]["base_rating"], 8.5)
|
|
self.assertEqual(saved["match_types"][1]["base_rating"],
|
|
app.MATCH_BASE_RATINGS["Tag Team"])
|
|
self.assertEqual(saved["match_types"][0]["outcomes"],
|
|
list(app.STANDARD_MATCH_OUTCOMES))
|
|
self.assertEqual(saved["match_types"][1]["outcomes"], ["pinfall", "submission"])
|
|
self.assertEqual(saved["championships"][2]["kind"], "women")
|
|
self.assertIsNone(saved["screenshot_directory"])
|
|
saved_with_override = app.validate_custom_game(
|
|
self.config(screenshot_directory=" ~/Game Shots ")
|
|
)
|
|
self.assertEqual(saved_with_override["screenshot_directory"], "~/Game Shots")
|
|
|
|
def test_validate_custom_game_rejects_invalid_definitions(self):
|
|
rejects = [
|
|
({"game_id": "sd1"}, "custom definitions cannot overwrite built-in games"),
|
|
({"game_id": "Bad ID"}, "game_id must use lowercase"),
|
|
({"roster": ["Ava Crusher", "ava crusher"]}, "unique"),
|
|
({"roster": ["Solo"]}, "at least two"),
|
|
({"women_roster": ["Not Listed"]}, "subset of the roster"),
|
|
({"match_types": [{"name": "Tag Tornado", "sides": [3, 3]}]},
|
|
"invalid side sizes"),
|
|
({"match_types": []}, "at least one match type"),
|
|
({"championships": [{"name": "Belt", "kind": "hardcore"}]}, "kind"),
|
|
({"light_heavyweight_roster": ["Not Listed"]}, "subset of the roster"),
|
|
({"platform": "dreamcast"}, "platform must be ps1"),
|
|
({"screenshot_directory": 3}, "screenshot_directory must be a path string"),
|
|
]
|
|
for overrides, message in rejects:
|
|
with self.subTest(overrides=overrides):
|
|
data = self.config(**overrides)
|
|
with self.assertRaises(ValueError) as caught:
|
|
app.validate_custom_game(data)
|
|
self.assertIn(message, str(caught.exception).lower())
|
|
|
|
def test_custom_games_use_platform_defaults_or_expanded_overrides(self):
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "CUSTOM_GAME_DIR", Path(folder) / "games"):
|
|
ps1 = self.config(game_id="custom-shot-ps1", platform="ps1")
|
|
ps2 = self.config(game_id="custom-shot-ps2", platform="ps2")
|
|
other = self.config(game_id="custom-shot-other", platform="other")
|
|
overridden = self.config(
|
|
game_id="custom-shot-override", platform="other",
|
|
screenshot_directory="~/GameScreens",
|
|
)
|
|
ps2_override = self.config(
|
|
game_id="custom-shot-ps2-override", platform="ps2",
|
|
screenshot_directory="~/PCSX2Alt",
|
|
)
|
|
relative = self.config(
|
|
game_id="custom-shot-relative", platform="other",
|
|
screenshot_directory="custom-screens",
|
|
)
|
|
for config in (ps1, ps2, other, overridden, ps2_override, relative):
|
|
app.save_custom_game(config, app.CUSTOM_GAME_DIR)
|
|
app.refresh_custom_games()
|
|
|
|
self.assertEqual(app.screenshot_directory_for_game(ps1["game_id"]),
|
|
app.DUCKSTATION_SCREENSHOT_DIR)
|
|
self.assertEqual(app.screenshot_directory_for_game(ps2["game_id"]),
|
|
app.PCSX2_SCREENSHOT_DIR)
|
|
self.assertIsNone(app.screenshot_directory_for_game(other["game_id"]))
|
|
self.assertFalse(app.game_has_screenshots(other["game_id"]))
|
|
self.assertEqual(app.screenshot_directory_for_game(overridden["game_id"]),
|
|
Path.home() / "GameScreens")
|
|
self.assertEqual(app.screenshot_directory_for_game(ps2_override["game_id"]),
|
|
Path.home() / "PCSX2Alt")
|
|
self.assertEqual(app.screenshot_directory_for_game(relative["game_id"]),
|
|
app.ROOT / "custom-screens")
|
|
self.assertTrue(app.game_has_screenshots(overridden["game_id"]))
|
|
|
|
def test_custom_light_heavyweight_eligibility_is_validated_and_applied(self):
|
|
config = self.config(
|
|
championships=[
|
|
{"name": "Fantasy Lightweight", "kind": "light_heavyweight"},
|
|
],
|
|
light_heavyweight_roster=["Ava Crusher", "Cara Storm"],
|
|
)
|
|
self.register(config)
|
|
profile = app.game_profile(config["game_id"])
|
|
self.assertEqual(
|
|
app.eligible_roster_for_division(
|
|
config["game_id"], "Fantasy Lightweight", profile=profile
|
|
),
|
|
["Ava Crusher", "Cara Storm"],
|
|
)
|
|
self.assertEqual(
|
|
app.championship_kind(config["game_id"], "Fantasy Lightweight", profile),
|
|
"light_heavyweight",
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "must contain eligible wrestlers"):
|
|
app.validate_custom_game(self.config(championships=[
|
|
{"name": "Fantasy Lightweight", "kind": "light_heavyweight"},
|
|
]))
|
|
|
|
def test_light_heavyweight_eligibility_editor_selects_roster_members(self):
|
|
config = {"roster": ["Ava Crusher", "Ben Brawler"],
|
|
"light_heavyweight_roster": []}
|
|
with patch.object(app, "ask_choice", side_effect=[0, 0, 2]), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
app.edit_light_heavyweight_roster(config)
|
|
self.assertEqual(config["light_heavyweight_roster"], ["Ava Crusher"])
|
|
|
|
def test_custom_game_screenshot_override_can_be_set_in_editor(self):
|
|
config = app.validate_custom_game(self.config())
|
|
with patch.object(app, "ask_choice", side_effect=[0, 0, 6]), \
|
|
patch.object(app, "ask", side_effect=["Rumble Royale", "Fantasy Fed", "~/Shots"]), \
|
|
patch.object(app, "save_custom_game", side_effect=lambda value: value):
|
|
app.edit_custom_profile(config)
|
|
self.assertEqual(config["screenshot_directory"], "~/Shots")
|
|
|
|
def test_saved_custom_game_registers_in_catalog(self):
|
|
saved = self.register(self.config())
|
|
self.assertTrue((app.CUSTOM_GAME_DIR / f"{saved['game_id']}.json").is_file())
|
|
game_id = saved["game_id"]
|
|
self.assertIn(game_id, app.GAMES)
|
|
self.assertIn(game_id, app.CUSTOM_GAME_CONFIGS)
|
|
self.assertNotIn(game_id, app.BUILTIN_GAME_IDS)
|
|
self.assertEqual(app.load_roster(game_id), saved["roster"])
|
|
self.assertEqual(app.game_title(game_id), "Rumble Royale")
|
|
self.assertEqual(app.game_display_name(game_id),
|
|
"Fantasy Fed — Rumble Royale")
|
|
self.assertEqual(app.championship_names_for_game(game_id),
|
|
["Rumble Belt", "Fantasy Tag", "Fantasy Women"])
|
|
self.assertTrue(app.game_has_screenshots(game_id))
|
|
self.assertEqual(app.eligible_roster_for_division(game_id, "Fantasy Women"),
|
|
["Cara Storm"])
|
|
self.assertEqual([item["name"] for item in app.match_type_definitions(game_id)],
|
|
["Scaffold", "Tag Tornado"])
|
|
|
|
def test_custom_game_survives_relaunch_via_refresh(self):
|
|
self.register(self.config())
|
|
self.assertEqual(app.refresh_custom_games(),
|
|
["custom-fantasy-fed-rumble-royale"])
|
|
self.assertEqual(app.refresh_custom_games(),
|
|
["custom-fantasy-fed-rumble-royale"])
|
|
|
|
def test_custom_game_card_round_trip_keeps_game_metadata(self):
|
|
game_id = self.config()["game_id"]
|
|
self.register(self.config())
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
path = Path(folder) / "card.md"
|
|
card = {
|
|
"game_id": game_id, "show": "Rumble Night", "date": "2026-11-02",
|
|
"matches": [self.filler_match("Scaffold", base_rating=8.5),
|
|
self.filler_match("Tag Tornado"),
|
|
self.filler_match("Tag Tornado")],
|
|
"path": str(path),
|
|
}
|
|
app.save_card(card)
|
|
loaded = app.load_card(path)
|
|
self.assertEqual(loaded["game_title"], "Rumble Royale")
|
|
self.assertEqual(loaded["federation_name"], "Fantasy Fed")
|
|
self.assertEqual([item["name"] for item in loaded["title_definitions"]],
|
|
["Rumble Belt", "Fantasy Tag", "Fantasy Women"])
|
|
self.assertEqual(loaded["matches"][0]["type"], "Scaffold")
|
|
self.assertEqual(loaded["matches"][0]["base_rating"], 8.5)
|
|
self.assertEqual(loaded["matches"][1]["valid_outcomes"], ["pinfall", "submission"])
|
|
|
|
def test_custom_match_base_rating_can_drive_the_match_rating(self):
|
|
match = {"type": "Scaffold", "base_rating": 8.5, "match_time_minutes": 12.9,
|
|
"near_falls": 2, "finishers": 3, "title_defense": False}
|
|
plain = dict(match, base_rating=None)
|
|
self.assertEqual(app.calculate_match_rating(match) - app.calculate_match_rating(plain),
|
|
8.5 - app.base_match_rating("Scaffold"))
|
|
|
|
def test_custom_game_season_uses_custom_title_divisions(self):
|
|
config = self.config()
|
|
game_id = config["game_id"]
|
|
self.register(config)
|
|
season = app.new_season_state("Fantasy Season", game_id,
|
|
champions={"Rumble Belt": ["Ava Crusher"],
|
|
"Fantasy Tag": ["Ben Brawler", "Dash Vance"]},
|
|
season_id="fantasy-season")
|
|
self.assertEqual([item["name"] for item in season["title_definitions"]],
|
|
["Rumble Belt", "Fantasy Tag", "Fantasy Women"])
|
|
self.assertEqual(set(season["rankings"]),
|
|
{"Rumble Belt", "Fantasy Tag", "Fantasy Women"})
|
|
self.assertEqual(season["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"])
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
roster = app.load_roster(game_id)
|
|
matches = app.build_season_matches(season, "Raw")
|
|
self.assertEqual(len(matches), 3)
|
|
for match in matches:
|
|
self.assertIn(match["ranking_division"], (None, "Rumble Belt", "Fantasy Tag",
|
|
"Fantasy Women"))
|
|
# A custom division result updates the custom rankings.
|
|
match = {"type": "Scaffold", "sides": [["Ava Crusher"], ["Ben Brawler"]],
|
|
"winner_side": 0, "ranking_division": "Rumble Belt",
|
|
"title_defense": False}
|
|
app.record_season_match(season, {"season_show": "Raw"}, match, 0)
|
|
table = season["rankings"]["Rumble Belt"]
|
|
self.assertEqual(table[app.side_identity(["Ava Crusher"])]["wins"], 1)
|
|
self.assertEqual(table[app.side_identity(["Ben Brawler"])]["losses"], 1)
|
|
# A successful defense keeps the custom champion on top.
|
|
defense = {"type": "Scaffold", "sides": [["Ava Crusher"], ["Cara Storm"]],
|
|
"winner_side": 0, "ranking_division": "Rumble Belt",
|
|
"title_defense": True}
|
|
app.record_season_match(season, {"season_show": "Raw"}, defense, 0)
|
|
self.assertEqual(season["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"])
|
|
self.assertEqual(season["champions"]["Rumble Belt"]["successful_defenses"], 1)
|
|
|
|
def test_custom_season_save_round_trip_keeps_title_definitions(self):
|
|
self.register(self.config())
|
|
season = app.new_season_state("Fantasy Season", self.config()["game_id"],
|
|
champions={"Rumble Belt": ["Ava Crusher"]},
|
|
season_id="fantasy-save")
|
|
with tempfile.TemporaryDirectory() as folder:
|
|
with patch.object(app, "SEASON_DIR", Path(folder)):
|
|
app.save_season(season)
|
|
loaded = app.load_season(season["season_id"])
|
|
self.assertEqual(loaded["game_title"], "Rumble Royale")
|
|
self.assertEqual(loaded["federation_name"], "Fantasy Fed")
|
|
self.assertEqual(loaded["champions"]["Rumble Belt"]["holder"], ["Ava Crusher"])
|
|
self.assertEqual([item["name"] for item in loaded["title_definitions"]],
|
|
["Rumble Belt", "Fantasy Tag", "Fantasy Women"])
|
|
self.assertEqual(loaded["shows"], app.DEFAULT_CUSTOM_SHOWS)
|
|
|
|
def test_choose_game_lists_builtin_games_then_custom_games(self):
|
|
self.register(self.config())
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", return_value=len(app.GAMES) - 1):
|
|
self.assertEqual(app.choose_game(), self.config()["game_id"])
|
|
|
|
CUSTOM_SHOWS = {"Raw": "Fantasy Rampage", "SmackDown": "Fantasy Clash",
|
|
"PPV": "Fantasy Super Card"}
|
|
|
|
def test_shows_validation_accepts_normalizes_and_rejects_bad_values(self):
|
|
saved = app.validate_custom_game(self.config(shows=self.CUSTOM_SHOWS))
|
|
self.assertEqual(saved["shows"], self.CUSTOM_SHOWS)
|
|
|
|
# Absent shows fall back to neutral placeholders, never WWE names.
|
|
self.assertEqual(app.validate_custom_game(self.config())["shows"],
|
|
app.DEFAULT_CUSTOM_SHOWS)
|
|
|
|
rejects = [
|
|
({"shows": ["Fantasy Rampage"]}, "shows must be an object"),
|
|
({"shows": {"Raw": " "}}, "non-empty string"),
|
|
({"shows": self.CUSTOM_SHOWS | {"Raw": "fantasy clash"}}, "unique"),
|
|
({"shows": {"Raw": 3}}, "non-empty string"),
|
|
]
|
|
for overrides, message in rejects:
|
|
with self.subTest(overrides=overrides):
|
|
data = self.config(**overrides)
|
|
with self.assertRaises(ValueError) as caught:
|
|
app.validate_custom_game(data)
|
|
self.assertIn(message, str(caught.exception).lower())
|
|
|
|
def test_game_show_names_resolve_wwe_names_only_for_builtins(self):
|
|
self.assertEqual(app.game_show_names("sd1")["SmackDown"],
|
|
"Thursday Night SmackDown!")
|
|
self.register(self.config())
|
|
self.assertEqual(app.game_show_names(self.config()["game_id"]),
|
|
app.DEFAULT_CUSTOM_SHOWS)
|
|
self.register(self.config(shows=self.CUSTOM_SHOWS))
|
|
game_id = self.config()["game_id"]
|
|
self.assertEqual(app.game_show_names(game_id), self.CUSTOM_SHOWS)
|
|
|
|
def test_built_in_clone_prefills_wwe_show_names(self):
|
|
profile = app.builtin_game_profile("sd2")
|
|
self.assertEqual(profile["shows"]["Raw"], "Monday Night Raw")
|
|
copied = app.builtin_profile_for_copy("sd2", "custom-clone-test")
|
|
self.assertEqual(copied["shows"], {"Raw": "Monday Night Raw",
|
|
"SmackDown": "Thursday Night SmackDown!",
|
|
"PPV": "WWF PPV"})
|
|
|
|
def test_booked_season_cards_use_custom_show_names(self):
|
|
game_id = self.register(self.config(shows=self.CUSTOM_SHOWS))["game_id"]
|
|
season = app.new_season_state("Show Name Season", game_id, champions={},
|
|
season_id="showname-season")
|
|
self.assertEqual(season["shows"], self.CUSTOM_SHOWS)
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "SEASON_DIR", Path(folder) / "seasons"), \
|
|
patch.object(app, "CARD_DIR", Path(folder) / "cards"), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
names = []
|
|
for show_number in (1, 2, 3):
|
|
season["next_show_number"] = show_number
|
|
card = app.book_next_season_show(season)
|
|
names.append(card["show"])
|
|
season["current_card"] = ""
|
|
card["matches"][0]["winner_side"] = 0
|
|
season_path = app.season_path(season["season_id"])
|
|
# The season save keeps its custom show-name snapshot on disk.
|
|
saved = json.loads(season_path.read_text("utf-8"))
|
|
self.assertEqual(saved["shows"], self.CUSTOM_SHOWS)
|
|
self.assertEqual(names, ["Fantasy Rampage", "Fantasy Clash", "Fantasy Super Card"])
|
|
|
|
def test_is_ppv_card_matches_custom_ppv_show_name_exactly(self):
|
|
self.register(self.config(shows=self.CUSTOM_SHOWS))
|
|
game_id = self.config()["game_id"]
|
|
event_card = {"game_id": game_id, "show": "Fantasy Super Card"}
|
|
weekly_card = {"game_id": game_id, "show": "Fantasy Rampage"}
|
|
self.assertTrue(app.is_ppv_card(event_card))
|
|
self.assertFalse(app.is_ppv_card(weekly_card))
|
|
# Case-insensitive exact match still applies.
|
|
self.assertTrue(app.is_ppv_card({"game_id": game_id, "show": "fantasy super card"}))
|
|
# Unknown card shows stay non-PPV even with a custom game.
|
|
self.assertFalse(app.is_ppv_card({"game_id": game_id, "show": "House Party"}))
|
|
|
|
def test_one_off_cards_use_game_show_names(self):
|
|
game_id = self.register(self.config(shows=self.CUSTOM_SHOWS))["game_id"]
|
|
with tempfile.TemporaryDirectory() as folder, \
|
|
patch.object(app, "CARD_DIR", Path(folder) / "cards"), \
|
|
contextlib.redirect_stdout(io.StringIO()):
|
|
with patch.object(app, "choose_game", return_value=game_id):
|
|
card = app.create_random_card()
|
|
self.assertIn(card["show"], set(self.CUSTOM_SHOWS.values()))
|
|
# The create-a-card show prompt defaults to the second weekly slot.
|
|
captured = {}
|
|
|
|
class Stop(Exception):
|
|
pass
|
|
|
|
def fake_ask(prompt, default=None):
|
|
captured["default"] = default
|
|
raise Stop()
|
|
|
|
with patch.object(app, "choose_game", return_value=game_id), \
|
|
patch.object(app, "ask", fake_ask), \
|
|
self.assertRaises(Stop):
|
|
app.create_card()
|
|
self.assertEqual(captured["default"], "Fantasy Clash")
|
|
|
|
def test_edit_custom_shows_renames_and_rejects_duplicates(self):
|
|
config = {"shows": dict(self.CUSTOM_SHOWS)}
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", side_effect=[0, 3]), \
|
|
patch.object(app, "ask", return_value="Fantasy Festival"):
|
|
app.edit_custom_shows(config)
|
|
self.assertEqual(config["shows"]["Raw"], "Fantasy Festival")
|
|
|
|
config = {"shows": dict(self.CUSTOM_SHOWS)}
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", side_effect=[1, 3]), \
|
|
patch.object(app, "ask", side_effect=["Fantasy Rampage", "Fantasy Zoom"]):
|
|
app.edit_custom_shows(config)
|
|
self.assertEqual(config["shows"]["SmackDown"], "Fantasy Zoom")
|
|
|
|
def test_view_roster_lists_names_read_only(self):
|
|
config = {"roster": ["Zulu Viper", "Ava Crusher", "Ben Brawler"],
|
|
"women_roster": ["Ava Crusher"]}
|
|
captured = io.StringIO()
|
|
with contextlib.redirect_stdout(captured), \
|
|
patch.object(app, "ask_choice", side_effect=[2, 3]):
|
|
app.edit_custom_roster(config)
|
|
text = captured.getvalue()
|
|
self.assertIn("Roster (3 wrestlers):", text)
|
|
self.assertIn(" 1. Ava Crusher", text)
|
|
self.assertIn(" 2. Ben Brawler", text)
|
|
self.assertIn(" 3. Zulu Viper", text)
|
|
self.assertEqual(config["roster"], ["Zulu Viper", "Ava Crusher", "Ben Brawler"])
|
|
self.assertEqual(config["women_roster"], ["Ava Crusher"])
|
|
|
|
def test_view_roster_on_empty_roster(self):
|
|
config = {"roster": [], "women_roster": []}
|
|
captured = io.StringIO()
|
|
with contextlib.redirect_stdout(captured), \
|
|
patch.object(app, "ask_choice", side_effect=[2, 3]):
|
|
app.edit_custom_roster(config)
|
|
self.assertIn("The roster is empty.", captured.getvalue())
|
|
|
|
def test_view_roster_dumps_large_roster_completely(self):
|
|
config = {"roster": [f"Wrestler {index:02d}" for index in range(65)],
|
|
"women_roster": []}
|
|
captured = io.StringIO()
|
|
with contextlib.redirect_stdout(captured), \
|
|
patch.object(app, "ask_choice", side_effect=[2, 3]):
|
|
app.edit_custom_roster(config)
|
|
text = captured.getvalue()
|
|
self.assertIn("Roster (65 wrestlers):", text)
|
|
self.assertIn(" 1. Wrestler 00", text)
|
|
self.assertIn(" 65. Wrestler 64", text)
|
|
|
|
def test_roster_menu_ordering_adds_view_between_remove_and_done(self):
|
|
seen = {}
|
|
config = {"roster": ["Ava Crusher"], "women_roster": []}
|
|
|
|
def capturing_choice(prompt, choices, default=None, labels=None):
|
|
if prompt == "Edit roster":
|
|
seen["labels"] = labels
|
|
return len(choices) - 1
|
|
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
patch.object(app, "ask_choice", side_effect=capturing_choice):
|
|
app.edit_custom_roster(config)
|
|
self.assertEqual(seen["labels"], ["Add a wrestler", "Remove a wrestler",
|
|
"View roster (read-only)", "Done (1 listed)"])
|
|
|
|
|
|
class FlashRenderingTests(unittest.TestCase):
|
|
def test_flash_updates_only_changed_rows_not_static_header(self):
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
with patch.object(app, "_render_image", return_value=([], 0)):
|
|
normal = app.render_card_lines(card)
|
|
flashing = app.render_card_lines(card, flash=True)
|
|
header_length = len(app.header_lines(card, 100))
|
|
self.assertEqual(normal[:header_length], flashing[:header_length])
|
|
|
|
output = io.StringIO()
|
|
app.write_changed_lines(normal, flashing, stream=output)
|
|
updates = output.getvalue()
|
|
self.assertNotIn("WWF.com", updates)
|
|
self.assertNotIn("SMACKDOWN!", updates)
|
|
self.assertNotIn("\033[2J", updates)
|
|
self.assertIn("WIN!", updates)
|
|
self.assertIn("LOSE", updates)
|
|
|
|
def test_full_card_layout_fits_typical_terminal_height(self):
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
card["matches"][2]["sides"] = [["Stone Cold Steve Austin"], ["Kane"]]
|
|
for match in card["matches"]:
|
|
match["promo"] = "First speaker: A short line.\nSecond: A reply.\nFirst: A close."
|
|
with (patch.object(app, "_render_image", side_effect=[(["logo"] * 6, 23), (["logo"] * 6, 38)]),
|
|
patch.object(app.shutil, "get_terminal_size", return_value=os.terminal_size((100, 24)))):
|
|
lines = app.render_card_lines(card)
|
|
self.assertLessEqual(len(lines), 24)
|
|
|
|
|
|
class HeaderLogoTests(unittest.TestCase):
|
|
def test_logo_selection_is_case_insensitive_and_show_dependent(self):
|
|
self.assertEqual(app.show_logo_path("Friday Night SmAcKdOwN"),
|
|
app.ROOT / "smackdown-logo-source.png")
|
|
self.assertEqual(app.show_logo_path("Monday Night RAW"),
|
|
app.ROOT / "raw-is-war-logo.png")
|
|
self.assertIsNone(app.show_logo_path("WrestleMania"))
|
|
|
|
def test_smackdown_takes_precedence_when_both_keywords_appear(self):
|
|
self.assertEqual(app.show_logo_path("Raw vs SmackDown"),
|
|
app.ROOT / "smackdown-logo-source.png")
|
|
|
|
def test_selected_logo_has_text_fallback_when_images_cannot_render(self):
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
card["show"] = "Monday Night Raw"
|
|
with patch.object(app, "_render_image", return_value=([], 0)):
|
|
header = "\n".join(app.header_lines(card, 100))
|
|
self.assertIn("RAW IS WAR", header)
|
|
|
|
def custom_card(self) -> dict:
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
card["federation_name"] = "Fantasy Fed"
|
|
card["game_title"] = "Rumble Royale"
|
|
return card
|
|
|
|
def test_is_wwf_federation_matches_canonical_name_and_alias(self):
|
|
for name in ("World Wrestling Federation", "world wrestling federation",
|
|
"WWF", " wwf "):
|
|
self.assertTrue(app.is_wwf_federation(name), name)
|
|
for name in ("Fantasy Fed", "WWE", "WCW", ""):
|
|
self.assertFalse(app.is_wwf_federation(name), name)
|
|
|
|
def test_corner_logo_only_renders_for_wwf_federations(self):
|
|
def image_mock(path, _columns, _rows):
|
|
if path.name == "wwfcomlogo.gif":
|
|
return ["IMG1"], 3
|
|
return ["C1"], 3
|
|
|
|
wwf_card = MarkdownTests().make_card(Path("card.md"))
|
|
wwf_card["show"] = "Monday Night Raw"
|
|
custom_card = self.custom_card()
|
|
custom_card["show"] = "Monday Night Raw"
|
|
with patch.object(app, "_render_image", side_effect=image_mock):
|
|
wwf_header = app.header_lines(wwf_card, 100)
|
|
custom_header = app.header_lines(custom_card, 100)
|
|
self.assertIn("IMG1", wwf_header[0])
|
|
self.assertFalse(any("IMG1" in line for line in custom_header))
|
|
# With no corner logo the centered show logo is truly centered (48 = (100-3)//2).
|
|
# With no corner logo the centered show logo is truly centered (48 = (100-3)//2)
|
|
# and the date still sits at the top-right of the same row.
|
|
self.assertTrue(custom_header[0].startswith(" " * 48 + "C1"), custom_header[0])
|
|
self.assertIn("2026-10-25", custom_header[0])
|
|
self.assertNotIn("WWF.com", "\n".join(app.header_lines(custom_card, 100)))
|
|
|
|
def test_wwf_alias_federations_render_the_corner_logo(self):
|
|
image_mock = lambda path, _columns, _rows: (["IMG1"], 3)
|
|
for federation in ("WWF", "world wrestling federation"):
|
|
card = self.custom_card()
|
|
card["federation_name"] = federation
|
|
with patch.object(app, "_render_image", side_effect=image_mock):
|
|
header = app.header_lines(card, 100)
|
|
self.assertIn("IMG1", header[0], federation)
|
|
|
|
def test_corner_logo_text_fallback_only_for_wwf_federations(self):
|
|
wwf_card = MarkdownTests().make_card(Path("card.md"))
|
|
custom_card = self.custom_card()
|
|
with patch.object(app, "_render_image", return_value=([], 0)):
|
|
wwf_header = "\n".join(app.header_lines(wwf_card, 100))
|
|
custom_header = "\n".join(app.header_lines(custom_card, 100))
|
|
self.assertIn("WWF.com", wwf_header)
|
|
self.assertNotIn("WWF.com", custom_header)
|
|
|
|
def test_narrow_custom_federation_header_drops_wwf_branding(self):
|
|
wwf_card = MarkdownTests().make_card(Path("card.md"))
|
|
custom_card = self.custom_card()
|
|
wwf_header = "\n".join(app.header_lines(wwf_card, 60))
|
|
custom_header = "\n".join(app.header_lines(custom_card, 60))
|
|
self.assertIn("WWF.com", wwf_header)
|
|
self.assertNotIn("WWF.com", custom_header)
|
|
self.assertIn("Rumble Royale", custom_header)
|
|
for header in (wwf_header, custom_header):
|
|
self.assertIn("2026-10-25", header)
|
|
|
|
|
|
class PromoFoldingTests(unittest.TestCase):
|
|
def card_with_promos(self) -> dict:
|
|
card = MarkdownTests().make_card(Path("card.md"))
|
|
card["matches"][2]["sides"] = [["Stone Cold Steve Austin"], ["Kane"]]
|
|
for match in card["matches"]:
|
|
match["promo"] = (
|
|
"The Rock: A deliberately long promo line that should wrap when expanded "
|
|
"instead of being clipped off at the edge of the terminal.\n"
|
|
"Jeff Hardy: A second complete response with enough detail to test folding.\n"
|
|
"The Rock: The final promo line remains available in the expanded view."
|
|
)
|
|
return card
|
|
|
|
def test_collapsed_promo_can_expand_to_all_wrapped_lines(self):
|
|
card = self.card_with_promos()
|
|
collapsed = app.match_lines(card["matches"][0], 1, 80, promo_selected=True)
|
|
expanded = app.match_lines(
|
|
card["matches"][0], 1, 80, promo_selected=True, promo_expanded=True
|
|
)
|
|
expanded_text = "\n".join(expanded)
|
|
self.assertTrue(any("▶ Promo:" in line for line in collapsed))
|
|
self.assertTrue(any("\033[7m" in line for line in collapsed))
|
|
self.assertTrue(any("▼ Promo (Enter to collapse)" in line for line in expanded))
|
|
self.assertIn("second complete response", expanded_text.lower())
|
|
self.assertIn("final promo line remains available", expanded_text)
|
|
self.assertGreater(len(expanded), len(collapsed))
|
|
|
|
def test_expanded_view_scrolls_body_but_keeps_header_pinned(self):
|
|
card = self.card_with_promos()
|
|
image_lines = ["logo"] * 6
|
|
image_mock = lambda path, _columns, _rows: (
|
|
image_lines, 23 if path.name == "wwfcomlogo.gif" else 38
|
|
)
|
|
with patch.object(app, "_render_image", side_effect=image_mock):
|
|
collapsed, _, _, _ = app.interactive_card_frame(card, 100, 24)
|
|
expanded, offset, capacity, spans = app.interactive_card_frame(
|
|
card, 100, 24, selected_promo=0, expanded_promos={0, 1, 2},
|
|
keep_selection_visible=True,
|
|
)
|
|
header = app.header_lines(card, 100)
|
|
self.assertEqual(len(expanded), 24)
|
|
self.assertGreater(offset, 0)
|
|
self.assertEqual(collapsed[:len(header)], expanded[:len(header)])
|
|
control_row, _ = spans[0]
|
|
view_row = len(header) + control_row - offset
|
|
self.assertIn("\033[7m", expanded[view_row])
|
|
self.assertGreaterEqual(capacity, 1)
|
|
|
|
def test_terminal_key_reader_maps_enter_arrows_and_page_keys(self):
|
|
for sequence, expected in ((b"\r", "enter"), (b"\x1b[A", "up"),
|
|
(b"\x1b[B", "down"), (b"\x1b[5~", "pageup"),
|
|
(b"\x1b[6~", "pagedown"), (b"q", "quit")):
|
|
read_fd, write_fd = os.pipe()
|
|
try:
|
|
os.write(write_fd, sequence)
|
|
self.assertEqual(app.read_view_key(read_fd), expected)
|
|
finally:
|
|
os.close(read_fd)
|
|
os.close(write_fd)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|