k-l-lambda commited on
Commit
ef95618
·
1 Parent(s): eac5346

feat: add room selector dropdown for VS People mode

Browse files
trigo-web/app/src/components/RoomSelector.vue ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div class="room-selector" ref="selectorRef">
3
+ <div class="dropdown-container" @click="toggleDropdown" :class="{ disabled }">
4
+ <div class="selected-room">
5
+ <span v-if="currentRoom" class="room-display">
6
+ {{ currentRoom }}
7
+ </span>
8
+ <span v-else class="placeholder">Select room...</span>
9
+ </div>
10
+ <span class="dropdown-arrow">{{ isOpen ? '▲' : '▼' }}</span>
11
+ </div>
12
+
13
+ <div v-if="isOpen" class="dropdown-menu">
14
+ <!-- Create New Room Option -->
15
+ <div class="dropdown-item create-room" @click="handleCreate">
16
+ <span class="item-icon">➕</span>
17
+ <span class="item-text">Create New Room</span>
18
+ </div>
19
+
20
+ <div class="dropdown-divider" v-if="rooms.length > 0"></div>
21
+
22
+ <!-- Room List -->
23
+ <div class="room-list">
24
+ <div v-if="loading" class="loading-state">
25
+ Loading rooms...
26
+ </div>
27
+ <div v-else-if="rooms.length === 0" class="empty-state">
28
+ No active rooms
29
+ </div>
30
+ <div
31
+ v-else
32
+ v-for="room in sortedRooms"
33
+ :key="room.id"
34
+ class="dropdown-item room-item"
35
+ :class="{
36
+ 'is-current': room.id === currentRoom,
37
+ 'is-full': room.isFull
38
+ }"
39
+ @click="handleSelect(room)"
40
+ >
41
+ <span class="room-id">{{ room.id }}</span>
42
+ <span class="player-count" :class="playerCountClass(room)">
43
+ {{ room.playerCount }}/2
44
+ </span>
45
+ <span class="room-status-badge" :class="room.status">
46
+ {{ formatStatus(room) }}
47
+ </span>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </template>
53
+
54
+
55
+ <script setup lang="ts">
56
+ import { ref, computed, onMounted, onUnmounted } from "vue";
57
+
58
+ export interface RoomSummary {
59
+ id: string;
60
+ playerCount: number;
61
+ maxPlayers: number;
62
+ status: "waiting" | "playing" | "finished";
63
+ isFull: boolean;
64
+ createdAt: string;
65
+ }
66
+
67
+ const props = defineProps<{
68
+ currentRoom: string | null;
69
+ rooms: RoomSummary[];
70
+ loading?: boolean;
71
+ disabled?: boolean;
72
+ }>();
73
+
74
+ const emit = defineEmits<{
75
+ create: [];
76
+ select: [roomId: string];
77
+ }>();
78
+
79
+ const isOpen = ref(false);
80
+ const selectorRef = ref<HTMLElement | null>(null);
81
+
82
+ const sortedRooms = computed(() => {
83
+ // Sort: current room first, then by player count (waiting rooms first), then by creation time
84
+ return [...props.rooms].sort((a, b) => {
85
+ // Current room always first
86
+ if (a.id === props.currentRoom) return -1;
87
+ if (b.id === props.currentRoom) return 1;
88
+ // Waiting rooms before playing
89
+ if (a.status === "waiting" && b.status !== "waiting") return -1;
90
+ if (a.status !== "waiting" && b.status === "waiting") return 1;
91
+ // By player count (less players first - more likely to join)
92
+ if (a.playerCount !== b.playerCount) return a.playerCount - b.playerCount;
93
+ // By creation time (newer first)
94
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
95
+ });
96
+ });
97
+
98
+ const toggleDropdown = () => {
99
+ if (props.disabled) return;
100
+ isOpen.value = !isOpen.value;
101
+ };
102
+
103
+ const handleCreate = () => {
104
+ isOpen.value = false;
105
+ emit("create");
106
+ };
107
+
108
+ const handleSelect = (room: RoomSummary) => {
109
+ if (room.isFull && room.id !== props.currentRoom) return;
110
+ isOpen.value = false;
111
+ emit("select", room.id);
112
+ };
113
+
114
+ const playerCountClass = (room: RoomSummary) => {
115
+ if (room.playerCount === 0) return "empty";
116
+ if (room.playerCount === 1) return "partial";
117
+ return "full";
118
+ };
119
+
120
+ const formatStatus = (room: RoomSummary) => {
121
+ if (room.isFull) return "Full";
122
+ if (room.status === "waiting") return "Waiting";
123
+ if (room.status === "playing") return "Playing";
124
+ return room.status;
125
+ };
126
+
127
+ // Close dropdown when clicking outside
128
+ const handleClickOutside = (event: MouseEvent) => {
129
+ if (selectorRef.value && !selectorRef.value.contains(event.target as Node)) {
130
+ isOpen.value = false;
131
+ }
132
+ };
133
+
134
+ onMounted(() => {
135
+ document.addEventListener("click", handleClickOutside);
136
+ });
137
+
138
+ onUnmounted(() => {
139
+ document.removeEventListener("click", handleClickOutside);
140
+ });
141
+ </script>
142
+
143
+
144
+ <style scoped lang="scss">
145
+ .room-selector {
146
+ position: relative;
147
+ display: inline-block;
148
+ }
149
+
150
+ .dropdown-container {
151
+ background: rgba(0, 0, 0, 0.3);
152
+ border: 1px solid rgba(255, 255, 255, 0.3);
153
+ border-radius: 4px;
154
+ padding: 0.4rem 0.6rem;
155
+ min-width: 140px;
156
+ cursor: pointer;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ gap: 0.5rem;
161
+ transition: background 0.2s;
162
+
163
+ &:hover:not(.disabled) {
164
+ background: rgba(0, 0, 0, 0.4);
165
+ }
166
+
167
+ &.disabled {
168
+ opacity: 0.5;
169
+ cursor: not-allowed;
170
+ }
171
+ }
172
+
173
+ .selected-room {
174
+ font-size: 0.9rem;
175
+ color: #fff;
176
+ }
177
+
178
+ .placeholder {
179
+ color: rgba(255, 255, 255, 0.5);
180
+ }
181
+
182
+ .dropdown-arrow {
183
+ font-size: 0.7rem;
184
+ color: rgba(255, 255, 255, 0.7);
185
+ }
186
+
187
+ .dropdown-menu {
188
+ position: absolute;
189
+ top: calc(100% + 4px);
190
+ left: 0;
191
+ min-width: 220px;
192
+ background: #3a3a3a;
193
+ border: 1px solid #505050;
194
+ border-radius: 4px;
195
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
196
+ z-index: 100;
197
+ max-height: 300px;
198
+ overflow-y: auto;
199
+ }
200
+
201
+ .dropdown-item {
202
+ padding: 0.6rem 0.75rem;
203
+ cursor: pointer;
204
+ display: flex;
205
+ align-items: center;
206
+ gap: 0.5rem;
207
+ transition: background 0.15s;
208
+
209
+ &:hover:not(.is-full) {
210
+ background: #4a4a4a;
211
+ }
212
+
213
+ &.is-full:not(.is-current) {
214
+ opacity: 0.5;
215
+ cursor: not-allowed;
216
+ }
217
+
218
+ &.is-current {
219
+ background: #505050;
220
+ border-left: 3px solid #e94560;
221
+ padding-left: calc(0.75rem - 3px);
222
+ }
223
+ }
224
+
225
+ .create-room {
226
+ color: #4ade80;
227
+ border-bottom: 1px solid #505050;
228
+
229
+ .item-icon {
230
+ font-size: 0.9rem;
231
+ }
232
+
233
+ .item-text {
234
+ font-size: 0.9rem;
235
+ }
236
+ }
237
+
238
+ .dropdown-divider {
239
+ height: 1px;
240
+ background: #505050;
241
+ margin: 0;
242
+ }
243
+
244
+ .room-list {
245
+ max-height: 240px;
246
+ overflow-y: auto;
247
+ }
248
+
249
+ .loading-state,
250
+ .empty-state {
251
+ padding: 1rem;
252
+ text-align: center;
253
+ color: rgba(255, 255, 255, 0.5);
254
+ font-size: 0.85rem;
255
+ }
256
+
257
+ .room-item {
258
+ .room-id {
259
+ font-family: monospace;
260
+ font-size: 0.85rem;
261
+ color: #fff;
262
+ flex: 1;
263
+ }
264
+
265
+ .player-count {
266
+ font-family: monospace;
267
+ font-size: 0.8rem;
268
+ padding: 0.1rem 0.3rem;
269
+ border-radius: 3px;
270
+
271
+ &.empty {
272
+ color: #4ade80;
273
+ }
274
+
275
+ &.partial {
276
+ color: #fbbf24;
277
+ }
278
+
279
+ &.full {
280
+ color: #ef4444;
281
+ }
282
+ }
283
+
284
+ .room-status-badge {
285
+ font-size: 0.75rem;
286
+ padding: 0.15rem 0.4rem;
287
+ border-radius: 3px;
288
+ min-width: 50px;
289
+ text-align: center;
290
+
291
+ &.waiting {
292
+ background: rgba(74, 222, 128, 0.15);
293
+ color: #4ade80;
294
+ }
295
+
296
+ &.playing {
297
+ background: rgba(251, 191, 36, 0.15);
298
+ color: #fbbf24;
299
+ }
300
+ }
301
+ }
302
+ </style>
trigo-web/app/src/composables/useSocket.ts CHANGED
@@ -159,6 +159,47 @@ export function useSocket() {
159
  };
