diff --git a/BaseClasses.py b/BaseClasses.py index 13e7c508..8a851fa5 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -894,13 +894,15 @@ class CollectionState(object): return self.fortress_loot(player) and normal_kill def can_kill_ender_dragon(self, player: int): + # Since it is possible to kill the dragon without getting any of the advancements related to it, we need to require that it can be respawned. + respawn_dragon = self.can_reach('The Nether', 'Region', player) and self.has('Ingot Crafting', player) if self.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.can_brew_potions(player) and self.can_enchant(player) + return respawn_dragon and self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and \ + self.has('Archery', player) and self.can_brew_potions(player) and self.can_enchant(player) if self.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) + return respawn_dragon and ((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 respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) def collect(self, item: Item, event: bool = False, location: Location = None) -> bool: diff --git a/Main.py b/Main.py index 20a7b055..0afcd26b 100644 --- a/Main.py +++ b/Main.py @@ -21,8 +21,6 @@ from Fill import distribute_items_restrictive, flood_items, balance_multiworld_p from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes from Utils import output_path, parse_player_names, get_options, __version__, version_tuple -from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data -from worlds.minecraft.Regions import minecraft_create_regions from worlds.generic.Rules import locality_rules from worlds import Games, lookup_any_item_name_to_id, AutoWorld import Patch @@ -196,9 +194,6 @@ def main(args, seed=None): AutoWorld.call_all(world, "create_regions") - for player in world.minecraft_player_ids: - minecraft_create_regions(world, player) - for player in world.alttp_player_ids: if world.open_pyramid[player] == 'goal': world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', @@ -260,9 +255,6 @@ def main(args, seed=None): AutoWorld.call_all(world, "generate_basic") - for player in world.minecraft_player_ids: - gen_minecraft(world, player) - logger.info("Running Item Plando") for item in world.itempool: @@ -511,7 +503,7 @@ def main(args, seed=None): for slot in world.hk_player_ids: slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot) for slot in world.minecraft_player_ids: - slot_data[slot] = fill_minecraft_slot_data(world, slot) + slot_data[slot] = AutoWorld.call_single(world, "fill_slot_data", slot) locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids} for location in world.get_filled_locations(): @@ -563,8 +555,6 @@ def main(args, seed=None): if multidata_task: multidata_task.result() # retrieve exception if one exists pool.shutdown() # wait for all queued tasks to complete - for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error - generate_mc_data(world, player) if not args.skip_playthrough: logger.info('Calculating playthrough.') create_playthrough(world) diff --git a/Options.py b/Options.py index 9a8d59e7..904d5125 100644 --- a/Options.py +++ b/Options.py @@ -175,6 +175,9 @@ class Range(Option, int): return cls(data) return cls.from_text(str(data)) + def get_option_name(self): + return str(self.value) + def __str__(self): return str(self.value) diff --git a/playerSettings.yaml b/playerSettings.yaml index e46428d4..398151d0 100644 --- a/playerSettings.yaml +++ b/playerSettings.yaml @@ -102,10 +102,7 @@ Factorio: burner-mining-drill: 19 stone-furnace: 19 Minecraft: - advancement_goal: # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game. - few: 0 # 30 advancements - normal: 1 # 50 - many: 0 # 70 + advancement_goal: 50 # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game. combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses. easy: 0 normal: 1 @@ -119,7 +116,7 @@ Minecraft: include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items. on: 0 off: 1 - shuffle_structures: # CURRENTLY DISABLED; enables shuffling of villages, outposts, fortresses, bastions, and end cities. + shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities. on: 0 off: 1 A Link to the Past: diff --git a/test/minecraft/TestMinecraft.py b/test/minecraft/TestMinecraft.py index 56ec7306..0ff4bf3c 100644 --- a/test/minecraft/TestMinecraft.py +++ b/test/minecraft/TestMinecraft.py @@ -1,11 +1,10 @@ import worlds.minecraft.Options from test.TestBase import TestBase from BaseClasses import MultiWorld -from worlds.minecraft import minecraft_gen_item_pool -from worlds.minecraft.Regions import minecraft_create_regions, link_minecraft_structures -from worlds.minecraft.Rules import set_rules +from worlds import AutoWorld +from worlds.minecraft import MinecraftWorld from worlds.minecraft.Items import MinecraftItem, item_table -import Options +from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty # Converts the name of an item into an item object def MCItemFactory(items, player: int): @@ -29,16 +28,16 @@ class TestMinecraft(TestBase): def setUp(self): self.world = MultiWorld(1) self.world.game[1] = "Minecraft" + self.world.worlds[1] = MinecraftWorld(self.world, 1) exclusion_pools = ['hard', 'insane', 'postgame'] for pool in exclusion_pools: setattr(self.world, f"include_{pool}_advancements", [False, False]) - setattr(self.world, "advancement_goal", [0, worlds.minecraft.Options.AdvancementGoal(value=0)]) - setattr(self.world, "shuffle_structures", [False, False]) - setattr(self.world, "combat_difficulty", [0, worlds.minecraft.Options.CombatDifficulty(value=1)]) - minecraft_create_regions(self.world, 1) - link_minecraft_structures(self.world, 1) - minecraft_gen_item_pool(self.world, 1) - set_rules(self.world, 1) + setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)}) + setattr(self.world, "shuffle_structures", {1: False}) + setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal + AutoWorld.call_single(self.world, "create_regions", 1) + AutoWorld.call_single(self.world, "generate_basic", 1) + AutoWorld.call_single(self.world, "set_rules", 1) def _get_items(self, item_pool, all_except): if all_except and len(all_except) > 0: diff --git a/worlds/__init__.py b/worlds/__init__.py index db5cc960..fcd9066d 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -32,7 +32,7 @@ assert len(lookup_any_location_name_to_id) == len(lookup_any_location_id_to_name network_data_package = {"lookup_any_location_id_to_name": lookup_any_location_id_to_name, "lookup_any_item_id_to_name": lookup_any_item_id_to_name, - "version": 6} + "version": 7} @enum.unique diff --git a/worlds/minecraft/Items.py b/worlds/minecraft/Items.py index 89de300e..d6f5e2f0 100644 --- a/worlds/minecraft/Items.py +++ b/worlds/minecraft/Items.py @@ -1,4 +1,4 @@ -from BaseClasses import Region, Entrance, Location, MultiWorld, Item +from BaseClasses import Item import typing class ItemData(typing.NamedTuple): @@ -46,6 +46,7 @@ item_table = { "8 Gold Ore": ItemData(45032, False), "Rotten Flesh": ItemData(45033, False), "Single Arrow": ItemData(45034, False), + "Bee Trap (Minecraft)": ItemData(45100, False), "Victory": ItemData(0, True) } @@ -67,8 +68,9 @@ item_frequencies = { "4 Lapis Lazuli": 2, "16 Porkchops": 8, "8 Gold Ore": 4, - "Rotten Flesh": 4, - "Single Arrow": 0 + "Rotten Flesh": 2, + "Single Arrow": 0, + "Bee Trap (Minecraft)": 2 } 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 index aff4c403..4ca20092 100644 --- a/worlds/minecraft/Locations.py +++ b/worlds/minecraft/Locations.py @@ -1,4 +1,4 @@ -from BaseClasses import Region, Entrance, Location, MultiWorld, Item +from BaseClasses import Location import typing class AdvData(typing.NamedTuple): @@ -114,6 +114,7 @@ exclusion_table = { "Two by Two": "100 XP", "Two Birds, One Arrow": "50 XP", "Arbalistic": "100 XP", + "Monsters Hunted": "100 XP", "Beaconator": "50 XP", "A Balanced Diet": "100 XP", "Uneasy Alliance": "100 XP", diff --git a/worlds/minecraft/Options.py b/worlds/minecraft/Options.py index 8dd6ba2d..a61e11a5 100644 --- a/worlds/minecraft/Options.py +++ b/worlds/minecraft/Options.py @@ -1,13 +1,11 @@ import typing - -from Options import Choice, Option, Toggle +from Options import Choice, Option, Toggle, Range -class AdvancementGoal(Choice): - option_few = 0 - option_normal = 1 - option_many = 2 - default = 1 +class AdvancementGoal(Range): + range_start = 0 + range_end = 87 + default = 50 class CombatDifficulty(Choice): diff --git a/worlds/minecraft/Regions.py b/worlds/minecraft/Regions.py index 2475e038..c35d2b7c 100644 --- a/worlds/minecraft/Regions.py +++ b/worlds/minecraft/Regions.py @@ -1,73 +1,44 @@ -from .Locations import MinecraftAdvancement, advancement_table -from BaseClasses import Region, Entrance, Location, MultiWorld, Item - -def minecraft_create_regions(world: MultiWorld, player: int): - - def MCRegion(region_name: str, exits=[]): - ret = Region(region_name, None, region_name, player) - ret.world = world - ret.locations = [ MinecraftAdvancement(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(player, exit, ret)) - return ret - - world.regions += [MCRegion(*r) for r in mc_regions] +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)) -def link_minecraft_structures(world: MultiWorld, player: int): - # 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}") from e + raise Exception(f"Could not obtain equal numbers of Minecraft exits and structures for player {player} ({world.player_names[player]})") num_regions = len(exits) pairs = {} - def check_valid_connection(exit, struct): - if (exit in exits) and (struct in structs) and (exit not in pairs): - return True - return False - def set_pair(exit, struct): - try: - assert exit in exits - assert struct in structs - except AssertionError as e: - raise Exception(f"Invalid connection: {exit} => {struct} for player {player}") - pairs[exit] = struct - exits.remove(exit) - structs.remove(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_names[player]})") - # Plando stuff. Remove any utilized exits/structs from the lists. - # Raise error if trying to put Nether Fortress in the End. + # Connect plando structures first if world.plando_connections[player]: - for connection in world.plando_connections[player]: - try: - if connection.entrance == 'The End Structure' and connection.exit == 'Nether Fortress': - raise Exception(f"Cannot place Nether Fortress in the End for player {player}") - set_pair(connection.entrance, connection.exit) - except Exception as e: - raise Exception(f"Could not connect using {connection}") from e + 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]: - # Can't put Nether Fortress in the End - if 'The End Structure' in exits and 'Nether Fortress' in structs: + structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, []))) + for struct in structs[:]: try: - end_struct = world.random.choice([s for s in structs if s != 'Nether Fortress']) - set_pair('The End Structure', end_struct) - except IndexError as e: # should only happen if structs is emptied by plando - raise Exception(f"Plando forced Nether Fortress in the End for player {player}") from e - world.random.shuffle(structs) - for exit, struct in zip(exits[:], structs[:]): + 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_names[player]})") set_pair(exit, struct) else: # write remaining default connections for (exit, struct) in default_connections: @@ -77,13 +48,15 @@ def link_minecraft_structures(world: MultiWorld, player: int): # Make sure we actually paired everything; might fail if plando try: assert len(exits) == len(structs) == 0 - except AssertionError as e: - raise Exception(f"Failed to connect all Minecraft structures for player {player}; check plando settings in yaml") from e + except AssertionError: + raise Exception(f"Failed to connect all Minecraft structures for player {player} ({world.player_names[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) + - for exit, struct in pairs.items(): - world.get_entrance(exit, player).connect(world.get_region(struct, player)) - if world.shuffle_structures[player]: - world.spoiler.set_entrance(exit, struct, 'entrance', player) # (Region name, list of exits) mc_regions = [ @@ -112,3 +85,9 @@ default_connections = { ('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 d986844f..ca7aced9 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -14,26 +14,19 @@ def set_rules(world: MultiWorld, player: int): (location.name not in postgame_advancements) and location.can_reach(state)] - # 92 total advancements, 16 are typically excluded, 1 is Free the End. Goal is to complete X advancements and then Free the End. - goal_map = { - 'few': 30, - 'normal': 50, - 'many': 70 - } - goal = goal_map[getattr(world, 'advancement_goal')[player].get_option_name()] - can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', - player) and state.can_kill_ender_dragon( - player) + # 92 total advancements. Goal is to complete X advancements and then Free the End. + # There are 5 advancements which cannot be included for dragon spawning (4 postgame, Free the End) + # Hence the true maximum is (92 - 5) = 87 + goal = int(world.advancement_goal[player].value) + can_complete = lambda state: len(reachable_locations(state)) >= goal and state.can_reach('The End', 'Region', player) and state.can_kill_ender_dragon(player) if world.logic[player] != 'nologic': world.completion_condition[player] = lambda state: state.has('Victory', player) - 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.has_iron_ingots(player)) - set_rule(world.get_entrance("End Portal", player), - lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) + 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.has_iron_ingots(player)) + set_rule(world.get_entrance("End Portal", player), lambda state: state.enter_stronghold(player) and state.has('3 Ender Pearls', player, 4)) set_rule(world.get_entrance("Overworld Structure 1", player), lambda state: state.can_adventure(player)) set_rule(world.get_entrance("Overworld Structure 2", player), lambda state: state.can_adventure(player)) set_rule(world.get_entrance("Nether Structure 1", player), lambda state: state.can_adventure(player)) @@ -44,158 +37,98 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Who is Cutting Onions?", player), lambda state: state.can_piglin_trade(player)) set_rule(world.get_location("Oh Shiny", player), lambda state: state.can_piglin_trade(player)) - set_rule(world.get_location("Suit Up", player), - lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Very Very Frightening", player), - lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant( - player) and \ - ((world.get_region('Village', player).entrances[ - 0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', - player)) or state.can_reach( - 'Zombie Doctor', 'Location', - player))) # need villager into the overworld for lightning strike - set_rule(world.get_location("Hot Stuff", player), - lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) - set_rule(world.get_location("Free the End", player), - lambda state: can_complete(state) and state.has('Ingot Crafting', player) and state.can_reach('The Nether', - 'Region', - player)) - set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.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("Suit Up", player), lambda state: state.has("Progressive Armor", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Very Very Frightening", player), lambda state: state.has("Channeling Book", player) and state.can_use_anvil(player) and state.can_enchant(player) and \ + ((world.get_region('Village', player).entrances[0].parent_region.name != 'The End' and state.can_reach('Village', 'Region', player)) or state.can_reach('Zombie Doctor', 'Location', player))) # need villager into the overworld for lightning strike + set_rule(world.get_location("Hot Stuff", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Free the End", player), lambda state: can_complete(state)) + set_rule(world.get_location("A Furious Cocktail", player), lambda state: state.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.can_kill_wither(player) and state.has_diamond_pickaxe(player) and - state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) - set_rule(world.get_location("Not Today, Thank You", player), - lambda state: state.has("Shield", player) and state.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.has_iron_ingots(player)) + set_rule(world.get_location("Bring Home the Beacon", player), lambda state: state.can_kill_wither(player) and + state.has_diamond_pickaxe(player) and state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) + set_rule(world.get_location("Not Today, Thank You", player), lambda state: state.has("Shield", player) and state.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.has_iron_ingots(player)) set_rule(world.get_location("Local Brewery", player), lambda state: state.can_brew_potions(player)) set_rule(world.get_location("The Next Generation", player), lambda state: can_complete(state)) 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.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", - player)) + set_rule(world.get_location("This Boat Has Legs", player), lambda state: (state.fortress_loot(player) or state.complete_raid(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.basic_combat(player)) - set_rule(world.get_location("How Did We Get Here?", player), - lambda state: state.can_brew_potions(player) and state.has_gold_ingots( - player) and # most effects; Absorption - state.can_reach('End City', 'Region', player) and state.can_reach('The Nether', 'Region', - player) and # Levitation; potion ingredients - state.has("Fishing Rod", player) and state.has("Archery", - player) and # Pufferfish, Nautilus Shells; spectral arrows + set_rule(world.get_location("How Did We Get Here?", player), lambda state: state.can_brew_potions(player) and + state.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.has_iron_ingots(player)) + set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state.has_iron_ingots(player)) set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state.basic_combat(player)) - set_rule(world.get_location("Two by Two", player), - lambda state: state.has_iron_ingots(player) and state.can_adventure( - player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots + set_rule(world.get_location("Two by Two", player), lambda state: state.has_iron_ingots(player) and state.can_adventure(player)) # shears > seagrass > turtles; 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.craft_crossbow(player) and state.can_enchant(player)) + set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state.craft_crossbow(player) and state.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.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.has_iron_ingots(player)) - set_rule(world.get_location("Zombie Doctor", player), - lambda state: state.can_brew_potions(player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Tactical Fishing", player), lambda state: state.has("Bucket", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Zombie Doctor", player), lambda state: state.can_brew_potions(player) and state.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.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.basic_combat(player)) set_rule(world.get_location("War Pigs", player), lambda state: state.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.can_use_anvil(player) and state.can_enchant( - player)) - set_rule(world.get_location("Arbalistic", player), - lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and - state.can_use_anvil(player) and state.can_enchant(player)) - set_rule(world.get_location("The End... Again...", player), - lambda state: can_complete(state) and state.has("Ingot Crafting", player) and state.can_reach('The Nether', - 'Region', - player)) # furnace for glass, nether for ghast tears + set_rule(world.get_location("Total Beelocation", player), lambda state: state.has("Silk Touch Book", player) and state.can_use_anvil(player) and state.can_enchant(player)) + set_rule(world.get_location("Arbalistic", player), lambda state: state.craft_crossbow(player) and state.has("Piercing IV Book", player) and + state.can_use_anvil(player) and state.can_enchant(player)) + set_rule(world.get_location("The End... Again...", player), lambda state: can_complete(state)) set_rule(world.get_location("Acquire Hardware", player), lambda state: state.has_iron_ingots(player)) - set_rule(world.get_location("Not Quite \"Nine\" Lives", player), - lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player)) - 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("Not Quite \"Nine\" Lives", player), lambda state: state.can_piglin_trade(player) and state.has("Resource Blocks", player)) + 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.basic_combat(player)) - set_rule(world.get_location("Hired Help", player), - lambda state: state.has("Resource Blocks", player) and state.has_iron_ingots(player)) + set_rule(world.get_location("Hired Help", player), lambda state: state.has("Resource Blocks", player) and state.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: can_complete(state) and state.has_bottle_mc(player)) + 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: can_complete(state) and state.has_bottle_mc(player)) set_rule(world.get_location("Adventure", player), lambda state: True) - set_rule(world.get_location("Monsters Hunted", player), - lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", - player)) # pufferfish for Water Breathing + set_rule(world.get_location("Monsters Hunted", player), lambda state: can_complete(state) and state.can_kill_wither(player) and state.has("Fishing Rod", player)) # pufferfish for Water Breathing set_rule(world.get_location("Enchanter", player), lambda state: state.can_enchant(player)) set_rule(world.get_location("Voluntary Exile", player), lambda state: state.basic_combat(player)) set_rule(world.get_location("Eye Spy", player), lambda state: state.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.has_gold_ingots( - player)) + set_rule(world.get_location("Serious Dedication", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player)) set_rule(world.get_location("Postmortal", player), lambda state: state.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.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.complete_raid(player)) - set_rule(world.get_location("Hidden in the Depths", player), - lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe( - player)) # bed mining :) - set_rule(world.get_location("Beaconator", player), - lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and + set_rule(world.get_location("Hidden in the Depths", player), lambda state: state.can_brew_potions(player) and state.has("Bed", player) and state.has_diamond_pickaxe(player)) # bed mining :) + set_rule(world.get_location("Beaconator", player), lambda state: state.can_kill_wither(player) and state.has_diamond_pickaxe(player) and state.has("Ingot Crafting", player) and state.has("Resource Blocks", player)) set_rule(world.get_location("Withering Heights", player), lambda state: state.can_kill_wither(player)) - set_rule(world.get_location("A Balanced Diet", player), - lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple - state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', - player)) # notch apple, chorus fruit + set_rule(world.get_location("A Balanced Diet", player), lambda state: state.has_bottle_mc(player) and state.has_gold_ingots(player) and # honey bottle; gapple + state.has("Resource Blocks", player) and state.can_reach('The End', 'Region', player)) # notch apple, chorus fruit set_rule(world.get_location("Subspace Bubble", player), lambda state: state.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.has_gold_ingots( - player)) - set_rule(world.get_location("Bee Our Guest", player), - lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) + set_rule(world.get_location("Country Lode, Take Me Home", player), lambda state: state.can_reach("Hidden in the Depths", "Location", player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Bee Our Guest", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) set_rule(world.get_location("What a Deal!", player), lambda state: True) - set_rule(world.get_location("Uneasy Alliance", player), - lambda state: state.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.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("Uneasy Alliance", player), lambda state: state.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.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: True) # 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.has_bottle_mc(player)) + set_rule(world.get_location("Sticky Situation", player), lambda state: state.has("Campfire", player) and state.has_bottle_mc(player)) set_rule(world.get_location("Ol' Betsy", player), lambda state: state.craft_crossbow(player)) - set_rule(world.get_location("Cover Me in Debris", player), - lambda state: state.has("Progressive Armor", player, 2) and + 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("Ingot Crafting", player) and - state.can_reach("Diamonds!", "Location", player) and state.can_reach("Hidden in the Depths", - "Location", player)) + 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 @@ -203,19 +136,13 @@ def set_rules(world: MultiWorld, player: int): set_rule(world.get_location("Time to Mine!", player), lambda state: True) set_rule(world.get_location("Hot Topic", player), lambda state: state.has("Ingot Crafting", player)) set_rule(world.get_location("Bake Bread", player), lambda state: True) - set_rule(world.get_location("The Lie", player), - lambda state: state.has_iron_ingots(player) and state.has("Bucket", player)) - set_rule(world.get_location("On a Rail", player), - lambda state: state.has_iron_ingots(player) and state.has('Progressive Tools', player, 2)) # powered rails + set_rule(world.get_location("The Lie", player), lambda state: state.has_iron_ingots(player) and state.has("Bucket", player)) + set_rule(world.get_location("On a Rail", player), lambda state: state.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.fortress_loot(player) or state.complete_raid(player)) and state.has("Fishing Rod", - player) and state.can_adventure( - player)) - set_rule(world.get_location("Overkill", player), lambda state: state.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("When Pigs Fly", player), lambda state: (state.fortress_loot(player) or state.complete_raid(player)) and + state.has("Fishing Rod", player) and state.can_adventure(player)) + set_rule(world.get_location("Overkill", player), lambda state: state.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.has("Resource Blocks", player) and state.has_gold_ingots(player)) + set_rule(world.get_location("Overpowered", player), lambda state: state.has("Resource Blocks", player) and state.has_gold_ingots(player)) diff --git a/worlds/minecraft/__init__.py b/worlds/minecraft/__init__.py index 91d0f0db..6cb12ad8 100644 --- a/worlds/minecraft/__init__.py +++ b/worlds/minecraft/__init__.py @@ -1,81 +1,92 @@ from .Items import MinecraftItem, item_table, item_frequencies -from .Locations import exclusion_table, events_table -from .Regions import link_minecraft_structures +from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table +from .Regions import mc_regions, link_minecraft_structures from .Rules import set_rules -from BaseClasses import MultiWorld +from BaseClasses import Region, Entrance from .Options import minecraft_options from ..AutoWorld import World +client_version = (0, 4) class MinecraftWorld(World): game: str = "Minecraft" options = minecraft_options -client_version = (0, 3) + def _get_mc_data(self): + exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", + "The End Structure"] + return { + 'world_seed': self.world.slot_seeds[self.player].getrandbits(32), + # consistent and doesn't interfere with other generation + 'seed_name': self.world.seed_name, + 'player_name': self.world.get_player_names(self.player), + 'player_id': self.player, + 'client_version': client_version, + 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits} + } -def get_mc_data(world: MultiWorld, player: int): - exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", - "The End Structure"] - return { - 'world_seed': world.slot_seeds[player].getrandbits(32), - # consistent and doesn't interfere with other generation - 'seed_name': world.seed_name, - 'player_name': world.get_player_names(player), - 'player_id': player, - 'client_version': client_version, - 'structures': {exit: world.get_entrance(exit, player).connected_region.name for exit in exits} - } + def generate_basic(self): + link_minecraft_structures(self.world, self.player) + + pool = [] + for item_name, item_data in item_table.items(): + for count in range(item_frequencies.get(item_name, 1)): + pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, self.player)) + + prefill_pool = {} + prefill_pool.update(events_table) + exclusion_pools = ['hard', 'insane', 'postgame'] + for key in exclusion_pools: + if not getattr(self.world, f"include_{key}_advancements")[self.player]: + prefill_pool.update(exclusion_table[key]) + + for loc_name, item_name in prefill_pool.items(): + item_data = item_table[item_name] + location = self.world.get_location(loc_name, self.player) + item = MinecraftItem(item_name, item_data.progression, item_data.code, self.player) + self.world.push_item(location, item, collect=False) + pool.remove(item) + location.event = item_data.progression + location.locked = True + + self.world.itempool += pool -def generate_mc_data(world: MultiWorld, player: int): - import base64, json - from Utils import output_path - - data = get_mc_data(world, player) - filename = f"AP_{world.seed_name}_P{player}_{world.get_player_names(player)}.apmc" - with open(output_path(filename), 'wb') as f: - f.write(base64.b64encode(bytes(json.dumps(data), 'utf-8'))) + def set_rules(self): + set_rules(self.world, self.player) -def fill_minecraft_slot_data(world: MultiWorld, player: int): - slot_data = get_mc_data(world, player) - for option_name in minecraft_options: - option = getattr(world, option_name)[player] - slot_data[option_name] = int(option.value) - return slot_data + def create_regions(self): + def MCRegion(region_name: str, exits=[]): + ret = Region(region_name, None, region_name, self.player) + ret.world = self.world + 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 + + self.world.regions += [MCRegion(*r) for r in mc_regions] -# Generates the item pool given the table and frequencies in Items.py. -def minecraft_gen_item_pool(world: MultiWorld, player: int): - pool = [] - for item_name, item_data in item_table.items(): - for count in range(item_frequencies.get(item_name, 1)): - pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, player)) + def generate_output(self): + import json + from base64 import b64encode + from Utils import output_path - prefill_pool = {} - prefill_pool.update(events_table) - exclusion_pools = ['hard', 'insane', 'postgame'] - for key in exclusion_pools: - if not getattr(world, f"include_{key}_advancements")[player]: - prefill_pool.update(exclusion_table[key]) - - for loc_name, item_name in prefill_pool.items(): - item_data = item_table[item_name] - location = world.get_location(loc_name, player) - item = MinecraftItem(item_name, item_data.progression, item_data.code, player) - world.push_item(location, item, collect=False) - pool.remove(item) - location.event = item_data.progression - location.locked = True - - world.itempool += pool + data = self._get_mc_data() + filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_names(self.player)}.apmc" + with open(output_path(filename), 'wb') as f: + f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) -# Generate Minecraft world. -def gen_minecraft(world: MultiWorld, player: int): - link_minecraft_structures(world, player) - minecraft_gen_item_pool(world, player) - set_rules(world, player) + def fill_slot_data(self): + slot_data = self._get_mc_data() + for option_name in minecraft_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = int(option.value) + return slot_data