Compare commits

...

25 Commits

Author SHA1 Message Date
Exempt-Medic
9cf4737909 Remove false claim that rules can be done in generate_basic 2025-04-05 07:29:38 -04:00
black-sliver
61e83a300b Clients: stop updating datapackage in persistent_storage (#4799)
Still uses things that are in there but stops writing to it.
2025-04-05 11:51:01 +02:00
Exempt-Medic
136a13aac7 Docs: Include that DeathLink cause can be an empty string (#4729) 2025-04-04 22:39:18 -04:00
massimilianodelliubaldini
2c90db9ae7 Docs: Additional detail and organization to adding games.md (#4805)
* Additional detail and organization to adding games.md

* Minor fixes.

* Update docs/adding games.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Code review updates.

* More updates.

* Client icon blurb.

* Update docs/adding games.md

Co-authored-by: qwint <qwint.42@gmail.com>

* Revert one line.

* Filler item name blurb.

* Updates for Violet.

* Reorganize client expectations.

* Missed a line delete.

* Doctor's orders

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-04-05 04:18:47 +02:00
Richard Snider
507e051a5a Core: Handle integer arguments in player names gracefully (#4151) 2025-04-05 03:36:20 +02:00
Scipio Wright
b5bf9ed1d7 TUNIC: Error message in the spot that UT errors at if you have an old APWorld #4788
Schnice and Shrimple
2025-04-05 00:53:13 +02:00
Fabian Dill
215eb7e473 core: increment version (#4808) 2025-04-04 23:25:37 +02:00
qwint
f42233699a Core: make accessibility_corrections only state.remove if the location was collected 2025-04-04 23:20:45 +02:00
massimilianodelliubaldini
1bec68df4d WebHost: Standardize some 404 redirects (#4642) 2025-04-04 23:11:45 +02:00
CodeGorilla
d8576e72eb Pokemon Red/Blue: Set allow_partial_entrances to true when building a state for ER #4802
Co-authored-by: CodeGorilla <3672561+Ars-Ignis@users.noreply.github.com>
2025-04-04 10:48:47 +02:00
Fabian Dill
7265468e8d kvui: fix [u] and [/u] appearing in copied hints (#4794) 2025-04-03 09:22:02 +02:00
Fabian Dill
d07f36dedd Core: increment version (#4787) 2025-04-02 05:35:39 +02:00
Scipio Wright
364a1b71ec TUNIC: Note Death Link and Trap Link in-game toggles on Game Info page (#4741)
* Note death link and trap link in game info page

* Update worlds/tunic/docs/en_TUNIC.md

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Turn it into a bulleted list
2025-04-01 19:55:19 -04:00
Sanjay Govind
daee6d210f CommonClient: don't update ui hints if there is no ui (#4791) 2025-04-02 01:54:27 +02:00
Bryce Wilson
96be0071e6 Pokemon Emerald: Move recent change to new version (#4793) 2025-04-02 00:50:39 +02:00
threeandthreee
ff8e1dfb47 Launcher: Remove an unnecessary global (#4785) 2025-04-01 21:28: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
65 changed files with 485 additions and 308 deletions

View File

@@ -413,7 +413,8 @@ class CommonContext:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
self.ui.update_hints()
if self.ui:
self.ui.update_hints()
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
@@ -624,9 +625,6 @@ class CommonContext:
def consume_network_data_package(self, data_package: dict):
self.update_data_package(data_package)
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)

View File

@@ -348,10 +348,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
if (location.item is not None and location.item.advancement and location.address is not None and not
location.locked and location.item.player not in minimal_players):
pool.append(location.item)
state.remove(location.item)
location.item = None
if location in state.advancements:
state.advancements.remove(location)
state.remove(location.item)
locations.append(location)
if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)

View File

@@ -279,22 +279,30 @@ def get_choice(option, root, value=None) -> Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
class SafeFormatter(string.Formatter):
def get_value(self, key, args, kwargs):
if isinstance(key, int):
if key < len(args):
return args[key]
else:
return "{" + str(key) + "}"
else:
return kwargs.get(key, "{" + key + "}")
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name.lower()] += 1
number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
"NUMBER": (number if number > 1 else ''),
"player": player,
"PLAYER": (player if player > 1 else '')})
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
# Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.0"
__version__ = "0.6.2"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")

View File

@@ -35,6 +35,12 @@ def start_playing():
@app.route('/games/<string:game>/info/<string:lang>')
@cache.cached()
def game_info(game, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in world.web.game_info_languages:
raise KeyError("Sorry, this game's info page is not available in that language yet.")
except KeyError:
return abort(404)
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
@@ -52,6 +58,12 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@cache.cached()
def tutorial(game, file, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
raise KeyError("Sorry, the tutorial is not available in that language yet.")
except KeyError:
return abort(404)
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))

View File

@@ -6,7 +6,7 @@ from typing import Dict, Union
from docutils.core import publish_parts
import yaml
from flask import redirect, render_template, request, Response
from flask import redirect, render_template, request, Response, abort
import Options
from Utils import local_path
@@ -142,7 +142,10 @@ def weighted_options_old():
@app.route("/games/<string:game>/weighted-options")
@cache.cached()
def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
try:
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
except KeyError:
return abort(404)
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
@app.route("/games/<string:game>/player-options")
@cache.cached()
def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
try:
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
except KeyError:
return abort(404)
# YAML generator for player-options

View File

@@ -42,10 +42,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView();
}
});
}).catch((error) => {
console.error(error);
gameInfo.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -49,10 +49,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView();
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
});
});

