Skip to content

Commit

Permalink
LttP: extract Dungeon and Boss from core (#1787)
Browse files Browse the repository at this point in the history
  • Loading branch information
Berserker66 authored May 20, 2023
1 parent a2ddd5c commit c845303
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 306 deletions.
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
76 changes: 49 additions & 27 deletions worlds/alttp/Bosses.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
from __future__ import annotations

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

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

if TYPE_CHECKING:
from . import ALTTPWorld


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 +185,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(world: "ALTTPWorld", bosses: List[str]) -> Tuple[List[str], List[Tuple[str, str]]]:
# Most to least restrictive order
boss_locations = boss_location_table.copy()
world.random.shuffle(boss_locations)
world.multiworld.random.shuffle(boss_locations)
boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location]))
already_placed_bosses: List[str] = []

Expand All @@ -184,12 +203,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(world, 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(world, boss, boss_locations)

return already_placed_bosses, boss_locations

Expand Down Expand Up @@ -224,20 +243,23 @@ 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(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
player = world.player
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted':
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 format_boss_location(location_name: str, level: str) -> str:
return location_name + (' (' + level + ')' if level else '')


def place_bosses(world, player: int) -> None:
def place_bosses(world: "ALTTPWorld") -> None:
multiworld = world.multiworld
player = world.player
# 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
already_placed_bosses: List[str] = []
remaining_locations: List[Tuple[str, str]] = []
# handle plando
Expand All @@ -246,14 +268,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(world, options)
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 +285,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 +297,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 +310,39 @@ 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(world, 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(world, 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(world, 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(world, 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(world: "ALTTPWorld", boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]:
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(world, boss, loc, level)
placed_bosses.append(boss)
else:
remainder.append((loc, level))
Expand Down
Loading

0 comments on commit c845303

Please sign in to comment.