Spaces:
Runtime error
Runtime error
Fixes
Browse files- .gitignore +1 -0
- backend/data/stats.db +0 -0
- backend/lobby/manager.py +17 -4
- backend/main.py +17 -9
- frontend/src/lib/components/Map.svelte +1 -1
- frontend/src/routes/+page.svelte +23 -4
.gitignore
CHANGED
|
@@ -8,3 +8,4 @@ __pycache__/
|
|
| 8 |
frontend/.svelte-kit/generated/
|
| 9 |
frontend/.svelte-kit/output/
|
| 10 |
real/backend/data/stats.db
|
|
|
|
|
|
| 8 |
frontend/.svelte-kit/generated/
|
| 9 |
frontend/.svelte-kit/output/
|
| 10 |
real/backend/data/stats.db
|
| 11 |
+
real/
|
backend/data/stats.db
ADDED
|
Binary file (20.5 kB). View file
|
|
|
backend/lobby/manager.py
CHANGED
|
@@ -217,13 +217,26 @@ class LobbyManager:
|
|
| 217 |
now = time.time()
|
| 218 |
result = []
|
| 219 |
for room in self._rooms.values():
|
| 220 |
-
if room.status =
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
result.append({
|
| 223 |
"room_id": room.room_id,
|
| 224 |
-
"player1":
|
| 225 |
-
"player2":
|
| 226 |
"elapsed": elapsed,
|
|
|
|
| 227 |
})
|
| 228 |
return result
|
| 229 |
|
|
|
|
| 217 |
now = time.time()
|
| 218 |
result = []
|
| 219 |
for room in self._rooms.values():
|
| 220 |
+
if room.status != "playing":
|
| 221 |
+
continue
|
| 222 |
+
elapsed = int(now - room.started_at) if room.started_at else 0
|
| 223 |
+
real_players = [p for p in room.players if p.sid != BOT_PLAYER_ID]
|
| 224 |
+
if len(real_players) == 2:
|
| 225 |
+
result.append({
|
| 226 |
+
"room_id": room.room_id,
|
| 227 |
+
"player1": real_players[0].name,
|
| 228 |
+
"player2": real_players[1].name,
|
| 229 |
+
"elapsed": elapsed,
|
| 230 |
+
"is_tutorial": False,
|
| 231 |
+
})
|
| 232 |
+
elif len(real_players) == 1 and len(room.players) == 1:
|
| 233 |
+
# Tutorial room: only one real player, no bot in room
|
| 234 |
result.append({
|
| 235 |
"room_id": room.room_id,
|
| 236 |
+
"player1": real_players[0].name,
|
| 237 |
+
"player2": None,
|
| 238 |
"elapsed": elapsed,
|
| 239 |
+
"is_tutorial": True,
|
| 240 |
})
|
| 241 |
return result
|
| 242 |
|
backend/main.py
CHANGED
|
@@ -856,12 +856,13 @@ async def observe(sid: str, data: dict) -> None:
|
|
| 856 |
async def create_room(sid: str, data: dict) -> None:
|
| 857 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 858 |
name = await sanitize_player_name(name)
|
|
|
|
|
|
|
| 859 |
room = lobby.create_room(sid, name)
|
| 860 |
await sio.enter_room(sid, room.room_id)
|
| 861 |
await sio.emit("room_created", {"room_id": room.room_id, "room": room.to_dict()}, to=sid)
|
| 862 |
log.info("Room %s created by %s", room.room_id, name)
|
| 863 |
# Start bot-offer countdown
|
| 864 |
-
_cancel_bot_timer(sid)
|
| 865 |
_bot_timers[sid] = asyncio.create_task(_schedule_bot_offer(sid, room.room_id))
|
| 866 |
|
| 867 |
|
|
@@ -891,6 +892,8 @@ async def join_room(sid: str, data: dict) -> None:
|
|
| 891 |
async def quick_match(sid: str, data: dict) -> None:
|
| 892 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 893 |
name = await sanitize_player_name(name)
|
|
|
|
|
|
|
| 894 |
room, is_new = lobby.quick_match(sid, name)
|
| 895 |
|
| 896 |
if not is_new:
|
|
@@ -977,6 +980,18 @@ async def cancel_construction(sid: str, data: dict) -> None:
|
|
| 977 |
await _emit_error(sid, f"Impossible d'annuler: {result.data.get('error', '?')}")
|
| 978 |
|
| 979 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
@sio.event
|
| 981 |
async def start_tutorial(sid: str, data: dict) -> None:
|
| 982 |
"""Start a solo tutorial game for the player."""
|
|
@@ -988,14 +1003,7 @@ async def start_tutorial(sid: str, data: dict) -> None:
|
|
| 988 |
|
| 989 |
# Leave & stop any previous game/tutorial so stale game_update events
|
| 990 |
# don't bleed into the new tutorial session.
|
| 991 |
-
|
| 992 |
-
if old_room:
|
| 993 |
-
old_room_id = old_room.room_id
|
| 994 |
-
await sio.leave_room(sid, old_room_id)
|
| 995 |
-
if old_room_id in engines:
|
| 996 |
-
await engines[old_room_id].stop()
|
| 997 |
-
del engines[old_room_id]
|
| 998 |
-
log.info("Partie précédente stoppée (nouveau tutoriel) room %s | parties actives: %d/%d", old_room_id, len(engines), MAX_SIMULTANEOUS_GAMES)
|
| 999 |
|
| 1000 |
if len(engines) >= MAX_SIMULTANEOUS_GAMES:
|
| 1001 |
log.warning("Limite de %d parties simultanées atteinte, impossible de démarrer le tutoriel pour %s", MAX_SIMULTANEOUS_GAMES, sid)
|
|
|
|
| 856 |
async def create_room(sid: str, data: dict) -> None:
|
| 857 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 858 |
name = await sanitize_player_name(name)
|
| 859 |
+
_cancel_bot_timer(sid)
|
| 860 |
+
await _cleanup_old_game(sid)
|
| 861 |
room = lobby.create_room(sid, name)
|
| 862 |
await sio.enter_room(sid, room.room_id)
|
| 863 |
await sio.emit("room_created", {"room_id": room.room_id, "room": room.to_dict()}, to=sid)
|
| 864 |
log.info("Room %s created by %s", room.room_id, name)
|
| 865 |
# Start bot-offer countdown
|
|
|
|
| 866 |
_bot_timers[sid] = asyncio.create_task(_schedule_bot_offer(sid, room.room_id))
|
| 867 |
|
| 868 |
|
|
|
|
| 892 |
async def quick_match(sid: str, data: dict) -> None:
|
| 893 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 894 |
name = await sanitize_player_name(name)
|
| 895 |
+
_cancel_bot_timer(sid)
|
| 896 |
+
await _cleanup_old_game(sid)
|
| 897 |
room, is_new = lobby.quick_match(sid, name)
|
| 898 |
|
| 899 |
if not is_new:
|
|
|
|
| 980 |
await _emit_error(sid, f"Impossible d'annuler: {result.data.get('error', '?')}")
|
| 981 |
|
| 982 |
|
| 983 |
+
async def _cleanup_old_game(sid: str) -> None:
|
| 984 |
+
"""Leave socket room and stop engine for any game the player is currently in."""
|
| 985 |
+
old_room = lobby.get_room_for_sid(sid)
|
| 986 |
+
if old_room:
|
| 987 |
+
old_room_id = old_room.room_id
|
| 988 |
+
await sio.leave_room(sid, old_room_id)
|
| 989 |
+
if old_room_id in engines:
|
| 990 |
+
await engines[old_room_id].stop()
|
| 991 |
+
del engines[old_room_id]
|
| 992 |
+
log.info("Partie précédente stoppée room %s | parties actives: %d/%d", old_room_id, len(engines), MAX_SIMULTANEOUS_GAMES)
|
| 993 |
+
|
| 994 |
+
|
| 995 |
@sio.event
|
| 996 |
async def start_tutorial(sid: str, data: dict) -> None:
|
| 997 |
"""Start a solo tutorial game for the player."""
|
|
|
|
| 1003 |
|
| 1004 |
# Leave & stop any previous game/tutorial so stale game_update events
|
| 1005 |
# don't bleed into the new tutorial session.
|
| 1006 |
+
await _cleanup_old_game(sid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1007 |
|
| 1008 |
if len(engines) >= MAX_SIMULTANEOUS_GAMES:
|
| 1009 |
log.warning("Limite de %d parties simultanées atteinte, impossible de démarrer le tutoriel pour %s", MAX_SIMULTANEOUS_GAMES, sid)
|
frontend/src/lib/components/Map.svelte
CHANGED
|
@@ -872,7 +872,7 @@
|
|
| 872 |
per-cell rects, works in all environments (no SVG mask url(#id) needed).
|
| 873 |
Computed reactively via $: fogRects so viewport panning is always in sync. -->
|
| 874 |
{#each fogRects as r}
|
| 875 |
-
<rect x={r.x} y={r.y} width={r.w} height={1} fill="rgba(0,0,0,{r.opacity})" pointer-events="none" shape-rendering="crispEdges" />
|
| 876 |
{/each}
|
| 877 |
|
| 878 |
<!-- Tutorial target beacon (rendered above fog so it's always visible) -->
|
|
|
|
| 872 |
per-cell rects, works in all environments (no SVG mask url(#id) needed).
|
| 873 |
Computed reactively via $: fogRects so viewport panning is always in sync. -->
|
| 874 |
{#each fogRects as r}
|
| 875 |
+
<rect x={r.x} y={r.y} width={r.w + 0.02} height={1.02} fill="rgba(0,0,0,{r.opacity})" pointer-events="none" shape-rendering="crispEdges" />
|
| 876 |
{/each}
|
| 877 |
|
| 878 |
<!-- Tutorial target beacon (rendered above fog so it's always visible) -->
|
frontend/src/routes/+page.svelte
CHANGED
|
@@ -166,6 +166,7 @@
|
|
| 166 |
if (game_state) {
|
| 167 |
gameState.set(game_state);
|
| 168 |
myPlayerId.set(null);
|
|
|
|
| 169 |
}
|
| 170 |
goto("/game");
|
| 171 |
},
|
|
@@ -404,8 +405,9 @@
|
|
| 404 |
type PlayingRoom = {
|
| 405 |
room_id: string;
|
| 406 |
player1: string;
|
| 407 |
-
player2: string;
|
| 408 |
elapsed: number;
|
|
|
|
| 409 |
};
|
| 410 |
let observeModalOpen = false;
|
| 411 |
let playingRooms: PlayingRoom[] = [];
|
|
@@ -702,9 +704,14 @@
|
|
| 702 |
{#each playingRooms as room (room.room_id)}
|
| 703 |
<li class="obs-item">
|
| 704 |
<div class="obs-players">
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 708 |
</div>
|
| 709 |
<div class="obs-meta">
|
| 710 |
<span class="obs-timer">{formatElapsed(room.elapsed)}</span>
|
|
@@ -1489,6 +1496,18 @@
|
|
| 1489 |
flex-shrink: 0;
|
| 1490 |
}
|
| 1491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1492 |
.obs-meta {
|
| 1493 |
display: flex;
|
| 1494 |
align-items: center;
|
|
|
|
| 166 |
if (game_state) {
|
| 167 |
gameState.set(game_state);
|
| 168 |
myPlayerId.set(null);
|
| 169 |
+
isTutorial.set(!!game_state.is_tutorial);
|
| 170 |
}
|
| 171 |
goto("/game");
|
| 172 |
},
|
|
|
|
| 405 |
type PlayingRoom = {
|
| 406 |
room_id: string;
|
| 407 |
player1: string;
|
| 408 |
+
player2: string | null;
|
| 409 |
elapsed: number;
|
| 410 |
+
is_tutorial: boolean;
|
| 411 |
};
|
| 412 |
let observeModalOpen = false;
|
| 413 |
let playingRooms: PlayingRoom[] = [];
|
|
|
|
| 704 |
{#each playingRooms as room (room.room_id)}
|
| 705 |
<li class="obs-item">
|
| 706 |
<div class="obs-players">
|
| 707 |
+
{#if room.is_tutorial}
|
| 708 |
+
<span class="obs-tutorial-badge">Tutorial</span>
|
| 709 |
+
<span class="obs-player">{room.player1}</span>
|
| 710 |
+
{:else}
|
| 711 |
+
<span class="obs-player">{room.player1}</span>
|
| 712 |
+
<span class="obs-vs">vs</span>
|
| 713 |
+
<span class="obs-player">{room.player2}</span>
|
| 714 |
+
{/if}
|
| 715 |
</div>
|
| 716 |
<div class="obs-meta">
|
| 717 |
<span class="obs-timer">{formatElapsed(room.elapsed)}</span>
|
|
|
|
| 1496 |
flex-shrink: 0;
|
| 1497 |
}
|
| 1498 |
|
| 1499 |
+
.obs-tutorial-badge {
|
| 1500 |
+
font-size: 0.65rem;
|
| 1501 |
+
font-weight: 700;
|
| 1502 |
+
text-transform: uppercase;
|
| 1503 |
+
letter-spacing: 0.06em;
|
| 1504 |
+
color: #0f1420;
|
| 1505 |
+
background: var(--lobby-accent-cyan);
|
| 1506 |
+
border-radius: 4px;
|
| 1507 |
+
padding: 2px 6px;
|
| 1508 |
+
flex-shrink: 0;
|
| 1509 |
+
}
|
| 1510 |
+
|
| 1511 |
.obs-meta {
|
| 1512 |
display: flex;
|
| 1513 |
align-items: center;
|