"""Joining a win rate to what was in the match. A win rate says better or worse. These tests are about the part that says at what - and about the two ways a join like this lies: counting a match whose scenario it could not find, and reporting a group of three matches as though the number meant something. """ import json import shutil import tempfile import unittest from pathlib import Path from sds.correlate import MIN_MATCHES, Group, band, by, join, report, sds_won def result(scenario, winner_kind, outcome="victory"): return { "scenario": f"/work/scenarios/{scenario}.mms", "outcome": outcome, "victoryTeam": 1 if winner_kind == "sds" else 2, "players": [ {"team": 1, "kind": "sds", "name": "sds North", "role": "sds North"}, {"team": 2, "kind": "princess", "name": "princess South", "role": "princess South"}, ], } def meta(scenario, tier=8000, era="jihad", gunnery=4, piloting=5, mass=55, clan=0): return { "scenario": scenario, "size": 4, "era": era, "seed": 1, "map": "Map Set 2/16x17 BattleTech", "target_bv": tier, "bv": tier, "bv_miss": 0, "mean_gunnery": gunnery, "mean_piloting": piloting, "units": [ { "name": f"Design {i}", "gunnery": gunnery, "piloting": piloting, "bv": tier // 4, "bv_base": tier // 4, "mass": mass, "role": "Brawler", "weight_class": "Medium", "walk_mp": 4, "run_mp": 6, "jump_mp": 0, "total_armor": 150, "weapon_count": 6, "heat_efficiency": 1.0, "firepower": 20.0, "max_range": 15, "clan": clan, "engine": "Fusion", "armor": "Standard", "year": 3050, } for i in range(4) ], } class CorrelateTest(unittest.TestCase): def setUp(self): self.run = Path(tempfile.mkdtemp()) self.suite = Path(tempfile.mkdtemp()) def tearDown(self): shutil.rmtree(self.run, ignore_errors=True) shutil.rmtree(self.suite, ignore_errors=True) def write(self, scenario, winner_kind, outcome="victory", **kwargs): (self.run / f"{scenario}.json").write_text( json.dumps(result(scenario, winner_kind, outcome)) ) (self.suite / f"{scenario}.meta.json").write_text(json.dumps(meta(scenario, **kwargs))) class TestJoining(CorrelateTest): def test_a_match_with_no_metadata_is_reported_not_counted(self): """The failure that would silently shrink every group.""" (self.run / "orphan.json").write_text(json.dumps(result("orphan", "sds"))) joined = join(self.run, self.suite) self.assertEqual(joined.joined, []) self.assertEqual(joined.missing_meta, {"orphan"}) def test_an_undecided_match_is_excluded(self): self.write("a", "sds", outcome="round-limit") joined = join(self.run, self.suite) self.assertEqual(joined.undecided, 1) self.assertEqual(joined.joined, []) def test_the_winner_is_read_by_kind_not_by_seat(self): self.assertTrue(sds_won(result("x", "sds"))) self.assertFalse(sds_won(result("x", "princess"))) self.assertIsNone(sds_won(result("x", "sds", outcome="stalled"))) class TestGrouping(CorrelateTest): def test_win_rate_is_grouped_by_the_chosen_key(self): for i in range(6): self.write(f"hi{i}", "sds", tier=10000) for i in range(4): self.write(f"lo{i}", "princess", tier=4000) joined = join(self.run, self.suite) groups = by(joined.joined, lambda m: m["target_bv"]) self.assertEqual(groups["10000"].won, 6) self.assertEqual(groups["10000"].rate, 1.0) self.assertEqual(groups["4000"].won, 0) def test_a_list_key_counts_the_match_in_every_group(self): """A force has four machines and the match is evidence about each.""" self.write("m", "sds") joined = join(self.run, self.suite) groups = by(joined.joined, lambda m: sorted({u["name"] for u in m["units"]})) self.assertEqual(len(groups), 4) self.assertTrue(all(g.total == 1 for g in groups.values())) def test_thin_groups_are_summed_rather_than_given_an_interval(self): for i in range(3): self.write(f"t{i}", "sds", era="dark-age") for i in range(MIN_MATCHES + 2): self.write(f"f{i}", "princess", era="jihad") text = report(self.run, self.suite) self.assertIn("groups under", text) self.assertNotIn("| dark-age |", text) def test_bands_put_a_number_in_a_row(self): self.assertEqual(band(35, (40, 60, 80)), "<40") self.assertEqual(band(60, (40, 60, 80)), "<80") self.assertEqual(band(95, (40, 60, 80)), ">=80") class TestReport(CorrelateTest): def test_the_overall_line_matches_the_groups(self): for i in range(MIN_MATCHES): self.write(f"w{i}", "sds") for i in range(MIN_MATCHES): self.write(f"l{i}", "princess") text = report(self.run, self.suite) self.assertIn(f"**{MIN_MATCHES}-{MIN_MATCHES}, 50.0%**", text) def test_it_says_mirrored_so_the_reader_knows_what_a_group_means(self): self.write("a", "sds") self.assertIn("mirrored", report(self.run, self.suite)) def test_an_empty_join_says_so_rather_than_dividing_by_zero(self): self.assertIn("Nothing to correlate", report(self.run, self.suite)) class TestGroupArithmetic(unittest.TestCase): def test_an_empty_group_claims_nothing(self): g = Group("x") self.assertEqual(g.total, 0) self.assertEqual(g.rate, 0.0) self.assertEqual(g.interval(), (0.0, 1.0)) class TestTerrainGrouping(unittest.TestCase): """A board name is not a group; the kind of ground on it is. Eighty-nine boards across four hundred matches gives five matches a board, and five matches is a story rather than a measurement. Terrain aggregates across boards, so "half the fights on heavy woods" gets an interval. """ def test_a_board_that_cannot_be_found_groups_as_nothing(self): """Not as a board with no woods on it.""" from sds.correlate import terrain_band self.assertIsNone(terrain_band({"map": "no such board"}, "woods", (0.1,))) self.assertIsNone(terrain_band({}, "woods", (0.1,))) self.assertIsNone(terrain_band({"map": ["a", "list"]}, "woods", (0.1,))) def test_real_boards_land_in_the_bands_their_terrain_implies(self): from sds.correlate import terrain_band heavy = terrain_band({"map": "Map Set 4/16x17 Heavy Forest 1"}, "woods", (0.1, 0.25, 0.5)) open_ = terrain_band({"map": "Map Set 5/16x17 Open Terrain 1"}, "woods", (0.1, 0.25, 0.5)) if heavy is None or open_ is None: self.skipTest("no MegaMek board data on this machine") self.assertEqual(heavy, "woods >=0.5") self.assertEqual(open_, "woods <0.1") def test_terrain_summary_reads_fractions_not_counts(self): from sds.board import blank, terrain_summary empty = terrain_summary(blank()) self.assertEqual(empty["woods"], 0.0) self.assertEqual(empty["water"], 0.0) self.assertEqual(empty["elevation_spread"], 0.0) def test_an_empty_board_summarises_to_nothing(self): from sds.board import Board, terrain_summary self.assertEqual(terrain_summary(Board(width=0, height=0)), {}) class TestUnattributedVictory(unittest.TestCase): """A victory MegaMek did not attribute is not a loss. Eleven matches in a 418-game run came back `victory` with `victoryTeam: 0` while the players were on teams 1 and 2. Reading the seat map for team 0 gives None, and `None == "sds"` is False - so every one was counted as a defeat, and the run's rate came out 1.5 points below the baseline's. """ def test_a_victory_for_a_team_with_no_players_is_undecided(self): document = result("x", "sds") document["victoryTeam"] = 0 self.assertIsNone(sds_won(document)) def test_a_normal_victory_still_resolves(self): self.assertTrue(sds_won(result("x", "sds"))) self.assertFalse(sds_won(result("x", "princess"))) def test_it_agrees_with_the_summary_counter(self): """The two counters disagreeing is what surfaced this.""" from sds.stats import winner document = result("x", "sds") document["victoryTeam"] = 0 self.assertIsNone(winner(document, "role")) self.assertIsNone(sds_won(document))