feat: player statistics screen with run history
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
store/db.go
33
store/db.go
@@ -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 {
|
||||
|
||||
20
ui/model.go
20
ui/model.go
@@ -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
30
ui/stats_view.go
Normal 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))
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user