Minecraft Randomizer

Squash merge, original Commits:

* Minecraft locations, items, and generation without logic

* added id lookup for minecraft

* typing import fix in minecraft/Items.py

* fix 2

* implementing Minecraft options and hard/postgame advancement exclusion

* first logic pass (75/80)

* logic pass 2 and proper completion conditions

* added insane difficulty pool, modified method of excluding item pools for easier extension

* bump network_data_package version

* minecraft testing framework

* switch Ancient Debris to Netherite Scrap to avoid advancement triggering on receiving that item

* Testing now functions, split tests up by advancement pane, added some story tests

* Newer testing framework: every advancement gets its own function, for ease of testing

* fixed logic for The End... Again...

* changed option names to "include_hard_advancements" etc.

* village/pillager-related advancements now require can_adventure: weapon + food

* a few minecraft tests

* rename "Flint & Steel" to "Flint and Steel" for parity with in-game name

* additional MC tests

* more tests, mostly nether-related tests

* more tests, removed anvil path for Two Birds One Arrow

* include Minecraft slot data, and a world seed for each Minecraft player slot

* Added new items: ender pearls, lapis, porkchops

* All remaining Minecraft tests

* formatting of Minecraft tests and logic for better readability

* require Wither kill for Monsters Hunted

* properly removed 8 Emeralds item from item pool

* enchanting required for wither; fishing rod required for water breathing; water breathing required for elder guardian kill

* Added 12 new advancements (ported from old achievement system)

* renamed "On a Rail" for consistency with modern advancements

* tests for the new advancements

* moved slot_data generation for minecraft into worlds/minecraft/__init__.py, added logic_version to slot_data

* output minecraft options in the spoiler log

* modified advancement goal values for new advancements

* make non-native Minecraft items appear as Shovel in ALttP, and unknown-game items as Power Stars

* fixed glowstone block logic for Not Quite Nine Lives

* setup for shuffling MC structures: building ER world and shuffling regions/entrances

* ensured Nether Fortresses can't be placed in the End

* finished logic for structure randomization

* fixed nonnative items always showing up as Hammers in ALttP shops

* output minecraft structure info in the spoiler

* generate .apmc file for communication with MC client

* fixed structure rando always using the same seed

* move stuff to worlds/minecraft/Regions.py

* make output apmc file have consistent name with other files

* added minecraft bottle macro; fixed tests imports

* generalizing MC region generation

* restructured structure shuffling in preparation for structure plando

* only output structure rando info in spoiler if they are shuffled

* Force structure rando to always be off, for the stable release

* added Minecraft options to player settings

* formally added combat_difficulty as an option

* Added Ender Dragon into playthrough, cleaned up goal map

* Added new difficulties: Easy, Normal, Hard combat

* moved .apmc generation time to prevent outputs on failed generation

* updated tests for new combat logic

* Fixed bug causing generation to fail; removed Nether Fortress event since it should no longer be needed with the fix

* moved all MC-specific functions into gen_minecraft

* renamed "logic_version" to "client_version"

* bug fixes
properly flagged event locations/items with id None
moved generation back to Main.py to fix mysterious generation failures

* moved link_minecraft_regions into minecraft init, left create_regions in Main for caching

* added seed_name, player_name, client_version to apmc file

* reenabled structure shuffle

* added entrance tests for minecraft

Co-authored-by: achuang <alexander.w.chuang@gmail.com>
This commit is contained in:
espeon65536
2021-05-08 07:38:57 -04:00
committed by GitHub
parent eb2a3009f4
commit 2f7e532f4f
15 changed files with 2005 additions and 15 deletions

View File

