The Pica y Fija client maintains several critical state variables to track the game progression, player turns, and UI state. This guide explains how client-side state management works.
Core State Variables
The client maintains the following global state variables:
let countdown = null ;
let timeLeft = 60 ;
let tiempoTurnoConfigurado = 60 ;
let miJugadorId = null ;
let juegoTerminado = false ;
State Variable Reference
countdown Stores the interval timer reference for the turn countdown. Used to clear the timer when turns end.
timeLeft Current seconds remaining in the active turn. Decrements every second during a player’s turn.
tiempoTurnoConfigurado The configured turn duration in seconds. Set when joining a room and used to reset timeLeft.
miJugadorId The current player’s ID (0 or 1). Used to distinguish “yo” (me) from “oponente” (opponent) in the results table.
juegoTerminado Boolean flag indicating if the game has ended. Prevents further guesses after a winner is determined.
Additional State Tracking
const turnosMostrados = new Set ();
The turnosMostrados Set prevents duplicate result rows from appearing in the table. Each turn is keyed by ${jugadorId}-${turno}.
Player ID Assignment
When a player joins a room, the server sends a salaLista event with player assignment:
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 ` ;
});
The player ID mapping inverts the server’s player number:
Server Player 1 → miJugadorId = 0
Server Player 2 → miJugadorId = 1
This allows proper identification of “yo” vs “oponente” in result displays.
Turn Management
The client handles turn state through the turn event from the server:
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 ' ;
}
});
Check if Game Ended
If juegoTerminado is true, the handler returns early to prevent turn processing.
Your Turn
When data.yourTurn is true:
Enable guess input and button
Clear previous guess value
Start the countdown timer
Play turn notification sound
Log turn notification
Opponent's Turn
When data.yourTurn is false:
Disable guess input and button
Stop the countdown timer
Log waiting message
Timer Implementation
The countdown timer provides visual feedback for remaining turn time:
Starting the Timer
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 );
}
Resets timeLeft to the configured turn duration
Updates the timer display immediately
Creates an interval that runs every 1000ms (1 second)
Decrements timeLeft and updates display each second
When time expires:
Stops the timer
Disables input controls
Logs timeout message
Emits pasarTurnoPorTiempo event to server
Stopping the Timer
function stopTimer () {
clearInterval ( countdown );
timer . innerText = '' ;
}
Updating the Display
function updateTimer () {
timer . innerText = `⏳ Tiempo restante: ${ timeLeft } s` ;
}
The timer display shows remaining seconds in a prominent yellow color:
< div id = "turnTimer" style = "margin-top:1rem; font-size: 1.2rem; color: #ffcc00;" ></ div >
Results Table Management
The results table displays all guesses made during the game:
Table Structure
< table id = "resultTable" hidden >
< thead class = "table-warning text-dark" >
< tr >
< th >< i class = "fas fa-user" ></ i ></ th > <!-- Jugador -->
< th >< i class = "fas fa-hashtag" ></ i ></ th > <!-- Turno -->
< th >< i class = "fas fa-keyboard" ></ i ></ th > <!-- Intento -->
< th >< i class = "fas fa-bullseye" ></ i ></ th > <!-- Picas -->
< th >< i class = "fas fa-check-circle" ></ i ></ th > <!-- Fijas -->
</ tr >
</ thead >
< tbody ></ tbody >
</ table >
Adding Results
When the server sends a result event, a new row is added:
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 ();
// ... game end detection
});
Duplicate Prevention : The turnosMostrados Set ensures each unique turn is only displayed once, even if the server sends multiple result events.
Color Coding
Player Label Color Current player (miJugadorId) “yo” White Opponent ”oponente” Orange
Result Filtering System
Players can filter the results table to view different perspectives:
< label for = "filtroTurnos" > Ver: </ label >
< select id = "filtroTurnos" onchange = " filtrarTurnos ()" class = "form-control" >
< option value = "todos" > Todos los turnos </ option >
< option value = "yo" > Solo mis turnos </ option >
< option value = "oponente" > Solo del oponente </ option >
</ select >
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' ;
}
});
}
Get Filter Value
Reads the selected option from the dropdown (todos, yo, or oponente)
Iterate Rows
Loops through all table rows using querySelectorAll('tr')
Apply Filter
Shows or hides rows based on the player label in the first column:
todos: Show all rows
yo: Show only current player’s rows
oponente: Show only opponent’s rows
Game End Detection
The client detects game completion when a result contains 4 fijas (exact matches):
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 );
}
}
Set Game Ended Flag
Sets juegoTerminado = true to prevent further guesses
Disable Controls
Disables the guess input and submit button
Stop Background Audio
Pauses the looping game music
Clear Timer
Stops the countdown timer
Display Result
Shows win/loss overlay based on which player won:
Current player won: ”🎉 ¡Ganaste!” + winner sound
Opponent won: ”💀 Perdiste” + loser sound
Overlay Display System
The overlay provides full-screen feedback for game results:
function showResult ( message , audio ) {
overlay . style . display = 'flex' ;
overlay . innerText = message ;
audio . play ();
}
The overlay element uses a semi-transparent black background with large centered text:
< div id = "overlay"
style = "display:none;
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
background:rgba(0,0,0,0.8);
color:#fff;
font-size:3rem;
justify-content:center;
align-items:center;
z-index:9999;" >
</ div >
The overlay uses z-index:9999 to ensure it appears above all other UI elements when displayed.
Alternative State Management (main.js)
The main.js file shows a simplified state management approach:
let secretSent = false ;
let countdown = null ;
let timeLeft = 60 ;
let isReady = false ;
Key differences from the main implementation:
Uses secretSent boolean instead of hiding UI elements
Includes isReady flag for player readiness
Fixed 60-second turn time (no tiempoTurnoConfigurado)
No miJugadorId tracking (simpler win/loss detection)
No duplicate turn prevention with turnosMostrados
The main.js implementation is an alternative/legacy version. The primary client implementation is in index.html with more robust state management.