A realtime multiplayer version of the boardgame Ricochet Robots
at master 21 kB view raw
1import "dotenv/config"; 2import { WebSocketServer } from "ws"; 3import { 4 tilesNoEmpty, 5 type GridType, 6 type N2, 7 type Rockets, 8 type TilesNoEmpty, 9} from "../../shared/grid"; 10import { 11 compareBid, 12 type CtoSPacket, 13 type CtoSPlayerBidPacket, 14 type CtoSPlayerChatPacket, 15 type CtoSPlayerJoinPacket, 16 type CtoSPlayerVerifyMovePacket, 17 type CtoSPlayerVerifyResetPacket, 18 type CtoSRequestGameEndPacket, 19 type CtoSRequestGameStartPacket, 20 type CtoSUpdateSettingsPacket, 21 type GameInfo, 22 type GameRoom, 23 type StoCEventPacket, 24 type StoCGameCompletedEvent, 25 type StoCGamEndEvent, 26 type StoCGameRevealBoardEvent, 27 type StoCGameStartEvent, 28 type StoCGameStartVerificationEvent, 29 type StoCGameVerifyFailedEvent, 30 type StoCGameVerifyNextEvent, 31 type StoCPacket, 32 type StoCPlayerBidEvent, 33 type StoCPlayerChatEvent, 34 type StoCPlayerJoinEvent, 35 type StoCPlayerJoinResponse, 36 type StoCPlayerVerifyCompletedEvent, 37 type StoCPlayerVerifyMoveEvent, 38 type StoCPlayerVerifyResetEvent, 39 type StoCResponsePacket, 40} from "~/shared/game"; 41import { db } from "../db"; 42import type WebSocket from "ws"; 43import { games, gameUsers, users } from "../db/schema"; 44import { and, eq } from "drizzle-orm"; 45import { validateSessionToken } from "../auth/validate"; 46import { defaultGrid } from "~/app/_components/defaultGrid"; 47import { currentTimeInSeconds } from "~/lib/time"; 48import { 49 getCompletedMove, 50 getMovementSquaresDown, 51 getMovementSquaresLeft, 52 getMovementSquaresRight, 53 getMovementSquaresUp, 54} from "~/shared/movement"; 55 56const wss = new WebSocketServer({ port: 3001 }); 57 58// gameId => WebSocket[] 59const gamePlayers = new Map<number, Set<WebSocket>>(); 60 61// gameId => non-persistent gamedata 62const gameRooms = new Map<number, GameRoom>(); 63 64const broadcastToRoom = ( 65 ws: WebSocket, 66 gameId: number, 67 packet: StoCEventPacket, 68) => { 69 if (!gamePlayers.has(gameId)) { 70 return; 71 } 72 73 console.log(`Broadcasting: ${JSON.stringify(packet)}`); 74 gamePlayers.get(gameId)?.forEach((client) => { 75 if (client.readyState === client.OPEN) { 76 client.send(JSON.stringify(packet)); 77 } 78 }); 79}; 80 81wss.on("connection", (ws) => { 82 ws.on("message", (message: string) => { 83 void (async () => { 84 try { 85 console.log("Received:", message.toString()); 86 const data = JSON.parse(message.toString()) as CtoSPacket; 87 88 let response: StoCResponsePacket | null = err("No response"); 89 switch (data.type) { 90 case "playerJoin": 91 response = await handlePlayerJoinPacket(ws, data); 92 break; 93 case "requestGameStart": 94 response = await handleRequestGameStartPacket(ws, data); 95 break; 96 case "requestGameEnd": 97 response = await handleRequestGameEndPacket(ws, data); 98 break; 99 case "playerChat": 100 response = await handlePlayerChatPacket(ws, data); 101 break; 102 case "playerBid": 103 response = await handlePlayerBidPacket(ws, data); 104 break; 105 case "playerVerifyMove": 106 response = await handlePlayerVerifyMovePacket(ws, data); 107 break; 108 case "playerVerifyReset": 109 response = await handlePlayerVerifyResetPacket(ws, data); 110 break; 111 case "updateSettings": 112 response = await handleUpdateSettingsPacket(ws, data); 113 break; 114 default: 115 console.log( 116 `Unimplemented message ${(data as { type: string }).type}`, 117 ); 118 } 119 120 if (response != null) { 121 if ("gameId" in data) { 122 if (gameRooms.has(data.gameId)) { 123 console.log( 124 "Rockets: ", 125 gameRooms.get(data.gameId)!.currentRockets, 126 ); 127 } 128 } 129 console.log("Sent:", JSON.stringify(response)); 130 ws.send(JSON.stringify(response)); 131 } 132 } catch (error) { 133 console.error("Error handling message:", error); 134 } 135 })(); 136 }); 137}); 138 139const err = (message: string): StoCResponsePacket => { 140 return { 141 type: "errorResponse", 142 data: { 143 message, 144 }, 145 }; 146}; 147 148const handlePlayerJoinPacket = async ( 149 ws: WebSocket, 150 packet: CtoSPlayerJoinPacket, 151): Promise<StoCResponsePacket> => { 152 const authUser = await validateSessionToken(packet.sessionToken); 153 if (!authUser) { 154 return err("You are not logged in"); 155 } 156 157 // Check if game exists 158 const game = ( 159 await db.select().from(games).where(eq(games.code, packet.data.gameCode)) 160 )[0]; 161 if (!game) { 162 return err("No game found with code"); 163 } 164 165 // Add connection to a set of connections for the specific game room for broadcasts etc. 166 if (!gamePlayers.has(game.id)) { 167 gamePlayers.set(game.id, new Set()); 168 } 169 gamePlayers.get(game.id)?.add(ws); 170 171 if (!gameRooms.has(game.id)) { 172 const board = defaultGrid; 173 const rockets = generateRockets(board); 174 175 gameRooms.set(game.id, { 176 board, 177 restorableRockets: { ...rockets }, 178 currentRockets: { ...rockets }, 179 currentBids: {}, 180 ingameState: "starting", 181 currentVerifyingPlayerId: null, 182 targetTile: null, 183 movesTaken: null, 184 wins: {}, 185 usedTiles: [], 186 settings: { 187 startingDelay: 10, 188 biddingCountdownTime: 60, 189 verificationTime: 60, 190 }, 191 }); 192 } 193 194 // Add player to persistent database of players in game if they aren't there already 195 const existingPlayer = ( 196 await db 197 .select() 198 .from(gameUsers) 199 .where( 200 and(eq(gameUsers.gameId, game.id), eq(gameUsers.userId, authUser.id)), 201 ) 202 )[0]; 203 if (!existingPlayer) { 204 await db.insert(gameUsers).values({ 205 gameId: game.id, 206 userId: authUser.id, 207 }); 208 209 // Send that a new player has joined to all connected clients 210 broadcastToRoom(ws, game.id, { 211 type: "playerJoinedEvent", 212 data: { 213 id: authUser.id, 214 username: authUser.username, 215 }, 216 } satisfies StoCPlayerJoinEvent); 217 } 218 const players = await db 219 .select() 220 .from(gameUsers) 221 .where(eq(gameUsers.gameId, game.id)) 222 .leftJoin(users, eq(users.id, gameUsers.userId)); 223 224 const gameRoom = gameRooms.get(game.id); 225 226 return { 227 type: "playerJoinResponse", 228 data: { 229 game: { 230 id: game.id, 231 name: game.name, 232 ownerId: game.ownerId, 233 state: game.state, 234 }, 235 players: players 236 .map((players) => players.users) 237 .map((user) => ({ 238 id: user!.id, 239 username: user!.username, 240 })), 241 room: gameRooms.get(game.id)!, 242 }, 243 } satisfies StoCPlayerJoinResponse; 244}; 245 246const generateRandomPositionInBounds = (board: GridType): N2 => { 247 const x = Math.floor(Math.random() * board[0]!.length); 248 const y = Math.floor(Math.random() * board.length); 249 250 return { 251 x, 252 y, 253 } satisfies N2; 254}; 255 256const generateRockets = (board: GridType): Rockets => { 257 const rocketIds = ["red", "green", "blue", "yellow", "silver"]; 258 const rocketPositions: Record<string, N2> = {}; 259 260 while (Object.keys(rocketPositions).length !== rocketIds.length) { 261 const position = generateRandomPositionInBounds(board); 262 if (board[position.y]![position.x]!.type !== "empty") { 263 continue; 264 } 265 266 if ( 267 Object.values(rocketPositions).find( 268 (pos) => pos.x === position.x && pos.y === position.y, 269 ) 270 ) { 271 continue; 272 } 273 274 if ([7, 8].includes(position.x) && [7, 8].includes(position.y)) { 275 continue; 276 } 277 278 const rocketId = rocketIds[Object.keys(rocketPositions).length]!; 279 rocketPositions[rocketId] = position; 280 } 281 282 console.log(rocketPositions); 283 return rocketPositions as Rockets; 284}; 285 286const getRandomTile = (usedTiles: TilesNoEmpty[]) => 287 tilesNoEmpty.filter((e) => !usedTiles.includes(e))[ 288 Math.floor(Math.random() * tilesNoEmpty.length) 289 ]!; 290 291const startGame = (ws: WebSocket, game: GameInfo) => { 292 const room = gameRooms.get(game.id)!; 293 294 console.log(room); 295 296 const startingDelay = room.settings.startingDelay; 297 298 room.ingameState = "starting"; 299 gameRooms.set(game.id, room); 300 301 broadcastToRoom(ws, game.id, { 302 type: "gameStartEvent", 303 data: { 304 startUnix: currentTimeInSeconds() + startingDelay, // Start game after startingDelay seconds 305 room, 306 }, 307 } satisfies StoCGameStartEvent); 308 309 setTimeout(() => { 310 (async () => { 311 const room = gameRooms.get(game.id); 312 if (!room) return; 313 314 const targetTile = getRandomTile(room.usedTiles); 315 316 room.ingameState = "nobid"; 317 room.targetTile = targetTile; 318 room.currentBids = {}; 319 room.currentRockets = { ...room.restorableRockets }; 320 room.currentVerifyingPlayerId = null; 321 room.usedTiles.push(targetTile); 322 gameRooms.set(game.id, room); 323 324 broadcastToRoom(ws, game.id, { 325 type: "gameRevealBoard", 326 data: { 327 room, 328 }, 329 } satisfies StoCGameRevealBoardEvent); 330 })(); 331 }, startingDelay * 1000); 332}; 333 334const handleRequestGameStartPacket = async ( 335 ws: WebSocket, 336 packet: CtoSRequestGameStartPacket, 337): Promise<StoCResponsePacket | null> => { 338 const authUser = await validateSessionToken(packet.sessionToken); 339 if (!authUser) { 340 return err("You are not logged in"); 341 } 342 343 // Check if game exists 344 const game = ( 345 await db.select().from(games).where(eq(games.id, packet.gameId)) 346 )[0]; 347 if (!game) { 348 return err("No game found with id"); 349 } 350 351 if (authUser.id !== game.ownerId) { 352 return err("You are not the owner"); 353 } 354 355 await db 356 .update(games) 357 .set({ 358 state: "ingame", 359 }) 360 .where(eq(games.id, packet.gameId)); 361 362 startGame(ws, game); 363 364 return null; 365}; 366 367const handleRequestGameEndPacket = async ( 368 ws: WebSocket, 369 packet: CtoSRequestGameEndPacket, 370): Promise<StoCResponsePacket | null> => { 371 const authUser = await validateSessionToken(packet.sessionToken); 372 if (!authUser) { 373 return err("You are not logged in"); 374 } 375 376 // Check if game exists 377 console.log(packet); 378 const game = ( 379 await db.select().from(games).where(eq(games.id, packet.gameId)) 380 )[0]; 381 if (!game) { 382 return err("No game found with id"); 383 } 384 385 if (authUser.id !== game.ownerId) { 386 return err("You are not the owner"); 387 } 388 389 await db 390 .update(games) 391 .set({ 392 state: "lobby", 393 }) 394 .where(eq(games.id, packet.gameId)); 395 396 broadcastToRoom(ws, game.id, { 397 type: "gameEndEvent", 398 } satisfies StoCGamEndEvent); 399 400 return null; 401}; 402 403const handlePlayerChatPacket = async ( 404 ws: WebSocket, 405 packet: CtoSPlayerChatPacket, 406): Promise<StoCResponsePacket | null> => { 407 const authUser = await validateSessionToken(packet.sessionToken); 408 if (!authUser) { 409 return err("You are not logged in"); 410 } 411 412 // Check if game exists 413 const game = ( 414 await db.select().from(games).where(eq(games.id, packet.gameId)) 415 )[0]; 416 if (!game) { 417 return err("No game found with code"); 418 } 419 420 broadcastToRoom(ws, game.id, { 421 type: "playerChatEvent", 422 data: { 423 playerId: authUser.id, 424 message: packet.data.message, 425 }, 426 } satisfies StoCPlayerChatEvent); 427 428 return null; 429}; 430 431const handleVerifyNext = (ws: WebSocket, endDelay: number, game: GameInfo) => { 432 const room = gameRooms.get(game.id); 433 if (room?.ingameState !== "verify") { 434 return; 435 } 436 437 console.log("Reached", Object.entries(room.currentBids).length); 438 if (Object.entries(room.currentBids).length === 1) { 439 room.ingameState = "failed"; 440 room.currentBids = {}; 441 room.currentRockets = { ...room.restorableRockets }; 442 room.currentVerifyingPlayerId = null; 443 room.movesTaken = 0; 444 gameRooms.set(game.id, room); 445 446 broadcastToRoom(ws, game.id, { 447 type: "gameVerifyFailed", 448 } satisfies StoCGameVerifyFailedEvent); 449 450 setTimeout(() => { 451 startGame(ws, game); 452 }, endDelay * 1000); 453 return; 454 } 455 456 const sortedBidEntries = Object.entries(room.currentBids).sort((a, b) => 457 compareBid(a[1], b[1]), 458 ); 459 const currentBidsWithoutCurrentVerifier = Object.fromEntries( 460 sortedBidEntries.filter( 461 (e) => e[0] !== room.currentVerifyingPlayerId!.toString(), 462 ), 463 ); 464 465 const newCurrentBids = currentBidsWithoutCurrentVerifier; 466 const newVerifyingPlayerId = parseInt( 467 Object.entries(currentBidsWithoutCurrentVerifier) 468 .sort((a, b) => compareBid(a[1], b[1]))[0]![0] 469 .toString(), 470 ); 471 472 room.currentBids = newCurrentBids; 473 room.currentVerifyingPlayerId = newVerifyingPlayerId; 474 room.currentRockets = { ...room.restorableRockets }; 475 room.movesTaken = 0; 476 gameRooms.set(game.id, room); 477 478 console.log("VerifyNext", room.currentRockets, room.restorableRockets); 479 480 broadcastToRoom(ws, game.id, { 481 type: "gameVerifyNext", 482 data: { 483 newCurrentBids, 484 newVerifyingPlayerId, 485 endUnix: currentTimeInSeconds() + endDelay, 486 }, 487 } satisfies StoCGameVerifyNextEvent); 488 489 setTimeout(() => { 490 handleVerifyNext(ws, endDelay, game); 491 }, endDelay * 1000); 492}; 493 494const gameStartVerification = async (ws: WebSocket, game: GameInfo) => { 495 const room = gameRooms.get(game.id); 496 497 if (room!.ingameState !== "countdown") { 498 return; 499 } 500 501 const verifyingPlayerId = parseInt( 502 Object.entries(room!.currentBids) 503 .sort((a, b) => compareBid(a[1], b[1]))[0]![0] 504 .toString(), 505 ); 506 507 room!.ingameState = "verify"; 508 room!.currentVerifyingPlayerId = verifyingPlayerId; 509 gameRooms.set(game.id, room!); 510 511 const endDelay = room!.settings.verificationTime; 512 513 broadcastToRoom(ws, game.id, { 514 type: "gameStartVerification", 515 data: { 516 playerId: verifyingPlayerId, 517 endUnix: currentTimeInSeconds() + endDelay, 518 }, 519 } satisfies StoCGameStartVerificationEvent); 520 521 setTimeout(() => { 522 handleVerifyNext(ws, endDelay, game); 523 }, endDelay * 1000); 524}; 525 526const handlePlayerBidPacket = async ( 527 ws: WebSocket, 528 packet: CtoSPlayerBidPacket, 529): Promise<StoCResponsePacket | null> => { 530 const authUser = await validateSessionToken(packet.sessionToken); 531 if (!authUser) { 532 return err("You are not logged in"); 533 } 534 535 // Check if game exists 536 const game = ( 537 await db.select().from(games).where(eq(games.id, packet.gameId)) 538 )[0]; 539 if (!game) { 540 return err("No game found with id"); 541 } 542 543 if (game.state !== "ingame") { 544 return err("Gamestate isn't ingame"); 545 } 546 547 const gameRoom = gameRooms.get(game.id); 548 if (!gameRoom) { 549 throw new Error("No game room"); 550 } 551 552 if (!["nobid", "countdown"].includes(gameRoom.ingameState)) { 553 return err(`Can't bid while ingameState is ${gameRoom.ingameState}`); 554 } 555 556 let updatedBid = false; 557 if (!Object.keys(gameRoom.currentBids).includes(authUser.id.toString())) { 558 gameRoom.currentBids[authUser.id] = packet.data; 559 updatedBid = true; 560 } else { 561 const currentBid = gameRoom.currentBids[authUser.id]; 562 if (packet.data.bid < currentBid!.bid) { 563 gameRoom.currentBids[authUser.id] = packet.data; 564 updatedBid = true; 565 } 566 } 567 568 let startedCountdown = false; 569 const endDelay = gameRoom.settings.biddingCountdownTime; 570 571 if (gameRoom.ingameState !== "countdown") { 572 gameRoom.ingameState = "countdown"; 573 startedCountdown = true; 574 575 setTimeout(() => { 576 gameStartVerification(ws, game); 577 }, endDelay * 1000); // setTimeout works in ms not seconds so i *1000 578 } 579 580 if (updatedBid) { 581 gameRooms.set(game.id, gameRoom); 582 broadcastToRoom(ws, game.id, { 583 type: "playerBidEvent", 584 data: { 585 playerId: authUser.id, 586 bid: packet.data, 587 endUnix: startedCountdown ? currentTimeInSeconds() + endDelay : null, 588 }, 589 } satisfies StoCPlayerBidEvent); 590 } 591 592 return null; 593}; 594 595const getTileColor = (tile: TilesNoEmpty): string => { 596 return tile.split("_")[0]!; 597}; 598 599const isBoardWon = ( 600 board: GridType, 601 rockets: Rockets, 602 targetTile: TilesNoEmpty, 603) => { 604 let targetTilePosition: N2 | null = null; 605 606 board.forEach((row, y) => 607 row.forEach((cell, x) => { 608 if (cell.type === targetTile) { 609 targetTilePosition = { 610 x, 611 y, 612 }; 613 } 614 }), 615 ); 616 617 if (targetTilePosition === null) { 618 throw new Error("Target tile was not found on board"); 619 } 620 621 const rocketOnGoal = Object.entries(rockets).find( 622 (rocket) => 623 rocket[1].x === targetTilePosition!.x && 624 rocket[1].y === targetTilePosition!.y, 625 ); 626 627 console.log("a"); 628 629 if (!rocketOnGoal) return false; 630 631 const goalColor = getTileColor(targetTile); 632 633 console.log("b", goalColor, rocketOnGoal[0]); 634 635 return goalColor === rocketOnGoal[0] || goalColor === "joker"; 636}; 637 638const handlePlayerVerifyMovePacket = async ( 639 ws: WebSocket, 640 packet: CtoSPlayerVerifyMovePacket, 641): Promise<StoCResponsePacket | null> => { 642 const authUser = await validateSessionToken(packet.sessionToken); 643 if (!authUser) { 644 return err("You are not logged in"); 645 } 646 647 // Check if game exists 648 const game = ( 649 await db.select().from(games).where(eq(games.id, packet.gameId)) 650 )[0]; 651 if (!game) { 652 return err("No game found with id"); 653 } 654 655 if (game.state !== "ingame") { 656 return err("Gamestate isn't ingame"); 657 } 658 659 const gameRoom = gameRooms.get(game.id); 660 if (!gameRoom) { 661 throw new Error("No game room"); 662 } 663 664 if (gameRoom.ingameState !== "verify") { 665 return err(`Can't bid while ingameState isn't verify`); 666 } 667 668 if (gameRoom.currentVerifyingPlayerId !== authUser.id) { 669 return err("You are not the current verifying player"); 670 } 671 672 console.log("Old", gameRoom.restorableRockets); 673 674 const movedTo = getCompletedMove( 675 gameRoom.board, 676 gameRoom.currentRockets, 677 packet.data.rocket, 678 packet.data.direction, 679 ); 680 681 if (!movedTo) { 682 return err("Invalid move: no available space in that direction"); 683 } 684 685 gameRoom.currentRockets[packet.data.rocket] = movedTo; 686 gameRoom.movesTaken = gameRoom.movesTaken ? gameRoom.movesTaken + 1 : 1; 687 gameRooms.set(game.id, gameRoom); 688 689 broadcastToRoom(ws, game.id, { 690 type: "playerVerifyMove", 691 data: packet.data, 692 } satisfies StoCPlayerVerifyMoveEvent); 693 694 const bid = gameRoom.currentBids[gameRoom.currentVerifyingPlayerId]?.bid; 695 696 console.log( 697 "Room:", 698 Object.fromEntries( 699 Object.entries(gameRoom).filter(([a, b]) => a !== "board"), 700 ), 701 ); 702 703 if ( 704 gameRoom.movesTaken === bid && 705 isBoardWon(gameRoom.board, gameRoom.currentRockets, gameRoom.targetTile!) 706 ) { 707 const { wins = {}, targetTile } = gameRoom; 708 const playerId = gameRoom.currentVerifyingPlayerId; 709 710 const playerWins = wins[playerId] ?? []; 711 const updatedWins = { 712 ...wins, 713 [playerId]: [...playerWins, targetTile!], 714 }; 715 716 gameRoom.movesTaken = 0; 717 gameRoom.currentBids = {}; 718 gameRoom.wins = updatedWins; 719 gameRoom.restorableRockets = { ...gameRoom.currentRockets }; 720 gameRoom.ingameState = "winner"; 721 gameRooms.set(game.id, gameRoom); 722 723 const winner = Object.entries(gameRoom.wins).find((e) => e[1].length === 3); 724 725 // Someone won 726 if (winner) { 727 await db 728 .update(games) 729 .set({ 730 winnerId: parseInt(winner[0].toString()), 731 }) 732 .where(eq(games.id, game.id)); 733 734 broadcastToRoom(ws, game.id, { 735 type: "gameCompleted", 736 } satisfies StoCGameCompletedEvent); 737 return null; 738 } 739 740 // No winner this round below 741 const endDelay = 10; 742 743 broadcastToRoom(ws, game.id, { 744 type: "playerVerifyCompleted", 745 data: { 746 wins: updatedWins, 747 endUnix: currentTimeInSeconds() + endDelay, 748 }, 749 } satisfies StoCPlayerVerifyCompletedEvent); 750 751 setTimeout(() => { 752 startGame(ws, game); 753 }, endDelay * 1000); 754 } 755 756 return null; 757}; 758 759const handlePlayerVerifyResetPacket = async ( 760 ws: WebSocket, 761 packet: CtoSPlayerVerifyResetPacket, 762): Promise<StoCResponsePacket | null> => { 763 const authUser = await validateSessionToken(packet.sessionToken); 764 if (!authUser) { 765 return err("You are not logged in"); 766 } 767 768 // Check if game exists 769 const game = ( 770 await db.select().from(games).where(eq(games.id, packet.gameId)) 771 )[0]; 772 if (!game) { 773 return err("No game found with id"); 774 } 775 776 if (game.state !== "ingame") { 777 return err("Gamestate isn't ingame"); 778 } 779 780 const gameRoom = gameRooms.get(game.id); 781 if (!gameRoom) { 782 throw new Error("No game room"); 783 } 784 785 if (gameRoom.ingameState !== "verify") { 786 return err(`Can't bid while ingameState isn't verify`); 787 } 788 789 if (gameRoom.currentVerifyingPlayerId !== authUser.id) { 790 return err("You are not the current verifying player"); 791 } 792 793 gameRoom.movesTaken = 0; 794 gameRoom.currentRockets = gameRoom.restorableRockets; 795 gameRooms.set(game.id, gameRoom); 796 797 broadcastToRoom(ws, game.id, { 798 type: "playerVerifyReset", 799 } satisfies StoCPlayerVerifyResetEvent); 800 801 return null; 802}; 803 804const handleUpdateSettingsPacket = async ( 805 ws: WebSocket, 806 packet: CtoSUpdateSettingsPacket, 807): Promise<StoCResponsePacket | null> => { 808 const authUser = await validateSessionToken(packet.sessionToken); 809 if (!authUser) { 810 return err("You are not logged in"); 811 } 812 813 // Check if game exists 814 const game = ( 815 await db.select().from(games).where(eq(games.id, packet.gameId)) 816 )[0]; 817 if (!game) { 818 return err("No game found with id"); 819 } 820 821 if (authUser.id !== game.ownerId) { 822 return err("You are not the owner"); 823 } 824 825 const room = gameRooms.get(game.id); 826 if (!room) return err("No room"); 827 828 room.settings = packet.data; 829 gameRooms.set(game.id, room); 830 831 return null; 832};