Files
dockipelago/worlds/apquest/rules.py
NewSoupVi e0cbf77dae APQuest: Implement New Game (#5393)
* APQuest

* Add confetti cannon

* ID change on enemy drop

* nevermind

* Write the apworld

* Actually implement hard mode

* split everything into multiple files

* Push out webworld into a file

* Comment

* Enemy health graphics

* more ruff rules

* graphics :)

* heal player when receiving health upgrade

* the dumbest client of all time

* Fix typo

* You can kinda play it now! Now we just need to render the game... :)))

* fix kvui imports again

* It's playable. Kind of

* oops

* Sounds and stuff

* exceptions for audio

* player sprite stuff

* Not attack without sword

* Make sure it plays correctly

* Collect behavior

* ruff

* don't need to clear checked_locations, but do need to still clear finished_game

* Connect calls disconnect, so this is not necessary

* more seemless reconnection

* Ok now I think it's correct

* Bgm

* Bgm

* minor adjustment

* More refactoring of graphics and sound

* add graphics

* Item column

* Fix enemies not regaining their health

* oops

* oops

* oops

* 6 health final boss on hard mode

* boss_6.png

* Display APQuest items correctly

* auto switch tabs

* some mypy stuff

* Intro song

* Confetti Cannon

* a bit more confetti work

* launcher component

* Graphics change

* graphics and cleanup

* fix apworld

* comment out horse and cat for now

* add docs

* copypasta

* ruff made my comment look unhinged

* Move that comment

* Fix typing and don't import kvui in nogui

* lmao that already exists I don't need to do it myself

* Must've just copied this from somewhere

* order change

* Add unit tests

* Notes about the client

* oops

* another intro song case

* Write WebWorld and setup guides

* Yes description provided

* thing

* how to play

* Music and Volume

* Add cat and horse player sprites

* updates

* Add hammer and breakable wall

* TODO

* replace wav with ogg

* Codeowners and readme

* finish unit tests

* lint

* Todid

* Update worlds/apquest/client/ap_quest_client.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Update worlds/apquest/client/custom_views.py

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* Filler pattern

* __future__ annotations

* twebhost

* Allow wasd and arrow keys

* correct wording

* oops

* just say the website

* append instead of +=

* qwint is onto my favoritism

* kitty alias

* Add a comment about preplaced items for assertAccessDependency

* Use classvar_matrix instead of MultiworldTestBase

* actually remove multiworld stuff from those tests

* missed one more

* Refactor a bit more

* Fix getting of the user path

* Actually explain components

* Meh

* Be a bit clearer about what's what

* oops

* More comments in the regions.py file

* Nevermind

* clarify regions further

* I use too many brackets

* Ok I'm done fr

* simplify wording

* missing .

* Add precollected example

* add note about precollected advancements

* missing s

* APQuest sound rework

* Volume slider

* I forgot I made this

* a

* fix volume of jingles

* Add math trap to game (only works in play_in_console mode so far)

* Math trap in apworld and client side

* Fix background during math trap

* fix leading 0

* Sound and further ui improvements for Math Trap

* fix music bug

* rename apquest subfolder to game

* Move comment to where it belongs

* Clear up language around components (hopefully)

* Clear up what CommonClient is

* Reword some more

* Mention Archipelago (the program) explicitly

* Update worlds/apquest/docs/en_APQuest.md

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Explain a bit more why you would use classvar matrix

* reword the assert raises stuff

* the volume slider thing is no longer true

* german game page

* Be more clear about why we're overriding Item and Location

* default item classification

* logically considered -> relevant to logic ()

* Update worlds/apquest/items.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* a word on the ambiguity of the word 'filler'

* more rewording

* amount -> number

* stress the necessity of appending to the multiworld itempool

* Update worlds/apquest/locations.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* get_location_names_with_ids

* slight rewording of the new helper method

* add some words about creating known location+item pairs

* Add some more words to worlds/apqeust/options.py

* more words in options.py

* 120 chars (thanks Ixrec >:((( LOL)

* Less confusing wording about rules, hopefully?

* victory -> completion

* remove the immediate creation of the hammer rule on the option region entrance

* access rule performance

* Make all imports module-level in world.py

* formatting

* get rid of noqa RUF012 (and also disable the rule in my local ruff.toml

* move comment for docstring closer to docstring in another place

* advancement????

* Missing function type annotations

* pass mypy again (I don't love this one but all the alternatives are equally bad)

* subclass instead of override

* I forgor to remove these

* Get rid of classvar_matrix and instead talk about some other stuff

* protect people a bit from the assertAccessDependency nonsense

* reword a bit more

* word

* More accessdependency text

* More accessdependency text

* More accessdependency text

* More accessdependency text

* oops

* this is supposed to be absolute

* Add some links to docs

* that's called game now

* Add an archipelago.json and explain what it means

* new line who dis

* reorganize a bit

* ignore instead of skip

* Update archipelago.json

* She new on my line till I

* Update archipelago.json

* add controls tab

* new ruff rule? idk

* WHOOPS

* Pack graphics into fewer files

* annoying ruff format thing

* Cleanup + mypy

* relative import

* Update worlds/apquest/client/custom_views.py

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Update generate_math_problem.py

* Update worlds/apquest/game/player.py

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

---------

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2025-11-25 00:38:06 +01:00

132 lines
7.6 KiB
Python

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)