Compare commits

...

10 Commits

Author SHA1 Message Date
NewSoupVi
77e6fa0010 The Witness: Move the Easter Egg Hunt option group lower so that the tooltip isn't cut off 2025-04-01 21:09:59 +02:00
LiquidCat64
d26db6f213 CV64: Fix some unrandomized locations containing unintended items on specific settings (#4728)
* Fix some unrandomized locations on specific settings.

* Remove now-unnecessary comment
2025-04-01 12:37:49 -04:00
Fabian Dill
bb6c753583 FFMQ: fix remote code execution (#4786) 2025-04-01 18:19:07 +02:00
Mysteryem
ca08e4b950 Super Metroid: Replace random module with world random in variaRandomizer (#4429) 2025-04-01 18:14:47 +02:00
Bryce Wilson
5a6b02dbd3 Pokemon Emerald: Fix pre-fill problems (#4686)
Co-authored-by: Mysteryem <Mysteryem@users.noreply.github.com>
2025-04-01 18:12:43 +02:00
jamesbrq
14416b1050 MLSS: Fix issue with door opening earlier than intended (#4737) 2025-04-01 18:10:51 +02:00
Carter Hesterman
da4e6fc532 Civ6: Sanitize player/item values before they go in the XML (#4755) 2025-04-01 18:09:59 +02:00
Justus Lind
57d8b69a6d Muse Dash: Update Song List to Muse Dash Legend. (#4775)
* Add Muse Dash Legend songs.

* Add a new SFX trap
2025-04-01 18:08:09 +02:00
Silvris
c9d8a8661c kvui: Fix hint tab formatting regression (#4778)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2025-04-01 18:06:49 +02:00
Fabian Dill
4a3d23e0e6 Core: update cx-Freeze to 8.0.0 & Worlds: fix packages missing __init__.py (#4773) 2025-04-01 16:29:32 +02:00
45 changed files with 292 additions and 226 deletions

View File

@@ -817,6 +817,12 @@ class HintLayout(BoxLayout):
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
def fix_heights(self):
for child in self.children:
fix_func = getattr(child, "fix_heights", None)
if fix_func:
fix_func()
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",

View File

@@ -19,7 +19,7 @@ from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==7.2.0'
requirement = 'cx-Freeze==8.0.0'
try:
import pkg_resources
try:

View File

@@ -0,0 +1,14 @@
import unittest
import os
class TestPackages(unittest.TestCase):
def test_packages_have_init(self):
"""Test that all world folders containing .py files also have a __init__.py file,
to indicate full package rather than namespace package."""
import Utils
worlds_path = Utils.local_path("worlds")
for dirpath, dirnames, filenames in os.walk(worlds_path):
with self.subTest(directory=dirpath):
self.assertEqual("__init__.py" in filenames, any(file.endswith(".py") for file in filenames))

View File

@@ -48,6 +48,10 @@ class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
opened_zipfile.writestr(filename, yml)
super().write_contents(opened_zipfile)
def sanitize_value(value: str) -> str:
"""Removes values that can cause issues in XML"""
return value.replace('"', "'").replace('&', 'and')
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
"""
@@ -63,7 +67,7 @@ def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str:
Returns the name of the player in the world
"""
if player != world.player:
return f"{world.multiworld.player_name[player]}{apo}s"
return sanitize_value(f"{world.multiworld.player_name[player]}{apo}s")
return "Your"
@@ -106,7 +110,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'{sanitize_value(location.item.name)}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '
@@ -122,7 +126,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
{"".join([f'{tab}<Row CivicType="{location.name}" '
f'Name="{get_formatted_player_name(world, location.item.player)} '
f'{location.item.name}" '
f'{sanitize_value(location.item.name)}" '
f'EraType="{world.location_table[location.name].era_type}" '
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
f'Cost="{get_cost(world, world.location_table[location.name])}" '

View File

View File

View File

@@ -644,6 +644,9 @@ class CV64PatchExtensions(APPatchExtension):
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
if not options["multi_hit_breakables"]:
rom_data.write_byte(0x10C7A1, 0x03)
# Replace the PowerUp in one of the lizard lockers if the lizard locker items aren't randomized.
if not options["lizard_locker_items"]:
rom_data.write_byte(0xBFCA07, 0x03)
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
# game PermaUps are distinguishable.
rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
@@ -714,7 +717,11 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
# Change the pointer to the Clock Tower final room 3HB door slab drops to not share its values with those of the
# 3HB slab near Renon at the top of the room.
if options["multi_hit_breakables"]:
rom_data.write_byte(0x10CF37, 0x04)
# Once-per-frame gameplay checks
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
@@ -1000,6 +1007,7 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict
"multi_hit_breakables": world.options.multi_hit_breakables.value,
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
"countdown": world.options.countdown.value,
"lizard_locker_items": world.options.lizard_locker_items.value,
"shopsanity": world.options.shopsanity.value,
"panther_dash": world.options.panther_dash.value,
"big_toss": world.options.big_toss.value,

View File

View File

@@ -3,7 +3,6 @@ import settings
import base64
import threading
import requests
import yaml
from worlds.AutoWorld import World, WebWorld
from BaseClasses import Tutorial
from .Regions import create_regions, location_table, set_rules, stage_set_rules, rooms, non_dead_end_crest_rooms,\
@@ -134,7 +133,7 @@ class FFMQWorld(World):
errors.append([api_url, err])
else:
if response.ok:
world.rooms = rooms_data[query] = yaml.load(response.text, yaml.Loader)
world.rooms = rooms_data[query] = Utils.parse_yaml(response.text)
break
else:
api_urls.remove(api_url)

View File

View File

View File

View File

View File

View File

View File

Binary file not shown.

View File

View File

@@ -52,11 +52,13 @@ class MuseDashCollections:
"Nyaa SFX Trap": STARTING_CODE + 8,
"Error SFX Trap": STARTING_CODE + 9,
"Focus Line Trap": STARTING_CODE + 10,
"Beefcake SFX Trap": STARTING_CODE + 11,
}
sfx_trap_items: List[str] = [
"Nyaa SFX Trap",
"Error SFX Trap",
"Beefcake SFX Trap",
]
filler_items: Dict[str, int] = {

View File

@@ -627,4 +627,10 @@ SONG_DATA: Dict[str, SongData] = {
"Sharp Bubbles": SongData(2900751, "83-3", "Cosmic Radio 2024", True, 7, 9, 11),
"Replay": SongData(2900752, "83-4", "Cosmic Radio 2024", True, 5, 7, 9),
"Cosmic Dusty Girl": SongData(2900753, "83-5", "Cosmic Radio 2024", True, 5, 7, 9),
"Meow Rock feat. Chun Ge, Yuan Shen": SongData(2900754, "84-0", "Muse Dash Legend", True, None, None, None),
"Even if you make an old radio song with AI": SongData(2900755, "84-1", "Muse Dash Legend", False, 3, 6, 8),
"Unusual Sketchbook": SongData(2900756, "84-2", "Muse Dash Legend", True, 6, 8, 11),
"TransientTears": SongData(2900757, "84-3", "Muse Dash Legend", True, 6, 8, 11),
"SHOOTING*STAR": SongData(2900758, "84-4", "Muse Dash Legend", False, 5, 7, 9),
"But the Blue Bird is Already Dead": SongData(2900759, "84-5", "Muse Dash Legend", False, 6, 8, 10),
}

View File

View File

@@ -14,6 +14,9 @@ _not_ used for logical access (the seed will never require you to catch somethin
- Now excludes the location "Navel Rock Top - Hidden Item Sacred Ash" if your goal is Champion and you didn't randomize
event tickets.
- Fixed handling of shuffle option for badges/HMs in the case that the player sets those items to nonlocal or uses
plando to put an item in one of those locations, or in the case that fill gets itself stuck on these items and has to
retry.
# 2.3.0

View File

@@ -8,7 +8,7 @@ import os
import pkgutil
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar, TextIO, Union
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
from BaseClasses import CollectionState, ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Fill import FillError, fill_restrictive
from Options import OptionError, Toggle
import settings
@@ -100,6 +100,7 @@ class PokemonEmeraldWorld(World):
required_client_version = (0, 4, 6)
item_pool: List[PokemonEmeraldItem]
badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]]
free_fly_location_id: int
@@ -185,7 +186,7 @@ class PokemonEmeraldWorld(World):
# In race mode we don't patch any item location information into the ROM
if self.multiworld.is_race and not self.options.remote_items:
logging.warning("Pokemon Emerald: Forcing Player %s (%s) to use remote items due to race mode.",
logging.warning("Pokemon Emerald: Forcing player %s (%s) to use remote items due to race mode.",
self.player, self.player_name)
self.options.remote_items.value = Toggle.option_true
@@ -197,7 +198,7 @@ class PokemonEmeraldWorld(World):
# Prevent setting the number of required legendaries higher than the number of enabled legendaries
if self.options.legendary_hunt_count.value > len(self.options.allowed_legendary_hunt_encounters.value):
logging.warning("Pokemon Emerald: Legendary hunt count for Player %s (%s) higher than number of allowed "
logging.warning("Pokemon Emerald: Legendary hunt count for player %s (%s) higher than number of allowed "
"legendary encounters. Reducing to number of allowed encounters.", self.player,
self.player_name)
self.options.legendary_hunt_count.value = len(self.options.allowed_legendary_hunt_encounters.value)
@@ -234,10 +235,17 @@ class PokemonEmeraldWorld(World):
max_norman_count = 4
if self.options.norman_count.value > max_norman_count:
logging.warning("Pokemon Emerald: Norman requirements for Player %s (%s) are unsafe in combination with "
logging.warning("Pokemon Emerald: Norman requirements for player %s (%s) are unsafe in combination with "
"other settings. Reducing to 4.", self.player, self.player_name)
self.options.norman_count.value = max_norman_count
# Shuffled badges/hms will always be placed locally, so add them to local_items
if self.options.badges == RandomizeBadges.option_shuffle:
self.options.local_items.value.update(self.item_name_groups["Badge"])
if self.options.hms == RandomizeHms.option_shuffle:
self.options.local_items.value.update(self.item_name_groups["HM"])
def create_regions(self) -> None:
from .regions import create_regions
all_regions = create_regions(self)
@@ -377,12 +385,11 @@ class PokemonEmeraldWorld(World):
item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories]
default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
# Take the itempool as-is
if self.options.item_pool_type == ItemPoolType.option_shuffled:
self.multiworld.itempool += default_itempool
# Recreate the itempool from random items
# Take the itempool as-is
self.item_pool = default_itempool
elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced):
# Recreate the itempool from random items
item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone",
"Money", "TM", "Held", "Misc", "Berry"]
@@ -392,6 +399,7 @@ class PokemonEmeraldWorld(World):
if not item.advancement:
item_category_counter.update([tag for tag in item.tags if tag in item_categories])
self.item_pool = []
item_category_weights = [item_category_counter.get(category) for category in item_categories]
item_category_weights = [weight if weight is not None else 0 for weight in item_category_weights]
@@ -436,19 +444,10 @@ class PokemonEmeraldWorld(World):
item_code = self.random.choice(fill_item_candidates_by_category[category])
item = self.create_item_by_code(item_code)
self.multiworld.itempool.append(item)
self.item_pool.append(item)
def set_rules(self) -> None:
from .rules import set_rules
set_rules(self)
self.multiworld.itempool += self.item_pool
def generate_basic(self) -> None:
# Create auth
# self.auth = self.random.randbytes(16) # Requires >=3.9
self.auth = self.random.getrandbits(16 * 8).to_bytes(16, "little")
randomize_types(self)
randomize_wild_encounters(self)
set_free_fly(self)
set_legendary_cave_entrances(self)
@@ -475,9 +474,20 @@ class PokemonEmeraldWorld(World):
if not self.options.key_items:
convert_unrandomized_items_to_events(LocationCategory.KEY)
def pre_fill(self) -> None:
# Badges and HMs that are set to shuffle need to be placed at
# their own subset of locations
def set_rules(self):
from .rules import set_rules
set_rules(self)
def connect_entrances(self):
randomize_wild_encounters(self)
self.shuffle_badges_hms()
# For entrance randomization, disconnect entrances here, randomize map, then
# undo badge/HM placement and re-shuffle them in the new map.
def shuffle_badges_hms(self) -> None:
my_progression_items = [item for item in self.item_pool if item.advancement]
my_locations = list(self.get_locations())
if self.options.badges == RandomizeBadges.option_shuffle:
badge_locations: List[PokemonEmeraldLocation]
badge_items: List[PokemonEmeraldItem]
@@ -502,41 +512,20 @@ class PokemonEmeraldWorld(World):
badge_priority["Knuckle Badge"] = 0
badge_items.sort(key=lambda item: badge_priority.get(item.name, 0))
# Un-exclude badge locations, since we need to put progression items on them
for location in badge_locations:
location.progress_type = LocationProgressType.DEFAULT \
if location.progress_type == LocationProgressType.EXCLUDED \
else location.progress_type
collection_state = self.multiworld.get_all_state(False)
# If HM shuffle is on, HMs are not placed and not in the pool, so
# `get_all_state` did not contain them. Collect them manually for
# this fill. We know that they will be included in all state after
# this stage.
# Build state
state = CollectionState(self.multiworld)
for item in my_progression_items:
state.collect(item, True)
# If HM shuffle is on, HMs are neither placed in locations nor in
# the item pool, so we also need to collect them.
if self.hm_shuffle_info is not None:
for _, item in self.hm_shuffle_info:
collection_state.collect(item)
state.collect(item, True)
state.sweep_for_advancements(my_locations)
# In specific very constrained conditions, fill_restrictive may run
# out of swaps before it finds a valid solution if it gets unlucky.
# This is a band-aid until fill/swap can reliably find those solutions.
attempts_remaining = 2
while attempts_remaining > 0:
attempts_remaining -= 1
self.random.shuffle(badge_locations)
try:
fill_restrictive(self.multiworld, collection_state, badge_locations, badge_items,
single_player_placement=True, lock=True, allow_excluded=True)
break
except FillError as exc:
if attempts_remaining == 0:
raise exc
# Shuffle badges
self.fill_subset_with_retries(badge_items, badge_locations, state)
logging.debug(f"Failed to shuffle badges for player {self.player}. Retrying.")
continue
# Badges are guaranteed to be either placed or in the multiworld's itempool now
if self.options.hms == RandomizeHms.option_shuffle:
hm_locations: List[PokemonEmeraldLocation]
hm_items: List[PokemonEmeraldItem]
@@ -559,33 +548,56 @@ class PokemonEmeraldWorld(World):
if self.options.badges == RandomizeBadges.option_vanilla and \
self.options.require_flash in (DarkCavesRequireFlash.option_both, DarkCavesRequireFlash.option_only_granite_cave):
hm_priority["HM05 Flash"] = 0
hm_items.sort(key=lambda item: hm_priority.get(item.name, 0))
hm_items.sort(key=lambda item: hm_priority.get(item.name, 0), reverse=True)
# Un-exclude HM locations, since we need to put progression items on them
for location in hm_locations:
location.progress_type = LocationProgressType.DEFAULT \
if location.progress_type == LocationProgressType.EXCLUDED \
else location.progress_type
# Build state
# Badges are either in the item pool, or already placed and collected during sweep
state = CollectionState(self.multiworld)
for item in my_progression_items:
state.collect(item, True)
state.sweep_for_advancements(my_locations)
collection_state = self.multiworld.get_all_state(False)
# Shuffle HMs
self.fill_subset_with_retries(hm_items, hm_locations, state)
# In specific very constrained conditions, fill_restrictive may run
# out of swaps before it finds a valid solution if it gets unlucky.
# This is a band-aid until fill/swap can reliably find those solutions.
attempts_remaining = 2
while attempts_remaining > 0:
attempts_remaining -= 1
self.random.shuffle(hm_locations)
try:
fill_restrictive(self.multiworld, collection_state, hm_locations, hm_items,
single_player_placement=True, lock=True, allow_excluded=True)
break
except FillError as exc:
if attempts_remaining == 0:
raise exc
def fill_subset_with_retries(self, items: list[PokemonEmeraldItem], locations: list[PokemonEmeraldLocation], state: CollectionState):
# Un-exclude locations, since we need to put progression items on them
for location in locations:
location.progress_type = LocationProgressType.DEFAULT \
if location.progress_type == LocationProgressType.EXCLUDED \
else location.progress_type
logging.debug(f"Failed to shuffle HMs for player {self.player}. Retrying.")
continue
# In specific very constrained conditions, `fill_restrictive` may run
# out of swaps before it finds a valid solution if it gets unlucky.
attempts_remaining = 2
while attempts_remaining > 0:
attempts_remaining -= 1
locations_copy = locations.copy()
items_copy = items.copy()
self.random.shuffle(locations_copy)
try:
fill_restrictive(self.multiworld, state, locations_copy, items_copy, single_player_placement=True,
lock=True)
break
except FillError as exc:
if attempts_remaining <= 0:
raise exc
# Undo partial item placement
for location in locations:
location.locked = False
if location.item is not None:
location.item.location = None
location.item = None
logging.debug(f"Failed to shuffle items for player {self.player} ({self.player_name}). Retrying.")
continue
def generate_basic(self) -> None:
# Create auth
self.auth = self.random.randbytes(16)
randomize_types(self)
def generate_output(self, output_directory: str) -> None:
self.modified_trainers = copy.deepcopy(emerald_data.trainers)

View File

@@ -124,7 +124,7 @@ class SMWorld(World):
Logic.factory('vanilla')
dummy_rom_file = Utils.user_path(SMSettings.RomFile.copy_to) # actual rom set in generate_output
self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player)
self.variaRando = VariaRandomizer(self.options, dummy_rom_file, self.player, self.multiworld.seed, self.random)
self.multiworld.state.smbm[self.player] = SMBoolManager(self.player, self.variaRando.maxDifficulty)
# keeps Nothing items local so no player will ever pickup Nothing
@@ -314,11 +314,11 @@ class SMWorld(World):
raise KeyError(f"Item {name} for {self.player_name} is invalid.")
def get_filler_item_name(self) -> str:
if self.multiworld.random.randint(0, 100) < self.options.minor_qty.value:
if self.random.randint(0, 100) < self.options.minor_qty.value:
power_bombs = self.options.power_bomb_qty.value
missiles = self.options.missile_qty.value
super_missiles = self.options.super_qty.value
roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles)
roll = self.random.randint(1, power_bombs + missiles + super_missiles)
if roll <= power_bombs:
return "Power Bomb"
elif roll <= power_bombs + missiles:
@@ -340,8 +340,8 @@ class SMWorld(World):
else:
nonChozoLoc.append(loc)
self.multiworld.random.shuffle(nonChozoLoc)
self.multiworld.random.shuffle(chozoLoc)
self.random.shuffle(nonChozoLoc)
self.random.shuffle(chozoLoc)
missingCount = len(self.NothingPool) - len(nonChozoLoc)
locations = nonChozoLoc
if (missingCount > 0):

View File

@@ -1,5 +1,4 @@
import copy
import random
from ..logic.logic import Logic
from ..utils.parameters import Knows
from ..graph.location import locationsDict
@@ -136,7 +135,8 @@ class GraphUtils:
refused[apName] = cause
return ret, refused
def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs):
@staticmethod
def updateLocClassesStart(startGraphArea, split, possibleMajLocs, preserveMajLocs, nLocs, random):
locs = locationsDict
preserveMajLocs = [locs[locName] for locName in preserveMajLocs if locs[locName].isClass(split)]
possLocs = [locs[locName] for locName in possibleMajLocs][:nLocs]
@@ -160,7 +160,8 @@ class GraphUtils:
ap = getAccessPoint(startApName)
return ap.Start['patches'] if 'patches' in ap.Start else []
def createBossesTransitions():
@staticmethod
def createBossesTransitions(random):
transitions = vanillaBossesTransitions
def isVanilla():
for t in vanillaBossesTransitions:
@@ -180,13 +181,15 @@ class GraphUtils:
transitions.append((src,dst))
return transitions
def createAreaTransitions(lightAreaRando=False):
@staticmethod
def createAreaTransitions(lightAreaRando=False, *, random):
if lightAreaRando:
return GraphUtils.createLightAreaTransitions()
return GraphUtils.createLightAreaTransitions(random=random)
else:
return GraphUtils.createRegularAreaTransitions()
return GraphUtils.createRegularAreaTransitions(random=random)
def createRegularAreaTransitions(apList=None, apPred=None):
@staticmethod
def createRegularAreaTransitions(apList=None, apPred=None, *, random):
if apList is None:
apList = Logic.accessPoints
if apPred is None:
@@ -239,7 +242,8 @@ class GraphUtils:
transitions.append((ap.Name, ap.Name))
# crateria can be forced in corner cases
def createMinimizerTransitions(startApName, locLimit, forcedAreas=None):
@staticmethod
def createMinimizerTransitions(startApName, locLimit, forcedAreas=None, *, random):
if forcedAreas is None:
forcedAreas = []
if startApName == 'Ceres':
@@ -316,7 +320,8 @@ class GraphUtils:
GraphUtils.log.debug("FINAL MINIMIZER areas: "+str(areas))
return transitions
def createLightAreaTransitions():
@staticmethod
def createLightAreaTransitions(random):
# group APs by area
aps = {}
totalCount = 0
@@ -407,7 +412,8 @@ class GraphUtils:
return rooms
def escapeAnimalsTransitions(graph, possibleTargets, firstEscape):
@staticmethod
def escapeAnimalsTransitions(graph, possibleTargets, firstEscape, random):
n = len(possibleTargets)
assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets)
GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape))

View File

@@ -1,4 +1,3 @@
import random
from ..utils import log
from ..utils.utils import getRangeDict, chooseFromRange
from ..rando.ItemLocContainer import ItemLocation
@@ -23,8 +22,9 @@ class Choice(object):
# simple random choice, that chooses an item first, then a locatio to put it in
class ItemThenLocChoice(Choice):
def __init__(self, restrictions):
def __init__(self, restrictions, random):
super(ItemThenLocChoice, self).__init__(restrictions)
self.random = random
def chooseItemLoc(self, itemLocDict, isProg):
itemList = self.getItemList(itemLocDict)
@@ -49,7 +49,7 @@ class ItemThenLocChoice(Choice):
return self.chooseItemRandom(itemList)
def chooseItemRandom(self, itemList):
return random.choice(itemList)
return self.random.choice(itemList)
def chooseLocation(self, locList, item, isProg):
if len(locList) == 0:
@@ -63,12 +63,12 @@ class ItemThenLocChoice(Choice):
return self.chooseLocationRandom(locList)
def chooseLocationRandom(self, locList):
return random.choice(locList)
return self.random.choice(locList)
# Choice specialization for prog speed based filler
class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
def __init__(self, restrictions, progSpeedParams, distanceProp, services):
super(ItemThenLocChoiceProgSpeed, self).__init__(restrictions)
def __init__(self, restrictions, progSpeedParams, distanceProp, services, random):
super(ItemThenLocChoiceProgSpeed, self).__init__(restrictions, random)
self.progSpeedParams = progSpeedParams
self.distanceProp = distanceProp
self.services = services
@@ -104,7 +104,7 @@ class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
if self.restrictions.isLateMorph() and canRollback and len(itemLocDict) == 1:
item, locList = list(itemLocDict.items())[0]
if item.Type == 'Morph':
morphLocs = self.restrictions.lateMorphCheck(container, locList)
morphLocs = self.restrictions.lateMorphCheck(container, locList, self.random)
if morphLocs is not None:
itemLocDict[item] = morphLocs
else:
@@ -115,7 +115,7 @@ class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
assert len(locs) == 1 and locs[0].Name == item.Name
return ItemLocation(item, locs[0])
# late doors check for random door colors
if self.restrictions.isLateDoors() and random.random() < self.lateDoorsProb:
if self.restrictions.isLateDoors() and self.random.random() < self.lateDoorsProb:
self.processLateDoors(itemLocDict, ap, container)
self.progressionItemLocs = progressionItemLocs
self.ap = ap
@@ -145,14 +145,14 @@ class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
def chooseLocationProg(self, locs, item):
locs = self.getLocsSpreadProgression(locs)
random.shuffle(locs)
self.random.shuffle(locs)
ret = self.getChooseFunc(self.chooseLocRanges, self.chooseLocFuncs)(locs)
self.log.debug('chooseLocationProg. ret='+ret.Name)
return ret
# get choose function from a weighted dict
def getChooseFunc(self, rangeDict, funcDict):
v = chooseFromRange(rangeDict)
v = chooseFromRange(rangeDict, self.random)
return funcDict[v]
@@ -209,6 +209,6 @@ class ItemThenLocChoiceProgSpeed(ItemThenLocChoice):
for i in range(len(availableLocations)):
loc = availableLocations[i]
d = distances[i]
if d == maxDist or random.random() >= self.spreadProb:
if d == maxDist or self.random.random() >= self.spreadProb:
locs.append(loc)
return locs

View File

@@ -1,5 +1,5 @@
import copy, time, random
import copy, time
from ..utils import log
from ..logic.cache import RequestCache
from ..rando.RandoServices import RandoServices
@@ -15,11 +15,11 @@ from ..graph.graph_utils import GraphUtils
# item pool is not empty).
# entry point is generateItems
class Filler(object):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity, *, random):
self.startAP = startAP
self.cache = RequestCache()
self.graph = graph
self.services = RandoServices(graph, restrictions, self.cache)
self.services = RandoServices(graph, restrictions, self.cache, random=random)
self.restrictions = restrictions
self.settings = restrictions.settings
self.endDate = endDate
@@ -108,9 +108,9 @@ class Filler(object):
# very simple front fill algorithm with no rollback and no "softlock checks" (== dessy algorithm)
class FrontFiller(Filler):
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity):
super(FrontFiller, self).__init__(startAP, graph, restrictions, emptyContainer, endDate)
self.choice = ItemThenLocChoice(restrictions)
def __init__(self, startAP, graph, restrictions, emptyContainer, endDate=infinity, *, random):
super(FrontFiller, self).__init__(startAP, graph, restrictions, emptyContainer, endDate, random=random)
self.choice = ItemThenLocChoice(restrictions, random)
self.stdStart = GraphUtils.isStandardStart(self.startAP)
def isEarlyGame(self):

View File

@@ -1,5 +1,5 @@
import random, copy
import copy
from ..utils import log
from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets, graphAreas, getAccessPoint
from ..logic.logic import Logic
@@ -11,13 +11,14 @@ from collections import defaultdict
# creates graph and handles randomized escape
class GraphBuilder(object):
def __init__(self, graphSettings):
def __init__(self, graphSettings, random):
self.graphSettings = graphSettings
self.areaRando = graphSettings.areaRando
self.bossRando = graphSettings.bossRando
self.escapeRando = graphSettings.escapeRando
self.minimizerN = graphSettings.minimizerN
self.log = log.get('GraphBuilder')
self.random = random
# builds everything but escape transitions
def createGraph(self, maxDiff):
@@ -48,18 +49,18 @@ class GraphBuilder(object):
objForced = forcedAreas.intersection(escAreas)
escAreasList = sorted(list(escAreas))
while len(objForced) < n and len(escAreasList) > 0:
objForced.add(escAreasList.pop(random.randint(0, len(escAreasList)-1)))
objForced.add(escAreasList.pop(self.random.randint(0, len(escAreasList)-1)))
forcedAreas = forcedAreas.union(objForced)
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)))
transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas)), random=self.random)
else:
if not self.bossRando:
transitions += vanillaBossesTransitions
else:
transitions += GraphUtils.createBossesTransitions()
transitions += GraphUtils.createBossesTransitions(self.random)
if not self.areaRando:
transitions += vanillaTransitions
else:
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando)
transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando, random=self.random)
ret = AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile)
Objectives.objDict[self.graphSettings.player].setGraph(ret, maxDiff)
return ret
@@ -100,7 +101,7 @@ class GraphBuilder(object):
self.escapeTimer(graph, paths, self.areaRando or escapeTrigger is not None)
self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer']))
# animals
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst)
GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst, self.random)
return True
def _getTargets(self, sm, graph, maxDiff):
@@ -110,7 +111,7 @@ class GraphBuilder(object):
if len(possibleTargets) == 0:
self.log.debug("Can't randomize escape, fallback to vanilla")
possibleTargets.append('Climb Bottom Left')
random.shuffle(possibleTargets)
self.random.shuffle(possibleTargets)
return possibleTargets
def getPossibleEscapeTargets(self, emptyContainer, graph, maxDiff):

