Refactor entire script into a class

- Shove all the logic in a class
- Allow for `cleanup_SECTION` functions, one that returns true will
delete the calling section in post-run
- Get rid of that awful awful `globals()` call, which was the main
motivation of this
- Document the new methods

This should result in a horrifying diff that claims 98% of the file has
changed but no actual change in logic or output.
This commit is contained in:
Trysdyn Black 2024-10-11 17:45:31 -07:00
parent eb924c14c2
commit 14ec718890

151
main.py
View file

@ -2,7 +2,7 @@
"""Parse BCEX (or BCCE) logs into json objects.""" """Parse BCEX (or BCCE) logs into json objects."""
__version__ = "0.4.0" __version__ = "0.4.1"
__author__ = "Trysdyn Black" __author__ = "Trysdyn Black"
import json import json
@ -10,7 +10,17 @@ import sys
from pathlib import Path from pathlib import Path
def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912 class Parser:
"""BCEX/BCCE spoiler logfile parser."""
def __init__(self, filename: str) -> None:
"""Initialize parser with filename."""
self.filename = filename
self.config_sections = {}
self.data_sections = {}
@staticmethod
def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912
""" """
Parse the MONSTERS section. Parse the MONSTERS section.
@ -67,8 +77,8 @@ def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912
return result return result
@staticmethod
def parse_REMONSTERATE(data: str) -> dict[str, dict]: def parse_REMONSTERATE(data: str) -> dict[str, dict]:
""" """
Parse the BCCE-only REMONSTERATE section. Parse the BCCE-only REMONSTERATE section.
@ -86,8 +96,8 @@ def parse_REMONSTERATE(data: str) -> dict[str, dict]:
return result return result
@staticmethod
def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901 def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
""" """
Parse the CHARACTERS section. Parse the CHARACTERS section.
@ -98,7 +108,11 @@ def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
Regardless of flavor, core logic will snap stats back into this section Regardless of flavor, core logic will snap stats back into this section
later. later.
""" """
replacements = {"Looks like": "looks", "World of Ruin location": "wor_location", "Notable equipment": "equipment"} replacements = {
"Looks like": "looks",
"World of Ruin location": "wor_location",
"Notable equipment": "equipment",
}
result = {} result = {}
@ -152,8 +166,8 @@ def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
return result return result
@staticmethod
def parse_STATS(data: str) -> dict[str, dict]: def parse_STATS(data: str) -> dict[str, dict]:
""" """
Parse the BCCE-only STATS section. Parse the BCCE-only STATS section.
@ -187,8 +201,8 @@ def parse_STATS(data: str) -> dict[str, dict]:
return result return result
@staticmethod
def parse_COMMANDS(data: str) -> dict[str, dict]: def parse_COMMANDS(data: str) -> dict[str, dict]:
""" """
Parse the COMMANDS section. Parse the COMMANDS section.
@ -227,8 +241,8 @@ def parse_COMMANDS(data: str) -> dict[str, dict]:
return commands return commands
@staticmethod
def parse_SEED(data: str) -> dict[str, bool | str]: def parse_SEED(data: str) -> dict[str, bool | str]:
""" """
Parse the injected SEED section. Parse the injected SEED section.
@ -248,8 +262,8 @@ def parse_SEED(data: str) -> dict[str, bool | str]:
"seed": data, "seed": data,
} }
@staticmethod
def parse_SECRET_ITEMS(data: str) -> list[str]: def parse_SECRET_ITEMS(data: str) -> list[str]:
""" """
Parse the BCCE-only SECRET ITEMS section. Parse the BCCE-only SECRET ITEMS section.
@ -259,12 +273,51 @@ def parse_SECRET_ITEMS(data: str) -> list[str]:
# I have no idea what this is lol, dump it to a list for now # I have no idea what this is lol, dump it to a list for now
return [line for line in data.split("\n") if not line.startswith("---")] return [line for line in data.split("\n") if not line.startswith("---")]
@staticmethod
def cleanup_STATS(data: dict) -> bool:
"""
Fold BCCE-only STATS section back into CHARACTERS data.
def load(filename: str) -> dict[str, str]: This returns BCCE logs back to how they were laid out in BCEX: where stat blocks
"""Load file and tokenize into sections.""" were simply part of the CHARACTERS data.
# Load our file, tokenize by section header (starting with ====)
with Path(filename).open(encoding="utf-8") as infile: The BCCE STATS section keys on character slot (Terra, Locke, etc) and not the
tok_data = infile.read().split("============================================================\n") new randomized character name, so some hunting has to happen here.
"""
for slot, stats in data.get("STATS", {}).items():
for c_data in data.get("CHARACTERS", {}).values():
if c_data["originally"].lower() == slot.lower():
c_data["stats"] = stats
return True
@staticmethod
def cleanup_COMMANDS(data: dict) -> bool:
"""Fold COMMANDS expanded descriptions into CHARACTERS command data."""
# If our COMMANDS section is missing or somehow missing a given command, we just
# repeat the command's name as its description. This should only be simple things
# like "fight" and "magic" unless something goes wrong.
command_info = data.get("COMMANDS", {})
for c_data in data.get("CHARACTERS", {}).values():
for command in c_data.get("commands", {}):
c_data["commands"][command] = command_info.get(command, command)
return False
@staticmethod
def cleanup_REMONSTERATE(data: dict) -> bool:
"""Fold REMONSTERATE section into MONSTERS section data."""
for name, info in data.get("REMONSTERATE", {}).items():
for m_name, m_info in data.get("MONSTERS", {}).items():
if name == m_name:
m_info["originally"] = info["originally"]
m_info["sprite"] = info["sprite"]
return True
def get_sections(self, data: str) -> dict[str, str]:
"""Split logfile text and return a dict of sections for parsing."""
tok_data = data.split("============================================================\n")
sections = {} sections = {}
@ -280,51 +333,43 @@ def load(filename: str) -> dict[str, str]:
section_header, section_data = s.split("\n", 1) section_header, section_data = s.split("\n", 1)
sections[section_header[5:]] = section_data sections[section_header[5:]] = section_data
self.config_sections = sections
return sections return sections
def parse(self) -> dict:
if __name__ == "__main__": """Fully parse the logfile and return the full data object."""
sections = load(sys.argv[1]) # Get individual sections to work on
with Path(self.filename).open(encoding="utf-8") as infile:
sections = self.get_sections(infile.read())
data = {} data = {}
# This mess tries to run a function named parse_SECTION for each section, # For each section attempt to run a parser function for it
# and just continues to the next section if one doesn't exist.
for k, v in sections.items(): for k, v in sections.items():
try:
section_func = f"parse_{k.replace(' ', '_')}" section_func = f"parse_{k.replace(' ', '_')}"
data[k] = globals()[section_func](v) if hasattr(self, section_func):
except KeyError: data[k] = getattr(self, section_func)(v)
continue
# Hydrate each character's command list with descriptions of the commands # Do post-parse cleanup. We need all sections parsed to do these
# This uses the COMMANDS section, but if one doesn't exist just repeat the command # Any cleanup function that returns true has its respective section deleted
# name because it should be simple things like "fight" and "magic" section_dels = set()
command_info = data.get("COMMANDS", {}) for k in data:
for c_data in data["CHARACTERS"].values(): section_func = f"cleanup_{k.replace(' ', '_')}"
for command in c_data["commands"]: if hasattr(self, section_func) and getattr(self, section_func)(data):
c_data["commands"][command] = command_info.get(command, command) section_dels.add(k)
# If we have a STATS block, snap it into CHARACTER data # Any section cleanup that returns true means delete that section
# BCCE broke this out into its own section for k in section_dels:
# Worse, it keys on slot name, not randomized character name if k in data:
if "STATS" in data: del data[k]
for slot, stats in data["STATS"].items():
for c_data in data["CHARACTERS"].values():
if c_data["originally"].lower() == slot.lower():
c_data["stats"] = stats
del data["STATS"] self.data_sections = data
return data
# If we ran BCCE Remonsterate, fold sprite data into monster block
if "REMONSTERATE" in data:
for name, info in data["REMONSTERATE"].items():
for m_name, m_info in data["MONSTERS"].items():
if name == m_name:
m_info["originally"] = info["originally"]
m_info["sprite"] = info["sprite"]
del data["REMONSTERATE"] if __name__ == "__main__":
p = Parser(sys.argv[1])
data = p.parse()
# Barf this pile of trash out # Barf this pile of trash out
print(json.dumps(data)) print(json.dumps(data))