Compare commits
No commits in common. "99e372a4db5608ae7a5b867ae40e50b2ee862e46" and "14640703395fc80b457cba4f4cef30046ba55fda" have entirely different histories.
99e372a4db
...
1464070339
242
cardsim/stats.go
242
cardsim/stats.go
@ -4,11 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// A StatsCollection contains stats.
|
// A StatsCollection contains stats.
|
||||||
@ -118,26 +114,9 @@ func (s statFunc[T]) Visible() bool {
|
|||||||
return s.visible
|
return s.visible
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractStats pulls all exported stats out of a struct. It puts methods before
|
// ExtractStats pulls all exported Stat fields (not functions) out of a struct.
|
||||||
// fields. If the calculated name of a method conflicts with the calculated
|
// If x cannot be resolved to a struct, it panics. It unwraps interfaces and
|
||||||
// name of a stat from a field, the method wins.
|
// follows pointers to try to find a struct.
|
||||||
//
|
|
||||||
// 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 {
|
func ExtractStats(x any) []Stat {
|
||||||
v := reflect.ValueOf(x)
|
v := reflect.ValueOf(x)
|
||||||
for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() {
|
for k := v.Kind(); k == reflect.Pointer || k == reflect.Interface; k = v.Kind() {
|
||||||
@ -146,122 +125,19 @@ func ExtractStats(x any) []Stat {
|
|||||||
if v.Kind() != reflect.Struct {
|
if v.Kind() != reflect.Struct {
|
||||||
panic(fmt.Errorf("%T is not a struct", x))
|
panic(fmt.Errorf("%T is not a struct", x))
|
||||||
}
|
}
|
||||||
typ := v.Type()
|
|
||||||
|
|
||||||
var ret []Stat
|
var ret []Stat
|
||||||
|
lim := v.NumField()
|
||||||
known := make(map[string]bool)
|
for i := 0; i < lim; i++ {
|
||||||
for _, vv := range []reflect.Value{v, v.Addr()} {
|
f := v.Field(i)
|
||||||
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() {
|
if !f.CanInterface() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
iface := f.Interface()
|
x := f.Interface()
|
||||||
if s, ok := iface.(Stat); ok {
|
if s, ok := x.(Stat); ok {
|
||||||
if known[s.StatName()] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
known[s.StatName()] = true
|
|
||||||
ret = append(ret, s)
|
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
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,105 +173,3 @@ func (s statSorter) Less(i, j int) bool {
|
|||||||
// Names differ only by capitalization, if that.
|
// Names differ only by capitalization, if that.
|
||||||
return lhs.StatName() < rhs.StatName()
|
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,5 +7,4 @@ require github.com/kr/pretty v0.3.1
|
|||||||
require (
|
require (
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.9.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,5 +6,3 @@ 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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
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,11 +11,6 @@ type SmokeTestCollection struct {
|
|||||||
Turns cardsim.Invisible[int]
|
Turns cardsim.Invisible[int]
|
||||||
|
|
||||||
Flavor cardsim.Stored[string]
|
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 {
|
func (c *SmokeTestCollection) Average() float64 {
|
||||||
@ -28,7 +23,3 @@ func (c *SmokeTestCollection) Stats() []cardsim.Stat {
|
|||||||
cardsim.SortStats(stats)
|
cardsim.SortStats(stats)
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SmokeTestCollection) StatTotalThings() float64 {
|
|
||||||
return float64(c.Things+c.MoreThings) + c.FloatyThings
|
|
||||||
}
|
|
||||||
|
@ -28,10 +28,6 @@ func main() {
|
|||||||
Name: "Flavor",
|
Name: "Flavor",
|
||||||
Value: "Lemon",
|
Value: "Lemon",
|
||||||
},
|
},
|
||||||
Things: 5,
|
|
||||||
MoreThings: 9,
|
|
||||||
FloatyThings: 123.456,
|
|
||||||
Label: "whee",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
p.Name = "Dave"
|
p.Name = "Dave"
|
||||||
|
Loading…
Reference in New Issue
Block a user