Mejoras y optimizaciones en general.

This commit is contained in:
2025-10-03 00:05:08 +02:00
parent bd76741bd2
commit d1a7442ffa
32 changed files with 3336 additions and 783 deletions

View File

@@ -3,12 +3,27 @@
<head>
<meta charset="UTF-8">
<title>Tres en Raya vs Máquina</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Tres en Raya vs Máquina</h1>
<div id="board"></div>
<div id="status"></div>
<h1 id="title">Tres en Raya vs Máquina</h1>
<div id="controls">
<label for="player-side">Tu ficha:</label>
<select id="player-side">
<option value="X" selected>X</option>
<option value="O">O</option>
</select>
</div>
<div id="board" role="grid" aria-labelledby="title"></div>
<div id="status" aria-live="polite"></div>
<div id="score">
Marcador — Tú: <span id="score-player">0</span> · Máquina: <span id="score-ai">0</span> · Empates: <span id="score-d">0</span>
</div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,122 +1,307 @@
'use strict';
// Referencias DOM
const boardDiv = document.getElementById('board');
const statusDiv = document.getElementById('status');
const restartBtn = document.getElementById('restart-btn');
let board, gameOver;
const playerSideSelect = document.getElementById('player-side');
const scorePlayerSpan = document.getElementById('score-player');
const scoreAiSpan = document.getElementById('score-ai');
const scoreDrawSpan = document.getElementById('score-d');
function initGame() {
board = Array(9).fill('');
gameOver = false;
statusDiv.textContent = "Tu turno (X)";
renderBoard();
// Constantes
const WIN_COMBOS = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Filas
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columnas
[0, 4, 8], [2, 4, 6] // Diagonales
];
let board = Array(9).fill('');
let gameOver = false;
let playerSide = 'X';
let aiSide = 'O';
let scores = { player: 0, ai: 0, D: 0 };
function loadScores() {
try {
const raw = localStorage.getItem('ttt_ai_scores');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
scores.player = Number(parsed.player) || 0;
scores.ai = Number(parsed.ai) || 0;
scores.D = Number(parsed.D) || 0;
}
}
} catch (_) {
// Ignorar errores
}
}
function renderBoard() {
boardDiv.innerHTML = '';
board.forEach((cell, idx) => {
const cellDiv = document.createElement('div');
cellDiv.className = 'cell';
cellDiv.textContent = cell;
cellDiv.onclick = () => handlePlayerMove(idx);
boardDiv.appendChild(cellDiv);
function saveScores() {
try {
localStorage.setItem('ttt_ai_scores', JSON.stringify(scores));
} catch (_) {
// Ignorar errores
}
}
function updateScoreUI() {
if (scorePlayerSpan) scorePlayerSpan.textContent = String(scores.player);
if (scoreAiSpan) scoreAiSpan.textContent = String(scores.ai);
if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D);
}
function initBoardDom() {
// Si ya existen 9 hijos, reutilizarlos (mejor rendimiento)
if (boardDiv.children.length === 9) {
Array.from(boardDiv.children).forEach(c => {
c.classList.remove('win');
c.textContent = '';
c.setAttribute('aria-label', 'Casilla');
c.removeEventListener('click', onCellClick);
c.addEventListener('click', onCellClick);
c.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') onCellClick(e);
});
});
boardDiv.setAttribute('aria-disabled', 'false');
return;
}
boardDiv.textContent = '';
for (let idx = 0; idx < 9; idx++) {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'cell';
cell.setAttribute('data-idx', String(idx));
cell.setAttribute('role', 'gridcell');
cell.setAttribute('aria-label', `Casilla ${idx + 1}`);
cell.addEventListener('click', onCellClick);
cell.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') onCellClick(e);
});
boardDiv.appendChild(cell);
}
boardDiv.setAttribute('aria-disabled', 'false');
}
function render() {
for (let i = 0; i < 9; i++) {
const el = boardDiv.children[i];
if (!el) continue;
el.textContent = board[i];
}
}
function setStatus(msg) {
statusDiv.textContent = msg;
}
function getWinCombo(b, player) {
for (const combo of WIN_COMBOS) {
const [a, m, c] = combo;
if (b[a] === player && b[m] === player && b[c] === player) {
return combo;
}
}
return null;
}
function isDraw(b) {
return b.every(cell => cell !== '') &&
!getWinCombo(b, 'X') && !getWinCombo(b, 'O');
}
function highlightCombo(combo) {
if (!combo) return;
combo.forEach(i => {
const el = boardDiv.children[i];
if (el) el.classList.add('win');
});
}
function handlePlayerMove(idx) {
if (gameOver || board[idx] !== '') return;
board[idx] = 'X';
renderBoard();
if (checkWinner('X')) {
statusDiv.textContent = `¡Tú ganas! 🎉`;
gameOver = true;
function endGame(result, combo) {
gameOver = true;
boardDiv.setAttribute('aria-disabled', 'true');
if (result === playerSide) {
setStatus('¡Tú ganas! 🎉');
scores.player += 1;
highlightCombo(combo);
} else if (result === aiSide) {
setStatus('¡La máquina gana! 🤖');
scores.ai += 1;
highlightCombo(combo);
} else {
setStatus('¡Empate!');
scores.D += 1;
}
updateScoreUI();
saveScores();
}
function onCellClick(e) {
const target = e.currentTarget;
const idx = Number(target.getAttribute('data-idx'));
if (gameOver || !Number.isInteger(idx)) return;
// Solo permitir movimiento del jugador cuando sea su turno
const turn = nextTurn(board);
if (turn !== playerSide) return;
if (board[idx] !== '') return;
// Movimiento jugador
board[idx] = playerSide;
render();
// Comprobar fin
const combo = getWinCombo(board, playerSide);
if (combo) {
endGame(playerSide, combo);
return;
}
if (isDraw()) {
statusDiv.textContent = "¡Empate!";
gameOver = true;
if (isDraw(board)) {
endGame('D', null);
return;
}
statusDiv.textContent = "Turno de la máquina (O)";
setTimeout(machineMove, 400);
// Turno de la máquina
setStatus(`Turno de la máquina (${aiSide})`);
setTimeout(machineMove, 350);
}
function nextTurn(b) {
const xCount = b.filter(v => v === 'X').length;
const oCount = b.filter(v => v === 'O').length;
// En Tic-Tac-Toe empieza 'X'; si hay igual cantidad, le toca a 'X', si X > O, le toca 'O'
return xCount === oCount ? 'X' : 'O';
}
// Minimax con poda alfa-beta
function minimax(b, depth, isMaximizing, alpha, beta) {
const aiWin = getWinCombo(b, aiSide);
const playerWin = getWinCombo(b, playerSide);
if (aiWin) return 10 - depth;
if (playerWin) return depth - 10;
if (isDraw(b)) return 0;
if (isMaximizing) {
let best = -Infinity;
for (let i = 0; i < 9; i++) {
if (b[i] === '') {
b[i] = aiSide;
const score = minimax(b, depth + 1, false, alpha, beta);
b[i] = '';
best = Math.max(best, score);
alpha = Math.max(alpha, best);
if (beta <= alpha) break;
}
}
return best;
} else {
let best = Infinity;
for (let i = 0; i < 9; i++) {
if (b[i] === '') {
b[i] = playerSide;
const score = minimax(b, depth + 1, true, alpha, beta);
b[i] = '';
best = Math.min(best, score);
beta = Math.min(beta, best);
if (beta <= alpha) break;
}
}
return best;
}
}
function machineMove() {
if (gameOver) return;
// Si no es turno de la máquina, salir
const turn = nextTurn(board);
if (turn !== aiSide) return;
// Elegir mejor jugada con Minimax
let bestScore = -Infinity;
let move = null;
for (let i = 0; i < 9; i++) {
if (board[i] === '') {
board[i] = 'O';
let score = minimax(board, 0, false);
board[i] = '';
if (score > bestScore) {
bestScore = score;
move = i;
}
}
}
if (move !== null) board[move] = 'O';
renderBoard();
if (checkWinner('O')) {
statusDiv.textContent = `¡La máquina gana! 🤖`;
gameOver = true;
return;
}
if (isDraw()) {
statusDiv.textContent = "¡Empate!";
gameOver = true;
return;
}
statusDiv.textContent = "Tu turno (X)";
}
// Algoritmo Minimax
function minimax(newBoard, depth, isMaximizing) {
if (checkWinnerOnBoard(newBoard, 'O')) return 10 - depth;
if (checkWinnerOnBoard(newBoard, 'X')) return depth - 10;
if (newBoard.every(cell => cell !== '')) return 0;
if (isMaximizing) {
let bestScore = -Infinity;
for (let i = 0; i < 9; i++) {
if (newBoard[i] === '') {
newBoard[i] = 'O';
let score = minimax(newBoard, depth + 1, false);
newBoard[i] = '';
bestScore = Math.max(score, bestScore);
}
}
return bestScore;
// Pequeña heurística: si está libre, prioriza centro (4) en primera jugada
if (board.every(c => c === '')) {
move = 4; // centro
} else {
let bestScore = Infinity;
for (let i = 0; i < 9; i++) {
if (newBoard[i] === '') {
newBoard[i] = 'X';
let score = minimax(newBoard, depth + 1, true);
newBoard[i] = '';
bestScore = Math.min(score, bestScore);
if (board[i] === '') {
board[i] = aiSide;
const score = minimax(board, 0, false, -Infinity, Infinity);
board[i] = '';
if (score > bestScore) {
bestScore = score;
move = i;
}
}
}
return bestScore;
// Si no encontró nada (no debería), elige primera libre
if (move === null) move = board.findIndex(c => c === '');
}
if (move !== null) {
board[move] = aiSide;
}
render();
// Comprobar fin
const combo = getWinCombo(board, aiSide);
if (combo) {
endGame(aiSide, combo);
return;
}
if (isDraw(board)) {
endGame('D', null);
return;
}
setStatus(`Tu turno (${playerSide})`);
}
function startGame() {
// Lado del jugador según selector
if (playerSideSelect && (playerSideSelect.value === 'X' || playerSideSelect.value === 'O')) {
playerSide = playerSideSelect.value;
} else {
playerSide = 'X';
}
aiSide = playerSide === 'X' ? 'O' : 'X';
// Reset
board = Array(9).fill('');
gameOver = false;
initBoardDom();
Array.from(boardDiv.children).forEach(c => c.classList.remove('win'));
render();
// Si empieza la IA (jugador eligió 'O'), IA mueve primero
const turn = nextTurn(board);
if (turn === aiSide) {
setStatus(`Turno de la máquina (${aiSide})`);
setTimeout(machineMove, 300);
} else {
setStatus(`Tu turno (${playerSide})`);
}
}
function checkWinner(player) {
return checkWinnerOnBoard(board, player);
// Listeners
restartBtn.addEventListener('click', startGame);
if (playerSideSelect) {
playerSideSelect.addEventListener('change', () => {
// Reiniciar al cambiar de lado
startGame();
});
}
function checkWinnerOnBoard(b, player) {
const wins = [
[0,1,2],[3,4,5],[6,7,8],
[0,3,6],[1,4,7],[2,5,8],
[0,4,8],[2,4,6]
];
return wins.some(combo => combo.every(i => b[i] === player));
}
function isDraw() {
return board.every(cell => cell !== '') && !checkWinner('X') && !checkWinner('O');
}
restartBtn.onclick = initGame;
initGame();
// Init
loadScores();
updateScoreUI();
startGame();

View File

@@ -119,4 +119,56 @@ h1 {
.cell {
font-size: 3em;
}
}
/* Controles y marcador */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select#player-side {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#score {
margin: 0.6rem auto 0.4rem auto;
font-size: 1em;
color: #444;
}
/* Botones-celda accesibles */
.cell {
border: none;
outline: none;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.35);
}
/* Resaltado de combinación ganadora */
.win {
background: #eaf7ff;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.25), 0 2px 10px rgba(61, 90, 254, 0.16);
}
@media (prefers-color-scheme: dark) {
#controls select#player-side {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
#score {
color: #a0a3b0;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
.win {
background: #172441;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.35), 0 2px 12px rgba(61, 90, 254, 0.22);
}
}

View File

@@ -3,12 +3,27 @@
<head>
<meta charset="UTF-8">
<title>Tres en Raya</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Tres en Raya (Tic Tac Toe)</h1>
<div id="board"></div>
<div id="status"></div>
<h1 id="title">Tres en Raya (Tic Tac Toe)</h1>
<div id="controls">
<label for="first-player">Primer jugador:</label>
<select id="first-player">
<option value="X" selected>X</option>
<option value="O">O</option>
</select>
</div>
<div id="board" role="grid" aria-labelledby="title"></div>
<div id="status" aria-live="polite"></div>
<div id="score">
Marcador — X: <span id="score-x">0</span> · O: <span id="score-o">0</span> · Empates: <span id="score-d">0</span>
</div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,52 +1,184 @@
'use strict';
const boardDiv = document.getElementById('board');
const statusDiv = document.getElementById('status');
const restartBtn = document.getElementById('restart-btn');
let board, currentPlayer, gameOver;
const firstPlayerSelect = document.getElementById('first-player');
const scoreXSpan = document.getElementById('score-x');
const scoreOSpan = document.getElementById('score-o');
const scoreDrawSpan = document.getElementById('score-d');
function initGame() {
board = Array(9).fill('');
currentPlayer = 'X';
gameOver = false;
statusDiv.textContent = "Turno de " + currentPlayer;
renderBoard();
}
const WIN_COMBOS = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // Filas
[0, 3, 6], [1, 4, 7], [2, 5, 8], // Columnas
[0, 4, 8], [2, 4, 6] // Diagonales
];
function renderBoard() {
boardDiv.innerHTML = '';
board.forEach((cell, idx) => {
const cellDiv = document.createElement('div');
cellDiv.className = 'cell';
cellDiv.textContent = cell;
cellDiv.onclick = () => handleCellClick(idx);
boardDiv.appendChild(cellDiv);
});
}
let board = Array(9).fill('');
let currentPlayer = 'X';
let gameOver = false;
let scores = { X: 0, O: 0, D: 0 };
function handleCellClick(idx) {
if (gameOver || board[idx] !== '') return;
board[idx] = currentPlayer;
renderBoard();
if (checkWinner(currentPlayer)) {
statusDiv.textContent = `¡${currentPlayer} gana! 🎉`;
gameOver = true;
} else if (board.every(cell => cell !== '')) {
statusDiv.textContent = "¡Empate!";
gameOver = true;
} else {
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
statusDiv.textContent = "Turno de " + currentPlayer;
function loadScores() {
try {
const raw = localStorage.getItem('ttt_scores');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
scores.X = Number(parsed.X) || 0;
scores.O = Number(parsed.O) || 0;
scores.D = Number(parsed.D) || 0;
}
}
} catch (_) {
// Ignorar errores de parsing o acceso
}
}
function checkWinner(player) {
const winCases = [
[0,1,2],[3,4,5],[6,7,8], // Filas
[0,3,6],[1,4,7],[2,5,8], // Columnas
[0,4,8],[2,4,6] // Diagonales
];
return winCases.some(combo => combo.every(i => board[i] === player));
function saveScores() {
try {
localStorage.setItem('ttt_scores', JSON.stringify(scores));
} catch (_) {
// Ignorar errores de almacenamiento
}
}
restartBtn.onclick = initGame;
function updateScoreUI() {
if (scoreXSpan) scoreXSpan.textContent = String(scores.X);
if (scoreOSpan) scoreOSpan.textContent = String(scores.O);
if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D);
}
initGame();
function initBoardDom() {
// Crear la cuadrícula de 3x3 solo si no existe
if (boardDiv.children.length === 9) {
// limpiar estado de clases win previas
Array.from(boardDiv.children).forEach(c => c.classList.remove('win'));
return;
}
boardDiv.textContent = '';
for (let idx = 0; idx < 9; idx++) {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'cell';
cell.setAttribute('data-idx', String(idx));
cell.setAttribute('aria-label', `Casilla ${idx + 1}`);
cell.setAttribute('role', 'gridcell');
cell.addEventListener('click', onCellClick);
cell.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') onCellClick(e);
});
boardDiv.appendChild(cell);
}
boardDiv.setAttribute('aria-disabled', 'false');
}
function render() {
for (let i = 0; i < 9; i++) {
const cellEl = boardDiv.children[i];
if (!cellEl) continue;
cellEl.textContent = board[i];
}
}
function setStatus(msg) {
statusDiv.textContent = msg;
}
function checkWinner(player) {
for (const combo of WIN_COMBOS) {
const [a, b, c] = combo;
if (board[a] === player && board[b] === player && board[c] === player) {
return { win: true, combo };
}
}
return { win: false, combo: null };
}
function highlightCombo(combo) {
if (!combo) return;
combo.forEach(i => {
const el = boardDiv.children[i];
if (el) el.classList.add('win');
});
}
function endGame(result, combo) {
gameOver = true;
boardDiv.setAttribute('aria-disabled', 'true');
if (result === 'X' || result === 'O') {
setStatus(`¡${result} gana! 🎉`);
scores[result] += 1;
highlightCombo(combo);
} else {
setStatus('¡Empate!');
scores.D += 1;
}
updateScoreUI();
saveScores();
}
function onCellClick(e) {
const target = e.currentTarget;
const idx = Number(target.getAttribute('data-idx'));
if (gameOver || !Number.isInteger(idx)) return;
if (board[idx] !== '') return;
board[idx] = currentPlayer;
render();
const { win, combo } = checkWinner(currentPlayer);
if (win) {
endGame(currentPlayer, combo);
return;
}
// Comprueba empate
if (board.every(cell => cell !== '')) {
endGame('D', null);
return;
}
// Cambiar turno
currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
setStatus(`Turno de ${currentPlayer}`);
}
function startGame() {
board = Array(9).fill('');
gameOver = false;
// Determinar primer jugador desde el selector si existe
if (firstPlayerSelect && (firstPlayerSelect.value === 'X' || firstPlayerSelect.value === 'O')) {
currentPlayer = firstPlayerSelect.value;
} else {
currentPlayer = 'X';
}
boardDiv.setAttribute('aria-disabled', 'false');
initBoardDom();
// Remover cualquier resaltado previo
Array.from(boardDiv.children).forEach(c => c.classList.remove('win'));
render();
setStatus(`Turno de ${currentPlayer}`);
}
// Listeners
restartBtn.addEventListener('click', startGame);
if (firstPlayerSelect) {
firstPlayerSelect.addEventListener('change', () => {
// Solo afecta si la partida no ha empezado o si no ha terminado aún
if (!gameOver && board.every(v => v === '')) {
currentPlayer = firstPlayerSelect.value;
setStatus(`Turno de ${currentPlayer}`);
}
});
}
// Init
loadScores();
updateScoreUI();
startGame();

View File

@@ -118,4 +118,56 @@ h1 {
.cell {
font-size: 3em;
}
}
/* Controles de partida y marcador */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select#first-player {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#score {
margin: 0.6rem auto 0.4rem auto;
font-size: 1em;
color: #444;
}
/* Ajustes para botones-celda (accesibilidad) */
.cell {
border: none;
outline: none;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.35);
}
/* Resaltado de combinación ganadora */
.win {
background: #eaf7ff;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.25), 0 2px 10px rgba(61, 90, 254, 0.16);
}
@media (prefers-color-scheme: dark) {
#controls select#first-player {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
#score {
color: #a0a3b0;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
.win {
background: #172441;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.35), 0 2px 12px rgba(61, 90, 254, 0.22);
}
}

