Files
Archipelago/worlds/apquest/test/test_hard_mode.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

118 lines
6.9 KiB
Python

from .bases import APQuestTestBase
class TestHardMode(APQuestTestBase):
options = {
"hard_mode": True,
}
def test_hard_mode_access(self) -> None:
# For the sake of brevity, we won't repeat anything we tested in easy mode.
# Instead, let's use this opportunity to talk a bit more about assertAccessDependency.
# Let's take the Enemy Drop location.
# In hard mode, the Enemy has two health. One swipe of the Sword does not kill it.
# This means that the Enemy has a chance to attack you back.
# If you only have the Sword, this attack kills you. After respawning, the Enemy has full health again.
# However, if you have a Shield, you can block the attack (resulting in half damage).
# Alternatively, if you have found a Health Upgrade, you can tank an extra hit.
# Why is this important?
# If we called assertAccessDependency with ["Right Room Enemy Drop"] and [["Shield"]], it would actually *fail*.
# This is because "Right Room Enemy Drop" is beatable without "Shield" - You can use "Health Upgrade" instead.
# However, we can call assertAccessDependency with *both* items like this:
with self.subTest("Test that you need either Shield or Health Upgrade to beat the Right Room Enemy"):
self.assertAccessDependency(
["Right Room Enemy Drop"],
[["Shield"], ["Health Upgrade"]],
only_check_listed=True,
)
# This tests that:
# 1. No Shield & No Health Upgrades -> Right Room Enemy Drop is not reachable.
# 2. Shield & No Health Upgrades -> Right Room Enemy Drop is reachable.
# 3. No Shield & All Health Upgrades -> Right Room Enemy Drop is reachable.
# Note: Every other item that isn't the Shield nor a Health Upgrade is collected into state.
# This even includes pre-placed items, which notably includes any event location/item pairs you created.
# In our case, it means we don't have to mention the Sword. By omitting it, it's assumed that we have it.
# This explains why the possible_items parameter is a list, but not why it's a list of lists.
# Let's look at the Final Boss Location. This location requires Sword, Shield, and both Health Upgrades.
# We could implement it like this:
with self.subTest("Test that the final boss isn't beatable without Sword, Shield, and both Health Upgrades"):
self.assertAccessDependency(
["Final Boss Defeated"],
[["Sword", "Shield", "Health Upgrade"]],
only_check_listed=True,
)
# This would now test the following:
# 1. Without Sword, nor Shield, nor any Health Upgrades, the final boss is not beatable.
# 2. With Sword, Shield, and all Health Upgrades, the final boss is beatable.
# But, it's not really advisable to do this.
# Think about it: If we implemented our logic incorrectly and forgot to add the Shield requirement,
# this call would still pass. We'd rather make sure that each item individually is required:
for item in ["Sword", "Shield", "Health Upgrade"]:
with self.subTest(f"Test that the final boss requires {item}"):
self.assertAccessDependency(
["Final Boss Defeated"],
[[item]],
only_check_listed=True,
)
# This now tests that:
# 1. Without Sword, you can't beat the Final Boss
# 2. With Sword, you can beat the Final Boss (if you have everything else)
# 3. Without Shield, you can't beat the Final Boss
# 4. With Shield, you can beat the Final Boss (if you have everything else)
# 5. Without Health Upgrades, you can't beat the Final Boss
# 6. With all Health Upgrades, you can beat the Final Boss (if you have everything else)
# 2., 4., and 6. are the exact same check, so it is a bit redundant.
# But crucially, we are ensuring that all three items are actually required.
# So that's not really why the inner elements are lists.
# So we ask again: Why are they lists? When is it ever useful?
# Fair warning: This is probably where you should stop reading this and skip to test_hard_mode_health_upgrades.
# But if you really want to know why:
# Having multiple elements in the inner lists is something that only comes up in more complex scenarios.
# APQuest doesn't have any of these scenarios, but let's imagine one for completeness' sake.
# Currently, the Enemy can be beaten with these item combinations:
# 1. Sword and Shield
# 2. Sword and Health Upgrade
# Let's say there was also a "Shield Bash". When using the Shield Bash, you cannot use the Shield to defend.
# This would mean there is a third valid combination:
# 3. Shield + Health Upgrade
# We have set up a scenario where none of the three items are enough on their own,
# but any combination of two items works.
# The best way to test this would be to call assertAccessDependency with:
# [["Sword", "Shield"], ["Sword", "Health Upgrade"], ["Shield", "Health Upgrade"]]
# If we omitted any item from any of the three sub-lists, the check would fail.
# This is because the item is still *mentioned* in one of the other two conditions,
# meaning it is not collected into state.
# Thus, this term cannot be simplified any further without testing something different to what we want to test.
# You can kinda think of assertAccessDependency as an OR(AND(item_list_1), AND(item_list_2), ...).
# Except this "AND" is a special "AND" which allows reducing each list to a single representative item.
# And also, the "OR" is special as well in that has to be exhaustive,
# where the set of completely unmentioned items must *not* be able to reach the location collectively.
# And *also*, each "AND" must be enough to access the location *out of the mentioned items*.
# ... I'm not sure this explanation helps anyone, but most of the time, you really don't have to think about it.
def test_hard_mode_health_upgrades(self) -> None:
# We'll also repeat our Health Upgrade test from the Easy Mode test case, but modified for Hard Mode.
# This will not be explained again here.
health_upgrades = self.get_items_by_name("Health Upgrade")
with self.subTest("Test that there are two Health Upgrades in the pool"):
self.assertEqual(len(health_upgrades), 2)
with self.subTest("Test that the Health Upgrades in the pool are progression, but not useful."):
self.assertFalse(any(health_upgrade.useful for health_upgrade in health_upgrades))
self.assertTrue(all(health_upgrade.advancement for health_upgrade in health_upgrades))