Minecraft rewrite (#1493)

* Minecraft: rewrite to modern AP standards

* Fix gitignore to not try to ignore the entire minecraft world

* minecraft: clean up MC-specific tests

* minecraft: use pkgutil instead of open

* minecraft: ship as apworld

* mc: update region to new api

* Increase upper limit on advancement and egg shard goals

* mc: reduce egg shard count by 5 for structure compasses

* Minecraft: add more tests
Ensures data loading works; tests beatability with various options at their max setting; new tests for 1.19 advancements

* test improvements

* mc: typing and imports cleanup

* parens

* mc: condense filler item code and override get_filler_item_name
This commit is contained in:
espeon65536 2023-03-08 21:13:52 -07:00 committed by GitHub
parent a95e51deda
commit 5e1aa52373
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1254 additions and 859 deletions

1
.gitignore vendored
View File

@ -169,6 +169,7 @@ cython_debug/
jdk*/
minecraft*/
minecraft_versions.json
!worlds/minecraft/
# pyenv
.python-version

View File

@ -53,6 +53,7 @@ apworlds: set = {
"Super Mario World",
"Stardew Valley",
"Timespinner",
"Minecraft",
}

View File

@ -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")

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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,
}

View File

@ -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']
}

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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"
]
}

View File

@ -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
}
}

View File

@ -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"
]
}
}

View File

@ -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"]
}
}

View File

@ -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"]],
])

View File

@ -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

View File

@ -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([

View File

@ -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)

View File

@ -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

View File

@ -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)