Skip to content

Commit b835b09

Browse files
feat: update game settings for visibility and bluff options in Pirámide and Truco
- Changed default hand visibility from "private" to "public" and bluffEnabled from true to false in multiple files. - Updated tests to reflect new default settings. - Modified UI elements to display updated visibility options. - Implemented game logic to prevent challenging claims when cards are visible. - Added new Truco game with multiplayer functionality using Socket.io. - Created initial game mechanics for Truco including card dealing, scoring, and player actions. - Enhanced styling for the Truco game interface.
1 parent d0d7245 commit b835b09

16 files changed

Lines changed: 663 additions & 83 deletions

File tree

PIRAMIDE-MOBILE-MULTIJUGADOR/functions/src/game.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ const VALID_VISIBILITY = new Set(["private", "public"]);
1414
const DEFAULT_SETTINGS = Object.freeze({
1515
mode: "classic",
1616
cardsPerPlayer: 4,
17-
handVisibility: "private",
18-
bluffEnabled: true,
17+
handVisibility: "public",
18+
bluffEnabled: false,
1919
powersEnabled: false,
2020
floorMultipliers: [1, 2, 4, 8, 16]
2121
});

PIRAMIDE-MOBILE-MULTIJUGADOR/functions/test/game.test.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ const {
1111
resolveClaimOutcome
1212
} = require("../src/game");
1313

