New Game Implementation: Civilization VI (#3736)

* Init

* remove submodule

* Init

* Update docs

* Fix tests

* Update to use apcivvi

* Update Readme and codeowners

* Minor changes

* Remove .value from options (except starting hint)

* Minor updates

* remove unnecessary property

* Cleanup Rules and Region

* Fix output file generation

* Implement feedback

* Remove 'AP' tag and fix issue with format strings and using same quotes

* Update worlds/civ_6/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Minor docs changes

* minor updates

* Small rework of create items

* Minor updates

* Remove unused variable

* Move client to Launcher Components with rest of similar clients

* Revert "Move client to Launcher Components with rest of similar clients"

This reverts commit f9fd5df9fdf19eaf4f1de54e21e3c33a74f02364.

* modify component

* Fix generation issues

* Fix tests

* Minor change

* Add improvement and test case

* Minor options changes

* .

* Preliminary Review

* Fix failing test due to slot data serialization

* Format json

* Remove exclude missable boosts

* Update options (update goody hut text, make research multiplier a range)

* Update docs punctuation and slot data init

* Move priority/excluded locations into options

* Implement docs PR feedback

* PR Feedback for options

* PR feedback misc

* Update location classification and fix client type

* Fix typings

* Update research cost multiplier

* Remove unnecessary location priority code

* Remove extrenous use of items()

* WIP PR Feedback

* WIP PR Feedback

* Add victory event

* Add option set for death link effect

* PR improvements

* Update post fill hint to support items with multiple classifications

* remove unnecessary len

* Move location exclusion logic

* Update test to use set instead of accidental dict

* Update docs around progressive eras and boost locations

* Update docs for options to be more readable

* Fix issue with filler items and prehints

* Update filler_data to be static

* Update links in docs

* Minor updates and PR feedback

* Update boosts data

* Update era required items

* Update existing techs

* Update existing techs

* move boost data class

* Update reward data

* Update prereq data

* Update new items and progressive districts

* Remove unused code

* Make filler item name func more efficient

* Update death link text

* Move Civ6 to the end of readme

* Fix bug with hidden locations and location.name

* Partial PR Feedback Implementation

* Format changes

* Minor review feedback

* Modify access rules to use list created in generate_early

* Modify boost rules to precalculate requirements

* Remove option checks from access rules

* Fix issue with pre initialized dicts

* Add inno setup for civ6 client

* Update inno_setup.iss

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
This commit is contained in:
Carter Hesterman
2025-03-10 07:53:26 -06:00
committed by GitHub
parent 21ffc0fc54
commit 5f73c245fc
37 changed files with 6111 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
from Fill import distribute_items_restrictive
from ..Data import get_boosts_data
from . import CivVITestBase
class TestBoostsanityIncluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "none",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_get_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
num_boost_locations = len(get_boosts_data())
self.assertEqual(found_locations, num_boost_locations)
def test_boosts_require_prereqs_no_progressives(self) -> None:
self.world_setup()
location = "BOOST_TECH_ADVANCED_BALLISTICS"
items_to_give = ["Refining", "Electricity", "Apprenticeship", "Industrialization"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
class TestBoostsanityIncludedNoProgressiveDistricts(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "districts_only",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_get_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
num_boost_locations = len(get_boosts_data())
self.assertEqual(found_locations, num_boost_locations)
class TestBoostsanityPrereqsWithProgressiveDistricts(CivVITestBase):
options = {
"progressive_eras": "true",
"boostsanity": "true",
"progression_style": "districts_only",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_require_progressive_prereqs_optional(self) -> None:
location = "BOOST_TECH_NUCLEAR_FUSION"
items_to_give = ["Progressive Industrial Zone", "Progressive Industrial Zone"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
def tests_boosts_require_correct_progressive_district_count(self) -> None:
location = "BOOST_TECH_RIFLING"
items_to_give = ["Mining", "Progressive Encampment", "Progressive Encampment"]
self.assertFalse(self.can_reach_location(location))
for prereq in items_to_give:
self.collect_by_name(prereq)
is_last_prereq = prereq == items_to_give[-1]
self.assertEqual(self.can_reach_location(location), is_last_prereq)
class TestBoostsanityExcluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"boostsanity": "false",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "false",
}
def test_boosts_are_not_included(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
locations = self.multiworld.get_locations(self.player)
found_locations = 0
for location in locations:
if "BOOST" in location.name:
found_locations += 1
self.assertEqual(found_locations, 0)

View File

@@ -0,0 +1,114 @@
from typing import Dict
from BaseClasses import ItemClassification
from Fill import distribute_items_restrictive
from ..Items import FillerItemRarity, filler_data
from . import CivVITestBase
class TestGoodyHutsIncluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
}
def test_goody_huts_get_included(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
expected_goody_huts = 10
found = 0
for location in self.multiworld.get_locations(self.player):
if location.name.startswith("GOODY_HUT_"):
found += 1
self.assertEqual(found, expected_goody_huts)
class TestGoodyHutsExcluded(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "false",
}
def test_goody_huts_are_not_included(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
found_goody_huts = 0
for location in self.multiworld.get_locations(self.player):
if location.name.startswith("GOODY_HUT_"):
found_goody_huts += 1
self.assertEqual(found_goody_huts, 0)
class TestFillerItemsIncludedByRarity(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
"boostsanity": "true"
}
def test_filler_items_are_included_by_rarity(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
rarity_counts: Dict[FillerItemRarity, int] = {
FillerItemRarity.COMMON: 0,
FillerItemRarity.UNCOMMON: 0,
FillerItemRarity.RARE: 0,
}
total_filler_items = 0
for item in self.multiworld.itempool:
if item.classification == ItemClassification.filler:
rarity = filler_data[item.name].rarity
rarity_counts[rarity] += 1
total_filler_items += 1
expected_counts = {
FillerItemRarity.COMMON: 101,
FillerItemRarity.UNCOMMON: 27,
FillerItemRarity.RARE: 4,
}
for rarity, expected in expected_counts.items():
self.assertEqual(rarity_counts[rarity], expected, f"Expected {expected} {rarity} items, found {rarity_counts[rarity]}")
class TestFillerItemsIncludedByRarityWithoutBoostsanity(CivVITestBase):
auto_construct = False
options = {
"progressive_eras": "true",
"progressive_districts": "true",
"shuffle_goody_hut_rewards": "true",
"boostsanity": "false"
}
def test_filler_items_are_included_by_rarity_without_boostsanity(self) -> None:
self.world_setup()
self.world.generate_early()
distribute_items_restrictive(self.multiworld)
rarity_counts: Dict[FillerItemRarity, int] = {
FillerItemRarity.COMMON: 0,
FillerItemRarity.UNCOMMON: 0,
FillerItemRarity.RARE: 0,
}
total_filler_items = 0
for item in self.multiworld.itempool:
if item.classification == ItemClassification.filler:
rarity = filler_data[item.name].rarity
rarity_counts[rarity] += 1
total_filler_items += 1
expected_counts = {
FillerItemRarity.COMMON: 7,
FillerItemRarity.UNCOMMON: 2,
FillerItemRarity.RARE: 1,
}
for rarity, expected in expected_counts.items():
self.assertEqual(rarity_counts[rarity], expected, f"Expected {expected} {rarity} items, found {rarity_counts[rarity]}")

View File

@@ -0,0 +1,234 @@
from typing import Callable, List
from BaseClasses import CollectionState
from ..Data import get_era_required_items_data
from ..Enum import EraType
from ..ProgressiveDistricts import convert_items_to_progressive_items
from ..Items import get_item_by_civ_name
from . import CivVITestBase
def collect_items_for_era(test: CivVITestBase, era: EraType) -> None:
era_required_items = get_era_required_items_data()
items = [
get_item_by_civ_name(item, test.world.item_table).name
for item in era_required_items[era.value]
]
test.collect_by_name(items)
def collect_items_for_era_progressive(test: CivVITestBase, era: EraType) -> None:
era_progression_items = get_era_required_items_data()
progressive_items = convert_items_to_progressive_items(
era_progression_items[era.value]
)
items = [
get_item_by_civ_name(item, test.world.item_table).name
for item in progressive_items
]
for item in items:
test.collect(test.get_item_by_name(item))
def verify_eras_accessible(
test: CivVITestBase,
state: CollectionState,
collect_func: Callable[[CivVITestBase, EraType], None],
) -> None:
"""Collect for an era, then check if the next era is accessible and the one after that is not"""
for era in EraType:
if era == EraType.ERA_ANCIENT:
test.assertTrue(state.can_reach(era.value, "Region", test.player))
else:
test.assertFalse(state.can_reach(era.value, "Region", test.player))
eras = [
EraType.ERA_ANCIENT,
EraType.ERA_CLASSICAL,
EraType.ERA_MEDIEVAL,
EraType.ERA_RENAISSANCE,
EraType.ERA_INDUSTRIAL,
EraType.ERA_MODERN,
EraType.ERA_ATOMIC,
EraType.ERA_INFORMATION,
EraType.ERA_FUTURE,
]
for i in range(len(eras) - 1):
collect_func(test, eras[i])
test.assertTrue(state.can_reach(eras[i + 1].value, "Region", test.player))
if i + 2 < len(eras):
test.assertFalse(state.can_reach(eras[i + 2].value, "Region", test.player))
class TestNonProgressiveRegionRequirements(CivVITestBase):
options = {
"progression_style": "none",
"boostsanity": "false",
}
def test_eras_are_accessible_without_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era)
class TestNonProgressiveRegionRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "none",
"boostsanity": "true",
}
def test_eras_are_accessible_without_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era)
class TestProgressiveDistrictRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "districts_only",
"boostsanity": "true",
}
def test_eras_are_accessible_with_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era_progressive)
class TestProgressiveDistrictRequirements(CivVITestBase):
options = {
"progression_style": "districts_only",
"boostsanity": "false",
}
def test_eras_are_accessible_with_progressive_districts(self) -> None:
state = self.multiworld.state
verify_eras_accessible(self, state, collect_items_for_era_progressive)
def test_progressive_districts_are_required(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Encampment"])
self.assertFalse(state.can_reach("ERA_CLASSICAL", "Region", self.player))
self.assertFalse(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_CLASSICAL", "Region", self.player))
self.assertFalse(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_RENAISSANCE", "Region", self.player))
self.assertFalse(state.can_reach("ERA_MODERN", "Region", self.player))
self.collect(self.get_item_by_name("Progressive Encampment"))
self.assertTrue(state.can_reach("ERA_MODERN", "Region", self.player))
class TestProgressiveEraRequirements(CivVITestBase):
options = {
"progression_style": "eras_and_districts",
}
def test_eras_are_accessible_with_progressive_eras(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Era"])
def check_eras_accessible(eras: List[EraType]):
for era in EraType:
if era in eras:
self.assertTrue(state.can_reach(era.value, "Region", self.player))
else:
self.assertFalse(state.can_reach(era.value, "Region", self.player))
progresive_era_item = self.get_item_by_name("Progressive Era")
accessible_eras = [EraType.ERA_ANCIENT]
check_eras_accessible(accessible_eras)
# Classical era requires 2 progressive era items
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_CLASSICAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MEDIEVAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_RENAISSANCE]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INDUSTRIAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MODERN]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_ATOMIC]
check_eras_accessible(accessible_eras)
# Since we collect 2 in the ancient era, information and future era have same logic requirement
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INFORMATION]
accessible_eras += [EraType.ERA_FUTURE]
check_eras_accessible(accessible_eras)
class TestProgressiveEraRequirementsWithBoostsanity(CivVITestBase):
options = {
"progression_style": "eras_and_districts",
"boostsanity": "true",
}
def test_eras_are_accessible_with_progressive_eras(self) -> None:
state = self.multiworld.state
self.collect_all_but(["Progressive Era"])
def check_eras_accessible(eras: List[EraType]):
for era in EraType:
if era in eras:
self.assertTrue(
state.can_reach(era.value, "Region", self.player),
"Failed for era: " + era.value,
)
else:
self.assertFalse(
state.can_reach(era.value, "Region", self.player),
"Failed for era: " + era.value,
)
progresive_era_item = self.get_item_by_name("Progressive Era")
accessible_eras = [EraType.ERA_ANCIENT]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_CLASSICAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MEDIEVAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_RENAISSANCE]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INDUSTRIAL]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_MODERN]
check_eras_accessible(accessible_eras)
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_ATOMIC]
check_eras_accessible(accessible_eras)
# Since we collect 2 in the ancient era, information and future era have same logic requirement
self.collect(progresive_era_item)
accessible_eras += [EraType.ERA_INFORMATION]
accessible_eras += [EraType.ERA_FUTURE]
check_eras_accessible(accessible_eras)

