Skip to main content

Overview

Pica y Fija implements a turn-based system where players alternate making guesses. Each turn has a configurable time limit, and the system automatically switches turns when time expires or a guess is made.

Turn Structure

Turn States

Active Turn

Player can make guesses, timer is counting down

Waiting Turn

Player must wait for opponent, input is disabled

Player Turn Assignment

Each room tracks the current turn by player ID:
server.js
rooms[code] = {
  host: socket,
  players: [socket],
  secrets: {},
  turn: 0, // Player 0 starts first
  turnCounts: [1, 1], // Track turn numbers for each player
  options: { tiempoTurno, publica, requiereAprobacion },
  gameOver: false
};
The turn property is either 0 or 1, representing which player (by ID) currently has the active turn.

Broadcasting Turns

The server notifies both players about whose turn it is:
server.js
function broadcastTurn(room) {
  room.players.forEach((s, i) => {
    s.emit('turn', { yourTurn: i === room.turn });
  });
}
This sends a personalized message to each player - yourTurn: true for the active player and yourTurn: false for the waiting player.

Client-Side Turn Handling

Receiving Turn Updates

The client responds to turn changes by enabling/disabling controls:
index.html
socket.on('turn', data => {
  if (juegoTerminado) return;
  if (data.yourTurn) {
    guessInput.disabled = false;
    guessBtn.disabled = false;
    guessInput.value = '';
    startTimer();
    log.innerText += '🟢 ¡Es tu turno!\n';
    sndTurno.play();
  } else {
    guessInput.disabled = true;
    guessBtn.disabled = true;
    stopTimer();
    log.innerText += '🔴 Esperando turno del oponente...\n';
  }
});
When it becomes your turn:
  • Input field is enabled
  • Guess button is enabled
  • Previous guess is cleared
  • Timer starts counting down
  • Audio cue plays
When it’s not your turn:
  • Input field is disabled
  • Guess button is disabled
  • Timer stops
  • Waiting message appears

Turn Timer System

Configuration

The timer duration is set when creating a room:
index.html
function crearSala() {
  const tiempoTurno = parseInt(document.getElementById('tiempoTurno').value);
  const publica = document.getElementById('salaPublica').checked;
  const requiereAprobacion = document.getElementById('requiereAprobacion').checked;
  socket.emit('crearSala', { tiempoTurno, publica, requiereAprobacion });
  lobby.style.display = 'none';
  zonaJuego.style.display = 'block';
}
The timer value is transmitted to both players when the room is ready:
index.html
socket.on('salaLista', data => {
  miJugadorId = data.jugador === 1 ? 0 : 1;
  tiempoTurnoConfigurado = data.tiempoTurno || 60;
  lobby.style.display = 'none';
  zonaJuego.style.display = 'block';
  log.innerText += `✅ Te uniste como Jugador ${data.jugador} a la sala ${data.sala}\n`;
});

Timer Implementation

The client manages the countdown timer:
index.html
let countdown = null;
let timeLeft = 60;
let tiempoTurnoConfigurado = 60;

function startTimer() {
  timeLeft = tiempoTurnoConfigurado;
  updateTimer();
  countdown = setInterval(() => {
    timeLeft--;
    updateTimer();
    if (timeLeft <= 0) {
      stopTimer();
      guessInput.disabled = true;
      guessBtn.disabled = true;
      log.innerText += '⏰ Se acabó el tiempo del turno. Pasando turno...\n';
      socket.emit('pasarTurnoPorTiempo');
    }
  }, 1000);
}

function stopTimer() {
  clearInterval(countdown);
  timer.innerText = '';
}

function updateTimer() {
  timer.innerText = `⏳ Tiempo restante: ${timeLeft}s`;
}
The timer runs on the client side, which means it’s vulnerable to manipulation. In a production environment, consider implementing server-side timer validation.

Visual Timer Display

The countdown is displayed in real-time:
index.html
<div id="turnTimer" style="margin-top:1rem; font-size: 1.2rem; color: #ffcc00;"></div>
Updated every second with the remaining time.

Turn Switching

After Successful Guess

When a player makes a valid guess (that doesn’t win the game), the turn automatically switches:
server.js
socket.on('guess', guess => {
  // ... validation and calculation ...
  
  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;
  } else {
    room.turn = opponentId; // Switch to opponent
    broadcastTurn(room);
  }
});

After Timer Expires

When time runs out, the client sends a turn-skip event:
server.js
socket.on('pasarTurnoPorTiempo', () => {
  const room = rooms[socket.roomCode];
  if (!room) return;
  
  // Only the active player can force a turn skip
  if (room.turn !== socket.playerId) return;
  
  // Switch to the other player
  room.turn = room.turn === 0 ? 1 : 0;
  broadcastTurn(room);
});
The server validates that only the player with the active turn can trigger pasarTurnoPorTiempo, preventing malicious turn skipping.

Turn Counter

Each player has an independent turn counter that increments with each guess:
server.js
const turnNumber = room.turnCounts[socket.playerId]++;
This counter is used to display turn numbers in the results table, allowing players to track how many attempts each player has made.

Turn Counter Initialization

server.js
rooms[code] = {
  // ...
  turnCounts: [1, 1], // Both players start at turn 1
  // ...
};

Turn Number Display

Turn numbers appear in the results table:
index.html
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>
`;

Turn Validation

The server enforces turn rules strictly:
server.js
if (room.turn !== socket.playerId) {
  socket.emit('error', 'No es tu turno');
  return;
}

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

Turn Order

Only the player whose playerId matches room.turn can make guesses

Game State

Once gameOver is true, no more guesses are accepted

Socket.IO Events

Client → Server

EventPayloadDescription
pasarTurnoPorTiempoNoneNotifies server that timer expired, requesting turn switch

Server → Client

EventPayloadDescription
turn{ yourTurn } (boolean)Notifies player if it’s their turn

Best Practices

1

Disable Input During Opponent's Turn

Always disable guess inputs when yourTurn is false to prevent confusion
2

Clear Previous Guesses

Reset the input field when a new turn starts to avoid accidental re-submissions
3

Provide Visual Feedback

Use colors, icons, and audio to clearly indicate whose turn it is
4

Server-Side Validation

Never trust client-side turn tracking - always validate on the server

Game Flow Example

This sequence shows how turns flow between players, including a timeout scenario where Player 2’s timer expires and the turn automatically passes back to Player 1.

Build docs developers (and LLMs) love