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:
Kistaro Windrider 2023-04-04 11:12:07 -07:00
parent 1464070339
commit 3e34e25f54
Signed by: kistaro
SSH Key Fingerprint: SHA256:TBE2ynfmJqsAf0CP6gsflA0q5X5wD5fVKWPsZ7eVUg8
4 changed files with 224 additions and 8 deletions

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
}