View File

@@ -1,5 +1,8 @@
# Adding Games
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
guide.
Adding a new game to Archipelago has two major parts:
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
@@ -13,30 +16,51 @@ it will not be detailed here.
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
to behave as expected are:
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
### Hard Requirements
In order for the game client to behave as expected, it must be able to perform these functions:
* Handle both secure and unsecure websocket connections
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
demand
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
normally expect from features such as starting inventory, item link replacement, or item cheating
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
a player or location attributed to them
* Reconnect if the connection is unstable and lost while playing
* Be able to change the port for saved connection info
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
privilege can be lost, requiring the room to be moved to a new port
* Reconnect if the connection is unstable and lost while playing
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
order.
* Receive items that were sent to the player while they were not connected to the server
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
strictly required
privilege can be lost, requiring the room to be moved to a new port
* Send a status update packet alerting the server that the player has completed their goal
Libraries for most modern languages and the spec for various packets can be found in the
[network protocol](/docs/network%20protocol.md) API reference document.
Regarding items and locations, the game client must be able to handle these tasks:
#### Location Handling
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
once, but the client was not connected when they happened: The client must send those location checks on connection
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
#### Item Handling
Receive and parse network packets from the server when the player receives an item.
* It must reward items to the player on demand, as items can come from other players at any time.
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
your items can be received **any** number of times.
* Admins and players may use server commands to create items without a player or location attributed to them. The
client must be able to handle these items.
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
guaranteed order.
* It must be able to receive items that were sent to the player while they were not connected to the server.
### Encouraged Features
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
if possible.
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
other clients. The icon size is 38x38 pixels, but it will accept larger images with downscaling.
## World
@@ -44,35 +68,90 @@ The world is your game integration for the Archipelago generator, webhost, and m
information necessary for creating the items and locations to be randomized, the logic for item placement, the
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
following requirements:
repository and creating a new world package in `/worlds/`.
* A folder within `/worlds/` that contains an `__init__.py`
* A `World` subclass where you create your world and define all of its rules
* A unique game name
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
definition
* The game_info doc must follow the format `{language_code}_{game_name}.md`
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
check out [world maintainer.md](/docs/world%20maintainer.md).
### Hard Requirements
A bare minimum world implementation must satisfy the following requirements:
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
* The `/worlds/{game}` folder contains an `__init__.py`
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
packaging
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
* The game folder has at least one setup doc
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
your world and define all of its rules and features
Within the `World` subclass you should also have:
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
subclass for webhost documentation and behaviors
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
ones you include.
* In your `WebWorld`, override the list of
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
or setup doc you included in the game folder.
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
`item_name_to_id` and `location_name_to_id`, respectively.
* Create an item when `create_item` is called both by your code and externally
* An `options_dataclass` defining the options players have available to them
* A `Region` for your player with the name "Menu" to start from
* Create a non-zero number of locations and add them to your regions
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
`item_name_to_id` and `location_name_to_id`, respectively.
* An implementation of `create_item` that can create an item when called by either your code or by another process
within Archipelago
* At least one `Region` for your player to start from (i.e. the Origin Region)
* The default name of this region is "Menu" but you may configure a different name with
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
* A non-zero number of locations, added to your regions
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
Notable caveats:
* The "Menu" region will always be considered the "start" for the player
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
### Encouraged Features
These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
if possible.
* An implementation of
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
filler items.
* An `options_dataclass` defining the options players have available to them
* This should be accompanied by a type hint for `options` with the same class name
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
for better organization on the webhost
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
for player convenience
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
for player convenience
* A dictionary of
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
for player convenience
* Other games may also benefit from your name group dictionaries for hints, features, etc.
### Discouraged or Prohibited Behavior
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
workarounds or preferred methods which should be used instead:
* All items submitted to the multiworld itempool must not be manually placed by the World.
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
multiworld itempool.
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
* It is discouraged to use `yaml.load` directly due to security concerns.
* When possible, use `Utils.yaml_load` instead, as this defaults to the safe loader.
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
Do **not** use `=` as this will overwrite all elements for all games in the seed.
* Instead, use `append`, `extend`, or `+=`.
### Notable Caveats
* The Origin Region will always be considered the "start" for the player
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
start of the game from anywhere
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
`append`, `extend`, or `+=`. **Do not use `=`**
* Regions are simply containers for locations that share similar access rules. They do not have to map to
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md).
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).

