Merge branch 'ArchipelagoMW:main' into main

This commit is contained in:
CookieCat
2023-10-26 21:50:08 -04:00
committed by GitHub
29 changed files with 342 additions and 125 deletions

View File

@@ -328,11 +328,6 @@ class MultiWorld():
""" the base name (without file extension) for each player's output file for a seed """
return f"AP_{self.seed_name}_P{player}_{self.get_file_safe_player_name(player).replace(' ', '_')}"
def initialize_regions(self, regions=None):
for region in regions if regions else self.regions:
region.multiworld = self
self._region_cache[region.player][region.name] = region
@functools.cached_property
def world_name_lookup(self):
return {self.player_name[player_id]: player_id for player_id in self.player_ids}

View File

@@ -67,14 +67,23 @@ def update(yes=False, force=False):
install_pkg_resources(yes=yes)
import pkg_resources
prev = "" # if a line ends in \ we store here and merge later
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
for line in requirementsfile:
if not line or line[0] == "#":
continue # ignore comments
if not line or line.lstrip(" \t")[0] == "#":
if not prev:
continue # ignore comments
line = ""
elif line.rstrip("\r\n").endswith("\\"):
prev = prev + line.rstrip("\r\n")[:-1] + " " # continue on next line
continue
line = prev + line
line = line.split("--hash=")[0] # remove hashes from requirement for version checking
prev = ""
if line.startswith(("https://", "git+https://")):
# extract name and version for url
rest = line.split('/')[-1]

View File

@@ -950,7 +950,10 @@ class CommonOptions(metaclass=OptionsMetaProperty):
else:
raise ValueError(f"{casing} is invalid casing for as_dict. "
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
option_results[display_name] = getattr(self, option_name).value
value = getattr(self, option_name).value
if isinstance(value, set):
value = sorted(value)
option_results[display_name] = value
else:
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
return option_results

View File

@@ -1,4 +1,4 @@
[pytest]
python_files = Test*.py
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
python_classes = Test
python_functions = test
python_functions = test

View File

@@ -477,8 +477,6 @@ def create_inverted_regions(world, player):
create_lw_region(world, player, 'Death Mountain Bunny Descent Area')
]
world.initialize_regions()
def mark_dark_world_regions(world, player):
# cross world caves may have some sections marked as both in_light_world, and in_dark_work.

View File

@@ -535,8 +535,6 @@ def set_up_take_anys(world, player):
take_any.shop.add_inventory(0, 'Blue Potion', 0, 0)
take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0, create_location=True)
world.initialize_regions()
def get_pool_core(world, player: int):
shuffle = world.shuffle[player]

View File

@@ -102,7 +102,7 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
'Red Pendant': ItemData(IC.progression, 'Crystal', (0x01, 0x32, 0x60, 0x00, 0x69, 0x03), None, None, None, None, None, None, "the red pendant"),
'Triforce': ItemData(IC.progression, None, 0x6A, '\n YOU WIN!', 'and the triforce', 'victorious kid', 'victory for sale', 'fungus for the win', 'greedy boy wins game again', 'the Triforce'),
'Power Star': ItemData(IC.progression, None, 0x6B, 'a small victory', 'and the power star', 'star-struck kid', 'star for sale', 'see stars with shroom', 'mario powers up again', 'a Power Star'),
'Triforce Piece': ItemData(IC.progression, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
'Triforce Piece': ItemData(IC.progression_skip_balancing, None, 0x6C, 'a small victory', 'and the thirdforce', 'triangular kid', 'triangle for sale', 'fungus for triangle', 'wise boy has triangle again', 'a Triforce Piece'),
'Crystal 1': ItemData(IC.progression, 'Crystal', (0x02, 0x34, 0x64, 0x40, 0x7F, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 2': ItemData(IC.progression, 'Crystal', (0x10, 0x34, 0x64, 0x40, 0x79, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Crystal 3': ItemData(IC.progression, 'Crystal', (0x40, 0x34, 0x64, 0x40, 0x6C, 0x06), None, None, None, None, None, None, "a blue crystal"),

View File

@@ -382,8 +382,6 @@ def create_regions(world, player):
create_dw_region(world, player, 'Dark Death Mountain Bunny Descent Area')
]
world.initialize_regions()
def create_lw_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
return _create_region(world, player, name, LTTPRegionType.LightWorld, 'Light World', locations, exits)

View File

@@ -23,13 +23,13 @@ def create_regions(world: MultiWorld, player: int):
entrance_map = {
"Level 1": lambda state:
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 9),
state.has("Booster Bumper", player, 1) and state.has("Treasure Bumper", player, 8),
"Level 2": lambda state:
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 17),
state.has("Booster Bumper", player, 2) and state.has("Treasure Bumper", player, 16),
"Level 3": lambda state:
state.has("Booster Bumper", player, 4) and state.has("Treasure Bumper", player, 25),
state.has("Booster Bumper", player, 3) and state.has("Treasure Bumper", player, 24),
"Level 4": lambda state:
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 33)
state.has("Booster Bumper", player, 5) and state.has("Treasure Bumper", player, 32)
}
for x, region_name in enumerate(region_map):

