AutoWorld: basic Item handling

This commit is contained in:
Fabian Dill 2021-07-12 13:54:47 +02:00
parent babd809fa6
commit 31c550d410
11 changed files with 84 additions and 51 deletions

View File

@ -272,6 +272,8 @@ class MultiWorld():
return next(location for location in self.get_locations() if return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player) location.item and location.item.name == item and location.item.player == player)
def create_item(self, item_name: str, player: int) -> Item:
return self.worlds[player].create_item(item_name)
def push_precollected(self, item: Item): def push_precollected(self, item: Item):
item.world = self item.world = self
@ -859,7 +861,6 @@ class CollectionState(object):
(self.has('Progressive Weapons', player, 1) and self.has('Bed', player))) (self.has('Progressive Weapons', player, 1) and self.has('Bed', player)))
return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player) return respawn_dragon and self.has('Progressive Weapons', player, 2) and self.has('Progressive Armor', player) and self.has('Archery', player)
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool: def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
if location: if location:
self.locations_checked.add(location) self.locations_checked.add(location)

View File

@ -9,7 +9,7 @@ import pickle
from typing import Dict, Tuple from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item from BaseClasses import MultiWorld, CollectionState, Region, Item
from worlds.alttp.Items import ItemFactory, item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \ from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
lookup_vanilla_location_to_entrance lookup_vanilla_location_to_entrance
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
@ -453,7 +453,7 @@ def main(args, seed=None):
for index, take_any in enumerate(takeanyregions): for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
world.retro[player]]: world.retro[player]]:
item = ItemFactory(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'], item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player) region.player)
player = region.player player = region.player
location_id = SHOP_ID_START + total_shop_slots + index location_id = SHOP_ID_START + total_shop_slots + index

View File

@ -516,10 +516,11 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.game = get_choice("game", weights) ret.game = get_choice("game", weights)
if ret.game not in weights: if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.") raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game] game_weights = weights[ret.game]
ret.local_items = set() ret.local_items = set()
for item_name in game_weights.get('local_items', []): for item_name in game_weights.get('local_items', []):
items = item_name_groups.get(item_name, {item_name}) items = world_type.item_name_groups.get(item_name, {item_name})
for item in items: for item in items:
if item in lookup_any_item_name_to_id: if item in lookup_any_item_name_to_id:
ret.local_items.add(item) ret.local_items.add(item)
@ -528,7 +529,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.non_local_items = set() ret.non_local_items = set()
for item_name in game_weights.get('non_local_items', []): for item_name in game_weights.get('non_local_items', []):
items = item_name_groups.get(item_name, {item_name}) items = world_type.item_name_groups.get(item_name, {item_name})
for item in items: for item in items:
if item in lookup_any_item_name_to_id: if item in lookup_any_item_name_to_id:
ret.non_local_items.add(item) ret.non_local_items.add(item)
@ -556,7 +557,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
else: else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
except Exception as e: except Exception as e:
raise Exception(f"Error generating option {option_name} in {ret.game}") raise Exception(f"Error generating option {option_name} in {ret.game}") from e
else: else:
setattr(ret, option_name, option(option.default)) setattr(ret, option_name, option(option.default))
if ret.game == "Minecraft": if ret.game == "Minecraft":
@ -778,7 +779,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
get_choice("direction", placement, "both") get_choice("direction", placement, "both")
)) ))
ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice('sprite', weights, "Link") ret.sprite = get_choice('sprite', weights, "Link")
if 'random_sprite_on_event' in weights: if 'random_sprite_on_event' in weights:

View File

@ -1,4 +1,6 @@
from BaseClasses import MultiWorld from typing import Dict, Set, Tuple
from BaseClasses import MultiWorld, Item, CollectionState
class AutoWorldRegister(type): class AutoWorldRegister(type):
@ -29,6 +31,9 @@ class World(metaclass=AutoWorldRegister):
player: int player: int
options: dict = {} options: dict = {}
topology_present: bool = False # indicate if world type has any meaningful layout/pathing topology_present: bool = False # indicate if world type has any meaningful layout/pathing
item_names: Set[str] = frozenset()
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
item_name_groups: Dict[str, Set[str]] = {}
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
self.world = world self.world = world
@ -49,15 +54,19 @@ class World(metaclass=AutoWorldRegister):
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
pass pass
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 0, 3 return 0, 0, 3
# end of Main.py calls # end of Main.py calls
def collect(self, state, item) -> bool: def collect(self, state: CollectionState, item: Item) -> bool:
"""Collect an item into state. For speed reasons items that aren't logically useful get skipped.""" """Collect an item into state. For speed reasons items that aren't logically useful get skipped."""
if item.advancement: if item.advancement:
state.prog_items[item.name, item.player] += 1 state.prog_items[item.name, item.player] += 1
return True # indicate that a logical state change has occured return True # indicate that a logical state change has occured
return False return False
def create_item(self, name: str) -> Item:
"""Create an item for this world type and player.
Warning: this may be called with self.world = None, for example by MultiServer"""
raise NotImplementedError

