feat(jogos): telão com contagem regressiva, jogadas lado a lado e resultado

- comando "telao" (validado: texto curto, tom, símbolos permitidos, até
  2 lados) e letreiro por cima do rosto: título enorme que estoura e
  pisca, as duas jogadas (VOCÊ ✊ × ✋ CARINHA), cor por resultado;
- pedra, papel e tesoura: 3, 2, 1 piscando, "JÁ!", o que ele leu da sua
  mão, "VENCI!!!" / "VOCÊ GANHOU!" / "EMPATE!" com placar e o campeão;
  quantos dedos e achou! também usam o telão;
- o cartão do painel mostra o mesmo destaque.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-11 22:33:09 -03:00
co-authored by Claude Opus 5.5
parent 3d954efc03
commit 82f9673692
6 changed files with 218 additions and 6 deletions
+49
View File
@@ -32,6 +32,21 @@
</div>
<p class="fase">{jogos.fase || 'Começando…'}</p>
<p class="vendo">A câmera vê: <strong>{jogos.vendo || '…'}</strong></p>
{#if jogos.destaque}
{@const d = jogos.destaque}
<div class="destaque tom-{d.tom ?? 'neutro'}">
{#if d.lados?.length}
<div class="lados">
{#each d.lados as l, i (i)}
{#if i}<span class="x">×</span>{/if}
<span class="lado"><small>{l.rotulo}</small><b>{l.simbolo}</b><small>{l.nome}</small></span>
{/each}
</div>
{/if}
{#if d.titulo}<strong class="titulo">{d.titulo}</strong>{/if}
{#if d.sub}<small>{d.sub}</small>{/if}
</div>
{/if}
{#if jogos.ativo === 'ppt'}
<p class="placar"><span>Você {jogos.placar.voce}</span> × <span>{jogos.placar.carinha} Carinha</span></p>
{/if}
@@ -77,6 +92,40 @@
font-variant-numeric: tabular-nums;
color: var(--texto) !important;
}
.destaque {
display: grid;
justify-items: center;
gap: 4px;
padding: var(--e-3);
border-radius: var(--raio);
background: var(--superficie-3);
text-align: center;
}
.destaque .titulo {
font-size: var(--t-xl);
font-weight: 800;
}
.destaque.tom-eu .titulo { color: #d39b00; }
.destaque.tom-voce .titulo { color: var(--ok); }
.destaque .lados {
display: flex;
align-items: center;
gap: var(--e-4);
}
.destaque .lado {
display: grid;
justify-items: center;
}
.destaque .lado b {
font-size: 2.2rem;
line-height: 1.1;
}
.destaque .x {
color: var(--texto-3);
}
.destaque small {
color: var(--texto-2);
}
.lista {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+42 -6
View File
@@ -21,11 +21,22 @@ const NOME: Record<Jogada, string> = { pedra: 'pedra', papel: 'papel', tesoura:
class Parou extends Error {}
export interface Telao {
titulo?: string;
sub?: string;
tom?: 'neutro' | 'eu' | 'voce' | 'empate' | 'conta';
piscar?: boolean;
segundos?: number;
lados?: { rotulo: string; simbolo: string; nome: string }[];
}
class Jogos {
ativo = $state<IdJogo | null>(null);
fase = $state(''); // o que está acontecendo agora (aparece no painel)
vendo = $state(''); // o que a câmera está lendo da sua mão
placar = $state({ voce: 0, carinha: 0 });
/** O que o telão do Carinha está mostrando (o painel mostra igual). */
destaque = $state<Telao | null>(null);
private rodada = 0;
private maos: Ponto[][] = [];
private faces = 0;
@@ -54,6 +65,11 @@ class Jogos {
private dizer = (texto: string) => this.ordem('dizer', texto);
private gesto = (nome: string) => this.ordem('gesto', nome);
private olhos = (simbolo: string, segundos = 2.5) => this.ordem('olhos', { simbolo, segundos });
/** Letreiro grande por cima do rosto (e no cartão do painel). */
private telao(t: Telao) {
this.destaque = t;
return this.ordem('telao', t);
}
private esperar(ms: number) {
return new Promise<void>((ok, nao) => {
@@ -93,6 +109,7 @@ class Jogos {
} finally {
this.ativo = null;
this.fase = '';
this.destaque = null;
this.olhos('', 0.5);
}
}
@@ -121,31 +138,42 @@ class Jogos {
}
this.rodada++;
this.fase = `Rodada ${this.rodada}: contando`;
for (const palavra of ['Pedra...', 'Papel...', 'Tesoura...']) {
await this.dizer(palavra);
await this.esperar(750);
const palavras = ['Pedra...', 'Papel...', 'Tesoura...'];
for (let i = 0; i < 3; i++) {
await this.telao({ titulo: String(3 - i), sub: palavras[i], tom: 'conta', piscar: true, segundos: 1.0 });
await this.dizer(palavras[i]);
await this.esperar(850);
}
const minha = (['pedra', 'papel', 'tesoura'] as Jogada[])[Math.floor(Math.random() * 3)];
this.jogadaEstavel.limpar();
await this.olhos(SIMBOLO[minha], 3.5);
await this.telao({ titulo: 'JÁ!', sub: 'Mostra a mão parada!', tom: 'conta', segundos: 1.8 });
await this.dizer('Já!');
this.fase = `Rodada ${this.rodada}: lendo a sua mão`;
const leitura = { v: null as Jogada | null };
await this.ate(() => (leitura.v = this.jogadaEstavel.valor()) !== null, 2200);
const sua = leitura.v;
if (!sua) {
await this.telao({ titulo: '?', sub: 'Não vi sua mão... de novo!', segundos: 2.2 });
await this.dizer('Não vi direito... de novo!');
await this.gesto('confuso');
await this.esperar(2200);
continue;
}
const r = vence(sua, minha);
if (r > 0) this.placar.voce++;
else if (r < 0) this.placar.carinha++;
await this.telao({
titulo: r > 0 ? 'VOCÊ GANHOU!' : r < 0 ? 'VENCI!!!' : 'EMPATE!',
tom: r > 0 ? 'voce' : r < 0 ? 'eu' : 'empate',
sub: `Você ${this.placar.voce} × ${this.placar.carinha} Carinha`,
segundos: 3.4,
lados: [{ rotulo: 'Você', simbolo: SIMBOLO[sua], nome: NOME[sua] }, { rotulo: 'Carinha', simbolo: SIMBOLO[minha], nome: NOME[minha] }],
});
if (r > 0) {
this.placar.voce++;
await this.dizer(`${NOME[sua]} ganha de ${NOME[minha]}... Você ganhou essa!`);
await this.gesto('triste');
} else if (r < 0) {
this.placar.carinha++;
await this.dizer(`${NOME[minha]} ganha de ${NOME[sua]}! Essa é minha!`);
await this.gesto('comemorar');
} else {
@@ -155,7 +183,10 @@ class Jogos {
this.fase = `Você ${this.placar.voce} × ${this.placar.carinha} Carinha`;
await this.esperar(3200);
}
if (this.placar.voce > this.placar.carinha) {
const ganhou = this.placar.voce > this.placar.carinha;
await this.telao({ titulo: ganhou ? 'VOCÊ É O CAMPEÃO!' : 'EU SOU O CAMPEÃO!!!', tom: ganhou ? 'voce' : 'eu', piscar: true,
sub: `Você ${this.placar.voce} × ${this.placar.carinha} Carinha`, segundos: 4.5 });
if (ganhou) {
await this.dizer(`Você ganhou de ${this.placar.voce} a ${this.placar.carinha}! Revanche depois, hein?`);
await this.gesto('rir');
} else {
@@ -179,8 +210,12 @@ class Jogos {
this.fase = `Rodada ${this.rodada} de 5: pediu ${alvo}`;
this.dedosEstavel.limpar();
if (mostra) await this.olhos(String(alvo), 6);
await this.telao(mostra ? { titulo: String(alvo), sub: 'Mostra esse tanto de dedos!', tom: 'conta', segundos: 3 }
: { titulo: '?', sub: `Mostra ${alvo} ${alvo === 1 ? 'dedo' : 'dedos'}!`, segundos: 3 });
await this.dizer(mostra ? 'Mostra esse tanto de dedos!' : `Mostra ${alvo} ${alvo === 1 ? 'dedo' : 'dedos'}!`);
const acertou = await this.ate(() => this.dedosEstavel.valor() === alvo, 9000);
await this.telao(acertou ? { titulo: '✔ ACERTOU!', tom: 'voce', sub: `${alvo} ${alvo === 1 ? 'dedo' : 'dedos'}`, segundos: 2.6 }
: { titulo: `ERAM ${alvo}!`, tom: 'empate', sub: 'Vamos pro próximo', segundos: 2.6 });
if (acertou) {
this.placar.voce++;
await this.dizer(['Isso!', 'Acertou!', 'Muito bem!', 'Boa!'][this.rodada % 4]);
@@ -218,6 +253,7 @@ class Jogos {
await this.dizer(['Cadê? Cadê?', 'Sumiu!', 'Onde você foi?'][this.rodada - 1]);
// apareceu de novo
if (await this.ate(() => this.faces > 0, 15000)) {
await this.telao({ titulo: 'ACHOU!', tom: 'voce', piscar: true, segundos: 2.2 });
await this.gesto('rir');
await this.dizer('Achou!');
this.placar.voce++;