View File

@@ -108,7 +108,7 @@ class BumpStikWorld(World):
item_pool += self._create_item_in_quantities(
name, frequencies[i])
item_delta = len(location_table) - len(item_pool) - 1
item_delta = len(location_table) - len(item_pool)
if item_delta > 0:
item_pool += self._create_item_in_quantities(
"Score Bonus", item_delta)
@@ -116,13 +116,16 @@ class BumpStikWorld(World):
self.multiworld.itempool += item_pool
def set_rules(self):
forbid_item(self.multiworld.get_location("Bonus Booster 5", self.player),
"Booster Bumper", self.player)
def generate_basic(self):
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).place_locked_item(
self.create_item(self.get_filler_item_name()))
for x in range(1, 32):
self.multiworld.get_location(f"Treasure Bumper {x + 1}", self.player).access_rule = \
lambda state, x = x: state.has("Treasure Bumper", self.player, x)
for x in range(1, 5):
self.multiworld.get_location(f"Bonus Booster {x + 1}", self.player).access_rule = \
lambda state, x = x: state.has("Booster Bumper", self.player, x)
self.multiworld.get_location("Level 5 - Cleared all Hazards", self.player).access_rule = \
lambda state: state.has("Hazard Bumper", self.player, 25)
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Booster Bumper", self.player, 5) and \
state.has("Treasure Bumper", self.player, 32)

View File

@@ -0,0 +1,39 @@
from . import BumpStikTestBase
class TestRuleLogic(BumpStikTestBase):
def testLogic(self):
for x in range(1, 33):
if x == 32:
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
self.collect(self.get_item_by_name("Treasure Bumper"))
if x % 8 == 0:
bb_count = round(x / 8)
if bb_count < 4:
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 1}"))
elif bb_count == 4:
bb_count += 1
for y in range(self.count("Booster Bumper"), bb_count):
self.assertTrue(self.can_reach_location(f"Bonus Booster {y + 1}"),
f"BB {y + 1} check not reachable with {self.count('Booster Bumper')} BBs")
if y < 4:
self.assertFalse(self.can_reach_location(f"Bonus Booster {y + 2}"),
f"BB {y + 2} check reachable with {self.count('Treasure Bumper')} TBs")
self.collect(self.get_item_by_name("Booster Bumper"))
if x < 31:
self.assertFalse(self.can_reach_location(f"Treasure Bumper {x + 2}"))
elif x == 31:
self.assertFalse(self.can_reach_location("Level 5 - 50,000+ Total Points"))
if x < 32:
self.assertTrue(self.can_reach_location(f"Treasure Bumper {x + 1}"),
f"TB {x + 1} check not reachable with {self.count('Treasure Bumper')} TBs")
elif x == 32:
self.assertTrue(self.can_reach_location("Level 5 - 50,000+ Total Points"))
self.assertFalse(self.can_reach_location("Level 5 - Cleared all Hazards"))
self.collect(self.get_items_by_name("Hazard Bumper"))
self.assertTrue(self.can_reach_location("Level 5 - Cleared all Hazards"))