View File

@@ -1,6 +1,6 @@
from ..utils.utils import randGaussBounds, getRangeDict, chooseFromRange
from ..utils import log
import logging, copy, random
import logging, copy
class Item:
__slots__ = ( 'Category', 'Class', 'Name', 'Code', 'Type', 'BeamBits', 'ItemBits', 'Id' )
@@ -335,7 +335,7 @@ class ItemManager:
itemCode = item.Code + modifier
return itemCode
def __init__(self, majorsSplit, qty, sm, nLocs, bossesItems, maxDiff):
def __init__(self, majorsSplit, qty, sm, nLocs, bossesItems, maxDiff, random):
self.qty = qty
self.sm = sm
self.majorsSplit = majorsSplit
@@ -344,6 +344,7 @@ class ItemManager:
self.maxDiff = maxDiff
self.majorClass = 'Chozo' if majorsSplit == 'Chozo' else 'Major'
self.itemPool = []
self.random = random
def newItemPool(self, addBosses=True):
self.itemPool = []
@@ -386,7 +387,7 @@ class ItemManager:
return ItemManager.Items[itemType].withClass(itemClass)
def createItemPool(self, exclude=None):
itemPoolGenerator = ItemPoolGenerator.factory(self.majorsSplit, self, self.qty, self.sm, exclude, self.nLocs, self.maxDiff)
itemPoolGenerator = ItemPoolGenerator.factory(self.majorsSplit, self, self.qty, self.sm, exclude, self.nLocs, self.maxDiff, self.random)
self.itemPool = itemPoolGenerator.getItemPool()
@staticmethod
@@ -402,20 +403,20 @@ class ItemPoolGenerator(object):
nbBosses = 9
@staticmethod
def factory(majorsSplit, itemManager, qty, sm, exclude, nLocs, maxDiff):
def factory(majorsSplit, itemManager, qty, sm, exclude, nLocs, maxDiff, random):
if majorsSplit == 'Chozo':
return ItemPoolGeneratorChozo(itemManager, qty, sm, maxDiff)
return ItemPoolGeneratorChozo(itemManager, qty, sm, maxDiff, random)
elif majorsSplit == 'Plando':
return ItemPoolGeneratorPlando(itemManager, qty, sm, exclude, nLocs, maxDiff)
return ItemPoolGeneratorPlando(itemManager, qty, sm, exclude, nLocs, maxDiff, random)
elif nLocs == ItemPoolGenerator.maxLocs:
if majorsSplit == "Scavenger":
return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff)
return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff, random)
else:
return ItemPoolGeneratorMajors(itemManager, qty, sm, maxDiff)
return ItemPoolGeneratorMajors(itemManager, qty, sm, maxDiff, random)
else:
return ItemPoolGeneratorMinimizer(itemManager, qty, sm, nLocs, maxDiff)
return ItemPoolGeneratorMinimizer(itemManager, qty, sm, nLocs, maxDiff, random)
def __init__(self, itemManager, qty, sm, maxDiff):
def __init__(self, itemManager, qty, sm, maxDiff, random):
self.itemManager = itemManager
self.qty = qty
self.sm = sm
@@ -423,12 +424,13 @@ class ItemPoolGenerator(object):
self.maxEnergy = 18 # 14E, 4R
self.maxDiff = maxDiff
self.log = log.get('ItemPool')
self.random = random
def isUltraSparseNoTanks(self):
# if low stuff botwoon is not known there is a hard energy req of one tank, even
# with both suits
lowStuffBotwoon = self.sm.knowsLowStuffBotwoon()
return random.random() < 0.5 and (lowStuffBotwoon.bool == True and lowStuffBotwoon.difficulty <= self.maxDiff)
return self.random.random() < 0.5 and (lowStuffBotwoon.bool == True and lowStuffBotwoon.difficulty <= self.maxDiff)
def calcMaxMinors(self):
pool = self.itemManager.getItemPool()
@@ -464,7 +466,7 @@ class ItemPoolGenerator(object):
rangeDict = getRangeDict(ammoQty)
self.log.debug("rangeDict: {}".format(rangeDict))
while len(self.itemManager.getItemPool()) < maxItems:
item = chooseFromRange(rangeDict)
item = chooseFromRange(rangeDict, self.random)
self.itemManager.addMinor(item)
else:
minorsTypes = ['Missile', 'Super', 'PowerBomb']
@@ -522,7 +524,7 @@ class ItemPoolGeneratorChozo(ItemPoolGenerator):
# no etank nor reserve
self.itemManager.removeItem('ETank')
self.itemManager.addItem('NoEnergy', 'Chozo')
elif random.random() < 0.5:
elif self.random.random() < 0.5:
# replace only etank with reserve
self.itemManager.removeItem('ETank')
self.itemManager.addItem('Reserve', 'Chozo')
@@ -535,9 +537,9 @@ class ItemPoolGeneratorChozo(ItemPoolGenerator):
# 4-6
# already 3E and 1R
alreadyInPool = 4
rest = randGaussBounds(2, 5)
rest = randGaussBounds(self.random, 2, 5)
if rest >= 1:
if random.random() < 0.5:
if self.random.random() < 0.5:
self.itemManager.addItem('Reserve', 'Minor')
else:
self.itemManager.addItem('ETank', 'Minor')
@@ -550,13 +552,13 @@ class ItemPoolGeneratorChozo(ItemPoolGenerator):
# 8-12
# add up to 3 Reserves or ETanks (cannot add more than 3 reserves)
for i in range(3):
if random.random() < 0.5:
if self.random.random() < 0.5:
self.itemManager.addItem('Reserve', 'Minor')
else:
self.itemManager.addItem('ETank', 'Minor')
# 7 already in the pool (3 E, 1 R, + the previous 3)
alreadyInPool = 7
rest = 1 + randGaussBounds(4, 3.7)
rest = 1 + randGaussBounds(self.random, 4, 3.7)
for i in range(rest):
self.itemManager.addItem('ETank', 'Minor')
# fill the rest with NoEnergy
@@ -581,10 +583,10 @@ class ItemPoolGeneratorChozo(ItemPoolGenerator):
return self.itemManager.getItemPool()
class ItemPoolGeneratorMajors(ItemPoolGenerator):
def __init__(self, itemManager, qty, sm, maxDiff):
super(ItemPoolGeneratorMajors, self).__init__(itemManager, qty, sm, maxDiff)
self.sparseRest = 1 + randGaussBounds(2, 5)
self.mediumRest = 3 + randGaussBounds(4, 3.7)
def __init__(self, itemManager, qty, sm, maxDiff, random):
super(ItemPoolGeneratorMajors, self).__init__(itemManager, qty, sm, maxDiff, random)
self.sparseRest = 1 + randGaussBounds(self.random,2, 5)
self.mediumRest = 3 + randGaussBounds(self.random, 4, 3.7)
self.ultraSparseNoTanks = self.isUltraSparseNoTanks()
def addNoEnergy(self):
@@ -609,7 +611,7 @@ class ItemPoolGeneratorMajors(ItemPoolGenerator):
# no energy at all
self.addNoEnergy()
else:
if random.random() < 0.5:
if self.random.random() < 0.5:
self.itemManager.addItem('ETank')
else:
self.itemManager.addItem('Reserve')
@@ -620,7 +622,7 @@ class ItemPoolGeneratorMajors(ItemPoolGenerator):
elif energyQty == 'sparse':
# 4-6
if random.random() < 0.5:
if self.random.random() < 0.5:
self.itemManager.addItem('Reserve')
else:
self.itemManager.addItem('ETank')
@@ -639,7 +641,7 @@ class ItemPoolGeneratorMajors(ItemPoolGenerator):
alreadyInPool = 2
n = getE(3)
for i in range(n):
if random.random() < 0.5:
if self.random.random() < 0.5:
self.itemManager.addItem('Reserve')
else:
self.itemManager.addItem('ETank')
@@ -676,15 +678,15 @@ class ItemPoolGeneratorMajors(ItemPoolGenerator):
return self.itemManager.getItemPool()
class ItemPoolGeneratorScavenger(ItemPoolGeneratorMajors):
def __init__(self, itemManager, qty, sm, maxDiff):
super(ItemPoolGeneratorScavenger, self).__init__(itemManager, qty, sm, maxDiff)
def __init__(self, itemManager, qty, sm, maxDiff, random):
super(ItemPoolGeneratorScavenger, self).__init__(itemManager, qty, sm, maxDiff, random)
def addNoEnergy(self):
self.itemManager.addItem('Nothing')
class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors):
def __init__(self, itemManager, qty, sm, nLocs, maxDiff):
super(ItemPoolGeneratorMinimizer, self).__init__(itemManager, qty, sm, maxDiff)
def __init__(self, itemManager, qty, sm, nLocs, maxDiff, random):
super(ItemPoolGeneratorMinimizer, self).__init__(itemManager, qty, sm, maxDiff, random)
self.maxItems = nLocs
self.calcMaxAmmo()
nMajors = len([itemName for itemName,item in ItemManager.Items.items() if item.Class == 'Major' and item.Category != 'Energy'])
@@ -716,8 +718,8 @@ class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors):
self.log.debug("maxEnergy: "+str(self.maxEnergy))
class ItemPoolGeneratorPlando(ItemPoolGenerator):
def __init__(self, itemManager, qty, sm, exclude, nLocs, maxDiff):
super(ItemPoolGeneratorPlando, self).__init__(itemManager, qty, sm, maxDiff)
def __init__(self, itemManager, qty, sm, exclude, nLocs, maxDiff, random):
super(ItemPoolGeneratorPlando, self).__init__(itemManager, qty, sm, maxDiff, random)
# in exclude dict:
# in alreadyPlacedItems:
# dict of 'itemType: count' of items already added in the plando.
@@ -805,7 +807,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator):
if ammoQty:
rangeDict = getRangeDict(ammoQty)
while len(self.itemManager.getItemPool()) < maxItems and remain > 0:
item = chooseFromRange(rangeDict)
item = chooseFromRange(rangeDict, self.random)
self.itemManager.addMinor(item)
remain -= 1

