Skip to content
14 changes: 11 additions & 3 deletions worlds/blueprince/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -37,7 +40,6 @@

INNER_ROOM_KEY = "is_inner_room"


##################
# ITEM CONSTANTS #
##################
Expand Down Expand Up @@ -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]] = {}
ITEMS_BY_GROUPS : Dict[str, Set[str]] = {}
LOCATIONS_BY_GROUPS : Dict[str, Set[str]] = {}
113 changes: 113 additions & 0 deletions worlds/blueprince/dares.py
Original file line number Diff line number Diff line change
@@ -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": {

},
}
17 changes: 9 additions & 8 deletions worlds/blueprince/data_items.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Dict, Set
from .constants import *

from BaseClasses import Item, ItemClassification
Expand Down Expand Up @@ -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}
}
47 changes: 23 additions & 24 deletions worlds/blueprince/data_other_locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]] = {}

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand All @@ -503,7 +503,6 @@ def dig_spot_rule(state: CollectionState, player: int) -> bool:
"Patio",
"Storeroom",
"Garage",
"Boiler Room",
"Pump Room",
"Workshop",
"Secret Garden",
Expand All @@ -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": {
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),

Expand All @@ -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),
Expand Down Expand Up @@ -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,
},
Expand All @@ -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,
},
Expand All @@ -1450,15 +1449,15 @@ 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
LOCATION_ROOM_KEY: "Entrance Hall",
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)),
}
}

Expand Down Expand Up @@ -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},
}
10 changes: 9 additions & 1 deletion worlds/blueprince/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)):
Expand Down
Loading
Loading