zmazanie nedohratych hier z lobby
This commit is contained in:
1 parent
115413a813
commit
3655e06587
2 files changed
+80
-37
No files matched your search
+56
-35
@@ -184,6 +184,9 @@ class Game:
|
||||
# Serializuje tahovu slucku botov -- dva sucasne _run_bot_turns tasky
|
||||
# by inak mohli tahat za to iste sedadlo.
|
||||
self.bot_lock = asyncio.Lock()
|
||||
# Grace timer po odchode posledneho cloveka. Novy odchod ho zrusi a
|
||||
# spusti odznova, takze navrat neresetuje cudzie okno.
|
||||
self.abandon_task: asyncio.Task | None = None
|
||||
|
||||
def start(self):
|
||||
self.bridzik_core = Bridzik()
|
||||
@@ -342,37 +345,64 @@ async def send_error(sid: str, message: str):
|
||||
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.
|
||||
# Ako dlho prezije prazdna hra. Na mobile sa socket bezne strati uz pri
|
||||
# zamknuti obrazovky, takze okamzite zmazanie rusilo lobby aj rozohrate partie.
|
||||
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_human_connected(game):
|
||||
del games[gid]
|
||||
await broadcast_lobby()
|
||||
GAME_ABANDON_GRACE_SECONDS = 30 * 60
|
||||
|
||||
|
||||
def _any_human_connected(game: "Game") -> bool:
|
||||
"""Boti su 'pripojeni' stale, pre opustenost lobby sa pocitaju len ludia."""
|
||||
"""Boti su 'pripojeni' stale, pre opustenost sa pocitaju len ludia."""
|
||||
return any(p.connected for p in game.players if not p.is_bot)
|
||||
|
||||
|
||||
def _arm_abandon(game: "Game") -> None:
|
||||
"""Spusti grace odznova. Predosly timer sa zrusi, aby navrat + novy odchod
|
||||
nedal staremu tasku zmazat hru hned."""
|
||||
if game.abandon_task is not None:
|
||||
game.abandon_task.cancel()
|
||||
delay = GAME_ABANDON_GRACE_SECONDS if game.started else LOBBY_ABANDON_GRACE_SECONDS
|
||||
game.abandon_task = asyncio.create_task(_abandon_if_empty(game.gid, delay))
|
||||
|
||||
|
||||
async def _abandon_if_empty(gid: str, delay: float) -> None:
|
||||
await asyncio.sleep(delay)
|
||||
game = games.get(gid)
|
||||
# Porovnanie s current_task odfiltruje aj hru s tym istym gid, ktoru medzitym
|
||||
# niekto ukoncil a obnovil (ma vlastny timer).
|
||||
if game is None or game.abandon_task is not asyncio.current_task() or _any_human_connected(game):
|
||||
return
|
||||
if not game.started:
|
||||
del games[gid]
|
||||
await broadcast_lobby()
|
||||
elif not game.bridzik_core.is_completed():
|
||||
await _finish_game(game)
|
||||
|
||||
|
||||
async def _finish_game(game: "Game") -> None:
|
||||
if games.pop(game.gid, None) is not game:
|
||||
return
|
||||
try:
|
||||
await history.mark_game_ended(game.gid)
|
||||
except Exception:
|
||||
games[game.gid] = game
|
||||
raise
|
||||
await sio.emit("game_ended", {"gid": game.gid}, room=game.gid)
|
||||
for player in game.players:
|
||||
if player.sid:
|
||||
sessions.pop(player.sid, None)
|
||||
await sio.leave_room(player.sid, game.gid)
|
||||
await broadcast_lobby()
|
||||
|
||||
|
||||
async def _mark_player_offline(game: "Game", player: "Player"):
|
||||
"""Mark player disconnected. An unstarted game with nobody left gets a
|
||||
delayed cleanup (mobile sockets drop on screen lock, so an immediate
|
||||
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)."""
|
||||
"""Oznac hraca offline. Ked nezostal ziadny clovek, grace timer hru
|
||||
neskor zmaze (nezacatu z lobby, rozohratu aj z DB)."""
|
||||
if games.get(game.gid) is not game:
|
||||
return
|
||||
player.connected = False
|
||||
if not _any_human_connected(game) and not game.started:
|
||||
asyncio.create_task(_cleanup_abandoned_lobby(game.gid))
|
||||
if not _any_human_connected(game):
|
||||
_arm_abandon(game)
|
||||
await sio.emit(
|
||||
"player_connection",
|
||||
{"order": player.order, "connected": False},
|
||||
@@ -400,7 +430,7 @@ async def _restore_unfinished_games():
|
||||
for info in await history.get_unfinished_games():
|
||||
if info["gid"] in games:
|
||||
continue
|
||||
_load_game_into_memory(info)
|
||||
_arm_abandon(_load_game_into_memory(info))
|
||||
|
||||
|
||||
def _load_game_into_memory(info: dict) -> "Game":
|
||||
@@ -786,8 +816,7 @@ async def start_game(sid, gid):
|
||||
async def end_game(sid, gid):
|
||||
"""Any seated player can permanently end a game that won't be finished --
|
||||
not just the host, so the other players aren't stuck forever if the host
|
||||
abandons the game. Marks it ended in the DB (so it won't be restored) and
|
||||
sends everyone back to the lobby."""
|
||||
abandons the game."""
|
||||
sess = sessions.get(sid)
|
||||
if sess is None or sess["gid"] != gid:
|
||||
return await send_error(sid, "Nie ste v tejto hre.")
|
||||
@@ -795,15 +824,7 @@ async def end_game(sid, gid):
|
||||
if game is None:
|
||||
return await send_error(sid, "Hra neexistuje.")
|
||||
|
||||
await history.mark_game_ended(gid)
|
||||
# Notify the room first (while players are still in it), then tear it down.
|
||||
await sio.emit("game_ended", {"gid": gid}, room=gid)
|
||||
for player in game.players:
|
||||
if player.sid:
|
||||
sessions.pop(player.sid, None)
|
||||
await sio.leave_room(player.sid, gid)
|
||||
del games[gid]
|
||||
await broadcast_lobby()
|
||||
await _finish_game(game)
|
||||
|
||||
|
||||
@sio.on("reconnect_to_game")
|
||||
@@ -899,7 +920,7 @@ async def restore_game(sid, gid):
|
||||
info = await history.reopen_game(gid, account["player_id"])
|
||||
if info is None:
|
||||
return await send_error(sid, "Hru sa nepodarilo obnovit.")
|
||||
_load_game_into_memory(info)
|
||||
_arm_abandon(_load_game_into_memory(info))
|
||||
await sio.emit("game_restored", {"gid": gid}, to=sid)
|
||||
await broadcast_lobby()
|
||||
|
||||
|
||||
+24
-2
@@ -1,4 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import api
|
||||
|
||||
@@ -218,7 +219,7 @@ class SocketLayerCase(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
# --- disconnect & cleanup --------------------------------------------
|
||||
|
||||
async def test_disconnect_marks_player_then_drops_empty_game(self):
|
||||
async def test_disconnect_marks_player_and_schedules_started_game_expiry(self):
|
||||
gid = await self.make_started_game()
|
||||
await api.disconnect("s0")
|
||||
self.assertFalse(api.games[gid].player_by_sid("s0").connected)
|
||||
@@ -227,7 +228,28 @@ class SocketLayerCase(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
for i in range(1, 4):
|
||||
await api.disconnect(f"s{i}")
|
||||
self.assertNotIn(gid, api.games) # everyone gone -> game removed
|
||||
self.assertIn(gid, api.games) # started game stays until its grace period expires
|
||||
self.assertIsNotNone(api.games[gid].abandon_task)
|
||||
|
||||
async def test_abandoned_started_game_expires_after_grace_period(self):
|
||||
gid = "abandoned"
|
||||
game = api.Game(gid, "Abandoned")
|
||||
game.start()
|
||||
game.players = [api.Player(None, f"P{seat}", seat, seat) for seat in range(4)]
|
||||
for player in game.players:
|
||||
player.connected = False
|
||||
api.games[gid] = game
|
||||
|
||||
with (
|
||||
patch.object(api, "GAME_ABANDON_GRACE_SECONDS", 0),
|
||||
patch.object(api.history, "mark_game_ended", new_callable=AsyncMock) as mark_ended,
|
||||
):
|
||||
api._arm_abandon(game)
|
||||
await game.abandon_task
|
||||
|
||||
self.assertNotIn(gid, api.games)
|
||||
mark_ended.assert_awaited_once_with(gid)
|
||||
self.assertEqual(len(self.events("game_ended")), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user