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