diff --git a/worlds/blueprince/constants.py b/worlds/blueprince/constants.py index 60353849b1bd..e6aed7a430e4 100644 --- a/worlds/blueprince/constants.py +++ b/worlds/blueprince/constants.py @@ -2,6 +2,9 @@ # OPTIONS CONSTANTS # ##################### +from typing import Dict, Set + + ROOM_DRAFT_SANITY = "room_draft_sanity" STANDARD_ITEM_SANITY = "standard_item_sanity" WORKSHOP_SANITY = "workshop_sanity" @@ -37,7 +40,6 @@ INNER_ROOM_KEY = "is_inner_room" - ################## # ITEM CONSTANTS # ################## @@ -176,10 +178,16 @@ TRADING_POST_GIVE = "GIVE" TRADING_POST_RECEIVE = "RECEIVE" +################## +# DARE CONSTANTS # +################## + +DARE_CAN_REACH_RULE = "Can Reach Rule" +DARE_IS_POSSIBLE_RULE = "Is Possible Rule" ######################## # Item/Location GROUPS # ######################## -ITEMS_BY_GROUPS : dict[str, list[str]] = {} -LOCATIONS_BY_GROUPS : dict[str, list[str]] = {} \ No newline at end of file +ITEMS_BY_GROUPS : Dict[str, Set[str]] = {} +LOCATIONS_BY_GROUPS : Dict[str, Set[str]] = {} diff --git a/worlds/blueprince/dares.py b/worlds/blueprince/dares.py new file mode 100644 index 000000000000..ce0f7a386b1d --- /dev/null +++ b/worlds/blueprince/dares.py @@ -0,0 +1,113 @@ +from typing import Union, TYPE_CHECKING + +from .constants import * +from .data_rooms import blue_rooms, red_rooms, bedrooms, shops, black_rooms, hallways, green_rooms + +from BaseClasses import CollectionState +from collections.abc import Callable + +def dare_is_possible(dare_name: str, state: CollectionState, player: int, win_day: bool = False) -> bool: + + if dare_name not in dares: + return False + + dare = dares[dare_name] + + if DARE_IS_POSSIBLE_RULE not in dare: + return True + + return dare[DARE_IS_POSSIBLE_RULE](state, player, win_day) + +def can_reach_with_dares(world, to_check: str, type_hint: str = "Region", win_day: bool = False) -> bool: + for d in world.dares: + if not can_reach_with_dare(d, to_check, type_hint, win_day): + return False + + return True + +def can_reach_with_dare(dare_name: str, to_check: str, type_hint: str = "Region", win_day: bool = False) -> bool: + + if dare_name not in dares: + return False + + dare = dares[dare_name] + + if DARE_CAN_REACH_RULE not in dare: + return True + + return dare[DARE_CAN_REACH_RULE](to_check, type_hint, win_day) + +dares : dict[str, dict[str, Callable]] = { + "Lavatory30s": { + DARE_IS_POSSIBLE_RULE: lambda state, player, win_day: state.can_reach_region("Lavatory", player) or win_day + }, # Can reach Lavatory or can win today + "NoNorthEntranceHall": { + # TODO: rework region logic so this can be implemented + }, + "DraftFirstEntranceHall": { + # TODO: check if conflicts with other dares + }, + "ExcatlyOnePurchasePerShop": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not (to_check == "Aquarium" and type_hint == "Region") or win_day + }, + "AlwaysAtLeast20Steps": { + # Should always be possible + }, + "NeverDraftFullRank": { + # Should always be possible + }, + "OpenEmptyBoxParlor": { + # Should always be possible, unless we lock the windup key + }, + "OnlyOneButtonUtilityCloset": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not ((to_check == "Gemstone Cavern" and type_hint == "Region") or to_check == "VAC Controls") + }, + "LeaveBlueprint": { + # Should always be possible + }, + "NoBilliardFail": { + # Should always be possible + }, + "NeverStepThePool": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not (to_check == "The Pool" and type_hint == "Region") + }, + "OpenEachLockedTrunk": { + # Should always be possible + }, + "NeverEnterMoreThan3x": { + # Should always be possible + }, + "NeverDraftDen": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not (to_check == "Den" and type_hint == "Region") + }, + "AlwaysDraftMostExpensive": { + # Should always be possible + }, + "AlwaysDraftRed": { + # Should always be possible + }, + "NeverEatFruit": { + # Should always be possible + }, + "NeverRideElevator": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not ((to_check in ["The Foundation", "Blackbridge Grotto", "Tunnel Area Past Red Door", "Reservoir Bottom"] and type_hint == "Region") or (to_check in ["Underpass Gate", "Treasure Trove Floorplan"] and type_hint == "Location")) + }, + "NeverDraftSouth": { + DARE_CAN_REACH_RULE: lambda to_check, type_hint, win_day: not (to_check == "Her Ladyship's Chambers" and type_hint == "Region") + }, + "EndDayAtLeast1Gem": { + # Should always be possible + }, + "Draft6DifferentColors": { + DARE_IS_POSSIBLE_RULE: lambda state, player, win_day: win_day or state.can_reach_region("Aquarium", player) + }, + "NeverExitEntranceHall": { + # TODO: region logic would need to be fully rewritten to support this + }, + "EndDay0Gem0Coin0Key": { + # Should always be possible + }, + "NeverHaveMoreThan2Items": { + + }, +} \ No newline at end of file diff --git a/worlds/blueprince/data_items.py b/worlds/blueprince/data_items.py index 1e87e2cf7ec5..b0b59d1ab713 100644 --- a/worlds/blueprince/data_items.py +++ b/worlds/blueprince/data_items.py @@ -1,3 +1,4 @@ +from typing import Dict, Set from .constants import * from BaseClasses import Item, ItemClassification @@ -614,12 +615,12 @@ # None of the Tier 5 items can be received, so there's no point in defining it atm -ITEMS_BY_GROUPS = { - "Upgrade Disks": [disk for disk in upgrade_disks], - "Sanctum Keys": [key for key in sanctum_keys], - "Keys": [key for key in keys], - "Showroom Items": [item for item in showroom_items], - "Armory Items": [item for item in armory_items], - "Workshop Items": [item for item in workshop_items], - "Standard Items": [item for item in other_items] +ITEMS_BY_GROUPS : Dict[str, Set[str]] = { + "Upgrade Disks": {disk for disk in upgrade_disks}, + "Sanctum Keys": {key for key in sanctum_keys}, + "Keys": {key for key in keys}, + "Showroom Items": {item for item in showroom_items}, + "Armory Items": {item for item in armory_items}, + "Workshop Items": {item for item in workshop_items}, + "Standard Items": {item for item in other_items} } \ No newline at end of file diff --git a/worlds/blueprince/data_other_locations.py b/worlds/blueprince/data_other_locations.py index eea65fab40f3..39106a3ac40e 100644 --- a/worlds/blueprince/data_other_locations.py +++ b/worlds/blueprince/data_other_locations.py @@ -3,7 +3,7 @@ from .constants import * from .data_rooms import rooms, core_rooms, classrooms, room_layout_lists from .data_items import * -# from .world import LOCATIONS_BY_GROUPS +from .dares import can_reach_with_dares room_location_mem : dict[str, list[int]] = {} @@ -189,7 +189,7 @@ def can_reach_item_location(item_name: str, state: CollectionState, player: int) "Underpass Gate": { LOCATION_ID_KEY: get_room_location_id("The Underpass", 0), LOCATION_ROOM_KEY: "The Underpass", - LOCATION_RULE_SIMPLE_COMMON: lambda state, world: state.can_reach_region("Boiler Room", world.player) + LOCATION_RULE_SIMPLE_COMMON: lambda state, world: state.can_reach_region("Boiler Room", world.player) and can_reach_with_dares(world, "Boiler Room", "Region") }, "Shelter Safe": { LOCATION_ID_KEY: get_room_location_id("Shelter", 0), @@ -338,7 +338,7 @@ def can_reach_item_location(item_name: str, state: CollectionState, player: int) "Treasure Trove Floorplan": { LOCATION_ID_KEY: get_room_location_id("The Underpass", 2), LOCATION_ROOM_KEY: "The Underpass", - LOCATION_RULE_SIMPLE_COMMON: lambda state, world: state.can_reach_region("Boiler Room", world.player), + LOCATION_RULE_SIMPLE_COMMON: lambda state, world: state.can_reach_region("Boiler Room", world.player) and can_reach_with_dares(world, "Boiler Room", "Region"), NONSANITY_LOCATION_KEY: "Treasure Trove" }, "Throne Room Floorplan": { @@ -489,7 +489,7 @@ def advanced_experiment_rule(state: CollectionState, player: int) -> bool: def trading_post_rule(item_name: str, state: CollectionState, player: int) -> bool: return state.can_reach_region("Trading Post", player) and any(can_reach_item_location(item, state, player) for item in get_trading_post_offers(item_name)) -def dig_spot_rule(state: CollectionState, player: int) -> bool: +def dig_spot_rule(state: CollectionState, player: int, world) -> bool: return any(state.can_reach_region(region, player) for region in [ "The Foundation", "Wine Cellar", @@ -503,7 +503,6 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Patio", "Storeroom", "Garage", - "Boiler Room", "Pump Room", "Workshop", "Secret Garden", @@ -514,7 +513,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Solarium", "Tunnel", "Conservatory", - ]) or (state.can_reach_region("Planetarium", player) and can_reach_item_location("TELESCOPE", state, player)) + ]) or (state.can_reach_region("Planetarium", player) and can_reach_item_location("TELESCOPE", state, player)) or (state.can_reach_region("Boiler Room", player) and can_reach_with_dares(world, "Boiler Room", "Region")) standard_item_pickup = { "BATTERY PACK First Pickup": { @@ -579,7 +578,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Garage", "Utility Closet", "Kitchen", - ]) or (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player)), + ]) or (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world)), LOCATION_RULE_COMPLEX: darkroom_rule, @@ -968,7 +967,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Lost And Found", ]), - LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("JACK HAMMER", state, world.player) and dig_spot_rule(state, world.player), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("JACK HAMMER", state, world.player) and dig_spot_rule(state, world.player, world), }, "TELESCOPE First Pickup": { LOCATION_ID_KEY: get_room_location_id("Campsite", 20), # Doesn't spawn there, but putting it there and adding spawn locations as requirements @@ -1131,7 +1130,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Billiard Room", ]), - LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world), LOCATION_RULE_COMPLEX: lambda state, world: (state.can_reach_region("Garage", world.player) and can_reach_item_location("CAR KEYS", state, world.player)) or trunk_rule(state, world.player), @@ -1152,7 +1151,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: ]), # Also ignoring chance to spawn in trunks for the moment - LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world), LOCATION_RULE_COMPLEX: lambda state, world: (state.can_reach_region("Freezer", world.player) and any(can_reach_item_location(item, state, world.player) for item in ["Burning Glass", "TORCH"]) and can_reach_item_location("PRISM KEY_0", state, world)) or trunk_rule(state, world.player), @@ -1416,7 +1415,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Music Room", ]), - LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player)) or state.can_reach_region("Trophy Room", world.player), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world)) or state.can_reach_region("Trophy Room", world.player), LOCATION_RULE_EXTREME: advanced_experiment_rule, }, @@ -1434,7 +1433,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Music Room", ]), - LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player)), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world)), LOCATION_RULE_COMPLEX: lavatory_rule, LOCATION_RULE_EXTREME: advanced_experiment_rule, }, @@ -1450,7 +1449,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: "Hovel", ]), # Can also spawn in Spare Hall, but we aren't adding upgraded rooms seperately atm. - LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player)) or state.can_reach_region("Drawing Room", world.player), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world)) or state.can_reach_region("Drawing Room", world.player), }, "Vault Key 370": { LOCATION_ID_KEY: get_room_location_id("Entrance Hall", 7), # Doesn't spawn there, but putting it there and adding spawn locations as requirements @@ -1458,7 +1457,7 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: LOCATION_ITEM_KEY: "VAULT KEY 370", LOCATION_RULE_SIMPLE_COMMON: lambda state, world: state.can_reach_region("Lost And Found", world.player), - LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player)), + LOCATION_RULE_SIMPLE_RARE: lambda state, world: (can_reach_item_location("SHOVEL", state, world.player) and dig_spot_rule(state, world.player, world)), } } @@ -1606,14 +1605,14 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool: locations = trophies | safes_and_small_gates | mora_jai_boxes | floorplans | shop_items | upgrade_disks | keys | misc_locations | item_pickups | workshop_contraptions LOCATIONS_BY_GROUPS |= { - "Trophies": [k for k in trophies], - "Safes and Small Gates": [k for k in safes_and_small_gates], - "Mora Jai Boxes": [k for k in mora_jai_boxes], - "Floorplans": [k for k in floorplans], - "Shop Items": [k for k in shop_items], - "Upgrade Disks": [k for k in upgrade_disks], - "Keys": [k for k in keys], - "Miscellaneous": [k for k in misc_locations], - "Item Pickups": [k for k in item_pickups], - "Workshop Contraptions": [k for k in workshop_contraptions], + "Trophies": {k for k in trophies}, + "Safes and Small Gates": {k for k in safes_and_small_gates}, + "Mora Jai Boxes": {k for k in mora_jai_boxes}, + "Floorplans": {k for k in floorplans}, + "Shop Items": {k for k in shop_items}, + "Upgrade Disks": {k for k in upgrade_disks}, + "Keys": {k for k in keys}, + "Miscellaneous": {k for k in misc_locations}, + "Item Pickups": {k for k in item_pickups}, + "Workshop Contraptions": {k for k in workshop_contraptions}, } \ No newline at end of file diff --git a/worlds/blueprince/regions.py b/worlds/blueprince/regions.py index d81ad77593ad..ced14a0d16bc 100644 --- a/worlds/blueprince/regions.py +++ b/worlds/blueprince/regions.py @@ -9,6 +9,7 @@ from .constants import * from .room_min_pieces import * from .data_other_locations import can_reach_item_location +from .dares import can_reach_with_dares if TYPE_CHECKING: from .world import BluePrinceWorld @@ -237,6 +238,13 @@ def create_and_connect_regions(world: BluePrinceWorld) -> None: lambda state: state.count_from_list_unique(classrooms, world.player) >= cnum, ) + elif k == "Aquarium": + entrance_hall.connect( + room, + f"Entrance Hall Aquarium", + lambda state: can_reach_pick_position("Aquarium", world, state) and can_reach_with_dares(world, "Aquarium"), + ) + # TODO: Add Her Ladyship's Chamber, it has weird requirements elif k == "Entrance Hall": continue @@ -565,7 +573,7 @@ def can_reach_pick_position(room: str, world: BluePrinceWorld, state: Collection return False -def matches_minimum_inventory(required: list[tuple[int]], inventory: dict[str, int]) -> bool: +def matches_minimum_inventory(required: list[tuple[int, int, int, int]], inventory: dict[str, int]) -> bool: inv = tuple(inventory[k] for k in inventory) for req in required: if all(inv[i] >= req[i] for i in range(4)): diff --git a/worlds/blueprince/world.py b/worlds/blueprince/world.py index e19d8f30bb5b..ecf99afae043 100644 --- a/worlds/blueprince/world.py +++ b/worlds/blueprince/world.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from typing import Any +from typing import Any, Set # Imports of base Archipelago modules must be absolute. from worlds.AutoWorld import World @@ -8,7 +8,7 @@ # Imports of your world's files must be relative. from . import items, locations, regions, rules, web_world from . import ( - options as blue_prince_optionss, + options as blue_prince_options, ) # rename due to a name conflict with World.options @@ -26,8 +26,8 @@ class BluePrinceWorld(World): web = web_world.BluePrinceWebWorld() # Set the Options - options_dataclass = blue_prince_optionss.BluePrinceOptions - options: blue_prince_optionss.BluePrinceOptions + options_dataclass = blue_prince_options.BluePrinceOptions + options: blue_prince_options.BluePrinceOptions # Our world class must have a static location_name_to_id and item_name_to_id defined. # We define these in regions.py and items.py respectively, so we just set them here. @@ -39,6 +39,8 @@ class BluePrinceWorld(World): item_name_groups = ITEMS_BY_GROUPS + dares : Set[str] = set() + # # Our world class must have certain functions ("steps") that get called during generation. # # The main ones are: create_regions, set_rules, create_items. # # For better structure and readability, we put each of these in their own file. @@ -58,11 +60,29 @@ def create_item(self, name: str) -> items.BluePrinceItem: def get_filler_item_name(self) -> str: return items.get_random_filler_item_name(self) - # # There may be data that the game client will need to modify the behavior of the game. - # # This is what slot_data exists for. Upon every client connection, the slot's slot_data is sent to the client. - # # slot_data is just a dictionary using basic types, that will be converted to json when sent to the client. - # def fill_slot_data(self) -> Mapping[str, Any]: - # # If you need access to the player's chosen options on the client side, there is a helper for that. - # return self.options.as_dict( - # "hard_mode", "hammer", "extra_starting_chest", "confetti_explosiveness", "player_sprite" - # ) \ No newline at end of file + # There may be data that the game client will need to modify the behavior of the game. + # This is what slot_data exists for. Upon every client connection, the slot's slot_data is sent to the client. + # slot_data is just a dictionary using basic types, that will be converted to json when sent to the client. + def fill_slot_data(self) -> Mapping[str, Any]: + # If you need access to the player's chosen options on the client side, there is a helper for that. + return self.options.as_dict( + "room_draft_sanity", + "locked_trunks_common", + "locked_trunks_rare", + "locked_trunks_complex", + "standard_item_sanity", + "workshop_sanity", + "upgrade_disk_sanity", + "key_sanity", + "special_shop_sanity", + "item_logic_mode", + "filler_item_distribution", + "trap_type_distribution", + "trap_percentage", + "death_link_type", + "death_link_grace", + "death_link_monk_exception", + "goal_type", + "goal_sanctum_solves", + "start_inventory", + )