From 7ead8fdf49572d862a2f309fb0285d5ec645ec00 Mon Sep 17 00:00:00 2001 From: Carter Hesterman Date: Fri, 17 Oct 2025 08:35:44 -0600 Subject: [PATCH] Civ 6: Add era requirements for boosts and update boost prereqs (#5296) * Resolve #5136 * Resolves #5210 --- worlds/civ_6/ItemData.py | 1 + worlds/civ_6/Locations.py | 5 +- worlds/civ_6/data/boosts.py | 32 ++++++------ worlds/civ_6/test/TestBoostsanity.py | 75 ++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 15 deletions(-) diff --git a/worlds/civ_6/ItemData.py b/worlds/civ_6/ItemData.py index 5f3c16a9..7ed1d713 100644 --- a/worlds/civ_6/ItemData.py +++ b/worlds/civ_6/ItemData.py @@ -20,6 +20,7 @@ class CivVIBoostData: Prereq: List[str] PrereqRequiredCount: int Classification: str + EraRequired: bool = False class GoodyHutRewardData(TypedDict): diff --git a/worlds/civ_6/Locations.py b/worlds/civ_6/Locations.py index 71f29f1c..7da824df 100644 --- a/worlds/civ_6/Locations.py +++ b/worlds/civ_6/Locations.py @@ -150,7 +150,10 @@ def generate_era_location_table() -> Dict[str, Dict[str, CivVILocationData]]: location = CivVILocationData( boost.Type, 0, 0, id_base, boost.EraType, CivVICheckType.BOOST ) - era_locations["ERA_ANCIENT"][boost.Type] = location + # If EraRequired is True, place the boost in its actual era + # Otherwise, place it in ERA_ANCIENT for early access + target_era = boost.EraType if boost.EraRequired else "ERA_ANCIENT" + era_locations[target_era][boost.Type] = location id_base += 1 return era_locations diff --git a/worlds/civ_6/data/boosts.py b/worlds/civ_6/data/boosts.py index 49cedfdf..d0bdecc0 100644 --- a/worlds/civ_6/data/boosts.py +++ b/worlds/civ_6/data/boosts.py @@ -210,8 +210,8 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_SQUARE_RIGGING", "ERA_RENAISSANCE", - ["TECH_GUNPOWDER"], - 1, + ["TECH_GUNPOWDER", "TECH_MILITARY_ENGINEERING", "TECH_MINING"], + 3, "DEFAULT", ), CivVIBoostData( @@ -252,15 +252,15 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_BALLISTICS", "ERA_INDUSTRIAL", - ["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING"], - 2, + ["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING", "TECH_BRONZE_WORKING"], + 3, "DEFAULT", ), CivVIBoostData( "BOOST_TECH_MILITARY_SCIENCE", "ERA_INDUSTRIAL", - ["TECH_STIRRUPS"], - 1, + ["TECH_BRONZE_WORKING", "TECH_STIRRUPS", "TECH_MINING"], + 3, "DEFAULT", ), CivVIBoostData( @@ -301,8 +301,8 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_REPLACEABLE_PARTS", "ERA_MODERN", - ["TECH_MILITARY_SCIENCE"], - 1, + ["TECH_MILITARY_SCIENCE", "TECH_MINING"], + 2, "DEFAULT", ), CivVIBoostData( @@ -343,8 +343,8 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_ADVANCED_FLIGHT", "ERA_ATOMIC", - ["TECH_FLIGHT"], - 1, + ["TECH_FLIGHT", "TECH_REFINING", "TECH_MINING"], + 3, "DEFAULT", ), CivVIBoostData( @@ -436,8 +436,8 @@ boosts: List[CivVIBoostData] = [ CivVIBoostData( "BOOST_TECH_COMPOSITES", "ERA_INFORMATION", - ["TECH_COMBUSTION"], - 1, + ["TECH_COMBUSTION", "TECH_REFINING", "TECH_MINING"], + 3, "DEFAULT", ), CivVIBoostData( @@ -470,7 +470,7 @@ boosts: List[CivVIBoostData] = [ "TECH_ELECTRICITY", "TECH_NUCLEAR_FISSION", ], - 1, + 4, "DEFAULT", ), CivVIBoostData( @@ -651,10 +651,11 @@ boosts: List[CivVIBoostData] = [ ), CivVIBoostData( "BOOST_CIVIC_FEUDALISM", - "ERA_MEDIEVAL", + "ERA_CLASSICAL", [], 0, "DEFAULT", + True, ), CivVIBoostData( "BOOST_CIVIC_CIVIL_SERVICE", @@ -662,6 +663,7 @@ boosts: List[CivVIBoostData] = [ [], 0, "DEFAULT", + True, ), CivVIBoostData( "BOOST_CIVIC_MERCENARIES", @@ -790,6 +792,7 @@ boosts: List[CivVIBoostData] = [ [], 0, "DEFAULT", + True ), CivVIBoostData( "BOOST_CIVIC_CONSERVATION", @@ -885,6 +888,7 @@ boosts: List[CivVIBoostData] = [ ["TECH_ROCKETRY"], 1, "DEFAULT", + True ), CivVIBoostData( "BOOST_CIVIC_GLOBALIZATION", diff --git a/worlds/civ_6/test/TestBoostsanity.py b/worlds/civ_6/test/TestBoostsanity.py index 6efed6c6..54ad74cb 100644 --- a/worlds/civ_6/test/TestBoostsanity.py +++ b/worlds/civ_6/test/TestBoostsanity.py @@ -105,3 +105,78 @@ class TestBoostsanityExcluded(CivVITestBase): if "BOOST" in location.name: found_locations += 1 self.assertEqual(found_locations, 0) + + +class TestBoostsanityEraRequired(CivVITestBase): + options = { + "boostsanity": "true", + "progression_style": "none", + "shuffle_goody_hut_rewards": "false", + } + + def test_era_required_boosts_not_accessible_early(self) -> None: + # BOOST_CIVIC_FEUDALISM has EraRequired=True and ERA_CLASSICAL + # It should NOT be accessible in Ancient era + self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM")) + + # BOOST_CIVIC_URBANIZATION has EraRequired=True and ERA_INDUSTRIAL + # It should NOT be accessible in Ancient era + self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION")) + + # BOOST_CIVIC_SPACE_RACE has EraRequired=True and ERA_ATOMIC + # It should NOT be accessible in Ancient era + self.assertFalse(self.can_reach_location("BOOST_CIVIC_SPACE_RACE")) + + # Regular boosts without EraRequired should be accessible + self.assertTrue(self.can_reach_location("BOOST_TECH_SAILING")) + self.assertTrue(self.can_reach_location("BOOST_CIVIC_MILITARY_TRADITION")) + + def test_era_required_boosts_accessible_in_correct_era(self) -> None: + # Collect items to reach Classical era + self.collect_by_name(["Mining", "Bronze Working", "Astrology", "Writing", + "Irrigation", "Sailing", "Animal Husbandry", + "State Workforce", "Foreign Trade"]) + + # BOOST_CIVIC_FEUDALISM should now be accessible in Classical era + self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM")) + + # BOOST_CIVIC_URBANIZATION still not accessible (requires Industrial) + self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION")) + + # Collect more items to reach Industrial era + self.collect_all_but(["TECH_ROCKETRY"]) + + # Now BOOST_CIVIC_URBANIZATION should be accessible + self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION")) + + +class TestBoostsanityEraRequiredWithProgression(CivVITestBase): + options = { + "boostsanity": "true", + "progression_style": "eras_and_districts", + "shuffle_goody_hut_rewards": "false", + } + + def test_era_required_with_progressive_eras(self) -> None: + # Collect all items except Progressive Era + self.collect_all_but(["Progressive Era"]) + + # Even with all other items, era-required boosts should not be accessible + self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM")) + self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION")) + + # Collect enough Progressive Era items to reach Classical (needs 2) + self.collect(self.get_item_by_name("Progressive Era")) + self.collect(self.get_item_by_name("Progressive Era")) + + # BOOST_CIVIC_FEUDALISM should now be accessible + self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM")) + + # But BOOST_CIVIC_URBANIZATION still requires Industrial era (needs 5 total) + self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION")) + + # Collect 3 more Progressive Era items to reach Industrial + self.collect_by_name(["Progressive Era", "Progressive Era", "Progressive Era"]) + + # Now BOOST_CIVIC_URBANIZATION should be accessible + self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))