View File

@@ -1,4 +1,4 @@
import sys, random, time
import sys, time
from ..utils import log
from ..logic.logic import Logic
@@ -14,7 +14,7 @@ from ..utils.doorsmanager import DoorsManager
# entry point for rando execution ("randomize" method)
class RandoExec(object):
def __init__(self, seedName, vcr, randoSettings, graphSettings, player):
def __init__(self, seedName, vcr, randoSettings, graphSettings, player, random):
self.errorMsg = ""
self.seedName = seedName
self.vcr = vcr
@@ -22,6 +22,7 @@ class RandoExec(object):
self.graphSettings = graphSettings
self.log = log.get('RandoExec')
self.player = player
self.random = random
# processes settings to :
# - create Restrictions and GraphBuilder objects
@@ -31,7 +32,7 @@ class RandoExec(object):
vcr = VCR(self.seedName, 'rando') if self.vcr == True else None
self.errorMsg = ""
split = self.randoSettings.restrictions['MajorMinor']
self.graphBuilder = GraphBuilder(self.graphSettings)
self.graphBuilder = GraphBuilder(self.graphSettings, self.random)
container = None
i = 0
attempts = 500 if self.graphSettings.areaRando or self.graphSettings.doorsColorsRando or split == 'Scavenger' else 1
@@ -43,10 +44,10 @@ class RandoExec(object):
while container is None and i < attempts and now <= endDate:
self.restrictions = Restrictions(self.randoSettings)
if self.graphSettings.doorsColorsRando == True:
DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player)
DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player, self.random)
self.areaGraph = self.graphBuilder.createGraph(self.randoSettings.maxDiff)
services = RandoServices(self.areaGraph, self.restrictions)
setup = RandoSetup(self.graphSettings, Logic.locations[:], services, self.player)
services = RandoServices(self.areaGraph, self.restrictions, random=self.random)
setup = RandoSetup(self.graphSettings, Logic.locations[:], services, self.player, self.random)
self.setup = setup
container = setup.createItemLocContainer(endDate, vcr)
if container is None:
@@ -78,7 +79,7 @@ class RandoExec(object):
n = nMaj
elif split == 'Chozo':
n = nChozo
GraphUtils.updateLocClassesStart(startAP.GraphArea, split, possibleMajLocs, preserveMajLocs, n)
GraphUtils.updateLocClassesStart(startAP.GraphArea, split, possibleMajLocs, preserveMajLocs, n, self.random)
def postProcessItemLocs(self, itemLocs, hide):
# hide some items like in dessy's
@@ -89,7 +90,7 @@ class RandoExec(object):
if (item.Category != "Nothing"
and loc.CanHidden == True
and loc.Visibility == 'Visible'):
if bool(random.getrandbits(1)) == True:
if bool(self.random.getrandbits(1)) == True:
loc.Visibility = 'Hidden'
# put nothing in unfilled locations
filledLocNames = [il.Location.Name for il in itemLocs]

