Skip to content

Commit

Permalink
Merge pull request #346 from phst-randomizer/rf-mail-shuffler
Browse files Browse the repository at this point in the history
  • Loading branch information
mike8699 authored Jun 2, 2023
2 parents 270f8b3 + 212a393 commit 592d3bb
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 148 deletions.
3 changes: 1 addition & 2 deletions ph_rando/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
if TYPE_CHECKING:
from click.decorators import FC

from ph_rando.shuffler.aux_models import Area, Mail
from ph_rando.shuffler.aux_models import Area


@dataclass
class ShufflerAuxData:
areas: dict[str, Area]
mail: list[Mail]
enemy_requirements: dict[str, str]
requirement_macros: dict[str, str]

Expand Down
1 change: 0 additions & 1 deletion ph_rando/patcher/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ def patcher_cli(

new_aux_data = parse_aux_data(
areas_directory=aux_data_directory,
mail_file=aux_data_directory.parent / 'mail.json',
enemy_mapping_file=Path(__file__).parents[1] / 'shuffler' / 'enemies.json',
macros_file=Path(__file__).parents[1] / 'shuffler' / 'macros.json',
)
Expand Down
56 changes: 49 additions & 7 deletions ph_rando/shuffler/_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@

from ph_rando.patcher._items import ITEMS
from ph_rando.shuffler._descriptors import EdgeDescriptor, NodeDescriptor
from ph_rando.shuffler.aux_models import Area, Check, Enemy, Exit, Mail, Room
from ph_rando.shuffler.aux_models import Area, Check, Enemy, Exit, Room

logger = logging.getLogger(__name__)

# Name of node that represents the singleton "Mailbox" object.
# In format <area>.<room>.<node_name>
# TODO: make this a configurable setting in shuffler
MAILBOX_NODE_NAME = 'Mail.Mail.Mail'


@dataclass
class Node:
Expand Down Expand Up @@ -467,8 +472,7 @@ def annotate_logic(areas: Iterable[Area], logic_directory: Path | None = None) -
case NodeDescriptor.MAIL.value:
if node.mailbox:
raise Exception(
f'node {node.name} contains more '
'than one `mail` descriptor'
f'Node "{node.name}" contains more than one mailbox!'
)
node.mailbox = True
case other:
Expand Down Expand Up @@ -520,9 +524,50 @@ def annotate_logic(areas: Iterable[Area], logic_directory: Path | None = None) -
area.rooms.append(room)


def connect_mail_nodes(areas: Iterable[Area], mail_node_name: str = MAILBOX_NODE_NAME) -> None:
"""Connect all nodes with a `mail` descriptor to the "Mail" node."""
mailbox_node: Node | None = None

# Find mailbox_node
found = False
for area in areas:
if found:
break
for room in area.rooms:
if found:
break
for node in room.nodes:
if found:
break
if node.name == mail_node_name:
mailbox_node = node
found = True

if not found:
raise Exception(f'Mailbox node "{mail_node_name}" not found!')

assert mailbox_node is not None # for type-checker

# Add edge between the mailbox node and each node that has a `mail` descriptor
for area in areas:
for room in area.rooms:
for node in room.nodes:
if node.mailbox:
node.edges.append(Edge(src=node, dest=mailbox_node, requirements=None))


def parse_aux_data(
areas_directory: Path, mail_file: Path, enemy_mapping_file: Path, macros_file: Path
areas_directory: Path | None = None,
enemy_mapping_file: Path | None = None,
macros_file: Path | None = None,
) -> ShufflerAuxData:
if areas_directory is None:
areas_directory = Path(__file__).parent / 'logic'
if enemy_mapping_file is None:
enemy_mapping_file = Path(__file__).parent / 'enemies.json'
if macros_file is None:
macros_file = Path(__file__).parent / 'macros.json'

areas: dict[str, Area] = {}
for file in areas_directory.rglob('*.json'):
with open(file) as fd:
Expand All @@ -537,14 +582,11 @@ def parse_aux_data(
else:
areas[area.name] = area

mail_items = [Mail(**item) for item in json.loads(mail_file.read_text())]

enemy_mapping = json.loads(enemy_mapping_file.read_text())
macros = json.loads(macros_file.read_text())

return ShufflerAuxData(
areas=areas,
mail=mail_items,
enemy_requirements=enemy_mapping,
requirement_macros=macros,
)
73 changes: 7 additions & 66 deletions ph_rando/shuffler/_shuffler.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
from collections import defaultdict, deque
from copy import copy, deepcopy
import logging
from pathlib import Path
import random

from ordered_set import OrderedSet

from ph_rando.common import ShufflerAuxData
from ph_rando.shuffler._parser import (
Edge,
Node,
annotate_logic,
parse_aux_data,
parse_edge_requirement,
requirements_met,
)
from ph_rando.shuffler._parser import Edge, Node, annotate_logic, connect_mail_nodes, parse_aux_data
from ph_rando.shuffler.aux_models import Area, Check, Mail

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -244,22 +236,6 @@ def assumed_search(
flags.add(flag)
found_new_items = True

# If this node contains a mailbox, check if any mail items
# are collectable and collect them.
if node.mailbox:
for item in aux_data.mail:
if item.contents is None or item in completed_checks:
continue
if requirements_met(
parse_edge_requirement(item.requirements),
items,
flags,
aux_data,
):
items.append(item.contents)
found_new_items = True
completed_checks.add(item)

if not found_new_items:
break

Expand All @@ -284,24 +260,13 @@ def _place_item(
# Figure out what nodes are accessible
reachable_nodes = assumed_search(starting_node, aux_data, remaining_item_pool)

# Out of the accessible nodes, get all item checks that are still empty
can_access_mailbox = False
for node in reachable_nodes:
if node.mailbox:
can_access_mailbox = True
for check in node.checks:
if candidates is not None and check not in candidates:
continue
if check.contents is None:
reachable_null_checks[check] = node.name

if can_access_mailbox:
for mail in aux_data.mail:
if candidates is not None and mail not in candidates:
continue
if mail.contents is None:
reachable_null_checks[mail] = f'Mail.{mail.name}'

else:
for area in aux_data.areas.values():
for room in area.rooms:
Expand All @@ -310,10 +275,6 @@ def _place_item(
if check.contents is None:
reachable_null_checks[check] = node.name

for mail in aux_data.mail:
if mail.contents is None:
reachable_null_checks[mail] = f'Mail.{mail.name}'

locations = list(reachable_null_checks.keys())
if len(locations) == 0:
raise AssumedFillFailed()
Expand Down Expand Up @@ -434,9 +395,6 @@ def assumed_fill(

item_pool.append(item)
check.contents = None # type: ignore
for mail_item in aux_data.mail:
item_pool.append(mail_item.contents)
mail_item.contents = None # type: ignore

# Shuffle the item pool
random.shuffle(item_pool)
Expand Down Expand Up @@ -464,30 +422,13 @@ def assumed_fill(
return aux_data


def init_logic_graph(
areas_directory: Path | None = None,
mail_file: Path | None = None,
enemy_mapping_file: Path | None = None,
macros_file: Path | None = None,
) -> ShufflerAuxData:
if areas_directory is None:
areas_directory = Path(__file__).parent / 'logic'
if mail_file is None:
mail_file = Path(__file__).parent / 'mail.json'
if enemy_mapping_file is None:
enemy_mapping_file = Path(__file__).parent / 'enemies.json'
if macros_file is None:
macros_file = Path(__file__).parent / 'macros.json'

aux_data = parse_aux_data(
areas_directory=areas_directory,
mail_file=mail_file,
enemy_mapping_file=enemy_mapping_file,
macros_file=macros_file,
)

annotate_logic(areas=aux_data.areas.values(), logic_directory=areas_directory)
def init_logic_graph() -> ShufflerAuxData:
aux_data = parse_aux_data()

annotate_logic(areas=aux_data.areas.values())

_connect_rooms(aux_data.areas)

connect_mail_nodes(aux_data.areas.values())

return aux_data
10 changes: 6 additions & 4 deletions ph_rando/shuffler/aux_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class Chest(BaseCheck):
zmb_mapobject_index: int = Field(..., description='Index of the chest in the defined zmb file')


class Mail(BaseCheck):
type: Literal['mail']
# TODO: what info is needed for patcher?


class Tree(BaseCheck):
type: Literal['tree']
zmb_file_path: str = Field(..., description='File path to the zmb the tree is on')
Expand Down Expand Up @@ -105,6 +110,7 @@ class MinigameRewardChest(BaseCheck):
| SalvageTreasure
| DigSpot
| MinigameRewardChest
| Mail
)


Expand Down Expand Up @@ -209,10 +215,6 @@ def json(self, *args: Any, **kwargs: Any) -> str:
return super().json(*args, exclude={'rooms': {'__all__': {'nodes', '_nodes'}}}, **kwargs)


class Mail(BaseCheck):
requirements: str


if __name__ == '__main__':
json_schema = Area.schema_json(indent=2)
with open(Path(__file__).parent / 'aux_schema.json', 'w') as fd:
Expand Down
31 changes: 31 additions & 0 deletions ph_rando/shuffler/aux_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,34 @@
"type"
]
},
"Mail": {
"title": "Mail",
"type": "object",
"properties": {
"name": {
"title": "Name",
"description": "The name of the item check",
"type": "string"
},
"contents": {
"title": "Contents",
"description": "The item that this check contains",
"type": "string"
},
"type": {
"title": "Type",
"enum": [
"mail"
],
"type": "string"
}
},
"required": [
"name",
"contents",
"type"
]
},
"Exit": {
"title": "Exit",
"type": "object",
Expand Down Expand Up @@ -430,6 +458,9 @@
},
{
"$ref": "#/definitions/MinigameRewardChest"
},
{
"$ref": "#/definitions/Mail"
}
]
}
Expand Down
66 changes: 66 additions & 0 deletions ph_rando/shuffler/logic/Mail.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"$schema": "../aux_schema.json",
"name": "Mail",
"rooms": [
{
"name": "Mail",
"chests": [
{
"name": "Salvatore",
"type": "mail",
"contents": "WisdomGem"
},
{
"name": "Romanos",
"type": "mail",
"contents": "RandomShipPart"
},
{
"name": "Jolene",
"type": "mail",
"contents": "JolenesLetter"
},
{
"name": "Joanne",
"type": "mail",
"contents": "RandomShipPart"
},
{
"name": "Linebeck",
"type": "mail",
"contents": "RandomShipPart"
},
{
"name": "Gongoron",
"type": "mail",
"contents": "CourageGem"
},
{
"name": "Aroo",
"type": "mail",
"contents": "WisdomGem"
},
{
"name": "Mutoh",
"type": "mail",
"contents": "PowerGem"
},
{
"name": "Beedle1",
"type": "mail",
"contents": "FreebieCard"
},
{
"name": "Beedle2",
"type": "mail",
"contents": "ComplimentCard"
},
{
"name": "Beedle3",
"type": "mail",
"contents": "ComplimentaryCard"
}
]
}
]
}
Loading

0 comments on commit 592d3bb

Please sign in to comment.