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 | ||
