summary refs log tree commit diff stats
path: root/player_logic.py
diff options
context:
space:
mode:
Diffstat (limited to 'player_logic.py')
-rw-r--r--player_logic.py298
1 files changed, 298 insertions, 0 deletions
diff --git a/player_logic.py b/player_logic.py new file mode 100644 index 0000000..217ad91 --- /dev/null +++ b/player_logic.py
@@ -0,0 +1,298 @@
1from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING
2
3from .items import ALL_ITEM_TABLE
4from .locations import ALL_LOCATION_TABLE, LocationClassification
5from .options import LocationChecks, ShuffleDoors, VictoryCondition
6from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \
7 PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, ROOMS, \
8 RoomAndPanel
9from .testing import LingoTestOptions
10
11if TYPE_CHECKING:
12 from . import LingoWorld
13
14
15class PlayerLocation(NamedTuple):
16 name: str
17 code: Optional[int] = None
18 panels: List[RoomAndPanel] = []
19
20
21class LingoPlayerLogic:
22 """
23 Defines logic after a player's options have been applied
24 """
25
26 ITEM_BY_DOOR: Dict[str, Dict[str, str]]
27
28 LOCATIONS_BY_ROOM: Dict[str, List[PlayerLocation]]
29 REAL_LOCATIONS: List[str]
30
31 EVENT_LOC_TO_ITEM: Dict[str, str]
32 REAL_ITEMS: List[str]
33
34 VICTORY_CONDITION: str
35 MASTERY_LOCATION: str
36 LEVEL_2_LOCATION: str
37
38 PAINTING_MAPPING: Dict[str, str]
39
40 FORCED_GOOD_ITEM: str
41
42 def add_location(self, room: str, loc: PlayerLocation):
43 self.LOCATIONS_BY_ROOM.setdefault(room, []).append(loc)
44
45 def set_door_item(self, room: str, door: str, item: str):
46 self.ITEM_BY_DOOR.setdefault(room, {})[door] = item
47
48 def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
49 if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
50 if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
51 self.set_door_item(room_name, door_data.name, door_data.item_name)
52 else:
53 progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
54 self.set_door_item(room_name, door_data.name, progressive_item_name)
55 self.REAL_ITEMS.append(progressive_item_name)
56 else:
57 self.set_door_item(room_name, door_data.name, door_data.item_name)
58
59 def __init__(self, world: "LingoWorld"):
60 self.ITEM_BY_DOOR = {}
61 self.LOCATIONS_BY_ROOM = {}
62 self.REAL_LOCATIONS = []
63 self.EVENT_LOC_TO_ITEM = {}
64 self.REAL_ITEMS = []
65 self.VICTORY_CONDITION = ""
66 self.MASTERY_LOCATION = ""
67 self.LEVEL_2_LOCATION = ""
68 self.PAINTING_MAPPING = {}
69 self.FORCED_GOOD_ITEM = ""
70
71 door_shuffle = world.options.shuffle_doors
72 color_shuffle = world.options.shuffle_colors
73 painting_shuffle = world.options.shuffle_paintings
74 location_checks = world.options.location_checks
75 victory_condition = world.options.victory_condition
76 early_color_hallways = world.options.early_color_hallways
77
78 if location_checks == LocationChecks.option_reduced and door_shuffle != ShuffleDoors.option_none:
79 raise Exception("You cannot have reduced location checks when door shuffle is on, because there would not "
80 "be enough locations for all of the door items.")
81
82 # Create an event for every room that represents being able to reach that room.
83 for room_name in ROOMS.keys():
84 roomloc_name = f"{room_name} (Reached)"
85 self.add_location(room_name, PlayerLocation(roomloc_name, None, []))
86 self.EVENT_LOC_TO_ITEM[roomloc_name] = roomloc_name
87
88 # Create an event for every door, representing whether that door has been opened. Also create event items for
89 # doors that are event-only.
90 for room_name, room_data in DOORS_BY_ROOM.items():
91 for door_name, door_data in room_data.items():
92 if door_shuffle == ShuffleDoors.option_none:
93 itemloc_name = f"{room_name} - {door_name} (Opened)"
94 self.add_location(room_name, PlayerLocation(itemloc_name, None, door_data.panels))
95 self.EVENT_LOC_TO_ITEM[itemloc_name] = itemloc_name
96 self.set_door_item(room_name, door_name, itemloc_name)
97 else:
98 # This line is duplicated from StaticLingoItems
99 if door_data.skip_item is False and door_data.event is False:
100 if door_data.group is not None and door_shuffle == ShuffleDoors.option_simple:
101 # Grouped doors are handled differently if shuffle doors is on simple.
102 self.set_door_item(room_name, door_name, door_data.group)
103 else:
104 self.handle_non_grouped_door(room_name, door_data, world)
105
106 if door_data.event:
107 self.add_location(room_name, PlayerLocation(door_data.item_name, None, door_data.panels))
108 self.EVENT_LOC_TO_ITEM[door_data.item_name] = door_data.item_name + " (Opened)"
109 self.set_door_item(room_name, door_name, door_data.item_name + " (Opened)")
110
111 # Create events for each achievement panel, so that we can determine when THE MASTER is accessible. We also
112 # create events for each counting panel, so that we can determine when LEVEL 2 is accessible.
113 for room_name, room_data in PANELS_BY_ROOM.items():
114 for panel_name, panel_data in room_data.items():
115 if panel_data.achievement:
116 event_name = room_name + " - " + panel_name + " (Achieved)"
117 self.add_location(room_name, PlayerLocation(event_name, None,
118 [RoomAndPanel(room_name, panel_name)]))
119 self.EVENT_LOC_TO_ITEM[event_name] = "Mastery Achievement"
120
121 if not panel_data.non_counting and victory_condition == VictoryCondition.option_level_2:
122 event_name = room_name + " - " + panel_name + " (Counted)"
123 self.add_location(room_name, PlayerLocation(event_name, None,
124 [RoomAndPanel(room_name, panel_name)]))
125 self.EVENT_LOC_TO_ITEM[event_name] = "Counting Panel Solved"
126
127 # Handle the victory condition. Victory conditions other than the chosen one become regular checks, so we need
128 # to prevent the actual victory condition from becoming a check.
129 self.MASTERY_LOCATION = "Orange Tower Seventh Floor - THE MASTER"
130 self.LEVEL_2_LOCATION = "N/A"
131
132 if victory_condition == VictoryCondition.option_the_end:
133 self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE END"
134 self.add_location("Orange Tower Seventh Floor", PlayerLocation("The End (Solved)"))
135 self.EVENT_LOC_TO_ITEM["The End (Solved)"] = "Victory"
136 elif victory_condition == VictoryCondition.option_the_master:
137 self.VICTORY_CONDITION = "Orange Tower Seventh Floor - THE MASTER"
138 self.MASTERY_LOCATION = "Orange Tower Seventh Floor - Mastery Achievements"
139
140 self.add_location("Orange Tower Seventh Floor", PlayerLocation(self.MASTERY_LOCATION, None, []))
141 self.EVENT_LOC_TO_ITEM[self.MASTERY_LOCATION] = "Victory"
142 elif victory_condition == VictoryCondition.option_level_2:
143 self.VICTORY_CONDITION = "Second Room - LEVEL 2"
144 self.LEVEL_2_LOCATION = "Second Room - Unlock Level 2"
145
146 self.add_location("Second Room", PlayerLocation(self.LEVEL_2_LOCATION, None,
147 [RoomAndPanel("Second Room", "LEVEL 2")]))
148 self.EVENT_LOC_TO_ITEM[self.LEVEL_2_LOCATION] = "Victory"
149
150 # Instantiate all real locations.
151 location_classification = LocationClassification.normal
152 if location_checks == LocationChecks.option_reduced:
153 location_classification = LocationClassification.reduced
154 elif location_checks == LocationChecks.option_insanity:
155 location_classification = LocationClassification.insanity
156
157 for location_name, location_data in ALL_LOCATION_TABLE.items():
158 if location_name != self.VICTORY_CONDITION:
159 if location_classification not in location_data.classification:
160 continue
161
162 self.add_location(location_data.room, PlayerLocation(location_name, location_data.code,
163 location_data.panels))
164 self.REAL_LOCATIONS.append(location_name)
165
166 # Instantiate all real items.
167 for name, item in ALL_ITEM_TABLE.items():
168 if item.should_include(world):
169 self.REAL_ITEMS.append(name)
170
171 # Create the paintings mapping, if painting shuffle is on.
172 if painting_shuffle:
173 # Shuffle paintings until we get something workable.
174 workable_paintings = False
175 for i in range(0, 20):
176 workable_paintings = self.randomize_paintings(world)
177 if workable_paintings:
178 break
179
180 if not workable_paintings:
181 raise Exception("This Lingo world was unable to generate a workable painting mapping after 20 "
182 "iterations. This is very unlikely to happen on its own, and probably indicates some "
183 "kind of logic error.")
184
185 if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
186 and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False:
187 # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
188 # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
189 # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are
190 # no extra checks in there. We only include the entrance to the Rhyme Room when color shuffle is off and
191 # door shuffle is on simple, because otherwise there are no extra checks in there.
192 good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
193
194 if not color_shuffle:
195 good_item_options.append("Pilgrim Room - Sun Painting")
196
197 if door_shuffle == ShuffleDoors.option_simple:
198 good_item_options += ["Welcome Back Doors"]
199
200 if not color_shuffle:
201 good_item_options.append("Rhyme Room Doors")
202 else:
203 good_item_options += ["Welcome Back Area - Shortcut to Starting Room"]
204
205 for painting_obj in PAINTINGS_BY_ROOM["Starting Room"]:
206 if not painting_obj.enter_only or painting_obj.required_door is None:
207 continue
208
209 # If painting shuffle is on, we only want to consider paintings that actually go somewhere.
210 if painting_shuffle and painting_obj.id not in self.PAINTING_MAPPING.keys():
211 continue
212
213 pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
214 good_item_options.append(pdoor.item_name)
215
216 # Copied from The Witness -- remove any plandoed items from the possible good items set.
217 for v in world.multiworld.plando_items[world.player]:
218 if v.get("from_pool", True):
219 for item_key in {"item", "items"}:
220 if item_key in v:
221 if type(v[item_key]) is str:
222 if v[item_key] in good_item_options:
223 good_item_options.remove(v[item_key])
224 elif type(v[item_key]) is dict:
225 for item, weight in v[item_key].items():
226 if weight and item in good_item_options:
227 good_item_options.remove(item)
228 else:
229 # Other type of iterable
230 for item in v[item_key]:
231 if item in good_item_options:
232 good_item_options.remove(item)
233
234 if len(good_item_options) > 0:
235 self.FORCED_GOOD_ITEM = world.random.choice(good_item_options)
236 self.REAL_ITEMS.remove(self.FORCED_GOOD_ITEM)
237 self.REAL_LOCATIONS.remove("Second Room - Good Luck")
238
239 def randomize_paintings(self, world: "LingoWorld") -> bool:
240 self.PAINTING_MAPPING.clear()
241
242 door_shuffle = world.options.shuffle_doors
243
244 # Determine the set of exit paintings. All required-exit paintings are included, as are all
245 # required-when-no-doors paintings if door shuffle is off. We then fill the set with random other paintings.
246 chosen_exits = []
247 if door_shuffle == ShuffleDoors.option_none:
248 chosen_exits = [painting_id for painting_id, painting in PAINTINGS.items()
249 if painting.required_when_no_doors]
250 chosen_exits += [painting_id for painting_id, painting in PAINTINGS.items()
251 if painting.exit_only and painting.required]
252 exitable = [painting_id for painting_id, painting in PAINTINGS.items()
253 if not painting.enter_only and not painting.disable and not painting.required]
254 chosen_exits += world.random.sample(exitable, PAINTING_EXITS - len(chosen_exits))
255
256 # Determine the set of entrance paintings.
257 enterable = [painting_id for painting_id, painting in PAINTINGS.items()
258 if not painting.exit_only and not painting.disable and painting_id not in chosen_exits]
259 chosen_entrances = world.random.sample(enterable, PAINTING_ENTRANCES)
260
261 # Create a mapping from entrances to exits.
262 for warp_exit in chosen_exits:
263 warp_enter = world.random.choice(chosen_entrances)
264
265 # Check whether this is a warp from a required painting room to another (or the same) required painting
266 # room. This could cause a cycle that would make certain regions inaccessible.
267 warp_exit_room = PAINTINGS[warp_exit].room
268 warp_enter_room = PAINTINGS[warp_enter].room
269
270 required_painting_rooms = REQUIRED_PAINTING_ROOMS
271 if door_shuffle == ShuffleDoors.option_none:
272 required_painting_rooms += REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS
273
274 if warp_exit_room in required_painting_rooms and warp_enter_room in required_painting_rooms:
275 # This shuffling is non-workable. Start over.
276 return False
277
278 chosen_entrances.remove(warp_enter)
279 self.PAINTING_MAPPING[warp_enter] = warp_exit
280
281 for warp_enter in chosen_entrances:
282 warp_exit = world.random.choice(chosen_exits)
283 self.PAINTING_MAPPING[warp_enter] = warp_exit
284
285 # The Eye Wall painting is unique in that it is both double-sided and also enter only (because it moves).
286 # There is only one eligible double-sided exit painting, which is the vanilla exit for this warp. If the
287 # exit painting is an entrance in the shuffle, we will disable the Eye Wall painting. Otherwise, Eye Wall
288 # is forced to point to the vanilla exit.
289 if "eye_painting_2" not in self.PAINTING_MAPPING.keys():
290 self.PAINTING_MAPPING["eye_painting"] = "eye_painting_2"
291
292 # Just for sanity's sake, ensure that all required painting rooms are accessed.
293 for painting_id, painting in PAINTINGS.items():
294 if painting_id not in self.PAINTING_MAPPING.values() \
295 and (painting.required or (painting.required_when_no_doors and door_shuffle == 0)):
296 return False
297
298 return True