Skip to content

Commit

Permalink
Merge branch 'main' into option-default-typing
Browse files Browse the repository at this point in the history
  • Loading branch information
beauxq authored Jan 15, 2024
2 parents ae7604d + 518b04c commit 07264bf
Show file tree
Hide file tree
Showing 21 changed files with 229 additions and 91 deletions.
10 changes: 5 additions & 5 deletions WebHostLib/templates/generate.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ <h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
</td>
<td>
<select name="collect_mode" id="collect_mode">
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !collect after goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !collect
</option>
Expand All @@ -93,9 +93,9 @@ <h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
{% if race -%}
<option value="disabled">Disabled in Race mode</option>
{%- else -%}
<option value="disabled">Disabled</option>
<option value="goal">Allow !remaining after goal completion</option>
<option value="enabled">Manual !remaining</option>
<option value="disabled">Disabled</option>
{%- endif -%}
</select>
</td>
Expand Down Expand Up @@ -185,12 +185,12 @@ <h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
</span>
</td>
<td>
<input type="checkbox" id="plando_items" name="plando_items" value="items">
<label for="plando_items">Items</label><br>

<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
<label for="plando_bosses">Bosses</label><br>

<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
<label for="plando_items">Items</label><br>

<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
<label for="plando_connections">Connections</label><br>

Expand Down
6 changes: 3 additions & 3 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,8 @@ class LogNetwork(IntEnum):
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
hint_cost: HintCost = HintCost(10)
release_mode: ReleaseMode = ReleaseMode("goal")
collect_mode: CollectMode = CollectMode("goal")
release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
Expand Down Expand Up @@ -673,7 +673,7 @@ class Race(IntEnum):
spoiler: Spoiler = Spoiler(3)
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses")
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")


class SNIOptions(Group):
Expand Down
127 changes: 127 additions & 0 deletions test/benchmark/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import time


class TimeIt:
def __init__(self, name: str, time_logger=None):
self.name = name
self.logger = time_logger
self.timer = None
self.end_timer = None

def __enter__(self):
self.timer = time.perf_counter()
return self

@property
def dif(self):
return self.end_timer - self.timer

def __exit__(self, exc_type, exc_val, exc_tb):
if not self.end_timer:
self.end_timer = time.perf_counter()
if self.logger:
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")


if __name__ == "__main__":
import argparse
import logging
import gc
import collections
import typing

# makes this module runnable from its folder.
import sys
import os
sys.path.remove(os.path.dirname(__file__))
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
os.chdir(new_home)
sys.path.append(new_home)

from Utils import init_logging, local_path
local_path.cached_path = new_home
from BaseClasses import MultiWorld, CollectionState, Location
from worlds import AutoWorld
from worlds.AutoWorld import call_all

init_logging("Benchmark Runner")
logger = logging.getLogger("Benchmark")


class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
rule_iterations: int = 100_000

if sys.version_info >= (3, 9):
@staticmethod
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
else:
@staticmethod
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))

def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations):
test_location.access_rule(state)
# if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule.
gc.collect()
return t.dif

def main(self):
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
summary_data: typing.Dict[str, collections.Counter[str]] = {
"empty_state": collections.Counter(),
"all_state": collections.Counter(),
}
try:
multiworld = MultiWorld(1)
multiworld.game[1] = game
multiworld.player_name = {1: "Tester"}
multiworld.set_seed(0)
multiworld.state = CollectionState(multiworld)
args = argparse.Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(getattr(option, "default"))
})
multiworld.set_options(args)

gc.collect()
for step in self.gen_steps:
with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step)
gc.collect()

locations = sorted(multiworld.get_unfilled_locations())
if not locations:
continue

all_state = multiworld.get_all_state(False)
for location in locations:
time_taken = self.location_test(location, multiworld.state, "empty_state")
summary_data["empty_state"][location.name] = time_taken

time_taken = self.location_test(location, all_state, "all_state")
summary_data["all_state"][location.name] = time_taken

total_empty_state = sum(summary_data["empty_state"].values())
total_all_state = sum(summary_data["all_state"].values())

logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
logger.info(f"Top times in empty_state:\n"
f"{self.format_times_from_counter(summary_data['empty_state'])}")
logger.info(f"Top times in all_state:\n"
f"{self.format_times_from_counter(summary_data['all_state'])}")

except Exception as e:
logger.exception(e)