14+
test("la configuración predeterminada usa cartas visibles", () => {
15+
assert.equal(DEFAULT_SETTINGS.handVisibility, "public");
16+
assert.equal(DEFAULT_SETTINGS.bluffEnabled, false);
17+
});
18+
1419
test("la modalidad pública desactiva el bluff", () => {
1520
const settings = normalizeSettings({ handVisibility: "public", bluffEnabled: true });
1621
assert.equal(settings.handVisibility, "public");

PIRAMIDE-MOBILE-MULTIJUGADOR/js/app.js

Lines changed: 15 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const model = {
1111
privateHand: {},
1212
publicHands: {},
1313
busy: false,
14-
confirmStart: false,
1514
revealedCardId: null,
1615
claimPicker: false,
1716
claimSelection: null,
@@ -151,7 +150,6 @@ function leaveLocalRoom() {
151150
room: null,
152151
privateHand: {},
153152
publicHands: {},
154-
confirmStart: false,
155153
revealedCardId: null,
156154
claimPicker: false,
157155
claimSelection: null
@@ -164,7 +162,7 @@ function subscribeToHandVisibility() {
164162
cleanupHandSubscription();
165163
return;
166164
}
167-
const visibility = model.room.settings?.handVisibility || "private";
165+
const visibility = model.room.settings?.handVisibility || "public";
168166
if (subscribedHandMode === visibility) return;
169167
cleanupHandSubscription();
170168
subscribedHandMode = visibility;
@@ -309,51 +307,24 @@ function visibilitySelector(settings) {
309307
const isPublic = settings.handVisibility === "public";
310308
return `<fieldset class="visibility-fieldset">
311309
<legend>Visibilidad de las cartas</legend>
312-
<label class="choice-card ${!isPublic ? "selected" : ""}">
313-
<input type="radio" name="handVisibility" value="private" ${!isPublic ? "checked" : ""}>
314-
<span class="choice-dot"></span>
315-
<span><strong>Cartas privadas</strong><small>Cada jugador ve solamente su mano.</small></span>
316-
</label>
317310
<label class="choice-card ${isPublic ? "selected" : ""}">
318311
<input type="radio" name="handVisibility" value="public" ${isPublic ? "checked" : ""}>
319312
<span class="choice-dot"></span>
320-
<span><strong>Visibles para todos</strong><small>Todas las manos permanecen a la vista.</small></span>
313+
<span><strong>Cartas visibles</strong><small>Todas las manos permanecen a la vista.</small></span>
314+
</label>
315+
<label class="choice-card ${!isPublic ? "selected" : ""}">
316+
<input type="radio" name="handVisibility" value="private" ${!isPublic ? "checked" : ""}>
317+
<span class="choice-dot"></span>
318+
<span><strong>Cartas abajo</strong><small>Cada jugador mantiene su mano oculta en su teléfono.</small></span>
321319
</label>
322320
</fieldset>`;
323321
}
324322

325323
function settingsReadOnly(settings) {
326324
const isPublic = settings.handVisibility === "public";
327325
return `<div class="readonly-settings">
328-
<span><small>Visibilidad</small><strong>${isPublic ? "Visibles para todos" : "Cartas privadas"}</strong></span>
326+
<span><small>Visibilidad</small><strong>${isPublic ? "Cartas visibles" : "Cartas abajo"}</strong></span>
329327
<span><small>Cartas</small><strong>${settings.cardsPerPlayer} por jugador</strong></span>
330-
<span><small>Multiplicadores</small><strong>${(settings.floorMultipliers || [1,2,4,8,16]).map(value => ${value}`).join(", ")}</strong></span>
331-
</div>`;
332-
}
333-
334-
function startSummary(settings) {
335-
const visibility = settings.handVisibility === "public" ? "Visibles para todos" : "Cartas privadas";
336-
return `<dl class="start-summary">
337-
<div><dt>Modo</dt><dd>Clásico</dd></div>
338-
<div><dt>Jugadores</dt><dd>${players().length}</dd></div>
339-
<div><dt>Cartas por jugador</dt><dd>${settings.cardsPerPlayer}</dd></div>
340-
<div><dt>Visibilidad</dt><dd>${visibility}</dd></div>
341-
<div><dt>Multiplicadores</dt><dd>${settings.floorMultipliers.map(value => ${value}`).join(", ")}</dd></div>
342-
</dl>`;
343-
}
344-
345-
function startConfirmation(settings) {
346-
if (!model.confirmStart) return "";
347-
return `<div class="modal-layer" role="presentation">
348-
<section class="modal panel" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
349-
<span class="eyebrow">Revisa la mesa</span>
350-
<h2 id="confirm-title">¿Todo listo?</h2>
351-
${startSummary(settings)}
352-
<div class="modal-actions">
353-
<button class="btn ghost" data-action="cancel-start">Volver</button>
354-
<button class="btn primary" data-action="confirm-start" ${model.busy ? "disabled" : ""}>Confirmar e iniciar</button>
355-
</div>
356-
</section>
357328
</div>`;
358329
}
359330

@@ -384,19 +355,14 @@ function renderLobby() {
384355
${[3,4,5,6].map(value => `<option value="${value}" ${settings.cardsPerPlayer === value ? "selected" : ""}>${value}</option>`).join("")}
385356
</select>
386357
</label>
387-
<div class="multiplier-setting">
388-
<span><strong>Multiplicadores por piso</strong><small>Define la carga de cada nivel.</small></span>
389-
<div class="multiplier-inputs">${(settings.floorMultipliers || [1,2,4,8,16]).map((value, index) => `<label><small>${index + 1}</small><input type="number" name="floorMultiplier${index}" min="1" max="99" value="${value}"></label>`).join("")}</div>
390-
</div>
391358
<button type="button" class="btn ghost full" data-action="close-room">Cerrar sala para todos</button>
392359
</form>` : settingsReadOnly(settings)}
393360
</section>
394361
</div>
395362
396-
${isHost() ? `<button class="btn primary start-room" data-action="review-start" ${players().length < 2 || model.busy ? "disabled" : ""}>
397-
${players().length < 2 ? "Esperando al menos 2 jugadores" : "Revisar e iniciar"} <span>→</span>
363+
${isHost() ? `<button class="btn primary start-room" data-action="start-room" ${players().length < 2 || model.busy ? "disabled" : ""}>
364+
${players().length < 2 ? "Esperando al menos 2 jugadores" : "Iniciar partida"} <span>→</span>
398365
</button>` : `<div class="waiting-host"><i></i> Esperando al anfitrión…</div>`}
399-
${startConfirmation(settings)}
400366
</section>`;
401367
}
402368

@@ -442,13 +408,14 @@ function activeClaimMarkup() {
442408
const claimant = model.room.players[claim.claimantUid];
443409
const target = model.room.players[claim.targetUid];
444410
const isTarget = claim.targetUid === model.user.uid;
411+
const canChallenge = model.room.settings.handVisibility === "private";
445412
return `<section class="claim-banner panel">
446413
<span class="eyebrow">Declaración pendiente</span>
447414
<h3>${escapeHTML(claimant?.name)} declara ${escapeHTML(claim.claimedValue)}</h3>
448415
<p>Le entrega una carga ×${claim.multiplier} a <strong>${escapeHTML(target?.name)}</strong>.</p>
449416
${isTarget ? `<div class="claim-actions">
450417
<button class="btn secondary" data-action="resolve-claim" data-decision="accept">Aceptar carga</button>
451-
<button class="btn danger" data-action="resolve-claim" data-decision="challenge">Desafiar</button>
418+
${canChallenge ? `<button class="btn danger" data-action="resolve-claim" data-decision="challenge">Desafiar</button>` : ""}
452419
</div>` : `<small>Esperando la decisión de ${escapeHTML(target?.name)}…</small>`}
453420
</section>`;
454421
}
@@ -610,7 +577,7 @@ function settingsFromForm(form) {
610577
scoringEnabled: true,
611578
maxPlayers: 8,
612579
multiplierType: "double",
613-
floorMultipliers: [0, 1, 2, 3, 4].map(index => Number(form.elements[`floorMultiplier${index}`].value))
580+
floorMultipliers: [1, 2, 4, 8, 16]
614581
};
615582
}
616583

@@ -675,16 +642,7 @@ root.addEventListener("click", event => {
675642
leaveLocalRoom();
676643
});
677644
}
678-
if (action === "review-start") {
679-
model.confirmStart = true;
680-
render();
681-
}
682-
if (action === "cancel-start") {
683-
model.confirmStart = false;
684-
render();
685-
}
686-
if (action === "confirm-start") {
687-
model.confirmStart = false;
645+
if (action === "start-room") {
688646
run(() => api.startOnlineGame({ code: model.roomCode }));
689647
}
690648
if (action === "reveal-card") {
@@ -782,7 +740,7 @@ function demoRoom(mode) {
782740
code: "VACILA",
783741
hostUid: "demo-host",
784742
status: "lobby",
785-
settings: { mode: "classic", handVisibility: "private", bluffEnabled: true, powersEnabled: false, scoringEnabled: true, cardsPerPlayer: 4, maxPlayers: 8, floorMultipliers: [1,2,4,8,16] },
743+
settings: { mode: "classic", handVisibility: "public", bluffEnabled: false, powersEnabled: false, scoringEnabled: true, cardsPerPlayer: 4, maxPlayers: 8, floorMultipliers: [1,2,4,8,16] },
786744
players: demoPlayers
787745
};
788746
}

PIRAMIDE-MOBILE-MULTIJUGADOR/js/firebase-service.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ const DEFAULT_SETTINGS = Object.freeze({
3838
cardsPerPlayer: 4,
3939
multiplierType: "double",
4040
floorMultipliers: [1, 2, 4, 8, 16],
41-
handVisibility: "private",
42-
bluffEnabled: true,
41+
handVisibility: "public",
42+
bluffEnabled: false,
4343
powersEnabled: false,
4444
scoringEnabled: true,
4545
maxPlayers: 8
@@ -573,6 +573,9 @@ async function resolveClaimClient(roomCode, decision) {
573573
if (!claim || claim.targetUid !== user.uid || claim.status !== "pending") {
574574
throw clientError("permission-denied", "Solo el objetivo puede resolver esta declaración.");
575575
}
576+
if (room.settings.handVisibility === "public" && decision === "challenge") {
577+
throw clientError("failed-precondition", "No se puede desafiar cuando las cartas son visibles.");
578+
}
576579
const lock = await runTransaction(ref(database, `rooms/${code}/activeClaim/status`), status => (
577580
status === "pending" ? "resolving" : undefined
578581
), { applyLocally: false });

TRUCO-MOBILE/Nuevo Documento de texto.txt

Whitespace-only changes.

TRUCO-MOBILE/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Truco - Sala (multijugador)
2+
3+
Proyecto inicial para el juego Truco con salas multijugador usando Socket.io.
4+
5+
Instalación y ejecución:
6+
7+
```bash
8+
npm install
9+
npm start
10+
```
11+
12+
Abre `http://localhost:3000` en el navegador.
13+
14+
Características iniciales:
15+
- Crear/Unirse a sala (máx 8 jugadores)
16+
- Visualizar lista de jugadores
17+
- Iniciar juego (por ahora envía evento de inicio)
18+
19+
Próximos pasos:
20+
- Implementar motor de reglas del Truco (envido, truco, puntos)
21+
- UI de cartas y flujo de juego
22+
- Soporte móvil y pulir estética

TRUCO-MOBILE/client.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
const nameInput = document.getElementById('name');
3+
const roomInput = document.getElementById('room');
4+
const joinBtn = document.getElementById('joinBtn');
5+
const roomInfo = document.getElementById('roomInfo');
6+
const roomNameSpan = document.getElementById('roomName');
7+
const playersList = document.getElementById('players');
8+
const startBtn = document.getElementById('startBtn');
9+
const lobby = document.getElementById('lobby');
10+
const gameSection = document.getElementById('game');
11+
const status = document.getElementById('status');
12+
const cardsArea = document.getElementById('cardsArea');
13+
const connErrorDiv = document.getElementById('connError');
14+
const btnTruco = document.getElementById('btnTruco');
15+
const btnEnvido = document.getElementById('btnEnvido');
16+
17+
// If socket.io client script isn't loaded (e.g., opened via file://), show helpful message
18+
if (typeof io === 'undefined') {
19+
if (connErrorDiv) connErrorDiv.classList.remove('hidden');
20+
// disable interactive buttons
21+
joinBtn.disabled = true;
22+
startBtn.disabled = true;
23+
if (btnTruco) btnTruco.disabled = true;
24+
if (btnEnvido) btnEnvido.disabled = true;
25+
console.error('Socket.io client not loaded. Start the server and open http://localhost:3000');
26+
return;
27+
}
28+
const socket = io();
29+
30+
let myHand = [];
31+
32+
joinBtn.addEventListener('click', () => {
33+
const name = nameInput.value.trim();
34+
const room = roomInput.value.trim();
35+
if (!name || !room) {
36+
alert('Ingresa nombre y sala');
37+
return;
38+
}
39+
socket.emit('joinRoom', { name, room }, (res) => {
40+
if (res && res.error) return alert(res.error);
41+
roomInfo.classList.remove('hidden');
42+
document.getElementById('roomName').textContent = room;
43+
});
44+
});
45+
46+
socket.on('roomData', ({ room, players }) => {
47+
playersList.innerHTML = '';
48+
players.forEach((p, i) => {
49+
const li = document.createElement('li');
50+
li.textContent = (i === 0 ? '(Host) ' : '') + p.name;
51+
playersList.appendChild(li);
52+
});
53+
});
54+
55+
startBtn.addEventListener('click', () => {
56+
const room = roomNameSpan.textContent;
57+
socket.emit('startGame', { room }, (res) => {
58+
if (res && res.error) return alert(res.error);
59+
});
60+
});
61+
62+
socket.on('gameStarted', (data) => {
63+
lobby.classList.add('hidden');
64+
gameSection.classList.remove('hidden');
65+
status.textContent = data.msg || 'Juego iniciado';
66+
renderSnapshot(data.snapshot);
67+
});
68+
69+
socket.on('hand', (hand) => {
70+
myHand = hand;
71+
renderHand();
72+
});
73+
74+
socket.on('gameUpdate', (snapshot) => {
75+
renderSnapshot(snapshot);
76+
});
77+
78+
btnTruco.addEventListener('click', ()=>{
79+
socket.emit('playerAction', { room: roomNameSpan.textContent, action: { type:'callTruco' } }, (res)=>{
80+
if(res && res.error) alert(res.error);
81+
});
82+
});
83+
84+
btnEnvido.addEventListener('click', ()=>{
85+
socket.emit('playerAction', { room: roomNameSpan.textContent, action: { type:'callEnvido' } }, (res)=>{
86+
if(res && res.error) alert(res.error);
87+
if(res && res.value) alert('Valor de tu envido: ' + res.value);
88+
});
89+
});
90+
91+
function renderHand(){
92+
cardsArea.innerHTML = '';
93+
myHand.forEach((c,i)=>{
94+
const b = document.createElement('button');
95+
b.textContent = `${c.rank} de ${c.suit}`;
96+
b.addEventListener('click', ()=>{
97+
socket.emit('playerAction', { room: roomNameSpan.textContent, action: { type:'play', cardIdx:i } }, (res)=>{
98+
if(res && res.error) alert(res.error);
99+
if(res && res.roundEnd) alert('Ronda terminada. Ganador: ' + res.roundWinner);
100+
});
101+
});
102+
cardsArea.appendChild(b);
103+
});
104+
}
105+
106+
function renderSnapshot(s){
107+
status.textContent = `Turno: ${s.players[s.turnIndex] ? s.players[s.turnIndex].name : '---' } | Truco: ${s.trucoLevel}`;
108+
// update players list scores
109+
playersList.innerHTML = '';
110+
s.players.forEach((p, i)=>{
111+
const li = document.createElement('li');
112+
li.textContent = `${p.name}${p.score} pts`;
113+
playersList.appendChild(li);
114+
});
115+
}
116+
});

0 commit comments

Comments
 (0)