View File

@@ -0,0 +1,5 @@
from test.TestBase import WorldTestBase
class BumpStikTestBase(WorldTestBase):
game = "Bumper Stickers"

View File

@@ -11,6 +11,8 @@ from . import Options, data
class DLCQuestItem(Item):
game: str = "DLCQuest"
coins: int = 0
coin_suffix: str = ""
offset = 120_000

View File

@@ -23,7 +23,10 @@ def add_coin(region: Region, coin: int, player: int, suffix: str):
location_coin = f"{region.name}{suffix}"
location = DLCQuestLocation(player, location_coin, None, region)
region.locations.append(location)
location.place_locked_item(create_event(player, number_coin))
event = create_event(player, number_coin)
event.coins = coin
event.coin_suffix = suffix
location.place_locked_item(event)
def create_regions(multiworld: MultiWorld, player: int, world_options: Options.DLCQuestOptions):

View File

@@ -7,41 +7,25 @@ from . import Options
from .Items import DLCQuestItem
def create_event(player, event: str):
def create_event(player, event: str) -> DLCQuestItem:
return DLCQuestItem(event, ItemClassification.progression, None, player)
def has_enough_coin(player: int, coin: int):
return lambda state: state.prog_items[" coins", player] >= coin
def has_enough_coin_freemium(player: int, coin: int):
return lambda state: state.prog_items[" coins freemium", player] >= coin
def set_rules(world, player, World_Options: Options.DLCQuestOptions):
def has_enough_coin(player: int, coin: int):
def has_coin(state, player: int, coins: int):
coin_possessed = 0
for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]:
name_coin = f"{i} coins"
if state.has(name_coin, player):
coin_possessed += i
return coin_possessed >= coins
return lambda state: has_coin(state, player, coin)
def has_enough_coin_freemium(player: int, coin: int):
def has_coin(state, player: int, coins: int):
coin_possessed = 0
for i in [20, 50, 90, 95, 130, 150, 154, 200]:
name_coin = f"{i} coins freemium"
if state.has(name_coin, player):
coin_possessed += i
return coin_possessed >= coins
return lambda state: has_coin(state, player, coin)
set_basic_rules(World_Options, has_enough_coin, player, world)
set_lfod_rules(World_Options, has_enough_coin_freemium, player, world)
set_basic_rules(World_Options, player, world)
set_lfod_rules(World_Options, player, world)
set_completion_condition(World_Options, player, world)
def set_basic_rules(World_Options, has_enough_coin, player, world):
def set_basic_rules(World_Options, player, world):
if World_Options.campaign == Options.Campaign.option_live_freemium_or_die:
return
set_basic_entrance_rules(player, world)
@@ -49,8 +33,8 @@ def set_basic_rules(World_Options, has_enough_coin, player, world):
set_basic_shuffled_items_rules(World_Options, player, world)
set_double_jump_glitchless_rules(World_Options, player, world)
set_easy_double_jump_glitch_rules(World_Options, player, world)
self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world)
set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world)
self_basic_coinsanity_funded_purchase_rules(World_Options, player, world)
set_basic_self_funded_purchase_rules(World_Options, player, world)
self_basic_win_condition(World_Options, player, world)
@@ -131,7 +115,7 @@ def set_easy_double_jump_glitch_rules(World_Options, player, world):
lambda state: state.has("Double Jump Pack", player))
def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world):
def self_basic_coinsanity_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(825 / World_Options.coinbundlequantity)
@@ -194,7 +178,7 @@ def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin,
math.ceil(5 / World_Options.coinbundlequantity)))
def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world):
def set_basic_self_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_none:
return
set_rule(world.get_location("Movement Pack", player),
@@ -241,14 +225,14 @@ def self_basic_win_condition(World_Options, player, world):
player))
def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world):
def set_lfod_rules(World_Options, player, world):
if World_Options.campaign == Options.Campaign.option_basic:
return
set_lfod_entrance_rules(player, world)
set_boss_door_requirements_rules(player, world)
set_lfod_self_obtained_items_rules(World_Options, player, world)
set_lfod_shuffled_items_rules(World_Options, player, world)
self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world)
set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world)
@@ -327,7 +311,7 @@ def set_lfod_shuffled_items_rules(World_Options, player, world):
lambda state: state.can_reach("Cut Content", 'region', player))
def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world):
def self_lfod_coinsanity_funded_purchase_rules(World_Options, player, world):
if World_Options.coinsanity != Options.CoinSanity.option_coin:
return
number_of_bundle = math.floor(889 / World_Options.coinbundlequantity)

