mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-25 13:03:22 -07:00
Merge branch 'main' into civ6-1.0
This commit is contained in:
@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
||||
|
||||
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
|
||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
|
||||
if world.is_dlc1():
|
||||
# Moderate: clear Rock the Boat without Ice Hat
|
||||
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Deep Sea without Ice Hat
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||
@@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
||||
|
||||
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||
@@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||
|
||||
@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
class AP_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
options_page = False
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# APSudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
||||
- Windows (most tested on Win10)
|
||||
- Other platforms might be able to build from source themselves; and may be included in the future.
|
||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
||||
|
||||
## General Concept
|
||||
|
||||
@@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run APSudoku.exe
|
||||
2. Under the 'Archipelago' tab at the top-right:
|
||||
- Enter the server url & port number
|
||||
1. Run the APSudoku executable.
|
||||
2. Under `Settings` → `Connection` at the top-right:
|
||||
- Enter the server address and port number
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Select DeathLink related settings (optional)
|
||||
- Press connect
|
||||
3. Go back to the 'Sudoku' tab
|
||||
- Click the various '?' buttons for information on how to play / control
|
||||
4. Choose puzzle difficulty
|
||||
5. Try to solve the Sudoku. Click 'Check' when done.
|
||||
- Press `Connect`
|
||||
4. Under the `Sudoku` tab
|
||||
- Choose puzzle difficulty
|
||||
- Click `Start` to generate a puzzle
|
||||
5. Try to solve the Sudoku. Click `Check` when done
|
||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
||||
|
||||
Info:
|
||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
||||
- Click the various `?` buttons for information on controls/how to play
|
||||
## DeathLink Support
|
||||
|
||||
If 'DeathLink' is enabled when you click 'Connect':
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
|
||||
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
If `DeathLink` is enabled when you click `Connect`:
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
- On receiving a DeathLink from another player, your puzzle resets.
|
||||
|
||||
@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
|
||||
self.multiworld.regions += [menu, board]
|
||||
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
itempool = []
|
||||
# Generate list of items
|
||||
items_to_create = []
|
||||
# Add the map width and height stuff
|
||||
itempool += ["Map Width"] * 5 # 10 - 5
|
||||
itempool += ["Map Height"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Width"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Height"] * 5 # 10 - 5
|
||||
# Add the map bombs
|
||||
itempool += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert itempool into real items
|
||||
itempool = [self.create_item(item) for item in itempool]
|
||||
items_to_create += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert list into real items
|
||||
itempool = [self.create_item(item) for item in items_to_create]
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
|
||||
@@ -238,15 +238,6 @@ class DS3ItemData:
|
||||
ds3_code = cast(int, self.ds3_code) + level,
|
||||
filler = False,
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return (self.name, self.ds3_code).__hash__()
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
if isinstance(other, self.__class__):
|
||||
return self.name == other.name and self.ds3_code == other.ds3_code
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class DarkSouls3Item(Item):
|
||||
|
||||
@@ -1504,16 +1504,19 @@ class DarkSouls3World(World):
|
||||
# We include all the items the game knows about so that users can manually request items
|
||||
# that aren't randomized, and then we _also_ include all the items that are placed in
|
||||
# practice `item_dictionary.values()` doesn't include upgraded or infused weapons.
|
||||
all_items = {
|
||||
cast(DarkSouls3Item, location.item).data
|
||||
items_by_name = {
|
||||
location.item.name: cast(DarkSouls3Item, location.item).data
|
||||
for location in self.multiworld.get_filled_locations()
|
||||
# item.code None is used for events, which we want to skip
|
||||
if location.item.code is not None and location.item.player == self.player
|
||||
}.union(item_dictionary.values())
|
||||
}
|
||||
for item in item_dictionary.values():
|
||||
if item.name not in items_by_name:
|
||||
items_by_name[item.name] = item
|
||||
|
||||
ap_ids_to_ds3_ids: Dict[str, int] = {}
|
||||
item_counts: Dict[str, int] = {}
|
||||
for item in all_items:
|
||||
for item in items_by_name.values():
|
||||
if item.ap_code is None: continue
|
||||
if item.ds3_code: ap_ids_to_ds3_ids[str(item.ap_code)] = item.ds3_code
|
||||
if item.count != 1: item_counts[str(item.ap_code)] = item.count
|
||||
|
||||
@@ -280,6 +280,8 @@ def generateRom(args, world: "LinksAwakeningWorld"):
|
||||
name = "Your"
|
||||
else:
|
||||
name = f"{world.multiworld.player_name[location.item.player]}'s"
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
name = name.replace("{", "").replace("}", "")
|
||||
|
||||
if isinstance(location, LinksAwakeningLocation):
|
||||
location_name = location.ladxr_item.metadata.name
|
||||
@@ -288,7 +290,9 @@ def generateRom(args, world: "LinksAwakeningWorld"):
|
||||
|
||||
hint = f"{name} {location.item} is at {location_name}"
|
||||
if location.player != world.player:
|
||||
hint += f" in {world.multiworld.player_name[location.player]}'s world"
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "")
|
||||
hint += f" in {player_name}'s world"
|
||||
|
||||
# Cap hint size at 85
|
||||
# Realistically we could go bigger but let's be safe instead
|
||||
|
||||
@@ -18,7 +18,8 @@ class ShopItem(ItemInfo):
|
||||
mw_text = ""
|
||||
if multiworld:
|
||||
mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}"
|
||||
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
mw_text = mw_text.replace("{", "").replace("}", "")
|
||||
|
||||
if self.custom_item_name:
|
||||
name = self.custom_item_name
|
||||
|
||||
@@ -114,6 +114,14 @@ class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
|
||||
|
||||
def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
|
||||
# TODO: Remove when the base patch is updated to include this change
|
||||
# Moves an NPC to avoid overlapping people during trainersanity
|
||||
patch.write_token(
|
||||
APTokenTypes.WRITE,
|
||||
0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord
|
||||
struct.pack("<H", 11)
|
||||
)
|
||||
|
||||
# Set free fly location
|
||||
if world.options.free_fly_location:
|
||||
patch.write_token(
|
||||
|
||||
@@ -26,6 +26,7 @@ class FishItem:
|
||||
|
||||
fresh_water = (Region.farm, Region.forest, Region.town, Region.mountain)
|
||||
ocean = (Region.beach,)
|
||||
tide_pools = (Region.tide_pools,)
|
||||
town_river = (Region.town,)
|
||||
mountain_lake = (Region.mountain,)
|
||||
forest_pond = (Region.forest,)
|
||||
@@ -118,13 +119,13 @@ midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 5
|
||||
spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60)
|
||||
|
||||
angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False)
|
||||
crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False)
|
||||
crimsonfish = create_fish(Fish.crimsonfish, tide_pools, season.summer, 95, True, False)
|
||||
glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False)
|
||||
legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False)
|
||||
mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False)
|
||||
|
||||
ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True)
|
||||
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True)
|
||||
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, tide_pools, season.summer, 95, True, True)
|
||||
glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True)
|
||||
legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True)
|
||||
radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True)
|
||||
|
||||
@@ -27,6 +27,7 @@ def collect_fishing_abilities(tester: SVTestBase):
|
||||
tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Beach Bridge"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False)
|
||||
|
||||
@@ -258,15 +258,19 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
|
||||
def collect_lots_of_money(self):
|
||||
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
|
||||
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25))
|
||||
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
|
||||
required_prog_items = int(round(real_total_prog_items * 0.25))
|
||||
for i in range(required_prog_items):
|
||||
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
|
||||
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
|
||||
|
||||
def collect_all_the_money(self):
|
||||
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
|
||||
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95))
|
||||
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
|
||||
required_prog_items = int(round(real_total_prog_items * 0.95))
|
||||
for i in range(required_prog_items):
|
||||
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
|
||||
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
|
||||
|
||||
def collect_everything(self):
|
||||
non_event_items = [item for item in self.multiworld.get_items() if item.code]
|
||||
|
||||
61
worlds/stardew_valley/test/rules/TestFishing.py
Normal file
61
worlds/stardew_valley/test/rules/TestFishing.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \
|
||||
ElevatorProgression, SpecialOrderLocations
|
||||
from ...strings.fish_names import Fish
|
||||
from ...test import SVTestBase
|
||||
|
||||
|
||||
class TestNeedRegionToCatchFish(SVTestBase):
|
||||
options = {
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
|
||||
SkillProgression.internal_name: SkillProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_vanilla,
|
||||
Fishsanity.internal_name: Fishsanity.option_all,
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
|
||||
}
|
||||
|
||||
def test_catch_fish_requires_region_unlock(self):
|
||||
fish_and_items = {
|
||||
Fish.crimsonfish: ["Beach Bridge"],
|
||||
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
|
||||
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
|
||||
Fish.mutant_carp: ["Rusty Key"],
|
||||
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
|
||||
Fish.lionfish: ["Boat Repair"],
|
||||
Fish.blue_discus: ["Island Obelisk", "Island West Turtle"],
|
||||
Fish.stingray: ["Boat Repair", "Island Resort"],
|
||||
Fish.ghostfish: ["Progressive Weapon"],
|
||||
Fish.stonefish: ["Progressive Weapon"],
|
||||
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.sandfish: ["Bus Repair"],
|
||||
Fish.scorpion_carp: ["Desert Obelisk"],
|
||||
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
|
||||
Fish.son_of_crimsonfish: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.radioactive_carp: ["Beach Bridge", "Rusty Key", "Boat Repair", "Island West Turtle", "Qi Walnut Room"],
|
||||
Fish.glacierfish_jr: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
}
|
||||
self.original_state = self.multiworld.state.copy()
|
||||
for fish in fish_and_items:
|
||||
with self.subTest(f"Region rules for {fish}"):
|
||||
self.collect_all_the_money()
|
||||
item_names = fish_and_items[fish]
|
||||
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
items = []
|
||||
for item_name in item_names:
|
||||
items.append(self.collect(item_name))
|
||||
with self.subTest(f"{fish} can be reached with {item_names}"):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
for item_required in items:
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
with self.subTest(f"{fish} requires {item_required.name}"):
|
||||
for item_to_collect in items:
|
||||
if item_to_collect.name != item_required.name:
|
||||
self.collect(item_to_collect)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
@@ -339,7 +339,8 @@ class TunicWorld(World):
|
||||
except KeyError:
|
||||
# logic bug, proceed with warning since it takes a long time to update AP
|
||||
warning(f"{location.name} is not logically accessible for {self.player_name}. "
|
||||
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.")
|
||||
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. "
|
||||
"If you are using Plando Items (excluding early locations), then this is likely the cause.")
|
||||
hint_text = "Inaccessible"
|
||||
else:
|
||||
while connection != ("Menu", None):
|
||||
|
||||
@@ -204,8 +204,11 @@ class WitnessWorld(World):
|
||||
]
|
||||
if early_items:
|
||||
random_early_item = self.random.choice(early_items)
|
||||
if self.options.puzzle_randomization == "sigma_expert" or self.options.victory_condition == "panel_hunt":
|
||||
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
||||
if (
|
||||
self.options.puzzle_randomization == "sigma_expert"
|
||||
or self.options.victory_condition == "panel_hunt"
|
||||
):
|
||||
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
|
||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||
else:
|
||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||
|
||||
@@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
|
||||
Door - 0x09FEE (Light Room Entry) - 0x0C339
|
||||
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
|
||||
Laser - 0x012FB (Laser) - 0x03608
|
||||
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
|
||||
159020 - 0x3351D (Sand Snake EP) - True - True
|
||||
159030 - 0x0053C (Facade Right EP) - True - True
|
||||
159031 - 0x00771 (Facade Left EP) - True - True
|
||||
@@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
|
||||
159739 - 0x00367 (Obelisk) - True - True
|
||||
|
||||
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
|
||||
158612 - 0x17C42 (Discard) - True - Triangles
|
||||
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
|
||||
Door - 0x00085 (Vault Door) - 0x002A6
|
||||
|
||||
@@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
|
||||
Door - 0x09FEE (Light Room Entry) - 0x0C339
|
||||
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
|
||||
Laser - 0x012FB (Laser) - 0x03608
|
||||
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
|
||||
159020 - 0x3351D (Sand Snake EP) - True - True
|
||||
159030 - 0x0053C (Facade Right EP) - True - True
|
||||
159031 - 0x00771 (Facade Left EP) - True - True
|
||||
@@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
|
||||
159739 - 0x00367 (Obelisk) - True - True
|
||||
|
||||
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
|
||||
158612 - 0x17C42 (Discard) - True - Arrows
|
||||
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Squares & Triangles & Stars & Stars + Same Colored Symbol
|
||||
Door - 0x00085 (Vault Door) - 0x002A6
|
||||
|
||||
@@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
|
||||
Door - 0x09FEE (Light Room Entry) - 0x0C339
|
||||
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
|
||||
Laser - 0x012FB (Laser) - 0x03608
|
||||
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
|
||||
159020 - 0x3351D (Sand Snake EP) - True - True
|
||||
159030 - 0x0053C (Facade Right EP) - True - True
|
||||
159031 - 0x00771 (Facade Left EP) - True - True
|
||||
@@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
|
||||
159739 - 0x00367 (Obelisk) - True - True
|
||||
|
||||
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
|
||||
158612 - 0x17C42 (Discard) - True - Triangles
|
||||
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares
|
||||
Door - 0x00085 (Vault Door) - 0x002A6
|
||||
|
||||
@@ -712,8 +712,7 @@ def get_compact_hint_args(hint: WitnessWordedHint, local_player_number: int) ->
|
||||
if hint.vague_location_hint and location.player == local_player_number:
|
||||
assert hint.area is not None # A local vague location hint should have an area argument
|
||||
return location.address, "containing_area:" + hint.area
|
||||
else:
|
||||
return location.address, location.player # Scouting does not matter for other players (currently)
|
||||
return location.address, location.player # Scouting does not matter for other players (currently)
|
||||
|
||||
# Is junk / undefined hint
|
||||
return -1, local_player_number
|
||||
|
||||
@@ -42,7 +42,7 @@ class WitnessPlayerItems:
|
||||
player_locations: WitnessPlayerLocations) -> None:
|
||||
"""Adds event items after logic changes due to options"""
|
||||
|
||||
self._world: "WitnessWorld" = world
|
||||
self._world: WitnessWorld = world
|
||||
self._multiworld: MultiWorld = world.multiworld
|
||||
self._player_id: int = world.player
|
||||
self._logic: WitnessPlayerLogic = player_logic
|
||||
|
||||
@@ -116,18 +116,19 @@ class WitnessPlayerLogic:
|
||||
self.HUNT_ENTITIES: Set[str] = set()
|
||||
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
"0x00509": "+1 Laser (Symmetry Laser)",
|
||||
"0x012FB": "+1 Laser (Desert Laser)",
|
||||
"0x00509": "+1 Laser",
|
||||
"0x012FB": "+1 Laser (Unredirected)",
|
||||
"0x09F98": "Desert Laser Redirection",
|
||||
"0x01539": "+1 Laser (Quarry Laser)",
|
||||
"0x181B3": "+1 Laser (Shadows Laser)",
|
||||
"0x014BB": "+1 Laser (Keep Laser)",
|
||||
"0x17C65": "+1 Laser (Monastery Laser)",
|
||||
"0x032F9": "+1 Laser (Town Laser)",
|
||||
"0x00274": "+1 Laser (Jungle Laser)",
|
||||
"0x0C2B2": "+1 Laser (Bunker Laser)",
|
||||
"0x00BF6": "+1 Laser (Swamp Laser)",
|
||||
"0x028A4": "+1 Laser (Treehouse Laser)",
|
||||
"0xFFD03": "+1 Laser (Redirected)",
|
||||
"0x01539": "+1 Laser",
|
||||
"0x181B3": "+1 Laser",
|
||||
"0x014BB": "+1 Laser",
|
||||
"0x17C65": "+1 Laser",
|
||||
"0x032F9": "+1 Laser",
|
||||
"0x00274": "+1 Laser",
|
||||
"0x0C2B2": "+1 Laser",
|
||||
"0x00BF6": "+1 Laser",
|
||||
"0x028A4": "+1 Laser",
|
||||
"0x17C34": "Mountain Entry",
|
||||
"0xFFF00": "Bottom Floor Discard Turns On",
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ Defines Region for The Witness, assigns locations to them,
|
||||
and connects them with the proper requirements
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, Tuple
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Region
|
||||
|
||||
@@ -38,7 +38,7 @@ class WitnessPlayerRegions:
|
||||
self.created_region_names: Set[str] = set()
|
||||
|
||||
@staticmethod
|
||||
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
||||
def make_lambda(item_requirement: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
|
||||
from .rules import _meets_item_requirements
|
||||
|
||||
"""
|
||||
@@ -79,7 +79,9 @@ class WitnessPlayerRegions:
|
||||
source_region
|
||||
)
|
||||
|
||||
connection.access_rule = self.make_lambda(final_requirement, world)
|
||||
rule = self.make_lambda(final_requirement, world)
|
||||
if rule is not None:
|
||||
connection.access_rule = rule
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
Defines the rules by which locations can be accessed,
|
||||
depending on the items received
|
||||
"""
|
||||
from typing import TYPE_CHECKING
|
||||
from collections import Counter
|
||||
from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Union
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
@@ -15,50 +16,22 @@ from .player_logic import WitnessPlayerLogic
|
||||
if TYPE_CHECKING:
|
||||
from . import WitnessWorld
|
||||
|
||||
laser_hexes = [
|
||||
"0x028A4",
|
||||
"0x00274",
|
||||
"0x032F9",
|
||||
"0x01539",
|
||||
"0x181B3",
|
||||
"0x0C2B2",
|
||||
"0x00509",
|
||||
"0x00BF6",
|
||||
"0x014BB",
|
||||
"0x012FB",
|
||||
"0x17C65",
|
||||
]
|
||||
|
||||
class SimpleItemRepresentation(NamedTuple):
|
||||
item_name: str
|
||||
item_count: int
|
||||
|
||||
|
||||
def _can_do_panel_hunt(world: "WitnessWorld") -> CollectionRule:
|
||||
def _can_do_panel_hunt(world: "WitnessWorld") -> SimpleItemRepresentation:
|
||||
required = world.panel_hunt_required_count
|
||||
player = world.player
|
||||
return lambda state: state.has("+1 Panel Hunt", player, required)
|
||||
|
||||
|
||||
def _has_laser(laser_hex: str, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
|
||||
player = world.player
|
||||
laser_name = static_witness_logic.ENTITIES_BY_HEX[laser_hex]["checkName"]
|
||||
|
||||
# Workaround for intentional naming inconsistency
|
||||
if laser_name == "Symmetry Island Laser":
|
||||
laser_name = "Symmetry Laser"
|
||||
|
||||
if laser_hex == "0x012FB" and redirect_required:
|
||||
return lambda state: state.has_all([f"+1 Laser ({laser_name})", "Desert Laser Redirection"], player)
|
||||
|
||||
return lambda state: state.has(f"+1 Laser ({laser_name})", player)
|
||||
return SimpleItemRepresentation("+1 Panel Hunt", required)
|
||||
|
||||
|
||||
def _has_lasers(amount: int, world: "WitnessWorld", redirect_required: bool) -> CollectionRule:
|
||||
laser_lambdas = []
|
||||
if redirect_required:
|
||||
return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Redirected)"], world.player, amount)
|
||||
|
||||
for laser_hex in laser_hexes:
|
||||
has_laser_lambda = _has_laser(laser_hex, world, redirect_required)
|
||||
|
||||
laser_lambdas.append(has_laser_lambda)
|
||||
|
||||
return lambda state: sum(laser_lambda(state) for laser_lambda in laser_lambdas) >= amount
|
||||
return lambda state: state.has_from_list(["+1 Laser", "+1 Laser (Unredirected)"], world.player, amount)
|
||||
|
||||
|
||||
def _can_do_expert_pp2(state: CollectionState, world: "WitnessWorld") -> bool:
|
||||
@@ -196,7 +169,13 @@ def _can_do_theater_to_tunnels(state: CollectionState, world: "WitnessWorld") ->
|
||||
)
|
||||
|
||||
|
||||
def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: WitnessPlayerLogic) -> CollectionRule:
|
||||
def _has_item(item: str, world: "WitnessWorld",
|
||||
player_logic: WitnessPlayerLogic) -> Union[CollectionRule, SimpleItemRepresentation]:
|
||||
"""
|
||||
Convert a single element of a WitnessRule into a CollectionRule, unless it is referring to an item,
|
||||
in which case we return it as an item-count pair ("SimpleItemRepresentation"). This allows some optimisation later.
|
||||
"""
|
||||
|
||||
assert item not in static_witness_logic.ENTITIES_BY_HEX, "Requirements can no longer contain entity hexes directly."
|
||||
|
||||
if item in player_logic.REFERENCE_LOGIC.ALL_REGIONS_BY_NAME:
|
||||
@@ -223,27 +202,90 @@ def _has_item(item: str, world: "WitnessWorld", player: int, player_logic: Witne
|
||||
return lambda state: _can_do_theater_to_tunnels(state, world)
|
||||
|
||||
prog_item = static_witness_logic.get_parent_progressive_item(item)
|
||||
return lambda state: state.has(prog_item, player, player_logic.MULTI_AMOUNTS[item])
|
||||
needed_amount = player_logic.MULTI_AMOUNTS[item]
|
||||
|
||||
simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount)
|
||||
return simple_rule
|
||||
|
||||
|
||||
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> CollectionRule:
|
||||
def optimize_requirement_option(requirement_option: List[Union[CollectionRule, SimpleItemRepresentation]])\
|
||||
-> List[Union[CollectionRule, SimpleItemRepresentation]]:
|
||||
"""
|
||||
Checks whether item and panel requirements are met for
|
||||
a panel
|
||||
This optimises out a requirement like [("Progressive Dots": 1), ("Progressive Dots": 2)] to only the "2" version.
|
||||
"""
|
||||
|
||||
lambda_conversion = [
|
||||
[_has_item(item, world, world.player, world.player_logic) for item in subset]
|
||||
direct_items = [rule for rule in requirement_option if isinstance(rule, tuple)]
|
||||
if not direct_items:
|
||||
return requirement_option
|
||||
|
||||
max_per_item: Dict[str, int] = Counter()
|
||||
for item_rule in direct_items:
|
||||
max_per_item[item_rule[0]] = max(max_per_item[item_rule[0]], item_rule[1])
|
||||
|
||||
return [
|
||||
rule for rule in requirement_option
|
||||
if not (isinstance(rule, tuple) and rule[1] < max_per_item[rule[0]])
|
||||
]
|
||||
|
||||
|
||||
def convert_requirement_option(requirement: List[Union[CollectionRule, SimpleItemRepresentation]],
|
||||
player: int) -> List[CollectionRule]:
|
||||
"""
|
||||
Converts a list of CollectionRules and SimpleItemRepresentations to just a list of CollectionRules.
|
||||
If the list is ONLY SimpleItemRepresentations, we can just return a CollectionRule based on state.has_all_counts()
|
||||
"""
|
||||
converted_sublist = []
|
||||
|
||||
for rule in requirement:
|
||||
if not isinstance(rule, tuple):
|
||||
converted_sublist.append(rule)
|
||||
continue
|
||||
|
||||
collection_rules = [rule for rule in requirement if not isinstance(rule, SimpleItemRepresentation)]
|
||||
item_rules = [rule for rule in requirement if isinstance(rule, SimpleItemRepresentation)]
|
||||
|
||||
if len(item_rules) == 0:
|
||||
item_rules_converted = []
|
||||
elif len(item_rules) == 1:
|
||||
item = item_rules[0][0]
|
||||
count = item_rules[0][1]
|
||||
item_rules_converted = [lambda state: state.has(item, player, count)]
|
||||
else:
|
||||
item_counts = {item_rule.item_name: item_rule.item_count for item_rule in item_rules}
|
||||
item_rules_converted = [lambda state: state.has_all_counts(item_counts, player)]
|
||||
|
||||
return collection_rules + item_rules_converted
|
||||
|
||||
|
||||
def _meets_item_requirements(requirements: WitnessRule, world: "WitnessWorld") -> Optional[CollectionRule]:
|
||||
"""
|
||||
Converts a WitnessRule into a CollectionRule.
|
||||
"""
|
||||
player = world.player
|
||||
|
||||
if requirements == frozenset({frozenset()}):
|
||||
return None
|
||||
|
||||
rule_conversion = [
|
||||
[_has_item(item, world, world.player_logic) for item in subset]
|
||||
for subset in requirements
|
||||
]
|
||||
|
||||
optimized_rule_conversion = [optimize_requirement_option(sublist) for sublist in rule_conversion]
|
||||
|
||||
fully_converted_rules = [convert_requirement_option(sublist, player) for sublist in optimized_rule_conversion]
|
||||
|
||||
if len(fully_converted_rules) == 1:
|
||||
if len(fully_converted_rules[0]) == 1:
|
||||
return fully_converted_rules[0][0]
|
||||
return lambda state: all(condition(state) for condition in fully_converted_rules[0])
|
||||
return lambda state: any(
|
||||
all(condition(state) for condition in sub_requirement)
|
||||
for sub_requirement in lambda_conversion
|
||||
for sub_requirement in fully_converted_rules
|
||||
)
|
||||
|
||||
|
||||
def make_lambda(entity_hex: str, world: "WitnessWorld") -> CollectionRule:
|
||||
def make_lambda(entity_hex: str, world: "WitnessWorld") -> Optional[CollectionRule]:
|
||||
"""
|
||||
Lambdas are created in a for loop so values need to be captured
|
||||
"""
|
||||
@@ -268,6 +310,8 @@ def set_rules(world: "WitnessWorld") -> None:
|
||||
entity_hex = associated_entity["entity_hex"]
|
||||
|
||||
rule = make_lambda(entity_hex, world)
|
||||
if rule is None:
|
||||
continue
|
||||
|
||||
location = world.get_location(location)
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from test.bases import WorldTestBase
|
||||
from test.general import gen_steps, setup_multiworld
|
||||
from test.multiworld.test_multiworlds import MultiworldTestBase
|
||||
from typing import Any, ClassVar, Dict, Iterable, List, Mapping, Union, cast
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, Location, Region
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
from test.general import gen_steps, setup_multiworld
|
||||
from test.multiworld.test_multiworlds import MultiworldTestBase
|
||||
|
||||
from .. import WitnessWorld
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from BaseClasses import CollectionState, Item
|
||||
from worlds.witness.test import WitnessTestBase, WitnessMultiworldTestBase
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
from worlds.witness.test import WitnessMultiworldTestBase, WitnessTestBase
|
||||
|
||||
|
||||
class TestMaxPanelHuntMinChecks(WitnessTestBase):
|
||||
@@ -13,7 +14,7 @@ class TestMaxPanelHuntMinChecks(WitnessTestBase):
|
||||
"shuffle_vault_boxes": False,
|
||||
}
|
||||
|
||||
def test_correct_panels_were_picked(self):
|
||||
def test_correct_panels_were_picked(self) -> None:
|
||||
with self.subTest("Check that 100 Hunt Panels were actually picked."):
|
||||
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", self.player)), 100)
|
||||
|
||||
@@ -63,45 +64,45 @@ class TestPanelHuntPostgame(WitnessMultiworldTestBase):
|
||||
"shuffle_discarded_panels": True,
|
||||
}
|
||||
|
||||
def test_panel_hunt_postgame(self):
|
||||
def test_panel_hunt_postgame(self) -> None:
|
||||
for player_minus_one, options in enumerate(self.options_per_world):
|
||||
player = player_minus_one + 1
|
||||
postgame_option = options["panel_hunt_postgame"]
|
||||
with self.subTest(f"Test that \"{postgame_option}\" results in 40 Hunt Panels."):
|
||||
with self.subTest(f'Test that "{postgame_option}" results in 40 Hunt Panels.'):
|
||||
self.assertEqual(len(self.multiworld.find_item_locations("+1 Panel Hunt", player)), 40)
|
||||
|
||||
# Test that the box gets extra checks from panel_hunt_postgame
|
||||
|
||||
with self.subTest("Test that \"everything_is_eligible\" has no Mountaintop Box Hunt Panels."):
|
||||
with self.subTest('Test that "everything_is_eligible" has no Mountaintop Box Hunt Panels.'):
|
||||
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 1, strict_check=False)
|
||||
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 1, strict_check=False)
|
||||
|
||||
with self.subTest("Test that \"disable_mountain_lasers_locations\" has a Hunt Panel for Short, but not Long."):
|
||||
with self.subTest('Test that "disable_mountain_lasers_locations" has a Hunt Panel for Short, but not Long.'):
|
||||
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 2, strict_check=False)
|
||||
self.assert_location_does_not_exist("Mountaintop Box Long (Panel Hunt)", 2, strict_check=False)
|
||||
|
||||
with self.subTest("Test that \"disable_challenge_lasers_locations\" has a Hunt Panel for Long, but not Short."):
|
||||
with self.subTest('Test that "disable_challenge_lasers_locations" has a Hunt Panel for Long, but not Short.'):
|
||||
self.assert_location_does_not_exist("Mountaintop Box Short (Panel Hunt)", 3, strict_check=False)
|
||||
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 3, strict_check=False)
|
||||
|
||||
with self.subTest("Test that \"disable_anything_locked_by_lasers\" has both Mountaintop Box Hunt Panels."):
|
||||
with self.subTest('Test that "disable_anything_locked_by_lasers" has both Mountaintop Box Hunt Panels.'):
|
||||
self.assert_location_exists("Mountaintop Box Short (Panel Hunt)", 4, strict_check=False)
|
||||
self.assert_location_exists("Mountaintop Box Long (Panel Hunt)", 4, strict_check=False)
|
||||
|
||||
# Check panel_hunt_postgame locations get disabled
|
||||
|
||||
with self.subTest("Test that \"everything_is_eligible\" does not disable any locked-by-lasers panels."):
|
||||
with self.subTest('Test that "everything_is_eligible" does not disable any locked-by-lasers panels.'):
|
||||
self.assert_location_exists("Mountain Floor 1 Right Row 5", 1)
|
||||
self.assert_location_exists("Mountain Bottom Floor Discard", 1)
|
||||
|
||||
with self.subTest("Test that \"disable_mountain_lasers_locations\" disables only Shortbox-Locked panels."):
|
||||
with self.subTest('Test that "disable_mountain_lasers_locations" disables only Shortbox-Locked panels.'):
|
||||
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 2)
|
||||
self.assert_location_exists("Mountain Bottom Floor Discard", 2)
|
||||
|
||||
with self.subTest("Test that \"disable_challenge_lasers_locations\" disables only Longbox-Locked panels."):
|
||||
with self.subTest('Test that "disable_challenge_lasers_locations" disables only Longbox-Locked panels.'):
|
||||
self.assert_location_exists("Mountain Floor 1 Right Row 5", 3)
|
||||
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 3)
|
||||
|
||||
with self.subTest("Test that \"everything_is_eligible\" disables only Shortbox-Locked panels."):
|
||||
with self.subTest('Test that "everything_is_eligible" disables only Shortbox-Locked panels.'):
|
||||
self.assert_location_does_not_exist("Mountain Floor 1 Right Row 5", 4)
|
||||
self.assert_location_does_not_exist("Mountain Bottom Floor Discard", 4)
|
||||
|
||||
Reference in New Issue
Block a user