fix rusenie hier ked vsetkych odpoji

This commit is contained in:
tim
2026-07-03 19:16:41 +02:00
parent c4800fca0e
commit 7886b3a6b8
3 changed files with 51 additions and 12 deletions
+29 -10
View File
@@ -1,3 +1,4 @@
import asyncio
import hmac import hmac
import json import json
import os import os
@@ -273,19 +274,37 @@ async def send_error(sid: str, message: str):
await sio.emit("error", {"error": message}, to=sid) await sio.emit("error", {"error": message}, to=sid)
# Ako dlho prezije nezacata hra, ked su vsetci hraci naraz offline. Na mobile
# sa socket bezne strati uz pri zamknuti obrazovky, takze okamzite zmazanie
# hry rusilo lobby, v ktorom hraci len cakali so zhasnutym telefonom.
LOBBY_ABANDON_GRACE_SECONDS = 10 * 60
async def _cleanup_abandoned_lobby(gid: str):
"""Po grace periode zmaz nezacatu hru, ak sa medzitym nikto nevratil.
Podmienka sa overuje az po uplynuti casu, takze pri navrate hraca je
task neskodny no-op (netreba nic rusit)."""
await asyncio.sleep(LOBBY_ABANDON_GRACE_SECONDS)
game = games.get(gid)
if game is not None and not game.started and not any(p.connected for p in game.players):
del games[gid]
await broadcast_lobby()
async def _mark_player_offline(game: "Game", player: "Player"): async def _mark_player_offline(game: "Game", player: "Player"):
"""Mark player disconnected. An unstarted game with nobody left is cleaned """Mark player disconnected. An unstarted game with nobody left gets a
up; a started game is kept in memory so it stays in the lobby and can be delayed cleanup (mobile sockets drop on screen lock, so an immediate
resumed (it's torn down only by end_game).""" delete would kill lobbies where everyone is just waiting); a started game
is kept in memory so it stays in the lobby and can be resumed (it's torn
down only by end_game)."""
player.connected = False player.connected = False
if not any(p.connected for p in game.players) and not game.started: if not any(p.connected for p in game.players) and not game.started:
del games[game.gid] asyncio.create_task(_cleanup_abandoned_lobby(game.gid))
else: await sio.emit(
await sio.emit( "player_connection",
"player_connection", {"order": player.order, "connected": False},
{"order": player.order, "connected": False}, room=game.gid,
room=game.gid, )
)
def _active_game(sid: str) -> "tuple[Game, dict] | None": def _active_game(sid: str) -> "tuple[Game, dict] | None":
+3
View File
@@ -132,6 +132,9 @@ export function setupSocketListeners() {
// Surface connection failures instead of silently buffering emits. // Surface connection failures instead of silently buffering emits.
socket.on('connect_error', (err: Error) => { socket.on('connect_error', (err: Error) => {
// `active` = klient sa automaticky pokusi znova (typicky prebudeny mobil,
// kym sa siet zobudi) -- prechodne, netreba strasit toastom.
if (socket.active) return;
useGameStore.getState().setError(`Spojenie so serverom zlyhalo: ${err.message}`); useGameStore.getState().setError(`Spojenie so serverom zlyhalo: ${err.message}`);
}); });
} }
+19 -2
View File
@@ -1,3 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { useGameStore } from '../store/gameStore'; import { useGameStore } from '../store/gameStore';
import { emit } from '../lib/socket'; import { emit } from '../lib/socket';
@@ -8,6 +9,8 @@ export default function Lobby() {
const navigate = useNavigate(); const navigate = useNavigate();
const myPlayer = useGameStore((s) => s.myPlayer); const myPlayer = useGameStore((s) => s.myPlayer);
const games = useGameStore((s) => s.games); const games = useGameStore((s) => s.games);
const [showCopied, setShowCopied] = useState(false);
const copiedTimeout = useRef<ReturnType<typeof setTimeout>>();
const game = games.find((g) => g.gid === gid); const game = games.find((g) => g.gid === gid);
const players = game?.players ?? []; const players = game?.players ?? [];
@@ -17,11 +20,25 @@ export default function Lobby() {
const handleLeave = () => leaveGame(navigate); const handleLeave = () => leaveGame(navigate);
const handleCopyCode = () => { const handleCopyCode = () => {
if (gid) navigator.clipboard.writeText(`${window.location.origin}/lobby/${gid}`); if (!gid) return;
navigator.clipboard.writeText(`${window.location.origin}/lobby/${gid}`);
setShowCopied(true);
clearTimeout(copiedTimeout.current);
copiedTimeout.current = setTimeout(() => setShowCopied(false), 1800);
}; };
useEffect(() => () => clearTimeout(copiedTimeout.current), []);
return ( return (
<div className="max-w-md mx-auto p-4 pt-8 min-h-screen"> <div className="max-w-md mx-auto p-4 pt-8 min-h-screen">
<div
className={`fixed top-4 left-1/2 -translate-x-1/2 px-4 py-2 rounded-lg bg-header border border-gold/30 text-gold text-sm shadow-lg transition-opacity duration-300 z-50 ${
showCopied ? 'opacity-100' : 'opacity-0 pointer-events-none'
}`}
>
Odkaz na hru skopírovaný
</div>
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h1 className="font-serif text-2xl text-gold">{game?.name ?? 'Hra'}</h1> <h1 className="font-serif text-2xl text-gold">{game?.name ?? 'Hra'}</h1>
<button onClick={handleLeave} className="text-sm text-green-dim hover:text-gold"> <button onClick={handleLeave} className="text-sm text-green-dim hover:text-gold">
@@ -38,7 +55,7 @@ export default function Lobby() {
onClick={handleCopyCode} onClick={handleCopyCode}
className="ml-3 px-3 py-1 rounded-lg text-sm border border-gold/30 text-gold hover:bg-gold hover:text-table transition-colors" className="ml-3 px-3 py-1 rounded-lg text-sm border border-gold/30 text-gold hover:bg-gold hover:text-table transition-colors"
> >
Kopírovať Kopírovať URL
</button> </button>
</div> </div>