View File

@@ -1,6 +1,6 @@
from typing import Union
from BaseClasses import Tutorial
from BaseClasses import Tutorial, CollectionState
from worlds.AutoWorld import WebWorld, World
from . import Options
from .Items import DLCQuestItem, ItemData, create_items, item_table
@@ -71,7 +71,6 @@ class DLCqworld(World):
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
self.multiworld.push_precollected(self.create_item("Movement Pack"))
def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem:
if isinstance(item, str):
item = item_table[item]
@@ -87,3 +86,19 @@ class DLCqworld(World):
"seed": self.random.randrange(99999999)
})
return options_dict
def collect(self, state: CollectionState, item: DLCQuestItem) -> bool:
change = super().collect(state, item)
if change:
suffix = item.coin_suffix
if suffix:
state.prog_items[suffix, self.player] += item.coins
return change
def remove(self, state: CollectionState, item: DLCQuestItem) -> bool:
change = super().remove(state, item)
if change:
suffix = item.coin_suffix
if suffix:
state.prog_items[suffix, self.player] -= item.coins
return change

View File

@@ -82,9 +82,7 @@ class MessengerWorld(World):
self.shop_prices, self.figurine_prices = shuffle_shop_prices(self)
def create_regions(self) -> None:
for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]:
if region.name in REGION_CONNECTIONS:
region.add_exits(REGION_CONNECTIONS[region.name])
self.multiworld.regions += [MessengerRegion(reg_name, self) for reg_name in REGIONS]
def create_items(self) -> None:
# create items that are always in the item pool
@@ -138,6 +136,8 @@ class MessengerWorld(World):
self.multiworld.itempool += itempool
def set_rules(self) -> None:
for reg_name, connections in REGION_CONNECTIONS.items():
self.multiworld.get_region(reg_name, self.player).add_exits(connections)
logic = self.options.logic_level
if logic == Logic.option_normal:
MessengerRules(self).set_messenger_rules()

View File

@@ -32,7 +32,6 @@ class MessengerRegion(Region):
loc_dict = {loc: world.location_name_to_id[loc] if loc in world.location_name_to_id else None
for loc in locations}
self.add_locations(loc_dict, MessengerLocation)
world.multiworld.regions.append(self)
class MessengerLocation(Location):

View File

@@ -3,7 +3,7 @@ from typing import Protocol, Set
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from . import pyevermizer
from .Options import EnergyCore
from .Options import EnergyCore, OutOfBounds, SequenceBreaks
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
@@ -61,4 +61,10 @@ class SecretOfEvermoreLogic(LogicMixin):
if w.energy_core == EnergyCore.option_fragments:
progress = pyevermizer.P_CORE_FRAGMENT
count = w.required_fragments
elif progress == pyevermizer.P_ALLOW_OOB:
if world.worlds[player].out_of_bounds == OutOfBounds.option_logic:
return True
elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic:
return True
return self._soe_count(progress, world, player, count) >= count

View File

