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); }); });