add PlaceShips message
This commit is contained in:
+62
-12
@@ -27,12 +27,13 @@ JWT_SECRET=... PUBLIC_ADDR=ws://localhost:8081/ws ./server
|
||||
|
||||
Все сообщения — JSON-массивы, первый элемент — числовой опкод.
|
||||
|
||||
| Опкод | Направление | Название | Формат |
|
||||
|-------|-------------|----------|--------|
|
||||
| 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, ...]` |
|
||||
|
||||
### JOIN (1)
|
||||
|
||||
@@ -85,8 +86,12 @@ JWT_SECRET=... PUBLIC_ADDR=ws://localhost:8081/ws ./server
|
||||
| 4 | ваш ход |
|
||||
| 5 | ожидайте ход соперника |
|
||||
|
||||
Сейчас рассылается только состояние `1` — при старте партии (когда оба
|
||||
игрока впервые присоединились).
|
||||
Смена состояний:
|
||||
- Состояние `1` рассылается обоим игрокам при старте партии (когда оба игрока впервые присоединились).
|
||||
- При получении расстановки кораблей от одного из игроков партия переходит в состояние ожидания соперника:
|
||||
игрок, сдавший корабли, получает состояние `3` («ожидайте готовности соперника»), а его противник — состояние `2` («соперник готов»).
|
||||
- Когда расстановку сдаёт второй игрок, игра переходит в фазу ходов: игрок, завершивший расстановку первым,
|
||||
получает состояние `4` («ваш ход»), а второй игрок — состояние `5` («ожидайте ход соперника»).
|
||||
|
||||
### ACK (4)
|
||||
|
||||
@@ -103,6 +108,51 @@ JWT_SECRET=... PUBLIC_ADDR=ws://localhost:8081/ws ./server
|
||||
Направление S->C зарезервировано для подтверждения будущих сообщений
|
||||
клиента (у которых пока нет номеров).
|
||||
|
||||
### PLACE_SHIPS (5)
|
||||
|
||||
Сообщение расстановки кораблей от клиента серверу. Принимается только от игрока,
|
||||
от которого ожидается расстановка в текущем состоянии партии (`StatePlacingShips` для
|
||||
обоих игроков; `StatePlayer1Ready` — только для игрока 2; `StatePlayer2Ready` — только
|
||||
для игрока 1). Повторные сообщения от уже расставившегося игрока или сообщения в фазе
|
||||
ходов отклоняются.
|
||||
|
||||
Формат сообщения:
|
||||
```json
|
||||
[5, ship0_orientation, ship0_x, ship0_y, ship1_orientation, ship1_x, ship1_y, ..., ship9_orientation, ship9_x, ship9_y]
|
||||
```
|
||||
|
||||
Сообщение содержит ровно 30 аргументов — по 3 аргумента на каждый из 10 кораблей
|
||||
в порядке возрастания размера:
|
||||
- 4 однопалубных корабля (размер 1, индексы 0..3)
|
||||
- 3 двухпалубных корабля (размер 2, индексы 4..6)
|
||||
- 2 трёхпалубных корабля (размер 3, индексы 7..8)
|
||||
- 1 четырёхпалубный корабль (размер 4, индекс 9)
|
||||
|
||||
Каждый корабль описывается 3-кортежем `(orientation, x, y)`:
|
||||
- `orientation` (boolean): `false` — горизонтальная ориентация (корабль продолжается
|
||||
вправо по оси X от начальной клетки `(x, y)`: клетки `(x + i, y)` для `i = 0..size-1`),
|
||||
`true` — вертикальная ориентация (корабль продолжается вниз по оси Y от начальной
|
||||
клетки `(x, y)`: клетки `(x, y + i)` для `i = 0..size-1`). Для однопалубных кораблей
|
||||
допустимо любое булево значение.
|
||||
- `x` (integer): координата X начала корабля (`0 <= x <= 9`).
|
||||
- `y` (integer): координата Y начала корабля (`0 <= y <= 9`).
|
||||
|
||||
Правила валидации расстановки:
|
||||
1. Количество кораблей строго 10, размеры строго соответствуют схеме 4x1, 3x2, 2x3, 1x4.
|
||||
2. Все клетки каждого корабля должны лежать строго в пределах игрового поля 10x10
|
||||
(`0 <= x < 10` и `0 <= y < 10`).
|
||||
3. Корабли не должны перекрываться и не должны соприкасаться сторонами или углами:
|
||||
между любыми двумя различными кораблями во всех направлениях (включая диагонали)
|
||||
всегда должен оставаться зазор минимум в 1 клетку.
|
||||
|
||||
При получении валидного сообщения сервер сохраняет расстановку в структуре матча
|
||||
(в виде сетки 10x10 с идентификаторами кораблей и списка кораблей) и переводит партию
|
||||
в следующее состояние:
|
||||
- Если первым расстановку завершил игрок 1:
|
||||
`StatePlacingShips` (1) -> `StatePlayer1Ready` (2) -> `StatePlayer1Turn` (4).
|
||||
- Если первым расстановку завершил игрок 2:
|
||||
`StatePlacingShips` (1) -> `StatePlayer2Ready` (3) -> `StatePlayer2Turn` (5).
|
||||
|
||||
## Коды закрытия соединения
|
||||
|
||||
| Код | Значение |
|
||||
@@ -166,13 +216,13 @@ internal/ws/ — websocket-эндпоинт: апгрейд, JOIN,
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
Покрыты: валидация токенов, разбор/сборка сообщений, сценарии менеджера
|
||||
(ожидание, старт, таймаут, повторный вход, замена соединения, ACK) и
|
||||
сквозные интеграционные тесты через настоящий websocket.
|
||||
Покрыты: валидация токенов, разбор/сборка сообщений (включая расстановку кораблей),
|
||||
сценарии менеджера (ожидание, старт, таймаут, повторный вход, замена соединения, ACK,
|
||||
расстановка кораблей с переходами состояний) и сквозные интеграционные тесты через настоящий websocket.
|
||||
|
||||
## TODO (следующие этапы)
|
||||
|
||||
- игровая логика (расстановка кораблей, ходы) и остальные состояния;
|
||||
- игровая логика стрельбы/ходов (расстановка кораблей и переходы к фазе ходов уже реализованы);
|
||||
- завершение партии по итогам игры с персонифицированными кодами
|
||||
победы/поражения;
|
||||
- репорт итогов партии на main-server (реестр завершённых партий уже
|
||||
|
||||
@@ -17,10 +17,34 @@ import (
|
||||
// that has already ended. The stored END message (if any) is replayed to the
|
||||
// connection before Join returns.
|
||||
var (
|
||||
ErrMatchFinished = errors.New("match already finished")
|
||||
ErrInvalidSeq = errors.New("invalid seq")
|
||||
ErrMatchFinished = errors.New("match already finished")
|
||||
ErrInvalidSeq = errors.New("invalid seq")
|
||||
ErrMatchNotStarted = errors.New("match not started")
|
||||
ErrUnexpectedState = errors.New("unexpected state for placing ships")
|
||||
ErrShipsAlreadyPlaced = errors.New("ships already placed")
|
||||
ErrInvalidSlot = errors.New("invalid slot index")
|
||||
)
|
||||
|
||||
// BoardSize is the width and height of the Sea Battle board.
|
||||
const BoardSize = protocol.BoardSize
|
||||
|
||||
// Board represents a 10x10 Sea Battle grid.
|
||||
// 0 indicates water/empty cell.
|
||||
// 1..10 indicates a ship (ship index 0..9 + 1).
|
||||
type Board [BoardSize][BoardSize]int8
|
||||
|
||||
// NewBoard constructs a Board from validated ships.
|
||||
func NewBoard(ships []protocol.Ship) Board {
|
||||
var b Board
|
||||
for i, ship := range ships {
|
||||
id := int8(i + 1)
|
||||
for _, cell := range ship.Cells() {
|
||||
b[cell.Y][cell.X] = id
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Server-side match states.
|
||||
const (
|
||||
StatePlacingShips = 1
|
||||
@@ -74,6 +98,10 @@ type PlayerSlot struct {
|
||||
unacked []queuedMessage
|
||||
lastState int // last state sent to this player, 0 if none
|
||||
everConnected bool // true after the first successful join
|
||||
|
||||
ships []protocol.Ship
|
||||
board Board
|
||||
shipsPlaced bool
|
||||
}
|
||||
|
||||
// Match is the state of a single game of sea battle.
|
||||
@@ -105,6 +133,29 @@ func (m *Match) State() int {
|
||||
return m.state
|
||||
}
|
||||
|
||||
// Ships returns a copy of the ships placed by the player in slotIdx,
|
||||
// or nil if ships have not been placed yet.
|
||||
func (m *Match) Ships(slotIdx int) []protocol.Ship {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if slotIdx < 0 || slotIdx >= len(m.slots) || !m.slots[slotIdx].shipsPlaced {
|
||||
return nil
|
||||
}
|
||||
res := make([]protocol.Ship, len(m.slots[slotIdx].ships))
|
||||
copy(res, m.slots[slotIdx].ships)
|
||||
return res
|
||||
}
|
||||
|
||||
// Board returns the board of the player in slotIdx and whether ships have been placed.
|
||||
func (m *Match) Board(slotIdx int) (Board, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if slotIdx < 0 || slotIdx >= len(m.slots) {
|
||||
return Board{}, false
|
||||
}
|
||||
return m.slots[slotIdx].board, m.slots[slotIdx].shipsPlaced
|
||||
}
|
||||
|
||||
func (m *Match) slotIndex(userID int64) int {
|
||||
switch userID {
|
||||
case m.playerIDs[0]:
|
||||
@@ -291,6 +342,64 @@ func (m *Manager) Ack(match *Match, idx int, seq int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlaceShips validates and stores the player's ship placement, advancing to the next match state.
|
||||
// The state order depends on which player completed the placement first:
|
||||
// - If Player 1 is first: StatePlacingShips -> StatePlayer1Ready -> StatePlayer1Turn
|
||||
// - If Player 2 is first: StatePlacingShips -> StatePlayer2Ready -> StatePlayer2Turn
|
||||
func (m *Manager) PlaceShips(ctx context.Context, match *Match, slotIdx int, ships []protocol.Ship) error {
|
||||
if err := protocol.ValidateShips(ships); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
match.mu.Lock()
|
||||
defer match.mu.Unlock()
|
||||
|
||||
if match.ended {
|
||||
return ErrMatchFinished
|
||||
}
|
||||
if !match.started {
|
||||
return ErrMatchNotStarted
|
||||
}
|
||||
if slotIdx < 0 || slotIdx >= len(match.slots) {
|
||||
return ErrInvalidSlot
|
||||
}
|
||||
|
||||
slot := match.slots[slotIdx]
|
||||
if slot.shipsPlaced {
|
||||
return ErrShipsAlreadyPlaced
|
||||
}
|
||||
|
||||
var nextState int
|
||||
switch match.state {
|
||||
case StatePlacingShips:
|
||||
if slotIdx == 0 {
|
||||
nextState = StatePlayer1Ready
|
||||
} else {
|
||||
nextState = StatePlayer2Ready
|
||||
}
|
||||
case StatePlayer1Ready:
|
||||
if slotIdx != 1 {
|
||||
return ErrUnexpectedState
|
||||
}
|
||||
nextState = StatePlayer1Turn
|
||||
case StatePlayer2Ready:
|
||||
if slotIdx != 0 {
|
||||
return ErrUnexpectedState
|
||||
}
|
||||
nextState = StatePlayer2Turn
|
||||
default:
|
||||
return ErrUnexpectedState
|
||||
}
|
||||
|
||||
slot.ships = make([]protocol.Ship, len(ships))
|
||||
copy(slot.ships, ships)
|
||||
slot.board = NewBoard(ships)
|
||||
slot.shipsPlaced = true
|
||||
|
||||
m.setStateLocked(ctx, match, nextState)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetState moves the match into a new server-side state and notifies both players.
|
||||
func (m *Manager) SetState(ctx context.Context, match *Match, state int) {
|
||||
match.mu.Lock()
|
||||
|
||||
@@ -454,3 +454,193 @@ func mustMatch(t *testing.T, m *Manager, id string) *Match {
|
||||
}
|
||||
return match
|
||||
}
|
||||
|
||||
func validTestShips() []protocol.Ship {
|
||||
return []protocol.Ship{
|
||||
// 4 size 1
|
||||
{Orientation: protocol.OrientationHorizontal, X: 9, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationVertical, X: 7, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 3, Y: 4, Size: 1},
|
||||
// 3 size 2
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 4, Size: 2},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 7, Y: 2, Size: 2},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 4, Y: 2, Size: 2},
|
||||
// 2 size 3
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 2, Size: 3},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 0, Size: 3},
|
||||
// 1 size 4
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 0, Size: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsP1First(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
if match.State() != StatePlacingShips {
|
||||
t.Fatalf("state = %d, want StatePlacingShips", match.State())
|
||||
}
|
||||
|
||||
ships := validTestShips()
|
||||
|
||||
// Player 1 places ships first
|
||||
if err := m.PlaceShips(context.Background(), match, 0, ships); err != nil {
|
||||
t.Fatalf("p1 place ships: %v", err)
|
||||
}
|
||||
if match.State() != StatePlayer1Ready {
|
||||
t.Fatalf("state = %d, want StatePlayer1Ready (2)", match.State())
|
||||
}
|
||||
|
||||
// Verify stored ships and board for p1
|
||||
p1Ships := match.Ships(0)
|
||||
if len(p1Ships) != len(ships) {
|
||||
t.Fatalf("got %d ships, want %d", len(p1Ships), len(ships))
|
||||
}
|
||||
p1Board, placed := match.Board(0)
|
||||
if !placed {
|
||||
t.Fatal("expected p1 board placed to be true")
|
||||
}
|
||||
// Check that ship 9 (size 4 at 0,0 horizontal) is in p1 board with id 10
|
||||
for x := 0; x < 4; x++ {
|
||||
if p1Board[0][x] != 10 {
|
||||
t.Fatalf("p1Board[0][%d] = %d, want 10", x, p1Board[0][x])
|
||||
}
|
||||
}
|
||||
|
||||
// Verify states delivered to p1 and p2
|
||||
p1Msgs, p2Msgs := c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentReady) {
|
||||
t.Fatalf("p1 got state %v, want StateWaitOpponentReady (3)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateOpponentReady) {
|
||||
t.Fatalf("p2 got state %v, want StateOpponentReady (2)", v)
|
||||
}
|
||||
|
||||
// Player 1 cannot place ships again
|
||||
if err := m.PlaceShips(context.Background(), match, 0, ships); err == nil {
|
||||
t.Fatal("expected error when player 1 places ships again")
|
||||
}
|
||||
|
||||
// Player 2 places ships
|
||||
if err := m.PlaceShips(context.Background(), match, 1, ships); err != nil {
|
||||
t.Fatalf("p2 place ships: %v", err)
|
||||
}
|
||||
if match.State() != StatePlayer1Turn {
|
||||
t.Fatalf("state = %d, want StatePlayer1Turn (4)", match.State())
|
||||
}
|
||||
|
||||
// Verify states delivered to p1 and p2 after game starts
|
||||
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
||||
t.Fatalf("p1 got state %v, want StateYourTurn (4)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
||||
t.Fatalf("p2 got state %v, want StateWaitOpponentTurn (5)", v)
|
||||
}
|
||||
|
||||
// Subsequent PlaceShips calls are rejected
|
||||
if err := m.PlaceShips(context.Background(), match, 1, ships); err == nil {
|
||||
t.Fatal("expected error when match is in turn phase")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsP2First(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
|
||||
ships := validTestShips()
|
||||
|
||||
// Player 2 places ships first
|
||||
if err := m.PlaceShips(context.Background(), match, 1, ships); err != nil {
|
||||
t.Fatalf("p2 place ships: %v", err)
|
||||
}
|
||||
if match.State() != StatePlayer2Ready {
|
||||
t.Fatalf("state = %d, want StatePlayer2Ready (3)", match.State())
|
||||
}
|
||||
|
||||
// Verify states delivered to p1 and p2
|
||||
p1Msgs, p2Msgs := c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateOpponentReady) {
|
||||
t.Fatalf("p1 got state %v, want StateOpponentReady (2)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentReady) {
|
||||
t.Fatalf("p2 got state %v, want StateWaitOpponentReady (3)", v)
|
||||
}
|
||||
|
||||
// Player 1 places ships
|
||||
if err := m.PlaceShips(context.Background(), match, 0, ships); err != nil {
|
||||
t.Fatalf("p1 place ships: %v", err)
|
||||
}
|
||||
if match.State() != StatePlayer2Turn {
|
||||
t.Fatalf("state = %d, want StatePlayer2Turn (5)", match.State())
|
||||
}
|
||||
|
||||
// Player 2 finished first, so Player 2 gets first turn!
|
||||
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
||||
t.Fatalf("p1 got state %v, want StateWaitOpponentTurn (5)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
||||
t.Fatalf("p2 got state %v, want StateYourTurn (4)", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsValidationFailure(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
|
||||
invalidShips := validTestShips()
|
||||
invalidShips[0].X = invalidShips[1].X // overlapping ships
|
||||
|
||||
if err := m.PlaceShips(context.Background(), match, 0, invalidShips); err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
if match.State() != StatePlacingShips {
|
||||
t.Fatalf("state changed to %d on error, want StatePlacingShips", match.State())
|
||||
}
|
||||
if match.Ships(0) != nil {
|
||||
t.Fatal("expected nil ships for p1 after failed placement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsBeforeMatchStarts(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1 := &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
|
||||
if err := m.PlaceShips(context.Background(), match, 0, validTestShips()); err != ErrMatchNotStarted {
|
||||
t.Fatalf("got err = %v, want ErrMatchNotStarted", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -277,6 +277,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Debug("invalid ACK seq", "match", match.ID, "user", uc.UserID, "seq", seq, "err", err)
|
||||
continue
|
||||
}
|
||||
case protocol.OpPlaceShips:
|
||||
ships, err := protocol.ParsePlaceShips(args)
|
||||
if err != nil {
|
||||
h.log.Debug("malformed PlaceShips", "match", match.ID, "user", uc.UserID, "err", err)
|
||||
continue
|
||||
}
|
||||
if err := protocol.ValidateShips(ships); err != nil {
|
||||
h.log.Debug("invalid ship placement", "match", match.ID, "user", uc.UserID, "err", err)
|
||||
continue
|
||||
}
|
||||
if err := h.Manager.PlaceShips(ctx, match, slotIdx, ships); err != nil {
|
||||
h.log.Debug("place ships rejected", "match", match.ID, "user", uc.UserID, "err", err)
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// Unknown opcodes are ignored for forward compatibility.
|
||||
h.log.Debug("unknown opcode", "match", match.ID, "user", uc.UserID, "opcode", opcode)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
|
||||
"github.com/ruby-sea-battle/game-server/internal/auth"
|
||||
"github.com/ruby-sea-battle/game-server/internal/game"
|
||||
"github.com/ruby-sea-battle/game-server/internal/protocol"
|
||||
"github.com/ruby-sea-battle/game-server/internal/ws"
|
||||
)
|
||||
|
||||
@@ -55,10 +57,16 @@ func userToken(t *testing.T, userID int64) string {
|
||||
func startServerWithManager(t *testing.T, joinTimeout time.Duration) (string, *game.Manager) {
|
||||
t.Helper()
|
||||
manager := game.NewManager(joinTimeout)
|
||||
log := slog.New(slog.NewTextHandler(testWriter{t}, nil))
|
||||
tw := &testWriter{t: t}
|
||||
log := slog.New(slog.NewTextHandler(tw, nil))
|
||||
handler := ws.NewHandler(manager, secret, publicAddr, []string{"*"}, log)
|
||||
srv := httptest.NewServer(handler)
|
||||
t.Cleanup(srv.Close)
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
tw.mu.Lock()
|
||||
tw.done = true
|
||||
tw.mu.Unlock()
|
||||
})
|
||||
return "ws" + strings.TrimPrefix(srv.URL, "http"), manager
|
||||
}
|
||||
|
||||
@@ -67,10 +75,18 @@ func startServer(t *testing.T, joinTimeout time.Duration) (url string) {
|
||||
return u
|
||||
}
|
||||
|
||||
type testWriter struct{ t *testing.T }
|
||||
type testWriter struct {
|
||||
mu sync.Mutex
|
||||
t *testing.T
|
||||
done bool
|
||||
}
|
||||
|
||||
func (w testWriter) Write(p []byte) (int, error) {
|
||||
w.t.Log(strings.TrimSpace(string(p)))
|
||||
func (w *testWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
if !w.done {
|
||||
w.t.Log(strings.TrimSpace(string(p)))
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
@@ -317,3 +333,99 @@ func TestManagerShutdownClosesConnectionsGoingAway(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validTestShips() []protocol.Ship {
|
||||
return []protocol.Ship{
|
||||
// 4 size 1
|
||||
{Orientation: protocol.OrientationHorizontal, X: 9, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationVertical, X: 7, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 4, Size: 1},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 3, Y: 4, Size: 1},
|
||||
// 3 size 2
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 4, Size: 2},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 7, Y: 2, Size: 2},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 4, Y: 2, Size: 2},
|
||||
// 2 size 3
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 2, Size: 3},
|
||||
{Orientation: protocol.OrientationHorizontal, X: 5, Y: 0, Size: 3},
|
||||
// 1 size 4
|
||||
{Orientation: protocol.OrientationHorizontal, X: 0, Y: 0, Size: 4},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaceShipsOverWebSocket(t *testing.T) {
|
||||
url, mgr := startServerWithManager(t, time.Minute)
|
||||
exp := time.Now().Add(time.Hour)
|
||||
gt := gameToken(t, "m-ws-placeships", 1, 2, publicAddr, exp)
|
||||
|
||||
c1, c2 := dial(t, url), dial(t, url)
|
||||
join(t, c1, gt, userToken(t, 1))
|
||||
join(t, c2, gt, userToken(t, 2))
|
||||
|
||||
// Both receive StatePlacingShips (seq 1)
|
||||
v1 := readMsg(t, c1)
|
||||
if v1[0] != float64(protocol.OpState) || v1[1] != float64(1) || v1[2] != float64(protocol.StatePlacingShips) {
|
||||
t.Fatalf("c1 unexpected initial state: %v", v1)
|
||||
}
|
||||
v2 := readMsg(t, c2)
|
||||
if v2[0] != float64(protocol.OpState) || v2[1] != float64(1) || v2[2] != float64(protocol.StatePlacingShips) {
|
||||
t.Fatalf("c2 unexpected initial state: %v", v2)
|
||||
}
|
||||
|
||||
// ACK seq 1
|
||||
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
||||
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(1))
|
||||
|
||||
// Player 1 sends PlaceShips
|
||||
ships := validTestShips()
|
||||
placeMsg := protocol.BuildPlaceShips(ships)
|
||||
if err := c1.Write(context.Background(), websocket.MessageText, placeMsg); err != nil {
|
||||
t.Fatalf("write PlaceShips c1: %v", err)
|
||||
}
|
||||
|
||||
// c1 gets StateWaitOpponentReady (3), c2 gets StateOpponentReady (2)
|
||||
v1 = readMsg(t, c1)
|
||||
if v1[0] != float64(protocol.OpState) || v1[1] != float64(2) || v1[2] != float64(protocol.StateWaitOpponentReady) {
|
||||
t.Fatalf("c1 got %v, want StateWaitOpponentReady (3)", v1)
|
||||
}
|
||||
v2 = readMsg(t, c2)
|
||||
if v2[0] != float64(protocol.OpState) || v2[1] != float64(2) || v2[2] != float64(protocol.StateOpponentReady) {
|
||||
t.Fatalf("c2 got %v, want StateOpponentReady (2)", v2)
|
||||
}
|
||||
|
||||
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
||||
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(2))
|
||||
|
||||
// Player 2 sends PlaceShips
|
||||
if err := c2.Write(context.Background(), websocket.MessageText, placeMsg); err != nil {
|
||||
t.Fatalf("write PlaceShips c2: %v", err)
|
||||
}
|
||||
|
||||
// Game starts: Player 1 was first, so c1 gets StateYourTurn (4), c2 gets StateWaitOpponentTurn (5)
|
||||
v1 = readMsg(t, c1)
|
||||
if v1[0] != float64(protocol.OpState) || v1[1] != float64(3) || v1[2] != float64(protocol.StateYourTurn) {
|
||||
t.Fatalf("c1 got %v, want StateYourTurn (4)", v1)
|
||||
}
|
||||
v2 = readMsg(t, c2)
|
||||
if v2[0] != float64(protocol.OpState) || v2[1] != float64(3) || v2[2] != float64(protocol.StateWaitOpponentTurn) {
|
||||
t.Fatalf("c2 got %v, want StateWaitOpponentTurn (5)", v2)
|
||||
}
|
||||
|
||||
_ = c1.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
||||
_ = c2.Write(context.Background(), websocket.MessageText, protocol.BuildAck(3))
|
||||
|
||||
// Check that the match on the server has the placements stored
|
||||
match := mgr.GetMatch("m-ws-placeships")
|
||||
if match == nil {
|
||||
t.Fatal("match not found on manager")
|
||||
}
|
||||
if len(match.Ships(0)) != 10 || len(match.Ships(1)) != 10 {
|
||||
t.Fatalf("ships not stored on match: p1=%d, p2=%d", len(match.Ships(0)), len(match.Ships(1)))
|
||||
}
|
||||
if _, ok := match.Board(0); !ok {
|
||||
t.Fatal("p1 board not placed")
|
||||
}
|
||||
if _, ok := match.Board(1); !ok {
|
||||
t.Fatal("p2 board not placed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user