View File

@@ -0,0 +1,125 @@
from BaseClasses import ItemClassification
from Fill import distribute_items_restrictive
from ..Enum import CivVICheckType
from . import CivVITestBase
class TestStartingHints(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression", "Useful", "Filler"}),
}
def test_all_tech_civic_items_are_hinted_default(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
for location_name, location_data in self.world.location_table.items():
if location_data.location_type == CivVICheckType.CIVIC or location_data.location_type == CivVICheckType.TECH:
self.assertIn(location_name, start_location_hints)
else:
self.assertNotIn(location_name, start_location_hints)
class TestOnlyProgressionItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression"}),
}
def test_only_progression_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertTrue(len(start_location_hints) > 0)
for hint in start_location_hints:
location_data = self.world.get_location(hint)
if location_data.item:
self.assertTrue(location_data.item.classification == ItemClassification.progression)
else:
self.assertTrue(False, "Location has no item")
class TestNoJunkItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Progression", "Useful"}),
"boostsanity": "true",
"shuffle_goody_hut_rewards": "true",
}
def test_no_junk_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
item = self.multiworld.get_location("TECH_AP_ANCIENT_01", self.player).item
self.assertIsNotNone(item)
if item:
item.classification = ItemClassification.filler
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertTrue(len(start_location_hints) > 0)
self.assertNotIn("TECH_AP_ANCIENT_01", start_location_hints)
class TestOnlyJunkItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({"Filler"}),
}
def test_only_junk_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
item = self.multiworld.get_location("TECH_AP_ANCIENT_01", self.player).item
self.assertIsNotNone(item)
if item:
item.classification = ItemClassification.filler
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertEqual(len(start_location_hints), 1)
self.assertIn("TECH_AP_ANCIENT_01", start_location_hints)
class TestNoItemsHinted(CivVITestBase):
run_default_tests = False # type: ignore
auto_construct = False
options = {
"progressive_eras": "true",
"death_link": "true",
"death_link_effect": "unit_killed",
"progressive_districts": "true",
"pre_hint_items": set({}),
}
def test_no_items_are_hinted(self) -> None:
self.world_setup()
distribute_items_restrictive(self.multiworld)
self.world.post_fill()
start_location_hints = self.world.options.start_location_hints.value
self.assertEqual(len(start_location_hints), 0)

View File

@@ -0,0 +1,8 @@
from typing import ClassVar
from test.bases import WorldTestBase
class CivVITestBase(WorldTestBase):
game = "Civilization VI"
player: ClassVar[int] = 1