Compare commits

...

16 Commits

Author SHA1 Message Date
NewSoupVi
4e08c48238 Revert "Core: update websockets (#4732)"
This reverts commit 42eaeb92f0.
2025-03-18 23:46:36 +01:00
CodeGorilla
57b94dba6f Options: Add a column for player ID to --csv_output (#4715) 2025-03-17 21:43:00 +01:00
ironminer888
0dd188e108 LADX: Add more specific "item icon guessing" support for some games (#4706)
* DKC3, PKMN R/B/Em, M&L specific item matches

* MLSS Bean types are now discrete

* Add Doom 1/2 items

* Add Doom 1/2 items, actually

* Add Inscryption items

* Add more SA2B items, Minecraft

* Add VVVVVV

* Add misc items, comma fixes

* Hat in Time items

* Misc changes

* Expand TODO

* Add more OoT items, Pokemon consumables

* KH2

* KH1, adjust KH2 items

* Formatting fixes

* more item changes, fix kh1 name

* Fix KH1 name

* Add Full Heal to MEDICINE graphics

* Final comma fixes before PR

* Add Full Restore as Medicine

* Move some names to generic, drink fixes, double-quotes consistency fix

* moved ROCK SMASH match to PHRASES dict

* Removed some redundant name checks, remove Old Amber check from Emerald

* Added "PASS" generic check as "LETTER" sprite

* Removed TODO

* Corrected KH1 name for real this time

* Icon assignment now uppers freogin item string during comparison

* Doom skull keys are now NIGHTMARE_KEY, added QUILL as generic for FEATHER

* KH2 armor is Blunic, accessories are Ribbons

* KH1 accessories/armor are Blunic

* "ROCK SMASH" is now "BOMB"

* Removed extra space
2025-03-17 11:50:57 -04:00
PoryGone
bf8c840293 Celeste 64: v1.3 Content Update (#4581)
### Features:

- New optional Location Checks
	- Checkpointsanity
- Hair Color
	- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
- Other Player Ghosts
	- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld

### Quality of Life:

- Checkpoint Warping
	- Received Checkpoint items allow for warping to their respective checkpoint
		- These items are on their respective checkpoint location if Checkpointsanity is disabled
	- Logic accounts for being able to warp to otherwise inaccessible areas
	- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
- New Options toggle to enable/disable background input

### Bug Fixes:

- Traffic Blocks now correctly appear disabled within Cassettes
2025-03-17 02:46:34 +01:00
black-sliver
c0244f3018 Tests: unroll 2 player gen, add parametrization helper, add docs (#4648)
* Tests: unroll test_multiworlds.TestTwoPlayerMulti

Also adds a helper function that other tests can use to unroll tests.

* Docs: add more details to docs/tests.md

* Explain parametrization, subtests and link to the new helper
* Mention some performance details and work-arounds
* Mention multithreading / pytest-xdist

* Tests: make param.classvar_matrix accept sets

* CI: add test/param.py to type checking

* Tests: add missing typing to test/param.py

* Tests: fix typo in test/param.py doc comment

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

* update docs

* Docs: reword note on performance

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-17 00:16:02 +01:00
black-sliver
8af8502202 CI: pin some actions (#4744) 2025-03-17 00:02:00 +01:00
Fabian Dill
42eaeb92f0 Core: update websockets (#4732) 2025-03-16 22:13:12 +01:00
Alchav
7f35eb8867 Pokémon R/B: Allow generating with all items linked (#4330)
* Pokémon R/B: Allow generating with all items linked

* check priority/excluded locations for pc_item

* Update regions.py

* Un-remove regions.py code
2025-03-16 12:33:24 -04:00
BadMagic100
785569c40c Core: Generic ER fails in stage 1 when the last available target is an indirect conditioned dead end (#4679)
* Add test that stage1 ER will not fail due to speculative sweeping an indirect conditioned dead end

* Skip speculative sweep if it's the last entrance placement

* Better implementation of needs_speculative_sweep

* pep8
2025-03-15 18:56:07 +01:00
Scipio Wright
a9eb70a881 OoT: Remove Outdated Spanish Setup Guide (#4736)
* Remove spanish setup guide from webworld

* Update __init__.py

* Update __init__.py
2025-03-15 07:16:06 -04:00
Scipio Wright
5d3d0c8625 WebHost: Update text for options you can't modify (#4614) 2025-03-15 07:10:07 -04:00
Scipio Wright
7e32feeea3 Webhost: Update random option wording on webhost (#4555)
* Update random option wording on webhost

* Update WebHostLib/templates/playerOptions/macros.html

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-03-15 07:09:04 -04:00
neocerber
0d1935e757 SC2: Add a description of mission order and the impact of collect on a SC2 world (#4398)
* Added mission order to randomized stuff, added a mention to the default option collect on goal, added an issue about mission order progress vs AP collect

* Remove false menion of collect being note modifyable after the mworld was gen

* Simplification of some sentences

* American spelling, header newline, and other

* Revert gray to grey, corrected some colors

* Forgot a gray -> grey

* Replace how the faction color option is described to side-step difference within yaml and client. Both fr/en.
2025-03-14 11:35:58 -04:00
Benny D
9b3ee018e9 Core/Various Worlds: Fix crash/freeze with unicode characters (#4671)
replace colorama.init with just_fix_windows_console
2025-03-14 08:24:37 +01:00
NewSoupVi
1de411ec89 The Witness: Change Regions, Areas and Connections from Dict[str, Any] to dataclasses&NamedTuples (#4415)
* Change Regions, Areas and Connections to dataclasses/NamedTuples

* Move to new file

* we do a little renaming

* Purge the 'lambda' naming in favor of 'rule' or 'WitnessRule'

* missed one

* unnecessary change

* omega oops

* NOOOOOOOO

* Merge error

* mypy thing
2025-03-13 23:59:09 +01:00
LiquidCat64
3192799bbf CVCotM: Clarify the Wii U VC version is unsupported (#4734)
* Comment out VC ROM hash usages and clarify that it's unsupported.

* Update worlds/cvcotm/docs/en_Castlevania - Circle of the Moon.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/cvcotm/docs/setup_en.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-13 00:21:09 +01:00
63 changed files with 1324 additions and 528 deletions

View File

@@ -2,6 +2,7 @@
"include": [
"../BizHawkClient.py",
"../Patch.py",
"../test/param.py",
"../test/general/test_groups.py",
"../test/general/test_helpers.py",
"../test/general/test_memory.py",

View File

@@ -36,9 +36,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@v1
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
with:
build-type: 'Release'
- name: Build tests

View File

@@ -511,7 +511,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1128,7 +1128,7 @@ def run_as_textclient(*args):
args = handle_url_arg(args, parser=parser)
# use colorama to display colored text highlighting on windows
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -261,7 +261,7 @@ if __name__ == '__main__':
parser = get_base_parser()
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -803,6 +803,6 @@ async def main():
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -370,7 +370,7 @@ if __name__ == "__main__":
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -47,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
from BaseClasses import ItemClassification
min_client_version = Version(0, 1, 6)
colorama.init()
colorama.just_fix_windows_console()
def remove_from_list(container, value):

View File

@@ -346,7 +346,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
"ID": player,
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
@@ -1591,7 +1592,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
game_option_names.append(display_name)
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
fields = ["ID", "Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)

View File

@@ -735,6 +735,6 @@ async def main() -> None:
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -500,7 +500,7 @@ def main():
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(_main())
colorama.deinit()

View File

@@ -446,6 +446,6 @@ if __name__ == '__main__':
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -213,7 +213,7 @@
{% endmacro %}
{% macro RandomizeButton(option_name, option) %}
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
<div class="randomize-button" data-tooltip="Pick a random value for this option.">
<label for="random-{{ option_name }}">
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
🎲

View File

@@ -100,7 +100,7 @@
{% else %}
<div class="unsupported-option">
This option is not supported. Please edit your .yaml file manually.
This option cannot be modified here. Please edit your .yaml file manually.
</div>
{% endif %}

View File

@@ -386,7 +386,7 @@ if __name__ == '__main__':
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -82,6 +82,38 @@ Unit tests can also be created using [TestBase](/test/bases.py#L16) or
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.
#### Parametrization
When defining a test that needs to cover a range of inputs it is useful to parameterize (to run the same test
for multiple inputs) the base test. Some important things to consider when attempting to parametrize your test are:
* [Subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests)
can be used to have parametrized assertions that show up similar to individual tests but without the overhead
of needing to instantiate multiple tests; however, subtests can not be multithreaded and do not have individual
timing data, so they are not suitable for slow tests.
* Archipelago's tests are test-runner-agnostic. That means tests are not allowed to use e.g. `@pytest.mark.parametrize`.
Instead, we define our own parametrization helpers in [test.param](/test/param.py).
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
or setting `WorldTestBase.run_default_tests` to False.
#### Performance Considerations
Archipelago is big enough that the runtime of unittests can have an impact on productivity.
Individual tests should take less than a second, so they can be properly multithreaded.
Ideally, thorough tests are directed at actual code/functionality. Do not just create and/or fill a ton of individual
Multiworlds that spend most of the test time outside what you actually want to test.
Consider generating/validating "random" games as part of your APWorld release workflow rather than having that be part
of continuous integration, and add minimal reproducers to the "normal" tests for problems that were found.
You can use [@unittest.skipIf](https://docs.python.org/3/library/unittest.html#unittest.skipIf) with an environment
variable to keep all the benefits of the test framework while not running the marked tests by default.
## Running Tests
#### Using Pycharm
@@ -100,3 +132,11 @@ next to the run and debug buttons.
#### Running Tests without Pycharm
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
#### Running Tests Multithreaded
pytest can run multiple test runners in parallel with the pytest-xdist extension.
Install with `pip install pytest-xdist`.
Run with `pytest -n12` to spawn 12 process that each run 1/12th of the tests.

View File

@@ -358,6 +358,34 @@ def randomize_entrances(
if on_connect:
on_connect(er_state, placed_exits)
def needs_speculative_sweep(dead_end: bool, require_new_exits: bool, placeable_exits: list[Entrance]) -> bool:
# speculative sweep is expensive. We currently only do it as a last resort, if we might cap off the graph
# entirely
if len(placeable_exits) > 1:
return False
# in certain stages of randomization we either expect or don't care if the search space shrinks.
# we should never speculative sweep here.
if dead_end or not require_new_exits or not perform_validity_check:
return False
# edge case - if all dead ends have pre-placed progression or indirect connections, they are pulled forward
# into the non dead end stage. In this case, and only this case, it's possible that the last connection may
# actually be placeable in stage 1. We need to skip speculative sweep in this case because we expect the graph
# to get capped off.
# check to see if we are proposing the last placement
if not coupled:
# in uncoupled, this check is easy as there will only be one target.
is_last_placement = len(entrance_lookup) == 1
else:
# a bit harder, there may be 1 or 2 targets depending on if the exit to place is one way or two way.
# if it is two way, we can safely assume that one of the targets is the logical pair of the exit.
desired_target_count = 2 if placeable_exits[0].randomization_type == EntranceType.TWO_WAY else 1
is_last_placement = len(entrance_lookup) == desired_target_count
# if it's not the last placement, we need a sweep
return not is_last_placement
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
@@ -371,11 +399,9 @@ def randomize_entrances(
# very last exit and check whatever exits we open up are functionally accessible.
# this requirement can be ignored on a beaten minimal, islands are no issue there.
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
or target_entrance.connected_region not in er_state.placed_regions)
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
and len(placeable_exits) == 1)
or target_entrance.connected_region not in er_state.placed_regions)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
if (needs_speculative_sweep(dead_end, require_new_exits, placeable_exits)
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
continue
do_placement(source_exit, target_entrance)

View File

@@ -218,7 +218,7 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual(80, len(result.pairings))
self.assertEqual(80, len(result.placements))
def test_coupling(self):
def test_coupled(self):
"""tests that in coupled mode, all 2 way transitions have an inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
@@ -236,6 +236,36 @@ class TestRandomizeEntrances(unittest.TestCase):
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_uncoupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_coupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_uncoupled(self):
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
multiworld = generate_test_multiworld()

View File

@@ -1,5 +1,5 @@
import unittest
from typing import List, Tuple
from typing import ClassVar, List, Tuple
from unittest import TestCase
from BaseClasses import CollectionState, Location, MultiWorld
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
from Options import Accessibility
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
from ..general import gen_steps, setup_multiworld
from ..param import classvar_matrix
class MultiworldTestBase(TestCase):
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
class TestTwoPlayerMulti(MultiworldTestBase):
game: ClassVar[str]
def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate."""
for world_type in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
world_type = AutoWorldRegister.world_types[self.game]
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

46
test/param.py Normal file
View File

@@ -0,0 +1,46 @@
import itertools
import sys
from typing import Any, Callable, Iterable
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
"""
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
than subtests.
The kwargs will be set as ClassVars in the newly created classes. Use as ::
@classvar_matrix(var_name=[value1, value2])
class MyTestCase(unittest.TestCase):
var_name: typing.ClassVar[...]
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
:return: A decorator to be applied to a class.
"""
keys: tuple[str]
values: Iterable[Iterable[Any]]
keys, values = zip(*kwargs.items())
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
def decorator(cls: type) -> None:
mod = sys.modules[cls.__module__]
for permutation in permutations_dicts:
class Unrolled(cls): # type: ignore
pass
for k, v in permutation.items():
setattr(Unrolled, k, v)
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
params = f"{{{params}}}"
Unrolled.__module__ = cls.__module__
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
setattr(mod, f"{cls.__name__}{params}", Unrolled)
return None
return decorator

View File

@@ -276,6 +276,6 @@ def launch(*launch_args: str) -> None:
Utils.init_logging("BizHawkClient", exception_logger="Client")
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -261,6 +261,6 @@ def launch():
# options = Utils.get_options()
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1,6 +1,31 @@
# Celeste 64 - Changelog
## v1.3
### Features:
- New optional Location Checks
- Checkpointsanity
- Hair Color
- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
- Other Player Ghosts
- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld
### Quality of Life:
- Checkpoint Warping
- Received Checkpoint items allow for warping to their respective checkpoint
- These items are on their respective checkpoint location if Checkpointsanity is disabled
- Logic accounts for being able to warp to otherwise inaccessible areas
- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
- New Options toggle to enable/disable background input
### Bug Fixes:
- Traffic Blocks now correctly appear disabled within Cassettes
## v1.2
### Features:

View File

@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
}
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
checkpoint_item_data_table: Dict[str, Celeste64ItemData] = {
ItemName.checkpoint_1: Celeste64ItemData(celeste_64_base_id + 0x20, ItemClassification.progression),
ItemName.checkpoint_2: Celeste64ItemData(celeste_64_base_id + 0x21, ItemClassification.progression),
ItemName.checkpoint_3: Celeste64ItemData(celeste_64_base_id + 0x22, ItemClassification.progression),
ItemName.checkpoint_4: Celeste64ItemData(celeste_64_base_id + 0x23, ItemClassification.progression),
ItemName.checkpoint_5: Celeste64ItemData(celeste_64_base_id + 0x24, ItemClassification.progression),
ItemName.checkpoint_6: Celeste64ItemData(celeste_64_base_id + 0x25, ItemClassification.progression),
ItemName.checkpoint_7: Celeste64ItemData(celeste_64_base_id + 0x26, ItemClassification.progression),
ItemName.checkpoint_8: Celeste64ItemData(celeste_64_base_id + 0x27, ItemClassification.progression),
ItemName.checkpoint_9: Celeste64ItemData(celeste_64_base_id + 0x28, ItemClassification.progression),
ItemName.checkpoint_10: Celeste64ItemData(celeste_64_base_id + 0x29, ItemClassification.progression),
}
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table,
**unlockable_item_data_table,
**move_item_data_table,
**checkpoint_item_data_table}
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}

View File

@@ -1,7 +1,7 @@
from typing import Dict, NamedTuple, Optional
from BaseClasses import Location
from .Names import LocationName
from .Names import LocationName, RegionName
celeste_64_base_id: int = 0xCA0000
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
}
friend_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
}
sign_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
}
car_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
LocationName.car_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x300 + 0x01),
}
checkpoint_location_data_table: Dict[str, Celeste64LocationData] = {
LocationName.checkpoint_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x400 + 0x00),
LocationName.checkpoint_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x01),
LocationName.checkpoint_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x02),
LocationName.checkpoint_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x03),
LocationName.checkpoint_5: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x04),
LocationName.checkpoint_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x05),
LocationName.checkpoint_7: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x400 + 0x06),
LocationName.checkpoint_8: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x400 + 0x07),
LocationName.checkpoint_9: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x400 + 0x08),
LocationName.checkpoint_10: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x400 + 0x09),
}
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
**friend_location_data_table,
**sign_location_data_table,
**car_location_data_table}
**car_location_data_table,
**checkpoint_location_data_table}
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}

