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) {
|
func (d *DB) TopRuns(limit int) ([]RunRecord, error) {
|
||||||
var runs []RunRecord
|
var runs []RunRecord
|
||||||
err := d.db.View(func(tx *bolt.Tx) error {
|
err := d.db.View(func(tx *bolt.Tx) error {
|
||||||
|
|||||||
20
ui/model.go
20
ui/model.go
@@ -21,6 +21,7 @@ const (
|
|||||||
screenShop
|
screenShop
|
||||||
screenResult
|
screenResult
|
||||||
screenHelp
|
screenHelp
|
||||||
|
screenStats
|
||||||
)
|
)
|
||||||
|
|
||||||
// StateUpdateMsg is sent by GameSession to update the view
|
// 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)
|
return m.updateResult(msg)
|
||||||
case screenHelp:
|
case screenHelp:
|
||||||
return m.updateHelp(msg)
|
return m.updateHelp(msg)
|
||||||
|
case screenStats:
|
||||||
|
return m.updateStats(msg)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -134,6 +137,12 @@ func (m Model) View() string {
|
|||||||
return renderResult(m.gameState, rankings)
|
return renderResult(m.gameState, rankings)
|
||||||
case screenHelp:
|
case screenHelp:
|
||||||
return renderHelp(m.width, m.height)
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -187,6 +196,8 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m = m.withRefreshedLobby()
|
m = m.withRefreshedLobby()
|
||||||
} else if isKey(key, "h") {
|
} else if isKey(key, "h") {
|
||||||
m.screen = screenHelp
|
m.screen = screenHelp
|
||||||
|
} else if isKey(key, "s") {
|
||||||
|
m.screen = screenStats
|
||||||
} else if isQuit(key) {
|
} else if isQuit(key) {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
}
|
}
|
||||||
@@ -194,6 +205,15 @@ func (m Model) updateTitle(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
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) {
|
func (m Model) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if key, ok := msg.(tea.KeyMsg); ok {
|
if key, ok := msg.(tea.KeyMsg); ok {
|
||||||
if isKey(key, "h") || isEnter(key) || isQuit(key) {
|
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().
|
menu := lipgloss.NewStyle().
|
||||||
Foreground(colorWhite).
|
Foreground(colorWhite).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Render("[Enter] Start [H] Help [Q] Quit")
|
Render("[Enter] Start [H] Help [S] Stats [Q] Quit")
|
||||||
|
|
||||||
content := lipgloss.JoinVertical(lipgloss.Center,
|
content := lipgloss.JoinVertical(lipgloss.Center,
|
||||||
logo,
|
logo,
|
||||||
|
|||||||
Reference in New Issue
Block a user