add PlaceShips message
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user