import { TetrisGame, TetrisState, GarbageEvent, COUNTDOWN_SECONDS, MIN_ATTACK_GAP_MS, WATCH_FLUSH_MS, BOARD_WIDTH, BOARD_HEIGHT, PLAYER_TIMEOUT_MS } 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() { 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), 1, clock); const last = () => all[all.length - 1]; const startGame = (...logins: string[]) => { logins.forEach(l => game.join(l)); game.start(logins[0]); clock.advance(COUNTDOWN_SECONDS * 1000); }; return { game, clock, all, watched, garbage, last, startGame }; } describe('tetris', () => { 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('do běžící hry se nelze připojit', () => { const ctx = setup(); ctx.startGame('alice'); expect(() => ctx.game.join('bob')).toThrow(GameError); }); });