Establishing basic WebSocket connections and rooms
The first step is to get the WebSocket connections up and running so players are able to:
- join either black or white team
- view total player count of each team as it changes in realtime
I’m using Socket.IO instead of vanilla WebSocket API because amongst other things, this library lifts a lot of the boilerplate setup out of my hands. I know I’ll need to implement something like a channel further down the line to emit team-related events like bidding and Socket.IO’s rooms offers just that. It also handles all the client connections for me so I don’t need to do manual cleanup and update team player count whenever a player disconnects.
This is the rudimentary setup I have for my server:
const cors = require("cors");
const express = require("express");
const { createServer } = require("http");
const { Server } = require("socket.io");
const app = express();
const httpServer = createServer(app);
// Adding this because my dev server runs on 3000
// while my SvelteKit app is running on port 5173
app.use(cors());
// Attach socket.io to http.Server instance
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:5173",
methods: ["GET", "POST"],
},
});
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
// All the event listening goes here
socket.on("disconnect", () => {
console.log("Client disconnected:", socket.id);
});
});
const PORT = process.env.PORT || 3000;
httpServer.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Whenever an action is made, the server maintains the authoritative state and the clients simply apply the updates so that even if some requests were missed, clients could self-correct easily on subsequent requests. This will also be important to the gameplay and bidding implementation later on.
On the client side, it was a little tricky to get state management set up correctly on the first try because unlike the standard CRUD applications, I had to consider not just the state changes that the current user’s actions cause, but also how other users’ actions propagate over.
I’m using SvelteKit in my client app and I have two stores implemented: reversiStore that keeps track of the main app state including number of players in either teams, and userStore that keeps track of which team the player is in.
When a user clicks on the Join team button, the client will save the team name in the userStore store and emit an event to the server.
<script lang="ts">
import { io } from 'socket.io-client';
import { userStore } from '$lib/stores/user-store';
// My Socket.IO connection's exposed at port 3000
const socket = io('http://localhost:3000');
</script>
<!-- [...] -->
<button onclick={() => {
userStore.joinTeam(team);
socket.emit('join-team', { team });
}}>
Join team
</button>
The server listening for the join-team event will put the client socket in the team room, get the new player count via the number of sockets attached to that room, and emit an event to all clients with this new info.
socket.on("join-team", ({ team }) => {
socket.join(team);
const playerCount = io.sockets.adapter.rooms.get(team).size;
io.emit("team-updated", {
team,
playerCount,
});
});
The final piece of this setup is the client listening for the team-updated event and updating the state with the team’s new player count in reversiStore.
socket.on("team-updated", ({ team, playerCount }) => {
reversiStore.updatePlayCount({ team, playerCount });
});
Here’s the server-client communication in action:

I’m not really a fan of Arc but boy do it be pretty for demos 😂