Core: Add connect_entrances world step/stage (#4420)
* Add connect_entrances * update ER docs * fix that test, but also ew * Add a test that asserts the new finalization * Rewrite test a bit * rewrite some more * blank line * rewrite rewrite rewrite * rewrite rewrite rewrite * RE. WRITE. * oops * Bruh * I guess, while we're at it * giga oops * It's been a long day * Switch KH1 over to this design with permission of GICU * Revert * Oops * Bc I like it * Update locations.py
This commit is contained in:
parent
96f469c737
commit
436c0a4104
1
Main.py
1
Main.py
|
@ -149,6 +149,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||||
multiworld.worlds[1].options.non_local_items.value = set()
|
multiworld.worlds[1].options.non_local_items.value = set()
|
||||||
multiworld.worlds[1].options.local_items.value = set()
|
multiworld.worlds[1].options.local_items.value = set()
|
||||||
|
|
||||||
|
AutoWorld.call_all(multiworld, "connect_entrances")
|
||||||
AutoWorld.call_all(multiworld, "generate_basic")
|
AutoWorld.call_all(multiworld, "generate_basic")
|
||||||
|
|
||||||
# remove starting inventory from pool items.
|
# remove starting inventory from pool items.
|
||||||
|
|
|
@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
|
||||||
|
|
||||||
#### When to call `randomize_entrances`
|
#### When to call `randomize_entrances`
|
||||||
|
|
||||||
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
|
The correct step for this is `World.connect_entrances`.
|
||||||
|
|
||||||
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
|
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`.
|
||||||
This means 2 things about when you can call ER:
|
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better
|
||||||
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
|
together.
|
||||||
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
|
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`.
|
||||||
and create your events before you call ER if you want to guarantee a correct output.
|
It is fine for your Entrances to be connected differently or not at all before this step.
|
||||||
|
|
||||||
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
|
|
||||||
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
|
|
||||||
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
|
|
||||||
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
|
|
||||||
well.
|
|
||||||
|
|
||||||
#### Informing your client about randomized entrances
|
#### Informing your client about randomized entrances
|
||||||
|
|
||||||
|
|
|
@ -490,6 +490,9 @@ In addition, the following methods can be implemented and are called in this ord
|
||||||
after this step. Locations cannot be moved to different regions after this step.
|
after this step. Locations cannot be moved to different regions after this step.
|
||||||
* `set_rules(self)`
|
* `set_rules(self)`
|
||||||
called to set access and item rules on locations and entrances.
|
called to set access and item rules on locations and entrances.
|
||||||
|
* `connect_entrances(self)`
|
||||||
|
by the end of this step, all entrances must exist and be connected to their source and target regions.
|
||||||
|
Entrance randomization should be done here.
|
||||||
* `generate_basic(self)`
|
* `generate_basic(self)`
|
||||||
player-specific randomization that does not affect logic can be done here.
|
player-specific randomization that does not affect logic can be done here.
|
||||||
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
|
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
|
||||||
|
|
|
@ -18,7 +18,15 @@ def run_locations_benchmark():
|
||||||
|
|
||||||
class BenchmarkRunner:
|
class BenchmarkRunner:
|
||||||
gen_steps: typing.Tuple[str, ...] = (
|
gen_steps: typing.Tuple[str, ...] = (
|
||||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
"generate_early",
|
||||||
|
"create_regions",
|
||||||
|
"create_items",
|
||||||
|
"set_rules",
|
||||||
|
"connect_entrances",
|
||||||
|
"generate_basic",
|
||||||
|
"pre_fill",
|
||||||
|
)
|
||||||
|
|
||||||
rule_iterations: int = 100_000
|
rule_iterations: int = 100_000
|
||||||
|
|
||||||
if sys.version_info >= (3, 9):
|
if sys.version_info >= (3, 9):
|
||||||
|
|
|
@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
|
||||||
from worlds import network_data_package
|
from worlds import network_data_package
|
||||||
from worlds.AutoWorld import World, call_all
|
from worlds.AutoWorld import World, call_all
|
||||||
|
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
gen_steps = (
|
||||||
|
"generate_early",
|
||||||
|
"create_regions",
|
||||||
|
"create_items",
|
||||||
|
"set_rules",
|
||||||
|
"connect_entrances",
|
||||||
|
"generate_basic",
|
||||||
|
"pre_fill",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_solo_multiworld(
|
def setup_solo_multiworld(
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import unittest
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister, call_all, World
|
||||||
|
from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
|
class TestBase(unittest.TestCase):
|
||||||
|
def test_entrance_connection_steps(self):
|
||||||
|
"""Tests that Entrances are connected and not changed after connect_entrances."""
|
||||||
|
def get_entrance_name_to_source_and_target_dict(world: World):
|
||||||
|
return [
|
||||||
|
(entrance.name, entrance.parent_region, entrance.connected_region)
|
||||||
|
for entrance in world.get_entrances()
|
||||||
|
]
|
||||||
|
|
||||||
|
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
|
||||||
|
additional_steps = ("generate_basic", "pre_fill")
|
||||||
|
|
||||||
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
with self.subTest("Game", game_name=game_name):
|
||||||
|
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||||
|
|
||||||
|
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
|
||||||
|
f"{game_name} had unconnected entrances after connect_entrances"
|
||||||
|
)
|
||||||
|
|
||||||
|
for step in additional_steps:
|
||||||
|
with self.subTest("Step", step=step):
|
||||||
|
call_all(multiworld, step)
|
||||||
|
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
|
||||||
|
)
|
|
@ -67,7 +67,7 @@ class TestBase(unittest.TestCase):
|
||||||
def test_itempool_not_modified(self):
|
def test_itempool_not_modified(self):
|
||||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
|
||||||
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
|
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
|
||||||
worlds_to_test = {game: world
|
worlds_to_test = {game: world
|
||||||
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
|
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
|
||||||
|
@ -84,7 +84,7 @@ class TestBase(unittest.TestCase):
|
||||||
def test_locality_not_modified(self):
|
def test_locality_not_modified(self):
|
||||||
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
|
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
|
||||||
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
|
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
|
||||||
for game_name, world_type in worlds_to_test.items():
|
for game_name, world_type in worlds_to_test.items():
|
||||||
with self.subTest("Game", game=game_name):
|
with self.subTest("Game", game=game_name):
|
||||||
|
|
|
@ -45,6 +45,12 @@ class TestBase(unittest.TestCase):
|
||||||
self.assertEqual(location_count, len(multiworld.get_locations()),
|
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||||
f"{game_name} modified locations count during rule creation")
|
f"{game_name} modified locations count during rule creation")
|
||||||
|
|
||||||
|
call_all(multiworld, "connect_entrances")
|
||||||
|
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||||
|
f"{game_name} modified region count during rule creation")
|
||||||
|
self.assertEqual(location_count, len(multiworld.get_locations()),
|
||||||
|
f"{game_name} modified locations count during rule creation")
|
||||||
|
|
||||||
call_all(multiworld, "generate_basic")
|
call_all(multiworld, "generate_basic")
|
||||||
self.assertEqual(region_count, len(multiworld.get_regions()),
|
self.assertEqual(region_count, len(multiworld.get_regions()),
|
||||||
f"{game_name} modified region count during generate_basic")
|
f"{game_name} modified region count during generate_basic")
|
||||||
|
|
|
@ -2,11 +2,11 @@ import unittest
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
from . import setup_solo_multiworld
|
from . import setup_solo_multiworld, gen_steps
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
class TestBase(unittest.TestCase):
|
||||||
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
|
gen_steps = gen_steps
|
||||||
|
|
||||||
default_settings_unreachable_regions = {
|
default_settings_unreachable_regions = {
|
||||||
"A Link to the Past": {
|
"A Link to the Past": {
|
||||||
|
|
|
@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister):
|
||||||
"""Method for setting the rules on the World's regions and locations."""
|
"""Method for setting the rules on the World's regions and locations."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def connect_entrances(self) -> None:
|
||||||
|
"""Method to finalize the source and target regions of the World's entrances"""
|
||||||
|
pass
|
||||||
|
|
||||||
def generate_basic(self) -> None:
|
def generate_basic(self) -> None:
|
||||||
"""
|
"""
|
||||||
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.
|
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.
|
||||||
|
|
|
@ -483,6 +483,8 @@ def create_regions(multiworld: MultiWorld, player: int, options):
|
||||||
for name, data in regions.items():
|
for name, data in regions.items():
|
||||||
multiworld.regions.append(create_region(multiworld, player, name, data))
|
multiworld.regions.append(create_region(multiworld, player, name, data))
|
||||||
|
|
||||||
|
|
||||||
|
def connect_entrances(multiworld: MultiWorld, player: int):
|
||||||
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
|
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
|
||||||
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
|
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
|
||||||
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
|
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
|
||||||
|
@ -500,6 +502,7 @@ def create_regions(multiworld: MultiWorld, player: int, options):
|
||||||
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
|
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
|
||||||
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
|
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
|
||||||
|
|
||||||
|
|
||||||
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
|
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
|
||||||
region = Region(name, player, multiworld)
|
region = Region(name, player, multiworld)
|
||||||
if data.locations:
|
if data.locations:
|
||||||
|
|
|
@ -6,7 +6,7 @@ from worlds.AutoWorld import WebWorld, World
|
||||||
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
|
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
|
||||||
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
|
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
|
||||||
from .Options import KH1Options, kh1_option_groups
|
from .Options import KH1Options, kh1_option_groups
|
||||||
from .Regions import create_regions
|
from .Regions import connect_entrances, create_regions
|
||||||
from .Rules import set_rules
|
from .Rules import set_rules
|
||||||
from .Presets import kh1_option_presets
|
from .Presets import kh1_option_presets
|
||||||
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
||||||
|
@ -243,6 +243,9 @@ class KH1World(World):
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self.multiworld, self.player, self.options)
|
create_regions(self.multiworld, self.player, self.options)
|
||||||
|
|
||||||
|
def connect_entrances(self):
|
||||||
|
connect_entrances(self.multiworld, self.player)
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
|
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
|
||||||
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
|
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
|
||||||
|
|
Loading…
Reference in New Issue