@@ -169,6 +169,11 @@ class MultiWorld():
def factorio_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Factorio")
@property
def minecraft_player_ids(self):
yield from (player for player in range(1, self.players + 1) if self.game[player] == "Minecraft")
def get_name_string_for_object(self, obj) -> str:
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})'
@@ -234,6 +239,7 @@ class MultiWorld():
def soft_collect(item):
if item.game == "A Link to the Past" and item.name.startswith('Progressive '):
# ALttP items
if 'Sword' in item.name:
if ret.has('Golden Sword', item.player):
pass
@@ -806,6 +812,91 @@ class CollectionState(object):
rules.append(self.has('Moon Pearl', player))
return all(rules)
# Minecraft logic functions
def has_iron_ingots(self, player: int):
return self.has('Progressive Tools', player) and self.has('Ingot Crafting', player)
def has_gold_ingots(self, player: int):
return self.has('Ingot Crafting', player) and (self.has('Progressive Tools', player, 2) or self.can_reach('The Nether', 'Region', player))
def has_diamond_pickaxe(self, player: int):
return self.has('Progressive Tools', player, 3) and self.has_iron_ingots(player)
def craft_crossbow(self, player: int):
return self.has('Archery', player) and self.has_iron_ingots(player)
def has_bottle_mc(self, player: int):
return self.has('Bottles', player) and self.has('Ingot Crafting', player)
def can_enchant(self, player: int):
return self.has('Enchanting', player) and self.has_diamond_pickaxe(player) # mine obsidian and lapis
def can_use_anvil(self, player: int):
return self.has('Enchanting', player) and self.has('Resource Blocks', player) and self.has_iron_ingots(player)
def fortress_loot(self, player: int): # saddles, blaze rods, wither skulls
return self.can_reach('Nether Fortress', 'Region', player) and self.basic_combat(player)
def can_brew_potions(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has_bottle_mc(player)
def can_piglin_trade(self, player: int):
return self.has_gold_ingots(player) and (self.can_reach('The Nether', 'Region', player) or self.can_reach('Bastion Remnant', 'Region', player))
def enter_stronghold(self, player: int):
return self.fortress_loot(player) and self.has('Brewing', player) and self.has('3 Ender Pearls', player)
# Difficulty-dependent functions
def combat_difficulty(self, player: int):
return self.world.combat_difficulty[player].get_option_name()
def can_adventure(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard':
return True
return self.has('Progressive Weapons', player) and (self.has('Ingot Crafting', player) or self.has('Campfire', player))
def basic_combat(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and \
self.has('Shield', player) and self.has_iron_ingots(player)
elif self.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.has_iron_ingots(player)
def complete_raid(self, player: int):
reach_regions = self.can_reach('Village', 'Region', player) and self.can_reach('Pillager Outpost', 'Region', player)
if self.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.has_iron_ingots(player)
elif self.combat_difficulty(player) == 'hard': # might be too hard?
return reach_regions and self.has('Progressive Weapons', player, 2) and self.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.has_iron_ingots(player) and \
self.has('Progressive Armor', player) and self.has('Shield', player)
def can_kill_wither(self, player: int):
build_wither = self.fortress_loot(player) and (self.can_reach('The Nether', 'Region', player) or self.can_piglin_trade(player))
normal_kill = self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.can_brew_potions(player) and self.can_enchant(player)
if self.combat_difficulty(player) == 'easy':
return build_wither and normal_kill and self.has('Archery', player)
elif self.combat_difficulty(player) == 'hard': # cheese kill using bedrock ceilings
return build_wither and (normal_kill or self.can_reach('The Nether', 'Region', player) or self.can_reach('The End', 'Region', player))
return build_wither and normal_kill
def can_kill_ender_dragon(self, player: int):
if self.combat_difficulty(player) == 'easy':
return self.has("Progressive Weapons", player, 3) and self.has("Progressive Armor", player, 2) and self.has('Archery', player) and \
self.can_brew_potions(player) and self.can_enchant(player)
if self.combat_difficulty(player) == 'hard':
return (self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player)) or \
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))
return self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
if location:
self.locations_checked.add(location)
@@ -1403,6 +1494,11 @@ class Spoiler(object):
for hk_option in Options.hollow_knight_options:
res = getattr(self.world, hk_option)[player]
outfile.write(f'{hk_option+":":33}{res}\n')
if player in self.world.minecraft_player_ids:
import Options
for mc_option in Options.minecraft_options:
res = getattr(self.world, mc_option)[player]
outfile.write(f'{mc_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n')
if player in self.world.alttp_player_ids:
for team in range(self.world.teams):
outfile.write('%s%s\n' % (
@@ -1418,7 +1514,7 @@ class Spoiler(object):
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %