Major stats upgrade.
StatLiteral: just emit a stat in the obvious way. Plus helper functions. Can also identify stats via struct tags, no more Stored type! Can also identify stat methods via name (with compatible types).
This commit is contained in:
parent
1464070339
commit
3e34e25f54
221
cardsim/stats.go
221
cardsim/stats.go
@ -4,7 +4,11 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
// A StatsCollection contains stats.
|
||||
@ -114,9 +118,25 @@ 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.
|
||||
// ExtractStats pulls all exported stats out of a struct. It puts fields before
|
||||
// method.
|
||||
//
|
||||
// A field is a stat if it is of some Stat type or is tagged with `cardsim:"stat"`,
|
||||
// `cardsim:"hidden"` (invisible stat), `cardsim:"round2"` (or any integer, 2 is
|
||||
// just an example), or `cardsim:"hiddenround3"`. `hiddenstat`, `statround`, and
|
||||
// `hiddenstatround` are also accepted, but other orders of these directives
|
||||
// are not. A "round" stat must be a float type and it will be rounded to
|
||||
// this number of decimal places.
|
||||
//
|
||||
// A method is a Stat if it takes 0 arguments, returns exactly 1 value, and
|
||||
// starts with Stat or HiddenStat.
|
||||
//
|
||||
// The name of these inferred stats is calculated by breaking the name into
|
||||
// separate words before each capital letter, unless there are consecutive
|
||||
// capital letters, which it interprets as an initialism (followed by the
|
||||
// start of another word, if it's not at the end). To insert a space between
|
||||
// consecutive capital letters, insert an underscore (`_`). This name inference
|
||||
// trims "Stat" and "HiddenStat" off the front of method names.
|
||||
func ExtractStats(x any) []Stat {
|
||||
v := reflect.ValueOf(x)
|
||||
for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() {
|
||||
@ -125,18 +145,101 @@ func ExtractStats(x any) []Stat {
|
||||
if v.Kind() != reflect.Struct {
|
||||
panic(fmt.Errorf("%T is not a struct", x))
|
||||
}
|
||||
typ := v.Type()
|
||||
|
||||
var ret []Stat
|
||||
lim := v.NumField()
|
||||
for i := 0; i < lim; i++ {
|
||||
f := v.Field(i)
|
||||
fields := reflect.VisibleFields(typ)
|
||||
for _, sf := range fields {
|
||||
if !sf.IsExported() {
|
||||
continue
|
||||
}
|
||||
f := v.FieldByIndex(sf.Index)
|
||||
if !f.CanInterface() {
|
||||
continue
|
||||
}
|
||||
x := f.Interface()
|
||||
if s, ok := x.(Stat); ok {
|
||||
if s, ok := f.Interface().(Stat); ok {
|
||||
ret = append(ret, s)
|
||||
continue
|
||||
}
|
||||
if t := sf.Tag.Get("cardsim"); t != "" {
|
||||
isStat := false
|
||||
isHidden := false
|
||||
t = strings.ToLower(t)
|
||||
t = strings.TrimSpace(t)
|
||||
if strings.HasPrefix(t, "hidden") {
|
||||
isStat = true
|
||||
isHidden = true
|
||||
t = t[6:]
|
||||
}
|
||||
if strings.HasPrefix(t, "stat") {
|
||||
isStat = true
|
||||
t = t[4:]
|
||||
}
|
||||
var val string
|
||||
if strings.HasPrefix(t, "round") {
|
||||
isStat = true
|
||||
t = t[5:]
|
||||
n, _ := strconv.Atoi(t)
|
||||
fs := fmt.Sprintf("%%.%df", n)
|
||||
val = fmt.Sprintf(fs, f.Interface())
|
||||
} else if isStat {
|
||||
val = fmt.Sprint(f.Interface())
|
||||
} else {
|
||||
continue // not identifiably a stat
|
||||
}
|
||||
ret = append(ret, &StatLiteral{
|
||||
Name: strings.Join(explode(sf.Name), " "),
|
||||
Value: val,
|
||||
IsVisible: !isHidden,
|
||||
})
|
||||
continue
|
||||
}
|
||||
// Else, not a stat.
|
||||
}
|
||||
|
||||
lim := typ.NumMethod()
|
||||
for i := 0; i < lim; i++ {
|
||||
m := typ.Method(i)
|
||||
if !m.IsExported() {
|
||||
continue
|
||||
}
|
||||
tm := m.Type
|
||||
if tm.NumIn() != 1 {
|
||||
// 1 arg -- receiver
|
||||
continue
|
||||
}
|
||||
if tm.NumOut() != 1 {
|
||||
continue
|
||||
}
|
||||
nameParts := explode(m.Name)
|
||||
if len(nameParts) < 2 {
|
||||
continue
|
||||
}
|
||||
isHidden := false
|
||||
if nameParts[0] == "Hidden" {
|
||||
isHidden = true
|
||||
nameParts = nameParts[1:]
|
||||
}
|
||||
if nameParts[0] != "Stat" {
|
||||
continue
|
||||
}
|
||||
n := strings.Join(nameParts[1:], ": ")
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
val := v.Method(i).Call([]reflect.Value{})
|
||||
if len(val) != 1 {
|
||||
// This shouldn't happen - we already checked Out. Weird.
|
||||
continue
|
||||
}
|
||||
if !val[0].CanInterface() {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, &StatLiteral{
|
||||
Name: n,
|
||||
Value: fmt.Sprint(val[0].Interface()),
|
||||
IsVisible: !isHidden,
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@ -173,3 +276,105 @@ func (s statSorter) Less(i, j int) bool {
|
||||
// Names differ only by capitalization, if that.
|
||||
return lhs.StatName() < rhs.StatName()
|
||||
}
|
||||
|
||||
// StatLiteral stores a ready-to-emit stat value.
|
||||
type StatLiteral struct {
|
||||
Name string
|
||||
Value string
|
||||
IsVisible bool
|
||||
}
|
||||
|
||||
func (s *StatLiteral) StatName() string {
|
||||
return s.Name
|
||||
}
|
||||
|
||||
func (s *StatLiteral) String() string {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
func (s *StatLiteral) Visible() bool {
|
||||
return s.IsVisible
|
||||
}
|
||||
|
||||
func EmitStat(name string, v any) *StatLiteral {
|
||||
return &StatLiteral{
|
||||
Name: name,
|
||||
Value: fmt.Sprint(v),
|
||||
IsVisible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func EmitHiddenStat(name string, v any) *StatLiteral {
|
||||
return &StatLiteral{
|
||||
Name: name,
|
||||
Value: fmt.Sprint(v),
|
||||
IsVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
func Statf(name string, f string, args ...any) *StatLiteral {
|
||||
return &StatLiteral{
|
||||
Name: name,
|
||||
Value: fmt.Sprintf(f, args...),
|
||||
IsVisible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func HiddentStatf(name string, f string, args ...any) *StatLiteral {
|
||||
return &StatLiteral{
|
||||
Name: name,
|
||||
Value: fmt.Sprintf(f, args...),
|
||||
IsVisible: false,
|
||||
}
|
||||
}
|
||||
|
||||
func RoundStat[N constraints.Float](name string, val N, decimals int) *StatLiteral {
|
||||
f := fmt.Sprintf("%%.%df", decimals)
|
||||
return &StatLiteral{
|
||||
Name: name,
|
||||
Value: fmt.Sprintf(f, val),
|
||||
IsVisible: true,
|
||||
}
|
||||
}
|
||||
|
||||
func RoundHiddenStat[N constraints.Float](name string, val N, decimals int) *StatLiteral {
|
||||
r := RoundStat(name, val, decimals)
|
||||
r.IsVisible = false
|
||||
return r
|
||||
}
|
||||
|
||||
// explode turns CamelCase into multiple strings. It recognizes initialisms. To
|
||||
// split consecutive capital letters into separate words instead of recognizing
|
||||
// them as an initialism, insert underscores.
|
||||
func explode(s string) []string {
|
||||
var parts []string
|
||||
started := 0
|
||||
initialism := false
|
||||
for i, r := range s {
|
||||
if unicode.IsUpper(r) {
|
||||
if initialism || (started == i) {
|
||||
continue
|
||||
}
|
||||
if started == i-1 {
|
||||
initialism = true
|
||||
continue
|
||||
}
|
||||
parts = append(parts, s[started:i])
|
||||
started = i
|
||||
continue
|
||||
}
|
||||
if r == '_' {
|
||||
parts = append(parts, s[started:i])
|
||||
initialism = false
|
||||
started = i + 1
|
||||
continue
|
||||
}
|
||||
if initialism {
|
||||
parts = append(parts, s[started:i-1])
|
||||
initialism = false
|
||||
started = i - 1
|
||||
}
|
||||
}
|
||||
parts = append(parts, s[started:])
|
||||
return parts
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -7,4 +7,5 @@ require github.com/kr/pretty v0.3.1
|
||||
require (
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||
)
|
||||
|
2
go.sum
2
go.sum
@ -6,3 +6,5 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
|
@ -11,6 +11,10 @@ type SmokeTestCollection struct {
|
||||
Turns cardsim.Invisible[int]
|
||||
|
||||
Flavor cardsim.Stored[string]
|
||||
|
||||
Things int `cardsim:"stat"`
|
||||
MoreThings int `cardsim:"hidden"`
|
||||
FloatyThings float64 `cardsim:"round1"`
|
||||
}
|
||||
|
||||
func (c *SmokeTestCollection) Average() float64 {
|
||||
@ -23,3 +27,7 @@ func (c *SmokeTestCollection) Stats() []cardsim.Stat {
|
||||
cardsim.SortStats(stats)
|
||||
return stats
|
||||
}
|
||||
|
||||
func (c *SmokeTestCollection) StatTotalThings() float64 {
|
||||
return float64(c.Things+c.MoreThings) + c.FloatyThings
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user