647 lines
20 KiB
Go
647 lines
20 KiB
Go
package game
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
"github.com/ruby-sea-battle/game-server/internal/auth"
|
|
"github.com/ruby-sea-battle/game-server/internal/protocol"
|
|
)
|
|
|
|
type fakeConn struct {
|
|
mu sync.Mutex
|
|
sent [][]byte
|
|
closed bool
|
|
closeCode int
|
|
}
|
|
|
|
func (f *fakeConn) Send(_ context.Context, data []byte) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
cp := make([]byte, len(data))
|
|
copy(cp, data)
|
|
f.sent = append(f.sent, cp)
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeConn) Close(code int, _ string) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.closed = true
|
|
f.closeCode = code
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeConn) messages() [][]byte {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
out := make([][]byte, len(f.sent))
|
|
copy(out, f.sent)
|
|
return out
|
|
}
|
|
|
|
func (f *fakeConn) isClosed() (bool, int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.closed, f.closeCode
|
|
}
|
|
|
|
func claims(exp time.Time) *auth.GameClaims {
|
|
return &auth.GameClaims{
|
|
MatchID: "match-1",
|
|
Player1ID: 10,
|
|
Player2ID: 20,
|
|
GameServer: "ws://game.example.com/ws",
|
|
RegisteredClaims: jwt.RegisteredClaims{
|
|
ExpiresAt: jwt.NewNumericDate(exp),
|
|
},
|
|
}
|
|
}
|
|
|
|
func parseMsg(t *testing.T, data []byte) []any {
|
|
t.Helper()
|
|
var v []any
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", data, err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func TestJoinFirstPlayerWaits(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
c1 := &fakeConn{}
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
|
|
_, idx, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
|
if err != nil {
|
|
t.Fatalf("join: %v", err)
|
|
}
|
|
if idx != 0 {
|
|
t.Fatalf("slot = %d, want 0", idx)
|
|
}
|
|
if got := len(c1.messages()); got != 0 {
|
|
t.Fatalf("first player got %d messages, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestSecondJoinStartsMatch(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
c1, c2 := &fakeConn{}, &fakeConn{}
|
|
|
|
if _, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID); err != nil {
|
|
t.Fatalf("join p1: %v", err)
|
|
}
|
|
if _, idx, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
|
t.Fatalf("join p2: %v", err)
|
|
} else if idx != 1 {
|
|
t.Fatalf("slot = %d, want 1", idx)
|
|
}
|
|
|
|
for i, c := range []*fakeConn{c1, c2} {
|
|
msgs := c.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("player %d got %d messages, want 1", i, len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
// [3, seq, StatePlacingShips]
|
|
if v[0] != float64(protocol.OpState) || v[1] != float64(1) || v[2] != float64(protocol.StatePlacingShips) {
|
|
t.Fatalf("player %d got unexpected message: %v", i, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJoinTimeoutEndsMatch(t *testing.T) {
|
|
m := NewManager(50 * time.Millisecond)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
c1 := &fakeConn{}
|
|
|
|
if _, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID); err != nil {
|
|
t.Fatalf("join: %v", err)
|
|
}
|
|
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
if closed, _ := c1.isClosed(); closed {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("timed out waiting for the match to end")
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
msgs := c1.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("got %d messages, want 1", len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
// [2, seq, -1, "join timeout exceeded"]
|
|
if v[0] != float64(protocol.OpEnd) || v[1] != float64(1) || v[2] != float64(protocol.EndCodeJoinTimeout) || v[3] != "join timeout exceeded" {
|
|
t.Fatalf("unexpected END message: %v", v)
|
|
}
|
|
if closed, code := c1.isClosed(); !closed || code != protocol.CloseCodeNormal {
|
|
t.Fatalf("conn closed=%v code=%d, want closed with %d", closed, code, protocol.CloseCodeNormal)
|
|
}
|
|
}
|
|
|
|
func TestJoinAfterFinishReplaysEnd(t *testing.T) {
|
|
m := NewManager(50 * time.Millisecond)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
c1 := &fakeConn{}
|
|
|
|
if _, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID); err != nil {
|
|
t.Fatalf("join: %v", err)
|
|
}
|
|
time.Sleep(200 * time.Millisecond) // let the join timer fire
|
|
|
|
// A late join with the same (still valid) token must not create a new
|
|
// match; the stored END is replayed instead. This also works for the
|
|
// player who never connected at all.
|
|
for _, userID := range []int64{gc.Player1ID, gc.Player2ID} {
|
|
c := &fakeConn{}
|
|
_, _, err := m.Join(context.Background(), c, gc, userID)
|
|
if err != ErrMatchFinished {
|
|
t.Fatalf("join after finish: err = %v, want ErrMatchFinished", err)
|
|
}
|
|
msgs := c.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("replayed %d messages, want 1", len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
if v[0] != float64(protocol.OpEnd) || v[2] != float64(protocol.EndCodeJoinTimeout) {
|
|
t.Fatalf("unexpected replayed message: %v", v)
|
|
}
|
|
if closed, _ := c.isClosed(); !closed {
|
|
t.Fatal("connection was not closed after replaying END")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReconnectReplacesConnection(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
old, c2 := &fakeConn{}, &fakeConn{}
|
|
|
|
if _, _, err := m.Join(context.Background(), old, gc, gc.Player1ID); 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)
|
|
}
|
|
// Game started: both got STATE, nobody acknowledged it yet.
|
|
|
|
// Player 1 reconnects: old connection must be closed, the unacknowledged
|
|
// STATE must be replayed to the new one.
|
|
fresh := &fakeConn{}
|
|
if _, idx, err := m.Join(context.Background(), fresh, gc, gc.Player1ID); err != nil {
|
|
t.Fatalf("rejoin p1: %v", err)
|
|
} else if idx != 0 {
|
|
t.Fatalf("slot = %d, want 0", idx)
|
|
}
|
|
|
|
if closed, code := old.isClosed(); !closed || code != protocol.CloseCodeReplaced {
|
|
t.Fatalf("old conn closed=%v code=%d, want closed with %d", closed, code, protocol.CloseCodeReplaced)
|
|
}
|
|
msgs := fresh.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("replayed %d messages, want 1", len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
if v[0] != float64(protocol.OpState) || v[2] != float64(protocol.StatePlacingShips) {
|
|
t.Fatalf("unexpected replayed message: %v", v)
|
|
}
|
|
|
|
// Leave with the old connection must not detach the new one.
|
|
m.Leave(old, mustMatch(t, m, gc.MatchID), 0)
|
|
m.Leave(fresh, mustMatch(t, m, gc.MatchID), 0)
|
|
}
|
|
|
|
func TestAckThenReconnectResendsCurrentState(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)
|
|
}
|
|
|
|
// Player 1 acknowledges the STATE message, then reconnects. The queue
|
|
// is empty, so the server re-sends the current state with a fresh seq.
|
|
if err := m.Ack(match, 0, 1); err != nil {
|
|
t.Fatalf("ack: %v", err)
|
|
}
|
|
|
|
fresh := &fakeConn{}
|
|
if _, _, err := m.Join(context.Background(), fresh, gc, gc.Player1ID); err != nil {
|
|
t.Fatalf("rejoin p1: %v", err)
|
|
}
|
|
msgs := fresh.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("got %d messages, want 1", len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
if v[0] != float64(protocol.OpState) || v[1] != float64(2) || v[2] != float64(protocol.StatePlacingShips) {
|
|
t.Fatalf("unexpected message: %v", v)
|
|
}
|
|
}
|
|
|
|
func TestServerStateTransitions(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)
|
|
}
|
|
// Initial state is StatePlacingShips
|
|
if match.State() != StatePlacingShips {
|
|
t.Fatalf("state = %d, want StatePlacingShips", match.State())
|
|
}
|
|
|
|
// Move to StatePlayer1Ready
|
|
m.SetState(context.Background(), match, StatePlayer1Ready)
|
|
p1Msgs, p2Msgs := c1.messages(), c2.messages()
|
|
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentReady) {
|
|
t.Fatalf("p1 got %v, want StateWaitOpponentReady (3)", v)
|
|
}
|
|
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateOpponentReady) {
|
|
t.Fatalf("p2 got %v, want StateOpponentReady (2)", v)
|
|
}
|
|
|
|
// Move to StatePlayer1Turn
|
|
m.SetState(context.Background(), match, StatePlayer1Turn)
|
|
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
|
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
|
t.Fatalf("p1 got %v, want StateYourTurn (4)", v)
|
|
}
|
|
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
|
t.Fatalf("p2 got %v, want StateWaitOpponentTurn (5)", v)
|
|
}
|
|
|
|
// Move to StatePlayer2Turn
|
|
m.SetState(context.Background(), match, StatePlayer2Turn)
|
|
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
|
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
|
t.Fatalf("p1 got %v, want StateWaitOpponentTurn (5)", v)
|
|
}
|
|
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
|
t.Fatalf("p2 got %v, want StateYourTurn (4)", v)
|
|
}
|
|
}
|
|
|
|
func TestAckValidation(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)
|
|
}
|
|
|
|
// seq <= 0 should fail
|
|
if err := m.Ack(match, 0, 0); err != ErrInvalidSeq {
|
|
t.Fatalf("ack 0: got err = %v, want ErrInvalidSeq", err)
|
|
}
|
|
if err := m.Ack(match, 0, -1); err != ErrInvalidSeq {
|
|
t.Fatalf("ack -1: got err = %v, want ErrInvalidSeq", err)
|
|
}
|
|
|
|
// seq > slot.seq (e.g. 2 when only 1 sent) should fail
|
|
if err := m.Ack(match, 0, 2); err != ErrInvalidSeq {
|
|
t.Fatalf("ack 2: got err = %v, want ErrInvalidSeq", err)
|
|
}
|
|
|
|
// seq == 1 should succeed
|
|
if err := m.Ack(match, 0, 1); err != nil {
|
|
t.Fatalf("ack 1: unexpected err = %v", err)
|
|
}
|
|
|
|
// Idempotent duplicate ack should succeed
|
|
if err := m.Ack(match, 0, 1); err != nil {
|
|
t.Fatalf("repeat ack 1: unexpected err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEndMatchCleansUpMatch(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)
|
|
}
|
|
|
|
// Match started successfully and is in m.matches
|
|
if m.GetMatch(gc.MatchID) == nil {
|
|
t.Fatal("expected match in m.matches")
|
|
}
|
|
|
|
// End match
|
|
m.EndMatch(match.ID, 0, "game completed")
|
|
|
|
// Match should be deleted from m.matches (preventing memory leak)
|
|
if m.GetMatch(gc.MatchID) != nil {
|
|
t.Fatal("expected match to be deleted from m.matches")
|
|
}
|
|
|
|
// Both connections should be notified and closed with CloseCodeNormal
|
|
for i, c := range []*fakeConn{c1, c2} {
|
|
msgs := c.messages()
|
|
lastMsg := parseMsg(t, msgs[len(msgs)-1])
|
|
if lastMsg[0] != float64(protocol.OpEnd) || lastMsg[3] != "game completed" {
|
|
t.Fatalf("player %d unexpected END: %v", i, lastMsg)
|
|
}
|
|
if closed, code := c.isClosed(); !closed || code != protocol.CloseCodeNormal {
|
|
t.Fatalf("player %d closed=%v code=%d, want closed with %d", i, closed, code, protocol.CloseCodeNormal)
|
|
}
|
|
}
|
|
|
|
// Late join should replay END
|
|
c3 := &fakeConn{}
|
|
_, _, err = m.Join(context.Background(), c3, gc, gc.Player1ID)
|
|
if err != ErrMatchFinished {
|
|
t.Fatalf("err = %v, want ErrMatchFinished", err)
|
|
}
|
|
msgs := c3.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("replayed %d messages, want 1", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestManagerShutdown(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
c1, c2 := &fakeConn{}, &fakeConn{}
|
|
|
|
_, _, 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)
|
|
}
|
|
|
|
m.Shutdown()
|
|
|
|
for i, c := range []*fakeConn{c1, c2} {
|
|
if closed, code := c.isClosed(); !closed || code != protocol.CloseCodeGoingAway {
|
|
t.Fatalf("player %d closed=%v code=%d, want closed with %d", i, closed, code, protocol.CloseCodeGoingAway)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJoinWhenMatchAlreadyEndedReplaysReason(t *testing.T) {
|
|
m := NewManager(time.Minute)
|
|
gc := claims(time.Now().Add(time.Hour))
|
|
c1 := &fakeConn{}
|
|
|
|
_, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
|
if err != nil {
|
|
t.Fatalf("join: %v", err)
|
|
}
|
|
|
|
// Trigger onJoinTimeout so match is ended
|
|
m.onJoinTimeout(gc.MatchID)
|
|
|
|
// Late join on ended match must send the reason before closing
|
|
c2 := &fakeConn{}
|
|
_, _, err = m.Join(context.Background(), c2, gc, gc.Player2ID)
|
|
if err != ErrMatchFinished {
|
|
t.Fatalf("got err = %v, want ErrMatchFinished", err)
|
|
}
|
|
msgs := c2.messages()
|
|
if len(msgs) != 1 {
|
|
t.Fatalf("got %d messages, want 1", len(msgs))
|
|
}
|
|
v := parseMsg(t, msgs[0])
|
|
if v[0] != float64(protocol.OpEnd) || v[2] != float64(protocol.EndCodeJoinTimeout) || v[3] != "join timeout exceeded" {
|
|
t.Fatalf("unexpected message: %v", v)
|
|
}
|
|
if closed, code := c2.isClosed(); !closed || code != protocol.CloseCodeNormal {
|
|
t.Fatalf("c2 closed=%v code=%d, want %d", closed, code, protocol.CloseCodeNormal)
|
|
}
|
|
}
|
|
|
|
func mustMatch(t *testing.T, m *Manager, id string) *Match {
|
|
t.Helper()
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
match, ok := m.matches[id]
|
|
if !ok {
|
|
t.Fatalf("match %q not found", id)
|
|
}
|
|
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)
|
|
}
|
|
}
|