Skip to content

Commit 94f3b68

Browse files
authored
Merge pull request #129 from phst-randomizer/edge-flags
2 parents 93e10fc + a458bdc commit 94f3b68

File tree

3 files changed

+76
-99
lines changed

3 files changed

+76
-99
lines changed

shuffler/_parser.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ class Edge:
4646
dest: Node
4747
constraints: str | None
4848

49-
def is_traversable(self, current_inventory: list[str]) -> bool:
49+
def is_traversable(self, current_inventory: list[str], current_flags: set[str]) -> bool:
5050
if self.constraints:
5151
parsed = edge_parser.parse_string(self.constraints)
52-
return edge_is_tranversable(parsed.as_list(), current_inventory)
52+
return edge_is_tranversable(parsed.as_list(), current_inventory, current_flags)
5353
return True
5454

5555

@@ -71,6 +71,7 @@ class Area:
7171

7272
class Descriptor(Enum):
7373
CHEST = 'chest'
74+
FLAG = 'flag'
7475
DOOR = 'door'
7576
ENTRANCE = 'entrance'
7677
EXIT = 'exit'
@@ -91,7 +92,7 @@ class Descriptor(Enum):
9192

9293
# pyparsing parser for parsing edges in .logic files:
9394
operand: pyparsing.ParserElement = (
94-
pyparsing.Keyword('item') | pyparsing.Keyword('flag')
95+
pyparsing.Keyword('item') | pyparsing.Keyword('flag') | pyparsing.Keyword('open')
9596
) + pyparsing.Word(pyparsing.alphas)
9697
edge_parser: pyparsing.ParserElement = pyparsing.infix_notation(
9798
operand,
@@ -102,7 +103,7 @@ class Descriptor(Enum):
102103
)
103104

104105

