mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-28 02:33:31 -07:00
Merge branch 'main' into main
This commit is contained in:
30
.github/labeler.yml
vendored
Normal file
30
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
'is: documentation':
|
||||
- changed-files:
|
||||
- all-globs-to-all-files: '{**/docs/**,**/README.md}'
|
||||
|
||||
'affects: webhost':
|
||||
- changed-files:
|
||||
- all-globs-to-any-file: 'WebHost.py'
|
||||
- all-globs-to-any-file: 'WebHostLib/**/*'
|
||||
|
||||
'affects: core':
|
||||
- changed-files:
|
||||
- all-globs-to-any-file:
|
||||
- '!*Client.py'
|
||||
- '!README.md'
|
||||
- '!LICENSE'
|
||||
- '!*.yml'
|
||||
- '!.gitignore'
|
||||
- '!**/docs/**'
|
||||
- '!typings/kivy/**'
|
||||
- '!test/**'
|
||||
- '!data/**'
|
||||
- '!.run/**'
|
||||
- '!.github/**'
|
||||
- '!worlds_disabled/**'
|
||||
- '!worlds/**'
|
||||
- '!WebHost.py'
|
||||
- '!WebHostLib/**'
|
||||
- any-glob-to-any-file: # exceptions to the above rules of "stuff that isn't core"
|
||||
- 'worlds/generic/**/*.py'
|
||||
- 'CommonClient.py'
|
||||
44
.github/workflows/label-pull-requests.yml
vendored
Normal file
44
.github/workflows/label-pull-requests.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Label Pull Request
|
||||
on:
|
||||
pull_request_target:
|
||||
types: ['opened', 'reopened', 'synchronize', 'ready_for_review', 'converted_to_draft', 'closed']
|
||||
branches: ['main']
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
name: 'Apply content-based labels'
|
||||
if: github.event.action == 'opened' || github.event.action == 'reopened' || github.event.action == 'synchronize'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
sync-labels: true
|
||||
peer_review:
|
||||
name: 'Apply peer review label'
|
||||
if: >-
|
||||
(github.event.action == 'opened' || github.event.action == 'reopened' ||
|
||||
github.event.action == 'ready_for_review') && !github.event.pull_request.draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Add label'
|
||||
run: "gh pr edit \"$PR_URL\" --add-label 'waiting-on: peer-review'"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
unblock_draft_prs:
|
||||
name: 'Remove waiting-on labels'
|
||||
if: github.event.action == 'converted_to_draft' || github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Remove labels'
|
||||
run: |-
|
||||
gh pr edit "$PR_URL" --remove-label 'waiting-on: peer-review' \
|
||||
--remove-label 'waiting-on: core-review' \
|
||||
--remove-label 'waiting-on: world-maintainer' \
|
||||
--remove-label 'waiting-on: author'
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -572,9 +572,10 @@ class MultiWorld():
|
||||
|
||||
def location_condition(location: Location):
|
||||
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
|
||||
if location.player in players["minimal"]:
|
||||
return False
|
||||
return True
|
||||
if location.player in players["locations"] or (location.item and location.item.player not in
|
||||
players["minimal"]):
|
||||
return True
|
||||
return False
|
||||
|
||||
def location_relevant(location: Location):
|
||||
"""Determine if this location is relevant to sweep."""
|
||||
@@ -823,8 +824,8 @@ class Entrance:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
world = self.parent_region.multiworld if self.parent_region else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
class Region:
|
||||
@@ -1040,8 +1041,8 @@ class Location:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
world = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
@@ -1175,7 +1176,7 @@ class Spoiler:
|
||||
{"player": player, "entrance": entrance, "exit": exit_, "direction": direction}
|
||||
|
||||
def create_playthrough(self, create_paths: bool = True) -> None:
|
||||
"""Destructive to the world while it is run, damage gets repaired afterwards."""
|
||||
"""Destructive to the multiworld while it is run, damage gets repaired afterwards."""
|
||||
from itertools import chain
|
||||
# get locations containing progress items
|
||||
multiworld = self.multiworld
|
||||
|
||||
186
Fill.py
186
Fill.py
@@ -27,12 +27,12 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
return new_state
|
||||
|
||||
|
||||
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
|
||||
"""
|
||||
:param world: Multiworld to be filled.
|
||||
:param multiworld: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
:param locations: Locations to be filled with item_pool
|
||||
:param item_pool: Items to fill into the locations
|
||||
@@ -68,7 +68,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items)
|
||||
|
||||
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
|
||||
has_beaten_game = multiworld.has_beaten_game(maximum_exploration_state)
|
||||
|
||||
while items_to_place:
|
||||
# if we have run out of locations to fill,break out of this loop
|
||||
@@ -80,8 +80,8 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
spot_to_fill: typing.Optional[Location] = None
|
||||
|
||||
# if minimal accessibility, only check whether location is reachable if game not beatable
|
||||
if world.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
|
||||
if multiworld.worlds[item_to_place.player].options.accessibility == Accessibility.option_minimal:
|
||||
perform_access_check = not multiworld.has_beaten_game(maximum_exploration_state,
|
||||
item_to_place.player) \
|
||||
if single_player_placement else not has_beaten_game
|
||||
else:
|
||||
@@ -122,11 +122,11 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
# Verify placing this item won't reduce available locations, which would be a useless swap.
|
||||
prev_state = swap_state.copy()
|
||||
prev_loc_count = len(
|
||||
world.get_reachable_locations(prev_state))
|
||||
multiworld.get_reachable_locations(prev_state))
|
||||
|
||||
swap_state.collect(item_to_place, True)
|
||||
new_loc_count = len(
|
||||
world.get_reachable_locations(swap_state))
|
||||
multiworld.get_reachable_locations(swap_state))
|
||||
|
||||
if new_loc_count >= prev_loc_count:
|
||||
# Add this item to the existing placement, and
|
||||
@@ -156,7 +156,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
else:
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
spot_to_fill.locked = lock
|
||||
placements.append(spot_to_fill)
|
||||
spot_to_fill.event = item_to_place.advancement
|
||||
@@ -173,7 +173,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
# validate all placements and remove invalid ones
|
||||
state = sweep_from_pool(base_state, [])
|
||||
for placement in placements:
|
||||
if world.accessibility[placement.item.player] != "minimal" and not placement.can_reach(state):
|
||||
if multiworld.worlds[placement.item.player].options.accessibility != "minimal" and not placement.can_reach(state):
|
||||
placement.item.location = None
|
||||
unplaced_items.append(placement.item)
|
||||
placement.item = None
|
||||
@@ -188,7 +188,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
if excluded_locations:
|
||||
for location in excluded_locations:
|
||||
location.progress_type = location.progress_type.DEFAULT
|
||||
fill_restrictive(world, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
fill_restrictive(multiworld, base_state, excluded_locations, unplaced_items, single_player_placement, lock,
|
||||
swap, on_place, allow_partial, False)
|
||||
for location in excluded_locations:
|
||||
if not location.item:
|
||||
@@ -196,7 +196,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
|
||||
if not allow_partial and len(unplaced_items) > 0 and len(locations) > 0:
|
||||
# There are leftover unplaceable items and locations that won't accept them
|
||||
if world.can_beat_game():
|
||||
if multiworld.can_beat_game():
|
||||
logging.warning(
|
||||
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
|
||||
else:
|
||||
@@ -206,7 +206,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations:
|
||||
item_pool.extend(unplaced_items)
|
||||
|
||||
|
||||
def remaining_fill(world: MultiWorld,
|
||||
def remaining_fill(multiworld: MultiWorld,
|
||||
locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> None:
|
||||
unplaced_items: typing.List[Item] = []
|
||||
@@ -261,7 +261,7 @@ def remaining_fill(world: MultiWorld,
|
||||
unplaced_items.append(item_to_place)
|
||||
continue
|
||||
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
multiworld.push_item(spot_to_fill, item_to_place, False)
|
||||
placements.append(spot_to_fill)
|
||||
placed += 1
|
||||
if not placed % 1000:
|
||||
@@ -278,19 +278,19 @@ def remaining_fill(world: MultiWorld,
|
||||
itempool.extend(unplaced_items)
|
||||
|
||||
|
||||
def fast_fill(world: MultiWorld,
|
||||
def fast_fill(multiworld: MultiWorld,
|
||||
item_pool: typing.List[Item],
|
||||
fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]:
|
||||
placing = min(len(item_pool), len(fill_locations))
|
||||
for item, location in zip(item_pool, fill_locations):
|
||||
world.push_item(location, item, False)
|
||||
multiworld.push_item(location, item, False)
|
||||
return item_pool[placing:], fill_locations[placing:]
|
||||
|
||||
|
||||
def accessibility_corrections(world: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, locations, pool=[]):
|
||||
maximum_exploration_state = sweep_from_pool(state, pool)
|
||||
minimal_players = {player for player in world.player_ids if world.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in world.get_locations() if location.player in minimal_players and
|
||||
minimal_players = {player for player in multiworld.player_ids if multiworld.worlds[player].options.accessibility == "minimal"}
|
||||
unreachable_locations = [location for location in multiworld.get_locations() if location.player in minimal_players and
|
||||
not location.can_reach(maximum_exploration_state)]
|
||||
for location in unreachable_locations:
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
@@ -304,36 +304,36 @@ def accessibility_corrections(world: MultiWorld, state: CollectionState, locatio
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
fill_restrictive(world, state, locations, pool, name="Accessibility Corrections")
|
||||
fill_restrictive(multiworld, state, locations, pool, name="Accessibility Corrections")
|
||||
|
||||
|
||||
def inaccessible_location_rules(world: MultiWorld, state: CollectionState, locations):
|
||||
def inaccessible_location_rules(multiworld: MultiWorld, state: CollectionState, locations):
|
||||
maximum_exploration_state = sweep_from_pool(state)
|
||||
unreachable_locations = [location for location in locations if not location.can_reach(maximum_exploration_state)]
|
||||
if unreachable_locations:
|
||||
def forbid_important_item_rule(item: Item):
|
||||
return not ((item.classification & 0b0011) and world.worlds[item.player].options.accessibility != 'minimal')
|
||||
return not ((item.classification & 0b0011) and multiworld.worlds[item.player].options.accessibility != 'minimal')
|
||||
|
||||
for location in unreachable_locations:
|
||||
add_item_rule(location, forbid_important_item_rule)
|
||||
|
||||
|
||||
def distribute_early_items(world: MultiWorld,
|
||||
def distribute_early_items(multiworld: MultiWorld,
|
||||
fill_locations: typing.List[Location],
|
||||
itempool: typing.List[Item]) -> typing.Tuple[typing.List[Location], typing.List[Item]]:
|
||||
""" returns new fill_locations and itempool """
|
||||
early_items_count: typing.Dict[typing.Tuple[str, int], typing.List[int]] = {}
|
||||
for player in world.player_ids:
|
||||
items = itertools.chain(world.early_items[player], world.local_early_items[player])
|
||||
for player in multiworld.player_ids:
|
||||
items = itertools.chain(multiworld.early_items[player], multiworld.local_early_items[player])
|
||||
for item in items:
|
||||
early_items_count[item, player] = [world.early_items[player].get(item, 0),
|
||||
world.local_early_items[player].get(item, 0)]
|
||||
early_items_count[item, player] = [multiworld.early_items[player].get(item, 0),
|
||||
multiworld.local_early_items[player].get(item, 0)]
|
||||
if early_items_count:
|
||||
early_locations: typing.List[Location] = []
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
loc_indexes_to_remove: typing.Set[int] = set()
|
||||
base_state = world.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in world.get_filled_locations() if loc.address is None))
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
for i, loc in enumerate(fill_locations):
|
||||
if loc.can_reach(base_state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
@@ -345,8 +345,8 @@ def distribute_early_items(world: MultiWorld,
|
||||
|
||||
early_prog_items: typing.List[Item] = []
|
||||
early_rest_items: typing.List[Item] = []
|
||||
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in world.player_ids}
|
||||
early_local_prog_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
early_local_rest_items: typing.Dict[int, typing.List[Item]] = {player: [] for player in multiworld.player_ids}
|
||||
item_indexes_to_remove: typing.Set[int] = set()
|
||||
for i, item in enumerate(itempool):
|
||||
if (item.name, item.player) in early_items_count:
|
||||
@@ -370,28 +370,28 @@ def distribute_early_items(world: MultiWorld,
|
||||
if len(early_items_count) == 0:
|
||||
break
|
||||
itempool = [item for i, item in enumerate(itempool) if i not in item_indexes_to_remove]
|
||||
for player in world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
player_local = early_local_rest_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
fill_restrictive(multiworld, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Items P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_rest_items.extend(early_local_rest_items[player])
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(world, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(multiworld, base_state, early_locations, early_rest_items, lock=True, allow_partial=True,
|
||||
name="Early Items")
|
||||
early_locations += early_priority_locations
|
||||
for player in world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
player_local = early_local_prog_items[player]
|
||||
fill_restrictive(world, base_state,
|
||||
fill_restrictive(multiworld, base_state,
|
||||
[loc for loc in early_locations if loc.player == player],
|
||||
player_local, lock=True, allow_partial=True, name=f"Local Early Progression P{player}")
|
||||
if player_local:
|
||||
logging.warning(f"Could not fulfill rules of early items: {player_local}")
|
||||
early_prog_items.extend(player_local)
|
||||
early_locations = [loc for loc in early_locations if not loc.item]
|
||||
fill_restrictive(world, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
fill_restrictive(multiworld, base_state, early_locations, early_prog_items, lock=True, allow_partial=True,
|
||||
name="Early Progression")
|
||||
unplaced_early_items = early_rest_items + early_prog_items
|
||||
if unplaced_early_items:
|
||||
@@ -400,18 +400,18 @@ def distribute_early_items(world: MultiWorld,
|
||||
itempool += unplaced_early_items
|
||||
|
||||
fill_locations.extend(early_locations)
|
||||
world.random.shuffle(fill_locations)
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
return fill_locations, itempool
|
||||
|
||||
|
||||
def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
fill_locations = sorted(world.get_unfilled_locations())
|
||||
world.random.shuffle(fill_locations)
|
||||
def distribute_items_restrictive(multiworld: MultiWorld) -> None:
|
||||
fill_locations = sorted(multiworld.get_unfilled_locations())
|
||||
multiworld.random.shuffle(fill_locations)
|
||||
# get items to distribute
|
||||
itempool = sorted(world.itempool)
|
||||
world.random.shuffle(itempool)
|
||||
itempool = sorted(multiworld.itempool)
|
||||
multiworld.random.shuffle(itempool)
|
||||
|
||||
fill_locations, itempool = distribute_early_items(world, fill_locations, itempool)
|
||||
fill_locations, itempool = distribute_early_items(multiworld, fill_locations, itempool)
|
||||
|
||||
progitempool: typing.List[Item] = []
|
||||
usefulitempool: typing.List[Item] = []
|
||||
@@ -425,7 +425,7 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
else:
|
||||
filleritempool.append(item)
|
||||
|
||||
call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
call_all(multiworld, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations)
|
||||
|
||||
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
|
||||
loc_type: [] for loc_type in LocationProgressType}
|
||||
@@ -446,34 +446,34 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(world, world.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, swap=False, on_place=mark_for_locking,
|
||||
name="Priority")
|
||||
accessibility_corrections(world, world.state, prioritylocations, progitempool)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
fill_restrictive(world, world.state, defaultlocations, progitempool, name="Progression")
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
|
||||
if progitempool:
|
||||
raise FillError(
|
||||
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
|
||||
accessibility_corrections(world, world.state, defaultlocations)
|
||||
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
for location in lock_later:
|
||||
if location.item:
|
||||
location.locked = True
|
||||
del mark_for_locking, lock_later
|
||||
|
||||
inaccessible_location_rules(world, world.state, defaultlocations)
|
||||
inaccessible_location_rules(multiworld, multiworld.state, defaultlocations)
|
||||
|
||||
remaining_fill(world, excludedlocations, filleritempool)
|
||||
remaining_fill(multiworld, excludedlocations, filleritempool)
|
||||
if excludedlocations:
|
||||
raise FillError(
|
||||
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
|
||||
|
||||
restitempool = filleritempool + usefulitempool
|
||||
|
||||
remaining_fill(world, defaultlocations, restitempool)
|
||||
remaining_fill(multiworld, defaultlocations, restitempool)
|
||||
|
||||
unplaced = restitempool
|
||||
unfilled = defaultlocations
|
||||
@@ -481,40 +481,40 @@ def distribute_items_restrictive(world: MultiWorld) -> None:
|
||||
if unplaced or unfilled:
|
||||
logging.warning(
|
||||
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
|
||||
items_counter = Counter(location.item.player for location in world.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in world.get_locations())
|
||||
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
|
||||
locations_counter = Counter(location.player for location in multiworld.get_locations())
|
||||
items_counter.update(item.player for item in unplaced)
|
||||
locations_counter.update(location.player for location in unfilled)
|
||||
print_data = {"items": items_counter, "locations": locations_counter}
|
||||
logging.info(f'Per-Player counts: {print_data})')
|
||||
|
||||
|
||||
def flood_items(world: MultiWorld) -> None:
|
||||
def flood_items(multiworld: MultiWorld) -> None:
|
||||
# get items to distribute
|
||||
world.random.shuffle(world.itempool)
|
||||
itempool = world.itempool
|
||||
multiworld.random.shuffle(multiworld.itempool)
|
||||
itempool = multiworld.itempool
|
||||
progress_done = False
|
||||
|
||||
# sweep once to pick up preplaced items
|
||||
world.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
|
||||
# fill world from top of itempool while we can
|
||||
# fill multiworld from top of itempool while we can
|
||||
while not progress_done:
|
||||
location_list = world.get_unfilled_locations()
|
||||
world.random.shuffle(location_list)
|
||||
location_list = multiworld.get_unfilled_locations()
|
||||
multiworld.random.shuffle(location_list)
|
||||
spot_to_fill = None
|
||||
for location in location_list:
|
||||
if location.can_fill(world.state, itempool[0]):
|
||||
if location.can_fill(multiworld.state, itempool[0]):
|
||||
spot_to_fill = location
|
||||
break
|
||||
|
||||
if spot_to_fill:
|
||||
item = itempool.pop(0)
|
||||
world.push_item(spot_to_fill, item, True)
|
||||
multiworld.push_item(spot_to_fill, item, True)
|
||||
continue
|
||||
|
||||
# ran out of spots, check if we need to step in and correct things
|
||||
if len(world.get_reachable_locations()) == len(world.get_locations()):
|
||||
if len(multiworld.get_reachable_locations()) == len(multiworld.get_locations()):
|
||||
progress_done = True
|
||||
continue
|
||||
|
||||
@@ -524,7 +524,7 @@ def flood_items(world: MultiWorld) -> None:
|
||||
for item in itempool:
|
||||
if item.advancement:
|
||||
candidate_item_to_place = item
|
||||
if world.unlocks_new_location(item):
|
||||
if multiworld.unlocks_new_location(item):
|
||||
item_to_place = item
|
||||
break
|
||||
|
||||
@@ -537,15 +537,15 @@ def flood_items(world: MultiWorld) -> None:
|
||||
raise FillError('No more progress items left to place.')
|
||||
|
||||
# find item to replace with progress item
|
||||
location_list = world.get_reachable_locations()
|
||||
world.random.shuffle(location_list)
|
||||
location_list = multiworld.get_reachable_locations()
|
||||
multiworld.random.shuffle(location_list)
|
||||
for location in location_list:
|
||||
if location.item is not None and not location.item.advancement:
|
||||
# safe to replace
|
||||
replace_item = location.item
|
||||
replace_item.location = None
|
||||
itempool.append(replace_item)
|
||||
world.push_item(location, item_to_place, True)
|
||||
multiworld.push_item(location, item_to_place, True)
|
||||
itempool.remove(item_to_place)
|
||||
break
|
||||
|
||||
@@ -755,7 +755,7 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked:
|
||||
location_1.event, location_2.event = location_2.event, location_1.event
|
||||
|
||||
|
||||
def distribute_planned(world: MultiWorld) -> None:
|
||||
def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
def warn(warning: str, force: typing.Union[bool, str]) -> None:
|
||||
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
|
||||
logging.warning(f'{warning}')
|
||||
@@ -768,24 +768,24 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
else:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = world.state.copy()
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_events()
|
||||
reachable = frozenset(world.get_reachable_locations(swept_state))
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
for loc in world.get_unfilled_locations():
|
||||
for loc in multiworld.get_unfilled_locations():
|
||||
if loc in reachable:
|
||||
early_locations[loc.player].append(loc.name)
|
||||
else: # not reachable with swept state
|
||||
non_early_locations[loc.player].append(loc.name)
|
||||
|
||||
world_name_lookup = world.world_name_lookup
|
||||
world_name_lookup = multiworld.world_name_lookup
|
||||
|
||||
block_value = typing.Union[typing.List[str], typing.Dict[str, typing.Any], str]
|
||||
plando_blocks: typing.List[typing.Dict[str, typing.Any]] = []
|
||||
player_ids = set(world.player_ids)
|
||||
player_ids = set(multiworld.player_ids)
|
||||
for player in player_ids:
|
||||
for block in world.plando_items[player]:
|
||||
for block in multiworld.plando_items[player]:
|
||||
block['player'] = player
|
||||
if 'force' not in block:
|
||||
block['force'] = 'silent'
|
||||
@@ -799,12 +799,12 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
else:
|
||||
target_world = block['world']
|
||||
|
||||
if target_world is False or world.players == 1: # target own world
|
||||
if target_world is False or multiworld.players == 1: # target own world
|
||||
worlds: typing.Set[int] = {player}
|
||||
elif target_world is True: # target any worlds besides own
|
||||
worlds = set(world.player_ids) - {player}
|
||||
worlds = set(multiworld.player_ids) - {player}
|
||||
elif target_world is None: # target all worlds
|
||||
worlds = set(world.player_ids)
|
||||
worlds = set(multiworld.player_ids)
|
||||
elif type(target_world) == list: # list of target worlds
|
||||
worlds = set()
|
||||
for listed_world in target_world:
|
||||
@@ -814,9 +814,9 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
continue
|
||||
worlds.add(world_name_lookup[listed_world])
|
||||
elif type(target_world) == int: # target world by slot number
|
||||
if target_world not in range(1, world.players + 1):
|
||||
if target_world not in range(1, multiworld.players + 1):
|
||||
failed(
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
|
||||
f"Cannot place item in world {target_world} as it is not in range of (1, {multiworld.players})",
|
||||
block['force'])
|
||||
continue
|
||||
worlds = {target_world}
|
||||
@@ -844,7 +844,7 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
item_list: typing.List[str] = []
|
||||
for key, value in items.items():
|
||||
if value is True:
|
||||
value = world.itempool.count(world.worlds[player].create_item(key))
|
||||
value = multiworld.itempool.count(multiworld.worlds[player].create_item(key))
|
||||
item_list += [key] * value
|
||||
items = item_list
|
||||
if isinstance(items, str):
|
||||
@@ -894,17 +894,17 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
count = block['count']
|
||||
failed(f"Plando count {count} greater than locations specified", block['force'])
|
||||
block['count'] = len(block['locations'])
|
||||
block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
|
||||
block['count']['target'] = multiworld.random.randint(block['count']['min'], block['count']['max'])
|
||||
|
||||
if block['count']['target'] > 0:
|
||||
plando_blocks.append(block)
|
||||
|
||||
# shuffle, but then sort blocks by number of locations minus number of items,
|
||||
# so less-flexible blocks get priority
|
||||
world.random.shuffle(plando_blocks)
|
||||
multiworld.random.shuffle(plando_blocks)
|
||||
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
|
||||
if len(block['locations']) > 0
|
||||
else len(world.get_unfilled_locations(player)) - block['count']['target']))
|
||||
else len(multiworld.get_unfilled_locations(player)) - block['count']['target']))
|
||||
|
||||
for placement in plando_blocks:
|
||||
player = placement['player']
|
||||
@@ -915,19 +915,19 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
maxcount = placement['count']['target']
|
||||
from_pool = placement['from_pool']
|
||||
|
||||
candidates = list(world.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
world.random.shuffle(candidates)
|
||||
world.random.shuffle(items)
|
||||
candidates = list(multiworld.get_unfilled_locations_for_players(locations, sorted(worlds)))
|
||||
multiworld.random.shuffle(candidates)
|
||||
multiworld.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
|
||||
for item_name in items:
|
||||
item = world.worlds[player].create_item(item_name)
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
for location in reversed(candidates):
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(world.state, item, False):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
@@ -945,21 +945,21 @@ def distribute_planned(world: MultiWorld) -> None:
|
||||
if count < placement['count']['min']:
|
||||
m = placement['count']['min']
|
||||
failed(
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {' '.join(err)}",
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
world.push_item(location, item, collect=False)
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.event = True # flag location to be checked during fill
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
try:
|
||||
world.itempool.remove(item)
|
||||
multiworld.itempool.remove(item)
|
||||
except ValueError:
|
||||
warn(
|
||||
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
|
||||
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
f"Error running plando for player {player} ({world.player_name[player]})") from e
|
||||
f"Error running plando for player {player} ({multiworld.player_name[player]})") from e
|
||||
|
||||
@@ -348,7 +348,8 @@ class LinksAwakeningClient():
|
||||
await asyncio.sleep(1.0)
|
||||
continue
|
||||
self.stop_bizhawk_spam = False
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii')} running {rom_name.decode('ascii')}")
|
||||
logger.info(f"Connected to Retroarch {version.decode('ascii', errors='replace')} "
|
||||
f"running {rom_name.decode('ascii', errors='replace')}")
|
||||
return
|
||||
except (BlockingIOError, TimeoutError, ConnectionResetError):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
282
Main.py
282
Main.py
@@ -30,49 +30,49 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
output_path.cached_path = args.outputpath
|
||||
|
||||
start = time.perf_counter()
|
||||
# initialize the world
|
||||
world = MultiWorld(args.multi)
|
||||
# initialize the multiworld
|
||||
multiworld = MultiWorld(args.multi)
|
||||
|
||||
logger = logging.getLogger()
|
||||
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
world.plando_options = args.plando_options
|
||||
multiworld.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
|
||||
multiworld.plando_options = args.plando_options
|
||||
|
||||
world.shuffle = args.shuffle.copy()
|
||||
world.logic = args.logic.copy()
|
||||
world.mode = args.mode.copy()
|
||||
world.difficulty = args.difficulty.copy()
|
||||
world.item_functionality = args.item_functionality.copy()
|
||||
world.timer = args.timer.copy()
|
||||
world.goal = args.goal.copy()
|
||||
world.boss_shuffle = args.shufflebosses.copy()
|
||||
world.enemy_health = args.enemy_health.copy()
|
||||
world.enemy_damage = args.enemy_damage.copy()
|
||||
world.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
world.countdown_start_time = args.countdown_start_time.copy()
|
||||
world.red_clock_time = args.red_clock_time.copy()
|
||||
world.blue_clock_time = args.blue_clock_time.copy()
|
||||
world.green_clock_time = args.green_clock_time.copy()
|
||||
world.dungeon_counters = args.dungeon_counters.copy()
|
||||
world.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
world.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
world.shop_shuffle = args.shop_shuffle.copy()
|
||||
world.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
world.sprite_pool = args.sprite_pool.copy()
|
||||
world.dark_room_logic = args.dark_room_logic.copy()
|
||||
world.plando_items = args.plando_items.copy()
|
||||
world.plando_texts = args.plando_texts.copy()
|
||||
world.plando_connections = args.plando_connections.copy()
|
||||
world.required_medallions = args.required_medallions.copy()
|
||||
world.game = args.game.copy()
|
||||
world.player_name = args.name.copy()
|
||||
world.sprite = args.sprite.copy()
|
||||
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
multiworld.shuffle = args.shuffle.copy()
|
||||
multiworld.logic = args.logic.copy()
|
||||
multiworld.mode = args.mode.copy()
|
||||
multiworld.difficulty = args.difficulty.copy()
|
||||
multiworld.item_functionality = args.item_functionality.copy()
|
||||
multiworld.timer = args.timer.copy()
|
||||
multiworld.goal = args.goal.copy()
|
||||
multiworld.boss_shuffle = args.shufflebosses.copy()
|
||||
multiworld.enemy_health = args.enemy_health.copy()
|
||||
multiworld.enemy_damage = args.enemy_damage.copy()
|
||||
multiworld.beemizer_total_chance = args.beemizer_total_chance.copy()
|
||||
multiworld.beemizer_trap_chance = args.beemizer_trap_chance.copy()
|
||||
multiworld.countdown_start_time = args.countdown_start_time.copy()
|
||||
multiworld.red_clock_time = args.red_clock_time.copy()
|
||||
multiworld.blue_clock_time = args.blue_clock_time.copy()
|
||||
multiworld.green_clock_time = args.green_clock_time.copy()
|
||||
multiworld.dungeon_counters = args.dungeon_counters.copy()
|
||||
multiworld.triforce_pieces_available = args.triforce_pieces_available.copy()
|
||||
multiworld.triforce_pieces_required = args.triforce_pieces_required.copy()
|
||||
multiworld.shop_shuffle = args.shop_shuffle.copy()
|
||||
multiworld.shuffle_prizes = args.shuffle_prizes.copy()
|
||||
multiworld.sprite_pool = args.sprite_pool.copy()
|
||||
multiworld.dark_room_logic = args.dark_room_logic.copy()
|
||||
multiworld.plando_items = args.plando_items.copy()
|
||||
multiworld.plando_texts = args.plando_texts.copy()
|
||||
multiworld.plando_connections = args.plando_connections.copy()
|
||||
multiworld.required_medallions = args.required_medallions.copy()
|
||||
multiworld.game = args.game.copy()
|
||||
multiworld.player_name = args.name.copy()
|
||||
multiworld.sprite = args.sprite.copy()
|
||||
multiworld.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
|
||||
|
||||
world.set_options(args)
|
||||
world.set_item_links()
|
||||
world.state = CollectionState(world)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
|
||||
multiworld.set_options(args)
|
||||
multiworld.set_item_links()
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
|
||||
|
||||
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||
@@ -103,93 +103,93 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
AutoWorld.call_stage(world, "assert_generate")
|
||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(world, "generate_early")
|
||||
AutoWorld.call_all(multiworld, "generate_early")
|
||||
|
||||
logger.info('')
|
||||
|
||||
for player in world.player_ids:
|
||||
for item_name, count in world.worlds[player].options.start_inventory.value.items():
|
||||
for player in multiworld.player_ids:
|
||||
for item_name, count in multiworld.worlds[player].options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
|
||||
for item_name, count in getattr(world.worlds[player].options,
|
||||
for item_name, count in getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.items():
|
||||
for _ in range(count):
|
||||
world.push_precollected(world.create_item(item_name, player))
|
||||
multiworld.push_precollected(multiworld.create_item(item_name, player))
|
||||
# remove from_pool items also from early items handling, as starting is plenty early.
|
||||
early = world.early_items[player].get(item_name, 0)
|
||||
early = multiworld.early_items[player].get(item_name, 0)
|
||||
if early:
|
||||
world.early_items[player][item_name] = max(0, early-count)
|
||||
multiworld.early_items[player][item_name] = max(0, early-count)
|
||||
remaining_count = count-early
|
||||
if remaining_count > 0:
|
||||
local_early = world.early_local_items[player].get(item_name, 0)
|
||||
local_early = multiworld.early_local_items[player].get(item_name, 0)
|
||||
if local_early:
|
||||
world.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
multiworld.early_items[player][item_name] = max(0, local_early - remaining_count)
|
||||
del local_early
|
||||
del early
|
||||
|
||||
logger.info('Creating World.')
|
||||
AutoWorld.call_all(world, "create_regions")
|
||||
logger.info('Creating MultiWorld.')
|
||||
AutoWorld.call_all(multiworld, "create_regions")
|
||||
|
||||
logger.info('Creating Items.')
|
||||
AutoWorld.call_all(world, "create_items")
|
||||
AutoWorld.call_all(multiworld, "create_items")
|
||||
|
||||
logger.info('Calculating Access Rules.')
|
||||
|
||||
for player in world.player_ids:
|
||||
for player in multiworld.player_ids:
|
||||
# items can't be both local and non-local, prefer local
|
||||
world.worlds[player].options.non_local_items.value -= world.worlds[player].options.local_items.value
|
||||
world.worlds[player].options.non_local_items.value -= set(world.local_early_items[player])
|
||||
multiworld.worlds[player].options.non_local_items.value -= multiworld.worlds[player].options.local_items.value
|
||||
multiworld.worlds[player].options.non_local_items.value -= set(multiworld.local_early_items[player])
|
||||
|
||||
AutoWorld.call_all(world, "set_rules")
|
||||
AutoWorld.call_all(multiworld, "set_rules")
|
||||
|
||||
for player in world.player_ids:
|
||||
exclusion_rules(world, player, world.worlds[player].options.exclude_locations.value)
|
||||
world.worlds[player].options.priority_locations.value -= world.worlds[player].options.exclude_locations.value
|
||||
for location_name in world.worlds[player].options.priority_locations.value:
|
||||
for player in multiworld.player_ids:
|
||||
exclusion_rules(multiworld, player, multiworld.worlds[player].options.exclude_locations.value)
|
||||
multiworld.worlds[player].options.priority_locations.value -= multiworld.worlds[player].options.exclude_locations.value
|
||||
for location_name in multiworld.worlds[player].options.priority_locations.value:
|
||||
try:
|
||||
location = world.get_location(location_name, player)
|
||||
location = multiworld.get_location(location_name, player)
|
||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||
if location_name not in world.worlds[player].location_name_to_id:
|
||||
if location_name not in multiworld.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to prioritize location {location_name} in player {player}'s world.") from e
|
||||
else:
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
# Set local and non-local item rules.
|
||||
if world.players > 1:
|
||||
locality_rules(world)
|
||||
if multiworld.players > 1:
|
||||
locality_rules(multiworld)
|
||||
else:
|
||||
world.worlds[1].options.non_local_items.value = set()
|
||||
world.worlds[1].options.local_items.value = set()
|
||||
multiworld.worlds[1].options.non_local_items.value = set()
|
||||
multiworld.worlds[1].options.local_items.value = set()
|
||||
|
||||
AutoWorld.call_all(world, "generate_basic")
|
||||
AutoWorld.call_all(multiworld, "generate_basic")
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
|
||||
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
||||
new_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(world.worlds[player].options,
|
||||
player: getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in world.player_ids
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = world.worlds[player]
|
||||
player_world: AutoWorld.World = multiworld.worlds[player]
|
||||
for count in items.values():
|
||||
for _ in range(count):
|
||||
new_items.append(player_world.create_filler())
|
||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||
for i, item in enumerate(world.itempool):
|
||||
for i, item in enumerate(multiworld.itempool):
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
target -= 1
|
||||
depletion_pool[item.player][item.name] -= 1
|
||||
# quick abort if we have found all items
|
||||
if not target:
|
||||
new_items.extend(world.itempool[i+1:])
|
||||
new_items.extend(multiworld.itempool[i+1:])
|
||||
break
|
||||
else:
|
||||
new_items.append(item)
|
||||
@@ -199,19 +199,19 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for player, remaining_items in depletion_pool.items():
|
||||
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
||||
if remaining_items:
|
||||
raise Exception(f"{world.get_player_name(player)}"
|
||||
raise Exception(f"{multiworld.get_player_name(player)}"
|
||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||
assert len(world.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
world.itempool[:] = new_items
|
||||
assert len(multiworld.itempool) == len(new_items), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_items
|
||||
|
||||
# temporary home for item links, should be moved out of Main
|
||||
for group_id, group in world.groups.items():
|
||||
for group_id, group in multiworld.groups.items():
|
||||
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
|
||||
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
|
||||
]:
|
||||
classifications: Dict[str, int] = collections.defaultdict(int)
|
||||
counters = {player: {name: 0 for name in shared_pool} for player in players}
|
||||
for item in world.itempool:
|
||||
for item in multiworld.itempool:
|
||||
if item.player in counters and item.name in shared_pool:
|
||||
counters[item.player][item.name] += 1
|
||||
classifications[item.name] |= item.classification
|
||||
@@ -246,13 +246,13 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
new_item.classification |= classifications[item_name]
|
||||
new_itempool.append(new_item)
|
||||
|
||||
region = Region("Menu", group_id, world, "ItemLink")
|
||||
world.regions.append(region)
|
||||
region = Region("Menu", group_id, multiworld, "ItemLink")
|
||||
multiworld.regions.append(region)
|
||||
locations = region.locations
|
||||
for item in world.itempool:
|
||||
for item in multiworld.itempool:
|
||||
count = common_item_count.get(item.player, {}).get(item.name, 0)
|
||||
if count:
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
|
||||
loc = Location(group_id, f"Item Link: {item.name} -> {multiworld.player_name[item.player]} {count}",
|
||||
None, region)
|
||||
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
|
||||
state.has(item_name, group_id_, count_)
|
||||
@@ -263,10 +263,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
new_itempool.append(item)
|
||||
|
||||
itemcount = len(world.itempool)
|
||||
world.itempool = new_itempool
|
||||
itemcount = len(multiworld.itempool)
|
||||
multiworld.itempool = new_itempool
|
||||
|
||||
while itemcount > len(world.itempool):
|
||||
while itemcount > len(multiworld.itempool):
|
||||
items_to_add = []
|
||||
for player in group["players"]:
|
||||
if group["link_replacement"]:
|
||||
@@ -274,64 +274,64 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
else:
|
||||
item_player = player
|
||||
if group["replacement_items"][player]:
|
||||
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
|
||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_item", item_player,
|
||||
group["replacement_items"][player]))
|
||||
else:
|
||||
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
|
||||
world.random.shuffle(items_to_add)
|
||||
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
|
||||
items_to_add.append(AutoWorld.call_single(multiworld, "create_filler", item_player))
|
||||
multiworld.random.shuffle(items_to_add)
|
||||
multiworld.itempool.extend(items_to_add[:itemcount - len(multiworld.itempool)])
|
||||
|
||||
if any(world.item_links.values()):
|
||||
world._all_state = None
|
||||
if any(multiworld.item_links.values()):
|
||||
multiworld._all_state = None
|
||||
|
||||
logger.info("Running Item Plando.")
|
||||
|
||||
distribute_planned(world)
|
||||
distribute_planned(multiworld)
|
||||
|
||||
logger.info('Running Pre Main Fill.')
|
||||
|
||||
AutoWorld.call_all(world, "pre_fill")
|
||||
AutoWorld.call_all(multiworld, "pre_fill")
|
||||
|
||||
logger.info(f'Filling the world with {len(world.itempool)} items.')
|
||||
logger.info(f'Filling the multiworld with {len(multiworld.itempool)} items.')
|
||||
|
||||
if world.algorithm == 'flood':
|
||||
flood_items(world) # different algo, biased towards early game progress items
|
||||
elif world.algorithm == 'balanced':
|
||||
distribute_items_restrictive(world)
|
||||
if multiworld.algorithm == 'flood':
|
||||
flood_items(multiworld) # different algo, biased towards early game progress items
|
||||
elif multiworld.algorithm == 'balanced':
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
AutoWorld.call_all(world, 'post_fill')
|
||||
AutoWorld.call_all(multiworld, 'post_fill')
|
||||
|
||||
if world.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(world)
|
||||
if multiworld.players > 1 and not args.skip_prog_balancing:
|
||||
balance_multiworld_progression(multiworld)
|
||||
else:
|
||||
logger.info("Progression balancing skipped.")
|
||||
|
||||
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
|
||||
world.random.passthrough = False
|
||||
multiworld.random.passthrough = False
|
||||
|
||||
if args.skip_output:
|
||||
logger.info('Done. Skipped output/spoiler generation. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
return multiworld
|
||||
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + world.seed_name
|
||||
outfilebase = 'AP_' + multiworld.seed_name
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
output_players = [player for player in world.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not world.worlds[player].generate_output.__code__]
|
||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||
is not multiworld.worlds[player].generate_output.__code__]
|
||||
with concurrent.futures.ThreadPoolExecutor(len(output_players) + 2) as pool:
|
||||
check_accessibility_task = pool.submit(world.fulfills_accessibility)
|
||||
check_accessibility_task = pool.submit(multiworld.fulfills_accessibility)
|
||||
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
|
||||
output_file_futures = [pool.submit(AutoWorld.call_stage, multiworld, "generate_output", temp_dir)]
|
||||
for player in output_players:
|
||||
# skip starting a thread for methods that say "pass".
|
||||
output_file_futures.append(
|
||||
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
|
||||
pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir))
|
||||
|
||||
# collect ER hint info
|
||||
er_hint_data: Dict[int, Dict[int, str]] = {}
|
||||
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
|
||||
AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data)
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
@@ -340,38 +340,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
games = {}
|
||||
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
|
||||
slot_info = {}
|
||||
names = [[name for player, name in sorted(world.player_name.items())]]
|
||||
for slot in world.player_ids:
|
||||
player_world: AutoWorld.World = world.worlds[slot]
|
||||
names = [[name for player, name in sorted(multiworld.player_name.items())]]
|
||||
for slot in multiworld.player_ids:
|
||||
player_world: AutoWorld.World = multiworld.worlds[slot]
|
||||
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
|
||||
client_versions[slot] = player_world.required_client_version
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
|
||||
world.player_types[slot])
|
||||
for slot, group in world.groups.items():
|
||||
games[slot] = world.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
|
||||
games[slot] = multiworld.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], multiworld.game[slot],
|
||||
multiworld.player_types[slot])
|
||||
for slot, group in multiworld.groups.items():
|
||||
games[slot] = multiworld.game[slot]
|
||||
slot_info[slot] = NetUtils.NetworkSlot(group["name"], multiworld.game[slot], multiworld.player_types[slot],
|
||||
group_members=sorted(group["players"]))
|
||||
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
|
||||
for player, world_precollected in world.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
|
||||
for player, world_precollected in multiworld.precollected_items.items()}
|
||||
precollected_hints = {player: set() for player in range(1, multiworld.players + 1 + len(multiworld.groups))}
|
||||
|
||||
for slot in world.player_ids:
|
||||
slot_data[slot] = world.worlds[slot].fill_slot_data()
|
||||
for slot in multiworld.player_ids:
|
||||
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
|
||||
|
||||
def precollect_hint(location):
|
||||
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False, entrance, location.item.flags)
|
||||
precollected_hints[location.player].add(hint)
|
||||
if location.item.player not in world.groups:
|
||||
if location.item.player not in multiworld.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
else:
|
||||
for player in world.groups[location.item.player]["players"]:
|
||||
for player in multiworld.groups[location.item.player]["players"]:
|
||||
precollected_hints[player].add(hint)
|
||||
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
|
||||
for location in world.get_filled_locations():
|
||||
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids}
|
||||
for location in multiworld.get_filled_locations():
|
||||
if type(location.address) == int:
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
"location.address should then also be None. Location: " \
|
||||
@@ -381,18 +381,18 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
f"{locations_data[location.player][location.address]}")
|
||||
locations_data[location.player][location.address] = \
|
||||
location.item.code, location.item.player, location.item.flags
|
||||
if location.name in world.worlds[location.player].options.start_location_hints:
|
||||
if location.name in multiworld.worlds[location.player].options.start_location_hints:
|
||||
precollect_hint(location)
|
||||
elif location.item.name in world.worlds[location.item.player].options.start_hints:
|
||||
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
|
||||
precollect_hint(location)
|
||||
elif any([location.item.name in world.worlds[player].options.start_hints
|
||||
for player in world.groups.get(location.item.player, {}).get("players", [])]):
|
||||
elif any([location.item.name in multiworld.worlds[player].options.start_hints
|
||||
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||
for game_world in world.worlds.values()
|
||||
for game_world in multiworld.worlds.values()
|
||||
}
|
||||
|
||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||
@@ -400,7 +400,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
multidata = {
|
||||
"slot_data": slot_data,
|
||||
"slot_info": slot_info,
|
||||
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
|
||||
"connect_names": {name: (0, player) for player, name in multiworld.player_name.items()},
|
||||
"locations": locations_data,
|
||||
"checks_in_area": checks_in_area,
|
||||
"server_options": baked_server_options,
|
||||
@@ -410,10 +410,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
"version": tuple(version_tuple),
|
||||
"tags": ["AP"],
|
||||
"minimum_versions": minimum_versions,
|
||||
"seed_name": world.seed_name,
|
||||
"seed_name": multiworld.seed_name,
|
||||
"datapackage": data_package,
|
||||
}
|
||||
AutoWorld.call_all(world, "modify_multidata", multidata)
|
||||
AutoWorld.call_all(multiworld, "modify_multidata", multidata)
|
||||
|
||||
multidata = zlib.compress(pickle.dumps(multidata), 9)
|
||||
|
||||
@@ -423,7 +423,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
output_file_futures.append(pool.submit(write_multidata))
|
||||
if not check_accessibility_task.result():
|
||||
if not world.can_beat_game():
|
||||
if not multiworld.can_beat_game():
|
||||
raise Exception("Game appears as unbeatable. Aborting.")
|
||||
else:
|
||||
logger.warning("Location Accessibility requirements not fulfilled.")
|
||||
@@ -436,12 +436,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
|
||||
if args.spoiler:
|
||||
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
multiworld.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
|
||||
|
||||
zipfilename = output_path(f"AP_{world.seed_name}.zip")
|
||||
zipfilename = output_path(f"AP_{multiworld.seed_name}.zip")
|
||||
logger.info(f"Creating final archive at {zipfilename}")
|
||||
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
@@ -449,4 +449,4 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
zf.write(file.path, arcname=file.name)
|
||||
|
||||
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
|
||||
return world
|
||||
return multiworld
|
||||
|
||||
@@ -195,10 +195,10 @@ def set_icon(window):
|
||||
window.tk.call('wm', 'iconphoto', window._w, logo)
|
||||
|
||||
def adjust(args):
|
||||
# Create a fake world and OOTWorld to use as a base
|
||||
world = MultiWorld(1)
|
||||
world.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(world, 1)
|
||||
# Create a fake multiworld and OOTWorld to use as a base
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.per_slot_randoms = {1: random}
|
||||
ootworld = OOTWorld(multiworld, 1)
|
||||
# Set options in the fake OOTWorld
|
||||
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
|
||||
result = getattr(args, name, None)
|
||||
|
||||
4
Utils.py
4
Utils.py
@@ -871,8 +871,8 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
Example usage in Main code:
|
||||
from Utils import visualize_regions
|
||||
for player in world.player_ids:
|
||||
visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml")
|
||||
for player in multiworld.player_ids:
|
||||
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
|
||||
"""
|
||||
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
|
||||
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{% for patch in room.seed.slots|list|sort(attribute="player_id") %}
|
||||
<tr>
|
||||
<td>{{ patch.player_id }}</td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:None@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
|
||||
<td>{{ patch.game }}</td>
|
||||
<td>
|
||||
{% if patch.data %}
|
||||
|
||||
@@ -27,6 +27,8 @@ There are also a number of community-supported libraries available that implemen
|
||||
| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | |
|
||||
| Rust | [ArchipelagoRS](https://github.com/ryanisaacg/archipelago_rs) | |
|
||||
| Lua | [lua-apclientpp](https://github.com/black-sliver/lua-apclientpp) | |
|
||||
| Game Maker + Studio 1.x | [gm-apclientpp](https://github.com/black-sliver/gm-apclientpp) | For GM7, GM8 and GMS1.x, maybe older |
|
||||
| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -27,14 +27,15 @@ Choice, and defining `alias_true = option_full`.
|
||||
- All options support `random` as a generic option. `random` chooses from any of the available values for that option,
|
||||
and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
|
||||
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's
|
||||
create our option class (with a docstring), give it a `display_name`, and add it to our game's options dataclass:
|
||||
As an example, suppose we want an option that lets the user start their game with a sword in their inventory, an option
|
||||
to let the player choose the difficulty, and an option to choose how much health the final boss has. Let's create our
|
||||
option classes (with a docstring), give them a `display_name`, and add them to our game's options dataclass:
|
||||
|
||||
```python
|
||||
# options.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, PerGameCommonOptions
|
||||
from Options import Toggle, Range, Choice, PerGameCommonOptions
|
||||
|
||||
|
||||
class StartingSword(Toggle):
|
||||
@@ -42,13 +43,33 @@ class StartingSword(Toggle):
|
||||
display_name = "Start With Sword"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Sets overall game difficulty."""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
option_hard = 2
|
||||
alias_beginner = 0 # same as easy but allows the player to use beginner as an alternative for easy in the result in their options
|
||||
alias_expert = 2 # same as hard
|
||||
default = 1 # default to normal
|
||||
|
||||
|
||||
class FinalBossHP(Range):
|
||||
"""Sets the HP of the final boss"""
|
||||
display_name = "Final Boss HP"
|
||||
range_start = 100
|
||||
range_end = 10000
|
||||
default = 2000
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExampleGameOptions(PerGameCommonOptions):
|
||||
starting_sword: StartingSword
|
||||
difficulty: Difficulty
|
||||
final_boss_health: FinalBossHP
|
||||
```
|
||||
|
||||
This will create a `Toggle` option, internally called `starting_sword`. To then submit this to the multiworld, we add it
|
||||
to our world's `__init__.py`:
|
||||
To then submit this to the multiworld, we add it to our world's `__init__.py`:
|
||||
|
||||
```python
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* 120 character per line for all source files.
|
||||
* Avoid white space errors like trailing spaces.
|
||||
|
||||
|
||||
## Python Code
|
||||
|
||||
* We mostly follow [PEP8](https://peps.python.org/pep-0008/). Read below to see the differences.
|
||||
@@ -18,9 +17,10 @@
|
||||
* Use type annotations where possible for function signatures and class members.
|
||||
* Use type annotations where appropriate for local variables (e.g. `var: List[int] = []`, or when the
|
||||
type is hard or impossible to deduce.) Clear annotations help developers look up and validate API calls.
|
||||
* New classes, attributes, and methods in core code should have docstrings that follow
|
||||
[reST style](https://peps.python.org/pep-0287/).
|
||||
* Worlds that do not follow PEP8 should still have a consistent style across its files to make reading easier.
|
||||
|
||||
|
||||
## Markdown
|
||||
|
||||
* We almost follow [Google's styleguide](https://google.github.io/styleguide/docguide/style.html).
|
||||
@@ -30,20 +30,17 @@
|
||||
* One space between bullet/number and text.
|
||||
* No lazy numbering.
|
||||
|
||||
|
||||
## HTML
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* kebab-case for ids and classes.
|
||||
|
||||
|
||||
## CSS
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* `{` on the same line as the selector.
|
||||
* No space between selector and `{`.
|
||||
|
||||
|
||||
## JS
|
||||
|
||||
* Indent with 2 spaces.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -197,7 +197,7 @@ begin
|
||||
begin
|
||||
// Is the installed version at least the packaged one ?
|
||||
Log('VC Redist x64 Version : found ' + strVersion);
|
||||
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
|
||||
Result := (CompareStr(strVersion, 'v14.38.33130') < 0);
|
||||
end
|
||||
else
|
||||
begin
|
||||
|
||||
@@ -26,7 +26,7 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
|
||||
game: # Pick a game to play
|
||||
A Link to the Past: 1
|
||||
requires:
|
||||
version: 0.4.3 # Version of Archipelago required for this yaml to work as expected.
|
||||
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
|
||||
A Link to the Past:
|
||||
progression_balancing:
|
||||
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
|
||||
|
||||
12
setup.py
12
setup.py
@@ -349,6 +349,18 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
|
||||
for folder in sdl2.dep_bins + glew.dep_bins:
|
||||
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
|
||||
print(f"copying {folder} -> {self.libfolder}")
|
||||
# windows needs Visual Studio C++ Redistributable
|
||||
# Installer works for x64 and arm64
|
||||
print("Downloading VC Redist")
|
||||
import certifi
|
||||
import ssl
|
||||
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
||||
with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe",
|
||||
context=context) as download:
|
||||
vc_redist = download.read()
|
||||
print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", )
|
||||
with open("VC_redist.x64.exe", "wb") as vc_file:
|
||||
vc_file.write(vc_redist)
|
||||
|
||||
for data in self.extra_data:
|
||||
self.installfile(Path(data))
|
||||
|
||||
@@ -1,127 +1,7 @@
|
||||
import time
|
||||
|
||||
|
||||
class TimeIt:
|
||||
def __init__(self, name: str, time_logger=None):
|
||||
self.name = name
|
||||
self.logger = time_logger
|
||||
self.timer = None
|
||||
self.end_timer = None
|
||||
|
||||
def __enter__(self):
|
||||
self.timer = time.perf_counter()
|
||||
return self
|
||||
|
||||
@property
|
||||
def dif(self):
|
||||
return self.end_timer - self.timer
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.end_timer:
|
||||
self.end_timer = time.perf_counter()
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import logging
|
||||
import gc
|
||||
import collections
|
||||
import typing
|
||||
|
||||
# makes this module runnable from its folder.
|
||||
import sys
|
||||
import os
|
||||
sys.path.remove(os.path.dirname(__file__))
|
||||
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
||||
os.chdir(new_home)
|
||||
sys.path.append(new_home)
|
||||
|
||||
from Utils import init_logging, local_path
|
||||
local_path.cached_path = new_home
|
||||
from BaseClasses import MultiWorld, CollectionState, Location
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
|
||||
class BenchmarkRunner:
|
||||
gen_steps: typing.Tuple[str, ...] = (
|
||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
rule_iterations: int = 100_000
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
else:
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
|
||||
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
|
||||
with TimeIt(f"{test_location.game} {self.rule_iterations} "
|
||||
f"runs of {test_location}.access_rule({state_name})", logger) as t:
|
||||
for _ in range(self.rule_iterations):
|
||||
test_location.access_rule(state)
|
||||
# if time is taken to disentangle complex ref chains,
|
||||
# this time should be attributed to the rule.
|
||||
gc.collect()
|
||||
return t.dif
|
||||
|
||||
def main(self):
|
||||
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
|
||||
summary_data: typing.Dict[str, collections.Counter[str]] = {
|
||||
"empty_state": collections.Counter(),
|
||||
"all_state": collections.Counter(),
|
||||
}
|
||||
try:
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed(0)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = argparse.Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(getattr(option, "default"))
|
||||
})
|
||||
multiworld.set_options(args)
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
with TimeIt(f"{game} step {step}", logger):
|
||||
call_all(multiworld, step)
|
||||
gc.collect()
|
||||
|
||||
locations = sorted(multiworld.get_unfilled_locations())
|
||||
if not locations:
|
||||
continue
|
||||
|
||||
all_state = multiworld.get_all_state(False)
|
||||
for location in locations:
|
||||
time_taken = self.location_test(location, multiworld.state, "empty_state")
|
||||
summary_data["empty_state"][location.name] = time_taken
|
||||
|
||||
time_taken = self.location_test(location, all_state, "all_state")
|
||||
summary_data["all_state"][location.name] = time_taken
|
||||
|
||||
total_empty_state = sum(summary_data["empty_state"].values())
|
||||
total_all_state = sum(summary_data["all_state"].values())
|
||||
|
||||
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
|
||||
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
|
||||
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
|
||||
logger.info(f"Top times in empty_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['empty_state'])}")
|
||||
logger.info(f"Top times in all_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['all_state'])}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
runner = BenchmarkRunner()
|
||||
runner.main()
|
||||
import path_change
|
||||
path_change.change_home()
|
||||
import load_worlds
|
||||
load_worlds.run_load_worlds_benchmark()
|
||||
import locations
|
||||
locations.run_locations_benchmark()
|
||||
|
||||
27
test/benchmark/load_worlds.py
Normal file
27
test/benchmark/load_worlds.py
Normal file
@@ -0,0 +1,27 @@
|
||||
def run_load_worlds_benchmark():
|
||||
"""List worlds and their load time.
|
||||
Note that any first-time imports will be attributed to that world, as it is cached afterwards.
|
||||
Likely best used with isolated worlds to measure their time alone."""
|
||||
import logging
|
||||
|
||||
from Utils import init_logging
|
||||
|
||||
# get some general imports cached, to prevent it from being attributed to one world.
|
||||
import orjson
|
||||
orjson.loads("{}") # orjson runs initialization on first use
|
||||
|
||||
import BaseClasses, Launcher, Fill
|
||||
|
||||
from worlds import world_sources
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
for module in world_sources:
|
||||
logger.info(f"{module} took {module.time_taken:.4f} seconds.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from path_change import change_home
|
||||
change_home()
|
||||
run_load_worlds_benchmark()
|
||||
101
test/benchmark/locations.py
Normal file
101
test/benchmark/locations.py
Normal file
@@ -0,0 +1,101 @@
|
||||
def run_locations_benchmark():
|
||||
import argparse
|
||||
import logging
|
||||
import gc
|
||||
import collections
|
||||
import typing
|
||||
import sys
|
||||
|
||||
from time_it import TimeIt
|
||||
|
||||
from Utils import init_logging
|
||||
from BaseClasses import MultiWorld, CollectionState, Location
|
||||
from worlds import AutoWorld
|
||||
from worlds.AutoWorld import call_all
|
||||
|
||||
init_logging("Benchmark Runner")
|
||||
logger = logging.getLogger("Benchmark")
|
||||
|
||||
class BenchmarkRunner:
|
||||
gen_steps: typing.Tuple[str, ...] = (
|
||||
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
|
||||
rule_iterations: int = 100_000
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
else:
|
||||
@staticmethod
|
||||
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
|
||||
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
|
||||
|
||||
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
|
||||
with TimeIt(f"{test_location.game} {self.rule_iterations} "
|
||||
f"runs of {test_location}.access_rule({state_name})", logger) as t:
|
||||
for _ in range(self.rule_iterations):
|
||||
test_location.access_rule(state)
|
||||
# if time is taken to disentangle complex ref chains,
|
||||
# this time should be attributed to the rule.
|
||||
gc.collect()
|
||||
return t.dif
|
||||
|
||||
def main(self):
|
||||
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
|
||||
summary_data: typing.Dict[str, collections.Counter[str]] = {
|
||||
"empty_state": collections.Counter(),
|
||||
"all_state": collections.Counter(),
|
||||
}
|
||||
try:
|
||||
multiworld = MultiWorld(1)
|
||||
multiworld.game[1] = game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed(0)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = argparse.Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(getattr(option, "default"))
|
||||
})
|
||||
multiworld.set_options(args)
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
with TimeIt(f"{game} step {step}", logger):
|
||||
call_all(multiworld, step)
|
||||
gc.collect()
|
||||
|
||||
locations = sorted(multiworld.get_unfilled_locations())
|
||||
if not locations:
|
||||
continue
|
||||
|
||||
all_state = multiworld.get_all_state(False)
|
||||
for location in locations:
|
||||
time_taken = self.location_test(location, multiworld.state, "empty_state")
|
||||
summary_data["empty_state"][location.name] = time_taken
|
||||
|
||||
time_taken = self.location_test(location, all_state, "all_state")
|
||||
summary_data["all_state"][location.name] = time_taken
|
||||
|
||||
total_empty_state = sum(summary_data["empty_state"].values())
|
||||
total_all_state = sum(summary_data["all_state"].values())
|
||||
|
||||
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
|
||||
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
|
||||
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
|
||||
logger.info(f"Top times in empty_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['empty_state'])}")
|
||||
logger.info(f"Top times in all_state:\n"
|
||||
f"{self.format_times_from_counter(summary_data['all_state'])}")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
runner = BenchmarkRunner()
|
||||
runner.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from path_change import change_home
|
||||
change_home()
|
||||
run_locations_benchmark()
|
||||
16
test/benchmark/path_change.py
Normal file
16
test/benchmark/path_change.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def change_home():
|
||||
"""Allow scripts to run from "this" folder."""
|
||||
old_home = os.path.dirname(__file__)
|
||||
sys.path.remove(old_home)
|
||||
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
|
||||
os.chdir(new_home)
|
||||
sys.path.append(new_home)
|
||||
# fallback to local import
|
||||
sys.path.append(old_home)
|
||||
|
||||
from Utils import local_path
|
||||
local_path.cached_path = new_home
|
||||
23
test/benchmark/time_it.py
Normal file
23
test/benchmark/time_it.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import time
|
||||
|
||||
|
||||
class TimeIt:
|
||||
def __init__(self, name: str, time_logger=None):
|
||||
self.name = name
|
||||
self.logger = time_logger
|
||||
self.timer = None
|
||||
self.end_timer = None
|
||||
|
||||
def __enter__(self):
|
||||
self.timer = time.perf_counter()
|
||||
return self
|
||||
|
||||
@property
|
||||
def dif(self):
|
||||
return self.end_timer - self.timer
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if not self.end_timer:
|
||||
self.end_timer = time.perf_counter()
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
|
||||
@@ -11,30 +11,30 @@ from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, Item
|
||||
from worlds.generic.Rules import CollectionRule, add_item_rule, locality_rules, set_rule
|
||||
|
||||
|
||||
def generate_multi_world(players: int = 1) -> MultiWorld:
|
||||
multi_world = MultiWorld(players)
|
||||
multi_world.player_name = {}
|
||||
multi_world.state = CollectionState(multi_world)
|
||||
def generate_multiworld(players: int = 1) -> MultiWorld:
|
||||
multiworld = MultiWorld(players)
|
||||
multiworld.player_name = {}
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for i in range(players):
|
||||
player_id = i+1
|
||||
world = World(multi_world, player_id)
|
||||
multi_world.game[player_id] = f"Game {player_id}"
|
||||
multi_world.worlds[player_id] = world
|
||||
multi_world.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", player_id, multi_world, "Menu Region Hint")
|
||||
multi_world.regions.append(region)
|
||||
world = World(multiworld, player_id)
|
||||
multiworld.game[player_id] = f"Game {player_id}"
|
||||
multiworld.worlds[player_id] = world
|
||||
multiworld.player_name[player_id] = "Test Player " + str(player_id)
|
||||
region = Region("Menu", player_id, multiworld, "Menu Region Hint")
|
||||
multiworld.regions.append(region)
|
||||
for option_key, option in Options.PerGameCommonOptions.type_hints.items():
|
||||
if hasattr(multi_world, option_key):
|
||||
getattr(multi_world, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
|
||||
if hasattr(multiworld, option_key):
|
||||
getattr(multiworld, option_key).setdefault(player_id, option.from_any(getattr(option, "default")))
|
||||
else:
|
||||
setattr(multi_world, option_key, {player_id: option.from_any(getattr(option, "default"))})
|
||||
setattr(multiworld, option_key, {player_id: option.from_any(getattr(option, "default"))})
|
||||
# TODO - remove this loop once all worlds use options dataclasses
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multi_world, option_key)[player_id]
|
||||
world.options = world.options_dataclass(**{option_key: getattr(multiworld, option_key)[player_id]
|
||||
for option_key in world.options_dataclass.type_hints})
|
||||
|
||||
multi_world.set_seed(0)
|
||||
multiworld.set_seed(0)
|
||||
|
||||
return multi_world
|
||||
return multiworld
|
||||
|
||||
|
||||
class PlayerDefinition(object):
|
||||
@@ -46,8 +46,8 @@ class PlayerDefinition(object):
|
||||
basic_items: List[Item]
|
||||
regions: List[Region]
|
||||
|
||||
def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
|
||||
self.multiworld = world
|
||||
def __init__(self, multiworld: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
|
||||
self.multiworld = multiworld
|
||||
self.id = id
|
||||
self.menu = menu
|
||||
self.locations = locations
|
||||
@@ -72,7 +72,7 @@ class PlayerDefinition(object):
|
||||
return region
|
||||
|
||||
|
||||
def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
def fill_region(multiworld: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
|
||||
items = items.copy()
|
||||
while len(items) > 0:
|
||||
location = region.locations.pop(0)
|
||||
@@ -80,7 +80,7 @@ def fill_region(world: MultiWorld, region: Region, items: List[Item]) -> List[It
|
||||
if location.item:
|
||||
return items
|
||||
item = items.pop(0)
|
||||
world.push_item(location, item, False)
|
||||
multiworld.push_item(location, item, False)
|
||||
location.event = item.advancement
|
||||
|
||||
return items
|
||||
@@ -94,15 +94,15 @@ def region_contains(region: Region, item: Item) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
|
||||
menu = multi_world.get_region("Menu", player_id)
|
||||
def generate_player_data(multiworld: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
|
||||
menu = multiworld.get_region("Menu", player_id)
|
||||
locations = generate_locations(location_count, player_id, None, menu)
|
||||
prog_items = generate_items(prog_item_count, player_id, True)
|
||||
multi_world.itempool += prog_items
|
||||
multiworld.itempool += prog_items
|
||||
basic_items = generate_items(basic_item_count, player_id, False)
|
||||
multi_world.itempool += basic_items
|
||||
multiworld.itempool += basic_items
|
||||
|
||||
return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items)
|
||||
return PlayerDefinition(multiworld, player_id, menu, locations, prog_items, basic_items)
|
||||
|
||||
|
||||
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
|
||||
@@ -134,15 +134,15 @@ def names(objs: list) -> Iterable[str]:
|
||||
class TestFillRestrictive(unittest.TestCase):
|
||||
def test_basic_fill(self):
|
||||
"""Tests `fill_restrictive` fills and removes the locations and items from their respective lists"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
@@ -152,16 +152,16 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_ordered_fill(self):
|
||||
"""Tests `fill_restrictive` fulfills set rules"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
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,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[0])
|
||||
@@ -169,8 +169,8 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_partial_fill(self):
|
||||
"""Tests that `fill_restrictive` returns unfilled locations"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
@@ -178,14 +178,14 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item0)
|
||||
@@ -195,19 +195,19 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_minimal_fill(self):
|
||||
"""Test that fill for minimal player can have unreachable items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.worlds[player1.id].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
|
||||
multiworld.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,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
@@ -220,15 +220,15 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
the non-minimal player get all items.
|
||||
"""
|
||||
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
player2 = generate_player_data(multi_world, 2, 3, 3)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
player2 = generate_player_data(multiworld, 2, 3, 3)
|
||||
|
||||
multi_world.accessibility[player1.id].value = multi_world.accessibility[player1.id].option_minimal
|
||||
multi_world.accessibility[player2.id].value = multi_world.accessibility[player2.id].option_locations
|
||||
multiworld.accessibility[player1.id].value = multiworld.accessibility[player1.id].option_minimal
|
||||
multiworld.accessibility[player2.id].value = multiworld.accessibility[player2.id].option_locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: True
|
||||
multi_world.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||
multiworld.completion_condition[player1.id] = lambda state: True
|
||||
multiworld.completion_condition[player2.id] = lambda state: state.has(player2.prog_items[2].name, player2.id)
|
||||
|
||||
set_rule(player1.locations[1], lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||
set_rule(player1.locations[2], lambda state: state.has(player1.prog_items[1].name, player1.id))
|
||||
@@ -241,28 +241,28 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
# fill remaining locations with remaining items
|
||||
location_pool = player1.locations[1:] + player2.locations
|
||||
item_pool = player1.prog_items[:-1] + player2.prog_items
|
||||
fill_restrictive(multi_world, multi_world.state, location_pool, item_pool)
|
||||
multi_world.state.sweep_for_events() # collect everything
|
||||
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
||||
multiworld.state.sweep_for_events() # collect everything
|
||||
|
||||
# all of player2's locations and items should be accessible (not all of player1's)
|
||||
for item in player2.prog_items:
|
||||
self.assertTrue(multi_world.state.has(item.name, player2.id),
|
||||
self.assertTrue(multiworld.state.has(item.name, player2.id),
|
||||
f'{item} is unreachable in {item.location}')
|
||||
|
||||
def test_reversed_fill(self):
|
||||
"""Test a different set of rules can be satisfied"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc0 = player1.locations[0]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations, player1.prog_items)
|
||||
|
||||
self.assertEqual(loc0.item, item1)
|
||||
@@ -270,13 +270,13 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multi_step_fill(self):
|
||||
"""Test that fill is able to satisfy multiple spheres"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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))
|
||||
@@ -285,7 +285,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
set_rule(locations[3], lambda state: state.has(
|
||||
items[1].name, player1.id))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
self.assertEqual(locations[0].item, items[1])
|
||||
@@ -295,25 +295,25 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_impossible_fill(self):
|
||||
"""Test that fill raises an error when it can't place any items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
items = player1.prog_items
|
||||
locations = player1.locations
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has(
|
||||
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))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_circular_fill(self):
|
||||
"""Test that fill raises an error when it can't place all items"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 3, 3)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 3, 3)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
@@ -322,46 +322,46 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
loc1 = player1.locations[1]
|
||||
loc2 = player1.locations[2]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_competing_fill(self):
|
||||
"""Test that fill raises an error when it can't place items in a way to satisfy the conditions"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
item0 = player1.prog_items[0]
|
||||
item1 = player1.prog_items[1]
|
||||
loc1 = player1.locations[1]
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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))
|
||||
|
||||
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
|
||||
self.assertRaises(FillError, fill_restrictive, multiworld, multiworld.state,
|
||||
player1.locations.copy(), player1.prog_items.copy())
|
||||
|
||||
def test_multiplayer_fill(self):
|
||||
"""Test that items can be placed across worlds"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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(
|
||||
multiworld.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 +
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player1.prog_items[1])
|
||||
@@ -371,21 +371,21 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_multiplayer_rules_fill(self):
|
||||
"""Test that fill across worlds satisfies the rules"""
|
||||
multi_world = generate_multi_world(2)
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
player2 = generate_player_data(multi_world, 2, 2, 2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
player2 = generate_player_data(multiworld, 2, 2, 2)
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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(
|
||||
multiworld.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))
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations +
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations +
|
||||
player2.locations, player1.prog_items + player2.prog_items)
|
||||
|
||||
self.assertEqual(player1.locations[0].item, player2.prog_items[0])
|
||||
@@ -395,10 +395,10 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
def test_restrictive_progress(self):
|
||||
"""Test that various spheres with different requirements can be filled"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, prog_item_count=25)
|
||||
items = player1.prog_items.copy()
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(player1.prog_items), player1.id)
|
||||
|
||||
player1.generate_region(player1.menu, 5)
|
||||
@@ -411,16 +411,16 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
player1.generate_region(player1.menu, 5, lambda state: state.has_all(
|
||||
names(items[17:22]), player1.id))
|
||||
|
||||
locations = multi_world.get_unfilled_locations()
|
||||
locations = multiworld.get_unfilled_locations()
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
locations, player1.prog_items)
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule(self):
|
||||
"""Test that item swap happens and works as intended"""
|
||||
# test for PR#1109
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 4, 4)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 4, 4)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
# for the test to work, item and location order is relevant: Sphere 1 last, allowed_item not last
|
||||
@@ -437,15 +437,15 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
self.assertTrue(sphere1_loc.can_fill(None, allowed_item, False), "Test is flawed")
|
||||
self.assertFalse(sphere1_loc.can_fill(None, items[2], False), "Test is flawed")
|
||||
# fill has to place items[1] in locations[0] which will result in a swap because of placement order
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
|
||||
# assert swap happened
|
||||
self.assertTrue(sphere1_loc.item, "Did not swap required item into Sphere 1")
|
||||
self.assertEqual(sphere1_loc.item, allowed_item, "Wrong item in Sphere 1")
|
||||
|
||||
def test_swap_to_earlier_location_with_item_rule2(self):
|
||||
"""Test that swap works before all items are placed"""
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 5, 5)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 5, 5)
|
||||
locations = player1.locations[:] # copy required
|
||||
items = player1.prog_items[:] # copy required
|
||||
# Two items provide access to sphere 2.
|
||||
@@ -477,7 +477,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
|
||||
# Now fill should place one_to_two1 in sphere1_loc1 or sphere1_loc2 via swap,
|
||||
# which it will attempt before two_to_three and three_to_four are placed, testing the behavior.
|
||||
fill_restrictive(multi_world, multi_world.state, player1.locations, player1.prog_items)
|
||||
fill_restrictive(multiworld, multiworld.state, player1.locations, player1.prog_items)
|
||||
# assert swap happened
|
||||
self.assertTrue(sphere1_loc1.item and sphere1_loc2.item, "Did not swap required item into Sphere 1")
|
||||
self.assertTrue(sphere1_loc1.item.name == one_to_two1 or
|
||||
@@ -486,29 +486,29 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
def test_double_sweep(self):
|
||||
"""Test that sweep doesn't duplicate Event items when sweeping"""
|
||||
# test for PR1114
|
||||
multi_world = generate_multi_world(1)
|
||||
player1 = generate_player_data(multi_world, 1, 1, 1)
|
||||
multiworld = generate_multiworld(1)
|
||||
player1 = generate_player_data(multiworld, 1, 1, 1)
|
||||
location = player1.locations[0]
|
||||
location.address = None
|
||||
location.event = True
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
multi_world.state.sweep_for_events()
|
||||
multi_world.state.sweep_for_events()
|
||||
self.assertTrue(multi_world.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multi_world.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
|
||||
def test_correct_item_instance_removed_from_pool(self):
|
||||
"""Test that a placed item gets removed from the submitted pool"""
|
||||
multi_world = generate_multi_world()
|
||||
player1 = generate_player_data(multi_world, 1, 2, 2)
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(multiworld, 1, 2, 2)
|
||||
|
||||
player1.prog_items[0].name = "Different_item_instance_but_same_item_name"
|
||||
player1.prog_items[1].name = "Different_item_instance_but_same_item_name"
|
||||
loc0 = player1.locations[0]
|
||||
|
||||
fill_restrictive(multi_world, multi_world.state,
|
||||
fill_restrictive(multiworld, multiworld.state,
|
||||
[loc0], player1.prog_items)
|
||||
|
||||
self.assertEqual(1, len(player1.prog_items))
|
||||
@@ -518,14 +518,14 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
def test_basic_distribute(self):
|
||||
"""Test that distribute_items_restrictive is deterministic"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
prog_items = player1.prog_items
|
||||
basic_items = player1.basic_items
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[0].item, basic_items[1])
|
||||
self.assertFalse(locations[0].event)
|
||||
@@ -538,52 +538,52 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_excluded_distribute(self):
|
||||
"""Test that distribute_items_restrictive doesn't put advancement items on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[2].progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertFalse(locations[1].item.advancement)
|
||||
self.assertFalse(locations[2].item.advancement)
|
||||
|
||||
def test_non_excluded_item_distribute(self):
|
||||
"""Test that useful items aren't placed on excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
basic_items = player1.basic_items
|
||||
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
basic_items[1].classification = ItemClassification.useful
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(locations[1].item, basic_items[0])
|
||||
|
||||
def test_too_many_excluded_distribute(self):
|
||||
"""Test that fill fails if it can't place all progression items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[1].progress_type = LocationProgressType.EXCLUDED
|
||||
locations[2].progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multiworld)
|
||||
|
||||
def test_non_excluded_item_must_distribute(self):
|
||||
"""Test that fill fails if it can't place useful items due to too many excluded locations"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
basic_items = player1.basic_items
|
||||
|
||||
@@ -592,47 +592,47 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
basic_items[0].classification = ItemClassification.useful
|
||||
basic_items[1].classification = ItemClassification.useful
|
||||
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
|
||||
self.assertRaises(FillError, distribute_items_restrictive, multiworld)
|
||||
|
||||
def test_priority_distribute(self):
|
||||
"""Test that priority locations receive advancement items"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.PRIORITY
|
||||
locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertTrue(locations[0].item.advancement)
|
||||
self.assertTrue(locations[3].item.advancement)
|
||||
|
||||
def test_excess_priority_distribute(self):
|
||||
"""Test that if there's more priority locations than advancement items, they can still fill"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
locations = player1.locations
|
||||
|
||||
locations[0].progress_type = LocationProgressType.PRIORITY
|
||||
locations[1].progress_type = LocationProgressType.PRIORITY
|
||||
locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertFalse(locations[3].item.advancement)
|
||||
|
||||
def test_multiple_world_priority_distribute(self):
|
||||
"""Test that priority fill can be satisfied for multiple worlds"""
|
||||
multi_world = generate_multi_world(3)
|
||||
multiworld = generate_multiworld(3)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
|
||||
multiworld, 2, 4, prog_item_count=1, basic_item_count=3)
|
||||
player3 = generate_player_data(
|
||||
multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
|
||||
multiworld, 3, 6, prog_item_count=4, basic_item_count=2)
|
||||
|
||||
player1.locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
player1.locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
@@ -644,7 +644,7 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
player3.locations[2].progress_type = LocationProgressType.PRIORITY
|
||||
player3.locations[3].progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertTrue(player1.locations[2].item.advancement)
|
||||
self.assertTrue(player1.locations[3].item.advancement)
|
||||
@@ -656,9 +656,9 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_remove_locations_in_fill_hook(self):
|
||||
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
multiworld, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
|
||||
removed_item: list[Item] = []
|
||||
removed_location: list[Location] = []
|
||||
@@ -667,21 +667,21 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
removed_item.append(filleritempool.pop(0))
|
||||
removed_location.append(fill_locations.pop(0))
|
||||
|
||||
multi_world.worlds[player1.id].fill_hook = fill_hook
|
||||
multiworld.worlds[player1.id].fill_hook = fill_hook
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertIsNone(removed_item[0].location)
|
||||
self.assertIsNone(removed_location[0].item)
|
||||
|
||||
def test_seed_robust_to_item_order(self):
|
||||
"""Test deterministic fill"""
|
||||
mw1 = generate_multi_world()
|
||||
mw1 = generate_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multi_world()
|
||||
mw2 = generate_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
mw2.itempool.append(mw2.itempool.pop(0))
|
||||
@@ -694,12 +694,12 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_seed_robust_to_location_order(self):
|
||||
"""Test deterministic fill even if locations in a region are reordered"""
|
||||
mw1 = generate_multi_world()
|
||||
mw1 = generate_multiworld()
|
||||
gen1 = generate_player_data(
|
||||
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
distribute_items_restrictive(mw1)
|
||||
|
||||
mw2 = generate_multi_world()
|
||||
mw2 = generate_multiworld()
|
||||
gen2 = generate_player_data(
|
||||
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
|
||||
reg = mw2.get_region("Menu", gen2.id)
|
||||
@@ -713,45 +713,45 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
|
||||
|
||||
def test_can_reserve_advancement_items_for_general_fill(self):
|
||||
"""Test that priority locations fill still satisfies item rules"""
|
||||
multi_world = generate_multi_world()
|
||||
multiworld = generate_multiworld()
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, prog_item_count=5)
|
||||
multiworld, 1, location_count=5, prog_item_count=5)
|
||||
items = player1.prog_items
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
multiworld.completion_condition[player1.id] = lambda state: state.has_all(
|
||||
names(items), player1.id)
|
||||
|
||||
location = player1.locations[0]
|
||||
location.progress_type = LocationProgressType.PRIORITY
|
||||
location.item_rule = lambda item: item not in items[:4]
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
self.assertEqual(location.item, items[4])
|
||||
|
||||
def test_non_excluded_local_items(self):
|
||||
"""Test that local items get placed locally in a multiworld"""
|
||||
multi_world = generate_multi_world(2)
|
||||
multiworld = generate_multiworld(2)
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, location_count=5, basic_item_count=5)
|
||||
multiworld, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, location_count=5, basic_item_count=5)
|
||||
multiworld, 2, location_count=5, basic_item_count=5)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
for item in multiworld.get_items():
|
||||
item.classification = ItemClassification.useful
|
||||
|
||||
multi_world.local_items[player1.id].value = set(names(player1.basic_items))
|
||||
multi_world.local_items[player2.id].value = set(names(player2.basic_items))
|
||||
locality_rules(multi_world)
|
||||
multiworld.local_items[player1.id].value = set(names(player1.basic_items))
|
||||
multiworld.local_items[player2.id].value = set(names(player2.basic_items))
|
||||
locality_rules(multiworld)
|
||||
|
||||
distribute_items_restrictive(multi_world)
|
||||
distribute_items_restrictive(multiworld)
|
||||
|
||||
for item in multi_world.get_items():
|
||||
for item in multiworld.get_items():
|
||||
self.assertEqual(item.player, item.location.player)
|
||||
self.assertFalse(item.location.event, False)
|
||||
|
||||
def test_early_items(self) -> None:
|
||||
"""Test that the early items API successfully places items early"""
|
||||
mw = generate_multi_world(2)
|
||||
mw = generate_multiworld(2)
|
||||
player1 = generate_player_data(mw, 1, location_count=5, basic_item_count=5)
|
||||
player2 = generate_player_data(mw, 2, location_count=5, basic_item_count=5)
|
||||
mw.early_items[1][player1.basic_items[0].name] = 1
|
||||
@@ -810,19 +810,19 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
|
||||
|
||||
def setUp(self) -> None:
|
||||
multi_world = generate_multi_world(2)
|
||||
self.multi_world = multi_world
|
||||
multiworld = generate_multiworld(2)
|
||||
self.multiworld = multiworld
|
||||
player1 = generate_player_data(
|
||||
multi_world, 1, prog_item_count=2, basic_item_count=40)
|
||||
multiworld, 1, prog_item_count=2, basic_item_count=40)
|
||||
self.player1 = player1
|
||||
player2 = generate_player_data(
|
||||
multi_world, 2, prog_item_count=2, basic_item_count=40)
|
||||
multiworld, 2, prog_item_count=2, basic_item_count=40)
|
||||
self.player2 = player2
|
||||
|
||||
multi_world.completion_condition[player1.id] = lambda state: state.has(
|
||||
multiworld.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(
|
||||
multiworld.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)
|
||||
|
||||
@@ -830,42 +830,42 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
# Sphere 1
|
||||
region = player1.generate_region(player1.menu, 20)
|
||||
items = fill_region(multi_world, region, [
|
||||
items = fill_region(multiworld, region, [
|
||||
player1.prog_items[0]] + items)
|
||||
|
||||
# Sphere 2
|
||||
region = player1.generate_region(
|
||||
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
|
||||
items = fill_region(
|
||||
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||
multiworld, region, [player1.prog_items[1], player2.prog_items[0]] + items)
|
||||
|
||||
# Sphere 3
|
||||
region = player2.generate_region(
|
||||
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
|
||||
fill_region(multi_world, region, [player2.prog_items[1]] + items)
|
||||
fill_region(multiworld, region, [player2.prog_items[1]] + items)
|
||||
|
||||
def test_balances_progression(self) -> None:
|
||||
"""Tests that progression balancing moves progression items earlier"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[1], self.player2.prog_items[0])
|
||||
|
||||
def test_balances_progression_light(self) -> None:
|
||||
"""Test that progression balancing still moves items earlier on minimum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 1
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 1
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 1
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 1
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
# TODO: arrange for a result that's different from the default
|
||||
self.assertRegionContains(
|
||||
@@ -873,13 +873,13 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
def test_balances_progression_heavy(self) -> None:
|
||||
"""Test that progression balancing moves items earlier on maximum value"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 99
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 99
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 99
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 99
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
# TODO: arrange for a result that's different from the default
|
||||
self.assertRegionContains(
|
||||
@@ -887,25 +887,25 @@ class TestBalanceMultiworldProgression(unittest.TestCase):
|
||||
|
||||
def test_skips_balancing_progression(self) -> None:
|
||||
"""Test that progression balancing is skipped when players have it disabled"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 0
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 0
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 0
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 0
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
def test_ignores_priority_locations(self) -> None:
|
||||
"""Test that progression items on priority locations don't get moved by balancing"""
|
||||
self.multi_world.progression_balancing[self.player1.id].value = 50
|
||||
self.multi_world.progression_balancing[self.player2.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player1.id].value = 50
|
||||
self.multiworld.progression_balancing[self.player2.id].value = 50
|
||||
|
||||
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
balance_multiworld_progression(self.multi_world)
|
||||
balance_multiworld_progression(self.multiworld)
|
||||
|
||||
self.assertRegionContains(
|
||||
self.player1.regions[2], self.player2.prog_items[0])
|
||||
|
||||
27
test/general/test_groups.py
Normal file
27
test/general/test_groups.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestNameGroups(TestCase):
|
||||
def test_item_name_groups_not_empty(self) -> None:
|
||||
"""
|
||||
Test that there are no empty item name groups, which is likely a bug.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.item_id_to_name:
|
||||
continue # ignore worlds without items
|
||||
with self.subTest(game=game_name):
|
||||
for name, group in world_type.item_name_groups.items():
|
||||
self.assertTrue(group, f"Item name group \"{name}\" of \"{game_name}\" is empty")
|
||||
|
||||
def test_location_name_groups_not_empty(self) -> None:
|
||||
"""
|
||||
Test that there are no empty location name groups, which is likely a bug.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if not world_type.location_id_to_name:
|
||||
continue # ignore worlds without locations
|
||||
with self.subTest(game=game_name):
|
||||
for name, group in world_type.location_name_groups.items():
|
||||
self.assertTrue(group, f"Location name group \"{name}\" of \"{game_name}\" is empty")
|
||||
@@ -43,15 +43,15 @@ class TestBase(unittest.TestCase):
|
||||
with self.subTest(group_name, group_name=group_name):
|
||||
self.assertNotIn(group_name, world_type.item_name_to_id)
|
||||
|
||||
def test_item_count_greater_equal_locations(self):
|
||||
"""Test that by the pre_fill step under default settings, each game submits items >= locations"""
|
||||
def test_item_count_equal_locations(self):
|
||||
"""Test that by the pre_fill step under default settings, each game submits items == locations"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
self.assertGreaterEqual(
|
||||
self.assertEqual(
|
||||
len(multiworld.itempool),
|
||||
len(multiworld.get_unfilled_locations()),
|
||||
f"{game_name} Item count MUST meet or exceed the number of locations",
|
||||
f"{game_name} Item count MUST match the number of locations",
|
||||
)
|
||||
|
||||
def test_items_in_datapackage(self):
|
||||
|
||||
@@ -11,14 +11,14 @@ class TestBase(unittest.TestCase):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
locations = Counter(location.name for location in multiworld.get_locations())
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
|
||||
self.assertEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location name {locations.most_common(1)}")
|
||||
|
||||
locations = Counter(location.address for location in multiworld.get_locations()
|
||||
if type(location.address) is int)
|
||||
if locations:
|
||||
self.assertLessEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
self.assertEqual(locations.most_common(1)[0][1], 1,
|
||||
f"{world_type.game} has duplicate of location ID {locations.most_common(1)}")
|
||||
|
||||
def test_locations_in_datapackage(self):
|
||||
"""Tests that created locations not filled before fill starts exist in the datapackage."""
|
||||
|
||||
@@ -36,15 +36,15 @@ class TestBase(unittest.TestCase):
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
excluded = world.worlds[1].options.exclude_locations.value
|
||||
state = world.get_all_state(False)
|
||||
for location in world.get_locations():
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
excluded = multiworld.worlds[1].options.exclude_locations.value
|
||||
state = multiworld.get_all_state(False)
|
||||
for location in multiworld.get_locations():
|
||||
if location.name not in excluded:
|
||||
with self.subTest("Location should be reached", location=location):
|
||||
self.assertTrue(location.can_reach(state), f"{location.name} unreachable")
|
||||
|
||||
for region in world.get_regions():
|
||||
for region in multiworld.get_regions():
|
||||
if region.name in unreachable_regions:
|
||||
with self.subTest("Region should be unreachable", region=region):
|
||||
self.assertFalse(region.can_reach(state))
|
||||
@@ -53,15 +53,15 @@ class TestBase(unittest.TestCase):
|
||||
self.assertTrue(region.can_reach(state))
|
||||
|
||||
with self.subTest("Completion Condition"):
|
||||
self.assertTrue(world.can_beat_game(state))
|
||||
self.assertTrue(multiworld.can_beat_game(state))
|
||||
|
||||
def test_default_empty_state_can_reach_something(self):
|
||||
"""Ensure empty state can reach at least one location with the defined options"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
world = setup_solo_multiworld(world_type)
|
||||
state = CollectionState(world)
|
||||
all_locations = world.get_locations()
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
state = CollectionState(multiworld)
|
||||
all_locations = multiworld.get_locations()
|
||||
if all_locations:
|
||||
locations = set()
|
||||
for location in all_locations:
|
||||
|
||||
@@ -3,7 +3,9 @@ import os
|
||||
import sys
|
||||
import warnings
|
||||
import zipimport
|
||||
from typing import Dict, List, NamedTuple, TypedDict
|
||||
import time
|
||||
import dataclasses
|
||||
from typing import Dict, List, TypedDict, Optional
|
||||
|
||||
from Utils import local_path, user_path
|
||||
|
||||
@@ -34,10 +36,12 @@ class DataPackage(TypedDict):
|
||||
games: Dict[str, GamesPackage]
|
||||
|
||||
|
||||
class WorldSource(NamedTuple):
|
||||
@dataclasses.dataclass(order=True)
|
||||
class WorldSource:
|
||||
path: str # typically relative path from this module
|
||||
is_zip: bool = False
|
||||
relative: bool = True # relative to regular world import folder
|
||||
time_taken: Optional[float] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})"
|
||||
@@ -50,6 +54,7 @@ class WorldSource(NamedTuple):
|
||||
|
||||
def load(self) -> bool:
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
@@ -69,6 +74,7 @@ class WorldSource(NamedTuple):
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{self.path}", "worlds")
|
||||
self.time_taken = time.perf_counter()-start
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
|
||||
@@ -271,7 +271,7 @@ class AdventureWorld(World):
|
||||
overworld_locations_copy = overworld.locations.copy()
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
|
||||
locations_copy = all_locations.copy()
|
||||
locations_copy = list(all_locations)
|
||||
for loc in all_locations:
|
||||
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
|
||||
locations_copy.remove(loc)
|
||||
|
||||
@@ -968,6 +968,9 @@ def standard_rules(world, player):
|
||||
|
||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 3))
|
||||
else:
|
||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
||||
|
||||
def toss_junk_item(world, player):
|
||||
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import zipfile
|
||||
from copy import deepcopy
|
||||
from .Regions import object_id_table
|
||||
from Main import __version__
|
||||
from Utils import __version__
|
||||
from worlds.Files import APContainer
|
||||
import pkgutil
|
||||
|
||||
|
||||
@@ -1356,5 +1356,5 @@ exclusion_table = {
|
||||
location_groups: typing.Dict[str, list]
|
||||
location_groups = {
|
||||
Region_Name: [loc for loc in Region_Locs if "Event" not in loc]
|
||||
for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs
|
||||
for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs and "Event" not in Region_Locs[0]
|
||||
}
|
||||
|
||||
@@ -606,11 +606,11 @@ hard_data_xemnas = {
|
||||
ItemName.LimitForm: 1,
|
||||
}
|
||||
final_leveling_access = {
|
||||
LocationName.MemorysSkyscaperMythrilCrystal,
|
||||
LocationName.RoxasEventLocation,
|
||||
LocationName.GrimReaper2,
|
||||
LocationName.Xaldin,
|
||||
LocationName.StormRider,
|
||||
LocationName.SunsetTerraceAbilityRing
|
||||
LocationName.UndergroundConcourseMythrilGem
|
||||
}
|
||||
|
||||
multi_form_region_access = {
|
||||
|
||||
@@ -146,7 +146,7 @@ def setReplacementName(key: str, value: str) -> None:
|
||||
|
||||
def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -> bytes:
|
||||
instr = instr.format(**_NAMES)
|
||||
s = instr.encode("ascii")
|
||||
s = instr.encode("ascii", errors="replace")
|
||||
s = s.replace(b"'", b"^")
|
||||
|
||||
def padLine(line: bytes) -> bytes:
|
||||
@@ -169,7 +169,7 @@ def formatText(instr: str, *, center: bool = False, ask: Optional[str] = None) -
|
||||
if result_line:
|
||||
result += padLine(result_line)
|
||||
if ask is not None:
|
||||
askbytes = ask.encode("ascii")
|
||||
askbytes = ask.encode("ascii", errors="replace")
|
||||
result = result.rstrip()
|
||||
while len(result) % 32 != 16:
|
||||
result += b' '
|
||||
|
||||
@@ -399,6 +399,26 @@ class Palette(Choice):
|
||||
option_pink = 4
|
||||
option_inverted = 5
|
||||
|
||||
class Music(Choice, LADXROption):
|
||||
"""
|
||||
[Vanilla] Regular Music
|
||||
[Shuffled] Shuffled Music
|
||||
[Off] No music
|
||||
"""
|
||||
ladxr_name = "music"
|
||||
option_vanilla = 0
|
||||
option_shuffled = 1
|
||||
option_off = 2
|
||||
|
||||
|
||||
def to_ladxr_option(self, all_options):
|
||||
s = ""
|
||||
if self.value == self.option_shuffled:
|
||||
s = "random"
|
||||
elif self.value == self.option_off:
|
||||
s = "off"
|
||||
return self.ladxr_name, s
|
||||
|
||||
class WarpImprovements(DefaultOffToggle):
|
||||
"""
|
||||
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
|
||||
@@ -444,6 +464,7 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
|
||||
'shuffle_maps': ShuffleMaps,
|
||||
'shuffle_compasses': ShuffleCompasses,
|
||||
'shuffle_stone_beaks': ShuffleStoneBeaks,
|
||||
'music': Music,
|
||||
'music_change_condition': MusicChangeCondition,
|
||||
'nag_messages': NagMessages,
|
||||
'ap_title_screen': APTitleScreen,
|
||||
|
||||
@@ -82,9 +82,8 @@ class LingoWorld(World):
|
||||
skips = int(non_traps * skip_percentage / 100.0)
|
||||
non_skips = non_traps - skips
|
||||
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
for i in range(0, non_skips):
|
||||
pool.append(self.create_item(filler_list[i % len(filler_list)]))
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
for i in range(0, skips):
|
||||
pool.append(self.create_item("Puzzle Skip"))
|
||||
@@ -130,3 +129,7 @@ class LingoWorld(World):
|
||||
slot_data["painting_entrance_to_exit"] = self.player_logic.painting_mapping
|
||||
|
||||
return slot_data
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
filler_list = [":)", "The Feeling of Being Lost", "Wanderlust", "Empty White Hallways"]
|
||||
return self.random.choice(filler_list)
|
||||
|
||||
@@ -1118,7 +1118,13 @@
|
||||
id: Cross Room/Panel_north_missing
|
||||
colors: green
|
||||
tag: forbid
|
||||
required_room: Outside The Bold
|
||||
required_panel:
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
DIAMONDS:
|
||||
id: Cross Room/Panel_diamonds_missing
|
||||
colors: green
|
||||
@@ -4414,9 +4420,14 @@
|
||||
colors: blue
|
||||
tag: forbid
|
||||
required_panel:
|
||||
room: The Bearer (West)
|
||||
panel: SMILE
|
||||
required_room: Outside The Bold
|
||||
- room: The Bearer (West)
|
||||
panel: SMILE
|
||||
- room: Outside The Bold
|
||||
panel: MOUTH
|
||||
- room: Outside The Bold
|
||||
panel: YEAST
|
||||
- room: Outside The Bold
|
||||
panel: WET
|
||||
Cross Tower (South):
|
||||
entrances: # No roof access
|
||||
The Bearer (North):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions
|
||||
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool
|
||||
|
||||
|
||||
class ShuffleDoors(Choice):
|
||||
@@ -136,3 +136,4 @@ class LingoOptions(PerGameCommonOptions):
|
||||
trap_percentage: TrapPercentage
|
||||
puzzle_skip_percentage: PuzzleSkipPercentage
|
||||
death_link: DeathLink
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
@@ -26,7 +26,8 @@ class MuseDashCollections:
|
||||
# MUSE_PLUS_DLC, # To be included when OptionSets are rendered as part of basic settings.
|
||||
# "maimai DX Limited-time Suite", # Part of Muse Plus. Goes away 31st Jan 2026.
|
||||
"Miku in Museland", # Paid DLC not included in Muse Plus
|
||||
"MSR Anthology", # Part of Muse Plus. Goes away 20th Jan 2024.
|
||||
"Rin Len's Mirrorland", # Paid DLC not included in Muse Plus
|
||||
"MSR Anthology", # Now no longer available.
|
||||
]
|
||||
|
||||
DIFF_OVERRIDES: List[str] = [
|
||||
|
||||
@@ -484,7 +484,7 @@ Hand in Hand|66-1|Miku in Museland|False|1|3|6|
|
||||
Cynical Night Plan|66-2|Miku in Museland|False|4|6|8|
|
||||
God-ish|66-3|Miku in Museland|False|4|7|10|
|
||||
Darling Dance|66-4|Miku in Museland|False|4|7|9|
|
||||
Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|
|
||||
Hatsune Creation Myth|66-5|Miku in Museland|False|6|8|10|11
|
||||
The Vampire|66-6|Miku in Museland|False|4|6|9|
|
||||
Future Eve|66-7|Miku in Museland|False|4|8|11|
|
||||
Unknown Mother Goose|66-8|Miku in Museland|False|4|8|10|
|
||||
@@ -509,4 +509,24 @@ INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10|
|
||||
Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9|
|
||||
HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10|
|
||||
Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11
|
||||
Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|
|
||||
Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|
|
||||
Twilight Poems|43-44|MD Plus Project|True|3|6|8|
|
||||
All My Friends feat. RANASOL|43-45|MD Plus Project|True|4|7|9|
|
||||
Heartache|43-46|MD Plus Project|True|5|7|10|
|
||||
Blue Lemonade|43-47|MD Plus Project|True|3|6|8|
|
||||
Haunted Dance|43-48|MD Plus Project|False|6|9|11|
|
||||
Hey Vincent.|43-49|MD Plus Project|True|6|8|10|
|
||||
Meteor feat. TEA|43-50|MD Plus Project|True|3|6|9|
|
||||
Narcissism Angel|43-51|MD Plus Project|True|1|3|6|
|
||||
AlterLuna|43-52|MD Plus Project|True|6|8|11|
|
||||
Niki Tousen|43-53|MD Plus Project|True|6|8|10|11
|
||||
Rettou Joutou|70-0|Rin Len's Mirrorland|False|4|7|9|
|
||||
Telecaster B-Boy|70-1|Rin Len's Mirrorland|False|5|7|10|
|
||||
Iya Iya Iya|70-2|Rin Len's Mirrorland|False|2|4|7|
|
||||
Nee Nee Nee|70-3|Rin Len's Mirrorland|False|4|6|8|
|
||||
Chaotic Love Revolution|70-4|Rin Len's Mirrorland|False|4|6|8|
|
||||
Dance of the Corpses|70-5|Rin Len's Mirrorland|False|2|5|8|
|
||||
Bitter Choco Decoration|70-6|Rin Len's Mirrorland|False|3|6|9|
|
||||
Dance Robot Dance|70-7|Rin Len's Mirrorland|False|4|7|10|
|
||||
Sweet Devil|70-8|Rin Len's Mirrorland|False|5|7|9|
|
||||
Someday'z Coming|70-9|Rin Len's Mirrorland|False|5|7|9|
|
||||
@@ -36,7 +36,7 @@ class AdditionalSongs(Range):
|
||||
- The final song count may be lower due to other settings.
|
||||
"""
|
||||
range_start = 15
|
||||
range_end = 508 # Note will probably not reach this high if any other settings are done.
|
||||
range_end = 528 # Note will probably not reach this high if any other settings are done.
|
||||
default = 40
|
||||
display_name = "Additional Song Count"
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
from typing import Dict
|
||||
|
||||
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region
|
||||
from . import Items, Locations
|
||||
|
||||
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
return Items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def create_location(player: int, name: str, region: Region) -> Location:
|
||||
return Locations.NoitaLocation(player, name, None, region)
|
||||
|
||||
|
||||
def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location:
|
||||
region = multiworld.get_region(region_name, player)
|
||||
|
||||
new_location = create_location(player, item, region)
|
||||
new_location.place_locked_item(create_event(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(multiworld: MultiWorld, player: int) -> None:
|
||||
for region, event in event_locks.items():
|
||||
create_locked_location_event(multiworld, player, region, event)
|
||||
|
||||
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: Dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
"Snowy Depths": "Portal to Holy Mountain 3",
|
||||
"Hiisi Base": "Portal to Holy Mountain 4",
|
||||
"Underground Jungle": "Portal to Holy Mountain 5",
|
||||
"The Vault": "Portal to Holy Mountain 6",
|
||||
"Temple of the Art": "Portal to Holy Mountain 7",
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
from typing import List, NamedTuple, Set
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
from . import Items, Locations
|
||||
from .Options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
|
||||
|
||||
class EntranceLock(NamedTuple):
|
||||
source: str
|
||||
destination: str
|
||||
event: str
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
|
||||
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
|
||||
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
|
||||
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
|
||||
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
|
||||
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
|
||||
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: List[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
"Underground Jungle Holy Mountain",
|
||||
"Vault Holy Mountain",
|
||||
"Temple of the Art Holy Mountain",
|
||||
"Laboratory Holy Mountain",
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: List[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
"Wand (Tier 4)", # Underground Jungle
|
||||
"Wand (Tier 5)", # The Vault
|
||||
"Wand (Tier 6)", # Temple of the Art
|
||||
]
|
||||
|
||||
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"]
|
||||
|
||||
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return sum(state.count(perk, player) for perk in perk_list) >= amount
|
||||
|
||||
|
||||
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return state.count("Orb", player) >= amount
|
||||
|
||||
|
||||
def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int):
|
||||
location = multiworld.get_location(location_name, player)
|
||||
GenericRules.forbid_items_for_player(location, items, player)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Rule Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
|
||||
def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None:
|
||||
for location_name in Locations.location_name_to_id.keys():
|
||||
if "Shop Item" in location_name:
|
||||
forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player)
|
||||
|
||||
|
||||
# Prevent high tier wands from appearing in early Holy Mountain shops
|
||||
def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None:
|
||||
for i, region_name in enumerate(holy_mountain_regions):
|
||||
wands_to_forbid = wand_tiers[i+1:]
|
||||
|
||||
locations_in_region = Locations.location_region_mapping[region_name].keys()
|
||||
for location_name in locations_in_region:
|
||||
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
|
||||
|
||||
# Prevent high tier wands from appearing in the Secret shop
|
||||
wands_to_forbid = wand_tiers[3:]
|
||||
locations_in_region = Locations.location_region_mapping["Secret Shop"].keys()
|
||||
for location_name in locations_in_region:
|
||||
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
|
||||
|
||||
|
||||
def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None:
|
||||
for lock in entrance_locks:
|
||||
location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player)
|
||||
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player))
|
||||
|
||||
|
||||
def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
|
||||
victory_condition = multiworld.victory_condition[player].value
|
||||
for lock in entrance_locks:
|
||||
location = multiworld.get_location(lock.event, player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_greed_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_pure_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2) and
|
||||
has_orb_count(state, player, items_needed)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, player, items_needed//2) and
|
||||
has_orb_count(state, player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(multiworld: MultiWorld, player: int):
|
||||
lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances
|
||||
magical_entrances = multiworld.get_region("Magical Temple", player).entrances
|
||||
wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances
|
||||
for entrance in lukki_entrances:
|
||||
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\
|
||||
state.has("All-Seeing Eye Perk", player)
|
||||
for entrance in magical_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
|
||||
for entrance in wizard_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
|
||||
|
||||
|
||||
def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
|
||||
victory_condition = multiworld.victory_condition[player].value
|
||||
victory_location = multiworld.get_location("Victory", player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Main Function
|
||||
# ----------------
|
||||
|
||||
|
||||
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
|
||||
if multiworld.players > 1:
|
||||
ban_items_from_shops(multiworld, player)
|
||||
ban_early_high_tier_wands(multiworld, player)
|
||||
lock_holy_mountains_into_spheres(multiworld, player)
|
||||
holy_mountain_unlock_conditions(multiworld, player)
|
||||
biome_unlock_conditions(multiworld, player)
|
||||
victory_unlock_conditions(multiworld, player)
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses:
|
||||
forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player)
|
||||
@@ -1,6 +1,8 @@
|
||||
from BaseClasses import Item, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Events, Items, Locations, Options, Regions, Rules
|
||||
from typing import Dict, Any
|
||||
from . import events, items, locations, regions, rules
|
||||
from .options import NoitaOptions
|
||||
|
||||
|
||||
class NoitaWeb(WebWorld):
|
||||
@@ -24,13 +26,14 @@ class NoitaWorld(World):
|
||||
"""
|
||||
|
||||
game = "Noita"
|
||||
option_definitions = Options.noita_options
|
||||
options: NoitaOptions
|
||||
options_dataclass = NoitaOptions
|
||||
|
||||
item_name_to_id = Items.item_name_to_id
|
||||
location_name_to_id = Locations.location_name_to_id
|
||||
item_name_to_id = items.item_name_to_id
|
||||
location_name_to_id = locations.location_name_to_id
|
||||
|
||||
item_name_groups = Items.item_name_groups
|
||||
location_name_groups = Locations.location_name_groups
|
||||
item_name_groups = items.item_name_groups
|
||||
location_name_groups = locations.location_name_groups
|
||||
data_version = 2
|
||||
|
||||
web = NoitaWeb()
|
||||
@@ -40,21 +43,21 @@ class NoitaWorld(World):
|
||||
raise Exception("Noita yaml's slot name has invalid character(s).")
|
||||
|
||||
# Returned items will be sent over to the client
|
||||
def fill_slot_data(self):
|
||||
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests",
|
||||
"pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price")
|
||||
|
||||
def create_regions(self) -> None:
|
||||
Regions.create_all_regions_and_connections(self.multiworld, self.player)
|
||||
Events.create_all_events(self.multiworld, self.player)
|
||||
regions.create_all_regions_and_connections(self)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return Items.create_item(self.player, name)
|
||||
return items.create_item(self.player, name)
|
||||
|
||||
def create_items(self) -> None:
|
||||
Items.create_all_items(self.multiworld, self.player)
|
||||
items.create_all_items(self)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
Rules.create_all_rules(self.multiworld, self.player)
|
||||
rules.create_all_rules(self)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(Items.filler_items)
|
||||
return self.random.choice(items.filler_items)
|
||||
|
||||
43
worlds/noita/events.py
Normal file
43
worlds/noita/events.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from BaseClasses import Item, ItemClassification, Location, Region
|
||||
from . import items, locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def create_event(player: int, name: str) -> Item:
|
||||
return items.NoitaItem(name, ItemClassification.progression, None, player)
|
||||
|
||||
|
||||
def create_location(player: int, name: str, region: Region) -> Location:
|
||||
return locations.NoitaLocation(player, name, None, region)
|
||||
|
||||
|
||||
def create_locked_location_event(player: int, region: Region, item: str) -> Location:
|
||||
new_location = create_location(player, item, region)
|
||||
new_location.place_locked_item(create_event(player, item))
|
||||
|
||||
region.locations.append(new_location)
|
||||
return new_location
|
||||
|
||||
|
||||
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
|
||||
for region_name, event in event_locks.items():
|
||||
region = created_regions[region_name]
|
||||
create_locked_location_event(world.player, region, event)
|
||||
|
||||
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
|
||||
|
||||
|
||||
# Maps region names to event names
|
||||
event_locks: Dict[str, str] = {
|
||||
"The Work": "Victory",
|
||||
"Mines": "Portal to Holy Mountain 1",
|
||||
"Coal Pits": "Portal to Holy Mountain 2",
|
||||
"Snowy Depths": "Portal to Holy Mountain 3",
|
||||
"Hiisi Base": "Portal to Holy Mountain 4",
|
||||
"Underground Jungle": "Portal to Holy Mountain 5",
|
||||
"The Vault": "Portal to Holy Mountain 6",
|
||||
"Temple of the Art": "Portal to Holy Mountain 7",
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
import itertools
|
||||
from collections import Counter
|
||||
from typing import Dict, List, NamedTuple, Set
|
||||
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
else:
|
||||
NoitaWorld = object
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
@@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
|
||||
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
|
||||
|
||||
|
||||
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
|
||||
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
|
||||
filler_pool = weights.copy()
|
||||
if multiworld.bad_effects[player].value == 0:
|
||||
if not world.options.bad_effects:
|
||||
del filler_pool["Trap"]
|
||||
|
||||
return multiworld.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=count)
|
||||
return world.random.choices(population=list(filler_pool.keys()),
|
||||
weights=list(filler_pool.values()),
|
||||
k=count)
|
||||
|
||||
|
||||
def create_all_items(multiworld: MultiWorld, player: int) -> None:
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
def create_all_items(world: NoitaWorld) -> None:
|
||||
player = world.player
|
||||
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool = (
|
||||
create_fixed_item_pool()
|
||||
+ create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player])
|
||||
+ create_spatial_awareness_item(multiworld.bosses_as_checks[player])
|
||||
+ create_kantele(multiworld.victory_condition[player])
|
||||
+ create_orb_items(world.options.victory_condition, world.options.extra_orbs)
|
||||
+ create_spatial_awareness_item(world.options.bosses_as_checks)
|
||||
+ create_kantele(world.options.victory_condition)
|
||||
)
|
||||
|
||||
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
|
||||
# 39 is the number of shop-valid items we need to guarantee
|
||||
if len(itempool) < 39:
|
||||
itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
|
||||
itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool))
|
||||
# this is so that it passes tests and gens if you have minimal locations and only one player
|
||||
if multiworld.players == 1:
|
||||
for location in multiworld.get_unfilled_locations(player):
|
||||
if world.multiworld.players == 1:
|
||||
for location in world.multiworld.get_unfilled_locations(player):
|
||||
if "Shop Item" in location.name:
|
||||
location.item = create_item(player, itempool.pop())
|
||||
locations_to_fill = len(multiworld.get_unfilled_locations(player))
|
||||
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
|
||||
|
||||
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
|
||||
multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
|
||||
world.multiworld.itempool += [create_item(player, name) for name in itempool]
|
||||
|
||||
|
||||
# 110000 - 110032
|
||||
@@ -201,11 +201,10 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
|
||||
}
|
||||
|
||||
|
||||
# Iterating the hidden chest and pedestal locations here to avoid clutter above
|
||||
def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
|
||||
if locinfo.ltype in ["chest", "pedestal"]:
|
||||
return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
|
||||
return {locname: locinfo.id}
|
||||
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
|
||||
if amt == 1:
|
||||
return {location_name: base_id}
|
||||
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
|
||||
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
|
||||
@@ -215,9 +214,11 @@ location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
for location_group in location_region_mapping.values():
|
||||
for locname, locinfo in location_group.items():
|
||||
location_name_to_id.update(generate_location_entries(locname, locinfo))
|
||||
if locinfo.ltype in ["chest", "pedestal"]:
|
||||
for i in range(20):
|
||||
location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
|
||||
else:
|
||||
location_name_groups[locinfo.ltype].add(locname)
|
||||
# Iterating the hidden chest and pedestal locations here to avoid clutter above
|
||||
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
|
||||
entries = make_location_range(locname, locinfo.id, amount)
|
||||
|
||||
location_name_to_id.update(entries)
|
||||
location_name_groups[locinfo.ltype].update(entries.keys())
|
||||
|
||||
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Dict
|
||||
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
|
||||
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class PathOption(Choice):
|
||||
@@ -99,16 +99,16 @@ class ShopPrice(Choice):
|
||||
default = 100
|
||||
|
||||
|
||||
noita_options: Dict[str, AssembleOptions] = {
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
"death_link": DeathLink,
|
||||
"bad_effects": Traps,
|
||||
"victory_condition": VictoryCondition,
|
||||
"path_option": PathOption,
|
||||
"hidden_chests": HiddenChests,
|
||||
"pedestal_checks": PedestalChecks,
|
||||
"orbs_as_checks": OrbsAsChecks,
|
||||
"bosses_as_checks": BossesAsChecks,
|
||||
"extra_orbs": ExtraOrbs,
|
||||
"shop_price": ShopPrice,
|
||||
}
|
||||
@dataclass
|
||||
class NoitaOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
death_link: DeathLink
|
||||
bad_effects: Traps
|
||||
victory_condition: VictoryCondition
|
||||
path_option: PathOption
|
||||
hidden_chests: HiddenChests
|
||||
pedestal_checks: PedestalChecks
|
||||
orbs_as_checks: OrbsAsChecks
|
||||
bosses_as_checks: BossesAsChecks
|
||||
extra_orbs: ExtraOrbs
|
||||
shop_price: ShopPrice
|
||||
@@ -1,48 +1,43 @@
|
||||
# Regions are areas in your game that you travel to.
|
||||
from typing import Dict, Set, List
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Entrance, MultiWorld, Region
|
||||
from . import Locations
|
||||
from BaseClasses import Entrance, Region
|
||||
from . import locations
|
||||
from .events import create_all_events
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
def add_location(player: int, loc_name: str, id: int, region: Region) -> None:
|
||||
location = Locations.NoitaLocation(player, loc_name, id, region)
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None:
|
||||
locations = Locations.location_region_mapping.get(region.name, {})
|
||||
for location_name, location_data in locations.items():
|
||||
def create_locations(world: "NoitaWorld", region: Region) -> None:
|
||||
locs = locations.location_region_mapping.get(region.name, {})
|
||||
for location_name, location_data in locs.items():
|
||||
location_type = location_data.ltype
|
||||
flag = location_data.flag
|
||||
|
||||
opt_orbs = multiworld.orbs_as_checks[player].value
|
||||
opt_bosses = multiworld.bosses_as_checks[player].value
|
||||
opt_paths = multiworld.path_option[player].value
|
||||
opt_num_chests = multiworld.hidden_chests[player].value
|
||||
opt_num_pedestals = multiworld.pedestal_checks[player].value
|
||||
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
|
||||
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
|
||||
amount = 0
|
||||
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
|
||||
amount = 1
|
||||
elif location_type == "chest" and flag <= world.options.path_option:
|
||||
amount = world.options.hidden_chests.value
|
||||
elif location_type == "pedestal" and flag <= world.options.path_option:
|
||||
amount = world.options.pedestal_checks.value
|
||||
|
||||
is_orb_allowed = location_type == "orb" and flag <= opt_orbs
|
||||
is_boss_allowed = location_type == "boss" and flag <= opt_bosses
|
||||
if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
|
||||
add_location(player, location_name, location_data.id, region)
|
||||
elif location_type == "chest" and flag <= opt_paths:
|
||||
for i in range(opt_num_chests):
|
||||
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
|
||||
elif location_type == "pedestal" and flag <= opt_paths:
|
||||
for i in range(opt_num_pedestals):
|
||||
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
|
||||
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
|
||||
locations.NoitaLocation)
|
||||
|
||||
|
||||
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
|
||||
def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
|
||||
new_region = Region(region_name, player, multiworld)
|
||||
add_locations(multiworld, player, new_region)
|
||||
def create_region(world: "NoitaWorld", region_name: str) -> Region:
|
||||
new_region = Region(region_name, world.player, world.multiworld)
|
||||
create_locations(world, new_region)
|
||||
return new_region
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
|
||||
return {name: create_region(multiworld, player, name) for name in noita_regions}
|
||||
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
|
||||
return {name: create_region(world, name) for name in noita_regions}
|
||||
|
||||
|
||||
# An "Entrance" is really just a connection between two regions
|
||||
@@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None:
|
||||
|
||||
|
||||
# Creates all regions and connections. Called from NoitaWorld.
|
||||
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
|
||||
created_regions = create_regions(multiworld, player)
|
||||
create_connections(player, created_regions)
|
||||
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
|
||||
created_regions = create_regions(world)
|
||||
create_connections(world.player, created_regions)
|
||||
create_all_events(world, created_regions)
|
||||
|
||||
multiworld.regions += created_regions.values()
|
||||
world.multiworld.regions += created_regions.values()
|
||||
|
||||
|
||||
# Oh, what a tangled web we weave
|
||||
172
worlds/noita/rules.py
Normal file
172
worlds/noita/rules.py
Normal file
@@ -0,0 +1,172 @@
|
||||
from typing import List, NamedTuple, Set, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from . import items, locations
|
||||
from .options import BossesAsChecks, VictoryCondition
|
||||
from worlds.generic import Rules as GenericRules
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import NoitaWorld
|
||||
|
||||
|
||||
class EntranceLock(NamedTuple):
|
||||
source: str
|
||||
destination: str
|
||||
event: str
|
||||
items_needed: int
|
||||
|
||||
|
||||
entrance_locks: List[EntranceLock] = [
|
||||
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
|
||||
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
|
||||
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
|
||||
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
|
||||
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
|
||||
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
|
||||
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
|
||||
]
|
||||
|
||||
|
||||
holy_mountain_regions: List[str] = [
|
||||
"Coal Pits Holy Mountain",
|
||||
"Snowy Depths Holy Mountain",
|
||||
"Hiisi Base Holy Mountain",
|
||||
"Underground Jungle Holy Mountain",
|
||||
"Vault Holy Mountain",
|
||||
"Temple of the Art Holy Mountain",
|
||||
"Laboratory Holy Mountain",
|
||||
]
|
||||
|
||||
|
||||
wand_tiers: List[str] = [
|
||||
"Wand (Tier 1)", # Coal Pits
|
||||
"Wand (Tier 2)", # Snowy Depths
|
||||
"Wand (Tier 3)", # Hiisi Base
|
||||
"Wand (Tier 4)", # Underground Jungle
|
||||
"Wand (Tier 5)", # The Vault
|
||||
"Wand (Tier 6)", # Temple of the Art
|
||||
]
|
||||
|
||||
|
||||
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
|
||||
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
|
||||
"Powder Pouch"}
|
||||
|
||||
perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
|
||||
|
||||
|
||||
# ----------------
|
||||
# Helper Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return sum(state.count(perk, player) for perk in perk_list) >= amount
|
||||
|
||||
|
||||
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
|
||||
return state.count("Orb", player) >= amount
|
||||
|
||||
|
||||
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
|
||||
for shop_location in shop_locations:
|
||||
location = world.multiworld.get_location(shop_location, world.player)
|
||||
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Rule Functions
|
||||
# ----------------
|
||||
|
||||
|
||||
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
|
||||
# def ban_items_from_shops(world: "NoitaWorld") -> None:
|
||||
# for location_name in Locations.location_name_to_id.keys():
|
||||
# if "Shop Item" in location_name:
|
||||
# forbid_items_at_location(world, location_name, items_hidden_from_shops)
|
||||
def ban_items_from_shops(world: "NoitaWorld") -> None:
|
||||
forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops)
|
||||
|
||||
|
||||
# Prevent high tier wands from appearing in early Holy Mountain shops
|
||||
def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
|
||||
for i, region_name in enumerate(holy_mountain_regions):
|
||||
wands_to_forbid = set(wand_tiers[i+1:])
|
||||
|
||||
locations_in_region = set(locations.location_region_mapping[region_name].keys())
|
||||
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
|
||||
|
||||
# Prevent high tier wands from appearing in the Secret shop
|
||||
wands_to_forbid = set(wand_tiers[3:])
|
||||
locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys())
|
||||
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
|
||||
|
||||
|
||||
def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
|
||||
for lock in entrance_locks:
|
||||
location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
|
||||
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
|
||||
|
||||
|
||||
def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
for lock in entrance_locks:
|
||||
location = world.multiworld.get_location(lock.event, world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_greed_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_pure_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed)
|
||||
)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
location.access_rule = lambda state, items_needed=lock.items_needed: (
|
||||
has_perk_count(state, world.player, items_needed//2) and
|
||||
has_orb_count(state, world.player, items_needed * 3)
|
||||
)
|
||||
|
||||
|
||||
def biome_unlock_conditions(world: "NoitaWorld"):
|
||||
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
|
||||
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
|
||||
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
|
||||
for entrance in lukki_entrances:
|
||||
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
|
||||
state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in magical_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
|
||||
for entrance in wizard_entrances:
|
||||
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
|
||||
|
||||
|
||||
def victory_unlock_conditions(world: "NoitaWorld") -> None:
|
||||
victory_condition = world.options.victory_condition.value
|
||||
victory_location = world.multiworld.get_location("Victory", world.player)
|
||||
|
||||
if victory_condition == VictoryCondition.option_pure_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
|
||||
elif victory_condition == VictoryCondition.option_peaceful_ending:
|
||||
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
|
||||
|
||||
|
||||
# ----------------
|
||||
# Main Function
|
||||
# ----------------
|
||||
|
||||
|
||||
def create_all_rules(world: "NoitaWorld") -> None:
|
||||
if world.multiworld.players > 1:
|
||||
ban_items_from_shops(world)
|
||||
ban_early_high_tier_wands(world)
|
||||
lock_holy_mountains_into_spheres(world)
|
||||
holy_mountain_unlock_conditions(world)
|
||||
biome_unlock_conditions(world)
|
||||
victory_unlock_conditions(world)
|
||||
|
||||
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
|
||||
if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
|
||||
toveri = world.multiworld.get_location("Toveri", world.player)
|
||||
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)
|
||||
@@ -1271,7 +1271,7 @@ class LogicTricks(OptionList):
|
||||
https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py
|
||||
"""
|
||||
display_name = "Logic Tricks"
|
||||
valid_keys = frozenset(normalized_name_tricks)
|
||||
valid_keys = tuple(normalized_name_tricks.keys())
|
||||
valid_keys_casefold = True
|
||||
|
||||
|
||||
|
||||
@@ -114,8 +114,10 @@ LOCATION_GROUPS = {
|
||||
"Littleroot Town - S.S. Ticket from Norman",
|
||||
"SS Tidal - Hidden Item in Lower Deck Trash Can",
|
||||
"SS Tidal - TM49 from Thief",
|
||||
"Safari Zone NE - Item on Ledge",
|
||||
"Safari Zone NE - Hidden Item North",
|
||||
"Safari Zone NE - Hidden Item East",
|
||||
"Safari Zone SE - Item in Grass",
|
||||
"Safari Zone SE - Hidden Item in South Grass 1",
|
||||
"Safari Zone SE - Hidden Item in South Grass 2",
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from .locations import location_data
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
BANK_EXCHANGE_RATE = 100000000
|
||||
BANK_EXCHANGE_RATE = 50000000
|
||||
|
||||
DATA_LOCATIONS = {
|
||||
"ItemIndex": (0x1A6E, 0x02),
|
||||
|
||||
@@ -42,7 +42,7 @@ item_table = {
|
||||
"Repel": ItemData(30, ItemClassification.filler, ["Consumables"]),
|
||||
"Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils", "Key Items"]),
|
||||
"Fire Stone": ItemData(32, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
|
||||
"Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones" "Key Items"]),
|
||||
"Thunder Stone": ItemData(33, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
|
||||
"Water Stone": ItemData(34, ItemClassification.progression_skip_balancing, ["Unique", "Evolution Stones", "Key Items"]),
|
||||
"HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
|
||||
@@ -16,6 +16,21 @@ generic_item_table = {
|
||||
"1Up Mushroom": 3626184
|
||||
}
|
||||
|
||||
action_item_table = {
|
||||
"Double Jump": 3626185,
|
||||
"Triple Jump": 3626186,
|
||||
"Long Jump": 3626187,
|
||||
"Backflip": 3626188,
|
||||
"Side Flip": 3626189,
|
||||
"Wall Kick": 3626190,
|
||||
"Dive": 3626191,
|
||||
"Ground Pound": 3626192,
|
||||
"Kick": 3626193,
|
||||
"Climb": 3626194,
|
||||
"Ledge Grab": 3626195
|
||||
}
|
||||
|
||||
|
||||
cannon_item_table = {
|
||||
"Cannon Unlock BoB": 3626200,
|
||||
"Cannon Unlock WF": 3626201,
|
||||
@@ -29,4 +44,4 @@ cannon_item_table = {
|
||||
"Cannon Unlock RR": 3626214
|
||||
}
|
||||
|
||||
item_table = {**generic_item_table, **cannon_item_table}
|
||||
item_table = {**generic_item_table, **action_item_table, **cannon_item_table}
|
||||
@@ -1,9 +1,10 @@
|
||||
import typing
|
||||
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
|
||||
|
||||
from .Items import action_item_table
|
||||
|
||||
class EnableCoinStars(DefaultOnToggle):
|
||||
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything"""
|
||||
"""Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything.
|
||||
Removes 15 locations from the pool."""
|
||||
display_name = "Enable 100 Coin Stars"
|
||||
|
||||
|
||||
@@ -13,56 +14,63 @@ class StrictCapRequirements(DefaultOnToggle):
|
||||
|
||||
|
||||
class StrictCannonRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy
|
||||
Checks are enabled"""
|
||||
"""If disabled, Stars that expect cannons may have to be acquired without them.
|
||||
Has no effect if Buddy Checks and Move Randomizer are disabled"""
|
||||
display_name = "Strict Cannon Requirements"
|
||||
|
||||
|
||||
class FirstBowserStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door to Bowser in the Dark World"""
|
||||
"""What percent of the total stars are required at the Star Door to Bowser in the Dark World"""
|
||||
display_name = "First Star Door Cost %"
|
||||
range_start = 0
|
||||
range_end = 50
|
||||
default = 8
|
||||
range_end = 40
|
||||
default = 7
|
||||
|
||||
|
||||
class BasementStarDoorCost(Range):
|
||||
"""How many stars are required at the Star Door in the Basement"""
|
||||
"""What percent of the total stars are required at the Star Door in the Basement"""
|
||||
display_name = "Basement Star Door %"
|
||||
range_start = 0
|
||||
range_end = 70
|
||||
default = 30
|
||||
range_end = 50
|
||||
default = 25
|
||||
|
||||
|
||||
class SecondFloorStarDoorCost(Range):
|
||||
"""How many stars are required to access the third floor"""
|
||||
"""What percent of the total stars are required to access the third floor"""
|
||||
display_name = 'Second Floor Star Door %'
|
||||
range_start = 0
|
||||
range_end = 90
|
||||
default = 50
|
||||
range_end = 70
|
||||
default = 42
|
||||
|
||||
|
||||
class MIPS1Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the first time"""
|
||||
"""What percent of the total stars are required to spawn MIPS the first time"""
|
||||
display_name = "MIPS 1 Star %"
|
||||
range_start = 0
|
||||
range_end = 40
|
||||
default = 15
|
||||
range_end = 35
|
||||
default = 12
|
||||
|
||||
|
||||
class MIPS2Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the second time."""
|
||||
"""What percent of the total stars are required to spawn MIPS the second time."""
|
||||
display_name = "MIPS 2 Star %"
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
range_end = 70
|
||||
default = 42
|
||||
|
||||
|
||||
class StarsToFinish(Range):
|
||||
"""How many stars are required at the infinite stairs"""
|
||||
display_name = "Endless Stairs Stars"
|
||||
"""What percent of the total stars are required at the infinite stairs"""
|
||||
display_name = "Endless Stairs Star %"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 70
|
||||
range_end = 90
|
||||
default = 58
|
||||
|
||||
|
||||
class AmountOfStars(Range):
|
||||
"""How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set"""
|
||||
"""How many stars exist.
|
||||
If there aren't enough locations to hold the given total, the total will be reduced."""
|
||||
display_name = "Total Power Stars"
|
||||
range_start = 35
|
||||
range_end = 120
|
||||
default = 120
|
||||
@@ -83,11 +91,13 @@ class BuddyChecks(Toggle):
|
||||
|
||||
|
||||
class ExclamationBoxes(Choice):
|
||||
"""Include 1Up Exclamation Boxes during randomization"""
|
||||
"""Include 1Up Exclamation Boxes during randomization.
|
||||
Adds 29 locations to the pool."""
|
||||
display_name = "Randomize 1Up !-Blocks"
|
||||
option_Off = 0
|
||||
option_1Ups_Only = 1
|
||||
|
||||
|
||||
class CompletionType(Choice):
|
||||
"""Set goal for game completion"""
|
||||
display_name = "Completion Goal"
|
||||
@@ -96,17 +106,32 @@ class CompletionType(Choice):
|
||||
|
||||
|
||||
class ProgressiveKeys(DefaultOnToggle):
|
||||
"""Keys will first grant you access to the Basement, then to the Secound Floor"""
|
||||
"""Keys will first grant you access to the Basement, then to the Second Floor"""
|
||||
display_name = "Progressive Keys"
|
||||
|
||||
class StrictMoveRequirements(DefaultOnToggle):
|
||||
"""If disabled, Stars that expect certain moves may have to be acquired without them. Only makes a difference
|
||||
if Move Randomization is enabled"""
|
||||
display_name = "Strict Move Requirements"
|
||||
|
||||
def getMoveRandomizerOption(action: str):
|
||||
class MoveRandomizerOption(Toggle):
|
||||
"""Mario is unable to perform this action until a corresponding item is picked up.
|
||||
This option is incompatible with builds using a 'nomoverando' branch."""
|
||||
display_name = f"Randomize {action}"
|
||||
return MoveRandomizerOption
|
||||
|
||||
|
||||
sm64_options: typing.Dict[str, type(Option)] = {
|
||||
"AreaRandomizer": AreaRandomizer,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
"ExclamationBoxes": ExclamationBoxes,
|
||||
"ProgressiveKeys": ProgressiveKeys,
|
||||
"EnableCoinStars": EnableCoinStars,
|
||||
"AmountOfStars": AmountOfStars,
|
||||
"StrictCapRequirements": StrictCapRequirements,
|
||||
"StrictCannonRequirements": StrictCannonRequirements,
|
||||
"StrictMoveRequirements": StrictMoveRequirements,
|
||||
"AmountOfStars": AmountOfStars,
|
||||
"FirstBowserStarDoorCost": FirstBowserStarDoorCost,
|
||||
"BasementStarDoorCost": BasementStarDoorCost,
|
||||
"SecondFloorStarDoorCost": SecondFloorStarDoorCost,
|
||||
@@ -114,7 +139,10 @@ sm64_options: typing.Dict[str, type(Option)] = {
|
||||
"MIPS2Cost": MIPS2Cost,
|
||||
"StarsToFinish": StarsToFinish,
|
||||
"death_link": DeathLink,
|
||||
"BuddyChecks": BuddyChecks,
|
||||
"ExclamationBoxes": ExclamationBoxes,
|
||||
"CompletionType" : CompletionType,
|
||||
"CompletionType": CompletionType,
|
||||
}
|
||||
|
||||
for action in action_item_table:
|
||||
# HACK: Disable randomization of double jump
|
||||
if action == 'Double Jump': continue
|
||||
sm64_options[f"MoveRandomizer{action.replace(' ','')}"] = getMoveRandomizerOption(action)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import typing
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location
|
||||
@@ -8,7 +7,8 @@ from .Locations import SM64Location, location_table, locBoB_table, locWhomp_tabl
|
||||
locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \
|
||||
locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \
|
||||
locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \
|
||||
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
|
||||
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
|
||||
|
||||
|
||||
class SM64Levels(int, Enum):
|
||||
BOB_OMB_BATTLEFIELD = 91
|
||||
@@ -55,7 +55,7 @@ sm64_level_to_paintings: typing.Dict[SM64Levels, str] = {
|
||||
SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock",
|
||||
SM64Levels.RAINBOW_RIDE: "Rainbow Ride"
|
||||
}
|
||||
sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() }
|
||||
sm64_paintings_to_level = {painting: level for (level, painting) in sm64_level_to_paintings.items() }
|
||||
|
||||
# sm64secrets is a dict of secret areas, same format as sm64paintings
|
||||
sm64_level_to_secrets: typing.Dict[SM64Levels, str] = {
|
||||
@@ -68,152 +68,163 @@ sm64_level_to_secrets: typing.Dict[SM64Levels, str] = {
|
||||
SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea",
|
||||
SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow"
|
||||
}
|
||||
sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() }
|
||||
sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secrets.items() }
|
||||
|
||||
sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level }
|
||||
sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets }
|
||||
sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level }
|
||||
sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets }
|
||||
|
||||
def create_regions(world: MultiWorld, player: int):
|
||||
regSS = Region("Menu", player, world, "Castle Area")
|
||||
create_default_locs(regSS, locSS_table, player)
|
||||
create_default_locs(regSS, locSS_table)
|
||||
world.regions.append(regSS)
|
||||
|
||||
regBoB = create_region("Bob-omb Battlefield", player, world)
|
||||
create_default_locs(regBoB, locBoB_table, player)
|
||||
create_locs(regBoB, "BoB: Big Bob-Omb on the Summit", "BoB: Footrace with Koopa The Quick",
|
||||
"BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy")
|
||||
create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regBoB.locations.append(SM64Location(player, "BoB: 100 Coins", location_table["BoB: 100 Coins"], regBoB))
|
||||
world.regions.append(regBoB)
|
||||
create_locs(regBoB, "BoB: 100 Coins")
|
||||
|
||||
regWhomp = create_region("Whomp's Fortress", player, world)
|
||||
create_default_locs(regWhomp, locWhomp_table, player)
|
||||
create_locs(regWhomp, "WF: Chip Off Whomp's Block", "WF: Shoot into the Wild Blue", "WF: Red Coins on the Floating Isle",
|
||||
"WF: Fall onto the Caged Island", "WF: Blast Away the Wall")
|
||||
create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regWhomp.locations.append(SM64Location(player, "WF: 100 Coins", location_table["WF: 100 Coins"], regWhomp))
|
||||
world.regions.append(regWhomp)
|
||||
create_locs(regWhomp, "WF: 100 Coins")
|
||||
|
||||
regJRB = create_region("Jolly Roger Bay", player, world)
|
||||
create_default_locs(regJRB, locJRB_table, player)
|
||||
create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave",
|
||||
"JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy")
|
||||
jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regJRB.locations.append(SM64Location(player, "JRB: 100 Coins", location_table["JRB: 100 Coins"], regJRB))
|
||||
world.regions.append(regJRB)
|
||||
create_locs(jrb_upper, "JRB: 100 Coins")
|
||||
|
||||
regCCM = create_region("Cool, Cool Mountain", player, world)
|
||||
create_default_locs(regCCM, locCCM_table, player)
|
||||
create_default_locs(regCCM, locCCM_table)
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regCCM.locations.append(SM64Location(player, "CCM: 100 Coins", location_table["CCM: 100 Coins"], regCCM))
|
||||
world.regions.append(regCCM)
|
||||
create_locs(regCCM, "CCM: 100 Coins")
|
||||
|
||||
regBBH = create_region("Big Boo's Haunt", player, world)
|
||||
create_default_locs(regBBH, locBBH_table, player)
|
||||
create_locs(regBBH, "BBH: Go on a Ghost Hunt", "BBH: Ride Big Boo's Merry-Go-Round",
|
||||
"BBH: Secret of the Haunted Books", "BBH: Seek the 8 Red Coins")
|
||||
bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room")
|
||||
create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regBBH.locations.append(SM64Location(player, "BBH: 100 Coins", location_table["BBH: 100 Coins"], regBBH))
|
||||
world.regions.append(regBBH)
|
||||
create_locs(regBBH, "BBH: 100 Coins")
|
||||
|
||||
regPSS = create_region("The Princess's Secret Slide", player, world)
|
||||
create_default_locs(regPSS, locPSS_table, player)
|
||||
world.regions.append(regPSS)
|
||||
create_default_locs(regPSS, locPSS_table)
|
||||
|
||||
regSA = create_region("The Secret Aquarium", player, world)
|
||||
create_default_locs(regSA, locSA_table, player)
|
||||
world.regions.append(regSA)
|
||||
create_default_locs(regSA, locSA_table)
|
||||
|
||||
regTotWC = create_region("Tower of the Wing Cap", player, world)
|
||||
create_default_locs(regTotWC, locTotWC_table, player)
|
||||
world.regions.append(regTotWC)
|
||||
create_default_locs(regTotWC, locTotWC_table)
|
||||
|
||||
regBitDW = create_region("Bowser in the Dark World", player, world)
|
||||
create_default_locs(regBitDW, locBitDW_table, player)
|
||||
world.regions.append(regBitDW)
|
||||
create_default_locs(regBitDW, locBitDW_table)
|
||||
|
||||
regBasement = create_region("Basement", player, world)
|
||||
world.regions.append(regBasement)
|
||||
create_region("Basement", player, world)
|
||||
|
||||
regHMC = create_region("Hazy Maze Cave", player, world)
|
||||
create_default_locs(regHMC, locHMC_table, player)
|
||||
create_locs(regHMC, "HMC: Swimming Beast in the Cavern", "HMC: Metal-Head Mario Can Move!",
|
||||
"HMC: Watch for Rolling Rocks", "HMC: Navigating the Toxic Maze","HMC: 1Up Block Past Rolling Rocks")
|
||||
hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins")
|
||||
create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regHMC.locations.append(SM64Location(player, "HMC: 100 Coins", location_table["HMC: 100 Coins"], regHMC))
|
||||
world.regions.append(regHMC)
|
||||
create_locs(hmc_red_coin_area, "HMC: 100 Coins")
|
||||
|
||||
regLLL = create_region("Lethal Lava Land", player, world)
|
||||
create_default_locs(regLLL, locLLL_table, player)
|
||||
create_locs(regLLL, "LLL: Boil the Big Bully", "LLL: Bully the Bullies",
|
||||
"LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling")
|
||||
create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regLLL.locations.append(SM64Location(player, "LLL: 100 Coins", location_table["LLL: 100 Coins"], regLLL))
|
||||
world.regions.append(regLLL)
|
||||
create_locs(regLLL, "LLL: 100 Coins")
|
||||
|
||||
regSSL = create_region("Shifting Sand Land", player, world)
|
||||
create_default_locs(regSSL, locSSL_table, player)
|
||||
create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid", "SSL: Inside the Ancient Pyramid",
|
||||
"SSL: Free Flying for 8 Red Coins", "SSL: Bob-omb Buddy",
|
||||
"SSL: 1Up Block Outside Pyramid", "SSL: 1Up Block Pyramid Left Path", "SSL: 1Up Block Pyramid Back")
|
||||
create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regSSL.locations.append(SM64Location(player, "SSL: 100 Coins", location_table["SSL: 100 Coins"], regSSL))
|
||||
world.regions.append(regSSL)
|
||||
create_locs(regSSL, "SSL: 100 Coins")
|
||||
|
||||
regDDD = create_region("Dire, Dire Docks", player, world)
|
||||
create_default_locs(regDDD, locDDD_table, player)
|
||||
create_locs(regDDD, "DDD: Board Bowser's Sub", "DDD: Chests in the Current", "DDD: Through the Jet Stream",
|
||||
"DDD: The Manta Ray's Reward", "DDD: Collect the Caps...")
|
||||
ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regDDD.locations.append(SM64Location(player, "DDD: 100 Coins", location_table["DDD: 100 Coins"], regDDD))
|
||||
world.regions.append(regDDD)
|
||||
create_locs(ddd_moving_poles, "DDD: 100 Coins")
|
||||
|
||||
regCotMC = create_region("Cavern of the Metal Cap", player, world)
|
||||
create_default_locs(regCotMC, locCotMC_table, player)
|
||||
world.regions.append(regCotMC)
|
||||
create_default_locs(regCotMC, locCotMC_table)
|
||||
|
||||
regVCutM = create_region("Vanish Cap under the Moat", player, world)
|
||||
create_default_locs(regVCutM, locVCutM_table, player)
|
||||
world.regions.append(regVCutM)
|
||||
create_default_locs(regVCutM, locVCutM_table)
|
||||
|
||||
regBitFS = create_region("Bowser in the Fire Sea", player, world)
|
||||
create_default_locs(regBitFS, locBitFS_table, player)
|
||||
world.regions.append(regBitFS)
|
||||
create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys())
|
||||
|
||||
regFloor2 = create_region("Second Floor", player, world)
|
||||
world.regions.append(regFloor2)
|
||||
create_region("Second Floor", player, world)
|
||||
|
||||
regSL = create_region("Snowman's Land", player, world)
|
||||
create_default_locs(regSL, locSL_table, player)
|
||||
create_default_locs(regSL, locSL_table)
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regSL.locations.append(SM64Location(player, "SL: 100 Coins", location_table["SL: 100 Coins"], regSL))
|
||||
world.regions.append(regSL)
|
||||
create_locs(regSL, "SL: 100 Coins")
|
||||
|
||||
regWDW = create_region("Wet-Dry World", player, world)
|
||||
create_default_locs(regWDW, locWDW_table, player)
|
||||
create_locs(regWDW, "WDW: Express Elevator--Hurry Up!")
|
||||
wdw_top = create_subregion(regWDW, "WDW: Top", "WDW: Shocking Arrow Lifts!", "WDW: Top o' the Town",
|
||||
"WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy")
|
||||
create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regWDW.locations.append(SM64Location(player, "WDW: 100 Coins", location_table["WDW: 100 Coins"], regWDW))
|
||||
world.regions.append(regWDW)
|
||||
create_locs(wdw_top, "WDW: 100 Coins")
|
||||
|
||||
regTTM = create_region("Tall, Tall Mountain", player, world)
|
||||
create_default_locs(regTTM, locTTM_table, player)
|
||||
ttm_middle = create_subregion(regTTM, "TTM: Middle", "TTM: Scary 'Shrooms, Red Coins", "TTM: Blast to the Lonely Mushroom",
|
||||
"TTM: Bob-omb Buddy", "TTM: 1Up Block on Red Mushroom")
|
||||
ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage",
|
||||
"TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM))
|
||||
world.regions.append(regTTM)
|
||||
create_locs(ttm_top, "TTM: 100 Coins")
|
||||
|
||||
regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world)
|
||||
create_default_locs(regTHIT, locTHI_table, player)
|
||||
create_region("Tiny-Huge Island (Huge)", player, world)
|
||||
create_region("Tiny-Huge Island (Tiny)", player, world)
|
||||
regTHI = create_region("Tiny-Huge Island", player, world)
|
||||
create_locs(regTHI, "THI: The Tip Top of the Huge Island", "THI: 1Up Block THI Small near Start")
|
||||
thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick",
|
||||
"THI: Five Itty Bitty Secrets", "THI: Wiggler's Red Coins", "THI: Bob-omb Buddy",
|
||||
"THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area")
|
||||
thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT))
|
||||
world.regions.append(regTHIT)
|
||||
regTHIH = create_region("Tiny-Huge Island (Huge)", player, world)
|
||||
world.regions.append(regTHIH)
|
||||
create_locs(thi_large_top, "THI: 100 Coins")
|
||||
|
||||
regFloor3 = create_region("Third Floor", player, world)
|
||||
world.regions.append(regFloor3)
|
||||
|
||||
regTTC = create_region("Tick Tock Clock", player, world)
|
||||
create_default_locs(regTTC, locTTC_table, player)
|
||||
create_locs(regTTC, "TTC: Stop Time for Red Coins")
|
||||
ttc_lower = create_subregion(regTTC, "TTC: Lower", "TTC: Roll into the Cage", "TTC: Get a Hand", "TTC: 1Up Block Midway Up")
|
||||
ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums")
|
||||
ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regTTC.locations.append(SM64Location(player, "TTC: 100 Coins", location_table["TTC: 100 Coins"], regTTC))
|
||||
world.regions.append(regTTC)
|
||||
create_locs(ttc_top, "TTC: 100 Coins")
|
||||
|
||||
regRR = create_region("Rainbow Ride", player, world)
|
||||
create_default_locs(regRR, locRR_table, player)
|
||||
create_locs(regRR, "RR: Swingin' in the Breeze", "RR: Tricky Triangles!",
|
||||
"RR: 1Up Block Top of Red Coin Maze", "RR: 1Up Block Under Fly Guy", "RR: Bob-omb Buddy")
|
||||
rr_maze = create_subregion(regRR, "RR: Maze", "RR: Coins Amassed in a Maze")
|
||||
create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow")
|
||||
create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky")
|
||||
if (world.EnableCoinStars[player].value):
|
||||
regRR.locations.append(SM64Location(player, "RR: 100 Coins", location_table["RR: 100 Coins"], regRR))
|
||||
world.regions.append(regRR)
|
||||
create_locs(rr_maze, "RR: 100 Coins")
|
||||
|
||||
regWMotR = create_region("Wing Mario over the Rainbow", player, world)
|
||||
create_default_locs(regWMotR, locWMotR_table, player)
|
||||
world.regions.append(regWMotR)
|
||||
create_default_locs(regWMotR, locWMotR_table)
|
||||
|
||||
regBitS = create_region("Bowser in the Sky", player, world)
|
||||
create_default_locs(regBitS, locBitS_table, player)
|
||||
world.regions.append(regBitS)
|
||||
create_locs(regBitS, "Bowser in the Sky 1Up Block")
|
||||
create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins")
|
||||
|
||||
|
||||
def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None):
|
||||
@@ -227,9 +238,30 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru
|
||||
sourceRegion.exits.append(connection)
|
||||
connection.connect(targetRegion)
|
||||
|
||||
def create_region(name: str, player: int, world: MultiWorld) -> Region:
|
||||
return Region(name, player, world)
|
||||
|
||||
def create_default_locs(reg: Region, locs, player):
|
||||
reg_names = [name for name, id in locs.items()]
|
||||
reg.locations += [SM64Location(player, loc_name, location_table[loc_name], reg) for loc_name in locs]
|
||||
def create_region(name: str, player: int, world: MultiWorld) -> Region:
|
||||
region = Region(name, player, world)
|
||||
world.regions.append(region)
|
||||
return region
|
||||
|
||||
|
||||
def create_subregion(source_region: Region, name: str, *locs: str) -> Region:
|
||||
region = Region(name, source_region.player, source_region.multiworld)
|
||||
connection = Entrance(source_region.player, name, source_region)
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(region)
|
||||
source_region.multiworld.regions.append(region)
|
||||
create_locs(region, *locs)
|
||||
return region
|
||||
|
||||
|
||||
def set_subregion_access_rule(world, player, region_name: str, rule):
|
||||
world.get_entrance(world, player, region_name).access_rule = rule
|
||||
|
||||
|
||||
def create_default_locs(reg: Region, default_locs: dict):
|
||||
create_locs(reg, *default_locs.keys())
|
||||
|
||||
|
||||
def create_locs(reg: Region, *locs: str):
|
||||
reg.locations += [SM64Location(reg.player, loc_name, location_table[loc_name], reg) for loc_name in locs]
|
||||
|
||||
@@ -1,36 +1,59 @@
|
||||
from ..generic.Rules import add_rule
|
||||
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
|
||||
from typing import Callable, Union, Dict, Set
|
||||
|
||||
def shuffle_dict_keys(world, obj: dict) -> dict:
|
||||
keys = list(obj.keys())
|
||||
values = list(obj.values())
|
||||
from BaseClasses import MultiWorld
|
||||
from ..generic.Rules import add_rule, set_rule
|
||||
from .Locations import location_table
|
||||
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\
|
||||
sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
|
||||
from .Items import action_item_table
|
||||
|
||||
def shuffle_dict_keys(world, dictionary: dict) -> dict:
|
||||
keys = list(dictionary.keys())
|
||||
values = list(dictionary.values())
|
||||
world.random.shuffle(keys)
|
||||
return dict(zip(keys,values))
|
||||
return dict(zip(keys, values))
|
||||
|
||||
def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set,
|
||||
swapdict: dict, world):
|
||||
def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_regions: Set[str],
|
||||
swapdict: Dict[SM64Levels, str], world):
|
||||
if entrance_map[entrance] in invalid_regions: # Unlucky :C
|
||||
replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items()
|
||||
replacement_regions = [(rand_entrance, rand_region) for rand_entrance, rand_region in swapdict.items()
|
||||
if rand_region not in invalid_regions]
|
||||
rand_region, rand_entrance = world.random.choice(replacement_regions)
|
||||
rand_entrance, rand_region = world.random.choice(replacement_regions)
|
||||
old_dest = entrance_map[entrance]
|
||||
entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest
|
||||
swapdict[rand_region] = entrance
|
||||
swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region
|
||||
swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest
|
||||
swapdict.pop(entrance)
|
||||
|
||||
def set_rules(world, player: int, area_connections: dict):
|
||||
def set_rules(world, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
|
||||
randomized_level_to_paintings = sm64_level_to_paintings.copy()
|
||||
randomized_level_to_secrets = sm64_level_to_secrets.copy()
|
||||
valid_move_randomizer_start_courses = [
|
||||
"Bob-omb Battlefield", "Jolly Roger Bay", "Cool, Cool Mountain",
|
||||
"Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
|
||||
"Dire, Dire Docks", "Snowman's Land"
|
||||
] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
|
||||
# If not shuffling later, ensure a valid start course on move randomizer
|
||||
if world.AreaRandomizer[player].value < 3 and move_rando_bitvec > 0:
|
||||
swapdict = randomized_level_to_paintings.copy()
|
||||
invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses}
|
||||
fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world)
|
||||
fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world)
|
||||
|
||||
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
|
||||
randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets)
|
||||
randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets }
|
||||
randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets}
|
||||
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
|
||||
randomized_entrances = shuffle_dict_keys(world,randomized_entrances)
|
||||
swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() }
|
||||
randomized_entrances = shuffle_dict_keys(world, randomized_entrances)
|
||||
# Guarantee first entrance is a course
|
||||
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world)
|
||||
swapdict = randomized_entrances.copy()
|
||||
if move_rando_bitvec == 0:
|
||||
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world)
|
||||
else:
|
||||
invalid_start_courses = {course for course in randomized_entrances.values() if course not in valid_move_randomizer_start_courses}
|
||||
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world)
|
||||
fix_reg(randomized_entrances, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world)
|
||||
# Guarantee BITFS is not mapped to DDD
|
||||
fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world)
|
||||
# Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
|
||||
@@ -43,27 +66,34 @@ def set_rules(world, player: int, area_connections: dict):
|
||||
# Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
|
||||
area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()})
|
||||
randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()}
|
||||
|
||||
|
||||
rf = RuleFactory(world, player, move_rando_bitvec)
|
||||
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"])
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3))
|
||||
connect_regions(world, player, randomized_entrances_s["Jolly Roger Bay"], randomized_entrances_s["The Secret Aquarium"],
|
||||
rf.build_rule("SF/BF | TJ & LG | MOVELESS & TJ"))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value))
|
||||
connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"],
|
||||
lambda state: state.has("Power Star", player, star_costs["FirstBowserDoorCost"]))
|
||||
|
||||
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
|
||||
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value))
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"],
|
||||
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]))
|
||||
connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"])
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
|
||||
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"],
|
||||
rf.build_rule("GP"))
|
||||
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"],
|
||||
lambda state: state.has("Power Star", player, star_costs["BasementDoorCost"]) and
|
||||
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
|
||||
|
||||
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
|
||||
|
||||
@@ -72,66 +102,127 @@ def set_rules(world, player: int, area_connections: dict):
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"])
|
||||
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"])
|
||||
connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)")
|
||||
connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)")
|
||||
connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island")
|
||||
connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island")
|
||||
|
||||
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value))
|
||||
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"]))
|
||||
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"])
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"])
|
||||
connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"])
|
||||
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value))
|
||||
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"]))
|
||||
|
||||
#Special Rules for some Locations
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player))
|
||||
add_rule(world.get_location("BBH: Eye to Eye in the Secret Room", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("DDD: Pole-Jumping for Red Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
|
||||
# Course Rules
|
||||
# Bob-omb Battlefield
|
||||
rf.assign_rule("BoB: Island", "CANN | CANNLESS & WC & TJ | CAPLESS & CANNLESS & LJ")
|
||||
rf.assign_rule("BoB: Mario Wings to the Sky", "CANN & WC | CAPLESS & CANN")
|
||||
rf.assign_rule("BoB: Behind Chain Chomp's Gate", "GP | MOVELESS")
|
||||
# Whomp's Fortress
|
||||
rf.assign_rule("WF: Tower", "{{WF: Chip Off Whomp's Block}}")
|
||||
rf.assign_rule("WF: Chip Off Whomp's Block", "GP")
|
||||
rf.assign_rule("WF: Shoot into the Wild Blue", "WK & TJ/SF | CANN")
|
||||
rf.assign_rule("WF: Fall onto the Caged Island", "CL & {WF: Tower} | MOVELESS & TJ | MOVELESS & LJ | MOVELESS & CANN")
|
||||
rf.assign_rule("WF: Blast Away the Wall", "CANN | CANNLESS & LG")
|
||||
# Jolly Roger Bay
|
||||
rf.assign_rule("JRB: Upper", "TJ/BF/SF/WK | MOVELESS & LG")
|
||||
rf.assign_rule("JRB: Red Coins on the Ship Afloat", "CL/CANN/TJ/BF/WK")
|
||||
rf.assign_rule("JRB: Blast to the Stone Pillar", "CANN+CL | CANNLESS & MOVELESS | CANN & MOVELESS")
|
||||
rf.assign_rule("JRB: Through the Jet Stream", "MC | CAPLESS")
|
||||
# Cool, Cool Mountain
|
||||
rf.assign_rule("CCM: Wall Kicks Will Work", "TJ/WK & CANN | CANNLESS & TJ/WK | MOVELESS")
|
||||
# Big Boo's Haunt
|
||||
rf.assign_rule("BBH: Third Floor", "WK+LG | MOVELESS & WK")
|
||||
rf.assign_rule("BBH: Roof", "LJ | MOVELESS")
|
||||
rf.assign_rule("BBH: Secret of the Haunted Books", "KK | MOVELESS")
|
||||
rf.assign_rule("BBH: Seek the 8 Red Coins", "BF/WK/TJ/SF")
|
||||
rf.assign_rule("BBH: Eye to Eye in the Secret Room", "VC")
|
||||
# Haze Maze Cave
|
||||
rf.assign_rule("HMC: Red Coin Area", "CL & WK/LG/BF/SF/TJ | MOVELESS & WK")
|
||||
rf.assign_rule("HMC: Pit Islands", "TJ+CL | MOVELESS & WK & TJ/LJ | MOVELESS & WK+SF+LG")
|
||||
rf.assign_rule("HMC: Metal-Head Mario Can Move!", "LJ+MC | CAPLESS & LJ+TJ | CAPLESS & MOVELESS & LJ/TJ/WK")
|
||||
rf.assign_rule("HMC: Navigating the Toxic Maze", "WK/SF/BF/TJ")
|
||||
rf.assign_rule("HMC: Watch for Rolling Rocks", "WK")
|
||||
# Lethal Lava Land
|
||||
rf.assign_rule("LLL: Upper Volcano", "CL")
|
||||
# Shifting Sand Land
|
||||
rf.assign_rule("SSL: Upper Pyramid", "CL & TJ/BF/SF/LG | MOVELESS")
|
||||
rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ/SF/BF & TJ+WC | TJ/SF/BF & CAPLESS | MOVELESS")
|
||||
# Dire, Dire Docks
|
||||
rf.assign_rule("DDD: Moving Poles", "CL & {{Bowser in the Fire Sea Key}} | TJ+DV+LG+WK & MOVELESS")
|
||||
rf.assign_rule("DDD: Through the Jet Stream", "MC | CAPLESS")
|
||||
rf.assign_rule("DDD: Collect the Caps...", "VC+MC | CAPLESS & VC")
|
||||
# Snowman's Land
|
||||
rf.assign_rule("SL: Snowman's Big Head", "BF/SF/CANN/TJ")
|
||||
rf.assign_rule("SL: In the Deep Freeze", "WK/SF/LG/BF/CANN/TJ")
|
||||
rf.assign_rule("SL: Into the Igloo", "VC & TJ/SF/BF/WK/LG | MOVELESS & VC")
|
||||
# Wet-Dry World
|
||||
rf.assign_rule("WDW: Top", "WK/TJ/SF/BF | MOVELESS")
|
||||
rf.assign_rule("WDW: Downtown", "NAR & LG & TJ/SF/BF | CANN | MOVELESS & TJ+DV")
|
||||
rf.assign_rule("WDW: Go to Town for Red Coins", "WK | MOVELESS & TJ")
|
||||
rf.assign_rule("WDW: Quick Race Through Downtown!", "VC & WK/BF | VC & TJ+LG | MOVELESS & VC & TJ")
|
||||
rf.assign_rule("WDW: Bob-omb Buddy", "TJ | SF+LG | NAR & BF/SF")
|
||||
# Tall, Tall Mountain
|
||||
rf.assign_rule("TTM: Top", "MOVELESS & TJ | LJ/DV & LG/KK | MOVELESS & WK & SF/LG | MOVELESS & KK/DV")
|
||||
rf.assign_rule("TTM: Blast to the Lonely Mushroom", "CANN | CANNLESS & LJ | MOVELESS & CANNLESS")
|
||||
# Tiny-Huge Island
|
||||
rf.assign_rule("THI: Pipes", "NAR | LJ/TJ/DV/LG | MOVELESS & BF/SF/KK")
|
||||
rf.assign_rule("THI: Large Top", "NAR | LJ/TJ/DV | MOVELESS")
|
||||
rf.assign_rule("THI: Wiggler's Red Coins", "WK")
|
||||
rf.assign_rule("THI: Make Wiggler Squirm", "GP | MOVELESS & DV")
|
||||
# Tick Tock Clock
|
||||
rf.assign_rule("TTC: Lower", "LG/TJ/SF/BF/WK")
|
||||
rf.assign_rule("TTC: Upper", "CL | SF+WK")
|
||||
rf.assign_rule("TTC: Top", "CL | SF+WK")
|
||||
rf.assign_rule("TTC: Stomp on the Thwomp", "LG & TJ/SF/BF")
|
||||
rf.assign_rule("TTC: Stop Time for Red Coins", "NAR | {TTC: Lower}")
|
||||
# Rainbow Ride
|
||||
rf.assign_rule("RR: Maze", "WK | LJ & SF/BF/TJ | MOVELESS & LG/TJ")
|
||||
rf.assign_rule("RR: Bob-omb Buddy", "WK | MOVELESS & LG")
|
||||
rf.assign_rule("RR: Swingin' in the Breeze", "LG/TJ/BF/SF")
|
||||
rf.assign_rule("RR: Tricky Triangles!", "LG/TJ/BF/SF")
|
||||
rf.assign_rule("RR: Cruiser", "WK/SF/BF/LG/TJ")
|
||||
rf.assign_rule("RR: House", "TJ/SF/BF/LG")
|
||||
rf.assign_rule("RR: Somewhere Over the Rainbow", "CANN")
|
||||
# Cavern of the Metal Cap
|
||||
rf.assign_rule("Cavern of the Metal Cap Red Coins", "MC | CAPLESS")
|
||||
# Vanish Cap Under the Moat
|
||||
rf.assign_rule("Vanish Cap Under the Moat Switch", "WK/TJ/BF/SF/LG | MOVELESS")
|
||||
rf.assign_rule("Vanish Cap Under the Moat Red Coins", "TJ/BF/SF/LG/WK & VC | CAPLESS & WK")
|
||||
# Bowser in the Fire Sea
|
||||
rf.assign_rule("BitFS: Upper", "CL")
|
||||
rf.assign_rule("Bowser in the Fire Sea Red Coins", "LG/WK")
|
||||
rf.assign_rule("Bowser in the Fire Sea 1Up Block Near Poles", "LG/WK")
|
||||
# Wing Mario Over the Rainbow
|
||||
rf.assign_rule("Wing Mario Over the Rainbow Red Coins", "TJ+WC")
|
||||
rf.assign_rule("Wing Mario Over the Rainbow 1Up Block", "TJ+WC")
|
||||
# Bowser in the Sky
|
||||
rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG")
|
||||
# 100 Coin Stars
|
||||
if world.EnableCoinStars[player]:
|
||||
add_rule(world.get_location("DDD: 100 Coins", player), lambda state: state.can_reach("Bowser in the Fire Sea", 'Region', player))
|
||||
add_rule(world.get_location("SL: Into the Igloo", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("RR: Somewhere Over the Rainbow", player), lambda state: state.has("Cannon Unlock RR", player))
|
||||
|
||||
if world.AreaRandomizer[player] or world.StrictCannonRequirements[player]:
|
||||
# If area rando is on, it may not be possible to modify WDW's starting water level,
|
||||
# which would make it impossible to reach downtown area without the cannon.
|
||||
add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player))
|
||||
|
||||
if world.StrictCapRequirements[player]:
|
||||
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("HMC: Metal-Head Mario Can Move!", player), lambda state: state.has("Metal Cap", player))
|
||||
add_rule(world.get_location("JRB: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player))
|
||||
add_rule(world.get_location("SSL: Free Flying for 8 Red Coins", player), lambda state: state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("DDD: Through the Jet Stream", player), lambda state: state.has("Metal Cap", player))
|
||||
add_rule(world.get_location("DDD: Collect the Caps...", player), lambda state: state.has("Metal Cap", player))
|
||||
add_rule(world.get_location("Vanish Cap Under the Moat Red Coins", player), lambda state: state.has("Vanish Cap", player))
|
||||
add_rule(world.get_location("Cavern of the Metal Cap Red Coins", player), lambda state: state.has("Metal Cap", player))
|
||||
if world.StrictCannonRequirements[player]:
|
||||
add_rule(world.get_location("WF: Blast Away the Wall", player), lambda state: state.has("Cannon Unlock WF", player))
|
||||
add_rule(world.get_location("JRB: Blast to the Stone Pillar", player), lambda state: state.has("Cannon Unlock JRB", player))
|
||||
add_rule(world.get_location("CCM: Wall Kicks Will Work", player), lambda state: state.has("Cannon Unlock CCM", player))
|
||||
add_rule(world.get_location("TTM: Blast to the Lonely Mushroom", player), lambda state: state.has("Cannon Unlock TTM", player))
|
||||
if world.StrictCapRequirements[player] and world.StrictCannonRequirements[player]:
|
||||
# Ability to reach the floating island. Need some of those coins to get 100 coin star as well.
|
||||
add_rule(world.get_location("BoB: Find the 8 Red Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("BoB: Shoot to the Island in the Sky", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
|
||||
if world.EnableCoinStars[player]:
|
||||
add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player))
|
||||
|
||||
#Rules for Secret Stars
|
||||
add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player))
|
||||
add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player))
|
||||
rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ")
|
||||
rf.assign_rule("WF: 100 Coins", "GP | MOVELESS")
|
||||
rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}")
|
||||
rf.assign_rule("HMC: 100 Coins", "GP")
|
||||
rf.assign_rule("SSL: 100 Coins", "{SSL: Upper Pyramid} | GP")
|
||||
rf.assign_rule("DDD: 100 Coins", "GP")
|
||||
rf.assign_rule("SL: 100 Coins", "VC | MOVELESS")
|
||||
rf.assign_rule("WDW: 100 Coins", "GP | {WDW: Downtown}")
|
||||
rf.assign_rule("TTC: 100 Coins", "GP")
|
||||
rf.assign_rule("THI: 100 Coins", "GP")
|
||||
rf.assign_rule("RR: 100 Coins", "GP & WK")
|
||||
# Castle Stars
|
||||
add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12))
|
||||
add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25))
|
||||
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35))
|
||||
|
||||
if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value:
|
||||
(world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value)
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
|
||||
if star_costs["MIPS1Cost"] > star_costs["MIPS2Cost"]:
|
||||
(star_costs["MIPS2Cost"], star_costs["MIPS1Cost"]) = (star_costs["MIPS1Cost"], star_costs["MIPS2Cost"])
|
||||
rf.assign_rule("MIPS 1", "DV | MOVELESS")
|
||||
rf.assign_rule("MIPS 2", "DV | MOVELESS")
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS1Cost"]))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, star_costs["MIPS2Cost"]))
|
||||
|
||||
world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player)
|
||||
|
||||
if world.CompletionType[player] == "last_bowser_stage":
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player)
|
||||
@@ -139,3 +230,145 @@ def set_rules(world, player: int, area_connections: dict):
|
||||
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \
|
||||
state.can_reach("Bowser in the Fire Sea", 'Region', player) and \
|
||||
state.can_reach("Bowser in the Sky", 'Region', player)
|
||||
|
||||
|
||||
class RuleFactory:
|
||||
|
||||
world: MultiWorld
|
||||
player: int
|
||||
move_rando_bitvec: bool
|
||||
area_randomizer: bool
|
||||
capless: bool
|
||||
cannonless: bool
|
||||
moveless: bool
|
||||
|
||||
token_table = {
|
||||
"TJ": "Triple Jump",
|
||||
"LJ": "Long Jump",
|
||||
"BF": "Backflip",
|
||||
"SF": "Side Flip",
|
||||
"WK": "Wall Kick",
|
||||
"DV": "Dive",
|
||||
"GP": "Ground Pound",
|
||||
"KK": "Kick",
|
||||
"CL": "Climb",
|
||||
"LG": "Ledge Grab",
|
||||
"WC": "Wing Cap",
|
||||
"MC": "Metal Cap",
|
||||
"VC": "Vanish Cap"
|
||||
}
|
||||
|
||||
class SM64LogicException(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, world, player, move_rando_bitvec):
|
||||
self.world = world
|
||||
self.player = player
|
||||
self.move_rando_bitvec = move_rando_bitvec
|
||||
self.area_randomizer = world.AreaRandomizer[player].value > 0
|
||||
self.capless = not world.StrictCapRequirements[player]
|
||||
self.cannonless = not world.StrictCannonRequirements[player]
|
||||
self.moveless = not world.StrictMoveRequirements[player] or not move_rando_bitvec > 0
|
||||
|
||||
def assign_rule(self, target_name: str, rule_expr: str):
|
||||
target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player)
|
||||
cannon_name = "Cannon Unlock " + target_name.split(':')[0]
|
||||
try:
|
||||
rule = self.build_rule(rule_expr, cannon_name)
|
||||
except RuleFactory.SM64LogicException as exception:
|
||||
raise RuleFactory.SM64LogicException(
|
||||
f"Error generating rule for {target_name} using rule expression {rule_expr}: {exception}")
|
||||
if rule:
|
||||
set_rule(target, rule)
|
||||
|
||||
def build_rule(self, rule_expr: str, cannon_name: str = '') -> Callable:
|
||||
expressions = rule_expr.split(" | ")
|
||||
rules = []
|
||||
for expression in expressions:
|
||||
or_clause = self.combine_and_clauses(expression, cannon_name)
|
||||
if or_clause is True:
|
||||
return None
|
||||
if or_clause is not False:
|
||||
rules.append(or_clause)
|
||||
if rules:
|
||||
if len(rules) == 1:
|
||||
return rules[0]
|
||||
else:
|
||||
return lambda state: any(rule(state) for rule in rules)
|
||||
else:
|
||||
return None
|
||||
|
||||
def combine_and_clauses(self, rule_expr: str, cannon_name: str) -> Union[Callable, bool]:
|
||||
expressions = rule_expr.split(" & ")
|
||||
rules = []
|
||||
for expression in expressions:
|
||||
and_clause = self.make_lambda(expression, cannon_name)
|
||||
if and_clause is False:
|
||||
return False
|
||||
if and_clause is not True:
|
||||
rules.append(and_clause)
|
||||
if rules:
|
||||
if len(rules) == 1:
|
||||
return rules[0]
|
||||
return lambda state: all(rule(state) for rule in rules)
|
||||
else:
|
||||
return True
|
||||
|
||||
def make_lambda(self, expression: str, cannon_name: str) -> Union[Callable, bool]:
|
||||
if '+' in expression:
|
||||
tokens = expression.split('+')
|
||||
items = set()
|
||||
for token in tokens:
|
||||
item = self.parse_token(token, cannon_name)
|
||||
if item is True:
|
||||
continue
|
||||
if item is False:
|
||||
return False
|
||||
items.add(item)
|
||||
if items:
|
||||
return lambda state: state.has_all(items, self.player)
|
||||
else:
|
||||
return True
|
||||
if '/' in expression:
|
||||
tokens = expression.split('/')
|
||||
items = set()
|
||||
for token in tokens:
|
||||
item = self.parse_token(token, cannon_name)
|
||||
if item is True:
|
||||
return True
|
||||
if item is False:
|
||||
continue
|
||||
items.add(item)
|
||||
if items:
|
||||
return lambda state: state.has_any(items, self.player)
|
||||
else:
|
||||
return False
|
||||
if '{{' in expression:
|
||||
return lambda state: state.can_reach(expression[2:-2], "Location", self.player)
|
||||
if '{' in expression:
|
||||
return lambda state: state.can_reach(expression[1:-1], "Region", self.player)
|
||||
item = self.parse_token(expression, cannon_name)
|
||||
if item in (True, False):
|
||||
return item
|
||||
return lambda state: state.has(item, self.player)
|
||||
|
||||
def parse_token(self, token: str, cannon_name: str) -> Union[str, bool]:
|
||||
if token == "CANN":
|
||||
return cannon_name
|
||||
if token == "CAPLESS":
|
||||
return self.capless
|
||||
if token == "CANNLESS":
|
||||
return self.cannonless
|
||||
if token == "MOVELESS":
|
||||
return self.moveless
|
||||
if token == "NAR":
|
||||
return not self.area_randomizer
|
||||
item = self.token_table.get(token, None)
|
||||
if not item:
|
||||
raise Exception(f"Invalid token: '{item}'")
|
||||
if item in action_item_table:
|
||||
if self.move_rando_bitvec & (1 << (action_item_table[item] - action_item_table['Double Jump'])) == 0:
|
||||
# This action item is not randomized.
|
||||
return True
|
||||
return item
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
import os
|
||||
import json
|
||||
from .Items import item_table, cannon_item_table, SM64Item
|
||||
from .Items import item_table, action_item_table, cannon_item_table, SM64Item
|
||||
from .Locations import location_table, SM64Location
|
||||
from .Options import sm64_options
|
||||
from .Rules import set_rules
|
||||
@@ -35,14 +35,44 @@ class SM64World(World):
|
||||
item_name_to_id = item_table
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 8
|
||||
data_version = 9
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
option_definitions = sm64_options
|
||||
|
||||
number_of_stars: int
|
||||
move_rando_bitvec: int
|
||||
filler_count: int
|
||||
star_costs: typing.Dict[str, int]
|
||||
|
||||
def generate_early(self):
|
||||
max_stars = 120
|
||||
if (not self.multiworld.EnableCoinStars[self.player].value):
|
||||
max_stars -= 15
|
||||
self.move_rando_bitvec = 0
|
||||
for action, itemid in action_item_table.items():
|
||||
# HACK: Disable randomization of double jump
|
||||
if action == 'Double Jump': continue
|
||||
if getattr(self.multiworld, f"MoveRandomizer{action.replace(' ','')}")[self.player].value:
|
||||
max_stars -= 1
|
||||
self.move_rando_bitvec |= (1 << (itemid - action_item_table['Double Jump']))
|
||||
if (self.multiworld.ExclamationBoxes[self.player].value > 0):
|
||||
max_stars += 29
|
||||
self.number_of_stars = min(self.multiworld.AmountOfStars[self.player].value, max_stars)
|
||||
self.filler_count = max_stars - self.number_of_stars
|
||||
self.star_costs = {
|
||||
'FirstBowserDoorCost': round(self.multiworld.FirstBowserStarDoorCost[self.player].value * self.number_of_stars / 100),
|
||||
'BasementDoorCost': round(self.multiworld.BasementStarDoorCost[self.player].value * self.number_of_stars / 100),
|
||||
'SecondFloorDoorCost': round(self.multiworld.SecondFloorStarDoorCost[self.player].value * self.number_of_stars / 100),
|
||||
'MIPS1Cost': round(self.multiworld.MIPS1Cost[self.player].value * self.number_of_stars / 100),
|
||||
'MIPS2Cost': round(self.multiworld.MIPS2Cost[self.player].value * self.number_of_stars / 100),
|
||||
'StarsToFinish': round(self.multiworld.StarsToFinish[self.player].value * self.number_of_stars / 100)
|
||||
}
|
||||
# Nudge MIPS 1 to match vanilla on default percentage
|
||||
if self.number_of_stars == 120 and self.multiworld.MIPS1Cost[self.player].value == 12:
|
||||
self.star_costs['MIPS1Cost'] = 15
|
||||
self.topology_present = self.multiworld.AreaRandomizer[self.player].value
|
||||
|
||||
def create_regions(self):
|
||||
@@ -50,7 +80,7 @@ class SM64World(World):
|
||||
|
||||
def set_rules(self):
|
||||
self.area_connections = {}
|
||||
set_rules(self.multiworld, self.player, self.area_connections)
|
||||
set_rules(self.multiworld, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec)
|
||||
if self.topology_present:
|
||||
# Write area_connections to spoiler log
|
||||
for entrance, destination in self.area_connections.items():
|
||||
@@ -72,31 +102,29 @@ class SM64World(World):
|
||||
return item
|
||||
|
||||
def create_items(self):
|
||||
starcount = self.multiworld.AmountOfStars[self.player].value
|
||||
if (not self.multiworld.EnableCoinStars[self.player].value):
|
||||
starcount = max(35,self.multiworld.AmountOfStars[self.player].value-15)
|
||||
starcount = max(starcount, self.multiworld.FirstBowserStarDoorCost[self.player].value,
|
||||
self.multiworld.BasementStarDoorCost[self.player].value, self.multiworld.SecondFloorStarDoorCost[self.player].value,
|
||||
self.multiworld.MIPS1Cost[self.player].value, self.multiworld.MIPS2Cost[self.player].value,
|
||||
self.multiworld.StarsToFinish[self.player].value)
|
||||
self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,starcount)]
|
||||
self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.multiworld.EnableCoinStars[self.player].value else 0))]
|
||||
|
||||
# 1Up Mushrooms
|
||||
self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,self.filler_count)]
|
||||
# Power Stars
|
||||
self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)]
|
||||
# Keys
|
||||
if (not self.multiworld.ProgressiveKeys[self.player].value):
|
||||
key1 = self.create_item("Basement Key")
|
||||
key2 = self.create_item("Second Floor Key")
|
||||
self.multiworld.itempool += [key1, key2]
|
||||
else:
|
||||
self.multiworld.itempool += [self.create_item("Progressive Key") for i in range(0,2)]
|
||||
|
||||
wingcap = self.create_item("Wing Cap")
|
||||
metalcap = self.create_item("Metal Cap")
|
||||
vanishcap = self.create_item("Vanish Cap")
|
||||
self.multiworld.itempool += [wingcap, metalcap, vanishcap]
|
||||
|
||||
# Caps
|
||||
self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]]
|
||||
# Cannons
|
||||
if (self.multiworld.BuddyChecks[self.player].value):
|
||||
self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()]
|
||||
else:
|
||||
# Moves
|
||||
self.multiworld.itempool += [self.create_item(action)
|
||||
for action, itemid in action_item_table.items()
|
||||
if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])]
|
||||
|
||||
def generate_basic(self):
|
||||
if not (self.multiworld.BuddyChecks[self.player].value):
|
||||
self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
|
||||
self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
|
||||
self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
|
||||
@@ -108,9 +136,7 @@ class SM64World(World):
|
||||
self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI"))
|
||||
self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR"))
|
||||
|
||||
if (self.multiworld.ExclamationBoxes[self.player].value > 0):
|
||||
self.multiworld.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)]
|
||||
else:
|
||||
if (self.multiworld.ExclamationBoxes[self.player].value == 0):
|
||||
self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom"))
|
||||
self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom"))
|
||||
self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom"))
|
||||
@@ -147,14 +173,10 @@ class SM64World(World):
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"AreaRando": self.area_connections,
|
||||
"FirstBowserDoorCost": self.multiworld.FirstBowserStarDoorCost[self.player].value,
|
||||
"BasementDoorCost": self.multiworld.BasementStarDoorCost[self.player].value,
|
||||
"SecondFloorDoorCost": self.multiworld.SecondFloorStarDoorCost[self.player].value,
|
||||
"MIPS1Cost": self.multiworld.MIPS1Cost[self.player].value,
|
||||
"MIPS2Cost": self.multiworld.MIPS2Cost[self.player].value,
|
||||
"StarsToFinish": self.multiworld.StarsToFinish[self.player].value,
|
||||
"MoveRandoVec": self.move_rando_bitvec,
|
||||
"DeathLink": self.multiworld.death_link[self.player].value,
|
||||
"CompletionType" : self.multiworld.CompletionType[self.player].value,
|
||||
"CompletionType": self.multiworld.CompletionType[self.player].value,
|
||||
**self.star_costs
|
||||
}
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
|
||||
@@ -81,7 +81,7 @@ for _loc in _locations:
|
||||
# item helpers
|
||||
_ingredients = (
|
||||
'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron',
|
||||
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet',
|
||||
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion',
|
||||
'Ash', 'Acorn'
|
||||
)
|
||||
_other_items = (
|
||||
|
||||
21
worlds/soe/test/test_item_mapping.py
Normal file
21
worlds/soe/test/test_item_mapping.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from unittest import TestCase
|
||||
from .. import SoEWorld
|
||||
|
||||
|
||||
class TestMapping(TestCase):
|
||||
def test_atlas_medallion_name_group(self) -> None:
|
||||
"""
|
||||
Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in item groups.
|
||||
"""
|
||||
self.assertIn("Any Atlas Medallion", SoEWorld.item_name_groups)
|
||||
|
||||
def test_atlas_medallion_name_items(self) -> None:
|
||||
"""
|
||||
Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in items.
|
||||
"""
|
||||
found_medallion = False
|
||||
for name in SoEWorld.item_name_to_id:
|
||||
self.assertNotIn("Atlas Amulet", name, "Expected Atlas Medallion, not Amulet")
|
||||
if "Atlas Medallion" in name:
|
||||
found_medallion = True
|
||||
self.assertTrue(found_medallion, "Did not find Atlas Medallion in items")
|
||||
@@ -124,6 +124,6 @@ List of supported mods:
|
||||
|
||||
## Multiplayer
|
||||
|
||||
You cannot play an Archipelago Slot in multiplayer at the moment. There is no short-terms plans to support that feature.
|
||||
You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature.
|
||||
|
||||
You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts.
|
||||
|
||||
@@ -84,4 +84,4 @@ See the [Supported mods documentation](https://github.com/agilbert1412/StardewAr
|
||||
|
||||
### Multiplayer
|
||||
|
||||
You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-terms plans to support that feature.
|
||||
You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature.
|
||||
|
||||
@@ -24,15 +24,15 @@ class TunicWeb(WebWorld):
|
||||
)
|
||||
]
|
||||
theme = "grassFlowers"
|
||||
game = "Tunic"
|
||||
game = "TUNIC"
|
||||
|
||||
|
||||
class TunicItem(Item):
|
||||
game: str = "Tunic"
|
||||
game: str = "TUNIC"
|
||||
|
||||
|
||||
class TunicLocation(Location):
|
||||
game: str = "Tunic"
|
||||
game: str = "TUNIC"
|
||||
|
||||
|
||||
class TunicWorld(World):
|
||||
@@ -41,7 +41,7 @@ class TunicWorld(World):
|
||||
about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will
|
||||
confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox!
|
||||
"""
|
||||
game = "Tunic"
|
||||
game = "TUNIC"
|
||||
web = TunicWeb()
|
||||
|
||||
data_version = 2
|
||||
|
||||
@@ -37,7 +37,7 @@ portal_mapping: List[Portal] = [
|
||||
destination="Furnace_gyro_lower"),
|
||||
Portal(name="Caustic Light Cave Entrance", region="Overworld",
|
||||
destination="Overworld Cave_"),
|
||||
Portal(name="Swamp Upper Entrance", region="Overworld Laurels",
|
||||
Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry",
|
||||
destination="Swamp Redux 2_wall"),
|
||||
Portal(name="Swamp Lower Entrance", region="Overworld",
|
||||
destination="Swamp Redux 2_conduit"),
|
||||
@@ -49,7 +49,7 @@ portal_mapping: List[Portal] = [
|
||||
destination="Atoll Redux_upper"),
|
||||
Portal(name="Atoll Lower Entrance", region="Overworld",
|
||||
destination="Atoll Redux_lower"),
|
||||
Portal(name="Special Shop Entrance", region="Overworld Laurels",
|
||||
Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry",
|
||||
destination="ShopSpecial_"),
|
||||
Portal(name="Maze Cave Entrance", region="Overworld",
|
||||
destination="Maze Room_"),
|
||||
@@ -57,7 +57,7 @@ portal_mapping: List[Portal] = [
|
||||
destination="Archipelagos Redux_upper"),
|
||||
Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace",
|
||||
destination="Archipelagos Redux_lower"),
|
||||
Portal(name="West Garden Laurels Entrance", region="Overworld Laurels",
|
||||
Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry",
|
||||
destination="Archipelagos Redux_lowest"),
|
||||
Portal(name="Temple Door Entrance", region="Overworld Temple Door",
|
||||
destination="Temple_main"),
|
||||
@@ -533,7 +533,9 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Overworld": RegionInfo("Overworld Redux"),
|
||||
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats),
|
||||
"Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest
|
||||
"Overworld Laurels": RegionInfo("Overworld Redux"), # all spots in Overworld that you need laurels to reach
|
||||
"Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot
|
||||
"Overworld Special Shop Entry": RegionInfo("Overworld Redux"), # special shop entry spot
|
||||
"Overworld West Garden Laurels Entry": RegionInfo("Overworld Redux"), # west garden laurels entry
|
||||
"Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region),
|
||||
"Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway
|
||||
"Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal
|
||||
@@ -710,7 +712,7 @@ for p1, p2 in hallways.items():
|
||||
hallway_helper[p2] = p1
|
||||
|
||||
# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints
|
||||
hallways_nmg: Dict[str, str] = {
|
||||
hallways_ur: Dict[str, str] = {
|
||||
"Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west",
|
||||
"East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower",
|
||||
"Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_",
|
||||
@@ -720,20 +722,22 @@ hallways_nmg: Dict[str, str] = {
|
||||
"ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_",
|
||||
"Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top",
|
||||
}
|
||||
hallway_helper_nmg: Dict[str, str] = {}
|
||||
for p1, p2 in hallways.items():
|
||||
hallway_helper[p1] = p2
|
||||
hallway_helper[p2] = p1
|
||||
hallway_helper_ur: Dict[str, str] = {}
|
||||
for p1, p2 in hallways_ur.items():
|
||||
hallway_helper_ur[p1] = p2
|
||||
hallway_helper_ur[p2] = p1
|
||||
|
||||
|
||||
# the key is the region you have, the value is the regions you get for having that region
|
||||
# this is mostly so we don't have to do something overly complex to get this information
|
||||
dependent_regions: Dict[Tuple[str, ...], List[str]] = {
|
||||
("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"):
|
||||
["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door",
|
||||
"Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"],
|
||||
["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door",
|
||||
"Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal",
|
||||
"Overworld Spawn Portal"],
|
||||
("Old House Front",):
|
||||
["Old House Front", "Old House Back"],
|
||||
("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"):
|
||||
@@ -818,12 +822,14 @@ dependent_regions: Dict[Tuple[str, ...], List[str]] = {
|
||||
|
||||
|
||||
dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = {
|
||||
("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal",
|
||||
"Overworld Ruined Passage Door"):
|
||||
["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door",
|
||||
"Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"],
|
||||
["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door",
|
||||
"Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal",
|
||||
"Overworld Spawn Portal"],
|
||||
# can laurels through the gate
|
||||
("Old House Front", "Old House Back"):
|
||||
["Old House Front", "Old House Back"],
|
||||
@@ -908,13 +914,14 @@ dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = {
|
||||
|
||||
dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = {
|
||||
# can use ladder storage to get to the well rail
|
||||
("Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal",
|
||||
"Overworld Ruined Passage Door"):
|
||||
["Overworld", "Overworld Belltower", "Overworld Laurels", "Overworld Ruined Passage Door",
|
||||
"Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door",
|
||||
"Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal",
|
||||
"Overworld Well to Furnace Rail"],
|
||||
["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry",
|
||||
"Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door",
|
||||
"Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal",
|
||||
"Overworld Spawn Portal", "Overworld Well to Furnace Rail"],
|
||||
# can laurels through the gate
|
||||
("Old House Front", "Old House Back"):
|
||||
["Old House Front", "Old House Back"],
|
||||
|
||||
@@ -53,9 +53,23 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||
or (state.has(laurels, player) and options.logic_rules))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Laurels"],
|
||||
connecting_region=regions["Overworld Swamp Upper Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld Laurels"].connect(
|
||||
regions["Overworld Swamp Upper Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Special Shop Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld Special Shop Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld West Garden Laurels Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld West Garden Laurels Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
@@ -230,7 +244,6 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||
connecting_region=regions["West Garden Laurels Exit"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
# todo: can you wake the boss, then grapple to it, then kill it?
|
||||
regions["West Garden after Boss"].connect(
|
||||
connecting_region=regions["West Garden"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
@@ -431,6 +444,13 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||
regions["Quarry"].connect(
|
||||
connecting_region=regions["Quarry Monastery Entry"])
|
||||
|
||||
regions["Quarry Monastery Entry"].connect(
|
||||
connecting_region=regions["Quarry Back"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Quarry Back"].connect(
|
||||
connecting_region=regions["Quarry Monastery Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Monastery Rope"].connect(
|
||||
connecting_region=regions["Quarry Back"])
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
|
||||
from BaseClasses import Region, ItemClassification, Item, Location
|
||||
from .locations import location_table
|
||||
from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_nmg, \
|
||||
from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_ur, \
|
||||
dependent_regions, dependent_regions_nmg, dependent_regions_ur
|
||||
from .er_rules import set_er_region_rules
|
||||
|
||||
@@ -10,11 +10,11 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class TunicERItem(Item):
|
||||
game: str = "Tunic"
|
||||
game: str = "TUNIC"
|
||||
|
||||
|
||||
class TunicERLocation(Location):
|
||||
game: str = "Tunic"
|
||||
game: str = "TUNIC"
|
||||
|
||||
|
||||
def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]:
|
||||
@@ -28,8 +28,8 @@ def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[i
|
||||
if hint_string == "":
|
||||
hint_string = portal.name
|
||||
|
||||
if logic_rules:
|
||||
hallways = hallway_helper_nmg
|
||||
if logic_rules == "unrestricted":
|
||||
hallways = hallway_helper_ur
|
||||
else:
|
||||
hallways = hallway_helper
|
||||
|
||||
|
||||
@@ -37,8 +37,9 @@ class LogicRules(Choice):
|
||||
"""Set which logic rules to use for your world.
|
||||
Restricted: Standard logic, no glitches.
|
||||
No Major Glitches: Ice grapples through doors, shooting the west bell, and boss quick kills are included in logic.
|
||||
* Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer
|
||||
Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early.
|
||||
*Special Shop is not in logic without the Hero's Laurels in Unrestricted due to soft lock potential.
|
||||
*Special Shop is not in logic without the Hero's Laurels due to soft lock potential.
|
||||
*Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
|
||||
*Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in
|
||||
Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on."""
|
||||
|
||||
@@ -130,8 +130,7 @@ def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> No
|
||||
multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \
|
||||
lambda state: has_mask(state, player, options)
|
||||
multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \
|
||||
lambda state: (state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks)) \
|
||||
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
|
||||
lambda state: state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks)
|
||||
multiworld.get_entrance("Quarry -> Rooted Ziggurat", player).access_rule = \
|
||||
lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)
|
||||
multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \
|
||||
|
||||
@@ -2,5 +2,5 @@ from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class TunicTestBase(WorldTestBase):
|
||||
game = "Tunic"
|
||||
game = "TUNIC"
|
||||
player: int = 1
|
||||
@@ -161,7 +161,7 @@ class WitnessWorld(World):
|
||||
early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()]
|
||||
if early_items:
|
||||
random_early_item = self.random.choice(early_items)
|
||||
if self.options.puzzle_randomization == 1:
|
||||
if self.options.puzzle_randomization == "sigma_expert":
|
||||
# In Expert, 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:
|
||||
@@ -184,7 +184,7 @@ class WitnessWorld(World):
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
|
||||
|
||||
needed_size = 3
|
||||
needed_size += self.options.puzzle_randomization == 1
|
||||
needed_size += self.options.puzzle_randomization == "sigma_expert"
|
||||
needed_size += self.options.shuffle_symbols
|
||||
needed_size += self.options.shuffle_doors > 0
|
||||
|
||||
@@ -284,7 +284,7 @@ class WitnessWorld(World):
|
||||
|
||||
audio_logs = get_audio_logs().copy()
|
||||
|
||||
if hint_amount != 0:
|
||||
if hint_amount:
|
||||
generated_hints = make_hints(self, hint_amount, self.own_itempool)
|
||||
|
||||
self.random.shuffle(audio_logs)
|
||||
|
||||
@@ -75,6 +75,9 @@ joke_hints = [
|
||||
"Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.",
|
||||
"Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.",
|
||||
"Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.",
|
||||
"Have you tried Final Fantasy Mystic Quest?\nApparently, it was made in an attempt to simplify Final Fantasy for the western market.\nThey were right, I suck at RPGs.",
|
||||
"Have you tried Shivers?\nWitness 2 should totally feature a haunted Museum.",
|
||||
"Have you tried Heretic?\nWait, there is a Doom Engine game where you can look UP AND DOWN???",
|
||||
|
||||
"One day I was fascinated by the subject of generation of waves by wind.",
|
||||
"I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?",
|
||||
@@ -148,7 +151,7 @@ joke_hints = [
|
||||
"You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!",
|
||||
"Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.",
|
||||
"How many of you have personally witnessed a total solar eclipse?",
|
||||
"In the Treehouse area, you will find \n[Error: Data not found] progression items.",
|
||||
"In the Treehouse area, you will find 69 progression items.\nNice.\n(Source: Just trust me)",
|
||||
"Lingo\nLingoing\nLingone",
|
||||
"The name of the captain was Albert Einstein.",
|
||||
"Panel impossible Sigma plz fix",
|
||||
@@ -173,15 +176,15 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
|
||||
wincon = world.options.victory_condition
|
||||
|
||||
if discards:
|
||||
if difficulty == 1:
|
||||
if difficulty == "sigma_expert":
|
||||
always.append("Arrows")
|
||||
else:
|
||||
always.append("Triangles")
|
||||
|
||||
if wincon == 0:
|
||||
if wincon == "elevator":
|
||||
always += ["Mountain Bottom Floor Final Room Entry (Door)", "Mountain Bottom Floor Doors"]
|
||||
|
||||
if wincon == 1:
|
||||
if wincon == "challenge":
|
||||
always += ["Challenge Entry (Panel)", "Caves Panels"]
|
||||
|
||||
return always
|
||||
|
||||
@@ -112,30 +112,12 @@ class WitnessPlayerItems:
|
||||
or name in logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
|
||||
}
|
||||
|
||||
# Adjust item classifications based on game settings.
|
||||
eps_shuffled = self._world.options.shuffle_EPs
|
||||
come_to_you = self._world.options.elevators_come_to_you
|
||||
difficulty = self._world.options.puzzle_randomization
|
||||
# Downgrade door items
|
||||
for item_name, item_data in self.item_data.items():
|
||||
if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)",
|
||||
"Monastery Shortcuts",
|
||||
"Quarry Boathouse Hook Control (Panel)",
|
||||
"Windmill Turn Control (Panel)"}:
|
||||
# Downgrade doors that only gate progress in EP shuffle.
|
||||
item_data.classification = ItemClassification.useful
|
||||
elif not come_to_you and not eps_shuffled and item_name in {"Quarry Elevator Control (Panel)",
|
||||
"Swamp Long Bridge (Panel)"}:
|
||||
# These Bridges/Elevators are not logical access because they may leave you stuck.
|
||||
item_data.classification = ItemClassification.useful
|
||||
elif item_name in {"River Monastery Garden Shortcut (Door)",
|
||||
"Monastery Laser Shortcut (Door)",
|
||||
"Orchard Second Gate (Door)",
|
||||
"Jungle Bamboo Laser Shortcut (Door)",
|
||||
"Caves Elevator Controls (Panel)"}:
|
||||
# Downgrade doors that don't gate progress.
|
||||
item_data.classification = ItemClassification.useful
|
||||
elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled):
|
||||
# PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary
|
||||
if not isinstance(item_data.definition, DoorItemDefinition):
|
||||
continue
|
||||
|
||||
if all(not self._logic.solvability_guaranteed(e_hex) for e_hex in item_data.definition.panel_id_hexes):
|
||||
item_data.classification = ItemClassification.useful
|
||||
|
||||
# Build the mandatory item list.
|
||||
@@ -228,7 +210,7 @@ class WitnessPlayerItems:
|
||||
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
|
||||
|
||||
if self._world.options.shuffle_discarded_panels:
|
||||
if self._world.options.puzzle_randomization == 1:
|
||||
if self._world.options.puzzle_randomization == "sigma_expert":
|
||||
output.add("Arrows")
|
||||
else:
|
||||
output.add("Triangles")
|
||||
|
||||
@@ -509,9 +509,9 @@ class WitnessPlayerLocations:
|
||||
if world.options.shuffle_vault_boxes:
|
||||
self.PANEL_TYPES_TO_SHUFFLE.add("Vault")
|
||||
|
||||
if world.options.shuffle_EPs == 1:
|
||||
if world.options.shuffle_EPs == "individual":
|
||||
self.PANEL_TYPES_TO_SHUFFLE.add("EP")
|
||||
elif world.options.shuffle_EPs == 2:
|
||||
elif world.options.shuffle_EPs == "obelisk_sides":
|
||||
self.PANEL_TYPES_TO_SHUFFLE.add("Obelisk Side")
|
||||
|
||||
for obelisk_loc in StaticWitnessLocations.OBELISK_SIDES:
|
||||
@@ -543,7 +543,7 @@ class WitnessPlayerLocations:
|
||||
)
|
||||
|
||||
event_locations = {
|
||||
p for p in player_logic.EVENT_PANELS
|
||||
p for p in player_logic.USED_EVENT_NAMES_BY_HEX
|
||||
}
|
||||
|
||||
self.EVENT_LOCATION_TABLE = {
|
||||
|
||||
@@ -101,8 +101,11 @@ class WitnessPlayerLogic:
|
||||
for option_entity in option:
|
||||
dep_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX.get(option_entity)
|
||||
|
||||
if option_entity in self.EVENT_NAMES_BY_HEX:
|
||||
if option_entity in self.ALWAYS_EVENT_NAMES_BY_HEX:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
elif (panel_hex, option_entity) in self.CONDITIONAL_EVENTS:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
self.USED_EVENT_NAMES_BY_HEX[option_entity] = self.CONDITIONAL_EVENTS[(panel_hex, option_entity)]
|
||||
elif option_entity in {"7 Lasers", "11 Lasers", "7 Lasers + Redirect", "11 Lasers + Redirect",
|
||||
"PP2 Weirdness", "Theater to Tunnels"}:
|
||||
new_items = frozenset({frozenset([option_entity])})
|
||||
@@ -170,14 +173,11 @@ class WitnessPlayerLogic:
|
||||
if adj_type == "Event Items":
|
||||
line_split = line.split(" - ")
|
||||
new_event_name = line_split[0]
|
||||
hex_set = line_split[1].split(",")
|
||||
entity_hex = line_split[1]
|
||||
dependent_hex_set = line_split[2].split(",")
|
||||
|
||||
for entity, event_name in self.EVENT_NAMES_BY_HEX.items():
|
||||
if event_name == new_event_name:
|
||||
self.DONT_MAKE_EVENTS.add(entity)
|
||||
|
||||
for hex_code in hex_set:
|
||||
self.EVENT_NAMES_BY_HEX[hex_code] = new_event_name
|
||||
for dependent_hex in dependent_hex_set:
|
||||
self.CONDITIONAL_EVENTS[(entity_hex, dependent_hex)] = new_event_name
|
||||
|
||||
return
|
||||
|
||||
@@ -253,51 +253,101 @@ class WitnessPlayerLogic:
|
||||
line = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[line]["checkName"]
|
||||
self.ADDED_CHECKS.add(line)
|
||||
|
||||
def make_options_adjustments(self, world: "WitnessWorld"):
|
||||
"""Makes logic adjustments based on options"""
|
||||
adjustment_linesets_in_order = []
|
||||
@staticmethod
|
||||
def handle_postgame(world: "WitnessWorld"):
|
||||
# In shuffle_postgame, panels that become accessible "after or at the same time as the goal" are disabled.
|
||||
# This has a lot of complicated considerations, which I'll try my best to explain.
|
||||
postgame_adjustments = []
|
||||
|
||||
# Postgame
|
||||
|
||||
doors = world.options.shuffle_doors >= 2
|
||||
lasers = world.options.shuffle_lasers
|
||||
early_caves = world.options.early_caves > 0
|
||||
# Make some quick references to some options
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
early_caves = world.options.early_caves
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
chal_lasers = world.options.challenge_lasers
|
||||
|
||||
mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mnt_lasers)
|
||||
# Goal is "short box" but short box requires more lasers than long box
|
||||
reverse_shortbox_goal = victory == "mountain_box_short" and mnt_lasers > chal_lasers
|
||||
|
||||
# Goal is "short box", and long box requires at least as many lasers as short box (as god intended)
|
||||
proper_shortbox_goal = victory == "mountain_box_short" and chal_lasers >= mnt_lasers
|
||||
|
||||
# Goal is "long box", but short box requires at least as many lasers than long box.
|
||||
reverse_longbox_goal = victory == "mountain_box_long" and mnt_lasers >= chal_lasers
|
||||
|
||||
# If goal is shortbox or "reverse longbox", you will never enter the mountain from the top before winning.
|
||||
mountain_enterable_from_top = not (victory == "mountain_box_short" or reverse_longbox_goal)
|
||||
|
||||
# Caves & Challenge should never have anything if doors are vanilla - definitionally "post-game"
|
||||
# This is technically imprecise, but it matches player expectations better.
|
||||
if not (early_caves or doors):
|
||||
postgame_adjustments.append(get_caves_exclusion_list())
|
||||
postgame_adjustments.append(get_beyond_challenge_exclusion_list())
|
||||
|
||||
# If Challenge is the goal, some panels on the way need to be left on, as well as Challenge Vault box itself
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(get_path_to_challenge_exclusion_list())
|
||||
postgame_adjustments.append(get_challenge_vault_box_exclusion_list())
|
||||
|
||||
# Challenge can only have something if the goal is not challenge or longbox itself.
|
||||
# In case of shortbox, it'd have to be a "reverse shortbox" situation where shortbox requires *more* lasers.
|
||||
# In that case, it'd also have to be a doors mode, but that's already covered by the previous block.
|
||||
if not (victory == "elevator" or reverse_shortbox_goal):
|
||||
postgame_adjustments.append(get_beyond_challenge_exclusion_list())
|
||||
if not victory == "challenge":
|
||||
postgame_adjustments.append(get_challenge_vault_box_exclusion_list())
|
||||
|
||||
# Mountain can't be reached if the goal is shortbox (or "reverse long box")
|
||||
if not mountain_enterable_from_top:
|
||||
postgame_adjustments.append(get_mountain_upper_exclusion_list())
|
||||
|
||||
# Same goes for lower mountain, but that one *can* be reached in remote doors modes.
|
||||
if not doors:
|
||||
postgame_adjustments.append(get_mountain_lower_exclusion_list())
|
||||
|
||||
# The Mountain Bottom Floor Discard is a bit complicated, so we handle it separately. ("it" == the Discard)
|
||||
# In Elevator Goal, it is definitionally in the post-game, unless remote doors is played.
|
||||
# In Challenge Goal, it is before the Challenge, so it is not post-game.
|
||||
# In Short Box Goal, you can win before turning it on, UNLESS Short Box requires MORE lasers than long box.
|
||||
# In Long Box Goal, it is always in the post-game because solving long box is what turns it on.
|
||||
if not ((victory == "elevator" and doors) or victory == "challenge" or (reverse_shortbox_goal and doors)):
|
||||
# We now know Bottom Floor Discard is in the post-game.
|
||||
# This has different consequences depending on whether remote doors is being played.
|
||||
# If doors are vanilla, Bottom Floor Discard locks a door to an area, which has to be disabled as well.
|
||||
if doors:
|
||||
postgame_adjustments.append(get_bottom_floor_discard_exclusion_list())
|
||||
else:
|
||||
postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# In Challenge goal + early_caves + vanilla doors, you could find something important on Bottom Floor Discard,
|
||||
# including the Caves Shortcuts themselves if playing "early_caves: start_inventory".
|
||||
# This is another thing that was deemed "unfun" more than fitting the actual definition of post-game.
|
||||
if victory == "challenge" and early_caves and not doors:
|
||||
postgame_adjustments.append(get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
# If we have a proper short box goal, long box will never be activated first.
|
||||
if proper_shortbox_goal:
|
||||
postgame_adjustments.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
|
||||
return postgame_adjustments
|
||||
|
||||
def make_options_adjustments(self, world: "WitnessWorld"):
|
||||
"""Makes logic adjustments based on options"""
|
||||
adjustment_linesets_in_order = []
|
||||
|
||||
# Make condensed references to some options
|
||||
|
||||
doors = world.options.shuffle_doors >= 2 # "Panels" mode has no overarching region accessibility implications.
|
||||
lasers = world.options.shuffle_lasers
|
||||
victory = world.options.victory_condition
|
||||
mnt_lasers = world.options.mountain_lasers
|
||||
chal_lasers = world.options.challenge_lasers
|
||||
|
||||
# Exclude panels from the post-game if shuffle_postgame is false.
|
||||
if not world.options.shuffle_postgame:
|
||||
if not (early_caves or doors):
|
||||
adjustment_linesets_in_order.append(get_caves_exclusion_list())
|
||||
if not victory == 1:
|
||||
adjustment_linesets_in_order.append(get_path_to_challenge_exclusion_list())
|
||||
adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list())
|
||||
adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list())
|
||||
|
||||
if not ((doors or early_caves) and (victory == 0 or (victory == 2 and mnt_lasers > chal_lasers))):
|
||||
adjustment_linesets_in_order.append(get_beyond_challenge_exclusion_list())
|
||||
if not victory == 1:
|
||||
adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list())
|
||||
|
||||
if not (doors or mountain_enterable_from_top):
|
||||
adjustment_linesets_in_order.append(get_mountain_lower_exclusion_list())
|
||||
|
||||
if not mountain_enterable_from_top:
|
||||
adjustment_linesets_in_order.append(get_mountain_upper_exclusion_list())
|
||||
|
||||
if not ((victory == 0 and doors) or victory == 1 or (victory == 2 and mnt_lasers > chal_lasers and doors)):
|
||||
if doors:
|
||||
adjustment_linesets_in_order.append(get_bottom_floor_discard_exclusion_list())
|
||||
else:
|
||||
adjustment_linesets_in_order.append(get_bottom_floor_discard_nondoors_exclusion_list())
|
||||
|
||||
if victory == 2 and chal_lasers >= mnt_lasers:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:", "0xFFF00 (Mountain Box Long)"])
|
||||
adjustment_linesets_in_order += self.handle_postgame(world)
|
||||
|
||||
# Exclude Discards / Vaults
|
||||
|
||||
if not world.options.shuffle_discarded_panels:
|
||||
# In disable_non_randomized, the discards are needed for alternate activation triggers, UNLESS both
|
||||
# (remote) doors and lasers are shuffled.
|
||||
@@ -309,18 +359,18 @@ class WitnessPlayerLogic:
|
||||
|
||||
if not world.options.shuffle_vault_boxes:
|
||||
adjustment_linesets_in_order.append(get_vault_exclusion_list())
|
||||
if not victory == 1:
|
||||
if not victory == "challenge":
|
||||
adjustment_linesets_in_order.append(get_challenge_vault_box_exclusion_list())
|
||||
|
||||
# Victory Condition
|
||||
|
||||
if victory == 0:
|
||||
if victory == "elevator":
|
||||
self.VICTORY_LOCATION = "0x3D9A9"
|
||||
elif victory == 1:
|
||||
elif victory == "challenge":
|
||||
self.VICTORY_LOCATION = "0x0356B"
|
||||
elif victory == 2:
|
||||
elif victory == "mountain_box_short":
|
||||
self.VICTORY_LOCATION = "0x09F7F"
|
||||
elif victory == 3:
|
||||
elif victory == "mountain_box_long":
|
||||
self.VICTORY_LOCATION = "0xFFF00"
|
||||
|
||||
# Long box can usually only be solved by opening Mountain Entry. However, if it requires 7 lasers or less
|
||||
@@ -338,36 +388,36 @@ class WitnessPlayerLogic:
|
||||
if world.options.shuffle_symbols:
|
||||
adjustment_linesets_in_order.append(get_symbol_shuffle_list())
|
||||
|
||||
if world.options.EP_difficulty == 0:
|
||||
if world.options.EP_difficulty == "normal":
|
||||
adjustment_linesets_in_order.append(get_ep_easy())
|
||||
elif world.options.EP_difficulty == 1:
|
||||
elif world.options.EP_difficulty == "tedious":
|
||||
adjustment_linesets_in_order.append(get_ep_no_eclipse())
|
||||
|
||||
if world.options.door_groupings == 1:
|
||||
if world.options.shuffle_doors == 1:
|
||||
if world.options.door_groupings == "regional":
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(get_simple_panels())
|
||||
elif world.options.shuffle_doors == 2:
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
elif world.options.shuffle_doors == 3:
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(get_simple_doors())
|
||||
adjustment_linesets_in_order.append(get_simple_additional_panels())
|
||||
else:
|
||||
if world.options.shuffle_doors == 1:
|
||||
if world.options.shuffle_doors == "panels":
|
||||
adjustment_linesets_in_order.append(get_complex_door_panels())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
elif world.options.shuffle_doors == 2:
|
||||
elif world.options.shuffle_doors == "doors":
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
elif world.options.shuffle_doors == 3:
|
||||
elif world.options.shuffle_doors == "mixed":
|
||||
adjustment_linesets_in_order.append(get_complex_doors())
|
||||
adjustment_linesets_in_order.append(get_complex_additional_panels())
|
||||
|
||||
if world.options.shuffle_boat:
|
||||
adjustment_linesets_in_order.append(get_boat())
|
||||
|
||||
if world.options.early_caves == 2:
|
||||
if world.options.early_caves == "starting_inventory":
|
||||
adjustment_linesets_in_order.append(get_early_caves_start_list())
|
||||
|
||||
if world.options.early_caves == 1 and not doors:
|
||||
if world.options.early_caves == "add_to_pool" and not doors:
|
||||
adjustment_linesets_in_order.append(get_early_caves_list())
|
||||
|
||||
if world.options.elevators_come_to_you:
|
||||
@@ -387,29 +437,25 @@ class WitnessPlayerLogic:
|
||||
obelisk = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[self.REFERENCE_LOGIC.EP_TO_OBELISK_SIDE[ep_hex]]
|
||||
obelisk_name = obelisk["checkName"]
|
||||
ep_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[ep_hex]["checkName"]
|
||||
self.EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[ep_hex] = f"{obelisk_name} - {ep_name}"
|
||||
else:
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + get_ep_obelisks()[1:])
|
||||
|
||||
if world.options.shuffle_EPs == 0:
|
||||
if not world.options.shuffle_EPs:
|
||||
adjustment_linesets_in_order.append(["Irrelevant Locations:"] + get_ep_all_individual()[1:])
|
||||
|
||||
yaml_disabled_eps = []
|
||||
|
||||
for yaml_disabled_location in self.YAML_DISABLED_LOCATIONS:
|
||||
if yaml_disabled_location not in self.REFERENCE_LOGIC.ENTITIES_BY_NAME:
|
||||
continue
|
||||
|
||||
loc_obj = self.REFERENCE_LOGIC.ENTITIES_BY_NAME[yaml_disabled_location]
|
||||
|
||||
if loc_obj["entityType"] == "EP" and world.options.shuffle_EPs != 0:
|
||||
yaml_disabled_eps.append(loc_obj["entity_hex"])
|
||||
if loc_obj["entityType"] == "EP":
|
||||
self.COMPLETELY_DISABLED_ENTITIES.add(loc_obj["entity_hex"])
|
||||
|
||||
if loc_obj["entityType"] in {"EP", "General", "Vault", "Discard"}:
|
||||
elif loc_obj["entityType"] in {"General", "Vault", "Discard"}:
|
||||
self.EXCLUDED_LOCATIONS.add(loc_obj["entity_hex"])
|
||||
|
||||
adjustment_linesets_in_order.append(["Disabled Locations:"] + yaml_disabled_eps)
|
||||
|
||||
for adjustment_lineset in adjustment_linesets_in_order:
|
||||
current_adjustment_type = None
|
||||
|
||||
@@ -459,7 +505,8 @@ class WitnessPlayerLogic:
|
||||
for option in connection[1]:
|
||||
individual_entity_requirements = []
|
||||
for entity in option:
|
||||
if entity in self.EVENT_NAMES_BY_HEX or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX:
|
||||
if (entity in self.ALWAYS_EVENT_NAMES_BY_HEX
|
||||
or entity not in self.REFERENCE_LOGIC.ENTITIES_BY_HEX):
|
||||
individual_entity_requirements.append(frozenset({frozenset({entity})}))
|
||||
else:
|
||||
entity_req = self.reduce_req_within_region(entity)
|
||||
@@ -476,6 +523,72 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections
|
||||
|
||||
def solvability_guaranteed(self, entity_hex: str):
|
||||
return not (
|
||||
entity_hex in self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY
|
||||
or entity_hex in self.COMPLETELY_DISABLED_ENTITIES
|
||||
or entity_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES
|
||||
)
|
||||
|
||||
def determine_unrequired_entities(self, world: "WitnessWorld"):
|
||||
"""Figure out which major items are actually useless in this world's settings"""
|
||||
|
||||
# Gather quick references to relevant options
|
||||
eps_shuffled = world.options.shuffle_EPs
|
||||
come_to_you = world.options.elevators_come_to_you
|
||||
difficulty = world.options.puzzle_randomization
|
||||
discards_shuffled = world.options.shuffle_discarded_panels
|
||||
boat_shuffled = world.options.shuffle_boat
|
||||
symbols_shuffled = world.options.shuffle_symbols
|
||||
disable_non_randomized = world.options.disable_non_randomized_puzzles
|
||||
postgame_included = world.options.shuffle_postgame
|
||||
goal = world.options.victory_condition
|
||||
doors = world.options.shuffle_doors
|
||||
shortbox_req = world.options.mountain_lasers
|
||||
longbox_req = world.options.challenge_lasers
|
||||
|
||||
# Make some helper booleans so it is easier to follow what's going on
|
||||
mountain_upper_is_in_postgame = (
|
||||
goal == "mountain_box_short"
|
||||
or goal == "mountain_box_long" and longbox_req <= shortbox_req
|
||||
)
|
||||
mountain_upper_included = postgame_included or not mountain_upper_is_in_postgame
|
||||
remote_doors = doors >= 2
|
||||
door_panels = doors == "panels" or doors == "mixed"
|
||||
|
||||
# It is easier to think about when these items *are* required, so we make that dict first
|
||||
# If the entity is disabled anyway, we don't need to consider that case
|
||||
is_item_required_dict = {
|
||||
"0x03750": eps_shuffled, # Monastery Garden Entry Door
|
||||
"0x275FA": eps_shuffled, # Boathouse Hook Control
|
||||
"0x17D02": eps_shuffled, # Windmill Turn Control
|
||||
"0x0368A": symbols_shuffled or door_panels, # Quarry Stoneworks Stairs Door
|
||||
"0x3865F": symbols_shuffled or door_panels or eps_shuffled, # Quarry Boathouse 2nd Barrier
|
||||
"0x17CC4": come_to_you or eps_shuffled, # Quarry Elevator Panel
|
||||
"0x17E2B": come_to_you and boat_shuffled or eps_shuffled, # Swamp Long Bridge
|
||||
"0x0CF2A": False, # Jungle Monastery Garden Shortcut
|
||||
"0x17CAA": remote_doors, # Jungle Monastery Garden Shortcut Panel
|
||||
"0x0364E": False, # Monastery Laser Shortcut Door
|
||||
"0x03713": remote_doors, # Monastery Laser Shortcut Panel
|
||||
"0x03313": False, # Orchard Second Gate
|
||||
"0x337FA": remote_doors, # Jungle Bamboo Laser Shortcut Panel
|
||||
"0x3873B": False, # Jungle Bamboo Laser Shortcut Door
|
||||
"0x335AB": False, # Caves Elevator Controls
|
||||
"0x335AC": False, # Caves Elevator Controls
|
||||
"0x3369D": False, # Caves Elevator Controls
|
||||
"0x01BEA": difficulty == "none" and eps_shuffled, # Keep PP2
|
||||
"0x0A0C9": eps_shuffled or discards_shuffled or disable_non_randomized, # Cargo Box Entry Door
|
||||
"0x09EEB": discards_shuffled or mountain_upper_included, # Mountain Floor 2 Elevator Control Panel
|
||||
"0x09EDD": mountain_upper_included, # Mountain Floor 2 Exit Door
|
||||
"0x17CAB": symbols_shuffled or not disable_non_randomized or "0x17CAB" not in self.DOOR_ITEMS_BY_ID,
|
||||
# Jungle Popup Wall Panel
|
||||
}
|
||||
|
||||
# Now, return the keys of the dict entries where the result is False to get unrequired major items
|
||||
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY |= {
|
||||
item_name for item_name, is_required in is_item_required_dict.items() if not is_required
|
||||
}
|
||||
|
||||
def make_event_item_pair(self, panel: str):
|
||||
"""
|
||||
Makes a pair of an event panel and its event item
|
||||
@@ -483,21 +596,23 @@ class WitnessPlayerLogic:
|
||||
action = " Opened" if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["entityType"] == "Door" else " Solved"
|
||||
|
||||
name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[panel]["checkName"] + action
|
||||
if panel not in self.EVENT_NAMES_BY_HEX:
|
||||
if panel not in self.USED_EVENT_NAMES_BY_HEX:
|
||||
warning("Panel \"" + name + "\" does not have an associated event name.")
|
||||
self.EVENT_NAMES_BY_HEX[panel] = name + " Event"
|
||||
pair = (name, self.EVENT_NAMES_BY_HEX[panel])
|
||||
self.USED_EVENT_NAMES_BY_HEX[panel] = name + " Event"
|
||||
pair = (name, self.USED_EVENT_NAMES_BY_HEX[panel])
|
||||
return pair
|
||||
|
||||
def make_event_panel_lists(self):
|
||||
self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
|
||||
|
||||
for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items():
|
||||
if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
|
||||
continue
|
||||
self.EVENT_PANELS.add(event_hex)
|
||||
self.USED_EVENT_NAMES_BY_HEX.update(self.ALWAYS_EVENT_NAMES_BY_HEX)
|
||||
|
||||
for panel in self.EVENT_PANELS:
|
||||
self.USED_EVENT_NAMES_BY_HEX = {
|
||||
event_hex: event_name for event_hex, event_name in self.USED_EVENT_NAMES_BY_HEX.items()
|
||||
if self.solvability_guaranteed(event_hex)
|
||||
}
|
||||
|
||||
for panel in self.USED_EVENT_NAMES_BY_HEX:
|
||||
pair = self.make_event_item_pair(panel)
|
||||
self.EVENT_ITEM_PAIRS[pair[0]] = pair[1]
|
||||
|
||||
@@ -510,6 +625,8 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES = set()
|
||||
|
||||
self.ENTITIES_WITHOUT_ENSURED_SOLVABILITY = set()
|
||||
|
||||
self.THEORETICAL_ITEMS = set()
|
||||
self.THEORETICAL_ITEMS_NO_MULTI = set()
|
||||
self.MULTI_AMOUNTS = defaultdict(lambda: 1)
|
||||
@@ -519,13 +636,13 @@ class WitnessPlayerLogic:
|
||||
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
|
||||
self.STARTING_INVENTORY = set()
|
||||
|
||||
self.DIFFICULTY = world.options.puzzle_randomization.value
|
||||
self.DIFFICULTY = world.options.puzzle_randomization
|
||||
|
||||
if self.DIFFICULTY == 0:
|
||||
if self.DIFFICULTY == "sigma_normal":
|
||||
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_normal
|
||||
elif self.DIFFICULTY == 1:
|
||||
elif self.DIFFICULTY == "sigma_expert":
|
||||
self.REFERENCE_LOGIC = StaticWitnessLogic.sigma_expert
|
||||
elif self.DIFFICULTY == 2:
|
||||
elif self.DIFFICULTY == "none":
|
||||
self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla
|
||||
|
||||
self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME)
|
||||
@@ -534,16 +651,14 @@ class WitnessPlayerLogic:
|
||||
|
||||
# Determining which panels need to be events is a difficult process.
|
||||
# At the end, we will have EVENT_ITEM_PAIRS for all the necessary ones.
|
||||
self.EVENT_PANELS = set()
|
||||
self.EVENT_ITEM_PAIRS = dict()
|
||||
self.DONT_MAKE_EVENTS = set()
|
||||
self.COMPLETELY_DISABLED_ENTITIES = set()
|
||||
self.PRECOMPLETED_LOCATIONS = set()
|
||||
self.EXCLUDED_LOCATIONS = set()
|
||||
self.ADDED_CHECKS = set()
|
||||
self.VICTORY_LOCATION = "0x0356B"
|
||||
|
||||
self.EVENT_NAMES_BY_HEX = {
|
||||
self.ALWAYS_EVENT_NAMES_BY_HEX = {
|
||||
"0x00509": "+1 Laser (Symmetry Laser)",
|
||||
"0x012FB": "+1 Laser (Desert Laser)",
|
||||
"0x09F98": "Desert Laser Redirection",
|
||||
@@ -556,10 +671,14 @@ class WitnessPlayerLogic:
|
||||
"0x0C2B2": "+1 Laser (Bunker Laser)",
|
||||
"0x00BF6": "+1 Laser (Swamp Laser)",
|
||||
"0x028A4": "+1 Laser (Treehouse Laser)",
|
||||
"0x09F7F": "Mountain Entry",
|
||||
"0x17C34": "Mountain Entry",
|
||||
"0xFFF00": "Bottom Floor Discard Turns On",
|
||||
}
|
||||
|
||||
self.USED_EVENT_NAMES_BY_HEX = {}
|
||||
self.CONDITIONAL_EVENTS = {}
|
||||
|
||||
self.make_options_adjustments(world)
|
||||
self.determine_unrequired_entities(world)
|
||||
self.make_dependency_reduced_checklist()
|
||||
self.make_event_panel_lists()
|
||||
|
||||
@@ -134,13 +134,13 @@ class WitnessRegions:
|
||||
world.multiworld.regions += final_regions_list
|
||||
|
||||
def __init__(self, locat: WitnessPlayerLocations, world: "WitnessWorld"):
|
||||
difficulty = world.options.puzzle_randomization.value
|
||||
difficulty = world.options.puzzle_randomization
|
||||
|
||||
if difficulty == 0:
|
||||
if difficulty == "sigma_normal":
|
||||
self.reference_logic = StaticWitnessLogic.sigma_normal
|
||||
elif difficulty == 1:
|
||||
elif difficulty == "sigma_expert":
|
||||
self.reference_logic = StaticWitnessLogic.sigma_expert
|
||||
elif difficulty == 2:
|
||||
elif difficulty == "none":
|
||||
self.reference_logic = StaticWitnessLogic.vanilla
|
||||
|
||||
self.locat = locat
|
||||
|
||||
@@ -170,7 +170,7 @@ def _has_item(item: str, world: "WitnessWorld", player: int,
|
||||
return lambda state: _can_do_expert_pp2(state, world)
|
||||
elif item == "Theater to Tunnels":
|
||||
return lambda state: _can_do_theater_to_tunnels(state, world)
|
||||
if item in player_logic.EVENT_PANELS:
|
||||
if item in player_logic.USED_EVENT_NAMES_BY_HEX:
|
||||
return _can_solve_panel(item, world, player, player_logic, locat)
|
||||
|
||||
prog_item = StaticWitnessLogic.get_parent_progressive_item(item)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
Event Items:
|
||||
Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9
|
||||
Bunker Laser Activation - 0x00061,0x17D01,0x17C42
|
||||
Shadows Laser Activation - 0x00021,0x17D28,0x17C71
|
||||
Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x17CF7
|
||||
Jungle Popup Wall Lifts - 0x17FA0,0x17D27,0x17F9B,0x17CAB
|
||||
Monastery Laser Activation - 0x17C65 - 0x00A5B,0x17CE7,0x17FA9
|
||||
Bunker Laser Activation - 0x0C2B2 - 0x00061,0x17D01,0x17C42
|
||||
Shadows Laser Activation - 0x181B3 - 0x00021,0x17D28,0x17C71
|
||||
Town Tower 4th Door Opens - 0x2779A - 0x17CFB,0x3C12B,0x17CF7
|
||||
Jungle Popup Wall Lifts - 0x1475B - 0x17FA0,0x17D27,0x17F9B,0x17CAB
|
||||
|
||||
Requirement Changes:
|
||||
0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9
|
||||
|
||||
Reference in New Issue
Block a user