feat: skryté multiplayerové minihry přes websocket
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
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
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.
This commit is contained in:
28 files changed
+5050
-2
No files matched your search
@@ -0,0 +1,110 @@
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user