Files
Luncher/server/src/tests/pongGame.test.ts
T
batmanisko 6cb1214f08
CI / Generate TypeScript types (push) Canceled after 7s
CI / Server unit tests (push) Canceled after 0s
CI / Build server (push) Canceled after 0s
CI / Build client (push) Canceled after 0s
CI / Playwright E2E tests (push) Canceled after 0s
CI / Build and push Docker image (push) Canceled after 0s
CI / Notify (push) Canceled after 0s
feat: skryté multiplayerové minihry přes websocket
Minihry se otevírají trojklikem na logo a napsáním kódu hry
("help" vypíše seznam do konzole prohlížeče):
- reakce: reakční hra, 3 kola, pořadí podle součtu časů
- snake: had pro více hráčů, simulaci drží server
- tetris: battle Tetris s odpadními řádky a lokálním tréninkem
- pong: Pong 1v1, ovládání myší i klávesami, bot se 4 obtížnostmi
- kresleni: kreslení a hádání jídel z dnešních menu

Ikony her se řídí zvoleným vzhledem. Stav her je v paměti serveru,
předpokládá se jedna replika.
2026-10-12 14:25:45 +02:00

125 lines
4.9 KiB
TypeScript

import { PongGame, PongState, TICK_MS, COUNTDOWN_SECONDS, FIELD_HEIGHT, PADDLE_MARGIN, PADDLE_WIDTH, PADDLE_HEIGHT, WIN_SCORE, SERVE_PAUSE_MS, botLogin, BotLevel } from '../pongGame';
import { GameError } from '../minigames';
import { fakeClock } from './helpers/fakeClock';
/** Deterministická pseudonáhoda, ať se boti nechovají pokaždé stejně jako s konstantou. */
function lcg(seed = 1) {
let x = seed;
return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
}
function setup(random?: () => number) {
const clock = fakeClock(random);
const states: PongState[] = [];
const game = new PongGame(s => states.push(s), s => states.push(s), clock);
const last = () => states[states.length - 1];
const player = (login: string) => last().players.find(p => p.login === login)!;
const startGame = () => {
game.join('alice');
game.join('bob');
game.start('alice');
clock.advance(COUNTDOWN_SECONDS * 1000);
};
const ticks = (n: number) => clock.advance(n * TICK_MS);
return { game, clock, last, player, startGame, ticks };
}
describe('pong', () => {
test('je jen pro dva hráče a hru spouští zakladatel', () => {
const ctx = setup();
ctx.game.join('alice');
expect(() => ctx.game.start('alice')).toThrow(GameError);
ctx.game.join('bob');
expect(() => ctx.game.join('carol')).toThrow(GameError);
expect(() => ctx.game.start('bob')).toThrow(GameError);
ctx.game.start('alice');
expect(ctx.last().phase).toBe('countdown');
});
test('pálka se hýbe podle vstupu a nevyjede z hřiště', () => {
const ctx = setup();
ctx.startGame();
ctx.game.input('alice', 'up');
ctx.ticks(10);
expect(ctx.player('alice').y).toBeLessThan(FIELD_HEIGHT / 2);
ctx.ticks(100);
expect(ctx.player('alice').y).toBe(45);
ctx.game.input('alice', 'stop');
const y = ctx.player('alice').y;
ctx.ticks(5);
expect(ctx.player('alice').y).toBe(y);
});
test('míček se od pálky odrazí', () => {
const ctx = setup();
ctx.startGame();
const face = PADDLE_MARGIN + PADDLE_WIDTH;
ctx.game.setBallForTest(face + 20, FIELD_HEIGHT / 2, -400, 0);
ctx.ticks(3);
expect(ctx.last().ball.x).toBeGreaterThan(face);
expect(ctx.player('bob').score).toBe(0);
});
test('minutý míček je bod pro soupeře a hra končí na vítězné skóre', () => {
const ctx = setup();
ctx.startGame();
for (let i = 0; i < WIN_SCORE; i++) {
// Míček u horního okraje, kam pálka uprostřed nedosáhne
ctx.game.setBallForTest(60, 10, -600, 0);
ctx.ticks(10);
if (i < WIN_SCORE - 1) {
expect(ctx.player('bob').score).toBe(i + 1);
ctx.clock.advance(SERVE_PAUSE_MS);
}
}
expect(ctx.last()).toMatchObject({ phase: 'finished', winner: 'bob' });
});
test('myš: pálka rychle dojede k cílové výšce a šipka myš přebije', () => {
const ctx = setup();
ctx.startGame();
ctx.game.aim('alice', 0);
ctx.ticks(5);
expect(ctx.player('alice').y).toBe(PADDLE_HEIGHT / 2);
ctx.game.input('alice', 'down');
ctx.ticks(5);
expect(ctx.player('alice').y).toBeGreaterThan(PADDLE_HEIGHT / 2);
});
test('bota přidá jen zakladatel a lze ho odebrat', () => {
const ctx = setup();
ctx.game.join('alice');
expect(() => ctx.game.addBot('bob', 'easy')).toThrow(GameError);
expect(() => ctx.game.addBot('alice', 'superhard')).toThrow(GameError);
ctx.game.addBot('alice', 'hard');
expect(ctx.last().players.map(p => p.login)).toEqual(['alice', botLogin('hard')]);
ctx.game.removeBot('alice');
expect(ctx.last().players).toHaveLength(1);
});
/** Odehraje zápas člověka, který myší dokonale sleduje míček, proti botovi. */
function playAgainstBot(level: BotLevel, maxSeconds: number) {
const ctx = setup(lcg(7));
ctx.game.join('alice');
ctx.game.addBot('alice', level);
ctx.game.start('alice');
ctx.clock.advance(COUNTDOWN_SECONDS * 1000);
for (let t = 0; t < maxSeconds * 1000 && ctx.last().phase === 'playing'; t += TICK_MS) {
ctx.game.aim('alice', ctx.last().ball.y);
ctx.ticks(1);
}
return ctx;
}
test('lehký bot proti dokonalému hráči prohraje', () => {
const ctx = playAgainstBot('easy', 300);
expect(ctx.last()).toMatchObject({ phase: 'finished', winner: 'alice' });
});
test('nemožný bot neztratí ani bod', () => {
const ctx = playAgainstBot('impossible', 120);
expect(ctx.player('alice').score).toBe(0);
expect(ctx.player(botLogin('impossible')).score).toBe(0);
});
});