View File

@@ -3,12 +3,28 @@
<head>
<meta charset="UTF-8">
<title>4 en Raya vs Máquina</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>4 en Raya vs Máquina</h1>
<div id="board"></div>
<div id="status"></div>
<h1 id="title">4 en Raya vs Máquina</h1>
<div id="controls">
<label for="difficulty">Dificultad IA:</label>
<select id="difficulty">
<option value="facil">Fácil (profundidad 1)</option>
<option value="normal" selected>Normal (profundidad 3)</option>
<option value="dificil">Difícil (profundidad 4)</option>
</select>
</div>
<div id="board" role="grid" aria-labelledby="title"></div>
<div id="status" aria-live="polite"></div>
<div id="score">
Marcador — Tú: <span id="score-human">0</span> · Máquina: <span id="score-ai">0</span> · Empates: <span id="score-d">0</span>
</div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,151 +1,468 @@
'use strict';
// Configuración del tablero y fichas
const ROWS = 6;
const COLS = 7;
const HUMAN = '🟡';
const AI = '❌';
// Referencias DOM
const boardDiv = document.getElementById('board');
const statusDiv = document.getElementById('status');
const restartBtn = document.getElementById('restart-btn');
let board, gameOver, currentPlayer;
const difficultySelect = document.getElementById('difficulty');
const scoreHumanSpan = document.getElementById('score-human');
const scoreAiSpan = document.getElementById('score-ai');
const scoreDrawSpan = document.getElementById('score-d');
function initGame() {
board = Array.from({length: ROWS}, () => Array(COLS).fill(''));
gameOver = false;
currentPlayer = HUMAN;
statusDiv.textContent = "Tu turno (🟡)";
renderBoard();
// Estado del juego
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(''));
let gameOver = false;
let currentPlayer = HUMAN;
let aiDepth = 3; // por defecto "normal"
let scores = { human: 0, ai: 0, D: 0 };
// Utilidades de almacenamiento
function loadScores() {
try {
const raw = localStorage.getItem('connect4_scores');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
scores.human = Number(parsed.human) || 0;
scores.ai = Number(parsed.ai) || 0;
scores.D = Number(parsed.D) || 0;
}
}
} catch (_) {
// Ignorar errores de almacenamiento/parsing
}
}
function saveScores() {
try {
localStorage.setItem('connect4_scores', JSON.stringify(scores));
} catch (_) {
// Ignorar errores de almacenamiento
}
}
function updateScoreUI() {
if (scoreHumanSpan) scoreHumanSpan.textContent = String(scores.human);
if (scoreAiSpan) scoreAiSpan.textContent = String(scores.ai);
if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D);
}
function renderBoard() {
boardDiv.innerHTML = '';
// Creamos todas las celdas primero
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const cellDiv = document.createElement('div');
cellDiv.className = 'cell';
cellDiv.textContent = board[row][col];
// Si es turno humano y no está terminado el juego
if (!gameOver && currentPlayer === HUMAN && board[row][col] === '') {
let lowestRow = getLowestEmptyRow(col);
if (lowestRow === row) {
cellDiv.style.cursor = "pointer";
}
// Inicialización del DOM del tablero (reutiliza nodos para rendimiento)
function initBoardDom() {
const totalCells = ROWS * COLS;
if (boardDiv.children.length === totalCells) {
// Reutilizar botones ya creados
Array.from(boardDiv.children).forEach((c) => {
c.classList.remove('win');
c.textContent = '';
c.removeEventListener('click', onCellClick);
c.addEventListener('click', onCellClick);
c.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') onCellClick(e);
});
});
boardDiv.setAttribute('aria-disabled', 'false');
return;
}
boardDiv.textContent = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'cell';
cell.setAttribute('data-row', String(r));
cell.setAttribute('data-col', String(c));
cell.setAttribute('role', 'gridcell');
cell.setAttribute('aria-label', `Fila ${r + 1}, Columna ${c + 1}`);
cell.addEventListener('click', onCellClick);
cell.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') onCellClick(e);
});
boardDiv.appendChild(cell);
}
}
boardDiv.setAttribute('aria-disabled', 'false');
}
// Render del tablero (sin reconstruir DOM)
function render() {
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const idx = r * COLS + c;
const el = boardDiv.children[idx];
if (!el) continue;
el.textContent = board[r][c];
// Indicador de clic solo en la fila más baja disponible de cada columna durante turno humano
if (!gameOver && currentPlayer === HUMAN && board[r][c] === '') {
const lowestRow = getLowestEmptyRow(c);
el.style.cursor = lowestRow === r ? 'pointer' : 'default';
} else {
el.style.cursor = 'default';
}
// El evento click siempre llama para la columna
cellDiv.onclick = () => {
if (!gameOver && currentPlayer === HUMAN) {
playerMove(col);
}
};
boardDiv.appendChild(cellDiv);
}
}
}
function setStatus(msg) {
statusDiv.textContent = msg;
}
// Lógica de movimiento
function getLowestEmptyRow(col) {
for (let r = ROWS - 1; r >= 0; r--) {
if (board[r][col] === '') return r;
}
return -1;
}
function playerMove(col) {
if (gameOver || currentPlayer !== HUMAN) return;
let row = getLowestEmptyRow(col);
if (row === -1) return; // columna llena
board[row][col] = HUMAN;
renderBoard();
if (checkWinner(HUMAN)) {
statusDiv.textContent = "¡Has ganado! 🎉";
gameOver = true;
return;
}
if (isFull()) {
statusDiv.textContent = "¡Empate!";
gameOver = true;
return;
}
currentPlayer = AI;
statusDiv.textContent = "Turno de la máquina (❌)";
setTimeout(machineMove, 450);
function isDraw() {
// Si la fila superior está llena, no hay movimientos posibles
return board[0].every((cell) => cell !== '');
}
function machineMove() {
if (gameOver || currentPlayer !== AI) return;
// IA sencilla: elige columna aleatoria libre
let validCols = [];
for (let c = 0; c < COLS; c++) {
if (board[0][c] === '') validCols.push(c);
}
if (validCols.length === 0) return;
let col = validCols[Math.floor(Math.random() * validCols.length)];
let row = getLowestEmptyRow(col);
board[row][col] = AI;
renderBoard();
if (checkWinner(AI)) {
statusDiv.textContent = "¡La máquina gana! 🤖";
gameOver = true;
return;
}
if (isFull()) {
statusDiv.textContent = "¡Empate!";
gameOver = true;
return;
}
currentPlayer = HUMAN;
statusDiv.textContent = "Tu turno (🟡)";
}
function isFull() {
return board[0].every(cell => cell !== '');
}
function checkWinner(player) {
// Comprobación de ganador; devuelve posiciones ganadoras para destacar
function checkWinnerPositions(b, player) {
// Horizontal
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS - 3; c++) {
if (
board[r][c] === player &&
board[r][c + 1] === player &&
board[r][c + 2] === player &&
board[r][c + 3] === player
) return true;
if (b[r][c] === player && b[r][c + 1] === player && b[r][c + 2] === player && b[r][c + 3] === player) {
return [[r, c], [r, c + 1], [r, c + 2], [r, c + 3]];
}
}
}
// Vertical
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < ROWS - 3; r++) {
if (
board[r][c] === player &&
board[r + 1][c] === player &&
board[r + 2][c] === player &&
board[r + 3][c] === player
) return true;
if (b[r][c] === player && b[r + 1][c] === player && b[r + 2][c] === player && b[r + 3][c] === player) {
return [[r, c], [r + 1, c], [r + 2, c], [r + 3, c]];
}
}
}
// Diagonal descendente \
for (let r = 0; r < ROWS - 3; r++) {
for (let c = 0; c < COLS - 3; c++) {
if (
board[r][c] === player &&
board[r + 1][c + 1] === player &&
board[r + 2][c + 2] === player &&
board[r + 3][c + 3] === player
) return true;
if (b[r][c] === player && b[r + 1][c + 1] === player && b[r + 2][c + 2] === player && b[r + 3][c + 3] === player) {
return [[r, c], [r + 1, c + 1], [r + 2, c + 2], [r + 3, c + 3]];
}
}
}
// Diagonal ascendente /
for (let r = 3; r < ROWS; r++) {
for (let c = 0; c < COLS - 3; c++) {
if (
board[r][c] === player &&
board[r - 1][c + 1] === player &&
board[r - 2][c + 2] === player &&
board[r - 3][c + 3] === player
) return true;
if (b[r][c] === player && b[r - 1][c + 1] === player && b[r - 2][c + 2] === player && b[r - 3][c + 3] === player) {
return [[r, c], [r - 1, c + 1], [r - 2, c + 2], [r - 3, c + 3]];
}
}
}
return false;
return null;
}
function checkWinner(player) {
return !!checkWinnerPositions(board, player);
}
restartBtn.onclick = initGame;
initGame();
function highlightWin(positions) {
if (!positions) return;
positions.forEach(([r, c]) => {
const idx = r * COLS + c;
const el = boardDiv.children[idx];
if (el) el.classList.add('win');
});
}
function endGame(result, winPositions) {
gameOver = true;
boardDiv.setAttribute('aria-disabled', 'true');
if (result === HUMAN) {
setStatus('¡Has ganado! 🎉');
scores.human += 1;
highlightWin(winPositions);
} else if (result === AI) {
setStatus('¡La máquina gana! 🤖');
scores.ai += 1;
highlightWin(winPositions);
} else {
setStatus('¡Empate!');
scores.D += 1;
}
updateScoreUI();
saveScores();
}
// Interacción usuario
function onCellClick(e) {
const target = e.currentTarget;
const row = Number(target.getAttribute('data-row'));
const col = Number(target.getAttribute('data-col'));
if (!Number.isInteger(row) || !Number.isInteger(col)) return;
if (gameOver || currentPlayer !== HUMAN) return;
const r = getLowestEmptyRow(col);
if (r === -1) return; // columna llena
board[r][col] = HUMAN;
render();
const winPos = checkWinnerPositions(board, HUMAN);
if (winPos) {
endGame(HUMAN, winPos);
return;
}
if (isDraw()) {
endGame('D', null);
return;
}
currentPlayer = AI;
setStatus('Turno de la máquina (❌)');
setTimeout(machineMove, 220);
}
// IA Minimax con evaluación heurística y profundidad limitada
function validMoves(b) {
const cols = [];
for (let c = 0; c < COLS; c++) {
if (b[0][c] === '') cols.push(c);
}
return cols;
}
function applyMove(b, col, player) {
for (let r = ROWS - 1; r >= 0; r--) {
if (b[r][col] === '') {
b[r][col] = player;
return r;
}
}
return -1;
}
function undoMove(b, col) {
// Quita la ficha más alta en la columna
for (let r = 0; r < ROWS; r++) {
if (b[r][col] !== '') {
b[r][col] = '';
return;
}
}
}
function evaluateWindow(cells) {
let aiCount = 0, humanCount = 0, empty = 0;
for (const v of cells) {
if (v === AI) aiCount++;
else if (v === HUMAN) humanCount++;
else empty++;
}
// Bloques con ambas fichas no suman
if (aiCount > 0 && humanCount > 0) return 0;
if (aiCount > 0) {
if (aiCount === 1) return 2;
if (aiCount === 2) return 10;
if (aiCount === 3) return 50;
if (aiCount === 4) return 100000;
}
if (humanCount > 0) {
if (humanCount === 1) return -2;
if (humanCount === 2) return -10;
if (humanCount === 3) return -55; // penaliza más
if (humanCount === 4) return -100000;
}
return 0;
}
function evaluateBoard(b) {
let score = 0;
// Centrismo: favorece columnas centrales (mejor conectividad)
const centerCol = Math.floor(COLS / 2);
let centerCount = 0;
for (let r = 0; r < ROWS; r++) {
if (b[r][centerCol] === AI) centerCount++;
}
score += centerCount * 3;
// Horizontal
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS - 3; c++) {
const window = [b[r][c], b[r][c + 1], b[r][c + 2], b[r][c + 3]];
score += evaluateWindow(window);
}
}
// Vertical
for (let c = 0; c < COLS; c++) {
for (let r = 0; r < ROWS - 3; r++) {
const window = [b[r][c], b[r + 1][c], b[r + 2][c], b[r + 3][c]];
score += evaluateWindow(window);
}
}
// Diagonal descendente \
for (let r = 0; r < ROWS - 3; r++) {
for (let c = 0; c < COLS - 3; c++) {
const window = [b[r][c], b[r + 1][c + 1], b[r + 2][c + 2], b[r + 3][c + 3]];
score += evaluateWindow(window);
}
}
// Diagonal ascendente /
for (let r = 3; r < ROWS; r++) {
for (let c = 0; c < COLS - 3; c++) {
const window = [b[r][c], b[r - 1][c + 1], b[r - 2][c + 2], b[r - 3][c + 3]];
score += evaluateWindow(window);
}
}
return score;
}
function minimax(b, depth, maximizing, alpha, beta) {
// Terminal: victoria/derrota/empate
const aiWin = checkWinnerPositions(b, AI);
const humanWin = checkWinnerPositions(b, HUMAN);
if (aiWin) return 1000000 + depth; // ganar rápido
if (humanWin) return -1000000 - depth; // perder tarde
if (validMoves(b).length === 0 || depth === 0) {
return evaluateBoard(b);
}
if (maximizing) {
let best = -Infinity;
// Ordenar movimientos por proximidad al centro
const moves = validMoves(b).sort((a, d) => Math.abs(a - Math.floor(COLS / 2)) - Math.abs(d - Math.floor(COLS / 2)));
for (const col of moves) {
applyMove(b, col, AI);
const val = minimax(b, depth - 1, false, alpha, beta);
undoMove(b, col);
best = Math.max(best, val);
alpha = Math.max(alpha, best);
if (beta <= alpha) break; // poda
}
return best;
} else {
let best = Infinity;
const moves = validMoves(b).sort((a, d) => Math.abs(a - Math.floor(COLS / 2)) - Math.abs(d - Math.floor(COLS / 2)));
for (const col of moves) {
applyMove(b, col, HUMAN);
const val = minimax(b, depth - 1, true, alpha, beta);
undoMove(b, col);
best = Math.min(best, val);
beta = Math.min(beta, best);
if (beta <= alpha) break; // poda
}
return best;
}
}
function getBestMove(b, depth) {
let bestScore = -Infinity;
let bestCol = null;
// Heurística inicial: priorizar centro si vacío
const center = Math.floor(COLS / 2);
if (b[0][center] === '') {
bestCol = center;
}
const moves = validMoves(b);
for (const col of moves) {
applyMove(b, col, AI);
const score = minimax(b, depth - 1, false, -Infinity, Infinity);
undoMove(b, col);
if (score > bestScore) {
bestScore = score;
bestCol = col;
}
}
// safety: si no hay mejor, primera válida
if (bestCol === null && moves.length > 0) bestCol = moves[0];
return bestCol;
}
function machineMove() {
if (gameOver || currentPlayer !== AI) return;
const col = getBestMove(board, aiDepth);
if (col === null) {
// Sin movimientos válidos: empate
endGame('D', null);
return;
}
const row = getLowestEmptyRow(col);
if (row === -1) {
// Columna llena por carrera, elegir siguiente válida
const altMoves = validMoves(board).filter((c) => c !== col);
if (!altMoves.length) {
endGame('D', null);
return;
}
const alt = altMoves[0];
const altRow = getLowestEmptyRow(alt);
if (altRow === -1) {
endGame('D', null);
return;
}
board[altRow][alt] = AI;
} else {
board[row][col] = AI;
}
render();
const winPos = checkWinnerPositions(board, AI);
if (winPos) {
endGame(AI, winPos);
return;
}
if (isDraw()) {
endGame('D', null);
return;
}
currentPlayer = HUMAN;
setStatus('Tu turno (🟡)');
}
function difficultyToDepth(val) {
switch (val) {
case 'facil': return 1;
case 'dificil': return 4;
case 'normal':
default: return 3;
}
}
function startGame() {
// Configurar dificultad
if (difficultySelect) {
aiDepth = difficultyToDepth(difficultySelect.value);
} else {
aiDepth = 3;
}
// Reset estado
board = Array.from({ length: ROWS }, () => Array(COLS).fill(''));
gameOver = false;
currentPlayer = HUMAN;
initBoardDom();
// Limpiar resaltados previos
Array.from(boardDiv.children).forEach((c) => c.classList.remove('win'));
render();
setStatus('Tu turno (🟡)');
boardDiv.setAttribute('aria-disabled', 'false');
}
// Listeners
restartBtn.addEventListener('click', startGame);
if (difficultySelect) {
difficultySelect.addEventListener('change', () => {
startGame();
});
}
// Init
loadScores();
updateScoreUI();
startGame();

