Files
dockipelago/worlds/Schedule_I/rules.py
Jonathan Tinney 7971961166
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
add schedule I, sonic 1/frontiers/heroes, spirit island
2026-04-02 23:46:36 -07:00

251 lines
10 KiB
Python

from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Dict, Any, Union
from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, set_rule
if TYPE_CHECKING:
from .world import Schedule1World
def set_all_rules(world: Schedule1World, locationData, regionData, victoryData) -> None:
# In order for AP to generate an item layout that is actually possible for the player to complete,
# we need to define rules for our Entrances and Locations.
# Note: Regions do not have rules, the Entrances connecting them do!
# We'll do entrances first, then locations, and then finally we set our victory condition.
set_all_entrance_rules(world, regionData)
set_all_location_rules(world, locationData)
set_completion_condition(world, victoryData)
def check_option_enabled(world: Schedule1World, option_name: str) -> bool:
"""Check if an option is enabled based on option name string from JSON."""
option_map = {
"randomize_customers": world.options.randomize_customers,
"randomize_dealers": world.options.randomize_dealers,
"randomize_suppliers": world.options.randomize_suppliers,
"randomize_level_unlocks": world.options.randomize_level_unlocks,
"randomize_cartel_influence": world.options.randomize_cartel_influence,
"randomize_business_properties": world.options.randomize_business_properties,
"randomize_drug_making_properties": world.options.randomize_drug_making_properties,
}
return bool(option_map.get(option_name, False))
def check_option_condition(world: Schedule1World, condition_key: str) -> bool:
"""
Parse and evaluate a compound option condition string.
Supports:
- Simple: "randomize_level_unlocks" (option must be true)
- Negation: "!randomize_level_unlocks" (option must be false)
- Compound AND: "randomize_level_unlocks&!randomize_customers"
(first must be true AND second must be false)
Returns True if the condition is satisfied, False otherwise.
"""
# Split by '&' to get individual conditions
parts = condition_key.split('&')
for part in parts:
part = part.strip()
if not part:
continue
# Check for negation prefix
if part.startswith('!'):
option_name = part[1:]
expected_value = False
else:
option_name = part
expected_value = True
# Get the actual option value
actual_value = check_option_enabled(world, option_name)
# If this part doesn't match expected, the whole condition fails
if actual_value != expected_value:
return False
return True
def build_requirement_check(world: Schedule1World, method_name: str, value: Any) -> Callable[[CollectionState], bool]:
"""Build a requirement check function based on the method name and value from JSON."""
if method_name == "has":
# value is a single item name string
return lambda state, v=value: state.has(v, world.player)
elif method_name == "has_any":
# value is a list of lists, e.g. [["Item1", "Item2"]]
# We take the first list as the items to check
items = value[0] if isinstance(value[0], list) else value
return lambda state, v=items: state.has_any(v, world.player)
elif method_name == "has_all":
# value is a list of item names
return lambda state, v=value: state.has_all(v, world.player)
elif method_name == "has_all_counts":
# value is a dict of {item_name: count}
return lambda state, v=value: state.has_all_counts(v, world.player)
elif method_name == "has_from_list":
# value can be:
# - A single dict: {item_name: count, ...} where all counts are the same
# - A list of dicts: [{item_name: count, ...}, ...] for multiple tiers
# For a list, we build checks for each dict and require all to pass
if isinstance(value, list):
# List of dicts - build a check for each dict
checks = []
for tier_dict in value:
keys = list(tier_dict.keys())
count = list(tier_dict.values())[0] # All values in a tier should be the same
checks.append((keys, count))
return lambda state, c=checks: all(
state.has_from_list(keys, world.player, count) for keys, count in c
)
else:
# Single dict
keys = list(value.keys())
count = list(value.values())[0] # All values should be the same count
return lambda state, k=keys, c=count: state.has_from_list(k, world.player, c)
# Default: always true
return lambda state: True
def build_rule_from_requirements(world: Schedule1World, requirements: Union[bool, Dict[str, Any]], use_or_logic: bool = False) -> Callable[[CollectionState], bool]:
"""
Build a rule function from the requirements structure.
requirements can be:
- True (always accessible)
- A dict with option conditions as keys
use_or_logic: If True, only ONE condition needs to be satisfied (for Customer/Dealer/Supplier tags)
If False, ALL applicable conditions must be satisfied
"""
if requirements is True:
return lambda state: True
if not isinstance(requirements, dict):
return lambda state: True
# Build list of (option_name, checks) pairs
condition_checks: list[tuple[str, list[Callable[[CollectionState], bool]]]] = []
for option_name, checks in requirements.items():
if not isinstance(checks, dict):
continue
check_functions = []
for method_name, value in checks.items():
check_func = build_requirement_check(world, method_name, value)
check_functions.append(check_func)
if check_functions:
condition_checks.append((option_name, check_functions))
if not condition_checks:
return lambda state: True
def rule_function(state: CollectionState) -> bool:
results = []
for condition_key, check_functions in condition_checks:
if check_option_condition(world, condition_key):
# This option condition is satisfied, so its checks matter
# All checks within this condition must pass
option_result = all(check(state) for check in check_functions)
results.append(option_result)
if not results:
# No applicable options enabled - rule passes
return True
if use_or_logic:
# For Customer/Dealer/Supplier: only one needs to pass
return any(results)
else:
# For all others: all must pass
return all(results)
return rule_function
def set_all_entrance_rules(world: Schedule1World, regionData) -> None:
"""Set entrance rules based on region connection requirements from regions.json."""
# Load all entrances into a dictionary once
entrances_dict: Dict[str, Any] = {}
for region_name, region_info in regionData.regions.items():
for connected_region_name, requirements in region_info.connections.items():
entrance_name = f"{region_name} to {connected_region_name}"
try:
entrances_dict[entrance_name] = world.get_entrance(entrance_name)
except KeyError:
# Entrance might not exist if region wasn't created
continue
# Set rules for each entrance
for region_name, region_info in regionData.regions.items():
for connected_region_name, requirements in region_info.connections.items():
entrance_name = f"{region_name} to {connected_region_name}"
if entrance_name not in entrances_dict:
continue
entrance = entrances_dict[entrance_name]
rule = build_rule_from_requirements(world, requirements, use_or_logic=False)
set_rule(entrance, rule)
def set_all_location_rules(world: Schedule1World, locationData) -> None:
"""Set location rules based on requirements from locations.json."""
# Build a dict of location name -> location object for locations that exist
locations_dict: Dict[str, Any] = {}
for loc_name, loc_data in locationData.locations.items():
# Skip supplier locations if randomize_suppliers is enabled (they don't exist)
if world.options.randomize_suppliers and "Supplier" in loc_data.tags:
continue
try:
locations_dict[loc_name] = world.get_location(loc_name)
except KeyError:
# Location might not exist
continue
# Set rules for each location
for loc_name, loc_data in locationData.locations.items():
if loc_name not in locations_dict:
continue
location = locations_dict[loc_name]
requirements = loc_data.requirements
# Determine if this location uses OR logic (Customer, Dealer, or Supplier tags)
tags = loc_data.tags
use_or_logic = any(tag in tags for tag in ["Customer", "Dealer", "Supplier"])
rule = build_rule_from_requirements(world, requirements, use_or_logic=use_or_logic)
set_rule(location, rule)
def set_completion_condition(world: Schedule1World, victoryData) -> None:
# Victory conditions are loaded from victory.json
# > 0 means cartel is necessary, and we need to check all applicable conditions
if world.options.goal > 0:
# Build the victory rule from the requirements in victory.json
# All applicable option conditions must pass (AND logic)
rule = build_rule_from_requirements(world, victoryData.requirements, use_or_logic=False)
world.multiworld.completion_condition[world.player] = rule
else:
# Otherwise, money is farmable no matter what.
world.multiworld.completion_condition[world.player] = lambda state: True