Compare commits

..

14 Commits

Author SHA1 Message Date
NewSoupVi
2522350ea5 Update docs/world maintainer.md
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 18:11:27 +02:00
NewSoupVi
773ed6ba99 Rewrite by BadMagic 2024-09-05 12:56:04 +02:00
NewSoupVi
6ea7c8654a Update world maintainer.md 2024-09-05 12:39:37 +02:00
NewSoupVi
351917b9fe Update world maintainer.md 2024-09-05 12:39:21 +02:00
NewSoupVi
505c514253 Update world maintainer.md 2024-09-05 12:37:12 +02:00
NewSoupVi
f8b59ffdad Update world maintainer.md 2024-09-05 12:34:12 +02:00
NewSoupVi
d89166a16f Update world maintainer.md 2024-09-05 12:33:46 +02:00
NewSoupVi
ec11530b08 Update world maintainer.md 2024-09-05 11:59:55 +02:00
NewSoupVi
cb469fb1bd Update world maintainer.md 2024-09-05 11:51:24 +02:00
NewSoupVi
b881a63727 Update world maintainer.md 2024-09-05 11:51:04 +02:00
NewSoupVi
5563e97fa6 Update world maintainer.md 2024-09-05 11:50:02 +02:00
NewSoupVi
3f031af119 Update world maintainer.md 2024-09-05 11:49:34 +02:00
NewSoupVi
6c401aa5a1 Update world maintainer.md 2024-09-05 11:47:02 +02:00
NewSoupVi
f6456b6e1f Docs: Specify process for adding a world maintainer to an existing world 2024-09-05 11:45:41 +02:00
30 changed files with 2200 additions and 2360 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1 @@
worlds/blasphemous/region_data.py linguist-generated=true worlds/blasphemous/region_data.py linguist-generated=true
worlds/yachtdice/YachtWeights.py linguist-generated=true

View File

@@ -692,25 +692,17 @@ class CollectionState():
def update_reachable_regions(self, player: int): def update_reachable_regions(self, player: int):
self.stale[player] = False self.stale[player] = False
world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player] reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player]) queue = deque(self.blocked_connections[player])
start: Region = world.get_region(world.origin_region_name) start = self.multiworld.get_region("Menu", player)
# init on first call - this can't be done on construction since the regions don't exist yet # init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions: if start not in reachable_regions:
reachable_regions.add(start) reachable_regions.add(start)
self.blocked_connections[player].update(start.exits) blocked_connections.update(start.exits)
queue.extend(start.exits) queue.extend(start.exits)
if world.explicit_indirect_conditions:
self._update_reachable_regions_explicit_indirect_conditions(player, queue)
else:
self._update_reachable_regions_auto_indirect_conditions(player, queue)
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
# run BFS on all connections, and keep track of those blocked by missing items # run BFS on all connections, and keep track of those blocked by missing items
while queue: while queue:
connection = queue.popleft() connection = queue.popleft()
@@ -730,29 +722,6 @@ class CollectionState():
if new_entrance in blocked_connections and new_entrance not in queue: if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance) queue.append(new_entrance)
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
new_connection: bool = True
# run BFS on all connections, and keep track of those blocked by missing items
while new_connection:
new_connection = False
while queue:
connection = queue.popleft()
new_region = connection.connected_region
if new_region in reachable_regions:
blocked_connections.remove(connection)
elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region"
reachable_regions.add(new_region)
blocked_connections.remove(connection)
blocked_connections.update(new_region.exits)
queue.extend(new_region.exits)
self.path[new_region] = (new_region.name, self.path.get(connection, None))
new_connection = True
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
queue.extend(blocked_connections)
def copy(self) -> CollectionState: def copy(self) -> CollectionState:
ret = CollectionState(self.multiworld) ret = CollectionState(self.multiworld)
ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()} ret.prog_items = {player: counter.copy() for player, counter in self.prog_items.items()}

View File

