From f148d1a23071d3c4b799f8230b8e9861a692a1d9 Mon Sep 17 00:00:00 2001 From: Martin Bruse Date: Sat, 16 May 2020 01:14:24 +0200 Subject: [PATCH] Implemented a way for the adjudicator to notify the client of limitations in the options structure. Used this to let Build and Disband orders notify the client about how many of them are allowed, so that the client an stop presenting options that aren't actually valid. --- go.mod | 1 + go.sum | 2 + godip.go | 60 ++++++++- godip_test.go | 195 +++++++++++++++++++++++++++ orders/build.go | 20 ++- orders/disband.go | 5 +- variants/classical/classical_test.go | 38 +++++- variants/testing/testing.go | 44 +++--- 8 files changed, 336 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 0f2c2261..ba5db904 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/zond/godip go 1.14 require ( + github.com/davecgh/go-spew v1.1.1 github.com/gorilla/mux v1.7.4 google.golang.org/appengine v1.6.5 ) diff --git a/go.sum b/go.sum index 6b9f0ae4..c640f6d3 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= diff --git a/godip.go b/godip.go index 4d182b6b..375742ed 100644 --- a/godip.go +++ b/godip.go @@ -322,18 +322,72 @@ type SrcProvince Province type OptionValue interface{} +type FilteredOptionValue struct { + Filter string + Value OptionValue +} + /* Options defines a tree of valid orders for a given situation */ type Options map[OptionValue]Options +func (self Options) BubbleFilters() Options { + _, result := self.bubbleFiltersHelper(false) + return result +} + +func (self Options) bubbleFiltersHelper(bubbleSelf bool) (string, Options) { + lastFilter := "" + filters := map[string]bool{} + bubbledChildren := Options{} + for k, v := range self { + if filtered, ok := k.(FilteredOptionValue); ok { + lastFilter = filtered.Filter + filters[lastFilter] = true + bubbledChildren[k] = v + } else { + childCommonFilter, newChild := v.bubbleFiltersHelper(true) + lastFilter = childCommonFilter + filters[lastFilter] = true + if childCommonFilter == "" { + bubbledChildren[k] = v + } else { + bubbledChildren[FilteredOptionValue{ + Filter: childCommonFilter, + Value: k, + }] = newChild + } + } + } + if !bubbleSelf || lastFilter == "" || len(filters) > 1 { + return "", bubbledChildren + } + bubbledSelf := Options{} + for k, v := range bubbledChildren { + filtered := k.(FilteredOptionValue) + bubbledSelf[filtered.Value] = v + } + return lastFilter, bubbledSelf +} + func (self Options) MarshalJSON() ([]byte, error) { repl := map[string]interface{}{} - for k, v := range self { - repl[fmt.Sprint(k)] = map[string]interface{}{ - "Type": reflect.ValueOf(k).Type().Name(), + for k, v := range self.BubbleFilters() { + kVal := reflect.ValueOf(k) + filter := "" + if kVal.Type() == reflect.TypeOf(FilteredOptionValue{}) { + filter = kVal.FieldByName("Filter").String() + kVal = kVal.FieldByName("Value") + } + val := map[string]interface{}{ + "Type": kVal.Type().Name(), "Next": v, } + if filter != "" { + val["Filter"] = filter + } + repl[fmt.Sprint(kVal.Interface())] = val } return json.Marshal(repl) } diff --git a/godip_test.go b/godip_test.go index 6d85b663..807825e5 100644 --- a/godip_test.go +++ b/godip_test.go @@ -1,10 +1,205 @@ package godip import ( + "reflect" "sort" "testing" ) +func TestOptionsBubbleFilters(t *testing.T) { + for _, tc := range []struct { + opts Options + bubbled Options + }{ + { + opts: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + bubbled: Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: Province("spa"), + }: Options{ + OrderType("Build"): Options{ + UnitType("Army"): Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + }, + { + opts: Options{ + Province("lon"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("lon"): Options{}, + }, + }, + }, + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + bubbled: Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: Province("spa"), + }: Options{ + OrderType("Build"): Options{ + UnitType("Army"): Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: Province("lon"), + }: Options{ + OrderType("Build"): Options{ + UnitType("Army"): Options{ + SrcProvince("lon"): Options{}, + }, + }, + }, + }, + }, + { + opts: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + OrderType("Disband"): Options{ + FilteredOptionValue{ + Filter: "MAX:Disband:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + bubbled: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + OrderType("Disband"): Options{ + FilteredOptionValue{ + Filter: "MAX:Disband:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + }, + { + opts: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Fleet"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + bubbled: Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: Province("spa"), + }: Options{ + OrderType("Build"): Options{ + UnitType("Army"): Options{ + SrcProvince("spa"): Options{}, + }, + UnitType("Fleet"): Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + }, + { + opts: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + UnitType("Fleet"): Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + bubbled: Options{ + Province("spa"): Options{ + OrderType("Build"): Options{ + FilteredOptionValue{ + Filter: "MAX:Build:1", + Value: UnitType("Army"), + }: Options{ + SrcProvince("spa"): Options{}, + }, + UnitType("Fleet"): Options{ + SrcProvince("spa"): Options{}, + }, + }, + }, + }, + }, + } { + bubbled := tc.opts.BubbleFilters() + if !reflect.DeepEqual(bubbled, tc.bubbled) { + t.Errorf("Got %+v, wanted %+v", bubbled, tc.bubbled) + } + } +} + func TestNationSorting(t *testing.T) { nations := Nations{ Austria, diff --git a/orders/build.go b/orders/build.go index c21c1719..3f385dda 100644 --- a/orders/build.go +++ b/orders/build.go @@ -148,26 +148,34 @@ func (self *build) Options(v godip.Validator, nation godip.Nation, src godip.Pro if _, _, ok = v.Unit(src); ok { return } + var wrapperFunc func(godip.OptionValue) godip.FilteredOptionValue if _, _, balance := AdjustmentStatus(v, me); balance < 1 { return + } else { + wrapperFunc = func(val godip.OptionValue) godip.FilteredOptionValue { + return godip.FilteredOptionValue{ + Filter: fmt.Sprintf("MAX:%v:%v", godip.Build, balance), + Value: val, + } + } } if v.Graph().Flags(src)[godip.Land] || v.Graph().Flags(src.Super())[godip.Land] { if result == nil { result = godip.Options{} } - if result[godip.Army] == nil { - result[godip.Army] = godip.Options{} + if result[wrapperFunc(godip.Army)] == nil { + result[wrapperFunc(godip.Army)] = godip.Options{} } - result[godip.Army][godip.SrcProvince(src.Super())] = nil + result[wrapperFunc(godip.Army)][godip.SrcProvince(src.Super())] = nil } if v.Graph().Flags(src)[godip.Sea] || v.Graph().Flags(src.Super())[godip.Sea] { if result == nil { result = godip.Options{} } - if result[godip.Fleet] == nil { - result[godip.Fleet] = godip.Options{} + if result[wrapperFunc(godip.Fleet)] == nil { + result[wrapperFunc(godip.Fleet)] = godip.Options{} } - result[godip.Fleet][godip.SrcProvince(src)] = nil + result[wrapperFunc(godip.Fleet)][godip.SrcProvince(src)] = nil } return } diff --git a/orders/disband.go b/orders/disband.go index 27cc3cdc..9d1fdb89 100644 --- a/orders/disband.go +++ b/orders/disband.go @@ -117,7 +117,10 @@ func (self *disband) Options(v godip.Validator, nation godip.Nation, src godip.P if unit.Nation == nation { if _, _, balance := AdjustmentStatus(v, unit.Nation); balance < 0 { result = godip.Options{ - godip.SrcProvince(actualSrc): nil, + godip.FilteredOptionValue{ + Filter: fmt.Sprintf("MAX:%v:%v", godip.Disband, -balance), + Value: godip.SrcProvince(actualSrc), + }: nil, } } } diff --git a/variants/classical/classical_test.go b/variants/classical/classical_test.go index 00568896..c647c132 100644 --- a/variants/classical/classical_test.go +++ b/variants/classical/classical_test.go @@ -400,15 +400,45 @@ func TestSTPBuildOptions(t *testing.T) { judge.Next() opts := judge.Phase().Options(judge, godip.Russia) tst.AssertNoOpt(t, opts, []string{"stp"}) - tst.AssertOpt(t, opts, []string{"stp/nc", "Build", "Fleet", "stp/nc"}) - tst.AssertOpt(t, opts, []string{"stp/sc", "Build", "Fleet", "stp/sc"}) + filter := "MAX:Build:1" + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/nc", "Build", "Fleet", "stp/nc"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/sc", "Build", "Fleet", "stp/sc"}) tst.AssertNoOpt(t, opts, []string{"stp/sc", "Build", "Fleet", "stp"}) - tst.AssertOpt(t, opts, []string{"stp/nc", "Build", "Army", "stp"}) - tst.AssertOpt(t, opts, []string{"stp/sc", "Build", "Army", "stp"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/nc", "Build", "Army", "stp"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/sc", "Build", "Army", "stp"}) tst.AssertNoOpt(t, opts, []string{"stp/sc", "Build", "Army", "stp/nc"}) tst.AssertNoOpt(t, opts, []string{"stp/sc", "Build", "Army", "stp/sc"}) } +func TestFilteredOptions(t *testing.T) { + judge := startState(t) + judge.SetOrder("stp", orders.Move("stp/sc", "fin")) + judge.SetOrder("sev", orders.Move("sev", "rum")) + judge.SetOrder("war", orders.Move("war", "sil")) + judge.SetOrder("vie", orders.Move("vie", "boh")) + judge.Next() + judge.Next() + judge.SetOrder("fin", orders.Move("fin", "swe")) + judge.SetOrder("boh", orders.Move("boh", "mun")) + judge.SetOrder("sil", orders.SupportMove("sil", "boh", "mun")) + judge.Next() + judge.SetOrder("mun", orders.Move("mun", "ruh")) + judge.Next() + opts := judge.Phase().Options(judge, godip.Russia) + filter := "MAX:Build:2" + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/nc", "Build", "Fleet", "stp/nc"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/sc", "Build", "Fleet", "stp/sc"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/nc", "Build", "Army", "stp"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"stp/sc", "Build", "Army", "stp"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"sev", "Build", "Fleet", "sev"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"sev", "Build", "Army", "sev"}) + opts = judge.Phase().Options(judge, godip.Germany) + filter = "MAX:Disband:1" + tst.AssertFilteredOpt(t, opts, filter, []string{"kie", "Disband", "kie"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"ber", "Disband", "ber"}) + tst.AssertFilteredOpt(t, opts, filter, []string{"ruh", "Disband", "ruh"}) +} + func TestSupportSTPOpts(t *testing.T) { judge := startState(t) opts := judge.Phase().Options(judge, godip.Russia) diff --git a/variants/testing/testing.go b/variants/testing/testing.go index b57ebcbf..22c754c0 100644 --- a/variants/testing/testing.go +++ b/variants/testing/testing.go @@ -7,11 +7,20 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" "github.com/zond/godip" "github.com/zond/godip/orders" "github.com/zond/godip/state" ) +func PP(i interface{}) string { + b, err := json.MarshalIndent(i, " ", " ") + if err != nil { + return spew.Sdump(i) + } + return string(b) +} + func AssertOrderValidity(t *testing.T, validator godip.Validator, order godip.Order, nat godip.Nation, err error) { if gotNat, e := order.Validate(validator); e != err { t.Errorf("%v should validate to %v, but got %v", order, err, e) @@ -77,25 +86,26 @@ func AssertOptionToMove(t *testing.T, j *state.State, nat godip.Nation, src godi } } -func hasOptHelper(opts map[string]interface{}, order []string, originalOpts map[string]interface{}, originalOrder []string) error { +func hasOptHelper(opts map[string]interface{}, filter string, order []string, originalOpts map[string]interface{}, originalOrder []string) error { if len(order) == 0 { return nil } - if _, found := opts[order[0]]; !found { - b, err := json.MarshalIndent(originalOpts, " ", " ") - if err != nil { - return err - } - b2, err := json.MarshalIndent(opts, " ", " ") - if err != nil { - return err - } - return fmt.Errorf("Got no option for %+v in %s, failed at %+v in %s, wanted it!", originalOrder, b, order, b2) + foundInter, found := opts[order[0]] + if !found { + return fmt.Errorf("Got no option for %+v in %v, failed at %+v in %v, wanted it!", originalOrder, PP(originalOpts), order, PP(opts)) } - return hasOptHelper(opts[order[0]].(map[string]interface{})["Next"].(map[string]interface{}), order[1:], originalOpts, originalOrder) + foundMap := foundInter.(map[string]interface{}) + if filter != "" && foundMap["Filter"] != filter { + return fmt.Errorf("Found %+v in %v, but didn't get the filter we wanted (%q)", originalOrder, PP(originalOpts), filter) + } + return hasOptHelper(foundMap["Next"].(map[string]interface{}), "", order[1:], originalOpts, originalOrder) } func hasOpt(opts godip.Options, order []string) error { + return hasFilteredOpt(opts, "", order) +} + +func hasFilteredOpt(opts godip.Options, filter string, order []string) error { b, err := json.MarshalIndent(opts, " ", " ") if err != nil { return err @@ -104,12 +114,16 @@ func hasOpt(opts godip.Options, order []string) error { if err := json.Unmarshal(b, &converted); err != nil { return err } - return hasOptHelper(converted, order, converted, order) + return hasOptHelper(converted, filter, order, converted, order) } func AssertOpt(t *testing.T, opts godip.Options, order []string) { - t.Run(strings.Join(order, "_"), func(t *testing.T) { - err := hasOpt(opts, order) + AssertFilteredOpt(t, opts, "", order) +} + +func AssertFilteredOpt(t *testing.T, opts godip.Options, filter string, order []string) { + t.Run(strings.Join(order, "_")+"/"+filter, func(t *testing.T) { + err := hasFilteredOpt(opts, filter, order) if err != nil { t.Error(err) }