Compare commits

...

12 Commits

Author SHA1 Message Date
NewSoupVi
40cc4dd9f1 Yacht Dice: Mark YachtWeights.py as "linguist-generated"
This means its diff will be collapsed by default on PRs that change it, because it is an "auto generated" file that does not need to be looked at by reviewers
2024-09-06 22:52:33 +02:00
Spineraks
a40744e6db Yacht Dice: logic fix and several other fixes (#3878)
* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

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

* Apply suggestions from code review

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

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

* Yacht Dice: setup: change release-link to latest

On the installation page, link to the latest release, instead of the page with all releases

* Several fixes and changes

-change apworld version
-Removed the extra roll (this was not intended)
-change extra_points_added to a mutable list to that it actually does something
-removed variables multipliers_added and items_added
-Rules, don't order by quantity, just by mean_score
-Changed the weights in general to make it faster

* 🐶

* Revert setup to what it was (latest, without S)

* remove temp weights file, shouldn't be here

* Made sure that there is not too many step score multipliers.

Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game.

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-06 22:50:57 +02:00
Draexzhan
d802f9652a Webhost: Fixed typo in userContent.html #3896
Changed "no" to "not"
2024-09-06 20:40:21 +02:00
NewSoupVi
cbdb4d7ce3 CODEOWNERS: Move OoT to "unmaintained" (#3894)
https://discord.com/channels/731205301247803413/1214608557077700720/1253206955879694336

Espeon might come back, but still, this world acts as unmaintained right now, so we should make this change, and then change it back if/when he's back.

@espeon65536 Just so you're aware of this change as well
2024-09-06 19:38:18 +02:00
Mysteryem
691ce6a248 The Witness: Fix nondeterministic entity hunt (#3892)
In `_get_next_random_batch()`, the `remaining_entities` and
`remaining_entity_weights` lists were being constructed by iterating
sets.

This patch changes the function to iterate a sorted copy of each set
instead.
2024-09-06 19:23:16 +02:00
Danaël V.
f9fc6944d3 Docs: Removing #archipelago-dev from places (#3876)
* Cleaning up (#4)

Cleanup

* Changed channel name

* Changed channel name

* Update docs/world maintainer.md

* Update docs/world maintainer.md
2024-09-05 22:55:19 +02:00
qwint
e984583e5e HK: speed up collect (a bit) (#3886)
* speed up collect, will be obsolete after #3786

* vi's a meanie
2024-09-05 21:19:37 +02:00
Exempt-Medic
7e03a87608 DOCS: Option Visibility and removing SpecialRange (#3889)
* Update options api.md

* Update options api.md

* Update docs/options api.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2024-09-05 21:18:58 +02:00
NewSoupVi
456bc481a3 Docs: Specify process for adding a world maintainer to an existing world (#3884)
* Docs: Specify process for adding a world maintainer to an existing world

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Update world maintainer.md

* Rewrite by BadMagic

* Update docs/world maintainer.md

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 21:16:44 +02:00
NewSoupVi
b4752cd32d The Witness: Implement "Variety" puzzles mode (#3239)
* Variety Rando (But WitnessLogicVariety.txt is wrong

* Actually variety the variety file (Ty Exempt-Medic <3)

* This will be preopened

* Tooltip explaining the different difficulties

* Remove ?, those were correct

* Less efficient but easier to follow

* Parentheses

* Fix some reqs

* Not Arrows in Variety

* Oops

* Happy medic, I made a wacky solution

* there we go

* Lint oops

* There

* that copy is unnecessary

* Turns out that copy is necessary still

* yes

* lol

* Rename to Umbra Variety

* missed one

* Erase the Eraser

* Fix remaining instances of 'variety' and don't have a symbol item on the gate in variety

* reorder difficulties

* inbetween

* ruff

* Fix Variety Invis requirements

* Fix wooden beams variety

* Fix PP2 variety

* Mirror changes from 'Variety Mode Puzzle Change 3.2.3'

* These also have Symmetry

* merge error prevention

* Update worlds/witness/data/static_items.py

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

* no elif after return

* add variety to the symbol requirement bleed test

* Add variety to one of the 'other settings' unit tests

* Add Variety minimal symbols unittest

* oops

* I did the dumb again

* .

* Incorporate changes from other PR into WitnesLogicVariety.txt

* Update worlds/witness/data/WitnessLogicVariety.txt

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

* Update worlds/witness/data/WitnessLogicVariety.txt

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

* Update the reqs as well haha

* Another difference, thanks Medic :§

* Wait no, this one was right

* lol

* apply changes to WitnessLogicVariety.txt

* Add most recent Variety changes

* oof

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 17:10:09 +02:00
Fabian Dill
ceec51b9e1 Core: Region handling customization (#3682) 2024-09-05 16:32:45 +02:00
NewSoupVi
d3312287a8 Docs: Mention indirect_conditions and that they are a *hard requirement* (with a few sharp exception cases) (#3552)
* Docs: Mention indirect_conditions and that they are a *hard requirement* (with hard exception cases)

I definitely don't feel like I wrote this in the best way, or in the best place, but it is a precedent that I think is necessary so we can treat it as "the law of the land".

* oops

* Update world api.md

* Update world api.md

* Update world api.md

* Update docs/world api.md

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

* I like within more here

* Update docs/world api.md

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

* Update world api.md

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-05 13:53:34 +02:00
30 changed files with 2370 additions and 2201 deletions

1
.gitattributes vendored
View File

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

View File

@@ -692,17 +692,25 @@ class CollectionState():
def update_reachable_regions(self, player: int):
self.stale[player] = False
world: AutoWorld.World = self.multiworld.worlds[player]
reachable_regions = self.reachable_regions[player]
blocked_connections = self.blocked_connections[player]
queue = deque(self.blocked_connections[player])
start = self.multiworld.get_region("Menu", player)
start: Region = world.get_region(world.origin_region_name)
# init on first call - this can't be done on construction since the regions don't exist yet
if start not in reachable_regions:
reachable_regions.add(start)
blocked_connections.update(start.exits)
self.blocked_connections[player].update(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
while queue:
connection = queue.popleft()
@@ -722,6 +730,29 @@ class CollectionState():
if new_entrance in blocked_connections and new_entrance not in queue:
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:
ret = CollectionState(self.multiworld)
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:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.

View File

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

View File

@@ -118,9 +118,6 @@
# Noita
/worlds/noita/ @ScipioWright @heinermann
# Ocarina of Time
/worlds/oot/ @espeon65536
# Old School Runescape
/worlds/osrs @digiholic
@@ -230,6 +227,9 @@
# Links Awakening DX
# /worlds/ladx/
# Ocarina of Time
# /worlds/oot/
## Disabled Unmaintained Worlds
# 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
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
`(Named/Special)Range`) support `random` as a generic option. `random` chooses from any of the available values for that
`(Named)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`.
However, you can override `from_text` and handle `text == "random"` to customize its behavior or
implement it for additional option types.
@@ -129,6 +129,23 @@ class Difficulty(Choice):
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
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

View File

@@ -38,7 +38,7 @@ Recommended steps
* 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.
* Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on
[Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
[Discord in #ap-core-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808)
* 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

View File

@@ -303,6 +303,31 @@ generation (entrance randomization).
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).
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
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
@@ -630,7 +655,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
Entrance should be
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L9).
a [`CollectionRule`](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/generic/Rules.py#L10).
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.
For an example, see [The Messenger](/worlds/messenger/rules.py).

View File

@@ -26,8 +26,17 @@ Unless these are shared between multiple people, we expect the following from ea
### Adding a World
When we merge your world into the core Archipelago repository, you automatically become world maintainer unless you
nominate someone else (i.e. there are multiple devs). You can define who is allowed to approve changes to your world
in the [CODEOWNERS](/docs/CODEOWNERS) document.
nominate someone else (i.e. there are multiple devs).
### Being added as a maintainer to an existing implementation
At any point, a world maintainer can approve the addition of another maintainer to their world.
In order to do this, either an existing maintainer or the new maintainer must open a PR updating the
[CODEOWNERS](/docs/CODEOWNERS) file.
This change must be approved by all existing maintainers of the affected world, the new maintainer candidate, and
one core maintainer.
To help the core team review the change, information about the new maintainer and their contributions should be
included in the PR description.
### Getting Voted
@@ -35,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.
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.
Voting shall be conducted on Discord in #archipelago-dev.
Voting shall be conducted on Discord in #ap-core-dev.
## Dropping out
@@ -51,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.
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.
Voting shall be conducted on Discord in #archipelago-dev. Commits that are a direct result of the voting shall include
Voting shall be conducted on Discord in #ap-core-dev. Commits that are a direct result of the voting shall include
date, voting members and final result in the commit message.
## Handling of Unmaintained Worlds

View File

@@ -292,6 +292,14 @@ class World(metaclass=AutoWorldRegister):
web: ClassVar[WebWorld] = WebWorld()
"""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"
"""autoset on creation. The MultiWorld object for the currently generating multiworld."""
player: int

View File

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

View File

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

View File

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

View File

@@ -204,11 +204,10 @@ class WitnessWorld(World):
]
if early_items:
random_early_item = self.random.choice(early_items)
if (
self.options.puzzle_randomization == "sigma_expert"
or self.options.victory_condition == "panel_hunt"
):
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
mode = self.options.puzzle_randomization
if mode == "sigma_expert" or mode == "umbra_variety" or self.options.victory_condition == "panel_hunt":
# In Expert and Variety, only tag the item as early, rather than forcing it onto the gate.
# Same with panel hunt, since the Tutorial Gate Open panel is used for something else
self.multiworld.local_early_items[self.player][random_early_item] = 1
else:
# Force the item onto the tutorial gate check and remove it from our random pool.
@@ -255,7 +254,7 @@ class WitnessWorld(World):
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
warning(
f"""Location "{loc}" had to be added to {self.player_name}'s world
f"""Location "{loc}" had to be added to {self.player_name}'s world
due to insufficient sphere 1 size."""
)

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,22 @@ ITEM_GROUPS: Dict[str, Set[str]] = {}
# item list during get_progression_items.
_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:
for item_name, definition in static_witness_logic.ALL_ITEMS.items():

View File

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

View File

@@ -215,6 +215,10 @@ def get_sigma_expert_logic() -> List[str]:
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]:
return get_adjustment_file("WitnessLogicVanilla.txt")

View File

@@ -145,7 +145,7 @@ class EntityHuntPicker:
remaining_entities, remaining_entity_weights = [], []
for area, eligible_entities in self.ELIGIBLE_ENTITIES_PER_AREA.items():
for panel in eligible_entities - self.HUNT_ENTITIES:
for panel in sorted(eligible_entities - self.HUNT_ENTITIES):
remaining_entities.append(panel)
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
if discards:
if difficulty == "sigma_expert":
if difficulty == "sigma_expert" or difficulty == "umbra_variety":
always.append("Arrows")
else:
always.append("Triangles")

View File

@@ -250,10 +250,15 @@ class PanelHuntDiscourageSameAreaFactor(Range):
class PuzzleRandomization(Choice):
"""
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"
option_sigma_normal = 0
option_sigma_expert = 1
option_umbra_variety = 3
option_none = 2

View File

@@ -7,7 +7,6 @@ from typing import TYPE_CHECKING, Dict, List, Set, cast
from BaseClasses import Item, ItemClassification, MultiWorld
from .data import static_items as static_witness_items
from .data import static_logic as static_witness_logic
from .data.item_definition_classes import (
DoorItemDefinition,
ItemCategory,
@@ -155,16 +154,12 @@ class WitnessPlayerItems:
"""
output: Set[str] = set()
if self._world.options.shuffle_symbols:
output = {"Dots", "Black/White Squares", "Symmetry", "Shapers", "Stars"}
discards_on = self._world.options.shuffle_discarded_panels
mode = self._world.options.puzzle_randomization.current_key
if self._world.options.shuffle_discarded_panels:
if self._world.options.puzzle_randomization == "sigma_expert":
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}
output = static_witness_items.ALWAYS_GOOD_SYMBOL_ITEMS | static_witness_items.MODE_SPECIFIC_GOOD_ITEMS[mode]
if discards_on:
output |= static_witness_items.MODE_SPECIFIC_GOOD_DISCARD_ITEMS[mode]
# 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

View File

@@ -87,12 +87,14 @@ class WitnessPlayerLogic:
self.DIFFICULTY = world.options.puzzle_randomization
self.REFERENCE_LOGIC: StaticWitnessLogicObj
if self.DIFFICULTY == "sigma_expert":
if self.DIFFICULTY == "sigma_normal":
self.REFERENCE_LOGIC = static_witness_logic.sigma_normal
elif self.DIFFICULTY == "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":
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.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME

View File

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

View File

@@ -96,6 +96,39 @@ class TestSymbolsRequiredToWinElevatorVanilla(WitnessTestBase):
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):
options = {
"shuffle_lasers": True,

View File

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

View File

@@ -46,6 +46,9 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
{
"puzzle_randomization": "none",
},
{
"puzzle_randomization": "umbra_variety",
}
]
common_options = {
@@ -63,12 +66,15 @@ class TestSymbolRequirementsMultiworld(WitnessMultiworldTestBase):
self.assertFalse(self.get_items_by_name("Arrows", 1))
self.assertTrue(self.get_items_by_name("Arrows", 2))
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."):
desert_discard = "0x17CE7"
triangles = frozenset({frozenset({"Triangles"})})
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[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[4].player_logic.REQUIREMENTS_BY_HEX[desert_discard], both)

View File

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