Skip to main content

Overview

Pica y Fija is a multiplayer number guessing game where each player selects a secret 4-digit number, then takes turns trying to guess their opponent’s number. The game provides feedback after each guess using “Picas” (correct digit, wrong position) and “Fijas” (correct digit, correct position).

Game Flow

Secret Number Selection

Rules

Secret numbers must follow strict validation rules:

4 Digits

Must be exactly 4 digits (0-9)

Unique Digits

All 4 digits must be different

Client-Side Submission

Players submit their secret number when ready:
index.html
function sendSecret() {
  socket.emit('secret', secretInput.value);
}
When confirmed, the UI updates to hide the input:
index.html
socket.on('info', message => {
  log.innerText += `🟡 ${message}\n`;
  if (message.includes('Número secreto guardado')) {
    numSecretSpan.innerText = `Número secreto: ${secretInput.value}`;
    secretInput.style.display = 'none';
    btnSecret.style.display = 'none';
    sndEnviarSecreto.play();
  }
});

Server-Side Validation

The server validates secret numbers before storing them:
server.js
socket.on('secret', secret => {
  const room = rooms[socket.roomCode];
  if (!room) return;

  if (!/^\d{4}$/.test(secret) || new Set(secret).size !== 4) {
    socket.emit('error', 'El número debe tener 4 cifras diferentes');
    return;
  }

  room.secrets[socket.playerId] = secret;
  socket.emit('info', 'Número secreto guardado');

  if (room.secrets[0] && room.secrets[1]) {
    io.to(socket.roomCode).emit('start');
    setTimeout(() => broadcastTurn(room), 200);
  }
});
The regex /^\d{4}$/ ensures exactly 4 digits, while new Set(secret).size !== 4 checks that all digits are unique.

Game Start Trigger

Once both players have submitted valid secret numbers, the game automatically starts:
index.html
socket.on('start', () => {
  lobby.style.display = 'none';
  zonaJuego.style.display = 'block';
  sndJuego.play();
  resultTable.hidden = false;
  log.innerText += '🎮 ¡El juego ha comenzado!\n';
});

Guessing Mechanism

Making a Guess

Players can only guess during their turn:
index.html
function sendGuess() {
  if (juegoTerminado) return;
  socket.emit('guess', guessInput.value);
}

Server-Side Processing

The server validates guesses and calculates Picas and Fijas:
server.js
socket.on('guess', guess => {
  const room = rooms[socket.roomCode];
  if (!room) return;

  if (room.turn !== socket.playerId) {
    socket.emit('error', 'No es tu turno');
    return;
  }

  if (room.gameOver) {
    socket.emit('error', 'El juego ya terminó');
    return;
  }

  if (!/^\d{4}$/.test(guess) || new Set(guess).size !== 4) {
    socket.emit('error', 'El intento debe tener 4 cifras distintas');
    return;
  }

  const opponentId = socket.playerId === 0 ? 1 : 0;
  const opponentSecret = room.secrets[opponentId];
  if (!opponentSecret) {
    socket.emit('error', 'El oponente aún no ha elegido su número');
    return;
  }

  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 someone wins, end game
  if (result.fijas === 4) {
    room.gameOver = true;
  } else {
    room.turn = opponentId;
    broadcastTurn(room);
  }
});

Picas y Fijas Algorithm

The core algorithm that calculates feedback:
server.js
function getPicasFijas(secret, guess) {
  let fijas = 0, picas = 0;
  for (let i = 0; i < 4; i++) {
    if (guess[i] === secret[i]) fijas++;
    else if (secret.includes(guess[i])) picas++;
  }
  return { fijas, picas };
}
The algorithm iterates through each digit position:
  1. Fijas: If the guess digit at position i matches the secret digit at the same position, increment fijas
  2. Picas: If the guess digit exists somewhere in the secret but not at the current position, increment picas
Example:
  • Secret: 1234
  • Guess: 1543
  • Result: 1 Fija (the 1), 2 Picas (the 3 and 4)

Feedback Display

Result Event

Both players receive the result of each guess:
index.html
socket.on('result', data => {
  const key = `${data.jugadorId}-${data.turno}`;
  if (turnosMostrados.has(key)) return;
  turnosMostrados.add(key);

  const jugadorLabel = data.jugadorId === miJugadorId ? 'yo' : 'oponente';
  const jugadorColor = data.jugadorId === miJugadorId ? 'white' : 'orange';

  const row = document.createElement('tr');
  row.innerHTML = `
    <td style="color:${jugadorColor}">${jugadorLabel}</td>
    <td style="color:${jugadorColor}">${data.turno}</td>
    <td style="color:${jugadorColor}">${data.intento}</td>
    <td style="color:${jugadorColor}">${data.picas}</td>
    <td style="color:${jugadorColor}">${data.fijas}</td>
  `;
  table.insertBefore(row, table.firstChild);
  filtrarTurnos();

  if (data.fijas === 4) {
    juegoTerminado = true;
    guessInput.disabled = true;
    guessBtn.disabled = true;
    sndJuego.pause(); 
    stopTimer();
    if (data.jugadorId === miJugadorId) {
      showResult('🎉 ¡Ganaste!', sndGanador);
    } else {
      showResult('💀 Perdiste', sndPerdedor);
    }
  }
});

Visual Indicators

The game uses color-coded feedback:
  • White: Your guesses
  • Orange: Opponent’s guesses
  • Results are displayed in a scrollable table with newest attempts at the top

Audio Feedback

The game includes immersive audio cues for key events:
index.html
<audio id="sndEnviarSecreto" src="assets/audio/enviar_secreto.mp3"></audio>
<audio id="sndTurno" src="assets/audio/turno.mp3"></audio>
<audio id="sndJuego" src="assets/audio/juego.mp3" loop></audio>
<audio id="sndGanador" src="assets/audio/ganador.mp3"></audio>
<audio id="sndPerdedor" src="assets/audio/perdedor.mp3"></audio>

Secret Submitted

sndEnviarSecreto.play() when number is saved

Your Turn

sndTurno.play() when it’s your turn to guess

Background Music

sndJuego.play() loops during active gameplay

Turn History Filtering

Players can filter the results table to see specific turns:
index.html
function filtrarTurnos() {
  const filtro = document.getElementById('filtroTurnos').value;
  const rows = table.querySelectorAll('tr');
  rows.forEach(row => {
    const jugador = row.children[0]?.textContent;
    if (filtro === 'todos') {
      row.style.display = '';
    } else if (filtro === 'yo') {
      row.style.display = jugador === 'yo' ? '' : 'none';
    } else if (filtro === 'oponente') {
      row.style.display = jugador === 'oponente' ? '' : 'none';
    }
  });
}
The game uses a Set to prevent duplicate result displays: turnosMostrados.has(key) ensures each turn is shown only once even if the event fires multiple times.

Socket.IO Events

Client → Server

EventPayloadDescription
secretsecret (string)Submits the player’s secret 4-digit number
guessguess (string)Submits a guess attempt

Server → Client

EventPayloadDescription
startNoneSignals that both secrets are set and game begins
result{ jugadorId, jugador, turno, intento, fijas, picas }Provides feedback for a guess
errormessage (string)Validation errors
infomessage (string)Confirmation messages

Win Condition

The game ends when a player achieves 4 Fijas (all digits correct in correct positions):
server.js
if (result.fijas === 4) {
  room.gameOver = true;
} else {
  room.turn = opponentId;
  broadcastTurn(room);
}
The gameOver flag prevents further guesses and the winning player sees a victory overlay while the losing player sees a defeat message.

Build docs developers (and LLMs) love