Overview
Elemental Battlecards uses Socket.IO for real-time bidirectional communication between clients and server. All multiplayer functionality is handled through socket events defined in socketManager.js.
Connection Management
Server-Side Socket Initialization
module . exports = function ( io ) {
const rooms = {}; // { code: { players: [], gameState: {} } }
global . activeRooms = rooms ; // Expose for debugging
io . on ( 'connection' , ( socket ) => {
console . log ( 'Socket conectado:' , socket . id , socket . handshake . address );
// Event handlers...
});
};
Client-Side Connection
Frontend/src/scenes/createRoomScene.js
import { io } from 'socket.io-client' ;
const SERVER_URL = `http:// ${ location . hostname } :3001` ;
this . socket = io ( SERVER_URL );
this . socket . on ( 'connect' , () => {
console . log ( 'Connected:' , this . socket . id );
});
Room Management Events
create_room
Direction : Client → Server
Purpose : Creates a new game room with a unique 6-digit code.
Client Request :
this . socket . emit ( 'create_room' , ( response ) => {
console . log ( response );
// { success: true, code: "123456", role: "host" }
});
Server Handler :
socket . on ( 'create_room' , ( cb ) => {
let code ;
do {
code = generateCode (); // 6-digit number
} while ( rooms [ code ]);
rooms [ code ] = {
players: [{ socketId: socket . id , role: 'host' }],
gameState: {
currentTurn: 'host' ,
turnNumber: 0
},
createdAt: Date . now ()
};
socket . join ( code );
socket . roomCode = code ;
socket . playerRole = 'host' ;
if ( typeof cb === 'function' ) {
cb ({ success: true , code , role: 'host' });
}
io . to ( code ). emit ( 'room_created' , { code });
});
Response :
Whether room creation succeeded
The 6-digit room code (e.g., “123456”)
Player’s role in the room (“host”)
room_created
Direction : Server → Client(s)
Purpose : Notifies all clients in the room that it was successfully created.
Payload :
Client Handler :
this . socket . on ( 'room_created' , ({ code }) => {
console . log ( 'Room created with code:' , code );
this . currentRoom = code ;
});
join_room
Direction : Client → Server
Purpose : Join an existing room using a 6-digit code.
Client Request :
const code = "123456" ;
this . socket . emit ( 'join_room' , { code }, ( response ) => {
if ( response . success ) {
console . log ( 'Joined room:' , response . code );
console . log ( 'Role:' , response . role ); // "guest"
} else {
console . error ( 'Failed to join:' , response . message );
}
});
Server Handler :
socket . on ( 'join_room' , ( data , cb ) => {
const code = ( data && data . code ) ? data . code . toString (). replace ( / \s + / g , '' ) : null ;
if ( ! code || ! rooms [ code ]) {
if ( typeof cb === 'function' ) {
cb ({ success: false , message: 'Sala no encontrada.' });
}
return ;
}
const room = rooms [ code ];
if ( room . players . length >= 2 ) {
if ( typeof cb === 'function' ) {
cb ({ success: false , message: 'Sala llena.' });
}
return ;
}
room . players . push ({ socketId: socket . id , role: 'guest' });
socket . join ( code );
socket . roomCode = code ;
socket . playerRole = 'guest' ;
if ( typeof cb === 'function' ) {
cb ({ success: true , code , role: 'guest' });
}
// Notify both players
io . to ( code ). emit ( 'player_joined' , {
players: room . players . length ,
canStart: room . players . length === 2
});
// Auto-start game when room is full
if ( room . players . length === 2 ) {
io . to ( code ). emit ( 'game_start' , {
currentTurn: 'host' ,
hostId: room . players [ 0 ]. socketId ,
guestId: room . players [ 1 ]. socketId
});
}
});
Request Payload :
6-digit room code to join
Response (Success) :
{
"success" : true ,
"code" : "123456" ,
"role" : "guest"
}
Response (Failure) :
{
"success" : false ,
"message" : "Sala no encontrada." | "Sala llena."
}
player_joined
Direction : Server → All Clients in Room
Purpose : Notifies when a player joins the room.
Payload :
{
players : 2 , // Total players in room
canStart : true // Whether game can start (2 players)
}
Client Handler :
this . socket . on ( 'player_joined' , ({ players , canStart }) => {
this . playersInRoom = players ;
if ( canStart ) {
this . startButton . textContent = '¡Iniciar Partida!' ;
}
});
player_left
Direction : Server → All Clients in Room
Purpose : Notifies when a player disconnects from the room.
No Payload
Client Handler :
this . socket . on ( 'player_left' , () => {
this . playersInRoom = Math . max ( 1 , this . playersInRoom - 1 );
console . log ( 'Player left, waiting for replacement...' );
});
Game Flow Events
game_start
Direction : Server → All Clients in Room
Purpose : Signals that the game can begin (2 players connected).
Payload :
{
currentTurn : "host" , // Who starts
hostId : "socket_id_1" , // Host socket ID
guestId : "socket_id_2" , // Guest socket ID
}
Client Handler :
this . socket . on ( 'game_start' , ( data ) => {
console . log ( 'Game starting...' , data );
// Transition to game scene
this . scene . start ( 'GameSceneLAN' , {
roomCode: this . currentRoom ,
socket: this . socket ,
playerRole: this . playerRole ,
gameStartData: data
});
});
game_event
Direction : Bidirectional (Client ↔ Server ↔ Other Client)
Purpose : Generic event for all in-game actions (place card, attack, fuse).
Client Send :
this . socket . emit ( 'game_event' , {
type: 'place_card' ,
position: 3 ,
cardIndex: 1
});
this . socket . emit ( 'game_event' , {
type: 'attack' ,
attackerIndex: 2 ,
targetIndex: 4 ,
result: 'attacker_wins'
});
this . socket . emit ( 'game_event' , {
type: 'fuse' ,
index1: 0 ,
index2: 1 ,
resultType: 'fuego' ,
resultLevel: 2
});
Server Handler :
socket . on ( 'game_event' , ( payload ) => {
const code = socket . roomCode ;
if ( ! code || ! rooms [ code ]) return ;
// Rebroadcast to other player(s) in room
socket . to ( code ). emit ( 'game_event' , payload );
console . log ( `Evento ${ payload . type } reenviado en sala ${ code } ` );
});
Client Receive :
this . socket . on ( 'game_event' , ( payload ) => {
switch ( payload . type ) {
case 'place_card' :
this . handleOpponentPlaceCard ( payload );
break ;
case 'attack' :
this . handleOpponentAttack ( payload );
break ;
case 'fuse' :
this . handleOpponentFuse ( payload );
break ;
}
});
Event Types :
Payload :{
type : "place_card" ,
position : 3 , // Field position (0-5)
cardIndex : 1 // Index in hand (optional)
}
Places a card face-down on the field. Payload :{
type : "attack" ,
attackerIndex : 2 , // Attacker field position
targetIndex : 4 , // Target field position
result : "attacker_wins" | "defender_wins" | "neutral"
}
Resolves combat between two cards. Payload :{
type : "fuse" ,
index1 : 0 , // First card position
index2 : 1 , // Second card position
resultType : "fuego" , // Type of fused card
resultLevel : 2 // Level of fused card
}
Combines two identical cards to level up.
Turn Management Events
end_turn
Direction : Client → Server
Purpose : Player signals their turn is complete.
Client Send :
this . socket . emit ( 'end_turn' , {
playerRole: this . playerRole // "host" or "guest"
});
Server Handler :
socket . on ( 'end_turn' , ( payload ) => {
const code = socket . roomCode ;
if ( ! code || ! rooms [ code ]) return ;
const room = rooms [ code ];
const currentRole = socket . playerRole ;
// Alternate turn
room . gameState . currentTurn = currentRole === 'host' ? 'guest' : 'host' ;
room . gameState . turnNumber ++ ;
// Notify both players
io . to ( code ). emit ( 'turn_changed' , {
currentTurn: room . gameState . currentTurn ,
turnNumber: room . gameState . turnNumber
});
});
turn_changed
Direction : Server → All Clients in Room
Purpose : Notifies all players of turn change.
Payload :
{
currentTurn : "guest" , // "host" or "guest"
turnNumber : 5 // Total turns elapsed
}
Client Handler :
this . socket . on ( 'turn_changed' , ({ currentTurn , turnNumber }) => {
console . log ( `Turn ${ turnNumber } : ${ currentTurn } 's turn` );
if ( currentTurn === this . playerRole ) {
this . startPlayerTurn ();
} else {
this . startOpponentTurn ();
}
});
Connection Events
disconnect
Direction : Client → Server (Automatic)
Purpose : Cleanup when a player disconnects.
Server Handler :
socket . on ( 'disconnect' , () => {
const code = socket . roomCode ;
console . log ( 'Socket desconectado:' , socket . id , 'sala:' , code );
if ( code && rooms [ code ]) {
// Remove player from room
rooms [ code ]. players = rooms [ code ]. players . filter (
p => p . socketId !== socket . id
);
if ( rooms [ code ]. players . length === 0 ) {
// Delete empty room
delete rooms [ code ];
console . log ( `Sala ${ code } eliminada (vacía)` );
} else {
// Notify remaining player
io . to ( code ). emit ( 'player_left' );
}
}
});
Room Data Structure
The server maintains room state in memory:
const rooms = {
"123456" : {
players: [
{ socketId: "abc123" , role: "host" },
{ socketId: "def456" , role: "guest" }
],
gameState: {
currentTurn: "host" , // "host" or "guest"
turnNumber: 0 // Increments each turn
},
createdAt: 1234567890 // Timestamp
}
}
Rooms are stored in memory and cleared when all players disconnect. For persistent games, implement database storage.
Complete Event Flow Example
Creating and Joining a Game
Playing a Turn
Error Handling
Client-Side
this . socket . on ( 'connect_error' , ( error ) => {
console . error ( 'Connection failed:' , error );
alert ( 'No se pudo conectar al servidor' );
});
this . socket . on ( 'error' , ( error ) => {
console . error ( 'Socket error:' , error );
});
Server-Side
io . on ( 'connection' , ( socket ) => {
socket . on ( 'error' , ( error ) => {
console . error ( 'Socket error for' , socket . id , error );
});
});
Testing Socket Events
You can test socket events using the browser console:
// In browser console
const { io } = require ( 'socket.io-client' );
const socket = io ( 'http://localhost:3001' );
socket . on ( 'connect' , () => {
console . log ( 'Connected:' , socket . id );
// Create room
socket . emit ( 'create_room' , ( res ) => {
console . log ( 'Room created:' , res );
});
});
// Listen to all events
const originalEmit = socket . emit ;
socket . emit = function ( ... args ) {
console . log ( 'Emitting:' , args [ 0 ], args [ 1 ]);
return originalEmit . apply ( socket , args );
};
const onevent = socket . onevent ;
socket . onevent = function ( packet ) {
console . log ( 'Receiving:' , packet . data [ 0 ], packet . data [ 1 ]);
return onevent . call ( socket , packet );
};
Best Practices
Validate on Server Always validate game events server-side to prevent cheating
Acknowledge Callbacks Use acknowledgment callbacks for critical operations
Handle Disconnects Implement graceful handling for unexpected disconnections
Room Cleanup Delete empty rooms to prevent memory leaks
Debugging
Enable Socket.IO debug logs:
localStorage . debug = 'socket.io-client:socket' ;
// Reload page to see detailed socket logs
DEBUG = socket.io* npm run dev
Access active rooms for debugging:
// Server-side (in console or debug route)
console . log ( global . activeRooms );
Next Steps
Game Scenes See how scenes use socket events
Contributing Learn how to extend the socket system