105-
def _evaluate_constraint(type: str, value: str, inventory: list[str]) -> bool:
106+
def _evaluate_constraint(type: str, value: str, inventory: list[str], flags: set[str]) -> bool:
106107
"""
107108
Given an edge constraint "type value", determines if the edge is traversable
108109
given the current game state (inventory, set flags, etc).
@@ -119,12 +120,16 @@ def _evaluate_constraint(type: str, value: str, inventory: list[str]) -> bool:
119120
case 'item':
120121
return value in inventory
121122
case 'flag':
122-
raise NotImplementedError("Edges with type 'flag' are not implemented yet.")
123+
return value in flags
124+
case 'open':
125+
return False # TODO: implement key logic
123126
case other:
124127
raise Exception(f'Invalid edge type "{other}"')
125128

126129

127-
def edge_is_tranversable(parsed_expr: EdgeExpression, inventory: list[str], result=True) -> bool:
130+
def edge_is_tranversable(
131+
parsed_expr: EdgeExpression, inventory: list[str], flags: set[str], result=True
132+
) -> bool:
128133
"""
129134
Determine if the given edge expression is traversable.
130135
@@ -148,7 +153,7 @@ def edge_is_tranversable(parsed_expr: EdgeExpression, inventory: list[str], resu
148153
if isinstance(parsed_expr[0], list):
149154
# TODO: remove 'type: ignore' comment below. see prev note about `EdgeExpression` type.
150155
sub_expression: EdgeExpression = parsed_expr.pop(0) # type: ignore
151-
current_result = edge_is_tranversable(sub_expression, inventory, result)
156+
current_result = edge_is_tranversable(sub_expression, inventory, flags, result)
152157
else:
153158
# Extract type and value (e.g., 'item' and 'Bombs')
154159
expr_type = parsed_expr.pop(0)
@@ -157,7 +162,7 @@ def edge_is_tranversable(parsed_expr: EdgeExpression, inventory: list[str], resu
157162
# so the shuffler needs to normalize everything to snake_case at runtime.
158163
expr_value = inflection.underscore(parsed_expr.pop(0))
159164

160-
current_result = _evaluate_constraint(expr_type, expr_value, inventory)
165+
current_result = _evaluate_constraint(expr_type, expr_value, inventory, flags)
161166

162167
# Apply any pending logical operations
163168
if current_op == '&':

shuffler/main.py

Lines changed: 30 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,50 +26,6 @@ def load_aux_data(directory: Path) -> list[Area]:
2626
return areas
2727

2828

29-
def randomize_aux_data(aux_data: list[Area]) -> list[Area]:
30-
"""
31-
Return aux data for the logic with the item locations randomized.
32-
33-
Note: the item locations are *not* guaranteed (and are unlikely) to be logic-compliant.
34-
This function just performs a "dumb" shuffle and returns the results.
35-
36-
Params:
37-
aux_data_directory: Directory that contains the initial aux data
38-
"""
39-
# List of every item in the game
40-
chest_items: list[str] = []
41-
42-
# Record all items in the game
43-
for area in aux_data:
44-
for room in area.rooms:
45-
for chest in room.chests or []:
46-
chest_items.append(chest.contents)
47-
48-
# Randomize the items
49-
for area in aux_data:
50-
for room in area.rooms:
51-
for chest in room.chests or []:
52-
chest.contents = chest_items.pop(random.randint(0, len(chest_items) - 1))
53-
return aux_data
54-
55-
56-
def edge_is_traversable(edge: Edge, inventory: list[str]):
57-
"""Determine if this edge is traversable given the current state of the game."""
58-
# TODO: implement this
59-
match edge.constraints:
60-
case 'item Sword':
61-
return 'oshus_sword' in inventory
62-
case '(item Bombs | item Bombchus)':
63-
return 'bombs' in inventory or 'bombchus' in inventory
64-
case 'item Bow':
65-
return 'bow' in inventory
66-
case 'item Boomerang':
67-
return 'boomerang' in inventory
68-
case 'flag BridgeRepaired':
69-
return True # TODO: for now, assume bridge is always repaired
70-
return False
71-
72-
7329
def get_chest_contents(
7430
area_name: str, room_name: str, chest_name: str, aux_data: list[Area]
7531
) -> str:
@@ -100,8 +56,9 @@ def assumed_search(
10056
edges: dict[str, list[Edge]],
10157
aux_data: list[Area],
10258
inventory: list[str],
59+
flags: set[str],
10360
visited_rooms: set[str],
104-
visited_nodes: set[str],
61+
visited_nodes: set[Node],
10562
) -> set[Node]:
10663
"""
10764
Calculate the set of nodes reachable from the `starting_node` given the current inventory.
@@ -120,8 +77,6 @@ def assumed_search(
12077
"""
12178
logging.debug(starting_node.name)
12279

123-
reachable_nodes: set[Node] = {starting_node}
124-
12580
doors_to_enter: list[str] = []
12681

12782
# For the current node, find all chests + "collect" their items and note every door so
@@ -137,6 +92,13 @@ def assumed_search(
13792
# nodes we couldn't before with this new item
13893
visited_nodes.clear()
13994
visited_rooms.clear()
95+
elif node_info.type == Descriptor.FLAG.value:
96+
if node_info.data not in flags:
97+
flags.add(node_info.data)
98+
# Reset visited nodes and rooms because we may now be able to reach
99+
# nodes we couldn't before with this new flag set
100+
visited_nodes.clear()
101+
visited_rooms.clear()
140102
for node_info in starting_node.contents:
141103
if node_info.type in (
142104
Descriptor.DOOR.value,
@@ -154,25 +116,32 @@ def assumed_search(
154116
doors_to_enter.append(full_room_name)
155117
visited_rooms.add(full_room_name)
156118

157-
visited_nodes.add(starting_node.name) # Acknowledge this node as "visited"
119+
visited_nodes.add(starting_node) # Acknowledge this node as "visited"
158120

159121
# Check which edges are traversable and do so if they are
160122
for edge in edges[starting_node.name]:
161-
if edge.dest.name in visited_nodes:
123+
if edge.dest in visited_nodes:
162124
continue
163-
if edge_is_traversable(edge, inventory):
125+
if edge.is_traversable(inventory, flags):
164126
logging.debug(f'{edge.source.name} -> {edge.dest.name}')
165-
return reachable_nodes.union(
127+
visited_nodes = visited_nodes.union(
166128
assumed_search(
167-
edge.dest, nodes, edges, aux_data, inventory, visited_rooms, visited_nodes
129+
edge.dest,
130+
nodes,
131+
edges,
132+
aux_data,
133+
inventory,
134+
flags,
135+
visited_rooms,
136+
visited_nodes,
168137
)
169138
)
170139

171140
# Go through each door and traverse each of their room graphs
172-
for door_name in doors_to_enter:
173-
area_name = door_name.split('.')[0]
174-
room_name = door_name.split('.')[1]
175-
door_name = door_name.split('.')[2]
141+
for door_to_enter in doors_to_enter:
142+
area_name = door_to_enter.split('.')[0]
143+
room_name = door_to_enter.split('.')[1]
144+
door_name = door_to_enter.split('.')[2]
176145
for area in aux_data:
177146
if area_name == area.name:
178147
for room in area.rooms:
@@ -199,21 +168,21 @@ def assumed_search(
199168
raise ValueError(
200169
f'ERROR: "{link}" is not a valid node name.'
201170
)
202-
203171
if link == other_node.name:
204172
logging.debug(f'{starting_node.name} -> {other_node.name}')
205-
return reachable_nodes.union(
173+
visited_nodes = visited_nodes.union(
206174
assumed_search(
207175
other_node,
208176
nodes,
209177
edges,
210178
aux_data,
211179
inventory,
180+
flags,
212181
visited_rooms,
213182
visited_nodes,
214183
)
215184
)
216-
return reachable_nodes
185+
return visited_nodes
217186

218187

219188
def shuffle(
@@ -270,7 +239,7 @@ def shuffle(
270239
i = I.pop()
271240

272241
# Determine all reachable logic nodes
273-
R = assumed_search(starting_node, nodes, edges, aux_data, deepcopy(I), set(), set())
242+
R = assumed_search(starting_node, nodes, edges, aux_data, deepcopy(I), set(), set(), set())
274243

275244
# Determine which of these nodes contain items, and thus are candidates for item placement
276245
candidates = [
@@ -291,7 +260,7 @@ def shuffle(
291260
break
292261
else:
293262
raise Exception(
294-
f'Error: shuffler ran out of locations to place item. Remaining items: {[i] + I}'
263+
f'Error: shuffler ran out of locations to place item. Remaining items: {[i] + I} ({len([i] + I)})'
295264
)
296265

297266
# TODO: these conditions should both become true at the same time, once shuffling

tests/test_shuffler.py

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -98,47 +98,50 @@ def test_aux_data_validation(filename: str):
9898

9999

100100
@pytest.mark.parametrize(
101-
'expression,inventory,expected_result',
101+
'expression,inventory,flags,expected_result',
102102
[
103103
# fmt: off
104104
# Test basic expressions
105-
('item Boomerang', ['boomerang', 'sword'], True),
106-
('item Boomerang', ['sword'], False),
105+
('item Boomerang', ['boomerang', 'sword'], {}, True),
106+
('item Boomerang', ['sword'], {}, False),
107107
# Test expressions with basic logic operators
108-
('item Boomerang & item Bombs', ['boomerang', 'bombs'], True),
109-
('item Boomerang & item Bombs', ['boomerang', 'sword'], False),
110-
('item Boomerang & item Bombs', ['bombs'], False),
111-
('item Boomerang & item Bombs', [], False),
112-
('item Boomerang | item Bombs', ['boomerang', 'bombs'], True),
113-
('item Boomerang | item Bombs', ['boomerang', 'sword'], True),
114-
('item Boomerang | item Bombs', ['bombs'], True),
115-
('item Boomerang | item Bombs', ['sword'], False),
108+
('item Boomerang & item Bombs', ['boomerang', 'bombs'], {}, True),
109+
('item Boomerang & item Bombs', ['boomerang', 'sword'], {}, False),
110+
('item Boomerang & item Bombs', ['bombs'], {}, False),
111+
('item Boomerang & item Bombs', [], {}, False),
112+
('item Boomerang | item Bombs', ['boomerang', 'bombs'], {}, True),
113+
('item Boomerang | item Bombs', ['boomerang', 'sword'], {}, True),
114+
('item Boomerang | item Bombs', ['bombs'], {}, True),
115+
('item Boomerang | item Bombs', ['sword'], {}, False),
116+
('item Boomerang | flag BridgeRepaired', ['bombs'], {'bridge_repaired'}, True),
117+
('item Boomerang | flag BridgeRepaired', [], {'bridge_repaired'}, True),
118+
('item Boomerang | flag BridgeRepaired', ['bombs'], {}, False),
116119
# Test nested expressions
117-
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'bombs', 'bombchus'], True),
118-
('item Boomerang & (item Bombs | item Bombchus | item Hammer)', ['boomerang', 'bombchus'], True), # noqa: E501
119-
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'bombs'], True),
120-
('item Boomerang & (item Bombs | item Bombchus)', ['bombs', 'bombchus'], False),
121-
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'sword'], False),
122-
('item Bombchus | item Bombs', ['bombs', 'bombchus', 'cannon'], True),
123-
('item Bombchus | item Bombs', ['bombs', 'boomerang', 'cannon'], True),
124-
('item Bombchus | item Bombs', ['bombchus', 'boomerang', 'cannon'], True),
125-
('item Bombchus | item Bombs', ['boomerang', 'cannon', 'sword'], False),
126-
('item Bombchus | item Bombs | item Sword', ['boomerang', 'cannon', 'sword'], True),
120+
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'bombs', 'bombchus'], {}, True),
121+
('item Boomerang & (item Bombs | item Bombchus | item Hammer)', ['boomerang', 'bombchus'], {}, True), # noqa: E501
122+
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'bombs'], {}, True),
123+
('item Boomerang & (item Bombs | item Bombchus)', ['bombs', 'bombchus'], {}, False),
124+
('item Boomerang & (item Bombs | item Bombchus)', ['boomerang', 'sword'], {}, False),
125+
('item Bombchus | item Bombs', ['bombs', 'bombchus', 'cannon'], {}, True),
126+
('item Bombchus | item Bombs', ['bombs', 'boomerang', 'cannon'], {}, True),
127+
('item Bombchus | item Bombs', ['bombchus', 'boomerang', 'cannon'], {}, True),
128+
('item Bombchus | item Bombs', ['boomerang', 'cannon', 'sword'], {}, False),
129+
('item Bombchus | item Bombs | item Sword', ['boomerang', 'cannon', 'sword'], {}, True),
127130
# Test more complex nested expressions
128-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['bombs'], False), # noqa: E501
129-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bombs'], True), # noqa: E501
130-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bombchus'], True), # noqa: E501
131-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'grappling_hook'], False), # noqa: E501
132-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bow'], False), # noqa: E501
133-
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'grappling_hook', 'bow'], True), # noqa: E501
131+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['bombs'], {}, False), # noqa: E501
132+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bombs'], {}, True), # noqa: E501
133+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bombchus'], {}, True), # noqa: E501
134+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'grappling_hook'], {}, False), # noqa: E501
135+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'bow'], {}, False), # noqa: E501
136+
('item Boomerang & ((item Bombs | item Bombchus) | (item GrapplingHook & item Bow))', ['boomerang', 'grappling_hook', 'bow'], {}, True), # noqa: E501
134137
# Test expression with a lot of redundant parentheses, which shouldn't affect results
135138
# other than additional performance overhead.
136-
('(((((item Sword | ((item Shield)))))))', ['sword'], True),
139+
('(((((item Sword | ((item Shield)))))))', ['sword'], {}, True),
137140
# fmt: on
138141
],
139142
)
140-
def test_edge_parser(expression: str, inventory: list[str], expected_result: bool):
143+
def test_edge_parser(expression: str, inventory: list[str], flags: set[str], expected_result: bool):
141144
assert (
142-
Edge(Node('test1', []), Node('test2', []), expression).is_traversable(inventory)
145+
Edge(Node('test1', []), Node('test2', []), expression).is_traversable(inventory, flags)
143146
== expected_result
144147
)

0 commit comments

Comments
 (0)