diff --git a/deploy/server-status.md b/deploy/server-status.md index 6bd9e24..526528b 100644 --- a/deploy/server-status.md +++ b/deploy/server-status.md @@ -1,13 +1,13 @@ # Servidor de Juegoland -Versión 20261004-11. Web, API y WebSockets en mesa-game, 0.0.0.0:3000. +Versión 20261004-12. Web, API y WebSockets en mesa-game, 0.0.0.0:3000. El proxy Nginx existente de la red apunta a http://192.168.18.155:3000. Nginx y el antiguo servicio mesa-web están deshabilitados en la VM. Acceso local: http://192.168.18.155:3000. Dominio público: https://juegoland.imaginacion.com. -78 pruebas y compilación Linux verificadas. Dos clientes reales han comprobado por el dominio público la aceptación, cookies, WebSocket, chat, movimiento, turno y resultado. Las identidades sintéticas se eliminaron después de la prueba. +81 pruebas y compilación Linux verificadas. En una comprobación anterior, dos clientes reales verificaron por el dominio público la aceptación, cookies, WebSocket, chat, movimiento, turno y resultado. Las identidades sintéticas se eliminaron después de esa prueba. El popup breve llega abierto en el HTML del servidor y pasa a modal nativo al inicializar JavaScript. Declara display, opacity y visibility en línea para evitar que una regla CSS de ocultación deje un modal invisible bloqueando la página. Se comprobó a 565×958 con reglas de ocultación !important y sin JavaScript en el navegador integrado. La comprobación directa en Edge sigue pendiente. El formulario puede aceptar y guardar la cookie sin JavaScript; conserva la invitación o ruta de entrada. «No acepto» es un enlace a https://imaginacion.com/ y Escape vuelve a la misma web. No hay página de condiciones completas. Open Graph y PNG de 1200×630 publicados para compartir. El despliegue conserva los archivos inmutables del cliente anterior para que las pestañas abiertas puedan seguir cargando sus componentes. Damas mantiene la ficha seleccionada durante capturas encadenadas y permite recorrido diagonal largo de las damas. Las partidas existentes siguen disponibles. @@ -16,4 +16,6 @@ La interfaz consume `src/tokens.css`: tipografía proporcional de 16 a 18 px, es El chat aprovecha primero el margen derecho libre. A 1907×955, los cinco juegos conservan exactamente la posición y tamaño del tablero al abrirlo; en parchís y oca también se conserva el ancho de los controles. Se retiró la regla que convertía el tablero en una columna elástica y estrechaba los controles a 180 px. Se verificaron además 390×844, 1024×900, 1440×900, 3818×1886 y 932×430, sin desbordamiento horizontal. En pantalla horizontal de poca altura, tablero y acciones permanecen en columnas; el apilado del parchís se reserva a tabletas con altura suficiente. +El parchís usa la distribución clásica del tablero de referencia, colores vivos, números orientados por brazo y avatar en el centro de cada casa. Solo las cuatro salidas muestran popover. En la oca lo muestran únicamente las casillas especiales. En mesas de dos jugadores el segundo puede elegir entre los tres colores libres; la asignación automática se reserva al último color de una paleta completa de cuatro u ocho jugadores. La geometría y la selección tienen pruebas de regresión. + PostgreSQL permanece en loopback, con base y rol dedicados. Credenciales y datos existentes se conservan. APT y npm operan por IPv4. Copias diarias activas a las 03:15 Europe/Madrid. diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md new file mode 100644 index 0000000..108d1f6 --- /dev/null +++ b/docs/HANDOFF.md @@ -0,0 +1,71 @@ +# Continuación de Juegoland — 4 de octubre de 2026 + +El usuario ha pedido terminar los cambios actuales, dejar este handoff, hacer commit y push y cerrar por hoy. Los componentes de cartas y los juegos nuevos quedan para la siguiente sesión. + +## Trabajo cerrado + +- El parchís sigue la distribución del tablero de referencia: rojo arriba a la izquierda, azul arriba a la derecha, verde abajo a la izquierda y amarillo abajo a la derecha. Las 68 casillas, sus números, las salidas 5/22/39/56 y los pasillos se dibujan con la geometría clásica. Los índices del motor se conservan, incluidos los de partidas antiguas. +- Solo las cuatro casillas de salida del parchís tienen popover. En la oca lo conservan las casillas especiales; las normales y el inicio no lo tienen. +- Los colores de las fichas son vivos y el amarillo es amarillo. La paleta compartida y los tokens visuales tienen los mismos colores sólidos; los textos del chat conservan tonos legibles para claro y oscuro. +- El centro de cada casa del parchís muestra el avatar del jugador. Si no hay avatar, aparecen sus iniciales; las casas libres mantienen un símbolo neutro. +- En mesas de dos o tres jugadores, cada jugador puede elegir entre todos los colores libres. Solo se asigna automáticamente el último color cuando se agota la paleta: cuatro jugadores con cuatro colores u ocho con ocho. Un color ocupado se rechaza y cambiar de color desmarca «listo». +- El arreglo anterior del chat se conserva: utiliza primero el margen derecho libre y no desplaza el tablero mientras haya espacio. + +## Código de referencia + +- `shared/parchis-board.mjs`: geometría de casillas, números, casas, salidas y pasillos. +- `src/lib/components/ParchisMap.svelte`: tablero, avatares, fichas, animaciones y popovers de salida. +- `src/lib/components/OcaBoard.svelte`: interacción solo en casillas especiales y contraste de números de fichas. +- `shared/player-colors.mjs`: paleta y función `receivesLastColor`. +- `server/platform.mjs` y `src/lib/components/PlayerColorChooser.svelte`: asignación y elección de colores. +- `src/tokens.css` y `src/app.css`: sistema visual. Consultar `docs/design-system.md`, `docs/typography.md` y `docs/room-layout.md` antes de tocar estilos. +- `tests/parchis-board.test.ts` y `tests/platform.test.ts`: posición de casillas y selección de colores, además de las pruebas existentes. + +## Verificación y despliegue + +La suite contiene 81 pruebas. Ejecutar `npm run design:check`, `npm run check`, `npm test -- --maxWorkers=1` y `npm run build` al modificar el proyecto. La revisión visual de este cambio utiliza dos identidades sintéticas y un servidor aislado en `127.0.0.1:4185`; sus archivos y capturas están en `artifacts/`, ignorado por Git. No reiniciar los servidores de otras sesiones de Codex. + +El estado del despliegue se registra en `deploy/server-status.md`. Al cierre de esta sesión quedó activada la versión `20261004-12`, con 81 pruebas aprobadas en Linux y respuesta HTTP 200 del endpoint público de salud. Comprobar allí y con `readlink /opt/mesa-abierta/current` la versión vigente antes de continuar. + +- Proyecto local: `G:\dev\svelte\games`, rama `main`. +- Repositorio: `https://g.activething.com/go/Juegoland.git`. +- VM: `192.168.18.155`. +- Servicio único: `mesa-game`, escucha en `0.0.0.0:3000` para web, API y WebSockets. +- Dominio vigente: `https://juegoland.imaginacion.com/`. Usar este dominio en invitaciones; las menciones antiguas a games/juegolang ya no son la dirección vigente. +- El proxy Nginx existente de la red apunta a `http://192.168.18.155:3000`. No instalar otro proxy ni Docker Compose en la VM. `mesa-web` y Nginx de la VM están deshabilitados. +- PostgreSQL y las credenciales actuales se conservan. No copiar secretos al repositorio ni imprimirlos. +- SSH por IPv4 con la clave local `$env:USERPROFILE\.ssh\senzapaura_deploy`. APT y npm deben usar IPv4. +- Despliegue nativo: `deploy/deploy-native.ps1 -Release -Activate`. Verifica, hace copia de seguridad, migra y activa la versión; no reutilizar identificadores existentes. + +## Siguiente sesión: componentes de cartas + +El usuario preparará imágenes de la baraja española y de póquer. Todavía no se han recibido ni se han creado estos componentes. Preparar una base reutilizable para carta, reverso, mano, mazo, descarte y bazas/mesa. Separar datos, imágenes, reglas e interacción; cada juego define sus acciones, animaciones y sonidos. + +Prever baraja española de 40/48 cartas y póquer de 52 con comodines opcionales. Identificar cartas por palo y valor mediante un manifiesto de recursos. Mantener manos legibles en móvil, selección accesible y acciones legales claras. No sustituir las imágenes que traerá el usuario por otra baraja. + +El servidor debe autorizar las acciones y ocultar las manos ajenas. Revisar `Platform.view` y la proyección `game.view(state, seat)` antes de implementar estados privados. Esta misma base sirve para dados ocultos y tableros privados de barcos. + +## Juegos solicitados para incorporar poco a poco + +Cartas: brisca, tute de dos jugadores, la 31 y cinquillo. Otras posibilidades mencionadas para valorar después: escoba, chinchón, tute por parejas, pocha y mus. + +Otros juegos solicitados: + +- Serpientes y escaleras: el usuario aportará las imágenes del tablero, como hizo con la oca. +- Guerra de barcos / hundir la flota: tableros privados de cada jugador. +- El ahorcado. +- Tres en raya. +- Conecta cuatro. +- Mancala: concretar la variante antes de implementar reglas. +- Shut the Box: concretar dados y puntuación. +- El mentiroso de dados y otros juegos de dados: concretar variantes y dados ocultos. + +No están implementados ni deben presentarse como disponibles en el catálogo. Incorporarlos uno a uno con motor, interfaz, reglas, sonidos, animaciones y validación completos. + +## Preferencias que hay que respetar + +Usar el skill `C:\Users\dev\.codex\skills\frontend-design\SKILL.md`. Conservar el logo transparente y la identidad de Juegoland, los tokens de tipografía y espaciado y las opciones de tema claro/oscuro. Revisar capturas en escritorio y móvil: tamaños, alineaciones y proporciones son una prioridad expresa del usuario. + +Mantener PIN de cuatro cifras, registro opcional, invitaciones con mensaje del anfitrión, avatares editables, notificación de mensajes con el chat cerrado y partidas pendientes. Sonidos y animaciones deben diferenciarse por juego y acción. El popup de entrada debe ser breve; «No acepto» y Escape llevan a `https://imaginacion.com/`. No volver a añadir páginas legales extensas ni un aviso en lugar del popup. + +Este handoff no programa tareas para mañana ni abre otra sesión. diff --git a/server/platform.mjs b/server/platform.mjs index 849cc39..b792524 100644 --- a/server/platform.mjs +++ b/server/platform.mjs @@ -6,6 +6,7 @@ import { paletteForRoom, assignLastColor, chooseDefaultColor, + receivesLastColor, } from '../shared/player-colors.mjs'; export class Platform { @@ -176,7 +177,7 @@ export class Platform { throw new Error( 'Solo puedes elegir color en la sala de espera de este juego.', ); - if (player.seat === room.maxPlayers - 1) + if (receivesLastColor(room, player)) throw new Error('El último jugador recibe un color automáticamente.'); if (!paletteForRoom(room).some((c) => c.id === colorId)) throw new Error('Ese color no está disponible en este juego.'); @@ -184,7 +185,7 @@ export class Platform { room.players.some( (p) => p !== player && - p.seat !== room.maxPlayers - 1 && + !receivesLastColor(room, p) && p.colorId === colorId, ) ) diff --git a/shared/parchis-board.mjs b/shared/parchis-board.mjs new file mode 100644 index 0000000..6b7c8a4 --- /dev/null +++ b/shared/parchis-board.mjs @@ -0,0 +1,54 @@ +// Classic board: three equal sectors on each axis, eight rectangular cells per arm. +export const parchisStarts = [4, 21, 38, 55]; +export const parchisHomes = [ + [400, 400], + [400, 0], + [0, 0], + [0, 400], +]; +const laneWidth = 200 / 3; +function cell(x, y, width, height, angle) { + const center = [x + width / 2, y + height / 2]; + const label = [...center]; + if (height === 25 && x !== 200 + laneWidth) + label[0] = x === 200 ? x + width - 11 : x + 11; + if (width === 25 && y !== 200 + laneWidth) + label[1] = y === 200 ? y + height - 11 : y + 11; + return { x, y, width, height, angle, center, label }; +} +const track = []; +for (let i = 7; i >= 0; i--) + track.push(cell(200 + 2 * laneWidth, 400 + i * 25, laneWidth, 25, 0)); +for (let i = 0; i < 8; i++) + track.push(cell(400 + i * 25, 200 + 2 * laneWidth, 25, laneWidth, 90)); +track.push(cell(575, 200 + laneWidth, 25, laneWidth, 90)); +for (let i = 7; i >= 0; i--) + track.push(cell(400 + i * 25, 200, 25, laneWidth, 90)); +for (let i = 7; i >= 0; i--) + track.push(cell(200 + 2 * laneWidth, i * 25, laneWidth, 25, 180)); +track.push(cell(200 + laneWidth, 0, laneWidth, 25, 180)); +for (let i = 0; i < 8; i++) track.push(cell(200, i * 25, laneWidth, 25, 180)); +for (let i = 7; i >= 0; i--) track.push(cell(i * 25, 200, 25, laneWidth, 270)); +track.push(cell(0, 200 + laneWidth, 25, laneWidth, 270)); +for (let i = 0; i < 8; i++) + track.push(cell(i * 25, 200 + 2 * laneWidth, 25, laneWidth, 270)); +for (let i = 0; i < 8; i++) + track.push(cell(200, 400 + i * 25, laneWidth, 25, 0)); +track.push(cell(200 + laneWidth, 575, laneWidth, 25, 0)); +export const parchisTrack = track; +export function parchisLane(house, step, outer = 65) { + const edge = outer === 65 ? 6 : 7; + const begin = outer === 65 ? 1 : 0; + return [ + () => cell(200 + laneWidth, 400 + (edge - step) * 25, laneWidth, 25, 0), + () => cell(400 + (edge - step) * 25, 200 + laneWidth, 25, laneWidth, 90), + () => cell(200 + laneWidth, (begin + step) * 25, laneWidth, 25, 180), + () => cell((begin + step) * 25, 200 + laneWidth, 25, laneWidth, 270), + ][house](); +} +export function parchisHomePoint(house, piece) { + return [ + parchisHomes[house][0] + 57 + (piece % 2) * 86, + parchisHomes[house][1] + 57 + Math.floor(piece / 2) * 86, + ]; +} diff --git a/shared/player-colors.mjs b/shared/player-colors.mjs index 477bfd6..a82b3a7 100644 --- a/shared/player-colors.mjs +++ b/shared/player-colors.mjs @@ -1,11 +1,11 @@ export const playerColors = [ - { id: 'amarillo', label: 'Amarillo', hex: '#b88925' }, - { id: 'azul', label: 'Azul', hex: '#4e84a6' }, - { id: 'rojo', label: 'Rojo', hex: '#b95f56' }, - { id: 'verde', label: 'Verde', hex: '#568264' }, + { id: 'amarillo', label: 'Amarillo', hex: '#ffde00' }, + { id: 'azul', label: 'Azul', hex: '#00aeef' }, + { id: 'rojo', label: 'Rojo', hex: '#ff2438' }, + { id: 'verde', label: 'Verde', hex: '#28c800' }, { id: 'negro', label: 'Negro', hex: '#30343b' }, - { id: 'naranja', label: 'Naranja', hex: '#c96b24' }, - { id: 'violeta', label: 'Violeta', hex: '#8654b8' }, + { id: 'naranja', label: 'Naranja', hex: '#ff861c' }, + { id: 'violeta', label: 'Violeta', hex: '#9855ef' }, { id: 'gris', label: 'Gris', hex: '#7d8794' }, ]; export const parchisColorOrder = playerColors.slice(0, 4).map((c) => c.id); @@ -14,6 +14,9 @@ export const canChooseColor = (room) => (room.gameId === 'parchis' && room.gameVersion !== '1.0.0'); export const paletteForRoom = (room) => playerColors.slice(0, room.maxPlayers > 4 ? 8 : 4); +export const receivesLastColor = (room, player) => + room.maxPlayers === paletteForRoom(room).length && + player?.seat === room.maxPlayers - 1; export const colorForPlayer = (player) => playerColors.find((c) => c.id === player?.colorId) ?? playerColors[player?.seat ?? 0] ?? @@ -22,7 +25,7 @@ export const playerColorClass = (player) => `player-color-${playerColors.indexOf(colorForPlayer(player))}`; export function assignLastColor(room) { const last = room.players.find((p) => p.seat === room.maxPlayers - 1); - if (!last) return; + if (!receivesLastColor(room, last)) return; const others = room.players.filter((p) => p !== last); if (others.length === room.maxPlayers - 1 && others.every((p) => p.colorId)) last.colorId = paletteForRoom(room).find( @@ -31,12 +34,12 @@ export function assignLastColor(room) { else delete last.colorId; } export function chooseDefaultColor(room, player) { - if (player.colorId || player.seat === room.maxPlayers - 1) return; + if (player.colorId || receivesLastColor(room, player)) return; const free = paletteForRoom(room).filter( (c) => !room.players.some( (p) => - p !== player && p.seat !== room.maxPlayers - 1 && p.colorId === c.id, + p !== player && !receivesLastColor(room, p) && p.colorId === c.id, ), ); player.colorId = ( diff --git a/src/app.css b/src/app.css index 74e842d..c4b247f 100644 --- a/src/app.css +++ b/src/app.css @@ -1420,7 +1420,7 @@ form input::placeholder { display: grid; place-items: center; font-size: 8px; - color: white; + color: var(--piece-number); padding: 0; position: relative; z-index: 2; diff --git a/src/lib/components/OcaBoard.svelte b/src/lib/components/OcaBoard.svelte index 20566c4..da53e49 100644 --- a/src/lib/components/OcaBoard.svelte +++ b/src/lib/components/OcaBoard.svelte @@ -103,36 +103,38 @@ /> {#each artwork.centers as _, n}{@const rect = tile(n)}{@const info = ocaCell(n)} - { - if (e.pointerType === 'mouse') hovered = n; - }} - onpointerleave={() => (hovered = null)} - onfocus={() => (hovered = n)} - onblur={() => (hovered = null)} - onclick={() => (selected = selected === n ? null : n)} - onkeydown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - selected = selected === n ? null : n; - } else if (e.key === 'Escape') dismiss(); - }} - > - - + {#if info.kind !== 'normal'} + { + if (e.pointerType === 'mouse') hovered = n; + }} + onpointerleave={() => (hovered = null)} + onfocus={() => (hovered = n)} + onblur={() => (hovered = null)} + onclick={() => (selected = selected === n ? null : n)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selected = selected === n ? null : n; + } else if (e.key === 'Escape') dismiss(); + }} + > + + + {/if} {/each} {#if inspected !== null}{@const rect = tile(inspected)} {seat + 1}{seat + 1} {/key} diff --git a/src/lib/components/ParchisMap.svelte b/src/lib/components/ParchisMap.svelte index a6d0f80..7281303 100644 --- a/src/lib/components/ParchisMap.svelte +++ b/src/lib/components/ParchisMap.svelte @@ -2,6 +2,14 @@ import { flip } from 'svelte/animate'; import type { Room, Action } from '../types'; import CellInfo from './CellInfo.svelte'; + import { playerColors } from '../../../shared/player-colors.mjs'; + import { + parchisTrack, + parchisStarts, + parchisHomes, + parchisLane, + parchisHomePoint, + } from '../../../shared/parchis-board.mjs'; let { room, disabled = false, @@ -11,287 +19,259 @@ disabled?: boolean; onaction: (action: Action) => void; } = $props(); + const colors = playerColors.slice(0, 4); + const houseLabels = ['amarilla', 'azul', 'roja', 'verde']; const goal = $derived(room.state?.goal ?? 71), outer = $derived(room.state?.outerLength ?? 64); - const colors = ['#b88925', '#4e84a6', '#b95f56', '#568264']; - const houseStarts = [4, 21, 38, 55]; - function houseForSeat(seat: number) { - return houseStarts.indexOf(room.state?.starts?.[seat] ?? houseStarts[seat]); - } - const homes = [ - [11, 11], - [0, 11], - [0, 0], - [11, 0], - ]; - const route: number[][] = []; - for (let x = 11; x < 19; x++) route.push([x, 8]); - route.push([18, 9]); - for (let x = 18; x > 10; x--) route.push([x, 10]); - for (let y = 11; y < 19; y++) route.push([10, y]); - route.push([9, 18]); - for (let y = 18; y > 10; y--) route.push([8, y]); - for (let x = 7; x >= 0; x--) route.push([x, 10]); - route.push([0, 9]); - for (let x = 0; x < 8; x++) route.push([x, 8]); - for (let y = 7; y >= 0; y--) route.push([8, y]); - route.push([9, 0]); - for (let y = 0; y < 8; y++) route.push([10, y]); - const track = Array.from({ length: 68 }, (_, i) => route[(i + 8) % 68]); - let hovered = $state(null), - selected = $state(null); + let hovered = $state(null), + selected = $state(null); const inspected = $derived(selected ?? hovered); - function describeCell(id: string) { - const [type, first, second] = id.split('-'), - index = Number(first); - if (type === 'goal') - return { - point: [285, 285], - info: { - title: 'Meta', - kind: 'advantage', - description: - 'Debes llegar con el número exacto. Cada ficha que entra da 10 pasos de premio. Ganas al llevar las cuatro.', - }, - }; - if (type === 'lane') { - const cell = lane(index, Number(second)); - return { - point: [cell[0] * 30 + 15, cell[1] * 30 + 15], - info: { - title: `Pasillo ${['amarillo', 'azul', 'rojo', 'verde'][index]} · ${Number(second) + 1}`, - kind: 'advantage', - description: - 'Solo entran las fichas de este color. Estás protegido de las capturas; necesitas la cuenta exacta para llegar a meta.', - }, - }; - } - const safe = room.state?.safe?.includes(index), - exit = houseStarts.includes(index), - cell = track[index]; - return { - point: [cell[0] * 30 + 15, cell[1] * 30 + 15], - info: { - title: `Casilla ${index + 1}${exit ? ' · Salida' : safe ? ' · Seguro' : ''}`, - kind: safe ? 'advantage' : 'danger', - description: exit - ? 'Sales de casa con un 5. Si la salida está ocupada por dos fichas, al salir puedes capturar al último rival que llegó.' - : safe - ? 'Aquí no pueden capturarte. Pueden compartirla dos fichas; dos del mismo color forman una barrera.' - : 'Si un rival llega a esta casilla, captura tu ficha y la devuelve a casa; recibe 20 pasos de premio.', - }, - }; + function houseForSeat(seat: number) { + return parchisStarts.indexOf( + room.state?.starts?.[seat] ?? parchisStarts[seat], + ); } function dismiss() { hovered = null; selected = null; } - function keyInfo(e: KeyboardEvent, id: string) { + function keyInfo(e: KeyboardEvent, index: number) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - selected = selected === id ? null : id; + selected = selected === index ? null : index; } else if (e.key === 'Escape') dismiss(); } - function lane(seat: number, step: number) { - const edge = outer === 65 ? 17 : 18, - begin = outer === 65 ? 1 : 0; - return [ - [edge - step, 9], - [9, edge - step], - [begin + step, 9], - [9, begin + step], - ][seat]; - } function point(seat: number, piece: number, progress: number) { const house = houseForSeat(seat); - if (progress === -1) - return [ - homes[house][0] * 30 + 85 + (piece % 2) * 70, - homes[house][1] * 30 + 85 + Math.floor(piece / 2) * 70, - ]; + if (progress === -1) return parchisHomePoint(house, piece); if (progress === goal) return [ - [315, 285], - [285, 315], - [255, 285], - [285, 255], + [300, 350], + [350, 300], + [300, 250], + [250, 300], ][house].map( - (v, i) => v + (i === 0 ? piece % 2 : Math.floor(piece / 2)) * 8 - 4, + (v, i) => v + (i === 0 ? piece % 2 : Math.floor(piece / 2)) * 18 - 9, ); const cell = progress >= outer - ? lane(house, progress - outer) - : track[((room.state?.starts?.[seat] ?? 0) + progress) % 68]; + ? parchisLane(house, progress - outer, outer) + : parchisTrack[ + ((room.state?.starts?.[seat] ?? parchisStarts[seat]) + progress) % + 68 + ]; const others = room.state?.pieces?.[seat] ?? []; const shared = others.filter((p) => p === progress).length > 1; + const offset = shared ? (others.indexOf(progress) === piece ? -11 : 11) : 0; return [ - cell[0] * 30 + - 15 + - (shared ? (others.indexOf(progress) === piece ? -5 : 5) : 0), - cell[1] * 30 + 15, + cell.center[0] + (cell.width > cell.height ? offset : 0), + cell.center[1] + (cell.height > cell.width ? offset : 0), ]; } + function exitInfo(index: number) { + return { + title: `Casilla ${index + 1} · Salida ${houseLabels[parchisStarts.indexOf(index)]}`, + kind: 'advantage', + description: + 'Sales de casa con un 5. Esta casilla es un seguro. Si dos fichas ocupan la salida, al salir puedes capturar al último rival que llegó.', + }; + }
- {#each homes as home, seat} - {@const owner = room.players.find((p) => houseForSeat(p.seat) === seat)} - - houseForSeat(p.seat) === house)} + {@const color = colors[house]} + + - - {#each Array(4) as _, i}{/each} + {#each Array(4) as _, piece}{@const spot = parchisHomePoint( + house, + piece, + )} + + {/each} + + {#if owner?.avatar} + + + {:else} + {owner ? owner.nick.slice(0, 2).toUpperCase() : '✳'} + {/if} {owner?.nick.slice(0, 20) ?? 'Casa libre'}{owner?.nick.slice(0, 20) ?? 'Casa libre'} {/each} - {#each track as cell, i} - {@const seat = houseStarts.indexOf(i)} + {#each colors as color, house} + {#each Array(7) as _, step}{@const cell = parchisLane(house, step)} + + {/each} + {/each} + {#each parchisTrack as cell, i} + {@const house = parchisStarts.indexOf(i)} {@const safe = room.state?.safe?.includes(i)} - {@const id = `track-${i}`} - { - if (e.pointerType === 'mouse') hovered = id; - }} - onpointerleave={() => (hovered = null)} - onfocus={() => (hovered = id)} - onblur={() => (hovered = null)} - onclick={() => (selected = selected === id ? null : id)} - onkeydown={(e) => keyInfo(e, id)} - > + {@const cap = [16, 33, 50, 67].includes(i)} + {#snippet trackDrawing()} = 0 ? colors[seat] : safe ? '#e1e8d8' : '#fffdf7'} - stroke="#c8c9b9" + x={cell.x} + y={cell.y} + width={cell.width} + height={cell.height} + fill={house >= 0 ? colors[house].hex : '#ffffff'} + stroke="#293447" stroke-width="0.8" /> + {#if safe}= 0 ? 'none' : '#dce3ec'} + stroke="#293447" + stroke-width="1.2" + />{/if} = 0 ? '#fff' : '#5c6855'} - font-size="9" - font-weight="600">{i + 1}{i + 1} - - {/each} - {#each colors as color, seat} - {#each Array(7) as _, step}{@const cell = lane( - seat, - step, - )}{@const id = `lane-${seat}-${step}`}= 0} + { - if (e.pointerType === 'mouse') hovered = id; + if (e.pointerType === 'mouse') hovered = i; }} onpointerleave={() => (hovered = null)} - onfocus={() => (hovered = id)} + onfocus={() => (hovered = i)} onblur={() => (hovered = null)} - onclick={() => (selected = selected === id ? null : id)} - onkeydown={(e) => keyInfo(e, id)} - >{/each} + onclick={() => (selected = selected === i ? null : i)} + onkeydown={(e) => keyInfo(e, i)} + data-cell={i + 1} + > + {@render trackDrawing()} + + {:else}{@render trackDrawing()}{/if} {/each} - - - - - { - if (e.pointerType === 'mouse') hovered = 'goal'; - }} - onpointerleave={() => (hovered = null)} - onfocus={() => (hovered = 'goal')} - onblur={() => (hovered = null)} - onclick={() => (selected = selected === 'goal' ? null : 'goal')} - onkeydown={(e) => keyInfo(e, 'goal')} - > - ✳ - + + + + {#each room.state?.pieces ?? [] as pieces, seat} {@const color = colors[houseForSeat(seat)]} - {#each pieces as progress, piece (piece)}{@const pos = point( - seat, - piece, - progress, - )} + {#each pieces as progress, piece (piece)} + {@const pos = point(seat, piece, progress)} {@const playable = !disabled && seat === room.seat && @@ -325,59 +305,62 @@ ? 'en meta' : progress >= outer ? 'pasillo final' - : `casilla ${(((room.state?.starts?.[seat] ?? 0) + progress) % 68) + 1}`} {#if playable}{/if} {progress === goal ? '✓' : piece + 1}{progress === goal ? '✓' : piece + 1} {/each} {/each} - {#if inspected}{@const detail = describeCell(inspected)}{/if} + /> + {/if}