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. type InfoPanel[C StatsCollection] interface { // Title returns the title of this InfoPanel, which is also used as the // label presented to the player to access this panel. Title(p *Player[C]) Message // 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 { return b.Name } // 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 } }