View File

@@ -129,4 +129,56 @@ h1 {
.cell {
font-size: 2em;
}
}
/* Controles de IA y marcador */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select#difficulty {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#score {
margin: 0.6rem auto 0.4rem auto;
font-size: 1em;
color: #444;
}
/* Botones-celda accesibles */
.cell {
border: none;
outline: none;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(21, 101, 192, 0.35);
}
/* Resaltado de combinación ganadora */
.win {
background: #c8e6ff;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.25), 0 2px 10px rgba(61, 90, 254, 0.16);
}
@media (prefers-color-scheme: dark) {
#controls select#difficulty {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
#score {
color: #a0a3b0;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
.win {
background: #172441;
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.35), 0 2px 12px rgba(61, 90, 254, 0.22);
}
}

View File

@@ -3,18 +3,39 @@
<head>
<meta charset="UTF-8">
<title>Adivina el Número</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>¡Adivina el número!</h1>
<div id="game-box">
<p>Estoy pensando en un número entre 1 y 100.</p>
<p>¿Puedes adivinarlo en 7 intentos?</p>
<input id="guess-input" type="number" min="1" max="100" placeholder="Tu número" />
<button id="guess-btn">Adivinar</button>
<button id="restart-btn" class="hidden">Jugar de nuevo</button>
<div id="info"></div>
<h1 id="title">¡Adivina el número!</h1>
<div id="game-box" role="group" aria-labelledby="title">
<p>Estoy pensando en un número entre 1 y <span id="range-max">100</span>.</p>
<p>¿Puedes adivinarlo en <span id="attempts-total">7</span> intentos?</p>
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="normal" selected>Normal (1-100, 7 intentos)</option>
<option value="facil">Fácil (1-50, 10 intentos)</option>
<option value="dificil">Difícil (1-200, 7 intentos)</option>
<option value="extremo">Extremo (1-1000, 10 intentos)</option>
</select>
<div class="input-row">
<label for="guess-input" class="sr-only">Tu número</label>
<input id="guess-input" type="number" min="1" max="100" placeholder="Tu número" inputmode="numeric" />
<button id="guess-btn">Adivinar</button>
<button id="restart-btn" class="hidden" aria-live="polite">Jugar de nuevo</button>
</div>
<div id="error" class="error" aria-live="polite"></div>
<div id="info" aria-live="polite"></div>
<div id="attempts">Intentos restantes: <span id="attempts-left">7</span></div>
<div id="best">Mejor marca: <span id="best-score"></span></div>
<div id="history">
<h2>Historial</h2>
<ul id="history-list"></ul>
</div>
</div>
<script src="script.js"></script>
</body>

View File

@@ -1,55 +1,156 @@
let randomNumber;
let attemptsLeft = 7;
'use strict';
const DIFFICULTIES = {
normal: { max: 100, attempts: 7 },
facil: { max: 50, attempts: 10 },
dificil: { max: 200, attempts: 7 },
extremo: { max: 1000, attempts: 10 }
};
let randomNumber = 0;
let attemptsLeft = 0;
let attemptsTotal = 0;
let maxNumber = 100;
let history = [];
let currentDifficulty = 'normal';
// DOM refs
const guessInput = document.getElementById('guess-input');
const guessBtn = document.getElementById('guess-btn');
const restartBtn = document.getElementById('restart-btn');
const infoDiv = document.getElementById('info');
const attemptsSpan = document.getElementById('attempts-left');
const difficultySelect = document.getElementById('difficulty');
const errorDiv = document.getElementById('error');
const rangeMaxSpan = document.getElementById('range-max');
const attemptsTotalSpan = document.getElementById('attempts-total');
const bestScoreSpan = document.getElementById('best-score');
const historyList = document.getElementById('history-list');
function getBest(difficulty) {
const key = `adivina_best_${difficulty}`;
const v = localStorage.getItem(key);
return v ? parseInt(v, 10) : null;
}
function setBest(difficulty, attemptsUsed) {
const prev = getBest(difficulty);
if (prev === null || attemptsUsed < prev) {
localStorage.setItem(`adivina_best_${difficulty}`, String(attemptsUsed));
bestScoreSpan.textContent = String(attemptsUsed);
}
}
function updateBestDisplay() {
const best = getBest(currentDifficulty);
bestScoreSpan.textContent = best !== null ? String(best) : '—';
}
function renderHistory() {
// Safe clear
while (historyList.firstChild) historyList.removeChild(historyList.firstChild);
history.forEach(item => {
const li = document.createElement('li');
li.textContent = `${item.guess}${item.hint}`;
historyList.appendChild(li);
});
}
function startGame() {
randomNumber = Math.floor(Math.random() * 100) + 1;
attemptsLeft = 7;
attemptsSpan.textContent = attemptsLeft;
currentDifficulty = (difficultySelect && difficultySelect.value) || 'normal';
const conf = DIFFICULTIES[currentDifficulty] || DIFFICULTIES.normal;
maxNumber = conf.max;
attemptsTotal = conf.attempts;
attemptsLeft = attemptsTotal;
randomNumber = Math.floor(Math.random() * maxNumber) + 1;
history = [];
// Update UI
if (rangeMaxSpan) rangeMaxSpan.textContent = String(maxNumber);
if (attemptsTotalSpan) attemptsTotalSpan.textContent = String(attemptsTotal);
attemptsSpan.textContent = String(attemptsLeft);
errorDiv.textContent = '';
infoDiv.textContent = '';
renderHistory();
updateBestDisplay();
// Reset input/button state
guessInput.value = '';
guessInput.disabled = false;
guessInput.setAttribute('min', '1');
guessInput.setAttribute('max', String(maxNumber));
guessBtn.disabled = false;
restartBtn.classList.add('hidden');
guessInput.focus();
}
function checkGuess() {
const guess = Number(guessInput.value);
errorDiv.textContent = '';
if (guess < 1 || guess > 100 || isNaN(guess)) {
infoDiv.textContent = "Por favor ingresa un número válido entre 1 y 100.";
const raw = guessInput.value.trim();
if (raw === '') {
errorDiv.textContent = 'Introduce un número.';
return;
}
const guess = Number(raw);
if (!Number.isFinite(guess) || !Number.isInteger(guess)) {
errorDiv.textContent = 'El valor debe ser un número entero.';
return;
}
if (guess < 1 || guess > maxNumber) {
errorDiv.textContent = `El número debe estar entre 1 y ${maxNumber}.`;
return;
}
attemptsLeft--;
attemptsSpan.textContent = attemptsLeft;
attemptsSpan.textContent = String(attemptsLeft);
if (guess === randomNumber) {
infoDiv.textContent = "¡Correcto! 🎉 Has adivinado el número.";
endGame(true);
} else if (attemptsLeft === 0) {
infoDiv.textContent = '¡Correcto! 🎉 Has adivinado el número.';
const usedAttempts = attemptsTotal - attemptsLeft;
history.push({ guess, hint: '✅ Correcto' });
renderHistory();
endGame(true, usedAttempts);
return;
}
const hint = guess < randomNumber ? '⬇️ Bajo' : '⬆️ Alto';
infoDiv.textContent = guess < randomNumber
? 'Demasiado bajo. Intenta nuevamente.'
: 'Demasiado alto. Intenta nuevamente.';
history.push({ guess, hint });
renderHistory();
if (attemptsLeft === 0) {
infoDiv.textContent = `¡Oh no! Te quedaste sin intentos. El número era ${randomNumber}.`;
endGame(false);
} else if (guess < randomNumber) {
infoDiv.textContent = "Demasiado bajo. Intenta nuevamente.";
} else {
infoDiv.textContent = "Demasiado alto. Intenta nuevamente.";
endGame(false, attemptsTotal);
}
}
function endGame(won) {
function endGame(won, usedAttempts) {
guessInput.disabled = true;
guessBtn.disabled = true;
restartBtn.classList.remove('hidden');
if (won) {
setBest(currentDifficulty, usedAttempts);
}
}
guessBtn.onclick = checkGuess;
restartBtn.onclick = startGame;
guessInput.onkeydown = (e) => { if (e.key === "Enter") checkGuess(); };
// Listeners (avoid inline handlers)
guessBtn.addEventListener('click', checkGuess);
restartBtn.addEventListener('click', startGame);
guessInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') checkGuess();
});
guessInput.addEventListener('input', () => {
errorDiv.textContent = '';
});
if (difficultySelect) {
difficultySelect.addEventListener('change', startGame);
}
// Init
startGame();

View File

@@ -1,43 +1,106 @@
:root {
--bg: #f7f7f7;
--text: #222;
--muted: #888;
--card-bg: #ffffff;
--shadow: 0 0 8px #bbb;
--accent: #2196f3;
--accent-hover: #1769aa;
--accent-contrast: #ffffff;
--error: #c62828;
}
body {
background: #f7f7f7;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
font-family: 'Montserrat', Arial, sans-serif;
text-align: center;
margin: 0;
padding: 20px;
}
h1 {
margin-top: 40px;
color: #222;
margin-top: 20px;
color: var(--text);
font-weight: 700;
}
#game-box {
background: white;
background: var(--card-bg);
width: 350px;
margin: 50px auto;
max-width: 94vw;
margin: 30px auto;
padding: 30px 20px;
border-radius: 12px;
box-shadow: 0 0 8px #bbb;
box-shadow: var(--shadow);
}
.input-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
}
input[type="number"] {
width: 100px;
width: 120px;
font-size: 1.1em;
padding: 6px;
margin-right: 12px;
padding: 8px 10px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 8px;
outline: none;
}
input[type="number"]:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.25);
}
select#difficulty {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
margin: 8px 0 14px 0;
}
button {
font-size: 1em;
padding: 7px 20px;
padding: 9px 22px;
border: none;
border-radius: 5px;
background: #2196f3;
color: white;
border-radius: 44px;
background: var(--accent);
color: var(--accent-contrast);
cursor: pointer;
margin-bottom: 10px;
transition: transform .12s, box-shadow .16s, background .16s;
}
button:hover {
background: #1769aa;
background: var(--accent-hover);
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(33,150,243,0.3);
}
button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(33,150,243,0.35);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
#error {
color: var(--error);
font-size: 0.95em;
min-height: 1.6em;
}
#info {
@@ -48,21 +111,93 @@ button:hover {
#attempts {
margin-top: 8px;
color: #888;
color: var(--muted);
}
#best {
margin-top: 6px;
color: var(--muted);
}
#history {
margin-top: 16px;
text-align: left;
}
#history h2 {
font-size: 1.05em;
margin: 8px 0;
color: var(--text);
}
#history-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 160px;
overflow-y: auto;
border-top: 1px dashed #ddd;
padding-top: 8px;
}
#history-list li {
padding: 6px 0;
border-bottom: 1px dashed #eee;
color: var(--text);
}
.hidden {
display: none;
}
.sr-only {
position: absolute !important;
height: 1px; width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
@media (max-width: 480px) {
#game-box {
padding: 24px 14px;
}
.input-row {
flex-direction: column;
gap: 8px;
}
}
@media (min-width: 728px) {
body,
#game-box,
input[type="number"],
button,
#info,
#attempts {
width: 95%;
font-size: 130%;
#game-box { width: 450px; }
body { font-size: 110%; }
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f1222;
--text: #eaeaf0;
--muted: #a0a3b0;
--card-bg: #171a2e;
--shadow: 0 0 0 rgba(0,0,0,0);
--accent: #3d5afe;
--accent-hover: #0a2459;
--accent-contrast: #ffffff;
--error: #ef5350;
}
#game-box {
box-shadow: 0 6px 24px rgba(61,90,254,0.12);
border: 1px solid rgba(255,255,255,0.06);
}
input[type="number"], select#difficulty {
background: #20233a;
color: var(--text);
border-color: #2c3252;
}
#history-list {
border-top-color: #2c3252;
}
#history-list li {
border-bottom-color: #2c3252;
}
}

View File

