fix: možnost připojit se ke hře Tetris během odpočtu před startem
This commit is contained in:
3 files changed
+62
-17
No files matched your search
@@ -318,7 +318,9 @@ export default function TetrisGame() {
|
||||
if (!login) return null;
|
||||
|
||||
const session = sessionRef.current;
|
||||
const showInvite = !open && phase === 'lobby' && !isPlayer;
|
||||
// Připojit se jde i během odpočtu před startem
|
||||
const joinable = phase === 'lobby' || phase === 'countdown';
|
||||
const showInvite = !open && joinable && !isPlayer;
|
||||
const showReturn = !open && isPlayer && inGame;
|
||||
const others = practice ? [] : (state?.players ?? []).filter(p => !(isPlayer && p.login === login));
|
||||
const ranked = [...(state?.players ?? [])].sort((a, b) => (a.place ?? 99) - (b.place ?? 99));
|
||||
@@ -433,6 +435,16 @@ export default function TetrisGame() {
|
||||
</>
|
||||
);
|
||||
case 'countdown':
|
||||
return (
|
||||
<>
|
||||
{!isPlayer && (
|
||||
<p className="text-center">
|
||||
Hra začne za <strong>{state?.countdown} s</strong> — ještě se můžete připojit.
|
||||
</p>
|
||||
)}
|
||||
{renderPlaying()}
|
||||
</>
|
||||
);
|
||||
case 'playing':
|
||||
return renderPlaying();
|
||||
case 'finished':
|
||||
@@ -482,17 +494,16 @@ export default function TetrisGame() {
|
||||
if (phase === 'idle' || phase === 'finished') {
|
||||
buttons.push(<Button key="new" onClick={() => send('tetris:join')}>{phase === 'idle' ? 'Založit lobby' : 'Nová hra'}</Button>);
|
||||
}
|
||||
if (phase === 'lobby') {
|
||||
if (!isPlayer) {
|
||||
buttons.push(<Button key="join" onClick={() => send('tetris:join')}>Připojit se</Button>);
|
||||
} else {
|
||||
buttons.push(<Button key="leave" variant="outline-secondary" onClick={() => send('tetris:leave')}>
|
||||
{login === state?.host ? 'Zrušit lobby' : 'Odejít'}
|
||||
</Button>);
|
||||
if (login === state?.host) {
|
||||
buttons.push(<Button key="start" onClick={() => send('tetris:start')}
|
||||
disabled={(state?.players.length ?? 0) < (state?.minPlayers ?? 1)}>Spustit hru</Button>);
|
||||
}
|
||||
if (joinable && !isPlayer) {
|
||||
buttons.push(<Button key="join" onClick={() => send('tetris:join')}>Připojit se</Button>);
|
||||
}
|
||||
if (phase === 'lobby' && isPlayer) {
|
||||
buttons.push(<Button key="leave" variant="outline-secondary" onClick={() => send('tetris:leave')}>
|
||||
{login === state?.host ? 'Zrušit lobby' : 'Odejít'}
|
||||
</Button>);
|
||||
if (login === state?.host) {
|
||||
buttons.push(<Button key="start" onClick={() => send('tetris:start')}
|
||||
disabled={(state?.players.length ?? 0) < (state?.minPlayers ?? 1)}>Spustit hru</Button>);
|
||||
}
|
||||
}
|
||||
buttons.push(<Button key="close" variant="secondary" onClick={() => setOpen(false)}>Zavřít</Button>);
|
||||
@@ -503,7 +514,9 @@ export default function TetrisGame() {
|
||||
<>
|
||||
{showInvite && (
|
||||
<button className="duel-invite tetris-invite" onClick={() => setOpen(true)}>
|
||||
{icon} <strong>{state?.host}</strong> zve na Tetris — připojit se
|
||||
{icon} {phase === 'countdown'
|
||||
? <>Tetris začíná za <strong>{state?.countdown} s</strong> — připojit se</>
|
||||
: <><strong>{state?.host}</strong> zve na Tetris — připojit se</>}
|
||||
</button>
|
||||
)}
|
||||
{showReturn && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TetrisGame, TetrisState, TetrisSettings, GarbageEvent, COUNTDOWN_SECONDS, MIN_ATTACK_GAP_MS, WATCH_FLUSH_MS, BOARD_WIDTH, BOARD_HEIGHT, PLAYER_TIMEOUT_MS, DEFAULT_SETTINGS } from '../tetrisGame';
|
||||
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';
|
||||
|
||||
@@ -195,4 +195,29 @@ describe('tetris', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { Clock, defaultClock, GameError, gameHandler } from "./minigames";
|
||||
* Multiplayerový Tetris (battle) přes websocket.
|
||||
*
|
||||
* Každý hráč hraje vlastní desku v prohlížeči (kvůli odezvě ovládání), server koordinuje:
|
||||
* lobby a odpočet, společný seed pro pořadí kostek, přeposílání desek soupeřům,
|
||||
* lobby a odpočet (během něj se ještě dá připojit), společný seed pro pořadí kostek, přeposílání desek soupeřům,
|
||||
* útoky (smazané řádky → odpadní řádky soupeři podle zvoleného režimu míření) a vyřazování.
|
||||
* Zakladatel lobby volí režim míření a způsob hodnocení: buď vyhrává poslední hráč ve hře
|
||||
* (sólo hra končí prohrou), nebo nejvyšší skóre po uplynutí časového limitu.
|
||||
@@ -17,7 +17,10 @@ import { Clock, defaultClock, GameError, gameHandler } from "./minigames";
|
||||
export const BOARD_WIDTH = 10;
|
||||
export const BOARD_HEIGHT = 20;
|
||||
export const MAX_PLAYERS = 8;
|
||||
export const COUNTDOWN_SECONDS = 3;
|
||||
/** Odpočet po spuštění hry — během něj se ještě mohou připojit opozdilci. */
|
||||
export const COUNTDOWN_SECONDS = 5;
|
||||
/** Kolik sekund odpočtu má nejméně zbývat po připojení dalšího hráče, ať se stihne připravit. */
|
||||
export const JOIN_GRACE_SECONDS = 3;
|
||||
/** Neodstartované lobby se po této době zruší (ms). */
|
||||
export const LOBBY_TIMEOUT_MS = 3 * 60 * 1000;
|
||||
/** Pojistka proti nekonečné hře (ms). */
|
||||
@@ -147,7 +150,7 @@ export class TetrisGame {
|
||||
this.phase = 'lobby';
|
||||
this.host = login;
|
||||
this.schedule(() => this.cancel(), LOBBY_TIMEOUT_MS);
|
||||
} else if (this.phase !== 'lobby') {
|
||||
} else if (this.phase !== 'lobby' && this.phase !== 'countdown') {
|
||||
throw new GameError('Hra už běží, počkejte na další.');
|
||||
}
|
||||
if (!this.players.has(login)) {
|
||||
@@ -155,6 +158,10 @@ export class TetrisGame {
|
||||
throw new GameError('Lobby je plné.');
|
||||
}
|
||||
this.players.set(login, { login, board: EMPTY_BOARD, score: 0, lines: 0, alive: true, lastAttack: 0, lastSeen: 0, garbageReceived: 0 });
|
||||
// Opozdilec připojený na poslední chvíli dostane čas se připravit (počet hráčů je omezený, odpočet se neprodlužuje donekonečna)
|
||||
if (this.phase === 'countdown' && this.countdown < JOIN_GRACE_SECONDS) {
|
||||
this.countdown = JOIN_GRACE_SECONDS;
|
||||
}
|
||||
}
|
||||
this.emitAll();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user