View File

@ -6,17 +6,19 @@ def GetBeemizerItem(world, player, item):
if world.beemizer[player] and item_name in trap_replaceable: if world.beemizer[player] and item_name in trap_replaceable:
if world.random.random() < world.beemizer[player] * 0.25: if world.random.random() < world.beemizer[player] * 0.25:
if world.random.random() < (0.5 + world.beemizer[player] * 0.1): if world.random.random() < (0.5 + world.beemizer[player] * 0.1):
return "Bee Trap" if isinstance(item, str) else ItemFactory("Bee Trap", player) return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
else: else:
return "Bee" if isinstance(item, str) else ItemFactory("Bee", player) return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
else: else:
return item return item
else: else:
return item return item
def ItemFactory(items, player): # should be replaced with direct world.create_item(item) call in the future
from worlds.alttp import ALttPItem def ItemFactory(items, player: int):
from worlds.alttp import ALTTPWorld
world = ALTTPWorld(None, player)
ret = [] ret = []
singleton = False singleton = False
if isinstance(items, str): if isinstance(items, str):
@ -24,7 +26,7 @@ def ItemFactory(items, player):
singleton = True singleton = True
for item in items: for item in items:
if item in item_table: if item in item_table:
ret.append(ALttPItem(item, *item_table[item], player)) ret.append(world.create_item(item))
else: else:
raise Exception(f"Unknown item {item}") raise Exception(f"Unknown item {item}")
@ -211,6 +213,8 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
'Open Floodgate': ItemData(True, 'Event', None, None, None, None, None, None, None, None), 'Open Floodgate': ItemData(True, 'Event', None, None, None, None, None, None, None, None),
} }
as_dict_item_table = {name: data._asdict() for name, data in item_table.items()}
progression_mapping = { progression_mapping = {
"Golden Sword": ("Progressive Sword", 4), "Golden Sword": ("Progressive Sword", 4),
"Tempered Sword": ("Progressive Sword", 3), "Tempered Sword": ("Progressive Sword", 3),
@ -268,8 +272,8 @@ for basename, substring in _simple_groups:
del (_simple_groups) del (_simple_groups)
progression_items = {name for name, data in item_table.items() if type(data[2]) == int and data[0]} progression_items = {name for name, data in item_table.items() if type(data.item_code) == int and data.advancement}
item_name_groups['Everything'] = {name for name, data in item_table.items() if type(data[2]) == int} item_name_groups['Everything'] = {name for name, data in item_table.items() if type(data.item_code) == int}
item_name_groups['Progression Items'] = progression_items item_name_groups['Progression Items'] = progression_items
item_name_groups['Non Progression Items'] = item_name_groups['Everything'] - progression_items item_name_groups['Non Progression Items'] = item_name_groups['Everything'] - progression_items

View File

@ -3,11 +3,15 @@ from typing import Optional
from BaseClasses import Location, Item, CollectionState from BaseClasses import Location, Item, CollectionState
from ..AutoWorld import World from ..AutoWorld import World
from .Options import alttp_options from .Options import alttp_options
from .Items import as_dict_item_table, item_name_groups, item_table
class ALTTPWorld(World): class ALTTPWorld(World):
game: str = "A Link to the Past" game: str = "A Link to the Past"
options = alttp_options options = alttp_options
topology_present = True topology_present = True
item_name_groups = item_name_groups
item_names = frozenset(item_table)
def collect(self, state: CollectionState, item: Item) -> bool: def collect(self, state: CollectionState, item: Item) -> bool:
if item.name.startswith('Progressive '): if item.name.startswith('Progressive '):
@ -66,6 +70,9 @@ class ALTTPWorld(World):
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name])
class ALttPLocation(Location): class ALttPLocation(Location):
game: str = "A Link to the Past" game: str = "A Link to the Past"
@ -80,16 +87,16 @@ class ALttPLocation(Location):
class ALttPItem(Item): class ALttPItem(Item):
game: str = "A Link to the Past" game: str = "A Link to the Past"
def __init__(self, name='', advancement=False, type=None, code=None, pedestal_hint=None, pedestal_credit=None, sickkid_credit=None, zora_credit=None, witch_credit=None, fluteboy_credit=None, hint_text=None, player=None): def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
super(ALttPItem, self).__init__(name, advancement, code, player) sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
super(ALttPItem, self).__init__(name, advancement, item_code, player)
self.type = type self.type = type
self._pedestal_hint_text = pedestal_hint self._pedestal_hint_text = pedestal_hint
self.pedestal_credit_text = pedestal_credit self.pedestal_credit_text = pedestal_credit
self.sickkid_credit_text = sickkid_credit self.sickkid_credit_text = sick_kid_credit
self.zora_credit_text = zora_credit self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = fluteboy_credit self.fluteboy_credit_text = flute_boy_credit
self._hint_text = hint_text self._hint_text = hint_text

