Skip to content

Commit

Permalink
SoE: use new AP API and naming and make APworld (#2701)
Browse files Browse the repository at this point in the history
* SoE: new file naming

also fixes test base deprecation

* SoE: use options_dataclass

* SoE: moar typing

* SoE: no more multiworld.random

* SoE: replace LogicMixin by SoEPlayerLogic object

* SoE: add test that rocket parts always exist

* SoE: Even moar typing

* SoE: can haz apworld now

* SoE: pep up test naming

* SoE: use self.options for trap chances

* SoE: remove unused import with outdated comment

* SoE: move flag and trap extraction to dataclass

as suggested by beauxq

* SoE: test trap option parsing and item generation
  • Loading branch information
black-sliver authored Jan 12, 2024
1 parent 47dd364 commit e00b5a7
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 219 deletions.
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Secret of Evermore",
"Slay the Spire",
"Sudoku",
"Super Mario 64",
Expand Down
70 changes: 0 additions & 70 deletions worlds/soe/Logic.py

This file was deleted.

166 changes: 73 additions & 93 deletions worlds/soe/__init__.py

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions worlds/soe/logic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import typing
from typing import Callable, Set

from . import pyevermizer
from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions

if typing.TYPE_CHECKING:
from BaseClasses import CollectionState

# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?

# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]


class SoEPlayerLogic:
__slots__ = "player", "out_of_bounds", "sequence_breaks", "has"
player: int
out_of_bounds: bool
sequence_breaks: bool

has: Callable[..., bool]
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""

def __init__(self, player: int, options: "SoEOptions"):
self.player = player
self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic
self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic

if options.energy_core == EnergyCore.option_fragments:
# override logic for energy core fragments
required_fragments = options.required_fragments.value

def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool:
if progress == pyevermizer.P_ENERGY_CORE:
progress = pyevermizer.P_CORE_FRAGMENT
count = required_fragments
return self._has(state, progress, count)

self.has = fragmented_has
else:
# default (energy core) logic
self.has = self._has

def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int:
"""
Returns reached count of one of evermizer's progress steps based on collected items.
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
"""
n = 0
for item in items:
for pvd in item.provides:
if pvd[1] == progress:
if state.has(item.name, self.player):
n += state.count(item.name, self.player) * pvd[0]
if n >= max_count > 0:
return n
for rule in rules:
for pvd in rule.provides:
if pvd[1] == progress and pvd[0] > 0:
has = True
for req in rule.requires:
if not self.has(state, req[1], req[0]):
has = False
break
if has:
n += pvd[0]
if n >= max_count > 0:
return n
return n

def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool:
"""Default implementation of has"""
if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB:
return True
if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
return True
return self._count(state, progress, count) >= count
97 changes: 58 additions & 39 deletions worlds/soe/Options.py → worlds/soe/options.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import typing
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol

from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, ProgressionBalancing, \
Range, Toggle


# typing boilerplate
class FlagsProtocol(typing.Protocol):
class FlagsProtocol(Protocol):
value: int
default: int
flags: typing.List[str]
flags: List[str]


class FlagProtocol(typing.Protocol):
class FlagProtocol(Protocol):
value: int
default: int
flag: str


# meta options
class EvermizerFlags:
flags: typing.List[str]
flags: List[str]

def to_flag(self: FlagsProtocol) -> str:
return self.flags[self.value]
Expand Down Expand Up @@ -200,13 +202,13 @@ class TrapCount(Range):

# more meta options
class ItemChanceMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
if 'item_name' in attrs:
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100

return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
return cast(ItemChanceMeta, cls)


class TrapChance(Range, metaclass=ItemChanceMeta):
Expand Down Expand Up @@ -247,33 +249,50 @@ class SoEProgressionBalancing(ProgressionBalancing):
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}