@@ -38,6 +38,12 @@ class OffOnFullChoice(Choice):
alias_chaos = 2
class OffOnLogicChoice(Choice):
option_off = 0
option_on = 1
option_logic = 2
# actual options
class Difficulty(EvermizerFlags, Choice):
"""Changes relative spell cost and stuff"""
@@ -93,10 +99,18 @@ class ExpModifier(Range):
default = 200
class FixSequence(EvermizerFlag, DefaultOnToggle):
"""Fix some sequence breaks"""
display_name = "Fix Sequence"
flag = '1'
class SequenceBreaks(EvermizerFlags, OffOnLogicChoice):
"""Disable, enable some sequence breaks or put them in logic"""
display_name = "Sequence Breaks"
default = 0
flags = ['', 'j', 'J']
class OutOfBounds(EvermizerFlags, OffOnLogicChoice):
"""Disable, enable the out-of-bounds glitch or put it in logic"""
display_name = "Out Of Bounds"
default = 0
flags = ['', 'u', 'U']
class FixCheats(EvermizerFlag, DefaultOnToggle):
@@ -240,7 +254,8 @@ soe_options: typing.Dict[str, AssembleOptions] = {
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
"fix_sequence": FixSequence,
"sequence_breaks": SequenceBreaks,
"out_of_bounds": OutOfBounds,
"fix_cheats": FixCheats,
"fix_infinite_ammo": FixInfiniteAmmo,
"fix_atlas_glitch": FixAtlasGlitch,

View File

@@ -10,12 +10,8 @@ from worlds.generic.Rules import add_item_rule, set_rule
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
try:
import pyevermizer # from package
except ImportError:
import traceback
traceback.print_exc()
from . import pyevermizer # as part of the source tree
import pyevermizer # from package
# from . import pyevermizer # as part of the source tree
from . import Logic # load logic mixin
from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
@@ -179,6 +175,8 @@ class SoEWorld(World):
evermizer_seed: int
connect_name: str
energy_core: int
sequence_breaks: int
out_of_bounds: int
available_fragments: int
required_fragments: int
@@ -191,6 +189,8 @@ class SoEWorld(World):
def generate_early(self) -> None:
# store option values that change logic
self.energy_core = self.multiworld.energy_core[self.player].value
self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value
self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value
self.required_fragments = self.multiworld.required_fragments[self.player].value
if self.required_fragments > self.multiworld.available_fragments[self.player].value:
self.multiworld.available_fragments[self.player].value = self.required_fragments
@@ -224,9 +224,8 @@ class SoEWorld(World):
max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
# TODO: generate *some* regions from locations' requirements?
r = Region('Menu', self.player, self.multiworld)
r.exits = [Entrance(self.player, 'New Game', r)]
self.multiworld.regions += [r]
menu = Region('Menu', self.player, self.multiworld)
self.multiworld.regions += [menu]
def get_sphere_index(evermizer_loc):
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
@@ -234,11 +233,14 @@ class SoEWorld(World):
return 2
return min(2, len(evermizer_loc.requires))
# create ingame region
ingame = Region('Ingame', self.player, self.multiworld)
# group locations into spheres (1, 2, 3+ at index 0, 1, 2)
spheres: typing.Dict[int, typing.Dict[int, typing.List[SoELocation]]] = {}
for loc in _locations:
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], r,
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
loc.difficulty > max_difficulty))
# location balancing data
@@ -280,18 +282,16 @@ class SoEWorld(World):
late_locations = self.multiworld.random.sample(late_bosses, late_count)
# add locations to the world
r = Region('Ingame', self.player, self.multiworld)
for sphere in spheres.values():
for locations in sphere.values():
for location in locations:
r.locations.append(location)
ingame.locations.append(location)
if location.name in late_locations:
location.progress_type = LocationProgressType.PRIORITY
r.locations.append(SoELocation(self.player, 'Done', None, r))
self.multiworld.regions += [r]
self.multiworld.get_entrance('New Game', self.player).connect(self.multiworld.get_region('Ingame', self.player))
ingame.locations.append(SoELocation(self.player, 'Done', None, ingame))
menu.connect(ingame, "New Game")
self.multiworld.regions += [ingame]
def create_items(self):
# add regular items to the pool

View File

