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
|
# Serializuje tahovu slucku botov -- dva sucasne _run_bot_turns tasky
|
||||||
# by inak mohli tahat za to iste sedadlo.
|
# by inak mohli tahat za to iste sedadlo.
|
||||||
self.bot_lock = asyncio.Lock()
|
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):
|
def start(self):
|
||||||
self.bridzik_core = Bridzik()
|
self.bridzik_core = Bridzik()
|
||||||
@@ -342,37 +345,64 @@ 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
|
# Ako dlho prezije prazdna hra. Na mobile sa socket bezne strati uz pri
|
||||||
# sa socket bezne strati uz pri zamknuti obrazovky, takze okamzite zmazanie
|
# zamknuti obrazovky, takze okamzite zmazanie rusilo lobby aj rozohrate partie.
|
||||||
# hry rusilo lobby, v ktorom hraci len cakali so zhasnutym telefonom.
|
|
||||||
LOBBY_ABANDON_GRACE_SECONDS = 10 * 60
|
LOBBY_ABANDON_GRACE_SECONDS = 10 * 60
|
||||||
|
GAME_ABANDON_GRACE_SECONDS = 30 * 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()
|
|
||||||
|
|
||||||
|
|
||||||
def _any_human_connected(game: "Game") -> bool:
|
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)
|
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"):
|
async def _mark_player_offline(game: "Game", player: "Player"):
|
||||||
"""Mark player disconnected. An unstarted game with nobody left gets a
|
"""Oznac hraca offline. Ked nezostal ziadny clovek, grace timer hru
|
||||||
delayed cleanup (mobile sockets drop on screen lock, so an immediate
|
neskor zmaze (nezacatu z lobby, rozohratu aj z DB)."""
|
||||||
delete would kill lobbies where everyone is just waiting); a started game
|
if games.get(game.gid) is not game:
|
||||||
is kept in memory so it stays in the lobby and can be resumed (it's torn
|
return
|
||||||
down only by end_game)."""
|
|
||||||
player.connected = False
|
player.connected = False
|
||||||
if not _any_human_connected(game) and not game.started:
|
if not _any_human_connected(game):
|
||||||
asyncio.create_task(_cleanup_abandoned_lobby(game.gid))
|
_arm_abandon(game)
|
||||||
await sio.emit(
|
await sio.emit(
|
||||||
"player_connection",
|
"player_connection",
|
||||||
{"order": player.order, "connected": False},
|
{"order": player.order, "connected": False},
|
||||||
@@ -400,7 +430,7 @@ async def _restore_unfinished_games():
|
|||||||
for info in await history.get_unfinished_games():
|
for info in await history.get_unfinished_games():
|
||||||
if info["gid"] in games:
|
if info["gid"] in games:
|
||||||
continue
|
continue
|
||||||
_load_game_into_memory(info)
|
_arm_abandon(_load_game_into_memory(info))
|
||||||
|
|
||||||
|
|
||||||
def _load_game_into_memory(info: dict) -> "Game":
|
def _load_game_into_memory(info: dict) -> "Game":
|
||||||
@@ -786,8 +816,7 @@ async def start_game(sid, gid):
|
|||||||
async def end_game(sid, gid):
|
async def end_game(sid, gid):
|
||||||
"""Any seated player can permanently end a game that won't be finished --
|
"""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
|
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
|
abandons the game."""
|
||||||
sends everyone back to the lobby."""
|
|
||||||
sess = sessions.get(sid)
|
sess = sessions.get(sid)
|
||||||
if sess is None or sess["gid"] != gid:
|
if sess is None or sess["gid"] != gid:
|
||||||
return await send_error(sid, "Nie ste v tejto hre.")
|
return await send_error(sid, "Nie ste v tejto hre.")
|
||||||
@@ -795,15 +824,7 @@ async def end_game(sid, gid):
|
|||||||
if game is None:
|
if game is None:
|
||||||
return await send_error(sid, "Hra neexistuje.")
|
return await send_error(sid, "Hra neexistuje.")
|
||||||
|
|
||||||
await history.mark_game_ended(gid)
|
await _finish_game(game)
|
||||||
# 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()
|
|
||||||
|
|
||||||
|
|
||||||
@sio.on("reconnect_to_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"])
|
info = await history.reopen_game(gid, account["player_id"])
|
||||||
if info is None:
|
if info is None:
|
||||||
return await send_error(sid, "Hru sa nepodarilo obnovit.")
|
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 sio.emit("game_restored", {"gid": gid}, to=sid)
|
||||||
await broadcast_lobby()
|
await broadcast_lobby()
|
||||||
|
|
||||||
|
|||||||
+24
-2
@@ -1,4 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import api
|
import api
|
||||||
|
|
||||||
@@ -218,7 +219,7 @@ class SocketLayerCase(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
# --- disconnect & cleanup --------------------------------------------
|
# --- 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()
|
gid = await self.make_started_game()
|
||||||
await api.disconnect("s0")
|
await api.disconnect("s0")
|
||||||
self.assertFalse(api.games[gid].player_by_sid("s0").connected)
|
self.assertFalse(api.games[gid].player_by_sid("s0").connected)
|
||||||
@@ -227,7 +228,28 @@ class SocketLayerCase(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
for i in range(1, 4):
|
for i in range(1, 4):
|
||||||
await api.disconnect(f"s{i}")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in new issue
Block a user