160
 
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  // Event listeners
163
  const onPlayerJoined = (handler: (data: { playerId: string; nickname: string }) => void): void => {
164
  const socket = getSocket();
@@ -268,6 +309,7 @@ export function useSocket() {
268
  joinRoom,
269
  leaveRoom,
270
  changeNickname,
 
271
  // Game actions
272
  makeMove,
273
  pass,
@@ -285,6 +327,9 @@ export function useSocket() {
285
  onGameReset,
286
  onPlayerDisconnected,
287
  onError,
 
 
 
288
  // Event cleanup
289
  offPlayerJoined,
290
  offPlayerLeft,
@@ -294,7 +339,10 @@ export function useSocket() {
294
  offGameEnded,
295
  offGameReset,
296
  offPlayerDisconnected,
297
- offError
 
 
 
298
  };
299
  }
300
 
 
159
  };
160
 
161
 
162
+ // Room list management
163
+
164
+ // List available rooms
165
+ const listRooms = (callback: (response: any) => void): void => {
166
+ const socket = getSocket();
167
+ socket.emit("listRooms", callback);
168
+ };
169
+
170
+ // Room list event listeners
171
+ const onRoomCreated = (handler: (data: any) => void): void => {
172
+ const socket = getSocket();
173
+ socket.on("roomCreated", handler);
174
+ };
175
+
176
+ const onRoomUpdated = (handler: (data: any) => void): void => {
177
+ const socket = getSocket();
178
+ socket.on("roomUpdated", handler);
179
+ };
180
+
181
+ const onRoomDeleted = (handler: (data: { roomId: string }) => void): void => {
182
+ const socket = getSocket();
183
+ socket.on("roomDeleted", handler);
184
+ };
185
+
186
+ // Room list event cleanup
187
+ const offRoomCreated = (handler?: any): void => {
188
+ const socket = getSocket();
189
+ socket.off("roomCreated", handler);
190
+ };
191
+
192
+ const offRoomUpdated = (handler?: any): void => {
193
+ const socket = getSocket();
194
+ socket.off("roomUpdated", handler);
195
+ };
196
+
197
+ const offRoomDeleted = (handler?: any): void => {
198
+ const socket = getSocket();
199
+ socket.off("roomDeleted", handler);
200
+ };
201
+
202
+
203
  // Event listeners
204
  const onPlayerJoined = (handler: (data: { playerId: string; nickname: string }) => void): void => {
205
  const socket = getSocket();
 
309
  joinRoom,
310
  leaveRoom,
311
  changeNickname,
312
+ listRooms,
313
  // Game actions
314
  makeMove,
315
  pass,
 
327
  onGameReset,
328
  onPlayerDisconnected,
329
  onError,
330
+ onRoomCreated,
331
+ onRoomUpdated,
332
+ onRoomDeleted,
333
  // Event cleanup
334
  offPlayerJoined,
335
  offPlayerLeft,
 
339
  offGameEnded,
340
  offGameReset,
341
  offPlayerDisconnected,
342
+ offError,
343
+ offRoomCreated,
344
+ offRoomUpdated,
345
+ offRoomDeleted
346
  };
347
  }
