pridanie pocty online/available hracov

This commit is contained in:
tim
2026-07-27 20:09:24 +02:00
parent 04185a0d97
commit 18ae869ee8
7 changed files with 153 additions and 28 deletions
+101 -7
View File
@@ -253,8 +253,52 @@ def public_games() -> list:
# --- emit helpers ---------------------------------------------------------
def _online_player_ids() -> set[int]:
"""Distinct logged-in players currently connected.
Counting *players* (by account id), not raw sockets, is what makes the
lobby counters stable and meaningful:
* one person with several tabs/devices counts once;
* a not-yet-logged-in visitor (still on the auth screen) is not a player
and is not counted;
* reconnect churn and phantom sockets lingering until the engine.io ping
timeout no longer inflate the number, because the identity is counted,
not the transient connection.
"""
return {acc["player_id"] for acc in accounts.values()}
def _in_game_player_ids() -> set[int]:
"""Players who currently occupy a seat in a game (any of their sockets is
seated) -- 'busy', i.e. not available to start/join another game."""
ids: set[int] = set()
for sid in sessions:
acc = accounts.get(sid)
if acc is not None:
ids.add(acc["player_id"])
return ids
def online_count() -> int:
"""Number of distinct logged-in players online (in the lobby or in a game)."""
return len(_online_player_ids())
def available_count() -> int:
"""Online players not currently seated in any game (free to join/create)."""
return len(_online_player_ids() - _in_game_player_ids())
async def broadcast_lobby():
await sio.emit("get_games", {"games": public_games()}, room=LOBBY)
await sio.emit(
"get_games",
{
"games": public_games(),
"online_count": online_count(),
"available_count": available_count(),
},
room=LOBBY,
)
async def send_game_status(gid: str):
@@ -455,6 +499,13 @@ async def send_error_room(gid: str, message: str):
await sio.emit("error", {"error": message}, room=gid)
def _public_identity(account: dict) -> dict:
"""The client only needs `player_id` to compare against game rosters (see
`isMember` in frontend/src/pages/GameList.tsx) -- `username` stays purely
server-side (used to name the seat on register_player/rejoin_game etc.)."""
return {"player_id": account["player_id"]}
# --- connection lifecycle -------------------------------------------------
@sio.event
@@ -465,8 +516,9 @@ async def connect(sid, environ, auth=None):
identity = await auth_module.player_by_token(token)
if identity is not None:
accounts[sid] = identity
await sio.emit("login", {"player": identity}, to=sid)
await sio.emit("get_games", {"games": public_games()}, to=sid)
await sio.emit("login", {"player": _public_identity(identity)}, to=sid)
# Broadcast (not just emit to sid) so everyone's online-players count stays live.
await broadcast_lobby()
@sio.event
@@ -478,6 +530,7 @@ async def disconnect(sid):
player = game.player_by_sid(sid)
if player is not None:
await _mark_player_offline(game, player)
# accounts/sessions already updated above, so the lobby counters are correct.
await broadcast_lobby()
@@ -513,7 +566,11 @@ async def confirm_account(sid, username, code):
return await send_error(sid, str(exc))
accounts[sid] = {"player_id": identity["player_id"], "username": identity["username"]}
await _record_login_event(sid, identity["player_id"])
await sio.emit("login", {"player": accounts[sid], "token": identity["token"]}, to=sid)
await sio.emit(
"login", {"player": _public_identity(accounts[sid]), "token": identity["token"]}, to=sid
)
# A new player just came online -> refresh everyone's lobby counters.
await broadcast_lobby()
@sio.on("login")
@@ -533,7 +590,28 @@ async def login(sid, username, code):
return await send_error(sid, str(exc))
accounts[sid] = {"player_id": identity["player_id"], "username": identity["username"]}
await _record_login_event(sid, identity["player_id"])
await sio.emit("login", {"player": accounts[sid], "token": identity["token"]}, to=sid)
await sio.emit(
"login", {"player": _public_identity(accounts[sid]), "token": identity["token"]}, to=sid
)
# A new player just came online -> refresh everyone's lobby counters.
await broadcast_lobby()
@sio.on("logout")
async def logout(sid):
"""De-authenticate this connection without touching the transport.
The previous approach (client calling `socket.disconnect()` then
immediately `socket.connect()`) was racy: python-socketio's disconnect
isn't instantaneous (the old sid can briefly linger past its ping-timeout
while a brand new sid is already connecting), so for that window BOTH
sids were counted in the lobby online/available totals -- exactly the
transient over-count reported. Simply dropping the account association
server-side avoids any connect/disconnect churn entirely.
"""
accounts.pop(sid, None)
# A player just went offline -> refresh everyone's lobby counters.
await broadcast_lobby()
# --- lobby ----------------------------------------------------------------
@@ -550,7 +628,15 @@ async def create_game(sid, name):
@sio.on("get_games")
async def get_games(sid, *args):
await sio.emit("get_games", {"games": public_games()}, to=sid)
await sio.emit(
"get_games",
{
"games": public_games(),
"online_count": online_count(),
"available_count": available_count(),
},
to=sid,
)
@sio.on("register_player")
@@ -800,7 +886,15 @@ async def restore_game(sid, gid):
if gid in games:
# Uz je v pamati (lobby) -- staci obnovit zoznam hier u klienta.
await sio.emit("game_restored", {"gid": gid}, to=sid)
return await sio.emit("get_games", {"games": public_games()}, to=sid)
return await sio.emit(
"get_games",
{
"games": public_games(),
"online_count": online_count(),
"available_count": available_count(),
},
to=sid,
)
info = await history.reopen_game(gid, account["player_id"])
if info is None: