diff options
Diffstat (limited to 'apworld/player_logic.py')
-rw-r--r-- | apworld/player_logic.py | 143 |
1 files changed, 140 insertions, 3 deletions
diff --git a/apworld/player_logic.py b/apworld/player_logic.py index a3b86bf..958abc5 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py | |||
@@ -5,22 +5,159 @@ if TYPE_CHECKING: | |||
5 | from . import Lingo2World | 5 | from . import Lingo2World |
6 | 6 | ||
7 | 7 | ||
8 | def calculate_letter_histogram(solution: str) -> dict[str, int]: | ||
9 | histogram = dict() | ||
10 | for l in solution: | ||
11 | if l.isalpha(): | ||
12 | real_l = l.upper() | ||
13 | histogram[real_l] = min(histogram.get(l, 0) + 1, 2) | ||
14 | |||
15 | return histogram | ||
16 | |||
17 | |||
18 | class AccessRequirements: | ||
19 | items: set[str] | ||
20 | rooms: set[str] | ||
21 | symbols: set[str] | ||
22 | letters: dict[str, int] | ||
23 | |||
24 | # This is an AND of ORs. | ||
25 | or_logic: list[list["AccessRequirements"]] | ||
26 | |||
27 | def __init__(self): | ||
28 | self.items = set() | ||
29 | self.rooms = set() | ||
30 | self.symbols = set() | ||
31 | self.letters = dict() | ||
32 | self.or_logic = list() | ||
33 | |||
34 | def add_solution(self, solution: str): | ||
35 | histogram = calculate_letter_histogram(solution) | ||
36 | |||
37 | for l, a in histogram.items(): | ||
38 | self.letters[l] = max(self.letters.get(l, 0), histogram.get(l)) | ||
39 | |||
40 | def merge(self, other: "AccessRequirements"): | ||
41 | for item in other.items: | ||
42 | self.items.add(item) | ||
43 | |||
44 | for room in other.rooms: | ||
45 | self.rooms.add(room) | ||
46 | |||
47 | for symbol in other.symbols: | ||
48 | self.symbols.add(symbol) | ||
49 | |||
50 | for letter, level in other.letters.items(): | ||
51 | self.letters[letter] = max(self.letters.get(letter, 0), level) | ||
52 | |||
53 | for disjunction in other.or_logic: | ||
54 | self.or_logic.append(disjunction) | ||
55 | |||
56 | |||
8 | class PlayerLocation(NamedTuple): | 57 | class PlayerLocation(NamedTuple): |
9 | code: int | None | 58 | code: int | None |
59 | reqs: AccessRequirements | ||
10 | 60 | ||
11 | 61 | ||
12 | class Lingo2PlayerLogic: | 62 | class Lingo2PlayerLogic: |
63 | world: "Lingo2World" | ||
64 | |||
13 | locations_by_room: dict[int, list[PlayerLocation]] | 65 | locations_by_room: dict[int, list[PlayerLocation]] |
14 | 66 | ||
67 | panel_reqs: dict[int, AccessRequirements] | ||
68 | proxy_reqs: dict[int, dict[str, AccessRequirements]] | ||
69 | door_reqs: dict[int, AccessRequirements] | ||
70 | |||
71 | real_items: list[str] | ||
72 | |||
15 | def __init__(self, world: "Lingo2World"): | 73 | def __init__(self, world: "Lingo2World"): |
74 | self.world = world | ||
16 | self.locations_by_room = {} | 75 | self.locations_by_room = {} |
76 | self.panel_reqs = dict() | ||
77 | self.proxy_reqs = dict() | ||
78 | self.door_reqs = dict() | ||
79 | self.real_items = list() | ||
17 | 80 | ||
18 | for door in world.static_logic.objects.doors: | 81 | for door in world.static_logic.objects.doors: |
19 | if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.LOCATION_ONLY]: | 82 | if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.LOCATION_ONLY]: |
20 | self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id)) | 83 | self.locations_by_room.setdefault(door.room_id, []).append(PlayerLocation(door.ap_id, |
84 | self.get_door_reqs(door.id))) | ||
85 | |||
86 | if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: | ||
87 | self.real_items.append(self.world.static_logic.get_door_item_name(door.id)) | ||
21 | 88 | ||
22 | for letter in world.static_logic.objects.letters: | 89 | for letter in world.static_logic.objects.letters: |
23 | self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id)) | 90 | self.locations_by_room.setdefault(letter.room_id, []).append(PlayerLocation(letter.ap_id, |
91 | AccessRequirements())) | ||
24 | 92 | ||
25 | for mastery in world.static_logic.objects.masteries: | 93 | for mastery in world.static_logic.objects.masteries: |
26 | self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id)) | 94 | self.locations_by_room.setdefault(mastery.room_id, []).append(PlayerLocation(mastery.ap_id, |
95 | AccessRequirements())) | ||
96 | |||
97 | def get_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: | ||
98 | if answer is None: | ||
99 | if panel_id not in self.panel_reqs: | ||
100 | self.panel_reqs[panel_id] = self.calculate_panel_reqs(panel_id, answer) | ||
101 | |||
102 | return self.panel_reqs.get(panel_id) | ||
103 | else: | ||
104 | if panel_id not in self.proxy_reqs or answer not in self.proxy_reqs.get(panel_id): | ||
105 | self.proxy_reqs.setdefault(panel_id, {})[answer] = self.calculate_panel_reqs(panel_id, answer) | ||
106 | |||
107 | return self.proxy_reqs.get(panel_id).get(answer) | ||
108 | |||
109 | def calculate_panel_reqs(self, panel_id: int, answer: str | None) -> AccessRequirements: | ||
110 | panel = self.world.static_logic.objects.panels[panel_id] | ||
111 | reqs = AccessRequirements() | ||
112 | |||
113 | reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.room_id)) | ||
114 | |||
115 | if answer is not None: | ||
116 | reqs.add_solution(answer) | ||
117 | elif len(panel.proxies) > 0: | ||
118 | for proxy in panel.proxies: | ||
119 | proxy_reqs = AccessRequirements() | ||
120 | proxy_reqs.add_solution(proxy.answer) | ||
121 | |||
122 | reqs.or_logic.append([proxy_reqs]) | ||
123 | else: | ||
124 | reqs.add_solution(panel.answer) | ||
125 | |||
126 | for symbol in panel.symbols: | ||
127 | reqs.symbols.add(symbol) | ||
128 | |||
129 | if panel.HasField("required_door"): | ||
130 | door_reqs = self.get_door_reqs(panel.required_door) | ||
131 | reqs.merge(door_reqs) | ||
132 | |||
133 | if panel.HasField("required_room"): | ||
134 | reqs.rooms.add(self.world.static_logic.get_room_region_name(panel.required_room)) | ||
135 | |||
136 | return reqs | ||
137 | |||
138 | def get_door_reqs(self, door_id: int) -> AccessRequirements: | ||
139 | if door_id not in self.door_reqs: | ||
140 | self.door_reqs[door_id] = self.calculate_door_reqs(door_id) | ||
141 | |||
142 | return self.door_reqs.get(door_id) | ||
143 | |||
144 | def calculate_door_reqs(self, door_id: int) -> AccessRequirements: | ||
145 | door = self.world.static_logic.objects.doors[door_id] | ||
146 | reqs = AccessRequirements() | ||
147 | |||
148 | use_item = False | ||
149 | if door.type in [common_pb2.DoorType.STANDARD, common_pb2.DoorType.ITEM_ONLY] and self.world.options.shuffle_doors: | ||
150 | use_item = True | ||
151 | |||
152 | if use_item: | ||
153 | reqs.items.add(self.world.static_logic.get_door_item_name(door.id)) | ||
154 | else: | ||
155 | # TODO: complete_at, control_center_color, switches, keyholders | ||
156 | for proxy in door.panels: | ||
157 | panel_reqs = self.get_panel_reqs(proxy.panel, proxy.answer if proxy.HasField("answer") else None) | ||
158 | reqs.merge(panel_reqs) | ||
159 | |||
160 | for room in door.rooms: | ||
161 | reqs.rooms.add(self.world.static_logic.get_room_region_name(room)) | ||
162 | |||
163 | return reqs | ||