@@ -77,4 +77,4 @@ There, you will find examples of games in the `worlds` folder:
You may also find developer documentation in the `docs` folder: You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs). [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord. If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.

View File

@@ -69,7 +69,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
You have not generated any seeds yet! You have no generated any seeds yet!
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@@ -118,6 +118,9 @@
# Noita # Noita
/worlds/noita/ @ScipioWright @heinermann /worlds/noita/ @ScipioWright @heinermann
# Ocarina of Time
/worlds/oot/ @espeon65536
# Old School Runescape # Old School Runescape
/worlds/osrs @digiholic /worlds/osrs @digiholic
@@ -227,9 +230,6 @@
# Links Awakening DX # Links Awakening DX
# /worlds/ladx/ # /worlds/ladx/
# Ocarina of Time
# /worlds/oot/
## Disabled Unmaintained Worlds ## Disabled Unmaintained Worlds
# The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are # The following worlds in this repo are currently unmaintained and disabled as they do not work in core. If you are

View File

@@ -24,7 +24,7 @@ display as `Value1` on the webhost.
files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a
Choice, and defining `alias_true = option_full`. Choice, and defining `alias_true = option_full`.
- All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or - All options with a fixed set of possible values (i.e. those which inherit from `Toggle`, `(Text)Choice` or
`(Named)Range`) support `random` as a generic option. `random` chooses from any of the available values for that `(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. option, and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`.
However, you can override `from_text` and handle `text == "random"` to customize its behavior or However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types. implement it for additional option types.
@@ -129,23 +129,6 @@ class Difficulty(Choice):
default = 1 default = 1
``` ```
### Option Visibility
Every option has a Visibility IntFlag, defaulting to `all` (`0b1111`). This lets you choose where the option will be
displayed. This only impacts where options are displayed, not how they can be used. Hidden options are still valid
options in a yaml. The flags are as follows:
* `none` (`0b0000`): This option is not shown anywhere
* `template` (`0b0001`): This option shows up in template yamls
* `simple_ui` (`0b0010`): This option shows up on the options page
* `complex_ui` (`0b0100`): This option shows up on the advanced/weighted options page
* `spoiler` (`0b1000`): This option shows up in spoiler logs
```python
from Options import Choice, Visibility
class HiddenChoiceOption(Choice):
visibility = Visibility.none
```
### Option Groups ### Option Groups
Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified Options may be categorized into groups for display on the WebHost. Option groups are displayed in the order specified
by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment by your world on the player-options and weighted-options pages. In the generated template files, there will be a comment

View File

@@ -38,7 +38,7 @@ Recommended steps
* Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details. * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details.
Generally, selecting the box for "Desktop Development with C++" will provide what you need. Generally, selecting the box for "Desktop Development with C++" will provide what you need.
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on * Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/)
* Run Generate.py which will prompt installation of missing modules, press enter to confirm * Run Generate.py which will prompt installation of missing modules, press enter to confirm

View File

