about summary refs log tree commit diff stats
path: root/apworld
diff options
context:
space:
mode:
Diffstat (limited to 'apworld')
-rw-r--r--apworld/CHANGELOG.md29
-rw-r--r--apworld/README.md48
-rw-r--r--apworld/__init__.py44
-rw-r--r--apworld/docs/en_Lingo_2.md4
-rw-r--r--apworld/items.py26
-rw-r--r--apworld/options.py101
-rw-r--r--apworld/player_logic.py314
-rw-r--r--apworld/regions.py28
-rw-r--r--apworld/requirements.txt2
-rw-r--r--apworld/rules.py40
-rw-r--r--apworld/static_logic.py38
-rw-r--r--apworld/version.py1
12 files changed, 600 insertions, 75 deletions
diff --git a/apworld/CHANGELOG.md b/apworld/CHANGELOG.md new file mode 100644 index 0000000..8931688 --- /dev/null +++ b/apworld/CHANGELOG.md
@@ -0,0 +1,29 @@
1# lingo2-archipelago Apworld Releases
2
3## v4.3 - 2025-09-13
4
5- Added a location for the anti-collectable in The Repetitive.
6- Added trap items. These remove letters from your keyboard until you use the
7 Key Return in The Entry, similar to the anti-collectable in The Repetitive.
8 This can be controlled using the `trap_percentage` option, which defaults to
9 zero.
10- Fixed crash on load when using Python 3.11.
11
12Download:
13[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.3/lingo2.apworld)<br/>
14Template YAML:
15[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v4.3/Lingo%202.yaml)<br/>
16Source:
17[v4.3](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v4.3)
18
19## v3.2 - 2025-09-12
20
21- Initial release for testing. Features include door shuffle, letter shuffle,
22 and symbol shuffle.
23
24Download:
25[lingo2.apworld](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/lingo2.apworld)<br/>
26Template YAML:
27[Lingo 2.yaml](https://files.fourisland.com/releases/lingo2-archipelago/apworld/v3.2/Lingo%202.yaml)<br/>
28Source:
29[v3.2](https://code.fourisland.com/lingo2-archipelago/tag/?h=apworld-v3.2)
diff --git a/apworld/README.md b/apworld/README.md new file mode 100644 index 0000000..13374b2 --- /dev/null +++ b/apworld/README.md
@@ -0,0 +1,48 @@
1# Lingo 2 Apworld
2
3The Lingo 2 Apworld allows you to generate Archipelago Multiworlds containing
4Lingo 2.
5
6## Installation
7
81. Download the Lingo 2 Apworld from
9 [the releases page](https://code.fourisland.com/lingo2-archipelago/about/apworld/CHANGELOG.md).
102. If you do not already have it, download and install the
11 [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/).
123. Double click on `lingo2.apworld` to install it, or copy it manually to the
13 `custom_worlds` folder of your Archipelago installation.
14
15## Running from source
16
17The apworld is mostly written in Python, which does not need to be compiled.
18However, there are two files that need to be generated before the apworld can be
19used.
20
21The first file is `data.binpb`, the datafile containing the randomizer logic.
22You can read about how to generate it on
23[its own README page](https://code.fourisland.com/lingo2-archipelago/about/data/README.md).
24Once you have it, put it in a subfolder of `apworld` called `generated`.
25
26The second generated file is `data_pb2.py`. This file allows Archipelago to read
27the datafile. We use `protoc`, the Protocol Buffer compiler, to generate it. As
28of 0.6.3, Archipelago has protobuf 3.20.3 packaged with it, which means we need
29to compile our proto file with a similar version.
30
31If you followed the steps to generate `data.binpb` and compiled the `datapacker`
32tool yourself, you will already have protobuf version 3.21.12 installed through
33vcpkg. You can then run a command similar to this in order to generate the
34python file.
35
36```shell
37.\out\build\x64-Debug\vcpkg_installed\x64-windows\tools\protobuf\protoc.exe -Iproto\ ^
38 --python_out=apworld\generated\ .\proto\data.proto
39```
40
41The exact path to `protoc.exe` is going to depend on where vcpkg installed its
42packages. The above location is where Visual Studio will probably put it.
43
44After generating those two files, the apworld should be functional. You can copy
45it into an Archipelago source tree (rename the folder `apworld` to `lingo2` if
46you do so) if you want to edit/debug the code. Otherwise, you can zip up the
47folder and rename it to `lingo2.apworld` in order to package it for
48distribution.
diff --git a/apworld/__init__.py b/apworld/__init__.py index 7ebdf56..8b2e42e 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -1,18 +1,27 @@
1""" 1"""
2Archipelago init file for Lingo 2 2Archipelago init file for Lingo 2
3""" 3"""
4from BaseClasses import ItemClassification, Item 4from BaseClasses import ItemClassification, Item, Tutorial
5from worlds.AutoWorld import WebWorld, World 5from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item 6from .items import Lingo2Item, ANTI_COLLECTABLE_TRAPS
7from .options import Lingo2Options 7from .options import Lingo2Options
8from .player_logic import Lingo2PlayerLogic 8from .player_logic import Lingo2PlayerLogic
9from .regions import create_regions 9from .regions import create_regions
10from .static_logic import Lingo2StaticLogic 10from .static_logic import Lingo2StaticLogic
11from .version import APWORLD_VERSION
11 12
12 13
13class Lingo2WebWorld(WebWorld): 14class Lingo2WebWorld(WebWorld):
14 rich_text_options_doc = True 15 rich_text_options_doc = True
15 theme = "grass" 16 theme = "grass"
17 tutorials = [Tutorial(
18 "Multiworld Setup Guide",
19 "A guide to playing Lingo 2 with Archipelago.",
20 "English",
21 "en_Lingo_2.md",
22 "setup/en",
23 ["hatkirby"]
24 )]
16 25
17 26
18class Lingo2World(World): 27class Lingo2World(World):
@@ -32,6 +41,8 @@ class Lingo2World(World):
32 static_logic = Lingo2StaticLogic() 41 static_logic = Lingo2StaticLogic()
33 item_name_to_id = static_logic.item_name_to_id 42 item_name_to_id = static_logic.item_name_to_id
34 location_name_to_id = static_logic.location_name_to_id 43 location_name_to_id = static_logic.location_name_to_id
44 item_name_groups = static_logic.item_name_groups
45 location_name_groups = static_logic.location_name_groups
35 46
36 player_logic: Lingo2PlayerLogic 47 player_logic: Lingo2PlayerLogic
37 48
@@ -51,13 +62,29 @@ class Lingo2World(World):
51 total_locations = sum(len(locs) for locs in self.player_logic.locations_by_room.values()) 62 total_locations = sum(len(locs) for locs in self.player_logic.locations_by_room.values())
52 63
53 item_difference = total_locations - len(pool) 64 item_difference = total_locations - len(pool)
65
66 if self.options.trap_percentage > 0:
67 num_traps = int(item_difference * self.options.trap_percentage / 100)
68 item_difference = item_difference - num_traps
69
70 trap_names = []
71 trap_weights = []
72 for letter_name, weight in self.static_logic.letter_weights.items():
73 trap_names.append(f"Anti {letter_name}")
74 trap_weights.append(weight)
75
76 bad_letters = self.random.choices(trap_names, weights=trap_weights, k=num_traps)
77 pool += [self.create_item(trap_name) for trap_name in bad_letters]
78
54 for i in range(0, item_difference): 79 for i in range(0, item_difference):
55 pool.append(self.create_item("Nothing")) 80 pool.append(self.create_item(self.get_filler_item_name()))
56 81
57 self.multiworld.itempool += pool 82 self.multiworld.itempool += pool
58 83
59 def create_item(self, name: str) -> Item: 84 def create_item(self, name: str) -> Item:
60 return Lingo2Item(name, ItemClassification.filler if name == "Nothing" else ItemClassification.progression, 85 return Lingo2Item(name, ItemClassification.filler if name == self.get_filler_item_name() else
86 ItemClassification.trap if name in ANTI_COLLECTABLE_TRAPS else
87 ItemClassification.progression,
61 self.item_name_to_id.get(name), self.player) 88 self.item_name_to_id.get(name), self.player)
62 89
63 def set_rules(self): 90 def set_rules(self):
@@ -65,14 +92,23 @@ class Lingo2World(World):
65 92
66 def fill_slot_data(self): 93 def fill_slot_data(self):
67 slot_options = [ 94 slot_options = [
95 "cyan_door_behavior",
68 "daedalus_roof_access", 96 "daedalus_roof_access",
69 "keyholder_sanity", 97 "keyholder_sanity",
98 "shuffle_control_center_colors",
70 "shuffle_doors", 99 "shuffle_doors",
100 "shuffle_gallery_paintings",
101 "shuffle_letters",
102 "shuffle_symbols",
71 "victory_condition", 103 "victory_condition",
72 ] 104 ]
73 105
74 slot_data = { 106 slot_data = {
75 **self.options.as_dict(*slot_options), 107 **self.options.as_dict(*slot_options),
108 "version": [self.static_logic.get_data_version(), APWORLD_VERSION],
76 } 109 }
77 110
78 return slot_data 111 return slot_data
112
113 def get_filler_item_name(self) -> str:
114 return "A Job Well Done"
diff --git a/apworld/docs/en_Lingo_2.md b/apworld/docs/en_Lingo_2.md new file mode 100644 index 0000000..977795a --- /dev/null +++ b/apworld/docs/en_Lingo_2.md
@@ -0,0 +1,4 @@
1# Lingo 2
2
3See [the project README](https://code.fourisland.com/lingo2-archipelago/about/)
4for installation instructions and frequently asked questions. \ No newline at end of file
diff --git a/apworld/items.py b/apworld/items.py index 971a709..28158c3 100644 --- a/apworld/items.py +++ b/apworld/items.py
@@ -1,5 +1,31 @@
1from .generated import data_pb2 as data_pb2
1from BaseClasses import Item 2from BaseClasses import Item
2 3
3 4
4class Lingo2Item(Item): 5class Lingo2Item(Item):
5 game: str = "Lingo 2" 6 game: str = "Lingo 2"
7
8
9SYMBOL_ITEMS: dict[data_pb2.PuzzleSymbol, str] = {
10 data_pb2.PuzzleSymbol.SUN: "Sun Symbol",
11 data_pb2.PuzzleSymbol.SPARKLES: "Sparkles Symbol",
12 data_pb2.PuzzleSymbol.ZERO: "Zero Symbol",
13 data_pb2.PuzzleSymbol.EXAMPLE: "Example Symbol",
14 data_pb2.PuzzleSymbol.BOXES: "Boxes Symbol",
15 data_pb2.PuzzleSymbol.PLANET: "Planet Symbol",
16 data_pb2.PuzzleSymbol.PYRAMID: "Pyramid Symbol",
17 data_pb2.PuzzleSymbol.CROSS: "Cross Symbol",
18 data_pb2.PuzzleSymbol.SWEET: "Sweet Symbol",
19 data_pb2.PuzzleSymbol.GENDER: "Gender Symbol",
20 data_pb2.PuzzleSymbol.AGE: "Age Symbol",
21 data_pb2.PuzzleSymbol.SOUND: "Sound Symbol",
22 data_pb2.PuzzleSymbol.ANAGRAM: "Anagram Symbol",
23 data_pb2.PuzzleSymbol.JOB: "Job Symbol",
24 data_pb2.PuzzleSymbol.STARS: "Stars Symbol",
25 data_pb2.PuzzleSymbol.NULL: "Null Symbol",
26 data_pb2.PuzzleSymbol.EVAL: "Eval Symbol",
27 data_pb2.PuzzleSymbol.LINGO: "Lingo Symbol",
28 data_pb2.PuzzleSymbol.QUESTION: "Question Symbol",
29}
30
31ANTI_COLLECTABLE_TRAPS: list[str] = [f"Anti {letter}" for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
diff --git a/apworld/options.py b/apworld/options.py index 3216dff..52d2034 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -1,13 +1,57 @@
1from dataclasses import dataclass 1from dataclasses import dataclass
2 2
3from Options import PerGameCommonOptions, Toggle, Choice 3from Options import PerGameCommonOptions, Toggle, Choice, DefaultOnToggle, Range
4 4
5 5
6class ShuffleDoors(Toggle): 6class ShuffleDoors(DefaultOnToggle):
7 """If enabled, most doors will open from receiving an item rather than fulfilling the in-game requirements.""" 7 """If enabled, most doors will open from receiving an item rather than fulfilling the in-game requirements."""
8 display_name = "Shuffle Doors" 8 display_name = "Shuffle Doors"
9 9
10 10
11class ShuffleControlCenterColors(Toggle):
12 """
13 Some doors open after solving the COLOR panel in the Control Center. If this option is enabled, these doors will
14 instead open upon receiving an item.
15 """
16 display_name = "Shuffle Control Center Colors"
17
18
19class ShuffleGalleryPaintings(Toggle):
20 """If enabled, gallery paintings will appear from receiving an item rather than by triggering them normally."""
21 display_name = "Shuffle Gallery Paintings"
22
23
24class ShuffleLetters(Choice):
25 """
26 Controls how letter unlocks are handled. Note that H1, I1, N1, and T1 will always be present at their vanilla
27 locations in the starting room, even if letters are shuffled remotely.
28
29 - **Vanilla**: All letters will be present at their vanilla locations.
30 - **Unlocked**: Players will start with their keyboards fully unlocked.
31 - **Progressive**: Two items will be added to the pool for every letter (one for H, I, N, and T). Receiving the
32 first item gives you the corresponding level 1 letter, and the second item gives you the corresponding level 2
33 letter.
34 - **Vanilla Cyan**: Players will start with all level 1 (purple) letters unlocked. Level 2 (cyan) letters will be
35 present at their vanilla locations.
36 - **Item Cyan**: Players will start with all level 1 (purple) letters unlocked. One item will be added to the pool
37 for every level 2 (cyan) letter.
38 """
39 display_name = "Shuffle Letters"
40 option_vanilla = 0
41 option_unlocked = 1
42 option_progressive = 2
43 option_vanilla_cyan = 3
44 option_item_cyan = 4
45
46
47class ShuffleSymbols(Toggle):
48 """
49 If enabled, 19 items will be added to the pool, representing the different symbols that can appear on a panel.
50 Players will be prevented from solving puzzles with symbols on them until all of the required symbols are unlocked.
51 """
52 display_name = "Shuffle Symbols"
53
54
11class KeyholderSanity(Toggle): 55class KeyholderSanity(Toggle):
12 """ 56 """
13 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder. 57 If enabled, 26 locations will be created for placing each key into its respective Green Ending keyholder.
@@ -17,6 +61,27 @@ class KeyholderSanity(Toggle):
17 display_name = "Keyholder Sanity" 61 display_name = "Keyholder Sanity"
18 62
19 63
64class CyanDoorBehavior(Choice):
65 """
66 Cyan-colored doors usually only open upon unlocking double letters. Some panels also only appear upon unlocking
67 double letters. This option determines how these unlocks should behave.
68
69 - **Collect H2**: In the base game, H2 is the first double letter you are intended to collect, so cyan doors only
70 open when you collect the H2 pickup in The Repetitive. Collecting the actual pickup is still required even with
71 remote letter shuffle enabled.
72 - **Any Double Letter**: Cyan doors will open when you have unlocked any cyan letter on your keyboard. In letter
73 shuffle, this means receiving a cyan letter, not picking up a cyan letter collectable.
74 - **Item**: Cyan doors will be grouped together in a single item.
75
76 Note that some cyan doors are impacted by door shuffle (e.g. the entrance to The Tower). When door shuffle is
77 enabled, these doors won't be affected by the value of this option.
78 """
79 display_name = "Cyan Door Behavior"
80 option_collect_h2 = 0
81 option_any_double_letter = 1
82 option_item = 2
83
84
20class DaedalusRoofAccess(Toggle): 85class DaedalusRoofAccess(Toggle):
21 """ 86 """
22 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus 87 If enabled, the player will be logically expected to be able to go from the castle entrance to any part of Daedalus
@@ -28,7 +93,23 @@ class DaedalusRoofAccess(Toggle):
28 93
29 94
30class VictoryCondition(Choice): 95class VictoryCondition(Choice):
31 """Victory condition.""" 96 """
97 This option determines what your goal is.
98
99 - **Gray Ending** (The Colorful)
100 - **Purple Ending** (The Sun Temple). This ordinarily requires all level 1 (purple) letters.
101 - **Mint Ending** (typing EXIT into the keyholders in Control Center)
102 - **Black Ending** (The Graveyard)
103 - **Blue Ending** (The Words)
104 - **Cyan Ending** (The Parthenon). This ordinarily requires almost all level 2 (cyan) letters.
105 - **Red Ending** (The Tower)
106 - **Plum Ending** (The Wondrous / The Door)
107 - **Orange Ending** (the castle in Daedalus)
108 - **Gold Ending** (The Gold). This involves going through the color rooms in Daedalus.
109 - **Yellow Ending** (The Gallery). This requires unlocking all gallery paintings.
110 - **Green Ending** (The Ancient). This requires filling all keyholders with specific letters.
111 - **White Ending** (Control Center). This combines every other ending.
112 """
32 display_name = "Victory Condition" 113 display_name = "Victory Condition"
33 option_gray_ending = 0 114 option_gray_ending = 0
34 option_purple_ending = 1 115 option_purple_ending = 1
@@ -45,9 +126,23 @@ class VictoryCondition(Choice):
45 option_white_ending = 12 126 option_white_ending = 12
46 127
47 128
129class TrapPercentage(Range):
130 """Replaces junk items with traps, at the specified rate."""
131 display_name = "Trap Percentage"
132 range_start = 0
133 range_end = 100
134 default = 0
135
136
48@dataclass 137@dataclass
49class Lingo2Options(PerGameCommonOptions): 138class Lingo2Options(PerGameCommonOptions):
50 shuffle_doors: ShuffleDoors 139 shuffle_doors: ShuffleDoors
140 shuffle_control_center_colors: ShuffleControlCenterColors
141 shuffle_gallery_paintings: ShuffleGalleryPaintings
142 shuffle_letters: ShuffleLetters
143 shuffle_symbols: ShuffleSymbols
51 keyholder_sanity: KeyholderSanity 144 keyholder_sanity: KeyholderSanity
145 cyan_door_behavior: CyanDoorBehavior
52 daedalus_roof_access: DaedalusRoofAccess 146 daedalus_roof_access: DaedalusRoofAccess
53 victory_condition: VictoryCondition 147 victory_condition: VictoryCondition
148 trap_percentage: TrapPercentage
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index dc1bdf0..17af77f 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py
@@ -1,7 +1,10 @@
1from enum import IntEnum, auto
2
1from .generated import data_pb2 as data_pb2 3from .generated import data_pb2 as data_pb2
4from .items import SYMBOL_ITEMS
2from typing import TYPE_CHECKING, NamedTuple 5from typing import TYPE_CHECKING, NamedTuple
3 6
4from .options import VictoryCondition 7from .options import VictoryCondition, ShuffleLetters, CyanDoorBehavior
5 8
6if TYPE_CHECKING: 9if TYPE_CHECKING:
7 from . import Lingo2World 10 from . import Lingo2World
@@ -14,10 +17,6 @@ def calculate_letter_histogram(solution: str) -> dict[str, int]:
14 real_l = l.upper() 17 real_l = l.upper()
15 histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2) 18 histogram[real_l] = min(histogram.get(real_l, 0) + 1, 2)
16 19
17 for free_letter in "HINT":
18 if histogram.get(free_letter, 0) == 1:
19 del histogram[free_letter]
20
21 return histogram 20 return histogram
22 21
23 22
@@ -25,25 +24,38 @@ class AccessRequirements:
25 items: set[str] 24 items: set[str]
26 progressives: dict[str, int] 25 progressives: dict[str, int]
27 rooms: set[str] 26 rooms: set[str]
28 symbols: set[str]
29 letters: dict[str, int] 27 letters: dict[str, int]
28 cyans: bool
30 29
31 # This is an AND of ORs. 30 # This is an AND of ORs.
32 or_logic: list[list["AccessRequirements"]] 31 or_logic: list[list["AccessRequirements"]]
33 32
33 # When complete_at is set, at least that many of the requirements in possibilities must be accessible. This should
34 # only be used for doors with complete_at > 1, as or_logic is more efficient for complete_at == 1.
35 complete_at: int | None
36 possibilities: list["AccessRequirements"]
37
34 def __init__(self): 38 def __init__(self):
35 self.items = set() 39 self.items = set()
36 self.progressives = dict() 40 self.progressives = dict()
37 self.rooms = set() 41 self.rooms = set()
38 self.symbols = set()
39 self.letters = dict() 42 self.letters = dict()
43 self.cyans = False
40 self.or_logic = list() 44 self.or_logic = list()
45 self.complete_at = None
46 self.possibilities = list()
41 47
42 def add_solution(self, solution: str): 48 def copy(self) -> "AccessRequirements":
43 histogram = calculate_letter_histogram(solution) 49 reqs = AccessRequirements()
44 50 reqs.items = self.items.copy()
45 for l, a in histogram.items(): 51 reqs.progressives = self.progressives.copy()
46 self.letters[l] = max(self.letters.get(l, 0), histogram.get(l)) 52 reqs.rooms = self.rooms.copy()
53 reqs.letters = self.letters.copy()
54 reqs.cyans = self.cyans
55 reqs.or_logic = [[other_req.copy() for other_req in disjunction] for disjunction in self.or_logic]
56 reqs.complete_at = self.complete_at
57 reqs.possibilities = self.possibilities.copy()
58 return reqs
47 59
48 def merge(self, other: "AccessRequirements"): 60 def merge(self, other: "AccessRequirements"):
49 for item in other.items: 61 for item in other.items:
@@ -55,15 +67,78 @@ class AccessRequirements:
55 for room in other.rooms: 67 for room in other.rooms:
56 self.rooms.add(room) 68 self.rooms.add(room)
57 69
58 for symbol in other.symbols:
59 self.symbols.add(symbol)
60
61 for letter, level in other.letters.items(): 70 for letter, level in other.letters.items():
62 self.letters[letter] = max(self.letters.get(letter, 0), level) 71 self.letters[letter] = max(self.letters.get(letter, 0), level)
63 72
73 self.cyans = self.cyans or other.cyans
74
64 for disjunction in other.or_logic: 75 for disjunction in other.or_logic:
65 self.or_logic.append(disjunction) 76 self.or_logic.append(disjunction)
66 77
78 if other.complete_at is not None:
79 # Merging multiple requirements that use complete_at sucks, and is part of why we want to minimize use of
80 # it. If both requirements use complete_at, we will cheat by using the or_logic field, which supports
81 # conjunctions of requirements.
82 if self.complete_at is not None:
83 print("Merging requirements with complete_at > 1. This is messy and should be avoided!")
84
85 left_req = AccessRequirements()
86 left_req.complete_at = self.complete_at
87 left_req.possibilities = self.possibilities
88 self.or_logic.append([left_req])
89
90 self.complete_at = None
91 self.possibilities = list()
92
93 right_req = AccessRequirements()
94 right_req.complete_at = other.complete_at
95 right_req.possibilities = other.possibilities
96 self.or_logic.append([right_req])
97 else:
98 self.complete_at = other.complete_at
99 self.possibilities = other.possibilities
100
101 def is_empty(self) -> bool:
102 return (len(self.items) == 0 and len(self.progressives) == 0 and len(self.rooms) == 0 and len(self.letters) == 0
103 and not self.cyans and len(self.or_logic) == 0 and self.complete_at is None)
104
105 def __eq__(self, other: "AccessRequirements"):
106 return (self.items == other.items and self.progressives == other.progressives and self.rooms == other.rooms and
107 self.letters == other.letters and self.cyans == other.cyans and self.or_logic == other.or_logic and
108 self.complete_at == other.complete_at and self.possibilities == other.possibilities)
109
110 def simplify(self):
111 resimplify = False
112
113 if len(self.or_logic) > 0:
114 old_or_logic = self.or_logic
115
116 def remove_redundant(sub_reqs: "AccessRequirements"):
117 sub_reqs.letters = {l: v for l, v in sub_reqs.letters.items() if self.letters.get(l, 0) < v}
118
119 self.or_logic = []
120 for disjunction in old_or_logic:
121 new_disjunction = []
122 for ssr in disjunction:
123 remove_redundant(ssr)
124 if not ssr.is_empty():
125 new_disjunction.append(ssr)
126 else:
127 new_disjunction.clear()
128 break
129 if len(new_disjunction) == 1:
130 self.merge(new_disjunction[0])
131 resimplify = True
132 elif len(new_disjunction) > 1:
133 if all(cjr == new_disjunction[0] for cjr in new_disjunction):
134 self.merge(new_disjunction[0])
135 resimplify = True
136 else:
137 self.or_logic.append(new_disjunction)
138
139 if resimplify:
140 self.simplify()
141
67 def __repr__(self): 142 def __repr__(self):
68 parts = [] 143 parts = []
69 if len(self.items) > 0: 144 if len(self.items) > 0:
@@ -72,13 +147,17 @@ class AccessRequirements:
72 parts.append(f"progressives={self.progressives}") 147 parts.append(f"progressives={self.progressives}")
73 if len(self.rooms) > 0: 148 if len(self.rooms) > 0:
74 parts.append(f"rooms={self.rooms}") 149 parts.append(f"rooms={self.rooms}")
75 if len(self.symbols) > 0:
76 parts.append(f"symbols={self.symbols}")
77 if len(self.letters) > 0: 150 if len(self.letters) > 0:
78 parts.append(f"letters={self.letters}") 151 parts.append(f"letters={self.letters}")
152 if self.cyans:
153 parts.append(f"cyans=True")
79 if len(self.or_logic) > 0: 154 if len(self.or_logic) > 0:
80 parts.append(f"or_logic={self.or_logic}") 155 parts.append(f"or_logic={self.or_logic}")
81 return f"AccessRequirements({", ".join(parts)})" 156 if self.complete_at is not None:
157 parts.append(f"complete_at={self.complete_at}")
158 if len(self.possibilities) > 0:
159 parts.append(f"possibilities={self.possibilities}")
160 return "AccessRequirements(" + ", ".join(parts) + ")"
82 161
83 162
84class PlayerLocation(NamedTuple): 163class PlayerLocation(NamedTuple):
@@ -86,6 +165,12 @@ class PlayerLocation(NamedTuple):
86 reqs: AccessRequirements 165 reqs: AccessRequirements
87 166
88 167
168class LetterBehavior(IntEnum):
169 VANILLA = auto()
170 ITEM = auto()
171 UNLOCKED = auto()
172
173
89class Lingo2PlayerLogic: 174class Lingo2PlayerLogic:
90 world: "Lingo2World" 175 world: "Lingo2World"
91 176
@@ -100,6 +185,8 @@ class Lingo2PlayerLogic:
100 185
101 real_items: list[str] 186 real_items: list[str]
102 187
188 double_letter_amount: dict[str, int]
189
103 def __init__(self, world: "Lingo2World"): 190 def __init__(self, world: "Lingo2World"):
104 self.world = world 191 self.world = world
105 self.locations_by_room = {} 192 self.locations_by_room = {}
@@ -109,6 +196,7 @@ class Lingo2PlayerLogic:
109 self.proxy_reqs = dict() 196 self.proxy_reqs = dict()
110 self.door_reqs = dict() 197 self.door_reqs = dict()
111 self.real_items = list() 198 self.real_items = list()
199 self.double_letter_amount = dict()
112 200
113 if self.world.options.shuffle_doors: 201 if self.world.options.shuffle_doors:
114 for progressive in world.static_logic.objects.progressives: 202 for progressive in world.static_logic.objects.progressives:
@@ -116,16 +204,60 @@ class Lingo2PlayerLogic:
116 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1) 204 self.item_by_door[progressive.doors[i]] = (progressive.name, i + 1)
117 self.real_items.append(progressive.name) 205 self.real_items.append(progressive.name)
118 206
207 for door_group in world.static_logic.objects.door_groups:
208 if door_group.type == data_pb2.DoorGroupType.CONNECTOR:
209 if not self.world.options.shuffle_doors:
210 continue
211 elif door_group.type == data_pb2.DoorGroupType.COLOR_CONNECTOR:
212 if not self.world.options.shuffle_control_center_colors:
213 continue
214 elif door_group.type == data_pb2.DoorGroupType.SHUFFLE_GROUP:
215 if not self.world.options.shuffle_doors:
216 continue
217 else:
218 continue
219
220 for door in door_group.doors:
221 self.item_by_door[door] = (door_group.name, 1)
222
223 self.real_items.append(door_group.name)
224
119 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled 225 # We iterate through the doors in two parts because it is essential that we determine which doors are shuffled
120 # before we calculate any access requirements. 226 # before we calculate any access requirements.
121 for door in world.static_logic.objects.doors: 227 for door in world.static_logic.objects.doors:
122 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: 228 if door.type in [data_pb2.DoorType.EVENT, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
123 if door.id in self.item_by_door: 229 continue
230
231 if door.id in self.item_by_door:
232 continue
233
234 if (door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.ITEM_ONLY] and
235 not self.world.options.shuffle_doors):
236 continue
237
238 if (door.type == data_pb2.DoorType.CONTROL_CENTER_COLOR and
239 not self.world.options.shuffle_control_center_colors):
240 continue
241
242 if door.type == data_pb2.DoorType.GALLERY_PAINTING and not self.world.options.shuffle_gallery_paintings:
243 continue
244
245 door_item_name = self.world.static_logic.get_door_item_name(door)
246 self.item_by_door[door.id] = (door_item_name, 1)
247 self.real_items.append(door_item_name)
248
249 # We handle cyan_door_behavior = Item after door shuffle, because cyan doors that are impacted by door shuffle
250 # should be exempt from cyan_door_behavior.
251 if world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
252 for door_group in world.static_logic.objects.door_groups:
253 if door_group.type != data_pb2.DoorGroupType.CYAN_DOORS:
124 continue 254 continue
125 255
126 door_item_name = self.world.static_logic.get_door_item_name(door) 256 for door in door_group.doors:
127 self.item_by_door[door.id] = (door_item_name, 1) 257 if not door in self.item_by_door:
128 self.real_items.append(door_item_name) 258 self.item_by_door[door] = (door_group.name, 1)
259
260 self.real_items.append(door_group.name)
129 261
130 for door in world.static_logic.objects.doors: 262 for door in world.static_logic.objects.doors:
131 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]: 263 if door.type in [data_pb2.DoorType.STANDARD, data_pb2.DoorType.LOCATION_ONLY, data_pb2.DoorType.GRAVESTONE]:
@@ -135,14 +267,20 @@ class Lingo2PlayerLogic:
135 for letter in world.static_logic.objects.letters: 267 for letter in world.static_logic.objects.letters:
136 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, 268 self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id,
137 AccessRequirements())) 269 AccessRequirements()))
270 behavior = self.get_letter_behavior(letter.key, letter.level2)
271 if behavior == LetterBehavior.VANILLA:
272 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
273 event_name = f"{letter_name} (Collected)"
274 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
138 275
139 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 276 if letter.level2:
140 event_name = f"{letter_name} (Collected)" 277 event_name = f"{letter_name} (Double Collected)"
141 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper() 278 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
279 elif behavior == LetterBehavior.ITEM:
280 self.real_items.append(letter.key.upper())
142 281
143 if letter.level2: 282 if behavior != LetterBehavior.UNLOCKED:
144 event_name = f"{letter_name} (Double Collected)" 283 self.double_letter_amount[letter.key.upper()] = self.double_letter_amount.get(letter.key.upper(), 0) + 1
145 self.event_loc_item_by_room.setdefault(letter.room_id, {})[event_name] = letter.key.upper()
146 284
147 for mastery in world.static_logic.objects.masteries: 285 for mastery in world.static_logic.objects.masteries:
148 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, 286 self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id,
@@ -170,11 +308,17 @@ class Lingo2PlayerLogic:
170 for keyholder in world.static_logic.objects.keyholders: 308 for keyholder in world.static_logic.objects.keyholders:
171 if keyholder.HasField("key"): 309 if keyholder.HasField("key"):
172 reqs = AccessRequirements() 310 reqs = AccessRequirements()
173 reqs.letters[keyholder.key.upper()] = 1 311
312 if self.get_letter_behavior(keyholder.key, False) != LetterBehavior.UNLOCKED:
313 reqs.letters[keyholder.key.upper()] = 1
174 314
175 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id, 315 self.locations_by_room.setdefault(keyholder.room_id, []).append(PlayerLocation(keyholder.ap_id,
176 reqs)) 316 reqs))
177 317
318 if self.world.options.shuffle_symbols:
319 for symbol_name in SYMBOL_ITEMS.values():
320 self.real_items.append(symbol_name)
321
178 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: 322 def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements:
179 if answer is None: 323 if answer is None:
180 if panel_id not in self.panel_reqs: 324 if panel_id not in self.panel_reqs:
@@ -194,28 +338,38 @@ class Lingo2PlayerLogic:
194 reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id)) 338 reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id))
195 339
196 if answer is not None: 340 if answer is not None:
197 reqs.add_solution(answer) 341 self.add_solution_reqs(reqs, answer)
198 elif len(panel.proxies) > 0: 342 elif len(panel.proxies) > 0:
199 possibilities = [] 343 possibilities = []
344 already_filled = False
200 345
201 for proxy in panel.proxies: 346 for proxy in panel.proxies:
202 proxy_reqs = AccessRequirements() 347 proxy_reqs = AccessRequirements()
203 proxy_reqs.add_solution(proxy.answer) 348 self.add_solution_reqs(proxy_reqs, proxy.answer)
204 349
205 possibilities.append(proxy_reqs) 350 if not proxy_reqs.is_empty():
351 possibilities.append(proxy_reqs)
352 else:
353 already_filled = True
354 break
206 355
207 if not any(proxy.answer == panel.answer for proxy in panel.proxies): 356 if not already_filled and not any(proxy.answer == panel.answer for proxy in panel.proxies):
208 proxy_reqs = AccessRequirements() 357 proxy_reqs = AccessRequirements()
209 proxy_reqs.add_solution(panel.answer) 358 self.add_solution_reqs(proxy_reqs, panel.answer)
210 359
211 possibilities.append(proxy_reqs) 360 if not proxy_reqs.is_empty():
361 possibilities.append(proxy_reqs)
362 else:
363 already_filled = True
212 364
213 reqs.or_logic.append(possibilities) 365 if not already_filled:
366 reqs.or_logic.append(possibilities)
214 else: 367 else:
215 reqs.add_solution(panel.answer) 368 self.add_solution_reqs(reqs, panel.answer)
216 369
217 for symbol in panel.symbols: 370 if self.world.options.shuffle_symbols:
218 reqs.symbols.add(symbol) 371 for symbol in panel.symbols:
372 reqs.items.add(SYMBOL_ITEMS.get(symbol))
219 373
220 if panel.HasField("required_door"): 374 if panel.HasField("required_door"):
221 door_reqs = self.get_door_open_reqs(panel.required_door) 375 door_reqs = self.get_door_open_reqs(panel.required_door)
@@ -238,31 +392,46 @@ class Lingo2PlayerLogic:
238 door = self.world.static_logic.objects.doors[door_id] 392 door = self.world.static_logic.objects.doors[door_id]
239 reqs = AccessRequirements() 393 reqs = AccessRequirements()
240 394
241 # TODO: lavender_cubes, endings
242 if not door.HasField("complete_at") or door.complete_at == 0: 395 if not door.HasField("complete_at") or door.complete_at == 0:
243 for proxy in door.panels: 396 for proxy in door.panels:
244 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) 397 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
245 reqs.merge(panel_reqs) 398 reqs.merge(panel_reqs)
246 elif door.complete_at == 1: 399 elif door.complete_at == 1:
247 reqs.or_logic.append([self.get_panel_reqs(proxy.panel, 400 disjunction = []
248 proxy.answer if proxy.HasField("answer") else None) 401 for proxy in door.panels:
249 for proxy in door.panels]) 402 proxy_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
403 if proxy_reqs.is_empty():
404 disjunction.clear()
405 break
406 else:
407 disjunction.append(proxy_reqs)
408 if len(disjunction) > 0:
409 reqs.or_logic.append(disjunction)
250 else: 410 else:
251 # TODO: Handle complete_at > 1 411 reqs.complete_at = door.complete_at
252 pass 412 for proxy in door.panels:
413 panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None)
414 reqs.possibilities.append(panel_reqs)
253 415
254 if door.HasField("control_center_color"): 416 if door.HasField("control_center_color"):
255 # TODO: Logic for ensuring two CC states aren't needed at once. 417 # TODO: Logic for ensuring two CC states aren't needed at once.
256 reqs.rooms.add("Control Center - Main Area") 418 reqs.rooms.add("Control Center - Main Area")
257 reqs.add_solution(door.control_center_color) 419 self.add_solution_reqs(reqs, door.control_center_color)
258 420
259 if door.double_letters: 421 if door.double_letters:
260 # TODO: When letter shuffle is on, change this to require any double letter instead. 422 if self.world.options.cyan_door_behavior == CyanDoorBehavior.option_collect_h2:
261 reqs.rooms.add("The Repetitive - Main Room") 423 reqs.rooms.add("The Repetitive - Main Room")
424 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_any_double_letter:
425 if self.world.options.shuffle_letters != ShuffleLetters.option_unlocked:
426 reqs.cyans = True
427 elif self.world.options.cyan_door_behavior == CyanDoorBehavior.option_item:
428 # There shouldn't be any locations that are cyan doors.
429 pass
262 430
263 for keyholder_uses in door.keyholders: 431 for keyholder_uses in door.keyholders:
264 key_name = keyholder_uses.key.upper() 432 key_name = keyholder_uses.key.upper()
265 if key_name not in reqs.letters: 433 if (self.get_letter_behavior(keyholder_uses.key, False) != LetterBehavior.UNLOCKED
434 and key_name not in reqs.letters):
266 reqs.letters[key_name] = 1 435 reqs.letters[key_name] = 1
267 436
268 keyholder = self.world.static_logic.objects.keyholders[keyholder_uses.keyholder] 437 keyholder = self.world.static_logic.objects.keyholders[keyholder_uses.keyholder]
@@ -273,12 +442,18 @@ class Lingo2PlayerLogic:
273 442
274 for ending_id in door.endings: 443 for ending_id in door.endings:
275 ending = self.world.static_logic.objects.endings[ending_id] 444 ending = self.world.static_logic.objects.endings[ending_id]
276 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)") 445
446 if self.world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name:
447 reqs.items.add("Victory")
448 else:
449 reqs.items.add(f"{ending.name.capitalize()} Ending (Achieved)")
277 450
278 for sub_door_id in door.doors: 451 for sub_door_id in door.doors:
279 sub_reqs = self.get_door_open_reqs(sub_door_id) 452 sub_reqs = self.get_door_open_reqs(sub_door_id)
280 reqs.merge(sub_reqs) 453 reqs.merge(sub_reqs)
281 454
455 reqs.simplify()
456
282 return reqs 457 return reqs
283 458
284 # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item 459 # This gets the requirements to open a door within the world. When a door is shuffled, this means having the item
@@ -296,3 +471,40 @@ class Lingo2PlayerLogic:
296 return reqs 471 return reqs
297 else: 472 else:
298 return self.get_door_reqs(door_id) 473 return self.get_door_reqs(door_id)
474
475 def get_letter_behavior(self, letter: str, level2: bool) -> LetterBehavior:
476 if self.world.options.shuffle_letters == ShuffleLetters.option_unlocked:
477 return LetterBehavior.UNLOCKED
478
479 if self.world.options.shuffle_letters in [ShuffleLetters.option_vanilla_cyan, ShuffleLetters.option_item_cyan]:
480 if level2:
481 if self.world.options.shuffle_letters == ShuffleLetters.option_vanilla_cyan:
482 return LetterBehavior.VANILLA
483 else:
484 return LetterBehavior.ITEM
485 else:
486 return LetterBehavior.UNLOCKED
487
488 if not level2 and letter in ["h", "i", "n", "t"]:
489 return LetterBehavior.UNLOCKED
490
491 if self.world.options.shuffle_letters == ShuffleLetters.option_progressive:
492 return LetterBehavior.ITEM
493
494 return LetterBehavior.VANILLA
495
496 def add_solution_reqs(self, reqs: AccessRequirements, solution: str):
497 histogram = calculate_letter_histogram(solution)
498
499 for l, a in histogram.items():
500 needed = min(a, 2)
501 level2 = (needed == 2)
502
503 if level2 and self.get_letter_behavior(l.lower(), True) == LetterBehavior.UNLOCKED:
504 needed = 1
505
506 if self.get_letter_behavior(l.lower(), False) == LetterBehavior.UNLOCKED:
507 needed = needed - 1
508
509 if needed > 0:
510 reqs.letters[l] = max(reqs.letters.get(l, 0), needed)
diff --git a/apworld/regions.py b/apworld/regions.py index e30493c..4f1dd55 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -11,12 +11,18 @@ if TYPE_CHECKING:
11 11
12 12
13def create_region(room, world: "Lingo2World") -> Region: 13def create_region(room, world: "Lingo2World") -> Region:
14 new_region = Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld) 14 return Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld)
15 15
16
17def create_locations(room, new_region: Region, world: "Lingo2World", regions: dict[str, Region]):
16 for location in world.player_logic.locations_by_room.get(room.id, {}): 18 for location in world.player_logic.locations_by_room.get(room.id, {}):
19 reqs = location.reqs.copy()
20 if new_region.name in reqs.rooms:
21 reqs.rooms.remove(new_region.name)
22
17 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 23 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
18 location.code, new_region) 24 location.code, new_region)
19 new_location.access_rule = make_location_lambda(location.reqs, world) 25 new_location.access_rule = make_location_lambda(reqs, world, regions)
20 new_region.locations.append(new_location) 26 new_region.locations.append(new_location)
21 27
22 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items(): 28 for event_name, item_name in world.player_logic.event_loc_item_by_room.get(room.id, {}).items():
@@ -25,17 +31,23 @@ def create_region(room, world: "Lingo2World") -> Region:
25 new_location.place_locked_item(event_item) 31 new_location.place_locked_item(event_item)
26 new_region.locations.append(new_location) 32 new_region.locations.append(new_location)
27 33
28 return new_region
29
30
31def create_regions(world: "Lingo2World"): 34def create_regions(world: "Lingo2World"):
32 regions = { 35 regions = {
33 "Menu": Region("Menu", world.player, world.multiworld) 36 "Menu": Region("Menu", world.player, world.multiworld)
34 } 37 }
35 38
39 region_and_room = []
40
41 # Create the regions in two stages. First, make the actual region objects and memoize them. Then, add all of the
42 # locations. This allows us to reference the actual region objects in the access rules for the locations, which is
43 # faster than having to look them up during access checking.
36 for room in world.static_logic.objects.rooms: 44 for room in world.static_logic.objects.rooms:
37 region = create_region(room, world) 45 region = create_region(room, world)
38 regions[region.name] = region 46 regions[region.name] = region
47 region_and_room.append((region, room))
48
49 for (region, room) in region_and_room:
50 create_locations(room, region, world, regions)
39 51
40 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") 52 regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game")
41 53
@@ -82,14 +94,18 @@ def create_regions(world: "Lingo2World"):
82 else: 94 else:
83 connection_name = f"{connection_name} (via panel {panel.name})" 95 connection_name = f"{connection_name} (via panel {panel.name})"
84 96
97 reqs.simplify()
98
85 if from_region in regions and to_region in regions: 99 if from_region in regions and to_region in regions:
86 connection = Entrance(world.player, connection_name, regions[from_region]) 100 connection = Entrance(world.player, connection_name, regions[from_region])
87 connection.access_rule = make_location_lambda(reqs, world) 101 connection.access_rule = make_location_lambda(reqs, world, regions)
88 102
89 regions[from_region].exits.append(connection) 103 regions[from_region].exits.append(connection)
90 connection.connect(regions[to_region]) 104 connection.connect(regions[to_region])
91 105
92 for region in reqs.rooms: 106 for region in reqs.rooms:
107 if region == from_region:
108 continue
93 world.multiworld.register_indirect_condition(regions[region], connection) 109 world.multiworld.register_indirect_condition(regions[region], connection)
94 110
95 world.multiworld.regions += regions.values() 111 world.multiworld.regions += regions.values()
diff --git a/apworld/requirements.txt b/apworld/requirements.txt index b701d11..dbc395b 100644 --- a/apworld/requirements.txt +++ b/apworld/requirements.txt
@@ -1 +1 @@
protobuf>=5.29.3 \ No newline at end of file protobuf==3.20.3
diff --git a/apworld/rules.py b/apworld/rules.py index 5e20de5..c077858 100644 --- a/apworld/rules.py +++ b/apworld/rules.py
@@ -1,14 +1,15 @@
1from collections.abc import Callable 1from collections.abc import Callable
2from typing import TYPE_CHECKING 2from typing import TYPE_CHECKING
3 3
4from BaseClasses import CollectionState 4from BaseClasses import CollectionState, Region
5from .player_logic import AccessRequirements 5from .player_logic import AccessRequirements
6 6
7if TYPE_CHECKING: 7if TYPE_CHECKING:
8 from . import Lingo2World 8 from . import Lingo2World
9 9
10 10
11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, world: "Lingo2World") -> bool: 11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, regions: list[Region],
12 world: "Lingo2World") -> bool:
12 if not all(state.has(item, world.player) for item in reqs.items): 13 if not all(state.has(item, world.player) for item in reqs.items):
13 return False 14 return False
14 15
@@ -18,18 +19,45 @@ def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirem
18 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms): 19 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
19 return False 20 return False
20 21
21 # TODO: symbols 22 if not all(state.can_reach(region) for region in regions):
23 return False
22 24
23 for letter_key, letter_level in reqs.letters.items(): 25 for letter_key, letter_level in reqs.letters.items():
24 if not state.has(letter_key, world.player, letter_level): 26 if not state.has(letter_key, world.player, letter_level):
25 return False 27 return False
26 28
29 if reqs.cyans:
30 if not any(state.has(letter, world.player, amount)
31 for letter, amount in world.player_logic.double_letter_amount.items()):
32 return False
33
27 if len(reqs.or_logic) > 0: 34 if len(reqs.or_logic) > 0:
28 if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, world) for sub_reqs in subjunction) 35 if not all(any(lingo2_can_satisfy_requirements(state, sub_reqs, [], world) for sub_reqs in subjunction)
29 for subjunction in reqs.or_logic): 36 for subjunction in reqs.or_logic):
30 return False 37 return False
31 38
39 if reqs.complete_at is not None:
40 completed = 0
41 checked = 0
42 for possibility in reqs.possibilities:
43 checked += 1
44 if lingo2_can_satisfy_requirements(state, possibility, [], world):
45 completed += 1
46 if completed >= reqs.complete_at:
47 break
48 elif len(reqs.possibilities) - checked + completed < reqs.complete_at:
49 # There aren't enough remaining possibilities for the check to pass.
50 return False
51 if completed < reqs.complete_at:
52 return False
53
32 return True 54 return True
33 55
34def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World") -> Callable[[CollectionState], bool]: 56def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World",
35 return lambda state: lingo2_can_satisfy_requirements(state, reqs, world) 57 regions: dict[str, Region]) -> Callable[[CollectionState], bool]:
58 # Replace required rooms with regions for the top level requirement, which saves looking up the regions during rule
59 # checking.
60 required_regions = [regions[room_name] for room_name in reqs.rooms]
61 new_reqs = reqs.copy()
62 new_reqs.rooms.clear()
63 return lambda state: lingo2_can_satisfy_requirements(state, new_reqs, required_regions, world)
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index a945bc0..e4d7d49 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -1,4 +1,5 @@
1from .generated import data_pb2 as data_pb2 1from .generated import data_pb2 as data_pb2
2from .items import SYMBOL_ITEMS, ANTI_COLLECTABLE_TRAPS
2import pkgutil 3import pkgutil
3 4
4class Lingo2StaticLogic: 5class Lingo2StaticLogic:
@@ -8,9 +9,17 @@ class Lingo2StaticLogic:
8 item_name_to_id: dict[str, int] 9 item_name_to_id: dict[str, int]
9 location_name_to_id: dict[str, int] 10 location_name_to_id: dict[str, int]
10 11
12 item_name_groups: dict[str, list[str]]
13 location_name_groups: dict[str, list[str]]
14
15 letter_weights: dict[str, int]
16
11 def __init__(self): 17 def __init__(self):
12 self.item_id_to_name = {} 18 self.item_id_to_name = {}
13 self.location_id_to_name = {} 19 self.location_id_to_name = {}
20 self.item_name_groups = {}
21 self.location_name_groups = {}
22 self.letter_weights = {}
14 23
15 file = pkgutil.get_data(__name__, "generated/data.binpb") 24 file = pkgutil.get_data(__name__, "generated/data.binpb")
16 self.objects = data_pb2.AllObjects() 25 self.objects = data_pb2.AllObjects()
@@ -29,31 +38,49 @@ class Lingo2StaticLogic:
29 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}" 38 letter_name = f"{letter.key.upper()}{'2' if letter.level2 else '1'}"
30 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}" 39 location_name = f"{self.get_room_object_map_name(letter)} - {letter_name}"
31 self.location_id_to_name[letter.ap_id] = location_name 40 self.location_id_to_name[letter.ap_id] = location_name
41 self.location_name_groups.setdefault("Letters", []).append(location_name)
32 42
33 if not letter.level2: 43 if not letter.level2:
34 self.item_id_to_name[letter.ap_id] = letter_name 44 self.item_id_to_name[letter.ap_id] = letter.key.upper()
45 self.item_name_groups.setdefault("Letters", []).append(letter.key.upper())
35 46
36 for mastery in self.objects.masteries: 47 for mastery in self.objects.masteries:
37 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery" 48 location_name = f"{self.get_room_object_map_name(mastery)} - Mastery"
38 self.location_id_to_name[mastery.ap_id] = location_name 49 self.location_id_to_name[mastery.ap_id] = location_name
50 self.location_name_groups.setdefault("Masteries", []).append(location_name)
39 51
40 for ending in self.objects.endings: 52 for ending in self.objects.endings:
41 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending" 53 location_name = f"{self.get_room_object_map_name(ending)} - {ending.name.title()} Ending"
42 self.location_id_to_name[ending.ap_id] = location_name 54 self.location_id_to_name[ending.ap_id] = location_name
55 self.location_name_groups.setdefault("Endings", []).append(location_name)
43 56
44 for progressive in self.objects.progressives: 57 for progressive in self.objects.progressives:
45 self.item_id_to_name[progressive.ap_id] = progressive.name 58 self.item_id_to_name[progressive.ap_id] = progressive.name
46 59
60 for door_group in self.objects.door_groups:
61 self.item_id_to_name[door_group.ap_id] = door_group.name
62
47 for keyholder in self.objects.keyholders: 63 for keyholder in self.objects.keyholders:
48 if keyholder.HasField("key"): 64 if keyholder.HasField("key"):
49 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder" 65 location_name = f"{self.get_room_object_location_prefix(keyholder)} - {keyholder.key.upper()} Keyholder"
50 self.location_id_to_name[keyholder.ap_id] = location_name 66 self.location_id_to_name[keyholder.ap_id] = location_name
67 self.location_name_groups.setdefault("Keyholders", []).append(location_name)
68
69 self.item_id_to_name[self.objects.special_ids["A Job Well Done"]] = "A Job Well Done"
51 70
52 self.item_id_to_name[self.objects.special_ids["Nothing"]] = "Nothing" 71 for symbol_name in SYMBOL_ITEMS.values():
72 self.item_id_to_name[self.objects.special_ids[symbol_name]] = symbol_name
73
74 for trap_name in ANTI_COLLECTABLE_TRAPS:
75 self.item_id_to_name[self.objects.special_ids[trap_name]] = trap_name
53 76
54 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} 77 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()}
55 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} 78 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()}
56 79
80 for panel in self.objects.panels:
81 for letter in panel.answer.upper():
82 self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1
83
57 def get_door_item_name(self, door: data_pb2.Door) -> str: 84 def get_door_item_name(self, door: data_pb2.Door) -> str:
58 return f"{self.get_map_object_map_name(door)} - {door.name}" 85 return f"{self.get_map_object_map_name(door)} - {door.name}"
59 86
@@ -77,7 +104,7 @@ class Lingo2StaticLogic:
77 if door.type != data_pb2.DoorType.STANDARD: 104 if door.type != data_pb2.DoorType.STANDARD:
78 return None 105 return None
79 106
80 if len(door.keyholders) > 0 or len(door.endings) > 0: 107 if len(door.keyholders) > 0 or len(door.endings) > 0 or door.HasField("complete_at"):
81 return None 108 return None
82 109
83 if len(door.panels) > 4: 110 if len(door.panels) > 4:
@@ -113,7 +140,7 @@ class Lingo2StaticLogic:
113 for panel_id in door.panels] 140 for panel_id in door.panels]
114 panel_names.sort() 141 panel_names.sort()
115 142
116 return f"{map_part} - {", ".join(panel_names)}" 143 return map_part + " - " + ", ".join(panel_names)
117 144
118 def get_door_location_name_by_id(self, door_id: int) -> str: 145 def get_door_location_name_by_id(self, door_id: int) -> str:
119 door = self.objects.doors[door_id] 146 door = self.objects.doors[door_id]
@@ -137,3 +164,6 @@ class Lingo2StaticLogic:
137 return f"{game_map.display_name} ({room.panel_display_name})" 164 return f"{game_map.display_name} ({room.panel_display_name})"
138 else: 165 else:
139 return game_map.display_name 166 return game_map.display_name
167
168 def get_data_version(self) -> int:
169 return self.objects.version
diff --git a/apworld/version.py b/apworld/version.py new file mode 100644 index 0000000..7a61db5 --- /dev/null +++ b/apworld/version.py
@@ -0,0 +1 @@
APWORLD_VERSION = 4