feat: deduplicate ship name on transfer
This commit is contained in:
@@ -11,6 +11,11 @@ TODO: Препроцессинг и сохранение приказов
|
|||||||
составленный приказ, но при этом необходимо повторить те команды, которые
|
составленный приказ, но при этом необходимо повторить те команды, которые
|
||||||
были отданы верно. К счастью, программа-клиент помогает игроку не запутаться
|
были отданы верно. К счастью, программа-клиент помогает игроку не запутаться
|
||||||
в этом процессе и берёт на себя контроль за целостностью приказов.
|
в этом процессе и берёт на себя контроль за целостностью приказов.
|
||||||
|
|
||||||
|
!!! Убедиться, что раса не покинула игру.
|
||||||
|
|
||||||
|
При производстве хода раса может быть исключена по TTL=0.
|
||||||
|
В этом случае нужно игнорировать некоторые приказы, например, передачу ей кораблей.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
e "github.com/iliadenisov/galaxy/internal/error"
|
e "github.com/iliadenisov/galaxy/internal/error"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
"github.com/iliadenisov/galaxy/internal/number"
|
"github.com/iliadenisov/galaxy/internal/number"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ShipGroup is a proxy func, nothing to cache
|
// ShipGroup is a proxy func, nothing to cache
|
||||||
@@ -329,20 +330,6 @@ func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
|
|||||||
p.UnpackCapital()
|
p.UnpackCapital()
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
TODO: Позволить передавать одноимённые группы.
|
|
||||||
|
|
||||||
При генерировании нового имени необходимо убедиться, что оно не превысит 30 символов.
|
|
||||||
|
|
||||||
> Если у расы, которой передается группа кораблей, уже определен класс кораблей с таким же
|
|
||||||
> названием, но другими характеристиками, принимающая раса так же получит новый
|
|
||||||
> класс кораблей, к названию которого будет добавлен случайный суффикс.
|
|
||||||
|
|
||||||
TODO: Убедиться, что раса не покинула игру.
|
|
||||||
|
|
||||||
При производстве хода раса может покинуть, а может и не покинуть игру,
|
|
||||||
в зхависимости от того, были ли ею отданы новые приказы.
|
|
||||||
*/
|
|
||||||
func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) {
|
func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) {
|
||||||
c.validateRaceIndex(ri)
|
c.validateRaceIndex(ri)
|
||||||
if ri == riAccept {
|
if ri == riAccept {
|
||||||
@@ -361,13 +348,14 @@ func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err erro
|
|||||||
st := c.ShipGroupShipClass(sgi)
|
st := c.ShipGroupShipClass(sgi)
|
||||||
|
|
||||||
var stAcc int
|
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 &&
|
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]) {
|
!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,
|
err = c.ShipClassCreate(riAccept,
|
||||||
st.Name,
|
name,
|
||||||
st.Drive.F(),
|
st.Drive.F(),
|
||||||
int(st.Armament),
|
int(st.Armament),
|
||||||
st.Weapons.F(),
|
st.Weapons.F(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package controller_test
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -226,9 +227,6 @@ func TestShipGroupTransfer(t *testing.T) {
|
|||||||
assert.ErrorContains(t,
|
assert.ErrorContains(t,
|
||||||
g.ShipGroupTransfer(Race_0.Name, Race_1.Name, uuid.New()),
|
g.ShipGroupTransfer(Race_0.Name, Race_1.Name, uuid.New()),
|
||||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
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)
|
orig := *c.ShipGroup(2)
|
||||||
assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(2).ID)) // group #2 (3)
|
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,
|
assert.ErrorContains(t,
|
||||||
g.ShipGroupTransfer(Race_1.Name, Race_0.Name, sg.ID),
|
g.ShipGroupTransfer(Race_1.Name, Race_0.Name, sg.ID),
|
||||||
e.GenericErrorText(e.ErrShipsBusy))
|
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) {
|
func TestShipGroupLoad(t *testing.T) {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const (
|
|||||||
ErrEntityInUse = 5006
|
ErrEntityInUse = 5006
|
||||||
ErrShipsBusy = 5007
|
ErrShipsBusy = 5007
|
||||||
ErrShipsNotOnSamePlanet = 5008
|
ErrShipsNotOnSamePlanet = 5008
|
||||||
ErrGiveawayGroupShipsTypeNotEqual = 5009
|
|
||||||
ErrUpgradeGroupNumberNotEnough = 5010
|
ErrUpgradeGroupNumberNotEnough = 5010
|
||||||
ErrUpgradeInsufficientResources = 5011
|
ErrUpgradeInsufficientResources = 5011
|
||||||
ErrSendShipHasNoDrives = 5012
|
ErrSendShipHasNoDrives = 5012
|
||||||
@@ -144,8 +143,6 @@ func GenericErrorText(code int) string {
|
|||||||
return "Ship(s) are'n free to use"
|
return "Ship(s) are'n free to use"
|
||||||
case ErrShipsNotOnSamePlanet:
|
case ErrShipsNotOnSamePlanet:
|
||||||
return "Ships not on the same planet"
|
return "Ships not on the same planet"
|
||||||
case ErrGiveawayGroupShipsTypeNotEqual:
|
|
||||||
return "Ship type already defined with different specifications"
|
|
||||||
case ErrInputTechUnknown:
|
case ErrInputTechUnknown:
|
||||||
return "Technology name unknown"
|
return "Technology name unknown"
|
||||||
case ErrInputTechInvalidMixing:
|
case ErrInputTechInvalidMixing:
|
||||||
|
|||||||
@@ -124,10 +124,6 @@ func NewShipsNotOnSamePlanetError(arg ...any) error {
|
|||||||
return newGenericError(ErrShipsNotOnSamePlanet, arg...)
|
return newGenericError(ErrShipsNotOnSamePlanet, arg...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGiveawayGroupShipsTypeNotEqualError(arg ...any) error {
|
|
||||||
return newGenericError(ErrGiveawayGroupShipsTypeNotEqual, arg...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTechUnknownError(arg ...any) error {
|
func NewTechUnknownError(arg ...any) error {
|
||||||
return newGenericError(ErrInputTechUnknown, arg...)
|
return newGenericError(ErrInputTechUnknown, arg...)
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -1,12 +1,16 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Allowed special characters
|
const (
|
||||||
const specialChars = "!@#$%^*-_=+~()[]{}"
|
maxNameLength = 30
|
||||||
|
specialChars = "!@#$%^*-_=+~()[]{}" // Allowed special characters
|
||||||
|
)
|
||||||
|
|
||||||
var allowedSpecialChars map[rune]bool
|
var allowedSpecialChars map[rune]bool
|
||||||
|
|
||||||
@@ -28,7 +32,7 @@ func ValidateTypeName(input string) (string, bool) {
|
|||||||
|
|
||||||
runes := []rune(trimmed)
|
runes := []rune(trimmed)
|
||||||
|
|
||||||
if len(runes) > 30 {
|
if len(runes) > maxNameLength {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,3 +79,21 @@ func ValidateTypeName(input string) (string, bool) {
|
|||||||
// Return the trimmed string and true if all conditions are met
|
// Return the trimmed string and true if all conditions are met
|
||||||
return trimmed, true
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util_test
|
package util_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"unicode/utf8"
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user