View File

@ -4,29 +4,30 @@ from BaseClasses import Region, Entrance, Location, Item
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \ from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \ all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table
from .Shapes import get_shapes from .Shapes import get_shapes
from .Mod import generate_mod from .Mod import generate_mod
from .Options import factorio_options from .Options import factorio_options
class FactorioItem(Item):
game = "Factorio"
class Factorio(World): class Factorio(World):
game: str = "Factorio" game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"} static_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes = {} custom_recipes = {}
additional_advancement_technologies = set() additional_advancement_technologies = set()
item_names = frozenset(tech_table)
def generate_basic(self): def generate_basic(self):
for tech_name, tech_id in base_tech_table.items(): for tech_name in base_tech_table:
if self.world.progressive and tech_name in tech_to_progressive_lookup: if self.world.progressive:
item_name = tech_to_progressive_lookup[tech_name] item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
tech_id = progressive_tech_table[item_name]
else: else:
item_name = tech_name item_name = item_name
tech_item = self.create_item(item_name)
tech_item = Item(item_name, item_name in advancement_technologies or
item_name in self.additional_advancement_technologies,
tech_id, self.player)
tech_item.game = "Factorio"
if tech_name in self.static_nodes: if tech_name in self.static_nodes:
self.world.get_location(tech_name, self.player).place_locked_item(tech_item) self.world.get_location(tech_name, self.player).place_locked_item(tech_item)
else: else:
@ -157,3 +158,9 @@ class Factorio(World):
if tech in tech_to_progressive_lookup: if tech in tech_to_progressive_lookup:
prog_add.add(tech_to_progressive_lookup[tech]) prog_add.add(tech_to_progressive_lookup[tech])
self.additional_advancement_technologies |= prog_add self.additional_advancement_technologies |= prog_add
def create_item(self, name: str) -> Item:
assert name in tech_table
return FactorioItem(name, name in advancement_technologies or
name in self.additional_advancement_technologies,
tech_table[name], self.player)

View File

@ -1,6 +1,7 @@
from typing import NamedTuple, Union from typing import NamedTuple, Union
import logging import logging
class PlandoItem(NamedTuple): class PlandoItem(NamedTuple):
item: str item: str
location: str location: str
@ -25,7 +26,3 @@ class PlandoConnection(NamedTuple):
entrance: str entrance: str
exit: str exit: str
direction: str # entrance, exit or both direction: str # entrance, exit or both
class World():
pass

View File

@ -1,4 +1,5 @@
import logging import logging
from typing import Set
logger = logging.getLogger("Hollow Knight") logger = logging.getLogger("Hollow Knight")
@ -14,6 +15,7 @@ from ..AutoWorld import World
class HKWorld(World): class HKWorld(World):
game: str = "Hollow Knight" game: str = "Hollow Knight"
options = hollow_knight_options options = hollow_knight_options
item_names: Set[str] = frozenset(item_table)
def generate_basic(self): def generate_basic(self):
# Link regions # Link regions
@ -22,8 +24,7 @@ class HKWorld(World):
# Generate item pool # Generate item pool
pool = [] pool = []
for item_name, item_data in item_table.items(): for item_name, item_data in item_table.items():
item = self.create_item(item_name)
item = HKItem(item_name, item_data.advancement, item_data.id, item_data.type, player=self.player)
if item_data.type == "Event": if item_data.type == "Event":
event_location = self.world.get_location(item_name, self.player) event_location = self.world.get_location(item_name, self.player)
@ -83,6 +84,9 @@ class HKWorld(World):
slot_data[option_name] = int(option.value) slot_data[option_name] = int(option.value)
return slot_data return slot_data
def create_item(self, name: str) -> Item:
item_data = item_table[name]
return HKItem(name, item_data.advancement, item_data.id, item_data.type, self.player)
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
ret = Region(name, None, name, player) ret = Region(name, None, name, player)

View File

