forked from ArchipelagoMW/Archipelago
-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathrules.py
More file actions
131 lines (106 loc) · 7.6 KB
/
Copy pathrules.py
File metadata and controls
131 lines (106 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import add_rule, set_rule
if TYPE_CHECKING:
from .world import APQuestWorld
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")
# An access rule is a function. We can define this function like any other function.
# This function must accept exactly one parameter: A "CollectionState".
# A CollectionState describes the current progress of the players in the multiworld, i.e. what items they have,
# which regions they've reached, etc.
# In an access rule, we can ask whether the player has a collected a certain item.
# We can do this via the state.has(...) function.
# This function takes an item name, a player number, and an optional count parameter (more on that below)
# Since a rule only takes a CollectionState parameter, but we also need the player number in the state.has call,
# our function needs to be locally defined so that it has access to the player number from the outer scope.
# In our case, we are inside a function that has access to the "world" parameter, so we can use world.player.
def can_destroy_bush(state: CollectionState) -> bool:
return state.has("Sword", world.player)
# Now we can set our "can_destroy_bush" rule to our entrance which requires slashing a bush to clear the path.
# One way to set rules is via the set_rule() function, which works on both Entrances and Locations.
set_rule(overworld_to_bottom_right_room, can_destroy_bush)
# Because the function has to be defined locally, most worlds prefer the lambda syntax.
set_rule(overworld_to_top_left_room, lambda state: state.has("Key", world.player))
# Conditions can depend on event items.
set_rule(right_room_to_final_boss_room, lambda state: state.has("Top Left Room Button Pressed", world.player))
# 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")
set_rule(overworld_to_top_middle_room, lambda state: state.has("Hammer", world.player))
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.
# Sometimes, you may want to have different rules depending on the player's chosen options.
# There is a wrong way to do this, and a right way to do this. Let's do the wrong way first.
right_room_enemy = world.get_location("Right Room Enemy Drop")
# DON'T DO THIS!!!!
set_rule(
right_room_enemy,
lambda state: (
state.has("Sword", world.player)
and (not world.options.hard_mode or state.has_any(("Shield", "Health Upgrade"), world.player))
),
)
# DON'T DO THIS!!!!
# Now, what's actually wrong with this? It works perfectly fine, right?
# If hard mode disabled, Sword is enough. If hard mode is enabled, we also need a Shield or a Health Upgrade.
# The access rule we just wrote does this correctly, so what's the problem?
# The problem is performance.
# Most of your world code doesn't need to be perfectly performant, since it just runs once per slot.
# However, access rules in particular are by far the hottest code path in Archipelago.
# An access rule will potentially be called thousands or even millions of times over the course of one generation.
# As a result, access rules are the one place where it's really worth putting in some effort to optimize.
# What's the performance problem here?
# Every time our access rule is called, it has to evaluate whether world.options.hard_mode is True or False.
# Wouldn't it be better if in easy mode, the access rule only checked for Sword to begin with?
# Wouldn't it also be better if in hard mode, it already knew it had to check Shield and Health Upgrade as well?
# Well, we can achieve this by doing the "if world.options.hard_mode" check outside the set_rule call,
# and instead having two *different* set_rule calls depending on which case we're in.
if world.options.hard_mode:
# If you have multiple conditions, you can obviously chain them via "or" or "and".
# However, there are also the nice helper functions "state.has_any" and "state.has_all".
set_rule(
right_room_enemy,
lambda state: (
state.has("Sword", world.player) and state.has_any(("Shield", "Health Upgrade"), world.player)
),
)
else:
set_rule(right_room_enemy, lambda state: state.has("Sword", world.player))
# Another way to chain multiple conditions is via the add_rule function.
# This makes the access rules a bit slower though, so it should only be used if your structure justifies it.
# In our case, it's pretty useful because hard mode and easy mode have different requirements.
final_boss = world.get_location("Final Boss Defeated")
# For the "known" requirements, it's still better to chain them using a normal "and" condition.
add_rule(final_boss, lambda state: state.has_all(("Sword", "Shield"), world.player))
if world.options.hard_mode:
# You can check for multiple copies of an item by using the optional count parameter of state.has().
add_rule(final_boss, lambda state: state.has("Health Upgrade", world.player, 2))
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.
# You can just set a completion condition directly like any other condition, referencing items the player receives:
world.multiworld.completion_condition[world.player] = lambda state: state.has_all(("Sword", "Shield"), world.player)
# 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.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)