add PlaceShips message

This commit is contained in:
2026-10-27 17:31:41 +03:00
parent 31ef12b5f2
commit 6392c2ef03
7 changed files with 843 additions and 27 deletions
+156 -8
View File
@@ -7,10 +7,11 @@
//
// Opcodes:
//
// 1 C->S JOIN [1, "<game_token>", "<user_token>"]
// 2 S->C END [2, <seq>, <code>, "<reason>"]
// 3 S->C STATE [3, <seq>, <state_code>]
// 4 C<->S ACK [4, <seq>]
// 1 C->S JOIN [1, "<game_token>", "<user_token>"]
// 2 S->C END [2, <seq>, <code>, "<reason>"]
// 3 S->C STATE [3, <seq>, <state_code>]
// 4 C<->S ACK [4, <seq>]
// 5 C->S PLACE_SHIPS [5, ship0_orientation, ship0_x, ship0_y, ...]
//
// Every state-changing server->client message (END, STATE) carries a
// per-player monotonically increasing sequence number. The client confirms
@@ -30,12 +31,54 @@ import (
// Opcodes.
const (
OpJoin = 1 // C->S: join a match
OpEnd = 2 // S->C: match ended
OpState = 3 // S->C: game state transition
OpAck = 4 // C<->S: delivery acknowledgement
OpJoin = 1 // C->S: join a match
OpEnd = 2 // S->C: match ended
OpState = 3 // S->C: game state transition
OpAck = 4 // C<->S: delivery acknowledgement
OpPlaceShips = 5 // C->S: ship placement
)
// Board and ship configuration constants.
const (
BoardSize = 10
ShipCount = 10
ShipArgs = 30 // 10 ships * 3 args each
OrientationHorizontal = false
OrientationVertical = true
)
// ShipSizes defines the expected sizes of the 10 ships in order:
// 4 ships of size 1, 3 ships of size 2, 2 ships of size 3, 1 ship of size 4.
var ShipSizes = [ShipCount]int{1, 1, 1, 1, 2, 2, 2, 3, 3, 4}
// Cell represents coordinates on the 10x10 board.
type Cell struct {
X int
Y int
}
// Ship represents a single ship placement on the board.
type Ship struct {
Orientation bool // false: horizontal, true: vertical
X int // column (0-9)
Y int // row (0-9)
Size int // length (1-4)
}
// Cells returns all board cells occupied by the ship.
func (s Ship) Cells() []Cell {
cells := make([]Cell, s.Size)
for i := 0; i < s.Size; i++ {
if s.Orientation {
cells[i] = Cell{X: s.X, Y: s.Y + i}
} else {
cells[i] = Cell{X: s.X + i, Y: s.Y}
}
}
return cells
}
// Game state codes. States are always sent from the receiver's point of
// view: a player never learns whether they are "player 1" or "player 2".
const (
@@ -103,6 +146,98 @@ func ParseAck(args []json.RawMessage) (seq int64, err error) {
return seq, nil
}
// ParsePlaceShips extracts the 10 ships from PLACE_SHIPS arguments.
// args must contain 30 elements: (orientation, x, y) for each of the 10 ships.
// The ships correspond to increasing sizes: 4 size 1, 3 size 2, 2 size 3, 1 size 4.
func ParsePlaceShips(args []json.RawMessage) ([]Ship, error) {
if len(args) != ShipArgs {
return nil, fmt.Errorf("PLACE_SHIPS expects %d arguments, got %d", ShipArgs, len(args))
}
ships := make([]Ship, ShipCount)
for i := 0; i < ShipCount; i++ {
base := i * 3
var orientation bool
if err := json.Unmarshal(args[base], &orientation); err != nil {
return nil, fmt.Errorf("ship %d orientation is not a boolean: %w", i, err)
}
var x int
if err := json.Unmarshal(args[base+1], &x); err != nil {
return nil, fmt.Errorf("ship %d x is not an integer: %w", i, err)
}
var y int
if err := json.Unmarshal(args[base+2], &y); err != nil {
return nil, fmt.Errorf("ship %d y is not an integer: %w", i, err)
}
ships[i] = Ship{
Orientation: orientation,
X: x,
Y: y,
Size: ShipSizes[i],
}
}
return ships, nil
}
// ValidateShips validates the ship placement on the 10x10 board:
// - exactly 10 ships with sizes [1, 1, 1, 1, 2, 2, 2, 3, 3, 4]
// - all ship cells are within bounds (0 <= x < 10, 0 <= y < 10)
// - no ships overlap or touch each other in any direction (including diagonals),
// ensuring a gap of at least 1 cell between any two ships.
func ValidateShips(ships []Ship) error {
if len(ships) != ShipCount {
return fmt.Errorf("expected %d ships, got %d", ShipCount, len(ships))
}
var grid [BoardSize][BoardSize]int
for y := 0; y < BoardSize; y++ {
for x := 0; x < BoardSize; x++ {
grid[y][x] = -1
}
}
for i, ship := range ships {
expectedSize := ShipSizes[i]
if ship.Size != expectedSize {
return fmt.Errorf("ship %d: expected size %d, got %d", i, expectedSize, ship.Size)
}
cells := ship.Cells()
// 1. Bounds check
for _, c := range cells {
if c.X < 0 || c.X >= BoardSize || c.Y < 0 || c.Y >= BoardSize {
return fmt.Errorf("ship %d out of bounds at (%d, %d)", i, c.X, c.Y)
}
}
// 2. Collision / gap check against already placed ships
for _, c := range cells {
for dy := -1; dy <= 1; dy++ {
for dx := -1; dx <= 1; dx++ {
nx, ny := c.X+dx, c.Y+dy
if nx >= 0 && nx < BoardSize && ny >= 0 && ny < BoardSize {
if other := grid[ny][nx]; other != -1 {
return fmt.Errorf("ship %d at (%d, %d) touches or overlaps with ship %d at (%d, %d)", i, c.X, c.Y, other, nx, ny)
}
}
}
}
}
// 3. Place ship on the grid
for _, c := range cells {
grid[c.Y][c.X] = i
}
}
return nil
}
func build(v []any) []byte {
data, err := json.Marshal(v)
if err != nil {
@@ -127,3 +262,16 @@ func BuildState(seq int64, state int) []byte {
func BuildAck(seq int64) []byte {
return build([]any{OpAck, seq})
}
// BuildPlaceShips builds a PLACE_SHIPS message from a slice of ships:
// [5, ship0_orientation, ship0_x, ship0_y, ship1_orientation, ship1_x, ship1_y, ...]
func BuildPlaceShips(ships []Ship) []byte {
v := make([]any, 1+len(ships)*3)
v[0] = OpPlaceShips
for i, s := range ships {
v[1+i*3] = s.Orientation
v[1+i*3+1] = s.X
v[1+i*3+2] = s.Y
}
return build(v)
}