@@ -1,18 +1,36 @@
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.8'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.9'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.10'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-win_amd64.whl#0.44.0 ; sys_platform == 'win32' and platform_machine == 'AMD64' and python_version == '3.11'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.8'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.9'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.10'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.11'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.8'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.9'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.10'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl#0.44.0 ; sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.11'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp38-cp38-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.8'
#pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_x86_64.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp39-cp39-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.9'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp310-cp310-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.10'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-cp311-cp311-macosx_10_9_universal2.whl#0.44.0 ; sys_platform == 'darwin' and python_version == '3.11'
pyevermizer @ https://github.com/black-sliver/pyevermizer/releases/download/v0.44.0/pyevermizer-0.44.0-1.tar.gz#0.44.0 ; python_version < '3.8' or python_version > '3.11' or (sys_platform != 'win32' and sys_platform != 'linux' and sys_platform != 'darwin') or (platform_machine != 'AMD64' and platform_machine != 'x86_64' and platform_machine != 'aarch64' and platform_machine != 'universal2' and platform_machine != 'arm64')
pyevermizer==0.46.1 \
--hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \
--hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \
--hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \
--hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \
--hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \
--hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \
--hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \
--hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \
--hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \
--hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \
--hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \
--hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \
--hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \
--hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \
--hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \
--hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \
--hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \
--hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \
--hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \
--hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \
--hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \
--hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \
--hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \
--hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \
--hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \
--hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \
--hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \
--hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \
--hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \
--hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \
--hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \
--hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \
--hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \
--hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \
--hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae

View File

@@ -1,5 +1,20 @@
from test.TestBase import WorldTestBase
from typing import Iterable
class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
satisfied=True) -> None:
"""
Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
"""
for location in reachable:
self.assertEqual(self.can_reach_location(location), satisfied,
f"{location} is unreachable but should be" if satisfied else
f"{location} is reachable but shouldn't be")
for location in unreachable:
self.assertFalse(self.can_reach_location(location),
f"{location} is reachable but shouldn't be")

View File

@@ -0,0 +1,51 @@
import typing
from . import SoETestBase
class OoBTest(SoETestBase):
"""Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
def testOoBAccess(self):
in_logic = self.options["out_of_bounds"] == "logic"
# some locations that just need a weapon + OoB
oob_reachable = [
"Aquagoth", "Sons of Sth.", "Mad Monk", "Magmar", # OoB can use volcano shop to skip rock skip
"Levitate", "Fireball", "Drain", "Speed",
"E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
]
# some locations that should still be unreachable
oob_unreachable = [
"Tiny", "Rimsala",
"Barrier", "Call Up", "Reflect", "Force Field", "Stop", # Stop guy doesn't spawn for the other entrances
"Pyramid bottom #118", "Tiny's hideout #160", "Tiny's hideout #161", "Greenhouse #275",
]
# OoB + Diamond Eyes
de_reachable = [
"Tiny's hideout #160",
]
# still unreachable
de_unreachable = [
"Tiny",
"Tiny's hideout #161",
]
self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=False)
self.collect_by_name("Gladiator Sword")
self.assertLocationReachability(reachable=oob_reachable, unreachable=oob_unreachable, satisfied=in_logic)
self.collect_by_name("Diamond Eye")
self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
def testOoBGoal(self):
# still need Energy Core with OoB if sequence breaks are not in logic
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
self.collect_by_name(item)
self.assertBeatable(False)
self.collect_by_name("Energy Core")
self.assertBeatable(True)
class OoBInLogicTest(OoBTest):
"""Tests that stuff that should be reachable/unreachable with out-of-bounds actually is."""
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "logic"}

View File

@@ -0,0 +1,45 @@
import typing
from . import SoETestBase
class SequenceBreaksTest(SoETestBase):
"""Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
def testSequenceBreaksAccess(self):
in_logic = self.options["sequence_breaks"] == "logic"
# some locations that just need any weapon + sequence break
break_reachable = [
"Sons of Sth.", "Mad Monk", "Magmar",
"Fireball",
"Volcano Room1 #73", "Pyramid top #135",
]
# some locations that should still be unreachable
break_unreachable = [
"Aquagoth", "Megataur", "Tiny", "Rimsala",
"Barrier", "Call Up", "Levitate", "Stop", "Drain", "Escape",
"Greenhouse #275", "E. Crustacia #107", "Energy Core #285", "Vanilla Gauge #57",
]
self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=False)
self.collect_by_name("Gladiator Sword")
self.assertLocationReachability(reachable=break_reachable, unreachable=break_unreachable, satisfied=in_logic)
self.collect_by_name("Spider Claw") # Gauge now just needs non-sword
self.assertEqual(self.can_reach_location("Vanilla Gauge #57"), in_logic)
self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
self.assertEqual(self.can_reach_location("Escape"), in_logic)
def testSequenceBreaksGoal(self):
in_logic = self.options["sequence_breaks"] == "logic"
# don't need Energy Core with sequence breaks in logic
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
self.assertBeatable(False)
self.collect_by_name(item)
self.assertBeatable(in_logic)
class SequenceBreaksInLogicTest(SequenceBreaksTest):
"""Tests that stuff that should be reachable/unreachable with sequence breaks actually is."""
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "logic"}

