Docker container that powers the high scores page at nethack.jerryaldrichiii.com
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

169 lines
3.8 KiB

package logparser
import (
"bufio"
"fmt"
"math"
"os"
"sort"
"strconv"
"strings"
"time"
)
type Record struct {
Version string
Points int
DeathLocation string
DeathDungeonLevel int
MaxLevel int
HP int
MaxHP int
NumDeaths int
EndDate string
StartDate string
UserID int
Role string
Race string
Gender string
Alignment string
Name string
DeathReason string
}
// ParseLog parses a NetHack logfile.
// If any errors are encountered, an empty []Record is returned with all the
// errors.
// See: https://nethackwiki.com/wiki/Logfile
func ParseLog(log *os.File) ([]Record, []error) {
scanner := bufio.NewScanner(log)
records := []Record{}
var err error
errors := []error{}
for scanner.Scan() {
record := Record{}
fields := strings.Fields(scanner.Text())
dungeonNum, _ := strconv.Atoi(fields[2])
dungeonLvl, _ := strconv.Atoi(fields[3])
record.DeathDungeonLevel = dungeonLvl
record.DeathLocation, err = ParseDungeon(dungeonNum, dungeonLvl)
if err != nil {
errors = append(errors, err)
}
record.Version = fields[0]
record.UserID, err = strconv.Atoi(fields[10])
if err != nil {
errors = append(errors, err)
}
record.Role = fields[11]
record.Race = fields[12]
record.Gender = fields[13]
record.Alignment = fields[14]
record.Name = strings.Split(fields[15], ",")[0]
remainingFields := strings.Join(fields[15:], " ")
record.DeathReason = strings.Split(remainingFields, record.Name+",")[1]
record.Points, err = strconv.Atoi(fields[1])
if err != nil {
errors = append(errors, err)
}
record.MaxLevel, err = strconv.Atoi(fields[4])
if err != nil {
errors = append(errors, err)
}
record.HP, err = strconv.Atoi(fields[5])
if err != nil {
errors = append(errors, err)
}
record.MaxHP, err = strconv.Atoi(fields[6])
if err != nil {
errors = append(errors, err)
}
record.NumDeaths, err = strconv.Atoi(fields[7])
if err != nil {
errors = append(errors, err)
}
startDate, err := time.Parse("20060102", fields[9])
if err != nil {
errors = append(errors, err)
}
record.StartDate = startDate.Format("2006-01-02")
endDate, err := time.Parse("20060102", fields[8])
if err != nil {
errors = append(errors, err)
}
record.EndDate = endDate.Format("2006-01-02")
records = append(records, record)
}
if len(errors) > 0 {
return []Record{}, errors
}
return records, nil
}
// ParseDungeon takes a dungeon number and a dungeon level returns the correct
// location.
// See: https://nethackwiki.com/wiki/Logfile
func ParseDungeon(dNum int, dLvl int) (string, error) {
if dNum < 0 || dNum > 7 {
return "", fmt.Errorf("Invalid dungeon number '%v'", dNum)
}
if dNum == 7 && (dLvl > -1 || dLvl < -5) {
return "", fmt.Errorf(
"Invalid plane '%v': Must be > -1 or < -5 if dungeon number is 7", dLvl,
)
}
if dNum != 7 && (dLvl < 0) {
return "", fmt.Errorf(
"Invalid dungeon level '%v': Must be > 0 if dungeon number != 7", dLvl,
)
}
// Dungeon Level is negative if on one of the planes
planes := []string{
"Plane of Earth",
"Plane of Air",
"Plane of Fire",
"Plane of Water",
"Astral Plane",
}
if dNum == 7 {
// Convert to positive float to get abosoulte then convert to int and
// subtract one for correct index
return planes[int(math.Abs(float64(dLvl)))-1], nil
}
dungeons := []string{
"The Dungeons of Doom",
"Gehennom",
"The Gnomish Mines",
"The Quest",
"Sokoban",
"Fort Ludios",
"Vlad's Tower",
}
return dungeons[dNum], nil
}
func TopX(records []Record, top int) []Record {
sorted := records
sort.Slice(sorted, func(i, j int) bool {
return records[i].Points > records[j].Points
})
if top > len(sorted) {
top = len(sorted)
}
return sorted[0:top]
}