soe_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty,
"energy_core": EnergyCore,
"required_fragments": RequiredFragments,
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
"sequence_breaks": SequenceBreaks,
"out_of_bounds": OutOfBounds,
"fix_cheats": FixCheats,
"fix_infinite_ammo": FixInfiniteAmmo,
"fix_atlas_glitch": FixAtlasGlitch,
"fix_wings_glitch": FixWingsGlitch,
"shorter_dialogs": ShorterDialogs,
"short_boss_rush": ShortBossRush,
"ingredienizer": Ingredienizer,
"sniffamizer": Sniffamizer,
"callbeadamizer": Callbeadamizer,
"musicmizer": Musicmizer,
"doggomizer": Doggomizer,
"turdo_mode": TurdoMode,
"death_link": DeathLink,
"trap_count": TrapCount,
"trap_chance_quake": TrapChanceQuake,
"trap_chance_poison": TrapChancePoison,
"trap_chance_confound": TrapChanceConfound,
"trap_chance_hud": TrapChanceHUD,
"trap_chance_ohko": TrapChanceOHKO,
"progression_balancing": SoEProgressionBalancing,
}
# noinspection SpellCheckingInspection
@dataclass
class SoEOptions(PerGameCommonOptions):
difficulty: Difficulty
energy_core: EnergyCore
required_fragments: RequiredFragments
available_fragments: AvailableFragments
money_modifier: MoneyModifier
exp_modifier: ExpModifier
sequence_breaks: SequenceBreaks
out_of_bounds: OutOfBounds
fix_cheats: FixCheats
fix_infinite_ammo: FixInfiniteAmmo
fix_atlas_glitch: FixAtlasGlitch
fix_wings_glitch: FixWingsGlitch
shorter_dialogs: ShorterDialogs
short_boss_rush: ShortBossRush
ingredienizer: Ingredienizer
sniffamizer: Sniffamizer
callbeadamizer: Callbeadamizer
musicmizer: Musicmizer
doggomizer: Doggomizer
turdo_mode: TurdoMode
death_link: DeathLink
trap_count: TrapCount
trap_chance_quake: TrapChanceQuake
trap_chance_poison: TrapChancePoison
trap_chance_confound: TrapChanceConfound
trap_chance_hud: TrapChanceHUD
trap_chance_ohko: TrapChanceOHKO
progression_balancing: SoEProgressionBalancing

@property
def trap_chances(self) -> Iterator[TrapChance]:
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, TrapChance):
yield option

@property
def flags(self) -> str:
flags = ''
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
flags += getattr(self, field.name).to_flag()
return flags
6 changes: 3 additions & 3 deletions worlds/soe/Patch.py → worlds/soe/patch.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import os
from typing import Optional
from typing import BinaryIO, Optional

import Utils
from worlds.Files import APDeltaPatch
Expand Down Expand Up @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str:
return file_name


def read_rom(stream, strip_header=True) -> bytes:
def read_rom(stream: BinaryIO, strip_header: bool=True) -> bytes:
"""Reads rom into bytearray and optionally strips off any smc header"""
data = stream.read()
if strip_header and len(data) % 0x400 == 0x200:
Expand All @@ -40,5 +40,5 @@ def read_rom(stream, strip_header=True) -> bytes:

if __name__ == '__main__':
import sys
print('Please use ../../Patch.py', file=sys.stderr)
print('Please use ../../patch.py', file=sys.stderr)
sys.exit(1)
13 changes: 12 additions & 1 deletion worlds/soe/test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
from typing import Iterable


Expand All @@ -18,3 +18,14 @@ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable:
for location in unreachable:
self.assertFalse(self.can_reach_location(location),
f"{location} is reachable but shouldn't be")

def testRocketPartsExist(self):
"""Tests that rocket parts exist and are unique"""
self.assertEqual(len(self.get_items_by_name("Gauge")), 1)
self.assertEqual(len(self.get_items_by_name("Wheel")), 1)
diamond_eyes = self.get_items_by_name("Diamond Eye")
self.assertEqual(len(diamond_eyes), 3)
# verify diamond eyes are individual items
self.assertFalse(diamond_eyes[0] is diamond_eyes[1])
self.assertFalse(diamond_eyes[0] is diamond_eyes[2])
self.assertFalse(diamond_eyes[1] is diamond_eyes[2])
4 changes: 2 additions & 2 deletions worlds/soe/test/test_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class AccessTest(SoETestBase):
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]

def testBronzeAxe(self):
def test_bronze_axe(self):
gourds = {
"Pyramid bottom": (118, 121, 122, 123, 124, 125),
"Pyramid top": (140,)
Expand All @@ -16,7 +16,7 @@ def testBronzeAxe(self):
items = [["Bronze Axe"]]
self.assertAccessDependency(locations, items)

def testBronzeSpearPlus(self):
def test_bronze_spear_plus(self):
locations = ["Megataur"]
items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
self.assertAccessDependency(locations, items)
Loading

0 comments on commit e00b5a7

Please sign in to comment.