feat: vytržení miniher do samostatného projektu
CI / Server unit tests (push) Successful in 26s
CI / Generate TypeScript types (push) Successful in 10s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 2m24s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
CI / Server unit tests (push) Successful in 26s
CI / Generate TypeScript types (push) Successful in 10s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 2m24s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 2s
This commit is contained in:
42 files changed
+136
-8224
No files matched your search
@@ -1,124 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { ReactionDuel, DuelState, PENALTY_MS, CLICK_WINDOW_MS, ROUND_PAUSE_MS, MIN_DELAY_MS, LOBBY_TIMEOUT_MS } from '../reactionDuel';
|
||||
import { GameError } from '../minigames';
|
||||
import { fakeClock } from './helpers/fakeClock';
|
||||
|
||||
function setup(minPlayers = 2) {
|
||||
const clock = fakeClock();
|
||||
const states: DuelState[] = [];
|
||||
const duel = new ReactionDuel(s => states.push(s), minPlayers, clock);
|
||||
return { duel, clock, last: () => states[states.length - 1] };
|
||||
}
|
||||
|
||||
/** Dohraje jedno kolo: počká na vyhlášení a nechá hráče kliknout s danými časy. */
|
||||
function playRound(ctx: ReturnType<typeof setup>, reactions: Record<string, number>) {
|
||||
ctx.clock.advance(MIN_DELAY_MS);
|
||||
expect(ctx.last().phase).toBe('go');
|
||||
const sorted = Object.entries(reactions).sort((a, b) => a[1] - b[1]);
|
||||
let elapsed = 0;
|
||||
for (const [login, ms] of sorted) {
|
||||
ctx.clock.advance(ms - elapsed);
|
||||
elapsed = ms;
|
||||
ctx.duel.click(login);
|
||||
}
|
||||
}
|
||||
|
||||
describe('reakční hra', () => {
|
||||
test('první hráč zakládá lobby a je hostitel', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.duel.join('bob');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'lobby', host: 'alice' });
|
||||
expect(ctx.last().players.map(p => p.login)).toEqual(['alice', 'bob']);
|
||||
});
|
||||
|
||||
test('hru může spustit jen hostitel a s dostatkem hráčů', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
expect(() => ctx.duel.start('alice')).toThrow(GameError);
|
||||
ctx.duel.join('bob');
|
||||
expect(() => ctx.duel.start('bob')).toThrow(GameError);
|
||||
ctx.duel.start('alice');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'waiting', round: 1 });
|
||||
});
|
||||
|
||||
test('do běžící hry se nelze připojit', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.duel.join('bob');
|
||||
ctx.duel.start('alice');
|
||||
expect(() => ctx.duel.join('carol')).toThrow(GameError);
|
||||
});
|
||||
|
||||
test('vyhrává nejnižší součet časů, nejpomalejší je poslední', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.duel.join('bob');
|
||||
ctx.duel.start('alice');
|
||||
playRound(ctx, { alice: 200, bob: 300 });
|
||||
expect(ctx.last()).toMatchObject({ phase: 'roundResult', round: 1 });
|
||||
ctx.clock.advance(ROUND_PAUSE_MS);
|
||||
playRound(ctx, { alice: 250, bob: 220 });
|
||||
ctx.clock.advance(ROUND_PAUSE_MS);
|
||||
playRound(ctx, { alice: 210, bob: 260 });
|
||||
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('finished');
|
||||
expect(state.players.find(p => p.login === 'alice')?.total).toBe(660);
|
||||
expect(state.players.find(p => p.login === 'bob')?.total).toBe(780);
|
||||
expect(state.winner).toBe('alice');
|
||||
expect(state.loser).toBe('bob');
|
||||
});
|
||||
|
||||
test('předčasný start a nekliknutí znamenají trestný čas', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.duel.join('bob');
|
||||
ctx.duel.start('alice');
|
||||
ctx.duel.click('alice'); // před vyhlášením
|
||||
ctx.clock.advance(MIN_DELAY_MS);
|
||||
ctx.clock.advance(CLICK_WINDOW_MS); // bob neklikne
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('roundResult');
|
||||
expect(state.players.find(p => p.login === 'alice')?.rounds[0]).toEqual({ ms: null, falseStart: true });
|
||||
expect(state.players.find(p => p.login === 'bob')?.rounds[0]).toEqual({ ms: null });
|
||||
expect(state.players.every(p => p.total === PENALTY_MS)).toBe(true);
|
||||
});
|
||||
|
||||
test('odchodem hostitele se lobby ruší', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.duel.join('bob');
|
||||
ctx.duel.leave('alice');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'idle', players: [] });
|
||||
});
|
||||
|
||||
test('neodstartované lobby po čase vyprší', () => {
|
||||
const ctx = setup();
|
||||
ctx.duel.join('alice');
|
||||
ctx.clock.advance(LOBBY_TIMEOUT_MS);
|
||||
expect(ctx.last().phase).toBe('idle');
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
import { SnakeGame, SnakeState, TICK_MS, COUNTDOWN_SECONDS, WIDTH, HEIGHT } from '../snakeGame';
|
||||
import { GameError } from '../minigames';
|
||||
import { fakeClock } from './helpers/fakeClock';
|
||||
|
||||
/** Náhoda vracející zadané hodnoty a pak nuly (jídlo pak padá do rohu [0,0]). */
|
||||
const sequence = (...values: number[]) => () => values.length ? values.shift()! : 0;
|
||||
|
||||
/** Hodnoty náhody, které umístí jídlo na danou buňku. */
|
||||
const foodAt = (x: number, y: number) => [x / WIDTH, y / HEIGHT];
|
||||
|
||||
function setup(random?: () => number) {
|
||||
const clock = fakeClock(random);
|
||||
const states: SnakeState[] = [];
|
||||
const game = new SnakeGame(s => states.push(s), s => states.push(s), 1, clock);
|
||||
const last = () => states[states.length - 1];
|
||||
const player = (login: string) => last().players.find(p => p.login === login)!;
|
||||
const startGame = (...logins: string[]) => {
|
||||
logins.forEach(l => game.join(l));
|
||||
game.start(logins[0]);
|
||||
clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
};
|
||||
const ticks = (n: number) => clock.advance(n * TICK_MS);
|
||||
return { game, clock, last, player, startGame, ticks };
|
||||
}
|
||||
|
||||
describe('had', () => {
|
||||
test('po odpočtu začne hra a had se posouvá o políčko za tick', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
expect(ctx.player('alice').body).toEqual([[5, 10], [4, 10], [3, 10]]);
|
||||
ctx.ticks(2);
|
||||
expect(ctx.player('alice').body).toEqual([[7, 10], [6, 10], [5, 10]]);
|
||||
});
|
||||
|
||||
test('zatáčí podle směru a obrat o 180° ignoruje', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
ctx.game.turn('alice', 'left'); // obrat — ignoruje se
|
||||
ctx.ticks(1);
|
||||
expect(ctx.player('alice').body[0]).toEqual([6, 10]);
|
||||
ctx.game.turn('alice', 'up');
|
||||
ctx.ticks(1);
|
||||
expect(ctx.player('alice').body[0]).toEqual([6, 9]);
|
||||
});
|
||||
|
||||
test('nesmyslný směr se ignoruje', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
ctx.game.turn('alice', 'diagonal');
|
||||
ctx.ticks(1);
|
||||
expect(ctx.player('alice').body[0]).toEqual([6, 10]);
|
||||
});
|
||||
|
||||
test('sněděné jídlo prodlouží hada a přidá bod', () => {
|
||||
const ctx = setup(sequence(...foodAt(7, 10)));
|
||||
ctx.startGame('alice');
|
||||
expect(ctx.last().food).toEqual([[7, 10]]);
|
||||
ctx.ticks(2);
|
||||
expect(ctx.player('alice').score).toBe(1);
|
||||
expect(ctx.player('alice').body).toHaveLength(4);
|
||||
expect(ctx.last().food).not.toContainEqual([7, 10]);
|
||||
});
|
||||
|
||||
test('náraz do zdi ukončí sólo hru', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
ctx.ticks(WIDTH);
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
expect(ctx.player('alice')).toMatchObject({ alive: false, place: 1 });
|
||||
});
|
||||
|
||||
test('ve hře více hráčů vyhrává poslední přeživší', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
// alice startuje v řádku 6, nahoru do zdi dorazí v 7. ticku
|
||||
ctx.game.turn('alice', 'up');
|
||||
ctx.ticks(7);
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
expect(ctx.player('bob')).toMatchObject({ alive: true, place: 1 });
|
||||
expect(ctx.player('alice')).toMatchObject({ alive: false, place: 2 });
|
||||
});
|
||||
|
||||
test('čelní srážka zabije oba hady', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
// alice (5,6)→ doprava, bob (26,13)← doleva; po 7 tickách alice zatočí dolů
|
||||
// a po dalších 7 se obě hlavy potkají na (12,13)
|
||||
ctx.ticks(7);
|
||||
ctx.game.turn('alice', 'down');
|
||||
ctx.ticks(7);
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('finished');
|
||||
expect(state.players.every(p => !p.alive)).toBe(true);
|
||||
});
|
||||
|
||||
test('do běžící hry se nelze připojit', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
expect(() => ctx.game.join('bob')).toThrow(GameError);
|
||||
});
|
||||
});
|
||||
@@ -1,451 +0,0 @@
|
||||
import {
|
||||
TankGame, TankState, MAPS, COLS, ROWS, TICK_MS, COUNTDOWN_SECONDS, RESPAWN_MS, DEATHMATCH_MS,
|
||||
LAST_MAN_LIVES, MAX_PLAYERS, TANK_RADIUS, ROCKET_AMMO, MINE_AMMO, MINE_ARM_MS, FIELD_WIDTH,
|
||||
} from '../tankGame';
|
||||
import { GameError } from '../minigames';
|
||||
import { fakeClock } from './helpers/fakeClock';
|
||||
|
||||
/** Deterministická pseudonáhoda pro boty. */
|
||||
function lcg(seed = 1) {
|
||||
let x = seed;
|
||||
return () => (x = (x * 1103515245 + 12345) % 2147483648) / 2147483648;
|
||||
}
|
||||
|
||||
function setup(random?: () => number) {
|
||||
const clock = fakeClock(random);
|
||||
const states: TankState[] = [];
|
||||
const game = new TankGame(s => states.push(s), s => states.push(s), 2, clock);
|
||||
const last = () => states[states.length - 1];
|
||||
const player = (login: string) => last().players.find(p => p.login === login)!;
|
||||
/** Odrážení je ve výchozím stavu vypnuté, testy ho zapínají — sebezničení odraženou střelou je nejjednodušší zásah. */
|
||||
const startGame = (mode: 'deathmatch' | 'lastman' = 'deathmatch', all = ['alice', 'bob'], map = 'arena', teams = false, bounce = true) => {
|
||||
all.forEach(l => game.join(l));
|
||||
game.configure(all[0], { mode, mapId: map, teams, bounce });
|
||||
game.start(all[0]);
|
||||
clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
};
|
||||
const ticks = (n: number) => clock.advance(n * TICK_MS);
|
||||
/** Jeden výstřel: stisk na jeden tick a puštění. */
|
||||
const shoot = (login: string) => {
|
||||
game.input(login, { fire: true });
|
||||
ticks(1);
|
||||
game.input(login, { fire: false });
|
||||
};
|
||||
return { game, clock, last, player, startGame, ticks, shoot };
|
||||
}
|
||||
|
||||
describe('tanky', () => {
|
||||
test('mapy mají správný rozměr, dost spawnů a všechna volná místa jsou propojená', () => {
|
||||
for (const map of MAPS) {
|
||||
expect(map.rows).toHaveLength(ROWS);
|
||||
map.rows.forEach(r => expect(r).toHaveLength(COLS));
|
||||
map.rows.forEach(r => expect(r).toMatch(/^[.#+~:%S]+$/));
|
||||
// Každý tým má na své půlce dost spawnů
|
||||
const leftSpawns = map.rows.join('').split('').filter((c, i) => c === 'S' && i % COLS < COLS / 2).length;
|
||||
expect(leftSpawns).toBeGreaterThanOrEqual(MAX_PLAYERS / 2);
|
||||
// Bedny se dají rozstřílet, takže se počítají jako průjezdné; zeď a voda ne
|
||||
const blocked = (c: string) => c === '#' || c === '~';
|
||||
const open: [number, number][] = [];
|
||||
map.rows.forEach((r, y) => [...r].forEach((c, x) => { if (!blocked(c)) open.push([x, y]); }));
|
||||
const seen = new Set([open[0].join()]);
|
||||
const queue = [open[0]];
|
||||
while (queue.length) {
|
||||
const [x, y] = queue.shift()!;
|
||||
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
const nx = x + dx, ny = y + dy;
|
||||
if (map.rows[ny]?.[nx] === undefined || blocked(map.rows[ny][nx]) || seen.has(`${nx},${ny}`)) continue;
|
||||
seen.add(`${nx},${ny}`);
|
||||
queue.push([nx, ny]);
|
||||
}
|
||||
}
|
||||
expect(seen.size).toBe(open.length);
|
||||
}
|
||||
});
|
||||
|
||||
test('hru spouští zakladatel a potřebuje alespoň dva tanky (boti se počítají)', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
expect(() => ctx.game.start('alice')).toThrow(GameError);
|
||||
expect(() => ctx.game.addBot('bob', 'easy')).toThrow(GameError);
|
||||
ctx.game.addBot('alice', 'easy');
|
||||
expect(ctx.last().players.map(p => p.login)).toContain('🤖 Bot 1 (Lehký)');
|
||||
ctx.game.start('alice');
|
||||
expect(ctx.last().phase).toBe('countdown');
|
||||
expect(() => ctx.game.join('bob')).toThrow(GameError);
|
||||
});
|
||||
|
||||
test('tank jede kupředu a zastaví se o okraj hřiště', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, Math.PI);
|
||||
ctx.game.input('alice', { drive: 1 });
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('alice').x).toBe(TANK_RADIUS);
|
||||
expect(ctx.player('alice').y).toBe(60);
|
||||
});
|
||||
|
||||
test('nesmyslné ovládání se ořízne', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 300, 60, 0);
|
||||
ctx.game.input('alice', { drive: 5, turn: 'x' });
|
||||
ctx.ticks(10);
|
||||
expect(ctx.player('alice').x).toBeGreaterThan(300);
|
||||
expect(ctx.player('alice').angle).toBe(0);
|
||||
});
|
||||
|
||||
test('směrové ovládání jede rovnou zvoleným směrem', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 300, 300, 0);
|
||||
ctx.game.input('alice', { dx: 0, dy: -1 });
|
||||
ctx.ticks(10);
|
||||
const p = ctx.player('alice');
|
||||
expect(p.angle).toBeCloseTo(-Math.PI / 2, 2);
|
||||
expect(p.x).toBeCloseTo(300, 0);
|
||||
expect(p.y).toBeLessThan(300);
|
||||
});
|
||||
|
||||
test('směrové ovládání bez stisknutého směru tank zastaví a nechá natočený', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 300, 300, 0);
|
||||
ctx.game.input('alice', { dx: -1, dy: 0 });
|
||||
ctx.ticks(1);
|
||||
ctx.game.input('alice', { dx: 0, dy: 0 });
|
||||
const { x, angle } = ctx.player('alice');
|
||||
expect(x).toBeLessThan(300);
|
||||
ctx.ticks(5);
|
||||
expect(ctx.player('alice').angle).toBe(angle);
|
||||
expect(ctx.player('alice').x).toBe(x);
|
||||
});
|
||||
|
||||
test('zásah zničí soupeře, přidá bod a soupeř se po chvíli znovu objeví', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 300, 60, 0);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('alice')).toMatchObject({ score: 1, kills: 1 });
|
||||
expect(ctx.player('bob')).toMatchObject({ alive: false, deaths: 1 });
|
||||
ctx.ticks(Math.ceil(RESPAWN_MS / TICK_MS) + 1);
|
||||
expect(ctx.player('bob')).toMatchObject({ alive: true, protected: true });
|
||||
});
|
||||
|
||||
test('věž se natočí za myší nezávisle na korbě a střílí tím směrem', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 60, 60, 0);
|
||||
ctx.game.placeForTest('bob', 60, 250, 0);
|
||||
ctx.game.aimAt('alice', { x: 60, y: 250 });
|
||||
ctx.ticks(10);
|
||||
expect(ctx.player('alice').turret).toBeCloseTo(Math.PI / 2, 1);
|
||||
expect(ctx.player('alice').angle).toBe(0);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('bob')).toMatchObject({ alive: false });
|
||||
expect(ctx.player('alice').kills).toBe(1);
|
||||
// Bez myši se věž vrátí do směru jízdy
|
||||
ctx.game.aimAt('alice', { x: null });
|
||||
ctx.ticks(10);
|
||||
expect(ctx.player('alice').turret).toBeCloseTo(0, 1);
|
||||
});
|
||||
|
||||
test('ve výchozím stavu je režim poslední přeživší a střela na zdi zanikne', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.join('bob');
|
||||
expect(ctx.last()).toMatchObject({ mode: 'lastman', bounce: false });
|
||||
ctx.game.configure('alice', { mapId: 'arena' });
|
||||
ctx.game.start('alice');
|
||||
ctx.clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
ctx.game.placeForTest('alice', 100, 60, -Math.PI / 2);
|
||||
ctx.game.placeForTest('bob', 900, 500, 0);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(20);
|
||||
expect(ctx.player('alice')).toMatchObject({ alive: true, lives: LAST_MAN_LIVES });
|
||||
expect(ctx.last().bullets).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('odražená vlastní střela zničí i střelce a stojí bod', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, -Math.PI / 2);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(20);
|
||||
expect(ctx.player('alice')).toMatchObject({ alive: false, score: -1, kills: 0 });
|
||||
});
|
||||
|
||||
test('střela rozbije bednu a zanikne', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 380, 20, 0);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(10);
|
||||
expect(ctx.last().tiles[0][11]).toBe('.');
|
||||
expect(ctx.last().tiles[0][12]).toBe('+');
|
||||
expect(ctx.last().bullets).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('výstřel s hlavní v bedně ji rozbije rovnou', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 425, 20, 0);
|
||||
ctx.shoot('alice');
|
||||
expect(ctx.last().tiles[0][11]).toBe('.');
|
||||
expect(ctx.last().bullets).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('na přežití vypadne, kdo přijde o všechny životy, a hra skončí', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('lastman');
|
||||
expect(ctx.player('bob').lives).toBe(LAST_MAN_LIVES);
|
||||
for (let i = 0; i < LAST_MAN_LIVES; i++) {
|
||||
ctx.game.placeForTest('alice', 900, 300, 0);
|
||||
ctx.game.placeForTest('bob', 100, 60, -Math.PI / 2);
|
||||
ctx.shoot('bob');
|
||||
ctx.ticks(20);
|
||||
if (i < LAST_MAN_LIVES - 1) ctx.ticks(Math.ceil(RESPAWN_MS / TICK_MS) + 1);
|
||||
}
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
expect(ctx.player('alice').place).toBe(1);
|
||||
expect(ctx.player('bob')).toMatchObject({ place: 2, lives: 0 });
|
||||
});
|
||||
|
||||
test('hra na čas skončí po vypršení limitu', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.clock.advance(DEATHMATCH_MS + 100);
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
expect(ctx.last().players.every(p => p.place !== undefined)).toBe(true);
|
||||
});
|
||||
|
||||
test('boti hrají mezi sebou a navzájem se ničí', () => {
|
||||
const ctx = setup(lcg(7));
|
||||
ctx.game.join('alice');
|
||||
ctx.game.configure('alice', { mode: 'deathmatch', mapId: 'maze' });
|
||||
ctx.game.addBot('alice', 'hard');
|
||||
ctx.game.addBot('alice', 'medium');
|
||||
ctx.game.start('alice');
|
||||
ctx.clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
ctx.clock.advance(60 * 1000);
|
||||
const bots = ctx.last().players.filter(p => p.bot);
|
||||
expect(bots.reduce((sum, b) => sum + b.kills, 0)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tanky — bonusy', () => {
|
||||
/** Položí bonus pod tank a nechá ho sebrat. */
|
||||
const pickUp = (ctx: ReturnType<typeof setup>, login: string, type: Parameters<TankGame['addPickupForTest']>[2]) => {
|
||||
ctx.ticks(1); // aktuální poloha po placeForTest
|
||||
const p = ctx.player(login);
|
||||
ctx.game.addPickupForTest(p.x, p.y, type);
|
||||
ctx.ticks(1);
|
||||
};
|
||||
|
||||
test('raketa vybuchne při zásahu, zničí tank a zbyde po něm hořící vrak', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 300, 60, 0);
|
||||
pickUp(ctx, 'alice', 'rocket');
|
||||
expect(ctx.player('alice').rockets).toBe(ROCKET_AMMO);
|
||||
ctx.shoot('alice');
|
||||
expect(ctx.last().bullets[0]).toMatchObject({ rocket: true });
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('alice')).toMatchObject({ rockets: ROCKET_AMMO - 1, kills: 1, shots: 1 });
|
||||
expect(ctx.player('bob').alive).toBe(false);
|
||||
expect(ctx.last().wrecks).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('výbuch rakety rozbije i sousední bedny', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 380, 20, 0);
|
||||
ctx.game.placeForTest('bob', 900, 500, 0);
|
||||
pickUp(ctx, 'alice', 'rocket');
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(10);
|
||||
expect(ctx.last().tiles[0].slice(11, 13)).toBe('..');
|
||||
expect(ctx.last().explosions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('mina se aktivuje až po chvíli a vybuchne pod soupeřem', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 500, 300, 0);
|
||||
pickUp(ctx, 'alice', 'mine');
|
||||
ctx.game.dropMine('alice');
|
||||
ctx.ticks(1);
|
||||
expect(ctx.player('alice').mines).toBe(MINE_AMMO - 1);
|
||||
expect(ctx.last().mines[0]).toMatchObject({ x: 80, y: 60, armed: false });
|
||||
ctx.game.placeForTest('alice', 500, 500, 0);
|
||||
ctx.ticks(Math.ceil(MINE_ARM_MS / TICK_MS));
|
||||
ctx.game.placeForTest('bob', 200, 60, Math.PI);
|
||||
ctx.game.input('bob', { drive: 1 });
|
||||
ctx.ticks(40);
|
||||
expect(ctx.player('bob').alive).toBe(false);
|
||||
expect(ctx.player('alice').kills).toBe(1);
|
||||
expect(ctx.last().mines).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('štít pohltí jeden zásah', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 300, 60, 0);
|
||||
pickUp(ctx, 'bob', 'shield');
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('bob')).toMatchObject({ alive: true, shield: false });
|
||||
});
|
||||
|
||||
test('turbo zrychlí jízdu', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 100, 300, 0);
|
||||
pickUp(ctx, 'alice', 'turbo');
|
||||
ctx.game.input('alice', { drive: 1 });
|
||||
ctx.game.input('bob', { drive: 1 });
|
||||
ctx.ticks(20);
|
||||
expect(ctx.player('alice').turbo).toBe(true);
|
||||
expect(ctx.player('alice').x - 100).toBeGreaterThan((ctx.player('bob').x - 100) * 1.4);
|
||||
});
|
||||
|
||||
test('naváděná střela se stočí k soupeři', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame();
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('bob', 300, 130, 0);
|
||||
pickUp(ctx, 'alice', 'homing');
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(40);
|
||||
expect(ctx.player('bob').alive).toBe(false);
|
||||
});
|
||||
|
||||
test('život navíc se ve hře na přežití vrátí, ale nepřesáhne maximum', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('lastman');
|
||||
ctx.game.placeForTest('alice', 100, 60, -Math.PI / 2);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(20);
|
||||
expect(ctx.player('alice').lives).toBe(LAST_MAN_LIVES - 1);
|
||||
ctx.ticks(Math.ceil(RESPAWN_MS / TICK_MS) + 1);
|
||||
pickUp(ctx, 'alice', 'heart');
|
||||
pickUp(ctx, 'alice', 'heart');
|
||||
expect(ctx.player('alice').lives).toBe(LAST_MAN_LIVES);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tanky — terén', () => {
|
||||
test('voda tank zastaví, ale střela ji přeletí', () => {
|
||||
const ctx = setup();
|
||||
// Les: řádek 7 (y 280–320) je řeka, v prvním sloupci bez brodu
|
||||
ctx.startGame('deathmatch', ['alice', 'bob'], 'forest');
|
||||
ctx.game.placeForTest('alice', 20, 240, Math.PI / 2);
|
||||
ctx.game.placeForTest('bob', 20, 380, 0);
|
||||
ctx.game.input('alice', { drive: 1 });
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('alice').y).toBeLessThanOrEqual(280 - TANK_RADIUS);
|
||||
ctx.game.input('alice', { drive: 0 });
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(20);
|
||||
expect(ctx.player('bob').alive).toBe(false);
|
||||
});
|
||||
|
||||
test('porost zpomaluje jízdu', () => {
|
||||
const ctx = setup();
|
||||
// Les: v prvním řádku jsou sloupce 3–4 (x 120–200) porost, třetí řádek vlevo je volný
|
||||
ctx.startGame('deathmatch', ['alice', 'bob'], 'forest');
|
||||
ctx.game.placeForTest('alice', 130, 20, 0);
|
||||
ctx.game.placeForTest('bob', 20, 100, 0);
|
||||
ctx.game.input('alice', { drive: 1 });
|
||||
ctx.game.input('bob', { drive: 1 });
|
||||
ctx.ticks(10);
|
||||
expect(ctx.player('alice').x - 130).toBeLessThan((ctx.player('bob').x - 20) * 0.7);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tanky — hardcore (tma)', () => {
|
||||
/** alice a těžký bot ve volném druhém řádku arény, bot natočený od alice. */
|
||||
function duel(hardcore: boolean, distance: number) {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.configure('alice', { mode: 'deathmatch', mapId: 'arena', hardcore });
|
||||
ctx.game.addBot('alice', 'hard');
|
||||
ctx.game.start('alice');
|
||||
ctx.clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
const bot = ctx.last().players.find(p => p.bot)!.login;
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest(bot, 100 + distance, 60, 0);
|
||||
ctx.ticks(30);
|
||||
return { ...ctx, bot };
|
||||
}
|
||||
|
||||
test('zakladatel ho zapne v lobby', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.configure('alice', { hardcore: true });
|
||||
expect(ctx.last().hardcore).toBe(true);
|
||||
});
|
||||
|
||||
test('bot ve tmě nevidí soupeře mimo světlo, bez tmy ano', () => {
|
||||
expect(duel(true, 600).last().bullets).toHaveLength(0);
|
||||
const lit = duel(false, 600);
|
||||
expect(lit.last().bullets.length > 0 || !lit.player('alice').alive).toBe(true);
|
||||
});
|
||||
|
||||
test('bot ve tmě vidí soupeře ve svém okolí', () => {
|
||||
const ctx = duel(true, 110);
|
||||
expect(ctx.last().bullets.length > 0 || !ctx.player('alice').alive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tanky — týmy', () => {
|
||||
const LOGINS = ['alice', 'bob', 'carol', 'dave'];
|
||||
|
||||
test('hráči se střídavě rozdělí do týmů a jde se přesunout', () => {
|
||||
const ctx = setup();
|
||||
LOGINS.forEach(l => ctx.game.join(l));
|
||||
ctx.game.configure('alice', { teams: true });
|
||||
expect(ctx.last().players.map(p => p.team)).toEqual([0, 1, 0, 1]);
|
||||
expect(() => ctx.game.switchTeam('bob', 'carol')).toThrow(GameError);
|
||||
ctx.game.switchTeam('bob');
|
||||
expect(ctx.player('bob').team).toBe(0);
|
||||
ctx.game.switchTeam('alice', 'bob');
|
||||
expect(ctx.player('bob').team).toBe(1);
|
||||
});
|
||||
|
||||
test('týmy začínají každý na své půlce a střela spoluhráčem proletí', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('deathmatch', LOGINS, 'arena', true);
|
||||
for (const p of ctx.last().players) {
|
||||
expect(p.x < FIELD_WIDTH / 2).toBe(p.team === 0);
|
||||
}
|
||||
// alice a carol jsou tým 0, bob tým 1
|
||||
ctx.game.placeForTest('alice', 100, 60, 0);
|
||||
ctx.game.placeForTest('carol', 200, 60, 0);
|
||||
ctx.game.placeForTest('bob', 300, 60, 0);
|
||||
ctx.shoot('alice');
|
||||
ctx.ticks(30);
|
||||
expect(ctx.player('carol').alive).toBe(true);
|
||||
expect(ctx.player('bob').alive).toBe(false);
|
||||
expect(ctx.last().teamScores).toEqual([1, 0]);
|
||||
});
|
||||
|
||||
test('na čas vyhrává tým s víc body', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('deathmatch', LOGINS, 'arena', true);
|
||||
ctx.game.placeForTest('bob', 100, 60, 0);
|
||||
ctx.game.placeForTest('alice', 300, 60, 0);
|
||||
ctx.shoot('bob');
|
||||
ctx.clock.advance(DEATHMATCH_MS);
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('finished');
|
||||
expect(state.winnerTeam).toBe(1);
|
||||
expect(state.players.filter(p => p.place! <= 2).every(p => p.team === 1)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
import { TetrisGame, TetrisState, TetrisSettings, GarbageEvent, COUNTDOWN_SECONDS, JOIN_GRACE_SECONDS, MIN_ATTACK_GAP_MS, WATCH_FLUSH_MS, BOARD_WIDTH, BOARD_HEIGHT, PLAYER_TIMEOUT_MS, DEFAULT_SETTINGS } from '../tetrisGame';
|
||||
import { GameError } from '../minigames';
|
||||
import { fakeClock } from './helpers/fakeClock';
|
||||
|
||||
const BOARD = '0'.repeat(BOARD_WIDTH * (BOARD_HEIGHT - 1)) + '1'.repeat(BOARD_WIDTH);
|
||||
|
||||
function setup(minPlayers = 1) {
|
||||
const clock = fakeClock();
|
||||
const all: TetrisState[] = [];
|
||||
const watched: TetrisState[] = [];
|
||||
const garbage: GarbageEvent[] = [];
|
||||
const game = new TetrisGame(s => all.push(s), s => watched.push(s), e => garbage.push(e), minPlayers, clock);
|
||||
const last = () => all[all.length - 1];
|
||||
const startWith = (settings: Partial<TetrisSettings>, ...logins: string[]) => {
|
||||
logins.forEach(l => game.join(l));
|
||||
game.configure(logins[0], settings);
|
||||
game.start(logins[0]);
|
||||
clock.advance(COUNTDOWN_SECONDS * 1000);
|
||||
};
|
||||
const startGame = (...logins: string[]) => startWith({}, ...logins);
|
||||
return { game, clock, all, watched, garbage, last, startGame, startWith };
|
||||
}
|
||||
|
||||
describe('tetris', () => {
|
||||
test('společnou hru nejde spustit jen s jedním hráčem', () => {
|
||||
const ctx = setup(2);
|
||||
ctx.game.join('alice');
|
||||
expect(() => ctx.game.start('alice')).toThrow(GameError);
|
||||
expect(ctx.last()).toMatchObject({ phase: 'lobby' });
|
||||
ctx.game.join('bob');
|
||||
ctx.game.start('alice');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'countdown' });
|
||||
});
|
||||
|
||||
test('po odpočtu začne hra se společným seedem', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'playing' });
|
||||
expect(typeof ctx.last().seed).toBe('number');
|
||||
});
|
||||
|
||||
test('desky se sledujícím posílají sloučeně', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
ctx.game.update('alice', { board: BOARD, score: 100, lines: 1 });
|
||||
ctx.game.update('bob', { board: BOARD, score: 40, lines: 0 });
|
||||
expect(ctx.watched).toHaveLength(0);
|
||||
ctx.clock.advance(WATCH_FLUSH_MS);
|
||||
expect(ctx.watched).toHaveLength(1);
|
||||
expect(ctx.watched[0].players.find(p => p.login === 'alice')).toMatchObject({ board: BOARD, score: 100, lines: 1 });
|
||||
});
|
||||
|
||||
test('neplatná deska se zahodí', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
ctx.game.update('alice', { board: 'x'.repeat(200), score: 5 });
|
||||
ctx.game.update('alice', { board: '0'.repeat(10) });
|
||||
ctx.clock.advance(WATCH_FLUSH_MS);
|
||||
expect(ctx.watched).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('smazání více řádků pošle odpad soupeři, jeden řádek nic', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
ctx.game.cleared('alice', 1);
|
||||
expect(ctx.garbage).toHaveLength(0);
|
||||
ctx.game.cleared('alice', 4);
|
||||
expect(ctx.garbage).toEqual([{ target: 'bob', from: 'alice', lines: 4 }]);
|
||||
});
|
||||
|
||||
test('útoky jsou omezené frekvencí a velikostí', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
ctx.game.cleared('alice', 2);
|
||||
ctx.game.cleared('alice', 2); // příliš brzy
|
||||
ctx.game.cleared('alice', 99); // nesmysl
|
||||
expect(ctx.garbage).toHaveLength(1);
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 3);
|
||||
expect(ctx.garbage.map(g => g.lines)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('útok jde soupeři, který dostal nejméně odpadu', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob', 'carol', 'dave');
|
||||
ctx.game.cleared('alice', 4); // všichni 0 → první z nich (random = 0)
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 2);
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 3);
|
||||
ctx.game.cleared('bob', 2); // carol 1, dave 2, alice 0 → alice
|
||||
ctx.game.cleared('carol', 2); // alice 1, bob 4, dave 2 → alice
|
||||
expect(ctx.garbage.map(g => g.target)).toEqual(['bob', 'carol', 'dave', 'alice', 'alice']);
|
||||
});
|
||||
|
||||
test('odražený odpad zmenší útok, víc než tabulka poslat nejde', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
ctx.game.cleared('alice', 4, 1);
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 2, 0); // vše odraženo → žádný útok
|
||||
ctx.game.cleared('alice', 2, 50);
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 3, -5);
|
||||
expect(ctx.garbage.map(g => g.lines)).toEqual([1, 1]);
|
||||
});
|
||||
|
||||
test('poslední hráč ve hře vyhrává', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob', 'carol');
|
||||
ctx.game.died('bob');
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
ctx.game.died('alice');
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('finished');
|
||||
const place = (l: string) => state.players.find(p => p.login === l)?.place;
|
||||
expect([place('carol'), place('alice'), place('bob')]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('mrtvý hráč neútočí a nedostává odpad', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob', 'carol');
|
||||
ctx.game.died('bob');
|
||||
ctx.game.cleared('bob', 4);
|
||||
expect(ctx.garbage).toHaveLength(0);
|
||||
ctx.game.cleared('alice', 4);
|
||||
expect(ctx.garbage[0].target).toBe('carol');
|
||||
});
|
||||
|
||||
test('hráč, který přestal posílat desku, je vyřazen', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice', 'bob');
|
||||
// alice hraje dál, bob zavřel okno
|
||||
for (let t = 0; t <= PLAYER_TIMEOUT_MS + 5000; t += 2000) {
|
||||
ctx.game.update('alice', { board: BOARD, score: 0, lines: 0 });
|
||||
ctx.clock.advance(2000);
|
||||
}
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
expect(ctx.last().players.find(p => p.login === 'alice')?.place).toBe(1);
|
||||
});
|
||||
|
||||
test('nastavení mění jen zakladatel v lobby, neplatné hodnoty se ignorují', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.join('bob');
|
||||
expect(ctx.last().settings).toEqual(DEFAULT_SETTINGS);
|
||||
expect(() => ctx.game.configure('bob', { targeting: 'random' })).toThrow(GameError);
|
||||
ctx.game.configure('alice', { targeting: 'payback', winMode: 'nesmysl', timeLimit: 7 });
|
||||
expect(ctx.last().settings).toEqual({ ...DEFAULT_SETTINGS, targeting: 'payback' });
|
||||
ctx.game.start('alice');
|
||||
expect(() => ctx.game.configure('alice', { winMode: 'score' })).toThrow(GameError);
|
||||
});
|
||||
|
||||
test('random míří náhodně bez ohledu na obdržený odpad', () => {
|
||||
const ctx = setup();
|
||||
ctx.startWith({ targeting: 'random' }, 'alice', 'bob', 'carol');
|
||||
ctx.game.cleared('alice', 4);
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 4); // random = 0 → pořád první soupeř
|
||||
expect(ctx.garbage.map(g => g.target)).toEqual(['bob', 'bob']);
|
||||
});
|
||||
|
||||
test('payback vrací útok poslednímu útočníkovi, jinak míří jako even', () => {
|
||||
const ctx = setup();
|
||||
ctx.startWith({ targeting: 'payback' }, 'alice', 'bob', 'carol');
|
||||
ctx.game.cleared('carol', 2); // carol nikdo nenapadl → even → alice
|
||||
ctx.game.cleared('alice', 2); // alice napadla carol → carol
|
||||
ctx.game.cleared('bob', 2); // bob nikdo nenapadl → even (alice 1, carol 1) → alice
|
||||
ctx.clock.advance(MIN_ATTACK_GAP_MS);
|
||||
ctx.game.cleared('alice', 2); // naposledy ji napadl bob → bob
|
||||
ctx.game.died('bob');
|
||||
ctx.game.cleared('bob', 2); // mrtvý neútočí
|
||||
ctx.game.cleared('carol', 2); // naposledy ji napadla alice → alice
|
||||
expect(ctx.garbage.map(g => `${g.from}>${g.target}`)).toEqual(['carol>alice', 'alice>carol', 'bob>alice', 'alice>bob', 'carol>alice']);
|
||||
});
|
||||
|
||||
test('hra na skóre končí časovým limitem a pořadí je podle skóre', () => {
|
||||
const ctx = setup();
|
||||
ctx.startWith({ winMode: 'score', timeLimit: 120 }, 'alice', 'bob', 'carol');
|
||||
expect(ctx.last().remainingMs).toBe(120_000);
|
||||
ctx.game.update('bob', { board: BOARD, score: 900, lines: 8 });
|
||||
ctx.game.died('bob');
|
||||
ctx.game.died('alice'); // zbývá jediný hráč, ale hraje se dál
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
ctx.game.update('alice', { board: BOARD, score: 100, lines: 1 }); // mrtvému se skóre už nemění
|
||||
ctx.game.update('carol', { board: BOARD, score: 500, lines: 4 });
|
||||
ctx.clock.advance(120_000);
|
||||
const state = ctx.last();
|
||||
expect(state.phase).toBe('finished');
|
||||
const place = (l: string) => state.players.find(p => p.login === l)?.place;
|
||||
expect([place('bob'), place('carol'), place('alice')]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('hra na skóre skončí dřív, když vypadnou všichni', () => {
|
||||
const ctx = setup();
|
||||
ctx.startWith({ winMode: 'score' }, 'alice', 'bob');
|
||||
ctx.game.died('alice');
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
ctx.game.died('bob');
|
||||
expect(ctx.last().phase).toBe('finished');
|
||||
});
|
||||
|
||||
test('do běžící hry se nelze připojit', () => {
|
||||
const ctx = setup();
|
||||
ctx.startGame('alice');
|
||||
expect(() => ctx.game.join('bob')).toThrow(GameError);
|
||||
});
|
||||
|
||||
test('během odpočtu se lze ještě připojit', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.start('alice');
|
||||
ctx.clock.advance(1000);
|
||||
ctx.game.join('bob');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'countdown', countdown: COUNTDOWN_SECONDS - 1 });
|
||||
expect(ctx.last().players.map(p => p.login)).toEqual(['alice', 'bob']);
|
||||
ctx.clock.advance((COUNTDOWN_SECONDS - 1) * 1000);
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
});
|
||||
|
||||
test('připojení na poslední chvíli prodlouží odpočet', () => {
|
||||
const ctx = setup();
|
||||
ctx.game.join('alice');
|
||||
ctx.game.start('alice');
|
||||
ctx.clock.advance((COUNTDOWN_SECONDS - 1) * 1000);
|
||||
ctx.game.join('bob');
|
||||
expect(ctx.last()).toMatchObject({ phase: 'countdown', countdown: JOIN_GRACE_SECONDS });
|
||||
ctx.clock.advance((JOIN_GRACE_SECONDS - 1) * 1000);
|
||||
expect(ctx.last().phase).toBe('countdown');
|
||||
ctx.clock.advance(1000);
|
||||
expect(ctx.last().phase).toBe('playing');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user