package game import ( "encoding/gob" "fmt" "os" "time" ) // score.h / rip.c score() / save.c rd_score+wr_score — the scoreboard. // The C scorefile was XOR-encrypted structs; the port stores a gob list at // ScorePath (empty path disables persistence but still shows the list). // numScores is the C numscores default: the top ten. const numScores = 10 // ScoreEnt is score.h struct sc_ent. type ScoreEnt struct { UID int Score int Flags int // 0=killed, 1=quit, 2=winner, 3=killed with amulet Monster byte Name string Level int Time int64 } // rdScore reads the scoreboard file (save.c rd_score). func (g *RogueGame) rdScore() []ScoreEnt { topTen := make([]ScoreEnt, numScores) if g.ScorePath == "" { return topTen } f, err := os.Open(g.ScorePath) if err != nil { return topTen } defer func() { _ = f.Close() }() // read-only handle var onDisk []ScoreEnt decErr := gob.NewDecoder(f).Decode(&onDisk) if decErr != nil { return topTen // unreadable scoreboard reads as empty, as in C } copy(topTen, onDisk) return topTen } // wrScore writes the scoreboard file (save.c wr_score). func (g *RogueGame) wrScore(topTen []ScoreEnt) { if g.ScorePath == "" { return } // lock_sc/unlock_sc: exclusive-create lock file with stale takeover. // The whole scoreboard write is best effort, as it was in C: a shared // scoreboard must never take the game down. lock := g.ScorePath + ".lck" for range 5 { lf, err := os.OpenFile(lock, //nolint:gosec // G304: configured path os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err == nil { _ = lf.Close() defer func() { _ = os.Remove(lock) }() break } fi, statErr := os.Stat(lock) if statErr == nil && time.Since(fi.ModTime()) > staleLockAge { _ = os.Remove(lock) continue } time.Sleep(time.Second) } f, err := os.OpenFile(g.ScorePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return } _ = gob.NewEncoder(f).Encode(topTen) _ = f.Close() } // staleLockAge is how old a scoreboard lock file may be before another // process assumes its owner died and takes it over (mach_dep.c lock_sc // aged its lock the same way). const staleLockAge = 10 * time.Second // score figures the score and posts it (rip.c score). flags -1 means just // display the list (the -s command line option). func (g *RogueGame) score(amount, flags int, monst byte) { if flags >= 0 && g.scr != nil && g.scr.term != nil { g.mvaddstr(NumLines-1, 0, "[Press return to continue]") g.refresh() g.waitFor('\n') } topTen := g.rdScore() // Insert her in list if need be ins := -1 if !g.NoScore && flags >= 0 { uid := os.Getuid() scp := len(topTen) for i := range topTen { if amount > topTen[i].Score { scp = i break } else if !g.AllScore && flags != 2 && topTen[i].UID == uid && topTen[i].Flags != 2 { // only one score per nowin uid scp = len(topTen) break } } if scp < len(topTen) { sc2 := len(topTen) - 1 if flags != 2 && !g.AllScore { for i := scp; i < len(topTen); i++ { if topTen[i].UID == uid && topTen[i].Flags != 2 { sc2 = i break } } } for sc2 > scp { topTen[sc2] = topTen[sc2-1] sc2-- } lvl := g.Depth if flags == 2 { lvl = g.MaxDepth } topTen[scp] = ScoreEnt{ UID: uid, Score: amount, Flags: flags, Monster: monst, Name: g.Whoami, Level: lvl, Time: time.Now().Unix(), } ins = scp } } // Build the list display label := "Rogueists" if g.AllScore { label = "Scores" } lines := []string{ fmt.Sprintf("Top Ten %s:", label), " Score Name", } highlight := -1 for i := range topTen { scp := &topTen[i] if scp.Score == 0 { break } line := fmt.Sprintf("%2d %5d %s: %s on level %d", i+1, scp.Score, scp.Name, g.data.scoreReasons[scp.Flags], scp.Level) if scp.Flags == 0 || scp.Flags == 3 { line += " by " + g.killname(scp.Monster, true) } line += "." if i == ins { highlight = len(lines) } lines = append(lines, line) } if g.scr != nil && g.scr.term != nil { g.clear() for i, line := range lines { if i == highlight { g.standout() } g.mvaddstr(i, 0, line) if i == highlight { g.standend() } } g.refresh() } else { for _, line := range lines { _, _ = fmt.Fprintln(os.Stdout, line) // CLI output } } // Update the list file if ins >= 0 { g.wrScore(topTen) } } // ShowScores implements the -s command line option: print the scoreboard // and nothing else. func (g *RogueGame) ShowScores() { g.NoScore = true g.score(0, -1, 0) }