diff --git a/internal/controller/command.go b/internal/controller/command.go index 302ccd6..e707880 100644 --- a/internal/controller/command.go +++ b/internal/controller/command.go @@ -11,6 +11,11 @@ TODO: Препроцессинг и сохранение приказов составленный приказ, но при этом необходимо повторить те команды, которые были отданы верно. К счастью, программа-клиент помогает игроку не запутаться в этом процессе и берёт на себя контроль за целостностью приказов. + +!!! Убедиться, что раса не покинула игру. + +При производстве хода раса может быть исключена по TTL=0. +В этом случае нужно игнорировать некоторые приказы, например, передачу ей кораблей. */ import ( diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index 646bb12..a685b1d 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -11,6 +11,7 @@ import ( e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/number" + "github.com/iliadenisov/galaxy/internal/util" ) // ShipGroup is a proxy func, nothing to cache @@ -329,20 +330,6 @@ func (c *Cache) unsafeUnloadCargo(sgi int, q float64) { p.UnpackCapital() } -/* -TODO: Позволить передавать одноимённые группы. - - При генерировании нового имени необходимо убедиться, что оно не превысит 30 символов. - - > Если у расы, которой передается группа кораблей, уже определен класс кораблей с таким же - > названием, но другими характеристиками, принимающая раса так же получит новый - > класс кораблей, к названию которого будет добавлен случайный суффикс. - -TODO: Убедиться, что раса не покинула игру. - - При производстве хода раса может покинуть, а может и не покинуть игру, - в зхависимости от того, были ли ею отданы новые приказы. -*/ func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) { c.validateRaceIndex(ri) if ri == riAccept { @@ -361,13 +348,14 @@ func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err erro st := c.ShipGroupShipClass(sgi) var stAcc int + var name = st.Name if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 && !st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) { - return e.NewGiveawayGroupShipsTypeNotEqualError("race %q, ship type %q", c.g.Race[riAccept].Name, c.g.Race[riAccept].ShipTypes[stAcc].Name) + name = util.AppendRandomSuffix(name) } - if stAcc < 0 { + if stAcc < 0 || name != st.Name { err = c.ShipClassCreate(riAccept, - st.Name, + name, st.Drive.F(), int(st.Armament), st.Weapons.F(), diff --git a/internal/controller/ship_group_test.go b/internal/controller/ship_group_test.go index 775f4eb..4012de7 100644 --- a/internal/controller/ship_group_test.go +++ b/internal/controller/ship_group_test.go @@ -3,6 +3,7 @@ package controller_test import ( "fmt" "slices" + "strings" "testing" "github.com/google/uuid" @@ -226,9 +227,6 @@ func TestShipGroupTransfer(t *testing.T) { assert.ErrorContains(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, uuid.New()), e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(0).ID), - e.GenericErrorText(e.ErrGiveawayGroupShipsTypeNotEqual)) orig := *c.ShipGroup(2) assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(2).ID)) // group #2 (3) @@ -275,6 +273,18 @@ func TestShipGroupTransfer(t *testing.T) { assert.ErrorContains(t, g.ShipGroupTransfer(Race_1.Name, Race_0.Name, sg.ID), e.GenericErrorText(e.ErrShipsBusy)) + + // transfer ship class with existing name + originalName := c.MustShipClass(Race_0_idx, ShipType_Cruiser).Name + assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(0).ID)) + var s *game.ShipType + for st := range c.ListShipTypes(Race_1_idx) { + if strings.HasPrefix(st.Name, originalName) && st.Name != originalName { + s = st + } + } + assert.NotNil(t, s) + assert.Greater(t, len(s.Name), len(originalName)) } func TestShipGroupLoad(t *testing.T) { diff --git a/internal/error/generic.go b/internal/error/generic.go index 084cb41..bf4f515 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -22,7 +22,6 @@ const ( ErrEntityInUse = 5006 ErrShipsBusy = 5007 ErrShipsNotOnSamePlanet = 5008 - ErrGiveawayGroupShipsTypeNotEqual = 5009 ErrUpgradeGroupNumberNotEnough = 5010 ErrUpgradeInsufficientResources = 5011 ErrSendShipHasNoDrives = 5012 @@ -144,8 +143,6 @@ func GenericErrorText(code int) string { return "Ship(s) are'n free to use" case ErrShipsNotOnSamePlanet: return "Ships not on the same planet" - case ErrGiveawayGroupShipsTypeNotEqual: - return "Ship type already defined with different specifications" case ErrInputTechUnknown: return "Technology name unknown" case ErrInputTechInvalidMixing: diff --git a/internal/error/input.go b/internal/error/input.go index 862f0ca..ce083d5 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -124,10 +124,6 @@ func NewShipsNotOnSamePlanetError(arg ...any) error { return newGenericError(ErrShipsNotOnSamePlanet, arg...) } -func NewGiveawayGroupShipsTypeNotEqualError(arg ...any) error { - return newGenericError(ErrGiveawayGroupShipsTypeNotEqual, arg...) -} - func NewTechUnknownError(arg ...any) error { return newGenericError(ErrInputTechUnknown, arg...) } diff --git a/internal/util/string.go b/internal/util/string.go index 942f39f..07a920f 100644 --- a/internal/util/string.go +++ b/internal/util/string.go @@ -1,12 +1,16 @@ package util import ( + "fmt" + "math/rand" "strings" "unicode" ) -// Allowed special characters -const specialChars = "!@#$%^*-_=+~()[]{}" +const ( + maxNameLength = 30 + specialChars = "!@#$%^*-_=+~()[]{}" // Allowed special characters +) var allowedSpecialChars map[rune]bool @@ -28,7 +32,7 @@ func ValidateTypeName(input string) (string, bool) { runes := []rune(trimmed) - if len(runes) > 30 { + if len(runes) > maxNameLength { return "", false } @@ -75,3 +79,21 @@ func ValidateTypeName(input string) (string, bool) { // Return the trimmed string and true if all conditions are met return trimmed, true } + +func AppendRandomSuffix(v string) string { + return AppendRandomSuffixGenerator(v, RandomSuffixGenerator) +} + +func AppendRandomSuffixGenerator(v string, s func() string) string { + suffix := []rune(s()) + str := []rune(v) + max := maxNameLength - len(suffix) + if len(str) > max { + str = str[:max] + } + return string(append(str, suffix...)) +} + +func RandomSuffixGenerator() string { + return fmt.Sprintf("%04d", rand.Intn(9999)) +} diff --git a/internal/util/string_test.go b/internal/util/string_test.go index 9aeab4a..ec74eb6 100644 --- a/internal/util/string_test.go +++ b/internal/util/string_test.go @@ -1,6 +1,7 @@ package util_test import ( + "strings" "testing" "unicode/utf8" @@ -238,3 +239,54 @@ func FuzzValidateString(f *testing.F) { } }) } + +func TestAppendRandomSuffixGenerator(t *testing.T) { + tests := []struct { + name string + input string + suffix string + expected string + }{ + { + name: "Regular String", + input: "Regular_String", + suffix: "1234", + expected: "Regular_String1234", + }, + { + name: "Zero Length String", + input: "", + suffix: "1234", + expected: "1234", + }, + { + name: "Edge Case String len=28", + input: "Edge_Case_String_ABCDEFGHIGK", + suffix: "1234", + expected: "Edge_Case_String_ABCDEFGHI1234", + }, + { + name: "Extra Long String len=31", + input: "Extra_Long_String_ABCDEFGHIGKLM", + suffix: "1234", + expected: "Extra_Long_String_ABCDEFGH1234", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := util.AppendRandomSuffixGenerator(tt.input, func() string { return tt.suffix }) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestRandomSuffixGenerator(t *testing.T) { + var last string + for range 100 { + s := util.RandomSuffixGenerator() + assert.Len(t, s, 4) + assert.NotEqual(t, last, s) + assert.True(t, strings.ContainsFunc(s, func(r rune) bool { return r >= '0' && r <= '9' })) + last = s + } +}