Files
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

289 lines
12 KiB
Python

import json
import pkgutil
import typing
from worlds.generic.Rules import add_rule, forbid_items_for_player
from . import StateLogic, location_table, EnemyRandomizer
from .Options import Goal, PitItems
from .Data import stars, pit_exclusive_tattle_stars_required, location_to_unit
from .Locations import get_location_ids, get_locations_by_tags, location_id_to_name
from .Options import PalaceSkip
if typing.TYPE_CHECKING:
from . import TTYDWorld
def set_rules(world: "TTYDWorld"):
for location, rule in create_lambda_from_json(pkgutil.get_data(__name__, "json/rules.json").decode(), world).items():
if location not in world.disabled_locations:
add_rule(world.multiworld.get_location(location, world.player), rule)
for location in ["Palace of Shadow Final Staircase: Ultra Shroom", "Palace of Shadow Final Staircase: Jammin' Jelly"]:
if location not in world.disabled_locations:
add_rule(world.multiworld.get_location(location, world.player), lambda state: state.has("stars", world.player, world.options.goal_stars))
for location in get_locations_by_tags("shop"):
if location.name in world.disabled_locations:
continue
forbid_items_for_player(world.get_location(location.name), set([item for item in stars.values()]), world.player)
for location in get_locations_by_tags("dazzle"):
if location.name in world.disabled_locations:
continue
forbid_items_for_player(world.get_location(location.name), {"Star Piece"}, world.player)
def set_tattle_rules(world: "TTYDWorld"):
for location in get_locations_by_tags("tattle"):
if location.name in world.disabled_locations:
continue
add_rule(world.get_location(location.name), lambda state: state.has("Goombella", world.player))
rules_dict = get_random_enemy_tattle_rules_dict(world) \
if world.options.enemy_randomizer != EnemyRandomizer.option_vanilla \
else get_tattle_rules_dict()
for location_name, locations in rules_dict.items():
if location_name in world.disabled_locations:
continue
if len(locations) == 0:
# Require access to Shadow Queen
if world.options.palace_skip == PalaceSkip.option_true and world.options.goal != Goal.option_shadow_queen:
extra_condition = lambda state: state.has("stars", world.player, world.options.palace_stars)
elif world.options.goal == Goal.option_shadow_queen:
extra_condition = lambda state: state.can_reach("Shadow Queen", "Location", world.player)
else:
extra_condition = lambda state: state.can_reach("Palace of Shadow Final Staircase: Ultra Shroom", "Location", world.player)
else:
# Require access to any of the listed locations
if world.options.pit_items != PitItems.option_all and location_name not in pit_exclusive_tattle_stars_required:
locations = [loc for loc in locations if loc not in get_location_ids(get_locations_by_tags("pit_floor"))]
if len(locations) == 0:
continue
valid_locations = [
location_id_to_name[loc] for loc in locations
if location_id_to_name[loc] not in world.disabled_locations
]
if len(valid_locations) == 0:
continue
extra_condition = lambda state, locs=valid_locations: any(
state.can_reach(loc, "Location", world.player) for loc in locs
)
add_rule(world.get_location(location_name), extra_condition)
def create_lambda_from_json(json_string: str, world: "TTYDWorld") -> typing.Dict[str, typing.Callable]:
lambda_functions = {}
for location, requirements in json.loads(json_string).items():
lambda_functions[location] = _build_single_lambda(requirements, world)
return lambda_functions
def _build_single_lambda(req: typing.Dict, world: "TTYDWorld") -> typing.Callable:
def build_expression(r):
if "or" in r:
conditions = [build_expression(condition) for condition in r["or"]]
return f"({' or '.join(conditions)})"
elif "and" in r:
conditions = [build_expression(condition) for condition in r["and"]]
return f"({' and '.join(conditions)})"
elif "has" in r:
has_value = r["has"]
if isinstance(has_value, str):
item = has_value
count = r.get("count", 1)
elif isinstance(has_value, dict):
item = has_value.get("item", "")
count = has_value.get("count", 1)
else:
item = str(has_value)
count = r.get("count", 1)
# Escape quotes in item names by using repr() which handles escaping properly
escaped_item = repr(item)
if count == 1:
return f'state.has({escaped_item}, world.player)'
else:
return f'state.has({escaped_item}, world.player, {count})'
elif "function" in r:
fn = r["function"]
if isinstance(fn, dict):
function_name = fn.get("name", "")
count = fn.get("count", None)
else:
function_name = fn
count = None
# Require count for chapter_completions (and validate it)
if function_name == "chapter_completions":
if count is None:
raise ValueError("chapter_completions requires 'count'")
count = int(count)
if count <= 0:
raise ValueError(f"chapter_completions count must be > 0, got {count}")
return f"StateLogic.{function_name}(state, world.player, {count})"
# For other functions, only pass count if provided
if count is not None:
return f"StateLogic.{function_name}(state, world.player, {int(count)})"
return f"StateLogic.{function_name}(state, world.player)"
elif "can_reach" in r:
location = r["can_reach"]
return f'state.can_reach({repr(location)}, "Location", world.player)'
else:
return "False"
expression = build_expression(req)
# Capture world and StateLogic in the lambda's closure
return eval(f"lambda state: {expression}", {"world": world, "StateLogic": StateLogic})
def get_tattle_rules_dict() -> dict[str, typing.List[int]]:
return {
"Tattle: Spania": [78780145, 78780267, 78780638],
"Tattle: Fuzzy": [78780170, 78780296, 78780638],
"Tattle: Koopa Troopa": [78780193, 78780170],
"Tattle: Blooper": [78780184],
"Tattle: Lord Crump": [78780511],
"Tattle: Cleft": [78780216, 78780639],
"Tattle: Bald Cleft": [78780165],
"Tattle: Bristle": [78780800, 78780296],
"Tattle: Gold Fuzzy": [78780170],
"Tattle: Paratroopa": [78780193],
"Tattle: Dull Bones": [78780193, 78780267, 78780615, 78780638],
"Tattle: Red Bones": [78780193, 78780615],
"Tattle: Hooktail": [78780209],
"Tattle: Pale Piranha": [78780216, 78780267],
"Tattle: Dark Puff": [78780216, 78780267, 78780639],
"Tattle: Vivian": [78780215],
"Tattle: Marilyn": [78780215, 78780622],
"Tattle: Beldam": [78780215, 78780622],
"Tattle: X-Naut": [78780231, 78780595],
"Tattle: Yux": [78780231],
"Tattle: Mini-Yux": [78780231],
"Tattle: Pider": [78780241, 78780267, 78780639],
"Tattle: Magnus von Grapple": [78780232],
"Tattle: KP Koopa": [78780267],
"Tattle: KP Paratroopa": [78780267],
"Tattle: Pokey": [78780267, 78780639],
"Tattle: Spiny": [78780267, 78780640],
"Tattle: Lakitu": [78780267, 78780640],
"Tattle: Bandit": [78780267, 78780640],
"Tattle: Big Bandit": [78780267],
"Tattle: Hyper Bald Cleft": [78780267],
"Tattle: Bob-omb": [78780267, 78780640],
"Tattle: Swooper": [78780287, 78780436],
"Tattle: Iron Cleft": [78780267],
"Tattle: Red Spike Top": [78780296],
"Tattle: Shady Koopa": [78780296, 78780641],
"Tattle: Shady Paratroopa": [78780296],
"Tattle: Green Fuzzy": [78780296, 78780470],
"Tattle: Flower Fuzzy": [78780296, 78780470],
"Tattle: Magikoopa": [78780511],
"Tattle: Red Magikoopa": [78780296],
"Tattle: White Magikoopa": [78780296],
"Tattle: Green Magikoopa": [78780296],
"Tattle: Hammer Bro": [78780296, 78780511],
"Tattle: Boomerang Bro": [78780296],
"Tattle: Fire Bro": [78780296],
"Tattle: Dark Craw": [78780296, 78780644],
"Tattle: Red Chomp": [78780296, 78780643],
"Tattle: Koopatrol": [78780511],
"Tattle: Dark Koopatrol": [78780296, 78780645],
"Tattle: Rawk Hawk": [78780295],
"Tattle: Macho Grubba": [78780287],
"Tattle: Hyper Goomba": [78780319],
"Tattle: Hyper Paragoomba": [78780319],
"Tattle: Crazee Dayzee": [78780327],
"Tattle: Hyper Spiky Goomba": [78780319],
"Tattle: Amazy Dayzee": [78780327],
"Tattle: Hyper Cleft": [78780329, 78780641],
"Tattle: Buzzy Beetle": [78780450],
"Tattle: Spike Top": [78780450],
"Tattle: Atomic Boo": [78780434],
"Tattle: Boo": [78780434],
"Tattle: Doopliss": [78780437, 78780622],
"Tattle: Ember": [78780503],
"Tattle: Putrid Piranha": [78780470],
"Tattle: Lava Bubble": [78780495, 78780642],
"Tattle: Bullet Bill": [78780497],
"Tattle: Bill Blaster": [78780497],
"Tattle: Bulky Bob-omb": [78780497, 78780642],
"Tattle: Parabuzzy": [78780503],
"Tattle: Cortez": [78780511],
"Tattle: Smorg": [78780554],
"Tattle: Ruff Puff": [78780538],
"Tattle: Poison Pokey": [78780541, 78780642],
"Tattle: Spiky Parabuzzy": [78780543, 78780642],
"Tattle: Ice Puff": [78780562, 78780643],
"Tattle: Frost Piranha": [78780562, 78780644],
"Tattle: Moon Cleft": [78780579, 78780643],
"Tattle: Z-Yux": [78780579],
"Tattle: Mini-Z-Yux": [78780579],
"Tattle: Elite X-Naut": [78780584],
"Tattle: X-Yux": [78780595],
"Tattle: Mini-X-Yux": [78780595],
"Tattle: X-Naut PhD": [78780595],
"Tattle: Magnus von Grapple 2.0": [78780604],
"Tattle: Spunia": [78780646, 78780156],
"Tattle: Swoopula": [78780605, 78780645],
"Tattle: Dry Bones": [78780605, 78780644],
"Tattle: Bombshell Bill": [78780605, 78780609],
"Tattle: B. Bill Blaster": [78780605, 78780609],
"Tattle: Phantom Ember": [78780634, 78780645],
"Tattle: Dark Bones": [78780609],
"Tattle: Chain-Chomp": [78780634, 78780645],
"Tattle: Dark Wizzerd": [78780634, 78780644],
"Tattle: Gloomtail": [78780634],
"Tattle: Sir Grodus": [],
"Tattle: Grodus X": [],
"Tattle: Kammy Koopa": [],
"Tattle: Bowser": [],
"Tattle: Shadow Queen": [],
"Tattle: Gloomba": [78780638],
"Tattle: Paragloomba": [78780639],
"Tattle: Spiky Gloomba": [78780640],
"Tattle: Dark Koopa": [78780641],
"Tattle: Dark Paratroopa": [78780642],
"Tattle: Badge Bandit": [78780643],
"Tattle: Dark Boo": [78780643],
"Tattle: Dark Lakitu": [78780644],
"Tattle: Sky-Blue Spiny": [78780644],
"Tattle: Wizzerd": [78780645],
"Tattle: Piranha Plant": [78780646],
"Tattle: Dark Bristle": [78780646],
"Tattle: Arantula": [78780646],
"Tattle: Elite Wizzerd": [78780647],
"Tattle: Swampire": [78780647],
"Tattle: Poison Puff": [78780647],
"Tattle: Bob-ulk": [78780647],
"Tattle: Bonetail": [78780647]
}
def get_random_enemy_tattle_rules_dict(world: "TTYDWorld") -> dict[str, list[int]]:
base_rules = get_tattle_rules_dict()
encounter_enemy_sets = [
(enc.location_id, set(enc.enemy_ids))
for enc in world.encounters
]
result: dict[str, list[int]] = {}
for key in base_rules:
tattle_ids = set(location_to_unit[location_table[key]]) # <-- FIX
matching_locations = [
loc_id
for loc_id, enemy_set in encounter_enemy_sets
if enemy_set & tattle_ids
]
# fallback to base rule if random finds nothing
result[key] = matching_locations if matching_locations else list(base_rules[key])
return result