mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
shapez: Implement New Game (#3960)
Adds shapez as a supported game in AP.
This commit is contained in:
417
worlds/shapez/__init__.py
Normal file
417
worlds/shapez/__init__.py
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
import math
|
||||||
|
from typing import Any, List, Dict, Tuple, Mapping
|
||||||
|
|
||||||
|
from Options import OptionError
|
||||||
|
from .data.strings import OTHER, ITEMS, CATEGORY, LOCATIONS, SLOTDATA, GOALS, OPTIONS
|
||||||
|
from .items import item_descriptions, item_table, ShapezItem, \
|
||||||
|
buildings_routing, buildings_processing, buildings_other, \
|
||||||
|
buildings_top_row, buildings_wires, gameplay_unlocks, upgrades, \
|
||||||
|
big_upgrades, filler, trap, bundles, belt_and_extractor, standard_traps, random_draining_trap, split_draining_traps, \
|
||||||
|
whacky_upgrade_traps
|
||||||
|
from .locations import ShapezLocation, addlevels, addupgrades, addachievements, location_description, \
|
||||||
|
addshapesanity, addshapesanity_ut, shapesanity_simple, init_shapesanity_pool, achievement_locations, \
|
||||||
|
level_locations, upgrade_locations, shapesanity_locations, categories
|
||||||
|
from .presets import options_presets
|
||||||
|
from .options import ShapezOptions
|
||||||
|
from worlds.AutoWorld import World, WebWorld
|
||||||
|
from BaseClasses import Item, Tutorial, LocationProgressType, MultiWorld
|
||||||
|
from .regions import create_shapez_regions, has_x_belt_multiplier
|
||||||
|
from ..generic.Rules import add_rule
|
||||||
|
|
||||||
|
|
||||||
|
class ShapezWeb(WebWorld):
|
||||||
|
options_presets = options_presets
|
||||||
|
rich_text_options_doc = True
|
||||||
|
theme = "stone"
|
||||||
|
game_info_languages = ['en', 'de']
|
||||||
|
setup_en = Tutorial(
|
||||||
|
"Multiworld Setup Guide",
|
||||||
|
"A guide to playing shapez with Archipelago:",
|
||||||
|
"English",
|
||||||
|
"setup_en.md",
|
||||||
|
"setup/en",
|
||||||
|
["BlastSlimey"]
|
||||||
|
)
|
||||||
|
setup_de = Tutorial(
|
||||||
|
setup_en.tutorial_name,
|
||||||
|
setup_en.description,
|
||||||
|
"Deutsch",
|
||||||
|
"setup_de.md",
|
||||||
|
"setup/de",
|
||||||
|
["BlastSlimey"]
|
||||||
|
)
|
||||||
|
datapackage_settings_en = Tutorial(
|
||||||
|
"Changing datapackage settings",
|
||||||
|
"3000 locations are too many or not enough? Here's how you can change that:",
|
||||||
|
"English",
|
||||||
|
"datapackage_settings_en.md",
|
||||||
|
"datapackage_settings/en",
|
||||||
|
["BlastSlimey"]
|
||||||
|
)
|
||||||
|
datapackage_settings_de = Tutorial(
|
||||||
|
datapackage_settings_en.tutorial_name,
|
||||||
|
datapackage_settings_en.description,
|
||||||
|
"Deutsch",
|
||||||
|
"datapackage_settings_de.md",
|
||||||
|
"datapackage_settings/de",
|
||||||
|
["BlastSlimey"]
|
||||||
|
)
|
||||||
|
tutorials = [setup_en, setup_de, datapackage_settings_en, datapackage_settings_de]
|
||||||
|
item_descriptions = item_descriptions
|
||||||
|
location_descriptions = location_description
|
||||||
|
|
||||||
|
|
||||||
|
class ShapezWorld(World):
|
||||||
|
"""
|
||||||
|
shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly
|
||||||
|
generated patches on an infinite canvas, without the need to manage your infinite resources or to pay for building
|
||||||
|
your factories.
|
||||||
|
"""
|
||||||
|
game = OTHER.game_name
|
||||||
|
options_dataclass = ShapezOptions
|
||||||
|
options: ShapezOptions
|
||||||
|
topology_present = True
|
||||||
|
web = ShapezWeb()
|
||||||
|
base_id = 20010707
|
||||||
|
item_name_to_id = {name: id for id, name in enumerate(item_table.keys(), base_id)}
|
||||||
|
location_name_to_id = {name: id for id, name in enumerate(level_locations + upgrade_locations
|
||||||
|
+ achievement_locations + shapesanity_locations, base_id)}
|
||||||
|
item_name_groups = {
|
||||||
|
"Main Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker},
|
||||||
|
"Processing Buildings": {*buildings_processing},
|
||||||
|
"Goal Buildings": {ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.rotator_ccw, ITEMS.color_mixer,
|
||||||
|
ITEMS.stacker, ITEMS.cutter_quad, ITEMS.painter_double, ITEMS.painter_quad, ITEMS.wires,
|
||||||
|
ITEMS.switch, ITEMS.const_signal},
|
||||||
|
"Most Useful Buildings": {ITEMS.balancer, ITEMS.tunnel, ITEMS.tunnel_tier_ii, ITEMS.comp_merger,
|
||||||
|
ITEMS.comp_splitter, ITEMS.trash, ITEMS.extractor_chain},
|
||||||
|
"Most Important Buildings": {*belt_and_extractor},
|
||||||
|
"Top Row Buildings": {*buildings_top_row},
|
||||||
|
"Wires Layer Buildings": {*buildings_wires},
|
||||||
|
"Gameplay Mechanics": {ITEMS.blueprints, ITEMS.wires},
|
||||||
|
"Upgrades": {*{ITEMS.upgrade(size, cat)
|
||||||
|
for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}
|
||||||
|
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
|
||||||
|
*{ITEMS.trap_upgrade(cat, size)
|
||||||
|
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}
|
||||||
|
for size in {"", CATEGORY.demonic}},
|
||||||
|
*{ITEMS.upgrade(size, CATEGORY.random)
|
||||||
|
for size in {CATEGORY.big, CATEGORY.small}}},
|
||||||
|
**{f"{cat} Upgrades": {*{ITEMS.upgrade(size, cat)
|
||||||
|
for size in {CATEGORY.big, CATEGORY.small, CATEGORY.gigantic, CATEGORY.rising}},
|
||||||
|
*{ITEMS.trap_upgrade(cat, size)
|
||||||
|
for size in {"", CATEGORY.demonic}}}
|
||||||
|
for cat in {CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting}},
|
||||||
|
"Bundles": {*bundles},
|
||||||
|
"Traps": {*standard_traps, *random_draining_trap, *split_draining_traps, *whacky_upgrade_traps},
|
||||||
|
}
|
||||||
|
location_name_groups = {
|
||||||
|
"Levels": {*level_locations},
|
||||||
|
"Upgrades": {*upgrade_locations},
|
||||||
|
"Achievements": {*achievement_locations},
|
||||||
|
"Shapesanity": {*shapesanity_locations},
|
||||||
|
**{f"{cat} Upgrades": {loc for loc in upgrade_locations if loc.startswith(cat)} for cat in categories},
|
||||||
|
"Only Belt and Extractor": {LOCATIONS.level(1), LOCATIONS.level(1, 1),
|
||||||
|
LOCATIONS.my_eyes, LOCATIONS.its_a_mess, LOCATIONS.getting_into_it,
|
||||||
|
LOCATIONS.perfectionist, LOCATIONS.oops, LOCATIONS.i_need_trains, LOCATIONS.gps,
|
||||||
|
LOCATIONS.a_long_time, LOCATIONS.addicted,
|
||||||
|
LOCATIONS.shapesanity(1), LOCATIONS.shapesanity(2), LOCATIONS.shapesanity(3)},
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, multiworld: MultiWorld, player: int):
|
||||||
|
super().__init__(multiworld, player)
|
||||||
|
|
||||||
|
# Defining instance attributes for each shapez world
|
||||||
|
# These are set to default values that should fail unit tests if not replaced with correct values
|
||||||
|
self.location_count: int = 0
|
||||||
|
self.level_logic: List[str] = []
|
||||||
|
self.upgrade_logic: List[str] = []
|
||||||
|
self.level_logic_type: str = ""
|
||||||
|
self.upgrade_logic_type: str = ""
|
||||||
|
self.random_logic_phase_length: List[int] = []
|
||||||
|
self.category_random_logic_amounts: Dict[str, int] = {}
|
||||||
|
self.maxlevel: int = 0
|
||||||
|
self.finaltier: int = 0
|
||||||
|
self.included_locations: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||||
|
self.client_seed: int = 0
|
||||||
|
self.shapesanity_names: List[str] = []
|
||||||
|
self.upgrade_traps_allowed: bool = False
|
||||||
|
|
||||||
|
# Universal Tracker support
|
||||||
|
self.ut_active: bool = False
|
||||||
|
self.passthrough: Dict[str, any] = {}
|
||||||
|
self.location_id_to_alias: Dict[int, str] = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||||
|
# Import the 75800 entries long shapesanity pool only once and only if it's actually needed
|
||||||
|
if len(shapesanity_simple) == 0:
|
||||||
|
init_shapesanity_pool()
|
||||||
|
|
||||||
|
def generate_early(self) -> None:
|
||||||
|
# Calculate all the important values used for generating a shapez world, with some of them being random
|
||||||
|
self.upgrade_traps_allowed: bool = (self.options.include_whacky_upgrades and
|
||||||
|
(not self.options.goal == GOALS.efficiency_iii) and
|
||||||
|
self.options.throughput_levels_ratio == 0)
|
||||||
|
|
||||||
|
# Load values from UT if this is a regenerated world
|
||||||
|
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||||
|
if OTHER.game_name in self.multiworld.re_gen_passthrough:
|
||||||
|
self.ut_active = True
|
||||||
|
self.passthrough = self.multiworld.re_gen_passthrough[OTHER.game_name]
|
||||||
|
self.maxlevel = self.passthrough[SLOTDATA.maxlevel]
|
||||||
|
self.finaltier = self.passthrough[SLOTDATA.finaltier]
|
||||||
|
self.client_seed = self.passthrough[SLOTDATA.seed]
|
||||||
|
self.level_logic = [self.passthrough[SLOTDATA.level_building(i+1)] for i in range(5)]
|
||||||
|
self.upgrade_logic = [self.passthrough[SLOTDATA.upgrade_building(i+1)] for i in range(5)]
|
||||||
|
self.level_logic_type = self.passthrough[SLOTDATA.rand_level_logic]
|
||||||
|
self.upgrade_logic_type = self.passthrough[SLOTDATA.rand_upgrade_logic]
|
||||||
|
self.random_logic_phase_length = [self.passthrough[SLOTDATA.phase_length(i)] for i in range(5)]
|
||||||
|
self.category_random_logic_amounts = {cat: self.passthrough[SLOTDATA.cat_buildings_amount(cat)]
|
||||||
|
for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
|
||||||
|
CATEGORY.processors_low, CATEGORY.painting_low]}
|
||||||
|
# Forces balancers, tunnel, and trash to not appear in regen to make UT more accurate
|
||||||
|
self.options.early_balancer_tunnel_and_trash.value = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
# "MAM" goal is supposed to be longer than vanilla, but to not have more options than necessary,
|
||||||
|
# both goal amounts for "MAM" and "Even fasterer" are set in a single option.
|
||||||
|
if self.options.goal == GOALS.mam and self.options.goal_amount < 27:
|
||||||
|
raise OptionError(self.player_name
|
||||||
|
+ ": When setting goal to 1 ('mam'), goal_amount must be at least 27 and not "
|
||||||
|
+ str(self.options.goal_amount.value))
|
||||||
|
|
||||||
|
# If lock_belt_and_extractor is true, the only sphere 1 locations will be achievements
|
||||||
|
if self.options.lock_belt_and_extractor and not self.options.include_achievements:
|
||||||
|
raise OptionError(self.player_name + ": Achievements must be included when belt and extractor are locked")
|
||||||
|
|
||||||
|
# Determines maxlevel and finaltier, which are needed for location and item generation
|
||||||
|
if self.options.goal == GOALS.vanilla:
|
||||||
|
self.maxlevel = 25
|
||||||
|
self.finaltier = 8
|
||||||
|
elif self.options.goal == GOALS.mam:
|
||||||
|
self.maxlevel = self.options.goal_amount - 1
|
||||||
|
self.finaltier = 8
|
||||||
|
elif self.options.goal == GOALS.even_fasterer:
|
||||||
|
self.maxlevel = 26
|
||||||
|
self.finaltier = self.options.goal_amount.value
|
||||||
|
else: # goal == efficiency_iii
|
||||||
|
self.maxlevel = 26
|
||||||
|
self.finaltier = 8
|
||||||
|
|
||||||
|
# Setting the seed for the game before any other randomization call is done
|
||||||
|
self.client_seed = self.random.randint(0, 100000)
|
||||||
|
|
||||||
|
# Determines the order of buildings for levels logic
|
||||||
|
if self.options.randomize_level_requirements:
|
||||||
|
self.level_logic_type = self.options.randomize_level_logic.current_key
|
||||||
|
if self.level_logic_type.endswith(OPTIONS.logic_shuffled) or self.level_logic_type == OPTIONS.logic_dopamine:
|
||||||
|
vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
|
||||||
|
while len(vanilla_list) > 0:
|
||||||
|
index = self.random.randint(0, len(vanilla_list)-1)
|
||||||
|
next_building = vanilla_list.pop(index)
|
||||||
|
if next_building == ITEMS.cutter:
|
||||||
|
vanilla_list.append(ITEMS.rotator)
|
||||||
|
if next_building == ITEMS.painter:
|
||||||
|
vanilla_list.append(ITEMS.color_mixer)
|
||||||
|
self.level_logic.append(next_building)
|
||||||
|
else:
|
||||||
|
self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||||
|
else:
|
||||||
|
self.level_logic_type = OPTIONS.logic_vanilla
|
||||||
|
self.level_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||||
|
|
||||||
|
# Determines the order of buildings for upgrades logic
|
||||||
|
if self.options.randomize_upgrade_requirements:
|
||||||
|
self.upgrade_logic_type = self.options.randomize_upgrade_logic.current_key
|
||||||
|
if self.upgrade_logic_type == OPTIONS.logic_hardcore:
|
||||||
|
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||||
|
elif self.upgrade_logic_type == OPTIONS.logic_category:
|
||||||
|
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.stacker, ITEMS.painter, ITEMS.color_mixer]
|
||||||
|
else:
|
||||||
|
vanilla_list = [ITEMS.cutter, ITEMS.painter, ITEMS.stacker]
|
||||||
|
while len(vanilla_list) > 0:
|
||||||
|
index = self.random.randint(0, len(vanilla_list)-1)
|
||||||
|
next_building = vanilla_list.pop(index)
|
||||||
|
if next_building == ITEMS.cutter:
|
||||||
|
vanilla_list.append(ITEMS.rotator)
|
||||||
|
if next_building == ITEMS.painter:
|
||||||
|
vanilla_list.append(ITEMS.color_mixer)
|
||||||
|
self.upgrade_logic.append(next_building)
|
||||||
|
else:
|
||||||
|
self.upgrade_logic_type = OPTIONS.logic_vanilla_like
|
||||||
|
self.upgrade_logic = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||||
|
|
||||||
|
# Determine lenghts of phases in level logic type "random"
|
||||||
|
self.random_logic_phase_length = [1, 1, 1, 1, 1]
|
||||||
|
if self.level_logic_type.startswith(OPTIONS.logic_random_steps):
|
||||||
|
remaininglength = self.maxlevel - 1
|
||||||
|
for phase in range(0, 5):
|
||||||
|
if self.random.random() < 0.1: # Make sure that longer phases are less frequent
|
||||||
|
self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength)
|
||||||
|
else:
|
||||||
|
self.random_logic_phase_length[phase] = self.random.randint(0, remaininglength // (6 - phase))
|
||||||
|
remaininglength -= self.random_logic_phase_length[phase]
|
||||||
|
|
||||||
|
# Determine amount of needed buildings for each category in upgrade logic type "category_random"
|
||||||
|
self.category_random_logic_amounts = {CATEGORY.belt_low: 0, CATEGORY.miner_low: 1,
|
||||||
|
CATEGORY.processors_low: 2, CATEGORY.painting_low: 3}
|
||||||
|
if self.upgrade_logic_type == OPTIONS.logic_category_random:
|
||||||
|
cats = [CATEGORY.belt_low, CATEGORY.miner_low, CATEGORY.processors_low, CATEGORY.painting_low]
|
||||||
|
nextcat = self.random.choice(cats)
|
||||||
|
self.category_random_logic_amounts[nextcat] = 0
|
||||||
|
cats.remove(nextcat)
|
||||||
|
for cat in cats:
|
||||||
|
self.category_random_logic_amounts[cat] = self.random.randint(0, 5)
|
||||||
|
|
||||||
|
def create_item(self, name: str) -> Item:
|
||||||
|
return ShapezItem(name, item_table[name](self.options), self.item_name_to_id[name], self.player)
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
return filler(self.random.random(), bool(self.options.include_whacky_upgrades))
|
||||||
|
|
||||||
|
def append_shapesanity(self, name: str) -> None:
|
||||||
|
"""This method is given as a parameter when creating the locations for shapesanity."""
|
||||||
|
self.shapesanity_names.append(name)
|
||||||
|
|
||||||
|
def add_alias(self, location_name: str, alias: str):
|
||||||
|
"""This method is given as a parameter when locations with helpful aliases for UT are created."""
|
||||||
|
if self.ut_active:
|
||||||
|
self.location_id_to_alias[self.location_name_to_id[location_name]] = alias
|
||||||
|
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
# Create list of all included level and upgrade locations based on player options
|
||||||
|
# This already includes the region to be placed in and the LocationProgressType
|
||||||
|
self.included_locations = {**addlevels(self.maxlevel, self.level_logic_type,
|
||||||
|
self.random_logic_phase_length),
|
||||||
|
**addupgrades(self.finaltier, self.upgrade_logic_type,
|
||||||
|
self.category_random_logic_amounts)}
|
||||||
|
|
||||||
|
# Add shapesanity to included location and creates the corresponding list based on player options
|
||||||
|
if self.ut_active:
|
||||||
|
self.shapesanity_names = self.passthrough[SLOTDATA.shapesanity]
|
||||||
|
self.included_locations.update(addshapesanity_ut(self.shapesanity_names, self.add_alias))
|
||||||
|
else:
|
||||||
|
self.included_locations.update(addshapesanity(self.options.shapesanity_amount.value, self.random,
|
||||||
|
self.append_shapesanity, self.add_alias))
|
||||||
|
|
||||||
|
# Add achievements to included locations based on player options
|
||||||
|
if self.options.include_achievements:
|
||||||
|
self.included_locations.update(addachievements(
|
||||||
|
bool(self.options.exclude_softlock_achievements), bool(self.options.exclude_long_playtime_achievements),
|
||||||
|
bool(self.options.exclude_progression_unreasonable), self.maxlevel, self.upgrade_logic_type,
|
||||||
|
self.category_random_logic_amounts, self.options.goal.current_key, self.included_locations,
|
||||||
|
self.add_alias, self.upgrade_traps_allowed))
|
||||||
|
|
||||||
|
# Save the final amount of to-be-filled locations
|
||||||
|
self.location_count = len(self.included_locations)
|
||||||
|
|
||||||
|
# Create regions and entrances based on included locations and player options
|
||||||
|
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
|
||||||
|
bool(self.options.allow_floating_layers.value),
|
||||||
|
self.included_locations, self.location_name_to_id,
|
||||||
|
self.level_logic, self.upgrade_logic,
|
||||||
|
self.options.early_balancer_tunnel_and_trash.current_key,
|
||||||
|
self.options.goal.current_key))
|
||||||
|
|
||||||
|
def create_items(self) -> None:
|
||||||
|
# Include guaranteed items (game mechanic unlocks and 7x4 big upgrades)
|
||||||
|
included_items: List[Item] = ([self.create_item(name) for name in buildings_processing.keys()]
|
||||||
|
+ [self.create_item(name) for name in buildings_routing.keys()]
|
||||||
|
+ [self.create_item(name) for name in buildings_other.keys()]
|
||||||
|
+ [self.create_item(name) for name in buildings_top_row.keys()]
|
||||||
|
+ [self.create_item(name) for name in buildings_wires.keys()]
|
||||||
|
+ [self.create_item(name) for name in gameplay_unlocks.keys()]
|
||||||
|
+ [self.create_item(name) for name in big_upgrades for _ in range(7)])
|
||||||
|
|
||||||
|
if not self.options.lock_belt_and_extractor:
|
||||||
|
for name in belt_and_extractor:
|
||||||
|
self.multiworld.push_precollected(self.create_item(name))
|
||||||
|
else: # This also requires self.options.include_achievements to be true
|
||||||
|
included_items.extend([self.create_item(name) for name in belt_and_extractor.keys()])
|
||||||
|
|
||||||
|
# Give a detailed error message if there are already more items than available locations.
|
||||||
|
# At the moment, this won't happen, but it's better for debugging in case a future update breaks things.
|
||||||
|
if len(included_items) > self.location_count:
|
||||||
|
raise RuntimeError(self.player_name + ": There are more guaranteed items than available locations")
|
||||||
|
|
||||||
|
# Get value from traps probability option and convert to float
|
||||||
|
traps_probability = self.options.traps_percentage/100
|
||||||
|
split_draining = bool(self.options.split_inventory_draining_trap)
|
||||||
|
# Fill remaining locations with fillers
|
||||||
|
for x in range(self.location_count - len(included_items)):
|
||||||
|
if self.random.random() < traps_probability:
|
||||||
|
# Fill with trap
|
||||||
|
included_items.append(self.create_item(trap(self.random.random(), split_draining,
|
||||||
|
self.upgrade_traps_allowed)))
|
||||||
|
else:
|
||||||
|
# Fil with random filler item
|
||||||
|
included_items.append(self.create_item(self.get_filler_item_name()))
|
||||||
|
|
||||||
|
# Add correct number of items to itempool
|
||||||
|
self.multiworld.itempool += included_items
|
||||||
|
|
||||||
|
# Add balancer, tunnel, and trash to early items if player options say so
|
||||||
|
if self.options.early_balancer_tunnel_and_trash == OPTIONS.sphere_1:
|
||||||
|
self.multiworld.early_items[self.player][ITEMS.balancer] = 1
|
||||||
|
self.multiworld.early_items[self.player][ITEMS.tunnel] = 1
|
||||||
|
self.multiworld.early_items[self.player][ITEMS.trash] = 1
|
||||||
|
|
||||||
|
def set_rules(self) -> None:
|
||||||
|
# Levels might need more belt speed if they require throughput per second. As the randomization of what levels
|
||||||
|
# need throughput happens in the client mod, this logic needs to be applied to all levels. This is applied to
|
||||||
|
# every individual level instead of regions, because they would need a much more complex calculation to prevent
|
||||||
|
# softlocks.
|
||||||
|
|
||||||
|
def f(x: int, name: str):
|
||||||
|
# These calculations are taken from the client mod
|
||||||
|
if x < 26:
|
||||||
|
throughput = math.ceil((2.999+x*0.333)*self.options.required_shapes_multiplier/10)
|
||||||
|
else:
|
||||||
|
throughput = min((4+(x-26)*0.25)*self.options.required_shapes_multiplier/10, 200)
|
||||||
|
if throughput/32 >= 1:
|
||||||
|
add_rule(self.get_location(name),
|
||||||
|
lambda state: has_x_belt_multiplier(state, self.player, throughput/32))
|
||||||
|
|
||||||
|
if not self.options.throughput_levels_ratio == 0:
|
||||||
|
f(0, LOCATIONS.level(1, 1))
|
||||||
|
f(19, LOCATIONS.level(20, 1))
|
||||||
|
f(19, LOCATIONS.level(20, 2))
|
||||||
|
for _x in range(self.maxlevel):
|
||||||
|
f(_x, LOCATIONS.level(_x+1))
|
||||||
|
if self.options.goal.current_key in [GOALS.vanilla, GOALS.mam]:
|
||||||
|
f(self.maxlevel, LOCATIONS.goal)
|
||||||
|
|
||||||
|
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||||
|
# Buildings logic; all buildings as individual parameters
|
||||||
|
level_logic_data = {SLOTDATA.level_building(x+1): self.level_logic[x] for x in range(5)}
|
||||||
|
upgrade_logic_data = {SLOTDATA.upgrade_building(x+1): self.upgrade_logic[x] for x in range(5)}
|
||||||
|
# Randomized values for certain logic types
|
||||||
|
logic_type_random_data = {SLOTDATA.phase_length(x): self.random_logic_phase_length[x] for x in range(0, 5)}
|
||||||
|
logic_type_cat_random_data = {SLOTDATA.cat_buildings_amount(cat): self.category_random_logic_amounts[cat]
|
||||||
|
for cat in [CATEGORY.belt_low, CATEGORY.miner_low,
|
||||||
|
CATEGORY.processors_low, CATEGORY.painting_low]}
|
||||||
|
|
||||||
|
# Options that are relevant to the mod
|
||||||
|
option_data = {
|
||||||
|
SLOTDATA.goal: self.options.goal.current_key,
|
||||||
|
SLOTDATA.maxlevel: self.maxlevel,
|
||||||
|
SLOTDATA.finaltier: self.finaltier,
|
||||||
|
SLOTDATA.req_shapes_mult: self.options.required_shapes_multiplier.value,
|
||||||
|
SLOTDATA.allow_float_layers: bool(self.options.allow_floating_layers),
|
||||||
|
SLOTDATA.rand_level_req: bool(self.options.randomize_level_requirements),
|
||||||
|
SLOTDATA.rand_upgrade_req: bool(self.options.randomize_upgrade_requirements),
|
||||||
|
SLOTDATA.rand_level_logic: self.level_logic_type,
|
||||||
|
SLOTDATA.rand_upgrade_logic: self.upgrade_logic_type,
|
||||||
|
SLOTDATA.throughput_levels_ratio: self.options.throughput_levels_ratio.value,
|
||||||
|
SLOTDATA.comp_growth_gradient: self.options.complexity_growth_gradient.value,
|
||||||
|
SLOTDATA.same_late: bool(self.options.same_late_upgrade_requirements),
|
||||||
|
SLOTDATA.toolbar_shuffling: bool(self.options.toolbar_shuffling),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {**level_logic_data, **upgrade_logic_data, **option_data, **logic_type_random_data,
|
||||||
|
**logic_type_cat_random_data, SLOTDATA.seed: self.client_seed,
|
||||||
|
SLOTDATA.shapesanity: self.shapesanity_names}
|
||||||
|
|
||||||
|
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Helper function for Universal Tracker"""
|
||||||
|
return slot_data
|
0
worlds/shapez/common/__init__.py
Normal file
0
worlds/shapez/common/__init__.py
Normal file
190
worlds/shapez/common/options.py
Normal file
190
worlds/shapez/common/options.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import random
|
||||||
|
import typing
|
||||||
|
|
||||||
|
from Options import FreeText, NumericOption
|
||||||
|
|
||||||
|
|
||||||
|
class FloatRangeText(FreeText, NumericOption):
|
||||||
|
"""FreeText option optimized for entering float numbers.
|
||||||
|
Supports everything that Range supports.
|
||||||
|
range_start and range_end have to be floats, while default has to be a string."""
|
||||||
|
|
||||||
|
default = "0.0"
|
||||||
|
value: float
|
||||||
|
range_start: float = 0.0
|
||||||
|
range_end: float = 1.0
|
||||||
|
|
||||||
|
def __init__(self, value: str):
|
||||||
|
super().__init__(value)
|
||||||
|
value = value.lower()
|
||||||
|
if value.startswith("random"):
|
||||||
|
self.value = self.weighted_range(value)
|
||||||
|
elif value == "default" and hasattr(self, "default"):
|
||||||
|
self.value = float(self.default)
|
||||||
|
elif value == "high":
|
||||||
|
self.value = self.range_end
|
||||||
|
elif value == "low":
|
||||||
|
self.value = self.range_start
|
||||||
|
elif self.range_start == 0.0 \
|
||||||
|
and hasattr(self, "default") \
|
||||||
|
and self.default != "0.0" \
|
||||||
|
and value in ("true", "false"):
|
||||||
|
# these are the conditions where "true" and "false" make sense
|
||||||
|
if value == "true":
|
||||||
|
self.value = float(self.default)
|
||||||
|
else: # "false"
|
||||||
|
self.value = 0.0
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.value = float(value)
|
||||||
|
except ValueError:
|
||||||
|
raise Exception(f"Invalid value for option {self.__class__.__name__}: {value}")
|
||||||
|
except OverflowError:
|
||||||
|
raise Exception(f"Out of range floating value for option {self.__class__.__name__}: {value}")
|
||||||
|
if self.value < self.range_start:
|
||||||
|
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||||
|
if self.value > self.range_end:
|
||||||
|
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_text(cls, text: str) -> typing.Any:
|
||||||
|
return cls(text)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def weighted_range(cls, text: str) -> float:
|
||||||
|
if text == "random-low":
|
||||||
|
return random.triangular(cls.range_start, cls.range_end, cls.range_start)
|
||||||
|
elif text == "random-high":
|
||||||
|
return random.triangular(cls.range_start, cls.range_end, cls.range_end)
|
||||||
|
elif text == "random-middle":
|
||||||
|
return random.triangular(cls.range_start, cls.range_end)
|
||||||
|
elif text.startswith("random-range-"):
|
||||||
|
return cls.custom_range(text)
|
||||||
|
elif text == "random":
|
||||||
|
return random.uniform(cls.range_start, cls.range_end)
|
||||||
|
else:
|
||||||
|
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||||
|
f"Acceptable values are: random, random-high, random-middle, random-low, "
|
||||||
|
f"random-range-low-<min>-<max>, random-range-middle-<min>-<max>, "
|
||||||
|
f"random-range-high-<min>-<max>, or random-range-<min>-<max>.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def custom_range(cls, text: str) -> float:
|
||||||
|
textsplit = text.split("-")
|
||||||
|
try:
|
||||||
|
random_range = [float(textsplit[len(textsplit) - 2]), float(textsplit[len(textsplit) - 1])]
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||||
|
except OverflowError:
|
||||||
|
raise Exception(f"Out of range floating value for option {cls.__name__}: {text}")
|
||||||
|
random_range.sort()
|
||||||
|
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
|
||||||
|
raise Exception(
|
||||||
|
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||||
|
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||||
|
if text.startswith("random-range-low"):
|
||||||
|
return random.triangular(random_range[0], random_range[1], random_range[0])
|
||||||
|
elif text.startswith("random-range-middle"):
|
||||||
|
return random.triangular(random_range[0], random_range[1])
|
||||||
|
elif text.startswith("random-range-high"):
|
||||||
|
return random.triangular(random_range[0], random_range[1], random_range[1])
|
||||||
|
else:
|
||||||
|
return random.uniform(random_range[0], random_range[1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_key(self) -> str:
|
||||||
|
return str(self.value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_option_name(cls, value: float) -> str:
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def __eq__(self, other: typing.Any):
|
||||||
|
if isinstance(other, NumericOption):
|
||||||
|
return self.value == other.value
|
||||||
|
else:
|
||||||
|
return typing.cast(bool, self.value == other)
|
||||||
|
|
||||||
|
def __lt__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||||
|
if isinstance(other, NumericOption):
|
||||||
|
return self.value < other.value
|
||||||
|
else:
|
||||||
|
return self.value < other
|
||||||
|
|
||||||
|
def __le__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||||
|
if isinstance(other, NumericOption):
|
||||||
|
return self.value <= other.value
|
||||||
|
else:
|
||||||
|
return self.value <= other
|
||||||
|
|
||||||
|
def __gt__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||||
|
if isinstance(other, NumericOption):
|
||||||
|
return self.value > other.value
|
||||||
|
else:
|
||||||
|
return self.value > other
|
||||||
|
|
||||||
|
def __ge__(self, other: typing.Union[int, float, NumericOption]) -> bool:
|
||||||
|
if isinstance(other, NumericOption):
|
||||||
|
return self.value >= other.value
|
||||||
|
else:
|
||||||
|
return self.value >= other
|
||||||
|
|
||||||
|
def __int__(self) -> int:
|
||||||
|
return int(self.value)
|
||||||
|
|
||||||
|
def __and__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("& operator not supported for float values")
|
||||||
|
|
||||||
|
def __floordiv__(self, other: typing.Any) -> int:
|
||||||
|
return int(self.value // float(other))
|
||||||
|
|
||||||
|
def __invert__(self) -> int:
|
||||||
|
raise TypeError("~ operator not supported for float values")
|
||||||
|
|
||||||
|
def __lshift__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("<< operator not supported for float values")
|
||||||
|
|
||||||
|
def __mod__(self, other: typing.Any) -> float:
|
||||||
|
return self.value % float(other)
|
||||||
|
|
||||||
|
def __neg__(self) -> float:
|
||||||
|
return -self.value
|
||||||
|
|
||||||
|
def __or__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("| operator not supported for float values")
|
||||||
|
|
||||||
|
def __pos__(self) -> float:
|
||||||
|
return +self.value
|
||||||
|
|
||||||
|
def __rand__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("& operator not supported for float values")
|
||||||
|
|
||||||
|
def __rfloordiv__(self, other: typing.Any) -> int:
|
||||||
|
return int(float(other) // self.value)
|
||||||
|
|
||||||
|
def __rlshift__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("<< operator not supported for float values")
|
||||||
|
|
||||||
|
def __rmod__(self, other: typing.Any) -> float:
|
||||||
|
return float(other) % self.value
|
||||||
|
|
||||||
|
def __ror__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("| operator not supported for float values")
|
||||||
|
|
||||||
|
def __round__(self, ndigits: typing.Optional[int] = None) -> float:
|
||||||
|
return round(self.value, ndigits)
|
||||||
|
|
||||||
|
def __rpow__(self, base: typing.Any) -> typing.Any:
|
||||||
|
return base ** self.value
|
||||||
|
|
||||||
|
def __rrshift__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError(">> operator not supported for float values")
|
||||||
|
|
||||||
|
def __rshift__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError(">> operator not supported for float values")
|
||||||
|
|
||||||
|
def __rxor__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("^ operator not supported for float values")
|
||||||
|
|
||||||
|
def __xor__(self, other: typing.Any) -> int:
|
||||||
|
raise TypeError("^ operator not supported for float values")
|
0
worlds/shapez/data/__init__.py
Normal file
0
worlds/shapez/data/__init__.py
Normal file
134
worlds/shapez/data/generate.py
Normal file
134
worlds/shapez/data/generate.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import itertools
|
||||||
|
import time
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from worlds.shapez.data.strings import SHAPESANITY, REGIONS
|
||||||
|
|
||||||
|
shapesanity_simple: Dict[str, str] = {}
|
||||||
|
shapesanity_1_4: Dict[str, str] = {}
|
||||||
|
shapesanity_two_sided: Dict[str, str] = {}
|
||||||
|
shapesanity_three_parts: Dict[str, str] = {}
|
||||||
|
shapesanity_four_parts: Dict[str, str] = {}
|
||||||
|
subshape_names = [SHAPESANITY.circle, SHAPESANITY.square, SHAPESANITY.star, SHAPESANITY.windmill]
|
||||||
|
color_names = [SHAPESANITY.red, SHAPESANITY.blue, SHAPESANITY.green, SHAPESANITY.yellow, SHAPESANITY.purple,
|
||||||
|
SHAPESANITY.cyan, SHAPESANITY.white, SHAPESANITY.uncolored]
|
||||||
|
short_subshapes = ["C", "R", "S", "W"]
|
||||||
|
short_colors = ["b", "c", "g", "p", "r", "u", "w", "y"]
|
||||||
|
|
||||||
|
|
||||||
|
def color_to_needed_building(color_list: List[str]) -> str:
|
||||||
|
for next_color in color_list:
|
||||||
|
if next_color in [SHAPESANITY.yellow, SHAPESANITY.purple, SHAPESANITY.cyan, SHAPESANITY.white,
|
||||||
|
"y", "p", "c", "w"]:
|
||||||
|
return REGIONS.mixed
|
||||||
|
for next_color in color_list:
|
||||||
|
if next_color not in [SHAPESANITY.uncolored, "u"]:
|
||||||
|
return REGIONS.painted
|
||||||
|
return REGIONS.uncol
|
||||||
|
|
||||||
|
|
||||||
|
def generate_shapesanity_pool() -> None:
|
||||||
|
# same shapes && same color
|
||||||
|
for color in color_names:
|
||||||
|
color_region = color_to_needed_building([color])
|
||||||
|
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.circle)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.square)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.star)] = REGIONS.sanity(REGIONS.full, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.full(color, SHAPESANITY.windmill)] = REGIONS.sanity(REGIONS.east_wind, color_region)
|
||||||
|
for shape in subshape_names:
|
||||||
|
for color in color_names:
|
||||||
|
color_region = color_to_needed_building([color])
|
||||||
|
shapesanity_simple[SHAPESANITY.half(color, shape)] = REGIONS.sanity(REGIONS.half, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.piece(color, shape)] = REGIONS.sanity(REGIONS.piece, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.cutout(color, shape)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
shapesanity_simple[SHAPESANITY.cornered(color, shape)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
|
||||||
|
# one color && 4 shapes (including empty)
|
||||||
|
for first_color, second_color, third_color, fourth_color in itertools.combinations(short_colors+["-"], 4):
|
||||||
|
colors = [first_color, second_color, third_color, fourth_color]
|
||||||
|
color_region = color_to_needed_building(colors)
|
||||||
|
shape_regions = [REGIONS.stitched, REGIONS.stitched] if fourth_color == "-" else [REGIONS.col_full, REGIONS.col_east_wind]
|
||||||
|
color_code = ''.join(colors)
|
||||||
|
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.circle)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.square)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.star)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_1_4[SHAPESANITY.full(color_code, SHAPESANITY.windmill)] = REGIONS.sanity(shape_regions[1], color_region)
|
||||||
|
|
||||||
|
# one shape && 4 colors (including empty)
|
||||||
|
for first_shape, second_shape, third_shape, fourth_shape in itertools.combinations(short_subshapes+["-"], 4):
|
||||||
|
for color in color_names:
|
||||||
|
shapesanity_1_4[SHAPESANITY.full(color, ''.join([first_shape, second_shape, third_shape, fourth_shape]))] \
|
||||||
|
= REGIONS.sanity(REGIONS.stitched, color_to_needed_building([color]))
|
||||||
|
|
||||||
|
combos = [shape + color for shape in short_subshapes for color in short_colors]
|
||||||
|
for first_combo, second_combo in itertools.permutations(combos, 2):
|
||||||
|
# 2-sided shapes
|
||||||
|
color_region = color_to_needed_building([first_combo[1], second_combo[1]])
|
||||||
|
ordered_combo = " ".join(sorted([first_combo, second_combo]))
|
||||||
|
shape_regions = (([REGIONS.east_wind, REGIONS.east_wind, REGIONS.col_half]
|
||||||
|
if first_combo[0] == "W" else [REGIONS.col_full, REGIONS.col_full, REGIONS.col_half])
|
||||||
|
if first_combo[0] == second_combo[0] else [REGIONS.stitched, REGIONS.half_half, REGIONS.stitched])
|
||||||
|
shapesanity_two_sided[SHAPESANITY.three_one(first_combo, second_combo)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.halfhalf(ordered_combo)] = REGIONS.sanity(shape_regions[1], color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.checkered(ordered_combo)] = REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.singles(ordered_combo, SHAPESANITY.adjacent_pos)] = REGIONS.sanity(shape_regions[2], color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.singles(ordered_combo, SHAPESANITY.cornered_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.two_one(first_combo, second_combo, SHAPESANITY.adjacent_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
shapesanity_two_sided[SHAPESANITY.two_one(first_combo, second_combo, SHAPESANITY.cornered_pos)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
for third_combo in combos:
|
||||||
|
if third_combo in [first_combo, second_combo]:
|
||||||
|
continue
|
||||||
|
# 3-part shapes
|
||||||
|
colors = [first_combo[1], second_combo[1], third_combo[1]]
|
||||||
|
color_region = color_to_needed_building(colors)
|
||||||
|
ordered_two = " ".join(sorted([second_combo, third_combo]))
|
||||||
|
if not (first_combo[1] == second_combo[1] == third_combo[1] or
|
||||||
|
first_combo[0] == second_combo[0] == third_combo[0]):
|
||||||
|
ordered_all = " ".join(sorted([first_combo, second_combo, third_combo]))
|
||||||
|
shapesanity_three_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
shape_regions = ([REGIONS.stitched, REGIONS.stitched] if not second_combo[0] == third_combo[0]
|
||||||
|
else (([REGIONS.east_wind, REGIONS.east_wind] if first_combo[0] == "W"
|
||||||
|
else [REGIONS.col_full, REGIONS.col_full])
|
||||||
|
if first_combo[0] == second_combo[0] else [REGIONS.col_half_half, REGIONS.stitched]))
|
||||||
|
shapesanity_three_parts[SHAPESANITY.two_one_one(first_combo, ordered_two, SHAPESANITY.adjacent_pos)] \
|
||||||
|
= REGIONS.sanity(shape_regions[0], color_region)
|
||||||
|
shapesanity_three_parts[SHAPESANITY.two_one_one(first_combo, ordered_two, SHAPESANITY.cornered_pos)] \
|
||||||
|
= REGIONS.sanity(shape_regions[1], color_region)
|
||||||
|
for fourth_combo in combos:
|
||||||
|
if fourth_combo in [first_combo, second_combo, third_combo]:
|
||||||
|
continue
|
||||||
|
if (first_combo[1] == second_combo[1] == third_combo[1] == fourth_combo[1] or
|
||||||
|
first_combo[0] == second_combo[0] == third_combo[0] == fourth_combo[0]):
|
||||||
|
continue
|
||||||
|
colors = [first_combo[1], second_combo[1], third_combo[1], fourth_combo[1]]
|
||||||
|
color_region = color_to_needed_building(colors)
|
||||||
|
ordered_all = " ".join(sorted([first_combo, second_combo, third_combo, fourth_combo]))
|
||||||
|
if ((first_combo[0] == second_combo[0] and third_combo[0] == fourth_combo[0]) or
|
||||||
|
(first_combo[0] == third_combo[0] and second_combo[0] == fourth_combo[0]) or
|
||||||
|
(first_combo[0] == fourth_combo[0] and third_combo[0] == second_combo[0])):
|
||||||
|
shapesanity_four_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.col_half_half, color_region)
|
||||||
|
else:
|
||||||
|
shapesanity_four_parts[SHAPESANITY.singles(ordered_all)] = REGIONS.sanity(REGIONS.stitched, color_region)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
start = time.time()
|
||||||
|
generate_shapesanity_pool()
|
||||||
|
print(time.time() - start)
|
||||||
|
with open("shapesanity_pool.py", "w") as outfile:
|
||||||
|
outfile.writelines(["shapesanity_simple = {\n"]
|
||||||
|
+ [f" \"{name}\": \"{shapesanity_simple[name]}\",\n"
|
||||||
|
for name in shapesanity_simple]
|
||||||
|
+ ["}\n\nshapesanity_1_4 = {\n"]
|
||||||
|
+ [f" \"{name}\": \"{shapesanity_1_4[name]}\",\n"
|
||||||
|
for name in shapesanity_1_4]
|
||||||
|
+ ["}\n\nshapesanity_two_sided = {\n"]
|
||||||
|
+ [f" \"{name}\": \"{shapesanity_two_sided[name]}\",\n"
|
||||||
|
for name in shapesanity_two_sided]
|
||||||
|
+ ["}\n\nshapesanity_three_parts = {\n"]
|
||||||
|
+ [f" \"{name}\": \"{shapesanity_three_parts[name]}\",\n"
|
||||||
|
for name in shapesanity_three_parts]
|
||||||
|
+ ["}\n\nshapesanity_four_parts = {\n"]
|
||||||
|
+ [f" \"{name}\": \"{shapesanity_four_parts[name]}\",\n"
|
||||||
|
for name in shapesanity_four_parts]
|
||||||
|
+ ["}\n"])
|
4
worlds/shapez/data/options.json
Normal file
4
worlds/shapez/data/options.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"max_levels_and_upgrades": 500,
|
||||||
|
"max_shapesanity": 1000
|
||||||
|
}
|
75814
worlds/shapez/data/shapesanity_pool.py
Normal file
75814
worlds/shapez/data/shapesanity_pool.py
Normal file
File diff suppressed because it is too large
Load Diff
337
worlds/shapez/data/strings.py
Normal file
337
worlds/shapez/data/strings.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
|
||||||
|
class OTHER:
|
||||||
|
game_name = "shapez"
|
||||||
|
|
||||||
|
|
||||||
|
class SLOTDATA:
|
||||||
|
goal = "goal"
|
||||||
|
maxlevel = "maxlevel"
|
||||||
|
finaltier = "finaltier"
|
||||||
|
req_shapes_mult = "required_shapes_multiplier"
|
||||||
|
allow_float_layers = "allow_floating_layers"
|
||||||
|
rand_level_req = "randomize_level_requirements"
|
||||||
|
rand_upgrade_req = "randomize_upgrade_requirements"
|
||||||
|
rand_level_logic = "randomize_level_logic"
|
||||||
|
rand_upgrade_logic = "randomize_upgrade_logic"
|
||||||
|
throughput_levels_ratio = "throughput_levels_ratio"
|
||||||
|
comp_growth_gradient = "complexity_growth_gradient"
|
||||||
|
same_late = "same_late_upgrade_requirements"
|
||||||
|
toolbar_shuffling = "toolbar_shuffling"
|
||||||
|
seed = "seed"
|
||||||
|
shapesanity = "shapesanity"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def level_building(number: int) -> str:
|
||||||
|
return f"Level building {number}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def upgrade_building(number: int) -> str:
|
||||||
|
return f"Upgrade building {number}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def phase_length(number: int) -> str:
|
||||||
|
return f"Phase {number} length"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cat_buildings_amount(category: str) -> str:
|
||||||
|
return f"{category} category buildings amount"
|
||||||
|
|
||||||
|
|
||||||
|
class GOALS:
|
||||||
|
vanilla = "vanilla"
|
||||||
|
mam = "mam"
|
||||||
|
even_fasterer = "even_fasterer"
|
||||||
|
efficiency_iii = "efficiency_iii"
|
||||||
|
|
||||||
|
|
||||||
|
class CATEGORY:
|
||||||
|
belt = "Belt"
|
||||||
|
miner = "Miner"
|
||||||
|
processors = "Processors"
|
||||||
|
painting = "Painting"
|
||||||
|
random = "Random"
|
||||||
|
belt_low = "belt"
|
||||||
|
miner_low = "miner"
|
||||||
|
processors_low = "processors"
|
||||||
|
painting_low = "painting"
|
||||||
|
big = "Big"
|
||||||
|
small = "Small"
|
||||||
|
gigantic = "Gigantic"
|
||||||
|
rising = "Rising"
|
||||||
|
demonic = "Demonic"
|
||||||
|
|
||||||
|
|
||||||
|
class OPTIONS:
|
||||||
|
logic_vanilla = "vanilla"
|
||||||
|
logic_stretched = "stretched"
|
||||||
|
logic_quick = "quick"
|
||||||
|
logic_random_steps = "random_steps"
|
||||||
|
logic_hardcore = "hardcore"
|
||||||
|
logic_dopamine = "dopamine"
|
||||||
|
logic_dopamine_overflow = "dopamine_overflow"
|
||||||
|
logic_vanilla_like = "vanilla_like"
|
||||||
|
logic_linear = "linear"
|
||||||
|
logic_category = "category"
|
||||||
|
logic_category_random = "category_random"
|
||||||
|
logic_shuffled = "shuffled"
|
||||||
|
sphere_1 = "sphere_1"
|
||||||
|
buildings_3 = "3_buildings"
|
||||||
|
buildings_5 = "5_buildings"
|
||||||
|
|
||||||
|
|
||||||
|
class REGIONS:
|
||||||
|
menu = "Menu"
|
||||||
|
belt = "Shape transportation"
|
||||||
|
extract = "Shape extraction"
|
||||||
|
main = "Main"
|
||||||
|
levels_1 = "Levels with 1 building"
|
||||||
|
levels_2 = "Levels with 2 buildings"
|
||||||
|
levels_3 = "Levels with 3 buildings"
|
||||||
|
levels_4 = "Levels with 4 buildings"
|
||||||
|
levels_5 = "Levels with 5 buildings"
|
||||||
|
upgrades_1 = "Upgrades with 1 building"
|
||||||
|
upgrades_2 = "Upgrades with 2 buildings"
|
||||||
|
upgrades_3 = "Upgrades with 3 buildings"
|
||||||
|
upgrades_4 = "Upgrades with 4 buildings"
|
||||||
|
upgrades_5 = "Upgrades with 5 buildings"
|
||||||
|
paint_not_quad = "Achievements with (double) painter"
|
||||||
|
cut_not_quad = "Achievements with half cutter"
|
||||||
|
rotate_cw = "Achievements with clockwise rotator"
|
||||||
|
stack_shape = "Achievements with stacker"
|
||||||
|
store_shape = "Achievements with storage"
|
||||||
|
trash_shape = "Achievements with trash"
|
||||||
|
blueprint = "Achievements with blueprints"
|
||||||
|
wiring = "Achievements with wires"
|
||||||
|
mam = "Achievements needing a MAM"
|
||||||
|
any_building = "Achievements with any placeable building"
|
||||||
|
all_buildings = "Achievements with all main buildings"
|
||||||
|
all_buildings_x1_6_belt = "Achievements with x1.6 belt speed"
|
||||||
|
full = "Full"
|
||||||
|
half = "Half"
|
||||||
|
piece = "Piece"
|
||||||
|
stitched = "Stitched"
|
||||||
|
east_wind = "East Windmill"
|
||||||
|
half_half = "Half-Half"
|
||||||
|
col_east_wind = "Colorful East Windmill"
|
||||||
|
col_half_half = "Colorful Half-Half"
|
||||||
|
col_full = "Colorful Full"
|
||||||
|
col_half = "Colorful Half"
|
||||||
|
uncol = "Uncolored"
|
||||||
|
painted = "Painted"
|
||||||
|
mixed = "Mixed"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sanity(processing: str, coloring: str):
|
||||||
|
return f"Shapesanity {processing} {coloring}"
|
||||||
|
|
||||||
|
|
||||||
|
class LOCATIONS:
|
||||||
|
my_eyes = "My eyes no longer hurt"
|
||||||
|
painter = "Painter"
|
||||||
|
cutter = "Cutter"
|
||||||
|
rotater = "Rotater"
|
||||||
|
wait_they_stack = "Wait, they stack?"
|
||||||
|
wires = "Wires"
|
||||||
|
storage = "Storage"
|
||||||
|
freedom = "Freedom"
|
||||||
|
the_logo = "The logo!"
|
||||||
|
to_the_moon = "To the moon"
|
||||||
|
its_piling_up = "It's piling up"
|
||||||
|
use_it_later = "I'll use it later"
|
||||||
|
efficiency_1 = "Efficiency 1"
|
||||||
|
preparing_to_launch = "Preparing to launch"
|
||||||
|
spacey = "SpaceY"
|
||||||
|
stack_overflow = "Stack overflow"
|
||||||
|
its_a_mess = "It's a mess"
|
||||||
|
faster = "Faster"
|
||||||
|
even_faster = "Even faster"
|
||||||
|
get_rid_of_them = "Get rid of them"
|
||||||
|
a_long_time = "It's been a long time"
|
||||||
|
addicted = "Addicted"
|
||||||
|
cant_stop = "Can't stop"
|
||||||
|
is_this_the_end = "Is this the end?"
|
||||||
|
getting_into_it = "Getting into it"
|
||||||
|
now_its_easy = "Now it's easy"
|
||||||
|
computer_guy = "Computer Guy"
|
||||||
|
speedrun_master = "Speedrun Master"
|
||||||
|
speedrun_novice = "Speedrun Novice"
|
||||||
|
not_idle_game = "Not an idle game"
|
||||||
|
efficiency_2 = "Efficiency 2"
|
||||||
|
branding_1 = "Branding specialist 1"
|
||||||
|
branding_2 = "Branding specialist 2"
|
||||||
|
king_of_inefficiency = "King of Inefficiency"
|
||||||
|
its_so_slow = "It's so slow"
|
||||||
|
mam = "MAM (Make Anything Machine)"
|
||||||
|
perfectionist = "Perfectionist"
|
||||||
|
next_dimension = "The next dimension"
|
||||||
|
oops = "Oops"
|
||||||
|
copy_pasta = "Copy-Pasta"
|
||||||
|
ive_seen_that_before = "I've seen that before ..."
|
||||||
|
memories = "Memories from the past"
|
||||||
|
i_need_trains = "I need trains"
|
||||||
|
a_bit_early = "A bit early?"
|
||||||
|
gps = "GPS"
|
||||||
|
goal = "Goal"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def level(number: int, additional: int = 0) -> str:
|
||||||
|
if not additional:
|
||||||
|
return f"Level {number}"
|
||||||
|
elif additional == 1:
|
||||||
|
return f"Level {number} Additional"
|
||||||
|
else:
|
||||||
|
return f"Level {number} Additional {additional}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def upgrade(category: str, tier: str) -> str:
|
||||||
|
return f"{category} Upgrade Tier {tier}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shapesanity(number: int) -> str:
|
||||||
|
return f"Shapesanity {number}"
|
||||||
|
|
||||||
|
|
||||||
|
class ITEMS:
|
||||||
|
cutter = "Cutter"
|
||||||
|
cutter_quad = "Quad Cutter"
|
||||||
|
rotator = "Rotator"
|
||||||
|
rotator_ccw = "Rotator (CCW)"
|
||||||
|
rotator_180 = "Rotator (180°)"
|
||||||
|
stacker = "Stacker"
|
||||||
|
painter = "Painter"
|
||||||
|
painter_double = "Double Painter"
|
||||||
|
painter_quad = "Quad Painter"
|
||||||
|
color_mixer = "Color Mixer"
|
||||||
|
|
||||||
|
belt = "Belt"
|
||||||
|
extractor = "Extractor"
|
||||||
|
extractor_chain = "Chaining Extractor"
|
||||||
|
balancer = "Balancer"
|
||||||
|
comp_merger = "Compact Merger"
|
||||||
|
comp_splitter = "Compact Splitter"
|
||||||
|
tunnel = "Tunnel"
|
||||||
|
tunnel_tier_ii = "Tunnel Tier II"
|
||||||
|
trash = "Trash"
|
||||||
|
|
||||||
|
belt_reader = "Belt Reader"
|
||||||
|
storage = "Storage"
|
||||||
|
switch = "Switch"
|
||||||
|
item_filter = "Item Filter"
|
||||||
|
display = "Display"
|
||||||
|
wires = "Wires"
|
||||||
|
const_signal = "Constant Signal"
|
||||||
|
logic_gates = "Logic Gates"
|
||||||
|
virtual_proc = "Virtual Processing"
|
||||||
|
blueprints = "Blueprints"
|
||||||
|
|
||||||
|
upgrade_big_belt = "Big Belt Upgrade"
|
||||||
|
upgrade_big_miner = "Big Miner Upgrade"
|
||||||
|
upgrade_big_proc = "Big Processors Upgrade"
|
||||||
|
upgrade_big_paint = "Big Painting Upgrade"
|
||||||
|
upgrade_small_belt = "Small Belt Upgrade"
|
||||||
|
upgrade_small_miner = "Small Miner Upgrade"
|
||||||
|
upgrade_small_proc = "Small Processors Upgrade"
|
||||||
|
upgrade_small_paint = "Small Painting Upgrade"
|
||||||
|
upgrade_gigantic_belt = "Gigantic Belt Upgrade"
|
||||||
|
upgrade_gigantic_miner = "Gigantic Miner Upgrade"
|
||||||
|
upgrade_gigantic_proc = "Gigantic Processors Upgrade"
|
||||||
|
upgrade_gigantic_paint = "Gigantic Painting Upgrade"
|
||||||
|
upgrade_rising_belt = "Rising Belt Upgrade"
|
||||||
|
upgrade_rising_miner = "Rising Miner Upgrade"
|
||||||
|
upgrade_rising_proc = "Rising Processors Upgrade"
|
||||||
|
upgrade_rising_paint = "Rising Painting Upgrade"
|
||||||
|
trap_upgrade_belt = "Belt Upgrade Trap"
|
||||||
|
trap_upgrade_miner = "Miner Upgrade Trap"
|
||||||
|
trap_upgrade_proc = "Processors Upgrade Trap"
|
||||||
|
trap_upgrade_paint = "Painting Upgrade Trap"
|
||||||
|
trap_upgrade_demonic_belt = "Demonic Belt Upgrade Trap"
|
||||||
|
trap_upgrade_demonic_miner = "Demonic Miner Upgrade Trap"
|
||||||
|
trap_upgrade_demonic_proc = "Demonic Processors Upgrade Trap"
|
||||||
|
trap_upgrade_demonic_paint = "Demonic Painting Upgrade Trap"
|
||||||
|
upgrade_big_random = "Big Random Upgrade"
|
||||||
|
upgrade_small_random = "Small Random Upgrade"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def upgrade(size: str, category: str) -> str:
|
||||||
|
return f"{size} {category} Upgrade"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def trap_upgrade(category: str, size: str = "") -> str:
|
||||||
|
return f"{size} {category} Upgrade Trap".strip()
|
||||||
|
|
||||||
|
bundle_blueprint = "Blueprint Shapes Bundle"
|
||||||
|
bundle_level = "Level Shapes Bundle"
|
||||||
|
bundle_upgrade = "Upgrade Shapes Bundle"
|
||||||
|
|
||||||
|
trap_locked = "Locked Building Trap"
|
||||||
|
trap_throttled = "Throttled Building Trap"
|
||||||
|
trap_malfunction = "Malfunctioning Trap"
|
||||||
|
trap_inflation = "Inflation Trap"
|
||||||
|
trap_draining_inv = "Inventory Draining Trap"
|
||||||
|
trap_draining_blueprint = "Blueprint Shapes Draining Trap"
|
||||||
|
trap_draining_level = "Level Shapes Draining Trap"
|
||||||
|
trap_draining_upgrade = "Upgrade Shapes Draining Trap"
|
||||||
|
trap_clear_belts = "Belts Clearing Trap"
|
||||||
|
|
||||||
|
goal = "Goal"
|
||||||
|
|
||||||
|
|
||||||
|
class SHAPESANITY:
|
||||||
|
circle = "Circle"
|
||||||
|
square = "Square"
|
||||||
|
star = "Star"
|
||||||
|
windmill = "Windmill"
|
||||||
|
red = "Red"
|
||||||
|
blue = "Blue"
|
||||||
|
green = "Green"
|
||||||
|
yellow = "Yellow"
|
||||||
|
purple = "Purple"
|
||||||
|
cyan = "Cyan"
|
||||||
|
white = "White"
|
||||||
|
uncolored = "Uncolored"
|
||||||
|
adjacent_pos = "Adjacent"
|
||||||
|
cornered_pos = "Cornered"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def full(color: str, subshape: str):
|
||||||
|
return f"{color} {subshape}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def half(color: str, subshape: str):
|
||||||
|
return f"Half {color} {subshape}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def piece(color: str, subshape: str):
|
||||||
|
return f"{color} {subshape} Piece"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cutout(color: str, subshape: str):
|
||||||
|
return f"Cut Out {color} {subshape}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cornered(color: str, subshape: str):
|
||||||
|
return f"Cornered {color} {subshape}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def three_one(first: str, second: str):
|
||||||
|
return f"3-1 {first} {second}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def halfhalf(combo: str):
|
||||||
|
return f"Half-Half {combo}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def checkered(combo: str):
|
||||||
|
return f"Checkered {combo}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def singles(combo: str, position: str = ""):
|
||||||
|
return f"{position} Singles {combo}".strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def two_one(first: str, second: str, position: str):
|
||||||
|
return f"{position} 2-1 {first} {second}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def two_one_one(first: str, second: str, position: str):
|
||||||
|
return f"{position} 2-1-1 {first} {second}"
|
35
worlds/shapez/docs/datapackage_settings_de.md
Normal file
35
worlds/shapez/docs/datapackage_settings_de.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Anleitung zum Ändern der maximalen Anzahl an Locations in shapez
|
||||||
|
|
||||||
|
## Wo finde ich die Einstellungen zum Erhöhen/Verringern der maximalen Anzahl an Locations?
|
||||||
|
|
||||||
|
Die Maximalwerte von `goal_amount` und `shapesanity_amount` sind fest eingebaute Einstellungen, die das Datenpaket des
|
||||||
|
Spiels beeinflussen. Sie sind in einer Datei names `options.json` innerhalb der APWorld festgelegt. Durch das Ändern
|
||||||
|
dieser Werte erschaffst du eine custom APWorld, die nur auf deinem PC existiert.
|
||||||
|
|
||||||
|
## Wie du die Datenpaket-Einstellungen änderst
|
||||||
|
|
||||||
|
Diese Anleitung ist für erfahrene Nutzer und kann in nicht richtig funktionierender Software resultieren, wenn sie nicht
|
||||||
|
ordnungsgemäß befolgt wird. Anwendung auf eigene Gefahr.
|
||||||
|
|
||||||
|
1. Navigiere zu `<AP-Installation>/lib/worlds`.
|
||||||
|
2. Benenne `shapez.apworld` zu `shapez.zip` um.
|
||||||
|
3. Öffne die Zip-Datei und navigiere zu `shapez/data/options.json`.
|
||||||
|
4. Ändere die Werte in dieser Datei nach Belieben und speichere die Datei.
|
||||||
|
- `max_shapesanity` kann nicht weniger als `4` sein, da dies die benötigte Mindestanzahl zum Verhindern von
|
||||||
|
FillErrors ist.
|
||||||
|
- `max_shapesanity` kann auch nicht mehr als `75800` sein, da dies die maximale Anzahl an möglichen Shapesanity-Namen
|
||||||
|
ist. Ansonsten könnte die Generierung der Multiworld fehlschlagen.
|
||||||
|
- `max_levels_and_upgrades` kann nicht weniger als `27` sein, da dies die Mindestanzahl für das `mam`-Ziel ist.
|
||||||
|
5. Schließe die Zip-Datei und benenne sie zurück zu `shapez.apworld`.
|
||||||
|
|
||||||
|
## Warum muss ich das ganze selbst machen?
|
||||||
|
|
||||||
|
Alle Spiele in Archipelago müssen eine Liste aller möglichen Locations **unabhängig der Spieler-Optionen**
|
||||||
|
bereitstellen. Diese Listen aller in einer Multiworld inkludierten Spiele werden in den Daten der Multiworld gespeichert
|
||||||
|
und an alle verbundenen Clients gesendet. Je mehr mögliche Locations, desto größer das Datenpaket. Und mit ~80000
|
||||||
|
möglichen Locations hatte shapez zu einem gewissen Zeitpunkt ein (von der Datenmenge her) größeres Datenpaket als alle
|
||||||
|
supporteten Spiele zusammen. Um also diese Datenmenge zu reduzieren wurden die ausgeschriebenen
|
||||||
|
Shapesanity-Locations-Namen (`Shapesanity Uncolored Circle`, `Shapesanity Blue Rectangle`, ...) durch standardisierte
|
||||||
|
Namen (`Shapesanity 1`, `Shapesanity 2`, ...) ersetzt. Durch das Ändern dieser Maximalwerte, und damit das Erstellen
|
||||||
|
einer custom APWorld, kannst du die Anzahl der möglichen Locations erhöhen, wirst aber auch gleichzeitig das Datenpaket
|
||||||
|
vergrößern.
|
33
worlds/shapez/docs/datapackage_settings_en.md
Normal file
33
worlds/shapez/docs/datapackage_settings_en.md
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Guide to change maximum locations in shapez
|
||||||
|
|
||||||
|
## Where do I find the settings to increase/decrease the amount of possible locations?
|
||||||
|
|
||||||
|
The maximum values of the `goal_amount` and `shapesanity_amount` are hardcoded settings that affect the datapackage.
|
||||||
|
They are stored in a file called `options.json` inside the apworld. By changing them, you will create a custom apworld
|
||||||
|
on your local machine.
|
||||||
|
|
||||||
|
## How to change datapackage options
|
||||||
|
|
||||||
|
This tutorial is for advanced users and can result in the software not working properly, if not read carefully.
|
||||||
|
Proceed at your own risk.
|
||||||
|
|
||||||
|
1. Go to `<AP installation>/lib/worlds`.
|
||||||
|
2. Rename `shapez.apworld` to `shapez.zip`.
|
||||||
|
3. Open the zip file and go to `shapez/data/options.json`.
|
||||||
|
4. Edit the values in this file to your desire and save the file.
|
||||||
|
- `max_shapesanity` cannot be lower than `4`, as this is the minimum amount to prevent FillErrors.
|
||||||
|
- `max_shapesanity` also cannot be higher than `75800`, as this is the maximum amount of possible shapesanity names.
|
||||||
|
Else the multiworld generation might fail.
|
||||||
|
- `max_levels_and_upgrades` cannot be lower than `27`, as this is the minimum amount for the `mam` goal to properly
|
||||||
|
work.
|
||||||
|
5. Close the zip and rename it back to `shapez.apworld`.
|
||||||
|
|
||||||
|
## Why do I have to do this manually?
|
||||||
|
|
||||||
|
For every game in Archipelago, there must be a list of all possible locations, **regardless of player options**. When
|
||||||
|
generating a multiworld, a list of all locations of all included games will be saved in the multiworld data and sent to
|
||||||
|
all clients. The higher the amount of possible locations, the bigger the datapackage. And having ~80000 possible
|
||||||
|
locations at one point made the datapackage for shapez bigger than all other supported games combined. So to reduce the
|
||||||
|
datapackage of shapez, the locations for shapesanity are named `Shapesanity 1`, `Shapesanity 2` etc. instead of their
|
||||||
|
actual names. By creating a custom apworld, you can increase the amount of possible locations, but you will also
|
||||||
|
increase the size of the datapackage at the same time.
|
71
worlds/shapez/docs/de_shapez.md
Normal file
71
worlds/shapez/docs/de_shapez.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# shapez
|
||||||
|
|
||||||
|
## Was für ein Spiel ist das?
|
||||||
|
|
||||||
|
shapez ist ein Automatisierungsspiel, in dem du Formen aus zufällig generierten Vorkommen in einer endlosen Welt
|
||||||
|
extrahierst, zerschneidest, rotierst, stapelst, anmalst und schließlich zum Zentrum beförderst, um Level abzuschließen
|
||||||
|
und Upgrades zu kaufen. Das Tutorial beinhaltet 26 Level, in denen du (fast) immer ein neues Gebäude oder eine neue
|
||||||
|
Spielmechanik freischaltest. Danach folgen endlos weitere Level mit zufällig generierten Vorgaben. Um das Spiel bzw.
|
||||||
|
deine Gebäude schneller zu machen, kannst du bis zu 1000 Upgrades (pro Kategorie) kaufen.
|
||||||
|
|
||||||
|
## Wo ist die Optionen-Seite?
|
||||||
|
|
||||||
|
Die [Spieler-Optionen-Seite für dieses Spiel](../player-options) enthält alle Optionen zum Erstellen und exportieren
|
||||||
|
einer YAML-Datei.
|
||||||
|
Zusätzlich gibt es zu diesem Spiel "Datenpaket-Einstellungen", die du nach
|
||||||
|
[dieser Anleitung](/tutorial/shapez/datapackage_settings/de) einstellen kannst.
|
||||||
|
|
||||||
|
## Inwiefern wird das Spiel randomisiert?
|
||||||
|
|
||||||
|
Alle Belohnungen aus den Tutorial-Level (das Freischalten von Gebäuden und Spielmechaniken) und Verbesserungen durch
|
||||||
|
Upgrades werden dem Itempool der Multiworld hinzugefügt. Außerdem werden, wenn so in den Spieler-Optionen festgelegt,
|
||||||
|
die Bedingungen zum Abschließen eines Levels und zum Kaufen der Upgrades randomisiert.
|
||||||
|
|
||||||
|
## Was ist das Ziel von shapez in Archipelago?
|
||||||
|
|
||||||
|
Da das Spiel eigentlich kein konkretes Ziel (nach dem Tutorial) hat, kann man sich zwischen (momentan) 4 verschiedenen
|
||||||
|
Zielen entscheiden:
|
||||||
|
1. Vanilla: Schließe Level 26 ab (eigentlich das Ende des Tutorials).
|
||||||
|
2. MAM: Schließe ein bestimmtes Level nach Level 26 ab, das zuvor in den Spieler-Optionen festgelegt wurde. Es ist
|
||||||
|
empfohlen, eine Maschine zu bauen, die alles automatisch herstellt ("Make-Anything-Machine", kurz MAM).
|
||||||
|
3. Even Fasterer: Kaufe alle Upgrades bis zu einer in den Spieler-Optionen festgelegten Stufe (nach Stufe 8).
|
||||||
|
4. Efficiency III: Liefere 256 Blaupausen-Formen pro Sekunde ins Zentrum.
|
||||||
|
|
||||||
|
## Welche Items können in den Welten anderer Spieler erscheinen?
|
||||||
|
|
||||||
|
- Freischalten verschiedener Gebäude
|
||||||
|
- Blaupausen freischalten
|
||||||
|
- Große Upgrades (addiert 1 zum Geschwindigkeitsmultiplikator)
|
||||||
|
- Kleine Upgrades (addiert 0.1 zum Geschwindigkeitsmultiplikator)
|
||||||
|
- Andere ungewöhnliche Upgrades (optional)
|
||||||
|
- Verschiedene Bündel, die bestimmte Formen enthalten
|
||||||
|
- Fallen, die bestimmte Formen aus dem Zentrum dränieren (ja, das Wort gibt es)
|
||||||
|
- Fallen, die zufällige Gebäude oder andere Spielmechaniken betreffen
|
||||||
|
|
||||||
|
## Was ist eine Location / ein Check?
|
||||||
|
|
||||||
|
- Level (minimum 1-25, bis zu 499 je nach Spieler-Optionen, mit zusätzlichen Checks für Level 1 und 20)
|
||||||
|
- Upgrades (minimum Stufen II-VIII (2-8), bis zu D (500) je nach Spieler-Optionen)
|
||||||
|
- Bestimmte Formen mindestens einmal ins Zentrum liefern ("Shapesanity", bis zu 1000 zufällig gewählte Definitionen)
|
||||||
|
- Errungenschaften (bis zu 45)
|
||||||
|
|
||||||
|
## Was passiert, wenn der Spieler ein Item erhält?
|
||||||
|
|
||||||
|
Ein Pop-Up erscheint, das das/die erhaltene(n) Item(s) und eventuell weitere Informationen auflistet.
|
||||||
|
|
||||||
|
## Was bedeuten die Namen dieser ganzen Shapesanity Dinger?
|
||||||
|
|
||||||
|
Hier ist ein Spicker für die Englischarbeit (bloß nicht dem Lehrer zeigen):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Kann ich auch weitere Mods neben dem AP Client installieren?
|
||||||
|
|
||||||
|
Zurzeit wird Kompatibilität mit anderen Mods nicht unterstützt, aber niemand kann dich davon abhalten, es trotzdem zu
|
||||||
|
versuchen. Mods, die das Gameplay verändern, werden wahrscheinlich nicht funktionieren, indem sie das Laden der
|
||||||
|
jeweiligen Mods verhindern oder das Spiel zum Abstürzen bringen, während einfache QoL-Mods vielleicht problemlos
|
||||||
|
funktionieren könnten. Wenn du es versuchst, dann also auf eigene Gefahr.
|
||||||
|
|
||||||
|
## Hast du wirklich eine deutschsprachige Infoseite geschrieben, obwohl man sie aktuell nur über Umwege erreichen kann und du eigentlich an dem Praktikumsportfolio arbeiten solltest?
|
||||||
|
|
||||||
|
Ja
|
65
worlds/shapez/docs/en_shapez.md
Normal file
65
worlds/shapez/docs/en_shapez.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# shapez
|
||||||
|
|
||||||
|
## What is this game?
|
||||||
|
|
||||||
|
shapez is an automation game about cutting, rotating, stacking, and painting shapes, that you extract from randomly
|
||||||
|
generated patches on an infinite canvas, and sending them to the hub to complete levels. The "tutorial", where you
|
||||||
|
unlock a new building or game mechanic (almost) each level, lasts until level 26, where you unlock freeplay with
|
||||||
|
infinitely more levels, that require a new, randomly generated shape. Alongside the levels, you can unlock upgrades,
|
||||||
|
that make your buildings work faster.
|
||||||
|
|
||||||
|
## Where is the options page?
|
||||||
|
|
||||||
|
The [player options page for this game](../player-options) contains all the options you need to configure
|
||||||
|
and export a config file.
|
||||||
|
There are also some advanced "datapackage settings" that can be changed by following
|
||||||
|
[this guide](/tutorial/shapez/datapackage_settings/en).
|
||||||
|
|
||||||
|
## What does randomization do to this game?
|
||||||
|
|
||||||
|
Buildings and gameplay mechanics, that you normally unlock by completing a level, and upgrade improvements are put
|
||||||
|
into the item pool of the multiworld. Also, if enabled, the requirements for completing a level or buying an upgrade are
|
||||||
|
randomized.
|
||||||
|
|
||||||
|
## What is the goal of shapez in Archipelago?
|
||||||
|
|
||||||
|
As the game has no actual goal where the game ends, there are (currently) 4 different goals you can choose from in the
|
||||||
|
player options:
|
||||||
|
1. Vanilla: Complete level 26 (the end of the tutorial).
|
||||||
|
2. MAM: Complete a player-specified level after level 26. It's recommended to build a Make-Anything-Machine (MAM).
|
||||||
|
3. Even Fasterer: Upgrade everything to a player-specified tier after tier 8.
|
||||||
|
4. Efficiency III: Deliver 256 blueprint shapes per second to the hub.
|
||||||
|
|
||||||
|
## Which items can be in another player's world?
|
||||||
|
|
||||||
|
- Unlock different buildings
|
||||||
|
- Unlock blueprints
|
||||||
|
- Big upgrade improvements (adds 1 to the multiplier)
|
||||||
|
- Small upgrade improvements (adds .1 to the multiplier)
|
||||||
|
- Other unusual upgrade improvements (optional)
|
||||||
|
- Different shapes bundles
|
||||||
|
- Inventory draining traps
|
||||||
|
- Different traps afflicting random buildings and game mechanics
|
||||||
|
|
||||||
|
## What is considered a location check?
|
||||||
|
|
||||||
|
- Levels (minimum 1-25, up to 499 depending on player options, with additional checks for levels 1 and 20)
|
||||||
|
- Upgrades (minimum tiers II-VIII (2-8), up to D (500) depending on player options)
|
||||||
|
- Delivering certain shapes at least once to the hub ("shapesanity", up to 1000 from a 75800 names pool)
|
||||||
|
- Achievements (up to 45)
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
A pop-up will show, which item(s) were received, with additional information on some of them.
|
||||||
|
|
||||||
|
## What do the names of all these shapesanity locations mean?
|
||||||
|
|
||||||
|
Here's a cheat sheet:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Can I use other mods alongside the AP client?
|
||||||
|
|
||||||
|
At the moment, compatibility with other mods is not supported, but not forbidden. Gameplay altering mods will most
|
||||||
|
likely crash the game or disable loading the afflicted mods, while QoL mods might work without problems. Try at your own
|
||||||
|
risk.
|
62
worlds/shapez/docs/setup_de.md
Normal file
62
worlds/shapez/docs/setup_de.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Setup-Anleitung für shapez: Archipelago
|
||||||
|
|
||||||
|
## Schnelle Links
|
||||||
|
|
||||||
|
- Info-Seite zum Spiel
|
||||||
|
* [English](/games/shapez/info/en)
|
||||||
|
* [Deutsch](/games/shapez/info/de)
|
||||||
|
- [Spieler-Optionen-Seite](/games/shapez/player-options)
|
||||||
|
|
||||||
|
## Benötigte Software
|
||||||
|
|
||||||
|
- Eine installierbare und aktuelle PC-Version von shapez ([Steam](https://store.steampowered.com/app/1318690/shapez/)).
|
||||||
|
- Die shapezipelago Mod von der [mod.io-Seite](https://mod.io/g/shapez/m/shapezipelago).
|
||||||
|
|
||||||
|
## Optionale Software
|
||||||
|
|
||||||
|
- Archipelago von der [Archipelago-Release-Seite](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
* (Für den Text-Client)
|
||||||
|
* (Alternativ kannst du auch die eingebaute Konsole (nur lesbar) nutzen, indem du beim Starten des Spiels den
|
||||||
|
`-dev`-Parameter verwendest)
|
||||||
|
- Universal Tracker (schau im `#future-game-design`-Thread für UT auf dem Discord-Server nach der aktuellen Anleitung)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Da das Spiel einen eingebauten Mod-Loader hat, musst du nur die "shapezipelago@X.X.X.js"-Datei in den dafür vorgesehenen
|
||||||
|
Ordner kopieren. Wenn du nicht weißt, wo dieser ist, dann öffne das Spiel, drücke auf "MODS" und schließlich auf
|
||||||
|
"MODORDNER ÖFFNEN".
|
||||||
|
|
||||||
|
Du solltest (egal ob vor oder nach der Installation) die Einstellungen des Spiels öffnen und `HINWEISE & TUTORIALS` im
|
||||||
|
Reiter `BENUTZEROBERFLÄCHE` ausschalten, da sie sonst den Upgrade-Shop verstecken wird, bis du ein paar Level
|
||||||
|
abgeschlossen hast.
|
||||||
|
|
||||||
|
## Erstellen deiner YAML-Datei
|
||||||
|
|
||||||
|
### Was ist eine YAML-Datei und wofür brauche ich die?
|
||||||
|
|
||||||
|
Deine persönliche YAML-Datei beinhaltet eine Reihe von Optionen, die der Zufallsgenerator zum Erstellen von deinem
|
||||||
|
Spiel benötigt. Jeder Spieler einer Multiworld stellt seine eigene YAML-Datei zur Verfügung. Dadurch kann jeder Spieler
|
||||||
|
sein Spiel nach seinem eigenen Geschmack gestalten, während andere Spieler unabhängig davon ihre eigenen Optionen
|
||||||
|
wählen können!
|
||||||
|
|
||||||
|
### Wo bekomme ich so eine YAML-Datei her?
|
||||||
|
|
||||||
|
Du kannst auf der [shapez-Spieler-Optionen-Seite](/games/shapez/player-options) eine YAML-Datei generieren oder ein
|
||||||
|
Template herunterladen.
|
||||||
|
|
||||||
|
## Einer MultiWorld beitreten
|
||||||
|
|
||||||
|
1. Öffne das Spiel.
|
||||||
|
2. Gib im Hauptmenü den Slot-Namen, die Adresse, den Port und das Passwort (optional) in die dafür vorgesehene Box ein.
|
||||||
|
3. Drücke auf "Connect".
|
||||||
|
- Erneutes Drücken trennt die Verbindung zum Server.
|
||||||
|
- Ob du verbunden bist, steht direkt daneben.
|
||||||
|
4. Starte ein neues Spiel.
|
||||||
|
|
||||||
|
Nachdem der Speicherstand erstellt wurde und du zum Hauptmenü zurückkehrst, wird das erneute Öffnen des Speicherstandes
|
||||||
|
erneut verbinden.
|
||||||
|
|
||||||
|
### Der Port/Die Adresse der MultiWorld hat sich geändert, wie trete ich mit meinem existierenden Speicherstand bei?
|
||||||
|
|
||||||
|
Wiederhole die Schritte 1-3 und öffne den existierenden Speicherstand. Dies wird außerdem die gespeicherten Login-Daten
|
||||||
|
überschreiben, sodass du dies nur einmal machen musst.
|
58
worlds/shapez/docs/setup_en.md
Normal file
58
worlds/shapez/docs/setup_en.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Setup Guide for shapez: Archipelago
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- Game Info Page
|
||||||
|
* [English](/games/shapez/info/en)
|
||||||
|
* [Deutsch](/games/shapez/info/de)
|
||||||
|
- [Player Options Page](/games/shapez/player-options)
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
- An installable, up-to-date PC version of shapez ([Steam](https://store.steampowered.com/app/1318690/shapez/)).
|
||||||
|
- The shapezipelago mod from the [mod.io page](https://mod.io/g/shapez/m/shapezipelago).
|
||||||
|
|
||||||
|
## Optional Software
|
||||||
|
|
||||||
|
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||||
|
* (Only for the TextClient)
|
||||||
|
* (If you want, you can use the built-in console as a read-only text client by launching the game
|
||||||
|
with the `-dev` parameter)
|
||||||
|
- Universal Tracker (check UT's `#future-game-design` thread in the discord server for instructions)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
As the game has a built-in mod loader, all you need to do is copy the `shapezipelago@X.X.X.js` mod file into the mods
|
||||||
|
folder. If you don't know where that is, open the game, click on `MODS`, and then `OPEN MODS FOLDER`.
|
||||||
|
|
||||||
|
It is recommended to go into the settings of the game and disable `HINTS & TUTORIALS` in the `USER INTERFACE` tab, as
|
||||||
|
this setting will disable the upgrade shop until you complete a few levels.
|
||||||
|
|
||||||
|
## Configuring your YAML file
|
||||||
|
|
||||||
|
### What is a YAML file and why do I need one?
|
||||||
|
|
||||||
|
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||||
|
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||||
|
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||||
|
|
||||||
|
### Where do I get a YAML file?
|
||||||
|
|
||||||
|
You can generate a yaml or download a template by visiting the
|
||||||
|
[shapez Player Options Page](/games/shapez/player-options)
|
||||||
|
|
||||||
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
|
1. Open the game.
|
||||||
|
2. In the main menu, type the slot name, address, port, and password (optional) into the input box.
|
||||||
|
3. Click "Connect".
|
||||||
|
- To disconnect, just press this button again.
|
||||||
|
- The status of your connection is shown right next to the button.
|
||||||
|
4. Create a new game.
|
||||||
|
|
||||||
|
After creating the save file and returning to the main menu, opening the save file again will automatically reconnect.
|
||||||
|
|
||||||
|
### The MultiWorld changed its port/address, how do I reconnect correctly with my existing save file?
|
||||||
|
|
||||||
|
Repeat steps 1-3 and open the existing save file. This will also overwrite the saved connection details, so you will
|
||||||
|
only have to do this once.
|
BIN
worlds/shapez/docs/shapesanity_full.png
Normal file
BIN
worlds/shapez/docs/shapesanity_full.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
279
worlds/shapez/items.py
Normal file
279
worlds/shapez/items.py
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
from typing import Dict, Callable, Any, List
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification as IClass
|
||||||
|
from .options import ShapezOptions
|
||||||
|
from .data.strings import GOALS, ITEMS, OTHER
|
||||||
|
|
||||||
|
|
||||||
|
def is_mam_achievement_included(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.progression if options.include_achievements and (not options.goal == GOALS.vanilla) else IClass.useful
|
||||||
|
|
||||||
|
|
||||||
|
def is_achievements_included(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.progression if options.include_achievements else IClass.useful
|
||||||
|
|
||||||
|
|
||||||
|
def is_goal_efficiency_iii(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.progression if options.goal == GOALS.efficiency_iii else IClass.useful
|
||||||
|
|
||||||
|
|
||||||
|
def always_progression(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.progression
|
||||||
|
|
||||||
|
|
||||||
|
def always_useful(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.useful
|
||||||
|
|
||||||
|
|
||||||
|
def always_filler(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.filler
|
||||||
|
|
||||||
|
|
||||||
|
def always_trap(options: ShapezOptions) -> IClass:
|
||||||
|
return IClass.trap
|
||||||
|
|
||||||
|
|
||||||
|
# Routing buildings are not needed to complete the game, but building factories without balancers and tunnels
|
||||||
|
# would be unreasonably complicated and time-consuming.
|
||||||
|
# Some buildings are not needed to complete the game, but are "logically needed" for the "MAM" achievement.
|
||||||
|
|
||||||
|
buildings_processing: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.cutter: always_progression,
|
||||||
|
ITEMS.cutter_quad: always_progression,
|
||||||
|
ITEMS.rotator: always_progression,
|
||||||
|
ITEMS.rotator_ccw: always_progression,
|
||||||
|
ITEMS.rotator_180: always_progression,
|
||||||
|
ITEMS.stacker: always_progression,
|
||||||
|
ITEMS.painter: always_progression,
|
||||||
|
ITEMS.painter_double: always_progression,
|
||||||
|
ITEMS.painter_quad: always_progression,
|
||||||
|
ITEMS.color_mixer: always_progression,
|
||||||
|
}
|
||||||
|
|
||||||
|
buildings_routing: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.balancer: always_progression,
|
||||||
|
ITEMS.comp_merger: always_progression,
|
||||||
|
ITEMS.comp_splitter: always_progression,
|
||||||
|
ITEMS.tunnel: always_progression,
|
||||||
|
ITEMS.tunnel_tier_ii: is_mam_achievement_included,
|
||||||
|
}
|
||||||
|
|
||||||
|
buildings_other: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.trash: always_progression,
|
||||||
|
ITEMS.extractor_chain: always_useful
|
||||||
|
}
|
||||||
|
|
||||||
|
buildings_top_row: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.belt_reader: is_mam_achievement_included,
|
||||||
|
ITEMS.storage: is_achievements_included,
|
||||||
|
ITEMS.switch: always_progression,
|
||||||
|
ITEMS.item_filter: is_mam_achievement_included,
|
||||||
|
ITEMS.display: always_useful
|
||||||
|
}
|
||||||
|
|
||||||
|
buildings_wires: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.wires: always_progression,
|
||||||
|
ITEMS.const_signal: always_progression,
|
||||||
|
ITEMS.logic_gates: is_mam_achievement_included,
|
||||||
|
ITEMS.virtual_proc: is_mam_achievement_included
|
||||||
|
}
|
||||||
|
|
||||||
|
gameplay_unlocks: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.blueprints: is_achievements_included
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrades: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.upgrade_big_belt: always_progression,
|
||||||
|
ITEMS.upgrade_big_miner: always_useful,
|
||||||
|
ITEMS.upgrade_big_proc: always_useful,
|
||||||
|
ITEMS.upgrade_big_paint: always_useful,
|
||||||
|
ITEMS.upgrade_small_belt: always_filler,
|
||||||
|
ITEMS.upgrade_small_miner: always_filler,
|
||||||
|
ITEMS.upgrade_small_proc: always_filler,
|
||||||
|
ITEMS.upgrade_small_paint: always_filler
|
||||||
|
}
|
||||||
|
|
||||||
|
whacky_upgrades: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.upgrade_gigantic_belt: always_progression,
|
||||||
|
ITEMS.upgrade_gigantic_miner: always_useful,
|
||||||
|
ITEMS.upgrade_gigantic_proc: always_useful,
|
||||||
|
ITEMS.upgrade_gigantic_paint: always_useful,
|
||||||
|
ITEMS.upgrade_rising_belt: always_progression,
|
||||||
|
ITEMS.upgrade_rising_miner: always_useful,
|
||||||
|
ITEMS.upgrade_rising_proc: always_useful,
|
||||||
|
ITEMS.upgrade_rising_paint: always_useful,
|
||||||
|
ITEMS.upgrade_big_random: always_useful,
|
||||||
|
ITEMS.upgrade_small_random: always_filler,
|
||||||
|
}
|
||||||
|
|
||||||
|
whacky_upgrade_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.trap_upgrade_belt: always_trap,
|
||||||
|
ITEMS.trap_upgrade_miner: always_trap,
|
||||||
|
ITEMS.trap_upgrade_proc: always_trap,
|
||||||
|
ITEMS.trap_upgrade_paint: always_trap,
|
||||||
|
ITEMS.trap_upgrade_demonic_belt: always_trap,
|
||||||
|
ITEMS.trap_upgrade_demonic_miner: always_trap,
|
||||||
|
ITEMS.trap_upgrade_demonic_proc: always_trap,
|
||||||
|
ITEMS.trap_upgrade_demonic_paint: always_trap,
|
||||||
|
}
|
||||||
|
|
||||||
|
bundles: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.bundle_blueprint: always_filler,
|
||||||
|
ITEMS.bundle_level: always_filler,
|
||||||
|
ITEMS.bundle_upgrade: always_filler
|
||||||
|
}
|
||||||
|
|
||||||
|
standard_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.trap_locked: always_trap,
|
||||||
|
ITEMS.trap_throttled: always_trap,
|
||||||
|
ITEMS.trap_malfunction: always_trap,
|
||||||
|
ITEMS.trap_inflation: always_trap,
|
||||||
|
ITEMS.trap_clear_belts: always_trap,
|
||||||
|
}
|
||||||
|
|
||||||
|
random_draining_trap: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.trap_draining_inv: always_trap
|
||||||
|
}
|
||||||
|
|
||||||
|
split_draining_traps: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.trap_draining_blueprint: always_trap,
|
||||||
|
ITEMS.trap_draining_level: always_trap,
|
||||||
|
ITEMS.trap_draining_upgrade: always_trap
|
||||||
|
}
|
||||||
|
|
||||||
|
belt_and_extractor: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
ITEMS.belt: always_progression,
|
||||||
|
ITEMS.extractor: always_progression
|
||||||
|
}
|
||||||
|
|
||||||
|
item_table: Dict[str, Callable[[ShapezOptions], IClass]] = {
|
||||||
|
**buildings_processing,
|
||||||
|
**buildings_routing,
|
||||||
|
**buildings_other,
|
||||||
|
**buildings_top_row,
|
||||||
|
**buildings_wires,
|
||||||
|
**gameplay_unlocks,
|
||||||
|
**upgrades,
|
||||||
|
**whacky_upgrades,
|
||||||
|
**whacky_upgrade_traps,
|
||||||
|
**bundles,
|
||||||
|
**standard_traps,
|
||||||
|
**random_draining_trap,
|
||||||
|
**split_draining_traps,
|
||||||
|
**belt_and_extractor
|
||||||
|
}
|
||||||
|
|
||||||
|
big_upgrades = [
|
||||||
|
ITEMS.upgrade_big_belt,
|
||||||
|
ITEMS.upgrade_big_miner,
|
||||||
|
ITEMS.upgrade_big_proc,
|
||||||
|
ITEMS.upgrade_big_paint
|
||||||
|
]
|
||||||
|
|
||||||
|
small_upgrades = [
|
||||||
|
ITEMS.upgrade_small_belt,
|
||||||
|
ITEMS.upgrade_small_miner,
|
||||||
|
ITEMS.upgrade_small_proc,
|
||||||
|
ITEMS.upgrade_small_paint
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def filler(random: float, whacky_allowed: bool) -> str:
|
||||||
|
"""Returns a random filler item."""
|
||||||
|
bundles_list = [*bundles]
|
||||||
|
return random_choice_nested(random, [
|
||||||
|
small_upgrades,
|
||||||
|
[
|
||||||
|
bundles_list,
|
||||||
|
bundles_list,
|
||||||
|
[
|
||||||
|
big_upgrades,
|
||||||
|
[*whacky_upgrades] if whacky_allowed else big_upgrades,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def trap(random: float, split_draining: bool, whacky_allowed: bool) -> str:
|
||||||
|
"""Returns a random trap item."""
|
||||||
|
pool = [
|
||||||
|
*standard_traps,
|
||||||
|
ITEMS.trap_draining_inv if not split_draining else [*split_draining_traps],
|
||||||
|
]
|
||||||
|
if whacky_allowed:
|
||||||
|
pool.append([*whacky_upgrade_traps])
|
||||||
|
return random_choice_nested(random, pool)
|
||||||
|
|
||||||
|
|
||||||
|
def random_choice_nested(random: float, nested: List[Any]) -> Any:
|
||||||
|
"""Helper function for getting a random element from a nested list."""
|
||||||
|
current: Any = nested
|
||||||
|
while isinstance(current, List):
|
||||||
|
index_float = random*len(current)
|
||||||
|
current = current[int(index_float)]
|
||||||
|
random = index_float-int(index_float)
|
||||||
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
item_descriptions = { # TODO replace keys with global strings and update with whacky upgrades
|
||||||
|
"Balancer": "A routing building, that can merge two belts into one, split a belt in two, " +
|
||||||
|
"or balance the items of two belts",
|
||||||
|
"Tunnel": "A routing building consisting of two parts, that allows for gaps in belts",
|
||||||
|
"Compact Merger": "A small routing building, that merges two belts into one",
|
||||||
|
"Tunnel Tier II": "A routing building consisting of two parts, that allows for even longer gaps in belts",
|
||||||
|
"Compact Splitter": "A small routing building, that splits a belt in two",
|
||||||
|
"Cutter": "A processing building, that cuts shapes vertically in two halves",
|
||||||
|
"Rotator": "A processing building, that rotates shapes 90 degrees clockwise",
|
||||||
|
"Painter": "A processing building, that paints shapes in a given color",
|
||||||
|
"Rotator (CCW)": "A processing building, that rotates shapes 90 degrees counter-clockwise",
|
||||||
|
"Color Mixer": "A processing building, that mixes two colors together to create a new one",
|
||||||
|
"Stacker": "A processing building, that combines two shapes with missing parts or puts one on top of the other",
|
||||||
|
"Quad Cutter": "A processing building, that cuts shapes in four quarter parts",
|
||||||
|
"Double Painter": "A processing building, that paints two shapes in a given color",
|
||||||
|
"Rotator (180°)": "A processing building, that rotates shapes 180 degrees",
|
||||||
|
"Quad Painter": "A processing building, that paint each quarter of a shape in another given color and requires " +
|
||||||
|
"wire inputs for each color to work",
|
||||||
|
"Trash": "A building, that destroys unused shapes",
|
||||||
|
"Chaining Extractor": "An upgrade to extractors, that can increase the output without balancers or mergers",
|
||||||
|
"Belt Reader": "A wired building, that shows the average amount of items passing through per second",
|
||||||
|
"Storage": "A building, that stores up to 5000 of a certain shape",
|
||||||
|
"Switch": "A building, that sends a constant boolean signal",
|
||||||
|
"Item Filter": "A wired building, that filters items based on wire input",
|
||||||
|
"Display": "A wired building, that displays a shape or color based on wire input",
|
||||||
|
"Wires": "The main building of the wires layer, that carries signals between other buildings",
|
||||||
|
"Constant Signal": "A building on the wires layer, that sends a constant shape, color, or boolean signal",
|
||||||
|
"Logic Gates": "Multiple buildings on the wires layer, that perform logical operations on wire signals",
|
||||||
|
"Virtual Processing": "Multiple buildings on the wires layer, that process wire signals like processor buildings",
|
||||||
|
"Blueprints": "A game mechanic, that allows copy-pasting multiple buildings at once",
|
||||||
|
"Big Belt Upgrade": "An upgrade, that adds 1 to the speed multiplier of belts, distributors, and tunnels",
|
||||||
|
"Big Miner Upgrade": "An upgrade, that adds 1 to the speed multiplier of extractors",
|
||||||
|
"Big Processors Upgrade": "An upgrade, that adds 1 to the speed multiplier of cutters, rotators, and stackers",
|
||||||
|
"Big Painting Upgrade": "An upgrade, that adds 1 to the speed multiplier of painters and color mixers",
|
||||||
|
"Small Belt Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of belts, distributors, and tunnels",
|
||||||
|
"Small Miner Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of extractors",
|
||||||
|
"Small Processors Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of cutters, rotators, and stackers",
|
||||||
|
"Small Painting Upgrade": "An upgrade, that adds 0.1 to the speed multiplier of painters and color mixers",
|
||||||
|
"Blueprint Shapes Bundle": "A bundle with 1000 blueprint shapes, instantly delivered to the hub",
|
||||||
|
"Level Shapes Bundle": "A bundle with some shapes needed for the current level, " +
|
||||||
|
"instantly delivered to the hub",
|
||||||
|
"Upgrade Shapes Bundle": "A bundle with some shapes needed for a random upgrade, " +
|
||||||
|
"instantly delivered to the hub",
|
||||||
|
"Inventory Draining Trap": "Randomly drains either blueprint shapes, current level requirement shapes, " +
|
||||||
|
"or random upgrade requirement shapes, by half",
|
||||||
|
"Blueprint Shapes Draining Trap": "Drains the stored blueprint shapes by half",
|
||||||
|
"Level Shapes Draining Trap": "Drains the current level requirement shapes by half",
|
||||||
|
"Upgrade Shapes Draining Trap": "Drains a random upgrade requirement shape by half",
|
||||||
|
"Locked Building Trap": "Locks a random building from being placed for 15-60 seconds",
|
||||||
|
"Throttled Building Trap": "Halves the speed of a random building for 15-60 seconds",
|
||||||
|
"Malfunctioning Trap": "Makes a random building process items incorrectly for 15-60 seconds",
|
||||||
|
"Inflation Trap": "Permanently increases the required shapes multiplier by 1. "
|
||||||
|
"In other words: Permanently increases required shapes by 10% of the standard amount.",
|
||||||
|
"Belt": "One of the most important buildings in the game, that transports your shapes and colors from one " +
|
||||||
|
"place to another",
|
||||||
|
"Extractor": "One of the most important buildings in the game, that extracts shapes from those randomly " +
|
||||||
|
"generated patches"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ShapezItem(Item):
|
||||||
|
game = OTHER.game_name
|
546
worlds/shapez/locations.py
Normal file
546
worlds/shapez/locations.py
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
from random import Random
|
||||||
|
from typing import List, Tuple, Dict, Optional, Callable
|
||||||
|
|
||||||
|
from BaseClasses import Location, LocationProgressType, Region
|
||||||
|
from .data.strings import CATEGORY, LOCATIONS, REGIONS, OPTIONS, GOALS, OTHER, SHAPESANITY
|
||||||
|
from .options import max_shapesanity, max_levels_and_upgrades
|
||||||
|
|
||||||
|
categories = [CATEGORY.belt, CATEGORY.miner, CATEGORY.processors, CATEGORY.painting]
|
||||||
|
|
||||||
|
translate: List[Tuple[int, str]] = [
|
||||||
|
(1000, "M"),
|
||||||
|
(900, "CM"),
|
||||||
|
(500, "D"),
|
||||||
|
(400, "CD"),
|
||||||
|
(100, "C"),
|
||||||
|
(90, "XC"),
|
||||||
|
(50, "L"),
|
||||||
|
(40, "XL"),
|
||||||
|
(10, "X"),
|
||||||
|
(9, "IX"),
|
||||||
|
(5, "V"),
|
||||||
|
(4, "IV"),
|
||||||
|
(1, "I")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def roman(num: int) -> str:
|
||||||
|
"""Converts positive non-zero integers into roman numbers."""
|
||||||
|
rom: str = ""
|
||||||
|
for key, val in translate:
|
||||||
|
while num >= key:
|
||||||
|
rom += val
|
||||||
|
num -= key
|
||||||
|
return rom
|
||||||
|
|
||||||
|
|
||||||
|
location_description = { # TODO change keys to global strings
|
||||||
|
"Level 1": "Levels are completed by delivering certain shapes in certain amounts to the hub. The required shape "
|
||||||
|
"and amount for the current level are always displayed on the hub.",
|
||||||
|
"Level 1 Additional": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||||
|
"Level 20 Additional": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||||
|
"Level 20 Additional 2": "In the vanilla game, levels 1 and 20 have unlock more than one building.",
|
||||||
|
"Level 26": "In the vanilla game, level 26 is the final level of the tutorial, unlocking freeplay.",
|
||||||
|
f"Level {max_levels_and_upgrades-1}": "This is the highest possible level that can contains an item, if your goal "
|
||||||
|
"is set to \"mam\"",
|
||||||
|
"Belt Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your hub. "
|
||||||
|
"This is the first upgrade in the belt, balancers, and tunnel category.",
|
||||||
|
"Miner Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your "
|
||||||
|
"hub. This is the first upgrade in the extractor category.",
|
||||||
|
"Processors Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in "
|
||||||
|
"your hub. This is the first upgrade in the cutter, rotators, and stacker category.",
|
||||||
|
"Painting Upgrade Tier II": "Upgrades can be purchased by having certain shapes in certain amounts stored in your "
|
||||||
|
"hub. This is the first upgrade in the painters and color mixer category.",
|
||||||
|
"Belt Upgrade Tier VIII": "This is the final upgrade in the belt, balancers, and tunnel category, if your goal is "
|
||||||
|
"**not** set to \"even_fasterer\".",
|
||||||
|
"Miner Upgrade Tier VIII": "This is the final upgrade in the extractor category, if your goal is **not** set to "
|
||||||
|
"\"even_fasterer\".",
|
||||||
|
"Processors Upgrade Tier VIII": "This is the final upgrade in the cutter, rotators, and stacker category, if your "
|
||||||
|
"goal is **not** set to \"even_fasterer\".",
|
||||||
|
"Painting Upgrade Tier VIII": "This is the final upgrade in the painters and color mixer category, if your goal is "
|
||||||
|
"**not** set to \"even_fasterer\".",
|
||||||
|
f"Belt Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the belt, "
|
||||||
|
"balancers, and tunnel category, if your goal is set to "
|
||||||
|
"\"even_fasterer\".",
|
||||||
|
f"Miner Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the extractor "
|
||||||
|
"category, if your goal is set to \"even_fasterer\".",
|
||||||
|
f"Processors Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the cutter, "
|
||||||
|
"rotators, and stacker category, if your goal is set "
|
||||||
|
"to \"even_fasterer\".",
|
||||||
|
f"Painting Upgrade Tier {roman(max_levels_and_upgrades)}": "This is the highest possible upgrade in the painters "
|
||||||
|
"and color mixer category, if your goal is set to "
|
||||||
|
"\"even_fasterer\".",
|
||||||
|
"My eyes no longer hurt": "This is an achievement, that is unlocked by activating dark mode.",
|
||||||
|
"Painter": "This is an achievement, that is unlocked by painting a shape using the painter or double painter.",
|
||||||
|
"Cutter": "This is an achievement, that is unlocked by cutting a shape in half using the cutter.",
|
||||||
|
"Rotater": "This is an achievement, that is unlocked by rotating a shape clock wise.",
|
||||||
|
"Wait, they stack?": "This is an achievement, that is unlocked by stacking two shapes on top of each other.",
|
||||||
|
"Wires": "This is an achievement, that is unlocked by completing level 20.",
|
||||||
|
"Storage": "This is an achievement, that is unlocked by storing a shape in a storage.",
|
||||||
|
"Freedom": "This is an achievement, that is unlocked by completing level 20. It is only included if the goal is "
|
||||||
|
"**not** set to vanilla.",
|
||||||
|
"The logo!": "This is an achievement, that is unlocked by producing the logo of the game.",
|
||||||
|
"To the moon": "This is an achievement, that is unlocked by producing the rocket shape.",
|
||||||
|
"It's piling up": "This is an achievement, that is unlocked by having 100.000 blueprint shapes stored in the hub.",
|
||||||
|
"I'll use it later": "This is an achievement, that is unlocked by having one million blueprint shapes stored in "
|
||||||
|
"the hub.",
|
||||||
|
"Efficiency 1": "This is an achievement, that is unlocked by delivering 25 blueprint shapes per second to the hub.",
|
||||||
|
"Preparing to launch": "This is an achievement, that is unlocked by delivering 10 rocket shapes per second to the "
|
||||||
|
"hub.",
|
||||||
|
"SpaceY": "This is an achievement, that is unlocked by 20 rocket shapes per second to the hub.",
|
||||||
|
"Stack overflow": "This is an achievement, that is unlocked by stacking 4 layers on top of each other.",
|
||||||
|
"It's a mess": "This is an achievement, that is unlocked by having 100 different shapes stored in the hub.",
|
||||||
|
"Faster": "This is an achievement, that is unlocked by upgrading everything to at least tier V.",
|
||||||
|
"Even faster": "This is an achievement, that is unlocked by upgrading everything to at least tier VIII.",
|
||||||
|
"Get rid of them": "This is an achievement, that is unlocked by transporting 1000 shapes into a trash can.",
|
||||||
|
"It's been a long time": "This is an achievement, that is unlocked by playing your save file for 10 hours "
|
||||||
|
"(combined playtime).",
|
||||||
|
"Addicted": "This is an achievement, that is unlocked by playing your save file for 20 hours (combined playtime).",
|
||||||
|
"Can't stop": "This is an achievement, that is unlocked by reaching level 50.",
|
||||||
|
"Is this the end?": "This is an achievement, that is unlocked by reaching level 100.",
|
||||||
|
"Getting into it": "This is an achievement, that is unlocked by playing your save file for 1 hour (combined "
|
||||||
|
"playtime).",
|
||||||
|
"Now it's easy": "This is an achievement, that is unlocked by placing a blueprint.",
|
||||||
|
"Computer Guy": "This is an achievement, that is unlocked by placing 5000 wires.",
|
||||||
|
"Speedrun Master": "This is an achievement, that is unlocked by completing level 12 in under 30 Minutes. This "
|
||||||
|
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||||
|
"Speedrun Novice": "This is an achievement, that is unlocked by completing level 12 in under 60 Minutes. This "
|
||||||
|
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||||
|
"Not an idle game": "This is an achievement, that is unlocked by completing level 12 in under 120 Minutes. This "
|
||||||
|
"location is excluded by default, as it can become inaccessible in a save file after that time.",
|
||||||
|
"Efficiency 2": "This is an achievement, that is unlocked by delivering 50 blueprint shapes per second to the hub.",
|
||||||
|
"Branding specialist 1": "This is an achievement, that is unlocked by delivering 25 logo shapes per second to the "
|
||||||
|
"hub.",
|
||||||
|
"Branding specialist 2": "This is an achievement, that is unlocked by delivering 50 logo shapes per second to the "
|
||||||
|
"hub.",
|
||||||
|
"King of Inefficiency": "This is an achievement, that is unlocked by **not** placing a counter clock wise rotator "
|
||||||
|
"until level 14. This location is excluded by default, as it can become inaccessible in a "
|
||||||
|
"save file after placing that building.",
|
||||||
|
"It's so slow": "This is an achievement, that is unlocked by completing level 12 **without** buying any belt "
|
||||||
|
"upgrade. This location is excluded by default, as it can become inaccessible in a save file after "
|
||||||
|
"buying that upgrade.",
|
||||||
|
"MAM (Make Anything Machine)": "This is an achievement, that is unlocked by completing any level after level 26 "
|
||||||
|
"**without** modifying your factory. It is recommended to build a Make Anything "
|
||||||
|
"Machine.",
|
||||||
|
"Perfectionist": "This is an achievement, that is unlocked by destroying more than 1000 buildings at once.",
|
||||||
|
"The next dimension": "This is an achievement, that is unlocked by opening the wires layer.",
|
||||||
|
"Oops": "This is an achievement, that is unlocked by delivering a shape, that neither a level requirement nor an "
|
||||||
|
"upgrade requirement.",
|
||||||
|
"Copy-Pasta": "This is an achievement, that is unlocked by placing a blueprint with at least 1000 buildings.",
|
||||||
|
"I've seen that before ...": "This is an achievement, that is unlocked by producing RgRyRbRr.",
|
||||||
|
"Memories from the past": "This is an achievement, that is unlocked by producing WrRgWrRg:CwCrCwCr:SgSgSgSg.",
|
||||||
|
"I need trains": "This is an achievement, that is unlocked by placing a 500 tiles long belt.",
|
||||||
|
"A bit early?": "This is an achievement, that is unlocked by producing the logo shape before reaching level 18. "
|
||||||
|
"This location is excluded by default, as it can become inaccessible in a save file after reaching "
|
||||||
|
"that level.",
|
||||||
|
"GPS": "This is an achievement, that is unlocked by placing 15 or more map markers.",
|
||||||
|
"Shapesanity 1": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||||
|
"requiring a certain roation, orientation, or ordering. Shapesanity 1 is always an uncolored "
|
||||||
|
"circle.",
|
||||||
|
"Shapesanity 2": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||||
|
"requiring a certain roation, orientation, or ordering. Shapesanity 2 is always an uncolored "
|
||||||
|
"square.",
|
||||||
|
"Shapesanity 3": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||||
|
"requiring a certain roation, orientation, or ordering. Shapesanity 3 is always an uncolored "
|
||||||
|
"star.",
|
||||||
|
"Shapesanity 4": "Shapesanity locations can be checked by delivering a described shape to the hub, without "
|
||||||
|
"requiring a certain roation, orientation, or ordering. Shapesanity 4 is always an uncolored "
|
||||||
|
"windmill.",
|
||||||
|
}
|
||||||
|
|
||||||
|
shapesanity_simple: Dict[str, str] = {}
|
||||||
|
shapesanity_1_4: Dict[str, str] = {}
|
||||||
|
shapesanity_two_sided: Dict[str, str] = {}
|
||||||
|
shapesanity_three_parts: Dict[str, str] = {}
|
||||||
|
shapesanity_four_parts: Dict[str, str] = {}
|
||||||
|
|
||||||
|
level_locations: List[str] = ([LOCATIONS.level(1, 1), LOCATIONS.level(20, 1), LOCATIONS.level(20, 2)]
|
||||||
|
+ [LOCATIONS.level(x) for x in range(1, max_levels_and_upgrades)])
|
||||||
|
upgrade_locations: List[str] = [LOCATIONS.upgrade(cat, roman(x))
|
||||||
|
for cat in categories for x in range(2, max_levels_and_upgrades+1)]
|
||||||
|
achievement_locations: List[str] = [LOCATIONS.my_eyes, LOCATIONS.painter, LOCATIONS.cutter, LOCATIONS.rotater,
|
||||||
|
LOCATIONS.wait_they_stack, LOCATIONS.wires, LOCATIONS.storage, LOCATIONS.freedom,
|
||||||
|
LOCATIONS.the_logo, LOCATIONS.to_the_moon, LOCATIONS.its_piling_up,
|
||||||
|
LOCATIONS.use_it_later, LOCATIONS.efficiency_1, LOCATIONS.preparing_to_launch,
|
||||||
|
LOCATIONS.spacey, LOCATIONS.stack_overflow, LOCATIONS.its_a_mess, LOCATIONS.faster,
|
||||||
|
LOCATIONS.even_faster, LOCATIONS.get_rid_of_them, LOCATIONS.a_long_time,
|
||||||
|
LOCATIONS.addicted, LOCATIONS.cant_stop, LOCATIONS.is_this_the_end,
|
||||||
|
LOCATIONS.getting_into_it, LOCATIONS.now_its_easy, LOCATIONS.computer_guy,
|
||||||
|
LOCATIONS.speedrun_master, LOCATIONS.speedrun_novice, LOCATIONS.not_idle_game,
|
||||||
|
LOCATIONS.efficiency_2, LOCATIONS.branding_1,
|
||||||
|
LOCATIONS.branding_2, LOCATIONS.king_of_inefficiency, LOCATIONS.its_so_slow,
|
||||||
|
LOCATIONS.mam, LOCATIONS.perfectionist, LOCATIONS.next_dimension, LOCATIONS.oops,
|
||||||
|
LOCATIONS.copy_pasta, LOCATIONS.ive_seen_that_before, LOCATIONS.memories,
|
||||||
|
LOCATIONS.i_need_trains, LOCATIONS.a_bit_early, LOCATIONS.gps]
|
||||||
|
shapesanity_locations: List[str] = [LOCATIONS.shapesanity(x) for x in range(1, max_shapesanity+1)]
|
||||||
|
|
||||||
|
|
||||||
|
def init_shapesanity_pool() -> None:
|
||||||
|
"""Imports the pregenerated shapesanity pool."""
|
||||||
|
from .data import shapesanity_pool
|
||||||
|
shapesanity_simple.update(shapesanity_pool.shapesanity_simple)
|
||||||
|
shapesanity_1_4.update(shapesanity_pool.shapesanity_1_4)
|
||||||
|
shapesanity_two_sided.update(shapesanity_pool.shapesanity_two_sided)
|
||||||
|
shapesanity_three_parts.update(shapesanity_pool.shapesanity_three_parts)
|
||||||
|
shapesanity_four_parts.update(shapesanity_pool.shapesanity_four_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def addlevels(maxlevel: int, logictype: str,
|
||||||
|
random_logic_phase_length: List[int]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||||
|
"""Returns a dictionary with all level locations based on player options (maxlevel INCLUDED).
|
||||||
|
If shape requirements are not randomized, the logic type is expected to be vanilla."""
|
||||||
|
|
||||||
|
# Level 1 is always directly accessible
|
||||||
|
locations: Dict[str, Tuple[str, LocationProgressType]] \
|
||||||
|
= {LOCATIONS.level(1): (REGIONS.main, LocationProgressType.PRIORITY),
|
||||||
|
LOCATIONS.level(1, 1): (REGIONS.main, LocationProgressType.PRIORITY)}
|
||||||
|
level_regions = [REGIONS.main, REGIONS.levels_1, REGIONS.levels_2, REGIONS.levels_3,
|
||||||
|
REGIONS.levels_4, REGIONS.levels_5]
|
||||||
|
|
||||||
|
def f(name: str, region: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||||
|
locations[name] = (region, progress)
|
||||||
|
|
||||||
|
if logictype.startswith(OPTIONS.logic_vanilla):
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(2), REGIONS.levels_1)
|
||||||
|
f(LOCATIONS.level(3), REGIONS.levels_1)
|
||||||
|
f(LOCATIONS.level(4), REGIONS.levels_1)
|
||||||
|
f(LOCATIONS.level(5), REGIONS.levels_2)
|
||||||
|
f(LOCATIONS.level(6), REGIONS.levels_2)
|
||||||
|
f(LOCATIONS.level(7), REGIONS.levels_3)
|
||||||
|
f(LOCATIONS.level(8), REGIONS.levels_3)
|
||||||
|
f(LOCATIONS.level(9), REGIONS.levels_4)
|
||||||
|
f(LOCATIONS.level(10), REGIONS.levels_4)
|
||||||
|
for x in range(11, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||||
|
|
||||||
|
elif logictype.startswith(OPTIONS.logic_stretched):
|
||||||
|
phaselength = maxlevel//6
|
||||||
|
f(LOCATIONS.level(20, 1), level_regions[20//phaselength])
|
||||||
|
f(LOCATIONS.level(20, 2), level_regions[20//phaselength])
|
||||||
|
for x in range(2, phaselength):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.main)
|
||||||
|
for x in range(phaselength, phaselength*2):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_1)
|
||||||
|
for x in range(phaselength*2, phaselength*3):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_2)
|
||||||
|
for x in range(phaselength*3, phaselength*4):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_3)
|
||||||
|
for x in range(phaselength*4, phaselength*5):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_4)
|
||||||
|
for x in range(phaselength*5, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||||
|
|
||||||
|
elif logictype.startswith(OPTIONS.logic_quick):
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(2), REGIONS.levels_1)
|
||||||
|
f(LOCATIONS.level(3), REGIONS.levels_2)
|
||||||
|
f(LOCATIONS.level(4), REGIONS.levels_3)
|
||||||
|
f(LOCATIONS.level(5), REGIONS.levels_4)
|
||||||
|
for x in range(6, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||||
|
|
||||||
|
elif logictype.startswith(OPTIONS.logic_random_steps):
|
||||||
|
next_level = 2
|
||||||
|
for phase in range(5):
|
||||||
|
for x in range(random_logic_phase_length[phase]):
|
||||||
|
f(LOCATIONS.level(next_level+x), level_regions[phase])
|
||||||
|
next_level += random_logic_phase_length[phase]
|
||||||
|
if next_level > 20:
|
||||||
|
f(LOCATIONS.level(20, 1), level_regions[phase])
|
||||||
|
f(LOCATIONS.level(20, 2), level_regions[phase])
|
||||||
|
for x in range(next_level, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||||
|
if next_level <= 20:
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_hardcore:
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.levels_5)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.levels_5)
|
||||||
|
for x in range(2, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_5)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_dopamine:
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.levels_2)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.levels_2)
|
||||||
|
for x in range(2, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.levels_2)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_dopamine_overflow:
|
||||||
|
f(LOCATIONS.level(20, 1), REGIONS.main)
|
||||||
|
f(LOCATIONS.level(20, 2), REGIONS.main)
|
||||||
|
for x in range(2, maxlevel+1):
|
||||||
|
f(LOCATIONS.level(x), REGIONS.main)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception(f"Illegal level logic type {logictype}")
|
||||||
|
|
||||||
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def addupgrades(finaltier: int, logictype: str,
|
||||||
|
category_random_logic_amounts: Dict[str, int]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||||
|
"""Returns a dictionary with all upgrade locations based on player options (finaltier INCLUDED).
|
||||||
|
If shape requirements are not randomized, give logic type 0."""
|
||||||
|
|
||||||
|
locations: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||||
|
upgrade_regions = [REGIONS.main, REGIONS.upgrades_1, REGIONS.upgrades_2, REGIONS.upgrades_3,
|
||||||
|
REGIONS.upgrades_4, REGIONS.upgrades_5]
|
||||||
|
|
||||||
|
def f(name: str, region: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||||
|
locations[name] = (region, progress)
|
||||||
|
|
||||||
|
if logictype == OPTIONS.logic_vanilla_like:
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.belt, "II"), REGIONS.main)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.miner, "II"), REGIONS.main)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "II"), REGIONS.main)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.painting, "II"), REGIONS.upgrades_3)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.belt, "III"), REGIONS.upgrades_2)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.miner, "III"), REGIONS.upgrades_2)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "III"), REGIONS.upgrades_1)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.painting, "III"), REGIONS.upgrades_3)
|
||||||
|
for x in range(4, finaltier+1):
|
||||||
|
tier = roman(x)
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_linear:
|
||||||
|
for x in range(2, 7):
|
||||||
|
tier = roman(x)
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, tier), upgrade_regions[x-2])
|
||||||
|
for x in range(7, finaltier+1):
|
||||||
|
tier = roman(x)
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_category:
|
||||||
|
for x in range(2, 7):
|
||||||
|
tier = roman(x)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.belt, tier), REGIONS.main)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.miner, tier), REGIONS.main)
|
||||||
|
for x in range(7, finaltier + 1):
|
||||||
|
tier = roman(x)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.belt, tier), REGIONS.upgrades_5)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.miner, tier), REGIONS.upgrades_5)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "II"), REGIONS.upgrades_1)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "III"), REGIONS.upgrades_2)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "IV"), REGIONS.upgrades_2)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "V"), REGIONS.upgrades_3)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, "VI"), REGIONS.upgrades_3)
|
||||||
|
for x in range(7, finaltier+1):
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, roman(x)), REGIONS.upgrades_5)
|
||||||
|
for x in range(2, 4):
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.painting, roman(x)), REGIONS.upgrades_4)
|
||||||
|
for x in range(4, finaltier+1):
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.painting, roman(x)), REGIONS.upgrades_5)
|
||||||
|
|
||||||
|
elif logictype == OPTIONS.logic_category_random:
|
||||||
|
for x in range(2, 7):
|
||||||
|
tier = roman(x)
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.belt, tier),
|
||||||
|
upgrade_regions[category_random_logic_amounts[CATEGORY.belt_low]])
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.miner, tier),
|
||||||
|
upgrade_regions[category_random_logic_amounts[CATEGORY.miner_low]])
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.processors, tier),
|
||||||
|
upgrade_regions[category_random_logic_amounts[CATEGORY.processors_low]])
|
||||||
|
f(LOCATIONS.upgrade(CATEGORY.painting, tier),
|
||||||
|
upgrade_regions[category_random_logic_amounts[CATEGORY.painting_low]])
|
||||||
|
for x in range(7, finaltier+1):
|
||||||
|
tier = roman(x)
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||||
|
|
||||||
|
else: # logictype == hardcore
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, "II"), REGIONS.main)
|
||||||
|
for x in range(3, finaltier+1):
|
||||||
|
tier = roman(x)
|
||||||
|
for cat in categories:
|
||||||
|
f(LOCATIONS.upgrade(cat, tier), REGIONS.upgrades_5)
|
||||||
|
|
||||||
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def addachievements(excludesoftlock: bool, excludelong: bool, excludeprogressive: bool,
|
||||||
|
maxlevel: int, upgradelogictype: str, category_random_logic_amounts: Dict[str, int],
|
||||||
|
goal: str, presentlocations: Dict[str, Tuple[str, LocationProgressType]],
|
||||||
|
add_alias: Callable[[str, str], None], has_upgrade_traps: bool
|
||||||
|
) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||||
|
"""Returns a dictionary with all achievement locations based on player options."""
|
||||||
|
|
||||||
|
locations: Dict[str, Tuple[str, LocationProgressType]] = dict()
|
||||||
|
upgrade_regions = [REGIONS.main, REGIONS.upgrades_1, REGIONS.upgrades_2, REGIONS.upgrades_3,
|
||||||
|
REGIONS.upgrades_4, REGIONS.upgrades_5]
|
||||||
|
|
||||||
|
def f(name: str, region: str, alias: str, progress: LocationProgressType = LocationProgressType.DEFAULT):
|
||||||
|
locations[name] = (region, progress)
|
||||||
|
add_alias(name, alias)
|
||||||
|
|
||||||
|
f(LOCATIONS.my_eyes, REGIONS.menu, "Activate dark mode")
|
||||||
|
f(LOCATIONS.painter, REGIONS.paint_not_quad, "Paint a shape (no Quad Painter)")
|
||||||
|
f(LOCATIONS.cutter, REGIONS.cut_not_quad, "Cut a shape (no Quad Cutter)")
|
||||||
|
f(LOCATIONS.rotater, REGIONS.rotate_cw, "Rotate a shape clock wise")
|
||||||
|
f(LOCATIONS.wait_they_stack, REGIONS.stack_shape, "Stack a shape")
|
||||||
|
f(LOCATIONS.storage, REGIONS.store_shape, "Store a shape in the storage")
|
||||||
|
f(LOCATIONS.the_logo, REGIONS.all_buildings, "Produce the shapez logo")
|
||||||
|
f(LOCATIONS.to_the_moon, REGIONS.all_buildings, "Produce the rocket shape")
|
||||||
|
f(LOCATIONS.its_piling_up, REGIONS.all_buildings, "100k blueprint shapes")
|
||||||
|
f(LOCATIONS.use_it_later, REGIONS.all_buildings, "1 million blueprint shapes")
|
||||||
|
|
||||||
|
f(LOCATIONS.stack_overflow, REGIONS.stack_shape, "4 layers shape")
|
||||||
|
f(LOCATIONS.its_a_mess, REGIONS.main, "100 different shapes in hub")
|
||||||
|
f(LOCATIONS.get_rid_of_them, REGIONS.trash_shape, "1000 shapes trashed")
|
||||||
|
f(LOCATIONS.getting_into_it, REGIONS.menu, "1 hour")
|
||||||
|
f(LOCATIONS.now_its_easy, REGIONS.blueprint, "Place a blueprint")
|
||||||
|
f(LOCATIONS.computer_guy, REGIONS.wiring, "Place 5000 wires")
|
||||||
|
f(LOCATIONS.perfectionist, REGIONS.any_building, "Destroy more than 1000 objects at once")
|
||||||
|
f(LOCATIONS.next_dimension, REGIONS.wiring, "Open the wires layer")
|
||||||
|
f(LOCATIONS.copy_pasta, REGIONS.blueprint, "Place a 1000 buildings blueprint")
|
||||||
|
f(LOCATIONS.ive_seen_that_before, REGIONS.all_buildings, "Produce RgRyRbRr")
|
||||||
|
f(LOCATIONS.memories, REGIONS.all_buildings, "Produce WrRgWrRg:CwCrCwCr:SgSgSgSg")
|
||||||
|
f(LOCATIONS.i_need_trains, REGIONS.belt, "Have a 500 tiles belt")
|
||||||
|
f(LOCATIONS.gps, REGIONS.menu, "15 map markers")
|
||||||
|
|
||||||
|
# Per second delivery achievements
|
||||||
|
f(LOCATIONS.preparing_to_launch, REGIONS.all_buildings, "10 rocket shapes / second")
|
||||||
|
if not has_upgrade_traps:
|
||||||
|
f(LOCATIONS.spacey, REGIONS.all_buildings, "20 rocket shapes / second")
|
||||||
|
f(LOCATIONS.efficiency_1, REGIONS.all_buildings, "25 blueprints shapes / second")
|
||||||
|
f(LOCATIONS.efficiency_2, REGIONS.all_buildings_x1_6_belt, "50 blueprints shapes / second")
|
||||||
|
f(LOCATIONS.branding_1, REGIONS.all_buildings, "25 logo shapes / second")
|
||||||
|
f(LOCATIONS.branding_2, REGIONS.all_buildings_x1_6_belt, "50 logo shapes / second")
|
||||||
|
|
||||||
|
# Achievements that depend on upgrades
|
||||||
|
f(LOCATIONS.even_faster, REGIONS.upgrades_5, "All upgrades on tier VIII")
|
||||||
|
if upgradelogictype == OPTIONS.logic_linear:
|
||||||
|
f(LOCATIONS.faster, REGIONS.upgrades_3, "All upgrades on tier V")
|
||||||
|
elif upgradelogictype == OPTIONS.logic_category_random:
|
||||||
|
f(LOCATIONS.faster, upgrade_regions[
|
||||||
|
max(category_random_logic_amounts[CATEGORY.belt_low],
|
||||||
|
category_random_logic_amounts[CATEGORY.miner_low],
|
||||||
|
category_random_logic_amounts[CATEGORY.processors_low],
|
||||||
|
category_random_logic_amounts[CATEGORY.painting_low])
|
||||||
|
], "All upgrades on tier V")
|
||||||
|
else:
|
||||||
|
f(LOCATIONS.faster, REGIONS.upgrades_5, "All upgrades on tier V")
|
||||||
|
|
||||||
|
# Achievements that depend on the level
|
||||||
|
f(LOCATIONS.wires, presentlocations[LOCATIONS.level(20)][0], "Complete level 20")
|
||||||
|
if not goal == GOALS.vanilla:
|
||||||
|
f(LOCATIONS.freedom, presentlocations[LOCATIONS.level(26)][0], "Complete level 26")
|
||||||
|
f(LOCATIONS.mam, REGIONS.mam, "Complete any level > 26 without modifications")
|
||||||
|
if maxlevel >= 50:
|
||||||
|
f(LOCATIONS.cant_stop, presentlocations[LOCATIONS.level(50)][0], "Reach level 50")
|
||||||
|
elif goal not in [GOALS.vanilla, GOALS.mam]:
|
||||||
|
f(LOCATIONS.cant_stop, REGIONS.levels_5, "Reach level 50")
|
||||||
|
if maxlevel >= 100:
|
||||||
|
f(LOCATIONS.is_this_the_end, presentlocations[LOCATIONS.level(100)][0], "Reach level 100")
|
||||||
|
elif goal not in [GOALS.vanilla, GOALS.mam]:
|
||||||
|
f(LOCATIONS.is_this_the_end, REGIONS.levels_5, "Reach level 100")
|
||||||
|
|
||||||
|
# Achievements that depend on player preferences
|
||||||
|
if excludeprogressive:
|
||||||
|
unreasonable_type = LocationProgressType.EXCLUDED
|
||||||
|
else:
|
||||||
|
unreasonable_type = LocationProgressType.DEFAULT
|
||||||
|
if not excludesoftlock:
|
||||||
|
f(LOCATIONS.speedrun_master, presentlocations[LOCATIONS.level(12)][0],
|
||||||
|
"Complete level 12 in under 30 min", unreasonable_type)
|
||||||
|
f(LOCATIONS.speedrun_novice, presentlocations[LOCATIONS.level(12)][0],
|
||||||
|
"Complete level 12 in under 60 min", unreasonable_type)
|
||||||
|
f(LOCATIONS.not_idle_game, presentlocations[LOCATIONS.level(12)][0],
|
||||||
|
"Complete level 12 in under 120 min", unreasonable_type)
|
||||||
|
f(LOCATIONS.its_so_slow, presentlocations[LOCATIONS.level(12)][0],
|
||||||
|
"Complete level 12 without upgrading belts", unreasonable_type)
|
||||||
|
f(LOCATIONS.king_of_inefficiency, presentlocations[LOCATIONS.level(14)][0],
|
||||||
|
"No ccw rotator until level 14", unreasonable_type)
|
||||||
|
f(LOCATIONS.a_bit_early, REGIONS.all_buildings,
|
||||||
|
"Produce logo shape before level 18", unreasonable_type)
|
||||||
|
if not excludelong:
|
||||||
|
f(LOCATIONS.a_long_time, REGIONS.menu, "10 hours")
|
||||||
|
f(LOCATIONS.addicted, REGIONS.menu, "20 hours")
|
||||||
|
|
||||||
|
# Achievements with a softlock chance of less than
|
||||||
|
# 1 divided by 2 to the power of the number of all atoms in the universe
|
||||||
|
f(LOCATIONS.oops, REGIONS.main, "Deliver an irrelevant shape")
|
||||||
|
|
||||||
|
return locations
|
||||||
|
|
||||||
|
|
||||||
|
def addshapesanity(amount: int, random: Random, append_shapesanity: Callable[[str], None],
|
||||||
|
add_alias: Callable[[str, str], None]) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||||
|
"""Returns a dictionary with a given number of random shapesanity locations."""
|
||||||
|
|
||||||
|
included_shapes: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||||
|
|
||||||
|
def f(name: str, region: str, alias: str, progress: LocationProgressType = LocationProgressType.DEFAULT) -> None:
|
||||||
|
included_shapes[name] = (region, progress)
|
||||||
|
append_shapesanity(alias)
|
||||||
|
shapes_list.remove((alias, region))
|
||||||
|
add_alias(name, alias)
|
||||||
|
|
||||||
|
# Always have at least 4 shapesanity checks because of sphere 1 usefulls + both hardcore logic
|
||||||
|
shapes_list = list(shapesanity_simple.items())
|
||||||
|
f(LOCATIONS.shapesanity(1), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||||
|
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.circle))
|
||||||
|
f(LOCATIONS.shapesanity(2), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||||
|
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.square))
|
||||||
|
f(LOCATIONS.shapesanity(3), REGIONS.sanity(REGIONS.full, REGIONS.uncol),
|
||||||
|
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.star))
|
||||||
|
f(LOCATIONS.shapesanity(4), REGIONS.sanity(REGIONS.east_wind, REGIONS.uncol),
|
||||||
|
SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.windmill))
|
||||||
|
|
||||||
|
# The pool switches dynamically depending on if either it's ratio or limit is reached
|
||||||
|
switched = 0
|
||||||
|
for counting in range(4, amount):
|
||||||
|
if switched == 0 and (len(shapes_list) == 0 or counting == amount//2):
|
||||||
|
shapes_list = list(shapesanity_1_4.items())
|
||||||
|
switched = 1
|
||||||
|
elif switched == 1 and (len(shapes_list) == 0 or counting == amount*7//12):
|
||||||
|
shapes_list = list(shapesanity_two_sided.items())
|
||||||
|
switched = 2
|
||||||
|
elif switched == 2 and (len(shapes_list) == 0 or counting == amount*5//6):
|
||||||
|
shapes_list = list(shapesanity_three_parts.items())
|
||||||
|
switched = 3
|
||||||
|
elif switched == 3 and (len(shapes_list) == 0 or counting == amount*11//12):
|
||||||
|
shapes_list = list(shapesanity_four_parts.items())
|
||||||
|
switched = 4
|
||||||
|
x = random.randint(0, len(shapes_list)-1)
|
||||||
|
next_shape = shapes_list.pop(x)
|
||||||
|
included_shapes[LOCATIONS.shapesanity(counting+1)] = (next_shape[1], LocationProgressType.DEFAULT)
|
||||||
|
append_shapesanity(next_shape[0])
|
||||||
|
add_alias(LOCATIONS.shapesanity(counting+1), next_shape[0])
|
||||||
|
|
||||||
|
return included_shapes
|
||||||
|
|
||||||
|
|
||||||
|
def addshapesanity_ut(shapesanity_names: List[str], add_alias: Callable[[str, str], None]
|
||||||
|
) -> Dict[str, Tuple[str, LocationProgressType]]:
|
||||||
|
"""Returns the same information as addshapesanity but will add specific values based on a UT rebuild."""
|
||||||
|
|
||||||
|
included_shapes: Dict[str, Tuple[str, LocationProgressType]] = {}
|
||||||
|
|
||||||
|
for name in shapesanity_names:
|
||||||
|
for options in [shapesanity_simple, shapesanity_1_4, shapesanity_two_sided, shapesanity_three_parts,
|
||||||
|
shapesanity_four_parts]:
|
||||||
|
if name in options:
|
||||||
|
next_shape = options[name]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Could not find shapesanity name {name}")
|
||||||
|
included_shapes[LOCATIONS.shapesanity(len(included_shapes)+1)] = (next_shape, LocationProgressType.DEFAULT)
|
||||||
|
add_alias(LOCATIONS.shapesanity(len(included_shapes)), name)
|
||||||
|
return included_shapes
|
||||||
|
|
||||||
|
|
||||||
|
class ShapezLocation(Location):
|
||||||
|
game = OTHER.game_name
|
||||||
|
|
||||||
|
def __init__(self, player: int, name: str, address: Optional[int], region: Region,
|
||||||
|
progress_type: LocationProgressType):
|
||||||
|
super(ShapezLocation, self).__init__(player, name, address, region)
|
||||||
|
self.progress_type = progress_type
|
310
worlds/shapez/options.py
Normal file
310
worlds/shapez/options.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import pkgutil
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
|
from Options import Toggle, Choice, PerGameCommonOptions, NamedRange, Range
|
||||||
|
from .common.options import FloatRangeText
|
||||||
|
|
||||||
|
datapackage_options = orjson.loads(pkgutil.get_data(__name__, "data/options.json"))
|
||||||
|
max_levels_and_upgrades = datapackage_options["max_levels_and_upgrades"]
|
||||||
|
max_shapesanity = datapackage_options["max_shapesanity"]
|
||||||
|
del datapackage_options
|
||||||
|
|
||||||
|
|
||||||
|
class Goal(Choice):
|
||||||
|
"""Sets the goal of your world.
|
||||||
|
|
||||||
|
- **Vanilla:** Complete level 26.
|
||||||
|
- **MAM:** Complete a specified level after level 26. Every level before that will be a location. It's recommended
|
||||||
|
to build a Make-Anything-Machine (MAM).
|
||||||
|
- **Even fasterer:** Upgrade everything to a specified tier after tier 8. Every upgrade before that will be a
|
||||||
|
location.
|
||||||
|
- **Efficiency III:** Deliver 256 blueprint shapes per second to the hub."""
|
||||||
|
display_name = "Goal"
|
||||||
|
rich_text_doc = True
|
||||||
|
option_vanilla = 0
|
||||||
|
option_mam = 1
|
||||||
|
option_even_fasterer = 2
|
||||||
|
option_efficiency_iii = 3
|
||||||
|
default = 0
|
||||||
|
|
||||||
|
|
||||||
|
class GoalAmount(NamedRange):
|
||||||
|
"""Specify, what level or tier (when either MAM or Even Fasterer is chosen as goal) is required to reach the goal.
|
||||||
|
|
||||||
|
If MAM is set as the goal, this has to be set to 27 or more. Else it will raise an error."""
|
||||||
|
display_name = "Goal amount"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 9
|
||||||
|
range_end = max_levels_and_upgrades
|
||||||
|
default = 27
|
||||||
|
special_range_names = {
|
||||||
|
"minimum_mam": 27,
|
||||||
|
"recommended_mam": 50,
|
||||||
|
"long_game_mam": 120,
|
||||||
|
"minimum_even_fasterer": 9,
|
||||||
|
"recommended_even_fasterer": 16,
|
||||||
|
"long_play_even_fasterer": 35,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredShapesMultiplier(Range):
|
||||||
|
"""Multiplies the amount of required shapes for levels and upgrades by value/10.
|
||||||
|
|
||||||
|
For level 1, the amount of shapes ranges from 3 to 300.
|
||||||
|
|
||||||
|
For level 26, it ranges from 5k to 500k."""
|
||||||
|
display_name = "Required shapes multiplier"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 1
|
||||||
|
range_end = 100
|
||||||
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
|
class AllowFloatingLayers(Toggle):
|
||||||
|
"""Toggle whether shape requirements are allowed to have floating layers (like the logo or the rocket shape).
|
||||||
|
|
||||||
|
However, be aware that floating shapes make MAMs much more complex."""
|
||||||
|
display_name = "Allow floating layers"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeLevelRequirements(Toggle):
|
||||||
|
"""Randomize the required shapes to complete levels."""
|
||||||
|
display_name = "Randomize level requirements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeUpgradeRequirements(Toggle):
|
||||||
|
"""Randomize the required shapes to buy upgrades."""
|
||||||
|
display_name = "Randomize upgrade requirements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeLevelLogic(Choice):
|
||||||
|
"""If level requirements are randomized, this sets how those random shapes are generated and how logic works for
|
||||||
|
levels. The shuffled variants shuffle the order of progression buildings obtained in the multiworld. The standard
|
||||||
|
order is: **cutter -> rotator -> painter -> color mixer -> stacker**
|
||||||
|
|
||||||
|
- **Vanilla:** Level 1 requires nothing, 2-4 require the first building, 5-6 require also the second, 7-8 the
|
||||||
|
third, 9-10 the fourth, and 11 and onwards the fifth and thereby all buildings.
|
||||||
|
- **Stretched:** After every floor(maxlevel/6) levels, another building is required.
|
||||||
|
- **Quick:** Every Level, except level 1, requires another building, with level 6 and onwards requiring all
|
||||||
|
buildings.
|
||||||
|
- **Random steps:** After a random amount of levels, another building is required, with level 1 always requiring
|
||||||
|
none. This can potentially generate like any other option.
|
||||||
|
- **Hardcore:** All levels (except level 1) have completely random shape requirements and thus require all
|
||||||
|
buildings. Expect early BKs.
|
||||||
|
- **Dopamine (overflow):** All levels (except level 1 and the goal) require 2 random buildings (or none in case of
|
||||||
|
overflow)."""
|
||||||
|
display_name = "Randomize level logic"
|
||||||
|
rich_text_doc = True
|
||||||
|
option_vanilla = 0
|
||||||
|
option_vanilla_shuffled = 1
|
||||||
|
option_stretched = 2
|
||||||
|
option_stretched_shuffled = 3
|
||||||
|
option_quick = 4
|
||||||
|
option_quick_shuffled = 5
|
||||||
|
option_random_steps = 6
|
||||||
|
option_random_steps_shuffled = 7
|
||||||
|
option_hardcore = 8
|
||||||
|
option_dopamine = 9
|
||||||
|
option_dopamine_overflow = 10
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
|
class RandomizeUpgradeLogic(Choice):
|
||||||
|
"""If upgrade requirements are randomized, this sets how those random shapes are generated
|
||||||
|
and how logic works for upgrades.
|
||||||
|
|
||||||
|
- **Vanilla-like:** Tier II requires up to two random buildings, III requires up to three random buildings,
|
||||||
|
and IV and onwards require all processing buildings.
|
||||||
|
- **Linear:** Tier II requires nothing, III-VI require another random building each,
|
||||||
|
and VII and onwards require all buildings.
|
||||||
|
- **Category:** Belt and miner upgrades require no building up to tier V, but onwards all buildings, processors
|
||||||
|
upgrades require the cutter (all tiers), rotator (tier III and onwards), and stacker (tier V and onwards), and
|
||||||
|
painting upgrades require the cutter, rotator, stacker, painter (all tiers) and color mixer (tiers V and onwards).
|
||||||
|
Tier VII and onwards will always require all buildings.
|
||||||
|
- **Category random:** Each upgrades category (up to tier VI) requires a random amount of buildings (in order),
|
||||||
|
with one category always requiring no buildings. Tier VII and onwards will always require all buildings.
|
||||||
|
- **Hardcore:** All tiers (except each tier II) have completely random shape requirements and thus require all
|
||||||
|
buildings. Expect early BKs."""
|
||||||
|
display_name = "Randomize upgrade logic"
|
||||||
|
rich_text_doc = True
|
||||||
|
option_vanilla_like = 0
|
||||||
|
option_linear = 1
|
||||||
|
option_category = 2
|
||||||
|
option_category_random = 3
|
||||||
|
option_hardcore = 4
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ThroughputLevelsRatio(NamedRange):
|
||||||
|
"""If level requirements are randomized, this sets the ratio of how many levels (approximately) will require either
|
||||||
|
a total amount or per second amount (throughput) of shapes delivered.
|
||||||
|
|
||||||
|
0 means only total, 100 means only throughput, and vanilla (-1) means only levels 14, 27 and beyond have throughput.
|
||||||
|
"""
|
||||||
|
display_name = "Throughput levels ratio"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 0
|
||||||
|
range_end = 100
|
||||||
|
default = 0
|
||||||
|
special_range_names = {
|
||||||
|
"vanilla": -1,
|
||||||
|
"only_total": 0,
|
||||||
|
"half_half": 50,
|
||||||
|
"only_throughput": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ComplexityGrowthGradient(FloatRangeText):
|
||||||
|
"""If level requirements are randomized, this determines how fast complexity will grow each level. In other words:
|
||||||
|
The higher you set this value, the more difficult lategame shapes will be.
|
||||||
|
|
||||||
|
Allowed values are floating numbers ranging from 0.0 to 10.0."""
|
||||||
|
display_name = "Complexity growth gradient"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 0.0
|
||||||
|
range_end = 10.0
|
||||||
|
default = "0.5"
|
||||||
|
|
||||||
|
|
||||||
|
class SameLateUpgradeRequirements(Toggle):
|
||||||
|
"""If upgrade requirements are randomized, should the last 3 shapes for each category be the same,
|
||||||
|
as in vanilla?"""
|
||||||
|
display_name = "Same late upgrade requirements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class EarlyBalancerTunnelAndTrash(Choice):
|
||||||
|
"""Makes the balancer, tunnel, and trash appear in earlier spheres.
|
||||||
|
|
||||||
|
- **None:** Complete randomization.
|
||||||
|
- **5 buildings:** Should be accessible before getting all 5 main buildings.
|
||||||
|
- **3 buildings:** Should be accessible before getting the first 3 main buildings for levels and upgrades.
|
||||||
|
- **Sphere 1:** Always accessible from start. **Beware of generation failures.**"""
|
||||||
|
display_name = "Early balancer, tunnel, and trash"
|
||||||
|
rich_text_doc = True
|
||||||
|
option_none = 0
|
||||||
|
option_5_buildings = 1
|
||||||
|
option_3_buildings = 2
|
||||||
|
option_sphere_1 = 3
|
||||||
|
default = 2
|
||||||
|
|
||||||
|
|
||||||
|
class LockBeltAndExtractor(Toggle):
|
||||||
|
"""Locks Belts and Extractors and adds them to the item pool.
|
||||||
|
|
||||||
|
**If you set this to true, achievements must also be included.**"""
|
||||||
|
display_name = "Lock Belt and Extractor"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class IncludeAchievements(Toggle):
|
||||||
|
"""Include up to 45 achievements (depending on other options) as additional locations."""
|
||||||
|
display_name = "Include Achievements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class ExcludeSoftlockAchievements(Toggle):
|
||||||
|
"""Exclude 6 achievements, that can become unreachable in a save file, if not achieved until a certain level."""
|
||||||
|
display_name = "Exclude softlock achievements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class ExcludeLongPlaytimeAchievements(Toggle):
|
||||||
|
"""Exclude 2 achievements, that require actively playing for a really long time."""
|
||||||
|
display_name = "Exclude long playtime achievements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class ExcludeProgressionUnreasonable(Toggle):
|
||||||
|
"""Exclude progression and useful items from being placed into softlock and long playtime achievements."""
|
||||||
|
display_name = "Exclude progression items in softlock and long playtime achievements"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
class ShapesanityAmount(Range):
|
||||||
|
"""Amount of single-layer shapes that will be included as locations."""
|
||||||
|
display_name = "Shapesanity amount"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 4
|
||||||
|
range_end = max_shapesanity
|
||||||
|
default = 50
|
||||||
|
|
||||||
|
|
||||||
|
class TrapsProbability(NamedRange):
|
||||||
|
"""The probability of any filler item (in percent) being replaced by a trap."""
|
||||||
|
display_name = "Traps Percentage"
|
||||||
|
rich_text_doc = True
|
||||||
|
range_start = 0
|
||||||
|
range_end = 100
|
||||||
|
default = 0
|
||||||
|
special_range_names = {
|
||||||
|
"none": 0,
|
||||||
|
"rare": 4,
|
||||||
|
"occasionally": 10,
|
||||||
|
"maximum_suffering": 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class IncludeWhackyUpgrades(Toggle):
|
||||||
|
"""Includes some very unusual upgrade items in generation (and logic), that greatly increase or decrease building
|
||||||
|
speeds. If the goal is set to Efficiency III or throughput levels ratio is not 0, decreasing upgrades (aka traps)
|
||||||
|
will always be disabled."""
|
||||||
|
display_name = "Include Whacky Upgrades"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class SplitInventoryDrainingTrap(Toggle):
|
||||||
|
"""If set to true, the inventory draining trap will be split into level, upgrade, and blueprint draining traps
|
||||||
|
instead of executing as one of those 3 randomly."""
|
||||||
|
display_name = "Split Inventory Draining Trap"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = False
|
||||||
|
|
||||||
|
|
||||||
|
class ToolbarShuffling(Toggle):
|
||||||
|
"""If set to true, the toolbars (main and wires layer) will be shuffled (including bottom and top row).
|
||||||
|
However, keybindings will still select the same building to place."""
|
||||||
|
display_name = "Toolbar Shuffling"
|
||||||
|
rich_text_doc = True
|
||||||
|
default = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShapezOptions(PerGameCommonOptions):
|
||||||
|
goal: Goal
|
||||||
|
goal_amount: GoalAmount
|
||||||
|
required_shapes_multiplier: RequiredShapesMultiplier
|
||||||
|
allow_floating_layers: AllowFloatingLayers
|
||||||
|
randomize_level_requirements: RandomizeLevelRequirements
|
||||||
|
randomize_upgrade_requirements: RandomizeUpgradeRequirements
|
||||||
|
randomize_level_logic: RandomizeLevelLogic
|
||||||
|
randomize_upgrade_logic: RandomizeUpgradeLogic
|
||||||
|
throughput_levels_ratio: ThroughputLevelsRatio
|
||||||
|
complexity_growth_gradient: ComplexityGrowthGradient
|
||||||
|
same_late_upgrade_requirements: SameLateUpgradeRequirements
|
||||||
|
early_balancer_tunnel_and_trash: EarlyBalancerTunnelAndTrash
|
||||||
|
lock_belt_and_extractor: LockBeltAndExtractor
|
||||||
|
include_achievements: IncludeAchievements
|
||||||
|
exclude_softlock_achievements: ExcludeSoftlockAchievements
|
||||||
|
exclude_long_playtime_achievements: ExcludeLongPlaytimeAchievements
|
||||||
|
exclude_progression_unreasonable: ExcludeProgressionUnreasonable
|
||||||
|
shapesanity_amount: ShapesanityAmount
|
||||||
|
traps_percentage: TrapsProbability
|
||||||
|
include_whacky_upgrades: IncludeWhackyUpgrades
|
||||||
|
split_inventory_draining_trap: SplitInventoryDrainingTrap
|
||||||
|
toolbar_shuffling: ToolbarShuffling
|
49
worlds/shapez/presets.py
Normal file
49
worlds/shapez/presets.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from .options import max_levels_and_upgrades, max_shapesanity
|
||||||
|
|
||||||
|
options_presets = {
|
||||||
|
"Most vanilla": {
|
||||||
|
"goal": "vanilla",
|
||||||
|
"randomize_level_requirements": False,
|
||||||
|
"randomize_upgrade_requirements": False,
|
||||||
|
"early_balancer_tunnel_and_trash": "3_buildings",
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": False,
|
||||||
|
"exclude_long_playtime_achievements": False,
|
||||||
|
"shapesanity_amount": 4,
|
||||||
|
"toolbar_shuffling": False,
|
||||||
|
},
|
||||||
|
"Minimum checks": {
|
||||||
|
"goal": "vanilla",
|
||||||
|
"include_achievements": False,
|
||||||
|
"shapesanity_amount": 4
|
||||||
|
},
|
||||||
|
"Maximum checks": {
|
||||||
|
"goal": "even_fasterer",
|
||||||
|
"goal_amount": max_levels_and_upgrades,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": False,
|
||||||
|
"exclude_long_playtime_achievements": False,
|
||||||
|
"shapesanity_amount": max_shapesanity
|
||||||
|
},
|
||||||
|
"Restrictive start": {
|
||||||
|
"goal": "vanilla",
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": "hardcore",
|
||||||
|
"randomize_upgrade_logic": "hardcore",
|
||||||
|
"early_balancer_tunnel_and_trash": "sphere_1",
|
||||||
|
"include_achievements": False,
|
||||||
|
"shapesanity_amount": 4
|
||||||
|
},
|
||||||
|
"Quick game": {
|
||||||
|
"goal": "efficiency_iii",
|
||||||
|
"required_shapes_multiplier": 1,
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": "hardcore",
|
||||||
|
"randomize_upgrade_logic": "hardcore",
|
||||||
|
"include_achievements": False,
|
||||||
|
"shapesanity_amount": 4,
|
||||||
|
"include_whacky_upgrades": True,
|
||||||
|
}
|
||||||
|
}
|
277
worlds/shapez/regions.py
Normal file
277
worlds/shapez/regions.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
from typing import Dict, Tuple, List
|
||||||
|
|
||||||
|
from BaseClasses import Region, MultiWorld, LocationProgressType, ItemClassification, CollectionState
|
||||||
|
from .items import ShapezItem
|
||||||
|
from .locations import ShapezLocation
|
||||||
|
from .data.strings import ITEMS, REGIONS, GOALS, LOCATIONS, OPTIONS
|
||||||
|
from worlds.generic.Rules import add_rule
|
||||||
|
|
||||||
|
shapesanity_processing = [REGIONS.full, REGIONS.half, REGIONS.piece, REGIONS.stitched, REGIONS.east_wind,
|
||||||
|
REGIONS.half_half, REGIONS.col_east_wind, REGIONS.col_half_half, REGIONS.col_full,
|
||||||
|
REGIONS.col_half]
|
||||||
|
shapesanity_coloring = [REGIONS.uncol, REGIONS.painted, REGIONS.mixed]
|
||||||
|
|
||||||
|
all_regions = [
|
||||||
|
REGIONS.menu, REGIONS.belt, REGIONS.extract, REGIONS.main,
|
||||||
|
REGIONS.levels_1, REGIONS.levels_2, REGIONS.levels_3, REGIONS.levels_4, REGIONS.levels_5,
|
||||||
|
REGIONS.upgrades_1, REGIONS.upgrades_2, REGIONS.upgrades_3, REGIONS.upgrades_4, REGIONS.upgrades_5,
|
||||||
|
REGIONS.paint_not_quad, REGIONS.cut_not_quad, REGIONS.rotate_cw, REGIONS.stack_shape, REGIONS.store_shape,
|
||||||
|
REGIONS.trash_shape, REGIONS.blueprint, REGIONS.wiring, REGIONS.mam, REGIONS.any_building,
|
||||||
|
REGIONS.all_buildings, REGIONS.all_buildings_x1_6_belt,
|
||||||
|
*[REGIONS.sanity(processing, coloring)
|
||||||
|
for processing in shapesanity_processing
|
||||||
|
for coloring in shapesanity_coloring],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def can_cut_half(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has(ITEMS.cutter, player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_rotate_90(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has_any((ITEMS.rotator, ITEMS.rotator_ccw), player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_rotate_180(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has_any((ITEMS.rotator, ITEMS.rotator_ccw, ITEMS.rotator_180), player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_stack(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has(ITEMS.stacker, player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_paint(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has_any((ITEMS.painter, ITEMS.painter_double), player) or can_use_quad_painter(state, player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_mix_colors(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has(ITEMS.color_mixer, player)
|
||||||
|
|
||||||
|
|
||||||
|
def has_tunnel(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has_any((ITEMS.tunnel, ITEMS.tunnel_tier_ii), player)
|
||||||
|
|
||||||
|
|
||||||
|
def has_balancer(state: CollectionState, player: int) -> bool:
|
||||||
|
return state.has(ITEMS.balancer, player) or state.has_all((ITEMS.comp_merger, ITEMS.comp_splitter), player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_use_quad_painter(state: CollectionState, player: int) -> bool:
|
||||||
|
return (state.has_all((ITEMS.painter_quad, ITEMS.wires), player) and
|
||||||
|
state.has_any((ITEMS.switch, ITEMS.const_signal), player))
|
||||||
|
|
||||||
|
|
||||||
|
def can_make_stitched_shape(state: CollectionState, player: int, floating: bool) -> bool:
|
||||||
|
return (can_stack(state, player) and
|
||||||
|
((state.has(ITEMS.cutter_quad, player) and not floating) or
|
||||||
|
(can_cut_half(state, player) and can_rotate_90(state, player))))
|
||||||
|
|
||||||
|
|
||||||
|
def can_build_mam(state: CollectionState, player: int, floating: bool) -> bool:
|
||||||
|
return (can_make_stitched_shape(state, player, floating) and can_paint(state, player) and
|
||||||
|
can_mix_colors(state, player) and has_balancer(state, player) and has_tunnel(state, player) and
|
||||||
|
state.has_all((ITEMS.belt_reader, ITEMS.storage, ITEMS.item_filter,
|
||||||
|
ITEMS.wires, ITEMS.logic_gates, ITEMS.virtual_proc), player))
|
||||||
|
|
||||||
|
|
||||||
|
def can_make_east_windmill(state: CollectionState, player: int) -> bool:
|
||||||
|
# Only used for shapesanity => single layers
|
||||||
|
return (can_stack(state, player) and
|
||||||
|
(state.has(ITEMS.cutter_quad, player) or (can_cut_half(state, player) and can_rotate_180(state, player))))
|
||||||
|
|
||||||
|
|
||||||
|
def can_make_half_half_shape(state: CollectionState, player: int) -> bool:
|
||||||
|
# Only used for shapesanity => single layers
|
||||||
|
return can_stack(state, player) and state.has_any((ITEMS.cutter, ITEMS.cutter_quad), player)
|
||||||
|
|
||||||
|
|
||||||
|
def can_make_half_shape(state: CollectionState, player: int) -> bool:
|
||||||
|
# Only used for shapesanity => single layers
|
||||||
|
return can_cut_half(state, player) or state.has_all((ITEMS.cutter_quad, ITEMS.stacker), player)
|
||||||
|
|
||||||
|
|
||||||
|
def has_x_belt_multiplier(state: CollectionState, player: int, needed: float) -> bool:
|
||||||
|
# Assumes there are no upgrade traps
|
||||||
|
multiplier = 1.0
|
||||||
|
# Rising upgrades do the least improvement if received before other upgrades
|
||||||
|
for _ in range(state.count(ITEMS.upgrade_rising_belt, player)):
|
||||||
|
multiplier *= 2
|
||||||
|
multiplier += state.count(ITEMS.upgrade_gigantic_belt, player)*10
|
||||||
|
multiplier += state.count(ITEMS.upgrade_big_belt, player)
|
||||||
|
multiplier += state.count(ITEMS.upgrade_small_belt, player)*0.1
|
||||||
|
return multiplier >= needed
|
||||||
|
|
||||||
|
|
||||||
|
def has_logic_list_building(state: CollectionState, player: int, buildings: List[str], index: int,
|
||||||
|
includeuseful: bool) -> bool:
|
||||||
|
|
||||||
|
# Includes balancer, tunnel, and trash in logic in order to make them appear in earlier spheres
|
||||||
|
if includeuseful and not (state.has(ITEMS.trash, player) and has_balancer(state, player) and
|
||||||
|
has_tunnel(state, player)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if buildings[index] == ITEMS.cutter:
|
||||||
|
if buildings.index(ITEMS.stacker) < index:
|
||||||
|
return state.has_any((ITEMS.cutter, ITEMS.cutter_quad), player)
|
||||||
|
else:
|
||||||
|
return can_cut_half(state, player)
|
||||||
|
elif buildings[index] == ITEMS.rotator:
|
||||||
|
return can_rotate_90(state, player)
|
||||||
|
elif buildings[index] == ITEMS.stacker:
|
||||||
|
return can_stack(state, player)
|
||||||
|
elif buildings[index] == ITEMS.painter:
|
||||||
|
return can_paint(state, player)
|
||||||
|
elif buildings[index] == ITEMS.color_mixer:
|
||||||
|
return can_mix_colors(state, player)
|
||||||
|
|
||||||
|
|
||||||
|
def create_shapez_regions(player: int, multiworld: MultiWorld, floating: bool,
|
||||||
|
included_locations: Dict[str, Tuple[str, LocationProgressType]],
|
||||||
|
location_name_to_id: Dict[str, int], level_logic_buildings: List[str],
|
||||||
|
upgrade_logic_buildings: List[str], early_useful: str, goal: str) -> List[Region]:
|
||||||
|
"""Creates and returns a list of all regions with entrances and all locations placed correctly."""
|
||||||
|
regions: Dict[str, Region] = {name: Region(name, player, multiworld) for name in all_regions}
|
||||||
|
|
||||||
|
# Creates ShapezLocations for every included location and puts them into the correct region
|
||||||
|
for name, data in included_locations.items():
|
||||||
|
regions[data[0]].locations.append(ShapezLocation(player, name, location_name_to_id[name],
|
||||||
|
regions[data[0]], data[1]))
|
||||||
|
|
||||||
|
# Create goal event
|
||||||
|
if goal in [GOALS.vanilla, GOALS.mam]:
|
||||||
|
goal_region = regions[REGIONS.levels_5]
|
||||||
|
elif goal == GOALS.even_fasterer:
|
||||||
|
goal_region = regions[REGIONS.upgrades_5]
|
||||||
|
else:
|
||||||
|
goal_region = regions[REGIONS.all_buildings]
|
||||||
|
goal_location = ShapezLocation(player, LOCATIONS.goal, None, goal_region, LocationProgressType.DEFAULT)
|
||||||
|
goal_location.place_locked_item(ShapezItem(ITEMS.goal, ItemClassification.progression_skip_balancing, None, player))
|
||||||
|
if goal == GOALS.efficiency_iii:
|
||||||
|
add_rule(goal_location, lambda state: has_x_belt_multiplier(state, player, 8))
|
||||||
|
goal_region.locations.append(goal_location)
|
||||||
|
multiworld.completion_condition[player] = lambda state: state.has(ITEMS.goal, player)
|
||||||
|
|
||||||
|
# Connect Menu to rest of regions
|
||||||
|
regions[REGIONS.menu].connect(regions[REGIONS.belt], "Placing belts", lambda state: state.has(ITEMS.belt, player))
|
||||||
|
regions[REGIONS.menu].connect(regions[REGIONS.extract], "Extracting shapes from patches",
|
||||||
|
lambda state: state.has_any((ITEMS.extractor, ITEMS.extractor_chain), player))
|
||||||
|
regions[REGIONS.extract].connect(
|
||||||
|
regions[REGIONS.main], "Transporting shapes over the canvas",
|
||||||
|
lambda state: state.has_any((ITEMS.belt, ITEMS.comp_merger, ITEMS.comp_splitter), player)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Connect achievement regions
|
||||||
|
regions[REGIONS.main].connect(regions[REGIONS.paint_not_quad], "Painting with (double) painter",
|
||||||
|
lambda state: state.has_any((ITEMS.painter, ITEMS.painter_double), player))
|
||||||
|
regions[REGIONS.extract].connect(regions[REGIONS.cut_not_quad], "Cutting with half cutter",
|
||||||
|
lambda state: can_cut_half(state, player))
|
||||||
|
regions[REGIONS.extract].connect(regions[REGIONS.rotate_cw], "Rotating clockwise",
|
||||||
|
lambda state: state.has(ITEMS.rotator, player))
|
||||||
|
regions[REGIONS.extract].connect(regions[REGIONS.stack_shape], "Stacking shapes",
|
||||||
|
lambda state: can_stack(state, player))
|
||||||
|
regions[REGIONS.extract].connect(regions[REGIONS.store_shape], "Storing shapes",
|
||||||
|
lambda state: state.has(ITEMS.storage, player))
|
||||||
|
regions[REGIONS.extract].connect(regions[REGIONS.trash_shape], "Trashing shapes",
|
||||||
|
lambda state: state.has(ITEMS.trash, player))
|
||||||
|
regions[REGIONS.main].connect(regions[REGIONS.blueprint], "Copying and placing blueprints",
|
||||||
|
lambda state: state.has(ITEMS.blueprints, player) and
|
||||||
|
can_make_stitched_shape(state, player, floating) and
|
||||||
|
can_paint(state, player) and can_mix_colors(state, player))
|
||||||
|
regions[REGIONS.menu].connect(regions[REGIONS.wiring], "Using the wires layer",
|
||||||
|
lambda state: state.has(ITEMS.wires, player))
|
||||||
|
regions[REGIONS.main].connect(regions[REGIONS.mam], "Building a MAM",
|
||||||
|
lambda state: can_build_mam(state, player, floating))
|
||||||
|
regions[REGIONS.menu].connect(regions[REGIONS.any_building], "Placing any building", lambda state: state.has_any((
|
||||||
|
ITEMS.belt, ITEMS.balancer, ITEMS.comp_merger, ITEMS.comp_splitter, ITEMS.tunnel, ITEMS.tunnel_tier_ii,
|
||||||
|
ITEMS.extractor, ITEMS.extractor_chain, ITEMS.cutter, ITEMS.cutter_quad, ITEMS.rotator, ITEMS.rotator_ccw,
|
||||||
|
ITEMS.rotator_180, ITEMS.stacker, ITEMS.painter, ITEMS.painter_double, ITEMS.painter_quad, ITEMS.color_mixer,
|
||||||
|
ITEMS.trash, ITEMS.belt_reader, ITEMS.storage, ITEMS.switch, ITEMS.item_filter, ITEMS.display, ITEMS.wires
|
||||||
|
), player))
|
||||||
|
regions[REGIONS.main].connect(regions[REGIONS.all_buildings], "Using all main buildings",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating) and
|
||||||
|
can_paint(state, player) and can_mix_colors(state, player))
|
||||||
|
regions[REGIONS.all_buildings].connect(regions[REGIONS.all_buildings_x1_6_belt],
|
||||||
|
"Delivering per second with 1.6x belt speed",
|
||||||
|
lambda state: has_x_belt_multiplier(state, player, 1.6))
|
||||||
|
|
||||||
|
# Progressively connect level and upgrade regions
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.levels_1], "Using first level building",
|
||||||
|
lambda state: has_logic_list_building(state, player, level_logic_buildings, 0, False))
|
||||||
|
regions[REGIONS.levels_1].connect(
|
||||||
|
regions[REGIONS.levels_2], "Using second level building",
|
||||||
|
lambda state: has_logic_list_building(state, player, level_logic_buildings, 1, False))
|
||||||
|
regions[REGIONS.levels_2].connect(
|
||||||
|
regions[REGIONS.levels_3], "Using third level building",
|
||||||
|
lambda state: has_logic_list_building(state, player, level_logic_buildings, 2,
|
||||||
|
early_useful == OPTIONS.buildings_3))
|
||||||
|
regions[REGIONS.levels_3].connect(
|
||||||
|
regions[REGIONS.levels_4], "Using fourth level building",
|
||||||
|
lambda state: has_logic_list_building(state, player, level_logic_buildings, 3, False))
|
||||||
|
regions[REGIONS.levels_4].connect(
|
||||||
|
regions[REGIONS.levels_5], "Using fifth level building",
|
||||||
|
lambda state: has_logic_list_building(state, player, level_logic_buildings, 4,
|
||||||
|
early_useful == OPTIONS.buildings_5))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.upgrades_1], "Using first upgrade building",
|
||||||
|
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 0, False))
|
||||||
|
regions[REGIONS.upgrades_1].connect(
|
||||||
|
regions[REGIONS.upgrades_2], "Using second upgrade building",
|
||||||
|
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 1, False))
|
||||||
|
regions[REGIONS.upgrades_2].connect(
|
||||||
|
regions[REGIONS.upgrades_3], "Using third upgrade building",
|
||||||
|
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 2,
|
||||||
|
early_useful == OPTIONS.buildings_3))
|
||||||
|
regions[REGIONS.upgrades_3].connect(
|
||||||
|
regions[REGIONS.upgrades_4], "Using fourth upgrade building",
|
||||||
|
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 3, False))
|
||||||
|
regions[REGIONS.upgrades_4].connect(
|
||||||
|
regions[REGIONS.upgrades_5], "Using fifth upgrade building",
|
||||||
|
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 4,
|
||||||
|
early_useful == OPTIONS.buildings_5))
|
||||||
|
|
||||||
|
# Connect Uncolored shapesanity regions to Main
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.full, REGIONS.uncol)], "Delivering unprocessed", lambda state: True)
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.half, REGIONS.uncol)], "Cutting in single half",
|
||||||
|
lambda state: can_make_half_shape(state, player))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.piece, REGIONS.uncol)], "Cutting in single piece",
|
||||||
|
lambda state: (can_cut_half(state, player) and can_rotate_90(state, player)) or
|
||||||
|
state.has(ITEMS.cutter_quad, player))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.half_half, REGIONS.uncol)], "Cutting and stacking into two halves",
|
||||||
|
lambda state: can_make_half_half_shape(state, player))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.stitched, REGIONS.uncol)], "Stitching complex shapes",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.east_wind, REGIONS.uncol)], "Rotating and stitching a single windmill half",
|
||||||
|
lambda state: can_make_east_windmill(state, player))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.col_full, REGIONS.uncol)], "Painting with a quad painter or stitching",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating) or can_use_quad_painter(state, player))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.col_east_wind, REGIONS.uncol)], "Why windmill, why?",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating) or
|
||||||
|
(can_use_quad_painter(state, player) and can_make_east_windmill(state, player)))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.col_half_half, REGIONS.uncol)], "Quad painting a half-half shape",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating) or
|
||||||
|
(can_use_quad_painter(state, player) and can_make_half_half_shape(state, player)))
|
||||||
|
regions[REGIONS.main].connect(
|
||||||
|
regions[REGIONS.sanity(REGIONS.col_half, REGIONS.uncol)], "Quad painting a half shape",
|
||||||
|
lambda state: can_make_stitched_shape(state, player, floating) or
|
||||||
|
(can_use_quad_painter(state, player) and can_make_half_shape(state, player)))
|
||||||
|
|
||||||
|
# Progressively connect colored shapesanity regions
|
||||||
|
for processing in shapesanity_processing:
|
||||||
|
regions[REGIONS.sanity(processing, REGIONS.uncol)].connect(
|
||||||
|
regions[REGIONS.sanity(processing, REGIONS.painted)], f"Painting a {processing.lower()} shape",
|
||||||
|
lambda state: can_paint(state, player))
|
||||||
|
regions[REGIONS.sanity(processing, REGIONS.painted)].connect(
|
||||||
|
regions[REGIONS.sanity(processing, REGIONS.mixed)], f"Mixing colors for a {processing.lower()} shape",
|
||||||
|
lambda state: can_mix_colors(state, player))
|
||||||
|
|
||||||
|
return [region for region in regions.values() if len(region.locations) or len(region.exits)]
|
213
worlds/shapez/test/__init__.py
Normal file
213
worlds/shapez/test/__init__.py
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from test.bases import WorldTestBase
|
||||||
|
from .. import options_presets, ShapezWorld
|
||||||
|
from ..data.strings import GOALS, OTHER, ITEMS, LOCATIONS, CATEGORY, OPTIONS, SHAPESANITY
|
||||||
|
from ..options import max_levels_and_upgrades, max_shapesanity
|
||||||
|
|
||||||
|
|
||||||
|
class ShapezTestBase(WorldTestBase):
|
||||||
|
game = OTHER.game_name
|
||||||
|
world: ShapezWorld
|
||||||
|
|
||||||
|
def test_location_count(self):
|
||||||
|
self.assertTrue(self.world.location_count > 0,
|
||||||
|
f"location_count is {self.world.location_count} for some reason.")
|
||||||
|
|
||||||
|
def test_logic_lists(self):
|
||||||
|
logic_buildings = [ITEMS.cutter, ITEMS.rotator, ITEMS.painter, ITEMS.color_mixer, ITEMS.stacker]
|
||||||
|
for building in logic_buildings:
|
||||||
|
count = self.world.level_logic.count(building)
|
||||||
|
self.assertTrue(count == 1, f"{building} was found {count} times in level_logic.")
|
||||||
|
count = self.world.upgrade_logic.count(building)
|
||||||
|
self.assertTrue(count == 1, f"{building} was found {count} times in upgrade_logic.")
|
||||||
|
self.assertTrue(len(self.world.level_logic) == 5,
|
||||||
|
f"level_logic contains {len(self.world.level_logic)} entries instead of the expected 5.")
|
||||||
|
self.assertTrue(len(self.world.upgrade_logic) == 5,
|
||||||
|
f"upgrade_logic contains {len(self.world.upgrade_logic)} entries instead of the expected 5.")
|
||||||
|
|
||||||
|
def test_random_logic_phase_length(self):
|
||||||
|
self.assertTrue(len(self.world.random_logic_phase_length) == 5,
|
||||||
|
f"random_logic_phase_length contains {len(self.world.random_logic_phase_length)} entries " +
|
||||||
|
f"instead of the expected 5.")
|
||||||
|
self.assertTrue(sum(self.world.random_logic_phase_length) < self.world.maxlevel,
|
||||||
|
f"The sum of all random phase lengths is greater than allowed: " +
|
||||||
|
str(sum(self.world.random_logic_phase_length)))
|
||||||
|
for length in self.world.random_logic_phase_length:
|
||||||
|
self.assertTrue(length in range(self.world.maxlevel),
|
||||||
|
f"Found an illegal value in random_logic_phase_length: {length}")
|
||||||
|
|
||||||
|
def test_category_random_logic_amounts(self):
|
||||||
|
self.assertTrue(len(self.world.category_random_logic_amounts) == 4,
|
||||||
|
f"Found {len(self.world.category_random_logic_amounts)} instead of 4 keys in "
|
||||||
|
f"category_random_logic_amounts.")
|
||||||
|
self.assertTrue(min(self.world.category_random_logic_amounts.values()) == 0,
|
||||||
|
"Found a value less than or no 0 in category_random_logic_amounts.")
|
||||||
|
self.assertTrue(max(self.world.category_random_logic_amounts.values()) <= 5,
|
||||||
|
"Found a value greater than 5 in category_random_logic_amounts.")
|
||||||
|
|
||||||
|
def test_maxlevel_and_finaltier(self):
|
||||||
|
self.assertTrue(self.world.maxlevel in range(25, max_levels_and_upgrades),
|
||||||
|
f"Found an illegal value for maxlevel: {self.world.maxlevel}")
|
||||||
|
self.assertTrue(self.world.finaltier in range(8, max_levels_and_upgrades+1),
|
||||||
|
f"Found an illegal value for finaltier: {self.world.finaltier}")
|
||||||
|
|
||||||
|
def test_included_locations(self):
|
||||||
|
self.assertTrue(len(self.world.included_locations) > 0, "Found no locations cached in included_locations.")
|
||||||
|
self.assertTrue(LOCATIONS.level(1) in self.world.included_locations.keys(),
|
||||||
|
"Could not find Level 1 (guraranteed location) cached in included_locations.")
|
||||||
|
self.assertTrue(LOCATIONS.upgrade(CATEGORY.belt, "II") in self.world.included_locations.keys(),
|
||||||
|
"Could not find Belt Upgrade Tier II (guraranteed location) cached in included_locations.")
|
||||||
|
self.assertTrue(LOCATIONS.shapesanity(1) in self.world.included_locations.keys(),
|
||||||
|
"Could not find Shapesanity 1 (guraranteed location) cached in included_locations.")
|
||||||
|
|
||||||
|
def test_shapesanity_names(self):
|
||||||
|
names_length = len(self.world.shapesanity_names)
|
||||||
|
locations_length = len([0 for loc in self.multiworld.get_locations(self.player) if "Shapesanity" in loc.name])
|
||||||
|
self.assertEqual(names_length, locations_length,
|
||||||
|
f"The amount of shapesanity names ({names_length}) does not match the amount of included " +
|
||||||
|
f"shapesanity locations ({locations_length}).")
|
||||||
|
self.assertTrue(SHAPESANITY.full(SHAPESANITY.uncolored, SHAPESANITY.circle) in self.world.shapesanity_names,
|
||||||
|
"Uncolored Circle is guaranteed but was not found in shapesanity_names.")
|
||||||
|
|
||||||
|
def test_efficiency_iii_no_softlock(self):
|
||||||
|
if self.world.options.goal == GOALS.efficiency_iii:
|
||||||
|
for item in self.multiworld.itempool:
|
||||||
|
self.assertFalse(item.name.endswith("Upgrade Trap"),
|
||||||
|
"Item pool contains an upgrade trap, which could make the efficiency_iii goal "
|
||||||
|
"unreachable if collected.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestGlobalOptionsImport(TestCase):
|
||||||
|
|
||||||
|
def test_global_options_import(self):
|
||||||
|
self.assertTrue(isinstance(max_levels_and_upgrades, int), f"The global option max_levels_and_upgrades is not " +
|
||||||
|
f"an integer, but instead a " +
|
||||||
|
f"{type(max_levels_and_upgrades)}.")
|
||||||
|
self.assertTrue(max_levels_and_upgrades >= 27, f"max_levels_and_upgrades must be at least 27, but is " +
|
||||||
|
f"{max_levels_and_upgrades} instead.")
|
||||||
|
self.assertTrue(isinstance(max_shapesanity, int), f"The global option max_shapesanity is not an integer, but " +
|
||||||
|
f"instead a {type(max_levels_and_upgrades)}.")
|
||||||
|
self.assertTrue(max_shapesanity >= 4, f"max_shapesanity must be at least 4, but is " +
|
||||||
|
f"{max_levels_and_upgrades} instead.")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMinimum(ShapezTestBase):
|
||||||
|
options = options_presets["Minimum checks"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestMaximum(ShapezTestBase):
|
||||||
|
options = options_presets["Maximum checks"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestRestrictive(ShapezTestBase):
|
||||||
|
options = options_presets["Restrictive start"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions1(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.vanilla,
|
||||||
|
"randomize_level_requirements": False,
|
||||||
|
"randomize_upgrade_requirements": False,
|
||||||
|
"complexity_growth_gradient": "0.1234",
|
||||||
|
"early_balancer_tunnel_and_trash": "none",
|
||||||
|
"lock_belt_and_extractor": True,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": False,
|
||||||
|
"exclude_long_playtime_achievements": False,
|
||||||
|
"exclude_progression_unreasonable": True,
|
||||||
|
"shapesanity_amount": max_shapesanity,
|
||||||
|
"traps_percentage": "random"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions2(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.mam,
|
||||||
|
"goal_amount": max_levels_and_upgrades,
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": OPTIONS.logic_random_steps,
|
||||||
|
"randomize_upgrade_logic": OPTIONS.logic_vanilla_like,
|
||||||
|
"complexity_growth_gradient": "2",
|
||||||
|
"early_balancer_tunnel_and_trash": OPTIONS.buildings_5,
|
||||||
|
"lock_belt_and_extractor": False,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": False,
|
||||||
|
"exclude_long_playtime_achievements": False,
|
||||||
|
"exclude_progression_unreasonable": False,
|
||||||
|
"shapesanity_amount": 4,
|
||||||
|
"traps_percentage": 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions3(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.even_fasterer,
|
||||||
|
"goal_amount": max_levels_and_upgrades,
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": f"{OPTIONS.logic_vanilla}_shuffled",
|
||||||
|
"randomize_upgrade_logic": OPTIONS.logic_linear,
|
||||||
|
"complexity_growth_gradient": "1e-003",
|
||||||
|
"early_balancer_tunnel_and_trash": OPTIONS.buildings_3,
|
||||||
|
"lock_belt_and_extractor": False,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": True,
|
||||||
|
"exclude_long_playtime_achievements": True,
|
||||||
|
"shapesanity_amount": "random",
|
||||||
|
"traps_percentage": 100,
|
||||||
|
"include_whacky_upgrades": True,
|
||||||
|
"split_inventory_draining_trap": True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions4(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.efficiency_iii,
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": f"{OPTIONS.logic_stretched}_shuffled",
|
||||||
|
"randomize_upgrade_logic": OPTIONS.logic_category,
|
||||||
|
"early_balancer_tunnel_and_trash": OPTIONS.sphere_1,
|
||||||
|
"lock_belt_and_extractor": False,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": True,
|
||||||
|
"exclude_long_playtime_achievements": True,
|
||||||
|
"shapesanity_amount": "random",
|
||||||
|
"traps_percentage": "random",
|
||||||
|
"include_whacky_upgrades": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions5(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.mam,
|
||||||
|
"goal_amount": "random-range-27-500",
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": f"{OPTIONS.logic_quick}_shuffled",
|
||||||
|
"randomize_upgrade_logic": OPTIONS.logic_category_random,
|
||||||
|
"lock_belt_and_extractor": False,
|
||||||
|
"include_achievements": True,
|
||||||
|
"exclude_softlock_achievements": True,
|
||||||
|
"exclude_long_playtime_achievements": True,
|
||||||
|
"shapesanity_amount": "random",
|
||||||
|
"traps_percentage": 100,
|
||||||
|
"split_inventory_draining_trap": False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllRelevantOptions6(ShapezTestBase):
|
||||||
|
options = {
|
||||||
|
"goal": GOALS.mam,
|
||||||
|
"goal_amount": "random-range-27-500",
|
||||||
|
"randomize_level_requirements": True,
|
||||||
|
"randomize_upgrade_requirements": True,
|
||||||
|
"randomize_level_logic": OPTIONS.logic_hardcore,
|
||||||
|
"randomize_upgrade_logic": OPTIONS.logic_hardcore,
|
||||||
|
"lock_belt_and_extractor": False,
|
||||||
|
"include_achievements": False,
|
||||||
|
"shapesanity_amount": "random",
|
||||||
|
"traps_percentage": "random"
|
||||||
|
}
|
Reference in New Issue
Block a user