@@ -3,18 +3,30 @@
<head>
<meta charset="UTF-8">
<title>Empareja la Bandera</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Empareja la Bandera</h1>
<h1 id="title">Empareja la Bandera</h1>
<p>Haz clic en una bandera y después en el país correspondiente. ¿Puedes emparejar todas?</p>
<div id="controls">
<label for="pairs-count">Número de parejas:</label>
<select id="pairs-count">
<option value="6">6</option>
<option value="8">8</option>
<option value="12" selected>12</option>
<option value="16">16</option>
</select>
</div>
<div id="score">Aciertos: <span id="score-value"></span> / <span id="score-total"></span></div>
<div id="game">
<div class="flags" id="flags"></div>
<div class="countries" id="countries"></div>
<div id="game" role="group" aria-labelledby="title">
<div class="flags" id="flags" aria-label="Banderas" role="list"></div>
<div class="countries" id="countries" aria-label="Países" role="list"></div>
</div>
<button id="restart-btn">Reiniciar</button>
<div id="status"></div>
<div id="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,3 +1,5 @@
'use strict';
// Lista completa con país y siglas según ISO 3166-1 alpha-2
const countryList = [
{ name: "España", code: "ES" }, { name: "Francia", code: "FR" }, { name: "Alemania", code: "DE" }, { name: "Italia", code: "IT" },
@@ -13,88 +15,119 @@ const countryList = [
function getFlagEmoji(code) {
// Las banderas se generan a partir de letras usando unicode, no por siglas textuales
// Solo funcionan para países con código alpha-2 y script latino
if (!code || code.length !== 2) return "";
if (!code || code.length !== 2) return '';
return String.fromCodePoint(
...code.toUpperCase().split("").map(c => 0x1F1E6 + c.charCodeAt(0) - 65)
...code.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65)
);
}
// Para elegir N países aleatorios distintos con bandera
// Elegir N países aleatorios distintos con bandera, sin mutar la lista original
function pickRandomCountries(list, n) {
const pool = list.slice(); // copia de seguridad
const chosen = [];
const used = {};
while (chosen.length < n && list.length > 0) {
let idx = Math.floor(Math.random() * list.length);
let c = list[idx];
let flag = getFlagEmoji(c.code);
if (flag && !used[c.code]) {
chosen.push({name: c.name, code: c.code, flag});
used[c.code] = true;
const used = new Set();
while (chosen.length < n && pool.length > 0) {
const idx = Math.floor(Math.random() * pool.length);
const c = pool[idx];
const flag = getFlagEmoji(c.code);
if (flag && !used.has(c.code)) {
chosen.push({ name: c.name, code: c.code, flag });
used.add(c.code);
}
list.splice(idx,1);
// Eliminar del pool para evitar repetir
pool.splice(idx, 1);
}
return chosen;
}
let flagsDiv = document.getElementById('flags');
let countriesDiv = document.getElementById('countries');
let statusDiv = document.getElementById('status');
let scoreSpan = document.getElementById('score-value');
let scoreTotal = document.getElementById('score-total');
let restartBtn = document.getElementById('restart-btn');
// Referencias DOM
const flagsDiv = document.getElementById('flags');
const countriesDiv = document.getElementById('countries');
const statusDiv = document.getElementById('status');
const scoreSpan = document.getElementById('score-value');
const scoreTotal = document.getElementById('score-total');
const restartBtn = document.getElementById('restart-btn');
const pairsSelect = document.getElementById('pairs-count');
let pairs = [], flags = [], countries = [], selectedFlag = null, selectedCountry = null, score = 0, totalPairs = 12;
// Estado
let pairs = [];
let flags = [];
let countries = [];
let selectedFlag = null;
let selectedCountry = null;
let score = 0;
let totalPairs = 12;
// Utilidades
function shuffle(arr) {
for (let i = arr.length-1; i>0; i--) {
const j = Math.floor(Math.random() * (i+1));
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
function startGame() {
let fullList = countryList.slice(); // copia
pairs = pickRandomCountries(fullList, totalPairs);
flags = pairs.map(o=>o);
countries = pairs.map(o=>o);
shuffle(flags); shuffle(countries);
score = 0;
scoreSpan.textContent = score;
scoreTotal.textContent = pairs.length;
selectedFlag = selectedCountry = null;
renderFlags();
renderCountries();
statusDiv.textContent = 'Empareja todas las banderas con su país';
function setStatus(msg) {
statusDiv.textContent = msg;
}
// Renderizado
function renderFlags() {
flagsDiv.innerHTML = '';
// Limpieza segura
flagsDiv.textContent = '';
flags.forEach((p, i) => {
const d = document.createElement('div');
const d = document.createElement('button');
d.type = 'button';
d.className = 'flag';
d.textContent = p.flag;
d.setAttribute('tabindex', 0);
d.onclick = () => selectFlag(i);
d.textContent = p.flag; // seguro (caracter unicode)
d.setAttribute('tabindex', '0');
d.setAttribute('role', 'listitem');
d.setAttribute('aria-label', `Bandera de ${p.name}`);
d.setAttribute('aria-disabled', p.matched ? 'true' : 'false');
d.setAttribute('aria-selected', selectedFlag === i ? 'true' : 'false');
if (p.matched) d.classList.add('matched');
if (selectedFlag === i) d.classList.add('selected');
d.addEventListener('click', () => selectFlag(i));
d.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectFlag(i);
}
});
flagsDiv.appendChild(d);
});
}
function renderCountries() {
countriesDiv.innerHTML = '';
countriesDiv.textContent = '';
countries.forEach((p, i) => {
const d = document.createElement('div');
const d = document.createElement('button');
d.type = 'button';
d.className = 'country';
d.textContent = p.name;
d.setAttribute('tabindex', 0);
d.onclick = () => selectCountry(i);
d.textContent = p.name; // seguro
d.setAttribute('tabindex', '0');
d.setAttribute('role', 'listitem');
d.setAttribute('aria-label', `País ${p.name}`);
d.setAttribute('aria-disabled', p.matched ? 'true' : 'false');
d.setAttribute('aria-selected', selectedCountry === i ? 'true' : 'false');
if (p.matched) d.classList.add('matched');
if (selectedCountry === i) d.classList.add('selected');
d.addEventListener('click', () => selectCountry(i));
d.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectCountry(i);
}
});
countriesDiv.appendChild(d);
});
}
// Lógica selección
function selectFlag(i) {
if (flags[i].matched) return;
selectedFlag = i;
@@ -110,32 +143,81 @@ function selectCountry(i) {
}
function checkMatch() {
if (selectedFlag === null || selectedCountry === null) return;
const flagObj = flags[selectedFlag];
const countryObj = countries[selectedCountry];
if (!flagObj || !countryObj) return;
if (flagObj.code === countryObj.code) {
flags[selectedFlag].matched = true;
countries[selectedCountry].matched = true;
score++;
scoreSpan.textContent = score;
statusDiv.textContent = '¡Correcto!';
scoreSpan.textContent = String(score);
setStatus('¡Correcto!');
renderFlags();
renderCountries();
if (score === pairs.length) {
statusDiv.textContent = '¡Has emparejado todas las banderas! 🎉';
setStatus('¡Has emparejado todas las banderas! 🎉');
}
} else {
statusDiv.textContent = 'No es correcto, intenta otra vez.';
setStatus('No es correcto, intenta otra vez.');
// Reset de selección tras breve pausa
setTimeout(() => {
statusDiv.textContent = '';
selectedFlag = selectedCountry = null;
setStatus('');
selectedFlag = null;
selectedCountry = null;
renderFlags();
renderCountries();
}, 850);
return;
}
// Reset selección tras acierto
selectedFlag = null;
selectedCountry = null;
}
restartBtn.onclick = startGame;
// Inicio/reinicio
function startGame() {
// Leer número de parejas desde selector, con límites
const desired = pairsSelect && Number(pairsSelect.value) ? Number(pairsSelect.value) : 12;
totalPairs = Math.max(4, Math.min(desired, countryList.length));
const fullList = countryList.slice(); // copia
pairs = pickRandomCountries(fullList, totalPairs);
// Si no se pudieron escoger las deseadas (por restricciones), ajustar total
if (pairs.length < totalPairs) {
totalPairs = pairs.length;
}
flags = pairs.map(o => ({ ...o }));
countries = pairs.map(o => ({ ...o }));
shuffle(flags);
shuffle(countries);
score = 0;
scoreSpan.textContent = String(score);
scoreTotal.textContent = String(pairs.length);
selectedFlag = null;
selectedCountry = null;
renderFlags();
renderCountries();
setStatus('Empareja todas las banderas con su país');
// Enfocar primera bandera para accesibilidad
const firstFlag = flagsDiv.querySelector('.flag');
if (firstFlag) firstFlag.focus();
}
// Listeners (evitar handlers inline)
restartBtn.addEventListener('click', startGame);
if (pairsSelect) {
pairsSelect.addEventListener('change', startGame);
}
// Init
startGame();

View File

@@ -73,4 +73,68 @@ h1 {
}
@media (max-width: 520px) {
.flags, .countries { grid-template-columns: 1fr;}
}
/* Controles y accesibilidad */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select#pairs-count {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
.flag, .country {
outline: none;
}
.flag:focus-visible, .country:focus-visible {
box-shadow: 0 0 0 3px rgba(35, 105, 143, 0.35);
}
.flag[aria-disabled="true"], .country[aria-disabled="true"] {
cursor: default;
opacity: 0.85;
}
/* Modo oscuro básico */
@media (prefers-color-scheme: dark) {
body {
background: #0f1222;
color: #eaeaf0;
}
#score, #status {
color: #a0a3b0;
}
.flag, .country {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12);
}
.flag.selected, .country.selected {
background: #1e3a8a;
border-color: #3d5afe;
box-shadow: 0 0 0 2px #3d5afe;
}
.flag.matched, .country.matched {
background: #1b5e20;
color: #d7ffd9;
border-color: #66bb6a;
box-shadow: 0 0 0 2px #66bb6a;
}
#restart-btn {
background: #3d5afe;
}
#restart-btn:hover {
background: #0a2459;
}
#controls select#pairs-count {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
}

View File

@@ -3,12 +3,31 @@
<head>
<meta charset="UTF-8">
<title>Buscaminas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Buscaminas</h1>
<div id="status"></div>
<div id="board"></div>
<h1 id="title">Buscaminas</h1>
<div id="controls">
<label for="size">Tamaño:</label>
<select id="size">
<option value="8">8x8</option>
<option value="10" selected>10x10</option>
<option value="12">12x12</option>
</select>
<label for="mines">Minas:</label>
<select id="mines">
<option value="10">10</option>
<option value="15" selected>15</option>
<option value="20">20</option>
<option value="25">25</option>
</select>
</div>
<div id="status" aria-live="polite"></div>
<div id="counters">🚩 Restantes: <span id="flags-left">0</span></div>
<div id="board" role="grid" aria-labelledby="title"></div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,43 +1,181 @@
const SIZE = 10;
const MINES = 15;
'use strict';
// Configuración por defecto (se puede cambiar desde los selectores del UI)
const DEFAULT_SIZE = 10;
const DEFAULT_MINES = 15;
// Referencias DOM
const boardDiv = document.getElementById('board');
const statusDiv = document.getElementById('status');
const restartBtn = document.getElementById('restart-btn');
let board, revealed, flagged, gameOver, cellsRevealed;
const sizeSelect = document.getElementById('size');
const minesSelect = document.getElementById('mines');
const flagsLeftSpan = document.getElementById('flags-left');
function initGame() {
board = Array.from({length: SIZE}, () => Array(SIZE).fill(0));
revealed = Array.from({length: SIZE}, () => Array(SIZE).fill(false));
flagged = Array.from({length: SIZE}, () => Array(SIZE).fill(false));
gameOver = false;
cellsRevealed = 0;
statusDiv.textContent = 'Haz clic izquierdo para revelar, clic derecho para marcar.';
placeMines();
calculateNumbers();
renderBoard();
// Estado
let SIZE = DEFAULT_SIZE;
let MINES = DEFAULT_MINES;
let board = []; // matriz de valores: 'M' para mina o 0..8 número de minas adyacentes
let revealed = []; // matriz booleana: celdas reveladas
let flagged = []; // matriz booleana: celdas marcadas
let gameOver = false;
let cellsRevealed = 0;
let flagsLeft = 0;
let firstClickMade = false;
// Utilidades
function setStatus(msg) {
statusDiv.textContent = msg;
}
function updateFlagsLeft() {
let count = 0;
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (flagged[r][c]) count++;
}
}
flagsLeft = Math.max(0, MINES - count);
if (flagsLeftSpan) flagsLeftSpan.textContent = String(flagsLeft);
}
function inBounds(r, c) {
return r >= 0 && r < SIZE && c >= 0 && c < SIZE;
}
function placeMines() {
let placed = 0;
while (placed < MINES) {
const r = Math.floor(Math.random() * SIZE);
const c = Math.floor(Math.random() * SIZE);
if (board[r][c] !== 'M') {
board[r][c] = 'M';
placed++;
// Inicializa estructuras y UI base
function startGame() {
// Leer valores desde selectores
const parsedSize = sizeSelect ? Number(sizeSelect.value) : DEFAULT_SIZE;
const parsedMines = minesSelect ? Number(minesSelect.value) : DEFAULT_MINES;
SIZE = Number.isInteger(parsedSize) ? Math.max(6, Math.min(parsedSize, 18)) : DEFAULT_SIZE;
MINES = Number.isInteger(parsedMines) ? Math.max(5, Math.min(parsedMines, SIZE * SIZE - 1)) : DEFAULT_MINES;
// Crear matrices
board = Array.from({ length: SIZE }, () => Array(SIZE).fill(0));
revealed = Array.from({ length: SIZE }, () => Array(SIZE).fill(false));
flagged = Array.from({ length: SIZE }, () => Array(SIZE).fill(false));
gameOver = false;
firstClickMade = false;
cellsRevealed = 0;
// Contador de banderas y estado
updateFlagsLeft();
setStatus('Haz clic izquierdo o Enter para revelar, clic derecho, F o Espacio para marcar banderas.');
// Ajustar grid dinámico
boardDiv.style.gridTemplateColumns = `repeat(${SIZE}, 1fr)`;
boardDiv.setAttribute('aria-disabled', 'false');
// Construir o reutilizar DOM del tablero
initBoardDom();
render();
}
// Crear/reutilizar celdas del tablero con listeners accesibles
function initBoardDom() {
const totalCells = SIZE * SIZE;
// Si el número de hijos no coincide, reconstruimos
if (boardDiv.children.length !== totalCells) {
boardDiv.textContent = '';
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'cell';
cell.setAttribute('data-row', String(r));
cell.setAttribute('data-col', String(c));
cell.setAttribute('role', 'gridcell');
cell.setAttribute('aria-label', `Fila ${r + 1}, Columna ${c + 1}`);
// Listeners accesibles
cell.addEventListener('click', onLeftClick);
cell.addEventListener('contextmenu', onRightClick);
cell.addEventListener('keydown', (e) => {
// Enter o NumpadEnter para revelar
if (e.key === 'Enter') {
e.preventDefault();
onLeftClick(e);
}
// Espacio o F para marcar
if (e.key === ' ' || e.key === 'Spacebar' || e.key.toLowerCase() === 'f') {
e.preventDefault();
onRightClick(e);
}
});
boardDiv.appendChild(cell);
}
}
} else {
// Reutilizar: limpiar clases y re-enganchar listeners
Array.from(boardDiv.children).forEach((cell) => {
cell.classList.remove('revealed', 'mine', 'flag');
cell.textContent = '';
cell.removeEventListener('click', onLeftClick);
cell.removeEventListener('contextmenu', onRightClick);
cell.addEventListener('click', onLeftClick);
cell.addEventListener('contextmenu', onRightClick);
});
}
}
// Renderiza las celdas según estado interno sin reconstruir DOM
function render() {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const idx = r * SIZE + c;
const cell = boardDiv.children[idx];
if (!cell) continue;
// Reset estilos base
cell.classList.remove('revealed', 'mine', 'flag');
if (revealed[r][c]) {
cell.classList.add('revealed');
// contenido según valor
if (board[r][c] === 'M') {
cell.classList.add('mine');
cell.textContent = '💣';
} else if (board[r][c] > 0) {
cell.textContent = String(board[r][c]);
} else {
cell.textContent = '';
}
cell.setAttribute('aria-disabled', 'true');
} else if (flagged[r][c]) {
cell.classList.add('flag');
cell.textContent = '🚩';
cell.setAttribute('aria-disabled', 'false');
} else {
cell.textContent = '';
cell.setAttribute('aria-disabled', 'false');
}
}
}
}
// Colocar minas, evitando la celda del primer clic (primera jugada segura)
function placeMines(excludeR, excludeC) {
let placed = 0;
const used = new Set();
while (placed < MINES) {
const r = Math.floor(Math.random() * SIZE);
const c = Math.floor(Math.random() * SIZE);
const key = r * SIZE + c;
if ((r === excludeR && c === excludeC) || used.has(key) || board[r][c] === 'M') continue;
board[r][c] = 'M';
used.add(key);
placed++;
}
}
// Calcula números adyacentes para cada celda
function calculateNumbers() {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (board[r][c] === 'M') continue;
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
let nr = r+i, nc = c+j;
if (nr >= 0 && nr < SIZE && nc >=0 && nc < SIZE && board[nr][nc] === 'M') count++;
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const nr = r + dr, nc = c + dc;
if (inBounds(nr, nc) && board[nr][nc] === 'M') count++;
}
}
board[r][c] = count;
@@ -45,90 +183,111 @@ function calculateNumbers() {
}
}
function renderBoard() {
boardDiv.innerHTML = '';
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const cell = document.createElement('div');
cell.classList.add('cell');
if (revealed[r][c]) {
cell.classList.add('revealed');
if (board[r][c] === 'M') {
cell.classList.add('mine');
cell.textContent = '💣';
} else if (board[r][c] > 0) {
cell.textContent = board[r][c];
} else {
cell.textContent = '';
}
} else if (flagged[r][c]) {
cell.classList.add('flag');
cell.textContent = '🚩';
} else {
cell.textContent = '';
}
// Click izquierdo
cell.onmousedown = (e) => {
if (gameOver) return;
if (e.button === 0) revealCell(r, c);
else if (e.button === 2) toggleFlag(r, c);
};
cell.oncontextmenu = (e) => e.preventDefault();
boardDiv.appendChild(cell);
}
}
}
// Revela una celda y expande si es 0 mediante BFS
function revealCell(r, c) {
if (!inBounds(r, c)) return;
if (revealed[r][c] || flagged[r][c]) return;
revealed[r][c] = true;
cellsRevealed++;
if (board[r][c] === 'M') {
endGame(false);
renderBoard();
render();
return;
} else if (board[r][c] === 0) {
// Revela recursivo
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
let nr = r+i, nc = c+j;
if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) {
if (!revealed[nr][nc]) revealCell(nr, nc);
}
if (board[r][c] === 0) {
const queue = [[r, c]];
while (queue.length) {
const [cr, cc] = queue.shift();
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const nr = cr + dr, nc = cc + dc;
if (inBounds(nr, nc) && !revealed[nr][nc] && !flagged[nr][nc]) {
revealed[nr][nc] = true;
cellsRevealed++;
if (board[nr][nc] === 0) queue.push([nr, nc]);
}
}
}
}
}
checkWin();
renderBoard();
}
// Alterna bandera en una celda (si no está revelada)
function toggleFlag(r, c) {
if (!inBounds(r, c)) return;
if (revealed[r][c]) return;
flagged[r][c] = !flagged[r][c];
renderBoard();
updateFlagsLeft();
}
// Comprueba victoria (todas las celdas no-mina están reveladas)
function checkWin() {
if (cellsRevealed === SIZE*SIZE - MINES) {
if (cellsRevealed === SIZE * SIZE - MINES) {
endGame(true);
}
}
// Finaliza el juego (muestra minas si pierdes)
function endGame(won) {
gameOver = true;
// Revela todas las minas si perdiste
boardDiv.setAttribute('aria-disabled', 'true');
if (!won) {
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
if (board[r][c] === 'M') revealed[r][c] = true;
}
}
statusDiv.textContent = "¡BOOM! Has perdido 💣";
setStatus('¡BOOM! Has perdido 💣');
} else {
statusDiv.textContent = "¡Felicidades! Has ganado 🎉";
setStatus('¡Felicidades! Has ganado 🎉');
}
}
restartBtn.onclick = initGame;
// Listeners por celda
function onLeftClick(e) {
if (gameOver) return;
const target = e.currentTarget;
const r = Number(target.getAttribute('data-row'));
const c = Number(target.getAttribute('data-col'));
if (!Number.isInteger(r) || !Number.isInteger(c)) return;
initGame();
// Primera jugada segura: colocamos minas después del primer clic
if (!firstClickMade) {
placeMines(r, c);
calculateNumbers();
firstClickMade = true;
}
revealCell(r, c);
render();
}
function onRightClick(e) {
e.preventDefault();
if (gameOver) return;
const target = e.currentTarget;
const r = Number(target.getAttribute('data-row'));
const c = Number(target.getAttribute('data-col'));
if (!Number.isInteger(r) || !Number.isInteger(c)) return;
toggleFlag(r, c);
render();
}
// Eventos globales
restartBtn.addEventListener('click', startGame);
if (sizeSelect) {
sizeSelect.addEventListener('change', startGame);
}
if (minesSelect) {
minesSelect.addEventListener('change', startGame);
}
// Init
startGame();