View File

@@ -15,3 +15,18 @@ ground_dash = "Ground Dash"
air_dash = "Air Dash"
skid_jump = "Skid Jump"
climb = "Climb"
# Checkpoint Items
checkpoint_1 = "Intro Checkpoint"
checkpoint_2 = "Granny Checkpoint"
checkpoint_3 = "South-East Tower Checkpoint"
checkpoint_4 = "Climb Sign Checkpoint"
checkpoint_5 = "Freeway Checkpoint"
checkpoint_6 = "Freeway Feather Checkpoint"
checkpoint_7 = "Feather Maze Checkpoint"
checkpoint_8 = "Double Dash House Checkpoint"
checkpoint_9 = "Badeline Tower Checkpoint"
checkpoint_10 = "Badeline Island Checkpoint"
# Item used for logic definitions that are not possible with the given options
cannot_access = "CANNOT ACCESS"

View File

@@ -10,7 +10,7 @@ strawberry_8 = "Traffic Block Strawberry"
strawberry_9 = "South-West Dash Refills Strawberry"
strawberry_10 = "South-East Tower Side Strawberry"
strawberry_11 = "Girders Strawberry"
strawberry_12 = "North-East Tower Bottom Strawberry"
strawberry_12 = "Badeline Tower Bottom Strawberry"
strawberry_13 = "Breakable Blocks Strawberry"
strawberry_14 = "Feather Maze Strawberry"
strawberry_15 = "Feather Chain Strawberry"
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
strawberry_17 = "Double Dash Puzzle Strawberry"
strawberry_18 = "Double Dash Spike Climb Strawberry"
strawberry_19 = "Double Dash Spring Strawberry"
strawberry_20 = "North-East Tower Breakable Bottom Strawberry"
strawberry_20 = "Badeline Tower Breakable Bottom Strawberry"
strawberry_21 = "Theo Tower Lower Cassette Strawberry"
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
strawberry_23 = "South End of Bridge Cassette Strawberry"
@@ -27,8 +27,8 @@ strawberry_25 = "Cassette Hidden in the House Strawberry"
strawberry_26 = "North End of Bridge Cassette Strawberry"
strawberry_27 = "Distant Feather Cassette Strawberry"
strawberry_28 = "Feather Arches Cassette Strawberry"
strawberry_29 = "North-East Tower Cassette Strawberry"
strawberry_30 = "Badeline Cassette Strawberry"
strawberry_29 = "Badeline Tower Cassette Strawberry"
strawberry_30 = "Badeline Island Cassette Strawberry"
# Friend Locations
granny_1 = "Granny Conversation 1"
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
# Car Locations
car_1 = "Intro Car"
car_2 = "Secret Car"
# Checkpoint Locations
checkpoint_1 = "Intro Checkpoint"
checkpoint_2 = "Granny Checkpoint"
checkpoint_3 = "South-East Tower Checkpoint"
checkpoint_4 = "Climb Sign Checkpoint"
checkpoint_5 = "Freeway Checkpoint"
checkpoint_6 = "Freeway Feather Checkpoint"
checkpoint_7 = "Feather Maze Checkpoint"
checkpoint_8 = "Double Dash House Checkpoint"
checkpoint_9 = "Badeline Tower Checkpoint"
checkpoint_10 = "Badeline Island Checkpoint"

View File