@@ -303,31 +303,6 @@ generation (entrance randomization).
An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state` An access rule is a function that returns `True` or `False` for a `Location` or `Entrance` based on the current `state`
(items that have been collected). (items that have been collected).
The two possible ways to make a [CollectionRule](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10) are:
- `def rule(state: CollectionState) -> bool:`
- `lambda state: ... boolean expression ...`
An access rule can be assigned through `set_rule(location, rule)`.
Access rules usually check for one of two things.
- Items that have been collected (e.g. `state.has("Sword", player)`)
- Locations, Regions or Entrances that have been reached (e.g. `state.can_reach_region("Boss Room")`)
Keep in mind that entrances and locations implicitly check for the accessibility of their parent region, so you do not need to check explicitly for it.
#### An important note on Entrance access rules:
When using `state.can_reach` within an entrance access condition, you must also use `multiworld.register_indirect_condition`.
For efficiency reasons, every time reachable regions are searched, every entrance is only checked once in a somewhat non-deterministic order.
This is fine when checking for items using `state.has`, because items do not change during a region sweep.
However, `state.can_reach` checks for the very same thing we are updating: Regions.
This can lead to non-deterministic behavior and, in the worst case, even generation failures.
Even doing `state.can_reach_location` or `state.can_reach_entrance` is problematic, as these functions call `state.can_reach_region` on the respective parent region.
**Therefore, it is considered unsafe to perform `state.can_reach` from within an access condition for an entrance**, unless you are checking for something that sits in the source region of the entrance.
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
### Item Rules ### Item Rules
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
@@ -655,7 +630,7 @@ def set_rules(self) -> None:
Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or Custom methods can be defined for your logic rules. The access rule that ultimately gets assigned to the Location or
Entrance should be Entrance should be
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10). a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other Typically, this is done by defining a lambda expression on demand at the relevant bit, typically calling other
functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly. functions, but this can also be achieved by defining a method with the appropriate format and assigning it directly.
For an example, see [The Messenger](/worlds/messenger/rules.py). For an example, see [The Messenger](/worlds/messenger/rules.py).

View File

@@ -44,7 +44,7 @@ When a world is unmaintained, the [core maintainers](https://github.com/orgs/Arc
can vote for a new maintainer if there is a candidate. can vote for a new maintainer if there is a candidate.
For a vote to pass, the majority of participating core maintainers must vote in the affirmative. For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
The time limit is 1 week, but can end early if the majority is reached earlier. The time limit is 1 week, but can end early if the majority is reached earlier.
Voting shall be conducted on Discord in #ap-core-dev. Voting shall be conducted on Discord in #archipelago-dev.
## Dropping out ## Dropping out
@@ -60,7 +60,7 @@ for example when they become unreachable.
For a vote to pass, the majority of participating core maintainers must vote in the affirmative. For a vote to pass, the majority of participating core maintainers must vote in the affirmative.
The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and The time limit is 2 weeks, but can end early if the majority is reached earlier AND the world maintainer was pinged and
made their case or was pinged and has been unreachable for more than 2 weeks already. made their case or was pinged and has been unreachable for more than 2 weeks already.
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
date, voting members and final result in the commit message. date, voting members and final result in the commit message.
## Handling of Unmaintained Worlds ## Handling of Unmaintained Worlds

View File

@@ -292,14 +292,6 @@ class World(metaclass=AutoWorldRegister):
web: ClassVar[WebWorld] = WebWorld() web: ClassVar[WebWorld] = WebWorld()
"""see WebWorld for options""" """see WebWorld for options"""
origin_region_name: str = "Menu"
"""Name of the Region from which accessibility is tested."""
explicit_indirect_conditions: bool = True
"""If True, the world implementation is supposed to use MultiWorld.register_indirect_condition() correctly.
If False, everything is rechecked at every step, which is slower computationally,
but may be desirable in complex/dynamic worlds."""
multiworld: "MultiWorld" multiworld: "MultiWorld"
"""autoset on creation. The MultiWorld object for the currently generating multiworld.""" """autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int player: int

View File

@@ -101,7 +101,6 @@ class Factorio(World):
tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]]
tech_mix: int = 0 tech_mix: int = 0
skip_silo: bool = False skip_silo: bool = False
origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation] science_locations: typing.List[FactorioScienceLocation]
settings: typing.ClassVar[FactorioSettings] settings: typing.ClassVar[FactorioSettings]
@@ -126,6 +125,9 @@ class Factorio(World):
def create_regions(self): def create_regions(self):
player = self.player player = self.player
random = self.multiworld.random random = self.multiworld.random
menu = Region("Menu", player, self.multiworld)
crash = Entrance(player, "Crash Land", menu)
menu.exits.append(crash)
nauvis = Region("Nauvis", player, self.multiworld) nauvis = Region("Nauvis", player, self.multiworld)
location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \ location_count = len(base_tech_table) - len(useless_technologies) - self.skip_silo + \
@@ -182,7 +184,8 @@ class Factorio(World):
event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player) event = FactorioItem(f"Automated {ingredient}", ItemClassification.progression, None, player)
location.place_locked_item(event) location.place_locked_item(event)
self.multiworld.regions.append(nauvis) crash.connect(nauvis)
self.multiworld.regions += [menu, nauvis]
def create_items(self) -> None: def create_items(self) -> None:
player = self.player player = self.player

View File

@@ -601,11 +601,11 @@ class HKWorld(World):
if change: if change:
for effect_name, effect_value in item_effects.get(item.name, {}).items(): for effect_name, effect_value in item_effects.get(item.name, {}).items():
state.prog_items[item.player][effect_name] += effect_value state.prog_items[item.player][effect_name] += effect_value
if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}: if item.name in {"Left_Mothwing_Cloak", "Right_Mothwing_Cloak"}:
if state.prog_items[item.player].get('RIGHTDASH', 0) and \ if state.prog_items[item.player].get('RIGHTDASH', 0) and \
state.prog_items[item.player].get('LEFTDASH', 0): state.prog_items[item.player].get('LEFTDASH', 0):
(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \ (state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"]) = \
([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2) ([max(state.prog_items[item.player]["RIGHTDASH"], state.prog_items[item.player]["LEFTDASH"])] * 2)
return change return change
def remove(self, state, item: HKItem) -> bool: def remove(self, state, item: HKItem) -> bool:

View File

@@ -45,7 +45,7 @@ class SubnauticaWorld(World):
options_dataclass = options.SubnauticaOptions options_dataclass = options.SubnauticaOptions
options: options.SubnauticaOptions options: options.SubnauticaOptions
required_client_version = (0, 5, 0) required_client_version = (0, 5, 0)
origin_region_name = "Planet 4546B"
creatures_to_scan: List[str] creatures_to_scan: List[str]
def generate_early(self) -> None: def generate_early(self) -> None:
@@ -66,9 +66,13 @@ class SubnauticaWorld(World):
creature_pool, self.options.creature_scans.value) creature_pool, self.options.creature_scans.value)
def create_regions(self): def create_regions(self):
# Create Region # Create Regions
menu_region = Region("Menu", self.player, self.multiworld)
planet_region = Region("Planet 4546B", self.player, self.multiworld) planet_region = Region("Planet 4546B", self.player, self.multiworld)
# Link regions together
menu_region.connect(planet_region, "Lifepod 5")
# Create regular locations # Create regular locations
location_names = itertools.chain((location["name"] for location in locations.location_table.values()), location_names = itertools.chain((location["name"] for location in locations.location_table.values()),
(creature + creatures.suffix for creature in self.creatures_to_scan)) (creature + creatures.suffix for creature in self.creatures_to_scan))
@@ -89,8 +93,11 @@ class SubnauticaWorld(World):
# make the goal event the victory "item" # make the goal event the victory "item"
location.item.name = "Victory" location.item.name = "Victory"
# Register region to multiworld # Register regions to multiworld
self.multiworld.regions.append(planet_region) self.multiworld.regions += [
menu_region,
planet_region
]
# refer to rules.py # refer to rules.py
set_rules = set_rules set_rules = set_rules

View File

@@ -204,10 +204,11 @@ class WitnessWorld(World):
] ]
if early_items: if early_items:
random_early_item = self.random.choice(early_items) random_early_item = self.random.choice(early_items)
mode = self.options.puzzle_randomization if (
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt": self.options.puzzle_randomization == "sigma_expert"
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate. or self.options.victory_condition == "panel_hunt"
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else ):
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
self.multiworld.local_early_items[self.player][random_early_item] = 1 self.multiworld.local_early_items[self.player][random_early_item] = 1
else: else:
# Force the item onto the tutorial gate check and remove it from our random pool. # Force the item onto the tutorial gate check and remove it from our random pool.

File diff suppressed because it is too large Load Diff

View File

@@ -13,22 +13,6 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
# item list during get_progression_items. # item list during get_progression_items.
_special_usefuls: List[str] = ["Puzzle Skip"] _special_usefuls: List[str] = ["Puzzle Skip"]
ALWAYS_GOOD_SYMBOL_ITEMS: Set[str] = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
MODE_SPECIFIC_GOOD_ITEMS: Dict[str, Set[str]] = {
"none": set(),
"sigma_normal": set(),
"sigma_expert": {"Triangles"},
"umbra_variety": {"Triangles"}
}
MODE_SPECIFIC_GOOD_DISCARD_ITEMS: Dict[str, Set[str]] = {
"none": {"Triangles"},
"sigma_normal": {"Triangles"},
"sigma_expert": {"Arrows"},
"umbra_variety": set() # Variety Discards use both Arrows and Triangles, so neither of them are that useful alone
}
def populate_items() -> None: def populate_items() -> None:
for item_name, definition in static_witness_logic.ALL_ITEMS.items(): for item_name, definition in static_witness_logic.ALL_ITEMS.items():

View File

@@ -17,7 +17,6 @@ from .utils import (
get_items, get_items,
get_sigma_expert_logic, get_sigma_expert_logic,
get_sigma_normal_logic, get_sigma_normal_logic,
get_umbra_variety_logic,
get_vanilla_logic, get_vanilla_logic,
logical_or_witness_rules, logical_or_witness_rules,
parse_lambda, parse_lambda,
@@ -293,11 +292,6 @@ def get_sigma_expert() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_sigma_expert_logic()) return StaticWitnessLogicObj(get_sigma_expert_logic())
@cache_argsless
def get_umbra_variety() -> StaticWitnessLogicObj:
return StaticWitnessLogicObj(get_umbra_variety_logic())
def __getattr__(name: str) -> StaticWitnessLogicObj: def __getattr__(name: str) -> StaticWitnessLogicObj:
if name == "vanilla": if name == "vanilla":
return get_vanilla() return get_vanilla()
@@ -305,8 +299,6 @@ def __getattr__(name: str) -> StaticWitnessLogicObj:
return get_sigma_normal() return get_sigma_normal()
if name == "sigma_expert": if name == "sigma_expert":
return get_sigma_expert() return get_sigma_expert()
if name == "umbra_variety":
return get_umbra_variety()
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")

View File

@@ -215,10 +215,6 @@ def get_sigma_expert_logic() -> List[str]:
return get_adjustment_file("WitnessLogicExpert.txt") return get_adjustment_file("WitnessLogicExpert.txt")
def get_umbra_variety_logic() -> List[str]:
return get_adjustment_file("WitnessLogicVariety.txt")
def get_vanilla_logic() -> List[str]: def get_vanilla_logic() -> List[str]:
return get_adjustment_file("WitnessLogicVanilla.txt") return get_adjustment_file("WitnessLogicVanilla.txt")

View File

@@ -145,7 +145,7 @@ class EntityHuntPicker:
remaining_entities, remaining_entity_weights = [], [] remaining_entities, remaining_entity_weights = [], []
for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items(): for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items():
for panel in sorted(eligible_entities - self.HUNT_ENTITIES): for panel in eligible_entities - self.HUNT_ENTITIES:
remaining_entities.append(panel) remaining_entities.append(panel)
remaining_entity_weights.append(allowance_per_area[area]) remaining_entity_weights.append(allowance_per_area[area])

View File

@@ -53,7 +53,7 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
wincon = world.options.victory_condition wincon = world.options.victory_condition
if discards: if discards:
if difficulty == "sigma_expert" or difficulty == "umbra_variety": if difficulty == "sigma_expert":
always.append("Arrows") always.append("Arrows")
else: else:
always.append("Triangles") always.append("Triangles")

View File

@@ -250,15 +250,10 @@ class PanelHuntDiscourageSameAreaFactor(Range):
class PuzzleRandomization(Choice): class PuzzleRandomization(Choice):
""" """
Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles. Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles.
"Sigma Normal" randomizes puzzles close to their original mechanics and difficulty.
"Sigma Expert" is an entirely new experience with extremely difficult random puzzles. Do not underestimate this mode, it is brutal.
"Umbra Variety" focuses on unique symbol combinations not featured in the original game. It is harder than Sigma Normal, but easier than Sigma Expert.
"None" means that the puzzles are unchanged from the original game.
""" """
display_name = "Puzzle Randomization" display_name = "Puzzle Randomization"
option_sigma_normal = 0 option_sigma_normal = 0
option_sigma_expert = 1 option_sigma_expert = 1
option_umbra_variety = 3
option_none = 2 option_none = 2

View File

@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import ( from .data.item_definition_classes import (
DoorItemDefinition, DoorItemDefinition,
ItemCategory, ItemCategory,
@@ -154,12 +155,16 @@ class WitnessPlayerItems:
""" """
output: Set[str] = set() output: Set[str] = set()
if self._world.options.shuffle_symbols: if self._world.options.shuffle_symbols:
discards_on = self._world.options.shuffle_discarded_panels output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
mode = self._world.options.puzzle_randomization.current_key
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode] if self._world.options.shuffle_discarded_panels:
if discards_on: if self._world.options.puzzle_randomization == "sigma_expert":
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode] output.add("Arrows")
else:
output.add("Triangles")
# Replace progressive items with their parents.
output = {static_witness_logic.get_parent_progressive_item(item) for item in output}
# Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved # Remove items that are mentioned in any plando options. (Hopefully, in the future, plando will get resolved
# before create_items so that we'll be able to check placed items instead of just removing all items mentioned # before create_items so that we'll be able to check placed items instead of just removing all items mentioned

View File

@@ -87,14 +87,12 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization self.DIFFICULTY = world.options.puzzle_randomization
self.REFERENCE_LOGIC: StaticWitnessLogicObj self.REFERENCE_LOGIC: StaticWitnessLogicObj
if self.DIFFICULTY == "sigma_normal": if self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
elif self.DIFFICULTY == "sigma_expert":
self.REFERENCE_LOGIC = static_witness_logic.sigma_expert self.REFERENCE_LOGIC = static_witness_logic.sigma_expert
elif self.DIFFICULTY == "umbra_variety":
self.REFERENCE_LOGIC = static_witness_logic.umbra_variety
elif self.DIFFICULTY == "none": elif self.DIFFICULTY == "none":
self.REFERENCE_LOGIC = static_witness_logic.vanilla self.REFERENCE_LOGIC = static_witness_logic.vanilla
else:
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy( self.CONNECTIONS_BY_REGION_NAME_THEORETICAL: Dict[str, Set[Tuple[str, WitnessRule]]] = copy.deepcopy(
self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME

View File

@@ -30,8 +30,6 @@ class WitnessPlayerRegions:
self.reference_logic = static_witness_logic.sigma_normal self.reference_logic = static_witness_logic.sigma_normal
elif difficulty == "sigma_expert": elif difficulty == "sigma_expert":
self.reference_logic = static_witness_logic.sigma_expert self.reference_logic = static_witness_logic.sigma_expert
elif difficulty == "umbra_variety":
self.reference_logic = static_witness_logic.umbra_variety
else: else:
self.reference_logic = static_witness_logic.vanilla self.reference_logic = static_witness_logic.vanilla

View File

@@ -96,39 +96,6 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
self.assert_can_beat_with_minimally(exact_requirement) self.assert_can_beat_with_minimally(exact_requirement)
class TestSymbolsRequiredToWinElevatorVariety(WitnessTestBase):
options = {
"shuffle_lasers": True,
"mountain_lasers": 1,
"victory_condition": "elevator",
"early_symbol_item": False,
"puzzle_randomization": "umbra_variety",
}
def test_symbols_to_win(self) -> None:
"""
In symbol shuffle, the only way to reach the Elevator is through Mountain Entry by descending the Mountain.
This requires a very specific set of symbol items per puzzle randomization mode.
In this case, we check Variety Puzzles.
"""
exact_requirement = {
"Monastery Laser": 1,
"Progressive Dots": 2,
"Progressive Stars": 2,
"Progressive Symmetry": 1,
"Black/White Squares": 1,
"Colored Squares": 1,
"Shapers": 1,
"Rotated Shapers": 1,
"Eraser": 1,
"Triangles": 1,
"Arrows": 1,
}
self.assert_can_beat_with_minimally(exact_requirement)
class TestPanelsRequiredToWinElevator(WitnessTestBase): class TestPanelsRequiredToWinElevator(WitnessTestBase):
options = { options = {
"shuffle_lasers": True, "shuffle_lasers": True,

View File

@@ -54,7 +54,6 @@ class TestMaxEntityShuffle(WitnessTestBase):
class TestPostgameGroupedDoors(WitnessTestBase): class TestPostgameGroupedDoors(WitnessTestBase):
options = { options = {
"puzzle_randomization": "umbra_variety",
"shuffle_postgame": True, "shuffle_postgame": True,
"shuffle_discarded_panels": True, "shuffle_discarded_panels": True,
"shuffle_doors": "doors", "shuffle_doors": "doors",

View File

@@ -46,9 +46,6 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
{ {
"puzzle_randomization": "none", "puzzle_randomization": "none",
}, },
{
"puzzle_randomization": "umbra_variety",
}
] ]
common_options = { common_options = {
@@ -66,15 +63,12 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
self.assertFalse(self.get_items_by_name("Arrows", 1)) self.assertFalse(self.get_items_by_name("Arrows", 1))
self.assertTrue(self.get_items_by_name("Arrows", 2)) self.assertTrue(self.get_items_by_name("Arrows", 2))
self.assertFalse(self.get_items_by_name("Arrows", 3)) self.assertFalse(self.get_items_by_name("Arrows", 3))
self.assertTrue(self.get_items_by_name("Arrows", 4))
with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."): with self.subTest("Test that Discards ask for Triangles in normal, but Arrows in expert."):
desert_discard = "0x17CE7" desert_discard = "0x17CE7"
triangles = frozenset({frozenset({"Triangles"})}) triangles = frozenset({frozenset({"Triangles"})})
arrows = frozenset({frozenset({"Arrows"})}) arrows = frozenset({frozenset({"Arrows"})})
both = frozenset({frozenset({"Triangles", "Arrows"})})
self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) self.assertEqual(self.multiworld.worlds[1].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows) self.assertEqual(self.multiworld.worlds[2].player_logic.REQUIREMENTS_BY_HEX[desert_discard], arrows)
self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles) self.assertEqual(self.multiworld.worlds[3].player_logic.REQUIREMENTS_BY_HEX[desert_discard], triangles)
self.assertEqual(self.multiworld.worlds[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)

View File

@@ -29,7 +29,7 @@ class Category:
mean_score = 0 mean_score = 0
for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items(): for key, value in yacht_weights[self.name, min(8, num_dice), min(8, num_rolls)].items():
mean_score += key * value / 100000 mean_score += key * value / 100000
return mean_score return mean_score * self.quantity
class ListState: class ListState:

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@ class YachtDiceWorld(World):
item_name_groups = item_groups item_name_groups = item_groups
ap_world_version = "2.1.2" ap_world_version = "2.1.1"
def _get_yachtdice_data(self): def _get_yachtdice_data(self):
return { return {
@@ -190,6 +190,7 @@ class YachtDiceWorld(World):
if self.frags_per_roll == 1: if self.frags_per_roll == 1:
self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory self.itempool += ["Roll"] * num_of_rolls_to_add # minus one because one is in start inventory
else: else:
self.itempool.append("Roll") # always add a full roll to make generation easier (will be early)
self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add) self.itempool += ["Roll Fragment"] * (self.frags_per_roll * num_of_rolls_to_add)
already_items = len(self.itempool) already_items = len(self.itempool)
@@ -230,10 +231,13 @@ class YachtDiceWorld(World):
weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice weights["Dice"] = weights["Dice"] / 5 * self.frags_per_dice
weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll weights["Roll"] = weights["Roll"] / 5 * self.frags_per_roll
extra_points_added = [0] # make it a mutible type so we can change the value in the function extra_points_added = 0
step_score_multipliers_added = [0] multipliers_added = 0
items_added = 0
def get_item_to_add(weights, extra_points_added, multipliers_added, items_added):
items_added += 1
def get_item_to_add(weights, extra_points_added, step_score_multipliers_added):
all_items = self.itempool + self.precollected all_items = self.itempool + self.precollected
dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment") dice_fragments_in_pool = all_items.count("Dice") * self.frags_per_dice + all_items.count("Dice Fragment")
if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice: if dice_fragments_in_pool + 1 >= 9 * self.frags_per_dice:
@@ -242,18 +246,21 @@ class YachtDiceWorld(World):
if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll: if roll_fragments_in_pool + 1 >= 6 * self.frags_per_roll:
weights["Roll"] = 0 # don't allow >= 6 rolls weights["Roll"] = 0 # don't allow >= 6 rolls
# Don't allow too many extra points # Don't allow too many multipliers
if extra_points_added[0] > 400: if multipliers_added > 50:
weights["Points"] = 0 weights["Fixed Score Multiplier"] = 0
if step_score_multipliers_added[0] > 10:
weights["Step Score Multiplier"] = 0 weights["Step Score Multiplier"] = 0
# Don't allow too many extra points
if extra_points_added > 300:
weights["Points"] = 0
# if all weights are zero, allow to add fixed score multiplier, double category, points. # if all weights are zero, allow to add fixed score multiplier, double category, points.
if sum(weights.values()) == 0: if sum(weights.values()) == 0:
weights["Fixed Score Multiplier"] = 1 if multipliers_added <= 50:
weights["Fixed Score Multiplier"] = 1
weights["Double category"] = 1 weights["Double category"] = 1
if extra_points_added[0] <= 400: if extra_points_added <= 300:
weights["Points"] = 1 weights["Points"] = 1
# Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item # Next, add the appropriate item. We'll slightly alter weights to avoid too many of the same item
@@ -267,10 +274,11 @@ class YachtDiceWorld(World):
return "Roll" if self.frags_per_roll == 1 else "Roll Fragment" return "Roll" if self.frags_per_roll == 1 else "Roll Fragment"
elif which_item_to_add == "Fixed Score Multiplier": elif which_item_to_add == "Fixed Score Multiplier":
weights["Fixed Score Multiplier"] /= 1.05 weights["Fixed Score Multiplier"] /= 1.05
multipliers_added += 1
return "Fixed Score Multiplier" return "Fixed Score Multiplier"
elif which_item_to_add == "Step Score Multiplier": elif which_item_to_add == "Step Score Multiplier":
weights["Step Score Multiplier"] /= 1.1 weights["Step Score Multiplier"] /= 1.1
step_score_multipliers_added[0] += 1 multipliers_added += 1
return "Step Score Multiplier" return "Step Score Multiplier"
elif which_item_to_add == "Double category": elif which_item_to_add == "Double category":
# Below entries are the weights to add each category. # Below entries are the weights to add each category.
@@ -295,15 +303,15 @@ class YachtDiceWorld(World):
choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0] choice = self.random.choices(list(probs.keys()), weights=list(probs.values()))[0]
if choice == "1 Point": if choice == "1 Point":
weights["Points"] /= 1.01 weights["Points"] /= 1.01
extra_points_added[0] += 1 extra_points_added += 1
return "1 Point" return "1 Point"
elif choice == "10 Points": elif choice == "10 Points":
weights["Points"] /= 1.1 weights["Points"] /= 1.1
extra_points_added[0] += 10 extra_points_added += 10
return "10 Points" return "10 Points"
elif choice == "100 Points": elif choice == "100 Points":
weights["Points"] /= 2 weights["Points"] /= 2
extra_points_added[0] += 100 extra_points_added += 100
return "100 Points" return "100 Points"
else: else:
raise Exception("Unknown point value (Yacht Dice)") raise Exception("Unknown point value (Yacht Dice)")
@@ -312,7 +320,7 @@ class YachtDiceWorld(World):
# adding 17 items as a start seems like the smartest way to get close to 1000 points # adding 17 items as a start seems like the smartest way to get close to 1000 points
for _ in range(17): for _ in range(17):
self.itempool.append(get_item_to_add(weights, extra_points_added, step_score_multipliers_added)) self.itempool.append(get_item_to_add(weights, extra_points_added, multipliers_added, items_added))
score_in_logic = dice_simulation_fill_pool( score_in_logic = dice_simulation_fill_pool(
self.itempool + self.precollected, self.itempool + self.precollected,
@@ -340,7 +348,7 @@ class YachtDiceWorld(World):
else: else:
# Keep adding items until a score of 1000 is in logic # Keep adding items until a score of 1000 is in logic
while score_in_logic < 1000: while score_in_logic < 1000:
item_to_add = get_item_to_add(weights, extra_points_added, step_score_multipliers_added) item_to_add = get_item_to_add(weights, extra_points_added, multipliers_added, items_added)
self.itempool.append(item_to_add) self.itempool.append(item_to_add)
if item_to_add == "1 Point": if item_to_add == "1 Point":
score_in_logic += 1 score_in_logic += 1