View File

@@ -146,4 +146,77 @@ h1 {
}
}
/* :::::::::::::::::::::::::::::: */
/* :::::::::::::::::::::::::::::: */
/* Controles (tamaño/minas) y contador de banderas */
#controls {
margin: 0.8rem auto 0.4rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#counters {
font-size: 1rem;
color: #333;
margin-bottom: 0.4rem;
}
/* Accesibilidad: foco visible en celdas */
.cell {
border: none;
outline: none;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(215, 38, 61, 0.35);
}
/* Estado deshabilitado para botón reinicio */
#restart-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
box-shadow: none;
}
/* Modo oscuro mejorado */
@media (prefers-color-scheme: dark) {
body {
background: #0f1222;
color: #eaeaf0;
}
#board {
background: #182a46;
box-shadow: 0 6px 24px rgba(61, 90, 254, 0.12);
border: 1px solid rgba(255,255,255,0.06);
}
.cell {
background: #20233a;
color: #eaeaf0;
box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12);
}
.cell.revealed { background: #264b74; }
.cell.mine { background: #7c1f2a; color: #fff; }
.cell.flag { background: #8a6d28; color: #ffd54f; }
#status, #counters {
color: #a0a3b0;
}
#restart-btn {
background: #3d5afe;
}
#restart-btn:hover {
background: #0a2459;
}
#controls select {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
.cell:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
}

View File

@@ -6,14 +6,20 @@
<meta name='description' content='Página web de juegos online de Fernando Méndez.'>
<title>FerMdez - Games</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:700,400&display=swap" rel="stylesheet">
<meta name="theme-color" content="#3d5afe">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
<link rel='icon' href='./media/favicon.ico' sizes='192x192' />
<link rel="manifest" href="manifest.webmanifest">
<meta name='keywords' content='fermdez, juegos, games, mini-juegos, fermdez juegos, fermdez games'/>
<meta property='og:type' content='website' />
<meta property='og:site_name' content='FerMdez' />
<meta property='og:title' content='Fernando Méndez' />
<meta property='og:description' content='Página web de juegos online de Fernando Méndez.' />
<meta property='og:image' content='https://games.fermdez.net/media/favicon.ico' />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-inline'; connect-src 'self'; base-uri 'self'; form-action 'self'">
<meta name="referrer" content="no-referrer">
<meta property='og:url' content='https://games.fermdez.net/' />
<style>
body {
@@ -157,7 +163,7 @@
<body>
<header>
<h1>Fermdez - Juegos</h1>
<p>¡Prueba estos mini juegos creados por <a href="https://fermdez.net/" target="_blank">Fernando Méndez</a>!<br />Elige, juega y supera tus récords.</p>
<p>¡Prueba estos mini juegos creados por <a href="https://fermdez.net/" target="_blank" rel="noopener noreferrer">Fernando Méndez</a>!<br />Elige, juega y supera tus récords.</p>
</header>
<main>
<div class="grid">
@@ -165,7 +171,7 @@
<div class="game-icon">🏓</div>
<div class="game-title">Pong Clásico</div>
<div class="game-desc">Juega al clásico Pong contra la máquina usando las flechas.</div>
<a class="play-btn" href="pong/" target="_blank">Jugar
<a class="play-btn" href="pong/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -173,7 +179,7 @@
<div class="game-icon">🔲</div>
<div class="game-title">Simon Dice</div>
<div class="game-desc">Recuerda y repite la secuencia de colores para avanzar de nivel.</div>
<a class="play-btn" href="simon-dice/" target="_blank">Jugar
<a class="play-btn" href="simon-dice/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -181,7 +187,7 @@
<div class="game-icon">🧮</div>
<div class="game-title">Buscaminas</div>
<div class="game-desc">Marca las minas y evita explotarlas en el tablero.</div>
<a class="play-btn" href="buscaminas/" target="_blank">Jugar
<a class="play-btn" href="buscaminas/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -189,7 +195,7 @@
<div class="game-icon">🃏</div>
<div class="game-title">Juego de Memoria</div>
<div class="game-desc">Descubre todas las parejas de cartas y ejercita tu memoria.</div>
<a class="play-btn" href="memoria/" target="_blank">Jugar
<a class="play-btn" href="memoria/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -197,7 +203,7 @@
<div class="game-icon">🃏</div>
<div class="game-title">Juego de Memoria Avanzado</div>
<div class="game-desc">Descubre todas las parejas de cartas y ejercita tu memoria.</div>
<a class="play-btn" href="memoria-v2/" target="_blank">Jugar
<a class="play-btn" href="memoria-v2/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -205,7 +211,7 @@
<div class="game-icon">⭕❌</div>
<div class="game-title">3 en Raya</div>
<div class="game-desc">Tres en línea clásico: reta a un amigo.</div>
<a class="play-btn" href="3-en-raya/" target="_blank">Jugar
<a class="play-btn" href="3-en-raya/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -213,7 +219,7 @@
<div class="game-icon">⭕❌</div>
<div class="game-title">3 en Raya vs Máquina</div>
<div class="game-desc">Tres en línea clásico: reta a una IA invencible.</div>
<a class="play-btn" href="3-en-raya-computer/" target="_blank">Jugar
<a class="play-btn" href="3-en-raya-computer/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -221,7 +227,7 @@
<div class="game-icon">🔵🔴🔵🔴</div>
<div class="game-title">4 en Raya vs Máquina</div>
<div class="game-desc">Conecta 4 fichas antes que la máquina en este clásico de estrategia.</div>
<a class="play-btn" href="4-en-raya/" target="_blank">Jugar
<a class="play-btn" href="4-en-raya/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -229,7 +235,7 @@
<div class="game-icon">🧩</div>
<div class="game-title">Puzzle de Números</div>
<div class="game-desc">Resuelve el clásico puzzle de 15 piezas deslizantes.</div>
<a class="play-btn" href="puzle-numeros/" target="_blank">Jugar
<a class="play-btn" href="puzle-numeros/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -237,7 +243,7 @@
<div class="game-icon">🔢</div>
<div class="game-title">Adivina el Número</div>
<div class="game-desc">Resuelve el número que ha pensado la máquina en menos de 7 intentos.</div>
<a class="play-btn" href="puzle-numeros/" target="_blank">Jugar
<a class="play-btn" href="adivina/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -245,7 +251,7 @@
<div class="game-icon">🐍</div>
<div class="game-title">Snake Game</div>
<div class="game-desc">Haz crecer la serpiente comiendo puntos, ¡no choques con la pared!</div>
<a class="play-btn" href="serpiente/" target="_blank">Jugar
<a class="play-btn" href="serpiente/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -253,7 +259,7 @@
<div class="game-icon">🧱</div>
<div class="game-title">Rompe Ladrillos</div>
<div class="game-desc">Rompe todos los ladrillos controlando la pala y la bola.</div>
<a class="play-btn" href="ladrillos" target="_blank">Jugar
<a class="play-btn" href="ladrillos/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -261,7 +267,7 @@
<div class="game-icon">⏱️</div>
<div class="game-title">Carrera de Reacción</div>
<div class="game-desc">Haz clic cuando la pantalla se ponga verde, ¡mide tu tiempo de reacción!</div>
<a class="play-btn" href="reflejos/" target="_blank">Jugar
<a class="play-btn" href="reflejos/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -269,7 +275,7 @@
<div class="game-icon">🦦</div>
<div class="game-title">Atrapa el Topo</div>
<div class="game-desc">Haz clic en el topo cuando aparezca y suma puntos.</div>
<a class="play-btn" href="topo/" target="_blank">Jugar
<a class="play-btn" href="topo/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
</div>
@@ -277,9 +283,34 @@
<div class="game-icon">🇪🇸</div>
<div class="game-title">Banderas</div>
<div class="game-desc">Empereja la bandera con su país correspondiente.</div>
<a class="play-btn" href="banderas/" target="_blank">Jugar
<a class="play-btn" href="banderas/" target="_blank" rel="noopener noreferrer">Jugar
<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
</a>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => {
// Forzar activación inmediata si hay un SW en espera
if (reg.waiting) {
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
}
// Detectar nuevas actualizaciones
reg.addEventListener('updatefound', () => {
const newSW = reg.installing;
if (newSW) {
newSW.addEventListener('statechange', () => {
if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
console.log('Nueva versión del Service Worker instalada');
}
});
}
});
})
.catch(err => console.error('Fallo al registrar el Service Worker:', err));
});
}
</script>
</div>
</div>
</main>

View File

