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."""
__version__ = "0.4.0"
__version__ = "0.4.1"
__author__ = "Trysdyn Black"
import json
@ -10,7 +10,17 @@ import sys
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.
@ -67,8 +77,8 @@ def parse_MONSTERS(data: str) -> dict[str, dict]: # noqa: C901, PLR0912
return result
def parse_REMONSTERATE(data: str) -> dict[str, dict]:
@staticmethod
def parse_REMONSTERATE(data: str) -> dict[str, dict]:
"""
Parse the BCCE-only REMONSTERATE section.
@ -86,8 +96,8 @@ def parse_REMONSTERATE(data: str) -> dict[str, dict]:
return result
def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
@staticmethod
def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
"""
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
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 = {}
@ -152,8 +166,8 @@ def parse_CHARACTERS(data: str) -> dict[str, dict]: # noqa: C901
return result
def parse_STATS(data: str) -> dict[str, dict]:
@staticmethod
def parse_STATS(data: str) -> dict[str, dict]:
"""
Parse the BCCE-only STATS section.
@ -187,8 +201,8 @@ def parse_STATS(data: str) -> dict[str, dict]:
return result
def parse_COMMANDS(data: str) -> dict[str, dict]:
@staticmethod
def parse_COMMANDS(data: str) -> dict[str, dict]:
"""
Parse the COMMANDS section.
@ -227,8 +241,8 @@ def parse_COMMANDS(data: str) -> dict[str, dict]:
return commands
def parse_SEED(data: str) -> dict[str, bool | str]:
@staticmethod
def parse_SEED(data: str) -> dict[str, bool | str]:
"""
Parse the injected SEED section.
@ -248,8 +262,8 @@ def parse_SEED(data: str) -> dict[str, bool | str]:
"seed": data,
}
def parse_SECRET_ITEMS(data: str) -> list[str]:
@staticmethod
def parse_SECRET_ITEMS(data: str) -> list[str]:
"""
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
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]:
"""Load file and tokenize into sections."""
# Load our file, tokenize by section header (starting with ====)
with Path(filename).open(encoding="utf-8") as infile:
tok_data = infile.read().split("============================================================\n")
This returns BCCE logs back to how they were laid out in BCEX: where stat blocks
were simply part of the CHARACTERS data.
The BCCE STATS section keys on character slot (Terra, Locke, etc) and not the
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 = {}
@ -280,51 +333,43 @@ def load(filename: str) -> dict[str, str]:
section_header, section_data = s.split("\n", 1)
sections[section_header[5:]] = section_data
self.config_sections = sections
return sections
if __name__ == "__main__":
sections = load(sys.argv[1])
def parse(self) -> dict:
"""Fully parse the logfile and return the full data object."""
# Get individual sections to work on
with Path(self.filename).open(encoding="utf-8") as infile:
sections = self.get_sections(infile.read())
data = {}
# This mess tries to run a function named parse_SECTION for each section,
# and just continues to the next section if one doesn't exist.
# For each section attempt to run a parser function for it
for k, v in sections.items():
try:
section_func = f"parse_{k.replace(' ', '_')}"
data[k] = globals()[section_func](v)
except KeyError:
continue
if hasattr(self, section_func):
data[k] = getattr(self, section_func)(v)
# Hydrate each character's command list with descriptions of the commands
# This uses the COMMANDS section, but if one doesn't exist just repeat the command
# name because it should be simple things like "fight" and "magic"
command_info = data.get("COMMANDS", {})
for c_data in data["CHARACTERS"].values():
for command in c_data["commands"]:
c_data["commands"][command] = command_info.get(command, command)
# Do post-parse cleanup. We need all sections parsed to do these
# Any cleanup function that returns true has its respective section deleted
section_dels = set()
for k in data:
section_func = f"cleanup_{k.replace(' ', '_')}"
if hasattr(self, section_func) and getattr(self, section_func)(data):
section_dels.add(k)
# If we have a STATS block, snap it into CHARACTER data
# BCCE broke this out into its own section
# Worse, it keys on slot name, not randomized character name
if "STATS" in data:
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
# Any section cleanup that returns true means delete that section
for k in section_dels:
if k in data:
del data[k]
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
print(json.dumps(data))