Skip to content

Commit e00b5a7

Browse files
authored
SoE: use new AP API and naming and make APworld (#2701)
* 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
1 parent 47dd364 commit e00b5a7

12 files changed

+298
-219
lines changed

setup.py

-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
"Ocarina of Time",
7777
"Overcooked! 2",
7878
"Raft",
79-
"Secret of Evermore",
8079
"Slay the Spire",
8180
"Sudoku",
8281
"Super Mario 64",

worlds/soe/Logic.py

-70
This file was deleted.

worlds/soe/__init__.py

+73-93
Large diffs are not rendered by default.

worlds/soe/logic.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import typing
2+
from typing import Callable, Set
3+
4+
from . import pyevermizer
5+
from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions
6+
7+
if typing.TYPE_CHECKING:
8+
from BaseClasses import CollectionState
9+
10+
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
11+
12+
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
13+
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
14+
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
15+
# Logic.items are all items and extra items excluding non-progression items and duplicates
16+
item_names: Set[str] = set()
17+
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
18+
if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]
19+
20+
21+
class SoEPlayerLogic:
22+
__slots__ = "player", "out_of_bounds", "sequence_breaks", "has"
23+
player: int
24+
out_of_bounds: bool
25+
sequence_breaks: bool
26+
27+
has: Callable[..., bool]
28+
"""
29+
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
30+
"""
31+
32+
def __init__(self, player: int, options: "SoEOptions"):
33+
self.player = player
34+
self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic
35+
self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic
36+
37+
if options.energy_core == EnergyCore.option_fragments:
38+
# override logic for energy core fragments
39+
required_fragments = options.required_fragments.value
40+
41+
def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool:
42+
if progress == pyevermizer.P_ENERGY_CORE:
43+
progress = pyevermizer.P_CORE_FRAGMENT
44+
count = required_fragments
45+
return self._has(state, progress, count)
46+
47+
self.has = fragmented_has
48+
else:
49+
# default (energy core) logic
50+
self.has = self._has
51+
52+
def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int:
53+
"""
54+
Returns reached count of one of evermizer's progress steps based on collected items.
55+
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
56+
"""
57+
n = 0
58+
for item in items:
59+
for pvd in item.provides:
60+
if pvd[1] == progress:
61+
if state.has(item.name, self.player):
62+
n += state.count(item.name, self.player) * pvd[0]
63+
if n >= max_count > 0:
64+
return n
65+
for rule in rules:
66+
for pvd in rule.provides:
67+
if pvd[1] == progress and pvd[0] > 0:
68+
has = True
69+
for req in rule.requires:
70+
if not self.has(state, req[1], req[0]):
71+
has = False
72+
break
73+
if has:
74+
n += pvd[0]
75+
if n >= max_count > 0:
76+
return n
77+
return n
78+
79+
def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool:
80+
"""Default implementation of has"""
81+
if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB:
82+
return True
83+
if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
84+
return True
85+
return self._count(state, progress, count) >= count

worlds/soe/Options.py renamed to worlds/soe/options.py

+58-39
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
import typing
1+
from dataclasses import dataclass, fields
2+
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol
23

3-
from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
4+
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, PerGameCommonOptions, ProgressionBalancing, \
5+
Range, Toggle
46

57

68
# typing boilerplate
7-
class FlagsProtocol(typing.Protocol):
9+
class FlagsProtocol(Protocol):
810
value: int
911
default: int
10-
flags: typing.List[str]
12+
flags: List[str]
1113

1214

13-
class FlagProtocol(typing.Protocol):
15+
class FlagProtocol(Protocol):
1416
value: int
1517
default: int
1618
flag: str
1719

1820

1921
# meta options
2022
class EvermizerFlags:
21-
flags: typing.List[str]
23+
flags: List[str]
2224

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

201203
# more meta options
202204
class ItemChanceMeta(AssembleOptions):
203-
def __new__(mcs, name, bases, attrs):
205+
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
204206
if 'item_name' in attrs:
205207
attrs["display_name"] = f"{attrs['item_name']} Chance"
206208
attrs["range_start"] = 0
207209
attrs["range_end"] = 100
208-
209-
return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
210+
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
211+
return cast(ItemChanceMeta, cls)
210212

211213

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

249251