@@ -3,15 +3,30 @@
<head>
<meta charset="UTF-8">
<title>Breakout</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Rompe Ladrillos</h1>
<p>Usa el ratón para mover el bloque. Rompe todos los ladrillos para ganar.</p>
<div id="controls">
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="easy">Fácil</option>
<option value="normal" selected>Normal</option>
<option value="hard">Difícil</option>
</select>
</div>
<canvas id="gameCanvas" width="540" height="480"></canvas>
<div id="score">Puntuación: <span id="score-value">0</span></div>
<div id="score">
Puntuación: <span id="score-value">0</span>
· Mejor: <span id="best-score">0</span>
</div>
<button id="restart-btn">Reiniciar</button>
<div id="game-over-message"></div>
<div id="game-over-message" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,77 +1,196 @@
'use strict';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreSpan = document.getElementById('score-value');
const bestScoreSpan = document.getElementById('best-score');
const restartBtn = document.getElementById('restart-btn');
const gameOverDiv = document.getElementById('game-over-message');
const difficultySelect = document.getElementById('difficulty');
const ballRadius = 8;
let x, y, dx, dy;
const paddleHeight = 12, paddleWidth = 75;
let paddleX;
const brickRowCount = 5, brickColumnCount = 7;
// Parámetros por dificultad
const DIFFICULTIES = {
easy: { ballSpeed: 2.6, paddleWidth: 95, brickRows: 4, brickCols: 6 },
normal: { ballSpeed: 3.2, paddleWidth: 80, brickRows: 5, brickCols: 7 },
hard: { ballSpeed: 4.0, paddleWidth: 70, brickRows: 6, brickCols: 8 }
};
// Estado del juego
let ballRadius = 8;
let x = 0, y = 0, dx = 0, dy = 0;
let paddleHeight = 12, paddleWidth = DIFFICULTIES.normal.paddleWidth;
let paddleX = 0;
let brickRowCount = DIFFICULTIES.normal.brickRows;
let brickColumnCount = DIFFICULTIES.normal.brickCols;
const brickWidth = 60, brickHeight = 20, brickPadding = 10, brickOffsetTop = 30, brickOffsetLeft = 30;
let bricks = [];
let score, gameOver;
let bricks = []; // matriz [col][row] con {x,y,status}
let score = 0, gameOver = false;
let animId = null;
// Controles/Listeners
let mouseMoveHandlerBound = null;
let keyDownHandlerBound = null;
let keyUpHandlerBound = null;
let keysPressed = { left: false, right: false };
// Utilidades de almacenamiento
function bestKey(diff) {
return `breakout_best_${diff}`;
}
function getBest(diff) {
const v = localStorage.getItem(bestKey(diff));
return v ? parseInt(v, 10) : 0;
}
function setBest(diff, val) {
const prev = getBest(diff);
if (val > prev) {
localStorage.setItem(bestKey(diff), String(val));
}
bestScoreSpan.textContent = String(getBest(diff));
}
// Lectura dificultad actual
function getCurrentDifficulty() {
const v = difficultySelect && difficultySelect.value ? difficultySelect.value : 'normal';
return DIFFICULTIES[v] ? v : 'normal';
}
function getCurrentConfig() {
return DIFFICULTIES[getCurrentDifficulty()];
}
// Inicializa ladrillos según dificultad
function setupBricks() {
bricks = [];
for(let c=0; c<brickColumnCount; c++) {
for (let c = 0; c < brickColumnCount; c++) {
bricks[c] = [];
for(let r=0; r<brickRowCount; r++) {
bricks[c][r] = { x:0, y:0, status:1 };
for (let r = 0; r < brickRowCount; r++) {
bricks[c][r] = { x: 0, y: 0, status: 1 };
}
}
}
// Reinicio y arranque de partida
function startGame() {
x = canvas.width/2;
y = canvas.height-30;
dx = 3;
dy = -3;
paddleX = (canvas.width-paddleWidth)/2;
// Configuración por dificultad
const conf = getCurrentConfig();
paddleWidth = conf.paddleWidth;
brickRowCount = conf.brickRows;
brickColumnCount = conf.brickCols;
// Estado base
x = canvas.width / 2;
y = canvas.height - 60;
const speed = conf.ballSpeed;
dx = speed; // velocidad horizontal inicial
dy = -speed; // velocidad vertical inicial
paddleX = (canvas.width - paddleWidth) / 2;
score = 0;
scoreSpan.textContent = score;
scoreSpan.textContent = String(score);
bestScoreSpan.textContent = String(getBest(getCurrentDifficulty()));
gameOver = false;
gameOverDiv.textContent = '';
// Preparar ladrillos (precalcular posiciones para rendimiento)
setupBricks();
document.addEventListener("mousemove", mouseMoveHandler);
draw();
precomputeBrickPositions();
// Listeners
attachListeners();
// Animación
if (animId) cancelAnimationFrame(animId);
animId = requestAnimationFrame(draw);
}
function mouseMoveHandler(e) {
const rect = canvas.getBoundingClientRect();
let relativeX = e.clientX - rect.left;
if(relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth/2;
if(paddleX < 0) paddleX = 0;
if(paddleX + paddleWidth > canvas.width) paddleX = canvas.width - paddleWidth;
}
}
function collisionDetection() {
for(let c=0; c<brickColumnCount; c++) {
for(let r=0; r<brickRowCount; r++) {
const b = bricks[c][r];
if(b.status == 1) {
if(x > b.x && x < b.x+brickWidth && y > b.y && y < b.y+brickHeight) {
dy = -dy;
b.status = 0;
score++;
scoreSpan.textContent = score;
if(score == brickRowCount*brickColumnCount) {
gameOver = true;
gameOverDiv.textContent = "¡Ganaste! 🏆";
return;
}
}
}
// Precalcular posiciones de ladrillos una vez
function precomputeBrickPositions() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const brickX = (c * (brickWidth + brickPadding)) + brickOffsetLeft;
const brickY = (r * (brickHeight + brickPadding)) + brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
}
}
}
// Listeners seguros (evitar duplicados)
function attachListeners() {
// Ratón
if (mouseMoveHandlerBound) {
document.removeEventListener('mousemove', mouseMoveHandlerBound);
}
mouseMoveHandlerBound = function (e) {
const rect = canvas.getBoundingClientRect();
let relativeX = e.clientX - rect.left;
if (relativeX > 0 && relativeX < canvas.width) {
paddleX = relativeX - paddleWidth / 2;
clampPaddle();
}
};
document.addEventListener('mousemove', mouseMoveHandlerBound, { passive: true });
// Teclado
if (keyDownHandlerBound) window.removeEventListener('keydown', keyDownHandlerBound);
if (keyUpHandlerBound) window.removeEventListener('keyup', keyUpHandlerBound);
keyDownHandlerBound = function (e) {
if (e.key === 'ArrowLeft' || e.key === 'Left') keysPressed.left = true;
if (e.key === 'ArrowRight' || e.key === 'Right') keysPressed.right = true;
};
keyUpHandlerBound = function (e) {
if (e.key === 'ArrowLeft' || e.key === 'Left') keysPressed.left = false;
if (e.key === 'ArrowRight' || e.key === 'Right') keysPressed.right = false;
};
window.addEventListener('keydown', keyDownHandlerBound);
window.addEventListener('keyup', keyUpHandlerBound);
// Botón reinicio
restartBtn.removeEventListener('click', onRestart);
restartBtn.addEventListener('click', onRestart);
// Cambio de dificultad reinicia partida
if (difficultySelect) {
difficultySelect.removeEventListener('change', onDifficultyChange);
difficultySelect.addEventListener('change', onDifficultyChange);
}
}
function detachListeners() {
if (mouseMoveHandlerBound) {
document.removeEventListener('mousemove', mouseMoveHandlerBound);
mouseMoveHandlerBound = null;
}
if (keyDownHandlerBound) {
window.removeEventListener('keydown', keyDownHandlerBound);
keyDownHandlerBound = null;
}
if (keyUpHandlerBound) {
window.removeEventListener('keyup', keyUpHandlerBound);
keyUpHandlerBound = null;
}
restartBtn.removeEventListener('click', onRestart);
if (difficultySelect) difficultySelect.removeEventListener('change', onDifficultyChange);
}
function onRestart() {
detachListeners();
startGame();
}
function onDifficultyChange() {
onRestart();
}
function clampPaddle() {
if (paddleX < 0) paddleX = 0;
if (paddleX + paddleWidth > canvas.width) paddleX = canvas.width - paddleWidth;
}
// Dibujo de elementos
function drawBall() {
ctx.beginPath();
ctx.arc(x, y, ballRadius, 0, Math.PI*2);
ctx.arc(x, y, ballRadius, 0, Math.PI * 2);
ctx.fillStyle = "#f9d923";
ctx.fill();
ctx.closePath();
@@ -79,22 +198,19 @@ function drawBall() {
function drawPaddle() {
ctx.beginPath();
ctx.rect(paddleX, canvas.height-paddleHeight-5, paddleWidth, paddleHeight);
ctx.rect(paddleX, canvas.height - paddleHeight - 5, paddleWidth, paddleHeight);
ctx.fillStyle = "#e94560";
ctx.fill();
ctx.closePath();
}
function drawBricks() {
for(let c=0; c<brickColumnCount; c++) {
for(let r=0; r<brickRowCount; r++) {
if(bricks[c][r].status == 1) {
const brickX = (c*(brickWidth+brickPadding))+brickOffsetLeft;
const brickY = (r*(brickHeight+brickPadding))+brickOffsetTop;
bricks[c][r].x = brickX;
bricks[c][r].y = brickY;
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
if (bricks[c][r].status === 1) {
const { x: bx, y: by } = bricks[c][r];
ctx.beginPath();
ctx.rect(brickX, brickY, brickWidth, brickHeight);
ctx.rect(bx, by, brickWidth, brickHeight);
ctx.fillStyle = "#393e46";
ctx.strokeStyle = "#f9d923";
ctx.fill();
@@ -105,43 +221,143 @@ function drawBricks() {
}
}
// Control de pala por teclado (suave)
function updatePaddleByKeyboard() {
const conf = getCurrentConfig();
const step = Math.max(4, conf.ballSpeed * 2.2);
if (keysPressed.left) {
paddleX -= step;
} else if (keysPressed.right) {
paddleX += step;
}
clampPaddle();
}
// Colisión círculo-rectángulo (ball vs rect)
function circleRectCollision(cx, cy, radius, rx, ry, rw, rh) {
// Punto más cercano del rect al círculo
const nearestX = Math.max(rx, Math.min(cx, rx + rw));
const nearestY = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - nearestX;
const dy = cy - nearestY;
return (dx * dx + dy * dy) <= (radius * radius);
}
function collisionWithBricks() {
for (let c = 0; c < brickColumnCount; c++) {
for (let r = 0; r < brickRowCount; r++) {
const b = bricks[c][r];
if (b.status !== 1) continue;
if (circleRectCollision(x, y, ballRadius, b.x, b.y, brickWidth, brickHeight)) {
// Determinar lado de impacto para invertir adecuadamente
// Calculamos centro del brick
const bxCenter = b.x + brickWidth / 2;
const byCenter = b.y + brickHeight / 2;
const dxCenter = x - bxCenter;
const dyCenter = y - byCenter;
if (Math.abs(dxCenter) > Math.abs(dyCenter)) {
dx = -dx; // impacto predominantemente lateral
} else {
dy = -dy; // impacto predominantemente superior/inferior
}
b.status = 0;
score++;
scoreSpan.textContent = String(score);
// Victoria si todos eliminados
if (score === brickRowCount * brickColumnCount) {
onWin();
return true;
}
}
}
}
return false;
}
function collisionWallsAndPaddle() {
// Paredes laterales
if (x + dx > canvas.width - ballRadius || x + dx < ballRadius) {
dx = -dx;
}
// Techo
if (y + dy < ballRadius) {
dy = -dy;
} else if (y + dy > canvas.height - ballRadius - paddleHeight - 5) {
// Posible contacto con la pala
if (x > paddleX && x < paddleX + paddleWidth) {
// Rebote con ángulo según sitio de impacto en la pala
const conf = getCurrentConfig();
const hitPos = (x - (paddleX + paddleWidth / 2)) / (paddleWidth / 2); // -1..1
const maxAngle = Math.PI / 3; // 60°
const angle = hitPos * maxAngle;
const speed = conf.ballSpeed;
dx = speed * Math.sin(angle);
dy = -Math.abs(speed * Math.cos(angle)); // hacia arriba
// Evitar quedarse pegado a la pala
y = canvas.height - ballRadius - paddleHeight - 6;
} else if (y + dy > canvas.height - ballRadius) {
// Fuera por abajo: Game Over
onGameOver();
return true;
}
}
return false;
}
function onWin() {
gameOver = true;
gameOverDiv.textContent = "¡Ganaste! 🏆";
setBest(getCurrentDifficulty(), score);
stopLoop();
}
function onGameOver() {
gameOver = true;
gameOverDiv.textContent = "¡Game Over! 😢";
stopLoop();
}
function stopLoop() {
if (animId) cancelAnimationFrame(animId);
animId = null;
detachListeners();
}
// Bucle principal
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawBricks();
drawBall();
drawPaddle();
collisionDetection();
// Rebote en las paredes
if(x + dx > canvas.width-ballRadius || x + dx < ballRadius) {
dx = -dx;
// Actualizar pala por teclado
updatePaddleByKeyboard();
// Colisiones
const hitBrick = collisionWithBricks();
if (hitBrick) {
// Si hubo victoria, el bucle se detuvo
return;
}
if(y + dy < ballRadius) {
dy = -dy;
} else if(y + dy > canvas.height-ballRadius-paddleHeight-5) {
// Rebote en la paleta
if(x > paddleX && x < paddleX + paddleWidth) {
dy = -dy;
} else if (y + dy > canvas.height-ballRadius) {
// GAME OVER
gameOver = true;
gameOverDiv.textContent = "¡Game Over! 😢";
return;
}
const endOrWall = collisionWallsAndPaddle();
if (endOrWall) {
return;
}
// Avance de la bola
x += dx;
y += dy;
if(!gameOver) {
requestAnimationFrame(draw);
if (!gameOver) {
animId = requestAnimationFrame(draw);
}
}
restartBtn.onclick = function() {
document.removeEventListener("mousemove", mouseMoveHandler);
startGame();
};
// Inicio
startGame();

View File

@@ -112,4 +112,43 @@ canvas {
#restart-btn {
padding: 0.5rem 1rem;
}
}
/* Controles (selector de dificultad) */
#controls {
margin: clamp(0.5rem, 2vw, 1rem) auto;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#controls select#difficulty {
font-size: clamp(0.9rem, 2vw, 1.1rem);
padding: 6px 10px;
border-radius: var(--radius);
border: 1px solid rgba(255,255,255,0.18);
background: var(--color-canvas-bg);
color: var(--color-text);
}
#controls select#difficulty:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.35);
}
/* Accesibilidad: foco visible en botón reinicio */
#restart-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.35);
}
/* Estado deshabilitado para el botón reinicio */
#restart-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Ajuste de separación del marcador con mejor puntuación */
#score {
display: inline-flex;
gap: 10px;
align-items: baseline;
}

View File

@@ -3,17 +3,29 @@
<head>
<meta charset="UTF-8">
<title>Juego de Memoria</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Juego de Memoria Avanzado</h1>
<h1 id="title">Juego de Memoria Avanzado</h1>
<p>Haz clic sobre las cartas para descubrir y encontrar las parejas.</p>
<div id="hud">
<span id="moves"></span>
<span id="timer"></span>
<div id="controls">
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="facil">Fácil (6 parejas)</option>
<option value="normal" selected>Normal (12 parejas)</option>
<option value="dificil">Difícil (18 parejas)</option>
</select>
</div>
<div id="game-board"></div>
<div id="status"></div>
<div id="hud">
<span id="moves" aria-live="polite"></span>
<span id="timer" aria-live="polite"></span>
<span id="best"></span>
</div>
<div id="game-board" role="grid" aria-labelledby="title"></div>
<div id="status" aria-live="polite"></div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,67 +1,209 @@
const symbols = [
'use strict';
// Símbolos del juego (pool amplio para distintas dificultades)
const SYMBOLS = [
'🐶','🌸','⚽','🍕','🎲','🌞','🚗','🍩',
'⭐','🚀','🎮','💎'
'⭐','🚀','🎮','💎','🐱','🍔','🍟','🎧',
'🍓','🍍','🥝','🍇','🍒','🍉','🍊','🧩',
'🎯','🪙','🧠','🦄','🦊','🦁','🐼','🐸',
'🏀','🏐','🎳','🎹','🎻','🥁','🎺','🎷'
];
let cards = [];
// Configuración por dificultad
const DIFFICULTIES = {
facil: { pairs: 6, maxMoves: 40, timeSec: 90 },
normal: { pairs: 12, maxMoves: 60, timeSec: 120 },
dificil: { pairs: 18, maxMoves: 85, timeSec: 180 }
};
// Estado
let deck = []; // símbolos duplicados y mezclados
let firstCard = null;
let secondCard = null;
let lockBoard = false;
let matches = 0;
let moves = 0;
const maxMoves = 45;
let timer = 120; // segundos
let timer = 0;
let timerInterval = null;
const boardDiv = document.getElementById("game-board");
const statusDiv = document.getElementById("status");
const restartBtn = document.getElementById("restart-btn");
const movesSpan = document.getElementById("moves");
const timerSpan = document.getElementById("timer");
let totalPairs = DIFFICULTIES.normal.pairs;
let maxMoves = DIFFICULTIES.normal.maxMoves;
// DOM
const boardDiv = document.getElementById('game-board');
const statusDiv = document.getElementById('status');
const restartBtn = document.getElementById('restart-btn');
const movesSpan = document.getElementById('moves');
const timerSpan = document.getElementById('timer');
const difficultySelect = document.getElementById('difficulty');
const bestSpan = document.getElementById('best');
// Utils
function shuffle(array) {
for(let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i+1));
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function pickSymbols(n) {
const pool = SYMBOLS.slice();
shuffle(pool);
return pool.slice(0, n);
}
function bestKey(diff) {
return `memoryv2_best_${diff}`;
}
function getBest(diff) {
const v = localStorage.getItem(bestKey(diff));
return v ? parseInt(v, 10) : null;
}
function setBestIfBetter(diff, movesUsed, timeLeft) {
// Métrica compuesta: menor movimientos prioriza, y mayor tiempo restante también importa
// Guardamos mejor (menores movimientos) y desempate por mayor tiempo restante
const key = bestKey(diff);
const prevRaw = localStorage.getItem(key);
let prev = prevRaw ? JSON.parse(prevRaw) : null;
const current = { moves: movesUsed, timeLeft: timeLeft };
if (!prev || current.moves < prev.moves || (current.moves === prev.moves && current.timeLeft > prev.timeLeft)) {
localStorage.setItem(key, JSON.stringify(current));
}
prev = JSON.parse(localStorage.getItem(key));
if (bestSpan) {
if (prev) bestSpan.textContent = `Mejor: ${prev.moves} mov · ${prev.timeLeft}s`;
else bestSpan.textContent = 'Mejor: —';
}
}
function updateBestDisplay(diff) {
const prevRaw = localStorage.getItem(bestKey(diff));
if (!bestSpan) return;
if (!prevRaw) {
bestSpan.textContent = 'Mejor: —';
} else {
const prev = JSON.parse(prevRaw);
bestSpan.textContent = `Mejor: ${prev.moves} mov · ${prev.timeLeft}s`;
}
}
function startGame() {
matches = 0;
moves = 0;
timer = 120;
function updateHUD() {
if (movesSpan) movesSpan.textContent = `Movimientos: ${moves} / ${maxMoves}`;
if (timerSpan) timerSpan.textContent = `Tiempo: ${timer}s`;
}
function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
function startTimer() {
stopTimer();
timerInterval = setInterval(() => {
timer--;
updateHUD();
if (timer <= 0) {
endGame(false, '¡Se acabó el tiempo!');
}
}, 1000);
}
function gridColumnsFor(totalCards) {
// Determina columnas para una rejilla compacta
if (totalCards <= 12) return 4;
if (totalCards <= 16) return 4;
if (totalCards <= 24) return 6;
if (totalCards <= 36) return 6;
return 6;
}
// Render tarjeta
function createCard(symbol, idx) {
const card = document.createElement('button');
card.type = 'button';
card.className = 'card';
card.dataset.index = String(idx);
card.dataset.symbol = symbol;
card.textContent = '';
card.setAttribute('aria-label', 'Carta');
card.setAttribute('aria-disabled', 'false');
card.addEventListener('click', () => flipCard(card));
card.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
flipCard(card);
}
});
return card;
}
function resetTurn() {
firstCard = null;
secondCard = null;
lockBoard = false;
statusDiv.textContent = '';
updateHUD();
clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
cards = [...symbols, ...symbols];
shuffle(cards);
boardDiv.innerHTML = '';
cards.forEach((symbol, idx) => {
const card = document.createElement("div");
card.className = "card";
card.dataset.index = idx;
card.dataset.symbol = symbol;
card.textContent = '';
card.onclick = () => flipCard(card);
boardDiv.appendChild(card);
});
}
function unflip(el) {
if (!el) return;
el.classList.remove('flipped');
el.textContent = '';
el.setAttribute('aria-disabled', 'false');
}
// Inicio/reinicio
function startGame() {
// Leer dificultad
const diff = (difficultySelect && difficultySelect.value) || 'normal';
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
totalPairs = conf.pairs;
maxMoves = conf.maxMoves;
timer = conf.timeSec;
// Reiniciar estado
matches = 0;
moves = 0;
firstCard = null;
secondCard = null;
lockBoard = false;
if (statusDiv) statusDiv.textContent = '';
updateHUD();
updateBestDisplay(diff);
// Construir deck
const chosen = pickSymbols(totalPairs);
deck = [...chosen, ...chosen];
shuffle(deck);
// Render tablero
boardDiv.textContent = '';
const totalCards = deck.length;
const cols = gridColumnsFor(totalCards);
boardDiv.style.gridTemplateColumns = `repeat(${cols}, minmax(45px, 1fr))`;
deck.forEach((symbol, idx) => {
const card = createCard(symbol, idx);
boardDiv.appendChild(card);
});
// Foco inicial y arranque de tiempo
const first = boardDiv.querySelector('.card');
if (first) first.focus();
startTimer();
}
// Lógica de flip
function flipCard(card) {
if (lockBoard) return;
if (card.classList.contains('flipped') || card.classList.contains('matched')) return;
card.classList.add('flipped');
card.textContent = card.dataset.symbol;
card.setAttribute('aria-disabled', 'true');
if (!firstCard) {
firstCard = card;
return; // ¡Esperamos por la segunda carta!
return;
}
if (card === firstCard) return;
secondCard = card;
lockBoard = true;
moves++;
@@ -77,55 +219,40 @@ function flipCard(card) {
firstCard.classList.add('hide');
secondCard.classList.add('hide');
resetTurn();
// Verifica victoria DESPUÉS de ocultar
if (matches === symbols.length) {
clearInterval(timerInterval);
if (matches === totalPairs) {
// Victoria
stopTimer();
statusDiv.textContent = `¡Felicidades! Lo lograste en ${moves} movimientos y te sobraron ${timer} segs 🎉`;
lockBoard = true; // Deshabilita el tablero tras terminar
setBestIfBetter((difficultySelect && difficultySelect.value) || 'normal', moves, timer);
lockBoard = true;
}
}, 800);
}, 600);
} else {
// No es PAR
setTimeout(() => {
firstCard.classList.remove('flipped');
secondCard.classList.remove('flipped');
firstCard.textContent = '';
secondCard.textContent = '';
unflip(firstCard);
unflip(secondCard);
resetTurn();
}, 900);
}, 700);
}
// DERROTA: por movimientos
// Derrota por límite de movimientos
if (moves >= maxMoves) {
endGame(false, "Has alcanzado el límite de movimientos. ¡Inténtalo otra vez!");
endGame(false, 'Has alcanzado el límite de movimientos. ¡Inténtalo otra vez!');
}
}
function updateHUD() {
movesSpan.textContent = `Movimientos: ${moves} / ${maxMoves}`;
timerSpan.textContent = `Tiempo: ${timer}s`;
}
function updateTimer() {
timer--;
updateHUD();
if (timer <= 0) {
endGame(false, "¡Se acabó el tiempo!");
}
}
function resetTurn() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
function endGame(win, msg) {
lockBoard = true;
clearInterval(timerInterval);
stopTimer();
statusDiv.textContent = msg;
}
restartBtn.onclick = startGame;
// Listeners
restartBtn.addEventListener('click', startGame);
if (difficultySelect) {
difficultySelect.addEventListener('change', startGame);
}
// Init
startGame();