View File

@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
| Name | Type | Notes |
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
| Name | Type | Notes |
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -606,8 +606,8 @@ from .items import get_item_type
def set_rules(self) -> None:
# For some worlds this step can be omitted if either a Logic mixin
# (see below) is used, it's easier to apply the rules from data during
# location generation or everything is in generate_basic
# (see below) is used or it's easier to apply the rules from data during
# location generation
# set a simple rule for an region
set_rule(self.multiworld.get_entrance("Boss Door", self.player),

View File

@@ -296,7 +296,7 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
else:
# Not a fan of the following few lines, but they work.
temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
text = "".join(part for part in temp if not part.startswith("["))
cmdinput = App.get_running_app().textinput
if not cmdinput.text:
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
@@ -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

@@ -88,7 +88,6 @@ processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
process.start()

View File

@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
)
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
game_info_languages = ["en", "fr"]
class ALTTPWorld(World):

View File

@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
)
tutorials = [setup, setup_fr]
game_info_languages = ["en", "fr"]
class AquariaWorld(World):

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

@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
)
tutorials = [setup_en, setup_de]
game_info_languages = ["en", "de"]
class CliqueWorld(World):

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

@@ -34,6 +34,7 @@ class DLCqwebworld(WebWorld):
["Deoxis"]
)
tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class DLCqworld(World):

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,\
@@ -44,6 +43,7 @@ class FFMQWebWorld(WebWorld):
)
tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class FFMQWorld(World):
@@ -134,7 +134,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

@@ -130,6 +130,7 @@ class OOTWeb(WebWorld):
tutorials = [setup, setup_fr, setup_de]
option_groups = oot_option_groups
game_info_languages = ["en", "de"]
class OOTWorld(World):

View File

View File

@@ -1,3 +1,11 @@
# 2.4.1
### Fixes
- 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.4.0
### Features

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

@@ -2414,6 +2414,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
loc.place_locked_item(badge)
state = multiworld.state.copy()
state.allow_partial_entrances = True
for item, data in item_table.items():
if (data.id or item in poke_data.pokemon_data) and data.classification == ItemClassification.progression \
and ("Badge" not in item or world.options.badgesanity):

View File

@@ -41,6 +41,7 @@ class Starcraft2WebWorld(WebWorld):
)
tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class SC2World(World):

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

@@ -88,6 +88,8 @@ Notes:
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.
## Is there anything else I should know?
You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips.
You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location.
You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance.
- You can go to [The TUNIC Randomizer Website](https://rando.tunic.run/) for a list of randomizer features as well as some helpful tips.
- You can use the Fairy Seeking Spell (ULU RDR) to locate the nearest unchecked location.
- You can use the Entrance Seeking Spell (RDR ULU) to locate the nearest unused entrance.
- Death Link can be toggled in game, and it can be set to receive traps instead of deaths.
- Trap Link can be toggled in-game as well, which makes it so other players with Trap Link enabled will share the effects of traps with you, and vice versa. Trap Link functions cross-game, but only with other games that have Trap Link implemented, and only some traps can be shared, depending on the game.

View File

@@ -330,7 +330,11 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
else:
if not portal2:
raise Exception(f"Could not find entrance named {p_exit} for "
f"plando connections in {player_name}'s YAML.")
f"plando connections in {player_name}'s YAML.\n"
f"If you are using Universal Tracker, the most likely reason for this error "
f"is that the host generated with a newer version of the APWorld.\n"
f"Please check the TUNIC Randomizer Github and place the newest APWorld in your "
f"custom_worlds folder, and remove the one in lib/worlds if there is one there.")
dead_ends.remove(portal2)
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa

View File

View File