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
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]
|
|
}
|
|
|