feat: deduplicate ship name on transfer

This commit is contained in:
Ilia Denisov
2026-02-17 22:02:17 +02:00
parent f394c105b0
commit de91d575d0
7 changed files with 100 additions and 30 deletions
+5
View File
@@ -11,6 +11,11 @@ TODO: Препроцессинг и сохранение приказов
составленный приказ, но при этом необходимо повторить те команды, которые составленный приказ, но при этом необходимо повторить те команды, которые
были отданы верно. К счастью, программа-клиент помогает игроку не запутаться были отданы верно. К счастью, программа-клиент помогает игроку не запутаться
в этом процессе и берёт на себя контроль за целостностью приказов. в этом процессе и берёт на себя контроль за целостностью приказов.
!!! Убедиться, что раса не покинула игру.
При производстве хода раса может быть исключена по TTL=0.
В этом случае нужно игнорировать некоторые приказы, например, передачу ей кораблей.
*/ */
import ( import (
+5 -17
View File
@@ -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(),
+13 -3
View File
@@ -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) {
-3
View File
@@ -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:
-4
View File
@@ -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
View File
@@ -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))
}
+52
View File
@@ -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
}
}