Skip to content
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

SoE: use new AP API and naming and make APworld #2701

Merged
merged 13 commits into from
Jan 12, 2024
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.

144 changes: 69 additions & 75 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
81 changes: 42 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
from typing import Any, cast, Dict, 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,34 @@ 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
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)
12 changes: 6 additions & 6 deletions worlds/soe/test/test_goal.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase):
"required_fragments": 20,
}

def testFragments(self):
def test_fragments(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False) # 0 fragments
fragments = self.get_items_by_name("Energy Core Fragment")
Expand All @@ -24,11 +24,11 @@ def testFragments(self):
self.assertEqual(self.count("Energy Core Fragment"), 21)
self.assertBeatable(True)

def testNoWeapon(self):
def test_no_weapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
self.assertBeatable(False)

def testNoRocket(self):
def test_no_rocket(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
self.assertBeatable(False)

Expand All @@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase):
"energy_core": "shuffle",
}

def testCore(self):
def test_core(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)

def testNoWeapon(self):
def test_no_weapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
self.assertBeatable(False)

def testNoRocket(self):
def test_no_rocket(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)
Loading
Loading