lufia2ac: coop support + update AP version number to 0.4.2 (#1868)

* Core: typing for async_start

* CommonClient: add a framework for clients to subscribe to data storage key notifications

* Core: update version to 0.4.2

* lufia2ac: coop support
This commit is contained in:
el-u 2023-06-29 15:06:58 +02:00 committed by GitHub
parent d0db728850
commit dfb3df4a8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 218 additions and 78 deletions

View File

@ -191,6 +191,10 @@ class CommonContext:
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
# data storage
stored_data: typing.Dict[str, typing.Any]
stored_data_notification_keys: typing.Set[str]
# internals
# current message box through kvui
_messagebox: typing.Optional["kvui.MessageBox"] = None
@ -226,6 +230,9 @@ class CommonContext:
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.stored_data = {}
self.stored_data_notification_keys = set()
self.input_queue = asyncio.Queue()
self.input_requests = 0
@ -467,6 +474,21 @@ class CommonContext:
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
# data storage
def set_notify(self, *keys: str) -> None:
"""Subscribe to be notified of changes to selected data storage keys.
The values can be accessed via the "stored_data" attribute of this context, which is a dictionary mapping the
names of the data storage keys to the latest values received from the server.
"""
if new_keys := (set(keys) - self.stored_data_notification_keys):
self.stored_data_notification_keys.update(new_keys)
async_start(self.send_msgs([{"cmd": "Get",
"keys": list(new_keys)},
{"cmd": "SetNotify",
"keys": list(new_keys)}]))
# DeathLink hooks
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
@ -737,6 +759,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
if ctx.locations_scouted:
msgs.append({"cmd": "LocationScouts",
"locations": list(ctx.locations_scouted)})
if ctx.stored_data_notification_keys:
msgs.append({"cmd": "Get",
"keys": list(ctx.stored_data_notification_keys)})
msgs.append({"cmd": "SetNotify",
"keys": list(ctx.stored_data_notification_keys)})
if msgs:
await ctx.send_msgs(msgs)
if ctx.finished_game:
@ -800,7 +827,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
elif cmd == "Retrieved":
ctx.stored_data.update(args["keys"])
elif cmd == "SetReply":
ctx.stored_data[args["key"]] = args["value"]
if args["key"] == "EnergyLink":
ctx.current_energy_link_value = args["value"]
if ctx.ui:

View File

@ -42,7 +42,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.4.1"
__version__ = "0.4.2"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@ -766,10 +766,10 @@ def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
return buffer
_faf_tasks: "Set[asyncio.Task[None]]" = set()
_faf_tasks: "Set[asyncio.Task[typing.Any]]" = set()
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None:
def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = None) -> None:
"""
Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget"
@ -782,7 +782,7 @@ def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str]
# ```
# This implementation follows the pattern given in that documentation.
task = asyncio.create_task(co, name=name)
task: asyncio.Task[typing.Any] = asyncio.create_task(co, name=name)
_faf_tasks.add(task)
task.add_done_callback(_faf_tasks.discard)

View File

@ -1,14 +1,16 @@
import logging
import time
import typing
import uuid
from logging import Logger
from typing import Optional
from typing import Dict, List, Optional
from NetUtils import ClientStatus, NetworkItem
from worlds.AutoSNIClient import SNIClient
from .Enemies import enemy_id_to_name
from .Items import start_id as items_start_id
from .Locations import start_id as locations_start_id
from .Options import BlueChestCount
if typing.TYPE_CHECKING:
from SNIClient import SNIContext
@ -59,6 +61,18 @@ class L2ACSNIClient(SNIClient):
if signature != b"ArchipelagoLufia":
return
uuid_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 16, 16)
if uuid_data is None:
return
coop_uuid: uuid.UUID = uuid.UUID(bytes=uuid_data)
if coop_uuid.version != 4:
coop_uuid = uuid.uuid4()
snes_buffered_write(ctx, L2AC_TX_ADDR + 16, coop_uuid.bytes)
blue_chests_key: str = f"lufia2ac_blue_chests_checked_T{ctx.team}_P{ctx.slot}"
ctx.set_notify(blue_chests_key)
# Goal
if not ctx.finished_game:
goal_data: Optional[bytes] = await snes_read(ctx, L2AC_GOAL_ADDR, 10)
@ -78,29 +92,47 @@ class L2ACSNIClient(SNIClient):
await ctx.send_death(f"{player_name} was totally defeated by {enemy_name}.")
# TX
tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 8)
tx_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR, 12)
if tx_data is not None:
snes_items_sent = int.from_bytes(tx_data[:2], "little")
client_items_sent = int.from_bytes(tx_data[2:4], "little")
client_ap_items_found = int.from_bytes(tx_data[4:6], "little")
snes_blue_chests_checked: int = int.from_bytes(tx_data[:2], "little")
snes_ap_items_found: int = int.from_bytes(tx_data[6:8], "little")
snes_other_locations_checked: int = int.from_bytes(tx_data[10:12], "little")
if client_items_sent < snes_items_sent:
location_id: int = locations_start_id + client_items_sent
location: str = ctx.location_names[location_id]
client_items_sent += 1
blue_chests_checked: Dict[str, int] = ctx.stored_data.get(blue_chests_key) or {}
if blue_chests_checked.get(str(coop_uuid), 0) < snes_blue_chests_checked:
blue_chests_checked[str(coop_uuid)] = snes_blue_chests_checked
if blue_chests_key in ctx.stored_data:
await ctx.send_msgs([{
"cmd": "Set",
"key": blue_chests_key,
"default": {},
"want_reply": True,
"operations": [{
"operation": "update",
"value": {str(coop_uuid): snes_blue_chests_checked},
}],
}])
total_blue_chests_checked: int = min(sum(blue_chests_checked.values()), BlueChestCount.range_end)
snes_buffered_write(ctx, L2AC_TX_ADDR + 8, total_blue_chests_checked.to_bytes(2, "little"))
location_ids: List[int] = [locations_start_id + i for i in range(total_blue_chests_checked)]
loc_data: Optional[bytes] = await snes_read(ctx, L2AC_TX_ADDR + 32, snes_other_locations_checked * 2)
if loc_data is not None:
location_ids.extend(locations_start_id + int.from_bytes(loc_data[2 * i:2 * i + 2], "little")
for i in range(snes_other_locations_checked))
if new_location_ids := [loc_id for loc_id in location_ids if loc_id not in ctx.locations_checked]:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": new_location_ids}])
for location_id in new_location_ids:
ctx.locations_checked.add(location_id)
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location_id]}])
snes_logger.info("%d/%d blue chests" % (
len(list(loc for loc in ctx.locations_checked if not loc & 0x100)),
len(list(loc for loc in ctx.missing_locations | ctx.checked_locations if not loc & 0x100))))
snes_logger.info("New Check: %s (%d/%d)" % (
location,
len(ctx.locations_checked),
len(ctx.missing_locations) + len(ctx.checked_locations)))
snes_buffered_write(ctx, L2AC_TX_ADDR + 2, client_items_sent.to_bytes(2, "little"))
ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
if client_ap_items_found < ap_items_found:
snes_buffered_write(ctx, L2AC_TX_ADDR + 4, ap_items_found.to_bytes(2, "little"))
client_ap_items_found: int = sum(net_item.player != ctx.slot for net_item in ctx.locations_info.values())
if client_ap_items_found > snes_ap_items_found:
snes_buffered_write(ctx, L2AC_TX_ADDR + 4, client_ap_items_found.to_bytes(2, "little"))
# RX
rx_data: Optional[bytes] = await snes_read(ctx, L2AC_RX_ADDR, 4)

View File

@ -9,9 +9,11 @@ start_id: int = Locations.start_id
class ItemType(Enum):
BLUE_CHEST = auto()
BOSS = auto()
CAPSULE_MONSTER = auto()
ENEMY_DROP = auto()
ENTRANCE_CHEST = auto()
IRIS_TREASURE = auto()
PARTY_MEMBER = auto()
RED_CHEST = auto()
RED_CHEST_PATCH = auto()
@ -451,15 +453,15 @@ l2ac_item_table: Dict[str, ItemData] = {
# 0x0199: "Bunnysuit"
# 0x019A: "Seethru cape"
# 0x019B: "Seethru silk"
# 0x019C: "Iris sword"
# 0x019D: "Iris shield"
# 0x019E: "Iris helmet"
# 0x019F: "Iris armor"
# 0x01A0: "Iris ring"
# 0x01A1: "Iris jewel"
# 0x01A2: "Iris staff"
# 0x01A3: "Iris pot"
# 0x01A4: "Iris tiara"
"Iris sword": ItemData(0x039C, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris shield": ItemData(0x039D, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris helmet": ItemData(0x039E, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris armor": ItemData(0x039F, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris ring": ItemData(0x03A0, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris jewel": ItemData(0x03A1, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris staff": ItemData(0x03A2, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris pot": ItemData(0x03A3, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
"Iris tiara": ItemData(0x03A4, ItemType.IRIS_TREASURE, ItemClassification.progression_skip_balancing),
# 0x01A5: "Power jelly"
# 0x01A6: "Jewel sonar"
# 0x01A7: "Hook"
@ -489,7 +491,7 @@ l2ac_item_table: Dict[str, ItemData] = {
# 0x01BF: "Truth key"
# 0x01C0: "Mermaid jade"
# 0x01C1: "Engine"
# 0x01C2: "Ancient key"
"Ancient key": ItemData(0x01C2, ItemType.BOSS, ItemClassification.progression_skip_balancing),
# 0x01C3: "Pretty flwr."
# 0x01C4: "Glass angel"
# 0x01C5: "VIP card"

View File

@ -1,9 +1,15 @@
from typing import Dict
from BaseClasses import Location
from .Options import BlueChestCount
start_id: int = 0xAC0000
l2ac_location_name_to_id: Dict[str, int] = {f"Blue chest {i + 1}": (start_id + i) for i in range(88)}
l2ac_location_name_to_id: Dict[str, int] = {
**{f"Blue chest {i + 1}": (start_id + i) for i in range(BlueChestCount.range_end + 7 + 6)},
**{f"Iris treasure {i + 1}": (start_id + 0x039C + i) for i in range(9)},
"Boss": start_id + 0x01C2,
}
class L2ACLocation(Location):

View File

@ -109,13 +109,13 @@ class BlueChestCount(Range):
more for each party member or capsule monster if you have shuffle_party_members/shuffle_capsule_monsters enabled.
(You will still encounter blue chests in your world after all the multiworld location checks have been exhausted,
but these chests will then generate items for yourself only.)
Supported values: 10 75
Supported values: 10 100
Default value: 25
"""
display_name = "Blue chest count"
range_start = 10
range_end = 75
range_end = 100
default = 25

