Skip to main content

Overview

Pica y Fija uses a real-time server architecture built on Node.js, Express, and Socket.IO to enable multiplayer gameplay. The server manages room creation, player connections, game state, and turn-based logic.

Technology Stack

Express

HTTP server for serving static assets and handling requests

Socket.IO

WebSocket library for real-time bidirectional communication

Node.js

Runtime environment for server-side JavaScript

Vite

Development server and build tool for the frontend

Server Initialization

The server setup combines Express for static file serving and Socket.IO for real-time communication:
server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const path = require('path');

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: "*" } });

// Serve static files from the public directory
app.use(express.static(path.join(__dirname, 'public')));

const PORT = process.env.PORT || 9090;
server.listen(PORT, () => {
    console.log(`Servidor Socket.IO corriendo en el puerto ${PORT}`);
});
The server uses CORS with origin: "*" to allow connections from any client. In production, you should restrict this to specific domains.

Room Management System

The server maintains game state using in-memory data structures. Each room is identified by a unique code and contains all game-related information.

Room Data Structure

server.js
let rooms = {}; // roomCode -> { host, players, options }
let publicRooms = [];

Room Object Schema

Each room in the rooms object has the following structure:
rooms[code] = {
    host: socket,              // Socket instance of the room creator
    players: [socket],         // Array of connected player sockets (max 2)
    secrets: {},               // Player secrets: { playerId: "1234" }
    turn: 0,                   // Current turn: 0 or 1
    turnCounts: [1, 1],        // Turn counters for each player
    options: {                 // Room configuration
        tiempoTurno,           // Turn time limit in seconds
        publica,               // Whether room is public
        requiereAprobacion     // Whether host approval is required
    },
    gameOver: false            // Game completion flag
};
host
Socket
The Socket.IO socket instance of the player who created the room
players
Array<Socket>
Array containing the socket instances of connected players. Maximum of 2 players.
secrets
Object
Maps player IDs to their secret 4-digit numbers: { 0: "1234", 1: "5678" }
turn
number
Indicates which player’s turn it is: 0 for player 1, 1 for player 2
turnCounts
Array<number>
Tracks the turn number for each player: [playerOneTurns, playerTwoTurns]
options.tiempoTurno
number
Time limit for each turn in seconds
options.publica
boolean
If true, the room is added to the public rooms list
options.requiereAprobacion
boolean
If true, the host must approve players joining the room
gameOver
boolean
Set to true when a player guesses correctly (4 fijas)

Room Code Generation

Room codes are generated as random 6-character alphanumeric strings:
server.js
function generateRoomCode() {
    return Math.random().toString(36).substring(2, 8).toUpperCase();
}
Example codes: XK9P2L, A7B3QW

Game State Management

Player Assignment

When players join a room, they are assigned sequential IDs:
server.js
// Room creator (host)
socket.playerId = 0;  // Player 1

// Second player
socket.playerId = 1;  // Player 2
Each socket stores its roomCode and playerId for tracking:
socket.roomCode = code;
socket.playerId = 0; // or 1
socket.join(code);   // Join Socket.IO room

Turn Management

Turns alternate between players after each valid guess:
server.js
function broadcastTurn(room) {
    room.players.forEach((s, i) => {
        s.emit('turn', { yourTurn: i === room.turn });
    });
}
The turn value switches between 0 and 1:
server.js
const opponentId = socket.playerId === 0 ? 1 : 0;
room.turn = opponentId;
broadcastTurn(room);

Game Logic: Picas y Fijas

The core game algorithm compares the guess against the secret number:
server.js
function getPicasFijas(secret, guess) {
    let fijas = 0, picas = 0;
    for (let i = 0; i < 4; i++) {
        if (guess[i] === secret[i]) fijas++;           // Correct digit, correct position
        else if (secret.includes(guess[i])) picas++;   // Correct digit, wrong position
    }
    return { fijas, picas };
}
  • Fijas (Fixed): Digits that match both value and position
  • Picas (Almost): Digits that exist in the secret but are in the wrong position
  • A winning guess has fijas === 4
  • Each digit must be unique (validated on both secret submission and guess)

Win Condition

The game ends when a player achieves 4 fijas:
server.js
const result = getPicasFijas(opponentSecret, guess);
const turnNumber = room.turnCounts[socket.playerId]++;

const data = {
    jugadorId: socket.playerId,
    jugador: socket.playerId === 0 ? 'yo' : 'oponente',
    turno: turnNumber,
    intento: guess,
    fijas: result.fijas,
    picas: result.picas
};

room.players.forEach(p => p.emit('result', data));

if (result.fijas === 4) {
    room.gameOver = true;  // End the game
} else {
    room.turn = opponentId;
    broadcastTurn(room);
}

Connection Lifecycle

1. Player Connects

io.on('connection', socket => {
    // Socket is connected and ready to receive events
});

2. Room Creation or Joining

Players either create a new room or join an existing one:
server.js
function joinRoom(socket, code) {
    const room = rooms[code];
    socket.join(code);
    room.players.push(socket);
    socket.roomCode = code;
    socket.playerId = 1;

    room.players[0].emit('info', 'Conectado como Jugador 1');
    room.players[1].emit('info', 'Conectado como Jugador 2');

    if (room.players.length === 2) {
        room.players.forEach((player, idx) => {
            player.emit('salaLista', {
                jugador: idx + 1,
                sala: code,
                tiempoTurno: room.options.tiempoTurno
            });
        });
    }
}

3. Game Play

Players submit secrets, then alternate guessing until someone wins.

4. Disconnection Handling

server.js
socket.on('disconnect', () => {
    const code = socket.roomCode;
    const room = rooms[code];

    if (room) {
        room.players = room.players.filter(p => p !== socket);
        if (room.players.length === 0) {
            delete rooms[code];
            publicRooms = publicRooms.filter(c => c !== code);
        } else {
            room.players[0].emit('info', 'Tu oponente se ha desconectado');
        }
    }
});
When a player disconnects, if the room becomes empty, it is deleted from memory. There is no persistence or reconnection logic.

Alternative WebSocket Implementation

The project also includes server2.js, a simpler WebSocket implementation using the ws library:
server2.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

let players = [];
let secrets = {};
let turn = 0;
let turnCounts = [1, 1];
This implementation:
  • Supports only one game at a time (no room system)
  • Uses native WebSocket instead of Socket.IO
  • Has simpler message handling with JSON stringification
  • Automatically resets state on disconnection
The main production server uses server.js with Socket.IO for its advanced room management and scalability.

Scalability Considerations

The current architecture stores all game state in memory. This means:
  • State is lost on server restart
  • Horizontal scaling requires a shared state solution (Redis, database)
  • Room limits are bounded by available memory
For production deployment at scale, consider:
  1. Redis Adapter for Socket.IO to enable multi-server deployments
  2. Database persistence for game history and statistics
  3. Session recovery to handle temporary disconnections
  4. Rate limiting to prevent abuse of room creation

Build docs developers (and LLMs) love