Compare commits

...

2 Commits

Author SHA1 Message Date
99e372a4db
Fix it
Pointer vs. value receivers are... interesting.
2023-04-04 11:37:02 -07:00
3e34e25f54
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).
2023-04-04 11:12:07 -07:00
5 changed files with 250 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,26 @@ 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 methods before
// fields. If the calculated name of a method conflicts with the calculated
// name of a stat from a field, the method wins.
//
// 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,19 +146,122 @@ 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)
known := make(map[string]bool)
for _, vv := range []reflect.Value{v, v.Addr()} {
xt := vv.Type()
lim := xt.NumMethod()
for i := 0; i < lim; i++ {
m := xt.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
}
if known[n] {
continue
}
known[n] = true
val := vv.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,
})
}
}
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 {
iface := f.Interface()
if s, ok := iface.(Stat); ok {
if known[s.StatName()] {
continue
}
known[s.StatName()] = true
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, iface)
} else if isStat {
val = fmt.Sprint(iface)
} else {
continue // not identifiably a stat
}
nm := strings.Join(explode(sf.Name), " ")
if known[nm] {
continue
}
known[nm] = true
ret = append(ret, &StatLiteral{
Name: nm,
Value: val,
IsVisible: !isHidden,
})
continue
}
// Else, not a stat.
}
return ret
}
@ -173,3 +297,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,11 @@ type SmokeTestCollection struct {
Turns cardsim.Invisible[int]
Flavor cardsim.Stored[string]
Things int `cardsim:"stat"`
MoreThings int `cardsim:"hidden"`
FloatyThings float64 `cardsim:"round1"`
Label string `cardsim:"stat"`
}
func (c *SmokeTestCollection) Average() float64 {
@ -23,3 +28,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
}

View File

@ -28,6 +28,10 @@ func main() {
Name: "Flavor",
Value: "Lemon",
},
Things: 5,
MoreThings: 9,
FloatyThings: 123.456,
Label: "whee",
},
)
p.Name = "Dave"