summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--apworld/__init__.py14
-rw-r--r--apworld/items.py5
-rw-r--r--apworld/locations.py2
-rw-r--r--apworld/options.py9
-rw-r--r--apworld/player_logic.py143
-rw-r--r--apworld/regions.py53
-rw-r--r--apworld/rules.py27
-rw-r--r--apworld/static_logic.py10
8 files changed, 254 insertions, 9 deletions
diff --git a/apworld/__init__.py b/apworld/__init__.py index 1544c7b..20c1454 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py
@@ -1,7 +1,9 @@
1""" 1"""
2Archipelago init file for Lingo 2 2Archipelago init file for Lingo 2
3""" 3"""
4from BaseClasses import ItemClassification, Item
4from worlds.AutoWorld import WebWorld, World 5from worlds.AutoWorld import WebWorld, World
6from .items import Lingo2Item
5from .options import Lingo2Options 7from .options import Lingo2Options
6from .player_logic import Lingo2PlayerLogic 8from .player_logic import Lingo2PlayerLogic
7from .regions import create_regions 9from .regions import create_regions
@@ -36,3 +38,15 @@ class Lingo2World(World):
36 38
37 def create_regions(self): 39 def create_regions(self):
38 create_regions(self) 40 create_regions(self)
41
42 from Utils import visualize_regions
43
44 visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
45
46 def create_items(self):
47 pool = [self.create_item(name) for name in self.player_logic.real_items]
48
49 self.multiworld.itempool += pool
50
51 def create_item(self, name: str) -> Item:
52 return Lingo2Item(name, ItemClassification.progression, self.item_name_to_id.get(name), self.player)
diff --git a/apworld/items.py b/apworld/items.py new file mode 100644 index 0000000..971a709 --- /dev/null +++ b/apworld/items.py
@@ -0,0 +1,5 @@
1from BaseClasses import Item
2
3
4class Lingo2Item(Item):
5 game: str = "Lingo 2"
diff --git a/apworld/locations.py b/apworld/locations.py index 818be39..108decb 100644 --- a/apworld/locations.py +++ b/apworld/locations.py
@@ -2,4 +2,4 @@ from BaseClasses import Location
2 2
3 3
4class Lingo2Location(Location): 4class Lingo2Location(Location):
5 game: str = "Lingo 2" \ No newline at end of file 5 game: str = "Lingo 2"
diff --git a/apworld/options.py b/apworld/options.py index f33f5af..77f0ae3 100644 --- a/apworld/options.py +++ b/apworld/options.py
@@ -1,8 +1,13 @@
1from dataclasses import dataclass 1from dataclasses import dataclass
2 2
3from Options import PerGameCommonOptions 3from Options import PerGameCommonOptions, Toggle
4
5
6class ShuffleDoors(Toggle):
7 """If enabled, most doors will open from receiving an item rather than fulfilling the in-game requirements."""
8 display_name = "Shuffle Doors"
4 9
5 10
6@dataclass 11@dataclass
7class Lingo2Options(PerGameCommonOptions): 12class Lingo2Options(PerGameCommonOptions):
8 pass 13 shuffle_doors: ShuffleDoors
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
8def 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
18class 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
8class PlayerLocation(NamedTuple): 57class PlayerLocation(NamedTuple):
9 code: int | None 58 code: int | None
59 reqs: AccessRequirements
10 60
11 61
12class Lingo2PlayerLogic: 62class 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
diff --git a/apworld/regions.py b/apworld/regions.py index d388678..2a850ef 100644 --- a/apworld/regions.py +++ b/apworld/regions.py
@@ -2,17 +2,20 @@ from typing import TYPE_CHECKING
2 2
3from BaseClasses import Region 3from BaseClasses import Region
4from .locations import Lingo2Location 4from .locations import Lingo2Location
5from .player_logic import AccessRequirements
6from .rules import make_location_lambda
5 7
6if TYPE_CHECKING: 8if TYPE_CHECKING:
7 from . import Lingo2World 9 from . import Lingo2World
8 10
9 11
10def create_region(room, world: "Lingo2World") -> Region: 12def create_region(room, world: "Lingo2World") -> Region:
11 new_region = Region(room.name, world.player, world.multiworld) 13 new_region = Region(world.static_logic.get_room_region_name(room.id), world.player, world.multiworld)
12 14
13 for location in world.player_logic.locations_by_room.get(room.id, {}): 15 for location in world.player_logic.locations_by_room.get(room.id, {}):
14 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code], 16 new_location = Lingo2Location(world.player, world.static_logic.location_id_to_name[location.code],
15 location.code, new_region) 17 location.code, new_region)
18 new_location.access_rule = make_location_lambda(location.reqs, world)
16 new_region.locations.append(new_location) 19 new_region.locations.append(new_location)
17 20
18 return new_region 21 return new_region
@@ -24,6 +27,52 @@ def create_regions(world: "Lingo2World"):
24 } 27 }
25 28
26 for room in world.static_logic.objects.rooms: 29 for room in world.static_logic.objects.rooms:
27 regions[room.name] = create_region(room, world) 30 region = create_region(room, world)
31 regions[region.name] = region
32
33 regions["Menu"].connect(regions["the_entry - Starting Room"], "Start Game")
34
35 # TODO: The requirements of the opposite trigger also matter.
36 for connection in world.static_logic.objects.connections:
37 from_region = world.static_logic.get_room_region_name(connection.from_room)
38 to_region = world.static_logic.get_room_region_name(connection.to_room)
39 connection_name = f"{from_region} -> {to_region}"
40
41 reqs = AccessRequirements()
42
43 if connection.HasField("required_door"):
44 reqs.merge(world.player_logic.get_door_reqs(connection.required_door))
45
46 door = world.static_logic.objects.doors[connection.required_door]
47 wmap = world.static_logic.objects.maps[door.map_id]
48 connection_name = f"{connection_name} (using {wmap.name} - {door.name})"
49
50 if connection.HasField("port"):
51 port = world.static_logic.objects.ports[connection.port]
52 connection_name = f"{connection_name} (via port {port.name})"
53
54 if port.HasField("required_door"):
55 reqs.merge(world.player_logic.get_door_reqs(port.required_door))
56
57 if connection.HasField("painting"):
58 painting = world.static_logic.objects.paintings[connection.painting]
59 connection_name = f"{connection_name} (via painting {painting.name})"
60
61 if painting.HasField("required_door"):
62 reqs.merge(world.player_logic.get_door_reqs(painting.required_door))
63
64 if connection.HasField("panel"):
65 proxy = connection.panel
66 reqs.merge(world.player_logic.get_panel_reqs(proxy.panel,
67 proxy.answer if proxy.HasField("answer") else None))
68
69 panel = world.static_logic.objects.panels[proxy.panel]
70 if proxy.HasField("answer"):
71 connection_name = f"{connection_name} (via panel {panel.name}/{proxy.answer})"
72 else:
73 connection_name = f"{connection_name} (via panel {panel.name})"
74
75 if from_region in regions and to_region in regions:
76 regions[from_region].connect(regions[to_region], connection_name, make_location_lambda(reqs, world))
28 77
29 world.multiworld.regions += regions.values() 78 world.multiworld.regions += regions.values()
diff --git a/apworld/rules.py b/apworld/rules.py new file mode 100644 index 0000000..05689e9 --- /dev/null +++ b/apworld/rules.py
@@ -0,0 +1,27 @@
1from collections.abc import Callable
2from typing import TYPE_CHECKING
3
4from BaseClasses import CollectionState
5from .player_logic import AccessRequirements
6
7if TYPE_CHECKING:
8 from . import Lingo2World
9
10
11def lingo2_can_satisfy_requirements(state: CollectionState, reqs: AccessRequirements, world: "Lingo2World") -> bool:
12 if not all(state.has(item, world.player) for item in reqs.items):
13 return False
14
15 if not all(state.can_reach_region(region_name, world.player) for region_name in reqs.rooms):
16 return False
17
18 # TODO: symbols, letters
19
20 for disjunction in reqs.or_logic:
21 if not any(lingo2_can_satisfy_requirements(state, sub_reqs, world) for sub_reqs in disjunction):
22 return False
23
24 return True
25
26def make_location_lambda(reqs: AccessRequirements, world: "Lingo2World") -> Callable[[CollectionState], bool]:
27 return lambda state: lingo2_can_satisfy_requirements(state, reqs, world)
diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 4fc38f8..ff58e96 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py
@@ -23,7 +23,7 @@ class Lingo2StaticLogic:
23 self.location_id_to_name[door.ap_id] = location_name 23 self.location_id_to_name[door.ap_id] = location_name
24 24
25 if door.type not in [common_pb2.DoorType.EVENT, common_pb2.DoorType.LOCATION_ONLY]: 25 if door.type not in [common_pb2.DoorType.EVENT, common_pb2.DoorType.LOCATION_ONLY]:
26 item_name = f"{self.objects.maps[door.map_id].name} - {door.name}" 26 item_name = self.get_door_item_name(door.id)
27 self.item_id_to_name[door.ap_id] = item_name 27 self.item_id_to_name[door.ap_id] = item_name
28 28
29 for letter in self.objects.letters: 29 for letter in self.objects.letters:
@@ -40,3 +40,11 @@ class Lingo2StaticLogic:
40 40
41 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()} 41 self.item_name_to_id = {name: ap_id for ap_id, name in self.item_id_to_name.items()}
42 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()} 42 self.location_name_to_id = {name: ap_id for ap_id, name in self.location_id_to_name.items()}
43
44 def get_door_item_name(self, door_id: int) -> str:
45 door = self.objects.doors[door_id]
46 return f"{self.objects.maps[door.map_id].name} - {door.name}"
47
48 def get_room_region_name(self, room_id: int) -> str:
49 room = self.objects.rooms[room_id]
50 return f"{self.objects.maps[room.map_id].name} - {room.name}"