From bcfd42970bac8676e2bfac0227344bd55e0dfee4 Mon Sep 17 00:00:00 2001 From: Kistaro Windrider Date: Sat, 1 Apr 2023 14:38:45 -0700 Subject: [PATCH] BasicStatsPanel Implement a stats panel with a name, intro text, and a rule for how to pull stats out of a collection. --- cardsim/infopanel.go | 156 ++++++++++++++++++++++++++++++++++++++++++- cardsim/stats.go | 6 +- 2 files changed, 159 insertions(+), 3 deletions(-) diff --git a/cardsim/infopanel.go b/cardsim/infopanel.go index fdbed04..01a3c5a 100644 --- a/cardsim/infopanel.go +++ b/cardsim/infopanel.go @@ -1,5 +1,10 @@ package cardsim +import ( + "fmt" + "strings" +) + // An InfoPanel displays some set of stats to the player. It does // not consume an action. It must not advance the state of the game // in any way. @@ -8,6 +13,155 @@ type InfoPanel[C StatsCollection] interface { // label presented to the player to access this panel. Title(p *Player[C]) (Message, error) - // Info returns the contents of this InfoPanel. + // Info returns the displayable contents of this InfoPanel. A nil Message + // in the output is interpreted as a paragraph break. Info(p *Player[C]) ([]Message, error) } + +// A StatFilter decides whether to show a specific stat in a BasicStatsPanel +// (and maybe other kinds of stats panels, if they choose to support this). +type StatFilter[C StatsCollection] func(p *Player[C], s Stat) bool + +// BasicStatsPanel shows some or all of the stats output by C, under +// a fixed name, introduced by a specific prompt. Stats are shown as a two +// column table with the name, then the value. +type BasicStatsPanel[C StatsCollection] struct { + // Name stores the name of this stats panel, which is also shown in the menu. + Name Message + + // Intro stores a message to always display before the stats. Optional. + Intro Message + + // Filter stores a function to decide what stats to show. If this is not + // provided, the BasicStatsPanel uses VisibleOrDebug by default. + Filter StatFilter[C] +} + +// VisibleOrDebug returns whether s is Visible or p is in debug mode, +// so a debug-mode player shows all stats. +func VisibleOrDebug[C StatsCollection](p *Player[C], s Stat) bool { + return p.DebugLevel > 0 || s.Visible() +} + +// Title implements `InfoPanel[C]` by returning b's `Name`. +func (b *BasicStatsPanel[C]) Title(p *Player[C]) (Message, error) { + return b.Name, nil +} + +// Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for +// whch b.Filter returns true. +func (b *BasicStatsPanel[C]) Info(p *Player[C]) ([]Message, error) { + stats := p.Stats.Stats() + cached := make([]cachedStat, 0, len(stats)) + longestName := 0 + filter := b.Filter + if filter == nil { + filter = VisibleOrDebug[C] + } + + for _, s := range stats { + if !filter(p, s) { + continue + } + name := s.StatName() + if len(name) > longestName { + longestName = len(name) + } + cached = append(cached, cachedStat{name, s.String(), s.Visible()}) + } + + if len(cached) == 0 { + return []Message{b.Intro, nil, MsgStr("No stats available")}, nil + } + + ret := make([]Message, 0, 2+len(cached)) + ret = append(ret, b.Intro, nil) + for _, s := range cached { + ret = append(ret, MsgStr(s.output(longestName))) + } + return ret, nil +} + +// cachedStat is an implementation detail of BasicStatsPanel.Info. It stores the +// values out of a stat so they do not need to be recalculated, in case they +// are expensive to calculate or the filter deciding which stats to output +// was expensive. +type cachedStat struct { + name string + value string + visible bool +} + +// output returns a string representing this stat, right-aligning the name in +// a nameWidth-wide field and prefixing it with a bullet: "•" for a visible +// stat and "◦" for an invisible stat. If nameWidth is 0, the name and +// colon are omitted. If it is negative, the name is emitted as-is with +// no alignment. If it is too short for the name and it is nonzero, +// it is truncated with "⋯". +func (c cachedStat) output(nameWidth int) string { + bullet := "◦" + if c.visible { + bullet = "•" + } + + if nameWidth == 0 { + return fmt.Sprintf("%s %s", bullet, c.value) + } + + name := c.name + if len(name) < nameWidth { + name = strings.Repeat(" ", nameWidth-len(name)) + name + } + if len(name) > nameWidth && nameWidth > 0 { + name = name[:nameWidth-1] + "⋯" + } + + return fmt.Sprintf("%s %s: %s", bullet, name, c.value) +} + +// StatsNamed returns a StatFilter[C] matching any stat with a listed name. +func StatsNamed[C StatsCollection](names ...string) StatFilter[C] { + nameSet := make(map[string]bool) + for _, n := range names { + nameSet[n] = true + } + return func(_ *Player[C], s Stat) bool { + return nameSet[s.StatName()] + } +} + +// VisibleOrDebugStatsNamed returns a StatFilter[C] matching any visible stat +// with a listed name, or any stat with a listed name if the player is in +// debug mode. +func VisibleOrDebugStatsNamed[C StatsCollection](names ...string) StatFilter[C] { + return All(VisibleOrDebug[C], StatsNamed[C](names...)) +} + +// All returns a StatFilter[C] that requires a Stat to match all provided +// filters. If no filters are provided, All matches every Stat (it's very easy +// to meet every requirement when there are no requirements). +func All[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] { + return func(p *Player[C], s Stat) bool { + for _, f := range ff { + if !f(p, s) { + return false + } + } + return true + } +} + +// Any returns a StatFilter[C] that requires a Stat to match any one or more of +// the filters provided. If no filters are provided, Any never matches a stat +// (it's very hard to meet at least one requirement out when there are no +// requirements). +func Any[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] { + return func(p *Player[C], s Stat) bool { + for _, f := range ff { + if f(p, s) { + return true + } + } + return false + } +} diff --git a/cardsim/stats.go b/cardsim/stats.go index 08aafce..51f5a8d 100644 --- a/cardsim/stats.go +++ b/cardsim/stats.go @@ -10,8 +10,10 @@ import ( // A StatsCollection contains stats. type StatsCollection interface { // Stats returns all the stats in this collection. It's okay for - // these to be copies rather than pointers. Stats will be presented - // to the player in this order. + // these to be copies rather than pointers. BasicStatsPanel presents + // stats to the player in this order. It's okay for this list to + // contain nil entries; these are interpreted as line breaks, + // section breaks, etc. Stats() []Stat }