View File

@ -5,7 +5,7 @@ from enum import IntFlag
from random import Random
from typing import Any, ClassVar, Dict, get_type_hints, Iterator, List, Set, Tuple
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial
from BaseClasses import Entrance, Item, ItemClassification, Location, MultiWorld, Region, Tutorial
from Options import AssembleOptions
from Utils import __version__
from worlds.AutoWorld import WebWorld, World
@ -50,10 +50,11 @@ class L2ACWorld(World):
item_name_groups: ClassVar[Dict[str, Set[str]]] = {
"Blue chest items": {name for name, data in l2ac_item_table.items() if data.type is ItemType.BLUE_CHEST},
"Capsule monsters": {name for name, data in l2ac_item_table.items() if data.type is ItemType.CAPSULE_MONSTER},
"Iris treasures": {name for name, data in l2ac_item_table.items() if data.type is ItemType.IRIS_TREASURE},
"Party members": {name for name, data in l2ac_item_table.items() if data.type is ItemType.PARTY_MEMBER},
}
data_version: ClassVar[int] = 1
required_client_version: Tuple[int, int, int] = (0, 3, 6)
data_version: ClassVar[int] = 2
required_client_version: Tuple[int, int, int] = (0, 4, 2)
# L2ACWorld specific properties
rom_name: bytearray
@ -107,17 +108,20 @@ class L2ACWorld(World):
L2ACLocation(self.player, f"Chest access {i + 1}-{i + CHESTS_PER_SPHERE}", None, ancient_dungeon)
chest_access.place_locked_item(prog_chest_access)
ancient_dungeon.locations.append(chest_access)
treasures = L2ACLocation(self.player, "Iris Treasures", None, ancient_dungeon)
treasures.place_locked_item(L2ACItem("Treasures collected", ItemClassification.progression, None, self.player))
ancient_dungeon.locations.append(treasures)
for iris in self.item_name_groups["Iris treasures"]:
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
iris_treasure: Location = \
L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon)
iris_treasure.place_locked_item(self.create_item(iris))
ancient_dungeon.locations.append(iris_treasure)
self.multiworld.regions.append(ancient_dungeon)
final_floor = Region("FinalFloor", self.player, self.multiworld, "Ancient Cave Final Floor")
ff_reached = L2ACLocation(self.player, "Final Floor reached", None, final_floor)
ff_reached.place_locked_item(L2ACItem("Final Floor access", ItemClassification.progression, None, self.player))
final_floor.locations.append(ff_reached)
boss = L2ACLocation(self.player, "Boss", None, final_floor)
boss.place_locked_item(L2ACItem("Boss victory", ItemClassification.progression, None, self.player))
boss: Location = L2ACLocation(self.player, "Boss", self.location_name_to_id["Boss"], final_floor)
boss.place_locked_item(self.create_item("Ancient key"))
final_floor.locations.append(boss)
self.multiworld.regions.append(final_floor)
@ -155,8 +159,9 @@ class L2ACWorld(World):
set_rule(self.multiworld.get_entrance("FinalFloorEntrance", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
set_rule(self.multiworld.get_location("Iris Treasures", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
for i in range(9):
set_rule(self.multiworld.get_location(f"Iris treasure {i + 1}", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
set_rule(self.multiworld.get_location("Boss", self.player),
lambda state: state.can_reach(f"Blue chest {self.o.blue_chest_count}", "Location", self.player))
if self.o.shuffle_capsule_monsters:
@ -170,13 +175,14 @@ class L2ACWorld(World):
lambda state: state.has("Final Floor access", self.player)
elif self.o.goal == Goal.option_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Treasures collected", self.player)
lambda state: state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required))
elif self.o.goal == Goal.option_boss:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Boss victory", self.player)
lambda state: state.has("Ancient key", self.player)
elif self.o.goal == Goal.option_boss_iris_treasure_hunt:
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Boss victory", self.player) and state.has("Treasures collected", self.player)
lambda state: (state.has("Ancient key", self.player) and
state.has_group("Iris treasures", self.player, int(self.o.iris_treasures_required)))
def generate_output(self, output_directory: str) -> None:
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")

View File

@ -45,8 +45,8 @@ org $8EA721 ; skip master fight dialogue
DB $1C,$45,$01 ; L2SASM JMP $8EA5FA+$0145
org $8EA74B ; skip master victory dialogue
DB $1C,$AC,$01 ; L2SASM JMP $8EA5FA+$01AC
org $8EA7AA ; skip master key dialogue
DB $1C,$CA,$01 ; L2SASM JMP $8EA5FA+$01CA
org $8EA7AA ; skip master key dialogue and animation
DB $1C,$EE,$01 ; L2SASM JMP $8EA5FA+$01EE
org $8EA7F4 ; skip master goodbye dialogue
DB $1C,$05,$02 ; L2SASM JMP $8EA5FA+$0205
org $8EA807 ; skip master not fight dialogue
@ -126,7 +126,7 @@ Init:
; transmit checks
; transmit checks from chests
pushpc
org $8EC1EB
JML TX ; overwrites JSL $83F559
@ -136,11 +136,17 @@ TX:
JSL $83F559 ; (overwritten instruction) chest opening animation
REP #$20
LDA $7FD4EF ; read chest item ID
BIT.w #$4000 ; test for blue chest flag
BIT.w #$0200 ; test for iris item flag
BEQ +
LDA $F02040 ; load check counter
JSR ReportLocationCheck
SEP #$20
JML $8EC331 ; skip item get process
+: BIT.w #$4200 ; test for blue chest flag
BEQ +
LDA $F02048 ; load total blue chests checked
CMP $D08010 ; compare against max AP item number
BPL +
LDA $F02040 ; load check counter
INC ; increment check counter
STA $F02040 ; store check counter
SEP #$20
@ -150,6 +156,41 @@ TX:
; transmit checks from script events
pushpc
org $80A435
; DB=$8E, x=0, m=1
JML ScriptTX ; overwrites STA $7FD4F1
pullpc
ScriptTX:
STA $7FD4F1 ; (overwritten instruction)
REP #$20
LDA $7FD4EF ; read script item id
CMP.w #$01C2 ; test for ancient key
BNE +
JSR ReportLocationCheck
SEP #$20
JML $80A47F ; skip item get process
+: SEP #$20
JML $80A439 ; continue item get process
ReportLocationCheck:
PHA ; remember item id
LDA $F0204A ; load other locations count
INC ; increment check counter
STA $F0204A ; store other locations count
DEC
ASL
TAX
PLA
STA $F02060,X ; store item id in checked locations list
RTS
; report event flag based goal completion
pushpc
org $D09000
@ -173,9 +214,9 @@ pullpc
Goal:
TDC
LDA $0797 ; load some event flags (iris sword, iris shield, ..., iris pot)
LDA $0797 ; load EV flags $C8-$CF (iris sword, iris shield, ..., iris pot)
TAX
LDA $0798 ; load some event flags (iris tiara, boss, others...)
LDA $0798 ; load EV flags $D0-$D7 (iris tiara, boss, others...)
TAY
AND.b #$02 ; test boss victory
LSR
@ -223,16 +264,32 @@ RX:
SpecialItemGet:
BPL + ; spells have high bit set
JSR LearnSpell
+: BIT.w #$0200 ; iris items
BEQ +
SEC
SBC.w #$039C
ASL
TAX
LDA $8ED8C3,X ; load predefined bitmask with a single bit set
ORA $0797
STA $0797 ; set iris item EV flag ($C8-$D0)
BRA ++
+: CMP.w #$01C2 ; ancient key
BNE +
LDA.w #$0200
ORA $0797
STA $0797 ; set boss item EV flag ($D1)
BRA ++
+: CMP.w #$01BF ; capsule monster items range from $01B8 to $01BE
BPL +
BPL ++
SBC.w #$01B1 ; party member items range from $01B2 to $01B7
BMI +
BMI ++
ASL
TAX
LDA $8ED8C7,X ; load predefined bitmask with a single bit set
ORA $F02018 ; set unlock bit for party member/capsule monster
STA $F02018
+: RTS
++: RTS
LearnSpell:
STA $0A0B
@ -634,7 +691,7 @@ StartInventory:
PHX
JSR LearnSpell
PLX
+: BIT.w #$C000 ; ignore blue chest items (and spells)
+: BIT.w #$C200 ; ignore spells, blue chest items, and iris items
BNE +
PHX
STA $09CF ; specify item ID
@ -1025,12 +1082,16 @@ pullpc
; $F0203D 1 death link enabled
; $F0203E 1 death link sent (monster id + 1)
; $F0203F 1 death link received
; $F02040 2 check counter (snes_items_sent)
; $F02042 2 check counter (client_items_sent)
; $F02040 2 check counter for this save file (snes_blue_chests_checked)
; $F02042 2 RESERVED
; $F02044 2 check counter (client_ap_items_found)
; $F02046 2 check counter (snes_ap_items_found)
; $F02048 2 check counter for the slot (total_blue_chests_checked)
; $F0204A 2 check counter for this save file (snes_other_locations_checked)
; $F02050 16 coop uuid
; $F02060 var list of checked locations
; $F027E0 16 saved RX counters
; $F02800 2 received counter
; $F02802 2 processed counter
; $F02804 inf list of received items
; $F06000 inf architect mode RNG state backups
; $F02804 var list of received items
; $F06000 var architect mode RNG state backups

View File

@ -39,9 +39,9 @@ Your Party Leader will hold up the item they received when not in a fight or in
###### Customization options:
- Choose a goal for your world. Possible goals are: 1) Reach the final floor; 2) Defeat the boss on the final floor; 3)
Retrieve a (customizable) number of iris treasures from the cave; 4) Retrieve the iris treasures *and* defeat the boss
Retrieve a (customizable) number of Iris treasures from the cave; 4) Retrieve the Iris treasures *and* defeat the boss
- You can also randomize the goal; The blue-haired NPC in front of the cafe can tell you about the selected objective
- Customize the chances of encountering blue chests, healing tiles, iris treasures, etc.
- Customize the chances of encountering blue chests, healing tiles, Iris treasures, etc.
- Customize the default party lineup and capsule monster
- Customize the party starting level as well as capsule monster level and form
- Customize the initial and final floor numbers
@ -61,6 +61,7 @@ Your Party Leader will hold up the item they received when not in a fight or in
- You can elect to lock the cave layout for the next run, giving you exactly the same floors and red chest contents as
on your previous attempt. This functionality is accessed via the bald NPC behind the counter at the Ancient Cave
Entrance
- Multiple people can connect to the same slot and collaboratively search for Iris treasures and blue chests
- Always start with Providence already in your inventory. (It is no longer obtained from red chests)
- (optional) Run button that allows you to move at faster than normal speed

