From d08d716966b3ccfc42b3897802ee0ba117148477 Mon Sep 17 00:00:00 2001 From: Jarno Date: Mon, 20 Dec 2021 14:26:16 +0100 Subject: [PATCH 01/36] [Timespinner] Added orb damage rando flag --- worlds/timespinner/Options.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index d3a848c6..da3cff08 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -50,6 +50,10 @@ class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" +class DamageRando(Toggle): + "Each orb has a high chance of having lower base damage and a low chance of having much higher base damage." + display_name = "Damage Rando" + # Some options that are available in the timespinner randomizer arent currently implemented timespinner_options: Dict[str, Toggle] = { "StartWithJewelryBox": StartWithJewelryBox, @@ -64,6 +68,7 @@ timespinner_options: Dict[str, Toggle] = { #"StinkyMaw": StinkyMaw, "GyreArchives": GyreArchives, "Cantoran": Cantoran, + "DamageRando": DamageRando, "DeathLink": DeathLink, } From d5abadc6d0c9c395b099265f394161e3eb4d0e6c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 20 Dec 2021 23:10:04 +0100 Subject: [PATCH 02/36] Requirements: remove no longer used appdirs and move kivy to core --- requirements.txt | 2 +- worlds/factorio/requirements.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8a35b905..0b7a077d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,6 @@ colorama>=0.4.4 websockets>=10.1 PyYAML>=6.0 fuzzywuzzy>=0.18.0 -appdirs>=1.4.4 jinja2>=3.0.3 schema>=0.7.4 +kivy>=2.0.0 diff --git a/worlds/factorio/requirements.txt b/worlds/factorio/requirements.txt index 92254420..5f0daad4 100644 --- a/worlds/factorio/requirements.txt +++ b/worlds/factorio/requirements.txt @@ -1,3 +1,2 @@ -kivy>=2.0.0 factorio-rcon-py>=1.2.1 schema>=0.7.4 From 97d6e805565b244ae6fe02e6625760b97ba9d310 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 21 Dec 2021 15:31:04 +0100 Subject: [PATCH 03/36] Bump --- Utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index 02a25e70..df5d3c96 100644 --- a/Utils.py +++ b/Utils.py @@ -23,7 +23,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.2.1" +__version__ = "0.2.2" version_tuple = tuplize_version(__version__) from yaml import load, dump, safe_load From 52e01c0925e76edaa090a8cf67f34e9f30a0e545 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 22 Dec 2021 14:00:41 +0100 Subject: [PATCH 04/36] Factorio: fill in some missing doc strings --- worlds/factorio/Options.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b8b06cb2..09df0eab 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -131,6 +131,8 @@ class RecipeTime(Choice): class Progressive(Choice): + """Merges together Technologies like "automation-1" to "automation-3" into 3 copies of "Progressive Automation", + which awards them in order.""" displayname = "Progressive Technologies" option_off = 0 option_grouped_random = 1 @@ -151,17 +153,19 @@ class RecipeIngredients(Choice): class FactorioStartItems(ItemDict): + """Mapping of Factorio internal item-name to amount granted on start.""" displayname = "Starting Items" verify_item_name = False default = {"burner-mining-drill": 19, "stone-furnace": 19} class FactorioFreeSampleBlacklist(OptionSet): + """Set of items that should never be granted from Free Samples""" displayname = "Free Sample Blacklist" class FactorioFreeSampleWhitelist(OptionSet): - """overrides any free sample blacklist present. This may ruin the balance of the mod, be forewarned.""" + """Overrides any free sample blacklist present. This may ruin the balance of the mod, be warned.""" displayname = "Free Sample Whitelist" @@ -180,6 +184,7 @@ class EvolutionTrapCount(TrapCount): class EvolutionTrapIncrease(Range): + """How much an Evolution Trap increases the enemy evolution""" displayname = "Evolution Trap % Effect" range_start = 1 default = 10 @@ -187,6 +192,8 @@ class EvolutionTrapIncrease(Range): class FactorioWorldGen(OptionDict): + """World Generation settings. Overview of options at https://wiki.factorio.com/Map_generator, + with in-depth documentation at https://lua-api.factorio.com/latest/Concepts.html#MapGenSettings""" displayname = "World Generation" # FIXME: do we want default be a rando-optimized default or in-game DS? value: typing.Dict[str, typing.Dict[str, typing.Any]] @@ -320,6 +327,7 @@ class FactorioWorldGen(OptionDict): class ImportedBlueprint(DefaultOnToggle): + """Allow or Disallow Blueprints from outside the current savegame.""" displayname = "Blueprints" From 6eab838a708ad7d5d15aa0214039892897deddf4 Mon Sep 17 00:00:00 2001 From: wafflesoup Date: Thu, 23 Dec 2021 13:21:20 -0600 Subject: [PATCH 05/36] Update plando_en.md fixed capitalization in Timespinner example --- WebHostLib/static/assets/tutorial/archipelago/plando_en.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/archipelago/plando_en.md b/WebHostLib/static/assets/tutorial/archipelago/plando_en.md index 7b2cdeee..52411c06 100644 --- a/WebHostLib/static/assets/tutorial/archipelago/plando_en.md +++ b/WebHostLib/static/assets/tutorial/archipelago/plando_en.md @@ -74,7 +74,7 @@ plando_items: - item: Empire Orb: 1 Radiant Orb: 1 - location: Starter Chest 1 + location: Starter chest 1 from_pool: true world: true percentage: 50 @@ -177,4 +177,4 @@ when you leave the interior you will exit to the cave 45 ledge. Going into the c lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take you to their locations as normal but leaving old man cave will exit at Agahnim's Tower. 2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the Minecraft -connection plando to work structure shuffle must be enabled. \ No newline at end of file +connection plando to work structure shuffle must be enabled. From 61310c50d7792d1ebb0afd1c9320691a039a6497 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Mon, 27 Dec 2021 15:29:09 +0100 Subject: [PATCH 06/36] Use absolute path when starting SNI Causes reliability issues when relative path is used. --- SNIClient.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 6d7540ea..f6509938 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -532,9 +532,9 @@ def launch_sni(ctx: Context): snes_logger.info(f"Attempting to start {sni_path}") import sys if not sys.stdout: # if it spawns a visible console, may as well populate it - subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path)) + subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path)) else: - subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, + subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: snes_logger.info( From 4ef0e054d616115db36de73ecbf2e5e644e02cce Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Fri, 24 Dec 2021 23:00:32 +0100 Subject: [PATCH 07/36] [TS] Move 3 transition chest under gyre archives flag + some refactoring --- worlds/timespinner/Locations.py | 95 +++++++++++++++------------------ 1 file changed, 43 insertions(+), 52 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 921e215e..b1fc164e 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -11,6 +11,9 @@ class LocationData(NamedTuple): rule: Callable = lambda state: True def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]: + # 1337000 - 1337155 Generic locations + # 1337171 - 1337175 New Pickup checks + # 1337246 - 1337249 Ancient Pyramid location_table: List[LocationData] = [ # PresentItemLocations LocationData('Tutorial', 'Yo Momma 1', 1337000), @@ -180,19 +183,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Royal towers (upper)', 'Aelana\'s pedestal', 1337154), LocationData('Royal towers (upper)', 'Aelana\'s chest', 1337155), - # 1337176 - 1337176 Cantoran - - # 1337177 - 1337236 Reserved - - # 1337237 - 1337238 GyreArchives - - # PyramidItemLocations - LocationData('Ancient Pyramid (right)', 'Transition chest 1', 1337239), - LocationData('Ancient Pyramid (right)', 'Transition chest 2', 1337240), - LocationData('Ancient Pyramid (right)', 'Transition chest 3', 1337241), - - # 1337242 - 1337245 GyreArchives - + #AncientPyramidLocations LocationData('Ancient Pyramid (left)', 'Why not it\'s right there', 1337246), LocationData('Ancient Pyramid (left)', 'Conviction guarded room', 1337247), LocationData('Ancient Pyramid (right)', 'Pit secret room', 1337248, lambda state: state._timespinner_can_break_walls(world, player)), @@ -200,48 +191,48 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Ancient Pyramid (right)', 'Killed Nightmare', EventId) ] - downloadable_locations: Tuple[LocationData, ...] = ( - # DownloadTerminals - LocationData('Library', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)), - LocationData('Library', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)), - # 1337158 Is Lost in time - LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)), - LocationData('Library', 'V terminal 1', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), - LocationData('Library', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), - LocationData('Library', 'V terminal 3', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), - LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)), - LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)), - LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)), - LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)), - LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)), - LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)), - LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)), - LocationData('The lab (power off)', 'Lab terminal right', 1337170, lambda state: state.has('Tablet', player)) - ) + # 1337156 - 1337170 Downloads + if not world or is_option_enabled(world, player, "DownloadableItems"): + location_table += ( + LocationData('Library', 'Library terminal 2', 1337156, lambda state: state.has('Tablet', player)), + LocationData('Library', 'Library terminal 1', 1337157, lambda state: state.has('Tablet', player)), + # 1337158 Is Lost in time + LocationData('Library', 'Library terminal 3', 1337159, lambda state: state.has('Tablet', player)), + LocationData('Library', 'V terminal 1', 1337160, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), + LocationData('Library', 'V terminal 2', 1337161, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), + LocationData('Library', 'V terminal 3', 1337162, lambda state: state.has_all({'Tablet', 'Library Keycard V'}, player)), + LocationData('Library top', 'Backer room terminal', 1337163, lambda state: state.has('Tablet', player)), + LocationData('Varndagroth tower right (elevator)', 'Medbay', 1337164, lambda state: state.has('Tablet', player) and state._timespinner_has_keycard_B(world, player)), + LocationData('The lab (upper)', 'Chest and download terminal', 1337165, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Lab terminal middle', 1337166, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Sentry platform terminal', 1337167, lambda state: state.has('Tablet', player)), + LocationData('The lab', 'Experiment 13 terminal', 1337168, lambda state: state.has('Tablet', player)), + LocationData('The lab', 'Lab terminal left', 1337169, lambda state: state.has('Tablet', player)), + LocationData('The lab (power off)', 'Lab terminal right', 1337170, lambda state: state.has('Tablet', player)) + ) - gyre_archives_locations: Tuple[LocationData, ...] = ( - LocationData('The lab (upper)', 'Ravenlord post fight (pedestal)', 1337237, lambda state: state.has('Merchant Crow', player)), - LocationData('Library top', 'Ifrit post fight (pedestal)', 1337238, lambda state: state.has('Kobo', player)), - LocationData('The lab (upper)', 'Ravenlord pre fight', 1337242, lambda state: state.has('Merchant Crow', player)), - LocationData('The lab (upper)', 'Ravenlord post fight (chest)', 1337243, lambda state: state.has('Merchant Crow', player)), - LocationData('Library top', 'Ifrit pre fight', 1337244, lambda state: state.has('Kobo', player)), - LocationData('Library top', 'Ifrit post fight (chest)', 1337245, lambda state: state.has('Kobo', player)), - ) + # 1337176 - 1337176 Cantoran + if not world or is_option_enabled(world, player, "Cantoran"): + location_table += ( + LocationData('Left Side forest Caves', 'Cantoran', 1337176), + ) - cantoran_locations: Tuple[LocationData, ...] = ( - LocationData('Left Side forest Caves', 'Cantoran', 1337176), - ) - - if not world: - return ( *location_table, *downloadable_locations, *gyre_archives_locations, *cantoran_locations ) - - if is_option_enabled(world, player, "DownloadableItems"): - location_table.extend(downloadable_locations) - if is_option_enabled(world, player, "GyreArchives"): - location_table.extend(gyre_archives_locations) - if is_option_enabled(world, player, "Cantoran"): - location_table.extend(cantoran_locations) + # 1337177 - 1337236 Reserved for future use + # 1337237 - 1337245 GyreArchives + if not world or is_option_enabled(world, player, "GyreArchives"): + location_table += ( + LocationData('The lab (upper)', 'Ravenlord post fight (pedestal)', 1337237, lambda state: state.has('Merchant Crow', player)), + LocationData('Library top', 'Ifrit post fight (pedestal)', 1337238, lambda state: state.has('Kobo', player)), + LocationData('Ancient Pyramid (right)', 'Transition chest 1', 1337239), + LocationData('Ancient Pyramid (right)', 'Transition chest 2', 1337240), + LocationData('Ancient Pyramid (right)', 'Transition chest 3', 1337241), + LocationData('The lab (upper)', 'Ravenlord pre fight', 1337242, lambda state: state.has('Merchant Crow', player)), + LocationData('The lab (upper)', 'Ravenlord post fight (chest)', 1337243, lambda state: state.has('Merchant Crow', player)), + LocationData('Library top', 'Ifrit pre fight', 1337244, lambda state: state.has('Kobo', player)), + LocationData('Library top', 'Ifrit post fight (chest)', 1337245, lambda state: state.has('Kobo', player)), + ) + return tuple(location_table) From 1f4ddc295ab017c6c1e084529cfa1a48354d2a35 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 27 Dec 2021 14:53:55 -0600 Subject: [PATCH 08/36] tutorials: Point lttp tutorial to SNC instead of Z3. Update some deprecated text. --- .../assets/tutorial/zelda3/multiworld_en.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md index f75d3b21..59a47563 100644 --- a/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md +++ b/WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md @@ -1,10 +1,10 @@ # A Link to the Past Randomizer Setup Guide ## Required Software -- [Z3Client](https://github.com/ArchipelagoMW/Z3Client/releases) or the SNIClient included with -[Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - - If installing Archipelago, make sure to check the box for SNIClient -> A Link to the Past Patch Setup during install, or SNI will not be included -- [SNI](https://github.com/alttpo/sni/releases) (Included in both Z3Client and SNIClient) +- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases) included with the main Archipelago install +or [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/releases) + - If installing Archipelago, make sure to check the box for `SNI Client - A Link to the Past Patch Setup` +- [SNI](https://github.com/alttpo/sni/releases) (Included in both clients from the first step) - Hardware or software capable of loading and playing SNES ROM files - An emulator capable of connecting to SNI ([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz), @@ -75,8 +75,9 @@ Firewall. 3. Click on **New Lua Script Window...** 4. In the new window, click **Browse...** 5. Select the connector lua file included with your client - - Z3Client users should download `sniConnector.lua` from the client download page - - SNIClient users should look in their Archipelago folder for `/sni/lua` + - SuperNintendoClient users should download `sniConnector.lua` from the client download page + - SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if +the emulator is 64-bit or 32-bit. ##### BizHawk 1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following @@ -87,9 +88,9 @@ Firewall. 3. Click on the Tools menu and click on **Lua Console** 4. Click Script -> Open Script... 5. Select the `Connector.lua` file you downloaded above - - Z3Client users should download `sniConnector.lua` from the client download page - - SNIClient users should look in their Archipelago folder for `/sni/lua` -6. Run the script by double-clicking it in the listing + - SuperNintendoClient users should download `sniConnector.lua` from the client download page + - SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if +the emulator is 64-bit or 32-bit. #### With hardware This guide assumes you have downloaded the correct firmware for your device. If you have not @@ -111,7 +112,8 @@ Status: Connected". ### Play the game When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations -on successfully joining a multiworld game! +on successfully joining a multiworld game! You can execute various commands in your client. For more information +regarding these commands you can use `/help` for local client commands and `!help` for server commands. ## Hosting a MultiWorld game The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple: From 3508cf21c7cabb9a75878c27569c41ed7b0b1303 Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Wed, 22 Dec 2021 20:15:56 -0500 Subject: [PATCH 09/36] WebHost: Add game listing for all players on room info page. --- WebHostLib/static/styles/hostRoom.css | 31 ++++++++++++++++ WebHostLib/templates/macros.html | 51 +++++++++++++++++++-------- WebHostLib/templates/userContent.html | 2 +- WebHostLib/upload.py | 9 ++++- 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/WebHostLib/static/styles/hostRoom.css b/WebHostLib/static/styles/hostRoom.css index bef8d147..cd1cf35b 100644 --- a/WebHostLib/static/styles/hostRoom.css +++ b/WebHostLib/static/styles/hostRoom.css @@ -18,3 +18,34 @@ border-radius: 3px; width: 500px; } + +#host-room table { + border-spacing: 0px; +} + +#host-room table tbody{ + background-color: #dce2bd; +} + +#host-room table tbody tr:hover{ + background-color: #e2eabb; +} + +#host-room table tbody td{ + padding: 4px 6px; + color: black; +} + +#host-room table tbody a{ + color: #234ae4; +} + +#host-room table thead td{ + background-color: #b0a77d; + color: black; + top: 0; +} + +#host-room table tbody td{ + border: 1px solid #bba967; +} diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 37ca89ee..548d281c 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -8,22 +8,43 @@ {%- endmacro %} {% macro list_patches_room(room) %} {% if room.seed.slots %} -
    + + + + + + + + + + + {% for patch in room.seed.slots|list|sort(attribute="player_id") %} - {% if patch.game == "Minecraft" %} -
  • - APMC for player {{ patch.player_id }} - {{ patch.player_name }}
  • - {% elif patch.game == "Factorio" %} -
  • - Mod for player {{ patch.player_id }} - {{ patch.player_name }}
  • - {% elif patch.game == "Ocarina of Time" %} -
  • - APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}
  • - {% else %} -
  • - Patch for player {{ patch.player_id }} - {{ patch.player_name }}
  • - {% endif %} + + + + + + + {% endfor %} - + +
    IdNameGameDownload LinkTracker Page
    {{ patch.player_id }}{{ patch.player_name }}{{ patch.game }} + {% if patch.game == "Minecraft" %} + + Download APMC File... + {% elif patch.game == "Factorio" %} + + Download Factorio Mod... + {% elif patch.game == "Ocarina of Time" %} + + Download APZ5 File... + {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid"] %} + + Download Patch File... + {% else %} + No file to download for this game. + {% endif %} + Tracker
    {% endif %} {%- endmacro -%} diff --git a/WebHostLib/templates/userContent.html b/WebHostLib/templates/userContent.html index ba7be082..6d32f100 100644 --- a/WebHostLib/templates/userContent.html +++ b/WebHostLib/templates/userContent.html @@ -2,7 +2,7 @@ {% block head %} {{ super() }} - Generate Game + User Content {% endblock %} diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index d17db9aa..47a52e6d 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -62,12 +62,19 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s elif file.filename.endswith(".archipelago"): try: multidata = zfile.open(file).read() - MultiServer.Context._decompress(multidata) except: flash("Could not load multidata. File may be corrupted or incompatible.") multidata = None if multidata: + decompressed_multidata = MultiServer.Context._decompress(multidata) + leftover_names = [(name, decompressed_multidata["names"][0].index(name) + 1) for name in + decompressed_multidata["names"][0] if name not in [slot.player_name for slot in slots]] + newslots = [(Slot(data=None, player_name=name, player_id=slot, game=decompressed_multidata["games"][slot])) + for name, slot in leftover_names] + for slot in newslots: + slots.add(slot) + flush() # commit slots seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), id=sid if sid else uuid.uuid4()) From ec570be17821b4390b6d211af4ce5cb6a2899bbd Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Mon, 27 Dec 2021 16:20:37 -0500 Subject: [PATCH 10/36] WebHost: Improve performance in player slot tracking during upload. --- WebHostLib/upload.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 47a52e6d..acf1c68f 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -68,10 +68,11 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if multidata: decompressed_multidata = MultiServer.Context._decompress(multidata) - leftover_names = [(name, decompressed_multidata["names"][0].index(name) + 1) for name in - decompressed_multidata["names"][0] if name not in [slot.player_name for slot in slots]] + player_names = {slot.player_name for slot in slots} + leftover_names = [(name, index+1) for index, name in + enumerate([name for name in decompressed_multidata["names"][0]])] newslots = [(Slot(data=None, player_name=name, player_id=slot, game=decompressed_multidata["games"][slot])) - for name, slot in leftover_names] + for name, slot in leftover_names if name not in player_names] for slot in newslots: slots.add(slot) From 844ff402cd38064250c27f24c02bbcb75f698bf0 Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Mon, 27 Dec 2021 16:40:54 -0500 Subject: [PATCH 11/36] WebHost: Improve player enumeration performance in upload.py --- WebHostLib/upload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index acf1c68f..7095d7d0 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -69,8 +69,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s if multidata: decompressed_multidata = MultiServer.Context._decompress(multidata) player_names = {slot.player_name for slot in slots} - leftover_names = [(name, index+1) for index, name in - enumerate([name for name in decompressed_multidata["names"][0]])] + leftover_names = [(name, index) for index, name in + enumerate((name for name in decompressed_multidata["names"][0]), start=1)] newslots = [(Slot(data=None, player_name=name, player_id=slot, game=decompressed_multidata["games"][slot])) for name, slot in leftover_names if name not in player_names] for slot in newslots: From 2e56c226db5058ac8cd458340aeff9a6ce13a857 Mon Sep 17 00:00:00 2001 From: Hussein Farran Date: Mon, 27 Dec 2021 16:41:21 -0500 Subject: [PATCH 12/36] WebHost: Patch downloads now prompt you with a dialog box/file save dialog. --- WebHostLib/templates/macros.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index 548d281c..549d3ace 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -26,16 +26,16 @@ {{ patch.game }} {% if patch.game == "Minecraft" %} - + Download APMC File... {% elif patch.game == "Factorio" %} - + Download Factorio Mod... {% elif patch.game == "Ocarina of Time" %} - + Download APZ5 File... {% elif patch.game in ["A Link to the Past", "Secret of Evermore", "Super Metroid"] %} - + Download Patch File... {% else %} No file to download for this game. From 6e4b255be52d280fb635226c94f537072e23d4f9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 28 Dec 2021 18:43:52 +0100 Subject: [PATCH 13/36] Options: make common options overridable in a game section WebHost: add prog balancing and accessibility to settings page --- BaseClasses.py | 2 -- Generate.py | 2 +- Options.py | 4 +++- WebHostLib/options.py | 9 +++++---- WebHostLib/templates/options.yaml | 7 ------- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 0c90f37c..bbf110d2 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1210,8 +1210,6 @@ class Spoiler(): if self.world.players > 1: outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player))) outfile.write('Game: %s\n' % self.world.game[player]) - for f_option, option in Options.common_options.items(): - write_option(f_option, option) for f_option, option in Options.per_game_common_options.items(): write_option(f_option, option) options = self.world.worlds[player].options diff --git a/Generate.py b/Generate.py index a8c88125..fc82ad43 100644 --- a/Generate.py +++ b/Generate.py @@ -469,7 +469,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b ret = argparse.Namespace() for option_key in Options.per_game_common_options: - if option_key in weights: + if option_key in weights and option_key not in Options.common_options: raise Exception(f"Option {option_key} has to be in a game's section, not on its own.") ret.game = get_choice("game", weights) diff --git a/Options.py b/Options.py index cd4d9148..ef786e47 100644 --- a/Options.py +++ b/Options.py @@ -334,7 +334,7 @@ class Accessibility(Choice): Locations: ensure everything can be reached and acquired. Items: ensure all logically relevant items can be acquired. Minimal: ensure what is needed to reach your goal can be acquired.""" - + displayname = "Accessibility" option_locations = 0 option_items = 1 option_minimal = 2 @@ -344,6 +344,7 @@ class Accessibility(Choice): class ProgressionBalancing(DefaultOnToggle): """A system that moves progression earlier, to try and prevent the player from getting stuck and bored early.""" + displayname = "Progression Balancing" common_options = { @@ -395,6 +396,7 @@ class DeathLink(Toggle): per_game_common_options = { + **common_options, # can be overwritten per-game "local_items": LocalItems, "non_local_items": NonLocalItems, "start_inventory": StartInventory, diff --git a/WebHostLib/options.py b/WebHostLib/options.py index c8a7b9a6..73bcff46 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -11,6 +11,8 @@ target_folder = os.path.join("WebHostLib", "static", "generated") def create(): + os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True) + def dictify_range(option): data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0, option.default: 50} @@ -26,14 +28,13 @@ def create(): return default_value for game_name, world in AutoWorldRegister.world_types.items(): + all_options = {**world.options, **Options.per_game_common_options} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( - options={**world.options, **Options.per_game_common_options}, + options=all_options, __version__=__version__, game=game_name, yaml_dump=yaml.dump, dictify_range=dictify_range, default_converter=default_converter, ) - os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True) - with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f: f.write(res) @@ -47,7 +48,7 @@ def create(): } game_options = {} - for option_name, option in world.options.items(): + for option_name, option in all_options.items(): if option.options: game_options[option_name] = this_option = { "type": "select", diff --git a/WebHostLib/templates/options.yaml b/WebHostLib/templates/options.yaml index 91da3a83..95fab2ae 100644 --- a/WebHostLib/templates/options.yaml +++ b/WebHostLib/templates/options.yaml @@ -29,13 +29,6 @@ game: requires: version: {{ __version__ }} # Version of Archipelago required for this yaml to work as expected. # Shared Options supported by all games: -accessibility: - items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations - locations: 50 # Guarantees you will be able to access all locations, and therefore all items - none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items -progression_balancing: - on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do - off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items. {%- macro range_option(option) %} # you can add additional values between minimum and maximum From 4ed45248eb0f78432bf11feaad48d06416bf5ad9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 29 Dec 2021 11:08:23 +0100 Subject: [PATCH 14/36] LttP: Rename "Dark World Shop" overworld door to Village of Outcasts Shop. Note: Now the overworld door, Region, Shop and inside door are named the same. --- worlds/alttp/EntranceShuffle.py | 14 +++++++------- worlds/alttp/InvertedRegions.py | 2 +- worlds/alttp/Regions.py | 2 +- worlds/alttp/Rom.py | 2 +- worlds/alttp/Rules.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index fb903f88..8b9e8bfa 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -2549,7 +2549,7 @@ DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', 'Big Bomb Shop', 'Dark Death Mountain Fairy', 'Dark Lake Hylia Shop', - 'Dark World Shop', + 'Village of Outcasts Shop', 'Red Shield Shop', 'Mire Shed', 'East Dark World Hint', @@ -2626,7 +2626,7 @@ Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', - 'Dark World Shop', + 'Village of Outcasts Shop', 'Dark World Lumberjack Shop', 'Dark World Potion Shop', 'Archery Game', @@ -2837,7 +2837,7 @@ Inverted_DW_Single_Cave_Doors = ['Bonk Fairy (Dark)', 'C-Shaped House', 'Bumper Cave (Top)', 'Dark Lake Hylia Shop', - 'Dark World Shop', + 'Village of Outcasts Shop', 'Red Shield Shop', 'Mire Shed', 'East Dark World Hint', @@ -2883,7 +2883,7 @@ Inverted_Bomb_Shop_Single_Cave_Doors = ['Waterfall of Wishing', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', - 'Dark World Shop', + 'Village of Outcasts Shop', 'Dark World Lumberjack Shop', 'Dark World Potion Shop', 'Archery Game', @@ -3543,7 +3543,7 @@ default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing'), ('Red Shield Shop', 'Red Shield Shop'), ('Dark Sanctuary Hint', 'Dark Sanctuary Hint'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), - ('Dark World Shop', 'Village of Outcasts Shop'), + ('Village of Outcasts Shop', 'Village of Outcasts Shop'), ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), ('Dark World Potion Shop', 'Dark World Potion Shop'), ('Archery Game', 'Archery Game'), @@ -3679,7 +3679,7 @@ inverted_default_connections = [('Waterfall of Wishing', 'Waterfall of Wishing' ('Dark World Hammer Peg Cave', 'Dark World Hammer Peg Cave'), ('Red Shield Shop', 'Red Shield Shop'), ('Fortune Teller (Dark)', 'Fortune Teller (Dark)'), - ('Dark World Shop', 'Village of Outcasts Shop'), + ('Village of Outcasts Shop', 'Village of Outcasts Shop'), ('Dark World Lumberjack Shop', 'Dark World Lumberjack Shop'), ('Dark World Potion Shop', 'Dark World Potion Shop'), ('Archery Game', 'Archery Game'), @@ -3981,7 +3981,7 @@ door_addresses = {'Links House': (0x00, (0x0104, 0x2c, 0x0506, 0x0a9a, 0x0832, 0 'Dark Sanctuary Hint': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)), 'Inverted Dark Sanctuary': (0x59, (0x0112, 0x53, 0x001e, 0x0400, 0x06e2, 0x0446, 0x0758, 0x046d, 0x075f, 0x00, 0x00, 0x0000, 0x0000)), 'Fortune Teller (Dark)': (0x65, (0x0122, 0x51, 0x0610, 0x04b4, 0x027e, 0x0507, 0x02f8, 0x0523, 0x0303, 0x0a, 0xf6, 0x091E, 0x0000)), - 'Dark World Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000)), + 'Village of Outcasts Shop': (0x5F, (0x010f, 0x58, 0x1058, 0x0814, 0x02be, 0x0868, 0x0338, 0x0883, 0x0343, 0x0a, 0xf6, 0x0000, 0x0000)), 'Dark World Lumberjack Shop': (0x56, (0x010f, 0x42, 0x041c, 0x0074, 0x04e2, 0x00c7, 0x0558, 0x00e3, 0x055f, 0x0a, 0xf6, 0x0000, 0x0000)), 'Dark World Potion Shop': (0x6E, (0x010f, 0x56, 0x080e, 0x04f4, 0x0c66, 0x0548, 0x0cd8, 0x0563, 0x0ce3, 0x0a, 0xf6, 0x0000, 0x0000)), 'Archery Game': (0x58, (0x0111, 0x69, 0x069e, 0x0ac4, 0x02ea, 0x0b18, 0x0368, 0x0b33, 0x036f, 0x0a, 0xf6, 0x09AC, 0x0000)), diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 4edcdf61..3a811612 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -184,7 +184,7 @@ def create_inverted_regions(world, player): create_dw_region(player, 'West Dark World', ['Frog', 'Flute Activation Spot'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Bumper Cave Entrance Rock', 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Inverted Dark Sanctuary', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop', 'West Dark World Teleporter', 'WDW Flute']), - create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop', 'Dark Grassy Lawn Flute']), + create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop', 'Dark Grassy Lawn Flute']), create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Dark World Hammer Peg Cave', 'Peg Area Rocks', 'Hammer Peg Area Flute']), create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Drop']), create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'), diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index a3f92bd2..a23c725a 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -176,7 +176,7 @@ def create_regions(world, player): 'Hype Cave - Bottom', 'Hype Cave - Generous Guy']), create_dw_region(player, 'West Dark World', ['Frog'], ['Village of Outcasts Drop', 'East Dark World River Pier', 'Brewery', 'C-Shaped House', 'Chest Game', 'Thieves Town', 'Graveyard Ledge Mirror Spot', 'Kings Grave Mirror Spot', 'Bumper Cave Entrance Rock', 'Skull Woods Forest', 'Village of Outcasts Pegs', 'Village of Outcasts Eastern Rocks', 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', 'Dark World Lumberjack Shop']), - create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Dark World Shop']), + create_dw_region(player, 'Dark Grassy Lawn', None, ['Grassy Lawn Pegs', 'Village of Outcasts Shop']), create_dw_region(player, 'Hammer Peg Area', ['Dark Blacksmith Ruins'], ['Bat Cave Drop Ledge Mirror Spot', 'Dark World Hammer Peg Cave', 'Peg Area Rocks']), create_dw_region(player, 'Bumper Cave Entrance', None, ['Bumper Cave (Bottom)', 'Bumper Cave Entrance Mirror Spot', 'Bumper Cave Entrance Drop']), create_cave_region(player, 'Fortune Teller (Dark)', 'a fortune teller'), diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 1ac4d99c..e72e22cd 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -2800,7 +2800,7 @@ OtherEntrances = {'Blinds Hideout': 'Blind\'s old house', 'C-Shaped House': 'The NE house in Village of Outcasts', 'Dark Death Mountain Fairy': 'The SW cave on dark DM', 'Dark Lake Hylia Shop': 'The building NW dark Lake Hylia', - 'Dark World Shop': 'The hammer sealed building', + 'Village of Outcasts Shop': 'The hammer sealed building', 'Red Shield Shop': 'The fenced in building', 'Mire Shed': 'The western hut in the mire', 'East Dark World Hint': 'The dark cave near the eastmost portal', diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index 29e8e081..589766d4 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -1007,7 +1007,7 @@ def set_big_bomb_rules(world, player): 'Red Shield Shop', 'Dark Sanctuary Hint', 'Fortune Teller (Dark)', - 'Dark World Shop', + 'Village of Outcasts Shop', 'Dark World Lumberjack Shop', 'Thieves Town', 'Skull Woods First Section Door', @@ -1331,7 +1331,7 @@ def set_inverted_big_bomb_rules(world, player): elif bombshop_entrance.name in LW_bush_entrances: # These entrances are behind bushes in LW so you need either Pearl or the tools to solve NDW bomb shop locations. add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Magic Mirror', player) and (state.has('Activated Flute', player) or state.has('Moon Pearl', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)))) - elif bombshop_entrance.name == 'Dark World Shop': + elif bombshop_entrance.name == 'Village of Outcasts Shop': # This is mostly the same as NDW but the Mirror path requires the Pearl, or using the Hammer add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.has('Activated Flute', player) or (state.can_lift_heavy_rocks(player) and state.has('Hammer', player)) or (state.has('Magic Mirror', player) and state.can_reach('Light World', 'Region', player) and (state.has('Moon Pearl', player) or state.has('Hammer', player)))) elif bombshop_entrance.name == 'Bumper Cave (Bottom)': From a10d7ae5b961297d470ed773c22361b5036e4986 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Thu, 30 Dec 2021 13:10:08 +0100 Subject: [PATCH 15/36] [Timespinner] Fixed some placement logics regarding gyre archives & military fortress Renamed 'Transition chest #' to 'Gyre chest #' --- worlds/timespinner/Locations.py | 30 +++++++++++++++--------------- worlds/timespinner/Regions.py | 14 +++++++++++++- worlds/timespinner/__init__.py | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index b1fc164e..74cf4c42 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -76,12 +76,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData('Sealed Caves (Sirens)', 'Upper sealed cave after sirens chest 1', 1337057), LocationData('Military Fortress', 'Military bomber chest', 1337058, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)), LocationData('Military Fortress', 'Close combat room', 1337059), - LocationData('Military Fortress', 'Military soldiers bridge', 1337060), - LocationData('Military Fortress', 'Military giantess room', 1337061), - LocationData('Military Fortress', 'Military giantess bridge', 1337062), - LocationData('Military Fortress', 'Military B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), - LocationData('Military Fortress', 'Military B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), - LocationData('Military Fortress', 'Military pedestal', 1337065, lambda state: state._timespinner_has_doublejump(world, player) and (state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player))), + LocationData('Military Fortress (hangar)', 'Military soldiers bridge', 1337060), + LocationData('Military Fortress (hangar)', 'Military giantess room', 1337061), + LocationData('Military Fortress (hangar)', 'Military giantess bridge', 1337062), + LocationData('Military Fortress (hangar)', 'Military B door chest 2', 1337063, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), + LocationData('Military Fortress (hangar)', 'Military B door chest 1', 1337064, lambda state: state._timespinner_has_doublejump(world, player) and state._timespinner_has_keycard_B(world, player)), + LocationData('Military Fortress (hangar)', 'Military pedestal', 1337065, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player)), LocationData('The lab', 'Coffee break', 1337066), LocationData('The lab', 'Lower trash right', 1337067, lambda state: state._timespinner_has_doublejump(world, player)), LocationData('The lab', 'Lower trash left', 1337068, lambda state: state._timespinner_has_upwarddash(world, player)), @@ -222,15 +222,15 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L # 1337237 - 1337245 GyreArchives if not world or is_option_enabled(world, player, "GyreArchives"): location_table += ( - LocationData('The lab (upper)', 'Ravenlord post fight (pedestal)', 1337237, lambda state: state.has('Merchant Crow', player)), - LocationData('Library top', 'Ifrit post fight (pedestal)', 1337238, lambda state: state.has('Kobo', player)), - LocationData('Ancient Pyramid (right)', 'Transition chest 1', 1337239), - LocationData('Ancient Pyramid (right)', 'Transition chest 2', 1337240), - LocationData('Ancient Pyramid (right)', 'Transition chest 3', 1337241), - LocationData('The lab (upper)', 'Ravenlord pre fight', 1337242, lambda state: state.has('Merchant Crow', player)), - LocationData('The lab (upper)', 'Ravenlord post fight (chest)', 1337243, lambda state: state.has('Merchant Crow', player)), - LocationData('Library top', 'Ifrit pre fight', 1337244, lambda state: state.has('Kobo', player)), - LocationData('Library top', 'Ifrit post fight (chest)', 1337245, lambda state: state.has('Kobo', player)), + LocationData('Ravenlord\'s Lair', 'Ravenlord post fight (pedestal)', 1337237), + LocationData('Ifrit\'s Lair', 'Ifrit post fight (pedestal)', 1337238), + LocationData('Temporal Gyre', 'Gyre chest 1', 1337239), + LocationData('Temporal Gyre', 'Gyre chest 2', 1337240), + LocationData('Temporal Gyre', 'Gyre chest 3', 1337241), + LocationData('Ravenlord\'s Lair', 'Ravenlord pre fight', 1337242), + LocationData('Ravenlord\'s Lair', 'Ravenlord post fight (chest)', 1337243), + LocationData('Ifrit\'s Lair', 'Ifrit pre fight', 1337244), + LocationData('Ifrit\'s Lair', 'Ifrit post fight (chest)', 1337245), ) return tuple(location_table) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 9db742eb..95cc52b8 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -14,15 +14,18 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Lower lake desolation'), create_region(world, player, locations_per_region, location_cache, 'Library'), create_region(world, player, locations_per_region, location_cache, 'Library top'), + create_region(world, player, locations_per_region, location_cache, 'Ifrit\'s Lair'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower left'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (upper)'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (lower)'), create_region(world, player, locations_per_region, location_cache, 'Varndagroth tower right (elevator)'), create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (Sirens)'), create_region(world, player, locations_per_region, location_cache, 'Military Fortress'), + create_region(world, player, locations_per_region, location_cache, 'Military Fortress (hangar)'), create_region(world, player, locations_per_region, location_cache, 'The lab'), create_region(world, player, locations_per_region, location_cache, 'The lab (power off)'), create_region(world, player, locations_per_region, location_cache, 'The lab (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Ravenlord\'s Lair'), create_region(world, player, locations_per_region, location_cache, 'Emperors tower'), create_region(world, player, locations_per_region, location_cache, 'Skeleton Shaft'), create_region(world, player, locations_per_region, location_cache, 'Sealed Caves (upper)'), @@ -40,6 +43,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData create_region(world, player, locations_per_region, location_cache, 'Royal towers (lower)'), create_region(world, player, locations_per_region, location_cache, 'Royal towers'), create_region(world, player, locations_per_region, location_cache, 'Royal towers (upper)'), + create_region(world, player, locations_per_region, location_cache, 'Temporal Gyre'), create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (left)'), create_region(world, player, locations_per_region, location_cache, 'Ancient Pyramid (right)'), create_region(world, player, locations_per_region, location_cache, 'Space time continuum') @@ -68,6 +72,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Library', 'Varndagroth tower left', lambda state: state._timespinner_has_keycard_D(world, player)) connect(world, player, names, 'Library', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) connect(world, player, names, 'Library top', 'Library') + connect(world, player, names, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) + connect(world, player, names, 'Ifrit\'s Lair', 'Library top') connect(world, player, names, 'Varndagroth tower left', 'Library') connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (upper)', lambda state: state._timespinner_has_keycard_C(world, player)) connect(world, player, names, 'Varndagroth tower left', 'Varndagroth tower right (lower)', lambda state: state._timespinner_has_keycard_B(world, player)) @@ -86,14 +92,20 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, 'Sealed Caves (Sirens)', 'Varndagroth tower right (lower)', lambda state: state.has('Elevator Keycard', player)) connect(world, player, names, 'Sealed Caves (Sirens)', 'Space time continuum', lambda state: state.has('Twin Pyramid Key', player)) connect(world, player, names, 'Military Fortress', 'Varndagroth tower right (lower)', lambda state: state._timespinner_can_kill_all_3_bosses(world, player)) - connect(world, player, names, 'Military Fortress', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Military Fortress', 'Temporal Gyre', lambda state: state.has('Timespinner Wheel', player)) + connect(world, player, names, 'Military Fortress', 'Military Fortress (hangar)', lambda state: state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Military Fortress (hangar)', 'Military Fortress') + connect(world, player, names, 'Military Fortress (hangar)', 'The lab', lambda state: state._timespinner_has_keycard_B(world, player) and state._timespinner_has_doublejump(world, player)) + connect(world, player, names, 'Temporal Gyre', 'Military Fortress') connect(world, player, names, 'The lab', 'Military Fortress') connect(world, player, names, 'The lab', 'The lab (power off)', lambda state: state._timespinner_has_doublejump_of_npc(world, player)) connect(world, player, names, 'The lab (power off)', 'The lab') connect(world, player, names, 'The lab (power off)', 'The lab (upper)', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) connect(world, player, names, 'The lab (upper)', 'The lab (power off)') + connect(world, player, names, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, names, 'The lab (upper)', 'Emperors tower', lambda state: state._timespinner_has_forwarddash_doublejump(world, player)) connect(world, player, names, 'The lab (upper)', 'Ancient Pyramid (left)', lambda state: state.has_all({'Timespinner Wheel', 'Timespinner Spindle', 'Timespinner Gear 1', 'Timespinner Gear 2', 'Timespinner Gear 3'}, player)) + connect(world, player, names, 'Ravenlord\'s Lair', 'The lab (upper)') connect(world, player, names, 'Emperors tower', 'The lab (upper)') connect(world, player, names, 'Skeleton Shaft', 'Lake desolation') connect(world, player, names, 'Skeleton Shaft', 'Sealed Caves (upper)', lambda state: state._timespinner_has_keycard_A(world, player)) diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index 623de7c6..cba17c95 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -18,7 +18,7 @@ class TimespinnerWorld(World): game = "Timespinner" topology_present = True remote_items = False - data_version = 4 + data_version = 5 item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} From 39869bcdc5b9c6a5989c7e2e7499162f1d482113 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Sat, 18 Dec 2021 17:54:19 -0700 Subject: [PATCH 16/36] Add basic fill test cases --- test/base/TestFill.py | 106 ++++++++++++++++++++++++++++++++++++++++++ test/base/__init__.py | 0 2 files changed, 106 insertions(+) create mode 100644 test/base/TestFill.py create mode 100644 test/base/__init__.py diff --git a/test/base/TestFill.py b/test/base/TestFill.py new file mode 100644 index 00000000..152bd8c3 --- /dev/null +++ b/test/base/TestFill.py @@ -0,0 +1,106 @@ +import unittest +from worlds.AutoWorld import World +from Fill import fill_restrictive +from BaseClasses import MultiWorld, Region, RegionType, Item, Location +from worlds.generic.Rules import set_rule + + +def generate_multi_world() -> MultiWorld: + multi_world = MultiWorld(1) + player1_id = 1 + world = World(multi_world, player1_id) + multi_world.game[player1_id] = world + multi_world.worlds[player1_id] = world + multi_world.player_name = {player1_id: "Test Player 1"} + multi_world.set_seed() + # args = Namespace() + # for name, option in world_type.options.items(): + # setattr(args, name, {1: option.from_any(option.default)}) + # multi_world.set_options(args) + multi_world.set_default_common_options() + + region = Region("Menu", RegionType.Generic, + "Menu Region Hint", player1_id, multi_world) + multi_world.regions.append(region) + + return multi_world + + +def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> list[Location]: + locations = [] + for i in range(count): + name = "player" + str(player_id) + "_location" + str(i) + locations.append(Location(player_id, name, address, region)) + return locations + + +def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> list[Location]: + items = [] + for i in range(count): + name = "player" + str(player_id) + "_item" + str(i) + items.append(Item(name, advancement, code, player_id)) + return items + + +class TestBase(unittest.TestCase): + def test_basic_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(2, player1_id, None, player1_menu) + items = generate_items(2, player1_id, True) + + item0 = items[0] + item1 = items[1] + loc0 = locations[0] + loc1 = locations[1] + + fill_restrictive(multi_world, multi_world.state, locations, items) + + self.assertEqual(loc0.item, item1) + self.assertEqual(loc1.item, item0) + self.assertEqual([], locations) + self.assertEqual([], items) + + def test_ordered_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(2, player1_id, None, player1_menu) + items = generate_items(2, player1_id, True) + + item0 = items[0] + item1 = items[1] + loc0 = locations[0] + loc1 = locations[1] + + multi_world.completion_condition[player1_id] = lambda state: state.has( + item0.name, player1_id) and state.has(item1.name, player1_id) + set_rule(loc1, lambda state: state.has(item0.name, player1_id)) + fill_restrictive(multi_world, multi_world.state, locations, items) + + self.assertEqual(loc0.item, item0) + self.assertEqual(loc1.item, item1) + + def test_reversed_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(2, player1_id, None, player1_menu) + items = generate_items(2, player1_id, True) + + item0 = items[0] + item1 = items[1] + loc0 = locations[0] + loc1 = locations[1] + + multi_world.completion_condition[player1_id] = lambda state: state.has( + item0.name, player1_id) and state.has(item1.name, player1_id) + set_rule(loc1, lambda state: state.has(item1.name, player1_id)) + fill_restrictive(multi_world, multi_world.state, locations, items) + + self.assertEqual(loc0.item, item1) + self.assertEqual(loc1.item, item0) diff --git a/test/base/__init__.py b/test/base/__init__.py new file mode 100644 index 00000000..e69de29b From 461961c3bee45a6f2a48e54773a46acb073200f4 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Mon, 20 Dec 2021 07:59:36 -0700 Subject: [PATCH 17/36] Add test locations to region --- test/base/TestFill.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/base/TestFill.py b/test/base/TestFill.py index 152bd8c3..d231fa15 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -30,7 +30,9 @@ def generate_locations(count: int, player_id: int, address: int = None, region: locations = [] for i in range(count): name = "player" + str(player_id) + "_location" + str(i) - locations.append(Location(player_id, name, address, region)) + location = Location(player_id, name, address, region) + locations.append(location) + region.locations.append(location) return locations @@ -103,4 +105,4 @@ class TestBase(unittest.TestCase): fill_restrictive(multi_world, multi_world.state, locations, items) self.assertEqual(loc0.item, item1) - self.assertEqual(loc1.item, item0) + self.assertEqual(loc1.item, item0) \ No newline at end of file From 6a34fe5184997b4da634bda2cea9f16ba4ce54b8 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Mon, 20 Dec 2021 17:47:04 -0700 Subject: [PATCH 18/36] Add fallback item swap for unreachable items --- Fill.py | 66 ++++++++++++++++++++++++++++++------------- test/base/TestFill.py | 48 +++++++++++++++++++++++++++++-- 2 files changed, 92 insertions(+), 22 deletions(-) diff --git a/Fill.py b/Fill.py index f434010b..9660e1af 100644 --- a/Fill.py +++ b/Fill.py @@ -3,7 +3,7 @@ import typing import collections import itertools -from BaseClasses import CollectionState, Location, MultiWorld +from BaseClasses import CollectionState, Location, MultiWorld, Item from worlds.generic import PlandoItem from worlds.AutoWorld import call_all @@ -12,15 +12,16 @@ class FillError(RuntimeError): pass -def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool, single_player_placement=False, - lock=False): - def sweep_from_pool(): - new_state = base_state.copy() - for item in itempool: - new_state.collect(item, True) - new_state.sweep_for_events() - return new_state +def sweep_from_pool(base_state: CollectionState, itempool: list[Item]): + new_state = base_state.copy() + for item in itempool: + new_state.collect(item, True) + new_state.sweep_for_events() + return new_state + +def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: list[Item], single_player_placement=False, + lock=False): unplaced_items = [] placements = [] @@ -29,13 +30,16 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, reachable_items.setdefault(item.player, []).append(item) while any(reachable_items.values()) and locations: - items_to_place = [items.pop() for items in reachable_items.values() if items] # grab one item per player + # grab one item per player + items_to_place = [items.pop() + for items in reachable_items.values() if items] for item in items_to_place: itempool.remove(item) - maximum_exploration_state = sweep_from_pool() + maximum_exploration_state = sweep_from_pool(base_state, itempool) has_beaten_game = world.has_beaten_game(maximum_exploration_state) for item_to_place in items_to_place: + spot_to_fill: Location = None if world.accessibility[item_to_place.player] == 'minimal': perform_access_check = not world.has_beaten_game(maximum_exploration_state, item_to_place.player) if single_player_placement else not has_beaten_game @@ -45,19 +49,41 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, for i, location in enumerate(locations): if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check): - spot_to_fill = locations.pop(i) # poping by index is faster than removing by content, + # poping by index is faster than removing by content, + spot_to_fill = locations.pop(i) # skipping a scan for the element break else: - # we filled all reachable spots. Maybe the game can be beaten anyway? - unplaced_items.append(item_to_place) - if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game(): - logging.warning( - f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})') - continue - raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. ' - f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') + # we filled all reachable spots. + # try swaping this item with previously placed items + for(i, location) in enumerate(placements): + placed_item = location.item + location.item = None + placed_item.location = None + swap_state = sweep_from_pool(base_state, itempool) + if (not single_player_placement or location.player == item_to_place.player) \ + and location.can_fill(swap_state, item_to_place, perform_access_check): + # Add this item to the exisiting placement, and + # add the old item to the back of the queue + spot_to_fill = placements.pop(i) + reachable_items.setdefault(placed_item.player, []).append(placed_item) + itempool.append(placed_item) + break + else: + # Item can't be placed here, restore original item + location.item = placed_item + placed_item.location = location + + if spot_to_fill == None: + # Maybe the game can be beaten anyway? + unplaced_items.append(item_to_place) + if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game(): + logging.warning( + f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})') + continue + raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. ' + f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') world.push_item(spot_to_fill, item_to_place, False) spot_to_fill.locked = lock diff --git a/test/base/TestFill.py b/test/base/TestFill.py index d231fa15..9b132b65 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -1,6 +1,7 @@ import unittest +import pytest from worlds.AutoWorld import World -from Fill import fill_restrictive +from Fill import FillError, fill_restrictive from BaseClasses import MultiWorld, Region, RegionType, Item, Location from worlds.generic.Rules import set_rule @@ -105,4 +106,47 @@ class TestBase(unittest.TestCase): fill_restrictive(multi_world, multi_world.state, locations, items) self.assertEqual(loc0.item, item1) - self.assertEqual(loc1.item, item0) \ No newline at end of file + self.assertEqual(loc1.item, item0) + + def test_impossible_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(2, player1_id, None, player1_menu) + items = generate_items(2, player1_id, True) + + item0 = items[0] + item1 = items[1] + loc0 = locations[0] + loc1 = locations[1] + + multi_world.completion_condition[player1_id] = lambda state: state.has( + item0.name, player1_id) and state.has(item1.name, player1_id) + set_rule(loc1, lambda state: state.has(item1.name, player1_id)) + set_rule(loc0, lambda state: state.has(item0.name, player1_id)) + with pytest.raises(FillError): + fill_restrictive(multi_world, multi_world.state, locations, items) + + def test_circular_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(3, player1_id, None, player1_menu) + items = generate_items(3, player1_id, True) + + item0 = items[0] + item1 = items[1] + item2 = items[2] + loc0 = locations[0] + loc1 = locations[1] + loc2 = locations[2] + + multi_world.completion_condition[player1_id] = lambda state: state.has( + item0.name, player1_id) and state.has(item1.name, player1_id) and state.has(item2.name, player1_id) + set_rule(loc1, lambda state: state.has(item0.name, player1_id)) + set_rule(loc2, lambda state: state.has(item1.name, player1_id)) + set_rule(loc0, lambda state: state.has(item2.name, player1_id)) + with pytest.raises(FillError): + fill_restrictive(multi_world, multi_world.state, locations, items) \ No newline at end of file From d719eb356f1c11d26223a47229302f9139b4d699 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Mon, 20 Dec 2021 18:14:50 -0700 Subject: [PATCH 19/36] Don't allow items to swap infinitly --- Fill.py | 11 +++++++++-- test/base/TestFill.py | 21 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index 9660e1af..a6864d87 100644 --- a/Fill.py +++ b/Fill.py @@ -2,6 +2,8 @@ import logging import typing import collections import itertools +from collections import Counter + from BaseClasses import CollectionState, Location, MultiWorld, Item from worlds.generic import PlandoItem @@ -25,6 +27,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, unplaced_items = [] placements = [] + swapped_items: Counter[Item] = Counter() reachable_items = {} for item in itempool: reachable_items.setdefault(item.player, []).append(item) @@ -59,15 +62,19 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # try swaping this item with previously placed items for(i, location) in enumerate(placements): placed_item = location.item + if swapped_items[placed_item.name] > 0: + continue location.item = None placed_item.location = None swap_state = sweep_from_pool(base_state, itempool) if (not single_player_placement or location.player == item_to_place.player) \ and location.can_fill(swap_state, item_to_place, perform_access_check): - # Add this item to the exisiting placement, and + # Add this item to the exisiting placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) - reachable_items.setdefault(placed_item.player, []).append(placed_item) + swapped_items[placed_item.name] += 1 + reachable_items.setdefault( + placed_item.player, []).append(placed_item) itempool.append(placed_item) break else: diff --git a/test/base/TestFill.py b/test/base/TestFill.py index 9b132b65..8e032109 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -149,4 +149,23 @@ class TestBase(unittest.TestCase): set_rule(loc2, lambda state: state.has(item1.name, player1_id)) set_rule(loc0, lambda state: state.has(item2.name, player1_id)) with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, locations, items) \ No newline at end of file + fill_restrictive(multi_world, multi_world.state, locations, items) + + def test_competing_fill_restrictive(self): + multi_world = generate_multi_world() + player1_id = 1 + player1_menu = multi_world.get_region("Menu", player1_id) + + locations = generate_locations(2, player1_id, None, player1_menu) + items = generate_items(2, player1_id, True) + + item0 = items[0] + item1 = items[1] + loc1 = locations[1] + + multi_world.completion_condition[player1_id] = lambda state: state.has( + item0.name, player1_id) and state.has(item0.name, player1_id) and state.has(item1.name, player1_id) + set_rule(loc1, lambda state: state.has(item0.name, player1_id) + and state.has(item1.name, player1_id)) + with pytest.raises(FillError): + fill_restrictive(multi_world, multi_world.state, locations, items) From 2f56e40fb780e6e38a408d8ad99592794c7305ca Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Mon, 20 Dec 2021 18:20:01 -0700 Subject: [PATCH 20/36] Include player information in swapped item count --- Fill.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index a6864d87..067ac981 100644 --- a/Fill.py +++ b/Fill.py @@ -27,7 +27,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, unplaced_items = [] placements = [] - swapped_items: Counter[Item] = Counter() + swapped_items = Counter() reachable_items = {} for item in itempool: reachable_items.setdefault(item.player, []).append(item) @@ -62,7 +62,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # try swaping this item with previously placed items for(i, location) in enumerate(placements): placed_item = location.item - if swapped_items[placed_item.name] > 0: + if swapped_items[placed_item.player, placed_item.name] > 0: continue location.item = None placed_item.location = None @@ -72,7 +72,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # Add this item to the exisiting placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) - swapped_items[placed_item.name] += 1 + swapped_items[placed_item.player, placed_item.name] += 1 reachable_items.setdefault( placed_item.player, []).append(placed_item) itempool.append(placed_item) From dc82b384c5f04c731551c707c4ab3e9898842448 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Mon, 20 Dec 2021 18:23:19 -0700 Subject: [PATCH 21/36] Add comment about swap count --- Fill.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Fill.py b/Fill.py index 067ac981..686923e9 100644 --- a/Fill.py +++ b/Fill.py @@ -62,6 +62,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # try swaping this item with previously placed items for(i, location) in enumerate(placements): placed_item = location.item + # Unplaceable items can sometimes be swapped infinitely. Limit the + # number of times we will swap an individual item to prevent this if swapped_items[placed_item.player, placed_item.name] > 0: continue location.item = None From e5fedb90a627f8d0074eb1478576c6cab588fac1 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Tue, 21 Dec 2021 22:55:10 -0700 Subject: [PATCH 22/36] Process swaped items last --- Fill.py | 11 +- test/base/TestFill.py | 251 ++++++++++++++++++++++++++---------------- 2 files changed, 163 insertions(+), 99 deletions(-) diff --git a/Fill.py b/Fill.py index 686923e9..f19cf0fd 100644 --- a/Fill.py +++ b/Fill.py @@ -2,7 +2,7 @@ import logging import typing import collections import itertools -from collections import Counter +from collections import Counter, deque from BaseClasses import CollectionState, Location, MultiWorld, Item @@ -14,7 +14,7 @@ class FillError(RuntimeError): pass -def sweep_from_pool(base_state: CollectionState, itempool: list[Item]): +def sweep_from_pool(base_state: CollectionState, itempool): new_state = base_state.copy() for item in itempool: new_state.collect(item, True) @@ -28,9 +28,9 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, placements = [] swapped_items = Counter() - reachable_items = {} + reachable_items: dict[str, deque] = {} for item in itempool: - reachable_items.setdefault(item.player, []).append(item) + reachable_items.setdefault(item.player, deque()).append(item) while any(reachable_items.values()) and locations: # grab one item per player @@ -75,8 +75,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # add the old item to the back of the queue spot_to_fill = placements.pop(i) swapped_items[placed_item.player, placed_item.name] += 1 - reachable_items.setdefault( - placed_item.player, []).append(placed_item) + reachable_items[placed_item.player].appendleft(placed_item) itempool.append(placed_item) break else: diff --git a/test/base/TestFill.py b/test/base/TestFill.py index 8e032109..259cc8e4 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -1,3 +1,4 @@ +from typing import NamedTuple import unittest import pytest from worlds.AutoWorld import World @@ -6,13 +7,19 @@ from BaseClasses import MultiWorld, Region, RegionType, Item, Location from worlds.generic.Rules import set_rule -def generate_multi_world() -> MultiWorld: - multi_world = MultiWorld(1) - player1_id = 1 - world = World(multi_world, player1_id) - multi_world.game[player1_id] = world - multi_world.worlds[player1_id] = world - multi_world.player_name = {player1_id: "Test Player 1"} +def generate_multi_world(players: int = 1) -> MultiWorld: + multi_world = MultiWorld(players) + multi_world.player_name = {} + for i in range(players): + player_id = i+1 + world = World(multi_world, player_id) + multi_world.game[player_id] = world + multi_world.worlds[player_id] = world + multi_world.player_name[player_id] = "Test Player " + str(player_id) + region = Region("Menu", RegionType.Generic, + "Menu Region Hint", player_id, multi_world) + multi_world.regions.append(region) + multi_world.set_seed() # args = Namespace() # for name, option in world_type.options.items(): @@ -20,13 +27,24 @@ def generate_multi_world() -> MultiWorld: # multi_world.set_options(args) multi_world.set_default_common_options() - region = Region("Menu", RegionType.Generic, - "Menu Region Hint", player1_id, multi_world) - multi_world.regions.append(region) - return multi_world +class PlayerDefinition(NamedTuple): + id: int + menu: Region + locations: list[Location] + prog_items: list[Item] + + +def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition: + menu = multi_world.get_region("Menu", player_id) + locations = generate_locations(location_count, player_id, None, menu) + prog_items = generate_items(prog_item_count, player_id, True) + + return PlayerDefinition(player_id, menu, locations, prog_items) + + def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> list[Location]: locations = [] for i in range(count): @@ -48,124 +66,171 @@ def generate_items(count: int, player_id: int, advancement: bool = False, code: class TestBase(unittest.TestCase): def test_basic_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 2, 2) - locations = generate_locations(2, player1_id, None, player1_menu) - items = generate_items(2, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc0 = player1.locations[0] + loc1 = player1.locations[1] - item0 = items[0] - item1 = items[1] - loc0 = locations[0] - loc1 = locations[1] - - fill_restrictive(multi_world, multi_world.state, locations, items) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) self.assertEqual(loc0.item, item1) self.assertEqual(loc1.item, item0) - self.assertEqual([], locations) - self.assertEqual([], items) + self.assertEqual([], player1.locations) + self.assertEqual([], player1.prog_items) def test_ordered_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 2, 2) - locations = generate_locations(2, player1_id, None, player1_menu) - items = generate_items(2, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc0 = player1.locations[0] + loc1 = player1.locations[1] - item0 = items[0] - item1 = items[1] - loc0 = locations[0] - loc1 = locations[1] - - multi_world.completion_condition[player1_id] = lambda state: state.has( - item0.name, player1_id) and state.has(item1.name, player1_id) - set_rule(loc1, lambda state: state.has(item0.name, player1_id)) - fill_restrictive(multi_world, multi_world.state, locations, items) + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item1.name, player1.id) + set_rule(loc1, lambda state: state.has(item0.name, player1.id)) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) self.assertEqual(loc0.item, item0) self.assertEqual(loc1.item, item1) def test_reversed_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 2, 2) - locations = generate_locations(2, player1_id, None, player1_menu) - items = generate_items(2, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc0 = player1.locations[0] + loc1 = player1.locations[1] - item0 = items[0] - item1 = items[1] - loc0 = locations[0] - loc1 = locations[1] - - multi_world.completion_condition[player1_id] = lambda state: state.has( - item0.name, player1_id) and state.has(item1.name, player1_id) - set_rule(loc1, lambda state: state.has(item1.name, player1_id)) - fill_restrictive(multi_world, multi_world.state, locations, items) + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item1.name, player1.id) + set_rule(loc1, lambda state: state.has(item1.name, player1.id)) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) self.assertEqual(loc0.item, item1) self.assertEqual(loc1.item, item0) + def test_multi_step_fill_restrictive(self): + multi_world = generate_multi_world() + player1 = generate_player_data(multi_world, 1, 4, 4) + + items = player1.prog_items + locations = player1.locations + + multi_world.completion_condition[player1.id] = lambda state: state.has( + items[2].name, player1.id) and state.has(items[3].name, player1.id) + set_rule(locations[1], lambda state: state.has(items[0].name, player1.id)) + set_rule(locations[2], lambda state: state.has(items[1].name, player1.id)) + set_rule(locations[3], lambda state: state.has(items[1].name, player1.id)) + + fill_restrictive(multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) + + self.assertEqual(locations[0].item, items[1]) + self.assertEqual(locations[1].item, items[2]) + self.assertEqual(locations[2].item, items[0]) + self.assertEqual(locations[3].item, items[3]) + def test_impossible_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 2, 2) - locations = generate_locations(2, player1_id, None, player1_menu) - items = generate_items(2, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc0 = player1.locations[0] + loc1 = player1.locations[1] - item0 = items[0] - item1 = items[1] - loc0 = locations[0] - loc1 = locations[1] - - multi_world.completion_condition[player1_id] = lambda state: state.has( - item0.name, player1_id) and state.has(item1.name, player1_id) - set_rule(loc1, lambda state: state.has(item1.name, player1_id)) - set_rule(loc0, lambda state: state.has(item0.name, player1_id)) + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item1.name, player1.id) + set_rule(loc1, lambda state: state.has(item1.name, player1.id)) + set_rule(loc0, lambda state: state.has(item0.name, player1.id)) with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, locations, items) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) def test_circular_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 3, 3) - locations = generate_locations(3, player1_id, None, player1_menu) - items = generate_items(3, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + item2 = player1.prog_items[2] + loc0 = player1.locations[0] + loc1 = player1.locations[1] + loc2 = player1.locations[2] - item0 = items[0] - item1 = items[1] - item2 = items[2] - loc0 = locations[0] - loc1 = locations[1] - loc2 = locations[2] - - multi_world.completion_condition[player1_id] = lambda state: state.has( - item0.name, player1_id) and state.has(item1.name, player1_id) and state.has(item2.name, player1_id) - set_rule(loc1, lambda state: state.has(item0.name, player1_id)) - set_rule(loc2, lambda state: state.has(item1.name, player1_id)) - set_rule(loc0, lambda state: state.has(item2.name, player1_id)) + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item1.name, player1.id) and state.has(item2.name, player1.id) + set_rule(loc1, lambda state: state.has(item0.name, player1.id)) + set_rule(loc2, lambda state: state.has(item1.name, player1.id)) + set_rule(loc0, lambda state: state.has(item2.name, player1.id)) with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, locations, items) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) def test_competing_fill_restrictive(self): multi_world = generate_multi_world() - player1_id = 1 - player1_menu = multi_world.get_region("Menu", player1_id) + player1 = generate_player_data(multi_world, 1, 2, 2) - locations = generate_locations(2, player1_id, None, player1_menu) - items = generate_items(2, player1_id, True) + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc1 = player1.locations[1] - item0 = items[0] - item1 = items[1] - loc1 = locations[1] - - multi_world.completion_condition[player1_id] = lambda state: state.has( - item0.name, player1_id) and state.has(item0.name, player1_id) and state.has(item1.name, player1_id) - set_rule(loc1, lambda state: state.has(item0.name, player1_id) - and state.has(item1.name, player1_id)) + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id) + set_rule(loc1, lambda state: state.has(item0.name, player1.id) + and state.has(item1.name, player1.id)) with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, locations, items) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) + + def test_multiplayer_fill_restrictive(self): + multi_world = generate_multi_world(2) + player1 = generate_player_data(multi_world, 1, 2, 2) + player2 = generate_player_data(multi_world, 2, 2, 2) + + multi_world.completion_condition[player1.id] = lambda state: state.has( + player1.prog_items[0].name, player1.id) and state.has( + player1.prog_items[1].name, player1.id) + multi_world.completion_condition[player2.id] = lambda state: state.has( + player2.prog_items[0].name, player2.id) and state.has( + player2.prog_items[1].name, player2.id) + + fill_restrictive(multi_world, multi_world.state, player1.locations + + player2.locations, player1.prog_items + player2.prog_items) + + self.assertEqual(player1.locations[0].item, player1.prog_items[1]) + self.assertEqual(player1.locations[1].item, player2.prog_items[1]) + self.assertEqual(player2.locations[0].item, player1.prog_items[0]) + self.assertEqual(player2.locations[1].item, player2.prog_items[0]) + + def test_multiplayer_rules_fill_restrictive(self): + multi_world = generate_multi_world(2) + player1 = generate_player_data(multi_world, 1, 2, 2) + player2 = generate_player_data(multi_world, 2, 2, 2) + + multi_world.completion_condition[player1.id] = lambda state: state.has( + player1.prog_items[0].name, player1.id) and state.has( + player1.prog_items[1].name, player1.id) + multi_world.completion_condition[player2.id] = lambda state: state.has( + player2.prog_items[0].name, player2.id) and state.has( + player2.prog_items[1].name, player2.id) + + set_rule(player2.locations[1], lambda state: state.has( + player2.prog_items[0].name, player2.id)) + # set_rule(player2.locations[1], lambda state: state.has(player2.prog_items[1])) + + fill_restrictive(multi_world, multi_world.state, player1.locations + + player2.locations, player1.prog_items + player2.prog_items) + + self.assertEqual(player1.locations[0].item, player2.prog_items[0]) + self.assertEqual(player1.locations[1].item, player2.prog_items[1]) + self.assertEqual(player2.locations[0].item, player1.prog_items[0]) + self.assertEqual(player2.locations[1].item, player1.prog_items[1]) From 18d262c1ae6ca11153c910553ca695693ddd1191 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Tue, 28 Dec 2021 11:57:48 -0700 Subject: [PATCH 23/36] Add test for minimal accessibility --- Fill.py | 8 ++-- test/base/TestFill.py | 85 ++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 25 deletions(-) diff --git a/Fill.py b/Fill.py index f19cf0fd..0300b221 100644 --- a/Fill.py +++ b/Fill.py @@ -62,7 +62,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # try swaping this item with previously placed items for(i, location) in enumerate(placements): placed_item = location.item - # Unplaceable items can sometimes be swapped infinitely. Limit the + # Unplaceable items can sometimes be swapped infinitely. Limit the # number of times we will swap an individual item to prevent this if swapped_items[placed_item.player, placed_item.name] > 0: continue @@ -74,8 +74,10 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, # Add this item to the exisiting placement, and # add the old item to the back of the queue spot_to_fill = placements.pop(i) - swapped_items[placed_item.player, placed_item.name] += 1 - reachable_items[placed_item.player].appendleft(placed_item) + swapped_items[placed_item.player, + placed_item.name] += 1 + reachable_items[placed_item.player].appendleft( + placed_item) itempool.append(placed_item) break else: diff --git a/test/base/TestFill.py b/test/base/TestFill.py index 259cc8e4..e350e68a 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -84,20 +84,38 @@ class TestBase(unittest.TestCase): def test_ordered_fill_restrictive(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) - - item0 = player1.prog_items[0] - item1 = player1.prog_items[1] - loc0 = player1.locations[0] - loc1 = player1.locations[1] + items = player1.prog_items + locations = player1.locations multi_world.completion_condition[player1.id] = lambda state: state.has( - item0.name, player1.id) and state.has(item1.name, player1.id) - set_rule(loc1, lambda state: state.has(item0.name, player1.id)) + items[0].name, player1.id) and state.has(items[1].name, player1.id) + set_rule(locations[1], lambda state: state.has( + items[0].name, player1.id)) fill_restrictive(multi_world, multi_world.state, - player1.locations, player1.prog_items) + player1.locations.copy(), player1.prog_items.copy()) - self.assertEqual(loc0.item, item0) - self.assertEqual(loc1.item, item1) + self.assertEqual(locations[0].item, items[0]) + self.assertEqual(locations[1].item, items[1]) + + def test_minimal_fill_restrictive(self): + multi_world = generate_multi_world() + player1 = generate_player_data(multi_world, 1, 2, 2) + + items = player1.prog_items + locations = player1.locations + + multi_world.accessibility[player1.id] = 'minimal' + multi_world.completion_condition[player1.id] = lambda state: state.has( + items[1].name, player1.id) + set_rule(locations[1], lambda state: state.has( + items[0].name, player1.id)) + + fill_restrictive(multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) + + self.assertEqual(locations[0].item, items[1]) + # Unnecessary unreachable Item + self.assertEqual(locations[1].item, items[0]) def test_reversed_fill_restrictive(self): multi_world = generate_multi_world() @@ -126,9 +144,12 @@ class TestBase(unittest.TestCase): multi_world.completion_condition[player1.id] = lambda state: state.has( items[2].name, player1.id) and state.has(items[3].name, player1.id) - set_rule(locations[1], lambda state: state.has(items[0].name, player1.id)) - set_rule(locations[2], lambda state: state.has(items[1].name, player1.id)) - set_rule(locations[3], lambda state: state.has(items[1].name, player1.id)) + set_rule(locations[1], lambda state: state.has( + items[0].name, player1.id)) + set_rule(locations[2], lambda state: state.has( + items[1].name, player1.id)) + set_rule(locations[3], lambda state: state.has( + items[1].name, player1.id)) fill_restrictive(multi_world, multi_world.state, player1.locations.copy(), player1.prog_items.copy()) @@ -138,22 +159,42 @@ class TestBase(unittest.TestCase): self.assertEqual(locations[2].item, items[0]) self.assertEqual(locations[3].item, items[3]) - def test_impossible_fill_restrictive(self): + def test_minimal_fill_restrictive(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) - item0 = player1.prog_items[0] - item1 = player1.prog_items[1] - loc0 = player1.locations[0] - loc1 = player1.locations[1] + items = player1.prog_items + locations = player1.locations + + multi_world.accessibility[player1.id] = 'minimal' + multi_world.completion_condition[player1.id] = lambda state: state.has( + items[1].name, player1.id) + set_rule(locations[1], lambda state: state.has( + items[0].name, player1.id)) + + fill_restrictive(multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) + + self.assertEqual(locations[0].item, items[1]) + # Unnecessary unreachable Item + self.assertEqual(locations[1].item, items[0]) + + def test_impossible_fill_restrictive(self): + multi_world = generate_multi_world() + player1 = generate_player_data(multi_world, 1, 2, 2) + items = player1.prog_items + locations = player1.locations multi_world.completion_condition[player1.id] = lambda state: state.has( - item0.name, player1.id) and state.has(item1.name, player1.id) - set_rule(loc1, lambda state: state.has(item1.name, player1.id)) - set_rule(loc0, lambda state: state.has(item0.name, player1.id)) + items[0].name, player1.id) and state.has(items[1].name, player1.id) + set_rule(locations[1], lambda state: state.has( + items[1].name, player1.id)) + set_rule(locations[0], lambda state: state.has( + items[0].name, player1.id)) + with pytest.raises(FillError): fill_restrictive(multi_world, multi_world.state, - player1.locations, player1.prog_items) + player1.locations.copy(), player1.prog_items.copy()) def test_circular_fill_restrictive(self): multi_world = generate_multi_world() From 3d657191700f3be1c4d228d162d28dbfaf091f00 Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Thu, 30 Dec 2021 08:22:46 -0700 Subject: [PATCH 24/36] Remove dependency on pytest --- test/base/TestFill.py | 43 ++++++++----------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/test/base/TestFill.py b/test/base/TestFill.py index e350e68a..f1da0d66 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -1,6 +1,5 @@ from typing import NamedTuple import unittest -import pytest from worlds.AutoWorld import World from Fill import FillError, fill_restrictive from BaseClasses import MultiWorld, Region, RegionType, Item, Location @@ -21,10 +20,6 @@ def generate_multi_world(players: int = 1) -> MultiWorld: multi_world.regions.append(region) multi_world.set_seed() - # args = Namespace() - # for name, option in world_type.options.items(): - # setattr(args, name, {1: option.from_any(option.default)}) - # multi_world.set_options(args) multi_world.set_default_common_options() return multi_world @@ -159,26 +154,6 @@ class TestBase(unittest.TestCase): self.assertEqual(locations[2].item, items[0]) self.assertEqual(locations[3].item, items[3]) - def test_minimal_fill_restrictive(self): - multi_world = generate_multi_world() - player1 = generate_player_data(multi_world, 1, 2, 2) - - items = player1.prog_items - locations = player1.locations - - multi_world.accessibility[player1.id] = 'minimal' - multi_world.completion_condition[player1.id] = lambda state: state.has( - items[1].name, player1.id) - set_rule(locations[1], lambda state: state.has( - items[0].name, player1.id)) - - fill_restrictive(multi_world, multi_world.state, - player1.locations.copy(), player1.prog_items.copy()) - - self.assertEqual(locations[0].item, items[1]) - # Unnecessary unreachable Item - self.assertEqual(locations[1].item, items[0]) - def test_impossible_fill_restrictive(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -192,9 +167,8 @@ class TestBase(unittest.TestCase): set_rule(locations[0], lambda state: state.has( items[0].name, player1.id)) - with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, - player1.locations.copy(), player1.prog_items.copy()) + self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) def test_circular_fill_restrictive(self): multi_world = generate_multi_world() @@ -212,9 +186,9 @@ class TestBase(unittest.TestCase): set_rule(loc1, lambda state: state.has(item0.name, player1.id)) set_rule(loc2, lambda state: state.has(item1.name, player1.id)) set_rule(loc0, lambda state: state.has(item2.name, player1.id)) - with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, - player1.locations, player1.prog_items) + + self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) def test_competing_fill_restrictive(self): multi_world = generate_multi_world() @@ -228,9 +202,9 @@ class TestBase(unittest.TestCase): item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has(item0.name, player1.id) and state.has(item1.name, player1.id)) - with pytest.raises(FillError): - fill_restrictive(multi_world, multi_world.state, - player1.locations, player1.prog_items) + + self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, + player1.locations.copy(), player1.prog_items.copy()) def test_multiplayer_fill_restrictive(self): multi_world = generate_multi_world(2) @@ -266,7 +240,6 @@ class TestBase(unittest.TestCase): set_rule(player2.locations[1], lambda state: state.has( player2.prog_items[0].name, player2.id)) - # set_rule(player2.locations[1], lambda state: state.has(player2.prog_items[1])) fill_restrictive(multi_world, multi_world.state, player1.locations + player2.locations, player1.prog_items + player2.prog_items) From b7676a3da2164e2a2e1ea296a2446497afa30768 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Dec 2021 12:31:44 -0500 Subject: [PATCH 25/36] Add "Start With" option for dungeon items --- worlds/alttp/Options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 01eda055..fb832d5b 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -34,6 +34,7 @@ class DungeonItem(Choice): option_own_world = 2 option_any_world = 3 option_different_world = 4 + option_start_with = 6 alias_true = 3 alias_false = 0 From c0f95755ffa36e8968ea4b4fe7b08f664259b3da Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Dec 2021 12:32:16 -0500 Subject: [PATCH 26/36] Add "Start With" option --- worlds/alttp/ItemPool.py | 79 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index eb9d096a..e63007ca 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -1,15 +1,15 @@ -from collections import namedtuple import logging +from collections import namedtuple from BaseClasses import Region, RegionType -from worlds.alttp.SubClasses import ALttPLocation -from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops +from Fill import FillError from worlds.alttp.Bosses import place_bosses from worlds.alttp.Dungeons import get_dungeon_item_pool_player from worlds.alttp.EntranceShuffle import connect_entrance -from Fill import FillError from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle +from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle +from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops +from worlds.alttp.SubClasses import ALttPLocation # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -226,6 +226,7 @@ for diff in {'easy', 'normal', 'hard', 'expert'}: def generate_itempool(world): player = world.player world = world.world + if world.difficulty[player] not in difficulties: raise NotImplementedError(f"Diffulty {world.difficulty[player]}") if world.goal[player] not in {'ganon', 'pedestal', 'bosses', 'triforcehunt', 'localtriforcehunt', 'icerodhunt', @@ -651,6 +652,74 @@ def get_pool_core(world, player: int): place_item(key_location, item_to_place) else: pool.extend([item_to_place]) + + if world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with: + precollected_items.append('Small Key (Hyrule Castle)') + precollected_items.append('Small Key (Desert Palace)') + precollected_items.append('Small Key (Tower of Hera)') + precollected_items.extend(['Small Key (Palace of Darkness)'] * 6) + precollected_items.append('Small Key (Thieves Town)') + precollected_items.extend(['Small Key (Skull Woods)'] * 3) + precollected_items.append('Small Key (Swamp Palace)') + precollected_items.extend(['Small Key (Ice Palace)'] * 2) + precollected_items.extend(['Small Key (Misery Mire)'] * 3) + precollected_items.extend(['Small Key (Turtle Rock)'] * 4) + precollected_items.extend(['Small Key (Agahnims Tower)'] * 2) + precollected_items.extend(['Small Key (Ganons Tower)'] * 4) + pool.extend(['Rupees (20)'] * 23) + pool.extend(['Arrows (10)'] * 3) + pool.extend(['Bombs (10)'] * 3) + + if world.compass_shuffle[player] == compass_shuffle.option_start_with: + precollected_items.append('Compass (Eastern Palace)') + precollected_items.append('Compass (Desert Palace)') + precollected_items.append('Compass (Tower of Hera)') + precollected_items.append('Compass (Palace of Darkness)') + precollected_items.append('Compass (Thieves Town)') + precollected_items.append('Compass (Skull Woods)') + precollected_items.append('Compass (Swamp Palace)') + precollected_items.append('Compass (Ice Palace)') + precollected_items.append('Compass (Misery Mire)') + precollected_items.append('Compass (Turtle Rock)') + precollected_items.append('Compass (Ganons Tower)') + pool.extend(['Rupees (20)'] * 9) + pool.append('Arrows (10)') + pool.append('Bombs (10)') + + if world.map_shuffle[player] == map_shuffle.option_start_with: + precollected_items.append('Map (Hyrule Castle)') + precollected_items.append('Map (Eastern Palace)') + precollected_items.append('Map (Desert Palace)') + precollected_items.append('Map (Tower of Hera)') + precollected_items.append('Map (Palace of Darkness)') + precollected_items.append('Map (Thieves Town)') + precollected_items.append('Map (Skull Woods)') + precollected_items.append('Map (Swamp Palace)') + precollected_items.append('Map (Ice Palace)') + precollected_items.append('Map (Misery Mire)') + precollected_items.append('Map (Turtle Rock)') + precollected_items.append('Map (Ganons Tower)') + pool.extend(['Rupees (20)'] * 10) + pool.append('Arrows (10)') + pool.append('Bombs (10)') + + if world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with: + precollected_items.append('Big Key (Eastern Palace)') + precollected_items.append('Big Key (Desert Palace)') + precollected_items.append('Big Key (Tower of Hera)') + precollected_items.append('Big Key (Palace of Darkness)') + precollected_items.append('Big Key (Thieves Town)') + precollected_items.append('Big Key (Skull Woods)') + precollected_items.append('Big Key (Swamp Palace)') + precollected_items.append('Big Key (Ice Palace)') + precollected_items.append('Big Key (Misery Mire)') + precollected_items.append('Big Key (Turtle Rock)') + precollected_items.append('Big Key (Ganons Tower)') + pool.extend(['Rupees (20)'] * 9) + pool.append('Arrows (10)') + pool.append('Bombs (10)') + + return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) From 35b1178c205538bad457e55f2c3967f6ead2b00a Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Dec 2021 12:32:49 -0500 Subject: [PATCH 27/36] Add "Start With" option --- worlds/alttp/Dungeons.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 4b0814d3..9356bb20 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -3,13 +3,22 @@ from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops -from worlds.alttp.Options import smallkey_shuffle +from worlds.alttp.Options import smallkey_shuffle, bigkey_shuffle, compass_shuffle, map_shuffle def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): + if world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with: + big_key = None + if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with: + small_keys = [] + for dungeonitem in dungeon_items: + if dungeonitem.type == 'Map' and world.map_shuffle[player] == map_shuffle.option_start_with: + dungeon_items.remove(dungeonitem) + if dungeonitem.type == 'Compass' and world.compass_shuffle[player] == compass_shuffle.option_start_with: + dungeon_items.remove(dungeonitem) dungeon = Dungeon(name, dungeon_regions, big_key, - [] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, + small_keys, dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon From 8fef6b8d8cb5200433c1b2dba8d3caa9021bd481 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 23 Dec 2021 13:02:52 -0500 Subject: [PATCH 28/36] Add "Start With" option --- worlds/alttp/Dungeons.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 9356bb20..2b1789c3 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -15,6 +15,7 @@ def create_dungeons(world, player): for dungeonitem in dungeon_items: if dungeonitem.type == 'Map' and world.map_shuffle[player] == map_shuffle.option_start_with: dungeon_items.remove(dungeonitem) + for dungeonitem in dungeon_items: if dungeonitem.type == 'Compass' and world.compass_shuffle[player] == compass_shuffle.option_start_with: dungeon_items.remove(dungeonitem) dungeon = Dungeon(name, dungeon_regions, big_key, From c42d489bf70b0b4f9376ede5a543041ad97ad4be Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 27 Dec 2021 23:13:22 -0500 Subject: [PATCH 29/36] Pull dungeon item replacements from diff extras --- worlds/alttp/ItemPool.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index e63007ca..ac399798 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -1,15 +1,15 @@ -import logging from collections import namedtuple +import logging from BaseClasses import Region, RegionType -from Fill import FillError +from worlds.alttp.SubClasses import ALttPLocation +from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops from worlds.alttp.Bosses import place_bosses from worlds.alttp.Dungeons import get_dungeon_item_pool_player from worlds.alttp.EntranceShuffle import connect_entrance +from Fill import FillError from worlds.alttp.Items import ItemFactory, GetBeemizerItem from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle -from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops -from worlds.alttp.SubClasses import ALttPLocation # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. @@ -653,6 +653,9 @@ def get_pool_core(world, player: int): else: pool.extend([item_to_place]) + dungitemreplacements = diff.extras[0] + diff.extras[1] + diff.extras[2] + diff.extras[3] + diff.extras[4] + world.random.shuffle(dungitemreplacements) + if world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with: precollected_items.append('Small Key (Hyrule Castle)') precollected_items.append('Small Key (Desert Palace)') @@ -666,9 +669,9 @@ def get_pool_core(world, player: int): precollected_items.extend(['Small Key (Turtle Rock)'] * 4) precollected_items.extend(['Small Key (Agahnims Tower)'] * 2) precollected_items.extend(['Small Key (Ganons Tower)'] * 4) - pool.extend(['Rupees (20)'] * 23) - pool.extend(['Arrows (10)'] * 3) - pool.extend(['Bombs (10)'] * 3) + pool.extend(dungitemreplacements[:29]) + dungitemreplacements = dungitemreplacements[29:] + if world.compass_shuffle[player] == compass_shuffle.option_start_with: precollected_items.append('Compass (Eastern Palace)') @@ -682,9 +685,8 @@ def get_pool_core(world, player: int): precollected_items.append('Compass (Misery Mire)') precollected_items.append('Compass (Turtle Rock)') precollected_items.append('Compass (Ganons Tower)') - pool.extend(['Rupees (20)'] * 9) - pool.append('Arrows (10)') - pool.append('Bombs (10)') + pool.extend(dungitemreplacements[:11]) + dungitemreplacements = dungitemreplacements[11:] if world.map_shuffle[player] == map_shuffle.option_start_with: precollected_items.append('Map (Hyrule Castle)') @@ -699,9 +701,8 @@ def get_pool_core(world, player: int): precollected_items.append('Map (Misery Mire)') precollected_items.append('Map (Turtle Rock)') precollected_items.append('Map (Ganons Tower)') - pool.extend(['Rupees (20)'] * 10) - pool.append('Arrows (10)') - pool.append('Bombs (10)') + pool.extend(dungitemreplacements[:12]) + dungitemreplacements = dungitemreplacements[12:] if world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with: precollected_items.append('Big Key (Eastern Palace)') @@ -715,9 +716,7 @@ def get_pool_core(world, player: int): precollected_items.append('Big Key (Misery Mire)') precollected_items.append('Big Key (Turtle Rock)') precollected_items.append('Big Key (Ganons Tower)') - pool.extend(['Rupees (20)'] * 9) - pool.append('Arrows (10)') - pool.append('Bombs (10)') + pool.extend(dungitemreplacements[:11]) return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, From d10ddb17b6304e384bd92ea0d02bd0747ac9ee90 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 28 Dec 2021 12:54:42 -0500 Subject: [PATCH 30/36] Let make_dungeon set up items, then replace --- worlds/alttp/Dungeons.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index 2b1789c3..4b0814d3 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -3,23 +3,13 @@ from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive from worlds.alttp.Items import ItemFactory from worlds.alttp.Regions import lookup_boss_drops -from worlds.alttp.Options import smallkey_shuffle, bigkey_shuffle, compass_shuffle, map_shuffle +from worlds.alttp.Options import smallkey_shuffle def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): - if world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with: - big_key = None - if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with: - small_keys = [] - for dungeonitem in dungeon_items: - if dungeonitem.type == 'Map' and world.map_shuffle[player] == map_shuffle.option_start_with: - dungeon_items.remove(dungeonitem) - for dungeonitem in dungeon_items: - if dungeonitem.type == 'Compass' and world.compass_shuffle[player] == compass_shuffle.option_start_with: - dungeon_items.remove(dungeonitem) dungeon = Dungeon(name, dungeon_regions, big_key, - small_keys, + [] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys, dungeon_items, player) for item in dungeon.all_items: item.dungeon = dungeon From 01a2376b74c0e97cec59453e18dc572690a5202c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Tue, 28 Dec 2021 12:55:13 -0500 Subject: [PATCH 31/36] Let make_dungeon set up items, then replace --- worlds/alttp/ItemPool.py | 86 +++++++++------------------------------- 1 file changed, 18 insertions(+), 68 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index ac399798..d6c6005b 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -372,14 +372,30 @@ def generate_itempool(world): dungeon_items = [item for item in get_dungeon_item_pool_player(world, player) if item.name not in world.worlds[player].dungeon_local_item_names] - + dungeon_item_replacements = difficulties[world.difficulty[player]].extras[0]\ + + difficulties[world.difficulty[player]].extras[1]\ + + difficulties[world.difficulty[player]].extras[2]\ + + difficulties[world.difficulty[player]].extras[3]\ + + difficulties[world.difficulty[player]].extras[4] + world.random.shuffle(dungeon_item_replacements) if world.goal[player] == 'icerodhunt': for item in dungeon_items: world.itempool.append(ItemFactory(GetBeemizerItem(world, player, 'Nothing'), player)) world.push_precollected(item) else: + for x in range(len(dungeon_items)-1, -1, -1): + item = dungeon_items[x] + logging.info(f'itemn:{x} item:{item} len:{len(dungeon_items)}') + if ((world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') + or (world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') + or (world.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') + or (world.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): + logging.info('Removing...') + dungeon_items.remove(item) + world.push_precollected(item) + world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) + logging.info(f'Done: {dungeon_items}') world.itempool.extend([item for item in dungeon_items]) - # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) # We mark one random heart container as an advancement item (or 4 heart pieces in expert mode) @@ -653,72 +669,6 @@ def get_pool_core(world, player: int): else: pool.extend([item_to_place]) - dungitemreplacements = diff.extras[0] + diff.extras[1] + diff.extras[2] + diff.extras[3] + diff.extras[4] - world.random.shuffle(dungitemreplacements) - - if world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with: - precollected_items.append('Small Key (Hyrule Castle)') - precollected_items.append('Small Key (Desert Palace)') - precollected_items.append('Small Key (Tower of Hera)') - precollected_items.extend(['Small Key (Palace of Darkness)'] * 6) - precollected_items.append('Small Key (Thieves Town)') - precollected_items.extend(['Small Key (Skull Woods)'] * 3) - precollected_items.append('Small Key (Swamp Palace)') - precollected_items.extend(['Small Key (Ice Palace)'] * 2) - precollected_items.extend(['Small Key (Misery Mire)'] * 3) - precollected_items.extend(['Small Key (Turtle Rock)'] * 4) - precollected_items.extend(['Small Key (Agahnims Tower)'] * 2) - precollected_items.extend(['Small Key (Ganons Tower)'] * 4) - pool.extend(dungitemreplacements[:29]) - dungitemreplacements = dungitemreplacements[29:] - - - if world.compass_shuffle[player] == compass_shuffle.option_start_with: - precollected_items.append('Compass (Eastern Palace)') - precollected_items.append('Compass (Desert Palace)') - precollected_items.append('Compass (Tower of Hera)') - precollected_items.append('Compass (Palace of Darkness)') - precollected_items.append('Compass (Thieves Town)') - precollected_items.append('Compass (Skull Woods)') - precollected_items.append('Compass (Swamp Palace)') - precollected_items.append('Compass (Ice Palace)') - precollected_items.append('Compass (Misery Mire)') - precollected_items.append('Compass (Turtle Rock)') - precollected_items.append('Compass (Ganons Tower)') - pool.extend(dungitemreplacements[:11]) - dungitemreplacements = dungitemreplacements[11:] - - if world.map_shuffle[player] == map_shuffle.option_start_with: - precollected_items.append('Map (Hyrule Castle)') - precollected_items.append('Map (Eastern Palace)') - precollected_items.append('Map (Desert Palace)') - precollected_items.append('Map (Tower of Hera)') - precollected_items.append('Map (Palace of Darkness)') - precollected_items.append('Map (Thieves Town)') - precollected_items.append('Map (Skull Woods)') - precollected_items.append('Map (Swamp Palace)') - precollected_items.append('Map (Ice Palace)') - precollected_items.append('Map (Misery Mire)') - precollected_items.append('Map (Turtle Rock)') - precollected_items.append('Map (Ganons Tower)') - pool.extend(dungitemreplacements[:12]) - dungitemreplacements = dungitemreplacements[12:] - - if world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with: - precollected_items.append('Big Key (Eastern Palace)') - precollected_items.append('Big Key (Desert Palace)') - precollected_items.append('Big Key (Tower of Hera)') - precollected_items.append('Big Key (Palace of Darkness)') - precollected_items.append('Big Key (Thieves Town)') - precollected_items.append('Big Key (Skull Woods)') - precollected_items.append('Big Key (Swamp Palace)') - precollected_items.append('Big Key (Ice Palace)') - precollected_items.append('Big Key (Misery Mire)') - precollected_items.append('Big Key (Turtle Rock)') - precollected_items.append('Big Key (Ganons Tower)') - pool.extend(dungitemreplacements[:11]) - - return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon, additional_pieces_to_place) From b65618030f09f62171770122414321fe5d8a4123 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Wed, 29 Dec 2021 07:35:23 -0500 Subject: [PATCH 32/36] Remove unnecessary logging.info --- worlds/alttp/ItemPool.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index d6c6005b..1d224569 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -385,16 +385,13 @@ def generate_itempool(world): else: for x in range(len(dungeon_items)-1, -1, -1): item = dungeon_items[x] - logging.info(f'itemn:{x} item:{item} len:{len(dungeon_items)}') if ((world.smallkey_shuffle[player] == smallkey_shuffle.option_start_with and item.type == 'SmallKey') or (world.bigkey_shuffle[player] == bigkey_shuffle.option_start_with and item.type == 'BigKey') or (world.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass') or (world.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')): - logging.info('Removing...') dungeon_items.remove(item) world.push_precollected(item) world.itempool.append(ItemFactory(dungeon_item_replacements.pop(), player)) - logging.info(f'Done: {dungeon_items}') world.itempool.extend([item for item in dungeon_items]) # logic has some branches where having 4 hearts is one possible requirement (of several alternatives) # rather than making all hearts/heart pieces progression items (which slows down generation considerably) From d437f0105a6f8579ab3f4361f912fbd527d3fb8a Mon Sep 17 00:00:00 2001 From: Brad Humphrey Date: Thu, 30 Dec 2021 10:50:18 -0700 Subject: [PATCH 33/36] Test remaining locations after swapping --- test/base/TestFill.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/test/base/TestFill.py b/test/base/TestFill.py index f1da0d66..38447b62 100644 --- a/test/base/TestFill.py +++ b/test/base/TestFill.py @@ -92,6 +92,31 @@ class TestBase(unittest.TestCase): self.assertEqual(locations[0].item, items[0]) self.assertEqual(locations[1].item, items[1]) + def test_fill_restrictive_remaining_locations(self): + multi_world = generate_multi_world() + player1 = generate_player_data(multi_world, 1, 3, 2) + + item0 = player1.prog_items[0] + item1 = player1.prog_items[1] + loc0 = player1.locations[0] + loc1 = player1.locations[1] + loc2 = player1.locations[2] + + multi_world.completion_condition[player1.id] = lambda state: state.has( + item0.name, player1.id) and state.has(item1.name, player1.id) + set_rule(loc1, lambda state: state.has( + item0.name, player1.id)) + #forces a swap + set_rule(loc2, lambda state: state.has( + item0.name, player1.id)) + fill_restrictive(multi_world, multi_world.state, + player1.locations, player1.prog_items) + + self.assertEqual(loc0.item, item0) + self.assertEqual(loc1.item, item1) + self.assertEqual(1, len(player1.locations)) + self.assertEqual(player1.locations[0], loc2) + def test_minimal_fill_restrictive(self): multi_world = generate_multi_world() player1 = generate_player_data(multi_world, 1, 2, 2) @@ -202,7 +227,7 @@ class TestBase(unittest.TestCase): item0.name, player1.id) and state.has(item0.name, player1.id) and state.has(item1.name, player1.id) set_rule(loc1, lambda state: state.has(item0.name, player1.id) and state.has(item1.name, player1.id)) - + self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state, player1.locations.copy(), player1.prog_items.copy()) From d13b7988b7e2d1120c3be43c9deb322aa4dec2df Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 31 Dec 2021 15:08:30 +0100 Subject: [PATCH 34/36] Core: undo change that made Python 3.9 required --- Fill.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Fill.py b/Fill.py index 0300b221..eb0a7d47 100644 --- a/Fill.py +++ b/Fill.py @@ -22,8 +22,8 @@ def sweep_from_pool(base_state: CollectionState, itempool): return new_state -def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: list[Item], single_player_placement=False, - lock=False): +def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: typing.List[Item], + single_player_placement=False, lock=False): unplaced_items = [] placements = [] From c550fdaee8445bc2af780964ad788ab39281c41d Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 Dec 2021 13:22:23 -0500 Subject: [PATCH 35/36] WebHost now generates a weighted-settings.json file for use with the upcoming weighted-settings page. --- WebHostLib/options.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 73bcff46..8411e138 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -27,7 +27,19 @@ def create(): return list(default_value) return default_value + weighted_settings = { + "baseOptions": { + "description": "Generated by https://archipelago.gg/", + "name": "Player", + "game": {}, + }, + "games": {}, + } + for game_name, world in AutoWorldRegister.world_types.items(): + if (world.hidden): + continue + all_options = {**world.options, **Options.per_game_common_options} res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( options=all_options, @@ -88,3 +100,11 @@ def create(): with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: f.write(json.dumps(player_settings, indent=2, separators=(',', ': '))) + + weighted_settings["baseOptions"]["game"][game_name] = 0 + weighted_settings["games"][game_name] = {} + weighted_settings["games"][game_name]["gameOptions"] = game_options + weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_name_to_id.keys()) + + with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f: + f.write(json.dumps(weighted_settings, indent=2, separators=(',', ': '))) From 4db4b5305e4d7ea5bcc0e5b856de52b7258f40d6 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Fri, 31 Dec 2021 20:05:36 +0100 Subject: [PATCH 36/36] [Docs] Added links to client implementations (#167) --- docs/network protocol.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 21c7d23d..355bfdfd 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -13,6 +13,10 @@ These steps should be followed in order to establish a gameplay connection with In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. +There are libraries available that implement the this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) and [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) + +For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) + ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. @@ -140,8 +144,8 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: | ---- | ---- | ----- | | hint_points | int | New argument. The client's current hint points. | | players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. | -| checked_locations | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | -| missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. | +| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | +| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. | All arguments for this packet are optional, only changes are sent.