BasicStatsPanel
Implement a stats panel with a name, intro text, and a rule for how to pull stats out of a collection.
This commit is contained in:
		| @@ -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 | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user