View File

@@ -3,13 +3,29 @@
<head>
<meta charset="UTF-8">
<title>Juego de Memoria</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Juego de Memoria</h1>
<h1 id="title">Juego de Memoria</h1>
<p>Haz clic en las cartas para darles la vuelta y encuentra las parejas.</p>
<div id="game-board"></div>
<div id="info"></div>
<div id="controls">
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="facil">Fácil (6 parejas)</option>
<option value="normal" selected>Normal (8 parejas)</option>
<option value="dificil">Difícil (12 parejas)</option>
<option value="extremo">Extremo (18 parejas)</option>
</select>
</div>
<div id="hud">
Movimientos: <span id="moves">0</span> · Tiempo: <span id="time">0s</span> · Mejor: <span id="best-score"></span>
</div>
<div id="game-board" role="grid" aria-labelledby="title"></div>
<div id="info" aria-live="polite"></div>
<button id="reset-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,85 +1,228 @@
const symbols = ['🍎','🍌','🍒','🍇','🍉','🍑','🍊','🍓'];
let cards = [];
'use strict';
// Símbolos disponibles (>= 18 únicos para niveles altos)
const SYMBOLS = [
'🍎','🍌','🍒','🍇','🍉','🍑','🍊','🍓',
'🥝','🍍','🥥','🍐','🍈','🍏','🍔','🍕',
'🎈','⭐','🌙','☀️','⚽','🏀','🎲','🎵',
'🐶','🐱','🐼','🐸','🦊','🦁','🦄','🐯',
'🚗','🚀','✈️','🚲','🏝️','🏰'
];
const DIFFICULTIES = {
facil: { pairs: 6 },
normal: { pairs: 8 },
dificil: { pairs: 12 },
extremo: { pairs: 18 }
};
// Estado del juego
let deck = []; // array de símbolos duplicados y mezclados
let firstCard = null;
let secondCard = null;
let lockBoard = false;
let matches = 0;
let moves = 0;
let totalPairs = DIFFICULTIES.normal.pairs;
let timerId = null;
let startTime = 0;
// DOM
const gameBoard = document.getElementById('game-board');
const infoDiv = document.getElementById('info');
const resetBtn = document.getElementById('reset-btn');
const difficultySelect = document.getElementById('difficulty');
const movesSpan = document.getElementById('moves');
const timeSpan = document.getElementById('time');
const bestSpan = document.getElementById('best-score');
// Utils
function shuffle(array) {
// Fisher-Yates shuffle
for(let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i+1));
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function pickSymbols(n) {
const pool = SYMBOLS.slice();
shuffle(pool);
return pool.slice(0, n);
}
function formatTime(seconds) {
const s = Math.max(0, Math.floor(seconds));
return `${s}s`;
}
function bestKey(diff) {
return `memory_best_time_${diff}`;
}
function getBest(diff) {
const v = localStorage.getItem(bestKey(diff));
return v ? parseInt(v, 10) : null;
}
function setBestIfBetter(diff, timeSec) {
const prev = getBest(diff);
if (prev === null || timeSec < prev) {
localStorage.setItem(bestKey(diff), String(timeSec));
}
const best = getBest(diff);
if (bestSpan) bestSpan.textContent = best !== null ? `${best}s` : '—';
}
function startTimer() {
stopTimer();
startTime = Date.now();
timerId = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000;
if (timeSpan) timeSpan.textContent = formatTime(elapsed);
}, 250);
}
function stopTimer() {
if (timerId) {
clearInterval(timerId);
timerId = null;
}
}
function gridColumnsFor(totalCards) {
// Aproxima columnas en función del total para formar rejilla compacta
if (totalCards <= 12) return 4;
if (totalCards <= 16) return 4;
if (totalCards <= 24) return 6;
if (totalCards <= 30) return 6;
return 6;
}
function setupBoard() {
// Leer dificultad
const diff = (difficultySelect && difficultySelect.value) || 'normal';
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
totalPairs = conf.pairs;
// Reiniciar estado
matches = 0;
moves = 0;
firstCard = null;
secondCard = null;
lockBoard = false;
infoDiv.textContent = '';
// Duplica los símbolos y los mezcla
cards = [...symbols, ...symbols];
shuffle(cards);
gameBoard.innerHTML = '';
cards.forEach((symbol, idx) => {
const cardEl = document.createElement('div');
if (movesSpan) movesSpan.textContent = String(moves);
if (infoDiv) infoDiv.textContent = '';
// Construir mazo
const chosen = pickSymbols(totalPairs);
deck = [...chosen, ...chosen];
shuffle(deck);
// Render tablero accesible
gameBoard.textContent = '';
const totalCards = deck.length;
const cols = gridColumnsFor(totalCards);
gameBoard.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
deck.forEach((symbol, idx) => {
const cardEl = document.createElement('button');
cardEl.type = 'button';
cardEl.className = 'card';
cardEl.dataset.index = idx;
cardEl.dataset.index = String(idx);
cardEl.dataset.symbol = symbol;
cardEl.textContent = '';
cardEl.onclick = () => flipCard(cardEl);
cardEl.setAttribute('role', 'gridcell');
cardEl.setAttribute('aria-label', 'Carta de memoria');
cardEl.setAttribute('aria-disabled', 'false');
cardEl.addEventListener('click', () => flipCard(cardEl));
cardEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
flipCard(cardEl);
}
});
gameBoard.appendChild(cardEl);
});
// Mostrar mejor tiempo
const best = getBest(diff);
if (bestSpan) bestSpan.textContent = best !== null ? `${best}s` : '—';
// Arrancar temporizador
startTimer();
// Foco inicial
const first = gameBoard.querySelector('.card');
if (first) first.focus();
}
function flipCard(cardEl) {
if (lockBoard) return;
if (cardEl.classList.contains('flipped') || cardEl.classList.contains('matched')) return;
// Voltear carta
cardEl.classList.add('flipped');
cardEl.textContent = cardEl.dataset.symbol;
cardEl.setAttribute('aria-disabled', 'true');
if (!firstCard) {
firstCard = cardEl;
} else if(!secondCard && cardEl !== firstCard) {
return;
}
if (!secondCard && cardEl !== firstCard) {
secondCard = cardEl;
lockBoard = true;
moves++;
if (movesSpan) movesSpan.textContent = String(moves);
if (firstCard.dataset.symbol === secondCard.dataset.symbol) {
// ¡Es pareja!
// Pareja encontrada
firstCard.classList.add('matched');
secondCard.classList.add('matched');
matches++;
resetFlipped(700);
if (matches === symbols.length) {
infoDiv.textContent = '¡Felicidades! Has encontrado todas las parejas 🎉';
}
} else {
// No es pareja, voltea las cartas después de un momento
setTimeout(() => {
firstCard.classList.remove('flipped');
secondCard.classList.remove('flipped');
firstCard.textContent = '';
secondCard.textContent = '';
resetFlipped(0);
}, 900);
resetSelection();
if (matches === totalPairs) {
// Fin de partida
const elapsed = Math.floor((Date.now() - startTime) / 1000);
if (infoDiv) infoDiv.textContent = `¡Felicidades! 🎉 Parejas: ${totalPairs} · Movimientos: ${moves} · Tiempo: ${elapsed}s`;
stopTimer();
const diff = (difficultySelect && difficultySelect.value) || 'normal';
setBestIfBetter(diff, elapsed);
}
}, 400);
} else {
// No es pareja, desvoltear tras un momento
setTimeout(() => {
unflip(firstCard);
unflip(secondCard);
resetSelection();
}, 700);
}
}
}
function resetFlipped(delay) {
setTimeout(() => {
firstCard = null;
secondCard = null;
lockBoard = false;
}, delay);
function unflip(el) {
if (!el) return;
el.classList.remove('flipped');
el.textContent = '';
el.setAttribute('aria-disabled', 'false');
}
resetBtn.onclick = setupBoard;
function resetSelection() {
firstCard = null;
secondCard = null;
lockBoard = false;
}
// Listeners
resetBtn.addEventListener('click', () => {
stopTimer();
setupBoard();
});
if (difficultySelect) {
difficultySelect.addEventListener('change', () => {
stopTimer();
setupBoard();
});
}
// Init
setupBoard();

View File

@@ -143,4 +143,85 @@ h1 {
.card { font-size: clamp(1rem, 20vw, 2rem); }
}
/* ::::::::::::::::::::::::::: */
/* ::::::::::::::::::::::::::: */
/* Controles y HUD */
#controls {
margin: 1rem auto 0.5rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
#controls select#difficulty {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
}
#hud {
font-size: 1rem;
color: #364f6b;
margin: 0.4rem 0 0.8rem 0;
}
/* Accesibilidad: foco visible y estado deshabilitado */
.card {
outline: none;
border: none;
}
.card:focus-visible {
box-shadow: 0 0 0 4px rgba(252, 81, 133, 0.35);
}
.card[aria-disabled="true"] {
cursor: default;
}
/* Ajustes de tablero para rejillas dinámicas desde JS */
#game-board {
/* JS establecerá grid-template-columns dinámicamente */
}
/* Modo oscuro mejorado */
@media (prefers-color-scheme: dark) {
body {
background: #0f1222;
color: #eaeaf0;
}
h1, #info {
color: #eaeaf0;
}
#hud {
color: #a0a3b0;
}
#controls select#difficulty {
background: #20233a;
color: #eaeaf0;
border-color: #2c3252;
}
#game-board {
/* sin cambios, deja que JS decida columnas */
}
.card {
background: #20233a;
color: #eaeaf0;
box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12);
}
.card.flipped {
background: #3d5afe;
color: #fff;
}
.card.matched {
background: #172441;
color: #fff;
opacity: 0.8;
}
#reset-btn {
background: #3d5afe;
}
#reset-btn:hover {
background: #0a2459;
}
.card:focus-visible {
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
}
}

View File

@@ -3,20 +3,34 @@
<head>
<meta charset="UTF-8">
<title>Piedra, Papel o Tijera</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Piedra, Papel o Tijera</h1>
<div id="scoreboard">
<h1 id="title">Piedra, Papel o Tijera</h1>
<div id="controls">
<label for="best-of">Partida:</label>
<select id="best-of">
<option value="1">A 1 ronda</option>
<option value="3" selected>Mejor de 3</option>
<option value="5">Mejor de 5</option>
<option value="7">Mejor de 7</option>
</select>
</div>
<div id="scoreboard" aria-live="polite">
Tú: <span id="user-score">0</span> |
Máquina: <span id="computer-score">0</span>
Máquina: <span id="computer-score">0</span> · Ronda: <span id="round">1</span>/<span id="rounds-total">3</span>
</div>
<div id="choices">
<button data-choice="piedra">🪨 Piedra</button>
<button data-choice="papel">📄 Papel</button>
<button data-choice="tijera">✂️ Tijera</button>
<div id="choices" role="group" aria-labelledby="title">
<button data-choice="piedra" aria-label="Piedra">🪨 Piedra</button>
<button data-choice="papel" aria-label="Papel">📄 Papel</button>
<button data-choice="tijera" aria-label="Tijera">✂️ Tijera</button>
</div>
<div id="result"></div>
<div id="result" aria-live="polite"></div>
<button id="reset-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,62 +1,151 @@
'use strict';
const userScoreSpan = document.getElementById('user-score');
const computerScoreSpan = document.getElementById('computer-score');
const resultDiv = document.getElementById('result');
const choiceButtons = document.querySelectorAll('#choices button');
const resetBtn = document.getElementById('reset-btn');
let userScore = 0;
let computerScore = 0;
const bestSelect = document.getElementById('best-of');
const roundSpan = document.getElementById('round');
const roundsTotalSpan = document.getElementById('rounds-total');
const choices = ['piedra', 'papel', 'tijera'];
let userScore = 0;
let computerScore = 0;
let roundNumber = 1;
let bestOf = 3;
let targetWins = 2;
let gameOver = false;
function computerPlay() {
const idx = Math.floor(Math.random() * 3);
const idx = Math.floor(Math.random() * choices.length);
return choices[idx];
}
function playRound(userChoice) {
const computerChoice = computerPlay();
let resultMsg = `Tu elección: ${emoji(userChoice)} ${capitalize(userChoice)}<br>
Máquina: ${emoji(computerChoice)} ${capitalize(computerChoice)}<br>`;
if (userChoice === computerChoice) {
resultMsg += "<strong>¡Empate!</strong>";
} else if (
(userChoice === 'piedra' && computerChoice === 'tijera') ||
(userChoice === 'papel' && computerChoice === 'piedra') ||
(userChoice === 'tijera' && computerChoice === 'papel')
) {
userScore++;
userScoreSpan.textContent = userScore;
resultMsg += "<strong>¡Ganaste esta ronda! 🎉</strong>";
} else {
computerScore++;
computerScoreSpan.textContent = computerScore;
resultMsg += "<strong>La máquina gana esta ronda.</strong>";
}
resultDiv.innerHTML = resultMsg;
}
function emoji(choice) {
if (choice === 'piedra') return '🪨';
if (choice === 'papel') return '📄';
if (choice === 'tijera') return '✂️';
return '';
}
function capitalize(word) {
return word.charAt(0).toUpperCase() + word.slice(1);
}
choiceButtons.forEach(btn => {
btn.onclick = () => playRound(btn.getAttribute('data-choice'));
});
function updateHUD() {
if (userScoreSpan) userScoreSpan.textContent = String(userScore);
if (computerScoreSpan) computerScoreSpan.textContent = String(computerScore);
if (roundSpan) roundSpan.textContent = String(roundNumber);
if (roundsTotalSpan) roundsTotalSpan.textContent = String(bestOf);
}
resetBtn.onclick = () => {
function setBestOf() {
const v = bestSelect && bestSelect.value ? parseInt(bestSelect.value, 10) : 3;
bestOf = Number.isInteger(v) ? Math.max(1, Math.min(v, 9)) : 3;
targetWins = Math.floor(bestOf / 2) + 1;
roundsTotalSpan.textContent = String(bestOf);
resetGame(); // reinicia con la nueva configuración
}
function endMatchIfNeeded() {
// Fin anticipado si alguien alcanza las victorias necesarias
if (userScore >= targetWins) {
gameOver = true;
resultDiv.textContent = `Has ganado la partida ${userScore}-${computerScore} (mejor de ${bestOf}). 🎉`;
return true;
}
if (computerScore >= targetWins) {
gameOver = true;
resultDiv.textContent = `La máquina gana la partida ${computerScore}-${userScore} (mejor de ${bestOf}). 🤖`;
return true;
}
// Fin por alcanzar el número máximo de rondas
if (roundNumber > bestOf) {
gameOver = true;
if (userScore > computerScore) {
resultDiv.textContent = `Has ganado la partida ${userScore}-${computerScore} (mejor de ${bestOf}). 🎉`;
} else if (computerScore > userScore) {
resultDiv.textContent = `La máquina gana la partida ${computerScore}-${userScore} (mejor de ${bestOf}). 🤖`;
} else {
resultDiv.textContent = `Empate global ${userScore}-${computerScore} (mejor de ${bestOf}).`;
}
return true;
}
return false;
}
function playRound(userChoice) {
if (gameOver) return;
const computerChoice = computerPlay();
let msg = `Tu elección: ${emoji(userChoice)} ${capitalize(userChoice)}\n` +
`Máquina: ${emoji(computerChoice)} ${capitalize(computerChoice)}\n`;
if (userChoice === computerChoice) {
msg += 'Resultado: ¡Empate!';
// Empate cuenta como ronda consumida
roundNumber += 1;
} else if (
(userChoice === 'piedra' && computerChoice === 'tijera') ||
(userChoice === 'papel' && computerChoice === 'piedra') ||
(userChoice === 'tijera' && computerChoice === 'papel')
) {
userScore += 1;
msg += 'Resultado: ¡Ganaste esta ronda! 🎉';
// Avanza ronda tras cada ronda jugada
roundNumber += 1;
} else {
computerScore += 1;
msg += 'Resultado: La máquina gana esta ronda.';
roundNumber += 1;
}
// Mostrar resultado de la ronda (usamos textContent para evitar HTML)
resultDiv.textContent = msg;
updateHUD();
// Comprobar fin anticipado o por límite de rondas
if (endMatchIfNeeded()) {
return;
}
}
function handleChoice(choice) {
playRound(choice);
}
function resetGame() {
userScore = 0;
computerScore = 0;
userScoreSpan.textContent = userScore;
computerScoreSpan.textContent = computerScore;
roundNumber = 1;
gameOver = false;
resultDiv.textContent = '';
};
updateHUD();
}
// Listeners (evitar handlers inline)
choiceButtons.forEach((btn) => {
btn.addEventListener('click', () => handleChoice(btn.getAttribute('data-choice')));
// Accesibilidad extra: Enter y Espacio activan el botón
btn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleChoice(btn.getAttribute('data-choice'));
}
});
});
resetBtn.addEventListener('click', resetGame);
if (bestSelect) {
bestSelect.addEventListener('change', setBestOf);
}
// Init
setBestOf();
updateHUD();