@ -1,14 +1,15 @@
from BaseClasses import Item from BaseClasses import Item
import typing import typing
class ItemData(typing.NamedTuple): class ItemData(typing.NamedTuple):
code: int code: int
progression: bool progression: bool
class MinecraftItem(Item): class MinecraftItem(Item):
game: str = "Minecraft" game: str = "Minecraft"
def __init__(self, name: str, progression: bool, code: int, player: int):
super().__init__(name, progression, code if code else None, player)
item_table = { item_table = {
"Archery": ItemData(45000, True), "Archery": ItemData(45000, True),

View File

@ -1,9 +1,12 @@
from typing import Dict, Set
from .Items import MinecraftItem, item_table, item_frequencies from .Items import MinecraftItem, item_table, item_frequencies
from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table from .Locations import MinecraftAdvancement, advancement_table, exclusion_table, events_table
from .Regions import mc_regions, link_minecraft_structures from .Regions import mc_regions, link_minecraft_structures
from .Rules import set_rules from .Rules import set_rules
from BaseClasses import Region, Entrance from BaseClasses import Region, Entrance, Item
from .Options import minecraft_options from .Options import minecraft_options
from ..AutoWorld import World from ..AutoWorld import World
@ -13,6 +16,7 @@ class MinecraftWorld(World):
game: str = "Minecraft" game: str = "Minecraft"
options = minecraft_options options = minecraft_options
topology_present = True topology_present = True
item_names = frozenset(item_table)
def _get_mc_data(self): def _get_mc_data(self):
exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2", exits = ["Overworld Structure 1", "Overworld Structure 2", "Nether Structure 1", "Nether Structure 2",
@ -27,7 +31,6 @@ class MinecraftWorld(World):
'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits} 'structures': {exit: self.world.get_entrance(exit, self.player).connected_region.name for exit in exits}
} }
def generate_basic(self): def generate_basic(self):
link_minecraft_structures(self.world, self.player) link_minecraft_structures(self.world, self.player)
@ -35,9 +38,9 @@ class MinecraftWorld(World):
pool_counts = item_frequencies.copy() pool_counts = item_frequencies.copy()
if getattr(self.world, "bee_traps")[self.player]: if getattr(self.world, "bee_traps")[self.player]:
pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4}) pool_counts.update({"Rotten Flesh": 0, "Bee Trap (Minecraft)": 4})
for item_name, item_data in item_table.items(): for item_name in item_table:
for count in range(pool_counts.get(item_name, 1)): for count in range(pool_counts.get(item_name, 1)):
pool.append(MinecraftItem(item_name, item_data.progression, item_data.code, self.player)) pool.append(self.create_item(item_name))
prefill_pool = {} prefill_pool = {}
prefill_pool.update(events_table) prefill_pool.update(events_table)
@ -49,7 +52,7 @@ class MinecraftWorld(World):
for loc_name, item_name in prefill_pool.items(): for loc_name, item_name in prefill_pool.items():
item_data = item_table[item_name] item_data = item_table[item_name]
location = self.world.get_location(loc_name, self.player) location = self.world.get_location(loc_name, self.player)
item = MinecraftItem(item_name, item_data.progression, item_data.code, self.player) item = self.create_item(item_name)
self.world.push_item(location, item, collect=False) self.world.push_item(location, item, collect=False)
pool.remove(item) pool.remove(item)
location.event = item_data.progression location.event = item_data.progression
@ -57,11 +60,9 @@ class MinecraftWorld(World):
self.world.itempool += pool self.world.itempool += pool
def set_rules(self): def set_rules(self):
set_rules(self.world, self.player) set_rules(self.world, self.player)
def create_regions(self): def create_regions(self):
def MCRegion(region_name: str, exits=[]): def MCRegion(region_name: str, exits=[]):
ret = Region(region_name, None, region_name, self.player) ret = Region(region_name, None, region_name, self.player)
@ -75,7 +76,6 @@ class MinecraftWorld(World):
self.world.regions += [MCRegion(*r) for r in mc_regions] self.world.regions += [MCRegion(*r) for r in mc_regions]
def generate_output(self): def generate_output(self):
import json import json
from base64 import b64encode from base64 import b64encode
@ -86,10 +86,13 @@ class MinecraftWorld(World):
with open(output_path(filename), 'wb') as f: with open(output_path(filename), 'wb') as f:
f.write(b64encode(bytes(json.dumps(data), 'utf-8'))) f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
def fill_slot_data(self): def fill_slot_data(self):
slot_data = self._get_mc_data() slot_data = self._get_mc_data()
for option_name in minecraft_options: for option_name in minecraft_options:
option = getattr(self.world, option_name)[self.player] option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = int(option.value) slot_data[option_name] = int(option.value)
return slot_data return slot_data
def create_item(self, name: str) -> Item:
item_data = item_table[name]
return MinecraftItem(name, item_data.progression, item_data.code, self.player)