mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-18 16:53:32 -07:00
* CachedRuleBuilderWorld * CachedRuleBuilderWorld * APQuest Rule Builder finished * Added comment Add a rule to check if the player has a Sword to destroy bushes. * Bump version + typo fix * Update worlds/apquest/rules.py Co-authored-by: Ian Robinson <drtchops@users.noreply.github.com> * Address Tchops' review comments --------- Co-authored-by: Ian Robinson <drtchops@users.noreply.github.com>
153 lines
8.9 KiB
Python
153 lines
8.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from rule_builder.options import OptionFilter
|
|
from rule_builder.rules import Has, HasAll, Rule
|
|
|
|
from .options import HardMode
|
|
|
|
if TYPE_CHECKING:
|
|
from .world import APQuestWorld
|
|
|
|
HAS_KEY = Has("Key") # Hmm, what could this be? A little foreshadowing perhaps? :) You'll find out if you keep reading!
|
|
|
|
|
|
def set_all_rules(world: APQuestWorld) -> 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)
|
|
set_all_location_rules(world)
|
|
set_completion_condition(world)
|
|
|
|
|
|
def set_all_entrance_rules(world: APQuestWorld) -> None:
|
|
# First, we need to actually grab our entrances. Luckily, there is a helper method for this.
|
|
overworld_to_bottom_right_room = world.get_entrance("Overworld to Bottom Right Room")
|
|
overworld_to_top_left_room = world.get_entrance("Overworld to Top Left Room")
|
|
right_room_to_final_boss_room = world.get_entrance("Right Room to Final Boss Room")
|
|
|
|
# Now, let's make some rules!
|
|
# First, let's handle the transition from the overworld to the bottom right room,
|
|
# which requires slashing a bush with the Sword.
|
|
# For this, we need a rule that says "player has a Sword".
|
|
# We can use a "Has"-type rule from the rule_builder module for this.
|
|
can_destroy_bush = Has("Sword")
|
|
|
|
# Now we can set our "can_destroy_bush" rule to the entrance which requires slashing a bush to clear the path.
|
|
# The easiest way to do this is by calling world.set_rule, which works for both Locations and Entrances.
|
|
world.set_rule(overworld_to_bottom_right_room, can_destroy_bush)
|
|
|
|
# Conditions can also depend on event items.
|
|
button_pressed = Has("Top Left Room Button Pressed")
|
|
world.set_rule(right_room_to_final_boss_room, button_pressed)
|
|
|
|
# Some entrance rules may only apply if the player enabled certain options.
|
|
# In our case, if the hammer option is enabled, we need to add the Hammer requirement to the Entrance from
|
|
# Overworld to the Top Middle Room.
|
|
if world.options.hammer:
|
|
overworld_to_top_middle_room = world.get_entrance("Overworld to Top Middle Room")
|
|
can_smash_brick = Has("Hammer")
|
|
world.set_rule(overworld_to_top_middle_room, can_smash_brick)
|
|
|
|
# So far, we've been using "Has" from the Rule Builder to make our rules.
|
|
# There is another way to make rules that you will see in a lot of older worlds.
|
|
# A rule can just be a function that takes a "state" argument and returns a bool.
|
|
# As a demonstration of what that looks like, let's do it with our final Entrance rule:
|
|
world.set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
|
|
# This style is not really recommended anymore, though.
|
|
# Notice how you have to explicitly capture world.player here so that the rule applies to the correct player?
|
|
# Well, Rule Builder does this part for you, inside of world.set_rule.
|
|
# This doesn't just result in shorter code, it also means you can define rules statically (at the module level).
|
|
# APQuest opts to create its Rule objects locally, but just to show what this would look like,
|
|
# we'll re-set the "Overworld to Top Left Room" rule to a constant defined at the top of this file:
|
|
world.set_rule(overworld_to_top_left_room, HAS_KEY)
|
|
|
|
# Beyond these structural advantages,
|
|
# Rule Builder also allows the core AP code to do a lot of under-the-hood optimizations.
|
|
# Rule Builder is quite comprehensive, and even if you have really esoteric rules,
|
|
# you can make custom rules by subclassing CustomRule.
|
|
|
|
def set_all_location_rules(world: APQuestWorld) -> None:
|
|
# Location rules work no differently from Entrance rules.
|
|
# Most of our locations are chests that can simply be opened by walking up to them.
|
|
# Thus, their logical requirements are covered by the Entrance rules of the Entrances that were required to
|
|
# reach the region that the chest sits in.
|
|
# However, our two enemies work differently.
|
|
# Entering the room with the enemy is not enough, you also need to have enough combat items to be able to defeat it.
|
|
# So, we need to set requirements on the Locations themselves.
|
|
# Since combat is a bit more complicated, we'll use this chance to cover some advanced access rule concepts.
|
|
|
|
# In "set_all_entrance_rules", we had a rule for a location that doesn't always exist.
|
|
# In this case, we had to check for its existence (by checking the player's chosen options) before setting the rule.
|
|
# Other times, you may have a situation where a location can have two different rules depending on the options.
|
|
# In our case, the enemy in the right room has more health if hard mode is selected,
|
|
# so ontop of the Sword, the player will either need one more health or a Shield in hard mode.
|
|
# First, let's make our sword condition.
|
|
can_defeat_basic_enemy: Rule = Has("Sword")
|
|
|
|
# Next, we'll check whether hard mode has been chosen in the player options.
|
|
if world.options.hard_mode:
|
|
# We'll make the condition for "Has a Shield or a Health Upgrade".
|
|
# We can chain two "Has" conditions together with the | operator to make "Has Shield or has Health Upgrade".
|
|
can_withstand_a_hit = Has("Shield") | Has("Health Upgrade")
|
|
|
|
# Now, we chain this rule to our Sword rule.
|
|
# Since we want both conditions to be true, in this case, we have to chain them in an "and" way.
|
|
# For this, we can use the & operator.
|
|
can_defeat_basic_enemy = can_defeat_basic_enemy & can_withstand_a_hit
|
|
|
|
# Finally, we set our rule onto the Right Room Eney Drop location.
|
|
right_room_enemy = world.get_location("Right Room Enemy Drop")
|
|
world.set_rule(right_room_enemy, can_defeat_basic_enemy)
|
|
|
|
# For the final boss, we also need to chain multiple conditions.
|
|
# First of all, you always need a Sword and a Shield.
|
|
# So far, we used the | and & operators to chain "Has" rules.
|
|
# Instead, we can also use HasAny for an or-chain of items, or HasAll for an and-chain of items.
|
|
has_sword_and_shield: Rule = HasAll("Sword", "Shield")
|
|
|
|
# In hard mode, the player also needs both Health Upgrades to survive long enough to defeat the boss.
|
|
# For this, we can use the optional "count" parameter for "Has".
|
|
has_both_health_upgrades = Has("Health Upgrade", count=2)
|
|
|
|
# Previously, we used an "if world.options.hard_mode" condition to check if we should apply the extra requirement.
|
|
# However, if you're comfortable with boolean logic, there is another way.
|
|
# OptionFilter is a rule component which isn't a "Rule" on its own, but when used in a boolean expression with
|
|
# rules, it acts like True if the option has the specified value, and acts like False otherwise.
|
|
hard_mode_is_off = OptionFilter(HardMode, False)
|
|
|
|
# So with this option-checking rule component in hand, we can write our boss condition like this:
|
|
can_defeat_final_boss = has_sword_and_shield & (hard_mode_is_off | has_both_health_upgrades)
|
|
# If you're not as comfortable with boolean logic, it might be somewhat confusing why this is correct.
|
|
# There is nothing wrong with using "if" conditions to check for options, if you find that easier to understand.
|
|
|
|
# Finally, we apply the rule to our "Final Boss Defeated" event location.
|
|
final_boss = world.get_location("Final Boss Defeated")
|
|
world.set_rule(final_boss, can_defeat_final_boss)
|
|
|
|
|
|
def set_completion_condition(world: APQuestWorld) -> None:
|
|
# Finally, we need to set a completion condition for our world, defining what the player needs to win the game.
|
|
# For this, we can use world.set_completion_rule.
|
|
# You can just set a completion condition directly like any other condition, referencing items the player receives:
|
|
world.set_completion_rule(HasAll("Sword", "Shield"))
|
|
|
|
# In our case, we went for the Victory event design pattern (see create_events() in locations.py).
|
|
# So lets undo what we just did, and instead set the completion condition to:
|
|
world.set_completion_rule(Has("Victory"))
|
|
|
|
|
|
# One final comment about rules:
|
|
# If your world exclusively uses Rule Builder rules (like APQuest), it's worth trying CachedRuleBuilderWorld.
|
|
# CachedRuleBuilderWorld is a subclass of World that has a bunch of caching magic to make rules faster.
|
|
# Just have your world class subclass CachedRuleBuilderWorld instead of World:
|
|
# class APQuestWorld(CachedRuleBuilderWorld): ...
|
|
# This may speed up your world, or it may make it slower.
|
|
# The exact factors are complex and not well understood, but there is no harm in trying it.
|
|
# Generate a few seeds and see if there is a noticeable difference!
|
|
# If you're wondering, author has checked: APQuest is too simple to see any benefits, so we'll stick with "World".
|