package cardsim import ( "fmt" "reflect" "sort" "strings" ) // 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. 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 } // A Stat is some value that can be printed as part of player status. // It may not be an actual stored value -- it might only be calculated. type Stat interface { // StatName returns the name of this stat, as displayed to the player. StatName() string // String prints the value of the stat. String() string // compatible with fmt.Stringer // Visible returns whether this stat should be displayed to the player // during regular gameplay. (Invisible stats are important for debugging.) Visible() bool } // Stored is a generic Stat implementation that stores a stat value and name. // It's visible to the player. type Stored[T any] struct { // Display name of this Stat. Name string // Value of this Stat. Can be overwritten. Value T } // Statname implements Stat. func (s Stored[T]) StatName() string { return s.Name } // String implements Stat and fmt.Stringer. func (s Stored[T]) String() string { return fmt.Sprint(s.Value) } // Visible implements Stat. func (Stored[T]) Visible() bool { return true } // Invisible is a generic Stat implementation that stores a stat value and name. // It's not visible to the player. type Invisible[T any] struct { // Display name of this Stat. Name string // Value of this Stat. Can be overwritten. Value T } // Statname implements Stat. func (i Invisible[T]) StatName() string { return i.Name } // String implements Stat and fmt.Stringer. func (i Invisible[T]) String() string { return fmt.Sprint(i.Value) } // Visible implements Stat. func (Invisible[T]) Visible() bool { return false } // StatFunc names a function as a stat visible to the player. func StatFunc[T any](name string, f func() T) Stat { return statFunc[T]{ f: f, name: name, visible: true, } } // InvisibleStatFunc names a function as a stat not visible to the player. func InvisibleStatFunc[T any](name string, f func() T) Stat { return statFunc[T]{ f: f, name: name, visible: false, } } // statFunc wraps a function as a stat. type statFunc[T any] struct { f func() T name string visible bool } func (s statFunc[T]) StatName() string { return s.name } func (s statFunc[T]) String() string { return fmt.Sprint(s.f()) } func (s statFunc[T]) Visible() bool { return s.visible } // ExtractStats pulls all exported Stat fields (not functions) out of a struct. // If x cannot be resolved to a struct, it panics. It unwraps interfaces and // follows pointers to try to find a struct. func ExtractStats(x any) []Stat { v := reflect.ValueOf(x) for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() { v = v.Elem() } if v.Kind() != reflect.Struct { panic(fmt.Errorf("%T is not a struct", x)) } var ret []Stat lim := v.NumField() for i := 0; i < lim; i++ { f := v.Field(i) if !f.CanInterface() { continue } x := f.Interface() if s, ok := x.(Stat); ok { ret = append(ret, s) } } return ret } // SortStats sorts the provided slice of stats in place. It puts all visible // stats before all invisible stats, then alphabetizes (case-insensitive). func SortStats(ss []Stat) { sort.Sort(statSorter(ss)) } // statSorter implements sort.Interface for []Stat. type statSorter []Stat // Len implements sort.Interface. func (s statSorter) Len() int { return len(s) } // Swap implements sort.Interface. func (s statSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Less implements sort.Interface. func (s statSorter) Less(i, j int) bool { lhs, rhs := s[i], s[j] if lhs.Visible() != rhs.Visible() { return lhs.Visible() } ln, rn := strings.ToLower(lhs.StatName()), strings.ToLower(rhs.StatName()) if ln != rn { return ln < rn } // Names differ only by capitalization, if that. return lhs.StatName() < rhs.StatName() }