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