diff --git a/.gitignore b/.gitignore index 4b1cf4a0..4a9f3402 100644 --- a/.gitignore +++ b/.gitignore @@ -169,6 +169,7 @@ cython_debug/ jdk*/ minecraft*/ minecraft_versions.json +!worlds/minecraft/ # pyenv .python-version diff --git a/setup.py b/setup.py index 1d80377f..7c55a4d2 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ apworlds: set = { "Super Mario World", "Stardew Valley", "Timespinner", + "Minecraft", } diff --git a/worlds/minecraft/Constants.py b/worlds/minecraft/Constants.py new file mode 100644 index 00000000..0d1101e8 --- /dev/null +++ b/worlds/minecraft/Constants.py @@ -0,0 +1,26 @@ +import os +import json +import pkgutil + +def load_data_file(*args) -> dict: + fname = os.path.join("data", *args) + return json.loads(pkgutil.get_data(__name__, fname).decode()) + +# For historical reasons, these values are different. +# They remain different to ensure datapackage consistency. +# Do not separate other games' location and item IDs like this. +item_id_offset: int = 45000 +location_id_offset: int = 42000 + +item_info = load_data_file("items.json") +item_name_to_id = {name: item_id_offset + index \ + for index, name in enumerate(item_info["all_items"])} +item_name_to_id["Bee Trap"] = item_id_offset + 100 # historical reasons + +location_info = load_data_file("locations.json") +location_name_to_id = {name: location_id_offset + index \ + for index, name in enumerate(location_info["all_locations"])} + +exclusion_info = load_data_file("excluded_locations.json") + +region_info = load_data_file("regions.json") diff --git a/worlds/minecraft/ItemPool.py b/worlds/minecraft/ItemPool.py new file mode 100644 index 00000000..78eeffca --- /dev/null +++ b/worlds/minecraft/ItemPool.py @@ -0,0 +1,52 @@ +from math import ceil +from typing import List + +from BaseClasses import MultiWorld, Item +from worlds.AutoWorld import World + +from . import Constants + +def get_junk_item_names(rand, k: int) -> str: + junk_weights = Constants.item_info["junk_weights"] + junk = rand.choices( + list(junk_weights.keys()), + weights=list(junk_weights.values()), + k=k) + return junk + +def build_item_pool(mc_world: World) -> List[Item]: + multiworld = mc_world.multiworld + player = mc_world.player + + itempool = [] + total_location_count = len(multiworld.get_unfilled_locations(player)) + + required_pool = Constants.item_info["required_pool"] + junk_weights = Constants.item_info["junk_weights"] + + # Add required progression items + for item_name, num in required_pool.items(): + itempool += [mc_world.create_item(item_name) for _ in range(num)] + + # Add structure compasses + if multiworld.structure_compasses[player]: + compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name] + for item_name in compasses: + itempool.append(mc_world.create_item(item_name)) + + # Dragon egg shards + if multiworld.egg_shards_required[player] > 0: + num = multiworld.egg_shards_available[player] + itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)] + + # Bee traps + bee_trap_percentage = multiworld.bee_traps[player] * 0.01 + if bee_trap_percentage > 0: + bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool))) + itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)] + + # Fill remaining itempool with randomly generated junk + junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool)) + itempool += [mc_world.create_item(name) for name in junk] + + return itempool diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py deleted file mode 100644 index 6cf8447c..00000000 --- a/worlds/minecraft/Items.py +++ /dev/null @@ -1,108 +0,0 @@ -from BaseClasses import Item -import typing - - -class ItemData(typing.NamedTuple): - code: typing.Optional[int] - progression: bool - - -class MinecraftItem(Item): - game: str = "Minecraft" - - -item_table = { - "Archery": ItemData(45000, True), - "Progressive Resource Crafting": ItemData(45001, True), - # "Resource Blocks": ItemData(45002, True), - "Brewing": ItemData(45003, True), - "Enchanting": ItemData(45004, True), - "Bucket": ItemData(45005, True), - "Flint and Steel": ItemData(45006, True), - "Bed": ItemData(45007, True), - "Bottles": ItemData(45008, True), - "Shield": ItemData(45009, True), - "Fishing Rod": ItemData(45010, True), - "Campfire": ItemData(45011, True), - "Progressive Weapons": ItemData(45012, True), - "Progressive Tools": ItemData(45013, True), - "Progressive Armor": ItemData(45014, True), - "8 Netherite Scrap": ItemData(45015, True), - "8 Emeralds": ItemData(45016, False), - "4 Emeralds": ItemData(45017, False), - "Channeling Book": ItemData(45018, True), - "Silk Touch Book": ItemData(45019, True), - "Sharpness III Book": ItemData(45020, False), - "Piercing IV Book": ItemData(45021, True), - "Looting III Book": ItemData(45022, False), - "Infinity Book": ItemData(45023, False), - "4 Diamond Ore": ItemData(45024, False), - "16 Iron Ore": ItemData(45025, False), - "500 XP": ItemData(45026, False), - "100 XP": ItemData(45027, False), - "50 XP": ItemData(45028, False), - "3 Ender Pearls": ItemData(45029, True), - "4 Lapis Lazuli": ItemData(45030, False), - "16 Porkchops": ItemData(45031, False), - "8 Gold Ore": ItemData(45032, False), - "Rotten Flesh": ItemData(45033, False), - "Single Arrow": ItemData(45034, False), - "32 Arrows": ItemData(45035, False), - "Saddle": ItemData(45036, True), - "Structure Compass (Village)": ItemData(45037, True), - "Structure Compass (Pillager Outpost)": ItemData(45038, True), - "Structure Compass (Nether Fortress)": ItemData(45039, True), - "Structure Compass (Bastion Remnant)": ItemData(45040, True), - "Structure Compass (End City)": ItemData(45041, True), - "Shulker Box": ItemData(45042, False), - "Dragon Egg Shard": ItemData(45043, True), - "Spyglass": ItemData(45044, True), - "Lead": ItemData(45045, True), - - "Bee Trap": ItemData(45100, False), - "Blaze Rods": ItemData(None, True), - "Defeat Ender Dragon": ItemData(None, True), - "Defeat Wither": ItemData(None, True), -} - -# 33 required items -required_items = { - "Archery": 1, - "Progressive Resource Crafting": 2, - "Brewing": 1, - "Enchanting": 1, - "Bucket": 1, - "Flint and Steel": 1, - "Bed": 1, - "Bottles": 1, - "Shield": 1, - "Fishing Rod": 1, - "Campfire": 1, - "Progressive Weapons": 3, - "Progressive Tools": 3, - "Progressive Armor": 2, - "8 Netherite Scrap": 2, - "Channeling Book": 1, - "Silk Touch Book": 1, - "Sharpness III Book": 1, - "Piercing IV Book": 1, - "Looting III Book": 1, - "Infinity Book": 1, - "3 Ender Pearls": 4, - "Saddle": 1, - "Spyglass": 1, - "Lead": 1, -} - -junk_weights = { - "4 Emeralds": 2, - "4 Diamond Ore": 1, - "16 Iron Ore": 1, - "50 XP": 4, - "16 Porkchops": 2, - "8 Gold Ore": 1, - "Rotten Flesh": 1, - "32 Arrows": 1, -} - -lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/minecraft/Locations.py b/worlds/minecraft/Locations.py deleted file mode 100644 index 46398ab1..00000000 --- a/worlds/minecraft/Locations.py +++ /dev/null @@ -1,192 +0,0 @@ -from BaseClasses import Location -import typing - - -class AdvData(typing.NamedTuple): - id: typing.Optional[int] - region: str - - -class MinecraftAdvancement(Location): - game: str = "Minecraft" - - def __init__(self, player: int, name: str, address: typing.Optional[int], parent): - super().__init__(player, name, address, parent) - self.event = not address - - -advancement_table = { - "Who is Cutting Onions?": AdvData(42000, 'Overworld'), - "Oh Shiny": AdvData(42001, 'Overworld'), - "Suit Up": AdvData(42002, 'Overworld'), - "Very Very Frightening": AdvData(42003, 'Overworld'), - "Hot Stuff": AdvData(42004, 'Overworld'), - "Free the End": AdvData(42005, 'The End'), - "A Furious Cocktail": AdvData(42006, 'Nether Fortress'), - "Best Friends Forever": AdvData(42007, 'Overworld'), - "Bring Home the Beacon": AdvData(42008, 'Nether Fortress'), - "Not Today, Thank You": AdvData(42009, 'Overworld'), - "Isn't It Iron Pick": AdvData(42010, 'Overworld'), - "Local Brewery": AdvData(42011, 'Nether Fortress'), - "The Next Generation": AdvData(42012, 'The End'), - "Fishy Business": AdvData(42013, 'Overworld'), - "Hot Tourist Destinations": AdvData(42014, 'The Nether'), - "This Boat Has Legs": AdvData(42015, 'The Nether'), - "Sniper Duel": AdvData(42016, 'Overworld'), - "Nether": AdvData(42017, 'The Nether'), - "Great View From Up Here": AdvData(42018, 'End City'), - "How Did We Get Here?": AdvData(42019, 'Nether Fortress'), - "Bullseye": AdvData(42020, 'Overworld'), - "Spooky Scary Skeleton": AdvData(42021, 'Nether Fortress'), - "Two by Two": AdvData(42022, 'The Nether'), - "Stone Age": AdvData(42023, 'Overworld'), - "Two Birds, One Arrow": AdvData(42024, 'Overworld'), - "We Need to Go Deeper": AdvData(42025, 'The Nether'), - "Who's the Pillager Now?": AdvData(42026, 'Pillager Outpost'), - "Getting an Upgrade": AdvData(42027, 'Overworld'), - "Tactical Fishing": AdvData(42028, 'Overworld'), - "Zombie Doctor": AdvData(42029, 'Overworld'), - "The City at the End of the Game": AdvData(42030, 'End City'), - "Ice Bucket Challenge": AdvData(42031, 'Overworld'), - "Remote Getaway": AdvData(42032, 'The End'), - "Into Fire": AdvData(42033, 'Nether Fortress'), - "War Pigs": AdvData(42034, 'Bastion Remnant'), - "Take Aim": AdvData(42035, 'Overworld'), - "Total Beelocation": AdvData(42036, 'Overworld'), - "Arbalistic": AdvData(42037, 'Overworld'), - "The End... Again...": AdvData(42038, 'The End'), - "Acquire Hardware": AdvData(42039, 'Overworld'), - "Not Quite \"Nine\" Lives": AdvData(42040, 'The Nether'), - "Cover Me With Diamonds": AdvData(42041, 'Overworld'), - "Sky's the Limit": AdvData(42042, 'End City'), - "Hired Help": AdvData(42043, 'Overworld'), - "Return to Sender": AdvData(42044, 'The Nether'), - "Sweet Dreams": AdvData(42045, 'Overworld'), - "You Need a Mint": AdvData(42046, 'The End'), - "Adventure": AdvData(42047, 'Overworld'), - "Monsters Hunted": AdvData(42048, 'Overworld'), - "Enchanter": AdvData(42049, 'Overworld'), - "Voluntary Exile": AdvData(42050, 'Pillager Outpost'), - "Eye Spy": AdvData(42051, 'Overworld'), - "The End": AdvData(42052, 'The End'), - "Serious Dedication": AdvData(42053, 'The Nether'), - "Postmortal": AdvData(42054, 'Village'), - "Monster Hunter": AdvData(42055, 'Overworld'), - "Adventuring Time": AdvData(42056, 'Overworld'), - "A Seedy Place": AdvData(42057, 'Overworld'), - "Those Were the Days": AdvData(42058, 'Bastion Remnant'), - "Hero of the Village": AdvData(42059, 'Village'), - "Hidden in the Depths": AdvData(42060, 'The Nether'), - "Beaconator": AdvData(42061, 'Nether Fortress'), - "Withering Heights": AdvData(42062, 'Nether Fortress'), - "A Balanced Diet": AdvData(42063, 'Village'), - "Subspace Bubble": AdvData(42064, 'The Nether'), - "Husbandry": AdvData(42065, 'Overworld'), - "Country Lode, Take Me Home": AdvData(42066, 'The Nether'), - "Bee Our Guest": AdvData(42067, 'Overworld'), - "What a Deal!": AdvData(42068, 'Village'), - "Uneasy Alliance": AdvData(42069, 'The Nether'), - "Diamonds!": AdvData(42070, 'Overworld'), - "A Terrible Fortress": AdvData(42071, 'Nether Fortress'), - "A Throwaway Joke": AdvData(42072, 'Overworld'), - "Minecraft": AdvData(42073, 'Overworld'), - "Sticky Situation": AdvData(42074, 'Overworld'), - "Ol' Betsy": AdvData(42075, 'Overworld'), - "Cover Me in Debris": AdvData(42076, 'The Nether'), - "The End?": AdvData(42077, 'The End'), - "The Parrots and the Bats": AdvData(42078, 'Overworld'), - "A Complete Catalogue": AdvData(42079, 'Village'), - "Getting Wood": AdvData(42080, 'Overworld'), - "Time to Mine!": AdvData(42081, 'Overworld'), - "Hot Topic": AdvData(42082, 'Overworld'), - "Bake Bread": AdvData(42083, 'Overworld'), - "The Lie": AdvData(42084, 'Overworld'), - "On a Rail": AdvData(42085, 'Overworld'), - "Time to Strike!": AdvData(42086, 'Overworld'), - "Cow Tipper": AdvData(42087, 'Overworld'), - "When Pigs Fly": AdvData(42088, 'Overworld'), - "Overkill": AdvData(42089, 'Nether Fortress'), - "Librarian": AdvData(42090, 'Overworld'), - "Overpowered": AdvData(42091, 'Bastion Remnant'), - "Wax On": AdvData(42092, 'Overworld'), - "Wax Off": AdvData(42093, 'Overworld'), - "The Cutest Predator": AdvData(42094, 'Overworld'), - "The Healing Power of Friendship": AdvData(42095, 'Overworld'), - "Is It a Bird?": AdvData(42096, 'Overworld'), - "Is It a Balloon?": AdvData(42097, 'The Nether'), - "Is It a Plane?": AdvData(42098, 'The End'), - "Surge Protector": AdvData(42099, 'Overworld'), - "Light as a Rabbit": AdvData(42100, 'Overworld'), - "Glow and Behold!": AdvData(42101, 'Overworld'), - "Whatever Floats Your Goat!": AdvData(42102, 'Overworld'), - "Caves & Cliffs": AdvData(42103, 'Overworld'), - "Feels like home": AdvData(42104, 'The Nether'), - "Sound of Music": AdvData(42105, 'Overworld'), - "Star Trader": AdvData(42106, 'Village'), - - # 1.19 advancements - "Birthday Song": AdvData(42107, 'Pillager Outpost'), - "Bukkit Bukkit": AdvData(42108, 'Overworld'), - "It Spreads": AdvData(42109, 'Overworld'), - "Sneak 100": AdvData(42110, 'Overworld'), - "When the Squad Hops into Town": AdvData(42111, 'Overworld'), - "With Our Powers Combined!": AdvData(42112, 'The Nether'), - "You've Got a Friend in Me": AdvData(42113, 'Pillager Outpost'), - - "Blaze Spawner": AdvData(None, 'Nether Fortress'), - "Ender Dragon": AdvData(None, 'The End'), - "Wither": AdvData(None, 'Nether Fortress'), -} - -exclusion_table = { - "hard": { - "Very Very Frightening", - "A Furious Cocktail", - "Two by Two", - "Two Birds, One Arrow", - "Arbalistic", - "Monsters Hunted", - "Beaconator", - "A Balanced Diet", - "Uneasy Alliance", - "Cover Me in Debris", - "A Complete Catalogue", - "Surge Protector", - "Sound of Music", - "Star Trader", - "When the Squad Hops into Town", - "With Our Powers Combined!", - }, - "unreasonable": { - "How Did We Get Here?", - "Adventuring Time", - }, -} - -def get_postgame_advancements(required_bosses): - - postgame_advancements = { - "ender_dragon": { - "Free the End", - "The Next Generation", - "The End... Again...", - "You Need a Mint", - "Monsters Hunted", - "Is It a Plane?", - }, - "wither": { - "Withering Heights", - "Bring Home the Beacon", - "Beaconator", - "A Furious Cocktail", - "How Did We Get Here?", - "Monsters Hunted", - } - } - - advancements = set() - if required_bosses in {"ender_dragon", "both"}: - advancements.update(postgame_advancements["ender_dragon"]) - if required_bosses in {"wither", "both"}: - advancements.update(postgame_advancements["wither"]) - return advancements diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 161d44d9..084a611e 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -6,7 +6,7 @@ class AdvancementGoal(Range): """Number of advancements required to spawn bosses.""" display_name = "Advancement Goal" range_start = 0 - range_end = 95 + range_end = 114 default = 40 @@ -14,7 +14,7 @@ class EggShardsRequired(Range): """Number of dragon egg shards to collect to spawn bosses.""" display_name = "Egg Shards Required" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -22,7 +22,7 @@ class EggShardsAvailable(Range): """Number of dragon egg shards available to collect.""" display_name = "Egg Shards Available" range_start = 0 - range_end = 40 + range_end = 74 default = 0 @@ -35,6 +35,14 @@ class BossGoal(Choice): option_both = 3 default = 1 + @property + def dragon(self): + return self.value % 2 == 1 + + @property + def wither(self): + return self.value > 1 + class ShuffleStructures(DefaultOnToggle): """Enables shuffling of villages, outposts, fortresses, bastions, and end cities.""" @@ -94,14 +102,16 @@ minecraft_options: typing.Dict[str, type(Option)] = { "egg_shards_required": EggShardsRequired, "egg_shards_available": EggShardsAvailable, "required_bosses": BossGoal, + "shuffle_structures": ShuffleStructures, "structure_compasses": StructureCompasses, - "bee_traps": BeeTraps, + "combat_difficulty": CombatDifficulty, "include_hard_advancements": HardAdvancements, "include_unreasonable_advancements": UnreasonableAdvancements, "include_postgame_advancements": PostgameAdvancements, + "bee_traps": BeeTraps, "send_defeated_mobs": SendDefeatedMobs, - "starting_items": StartingItems, "death_link": DeathLink, + "starting_items": StartingItems, } diff --git a/worlds/minecraft/Regions.py b/worlds/minecraft/Regions.py deleted file mode 100644 index d9f3f1b5..00000000 --- a/worlds/minecraft/Regions.py +++ /dev/null @@ -1,93 +0,0 @@ - -def link_minecraft_structures(world, player): - - # Link mandatory connections first - for (exit, region) in mandatory_connections: - world.get_entrance(exit, player).connect(world.get_region(region, player)) - - # Get all unpaired exits and all regions without entrances (except the Menu) - # This function is destructive on these lists. - exits = [exit.name for r in world.regions if r.player == player for exit in r.exits if exit.connected_region == None] - structs = [r.name for r in world.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] - exits_spoiler = exits[:] # copy the original order for the spoiler log - try: - assert len(exits) == len(structs) - except AssertionError as e: # this should never happen - raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_name[player]})") - - pairs = {} - - def set_pair(exit, struct): - if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): - pairs[exit] = struct - exits.remove(exit) - structs.remove(struct) - else: - raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({world.player_name[player]})") - - # Connect plando structures first - if world.plando_connections[player]: - for conn in world.plando_connections[player]: - set_pair(conn.entrance, conn.exit) - - # The algorithm tries to place the most restrictive structures first. This algorithm always works on the - # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. - if world.shuffle_structures[player]: - structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) - for struct in structs[:]: - try: - exit = world.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) - except IndexError: - raise Exception(f"No valid structure placements remaining for player {player} ({world.player_name[player]})") - set_pair(exit, struct) - else: # write remaining default connections - for (exit, struct) in default_connections: - if exit in exits: - set_pair(exit, struct) - - # Make sure we actually paired everything; might fail if plando - try: - assert len(exits) == len(structs) == 0 - except AssertionError: - raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_name[player]})") - - for exit in exits_spoiler: - world.get_entrance(exit, player).connect(world.get_region(pairs[exit], player)) - if world.shuffle_structures[player] or world.plando_connections[player]: - world.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) - - - -# (Region name, list of exits) -mc_regions = [ - ('Menu', ['New World']), - ('Overworld', ['Nether Portal', 'End Portal', 'Overworld Structure 1', 'Overworld Structure 2']), - ('The Nether', ['Nether Structure 1', 'Nether Structure 2']), - ('The End', ['The End Structure']), - ('Village', []), - ('Pillager Outpost', []), - ('Nether Fortress', []), - ('Bastion Remnant', []), - ('End City', []) -] - -# (Entrance, region pointed to) -mandatory_connections = [ - ('New World', 'Overworld'), - ('Nether Portal', 'The Nether'), - ('End Portal', 'The End') -] - -default_connections = [ - ('Overworld Structure 1', 'Village'), - ('Overworld Structure 2', 'Pillager Outpost'), - ('Nether Structure 1', 'Nether Fortress'), - ('Nether Structure 2', 'Bastion Remnant'), - ('The End Structure', 'End City') -] - -# Structure: illegal locations -illegal_connections = { - 'Nether Fortress': ['The End Structure'] -} - diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 2ec95237..dae4241b 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -1,317 +1,313 @@ -from ..generic.Rules import set_rule, add_rule -from .Locations import exclusion_table, get_postgame_advancements -from BaseClasses import MultiWorld -from ..AutoWorld import LogicMixin +import typing +from collections.abc import Callable + +from BaseClasses import CollectionState +from worlds.generic.Rules import exclusion_rules +from worlds.AutoWorld import World + +from . import Constants + +# Helper functions +# moved from logicmixin + +def has_iron_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_copper_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player) + +def has_gold_ingots(state: CollectionState, player: int) -> bool: + return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player)) + +def has_diamond_pickaxe(state: CollectionState, player: int) -> bool: + return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player) + +def craft_crossbow(state: CollectionState, player: int) -> bool: + return state.has('Archery', player) and has_iron_ingots(state, player) + +def has_bottle(state: CollectionState, player: int) -> bool: + return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player) + +def has_spyglass(state: CollectionState, player: int) -> bool: + return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player) + +def can_enchant(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis + +def can_use_anvil(state: CollectionState, player: int) -> bool: + return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player) + +def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls + return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player) + +def can_brew_potions(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player) + +def can_piglin_trade(state: CollectionState, player: int) -> bool: + return has_gold_ingots(state, player) and ( + state.can_reach('The Nether', 'Region', player) or + state.can_reach('Bastion Remnant', 'Region', player)) + +def overworld_villager(state: CollectionState, player: int) -> bool: + village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name + if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village + return (state.can_reach('Zombie Doctor', 'Location', player) or + (has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player))) + elif village_region == 'The End': + return state.can_reach('Zombie Doctor', 'Location', player) + return state.can_reach('Village', 'Region', player) + +def enter_stronghold(state: CollectionState, player: int) -> bool: + return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player) + +# Difficulty-dependent functions +def combat_difficulty(state: CollectionState, player: int) -> bool: + return state.multiworld.combat_difficulty[player].current_key + +def can_adventure(state: CollectionState, player: int) -> bool: + death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player) + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check + elif combat_difficulty(state, player) == 'hard': + return True + return (state.has('Progressive Weapons', player) and death_link_check and + (state.has('Progressive Resource Crafting', player) or state.has('Campfire', player))) + +def basic_combat(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \ + state.has('Shield', player) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': + return True + return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player) + +def complete_raid(state: CollectionState, player: int) -> bool: + reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player) + if combat_difficulty(state, player) == 'easy': + return reach_regions and \ + state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \ + state.has('Shield', player) and state.has('Archery', player) and \ + state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player) + elif combat_difficulty(state, player) == 'hard': # might be too hard? + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + (state.has('Progressive Armor', player) or state.has('Shield', player)) + return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \ + state.has('Progressive Armor', player) and state.has('Shield', player) + +def can_kill_wither(state: CollectionState, player: int) -> bool: + normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'easy': + return fortress_loot(state, player) and normal_kill and state.has('Archery', player) + elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings + return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player)) + return fortress_loot(state, player) and normal_kill + +def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool: + return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \ + state.has('Progressive Resource Crafting', player) # smelt sand into glass + +def can_kill_ender_dragon(state: CollectionState, player: int) -> bool: + if combat_difficulty(state, player) == 'easy': + return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \ + state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player) + if combat_difficulty(state, player) == 'hard': + return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \ + (state.has('Progressive Weapons', player, 1) and state.has('Bed', player)) + return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player) + +def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool: + if not state.multiworld.structure_compasses[player]: + return True + return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) -class MinecraftLogic(LogicMixin): +def get_rules_lookup(player: int): + rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = { + "entrances": { + "Nether Portal": lambda state: (state.has('Flint and Steel', player) and + (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and + has_iron_ingots(state, player)), + "End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4), + "Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)), + "Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)), + "Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)), + "Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)), + "The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)), + }, + "locations": { + "Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Wither": lambda state: can_kill_wither(state, player), + "Blaze Rods": lambda state: fortress_loot(state, player), - def _mc_has_iron_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_copper_ingots(self, player: int): - return self.has('Progressive Tools', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_gold_ingots(self, player: int): - return self.has('Progressive Resource Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player)) - - def _mc_has_diamond_pickaxe(self, player: int): - return self.has('Progressive Tools', player, 3) and self._mc_has_iron_ingots(player) - - def _mc_craft_crossbow(self, player: int): - return self.has('Archery', player) and self._mc_has_iron_ingots(player) - - def _mc_has_bottle(self, player: int): - return self.has('Bottles', player) and self.has('Progressive Resource Crafting', player) - - def _mc_has_spyglass(self, player: int): - return self._mc_has_copper_ingots(player) and self.has('Spyglass', player) and self._mc_can_adventure(player) - - def _mc_can_enchant(self, player: int): - return self.has('Enchanting', player) and self._mc_has_diamond_pickaxe(player) # mine obsidian and lapis - - def _mc_can_use_anvil(self, player: int): - return self.has('Enchanting', player) and self.has('Progressive Resource Crafting', player, 2) and self._mc_has_iron_ingots(player) - - def _mc_fortress_loot(self, player: int): # saddles, blaze rods, wither skulls - return self.can_reach('Nether Fortress', 'Region', player) and self._mc_basic_combat(player) - - def _mc_can_brew_potions(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self._mc_has_bottle(player) - - def _mc_can_piglin_trade(self, player: int): - return self._mc_has_gold_ingots(player) and ( - self.can_reach('The Nether', 'Region', player) or - self.can_reach('Bastion Remnant', 'Region', player)) - - def _mc_overworld_villager(self, player: int): - village_region = self.multiworld.get_region('Village', player).entrances[0].parent_region.name - if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village - return (self.can_reach('Zombie Doctor', 'Location', player) or - (self._mc_has_diamond_pickaxe(player) and self.can_reach('Village', 'Region', player))) - elif village_region == 'The End': - return self.can_reach('Zombie Doctor', 'Location', player) - return self.can_reach('Village', 'Region', player) - - def _mc_enter_stronghold(self, player: int): - return self.has('Blaze Rods', player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player) - - # Difficulty-dependent functions - def _mc_combat_difficulty(self, player: int): - return self.multiworld.combat_difficulty[player].current_key - - def _mc_can_adventure(self, player: int): - death_link_check = not self.multiworld.death_link[player] or self.has('Bed', player) - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and death_link_check - elif self._mc_combat_difficulty(player) == 'hard': - return True - return (self.has('Progressive Weapons', player) and death_link_check and - (self.has('Progressive Resource Crafting', player) or self.has('Campfire', player))) - - def _mc_basic_combat(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \ - self.has('Shield', player) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': - return True - return self.has('Progressive Weapons', player) and (self.has('Progressive Armor', player) or self.has('Shield', player)) and self._mc_has_iron_ingots(player) - - def _mc_complete_raid(self, player: int): - reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player) - if self._mc_combat_difficulty(player) == 'easy': - return reach_regions and \ - self.has('Progressive Weapons', player, 3) and self.has('Progressive Armor', player, 2) and \ - self.has('Shield', player) and self.has('Archery', player) and \ - self.has('Progressive Tools', player, 2) and self._mc_has_iron_ingots(player) - elif self._mc_combat_difficulty(player) == 'hard': # might be too hard? - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - (self.has('Progressive Armor', player) or self.has('Shield', player)) - return reach_regions and self.has('Progressive Weapons', player, 2) and self._mc_has_iron_ingots(player) and \ - self.has('Progressive Armor', player) and self.has('Shield', player) - - def _mc_can_kill_wither(self, player: int): - normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'easy': - return self._mc_fortress_loot(player) and normal_kill and self.has('Archery', player) - elif self._mc_combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings - return self._mc_fortress_loot(player) and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player)) - return self._mc_fortress_loot(player) and normal_kill - - def _mc_can_respawn_ender_dragon(self, player: int): - return self.can_reach('The Nether', 'Region', player) and self.can_reach('The End', 'Region', player) and \ - self.has('Progressive Resource Crafting', player) # smelt sand into glass - - def _mc_can_kill_ender_dragon(self, player: int): - if self._mc_combat_difficulty(player) == 'easy': - return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ - self.has('Archery', player) and self._mc_can_brew_potions(player) and self._mc_can_enchant(player) - if self._mc_combat_difficulty(player) == 'hard': - return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \ - (self.has('Progressive Weapons', player, 1) and self.has('Bed', player)) - return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) - - def _mc_has_structure_compass(self, entrance_name: str, player: int): - if not self.multiworld.structure_compasses[player]: - return True - return self.has(f"Structure Compass ({self.multiworld.get_entrance(entrance_name, player).connected_region.name})", player) - -# Sets rules on entrances and advancements that are always applied -def set_advancement_rules(world: MultiWorld, player: int): - - # Retrieves the appropriate structure compass for the given entrance - def get_struct_compass(entrance_name): - struct = world.get_entrance(entrance_name, player).connected_region.name - return f"Structure Compass ({struct})" - - set_rule(world.get_entrance("Nether Portal", player), lambda state: state.has('Flint and Steel', player) and - (state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and - state._mc_has_iron_ingots(player)) - set_rule(world.get_entrance("End Portal", player), lambda state: state._mc_enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) - set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 1", player)) - set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Overworld Structure 2", player)) - set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 1", player)) - set_rule(world.get_entrance("Nether Structure 2", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("Nether Structure 2", player)) - set_rule(world.get_entrance("The End Structure", player), lambda state: state._mc_can_adventure(player) and state._mc_has_structure_compass("The End Structure", player)) - - set_rule(world.get_location("Ender Dragon", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Wither", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("Blaze Spawner", player), lambda state: state._mc_fortress_loot(player)) - - set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Oh Shiny", player), lambda state: state._mc_can_piglin_trade(player)) - set_rule(world.get_location("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("A Furious Cocktail", player), lambda state: state._mc_can_brew_potions(player) and - state.has("Fishing Rod", player) and # Water Breathing - state.can_reach('The Nether', 'Region', player) and # Regeneration, Fire Resistance, gold nuggets - state.can_reach('Village', 'Region', player) and # Night Vision, Invisibility - state.can_reach('Bring Home the Beacon', 'Location', player)) # Resistance - # set_rule(world.get_location("Best Friends Forever", player), lambda state: True) - set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state._mc_can_kill_wither(player) and - state._mc_has_diamond_pickaxe(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Isn't It Iron Pick", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Local Brewery", player), lambda state: state._mc_can_brew_potions(player)) - set_rule(world.get_location("The Next Generation", player), lambda state: state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Fishy Business", player), lambda state: state.has("Fishing Rod", player)) - # set_rule(world.get_location("Hot Tourist Destinations", player), lambda state: True) - set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player)) - set_rule(world.get_location("Sniper Duel", player), lambda state: state.has("Archery", player)) - # set_rule(world.get_location("Nether", player), lambda state: True) - set_rule(world.get_location("Great View From Up Here", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("How Did We Get Here?", player), lambda state: state._mc_can_brew_potions(player) and - state._mc_has_gold_ingots(player) and # Absorption - state.can_reach('End City', 'Region', player) and # Levitation - state.can_reach('The Nether', 'Region', player) and # potion ingredients - state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows - state.can_reach("Bring Home the Beacon", "Location", player) and # Haste - state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village - set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots - # set_rule(world.get_location("Stone Age", player), lambda state: True) - set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player)) - # set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True) - set_rule(world.get_location("Who's the Pillager Now?", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Getting an Upgrade", player), lambda state: state.has("Progressive Tools", player)) - set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Zombie Doctor", player), lambda state: state._mc_can_brew_potions(player) and state._mc_has_gold_ingots(player)) - # set_rule(world.get_location("The City at the End of the Game", player), lambda state: True) - set_rule(world.get_location("Ice Bucket Challenge", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Remote Getaway", player), lambda state: True) - set_rule(world.get_location("Into Fire", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("War Pigs", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Take Aim", player), lambda state: state.has("Archery", player)) - set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("Arbalistic", player), lambda state: state._mc_craft_crossbow(player) and state.has("Piercing IV Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player)) - set_rule(world.get_location("Acquire Hardware", player), lambda state: state._mc_has_iron_ingots(player)) - set_rule(world.get_location("Not Quite \"Nine\" Lives", player), lambda state: state._mc_can_piglin_trade(player) and state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Cover Me With Diamonds", player), lambda state: state.has("Progressive Armor", player, 2) and state.can_reach("Diamonds!", "Location", player)) - set_rule(world.get_location("Sky's the Limit", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Hired Help", player), lambda state: state.has("Progressive Resource Crafting", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("Return to Sender", player), lambda state: True) - set_rule(world.get_location("Sweet Dreams", player), lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player)) - set_rule(world.get_location("You Need a Mint", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), lambda state: state._mc_can_respawn_ender_dragon(player) and state._mc_can_kill_ender_dragon(player) and - state._mc_can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing - set_rule(world.get_location("Enchanter", player), lambda state: state._mc_can_enchant(player)) - set_rule(world.get_location("Voluntary Exile", player), lambda state: state._mc_basic_combat(player)) - set_rule(world.get_location("Eye Spy", player), lambda state: state._mc_enter_stronghold(player)) - # set_rule(world.get_location("The End", player), lambda state: True) - set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Postmortal", player), lambda state: state._mc_complete_raid(player)) - # set_rule(world.get_location("Monster Hunter", player), lambda state: True) - set_rule(world.get_location("Adventuring Time", player), lambda state: state._mc_can_adventure(player)) - # set_rule(world.get_location("A Seedy Place", player), lambda state: True) - # set_rule(world.get_location("Those Were the Days", player), lambda state: True) - set_rule(world.get_location("Hero of the Village", player), lambda state: state._mc_complete_raid(player)) - set_rule(world.get_location("Hidden in the Depths", player), lambda state: state._mc_can_brew_potions(player) and state.has("Bed", player) and state._mc_has_diamond_pickaxe(player)) # bed mining :) - set_rule(world.get_location("Beaconator", player), lambda state: state._mc_can_kill_wither(player) and state._mc_has_diamond_pickaxe(player) and - state.has("Progressive Resource Crafting", player, 2)) - set_rule(world.get_location("Withering Heights", player), lambda state: state._mc_can_kill_wither(player)) - set_rule(world.get_location("A Balanced Diet", player), lambda state: state._mc_has_bottle(player) and state._mc_has_gold_ingots(player) and # honey bottle; gapple - state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit - set_rule(world.get_location("Subspace Bubble", player), lambda state: state._mc_has_diamond_pickaxe(player)) - # set_rule(world.get_location("Husbandry", player), lambda state: True) - set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state._mc_has_gold_ingots(player)) - set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - # set_rule(world.get_location("What a Deal!", player), lambda state: True) - set_rule(world.get_location("Uneasy Alliance", player), lambda state: state._mc_has_diamond_pickaxe(player) and state.has('Fishing Rod', player)) - set_rule(world.get_location("Diamonds!", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # set_rule(world.get_location("A Terrible Fortress", player), lambda state: True) # since you don't have to fight anything - set_rule(world.get_location("A Throwaway Joke", player), lambda state: state._mc_can_adventure(player)) # kill drowned - # set_rule(world.get_location("Minecraft", player), lambda state: True) - set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state._mc_has_bottle(player)) - set_rule(world.get_location("Ol' Betsy", player), lambda state: state._mc_craft_crossbow(player)) - set_rule(world.get_location("Cover Me in Debris", player), lambda state: state.has("Progressive Armor", player, 2) and - state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and - state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", "Location", player)) - # set_rule(world.get_location("The End?", player), lambda state: True) - # set_rule(world.get_location("The Parrots and the Bats", player), lambda state: True) - # set_rule(world.get_location("A Complete Catalogue", player), lambda state: True) # kill fish for raw - # set_rule(world.get_location("Getting Wood", player), lambda state: True) - # set_rule(world.get_location("Time to Mine!", player), lambda state: True) - set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Progressive Resource Crafting", player)) - # set_rule(world.get_location("Bake Bread", player), lambda state: True) - set_rule(world.get_location("The Lie", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player)) - set_rule(world.get_location("On a Rail", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails - # set_rule(world.get_location("Time to Strike!", player), lambda state: True) - # set_rule(world.get_location("Cow Tipper", player), lambda state: True) - set_rule(world.get_location("When Pigs Fly", player), lambda state: (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and - state.has("Saddle", player) and state.has("Fishing Rod", player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Overkill", player), lambda state: state._mc_can_brew_potions(player) and - (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))) # strength 1 + stone axe crit OR strength 2 + wood axe crit - set_rule(world.get_location("Librarian", player), lambda state: state.has("Enchanting", player)) - set_rule(world.get_location("Overpowered", player), lambda state: state._mc_has_iron_ingots(player) and - state.has('Progressive Tools', player, 2) and state._mc_basic_combat(player)) # mine gold blocks w/ iron pick - set_rule(world.get_location("Wax On", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("Wax Off", player), lambda state: state._mc_has_copper_ingots(player) and state.has('Campfire', player) and - state.has('Progressive Resource Crafting', player, 2)) - set_rule(world.get_location("The Cutest Predator", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("The Healing Power of Friendship", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Is It a Bird?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_adventure(player)) - set_rule(world.get_location("Is It a Balloon?", player), lambda state: state._mc_has_spyglass(player)) - set_rule(world.get_location("Is It a Plane?", player), lambda state: state._mc_has_spyglass(player) and state._mc_can_respawn_ender_dragon(player)) - set_rule(world.get_location("Surge Protector", player), lambda state: state.has("Channeling Book", player) and - state._mc_can_use_anvil(player) and state._mc_can_enchant(player) and state._mc_overworld_villager(player)) - set_rule(world.get_location("Light as a Rabbit", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has('Bucket', player)) - set_rule(world.get_location("Glow and Behold!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Whatever Floats Your Goat!", player), lambda state: state._mc_can_adventure(player)) - set_rule(world.get_location("Caves & Cliffs", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2)) - set_rule(world.get_location("Feels like home", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and - (state._mc_fortress_loot(player) or state._mc_complete_raid(player)) and state.has("Saddle", player)) - set_rule(world.get_location("Sound of Music", player), lambda state: state.can_reach("Diamonds!", "Location", player) and state._mc_basic_combat(player)) - set_rule(world.get_location("Star Trader", player), lambda state: state._mc_has_iron_ingots(player) and state.has('Bucket', player) and - (state.can_reach("The Nether", 'Region', player) or state.can_reach("Nether Fortress", 'Region', player) or state._mc_can_piglin_trade(player)) and # soul sand for water elevator - state._mc_overworld_villager(player)) - - # 1.19 advancements - - # can make a cake, and a noteblock, and can reach a pillager outposts for allays - set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) - # can get to outposts. - # set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True) - # craft bucket and adventure to find frog spawning biome - set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) - # I don't like this one its way to easy to get. just a pain to find. - set_rule(world.get_location("It Spreads", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - # literally just a duplicate of It spreads. - set_rule(world.get_location("Sneak 100", player), lambda state: state._mc_can_adventure(player) and state._mc_has_iron_ingots(player) and state.has("Progressive Tools", player, 2)) - set_rule(world.get_location("When the Squad Hops into Town", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) - # lead frogs to the nether and a basalt delta's biomes to find magma cubes. - set_rule(world.get_location("With Our Powers Combined!", player), lambda state: state._mc_can_adventure(player) and state.has("Lead", player)) + "Who is Cutting Onions?": lambda state: can_piglin_trade(state, player), + "Oh Shiny": lambda state: can_piglin_trade(state, player), + "Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player), + "Very Very Frightening": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "A Furious Cocktail": lambda state: (can_brew_potions(state, player) and + state.has("Fishing Rod", player) and # Water Breathing + state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets + state.can_reach("Village", "Region", player) and # Night Vision, Invisibility + state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance + "Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and + has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)), + "Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player), + "Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Local Brewery": lambda state: can_brew_potions(state, player), + "The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Fishy Business": lambda state: state.has("Fishing Rod", player), + "This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player)), + "Sniper Duel": lambda state: state.has("Archery", player), + "Great View From Up Here": lambda state: basic_combat(state, player), + "How Did We Get Here?": lambda state: (can_brew_potions(state, player) and + has_gold_ingots(state, player) and # Absorption + state.can_reach('End City', 'Region', player) and # Levitation + state.can_reach('The Nether', 'Region', player) and # potion ingredients + state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows + state.can_reach("Bring Home the Beacon", "Location", player) and # Haste + state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village + "Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and + has_iron_ingots(state, player)), + "Spooky Scary Skeleton": lambda state: basic_combat(state, player), + "Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player), + "Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player), + "Who's the Pillager Now?": lambda state: craft_crossbow(state, player), + "Getting an Upgrade": lambda state: state.has("Progressive Tools", player), + "Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player), + "Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player), + "Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player), + "Into Fire": lambda state: basic_combat(state, player), + "War Pigs": lambda state: basic_combat(state, player), + "Take Aim": lambda state: state.has("Archery", player), + "Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player), + "Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and + can_use_anvil(state, player) and can_enchant(state, player)), + "The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player), + "Acquire Hardware": lambda state: has_iron_ingots(state, player), + "Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2), + "Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)), + "Sky's the Limit": lambda state: basic_combat(state, player), + "Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player), + "Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player), + "You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player), + "Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and + can_kill_wither(state, player) and state.has("Fishing Rod", player)), + "Enchanter": lambda state: can_enchant(state, player), + "Voluntary Exile": lambda state: basic_combat(state, player), + "Eye Spy": lambda state: enter_stronghold(state, player), + "Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and + has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)), + "Postmortal": lambda state: complete_raid(state, player), + "Adventuring Time": lambda state: can_adventure(state, player), + "Hero of the Village": lambda state: complete_raid(state, player), + "Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player), + "Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and + state.has("Progressive Resource Crafting", player, 2)), + "Withering Heights": lambda state: can_kill_wither(state, player), + "A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple + state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit + "Subspace Bubble": lambda state: has_diamond_pickaxe(state, player), + "Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player), + "Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player), + "Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "A Throwaway Joke": lambda state: can_adventure(state, player), + "Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player), + "Ol' Betsy": lambda state: craft_crossbow(state, player), + "Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and + state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and + has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and + can_brew_potions(state, player) and state.has("Bed", player)), + "Hot Topic": lambda state: state.has("Progressive Resource Crafting", player), + "The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player), + "On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2), + "When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and + state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)), + "Overkill": lambda state: (can_brew_potions(state, player) and + (state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))), + "Librarian": lambda state: state.has("Enchanting", player), + "Overpowered": lambda state: (has_iron_ingots(state, player) and + state.has('Progressive Tools', player, 2) and basic_combat(state, player)), + "Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and + state.has('Progressive Resource Crafting', player, 2)), + "The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player), + "Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player), + "Is It a Balloon?": lambda state: has_spyglass(state, player), + "Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player), + "Surge Protector": lambda state: (state.has("Channeling Book", player) and + can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)), + "Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player), + "Glow and Behold!": lambda state: can_adventure(state, player), + "Whatever Floats Your Goat!": lambda state: can_adventure(state, player), + "Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2), + "Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and + (fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)), + "Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player), + "Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and + (state.can_reach("The Nether", 'Region', player) or + state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator + can_piglin_trade(state, player)) and + overworld_villager(state, player)), + "Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player), + "Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player), + "It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2), + "When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player), + "With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player), + } + } + return rules_lookup -# Sets rules on completion condition and postgame advancements -def set_completion_rules(world: MultiWorld, player: int): - def reachable_locations(state): - postgame_advancements = get_postgame_advancements(world.required_bosses[player].current_key) - return [location for location in world.get_locations() if - location.player == player and - location.name not in postgame_advancements and - location.address != None and - location.can_reach(state)] +def set_rules(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player - def defeated_required_bosses(state): - return (world.required_bosses[player].current_key not in {"ender_dragon", "both"} or state.has("Defeat Ender Dragon", player)) and \ - (world.required_bosses[player].current_key not in {"wither", "both"} or state.has("Defeat Wither", player)) + rules_lookup = get_rules_lookup(player) - # 103 total advancements. Goal is to complete X advancements and then defeat the dragon. - # There are 11 possible postgame advancements; 5 for dragon, 5 for wither, 1 shared between them - # Hence the max for completion is 92 - egg_shards = min(world.egg_shards_required[player], world.egg_shards_available[player]) - completion_requirements = lambda state: len(reachable_locations(state)) >= world.advancement_goal[player] and \ - state.has("Dragon Egg Shard", player, egg_shards) - world.completion_condition[player] = lambda state: completion_requirements(state) and defeated_required_bosses(state) - # Set rules on postgame advancements - for adv_name in get_postgame_advancements(world.required_bosses[player].current_key): - add_rule(world.get_location(adv_name, player), completion_requirements) + # Set entrance rules + for entrance_name, rule in rules_lookup["entrances"].items(): + multiworld.get_entrance(entrance_name, player).access_rule = rule + + # Set location rules + for location_name, rule in rules_lookup["locations"].items(): + multiworld.get_location(location_name, player).access_rule = rule + + # Set rules surrounding completion + bosses = multiworld.required_bosses[player] + postgame_advancements = set() + if bosses.dragon: + postgame_advancements.update(Constants.exclusion_info["ender_dragon"]) + if bosses.wither: + postgame_advancements.update(Constants.exclusion_info["wither"]) + + def location_count(state: CollectionState) -> bool: + return len([location for location in multiworld.get_locations(player) if + location.address != None and + location.can_reach(state)]) + + def defeated_bosses(state: CollectionState) -> bool: + return ((not bosses.dragon or state.has("Ender Dragon", player)) + and (not bosses.wither or state.has("Wither", player))) + + egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player]) + completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player] + and state.has("Dragon Egg Shard", player, egg_shards)) + multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state) + + # Set exclusions on hard/unreasonable/postgame + excluded_advancements = set() + if not multiworld.include_hard_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["hard"]) + if not multiworld.include_unreasonable_advancements[player]: + excluded_advancements.update(Constants.exclusion_info["unreasonable"]) + if not multiworld.include_postgame_advancements[player]: + excluded_advancements.update(postgame_advancements) + exclusion_rules(multiworld, player, excluded_advancements) diff --git a/worlds/minecraft/Structures.py b/worlds/minecraft/Structures.py new file mode 100644 index 00000000..95bafc9e --- /dev/null +++ b/worlds/minecraft/Structures.py @@ -0,0 +1,57 @@ +from worlds.AutoWorld import World + +from . import Constants + +def shuffle_structures(mc_world: World) -> None: + multiworld = mc_world.multiworld + player = mc_world.player + + default_connections = Constants.region_info["default_connections"] + illegal_connections = Constants.region_info["illegal_connections"] + + # Get all unpaired exits and all regions without entrances (except the Menu) + # This function is destructive on these lists. + exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None] + structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu'] + exits_spoiler = exits[:] # copy the original order for the spoiler log + + pairs = {} + + def set_pair(exit, struct): + if (exit in exits) and (struct in structs) and (exit not in illegal_connections.get(struct, [])): + pairs[exit] = struct + exits.remove(exit) + structs.remove(struct) + else: + raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})") + + # Connect plando structures first + if multiworld.plando_connections[player]: + for conn in multiworld.plando_connections[player]: + set_pair(conn.entrance, conn.exit) + + # The algorithm tries to place the most restrictive structures first. This algorithm always works on the + # relatively small set of restrictions here, but does not work on all possible inputs with valid configurations. + if multiworld.shuffle_structures[player]: + structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) + for struct in structs[:]: + try: + exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])]) + except IndexError: + raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})") + set_pair(exit, struct) + else: # write remaining default connections + for (exit, struct) in default_connections: + if exit in exits: + set_pair(exit, struct) + + # Make sure we actually paired everything; might fail if plando + try: + assert len(exits) == len(structs) == 0 + except AssertionError: + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})") + + for exit in exits_spoiler: + multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player)) + if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]: + multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 3d84f098..a685d1ab 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,17 +1,16 @@ import os import json from base64 import b64encode, b64decode -from math import ceil +from typing import Dict, Any -from .Items import MinecraftItem, item_table, required_items, junk_weights -from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, get_postgame_advancements -from .Regions import mc_regions, link_minecraft_structures, default_connections -from .Rules import set_advancement_rules, set_completion_rules -from worlds.generic.Rules import exclusion_rules +from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Location +from worlds.AutoWorld import World, WebWorld -from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification +from . import Constants from .Options import minecraft_options -from ..AutoWorld import World, WebWorld +from .Structures import shuffle_structures +from .ItemPool import build_item_pool, get_junk_item_names +from .Rules import set_rules client_version = 9 @@ -71,13 +70,13 @@ class MinecraftWorld(World): topology_present = True web = MinecraftWebWorld() - item_name_to_id = {name: data.code for name, data in item_table.items()} - location_name_to_id = {name: data.id for name, data in advancement_table.items()} + item_name_to_id = Constants.item_name_to_id + location_name_to_id = Constants.location_name_to_id data_version = 7 - def _get_mc_data(self): - exits = [connection[0] for connection in default_connections] + def _get_mc_data(self) -> Dict[str, Any]: + exits = [connection[0] for connection in Constants.region_info["default_connections"]] return { 'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32), 'seed_name': self.multiworld.seed_name, @@ -96,74 +95,70 @@ class MinecraftWorld(World): 'race': self.multiworld.is_race, } - def generate_basic(self): + def create_item(self, name: str) -> Item: + item_class = ItemClassification.filler + if name in Constants.item_info["progression_items"]: + item_class = ItemClassification.progression + elif name in Constants.item_info["useful_items"]: + item_class = ItemClassification.useful + elif name in Constants.item_info["trap_items"]: + item_class = ItemClassification.trap - # Generate item pool - itempool = [] - junk_pool = junk_weights.copy() - # Add all required progression items - for (name, num) in required_items.items(): - itempool += [name] * num - # Add structure compasses if desired - if self.multiworld.structure_compasses[self.player]: - structures = [connection[1] for connection in default_connections] - for struct_name in structures: - itempool.append(f"Structure Compass ({struct_name})") - # Add dragon egg shards - if self.multiworld.egg_shards_required[self.player] > 0: - itempool += ["Dragon Egg Shard"] * self.multiworld.egg_shards_available[self.player] - # Add bee traps if desired - bee_trap_quantity = ceil(self.multiworld.bee_traps[self.player] * (len(self.location_names) - len(itempool)) * 0.01) - itempool += ["Bee Trap"] * bee_trap_quantity - # Fill remaining items with randomly generated junk - itempool += self.multiworld.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), k=len(self.location_names) - len(itempool)) - # Convert itempool into real items - itempool = [item for item in map(lambda name: self.create_item(name), itempool)] + return MinecraftItem(name, item_class, self.item_name_to_id.get(name, None), self.player) - # Choose locations to automatically exclude based on settings - exclusion_pool = set() - exclusion_types = ['hard', 'unreasonable'] - for key in exclusion_types: - if not getattr(self.multiworld, f"include_{key}_advancements")[self.player]: - exclusion_pool.update(exclusion_table[key]) - # For postgame advancements, check with the boss goal - exclusion_pool.update(get_postgame_advancements(self.multiworld.required_bosses[self.player].current_key)) - exclusion_rules(self.multiworld, self.player, exclusion_pool) + def create_event(self, region_name: str, event_name: str) -> None: + region = self.multiworld.get_region(region_name, self.player) + loc = MinecraftLocation(self.player, event_name, None, region) + loc.place_locked_item(self.create_event_item(event_name)) + region.locations.append(loc) - # Prefill event locations with their events - self.multiworld.get_location("Blaze Spawner", self.player).place_locked_item(self.create_item("Blaze Rods")) - self.multiworld.get_location("Ender Dragon", self.player).place_locked_item(self.create_item("Defeat Ender Dragon")) - self.multiworld.get_location("Wither", self.player).place_locked_item(self.create_item("Defeat Wither")) + def create_event_item(self, name: str) -> None: + item = self.create_item(name) + item.classification = ItemClassification.progression + return item - self.multiworld.itempool += itempool + def create_regions(self) -> None: + # Create regions + for region_name, exits in Constants.region_info["regions"]: + r = Region(region_name, self.player, self.multiworld) + for exit_name in exits: + r.exits.append(Entrance(self.player, exit_name, r)) + self.multiworld.regions.append(r) - def get_filler_item_name(self) -> str: - return self.multiworld.random.choices(list(junk_weights.keys()), weights=list(junk_weights.values()))[0] + # Bind mandatory connections + for entr_name, region_name in Constants.region_info["mandatory_connections"]: + e = self.multiworld.get_entrance(entr_name, self.player) + r = self.multiworld.get_region(region_name, self.player) + e.connect(r) - def set_rules(self): - set_advancement_rules(self.multiworld, self.player) - set_completion_rules(self.multiworld, self.player) + # Add locations + for region_name, locations in Constants.location_info["locations_by_region"].items(): + region = self.multiworld.get_region(region_name, self.player) + for loc_name in locations: + loc = MinecraftLocation(self.player, loc_name, + self.location_name_to_id.get(loc_name, None), region) + region.locations.append(loc) - def create_regions(self): - def MCRegion(region_name: str, exits=[]): - ret = Region(region_name, self.player, self.multiworld) - ret.locations = [MinecraftAdvancement(self.player, loc_name, loc_data.id, ret) - for loc_name, loc_data in advancement_table.items() - if loc_data.region == region_name] - for exit in exits: - ret.exits.append(Entrance(self.player, exit, ret)) - return ret + # Add events + self.create_event("Nether Fortress", "Blaze Rods") + self.create_event("The End", "Ender Dragon") + self.create_event("Nether Fortress", "Wither") - self.multiworld.regions += [MCRegion(*r) for r in mc_regions] - link_minecraft_structures(self.multiworld, self.player) + # Shuffle the connections + shuffle_structures(self) - def generate_output(self, output_directory: str): + def create_items(self) -> None: + self.multiworld.itempool += build_item_pool(self) + + set_rules = set_rules + + def generate_output(self, output_directory: str) -> None: data = self._get_mc_data() filename = f"AP_{self.multiworld.get_out_file_name_base(self.player)}.apmc" with open(os.path.join(output_directory, filename), 'wb') as f: f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) - def fill_slot_data(self): + def fill_slot_data(self) -> dict: slot_data = self._get_mc_data() for option_name in minecraft_options: option = getattr(self.multiworld, option_name)[self.player] @@ -171,20 +166,16 @@ class MinecraftWorld(World): slot_data[option_name] = int(option.value) return slot_data - def create_item(self, name: str) -> Item: - item_data = item_table[name] - if name == "Bee Trap": - classification = ItemClassification.trap - # prevent books from going on excluded locations - elif name in ("Sharpness III Book", "Infinity Book", "Looting III Book"): - classification = ItemClassification.useful - elif item_data.progression: - classification = ItemClassification.progression - else: - classification = ItemClassification.filler - item = MinecraftItem(name, classification, item_data.code, self.player) + def get_filler_item_name(self) -> str: + return get_junk_item_names(self.multiworld.random, 1)[0] + + +class MinecraftLocation(Location): + game = "Minecraft" + +class MinecraftItem(Item): + game = "Minecraft" - return item def mc_update_output(raw_data, server, port): data = json.loads(b64decode(raw_data)) diff --git a/worlds/minecraft/data/excluded_locations.json b/worlds/minecraft/data/excluded_locations.json new file mode 100644 index 00000000..2f6fbbba --- /dev/null +++ b/worlds/minecraft/data/excluded_locations.json @@ -0,0 +1,40 @@ +{ + "hard": [ + "Very Very Frightening", + "A Furious Cocktail", + "Two by Two", + "Two Birds, One Arrow", + "Arbalistic", + "Monsters Hunted", + "Beaconator", + "A Balanced Diet", + "Uneasy Alliance", + "Cover Me in Debris", + "A Complete Catalogue", + "Surge Protector", + "Sound of Music", + "Star Trader", + "When the Squad Hops into Town", + "With Our Powers Combined!" + ], + "unreasonable": [ + "How Did We Get Here?", + "Adventuring Time" + ], + "ender_dragon": [ + "Free the End", + "The Next Generation", + "The End... Again...", + "You Need a Mint", + "Monsters Hunted", + "Is It a Plane?" + ], + "wither": [ + "Withering Heights", + "Bring Home the Beacon", + "Beaconator", + "A Furious Cocktail", + "How Did We Get Here?", + "Monsters Hunted" + ] +} \ No newline at end of file diff --git a/worlds/minecraft/data/items.json b/worlds/minecraft/data/items.json new file mode 100644 index 00000000..7d35d18a --- /dev/null +++ b/worlds/minecraft/data/items.json @@ -0,0 +1,128 @@ +{ + "all_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "8 Emeralds", + "4 Emeralds", + "Channeling Book", + "Silk Touch Book", + "Sharpness III Book", + "Piercing IV Book", + "Looting III Book", + "Infinity Book", + "4 Diamond Ore", + "16 Iron Ore", + "500 XP", + "100 XP", + "50 XP", + "3 Ender Pearls", + "4 Lapis Lazuli", + "16 Porkchops", + "8 Gold Ore", + "Rotten Flesh", + "Single Arrow", + "32 Arrows", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Shulker Box", + "Dragon Egg Shard", + "Spyglass", + "Lead", + "Bee Trap" + ], + "progression_items": [ + "Archery", + "Progressive Resource Crafting", + "Resource Blocks", + "Brewing", + "Enchanting", + "Bucket", + "Flint and Steel", + "Bed", + "Bottles", + "Shield", + "Fishing Rod", + "Campfire", + "Progressive Weapons", + "Progressive Tools", + "Progressive Armor", + "8 Netherite Scrap", + "Channeling Book", + "Silk Touch Book", + "Piercing IV Book", + "3 Ender Pearls", + "Saddle", + "Structure Compass (Village)", + "Structure Compass (Pillager Outpost)", + "Structure Compass (Nether Fortress)", + "Structure Compass (Bastion Remnant)", + "Structure Compass (End City)", + "Dragon Egg Shard", + "Spyglass", + "Lead" + ], + "useful_items": [ + "Sharpness III Book", + "Looting III Book", + "Infinity Book" + ], + "trap_items": [ + "Bee Trap" + ], + + "required_pool": { + "Archery": 1, + "Progressive Resource Crafting": 2, + "Brewing": 1, + "Enchanting": 1, + "Bucket": 1, + "Flint and Steel": 1, + "Bed": 1, + "Bottles": 1, + "Shield": 1, + "Fishing Rod": 1, + "Campfire": 1, + "Progressive Weapons": 3, + "Progressive Tools": 3, + "Progressive Armor": 2, + "8 Netherite Scrap": 2, + "Channeling Book": 1, + "Silk Touch Book": 1, + "Sharpness III Book": 1, + "Piercing IV Book": 1, + "Looting III Book": 1, + "Infinity Book": 1, + "3 Ender Pearls": 4, + "Saddle": 1, + "Spyglass": 1, + "Lead": 1 + }, + "junk_weights": { + "4 Emeralds": 2, + "4 Diamond Ore": 1, + "16 Iron Ore": 1, + "50 XP": 4, + "16 Porkchops": 2, + "8 Gold Ore": 1, + "Rotten Flesh": 1, + "32 Arrows": 1 + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/locations.json b/worlds/minecraft/data/locations.json new file mode 100644 index 00000000..7cd00e58 --- /dev/null +++ b/worlds/minecraft/data/locations.json @@ -0,0 +1,250 @@ +{ + "all_locations": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Free the End", + "A Furious Cocktail", + "Best Friends Forever", + "Bring Home the Beacon", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Local Brewery", + "The Next Generation", + "Fishy Business", + "Hot Tourist Destinations", + "This Boat Has Legs", + "Sniper Duel", + "Nether", + "Great View From Up Here", + "How Did We Get Here?", + "Bullseye", + "Spooky Scary Skeleton", + "Two by Two", + "Stone Age", + "Two Birds, One Arrow", + "We Need to Go Deeper", + "Who's the Pillager Now?", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "The City at the End of the Game", + "Ice Bucket Challenge", + "Remote Getaway", + "Into Fire", + "War Pigs", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "The End... Again...", + "Acquire Hardware", + "Not Quite \"Nine\" Lives", + "Cover Me With Diamonds", + "Sky's the Limit", + "Hired Help", + "Return to Sender", + "Sweet Dreams", + "You Need a Mint", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Voluntary Exile", + "Eye Spy", + "The End", + "Serious Dedication", + "Postmortal", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Those Were the Days", + "Hero of the Village", + "Hidden in the Depths", + "Beaconator", + "Withering Heights", + "A Balanced Diet", + "Subspace Bubble", + "Husbandry", + "Country Lode, Take Me Home", + "Bee Our Guest", + "What a Deal!", + "Uneasy Alliance", + "Diamonds!", + "A Terrible Fortress", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "Cover Me in Debris", + "The End?", + "The Parrots and the Bats", + "A Complete Catalogue", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Overkill", + "Librarian", + "Overpowered", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Is It a Balloon?", + "Is It a Plane?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Feels like home", + "Sound of Music", + "Star Trader", + "Birthday Song", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town", + "With Our Powers Combined!", + "You've Got a Friend in Me" + ], + "locations_by_region": { + "Overworld": [ + "Who is Cutting Onions?", + "Oh Shiny", + "Suit Up", + "Very Very Frightening", + "Hot Stuff", + "Best Friends Forever", + "Not Today, Thank You", + "Isn't It Iron Pick", + "Fishy Business", + "Sniper Duel", + "Bullseye", + "Stone Age", + "Two Birds, One Arrow", + "Getting an Upgrade", + "Tactical Fishing", + "Zombie Doctor", + "Ice Bucket Challenge", + "Take Aim", + "Total Beelocation", + "Arbalistic", + "Acquire Hardware", + "Cover Me With Diamonds", + "Hired Help", + "Sweet Dreams", + "Adventure", + "Monsters Hunted", + "Enchanter", + "Eye Spy", + "Monster Hunter", + "Adventuring Time", + "A Seedy Place", + "Husbandry", + "Bee Our Guest", + "Diamonds!", + "A Throwaway Joke", + "Minecraft", + "Sticky Situation", + "Ol' Betsy", + "The Parrots and the Bats", + "Getting Wood", + "Time to Mine!", + "Hot Topic", + "Bake Bread", + "The Lie", + "On a Rail", + "Time to Strike!", + "Cow Tipper", + "When Pigs Fly", + "Librarian", + "Wax On", + "Wax Off", + "The Cutest Predator", + "The Healing Power of Friendship", + "Is It a Bird?", + "Surge Protector", + "Light as a Rabbit", + "Glow and Behold!", + "Whatever Floats Your Goat!", + "Caves & Cliffs", + "Sound of Music", + "Bukkit Bukkit", + "It Spreads", + "Sneak 100", + "When the Squad Hops into Town" + ], + "The Nether": [ + "Hot Tourist Destinations", + "This Boat Has Legs", + "Nether", + "Two by Two", + "We Need to Go Deeper", + "Not Quite \"Nine\" Lives", + "Return to Sender", + "Serious Dedication", + "Hidden in the Depths", + "Subspace Bubble", + "Country Lode, Take Me Home", + "Uneasy Alliance", + "Cover Me in Debris", + "Is It a Balloon?", + "Feels like home", + "With Our Powers Combined!" + ], + "The End": [ + "Free the End", + "The Next Generation", + "Remote Getaway", + "The End... Again...", + "You Need a Mint", + "The End", + "The End?", + "Is It a Plane?" + ], + "Village": [ + "Postmortal", + "Hero of the Village", + "A Balanced Diet", + "What a Deal!", + "A Complete Catalogue", + "Star Trader" + ], + "Nether Fortress": [ + "A Furious Cocktail", + "Bring Home the Beacon", + "Local Brewery", + "How Did We Get Here?", + "Spooky Scary Skeleton", + "Into Fire", + "Beaconator", + "Withering Heights", + "A Terrible Fortress", + "Overkill" + ], + "Pillager Outpost": [ + "Who's the Pillager Now?", + "Voluntary Exile", + "Birthday Song", + "You've Got a Friend in Me" + ], + "Bastion Remnant": [ + "War Pigs", + "Those Were the Days", + "Overpowered" + ], + "End City": [ + "Great View From Up Here", + "The City at the End of the Game", + "Sky's the Limit" + ] + } +} \ No newline at end of file diff --git a/worlds/minecraft/data/regions.json b/worlds/minecraft/data/regions.json new file mode 100644 index 00000000..c9e51e48 --- /dev/null +++ b/worlds/minecraft/data/regions.json @@ -0,0 +1,28 @@ +{ + "regions": [ + ["Menu", ["New World"]], + ["Overworld", ["Nether Portal", "End Portal", "Overworld Structure 1", "Overworld Structure 2"]], + ["The Nether", ["Nether Structure 1", "Nether Structure 2"]], + ["The End", ["The End Structure"]], + ["Village", []], + ["Pillager Outpost", []], + ["Nether Fortress", []], + ["Bastion Remnant", []], + ["End City", []] + ], + "mandatory_connections": [ + ["New World", "Overworld"], + ["Nether Portal", "The Nether"], + ["End Portal", "The End"] + ], + "default_connections": [ + ["Overworld Structure 1", "Village"], + ["Overworld Structure 2", "Pillager Outpost"], + ["Nether Structure 1", "Nether Fortress"], + ["Nether Structure 2", "Bastion Remnant"], + ["The End Structure", "End City"] + ], + "illegal_connections": { + "Nether Fortress": ["The End Structure"] + } +} \ No newline at end of file diff --git a/worlds/minecraft/test/TestAdvancements.py b/worlds/minecraft/test/TestAdvancements.py index 5fc64f76..321aef1a 100644 --- a/worlds/minecraft/test/TestAdvancements.py +++ b/worlds/minecraft/test/TestAdvancements.py @@ -1,10 +1,14 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase # Format: # [location, expected_result, given_items, [excluded_items]] # Every advancement has its own test, named by its internal ID number. -class TestAdvancements(TestMinecraft): +class TestAdvancements(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def test_42000(self): self.run_location_tests([ @@ -1278,3 +1282,129 @@ class TestAdvancements(TestMinecraft): ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]], ["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]], ]) + + # bucket, iron pick + def test_42103(self): + self.run_location_tests([ + ["Caves & Cliffs", False, []], + ["Caves & Cliffs", False, [], ["Bucket"]], + ["Caves & Cliffs", False, [], ["Progressive Tools"]], + ["Caves & Cliffs", False, [], ["Progressive Resource Crafting"]], + ["Caves & Cliffs", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Bucket"]], + ]) + + # bucket, fishing rod, saddle, combat + def test_42104(self): + self.run_location_tests([ + ["Feels like home", False, []], + ["Feels like home", False, [], ['Progressive Resource Crafting']], + ["Feels like home", False, [], ['Progressive Tools']], + ["Feels like home", False, [], ['Progressive Weapons']], + ["Feels like home", False, [], ['Progressive Armor', 'Shield']], + ["Feels like home", False, [], ['Fishing Rod']], + ["Feels like home", False, [], ['Saddle']], + ["Feels like home", False, [], ['Bucket']], + ["Feels like home", False, [], ['Flint and Steel']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor', 'Fishing Rod']], + ["Feels like home", True, ['Saddle', 'Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield', 'Fishing Rod']], + ]) + + # iron pick, combat + def test_42105(self): + self.run_location_tests([ + ["Sound of Music", False, []], + ["Sound of Music", False, [], ["Progressive Tools"]], + ["Sound of Music", False, [], ["Progressive Resource Crafting"]], + ["Sound of Music", False, [], ["Progressive Weapons"]], + ["Sound of Music", False, [], ["Progressive Armor", "Shield"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Progressive Armor"]], + ["Sound of Music", True, ["Progressive Tools", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons", "Shield"]], + ]) + + # bucket, nether, villager + def test_42106(self): + self.run_location_tests([ + ["Star Trader", False, []], + ["Star Trader", False, [], ["Bucket"]], + ["Star Trader", False, [], ["Flint and Steel"]], + ["Star Trader", False, [], ["Progressive Tools"]], + ["Star Trader", False, [], ["Progressive Resource Crafting"]], + ["Star Trader", False, [], ["Progressive Weapons"]], + ["Star Trader", True, ["Bucket", "Flint and Steel", "Progressive Tools", "Progressive Resource Crafting", "Progressive Weapons"]], + ]) + + # bucket, redstone -> iron pick, pillager outpost -> adventure + def test_42107(self): + self.run_location_tests([ + ["Birthday Song", False, []], + ["Birthday Song", False, [], ["Bucket"]], + ["Birthday Song", False, [], ["Progressive Tools"]], + ["Birthday Song", False, [], ["Progressive Weapons"]], + ["Birthday Song", False, [], ["Progressive Resource Crafting"]], + ["Birthday Song", True, ["Progressive Resource Crafting", "Progressive Tools", "Progressive Tools", "Progressive Weapons", "Bucket"]], + ]) + + # bucket, adventure + def test_42108(self): + self.run_location_tests([ + ["Bukkit Bukkit", False, []], + ["Bukkit Bukkit", False, [], ["Bucket"]], + ["Bukkit Bukkit", False, [], ["Progressive Tools"]], + ["Bukkit Bukkit", False, [], ["Progressive Weapons"]], + ["Bukkit Bukkit", False, [], ["Progressive Resource Crafting"]], + ["Bukkit Bukkit", True, ["Bucket", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42109(self): + self.run_location_tests([ + ["It Spreads", False, []], + ["It Spreads", False, [], ["Progressive Tools"]], + ["It Spreads", False, [], ["Progressive Weapons"]], + ["It Spreads", False, [], ["Progressive Resource Crafting"]], + ["It Spreads", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # iron pick, adventure + def test_42110(self): + self.run_location_tests([ + ["Sneak 100", False, []], + ["Sneak 100", False, [], ["Progressive Tools"]], + ["Sneak 100", False, [], ["Progressive Weapons"]], + ["Sneak 100", False, [], ["Progressive Resource Crafting"]], + ["Sneak 100", True, ["Progressive Tools", "Progressive Tools", "Progressive Weapons", "Progressive Resource Crafting"]], + ]) + + # adventure, lead + def test_42111(self): + self.run_location_tests([ + ["When the Squad Hops into Town", False, []], + ["When the Squad Hops into Town", False, [], ["Progressive Weapons"]], + ["When the Squad Hops into Town", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["When the Squad Hops into Town", False, [], ["Lead"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Campfire"]], + ["When the Squad Hops into Town", True, ["Progressive Weapons", "Lead", "Progressive Resource Crafting"]], + ]) + + # adventure, lead, nether + def test_42112(self): + self.run_location_tests([ + ["With Our Powers Combined!", False, []], + ["With Our Powers Combined!", False, [], ["Lead"]], + ["With Our Powers Combined!", False, [], ["Bucket", "Progressive Tools"]], + ["With Our Powers Combined!", False, [], ["Flint and Steel"]], + ["With Our Powers Combined!", False, [], ["Progressive Weapons"]], + ["With Our Powers Combined!", False, [], ["Progressive Resource Crafting"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Bucket"]], + ["With Our Powers Combined!", True, ["Lead", "Progressive Weapons", "Progressive Resource Crafting", "Flint and Steel", "Progressive Tools", "Progressive Tools", "Progressive Tools"]], + ]) + + # pillager outpost -> adventure + def test_42113(self): + self.run_location_tests([ + ["You've Got a Friend in Me", False, []], + ["You've Got a Friend in Me", False, [], ["Progressive Weapons"]], + ["You've Got a Friend in Me", False, [], ["Campfire", "Progressive Resource Crafting"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Campfire"]], + ["You've Got a Friend in Me", True, ["Progressive Weapons", "Progressive Resource Crafting"]], + ]) diff --git a/worlds/minecraft/test/TestDataLoad.py b/worlds/minecraft/test/TestDataLoad.py new file mode 100644 index 00000000..c14eef07 --- /dev/null +++ b/worlds/minecraft/test/TestDataLoad.py @@ -0,0 +1,60 @@ +import unittest + +from .. import Constants + +class TestDataLoad(unittest.TestCase): + + def test_item_data(self): + item_info = Constants.item_info + + # All items in sub-tables are in all_items + all_items: set = set(item_info['all_items']) + assert set(item_info['progression_items']) <= all_items + assert set(item_info['useful_items']) <= all_items + assert set(item_info['trap_items']) <= all_items + assert set(item_info['required_pool'].keys()) <= all_items + assert set(item_info['junk_weights'].keys()) <= all_items + + # No overlapping ids (because of bee trap stuff) + all_ids: set = set(Constants.item_name_to_id.values()) + assert len(all_items) == len(all_ids) + + def test_location_data(self): + location_info = Constants.location_info + exclusion_info = Constants.exclusion_info + + # Every location has a region and every region's locations are in all_locations + all_locations: set = set(location_info['all_locations']) + all_locs_2: set = set() + for v in location_info['locations_by_region'].values(): + all_locs_2.update(v) + assert all_locations == all_locs_2 + + # All exclusions are locations + for v in exclusion_info.values(): + assert set(v) <= all_locations + + def test_region_data(self): + region_info = Constants.region_info + + # Every entrance and region in mandatory/default/illegal connections is a real entrance and region + all_regions = set() + all_entrances = set() + for v in region_info['regions']: + assert isinstance(v[0], str) + assert isinstance(v[1], list) + all_regions.add(v[0]) + all_entrances.update(v[1]) + + for v in region_info['mandatory_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for v in region_info['default_connections']: + assert v[0] in all_entrances + assert v[1] in all_regions + + for k, v in region_info['illegal_connections'].items(): + assert k in all_regions + assert set(v) <= all_entrances + diff --git a/worlds/minecraft/test/TestEntrances.py b/worlds/minecraft/test/TestEntrances.py index 8e80a135..946eb23d 100644 --- a/worlds/minecraft/test/TestEntrances.py +++ b/worlds/minecraft/test/TestEntrances.py @@ -1,7 +1,11 @@ -from .TestMinecraft import TestMinecraft +from . import MCTestBase -class TestEntrances(TestMinecraft): +class TestEntrances(MCTestBase): + options = { + "shuffle_structures": False, + "structure_compasses": False + } def testPortals(self): self.run_entrance_tests([ diff --git a/worlds/minecraft/test/TestMinecraft.py b/worlds/minecraft/test/TestMinecraft.py deleted file mode 100644 index dc5c81c0..00000000 --- a/worlds/minecraft/test/TestMinecraft.py +++ /dev/null @@ -1,68 +0,0 @@ -from test.TestBase import TestBase -from BaseClasses import MultiWorld, ItemClassification -from worlds import AutoWorld -from worlds.minecraft import MinecraftWorld -from worlds.minecraft.Items import MinecraftItem, item_table -from Options import Toggle -from worlds.minecraft.Options import AdvancementGoal, EggShardsRequired, EggShardsAvailable, BossGoal, BeeTraps, \ - ShuffleStructures, CombatDifficulty - - -# Converts the name of an item into an item object -def MCItemFactory(items, player: int): - ret = [] - singleton = False - if isinstance(items, str): - items = [items] - singleton = True - for item in items: - if item in item_table: - ret.append(MinecraftItem( - item, ItemClassification.progression if item_table[item].progression else ItemClassification.filler, - item_table[item].code, player - )) - else: - raise Exception(f"Unknown item {item}") - - if singleton: - return ret[0] - return ret - - -class TestMinecraft(TestBase): - - def setUp(self): - self.multiworld = MultiWorld(1) - self.multiworld.game[1] = "Minecraft" - self.multiworld.worlds[1] = MinecraftWorld(self.multiworld, 1) - exclusion_pools = ['hard', 'unreasonable', 'postgame'] - for pool in exclusion_pools: - setattr(self.multiworld, f"include_{pool}_advancements", {1: False}) - setattr(self.multiworld, "advancement_goal", {1: AdvancementGoal(30)}) - setattr(self.multiworld, "egg_shards_required", {1: EggShardsRequired(0)}) - setattr(self.multiworld, "egg_shards_available", {1: EggShardsAvailable(0)}) - setattr(self.multiworld, "required_bosses", {1: BossGoal(1)}) # ender dragon - setattr(self.multiworld, "shuffle_structures", {1: ShuffleStructures(False)}) - setattr(self.multiworld, "bee_traps", {1: BeeTraps(0)}) - setattr(self.multiworld, "combat_difficulty", {1: CombatDifficulty(1)}) # normal - setattr(self.multiworld, "structure_compasses", {1: Toggle(False)}) - setattr(self.multiworld, "death_link", {1: Toggle(False)}) - AutoWorld.call_single(self.multiworld, "create_regions", 1) - AutoWorld.call_single(self.multiworld, "generate_basic", 1) - AutoWorld.call_single(self.multiworld, "set_rules", 1) - - def _get_items(self, item_pool, all_except): - if all_except and len(all_except) > 0: - items = self.multiworld.itempool[:] - items = [item for item in items if - item.name not in all_except and not ("Bottle" in item.name and "AnyBottle" in all_except)] - items.extend(MCItemFactory(item_pool[0], 1)) - else: - items = MCItemFactory(item_pool[0], 1) - return self.get_state(items) - - def _get_items_partial(self, item_pool, missing_item): - new_items = item_pool[0].copy() - new_items.remove(missing_item) - items = MCItemFactory(new_items, 1) - return self.get_state(items) diff --git a/worlds/minecraft/test/TestOptions.py b/worlds/minecraft/test/TestOptions.py new file mode 100644 index 00000000..668ed500 --- /dev/null +++ b/worlds/minecraft/test/TestOptions.py @@ -0,0 +1,49 @@ +from . import MCTestBase +from ..Constants import region_info +from ..Options import minecraft_options + +from BaseClasses import ItemClassification + +class AdvancementTestBase(MCTestBase): + options = { + "advancement_goal": minecraft_options["advancement_goal"].range_end + } + # beatability test implicit + +class ShardTestBase(MCTestBase): + options = { + "egg_shards_required": minecraft_options["egg_shards_required"].range_end, + "egg_shards_available": minecraft_options["egg_shards_available"].range_end + } + + # check that itempool is not overfilled with shards + def test_itempool(self): + assert len(self.multiworld.get_unfilled_locations()) == len(self.multiworld.itempool) + +class CompassTestBase(MCTestBase): + def test_compasses_in_pool(self): + structures = [x[1] for x in region_info["default_connections"]] + itempool_str = {item.name for item in self.multiworld.itempool} + for struct in structures: + assert f"Structure Compass ({struct})" in itempool_str + +class NoBeeTestBase(MCTestBase): + options = { + "bee_traps": 0 + } + + # With no bees, there are no traps in the pool + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.trap + + +class AllBeeTestBase(MCTestBase): + options = { + "bee_traps": 100 + } + + # With max bees, there are no filler items, only bee traps + def test_bees(self): + for item in self.multiworld.itempool: + assert item.classification != ItemClassification.filler diff --git a/worlds/minecraft/test/__init__.py b/worlds/minecraft/test/__init__.py index e69de29b..acf9b794 100644 --- a/worlds/minecraft/test/__init__.py +++ b/worlds/minecraft/test/__init__.py @@ -0,0 +1,33 @@ +from test.TestBase import TestBase, WorldTestBase +from .. import MinecraftWorld + + +class MCTestBase(WorldTestBase, TestBase): + game = "Minecraft" + player: int = 1 + + def _create_items(self, items, player): + singleton = False + if isinstance(items, str): + items = [items] + singleton = True + ret = [self.multiworld.worlds[player].create_item(item) for item in items] + if singleton: + return ret[0] + return ret + + def _get_items(self, item_pool, all_except): + if all_except and len(all_except) > 0: + items = self.multiworld.itempool[:] + items = [item for item in items if item.name not in all_except] + items.extend(self._create_items(item_pool[0], 1)) + else: + items = self._create_items(item_pool[0], 1) + return self.get_state(items) + + def _get_items_partial(self, item_pool, missing_item): + new_items = item_pool[0].copy() + new_items.remove(missing_item) + items = self._create_items(new_items, 1) + return self.get_state(items) +