mancala/apps/backend/tests/game/GameManager.test.ts
Halit Aksoy ed2a909f16 fix(backend): clean up spectators and finished games
- Spectators are removed from games when they disconnect; the lookup
  iterated an iterator with for...in and never found anything
- Finished and left games are removed from memory, not only from the
  player mappings
- The connection info is sent once when a game starts
2026-10-16 00:08:38 +03:00

75 lines
2.9 KiB
TypeScript

import {
channel_game_move,
channel_leave_game,
channel_listen_game_events,
channel_new_game,
channel_on_game_start,
channel_on_user_connection_change,
} from "@mancala/core";
import { GameManager } from "../../src/game/GameManager";
import { GameStoreImpl } from "../../src/game/gamestore/GameStoreImpl";
import { MatchMakerImpl } from "../../src/matchmaker/MatchMakerImpl";
import { BackendGame } from "../../src/models/Game";
import { FakeRtmt } from "./FakeRtmt";
const ALICE = { id: "kc:alice", name: "alice", isGuest: false };
const BOB = { id: "kc:bob", name: "bob", isGuest: false };
const CAROL = { id: "kc:carol", name: "carol", isGuest: false };
describe("GameManager", () => {
let rtmt: FakeRtmt;
let gameStore: GameStoreImpl;
beforeEach(() => {
rtmt = new FakeRtmt();
gameStore = new GameStoreImpl();
new GameManager({ rtmt, gameStore, matchMaker: new MatchMakerImpl({ rtmt }) });
[ALICE, BOB, CAROL].forEach((user) => rtmt.connect(user));
});
const startGame = (): BackendGame => {
rtmt.receive(ALICE.id, channel_new_game);
rtmt.receive(BOB.id, channel_new_game);
const game = gameStore.getGameByUser(ALICE.id);
if (!game) throw new Error("game did not start");
return game;
};
it("sends the connection info once when a game starts", () => {
startGame();
expect(rtmt.messagesTo(ALICE.id, channel_on_game_start)).toHaveLength(1);
expect(rtmt.messagesTo(ALICE.id, channel_on_user_connection_change)).toHaveLength(1);
});
it("removes a game from memory when a player leaves", () => {
const game = startGame();
rtmt.receive(ALICE.id, channel_leave_game);
expect(gameStore.get(game.id)).toBeUndefined();
expect(gameStore.getGameByUser(BOB.id)).toBeUndefined();
});
it("removes a game from memory when it ends by the rules", () => {
const game = startGame();
// Alice has a single stone in her last pit, next to her store; playing it empties her side.
const aliceIsPlayer1 = game.mancalaGame.player1Id === ALICE.id;
const board = game.mancalaGame.board;
board.pits.forEach((pit) => (pit.stoneCount = 0));
board.pits[aliceIsPlayer1 ? 5 : 12].stoneCount = 1;
board.pits[aliceIsPlayer1 ? 7 : 0].stoneCount = 1;
game.mancalaGame.turnPlayerId = ALICE.id;
rtmt.receive(ALICE.id, channel_game_move, { index: 5 });
expect(game.mancalaGame.state).toBe("ended");
expect(gameStore.get(game.id)).toBeUndefined();
});
it("stops counting a spectator who disconnects", () => {
const game = startGame();
rtmt.receive(CAROL.id, channel_listen_game_events, game.id);
expect(game.spectatorIds).toEqual([CAROL.id]);
expect(gameStore.getSpeactatingGameIdsByUser(CAROL.id)).toEqual([game.id]);
rtmt.disconnect(CAROL.id);
expect(game.spectatorIds).toEqual([]);
});
});