diff --git a/internal/controller/controller_export_test.go b/internal/controller/controller_export_test.go index cce052f..829c80b 100644 --- a/internal/controller/controller_export_test.go +++ b/internal/controller/controller_export_test.go @@ -70,3 +70,7 @@ func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) { func (c *Cache) ListRouteEligibleGroupIds(pn uint) iter.Seq[int] { return c.listRouteEligibleGroupIds(pn) } + +func (c *Cache) ListMoveableGroupIds() iter.Seq[int] { + return c.listMoveableGroupIds() +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index dda64e2..0db83e2 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -94,9 +94,9 @@ func newGame() *game.Game { Width: 1000, Height: 1000, Planet: []game.Planet{ - controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 0, 0, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)), - controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), - controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), + controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 1, 1, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)), + controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), + controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), controller.NewPlanet(3, "Planet_3", uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)), }, }, diff --git a/internal/controller/fleet.go b/internal/controller/fleet.go index 16a3ab1..4d86171 100644 --- a/internal/controller/fleet.go +++ b/internal/controller/fleet.go @@ -250,6 +250,25 @@ func (c *Cache) FleetGroups(ri, fi int) iter.Seq[*game.ShipGroup] { } } +func (c *Cache) fleetGroupIds(ri, fi int) iter.Seq[int] { + c.validateRaceIndex(ri) + c.validateFleetIndex(fi) + return func(yield func(int) bool) { + for i := range c.ShipGroupsIndex() { + sg := c.ShipGroup(i) + if c.g.Race[ri].ID != sg.OwnerID { + continue + } + if sg.FleetID == nil || c.MustFleetIndex(*sg.FleetID) != fi { + continue + } + if !yield(i) { + return + } + } + } +} + func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] { c.validateRaceIndex(ri) return func(yield func(*game.Fleet) bool) { diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index cf84638..b190a8b 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -15,9 +15,18 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error { // 02. Враждующие корабли вступают в схватку. battles := ProduceBattles(c.Cache) - // 03. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство. + // 03. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство (но ещё не полетели) c.Cache.EnrouteGroups() + // 04. Корабли пролетают сквозь гиперпространство. + c.Cache.MoveShipGroups() + + // 05. Корабли, где это возможно, объединяются в группы. + c.Cache.CmdJoinEqualGroups() + + // 06. Враждующие корабли снова вступают в схватку (это происходит после выхода из гиперпространства). + battles = append(battles, ProduceBattles(c.Cache)...) + /*** Last steps ***/ // Store battles diff --git a/internal/controller/route_test.go b/internal/controller/route_test.go index 22b68e5..40fb08a 100644 --- a/internal/controller/route_test.go +++ b/internal/controller/route_test.go @@ -137,9 +137,6 @@ func TestEnrouteGroups_SplitGroup(t *testing.T) { c, g := newCache() assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", R0_Planet_2_num, R1_Planet_1_num)) c.MustPlanet(R0_Planet_0_num).Colonists = 65 @@ -162,9 +159,6 @@ func TestEnrouteGroups_GroupSorting(t *testing.T) { c, g := newCache() assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", R0_Planet_0_num, R0_Planet_2_num)) - // assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", R0_Planet_2_num, R1_Planet_1_num)) c.MustPlanet(R0_Planet_0_num).Colonists = 100 diff --git a/internal/controller/ship_group_move.go b/internal/controller/ship_group_move.go new file mode 100644 index 0000000..62f1325 --- /dev/null +++ b/internal/controller/ship_group_move.go @@ -0,0 +1,62 @@ +package controller + +import ( + "fmt" + "iter" + + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/util" +) + +func (c *Cache) MoveShipGroups() { + moved := make(map[int]bool) + for i := range c.listMoveableGroupIds() { + if v, ok := moved[i]; ok && v { + continue + } + sg := c.ShipGroup(i) + + if sg.FleetID != nil { + fi := c.MustFleetIndex(*sg.FleetID) + delta := c.FleetSpeed(c.g.Fleets[fi]) + for fgi := range c.fleetGroupIds(c.RaceIndex(sg.OwnerID), c.MustFleetIndex(*sg.FleetID)) { + c.moveShipGroup(fgi, delta) + moved[fgi] = true + } + continue + } + + c.moveShipGroup(i, sg.Speed(c.ShipGroupShipClass(i))) + moved[i] = true + } +} + +func (c *Cache) moveShipGroup(i int, delta float64) { + sg := c.ShipGroup(i) + originX, originY, ok := sg.Coord() + if !ok { + panic(fmt.Sprintf("ship group state invalid: %v", sg.State())) + } + destPlanet := c.MustPlanet(sg.Destination) + arrived := false + sg.StateInSpace.X, sg.StateInSpace.Y, arrived = + util.NextTravelCoord(c.g.Map.Width, c.g.Map.Height, originX, originY, destPlanet.X, destPlanet.Y, delta) + if arrived { + sg.StateInSpace = nil + } +} + +func (c *Cache) listMoveableGroupIds() iter.Seq[int] { + return func(yield func(int) bool) { + for i := range c.ShipGroupsIndex() { + sg := c.ShipGroup(i) + state := sg.State() + if !(state == game.StateInOrbit || state == game.StateLaunched || state == game.StateInSpace) { + continue + } + if !yield(i) { + return + } + } + } +} diff --git a/internal/controller/ship_group_move_test.go b/internal/controller/ship_group_move_test.go new file mode 100644 index 0000000..044b8bc --- /dev/null +++ b/internal/controller/ship_group_move_test.go @@ -0,0 +1,49 @@ +package controller_test + +import ( + "slices" + "testing" + + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/stretchr/testify/assert" +) + +func TestListMoveableGroupIds(t *testing.T) { + c, g := newCache() + + // 1: idx = 0 / [v] Non-Fleet group + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + + // 2: idx = 1 / [v] In-Fleet group + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) + // 3: idx = 2 / [v] In-Fleet group + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 2, 0)) + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 3, 0)) + + // 4: idx = 3 / [v] In_Space + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) + c.ShipGroup(3).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 31.337, + } + + // 5: idx = 4 / [x] In_Upgrage + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) + c.ShipGroup(4).StateUpgrade = &game.InUpgrade{ + UpgradeTech: []game.UpgradePreference{}, + } + + // 6: idx = 5 / [v] Just launched group + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + assert.NoError(t, g.SendGroup(Race_0.Name, 6, R0_Planet_2_num, 0)) + + movableGroups := slices.Collect(c.ListMoveableGroupIds()) + assert.Len(t, movableGroups, 5) + for _, i := range movableGroups { + sg := c.ShipGroup(i) + assert.NotEqual(t, game.StateUpgrade, sg.State()) + assert.NotEqual(t, game.StateTransfer, sg.State()) // TODO: Transfer state movable or not? + } +} diff --git a/internal/controller/ship_group_send.go b/internal/controller/ship_group_send.go index ff26688..69c9d28 100644 --- a/internal/controller/ship_group_send.go +++ b/internal/controller/ship_group_send.go @@ -72,10 +72,16 @@ func (c *Cache) LaunchShips(sg *game.ShipGroup, destination uint) *game.ShipGrou for i := range c.ShipGroupsIndex() { if c.ShipGroup(i).OwnerID == sg.OwnerID && c.ShipGroup(i).Index == sg.Index { state := c.ShipGroup(i).State() - if state != game.StateInOrbit && state != game.StateLaunched { + var p *game.Planet + switch state { + case game.StateInOrbit: + p = c.MustPlanet(sg.Destination) + case game.StateLaunched: + p = c.MustPlanet(sg.StateInSpace.Origin) + default: panic("state invalid") } - c.g.ShipGroups[i] = LaunchShips(*sg, destination) + c.g.ShipGroups[i] = LaunchShips(*sg, destination, p.X, p.Y) return &c.g.ShipGroups[i] } } @@ -96,9 +102,11 @@ func (c *Cache) UnsendShips(sg *game.ShipGroup) *game.ShipGroup { panic("ship group not found") } -func LaunchShips(sg game.ShipGroup, destination uint) game.ShipGroup { +func LaunchShips(sg game.ShipGroup, destination uint, originX, originY float64) game.ShipGroup { sg.StateInSpace = &game.InSpace{ Origin: sg.Destination, + X: originX, + Y: originY, } sg.Destination = destination return sg diff --git a/internal/controller/ship_group_send_test.go b/internal/controller/ship_group_send_test.go index 43595b9..aa97191 100644 --- a/internal/controller/ship_group_send_test.go +++ b/internal/controller/ship_group_send_test.go @@ -44,26 +44,32 @@ func TestSendGroup(t *testing.T) { g.SendGroup(Race_0.Name, 1, 3, 0), e.GenericErrorText(e.ErrSendUnreachableDestination)) - assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 3)) // send 3 of 10 + assert.NoError(t, g.SendGroup(Race_0.Name, 1, R0_Planet_2_num, 3)) // send 3 of 10 assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4) assert.Equal(t, uint(7), c.ShipGroup(0).Number) assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State()) assert.Equal(t, uint(3), c.ShipGroup(3).Number) assert.Equal(t, game.StateLaunched, c.ShipGroup(3).State()) + assert.NotNil(t, c.ShipGroup(3).StateInSpace) + assert.Equal(t, c.MustPlanet(R0_Planet_0_num).X, c.ShipGroup(3).StateInSpace.X) + assert.Equal(t, c.MustPlanet(R0_Planet_0_num).Y, c.ShipGroup(3).StateInSpace.Y) - assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 2)) // un-send 2 of 3 + assert.NoError(t, g.SendGroup(Race_0.Name, 4, R0_Planet_0_num, 2)) // un-send 2 of 3 assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4) assert.Equal(t, uint(9), c.MustShipGroup(Race_0_idx, 1).Number) assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 1).State()) assert.Equal(t, uint(1), c.MustShipGroup(Race_0_idx, 4).Number) assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 4).State()) + assert.NotNil(t, c.MustShipGroup(Race_0_idx, 4).StateInSpace) + assert.Equal(t, c.MustPlanet(R0_Planet_0_num).X, c.MustShipGroup(Race_0_idx, 4).StateInSpace.X) + assert.Equal(t, c.MustPlanet(R0_Planet_0_num).Y, c.MustShipGroup(Race_0_idx, 4).StateInSpace.Y) - assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 0)) // un-send the rest 1 + assert.NoError(t, g.SendGroup(Race_0.Name, 4, R0_Planet_0_num, 0)) // un-send the rest 1 assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3) assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 1).Number) assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 1).State()) - assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 0)) + assert.NoError(t, g.SendGroup(Race_0.Name, 1, R0_Planet_2_num, 0)) assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3) assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 1).Number) assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 1).State()) diff --git a/internal/model/game/group.go b/internal/model/game/group.go index 8ccef82..f7f2754 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -47,7 +47,7 @@ type InSpace struct { X float64 `json:"x"` Y float64 `json:"y"` // zero is for Launched status - // TODO: calculate range dynamically + // TODO: calculate range dynamically -BUT- if affects ShipGroup.State() Range float64 `json:"range"` } @@ -154,6 +154,14 @@ func (sg ShipGroup) OnPlanet() (uint, bool) { } } +func (sg ShipGroup) Coord() (float64, float64, bool) { + state := sg.State() + if state == StateInSpace || state == StateLaunched { + return sg.StateInSpace.X, sg.StateInSpace.Y, true + } + return 0, 0, false +} + func (sg ShipGroup) Equal(other ShipGroup) bool { return sg.OwnerID == other.OwnerID && sg.TypeID == other.TypeID && diff --git a/internal/util/map.go b/internal/util/map.go index af1d958..2275d41 100644 --- a/internal/util/map.go +++ b/internal/util/map.go @@ -3,6 +3,72 @@ package util import "math" func ShortDistance(w, h uint32, x1, y1, x2, y2 float64) float64 { + return math.Hypot(deltas(w, h, x1, y1, x2, y2)) +} + +func NextTravelCoord(w, h uint32, x1, y1, x2, y2, delta float64) (float64, float64, bool) { + deltaX, deltaY := deltas(w, h, x1, y1, x2, y2) + distance := math.Hypot(deltaX, deltaY) + if distance <= delta { + return x2, y2, true + } + // TODO: refactor - remove extra vars + xa := 0. + ya := 0. + xb := deltaX + yb := deltaY + d := distance + d2 := delta + xc := xa - (d2*(xa-xb))/d + yc := ya - (d2*(ya-yb))/d + + // --- + var tx, ty float64 + + if math.Abs(x2-x1) > float64(w/2) { + // moving across X boundary + if x2 < x1 { + // moving across higher border + tx = math.Mod(x1+xc, float64(w)) + } else { + // moving across lower border + tx = x1 - xc + if tx < 0 { + tx = float64(w) + tx + } + } + } else { + if x2 < x1 { + tx = x1 - xc + } else { + tx = x1 + xc + } + } + + if math.Abs(y2-y1) > float64(h/2) { + // moving across Y boundary + if y2 < y1 { + // moving across higher border + ty = math.Mod(y1+yc, float64(h)) + } else { + // moving across lower border + ty = y1 - yc + if ty < 0 { + ty = float64(h) + ty + } + } + } else { + if y2 < y1 { + ty = y1 - yc + } else { + ty = y1 + yc + } + } + + return tx, ty, false +} + +func deltas(w, h uint32, x1, y1, x2, y2 float64) (float64, float64) { dx := math.Abs(x2 - x1) dy := math.Abs(y2 - y1) if dx > float64(w/2) { @@ -11,6 +77,5 @@ func ShortDistance(w, h uint32, x1, y1, x2, y2 float64) float64 { if dy > float64(h/2) { dy = float64(h) - dy } - return math.Sqrt(math.Pow(dx, 2) + math.Pow(dy, 2)) - + return dx, dy } diff --git a/internal/util/map_test.go b/internal/util/map_test.go index 87ef438..648f9a5 100644 --- a/internal/util/map_test.go +++ b/internal/util/map_test.go @@ -25,3 +25,32 @@ func TestShortDistance(t *testing.T) { }) } } + +func TestNextTravelCoord(t *testing.T) { + for i, tc := range []struct { + w, h uint32 + ox, oy, dx, dy, delta float64 + tx, ty float64 + arrived bool + }{ + {w: 10, h: 10, ox: 0.0, oy: 0.0, dx: 2.0, dy: 0.0, delta: 1.0, tx: 1.0, ty: 0.0, arrived: false}, + {w: 10, h: 10, ox: 0.0, oy: 0.0, dx: 0.0, dy: 2.0, delta: 1.0, tx: 0.0, ty: 1.0, arrived: false}, + + {w: 10, h: 10, ox: 1.0, oy: 1.0, dx: 9.0, dy: 1.0, delta: 1.0, tx: 0.0, ty: 1.0, arrived: false}, + {w: 10, h: 10, ox: 1.0, oy: 9.5, dx: 1.0, dy: 1.0, delta: 1.0, tx: 1.0, ty: 0.5, arrived: false}, + + {w: 10, h: 10, ox: 1.0, oy: 1.0, dx: 5.0, dy: 5.0, delta: 2.0, tx: 2.414, ty: 2.414, arrived: false}, + {w: 10, h: 10, ox: 1.0, oy: 1.0, dx: 9.0, dy: 9.0, delta: 2.0, tx: 9.586, ty: 9.586, arrived: false}, + + {w: 10, h: 10, ox: 5.0, oy: 5.0, dx: 9.0, dy: 9.0, delta: 6.0, tx: 9.0, ty: 9.0, arrived: true}, + {w: 10, h: 10, ox: 6.0, oy: 6.0, dx: 10.0, dy: 10.0, delta: 6.0, tx: 10.0, ty: 10.0, arrived: true}, + {w: 10, h: 10, ox: 1.0, oy: 2.0, dx: 7.0, dy: 8.0, delta: 6.0, tx: 7.0, ty: 8.0, arrived: true}, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + tx, ty, arrived := util.NextTravelCoord(tc.w, tc.h, tc.ox, tc.oy, tc.dx, tc.dy, tc.delta) + assert.Equal(t, tc.arrived, arrived) + assert.Equal(t, tc.tx, number.Fixed3(tx)) + assert.Equal(t, tc.ty, number.Fixed3(ty)) + }) + } +}