bugfixes and refactoring in game-server

This commit is contained in:
2026-10-27 15:55:48 +03:00
parent 3629411030
commit 31ef12b5f2
9 changed files with 732 additions and 66 deletions
+192 -1
View File
@@ -237,7 +237,9 @@ func TestAckThenReconnectResendsCurrentState(t *testing.T) {
// Player 1 acknowledges the STATE message, then reconnects. The queue
// is empty, so the server re-sends the current state with a fresh seq.
m.Ack(match, 0, 1)
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 {
@@ -253,6 +255,195 @@ func TestAckThenReconnectResendsCurrentState(t *testing.T) {
}
}
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()