shapez: Implement New Game (#3960)

Adds shapez as a supported game in AP.
This commit is contained in:
BlastSlimey
2025-05-21 14:30:39 +02:00
committed by GitHub
parent 3069deb019
commit d5bacaba63
21 changed files with 78894 additions and 0 deletions

417
worlds/shapez/__init__.py Normal file
View 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

View File

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

View File

View 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"])

View File

@@ -0,0 +1,4 @@
{
"max_levels_and_upgrades": 500,
"max_shapesanity": 1000
}

File diff suppressed because it is too large Load Diff

View 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}"

View 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.

View 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.

View 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):
![image](https://raw.githubusercontent.com/BlastSlimey/Archipelago/refs/heads/main/worlds/shapez/docs/shapesanity_full.png)
## 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

View 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:
![image](https://raw.githubusercontent.com/BlastSlimey/Archipelago/refs/heads/main/worlds/shapez/docs/shapesanity_full.png)
## 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.

View 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.

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

279
worlds/shapez/items.py Normal file
View 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
View 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
View 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
View 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
View 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)]

View 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"
}