feat: player statistics screen with run history

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 14:51:44 +09:00
parent 0dce30f23f
commit afdda5d72b
4 changed files with 84 additions and 1 deletions

View File

@@ -76,6 +76,39 @@ func (d *DB) SaveRun(player string, floor, score int) error {
})
}
type PlayerStats struct {
TotalRuns int
BestFloor int
TotalGold int
TotalKills int
Victories int
}
func (d *DB) GetStats(player string) (PlayerStats, error) {
var stats PlayerStats
err := d.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketRankings)
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var r RunRecord
if json.Unmarshal(v, &r) == nil && r.Player == player {
stats.TotalRuns++
if r.Floor > stats.BestFloor {
stats.BestFloor = r.Floor
}
stats.TotalGold += r.Score
if r.Floor >= 20 {
stats.Victories++
}
}
return nil
})
})
return stats, err
}
func (d *DB) TopRuns(limit int) ([]RunRecord, error) {
var runs []RunRecord
err := d.db.View(func(tx *bolt.Tx) error {

View File

@@ -21,6 +21,7 @@ const (
screenShop
screenResult
screenHelp
screenStats
)
// StateUpdateMsg is sent by GameSession to update the view
@@ -107,6 +108,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateResult(msg)
case screenHelp:
return m.updateHelp(msg)
case screenStats:
return m.updateStats(msg)
}
return m, nil
}
@@ -134,6 +137,12 @@ func (m Model) View() string {
return renderResult(m.gameState, rankings)
case screenHelp:
return renderHelp(m.width, m.height)
case screenStats:
var stats store.PlayerStats
if m.store != nil {
stats, _ = m.store.GetStats(m.playerName)
}
return renderStats(m.playerName, stats, m.width, m.height)
}
return ""
}
@@ -187,6 +196,8 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
m = m.withRefreshedLobby()
} else if isKey(key, "h") {
m.screen = screenHelp
} else if isKey(key, "s") {
m.screen = screenStats
} else if isQuit(key) {
return m, tea.Quit
}
@@ -194,6 +205,15 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m Model) updateStats(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
if isKey(key, "s") || isEnter(key) || isQuit(key) {
m.screen = screenTitle
}
}
return m, nil
}
func (m Model) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyMsg); ok {
if isKey(key, "h") || isEnter(key) || isQuit(key) {

30
ui/stats_view.go Normal file
View File

@@ -0,0 +1,30 @@
package ui
import (
"fmt"
"github.com/charmbracelet/lipgloss"
"github.com/tolelom/catacombs/store"
)
func renderStats(playerName string, stats store.PlayerStats, width, height int) string {
title := styleHeader.Render("── Player Statistics ──")
var content string
content += stylePlayer.Render(fmt.Sprintf(" %s", playerName)) + "\n\n"
content += fmt.Sprintf(" Total Runs: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalRuns)))
content += fmt.Sprintf(" Best Floor: %s\n", styleGold.Render(fmt.Sprintf("B%d", stats.BestFloor)))
content += fmt.Sprintf(" Total Gold: %s\n", styleGold.Render(fmt.Sprintf("%d", stats.TotalGold)))
content += fmt.Sprintf(" Victories: %s\n", styleHeal.Render(fmt.Sprintf("%d", stats.Victories)))
winRate := 0.0
if stats.TotalRuns > 0 {
winRate = float64(stats.Victories) / float64(stats.TotalRuns) * 100
}
content += fmt.Sprintf(" Win Rate: %s\n", styleSystem.Render(fmt.Sprintf("%.1f%%", winRate)))
footer := styleSystem.Render("[S] Back")
return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center,
lipgloss.JoinVertical(lipgloss.Center, title, "", content, "", footer))
}

View File

@@ -44,7 +44,7 @@ func renderTitle(width, height int) string {
menu := lipgloss.NewStyle().
Foreground(colorWhite).
Bold(true).
Render("[Enter] Start [H] Help [Q] Quit")
Render("[Enter] Start [H] Help [S] Stats [Q] Quit")
content := lipgloss.JoinVertical(lipgloss.Center,
logo,