@@ -0,0 +1,13 @@
# Level Base Regions
forsaken_city = "Forsaken City"
# Forsaken City Regions
intro_islands = "Intro Islands"
granny_island = "Granny Island"
highway_island = "Freeway Island"
nw_girders_island = "North-West Girders Island"
ne_feathers_island = "North-East Feathers Island"
se_house_island = "South-East House Island"
badeline_tower_lower = "Badeline Tower Lower"
badeline_tower_upper = "Badeline Tower Upper"
badeline_island = "Badeline Island"

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass
import random
from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
from Options import Choice, TextChoice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions, OptionError
from worlds.AutoWorld import World
class DeathLinkAmnesty(Range):
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
"""
display_name = "Total Strawberries"
range_start = 0
range_end = 46
range_end = 55
default = 20
class StrawberriesRequiredPercentage(Range):
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
"""
display_name = "Carsanity"
class Checkpointsanity(Toggle):
"""
Whether activating Checkpoints grants location checks
Activating this will also shuffle items into the pool which allow usage and warping to each Checkpoint
"""
display_name = "Checkpointsanity"
class ColorChoice(TextChoice):
option_strawberry = 0xDB2C00
option_empty = 0x6EC0FF
option_double = 0xFA91FF
option_golden = 0xF2D450
option_baddy = 0x9B3FB5
option_fire_red = 0xFF0000
option_maroon = 0x800000
option_salmon = 0xFF3A65
option_orange = 0xD86E0A
option_lime_green = 0x8DF920
option_bright_green = 0x0DAF05
option_forest_green = 0x132818
option_royal_blue = 0x0036BF
option_brown = 0xB78726
option_black = 0x000000
option_white = 0xFFFFFF
option_grey = 0x808080
option_any_color = -1
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
if text == "random":
choice_list = list(cls.name_lookup)
choice_list.remove(cls.option_any_color)
return cls(random.choice(choice_list))
return super().from_text(text)
class MadelineOneDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has one dash
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline One Dash Hair Color"
default = ColorChoice.option_strawberry
class MadelineTwoDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has two dashes
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline Two Dash Hair Color"
default = ColorChoice.option_double
class MadelineNoDashHairColor(ColorChoice):
"""
What color Madeline's hair is when she has no dashes
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline No Dash Hair Color"
default = ColorChoice.option_empty
class MadelineFeatherHairColor(ColorChoice):
"""
What color Madeline's hair is when she has a feather
The `any_color` option will choose a fully random color
A custom color entry may be supplied as a 6-character RGB hex color code
e.g. F542C8
"""
display_name = "Madeline Feather Hair Color"
default = ColorChoice.option_golden
class BadelineChaserSource(Choice):
"""
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
Friendsanity,
Signsanity,
Carsanity,
Checkpointsanity,
]),
OptionGroup("Aesthetic Options", [
MadelineOneDashHairColor,
MadelineTwoDashHairColor,
MadelineNoDashHairColor,
MadelineFeatherHairColor,
]),
OptionGroup("Badeline Chasers", [
BadelineChaserSource,
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
friendsanity: Friendsanity
signsanity: Signsanity
carsanity: Carsanity
checkpointsanity: Checkpointsanity
madeline_one_dash_hair_color: MadelineOneDashHairColor
madeline_two_dash_hair_color: MadelineTwoDashHairColor
madeline_no_dash_hair_color: MadelineNoDashHairColor
madeline_feather_hair_color: MadelineFeatherHairColor
badeline_chaser_source: BadelineChaserSource
badeline_chaser_frequency: BadelineChaserFrequency
badeline_chaser_speed: BadelineChaserSpeed
def resolve_options(world: World):
# One Dash Hair
if isinstance(world.options.madeline_one_dash_hair_color.value, str):
try:
world.madeline_one_dash_hair_color = int(world.options.madeline_one_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_one_dash_hair_color`:"
f"{world.options.madeline_one_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_one_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_one_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_one_dash_hair_color = world.options.madeline_one_dash_hair_color.value
# Two Dash Hair
if isinstance(world.options.madeline_two_dash_hair_color.value, str):
try:
world.madeline_two_dash_hair_color = int(world.options.madeline_two_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_two_dash_hair_color`:"
f"{world.options.madeline_two_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_two_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_two_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_two_dash_hair_color = world.options.madeline_two_dash_hair_color.value
# No Dash Hair
if isinstance(world.options.madeline_no_dash_hair_color.value, str):
try:
world.madeline_no_dash_hair_color = int(world.options.madeline_no_dash_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_no_dash_hair_color`:"
f"{world.options.madeline_no_dash_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_no_dash_hair_color.value == ColorChoice.option_any_color:
world.madeline_no_dash_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_no_dash_hair_color = world.options.madeline_no_dash_hair_color.value
# Feather Hair
if isinstance(world.options.madeline_feather_hair_color.value, str):
try:
world.madeline_feather_hair_color = int(world.options.madeline_feather_hair_color.value.strip("#")[:6], 16)
except ValueError:
raise OptionError(f"Invalid input for option `madeline_feather_hair_color`:"
f"{world.options.madeline_feather_hair_color.value} for "
f"{world.player_name}")
elif world.options.madeline_feather_hair_color.value == ColorChoice.option_any_color:
world.madeline_feather_hair_color = world.random.randint(0, 0xFFFFFF)
else:
world.madeline_feather_hair_color = world.options.madeline_feather_hair_color.value

View File

@@ -1,11 +1,23 @@
from typing import Dict, List, NamedTuple
from .Names import RegionName
class Celeste64RegionData(NamedTuple):
connecting_regions: List[str] = []
region_data_table: Dict[str, Celeste64RegionData] = {
"Menu": Celeste64RegionData(["Forsaken City"]),
"Forsaken City": Celeste64RegionData(),
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
RegionName.forsaken_city: Celeste64RegionData([RegionName.intro_islands, RegionName.granny_island, RegionName.highway_island, RegionName.ne_feathers_island, RegionName.se_house_island, RegionName.badeline_tower_upper, RegionName.badeline_island]),
RegionName.intro_islands: Celeste64RegionData([RegionName.granny_island]),
RegionName.granny_island: Celeste64RegionData([RegionName.highway_island, RegionName.nw_girders_island, RegionName.badeline_tower_lower, RegionName.se_house_island]),
RegionName.highway_island: Celeste64RegionData([RegionName.granny_island, RegionName.ne_feathers_island, RegionName.nw_girders_island]),
RegionName.nw_girders_island: Celeste64RegionData([RegionName.highway_island]),
RegionName.ne_feathers_island: Celeste64RegionData([RegionName.se_house_island, RegionName.highway_island, RegionName.badeline_tower_lower, RegionName.badeline_tower_upper]),
RegionName.se_house_island: Celeste64RegionData([RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_lower]),
RegionName.badeline_tower_lower: Celeste64RegionData([RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_upper]),
RegionName.badeline_tower_upper: Celeste64RegionData([RegionName.badeline_island, RegionName.badeline_tower_lower, RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island]),
RegionName.badeline_island: Celeste64RegionData([RegionName.badeline_tower_upper, RegionName.granny_island, RegionName.highway_island]),
}

View File

@@ -1,265 +1,85 @@
from typing import Dict, List
from typing import Dict, List, Tuple, Callable
from BaseClasses import CollectionState
from BaseClasses import CollectionState, Region
from worlds.generic.Rules import set_rule
from . import Celeste64World
from .Names import ItemName, LocationName
from .Names import ItemName, LocationName, RegionName
def set_rules(world: Celeste64World):
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.active_logic_mapping = location_standard_moves_logic
else:
world.active_logic_mapping = location_standard_logic
world.active_logic_mapping = location_standard_moves_logic
world.active_region_logic_mapping = region_standard_moves_logic
else:
if world.options.move_shuffle:
world.active_logic_mapping = location_hard_moves_logic
else:
world.active_logic_mapping = location_hard_logic
world.active_logic_mapping = location_hard_moves_logic
world.active_region_logic_mapping = region_hard_moves_logic
for location in world.multiworld.get_locations(world.player):
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
if world.options.logic_difficulty == "standard":
if world.options.move_shuffle:
world.goal_logic_mapping = goal_standard_moves_logic
else:
world.goal_logic_mapping = goal_standard_logic
else:
if world.options.move_shuffle:
world.goal_logic_mapping = goal_hard_moves_logic
else:
world.goal_logic_mapping = goal_hard_logic
# Completion condition.
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
goal_hard_logic: List[List[str]] = [[]]
goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
location_standard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_6: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_7: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_8: [[ItemName.traffic_block]],
LocationName.strawberry_9: [[ItemName.dash_refill]],
LocationName.strawberry_11: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.double_dash_refill]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
[ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
[ItemName.cassette, ItemName.traffic_block]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.sign_3: [[ItemName.dash_refill],
[ItemName.traffic_block]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
[ItemName.dash_refill, ItemName.feather],
[ItemName.traffic_block, ItemName.feather]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_hard_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_13: [[ItemName.breakables]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
LocationName.strawberry_20: [[ItemName.breakables]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
LocationName.strawberry_22: [[ItemName.cassette]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
LocationName.strawberry_24: [[ItemName.cassette]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
LocationName.strawberry_26: [[ItemName.cassette]],
LocationName.strawberry_27: [[ItemName.cassette]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
LocationName.strawberry_29: [[ItemName.cassette]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
LocationName.sign_2: [[ItemName.breakables]],
LocationName.car_2: [[ItemName.breakables]],
}
location_standard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.strawberry_2: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_5: [[ItemName.air_dash]],
LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
[ItemName.breakables, ItemName.ground_dash]],
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_16: [[ItemName.feather]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_26: [[ItemName.cassette, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.granny_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_2: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.granny_3: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.sign_1: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.climb]],
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
[ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
}
location_hard_moves_logic: Dict[str, List[List[str]]] = {
LocationName.strawberry_3: [[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_5: [[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_8: [[ItemName.traffic_block],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_10: [[ItemName.air_dash],
[ItemName.climb]],
LocationName.strawberry_11: [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump]],
LocationName.strawberry_12: [[ItemName.feather],
[ItemName.ground_dash],
[ItemName.air_dash]],
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
[ItemName.air_dash, ItemName.climb]],
[ItemName.air_dash, ItemName.climb],
[ItemName.double_dash_refill, ItemName.air_dash]],
LocationName.strawberry_15: [[ItemName.feather],
[ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
@@ -287,42 +107,94 @@ location_hard_moves_logic: Dict[str, List[List[str]]] = {
[ItemName.cassette, ItemName.feather, ItemName.climb]],
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.air_dash, ItemName.climb]],
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
[ItemName.breakables, ItemName.air_dash]],
}
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
region_standard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
(RegionName.intro_islands, RegionName.granny_island): [[ItemName.ground_dash],
[ItemName.air_dash],
[ItemName.skid_jump],
[ItemName.climb]],
(RegionName.granny_island, RegionName.highway_island): [[ItemName.air_dash, ItemName.dash_refill]],
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.climb, ItemName.dash_refill]],
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill]],
(RegionName.highway_island, RegionName.granny_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.dash_refill]],
(RegionName.highway_island, RegionName.ne_feathers_island): [[ItemName.feather]],
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.cannot_access]],
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block]],
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.climb, ItemName.air_dash, ItemName.feather]],
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.air_dash, ItemName.traffic_block, ItemName.double_dash_refill]],
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.double_dash_refill]],
(RegionName.badeline_tower_lower, RegionName.se_house_island): [[ItemName.cannot_access]],
(RegionName.badeline_tower_lower, RegionName.ne_feathers_island): [[ItemName.air_dash, ItemName.breakables, ItemName.feather]],
(RegionName.badeline_tower_lower, RegionName.granny_island): [[ItemName.cannot_access]],
(RegionName.badeline_tower_lower, RegionName.badeline_tower_upper): [[ItemName.cannot_access]],
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
(RegionName.badeline_tower_upper, RegionName.se_house_island): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.ne_feathers_island): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.granny_island): [[ItemName.dash_refill]],
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
}
region_hard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.double_dash_refill], [ItemName.ground_dash]],
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.air_dash, ItemName.ground_dash]],
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.ground_dash]],
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash], [ItemName.skid_jump]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.feather]],
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.traffic_block]],
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.traffic_block],
[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.skid_jump],
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.traffic_block],
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.skid_jump]],
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
}
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
if loc not in world.active_logic_mapping:
return True
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
return False
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
return False
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
if region_connection not in world.active_region_logic_mapping:
return True
for possible_access in world.goal_logic_mapping:
for possible_access in world.active_region_logic_mapping[region_connection]:
if state.has_all(possible_access, world.player):
return True
return False
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
return False
goal_region: Region = world.multiworld.get_region(RegionName.badeline_island, world.player)
return state.can_reach(goal_region)
def connect_region(world: Celeste64World, region: Region, dest_regions: List[str]):
rules: Dict[str, Callable[[CollectionState], bool]] = {}
for dest_region in dest_regions:
region_connection: Tuple[str] = (region.name, dest_region)
rules[dest_region] = lambda state, region_connection=region_connection: region_connection_rule(state, world, region_connection)
region.add_exits(dest_regions, rules)

View File

@@ -1,13 +1,15 @@
from copy import deepcopy
from typing import Dict, List
from typing import Dict, List, Tuple
from BaseClasses import ItemClassification, Location, Region, Tutorial
from worlds.AutoWorld import WebWorld, World
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table,\
checkpoint_item_data_table, item_table
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
sign_location_data_table, car_location_data_table, location_table
sign_location_data_table, car_location_data_table, checkpoint_location_data_table,\
location_table
from .Names import ItemName, LocationName
from .Options import Celeste64Options, celeste_64_option_groups
from .Options import Celeste64Options, celeste_64_option_groups, resolve_options
class Celeste64WebWorld(WebWorld):
@@ -42,8 +44,15 @@ class Celeste64World(World):
# Instance Data
strawberries_required: int
active_logic_mapping: Dict[str, List[List[str]]]
goal_logic_mapping: Dict[str, List[List[str]]]
active_region_logic_mapping: Dict[Tuple[str], List[List[str]]]
madeline_one_dash_hair_color: int
madeline_two_dash_hair_color: int
madeline_no_dash_hair_color: int
madeline_feather_hair_color: int
def generate_early(self) -> None:
resolve_options(self)
def create_item(self, name: str) -> Celeste64Item:
# Only make required amount of strawberries be Progression
@@ -76,25 +85,49 @@ class Celeste64World(World):
for name in unlockable_item_data_table.keys()
if name not in self.options.start_inventory]
if self.options.move_shuffle:
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
chosen_start_item: str = ""
if self.options.move_shuffle:
if self.options.logic_difficulty == "standard":
# If the start_inventory already includes a move, don't worry about giving it one
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
chosen_start_move = self.random.choice(move_items_for_itempool)
move_items_for_itempool.remove(chosen_start_move)
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
if name != ItemName.skid_jump]
if self.options.checkpointsanity:
possible_unwalls.extend([name for name in checkpoint_item_data_table.keys()
if name != ItemName.checkpoint_1 and name != ItemName.checkpoint_10])
# If the start_inventory already includes a move or checkpoint, don't worry about giving it one
if not [item for item in possible_unwalls if item in self.multiworld.precollected_items[self.player]]:
chosen_start_item = self.random.choice(possible_unwalls)
if self.options.carsanity:
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
intro_car_loc.place_locked_item(self.create_item(chosen_start_item))
location_count -= 1
else:
self.multiworld.push_precollected(self.create_item(chosen_start_move))
self.multiworld.push_precollected(self.create_item(chosen_start_item))
item_pool += [self.create_item(name)
for name in move_items_for_itempool
if name not in self.options.start_inventory]
for name in move_item_data_table.keys()
if name not in self.multiworld.precollected_items[self.player]
and name != chosen_start_item]
else:
for start_move in move_item_data_table.keys():
self.multiworld.push_precollected(self.create_item(start_move))
if self.options.checkpointsanity:
location_count += 9
goal_checkpoint_loc: Location = self.multiworld.get_location(LocationName.checkpoint_10, self.player)
goal_checkpoint_loc.place_locked_item(self.create_item(ItemName.checkpoint_10))
item_pool += [self.create_item(name)
for name in checkpoint_item_data_table.keys()
if name not in self.multiworld.precollected_items[self.player]
and name != ItemName.checkpoint_10
and name != chosen_start_item]
else:
for item_name in checkpoint_item_data_table.keys():
checkpoint_loc: Location = self.multiworld.get_location(item_name, self.player)
checkpoint_loc.place_locked_item(self.create_item(item_name))
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
@@ -140,18 +173,23 @@ class Celeste64World(World):
if location_data.region == region_name
}, Celeste64Location)
region.add_exits(region_data_table[region_name].connecting_regions)
region.add_locations({
location_name: location_data.address for location_name, location_data in checkpoint_location_data_table.items()
if location_data.region == region_name
}, Celeste64Location)
from .Rules import connect_region
connect_region(self, region, region_data_table[region_name].connecting_regions)
# Have to do this here because of other games using State in a way that's bad
from .Rules import set_rules
set_rules(self)
def get_filler_item_name(self) -> str:
return ItemName.raspberry
def set_rules(self) -> None:
from .Rules import set_rules
set_rules(self)
def fill_slot_data(self):
return {
"death_link": self.options.death_link.value,
@@ -161,6 +199,11 @@ class Celeste64World(World):
"friendsanity": self.options.friendsanity.value,
"signsanity": self.options.signsanity.value,
"carsanity": self.options.carsanity.value,
"checkpointsanity": self.options.checkpointsanity.value,
"madeline_one_dash_hair_color": self.madeline_one_dash_hair_color,
"madeline_two_dash_hair_color": self.madeline_two_dash_hair_color,
"madeline_no_dash_hair_color": self.madeline_no_dash_hair_color,
"madeline_feather_hair_color": self.madeline_feather_hair_color,
"badeline_chaser_source": self.options.badeline_chaser_source.value,
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,

View File

@@ -19,8 +19,8 @@ from worlds.AutoWorld import WebWorld, World
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
get_start_inventory_data
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
CVCOTM_VC_US_HASH
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH
# CVCOTM_VC_US_HASH
from .client import CastlevaniaCotMClient
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
"""File name of the Castlevania CotM US rom"""
copy_to = "Castlevania - Circle of the Moon (USA).gba"
description = "Castlevania CotM (US) ROM File"
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
# md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
rom_file: RomFile = RomFile(RomFile.copy_to)

View File

@@ -153,11 +153,10 @@ Advance Collection ROM; most notably the fact that the audio does not function w
which is currently a requirement to connect to a multiworld. This happens because all audio code was stripped
from the ROM, and all sound is instead played by the collection through external means.
For this reason, it is most recommended to obtain the ROM by dumping it from an original cartridge of the game that you legally own.
Though, the Advance Collection *can* still technically be an option if you cannot do that and don't mind the lack of sound.
The Wii U Virtual Console version does not work due to changes in the code in that version.
The Wii U Virtual Console version is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
game that you legally own. However, the Advance Collection *is* an option if you cannot do that and don't mind the lack of sound.
Regardless of which released ROM you intend to try playing with, the US version of the game is required.

View File

@@ -4,7 +4,7 @@
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
- A Castlevania: Circle of the Moon ROM of the US version specifically. The Archipelago community cannot provide this.
The Castlevania Advance Collection ROM can technically be used, but it has no audio. The Wii U Virtual Console ROM is untested.
The Castlevania Advance Collection ROM can be used, but it has no audio. The Wii U Virtual Console ROM does not work.
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
### Configuring BizHawk

View File

@@ -22,11 +22,9 @@ if TYPE_CHECKING:
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection ROM
CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
# CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
# NOTE: The Wii U VC version is untested as of when this comment was written. I am only including its hash in case it
# does work. If someone who has it can confirm it does indeed work, this comment should be removed. If it doesn't, the
# hash should be removed in addition. See the Game Page for more information about supported versions.
# The Wii U VC version is not currently supported. See the Game Page for more info.
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
# hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
patch_file_ending: str = ".apcvcotm"
result_file_ending: str = ".gba"
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA."
"Get the correct game and version, then dump it.")
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)

View File

@@ -530,7 +530,7 @@ server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
def launch():
import colorama
global executable, server_settings, server_args
colorama.init()
colorama.just_fix_windows_console()
if server_settings:
server_settings = os.path.abspath(server_settings)

View File

@@ -295,6 +295,6 @@ def launch():
parser = get_base_parser(description="KH1 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -981,6 +981,6 @@ def launch():
parser = get_base_parser(description="KH2 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -107,6 +107,7 @@ SYNONYMS = {
'JUMP': 'FEATHER',
'PLUME': 'FEATHER',
'WING': 'FEATHER',
"QUILL": "FEATHER",
# SHOVEL
'DIG': 'SHOVEL',
@@ -343,6 +344,8 @@ SYNONYMS = {
# TRADING_ITEM_LETTER
'CARD': 'TRADING_ITEM_LETTER',
'MESSAGE': 'TRADING_ITEM_LETTER',
"TICKET": 'TRADING_ITEM_LETTER',
"PASS": 'TRADING_ITEM_LETTER',
# TRADING_ITEM_BROOM
'SWEEP': 'TRADING_ITEM_BROOM',
@@ -365,6 +368,8 @@ SYNONYMS = {
'MIRROR': 'TRADING_ITEM_MAGNIFYING_GLASS',
'SCOPE': 'TRADING_ITEM_MAGNIFYING_GLASS',
'XRAY': 'TRADING_ITEM_MAGNIFYING_GLASS',
"DETECTOR": 'TRADING_ITEM_MAGNIFYING_GLASS',
"ITEMFINDER": 'TRADING_ITEM_MAGNIFYING_GLASS',
# PIECE_OF_POWER
'TRIANGLE': 'PIECE_OF_POWER',
@@ -378,6 +383,7 @@ PHRASES = {
'BOSS KEY': 'NIGHTMARE_KEY',
'HEART PIECE': 'HEART_PIECE',
'PIECE OF HEART': 'HEART_PIECE',
"ROCK SMASH": 'BOMB',
}
# All following will only be used to match items for the specific game.
@@ -404,6 +410,16 @@ GAME_SPECIFIC_PHRASES = {
'Ocarina of Time': {
'COJIRO': 'ROOSTER',
"Goron Tunic": "RED_TUNIC",
"Zora Tunic": "BLUE_TUNIC",
"Wallet": "MAGIC_POWDER",
"Medallion": "PIECE_OF_POWER",
"Kokiri Emerald": "RUPEES_500",
"Goron Ruby": "RUPEES_500",
"Zora Sapphire": "RUPEES_500",
"Dins Fire": "MAGIC_ROD", # Fire shield
"Nayrus Love": "MAGIC_ROD", # Protective barrier
"Farores Wind": "MAGIC_ROD", # Create/use warp point in dungeons
},
'SMZ3': {
@@ -417,10 +433,14 @@ GAME_SPECIFIC_PHRASES = {
'Sonic Adventure 2 Battle': {
'CHAOS EMERALD': 'PIECE_OF_POWER',
"Rings": "RUPEES_20", # This should only affect filler Rings currency, not Flame Ring upgrade
"Grapes": "TRADING_ITEM_PINEAPPLE",
"Pick Nails": "SHOVEL", # Digging upgrade
},
'Super Mario 64': {
'POWER STAR': 'PIECE_OF_POWER',
"Key": "NIGHTMARE_KEY" # Affect 2nd Floor / Basement / Progressive keys
},
'Super Mario World': {
@@ -528,4 +548,336 @@ GAME_SPECIFIC_PHRASES = {
'2500 Tokens': 'RUPEES_500',
'5000 Tokens': 'RUPEES_500',
},
"Donkey Kong Country 3": {
"Flupperius Petallus Pongus": "TRADING_ITEM_HIBISCUS", # It's a flower in the game
"Banana Bird": "ROOSTER", # Made sure this is a BIRD, not a BANANA
},
"Pokemon Red and Blue": {
# Key Items
"Old Amber": "STONE_BEAK", # Aerodactyl's fossil should still be a fossil
"Coin Case": "MAGIC_POWDER", # This shouldn't spawn as RUPEES
"Bike Voucher": "TRADING_ITEM_LETTER",
"Oak's Parcel": "TRADING_ITEM_LETTER",
# Drinks always get converted to MEDICINE
"Soda Pop": "MEDICINE",
"Fresh Water": "MEDICINE",
# Consumables
"Elixir": "MEDICINE",
"Ether": "MEDICINE",
"Antidote": "MEDICINE",
"Awakening": "MEDICINE",
"Burn Heal": "MEDICINE",
"Ice Heal": "MEDICINE",
"Paralyze Heal": "MEDICINE",
"Full Heal": "MEDICINE",
"Full Restore": "MEDICINE",
},
"Pokemon Emerald": {
"Coin Case": "MAGIC_POWDER", # This shouldn't spawn as RUPEES
# Drinks always get converted to MEDICINE
"Soda Pop": "MEDICINE",
"Fresh Water": "MEDICINE",
# Consumables
"Elixir": "MEDICINE",
"Ether": "MEDICINE",
"Antidote": "MEDICINE",
"Awakening": "MEDICINE",
"Burn Heal": "MEDICINE",
"Ice Heal": "MEDICINE",
"Paralyze Heal": "MEDICINE",
"Full Heal": "MEDICINE",
"Full Restore": "MEDICINE",
"Nanab Berry": "TRADING_ITEM_BANANAS", # Special exception for Nanab Berry, which look like bananas
"Berry": "TRADING_ITEM_PINEAPPLE",
"Mail": "TRADING_ITEM_LETTER", # Snail mail, not chain mail
},
"Mario & Luigi Superstar Saga": {
# Key Items
"Peach's Extra Dress": "RED_TUNIC",
"Peasley's Rose": "TRADING_ITEM_HIBISCUS",
"Beanstar": "PIECE_OF_POWER", # Hits both Fake Beanstar and pieces of the real Beanstar, hopefully
"Beanstone": "RUPEES_500", # They're gemstones
"Firebrand": "POWER_BRACELET", # Magic power that affects Mario/Luigi's hands, either this or MAGIC_ROD would be okay
"Thunderhand": "POWER_BRACELET", # Ditto
# 1-UP Super fix
"1-UP Super": "TOADSTOOL",
# Drinks --> medicine
# Syrup bottles
"Syrup": "MEDICINE",
# Coffee blends
"Hoolumbian": "MEDICINE",
"Chuckoccino": "MEDICINE",
"Teeheespresso": "MEDICINE",
"Blend": "MEDICINE", # for all coffee blends
# Secret Scrolls --> MESSAGE
"Secret Scroll": "TRADING_ITEM_LETTER",
# Goblets --> MEDICINE
"Goblet": "MEDICINE",
# Pearl Beans --> Fruit
"Pearl Bean": 'TRADING_ITEM_PINEAPPLE',
# Bros. Armor --> Blue Tunic
"Pants": "BLUE_TUNIC",
"Jeans": "BLUE_TUNIC",
"Trousers": "BLUE_TUNIC",
"Slacks": "BLUE_TUNIC",
"Casual Coral": "BLUE_TUNIC",
"Shroom Bells": "BLUE_TUNIC",
# Badges --> Ribbon
"Badge": "TRADING_ITEM_RIBBON",
"Soulful Bros.": "TRADING_ITEM_RIBBON",
"Bros. Rock": "TRADING_ITEM_RIBBON",
# Misc. Beans --> Acorns
"Hoo Bean": "GUARDIAN_ACORN", # Beans and nuts are similar enough, right?
"Chuckle Bean": "GUARDIAN_ACORN",
"Hee Bean": "GUARDIAN_ACORN",
"Woo Bean": "GUARDIAN_ACORN",
},
"DOOM 1993": {
"Keycard": "KEY",
"Computer area map": "MAP",
"Box of": "SINGLE_ARROW", # bullets, rockets, or shotgun shells
"Energy cell pack": "SINGLE_ARROW",
"Chainsaw": "SWORD",
"Medikit": "MEDICINE",
"Skull key": "NIGHTMARE_KEY",
},
"DOOM II": {
"Keycard": "KEY",
"Computer area map": "MAP",
"Box of": "SINGLE_ARROW", # bullets, rockets, or shotgun shells
"Energy cell pack": "SINGLE_ARROW",
"Chainsaw": "SWORD",
"Medikit": "MEDICINE",
"Skull key": "NIGHTMARE_KEY",
},
"Inscryption": {
"Extra Candle": "HEART_CONTAINER", # Candles act as extra health
"Magnificus Eye": "TRADING_ITEM_MAGNIFYING_GLASS", # Needed to see hidden drawings / messages
"Monocle": "TRADING_ITEM_MAGNIFYING_GLASS", # Ditto
"Pile Of Meat": "TRADING_ITEM_DOG_FOOD",
"Angler Hook": "TRADING_ITEM_FISHING_HOOK", # Good fish.
"Currency": "RUPEES_20",
},
"Minecraft": {
"Progressive Weapons": "SWORD",
"Progressive Tools": "SHOVEL",
"Archery": "BOW",
"Emerald": "RUPEES_20",
"Brewing": "MEDICINE",
"Spyglass": 'TRADING_ITEM_MAGNIFYING_GLASS',
"Porkchop": "TRADING_ITEM_DOG_FOOD"
},
"VVVVVV": {
"Trinket": "PIECE_OF_POWER",
},
"A Hat in Time": {
"Time Piece": "PIECE_OF_POWER",
"Metro Ticket": "TRADING_ITEM_LETTER",
"Snatcher's Contract": "TRADING_ITEM_LETTER",
"Pon": "RUPEES_20",
},
"Kingdom Hearts 2": {
# Goal items / Collectibles
"Proof of": "PIECE_OF_POWER",
"Lucky Emblem": "PIECE_OF_POWER",
"Secret Ansem's Report": "TRADING_ITEM_LETTER",
# Sora Keyblades
"Bond of Flame": "SWORD",
"Circle of Life": "SWORD",
"Decisive Pumpkin": "SWORD",
"Fatal Crest": "SWORD",
"Fenrir": "SWORD",
"Follow the Wind": "SWORD",
"Guardian Soul": "SWORD",
"Gull Wing": "SWORD",
"Hero's Crest": "SWORD",
"Hidden Dragon": "SWORD",
"Monochrome": "SWORD",
"Mysterious Abyss": "SWORD",
"Oathkeeper": "SWORD",
"Oblivion": "SWORD",
"Photon Debugger": "SWORD",
"Pureblood": "SWORD",
"Rumbling Rose": "SWORD",
"Sleeping Lion": "SWORD",
"Star Seeker": "SWORD",
"Sweet Memories": "SWORD",
"Two Become One": "SWORD",
"Ultima Weapon": "SWORD",
"Winner's Proof": "SWORD",
"Wishing Lamp": "SWORD",
# Donald Staves
"Centurion+": "MAGIC_ROD",
"Nobody Lance": "MAGIC_ROD",
"Precious Mushroom": "MAGIC_ROD",
"Precious Mushroom+": "MAGIC_ROD",
"Premium Mushroom": "MAGIC_ROD",
"Rising Dragon": "MAGIC_ROD",
"Save The Queen+": "MAGIC_ROD",
"Shaman's Relic": "MAGIC_ROD",
"Victory Bell": "MAGIC_ROD",
# Goofy Shields
"Akashic Record": "SHIELD",
"Frozen Pride+": "SHIELD",
"Majestic Mushroom": "SHIELD",
"Majestic Mushroom+": "SHIELD",
"Nobody Guard": "SHIELD",
"Ogre Shield": "SHIELD",
"Save The King+": "SHIELD",
"Ultimate Mushroom": "SHIELD",
# Accessories as RIBBON
"Star Charm": "TRADING_ITEM_RIBBON",
"Ring": "TRADING_ITEM_RIBBON",
"Earring": "TRADING_ITEM_RIBBON",
"Shadow Archive": "TRADING_ITEM_RIBBON",
"Shadow Archive+": "TRADING_ITEM_RIBBON",
"Full Bloom": "TRADING_ITEM_RIBBON",
"Full Bloom+": "TRADING_ITEM_RIBBON",
# Armor as BLUE_TUNIC
"Bandanna": "BLUE_TUNIC",
"Belt": "BLUE_TUNIC",
"Band": "BLUE_TUNIC",
"Bangle": "BLUE_TUNIC",
"Armlet": "BLUE_TUNIC",
"Trinket": "BLUE_TUNIC",
"Charm": "BLUE_TUNIC",
"Anklet": "BLUE_TUNIC",
"Chain": "BLUE_TUNIC",
"Acrisius": "BLUE_TUNIC",
"Ribbon": "BLUE_TUNIC",
# Magic
"Element": "MAGIC_ROD",
# Other
"Munny Pouch": "MAGIC_POWDER",
"Ether": "MEDICINE",
"Elixir": "MEDICINE",
"Megalixir": "MEDICINE",
},
"Kingdom Hearts": {
# Goal/collectible items
"Ansem's Report": "TRADING_ITEM_LETTER",
# Dalmatian puppies
"Puppy": "BOWWOW",
"Puppies": "BOWWOW",
# Sora Keyblades
"Jungle King": "SWORD",
"Three Wishes": "SWORD",
"Fairy Harp": "SWORD",
"Pumpkinhead": "SWORD",
"Crabclaw": "SWORD",
"Divine Rose": "SWORD",
"Spellbinder": "SWORD",
"Olympia": "SWORD",
"Lionheart": "SWORD",
"Metal Chocobo": "SWORD",
"Oathkeeper": "SWORD",
"Oblivion": "SWORD",
"Lady Luck": "SWORD",
"Wishing Star": "SWORD",
"Ultima Weapon": "SWORD",
"Diamond Dust": "SWORD",
"One-Winged Angel": "SWORD",
# Donald Staves
"Morning Star": "MAGIC_ROD",
"Shooting Star": "MAGIC_ROD",
"Warhammer": "MAGIC_ROD",
"Silver Mallet": "MAGIC_ROD",
"Grand Mallet": "MAGIC_ROD",
"Lord Fortune": "MAGIC_ROD",
"Violetta": "MAGIC_ROD",
"Save the Queen": "MAGIC_ROD",
"Wizard's Relic": "MAGIC_ROD",
"Meteor Strike": "MAGIC_ROD",
"Fantasista": "MAGIC_ROD",
# Goofy Shields
"Smasher": "SHIELD",
"Gigas Fist": "SHIELD",
"Save the King": "SHIELD",
"Defender": "SHIELD",
"Seven Elements": "SHIELD",
# Magic
"Progressive Fire": "MAGIC_ROD",
"Progressive Blizzard": "MAGIC_ROD",
"Progressive Thunder": "MAGIC_ROD",
"Progressive Cure": "MAGIC_ROD",
"Progressive Gravity": "MAGIC_ROD",
"Progressive Stop": "MAGIC_ROD",
"Progressive Aero": "MAGIC_ROD",
# Accessories / armor (Let's go with BLUE_TUNIC for these, these items are closer to RPG armor anyways)
"Chain": "BLUE_TUNIC",
"Ring": "BLUE_TUNIC",
"Band": "BLUE_TUNIC",
"Three Stars": "BLUE_TUNIC",
"Stud": "BLUE_TUNIC",
"Earring": "BLUE_TUNIC",
"Bangle": "BLUE_TUNIC",
"Armlet": "BLUE_TUNIC",
"Moogle Badge": "BLUE_TUNIC",
"Cosmic Arts": "BLUE_TUNIC",
"Heartguard": "BLUE_TUNIC",
"Crystal Crown": "BLUE_TUNIC",
"Ribbon": "BLUE_TUNIC",
"Brave Warrior": "BLUE_TUNIC",
"Ifrit's Horn": "BLUE_TUNIC",
"White Fang": "BLUE_TUNIC",
"Ray of Light": "BLUE_TUNIC",
"Circlet": "BLUE_TUNIC",
"Raven's Claw": "BLUE_TUNIC",
"Omega Arts": "BLUE_TUNIC",
"Royal Crown": "BLUE_TUNIC",
"Prime Cap": "BLUE_TUNIC",
"Belt": "BLUE_TUNIC",
"EXP Bracelet": "BLUE_TUNIC",
"EXP Necklace": "BLUE_TUNIC",
# Other
"Glide": "FEATHER",
"Ether": "MEDICINE",
"Elixir": "MEDICINE",
"Megalixir": "MEDICINE",
}
}

View File

@@ -444,7 +444,7 @@ class LinksAwakeningWorld(World):
phrases.update(ItemIconGuessing.GAME_SPECIFIC_PHRASES[foreign_game])
for phrase, icon in phrases.items():
if phrase in uppered:
if phrase.upper() in uppered:
return icon
# pattern for breaking down camelCase, also separates out digits
pattern = re.compile(r"(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=\d)")

View File

@@ -100,14 +100,15 @@ class OOTWeb(WebWorld):
["Edos"]
)
setup_es = Tutorial(
setup.tutorial_name,
setup.description,
"Español",
"setup_es.md",
"setup/es",
setup.authors
)
# Very out of date, requires updating to match current
# setup_es = Tutorial(
# setup.tutorial_name,
# setup.description,
# "Español",
# "setup_es.md",
# "setup/es",
# setup.authors
# )
setup_fr = Tutorial(
setup.tutorial_name,
@@ -127,7 +128,7 @@ class OOTWeb(WebWorld):
["Held_der_Zeit"]
)
tutorials = [setup, setup_es, setup_fr, setup_de]
tutorials = [setup, setup_fr, setup_de]
option_groups = oot_option_groups

View File

@@ -111,6 +111,7 @@ class PokemonRedBlueWorld(World):
self.dexsanity_table = []
self.trainersanity_table = []
self.local_locs = []
self.pc_item = None
@classmethod
def stage_assert_generate(cls, multiworld: MultiWorld):
@@ -289,7 +290,9 @@ class PokemonRedBlueWorld(World):
multiworld.random.shuffle(itempool)
unplaced_items = []
for i, item in enumerate(itempool):
if item.player == loc.player and loc.can_fill(multiworld.state, item, False):
if ((item.player == loc.player or (item.player in multiworld.groups
and loc.player in multiworld.groups[item.player]["players"]))
and loc.can_fill(multiworld.state, item, False)):
if item.advancement:
pool = progitempool
elif item.useful:
@@ -308,8 +311,6 @@ class PokemonRedBlueWorld(World):
break
else:
unplaced_items.append(item)
else:
raise FillError(f"Pokemon Red and Blue local item fill failed for player {loc.player}: could not place {item.name}")
progitempool += [item for item in unplaced_items if item.advancement]
usefulitempool += [item for item in unplaced_items if item.useful]
filleritempool += [item for item in unplaced_items if (not item.advancement) and (not item.useful)]
@@ -446,15 +447,12 @@ class PokemonRedBlueWorld(World):
if loc.item is None:
locs.add(loc)
if not self.options.key_items_only:
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
if loc.item is None:
locs.add(loc)
for loc in sorted(locs):
if loc.name in self.options.priority_locations.value:
add_item_rule(loc, lambda i: i.advancement)
add_item_rule(loc, lambda i: i.player == self.player)
add_item_rule(loc, lambda i: i.player == self.player
or (i.player in self.multiworld.groups
and self.player in self.multiworld.groups[i.player]["players"]))
if self.options.old_man == "early_parcel" and loc.name != "Player's House 2F - Player's PC":
add_item_rule(loc, lambda i: i.name != "Oak's Parcel")
@@ -520,6 +518,14 @@ class PokemonRedBlueWorld(World):
else:
raise Exception("Failed to remove corresponding item while deleting unreachable Dexsanity location")
if not self.options.key_items_only:
loc = self.multiworld.get_location("Player's House 2F - Player's PC", self.player)
# Absolutely cannot have another player's item
if loc.item is not None and loc.item.player != self.player:
self.multiworld.itempool.append(loc.item)
loc.item = None
loc.place_locked_item(self.pc_item)
@classmethod
def stage_post_fill(cls, multiworld):
# Convert all but one of each instance of a wild Pokemon to useful classification.

View File

@@ -1579,6 +1579,18 @@ def create_regions(world):
world.item_pool.append(item)
world.random.shuffle(world.item_pool)
if not world.options.key_items_only:
if "Player's House 2F - Player's PC" in world.options.exclude_locations:
acceptable_item = lambda item: item.excludable
elif "Player's House 2F - Player's PC" in world.options.priority_locations:
acceptable_item = lambda item: item.advancement
else:
acceptable_item = lambda item: True
for i, item in enumerate(world.item_pool):
if acceptable_item(item):
world.pc_item = world.item_pool.pop(i)
break
advancement_items = [item.name for item in world.item_pool if item.advancement] \
+ [item.name for item in world.multiworld.precollected_items[world.player] if
item.advancement]

View File

@@ -1625,6 +1625,6 @@ def get_location_offset(mission_id):
def launch():
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -1,10 +1,13 @@
# StarCraft 2
## Game page in other languages:
* [Français](/games/Starcraft%202/info/fr)
## What does randomization do to this game?
### Items and locations
The following unlocks are randomized as items:
1. Your ability to build any non-worker unit.
2. Unit specific upgrades including some combinations not available in the vanilla campaigns, such as both strain
@@ -34,18 +37,28 @@ When you receive items, they will immediately become available, even during a mi
notified via a text box in the top-right corner of the game screen.
Item unlocks are also logged in the Archipelago client.
### Mission order
The missions and the order in which they need to be completed, referred to as the mission order, can also be randomized.
The four StarCraft 2 campaigns can be used to populate the mission order.
Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
The default mission order follows the structure of the selected campaigns but several other options are available,
e.g., blitz, grid, etc.
Missions are launched through the StarCraft 2 Archipelago client, through the StarCraft 2 Launcher tab.
The between mission segments on the Hyperion, the Leviathan, and the Spear of Adun are not included.
Additionally, metaprogression currencies such as credits and Solarite are not used.
Available missions are in blue; missions where all locations were collected are in white.
If you move your mouse over a mission, the uncollected locations will be displayed, categorized by type.
Unavailable missions are in grey; their requirements will also be shown there.
## What is the goal of this game when randomized?
The goal is to beat the final mission in the mission order.
The yaml configuration file controls the mission order (e.g. blitz, grid, etc.), which combination of the four
StarCraft 2 campaigns can be used to populate the mission order and how missions are shuffled.
The yaml configuration file controls the mission order, which combination of the four StarCraft 2 campaigns can be
used, and how missions are shuffled.
Since the first two options determine the number of missions in a StarCraft 2 world, they can be used to customize the
expected time to complete the world.
Note that the evolution missions from Heart of the Swarm are not included in the randomizer.
## What non-randomized changes are there from vanilla StarCraft 2?
@@ -78,9 +91,7 @@ Will overwrite existing files
* `/game_speed [game_speed]` Overrides the game speed for the world
* Options: default, slower, slow, normal, fast, faster
* `/color [faction] [color]` Changes your color for one of your playable factions.
* Faction options: raynor, kerrigan, primal, protoss, nova
* Color options: white, red, blue, teal, purple, yellow, orange, green, lightpink, violet, lightgrey, darkgreen,
brown, lightgreen, darkgrey, pink, rainbow, random, default
* Run without arguments to list all factions and colors that are available.
* `/option [option_name] [option_value]` Sets an option normally controlled by your yaml after generation.
* Run without arguments to list all options.
* Options pertain to automatic cutscene skipping, Kerrigan presence, Spear of Adun presence, starting resource
@@ -100,6 +111,19 @@ Additionally, upgrades are grouped beneath their corresponding units or building
A filter parameter can be provided, e.g., `/received Thor`, to limit the number of items shown.
Every item whose name, race, or group name contains the provided parameter will be shown.
## Particularities in a multiworld
### Collect on goal completion
One of the default options of multiworlds is that once a world has achieved its goal, it collects its items from all
other worlds.
If you do not want this to happen, you should ask the person generating the multiworld to set the `Collect Permission`
option to something else, e.g., manual.
If the generation is not done via the website, the person that does the generation should modify the `collect_mode`
option in their `host.yaml` file prior to generation.
If the multiworld has already been generated, the host can use the command `/option collect_mode [value]` to change
this option.
## Known issues
- StarCraft 2 Archipelago does not support loading a saved game.
@@ -108,3 +132,7 @@ For this reason, it is recommended to play on a difficulty level lower than what
To restart a mission, use the StarCraft 2 Client.
- A crash report is often generated when a mission is closed.
This does not affect the game and can be ignored.
- Currently, the StarCraft 2 client uses the Victory locations to determine which missions have been completed.
As a result, the Archipelago collect feature can sometime grant access to missions that are connected to a mission that
you did not complete.

View File

@@ -2,6 +2,8 @@
## Quel est l'effet de la *randomization* sur ce jeu ?
### *Items* et *locations*
Les éléments qui suivent sont les *items* qui sont *randomized* et qui doivent être débloqués pour être utilisés dans
le jeu:
1. La capacité de produire des unités, excepté les drones/probes/scv.
@@ -37,21 +39,33 @@ Quand vous recevez un *item*, il devient immédiatement disponible, même pendan
la boîte de texte situé dans le coin en haut à droite de *StarCraft 2*.
L'acquisition d'un *item* est aussi indiquée dans le client d'Archipelago.
### *Mission order*
Les missions et l'ordre dans lequel elles doivent être complétées, dénoté *mission order*, peuvent également être
*randomized*.
Les quatre campagnes de *StarCraft 2* peuvent être utilisées pour remplir le *mission order*.
Notez que les missions d'évolution de *Heart of the Swarm* ne sont pas incluses dans le *randomizer*.
Par défaut, le *mission order* suit la structure des campagnes sélectionnées, mais plusieurs autres options sont
disponibles, comme *blitz*, *grid*, etc.
Les missions peuvent être lancées par le client *StarCraft 2 Archipelago*, via l'interface graphique de l'onglet
*StarCraft 2 Launcher*.
Les segments qui se passent sur l'*Hyperion*, un Léviathan et la *Spear of Adun* ne sont pas inclus.
De plus, les points de progression tels que les crédits ou la Solarite ne sont pas utilisés dans *StarCraft 2
De plus, les points de progression, tels que les crédits ou la Solarite, ne sont pas utilisés dans *StarCraft 2
Archipelago*.
Les missions accessibles ont leur nom en bleu, tandis que celles où toutes les *locations* ont été collectées
apparaissent en blanc.
En plaçant votre souris sur une mission, les *locations* non collectées saffichent, classées par catégorie.
Les missions qui ne sont pas accessibles ont leur nom en gris et leurs prérequis seront également affichés à cet endroit.
## Quel est le but de ce jeu quand il est *randomized*?
Le but est de réussir la mission finale du *mission order* (e.g. *blitz*, *grid*, etc.).
Le fichier de configuration yaml permet de spécifier le *mission order*, lesquelles des quatre campagnes de
*StarCraft 2* peuvent être utilisées pour remplir le *mission order* et comment les missions sont distribuées dans le
*mission order*.
Le fichier de configuration yaml permet de spécifier le *mission order*, quelle combinaison des quatre campagnes de
*StarCraft 2* peuvent être utilisée et comment les missions sont distribuées dans le *mission order*.
Étant donné que les deux premières options déterminent le nombre de missions dans un monde de *StarCraft 2*, elles
peuvent être utilisées pour moduler le temps nécessaire pour terminer le monde.
Notez que les missions d'évolution de Heart of the Swarm ne sont pas incluses dans le *randomizer*.
## Quelles sont les modifications non aléatoires comparativement à la version de base de *StarCraft 2*
@@ -89,9 +103,7 @@ Les fichiers existants vont être écrasés.
* `/game_speed [game_speed]` Remplace la vitesse du jeu pour le monde.
* Les options sont *default*, *slower*, *slow*, *normal*, *fast*, and *faster*.
* `/color [faction] [color]` Remplace la couleur d'une des *factions* qui est jouable.
* Les options de *faction*: raynor, kerrigan, primal, protoss, nova.
* Les options de couleur: *white*, *red*, *blue*, *teal*, *purple*, *yellow*, *orange*, *green*, *lightpink*,
*violet*, *lightgrey*, *darkgreen*, *brown*, *lightgreen*, *darkgrey*, *pink*, *rainbow*, *random*, *default*.
* Si la commande est lancée sans option, la liste des *factions* et des couleurs disponibles sera affichée.
* `/option [option_name] [option_value]` Permet de changer un option normalement définit dans le *yaml*.
* Si la commande est lancée sans option, la liste des options qui sont modifiables va être affichée.
* Les options qui peuvent être changées avec cette commande incluent sauter les cinématiques automatiquement, la
@@ -114,6 +126,19 @@ De plus, les améliorations sont regroupées sous leurs unités/bâtiments corre
Un paramètre de filtrage peut aussi être fourni, e.g., `/received Thor`, pour limiter le nombre d'*items* affichés.
Tous les *items* dont le nom, la race ou le nom de groupe contient le paramètre fourni seront affichés.
## Particularités dans un multiworld
### *Collect on goal completion*
L'une des options par défaut des *multiworlds* est qu'une fois qu'un monde a atteint son objectif final, il collecte
tous ses *items*, incluant ceux dans les autres mondes.
Si vous ne souhaitez pas que cela se produise, vous devez demander à la personne générant le *multiworld* de changer
l'option *Collect Permission*.
Si la génération n'est pas effectuée via le site web, la personne qui effectue la génération doit modifier l'option
`collect_mode` dans son fichier *host.yaml* avant la génération.
Si le *multiworld* a déjà été généré, l'hôte peut utiliser la commande `/option collect_mode [valeur]` pour modifier
cette option.
## Problèmes connus
- *StarCraft 2 Archipelago* ne supporte pas le chargement d'une sauvegarde.
@@ -123,3 +148,7 @@ normalement à l'aise.
Pour redémarrer une mission, utilisez le client de *StarCraft 2 Archipelago*.
- Un rapport d'erreur est souvent généré lorsqu'une mission est fermée.
Cela n'affecte pas le jeu et peut être ignoré.
- Actuellement, le client de *StarCraft 2* utilise la *location* associée à la victoire d'une mission pour déterminer
si celle-ci a été complétée.
En conséquence, la fonctionnalité *collect* d'*Archipelago* peut rendre accessible des missions connectées à une
mission que vous n'avez pas terminée.

View File

@@ -41,6 +41,7 @@ Remember the name you enter in the options page or in the yaml file, you'll need
Check out [Creating a YAML](/tutorial/Archipelago/setup/en#creating-a-yaml) for more game-agnostic information.
### Common yaml questions
#### How do I know I set my yaml up correctly?
The simplest way to check is to use the website [validator](/check).

View File

@@ -49,6 +49,7 @@ Si vous désirez des informations et/ou instructions générales sur l'utilisati
veuillez consulter [*Creating a YAML*](/tutorial/Archipelago/setup/en#creating-a-yaml).
### Questions récurrentes à propos du fichier *yaml*
#### Comment est-ce que je sais que mon *yaml* est bien défini?
La manière la plus simple de valider votre *yaml* est d'utiliser le

View File

@@ -0,0 +1,33 @@
from dataclasses import dataclass, field
from typing import FrozenSet, List, NamedTuple
# A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement.
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
# {} is an unusable requirement.
# {{}} is an always usable requirement.
WitnessRule = FrozenSet[FrozenSet[str]]
@dataclass
class AreaDefinition:
name: str
regions: List[str] = field(default_factory=list)
@dataclass
class RegionDefinition:
name: str
short_name: str
area: AreaDefinition
logical_entities: List[str] = field(default_factory=list)
physical_entities: List[str] = field(default_factory=list)
class ConnectionDefinition(NamedTuple):
target_region: str
traversal_rule: WitnessRule
@property
def can_be_traversed(self) -> bool:
return bool(self.traversal_rule)

View File

@@ -486,5 +486,5 @@ for key, item in ALL_LOCATIONS_TO_IDS.items():
ALL_LOCATIONS_TO_ID[key] = item
for loc in ALL_LOCATIONS_TO_IDS:
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"]["name"]
area = static_witness_logic.ENTITIES_BY_NAME[loc]["area"].name
AREA_LOCATION_GROUPS.setdefault(area, set()).add(loc)

View File

@@ -1,8 +1,9 @@
from collections import Counter, defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple
from typing import Any, Dict, FrozenSet, List, Optional, Set
from Utils import cache_argsless
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
from .item_definition_classes import (
CATEGORY_NAME_MAPPINGS,
DoorItemDefinition,
@@ -13,7 +14,6 @@ from .item_definition_classes import (
)
from .settings.easter_eggs import EASTER_EGGS
from .utils import (
WitnessRule,
define_new_region,
get_items,
get_sigma_expert_logic,
@@ -21,7 +21,7 @@ from .utils import (
get_umbra_variety_logic,
get_vanilla_logic,
logical_or_witness_rules,
parse_lambda,
parse_witness_rule,
)
@@ -31,10 +31,10 @@ class StaticWitnessLogicObj:
lines = get_sigma_normal_logic()
# All regions with a list of panels in them and the connections to other regions, before logic adjustments
self.ALL_REGIONS_BY_NAME: Dict[str, Dict[str, Any]] = {}
self.ALL_AREAS_BY_NAME: Dict[str, Dict[str, Any]] = {}
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, Dict[str, Set[WitnessRule]]] = defaultdict(lambda: defaultdict(set))
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = {}
self.ALL_REGIONS_BY_NAME: Dict[str, RegionDefinition] = {}
self.ALL_AREAS_BY_NAME: Dict[str, AreaDefinition] = {}
self.CONNECTIONS_WITH_DUPLICATES: Dict[str, List[ConnectionDefinition]] = defaultdict(list)
self.STATIC_CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = {}
self.ENTITIES_BY_HEX: Dict[str, Dict[str, Any]] = {}
self.ENTITIES_BY_NAME: Dict[str, Dict[str, Any]] = {}
@@ -55,15 +55,15 @@ class StaticWitnessLogicObj:
area_counts: Dict[str, int] = Counter()
for region_name, entity_amount in EASTER_EGGS.items():
region_object = self.ALL_REGIONS_BY_NAME[region_name]
correct_area = region_object["area"]
correct_area = region_object.area
for _ in range(entity_amount):
location_id = 160200 + egg_counter
entity_hex = hex(0xEE000 + egg_counter)
egg_counter += 1
area_counts[correct_area["name"]] += 1
full_entity_name = f"{correct_area['name']} Easter Egg {area_counts[correct_area['name']]}"
area_counts[correct_area.name] += 1
full_entity_name = f"{correct_area.name} Easter Egg {area_counts[correct_area.name]}"
self.ENTITIES_BY_HEX[entity_hex] = {
"checkName": full_entity_name,
@@ -81,11 +81,11 @@ class StaticWitnessLogicObj:
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": frozenset({frozenset({})})
}
region_object["entities"].append(entity_hex)
region_object["physical_entities"].append(entity_hex)
region_object.logical_entities.append(entity_hex)
region_object.physical_entities.append(entity_hex)
easter_egg_region = self.ALL_REGIONS_BY_NAME["Easter Eggs"]
easter_egg_area = easter_egg_region["area"]
easter_egg_area = easter_egg_region.area
for i in range(sum(EASTER_EGGS.values())):
location_id = 160000 + i
entity_hex = hex(0xEE200 + i)
@@ -111,19 +111,15 @@ class StaticWitnessLogicObj:
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": frozenset({frozenset({})})
}
easter_egg_region["entities"].append(entity_hex)
easter_egg_region["physical_entities"].append(entity_hex)
easter_egg_region.logical_entities.append(entity_hex)
easter_egg_region.physical_entities.append(entity_hex)
def read_logic_file(self, lines: List[str]) -> None:
"""
Reads the logic file and does the initial population of data structures
"""
current_region = {}
current_area: Dict[str, Any] = {
"name": "Misc",
"regions": [],
}
current_area = AreaDefinition("Misc")
current_region = RegionDefinition("Fake", "Fake", current_area) # Unused, but makes PyCharm & mypy shut up
self.ALL_AREAS_BY_NAME["Misc"] = current_area
for line in lines:
@@ -133,19 +129,16 @@ class StaticWitnessLogicObj:
if line[-1] == ":":
new_region_and_connections = define_new_region(line, current_area)
current_region = new_region_and_connections[0]
region_name = current_region["name"]
region_name = current_region.name
self.ALL_REGIONS_BY_NAME[region_name] = current_region
for connection in new_region_and_connections[1]:
self.CONNECTIONS_WITH_DUPLICATES[region_name][connection[0]].add(connection[1])
current_area["regions"].append(region_name)
self.CONNECTIONS_WITH_DUPLICATES[region_name].append(connection)
current_area.regions.append(region_name)
continue
if line[0] == "=":
area_name = line[2:-2]
current_area = {
"name": area_name,
"regions": [],
}
current_area = AreaDefinition(area_name, [])
self.ALL_AREAS_BY_NAME[area_name] = current_area
continue
@@ -158,9 +151,9 @@ class StaticWitnessLogicObj:
entity_hex = entity_name_full[0:7]
entity_name = entity_name_full[9:-1]
required_panel_lambda = line_split.pop(0)
entity_requirement_string = line_split.pop(0)
full_entity_name = current_region["shortName"] + " " + entity_name
full_entity_name = current_region.short_name + " " + entity_name
if location_id == "Door" or location_id == "Laser":
self.ENTITIES_BY_HEX[entity_hex] = {
@@ -177,18 +170,18 @@ class StaticWitnessLogicObj:
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = {
"entities": parse_lambda(required_panel_lambda)
"entities": parse_witness_rule(entity_requirement_string)
}
# Lasers and Doors exist in a region, but don't have a regional *requirement*
# If a laser is activated, you don't need to physically walk up to it for it to count
# As such, logically, they behave more as if they were part of the "Entry" region
self.ALL_REGIONS_BY_NAME["Entry"]["entities"].append(entity_hex)
self.ALL_REGIONS_BY_NAME["Entry"].logical_entities.append(entity_hex)
# However, it will also be important to keep track of their physical location for postgame purposes.
current_region["physical_entities"].append(entity_hex)
current_region.physical_entities.append(entity_hex)
continue
required_item_lambda = line_split.pop(0)
item_requirement_string = line_split.pop(0)
laser_names = {
"Laser",
@@ -224,18 +217,18 @@ class StaticWitnessLogicObj:
entity_type = "Panel"
location_type = "General"
required_items = parse_lambda(required_item_lambda)
required_panels = parse_lambda(required_panel_lambda)
required_items = parse_witness_rule(item_requirement_string)
required_entities = parse_witness_rule(entity_requirement_string)
required_items = frozenset(required_items)
requirement = {
"entities": required_panels,
"entities": required_entities,
"items": required_items
}
if entity_type == "Obelisk Side":
eps = set(next(iter(required_panels)))
eps = set(next(iter(required_entities)))
eps -= {"Theater to Tunnels"}
eps_ints = {int(h, 16) for h in eps}
@@ -260,39 +253,43 @@ class StaticWitnessLogicObj:
self.ENTITIES_BY_NAME[self.ENTITIES_BY_HEX[entity_hex]["checkName"]] = self.ENTITIES_BY_HEX[entity_hex]
self.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX[entity_hex] = requirement
current_region["entities"].append(entity_hex)
current_region["physical_entities"].append(entity_hex)
current_region.logical_entities.append(entity_hex)
current_region.physical_entities.append(entity_hex)
self.add_easter_eggs()
def reverse_connection(self, source_region: str, connection: Tuple[str, Set[WitnessRule]]) -> None:
target = connection[0]
traversal_options = connection[1]
def reverse_connection(self, source_region: str, connection: ConnectionDefinition) -> None:
# Reverse this connection with all its possibilities, except the ones marked as "OneWay".
for requirement in traversal_options:
remaining_options = set()
for option in requirement:
if not any(req == "TrueOneWay" for req in option):
remaining_options.add(option)
remaining_options: Set[FrozenSet[str]] = set()
for sub_option in connection.traversal_rule:
if not any(req == "TrueOneWay" for req in sub_option):
remaining_options.add(sub_option)
if remaining_options:
self.CONNECTIONS_WITH_DUPLICATES[target][source_region].add(frozenset(remaining_options))
reversed_connection = ConnectionDefinition(source_region, frozenset(remaining_options))
if reversed_connection.can_be_traversed:
self.CONNECTIONS_WITH_DUPLICATES[connection.target_region].append(reversed_connection)
def reverse_connections(self) -> None:
# Iterate all connections
for region_name, connections in list(self.CONNECTIONS_WITH_DUPLICATES.items()):
for connection in connections.items():
for connection in connections:
self.reverse_connection(region_name, connection)
def combine_connections(self) -> None:
# All regions need to be present, and this dict is copied later - Thus, defaultdict is not the correct choice.
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: set() for region_name in self.ALL_REGIONS_BY_NAME}
self.STATIC_CONNECTIONS_BY_REGION_NAME = {region_name: [] for region_name in self.ALL_REGIONS_BY_NAME}
for source, connections in self.CONNECTIONS_WITH_DUPLICATES.items():
for target, requirement in connections.items():
combined_req = logical_or_witness_rules(requirement)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].add((target, combined_req))
# Organize rules by target region
traversal_options_by_target_region = defaultdict(list)
for target_region, traversal_option in connections:
traversal_options_by_target_region[target_region].append(traversal_option)
# Combine connections to the same target region into one connection
for target, traversal_rules in traversal_options_by_target_region.items():
combined_rule = logical_or_witness_rules(traversal_rules)
combined_connection = ConnectionDefinition(target, combined_rule)
self.STATIC_CONNECTIONS_BY_REGION_NAME[source].append(combined_connection)
# Item data parsed from WitnessItems.txt

View File

@@ -2,17 +2,12 @@ from datetime import date
from math import floor
from pkgutil import get_data
from random import Random
from typing import Any, Collection, Dict, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
from typing import Collection, FrozenSet, Iterable, List, Optional, Set, Tuple, TypeVar
from .definition_classes import AreaDefinition, ConnectionDefinition, RegionDefinition, WitnessRule
T = TypeVar("T")
# A WitnessRule is just an or-chain of and-conditions.
# It represents the set of all options that could fulfill this requirement.
# E.g. if something requires "Dots or (Shapers and Stars)", it'd be represented as: {{"Dots"}, {"Shapers, "Stars"}}
# {} is an unusable requirement.
# {{}} is an always usable requirement.
WitnessRule = FrozenSet[FrozenSet[str]]
def cast_not_none(value: Optional[T]) -> T:
assert value is not None
@@ -62,7 +57,7 @@ def build_weighted_int_list(inputs: Collection[float], total: int) -> List[int]:
return rounded_output
def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[str, Any], Set[Tuple[str, WitnessRule]]]:
def define_new_region(region_string: str, area: AreaDefinition) -> Tuple[RegionDefinition, List[ConnectionDefinition]]:
"""
Returns a region object by parsing a line in the logic file
"""
@@ -77,35 +72,28 @@ def define_new_region(region_string: str, area: dict[str, Any]) -> Tuple[Dict[st
region_name = region_name_split[0]
region_name_simple = region_name_split[1][:-1]
options = set()
options = []
for _ in range(len(line_split) // 2):
connected_region = line_split.pop(0)
corresponding_lambda = line_split.pop(0)
traversal_rule_string = line_split.pop(0)
options.add(
(connected_region, parse_lambda(corresponding_lambda))
)
options.append(ConnectionDefinition(connected_region, parse_witness_rule(traversal_rule_string)))
region_obj = RegionDefinition(region_name, region_name_simple, area)
region_obj = {
"name": region_name,
"shortName": region_name_simple,
"entities": [],
"physical_entities": [],
"area": area,
}
return region_obj, options
def parse_lambda(lambda_string: str) -> WitnessRule:
def parse_witness_rule(rule_string: str) -> WitnessRule:
"""
Turns a lambda String literal like this: a | b & c
into a set of sets like this: {{a}, {b, c}}
The lambda has to be in DNF.
Turns a rule string literal like this: a | b & c
into a set of sets (called "WitnessRule") like this: {{a}, {b, c}}
The rule string has to be in DNF.
"""
if lambda_string == "True":
if rule_string == "True":
return frozenset([frozenset()])
split_ands = set(lambda_string.split(" | "))
split_ands = set(rule_string.split(" | "))
return frozenset({frozenset(a.split(" & ")) for a in split_ands})

View File

@@ -129,7 +129,7 @@ class EntityHuntPicker:
eligible_panels_by_area = defaultdict(set)
for eligible_panel in all_eligible_panels:
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"]["name"]
associated_area = static_witness_logic.ENTITIES_BY_HEX[eligible_panel]["area"].name
eligible_panels_by_area[associated_area].add(eligible_panel)
return all_eligible_panels, eligible_panels_by_area

View File

@@ -18,7 +18,7 @@ if __name__ == "__main__":
for entity_id, entity_object in static_witness_logic.ENTITIES_BY_HEX.items():
location_id = entity_object["id"]
area = entity_object["area"]["name"]
area = entity_object["area"].name
area_to_entity_ids[area].append(entity_id)
if location_id is None:

View File

@@ -464,7 +464,7 @@ def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[st
def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.keys())
potential_areas = list(static_witness_logic.ALL_AREAS_BY_NAME.values())
locations_per_area = {}
items_per_area = {}
@@ -472,14 +472,14 @@ def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]]
for area in potential_areas:
regions = [
world.get_region(region)
for region in static_witness_logic.ALL_AREAS_BY_NAME[area]["regions"]
for region in area.regions
if region in world.player_regions.created_region_names
]
locations = [location for region in regions for location in region.get_locations() if not location.is_event]
if locations:
locations_per_area[area] = locations
items_per_area[area] = [location.item for location in locations]
locations_per_area[area.name] = locations
items_per_area[area.name] = [location.item for location in locations]
return locations_per_area, items_per_area
@@ -516,7 +516,7 @@ def word_area_hint(world: "WitnessWorld", hinted_area: str, area_items: List[Ite
hunt_panels = None
if world.options.victory_condition == "panel_hunt" and hinted_area != "Easter Eggs":
hunt_panels = sum(
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"]["name"] == hinted_area
static_witness_logic.ENTITIES_BY_HEX[hunt_entity]["area"].name == hinted_area
for hunt_entity in world.player_logic.HUNT_ENTITIES
)
@@ -620,7 +620,7 @@ def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
already_hinted_locations |= {
loc for loc in world.multiworld.get_reachable_locations(state, world.player)
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
if loc.address and static_witness_logic.ENTITIES_BY_NAME[loc.name]["area"].name == "Tutorial (Inside)"
}
intended_location_hints = hint_amount - area_hints

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Tuple
from schema import And, Schema

View File

@@ -20,10 +20,10 @@ from collections import defaultdict
from typing import TYPE_CHECKING, Dict, List, Set, Tuple, cast
from .data import static_logic as static_witness_logic
from .data.definition_classes import ConnectionDefinition, WitnessRule
from .data.item_definition_classes import DoorItemDefinition, ItemCategory, ProgressiveItemDefinition
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import (
WitnessRule,
get_boat,
get_caves_except_path_to_challenge_exclusion_list,
get_complex_additional_panels,
@@ -47,7 +47,7 @@ from .data.utils import (
get_vault_exclusion_list,
logical_and_witness_rules,
logical_or_witness_rules,
parse_lambda,
parse_witness_rule,
)
from .entity_hunt import EntityHuntPicker
@@ -97,10 +97,10 @@ class WitnessPlayerLogic:
elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.CONNECTIONS_BY_REGION_NAME: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.CONNECTIONS_BY_REGION_NAME: Dict[str, List[ConnectionDefinition]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME
)
self.DEPENDENT_REQUIREMENTS_BY_HEX: Dict[str, Dict[str, WitnessRule]] = copy.deepcopy(
@@ -178,7 +178,7 @@ class WitnessPlayerLogic:
entity_obj = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity_hex]
if entity_obj["region"] is not None and entity_obj["region"]["name"] in self.UNREACHABLE_REGIONS:
if entity_obj["region"] is not None and entity_obj["region"].name in self.UNREACHABLE_REGIONS:
return frozenset()
# For the requirement of an entity, we consider two things:
@@ -270,7 +270,7 @@ class WitnessPlayerLogic:
new_items = theoretical_new_items
if dep_obj["region"] and entity_obj["region"] != dep_obj["region"]:
new_items = frozenset(
frozenset(possibility | {dep_obj["region"]["name"]})
frozenset(possibility | {dep_obj["region"].name})
for possibility in new_items
)
@@ -359,11 +359,11 @@ class WitnessPlayerLogic:
line_split = line.split(" - ")
requirement = {
"entities": parse_lambda(line_split[1]),
"entities": parse_witness_rule(line_split[1]),
}
if len(line_split) > 2:
required_items = parse_lambda(line_split[2])
required_items = parse_witness_rule(line_split[2])
items_actually_in_the_game = [
item_name for item_name, item_definition in static_witness_logic.ALL_ITEMS.items()
if item_definition.category is ItemCategory.SYMBOL
@@ -394,26 +394,31 @@ class WitnessPlayerLogic:
return
if adj_type == "New Connections":
# This adjustment type does not actually reverse the connection if it could be reversed.
# If needed, this might be added later
line_split = line.split(" - ")
source_region = line_split[0]
target_region = line_split[1]
panel_set_string = line_split[2]
for connection in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region]:
if connection[0] == target_region:
if connection.target_region == target_region:
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].remove(connection)
if panel_set_string == "TrueOneWay":
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(
(target_region, frozenset({frozenset(["TrueOneWay"])}))
)
# This means the connection can be completely replaced
only_connection = ConnectionDefinition(target_region, frozenset({frozenset(["TrueOneWay"])}))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(only_connection)
else:
new_lambda = logical_or_witness_rules([connection[1], parse_lambda(panel_set_string)])
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add((target_region, new_lambda))
combined_rule = logical_or_witness_rules(
[connection.traversal_rule, parse_witness_rule(panel_set_string)]
)
combined_connection = ConnectionDefinition(target_region, combined_rule)
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(combined_connection)
break
else:
new_conn = (target_region, parse_lambda(panel_set_string))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].add(new_conn)
new_connection = ConnectionDefinition(target_region, parse_witness_rule(panel_set_string))
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL[source_region].append(new_connection)
if adj_type == "Added Locations":
if "0x" in line:
@@ -558,7 +563,7 @@ class WitnessPlayerLogic:
self.AVAILABLE_EASTER_EGGS_PER_REGION = defaultdict(int)
for entity_hex in self.AVAILABLE_EASTER_EGGS:
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"]["name"]
region_name = static_witness_logic.ENTITIES_BY_HEX[entity_hex]["region"].name
self.AVAILABLE_EASTER_EGGS_PER_REGION[region_name] += 1
eggs_per_check, logically_required_eggs_per_check = world.options.easter_egg_hunt.get_step_and_logical_step()
@@ -796,7 +801,7 @@ class WitnessPlayerLogic:
next_region = regions_to_check.pop()
for region_exit in self.CONNECTIONS_BY_REGION_NAME[next_region]:
target = region_exit[0]
target = region_exit.target_region
if target in reachable_regions:
continue
@@ -844,7 +849,7 @@ class WitnessPlayerLogic:
# First, entities in unreachable regions are obviously themselves unreachable.
for region in new_unreachable_regions:
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region]["physical_entities"]:
for entity in static_witness_logic.ALL_REGIONS_BY_NAME[region].physical_entities:
# Never disable the Victory Location.
if entity == self.VICTORY_LOCATION:
continue
@@ -879,11 +884,11 @@ class WitnessPlayerLogic:
if not new_unreachable_regions and not newly_discovered_disabled_entities:
return
def reduce_connection_requirement(self, connection: Tuple[str, WitnessRule]) -> WitnessRule:
def reduce_connection_requirement(self, connection: ConnectionDefinition) -> ConnectionDefinition:
all_possibilities = []
# Check each traversal option individually
for option in connection[1]:
for option in connection.traversal_rule:
individual_entity_requirements: List[WitnessRule] = []
for entity in option:
# If a connection requires solving a disabled entity, it is not valid.
@@ -901,7 +906,7 @@ class WitnessPlayerLogic:
entity_req = self.get_entity_requirement(entity)
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]:
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"]["name"]
region_name = self.REFERENCE_LOGIC.ENTITIES_BY_HEX[entity]["region"].name
entity_req = logical_and_witness_rules([entity_req, frozenset({frozenset({region_name})})])
individual_entity_requirements.append(entity_req)
@@ -909,7 +914,7 @@ class WitnessPlayerLogic:
# Merge all possible requirements into one DNF condition.
all_possibilities.append(logical_and_witness_rules(individual_entity_requirements))
return logical_or_witness_rules(all_possibilities)
return ConnectionDefinition(connection.target_region, logical_or_witness_rules(all_possibilities))
def make_dependency_reduced_checklist(self) -> None:
"""
@@ -942,14 +947,14 @@ class WitnessPlayerLogic:
# Make independent region connection requirements based on the entities they require
for region, connections in self.CONNECTIONS_BY_REGION_NAME_THEORETICAL.items():
new_connections = set()
new_connections = []
for connection in connections:
overall_requirement = self.reduce_connection_requirement(connection)
reduced_connection = self.reduce_connection_requirement(connection)
# If there is a way to use this connection, add it.
if overall_requirement:
new_connections.add((connection[0], overall_requirement))
if reduced_connection.can_be_traversed:
new_connections.append(reduced_connection)
self.CONNECTIONS_BY_REGION_NAME[region] = new_connections

View File

@@ -10,8 +10,9 @@ from BaseClasses import Entrance, Region
from worlds.generic.Rules import CollectionRule
from .data import static_logic as static_witness_logic
from .data.definition_classes import WitnessRule
from .data.static_logic import StaticWitnessLogicObj
from .data.utils import WitnessRule, optimize_witness_rule
from .data.utils import optimize_witness_rule
from .locations import WitnessPlayerLocations
from .player_logic import WitnessPlayerLogic
@@ -114,7 +115,7 @@ class WitnessPlayerRegions:
if k not in player_logic.UNREACHABLE_REGIONS
}
event_locations_per_region = defaultdict(dict)
event_locations_per_region: Dict[str, Dict[str, int]] = defaultdict(dict)
for event_location, event_item_and_entity in player_logic.EVENT_ITEM_PAIRS.items():
entity_or_region = event_item_and_entity[1]
@@ -126,13 +127,13 @@ class WitnessPlayerRegions:
if region is None:
region_name = "Entry"
else:
region_name = region["name"]
region_name = region.name
order = self.reference_logic.ENTITIES_BY_HEX[entity_or_region]["order"]
event_locations_per_region[region_name][event_location] = order
for region_name, region in regions_to_create.items():
location_entities_for_this_region = [
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region["entities"]
self.reference_logic.ENTITIES_BY_HEX[entity] for entity in region.logical_entities
]
locations_for_this_region = {
entity["checkName"]: entity["order"] for entity in location_entities_for_this_region

View File

@@ -10,7 +10,7 @@ from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule, set_rule
from .data import static_logic as static_witness_logic
from .data.utils import WitnessRule
from .data.definition_classes import WitnessRule
from .player_logic import WitnessPlayerLogic
if TYPE_CHECKING:

View File

@@ -516,6 +516,6 @@ async def main() -> None:
def launch() -> None:
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -177,7 +177,7 @@ def main() -> None:
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(_main())