View File

@@ -65,22 +65,38 @@ class SubnauticaWorld(World):
creature_pool, self.options.creature_scans.value)
def create_regions(self):
self.multiworld.regions += [
self.create_region("Menu", None, ["Lifepod 5"]),
self.create_region("Planet 4546B",
locations.events +
[location["name"] for location in locations.location_table.values()] +
[creature + creatures.suffix for creature in self.creatures_to_scan])
]
# Create Regions
menu_region = Region("Menu", self.player, self.multiworld)
planet_region = Region("Planet 4546B", self.player, self.multiworld)
# Link regions
self.multiworld.get_entrance("Lifepod 5", self.player).connect(self.multiworld.get_region("Planet 4546B", self.player))
# Link regions together
menu_region.connect(planet_region, "Lifepod 5")
# Create regular locations
location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
(creature + creatures.suffix for creature in self.creatures_to_scan))
for location_name in location_names:
loc_id = self.location_name_to_id[location_name]
location = SubnauticaLocation(self.player, location_name, loc_id, planet_region)
planet_region.locations.append(location)
# Create events
goal_event_name = self.options.goal.get_event_name()
for event in locations.events:
self.multiworld.get_location(event, self.player).place_locked_item(
location = SubnauticaLocation(self.player, event, None, planet_region)
planet_region.locations.append(location)
location.place_locked_item(
SubnauticaItem(event, ItemClassification.progression, None, player=self.player))
# make the goal event the victory "item"
self.multiworld.get_location(self.options.goal.get_event_name(), self.player).item.name = "Victory"
if event == goal_event_name:
# make the goal event the victory "item"
location.item.name = "Victory"
# Register regions to multiworld
self.multiworld.regions += [
menu_region,
planet_region
]
# refer to Rules.py
set_rules = set_rules

View File

@@ -305,7 +305,7 @@ Hydraulic Volt Crusher; Calamity;
Life Fruit; ; (@mech_boss(1) & Wall of Flesh) | (@calamity & (Living Shard | Wall of Flesh));
Get a Life; Achievement; Life Fruit;
Topped Off; Achievement; Life Fruit;
Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & (@mech_boss(1) | #Old One's Army Tier 3);
Old One's Army Tier 2; Location | Item; #Old One's Army Tier 1 & ((Wall of Flesh & @mech_boss(1)) | #Old One's Army Tier 3);
// Brimstone Elemental
Infernal Suevite; Calamity; @pickaxe(150) | Brimstone Elemental;
@@ -410,7 +410,7 @@ Scoria Bar; Calamity;
Seismic Hampick; Calamity | Pickaxe(210) | Hammer(95); Hardmode Anvil & Scoria Bar;
Life Alloy; Calamity; (Hardmode Anvil & Cryonic Bar & Perennial Bar & Scoria Bar) | Necromantic Geode;
Advanced Display; Calamity; Hardmode Anvil & Mysterious Circuitry & Dubious Plating & Life Alloy & Long Ranged Sensor Array;
Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Golem;
Old One's Army Tier 3; Location | Item; #Old One's Army Tier 1 & Wall of Flesh & Golem;
// Martian Madness
Martian Madness; Location | Item; Wall of Flesh & Golem;