348
 
trigo-web/app/src/views/TrigoView.vue CHANGED
@@ -124,6 +124,15 @@
124
  </div>
125
 
126
  <div class="header-controls" v-else-if="gameMode === 'vs-people'">
 
 
 
 
 
 
 
 
 
127
  <select
128
  v-model="preferredColor"
129
  class="color-preference-select"
@@ -339,6 +348,7 @@
339
  import { useSocket } from "@/composables/useSocket";
340
  import { useRoomHash } from "@/composables/useRoomHash";
341
  import InlineNicknameEditor from "@/components/InlineNicknameEditor.vue";
 
342
  import { storeToRefs } from "pinia";
343
  import type { BoardShape } from "../../../inc/trigo";
344
  import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
@@ -375,6 +385,10 @@
375
  const { getRoomIdFromHash, updateHash, clearHash, isValidRoomId } = useRoomHash();
376
  const isJoiningRoom = ref(false);
377
 
 
 
 
 
378
  // Helper functions for board shape parsing
379
  const parseBoardShape = (shapeStr: string): BoardShape => {
380
  const parts = shapeStr
@@ -1079,6 +1093,73 @@
1079
  }
1080
 
1081
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1082
  /**
1083
  * Initialize multiplayer room based on URL hash
1084
  * - No hash: create new room
@@ -1498,6 +1579,9 @@
1498
  console.error("[TrigoView] Socket error:", data.message);
1499
  });
1500
 
 
 
 
1501
  // Set player ID when socket connects
1502
  if (socketApi.socket.id) {
1503
  playerStore.setPlayerId(socketApi.socket.id);
@@ -1515,6 +1599,9 @@
1515
  playerStore.setPlayerId(socketApi.socket.id);
1516
  }
1517
 
 
 
 
1518
  // Initialize room based on URL hash (only if not already in a room)
1519
  if (!playerStore.roomId && !isJoiningRoom.value) {
1520
  console.log("[TrigoView] Initializing room after socket connection");
@@ -1536,6 +1623,7 @@
1536
  // If socket is already connected, initialize room immediately
1537
  if (socketApi.socket.connected) {
1538
  console.log("[TrigoView] Socket already connected, initializing room");
 
1539
  initializeMultiplayerRoom();
1540
  }
1541
  }
@@ -1582,6 +1670,7 @@
1582
  socketApi.offGameEnded();
1583
  socketApi.offGameReset();
1584
  socketApi.offError();
 
1585
 
1586
  // Remove hashchange listener
1587
  window.removeEventListener("hashchange", handleHashChange);
 
124
  </div>
125
 
126
  <div class="header-controls" v-else-if="gameMode === 'vs-people'">
127
+ <room-selector
128
+ :current-room="playerStore.roomId"
129
+ :rooms="roomList"
130
+ :loading="isLoadingRooms"
131
+ :disabled="gameStarted && playerStore.hasOpponent"
132
+ @create="handleCreateRoom"
133
+ @select="handleRoomSelect"
134
+ />
135
+
136
  <select
137
  v-model="preferredColor"
138
  class="color-preference-select"
 
348
  import { useSocket } from "@/composables/useSocket";
349
  import { useRoomHash } from "@/composables/useRoomHash";
350
  import InlineNicknameEditor from "@/components/InlineNicknameEditor.vue";
351
+ import RoomSelector, { type RoomSummary } from "@/components/RoomSelector.vue";
352
  import { storeToRefs } from "pinia";
353
  import type { BoardShape } from "../../../inc/trigo";
354
  import { Stone, validateMove, StoneType, validateTGN } from "../../../inc/trigo";
 
385
  const { getRoomIdFromHash, updateHash, clearHash, isValidRoomId } = useRoomHash();
386
  const isJoiningRoom = ref(false);
387
 
388
+ // Room list state (for VS People mode room selector)
389
+ const roomList = ref<RoomSummary[]>([]);
390
+ const isLoadingRooms = ref(false);
391
+
392
  // Helper functions for board shape parsing
393
  const parseBoardShape = (shapeStr: string): BoardShape => {
394
  const parts = shapeStr
 
1093
  }
1094
 
1095
 
1096
+ /**
1097
+ * Room list management for room selector dropdown
1098
+ */
1099
+ function fetchRoomList() {
1100
+ isLoadingRooms.value = true;
1101
+ socketApi.listRooms((response: any) => {
1102
+ isLoadingRooms.value = false;
1103
+ if (response.success) {
1104
+ roomList.value = response.rooms;
1105
+ }
1106
+ });
1107
+ }
1108
+
1109
+ function setupRoomListeners() {
1110
+ socketApi.onRoomCreated((data: RoomSummary) => {
1111
+ // Add new room to list if not already present
1112
+ if (!roomList.value.find(r => r.id === data.id)) {
1113
+ roomList.value.push(data);
1114
+ }
1115
+ });
1116
+
1117
+ socketApi.onRoomUpdated((data: RoomSummary) => {
1118
+ const index = roomList.value.findIndex(r => r.id === data.id);
1119
+ if (index >= 0) {
1120
+ roomList.value[index] = data;
1121
+ } else {
1122
+ roomList.value.push(data);
1123
+ }
1124
+ });
1125
+
1126
+ socketApi.onRoomDeleted((data: { roomId: string }) => {
1127
+ roomList.value = roomList.value.filter(r => r.id !== data.roomId);
1128
+ });
1129
+ }
1130
+
1131
+ function cleanupRoomListeners() {
1132
+ socketApi.offRoomCreated();
1133
+ socketApi.offRoomUpdated();
1134
+ socketApi.offRoomDeleted();
1135
+ }
1136
+
1137
+ // Room selector event handlers
1138
+ async function handleRoomSelect(roomId: string) {
1139
+ if (playerStore.roomId === roomId) return; // Already in this room
1140
+
1141
+ // Leave current room if any
1142
+ if (playerStore.roomId) {
1143
+ socketApi.leaveRoom();
1144
+ playerStore.leaveRoom();
1145
+ }
1146
+
1147
+ // Join selected room
1148
+ await joinRoomByHash(roomId);
1149
+ }
1150
+
1151
+ async function handleCreateRoom() {
1152
+ // Leave current room if any
1153
+ if (playerStore.roomId) {
1154
+ socketApi.leaveRoom();
1155
+ playerStore.leaveRoom();
1156
+ }
1157
+
1158
+ clearHash();
1159
+ await createAndJoinRoom();
1160
+ }
1161
+
1162
+
1163
  /**
1164
  * Initialize multiplayer room based on URL hash
1165
  * - No hash: create new room
 
1579
  console.error("[TrigoView] Socket error:", data.message);
1580
  });
1581
 
1582
+ // Setup room list listeners for room selector
1583
+ setupRoomListeners();
1584
+
1585
  // Set player ID when socket connects
1586
  if (socketApi.socket.id) {
1587
  playerStore.setPlayerId(socketApi.socket.id);
 
1599
  playerStore.setPlayerId(socketApi.socket.id);
1600
  }
1601
 
1602
+ // Fetch room list for room selector
1603
+ fetchRoomList();
1604
+
1605
  // Initialize room based on URL hash (only if not already in a room)
1606
  if (!playerStore.roomId && !isJoiningRoom.value) {
1607
  console.log("[TrigoView] Initializing room after socket connection");
 
1623
  // If socket is already connected, initialize room immediately
1624
  if (socketApi.socket.connected) {
1625
  console.log("[TrigoView] Socket already connected, initializing room");
1626
+ fetchRoomList();
1627
  initializeMultiplayerRoom();
1628
  }
1629
  }
 
1670
  socketApi.offGameEnded();
1671
  socketApi.offGameReset();
1672
  socketApi.offError();
1673
+ cleanupRoomListeners();
1674
 
1675
  // Remove hashchange listener
1676
  window.removeEventListener("hashchange", handleHashChange);
trigo-web/backend/src/sockets/gameSocket.ts CHANGED
@@ -1,9 +1,32 @@
1
  import { Server, Socket } from "socket.io";
2
  import { GameManager } from "../services/gameManager";
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  export function setupSocketHandlers(io: Server, socket: Socket, gameManager: GameManager) {
5
  console.log(`Setting up socket handlers for ${socket.id}`);
6
 
 
 
 
 
 
 
 
 
 
 
7
  // Join room
8
  socket.on(
9
  "joinRoom",
@@ -115,6 +138,16 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
115
  nickname: nickname
116
  });
117
 
 
 
 
 
 
 
 
 
 
 
118
  console.log(
119
  `Player ${socket.id} ${roomId ? "joined" : "created"} room ${room.id}`
120
  );
@@ -145,13 +178,22 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
145
  socket.on("leaveRoom", () => {
146
  const room = gameManager.getPlayerRoom(socket.id);
147
  if (room) {
 
148
  socket.leave(room.id);
149
  gameManager.leaveRoom(room.id, socket.id);
150
 
151
- // Notify others
152
- socket.to(room.id).emit("playerLeft", {
153
  playerId: socket.id
154
  });
 
 
 
 
 
 
 
 
155
  }
156
  });
157
 
@@ -424,10 +466,19 @@ export function setupSocketHandlers(io: Server, socket: Socket, gameManager: Gam
424
  console.log(`Client disconnected: ${socket.id}`);
425
  const room = gameManager.getPlayerRoom(socket.id);
426
  if (room) {
 
427
  gameManager.leaveRoom(room.id, socket.id);
428
  socket.to(room.id).emit("playerDisconnected", {
429
  playerId: socket.id
430
  });
 
 
 
 
 
 
 
 
431
  }
432
  });
433
  }
 
1
  import { Server, Socket } from "socket.io";
2
  import { GameManager } from "../services/gameManager";
3
 
4
+ // Helper to get room summary for room list
5
+ function getRoomSummary(room: any) {
6
+ const connectedPlayers = Object.values(room.players).filter((p: any) => p.connected);
7
+ return {
8
+ id: room.id,
9
+ playerCount: connectedPlayers.length,
10
+ maxPlayers: 2,
11
+ status: room.gameState.gameStatus,
12
+ isFull: connectedPlayers.length >= 2,
13
+ createdAt: room.createdAt.toISOString()
14
+ };
15
+ }
16
+
17
  export function setupSocketHandlers(io: Server, socket: Socket, gameManager: GameManager) {
18
  console.log(`Setting up socket handlers for ${socket.id}`);
19
 
20
+ // List available rooms
21
+ socket.on("listRooms", (callback?: (response: any) => void) => {
22
+ const rooms = gameManager.getActiveRooms();
23
+ const roomList = rooms.map(room => getRoomSummary(room));
24
+
25
+ if (callback) {
26
+ callback({ success: true, rooms: roomList });
27
+ }
28
+ });
29
+
30
  // Join room
31
  socket.on(
32
  "joinRoom",
 
138
  nickname: nickname
139
  });
140
 
141
+ // Broadcast room update to all sockets (for room list)
142
+ const roomSummary = getRoomSummary(room);
143
+ if (roomId) {
144
+ // Joined existing room
145
+ io.emit("roomUpdated", roomSummary);
146
+ } else {
147
+ // Created new room
148
+ io.emit("roomCreated", roomSummary);
149
+ }
150
+
151
  console.log(
152
  `Player ${socket.id} ${roomId ? "joined" : "created"} room ${room.id}`
153
  );
 
178
  socket.on("leaveRoom", () => {
179
  const room = gameManager.getPlayerRoom(socket.id);
180
  if (room) {
181
+ const roomId = room.id;
182
  socket.leave(room.id);
183
  gameManager.leaveRoom(room.id, socket.id);
184
 
185
+ // Notify others in room
186
+ socket.to(roomId).emit("playerLeft", {
187
  playerId: socket.id
188
  });
189
+
190
+ // Broadcast room update or deletion to all sockets
191
+ const updatedRoom = gameManager.getRoom(roomId);
192
+ if (updatedRoom) {
193
+ io.emit("roomUpdated", getRoomSummary(updatedRoom));
194
+ } else {
195
+ io.emit("roomDeleted", { roomId });
196
+ }
197
  }
198
  });
199
 
 
466
  console.log(`Client disconnected: ${socket.id}`);
467
  const room = gameManager.getPlayerRoom(socket.id);
468
  if (room) {
469
+ const roomId = room.id;
470
  gameManager.leaveRoom(room.id, socket.id);
471
  socket.to(room.id).emit("playerDisconnected", {
472
  playerId: socket.id
473
  });
474
+
475
+ // Broadcast room update or deletion to all sockets
476
+ const updatedRoom = gameManager.getRoom(roomId);
477
+ if (updatedRoom) {
478
+ io.emit("roomUpdated", getRoomSummary(updatedRoom));
479
+ } else {
480
+ io.emit("roomDeleted", { roomId });
481
+ }
482
  }
483
  });
484
  }