#!/usr/bin/env python3 # Eryn Wells ''' Converts a Nethack logfile (or record file) to JSON for easier parsing by my website's templating engine. See https://nethackwiki.com/wiki/Logfile for information about the format of these logfiles. ''' import argparse import datetime import json from locale import normalize import os.path import subprocess import sys DUNGEONS = { 0: 'The Dungeons of Doom', 1: 'Gehennom', 2: 'The Gnomish Mines', 3: 'The Quest', 4: 'Sokoban', 5: 'Fort Ludios', 6: 'Vlad’s Tower', 7: 'The Elemental Planes', } # The "dungeon level" field is normally positive, but negatives indicate one of these # levels. SPECIAL_DUNGEON_LEVELS = { -5: 'Astral Plane', -4: 'Plane of Water', -3: 'Plane of Fire', -2: 'Plane of Air', -1: 'Plane of Earth', } RACES = { 'Elf': 'Elf', 'Gno': 'Gnome', 'Hum': 'Human', 'Dwa': 'Dwarf', } ROLES = { 'Arc': 'Archaeologist', 'Kni': 'Knight', 'Mon': 'Monk', 'Pri': 'Priest', 'Ran': 'Ranger', 'Rog': 'Rogue', 'Sam': 'Samurai', 'Val': 'Valkyrie', } GENDERS = { 'Fem': 'Female', 'Mal': 'Male', } ALIGNMENTS = { 'Law': 'Lawful', 'Neu': 'Neutral', 'Cha': 'Chaotic', } def parse_args(argv, *a, **kw): parser = argparse.ArgumentParser(*a, **kw) parser.add_argument('-o', '--output', help='Path to the output file') parser.add_argument('logfile', help='Path to the Nethack log file to convert') args = parser.parse_args(argv) return args def main(argv): args = parse_args(argv[1:], prog=argv[0]) if not os.path.isfile(args.logfile): print('Given path is not a real file!', file=sys.stderr) return -1 hostname = subprocess.check_output(['hostname', '-s']).decode('ascii').strip() records = [] with open(args.logfile) as logfile: for record in logfile: fields = record.split(maxsplit=15) dungeon_number = int(fields[2]) dungeon_name = DUNGEONS[dungeon_number] dungeon_level = int(fields[3]) if dungeon_level > 0: dungeon_level_descriptive = f"Level {dungeon_level}" else: dungeon_level_descriptive = SPECIAL_DUNGEON_LEVELS[dungeon_level] max_dungeon_level = int(fields[4]) if max_dungeon_level > 0: max_dungeon_level_descriptive = f"Level {max_dungeon_level}" else: max_dungeon_level_descriptive = SPECIAL_DUNGEON_LEVELS[max_dungeon_level] start_date = datetime.datetime.strptime(fields[9], '%Y%m%d').strftime('%Y-%m-%d') end_date = datetime.datetime.strptime(fields[8], '%Y%m%d').strftime('%Y-%m-%d') name, cause_of_death = (s.strip() for s in fields[15].split(',', maxsplit=1)) role = fields[11] race = fields[12] gender = fields[13] alignment = fields[14] records.append({ 'score': int(fields[1]), 'dungeon': { 'n': int(fields[2]), 'name': dungeon_name, 'level': {'n': dungeon_level, 'descriptive': dungeon_level_descriptive}, 'max_level': {'n': max_dungeon_level, 'descriptive': max_dungeon_level_descriptive}, }, 'end_date': end_date, 'start_date': start_date, 'character': { 'name': name, 'descriptor': f'{name}-{role}-{race}-{gender}-{alignment}', 'hp': {'n': int(fields[5]), 'max': int(fields[6])}, 'role': {'short': role, 'descriptive': ROLES[role]}, 'race': {'short': race, 'descriptive': RACES[race]}, 'gender': {'short': gender, 'descriptive': GENDERS[gender]}, 'alignment': {'short': alignment, 'descriptive': ALIGNMENTS[alignment]}, }, 'death': {'n': int(fields[7]), 'cause': cause_of_death}, 'system': { 'hostname': hostname, 'user_id': int(fields[10]), 'nethack_version': fields[0], } }) output_object = { 'generated': datetime.datetime.now().isoformat(), 'logfile': records, } logfile_has_changed = False output_path = args.output output_file = None if output_path and output_path != '-': with open(output_path, 'r') as existing_logfile: existing_logfile_object = json.load(existing_logfile) logfile_has_changed = existing_logfile_object.get('logfile', {}) != records if logfile_has_changed: output_file = open(output_path, 'w') else: output_file = sys.stdout if output_file: json.dump(output_object, output_file, indent=2) output_file.write('\n') output_file.close() else: print('No changes to logfile') return 0 if __name__ == '__main__': import sys result = main(sys.argv) sys.exit(0 if not result else result)