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()
|
||||
|
||||
Reference in New Issue
Block a user