250-
soe_options: typing.Dict[str, AssembleOptions] = {
251-
"difficulty": Difficulty,
252-
"energy_core": EnergyCore,
253-
"required_fragments": RequiredFragments,
254-
"available_fragments": AvailableFragments,
255-
"money_modifier": MoneyModifier,
256-
"exp_modifier": ExpModifier,
257-
"sequence_breaks": SequenceBreaks,
258-
"out_of_bounds": OutOfBounds,
259-
"fix_cheats": FixCheats,
260-
"fix_infinite_ammo": FixInfiniteAmmo,
261-
"fix_atlas_glitch": FixAtlasGlitch,
262-
"fix_wings_glitch": FixWingsGlitch,
263-
"shorter_dialogs": ShorterDialogs,
264-
"short_boss_rush": ShortBossRush,
265-
"ingredienizer": Ingredienizer,
266-
"sniffamizer": Sniffamizer,
267-
"callbeadamizer": Callbeadamizer,
268-
"musicmizer": Musicmizer,
269-
"doggomizer": Doggomizer,
270-
"turdo_mode": TurdoMode,
271-
"death_link": DeathLink,
272-
"trap_count": TrapCount,
273-
"trap_chance_quake": TrapChanceQuake,
274-
"trap_chance_poison": TrapChancePoison,
275-
"trap_chance_confound": TrapChanceConfound,
276-
"trap_chance_hud": TrapChanceHUD,
277-
"trap_chance_ohko": TrapChanceOHKO,
278-
"progression_balancing": SoEProgressionBalancing,
279-
}
252+
# noinspection SpellCheckingInspection
253+
@dataclass
254+
class SoEOptions(PerGameCommonOptions):
255+
difficulty: Difficulty
256+
energy_core: EnergyCore
257+
required_fragments: RequiredFragments
258+
available_fragments: AvailableFragments
259+
money_modifier: MoneyModifier
260+
exp_modifier: ExpModifier
261+
sequence_breaks: SequenceBreaks
262+
out_of_bounds: OutOfBounds
263+
fix_cheats: FixCheats
264+
fix_infinite_ammo: FixInfiniteAmmo
265+
fix_atlas_glitch: FixAtlasGlitch
266+
fix_wings_glitch: FixWingsGlitch
267+
shorter_dialogs: ShorterDialogs
268+
short_boss_rush: ShortBossRush
269+
ingredienizer: Ingredienizer
270+
sniffamizer: Sniffamizer
271+
callbeadamizer: Callbeadamizer
272+
musicmizer: Musicmizer
273+
doggomizer: Doggomizer
274+
turdo_mode: TurdoMode
275+
death_link: DeathLink
276+
trap_count: TrapCount
277+
trap_chance_quake: TrapChanceQuake
278+
trap_chance_poison: TrapChancePoison
279+
trap_chance_confound: TrapChanceConfound
280+
trap_chance_hud: TrapChanceHUD
281+
trap_chance_ohko: TrapChanceOHKO
282+
progression_balancing: SoEProgressionBalancing
283+
284+
@property
285+
def trap_chances(self) -> Iterator[TrapChance]:
286+
for field in fields(self):
287+
option = getattr(self, field.name)
288+
if isinstance(option, TrapChance):
289+
yield option
290+
291+
@property
292+
def flags(self) -> str:
293+
flags = ''
294+
for field in fields(self):
295+
option = getattr(self, field.name)
296+
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
297+
flags += getattr(self, field.name).to_flag()
298+
return flags

worlds/soe/Patch.py renamed to worlds/soe/patch.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
from typing import Optional
2+
from typing import BinaryIO, Optional
33

44
import Utils
55
from worlds.Files import APDeltaPatch
@@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str:
3030
return file_name
3131

3232

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

4141
if __name__ == '__main__':
4242
import sys
43-
print('Please use ../../Patch.py', file=sys.stderr)
43+
print('Please use ../../patch.py', file=sys.stderr)
4444
sys.exit(1)

worlds/soe/test/__init__.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from test.TestBase import WorldTestBase
1+
from test.bases import WorldTestBase
22
from typing import Iterable
33

44

@@ -18,3 +18,14 @@ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable:
1818
for location in unreachable:
1919
self.assertFalse(self.can_reach_location(location),
2020
f"{location} is reachable but shouldn't be")
21+
22+
def testRocketPartsExist(self):
23+
"""Tests that rocket parts exist and are unique"""
24+
self.assertEqual(len(self.get_items_by_name("Gauge")), 1)
25+
self.assertEqual(len(self.get_items_by_name("Wheel")), 1)
26+
diamond_eyes = self.get_items_by_name("Diamond Eye")
27+
self.assertEqual(len(diamond_eyes), 3)
28+
# verify diamond eyes are individual items
29+
self.assertFalse(diamond_eyes[0] is diamond_eyes[1])
30+
self.assertFalse(diamond_eyes[0] is diamond_eyes[2])
31+
self.assertFalse(diamond_eyes[1] is diamond_eyes[2])

worlds/soe/test/test_access.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class AccessTest(SoETestBase):
77
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
88
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
99

10-
def testBronzeAxe(self):
10+
def test_bronze_axe(self):
1111
gourds = {
1212
"Pyramid bottom": (118, 121, 122, 123, 124, 125),
1313
"Pyramid top": (140,)
@@ -16,7 +16,7 @@ def testBronzeAxe(self):
1616
items = [["Bronze Axe"]]
1717
self.assertAccessDependency(locations, items)
1818

19-
def testBronzeSpearPlus(self):
19+
def test_bronze_spear_plus(self):
2020
locations = ["Megataur"]
2121
items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
2222
self.assertAccessDependency(locations, items)

0 commit comments

Comments
 (0)