View File

@@ -1,5 +1,4 @@
import copy, random, sys, logging, os
import copy, sys, logging, os
from enum import Enum, unique
from ..utils import log
from ..utils.parameters import infinity
@@ -19,12 +18,13 @@ class ComebackCheckType(Enum):
# collection of stateless services to be used mainly by fillers
class RandoServices(object):
def __init__(self, graph, restrictions, cache=None):
def __init__(self, graph, restrictions, cache=None, *, random):
self.restrictions = restrictions
self.settings = restrictions.settings
self.areaGraph = graph
self.cache = cache
self.log = log.get('RandoServices')
self.random = random
@staticmethod
def printProgress(s):
@@ -217,7 +217,7 @@ class RandoServices(object):
# choose a morph item location in that context
morphItemLoc = ItemLocation(
morph,
random.choice(morphLocs)
self.random.choice(morphLocs)
)
# acquire morph in new context and see if we can still open new locs
newAP = self.collect(ap, containerCpy, morphItemLoc)
@@ -232,7 +232,7 @@ class RandoServices(object):
if morphLocItem is None or len(itemLocDict) == 1:
# no morph, or it is the only possibility: nothing to do
return
morphLocs = self.restrictions.lateMorphCheck(container, itemLocDict[morphLocItem])
morphLocs = self.restrictions.lateMorphCheck(container, itemLocDict[morphLocItem], self.random)
if morphLocs is not None:
itemLocDict[morphLocItem] = morphLocs
else:
@@ -380,10 +380,10 @@ class RandoServices(object):
(itemLocDict, isProg) = self.getPossiblePlacements(ap, container, ComebackCheckType.NoCheck)
assert not isProg
items = list(itemLocDict.keys())
random.shuffle(items)
self.random.shuffle(items)
for item in items:
cont = copy.copy(container)
loc = random.choice(itemLocDict[item])
loc = self.random.choice(itemLocDict[item])
itemLoc1 = ItemLocation(item, loc)
self.log.debug("itemLoc1 attempt: "+getItemLocStr(itemLoc1))
newAP = self.collect(ap, cont, itemLoc1)
@@ -391,8 +391,8 @@ class RandoServices(object):
self.cache.reset()
(ild, isProg) = self.getPossiblePlacements(newAP, cont, ComebackCheckType.NoCheck)
if isProg:
item2 = random.choice(list(ild.keys()))
itemLoc2 = ItemLocation(item2, random.choice(ild[item2]))
item2 = self.random.choice(list(ild.keys()))
itemLoc2 = ItemLocation(item2, self.random.choice(ild[item2]))
self.log.debug("itemLoc2: "+getItemLocStr(itemLoc2))
return (itemLoc1, itemLoc2)
return None

View File

@@ -1,5 +1,4 @@
import sys, random
import sys
from collections import defaultdict
from ..rando.Items import ItemManager
from ..utils.utils import getRangeDict, chooseFromRange
@@ -32,11 +31,11 @@ class RandoSettings(object):
def isPlandoRando(self):
return self.PlandoOptions is not None
def getItemManager(self, smbm, nLocs, bossesItems):
def getItemManager(self, smbm, nLocs, bossesItems, random):
if not self.isPlandoRando():
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, bossesItems, self.maxDiff)
return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, bossesItems, self.maxDiff, random)
else:
return ItemManager('Plando', self.qty, smbm, nLocs, bossesItems, self.maxDiff)
return ItemManager('Plando', self.qty, smbm, nLocs, bossesItems, self.maxDiff, random)
def getExcludeItems(self, locations):
if not self.isPlandoRando():
@@ -94,7 +93,7 @@ class ProgSpeedParameters(object):
self.restrictions = restrictions
self.nLocs = nLocs
def getVariableSpeed(self):
def getVariableSpeed(self, random):
ranges = getRangeDict({
'slowest':7,
'slow':20,
@@ -102,7 +101,7 @@ class ProgSpeedParameters(object):
'fast':27,
'fastest':11
})
return chooseFromRange(ranges)
return chooseFromRange(ranges, random)
def getMinorHelpProb(self, progSpeed):
if self.restrictions.split != 'Major':
@@ -134,7 +133,7 @@ class ProgSpeedParameters(object):
def isSlow(self, progSpeed):
return progSpeed == "slow" or (progSpeed == "slowest" and self.restrictions.split == "Chozo")
def getItemLimit(self, progSpeed):
def getItemLimit(self, progSpeed, random):
itemLimit = self.nLocs
if self.isSlow(progSpeed):
itemLimit = int(self.nLocs*0.209) # 21 for 105

View File

@@ -1,4 +1,4 @@
import copy, random
import copy
from ..utils import log
from ..utils.utils import randGaussBounds
@@ -16,8 +16,9 @@ from ..rom.rom_patches import RomPatches
# checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions
# the entry point is createItemLocContainer
class RandoSetup(object):
def __init__(self, graphSettings, locations, services, player):
def __init__(self, graphSettings, locations, services, player, random):
self.sm = SMBoolManager(player, services.settings.maxDiff)
self.random = random
self.settings = services.settings
self.graphSettings = graphSettings
self.startAP = graphSettings.startAP
@@ -31,7 +32,7 @@ class RandoSetup(object):
# print("nLocs Setup: "+str(len(self.locations)))
# in minimizer we can have some missing boss locs
bossesItems = [loc.BossItemType for loc in self.locations if loc.isBoss()]
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations), bossesItems)
self.itemManager = self.settings.getItemManager(self.sm, len(self.locations), bossesItems, random)
self.forbiddenItems = []
self.restrictedLocs = []
self.lastRestricted = []
@@ -165,7 +166,7 @@ class RandoSetup(object):
return True
self.log.debug("********* PRE RANDO START")
container = copy.copy(self.container)
filler = FrontFiller(self.startAP, self.areaGraph, self.restrictions, container)
filler = FrontFiller(self.startAP, self.areaGraph, self.restrictions, container, random=self.random)
condition = filler.createStepCountCondition(4)
(isStuck, itemLocations, progItems) = filler.generateItems(condition)
self.log.debug("********* PRE RANDO END")
@@ -345,9 +346,9 @@ class RandoSetup(object):
def getForbiddenItemsFromList(self, itemList):
self.log.debug('getForbiddenItemsFromList: ' + str(itemList))
remove = []
n = randGaussBounds(len(itemList))
n = randGaussBounds(self.random, len(itemList))
for i in range(n):
idx = random.randint(0, len(itemList) - 1)
idx = self.random.randint(0, len(itemList) - 1)
item = itemList.pop(idx)
if item is not None:
remove.append(item)

View File

@@ -1,4 +1,4 @@
import copy, random
import copy
from ..utils import log
from ..graph.graph_utils import getAccessPoint
from ..rando.ItemLocContainer import getLocListStr
@@ -112,7 +112,7 @@ class Restrictions(object):
return item.Class == "Minor"
# return True if we can keep morph as a possibility
def lateMorphCheck(self, container, possibleLocs):
def lateMorphCheck(self, container, possibleLocs, random):
# the closer we get to the limit the higher the chances of allowing morph
proba = random.randint(0, self.lateMorphLimit)
if self.split == 'Full':

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python3
from Utils import output_path
import argparse, os.path, json, sys, shutil, random, copy, requests
import argparse, os.path, json, sys, shutil, copy, requests
from .rando.RandoSettings import RandoSettings, GraphSettings
from .rando.RandoExec import RandoExec
@@ -39,7 +39,7 @@ objectives = defaultMultiValues['objective']
tourians = defaultMultiValues['tourian']
areaRandomizations = defaultMultiValues['areaRandomization']
def randomMulti(args, param, defaultMultiValues):
def randomMulti(args, param, defaultMultiValues, random):
value = args[param]
isRandom = False
@@ -250,10 +250,11 @@ class VariaRandomizer:
parser.add_argument('--tourianList', help="list to choose from when random",
dest='tourianList', nargs='?', default=None)
def __init__(self, options, rom, player):
def __init__(self, options, rom, player, seed, random):
# parse args
self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values
self.player = player
self.random = random
args = self.args
args.rom = rom
# args.startLocation = to_pascal_case_with_space(options.startLocation.current_key)
@@ -323,11 +324,13 @@ class VariaRandomizer:
logger.debug("preset: {}".format(preset))
# if no seed given, choose one
if args.seed == 0:
self.seed = random.randrange(sys.maxsize)
else:
self.seed = args.seed
# Archipelago provides a seed for the multiworld.
self.seed = seed
# # if no seed given, choose one
# if args.seed == 0:
# self.seed = random.randrange(sys.maxsize)
# else:
# self.seed = args.seed
logger.debug("seed: {}".format(self.seed))
if args.raceMagic is not None:
@@ -360,12 +363,12 @@ class VariaRandomizer:
logger.debug("maxDifficulty: {}".format(self.maxDifficulty))
# handle random parameters with dynamic pool of values
(_, progSpeed) = randomMulti(args.__dict__, "progressionSpeed", speeds)
(_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs)
(majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits)
(_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours)
(_, args.tourian) = randomMulti(args.__dict__, "tourian", tourians)
(areaRandom, args.area) = randomMulti(args.__dict__, "area", areaRandomizations)
(_, progSpeed) = randomMulti(args.__dict__, "progressionSpeed", speeds, random)
(_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs, random)
(majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits, random)
(_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours, random)
(_, args.tourian) = randomMulti(args.__dict__, "tourian", tourians, random)
(areaRandom, args.area) = randomMulti(args.__dict__, "area", areaRandomizations, random)
areaRandomization = args.area in ['light', 'full']
lightArea = args.area == 'light'
@@ -626,7 +629,7 @@ class VariaRandomizer:
if args.objective:
if (args.objectiveRandom):
availableObjectives = [goal for goal in objectives if goal != "collect 100% items"] if "random" in args.objectiveList else args.objectiveList
self.objectivesManager.setRandom(args.nbObjective, availableObjectives)
self.objectivesManager.setRandom(args.nbObjective, availableObjectives, self.random)
else:
maxActiveGoals = Objectives.maxActiveGoals - addedObjectives
if len(args.objective) > maxActiveGoals:
@@ -660,7 +663,7 @@ class VariaRandomizer:
# print("energyQty:{}".format(energyQty))
#try:
self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player)
self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player, self.random)
self.container = self.randoExec.randomize()
# if we couldn't find an area layout then the escape graph is not created either
# and getDoorConnections will crash if random escape is activated.
@@ -690,7 +693,7 @@ class VariaRandomizer:
'gameend.ips', 'grey_door_animals.ips', 'low_timer.ips', 'metalimals.ips',
'phantoonimals.ips', 'ridleyimals.ips']
if args.escapeRando == False:
args.patches.append(random.choice(animalsPatches))
args.patches.append(self.random.choice(animalsPatches))
args.patches.append("Escape_Animals_Change_Event")
else:
optErrMsgs.append("Ignored animals surprise because of escape randomization")
@@ -760,9 +763,9 @@ class VariaRandomizer:
# patch local rom
# romFileName = args.rom
# shutil.copyfile(romFileName, outputFilename)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, player=self.player, random=self.random)
else:
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic)
romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic, random=self.random)
if customPrePatchApply != None:
customPrePatchApply(romPatcher)

View File

@@ -49,7 +49,7 @@ class RomPatcher:
'DoorsColors': ['beam_doors_plms.ips', 'beam_doors_gfx.ips', 'red_doors.ips']
}
def __init__(self, settings=None, romFileName=None, magic=None, player=0):
def __init__(self, settings=None, romFileName=None, magic=None, player=0, *, random):
self.log = log.get('RomPatcher')
self.settings = settings
#self.romFileName = romFileName
@@ -76,6 +76,7 @@ class RomPatcher:
0x93ea: self.forceRoomCRE
}
self.player = player
self.random = random
def patchRom(self):
self.applyIPSPatches()
@@ -496,9 +497,9 @@ class RomPatcher:
self.ipsPatches = []
def writeSeed(self, seed):
random.seed(seed)
seedInfo = random.randint(0, 0xFFFF)
seedInfo2 = random.randint(0, 0xFFFF)
r = random.Random(seed)
seedInfo = r.randint(0, 0xFFFF)
seedInfo2 = r.randint(0, 0xFFFF)
self.romFile.writeWord(seedInfo, snes_to_pc(0xdfff00))
self.romFile.writeWord(seedInfo2)
@@ -1066,7 +1067,7 @@ class RomPatcher:
def writeObjectives(self, itemLocs, tourian):
objectives = Objectives.objDict[self.player]
objectives.writeGoals(self.romFile)
objectives.writeGoals(self.romFile, self.random)
objectives.writeIntroObjectives(self.romFile, tourian)
self.writeItemsMasks(itemLocs)
# hack bomb_torizo.ips to wake BT in all cases if necessary, ie chozo bots objective is on, and nothing at bombs

View File

@@ -1,4 +1,3 @@
import random
from enum import IntEnum,IntFlag
import copy
from ..logic.smbool import SMBool
@@ -123,7 +122,7 @@ class Door(object):
else:
return [color for color in colorsList if color not in self.forbiddenColors]
def randomize(self, allowGreyDoors):
def randomize(self, allowGreyDoors, random):
if self.canRandomize():
if self.canGrey and allowGreyDoors:
self.setColor(random.choice(self.filterColorList(colorsListGrey)))
@@ -347,9 +346,9 @@ class DoorsManager():
currentDoors['CrabShaftRight'].forceBlue()
@staticmethod
def randomize(allowGreyDoors, player):
def randomize(allowGreyDoors, player, random):
for door in DoorsManager.doorsDict[player].values():
door.randomize(allowGreyDoors)
door.randomize(allowGreyDoors, random)
# set both ends of toilet to the same color to avoid soft locking in area rando
toiletTop = DoorsManager.doorsDict[player]['PlasmaSparkBottom']
toiletBottom = DoorsManager.doorsDict[player]['OasisTop']

View File

@@ -1,5 +1,4 @@
import copy
import random
from ..rom.addresses import Addresses
from ..rom.rom import pc_to_snes
from ..logic.helpers import Bosses
@@ -28,7 +27,7 @@ class Synonyms(object):
]
alreadyUsed = []
@staticmethod
def getVerb():
def getVerb(random):
verb = random.choice(Synonyms.killSynonyms)
while verb in Synonyms.alreadyUsed:
verb = random.choice(Synonyms.killSynonyms)
@@ -88,10 +87,10 @@ class Goal(object):
# not all objectives require an ap (like limit objectives)
return self.clearFunc(smbm, ap)
def getText(self):
def getText(self, random):
out = "{}. ".format(self.rank)
if self.useSynonym:
out += self.text.format(Synonyms.getVerb())
out += self.text.format(Synonyms.getVerb(random))
else:
out += self.text
assert len(out) <= 28, "Goal text '{}' is too long: {}, max 28".format(out, len(out))
@@ -676,7 +675,7 @@ class Objectives(object):
return [goal.name for goal in _goals.values() if goal.available and (not removeNothing or goal.name != "nothing")]
# call from rando
def setRandom(self, nbGoals, availableGoals):
def setRandom(self, nbGoals, availableGoals, random):
while self.nbActiveGoals < nbGoals and availableGoals:
goalName = random.choice(availableGoals)
self.addGoal(goalName)
@@ -702,7 +701,7 @@ class Objectives(object):
LOG.debug("tourianRequired: {}".format(self.tourianRequired))
# call from rando
def writeGoals(self, romFile):
def writeGoals(self, romFile, random):
# write check functions
romFile.seek(Addresses.getOne('objectivesList'))
for goal in self.activeGoals:
@@ -736,7 +735,7 @@ class Objectives(object):
space = 3 if self.nbActiveGoals == 5 else 4
for i, goal in enumerate(self.activeGoals):
addr = baseAddr + i * lineLength * space
text = goal.getText()
text = goal.getText(random)
romFile.seek(addr)
for c in text:
if c not in char2tile:

View File

@@ -1,5 +1,5 @@
import io
import os, json, re, random
import os, json, re
import pathlib
import sys
from typing import Any
@@ -88,7 +88,7 @@ def normalizeRounding(n):
# gauss random in [0, r] range
# the higher the slope, the less probable extreme values are.
def randGaussBounds(r, slope=5):
def randGaussBounds(random, r, slope=5):
r = float(r)
n = normalizeRounding(random.gauss(r/2, r/slope))
if n < 0:
@@ -111,7 +111,7 @@ def getRangeDict(weightDict):
return rangeDict
def chooseFromRange(rangeDict):
def chooseFromRange(rangeDict, random):
r = random.random()
val = None
for v in sorted(rangeDict, key=rangeDict.get):

View File

View File

View File

View File

@@ -617,7 +617,7 @@ if is_easter_time():
easter_special_option_group = OptionGroup("EASTER SPECIAL", [
EasterEggHunt,
])
witness_option_groups = [easter_special_option_group, *witness_option_groups]
witness_option_groups.insert(2, easter_special_option_group)
else:
silly_options_group = next(group for group in witness_option_groups if group.name == "Silly Options")
silly_options_group.options.append(EasterEggHunt)