View File

@@ -3,13 +3,25 @@
<head>
<meta charset="UTF-8">
<title>Pong Game</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Pong Clásico</h1>
<h1 id="title">Pong Clásico</h1>
<p>Usa las flechas arriba/abajo para mover tu pala. ¡No dejes que la bola pase!</p>
<canvas id="pong" width="600" height="400"></canvas>
<div id="score"></div>
<div id="controls">
<label for="difficulty">Dificultad:</label>
<select id="difficulty">
<option value="facil">Fácil</option>
<option value="normal" selected>Normal</option>
<option value="dificil">Difícil</option>
</select>
<button id="pause-btn">Pausar</button>
</div>
<canvas id="pong" width="600" height="400" aria-label="Tablero Pong"></canvas>
<div id="score" aria-live="polite"></div>
<button id="restart-btn">Reiniciar</button>
<script src="script.js"></script>
</body>

View File

@@ -1,125 +1,332 @@
'use strict';
const canvas = document.getElementById('pong');
const ctx = canvas.getContext('2d');
const scoreDiv = document.getElementById('score');
const restartBtn = document.getElementById('restart-btn');
const difficultySelect = document.getElementById('difficulty');
const pauseBtn = document.getElementById('pause-btn');
const w = canvas.width, h = canvas.height;
const paddleHeight = 80, paddleWidth = 14;
let playerY = h/2 - paddleHeight/2, aiY = h/2 - paddleHeight/2;
// Configuraciones por dificultad
const DIFFICULTIES = {
facil: { ballSpeed: 5.0, aiMaxSpeed: 4.0, paddleHeight: 95 },
normal: { ballSpeed: 6.5, aiMaxSpeed: 5.5, paddleHeight: 80 },
dificil: { ballSpeed: 8.0, aiMaxSpeed: 7.0, paddleHeight: 70 }
};
// Estado
let paddleHeight = DIFFICULTIES.normal.paddleHeight;
const paddleWidth = 14;
let playerY = h / 2 - paddleHeight / 2;
let aiY = h / 2 - paddleHeight / 2;
let playerScore = 0, aiScore = 0;
let ball = {
x: w/2, y: h/2,
size: 12,
speed: 5,
velX: 5,
velY: -4
x: w / 2,
y: h / 2,
r: 12,
baseSpeed: DIFFICULTIES.normal.ballSpeed,
velX: DIFFICULTIES.normal.ballSpeed,
velY: -4.0
};
let up = false, down = false;
let running = true;
let rafId = null;
let aiMaxSpeed = DIFFICULTIES.normal.aiMaxSpeed;
// Dibujo de todo (juego, paletas, bola, marcador)
function draw() {
// Utilidades
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function setDifficulty() {
const diff = difficultySelect && difficultySelect.value ? difficultySelect.value : 'normal';
const conf = DIFFICULTIES[diff] || DIFFICULTIES.normal;
paddleHeight = conf.paddleHeight;
aiMaxSpeed = conf.aiMaxSpeed;
ball.baseSpeed = conf.ballSpeed;
// Reposicionar palas con nueva altura
playerY = clamp(playerY, 0, h - paddleHeight);
aiY = clamp(aiY, 0, h - paddleHeight);
// Reiniciar bola con la velocidad base de la dificultad
resetBall(true);
updateScoreHUD();
}
function updateScoreHUD() {
scoreDiv.textContent = `Tú: ${playerScore} | Máquina: ${aiScore}`;
}
function drawNet() {
ctx.fillStyle = '#f2e9e4';
for (let i = 15; i < h; i += 30) {
ctx.fillRect(w / 2 - 2, i, 4, 14);
}
}
function drawScene() {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#f2e9e4";
// Jugador (izq)
// Palas
ctx.fillStyle = '#f2e9e4';
ctx.fillRect(16, playerY, paddleWidth, paddleHeight);
// AI (der)
ctx.fillRect(w-16-paddleWidth, aiY, paddleWidth, paddleHeight);
ctx.fillRect(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight);
// Bola
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.size, 0, 2 * Math.PI);
ctx.fillStyle = "#f6c90e";
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fillStyle = '#f6c90e';
ctx.fill();
// Red central
for(let i=15;i<h;i+=30){
ctx.fillRect(w/2-2,i,4,14);
drawNet();
// HUD
updateScoreHUD();
}
function pauseGame() {
if (!running) return;
running = false;
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
if (pauseBtn) pauseBtn.textContent = 'Reanudar';
}
function resumeGame() {
if (running) return;
running = true;
if (pauseBtn) pauseBtn.textContent = 'Pausar';
rafId = requestAnimationFrame(gameLoop);
}
function togglePause() {
if (running) pauseGame();
else resumeGame();
}
function resetBall(hardReset = false) {
ball.x = w / 2;
ball.y = h / 2;
const speed = ball.baseSpeed;
ball.velX = (Math.random() > 0.5 ? speed : -speed);
ball.velY = (Math.random() - 0.5) * speed * 1.2;
if (!hardReset) {
pauseGame();
setTimeout(() => resumeGame(), 700);
}
// Score
scoreDiv.innerHTML = `<b>Tú:</b> ${playerScore} &nbsp; | &nbsp; <b>Máquina:</b> ${aiScore}`;
}
function handlePlayerMovement() {
const playerSpeed = 7.0;
if (up) playerY -= playerSpeed;
if (down) playerY += playerSpeed;
playerY = clamp(playerY, 0, h - paddleHeight);
}
function predictBallYAtX(targetX) {
// Predicción simple: extrapola y con rebotes verticales
let px = ball.x;
let py = ball.y;
let vx = ball.velX;
let vy = ball.velY;
const r = ball.r;
const maxSteps = 1000;
for (let i = 0; i < maxSteps; i++) {
// Avanza
px += vx;
py += vy;
// Rebote arriba/abajo
if (py - r < 0) {
py = r;
vy = -vy;
} else if (py + r > h) {
py = h - r;
vy = -vy;
}
// Si hemos cruzado el targetX
if ((vx > 0 && px + r >= targetX) || (vx < 0 && px - r <= targetX)) {
return py;
}
// Seguridad: si vx se hace cero (no debería), rompe
if (Math.abs(vx) < 0.0001) break;
}
return py;
}
function handleAIMovement() {
// IA solo sigue cuando la bola va hacia ella, si no recentra lentamente
const aiCenter = aiY + paddleHeight / 2;
let targetY;
if (ball.velX > 0) {
// Objetivo es el centro de la pala cuando la bola llegue al borde derecho
const targetX = w - 16 - paddleWidth - ball.r;
targetY = predictBallYAtX(targetX);
} else {
// Bola alejándose: recentrar
targetY = h / 2;
}
const desired = targetY - aiCenter;
const step = clamp(desired, -aiMaxSpeed, aiMaxSpeed);
aiY += step;
aiY = clamp(aiY, 0, h - paddleHeight);
}
function collideBallWithWalls() {
// Arriba/abajo
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.velY *= -1;
} else if (ball.y + ball.r > h) {
ball.y = h - ball.r;
ball.velY *= -1;
}
}
function collideBallWithPaddle(px, py, pw, ph, reverseXSign) {
// Chequeo de colisión círculo-rectángulo
const nearestX = clamp(ball.x, px, px + pw);
const nearestY = clamp(ball.y, py, py + ph);
const dx = ball.x - nearestX;
const dy = ball.y - nearestY;
const dist2 = dx * dx + dy * dy;
if (dist2 <= ball.r * ball.r) {
// Rebote con ángulo según punto de impacto relativo
const hitPos = ((nearestY - py) - ph / 2) / (ph / 2); // -1..1 con referencia al centro
const maxAngle = Math.PI / 3; // 60 grados
const speed = Math.max(Math.abs(ball.velX), Math.abs(ball.velY), ball.baseSpeed);
const angle = hitPos * maxAngle;
const newVX = speed * Math.cos(angle);
const newVY = speed * Math.sin(angle);
ball.velX = reverseXSign ? -Math.abs(newVX) : Math.abs(newVX);
ball.velY = newVY;
// Pequeño empujón para salir del rect
ball.x += ball.velX * 0.2;
ball.y += ball.velY * 0.2;
}
}
function checkGoals() {
// Gol IA (bola sale por izquierda)
if (ball.x + ball.r < 0) {
aiScore++;
resetBall();
return true;
}
// Gol jugador (bola sale por derecha)
if (ball.x - ball.r > w) {
playerScore++;
resetBall();
return true;
}
return false;
}
function stepBall() {
ball.x += ball.velX;
ball.y += ball.velY;
}
function gameLoop() {
if (!running) return;
// Jugador
if (up) playerY -= 7;
if (down) playerY += 7;
playerY = Math.max(0, Math.min(h-paddleHeight, playerY));
// AI: sigue la bola
if (aiY + paddleHeight/2 < ball.y - 12) aiY += 5;
else if (aiY + paddleHeight/2 > ball.y + 12) aiY -= 5;
aiY = Math.max(0, Math.min(h-paddleHeight, aiY));
handlePlayerMovement();
handleAIMovement();
// Bola
ball.x += ball.velX;
ball.y += ball.velY;
stepBall();
collideBallWithWalls();
// Colisión con pared arriba/abajo
if (ball.y - ball.size < 0 || ball.y + ball.size > h) ball.velY *= -1;
// Colisiones con palas
// Jugador (izquierda)
collideBallWithPaddle(16, playerY, paddleWidth, paddleHeight, false);
// IA (derecha)
collideBallWithPaddle(w - 16 - paddleWidth, aiY, paddleWidth, paddleHeight, true);
// Colisión con paleta jugador
if (
ball.x - ball.size < 16 + paddleWidth &&
ball.y > playerY && ball.y < playerY + paddleHeight
) {
ball.velX = Math.abs(ball.velX);
ball.velY += (Math.random() - 0.5) * 2.5;
if (checkGoals()) {
drawScene();
return;
}
// Colisión con paleta AI
if (
ball.x + ball.size > w-16-paddleWidth &&
ball.y > aiY && ball.y < aiY + paddleHeight
) {
ball.velX = -Math.abs(ball.velX);
ball.velY += (Math.random() - 0.5) * 2.5;
}
// Gol jugador
if (ball.x - ball.size < 0) {
aiScore++;
resetBall();
}
// Gol máquina
if (ball.x + ball.size > w) {
playerScore++;
resetBall();
}
draw();
requestAnimationFrame(gameLoop);
drawScene();
rafId = requestAnimationFrame(gameLoop);
}
function resetBall() {
ball.x = w/2; ball.y = h/2;
ball.velX = (Math.random() > 0.5 ? 5 : -5);
ball.velY = (Math.random() - 0.5) * 7;
running = false;
setTimeout(() => {
running = true;
requestAnimationFrame(gameLoop);
}, 900);
}
document.addEventListener('keydown', e => {
if (e.key === 'ArrowUp') up = true;
if (e.key === 'ArrowDown') down = true;
});
document.addEventListener('keyup', e => {
if (e.key === 'ArrowUp') up = false;
if (e.key === 'ArrowDown') down = false;
});
restartBtn.onclick = () => {
playerScore = aiScore = 0;
playerY = h/2 - paddleHeight/2;
aiY = h/2 - paddleHeight/2;
resetBall();
function resetGame() {
playerScore = 0;
aiScore = 0;
playerY = h / 2 - paddleHeight / 2;
aiY = h / 2 - paddleHeight / 2;
resetBall(true);
running = true;
requestAnimationFrame(gameLoop);
};
updateScoreHUD();
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(gameLoop);
}
draw();
gameLoop();
function attachEvents() {
// Teclado (evita scroll con flechas)
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
up = true;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
down = true;
}
});
window.addEventListener('keyup', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
up = false;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
down = false;
}
});
// Pausa
if (pauseBtn) {
pauseBtn.addEventListener('click', togglePause);
}
// Dificultad
if (difficultySelect) {
difficultySelect.addEventListener('change', () => {
setDifficulty();
});
}
// Reinicio
restartBtn.addEventListener('click', resetGame);
}
function startGame() {
attachEvents();
setDifficulty();
drawScene();
rafId = requestAnimationFrame(gameLoop);
}
// Init
startGame();

View File

@@ -33,4 +33,53 @@ h1 {
}
#restart-btn:hover {
background: #232946;
}
/* Controles y accesibilidad */
#controls {
margin: 0.8rem auto 0.4rem auto;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
#controls select#difficulty {
font-size: 1em;
padding: 6px 10px;
border-radius: 8px;
border: 1px solid #ddd;
background: #232946;
color: #f2e9e4;
}
#pause-btn {
font-size: 1em;
padding: 6px 16px;
border: none;
border-radius: 8px;
background: #4a4e69;
color: #fff;
cursor: pointer;
transition: background 0.2s;
}
#pause-btn:hover {
background: #232946;
}
/* Foco accesible */
#pause-btn:focus-visible,
#restart-btn:focus-visible,
#controls select#difficulty:focus-visible {
outline: none;
box-shadow: 0 0 0 4px rgba(246, 201, 14, 0.35);
}
/* Estado deshabilitado para botones */
#pause-btn:disabled,
#restart-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Ajuste del marcador */
#score {
font-weight: 600;
}