Skip to content

LttP: extract Dungeon and Boss from core #1787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 20, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 0 additions & 65 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ def __init__(self, players: int):
self.player_types = {player: NetUtils.SlotType.player for player in self.player_ids}
self.glitch_triforce = False
self.algorithm = 'balanced'
self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
self.groups = {}
self.regions = []
self.shops = []
Expand Down Expand Up @@ -386,12 +385,6 @@ def get_location(self, location: str, player: int) -> Location:
self._recache()
return self._location_cache[location, player]

def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
try:
return self.dungeons[dungeonname, player]
except KeyError as e:
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e

def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
Expand Down Expand Up @@ -801,7 +794,6 @@ class Region:
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
dungeon: Optional[Dungeon] = None

def __init__(self, name: str, player: int, multiworld: MultiWorld, hint: Optional[str] = None):
self.name = name
Expand Down Expand Up @@ -904,63 +896,6 @@ def __str__(self):
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'


class Dungeon(object):
def __init__(self, name: str, regions: List[Region], big_key: Item, small_keys: List[Item],
dungeon_items: List[Item], player: int):
self.name = name
self.regions = regions
self.big_key = big_key
self.small_keys = small_keys
self.dungeon_items = dungeon_items
self.bosses = dict()
self.player = player
self.multiworld = None

@property
def boss(self) -> Optional[Boss]:
return self.bosses.get(None, None)

@boss.setter
def boss(self, value: Optional[Boss]):
self.bosses[None] = value

@property
def keys(self) -> List[Item]:
return self.small_keys + ([self.big_key] if self.big_key else [])

@property
def all_items(self) -> List[Item]:
return self.dungeon_items + self.keys

def is_dungeon_item(self, item: Item) -> bool:
return item.player == self.player and item.name in (dungeon_item.name for dungeon_item in self.all_items)

def __eq__(self, other: Dungeon) -> bool:
if not other:
return False
return self.name == other.name and self.player == other.player

def __repr__(self):
return self.__str__()

def __str__(self):
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'


class Boss():
def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
self.player = player

def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)

def __repr__(self):
return f"Boss({self.name})"


class LocationProgressType(IntEnum):
DEFAULT = 1
PRIORITY = 2
Expand Down
65 changes: 41 additions & 24 deletions worlds/alttp/Bosses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
from __future__ import annotations

import logging
from typing import Optional, Union, List, Tuple, Callable, Dict

from BaseClasses import Boss
from Fill import FillError
from .Options import LTTPBosses as Bosses
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source
from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, \
has_melee_weapon, has_fire_source


class Boss:
def __init__(self, name: str, enemizer_name: str, defeat_rule: Callable, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
self.player = player

def can_defeat(self, state) -> bool:
return self.defeat_rule(state, self.player)

def __repr__(self):
return f"Boss({self.name})"


def BossFactory(boss: str, player: int) -> Optional[Boss]:
Expand Down Expand Up @@ -166,10 +182,10 @@ def GanonDefeatRule(state, player: int) -> bool:
]


def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
def place_plando_bosses(bosses: List[str], multiworld, world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]:
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
multiworld.random.shuffle(boss_locations)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we only use multiworld here for random. see the other comment about random

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am keeping this intentionally like this for now.

boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
already_placed_bosses: List[str] = []

Expand All @@ -184,12 +200,12 @@ def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str
level = loc[-1]
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
place_boss(world, player, boss, loc, level)
place_boss(multiworld, world, player, boss, loc, level)
already_placed_bosses.append(boss)
boss_locations.remove((loc, level))
else: # boss chosen with no specified locations
boss = boss.title()
boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations)
boss_locations, already_placed_bosses = place_where_possible(multiworld, world, player, boss, boss_locations)

return already_placed_bosses, boss_locations

Expand Down Expand Up @@ -224,20 +240,20 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) ->
for boss in boss_table if not boss.startswith("Agahnim"))


def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and world.mode[player] == 'inverted':
def place_boss(multiworld, world, player: int, boss: str, location: str, level: Optional[str]) -> None:
if location == 'Ganons Tower' and multiworld.mode[player] == 'inverted':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I agree, with el-u's comment. It'd be better to either pass multiworld+player or world, not both.)
Here we could do world.multiworld.mode and once options are part of world, this can be simplified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

