Files
Grinch-AP/worlds/jakanddaxter/rules.py
massimilianodelliubaldini d19bf98dc4 Jak and Daxter: Post-merge Polish (#5031)
- Cleans up a few missed references in the setup guide.
- Refactors Options class to use metaclass and decorators to enforce friendly limits on multiple levels.    
  - Templates generated from the website, even ones with `random` should not fail generation because the website will only allow values inside the friendly limits. 
  - _Uploaded_ yamls to the website with `random`, should also now respect friendly limits without the need for `random-range` shenanigans.
  - _Uploaded_ yamls to the website, or yamls that are used to generate locally, that have hard-defined values outside the friendly limits, will be clamped/dragged/massaged into those limits (with logged warnings).
- Removed an early completion goal that was playing havoc with fill. Not enough people seem to use this goal, so its loss will not be mourned.
2025-05-30 16:31:00 +02:00

271 lines
13 KiB
Python

import logging
import math
import typing
from BaseClasses import CollectionState
from Options import OptionError
from .options import (EnableOrbsanity,
GlobalOrbsanityBundleSize,
PerLevelOrbsanityBundleSize,
FireCanyonCellCount,
MountainPassCellCount,
LavaTubeCellCount,
CitizenOrbTradeAmount,
OracleOrbTradeAmount)
from .locs import cell_locations as cells
from .locations import location_table
from .levels import level_table
if typing.TYPE_CHECKING:
from . import JakAndDaxterWorld
def set_orb_trade_rule(world: "JakAndDaxterWorld"):
options = world.options
player = world.player
if options.enable_orbsanity == EnableOrbsanity.option_off:
world.can_trade = lambda state, required_orbs, required_previous_trade: (
can_trade_vanilla(state, player, world, required_orbs, required_previous_trade))
else:
world.can_trade = lambda state, required_orbs, required_previous_trade: (
can_trade_orbsanity(state, player, world, required_orbs, required_previous_trade))
def recalculate_reachable_orbs(state: CollectionState, player: int, world: "JakAndDaxterWorld") -> None:
# Recalculate every level, every time the cache is stale, because you don't know
# when a specific bundle of orbs in one level may unlock access to another.
accessible_total_orbs = 0
for level in level_table:
accessible_level_orbs = count_reachable_orbs_level(state, world, level)
accessible_total_orbs += accessible_level_orbs
state.prog_items[player][f"{level} Reachable Orbs".lstrip()] = accessible_level_orbs
# Also recalculate the global count, still used even when Orbsanity is Off.
state.prog_items[player]["Reachable Orbs"] = accessible_total_orbs
state.prog_items[player]["Reachable Orbs Fresh"] = True
def count_reachable_orbs_global(state: CollectionState,
world: "JakAndDaxterWorld") -> int:
accessible_orbs = 0
for level_regions in world.level_to_orb_regions.values():
for region in level_regions:
if region.can_reach(state):
accessible_orbs += region.orb_count
return accessible_orbs
def count_reachable_orbs_level(state: CollectionState,
world: "JakAndDaxterWorld",
level_name: str = "") -> int:
accessible_orbs = 0
for region in world.level_to_orb_regions[level_name]:
if region.can_reach(state):
accessible_orbs += region.orb_count
return accessible_orbs
def can_reach_orbs_global(state: CollectionState,
player: int,
world: "JakAndDaxterWorld",
orb_amount: int) -> bool:
if not state.prog_items[player]["Reachable Orbs Fresh"]:
recalculate_reachable_orbs(state, player, world)
return state.has("Reachable Orbs", player, orb_amount)
def can_reach_orbs_level(state: CollectionState,
player: int,
world: "JakAndDaxterWorld",
level_name: str,
orb_amount: int) -> bool:
if not state.prog_items[player]["Reachable Orbs Fresh"]:
recalculate_reachable_orbs(state, player, world)
return state.has(f"{level_name} Reachable Orbs", player, orb_amount)
def can_trade_vanilla(state: CollectionState,
player: int,
world: "JakAndDaxterWorld",
required_orbs: int,
required_previous_trade: typing.Optional[int] = None) -> bool:
# With Orbsanity Off, Reachable Orbs are in fact Tradeable Orbs.
if not state.prog_items[player]["Reachable Orbs Fresh"]:
recalculate_reachable_orbs(state, player, world)
if required_previous_trade:
name_of_previous_trade = location_table[cells.to_ap_id(required_previous_trade)]
return (state.has("Reachable Orbs", player, required_orbs)
and state.can_reach_location(name_of_previous_trade, player=player))
return state.has("Reachable Orbs", player, required_orbs)
def can_trade_orbsanity(state: CollectionState,
player: int,
world: "JakAndDaxterWorld",
required_orbs: int,
required_previous_trade: typing.Optional[int] = None) -> bool:
# Yes, even Orbsanity trades may unlock access to new Reachable Orbs.
if not state.prog_items[player]["Reachable Orbs Fresh"]:
recalculate_reachable_orbs(state, player, world)
if required_previous_trade:
name_of_previous_trade = location_table[cells.to_ap_id(required_previous_trade)]
return (state.has("Tradeable Orbs", player, required_orbs)
and state.can_reach_location(name_of_previous_trade, player=player))
return state.has("Tradeable Orbs", player, required_orbs)
def can_free_scout_flies(state: CollectionState, player: int) -> bool:
return state.has("Jump Dive", player) or state.has_all({"Crouch", "Crouch Uppercut"}, player)
def can_fight(state: CollectionState, player: int) -> bool:
return state.has_any(("Jump Dive", "Jump Kick", "Punch", "Kick"), player)
def clamp_cell_limits(world: "JakAndDaxterWorld") -> str:
options = world.options
friendly_message = ""
if options.fire_canyon_cell_count.value > FireCanyonCellCount.friendly_maximum:
old_value = options.fire_canyon_cell_count.value
options.fire_canyon_cell_count.value = FireCanyonCellCount.friendly_maximum
friendly_message += (f" "
f"{options.fire_canyon_cell_count.display_name} must be no greater than "
f"{FireCanyonCellCount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.mountain_pass_cell_count.value > MountainPassCellCount.friendly_maximum:
old_value = options.mountain_pass_cell_count.value
options.mountain_pass_cell_count.value = MountainPassCellCount.friendly_maximum
friendly_message += (f" "
f"{options.mountain_pass_cell_count.display_name} must be no greater than "
f"{MountainPassCellCount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.lava_tube_cell_count.value > LavaTubeCellCount.friendly_maximum:
old_value = options.lava_tube_cell_count.value
options.lava_tube_cell_count.value = LavaTubeCellCount.friendly_maximum
friendly_message += (f" "
f"{options.lava_tube_cell_count.display_name} must be no greater than "
f"{LavaTubeCellCount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
return friendly_message
def clamp_trade_total_limits(world: "JakAndDaxterWorld"):
"""Check if we need to recalculate the 2 trade orb options so the total fits under 2000. If so let's keep them
proportional relative to each other. Then we'll recalculate total_trade_orbs. Remember this situation is
only possible if both values are greater than 0, otherwise the absolute maximums would keep them under 2000."""
options = world.options
friendly_message = ""
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
if world.total_trade_orbs > 2000:
old_total = world.total_trade_orbs
old_citizen_value = options.citizen_orb_trade_amount.value
old_oracle_value = options.oracle_orb_trade_amount.value
coefficient = old_oracle_value / old_citizen_value
options.citizen_orb_trade_amount.value = math.floor(2000 / (9 + (6 * coefficient)))
options.oracle_orb_trade_amount.value = math.floor(coefficient * options.citizen_orb_trade_amount.value)
world.total_trade_orbs = (9 * options.citizen_orb_trade_amount) + (6 * options.oracle_orb_trade_amount)
friendly_message += (f" "
f"Required number of orbs ({old_total}) must be no greater than total orbs in the game "
f"(2000). Reduced the value of {world.options.citizen_orb_trade_amount.display_name} "
f"from {old_citizen_value} to {options.citizen_orb_trade_amount.value} and "
f"{world.options.oracle_orb_trade_amount.display_name} from {old_oracle_value} to "
f"{options.oracle_orb_trade_amount.value}.\n")
return friendly_message
def enforce_mp_friendly_limits(world: "JakAndDaxterWorld"):
options = world.options
friendly_message = ""
if options.enable_orbsanity == EnableOrbsanity.option_global:
if options.global_orbsanity_bundle_size.value < GlobalOrbsanityBundleSize.friendly_minimum:
old_value = options.global_orbsanity_bundle_size.value
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_minimum
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no less than "
f"{GlobalOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.global_orbsanity_bundle_size.value > GlobalOrbsanityBundleSize.friendly_maximum:
old_value = options.global_orbsanity_bundle_size.value
options.global_orbsanity_bundle_size.value = GlobalOrbsanityBundleSize.friendly_maximum
friendly_message += (f" "
f"{options.global_orbsanity_bundle_size.display_name} must be no greater than "
f"{GlobalOrbsanityBundleSize.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.enable_orbsanity == EnableOrbsanity.option_per_level:
if options.level_orbsanity_bundle_size.value < PerLevelOrbsanityBundleSize.friendly_minimum:
old_value = options.level_orbsanity_bundle_size.value
options.level_orbsanity_bundle_size.value = PerLevelOrbsanityBundleSize.friendly_minimum
friendly_message += (f" "
f"{options.level_orbsanity_bundle_size.display_name} must be no less than "
f"{PerLevelOrbsanityBundleSize.friendly_minimum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.citizen_orb_trade_amount.value > CitizenOrbTradeAmount.friendly_maximum:
old_value = options.citizen_orb_trade_amount.value
options.citizen_orb_trade_amount.value = CitizenOrbTradeAmount.friendly_maximum
friendly_message += (f" "
f"{options.citizen_orb_trade_amount.display_name} must be no greater than "
f"{CitizenOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
if options.oracle_orb_trade_amount.value > OracleOrbTradeAmount.friendly_maximum:
old_value = options.oracle_orb_trade_amount.value
options.oracle_orb_trade_amount.value = OracleOrbTradeAmount.friendly_maximum
friendly_message += (f" "
f"{options.oracle_orb_trade_amount.display_name} must be no greater than "
f"{OracleOrbTradeAmount.friendly_maximum} (was {old_value}), "
f"changed option to appropriate value.\n")
friendly_message += clamp_cell_limits(world)
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
logging.warning(f"{world.player_name}: Your options have been modified to avoid disrupting the multiworld.\n"
f"{friendly_message}"
f"You can access more advanced options by setting 'enforce_friendly_options' in the seed "
f"generator's host.yaml to false and generating locally. (Use at your own risk!)")
def enforce_mp_absolute_limits(world: "JakAndDaxterWorld"):
friendly_message = ""
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
f"{friendly_message}")
def enforce_sp_limits(world: "JakAndDaxterWorld"):
friendly_message = ""
friendly_message += clamp_cell_limits(world)
friendly_message += clamp_trade_total_limits(world)
if friendly_message != "":
logging.warning(f"{world.player_name}: Your options have been modified to avoid seed generation failures.\n"
f"{friendly_message}")