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:
function sendSecret () {
socket . emit ( 'secret' , secretInput . value );
}
When confirmed, the UI updates to hide the input:
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:
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:
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:
function sendGuess () {
if ( juegoTerminado ) return ;
socket . emit ( 'guess' , guessInput . value );
}
Server-Side Processing
The server validates guesses and calculates Picas and Fijas:
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:
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:
Fijas : If the guess digit at position i matches the secret digit at the same position, increment fijas
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:
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:
< 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:
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
Event Payload Description secretsecret (string)Submits the player’s secret 4-digit number guessguess (string)Submits a guess attempt
Server → Client
Event Payload Description startNone Signals 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):
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.