location = 'Inverted Ganons Tower'
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player)
world.dungeons[location].bosses[level] = BossFactory(boss, player)


def format_boss_location(location: str, level: str) -> str:
return location + (' (' + level + ')' if level else '')


def place_bosses(world, player: int) -> None:
def place_bosses(multiworld, world, player: int) -> None:
Copy link
Collaborator

@el-u el-u May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pass around multiworld separately everywhere (in addition to world) when one could access it via world.multiworld where needed?

(Yes, I realize that in the old code the world argument is of type MultiWorld and that therefore in the new code it is actually the world argument that has been newly added, but still, that makes passing the multiworld redundant.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

# will either be an int or a lower case string with ';' between options
boss_shuffle: Union[str, int] = world.boss_shuffle[player].value
boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above and also the random might go away when/if we fade out non-slot-random? multiworld is only required for random and the one option, so you could capture both from world.multiworld at the top.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando
Expand All @@ -246,14 +262,14 @@ def place_bosses(world, player: int) -> None:
options = boss_shuffle.split(";")
boss_shuffle = Bosses.options[options.pop()]
# place our plando bosses
already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player)
already_placed_bosses, remaining_locations = place_plando_bosses(options, multiworld, world, player)
if boss_shuffle == Bosses.option_none: # vanilla boss locations
return

# Most to least restrictive order
if not remaining_locations and not already_placed_bosses:
remaining_locations = boss_location_table.copy()
world.random.shuffle(remaining_locations)
multiworld.random.shuffle(remaining_locations)
remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))

all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons
Expand All @@ -263,7 +279,7 @@ def place_bosses(world, player: int) -> None:
if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled
bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm']
else: # all bosses present, the three duplicates chosen at random
bosses = placeable_bosses + world.random.sample(placeable_bosses, 3)
bosses = placeable_bosses + multiworld.random.sample(placeable_bosses, 3)

# there is probably a better way to do this
while already_placed_bosses:
Expand All @@ -275,7 +291,7 @@ def place_bosses(world, player: int) -> None:

logging.debug('Bosses chosen %s', bosses)

world.random.shuffle(bosses)
multiworld.random.shuffle(bosses)
for loc, level in remaining_locations:
for _ in range(len(bosses)):
boss = bosses.pop()
Expand All @@ -288,39 +304,40 @@ def place_bosses(world, player: int) -> None:
else:
raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}')

place_boss(world, player, boss, loc, level)
place_boss(multiworld, world, player, boss, loc, level)

elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random
for loc, level in remaining_locations:
try:
boss = world.random.choice(
boss = multiworld.random.choice(
[b for b in placeable_bosses if can_place_boss(b, loc, level)])
except IndexError:
raise FillError(f'Could not place boss for location {format_boss_location(loc, level)}')
else:
place_boss(world, player, boss, loc, level)
place_boss(multiworld, world, player, boss, loc, level)

elif boss_shuffle == Bosses.option_singularity:
primary_boss = world.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations)
primary_boss = multiworld.random.choice(placeable_bosses)
remaining_boss_locations, _ = place_where_possible(multiworld, world, player, primary_boss, remaining_locations)
if remaining_boss_locations:
# pick a boss to go into the remaining locations
remaining_boss = world.random.choice([boss for boss in placeable_bosses if all(
remaining_boss = multiworld.random.choice([boss for boss in placeable_bosses if all(
can_place_boss(boss, loc, level) for loc, level in remaining_boss_locations)])
remaining_boss_locations, _ = place_where_possible(world, player, remaining_boss, remaining_boss_locations)
remaining_boss_locations, _ = place_where_possible(multiworld, world, player, remaining_boss,
remaining_boss_locations)
if remaining_boss_locations:
raise Exception("Unfilled boss locations!")
else:
raise FillError(f"Could not find boss shuffle mode {boss_shuffle}")


def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
def place_where_possible(multiworld, world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we only pass it down to the next stage

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be fixed now

remainder: List[Tuple[str, str]] = []
placed_bosses: List[str] = []
for loc, level in boss_locations:
# place that boss where it can go
if can_place_boss(boss, loc, level):
place_boss(world, player, boss, loc, level)
place_boss(multiworld, world, player, boss, loc, level)
placed_bosses.append(boss)
else:
remainder.append((loc, level))
Expand Down
Loading