runner = BenchmarkRunner()
runner.main()
2 changes: 1 addition & 1 deletion worlds/shivers/Rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def get_rules_lookup(player: int):
"Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
"Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
"Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
"Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player)
"Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
},
"elevators": {
"Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
Expand Down
9 changes: 5 additions & 4 deletions worlds/soe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_item_rule, set_rule
from .logic import SoEPlayerLogic
from .options import AvailableFragments, Difficulty, EnergyCore, RequiredFragments, SoEOptions, TrapChance
from .options import Difficulty, EnergyCore, SoEOptions
from .patch import SoEDeltaPatch, get_base_rom_path

if typing.TYPE_CHECKING:
from BaseClasses import MultiWorld, CollectionState

__all__ = ["pyevermizer", "SoEWorld"]


"""
In evermizer:
Expand Down Expand Up @@ -158,7 +161,7 @@ class RomFile(settings.SNESRomPath):
class SoEWorld(World):
"""
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
space station where the final boss must be defeated.
space station where the final boss must be defeated.
"""
game: typing.ClassVar[str] = "Secret of Evermore"
options_dataclass = SoEOptions
Expand Down Expand Up @@ -370,8 +373,6 @@ def generate_basic(self) -> None:
self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?

def generate_output(self, output_directory: str) -> None:
from dataclasses import asdict

player_name = self.multiworld.get_player_name(self.player)
self.connect_name = player_name[:32]
while len(self.connect_name.encode('utf-8')) > 32:
Expand Down
8 changes: 5 additions & 3 deletions worlds/soe/options.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Literal, Tuple, Protocol, Union

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


# typing boilerplate
Expand Down Expand Up @@ -293,5 +293,7 @@ def flags(self) -> str:
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
flags += getattr(self, field.name).to_flag()
assert isinstance(option, Option)
# noinspection PyUnresolvedReferences
flags += option.to_flag()
return flags
2 changes: 1 addition & 1 deletion worlds/soe/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str:
return file_name


def read_rom(stream: BinaryIO, strip_header: bool=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 Down
4 changes: 2 additions & 2 deletions worlds/soe/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase):
game = "Secret of Evermore"

def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
satisfied=True) -> None:
satisfied: bool = True) -> None:
"""
Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
Expand All @@ -19,7 +19,7 @@ def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable:
self.assertFalse(self.can_reach_location(location),
f"{location} is reachable but shouldn't be")

def testRocketPartsExist(self):
def testRocketPartsExist(self) -> None:
"""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)
Expand Down
6 changes: 3 additions & 3 deletions worlds/soe/test/test_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

class AccessTest(SoETestBase):
@staticmethod
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]

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

def test_bronze_spear_plus(self):
def test_bronze_spear_plus(self) -> None:
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 test_fragments(self):
def test_fragments(self) -> None:
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 test_fragments(self):
self.assertEqual(self.count("Energy Core Fragment"), 21)
self.assertBeatable(True)

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

def test_no_rocket(self):
def test_no_rocket(self) -> None:
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 test_core(self):
def test_core(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)

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

def test_no_rocket(self):
def test_no_rocket(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)
4 changes: 2 additions & 2 deletions worlds/soe/test/test_oob.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class OoBTest(SoETestBase):
"""Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}

def test_oob_access(self):
def test_oob_access(self) -> None:
in_logic = self.options["out_of_bounds"] == "logic"

# some locations that just need a weapon + OoB
Expand Down Expand Up @@ -37,7 +37,7 @@ def test_oob_access(self):
self.collect_by_name("Diamond Eye")
self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)

def test_oob_goal(self):
def test_oob_goal(self) -> None:
# still need Energy Core with OoB if sequence breaks are not in logic
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
self.collect_by_name(item)
Expand Down
4 changes: 2 additions & 2 deletions worlds/soe/test/test_sequence_breaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase):
"""Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}

def test_sequence_breaks_access(self):
def test_sequence_breaks_access(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"

# some locations that just need any weapon + sequence break
Expand All @@ -30,7 +30,7 @@ def test_sequence_breaks_access(self):
self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
self.assertEqual(self.can_reach_location("Escape"), in_logic)

def test_sequence_breaks_goal(self):
def test_sequence_breaks_goal(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"

# don't need Energy Core with sequence breaks in logic
Expand Down
3 changes: 2 additions & 1 deletion worlds/soe/test/test_traps.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ def test_dataclass(self) -> None:

def test_trap_count(self) -> None:
"""Test that total trap count is correct"""
self.assertEqual(self.options["trap_count"], len(self.get_items_by_name(self.option_name_to_item_name.values())))
self.assertEqual(self.options["trap_count"],
len(self.get_items_by_name(self.option_name_to_item_name.values())))


class TestTrapAllZeroChance(Bases.TrapTestBase):
Expand Down
2 changes: 2 additions & 0 deletions worlds/stardew_valley/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp
logic.received("Bus Repair").simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player),
logic.received(Wallet.skull_key).simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player),
logic.received("Club Card").simplify())
for floor in range(25, 200 + 25, 25):
MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player),
logic.can_mine_to_skull_cavern_floor(floor).simplify())
Expand Down
Loading

0 comments on commit 07264bf

Please sign in to comment.