View File

@ -4,7 +4,7 @@ from . import L2ACTestBase
class TestDefault(L2ACTestBase):
def test_everything(self) -> None:
self.collect_all_but(["Boss victory"])
self.collect_all_but(["Ancient key"])
self.assertBeatable(True)
def test_nothing(self) -> None:
@ -17,7 +17,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
}
def test_everything(self) -> None:
self.collect_all_but(["Boss victory"])
self.collect_all_but(["Ancient key"])
self.assertBeatable(True)
def test_best_party(self) -> None:
@ -25,7 +25,7 @@ class TestShuffleCapsuleMonsters(L2ACTestBase):
self.assertBeatable(True)
def test_no_darbi(self) -> None:
self.collect_all_but(["Boss victory", "DARBI"])
self.collect_all_but(["Ancient key", "DARBI"])
self.assertBeatable(False)
@ -35,7 +35,7 @@ class TestShufflePartyMembers(L2ACTestBase):
}
def test_everything(self) -> None:
self.collect_all_but(["Boss victory"])
self.collect_all_but(["Ancient key"])
self.assertBeatable(True)
def test_best_party(self) -> None:
@ -43,15 +43,15 @@ class TestShufflePartyMembers(L2ACTestBase):
self.assertBeatable(True)
def test_no_dekar(self) -> None:
self.collect_all_but(["Boss victory", "Dekar"])
self.collect_all_but(["Ancient key", "Dekar"])
self.assertBeatable(False)
def test_no_guy(self) -> None:
self.collect_all_but(["Boss victory", "Guy"])
self.collect_all_but(["Ancient key", "Guy"])
self.assertBeatable(False)
def test_no_arty(self) -> None:
self.collect_all_but(["Boss victory", "Arty"])
self.collect_all_but(["Ancient key", "Arty"])
self.assertBeatable(False)
@ -62,7 +62,7 @@ class TestShuffleBoth(L2ACTestBase):
}
def test_everything(self) -> None:
self.collect_all_but(["Boss victory"])
self.collect_all_but(["Ancient key"])
self.assertBeatable(True)
def test_best_party(self) -> None:
@ -70,17 +70,17 @@ class TestShuffleBoth(L2ACTestBase):
self.assertBeatable(True)
def test_no_dekar(self) -> None:
self.collect_all_but(["Boss victory", "Dekar"])
self.collect_all_but(["Ancient key", "Dekar"])
self.assertBeatable(False)
def test_no_guy(self) -> None:
self.collect_all_but(["Boss victory", "Guy"])
self.collect_all_but(["Ancient key", "Guy"])
self.assertBeatable(False)
def test_no_arty(self) -> None:
self.collect_all_but(["Boss victory", "Arty"])
self.collect_all_but(["Ancient key", "Arty"])
self.assertBeatable(False)
def test_no_darbi(self) -> None:
self.collect_all_but(["Boss victory", "DARBI"])
self.collect_all_but(["Ancient key", "DARBI"])
self.assertBeatable(False)