add PlaceShips message
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -69,3 +69,196 @@ func TestParseAck(t *testing.T) {
|
||||
t.Fatalf("seq = %d, want 12", seq)
|
||||
}
|
||||
}
|
||||
|
||||
func validTestShips() []Ship {
|
||||
return []Ship{
|
||||
// 4 size 1
|
||||
{Orientation: OrientationHorizontal, X: 9, Y: 4, Size: 1},
|
||||
{Orientation: OrientationVertical, X: 7, Y: 4, Size: 1},
|
||||
{Orientation: OrientationHorizontal, X: 5, Y: 4, Size: 1},
|
||||
{Orientation: OrientationHorizontal, X: 3, Y: 4, Size: 1},
|
||||
// 3 size 2
|
||||
{Orientation: OrientationHorizontal, X: 0, Y: 4, Size: 2},
|
||||
{Orientation: OrientationHorizontal, X: 7, Y: 2, Size: 2},
|
||||
{Orientation: OrientationHorizontal, X: 4, Y: 2, Size: 2},
|
||||
// 2 size 3
|
||||
{Orientation: OrientationHorizontal, X: 0, Y: 2, Size: 3},
|
||||
{Orientation: OrientationHorizontal, X: 5, Y: 0, Size: 3},
|
||||
// 1 size 4
|
||||
{Orientation: OrientationHorizontal, X: 0, Y: 0, Size: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsRoundtrip(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
data := BuildPlaceShips(ships)
|
||||
|
||||
opcode, args, err := ParseMessage(data)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMessage: %v", err)
|
||||
}
|
||||
if opcode != OpPlaceShips {
|
||||
t.Fatalf("opcode = %d, want %d", opcode, OpPlaceShips)
|
||||
}
|
||||
|
||||
parsed, err := ParsePlaceShips(args)
|
||||
if err != nil {
|
||||
t.Fatalf("ParsePlaceShips: %v", err)
|
||||
}
|
||||
if len(parsed) != len(ships) {
|
||||
t.Fatalf("got %d ships, want %d", len(parsed), len(ships))
|
||||
}
|
||||
for i := range ships {
|
||||
if parsed[i] != ships[i] {
|
||||
t.Fatalf("ship %d = %+v, want %+v", i, parsed[i], ships[i])
|
||||
}
|
||||
}
|
||||
|
||||
if err := ValidateShips(parsed); err != nil {
|
||||
t.Fatalf("ValidateShips: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePlaceShipsErrors(t *testing.T) {
|
||||
t.Run("wrong argument count", func(t *testing.T) {
|
||||
args := make([]json.RawMessage, 29)
|
||||
for i := range args {
|
||||
args[i] = json.RawMessage(`0`)
|
||||
}
|
||||
if _, err := ParsePlaceShips(args); err == nil {
|
||||
t.Fatal("expected error for 29 arguments")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("orientation not boolean", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
raw := make([]any, 31)
|
||||
raw[0] = OpPlaceShips
|
||||
for i, s := range ships {
|
||||
raw[1+i*3] = s.Orientation
|
||||
raw[1+i*3+1] = s.X
|
||||
raw[1+i*3+2] = s.Y
|
||||
}
|
||||
raw[1] = "invalid_orientation" // string instead of bool
|
||||
data, _ := json.Marshal(raw)
|
||||
_, args, _ := ParseMessage(data)
|
||||
if _, err := ParsePlaceShips(args); err == nil {
|
||||
t.Fatal("expected error for non-boolean orientation")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("x not integer", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
raw := make([]any, 31)
|
||||
raw[0] = OpPlaceShips
|
||||
for i, s := range ships {
|
||||
raw[1+i*3] = s.Orientation
|
||||
raw[1+i*3+1] = s.X
|
||||
raw[1+i*3+2] = s.Y
|
||||
}
|
||||
raw[2] = 1.5 // float instead of integer
|
||||
data, _ := json.Marshal(raw)
|
||||
_, args, _ := ParseMessage(data)
|
||||
if _, err := ParsePlaceShips(args); err == nil {
|
||||
t.Fatal("expected error for float x")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("y not integer", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
raw := make([]any, 31)
|
||||
raw[0] = OpPlaceShips
|
||||
for i, s := range ships {
|
||||
raw[1+i*3] = s.Orientation
|
||||
raw[1+i*3+1] = s.X
|
||||
raw[1+i*3+2] = s.Y
|
||||
}
|
||||
raw[3] = "zero" // string instead of integer
|
||||
data, _ := json.Marshal(raw)
|
||||
_, args, _ := ParseMessage(data)
|
||||
if _, err := ParsePlaceShips(args); err == nil {
|
||||
t.Fatal("expected error for string y")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateShips(t *testing.T) {
|
||||
t.Run("valid placement passes", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
if err := ValidateShips(ships); err != nil {
|
||||
t.Fatalf("unexpected validation error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong ship count", func(t *testing.T) {
|
||||
ships := validTestShips()[:9]
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for 9 ships")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrong ship size", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
ships[0].Size = 2 // ship 0 must have size 1
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for wrong ship size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative coordinate", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
ships[0].X = -1
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for negative X")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("out of bounds horizontal", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
// Ship 9 has size 4; at X=7, cells are 7, 8, 9, 10
|
||||
ships[9].X = 7
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for horizontal ship sticking out of bounds")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("out of bounds vertical", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
// Ship 9 has size 4, vertical at Y=7: cells are 7, 8, 9, 10
|
||||
ships[9].Orientation = OrientationVertical
|
||||
ships[9].Y = 7
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for vertical ship sticking out of bounds")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("overlapping ships", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
// Place ship 0 at exact same cell as ship 1
|
||||
ships[0].X = ships[1].X
|
||||
ships[0].Y = ships[1].Y
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for overlapping ships")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("orthogonal touching", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
// Ship 0 is at (9, 4), place ship 1 at (9, 5) -> touches orthogonally
|
||||
ships[1].X = 9
|
||||
ships[1].Y = 5
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for orthogonally touching ships")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("diagonal touching", func(t *testing.T) {
|
||||
ships := validTestShips()
|
||||
// Ship 0 is at (9, 4), place ship 1 at (8, 5) -> touches diagonally
|
||||
ships[1].X = 8
|
||||
ships[1].Y = 5
|
||||
if err := ValidateShips(ships); err == nil {
|
||||
t.Fatal("expected error for diagonally touching ships")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user