| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Vita Mahjong</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @keyframes tileHover { |
| 0% { transform: translateY(0); } |
| 50% { transform: translateY(-5px); } |
| 100% { transform: translateY(0); } |
| } |
| |
| .tile-hover:hover { |
| animation: tileHover 0.3s ease; |
| transform: translateY(-5px); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2); |
| } |
| |
| .tile-selected { |
| transform: translateY(-15px); |
| box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.2); |
| z-index: 10; |
| } |
| |
| .tile-matched { |
| animation: fadeOut 0.5s ease forwards; |
| } |
| |
| @keyframes fadeOut { |
| to { |
| opacity: 0; |
| transform: scale(0.8); |
| } |
| } |
| |
| .board-container { |
| perspective: 1000px; |
| } |
| |
| .tile { |
| transition: all 0.3s ease; |
| transform-style: preserve-3d; |
| } |
| |
| .tile-inner { |
| position: relative; |
| width: 100%; |
| height: 100%; |
| transform-style: preserve-3d; |
| } |
| |
| .tile-front, .tile-back { |
| position: absolute; |
| width: 100%; |
| height: 100%; |
| backface-visibility: hidden; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .tile-front { |
| background: linear-gradient(145deg, #f0f0f0, #ffffff); |
| transform: rotateY(180deg); |
| } |
| |
| .tile-back { |
| background: linear-gradient(145deg, #4f46e5, #7c3aed); |
| color: white; |
| } |
| |
| .flipped { |
| transform: rotateY(180deg); |
| } |
| |
| .level-complete { |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| 0% { transform: scale(1); } |
| 50% { transform: scale(1.05); } |
| 100% { transform: scale(1); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen font-sans"> |
| <div class="container mx-auto px-4 py-8"> |
| |
| <header class="flex justify-between items-center mb-8"> |
| <div class="flex items-center"> |
| <i class="fas fa-dragon text-4xl text-purple-600 mr-3"></i> |
| <h1 class="text-3xl font-bold text-gray-800">Vita Mahjong</h1> |
| </div> |
| <div class="flex items-center space-x-4"> |
| <div class="bg-white rounded-lg shadow p-3 flex items-center"> |
| <i class="fas fa-clock text-purple-600 mr-2"></i> |
| <span id="timer" class="font-bold">00:00</span> |
| </div> |
| <div class="bg-white rounded-lg shadow p-3 flex items-center"> |
| <i class="fas fa-layer-group text-purple-600 mr-2"></i> |
| <span id="level" class="font-bold">Level 1</span> |
| </div> |
| <div class="bg-white rounded-lg shadow p-3 flex items-center"> |
| <i class="fas fa-star text-yellow-500 mr-2"></i> |
| <span id="score" class="font-bold">0</span> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <div class="flex justify-between mb-6"> |
| <div class="flex space-x-3"> |
| <button id="new-game" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg shadow flex items-center"> |
| <i class="fas fa-plus-circle mr-2"></i> New Game |
| </button> |
| <button id="hint" class="bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-lg shadow flex items-center"> |
| <i class="fas fa-lightbulb mr-2"></i> Hint |
| </button> |
| </div> |
| <div> |
| <button id="sound-toggle" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded-lg shadow flex items-center"> |
| <i class="fas fa-volume-up mr-2"></i> Sound On |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="board-container bg-white rounded-xl shadow-xl p-6 mb-6"> |
| <div id="board" class="grid grid-cols-8 gap-3 mx-auto"></div> |
| </div> |
|
|
| |
| <div id="game-status" class="text-center mb-6 hidden"> |
| <div class="inline-block bg-green-100 border-l-4 border-green-500 text-green-700 p-4 rounded-lg"> |
| <p class="font-bold">Level Complete!</p> |
| </div> |
| </div> |
|
|
| |
| <div id="level-complete-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> |
| <div class="bg-white rounded-xl shadow-2xl p-8 max-w-md w-full level-complete"> |
| <div class="text-center"> |
| <div class="text-6xl text-yellow-500 mb-4"> |
| <i class="fas fa-trophy"></i> |
| </div> |
| <h2 class="text-3xl font-bold text-gray-800 mb-2">Level Complete!</h2> |
| <p class="text-gray-600 mb-6">Great job! Ready for the next challenge?</p> |
| <div class="grid grid-cols-2 gap-4 mb-6"> |
| <div class="bg-purple-50 rounded-lg p-3"> |
| <p class="text-sm text-purple-600">Time</p> |
| <p id="level-time" class="font-bold text-xl">00:45</p> |
| </div> |
| <div class="bg-purple-50 rounded-lg p-3"> |
| <p class="text-sm text-purple-600">Score</p> |
| <p id="level-score" class="font-bold text-xl">+250</p> |
| </div> |
| </div> |
| <button id="next-level" class="w-full bg-purple-600 hover:bg-purple-700 text-white py-3 rounded-lg shadow-lg font-bold"> |
| Next Level <i class="fas fa-arrow-right ml-2"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="game-over-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> |
| <div class="bg-white rounded-xl shadow-2xl p-8 max-w-md w-full"> |
| <div class="text-center"> |
| <div class="text-6xl text-red-500 mb-4"> |
| <i class="fas fa-gamepad"></i> |
| </div> |
| <h2 class="text-3xl font-bold text-gray-800 mb-2">Game Over</h2> |
| <p class="text-gray-600 mb-6">Better luck next time!</p> |
| <div class="grid grid-cols-2 gap-4 mb-6"> |
| <div class="bg-purple-50 rounded-lg p-3"> |
| <p class="text-sm text-purple-600">Level Reached</p> |
| <p id="final-level" class="font-bold text-xl">3</p> |
| </div> |
| <div class="bg-purple-50 rounded-lg p-3"> |
| <p class="text-sm text-purple-600">Total Score</p> |
| <p id="final-score" class="font-bold text-xl">750</p> |
| </div> |
| </div> |
| <button id="play-again" class="w-full bg-purple-600 hover:bg-purple-700 text-white py-3 rounded-lg shadow-lg font-bold"> |
| Play Again <i class="fas fa-redo ml-2"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const state = { |
| board: [], |
| level: 1, |
| score: 0, |
| time: 0, |
| timerInterval: null, |
| selectedTiles: [], |
| matchedPairs: 0, |
| totalPairs: 0, |
| soundEnabled: true, |
| gameActive: false |
| }; |
| |
| |
| const boardElement = document.getElementById('board'); |
| const timerElement = document.getElementById('timer'); |
| const levelElement = document.getElementById('level'); |
| const scoreElement = document.getElementById('score'); |
| const newGameButton = document.getElementById('new-game'); |
| const hintButton = document.getElementById('hint'); |
| const soundToggleButton = document.getElementById('sound-toggle'); |
| const gameStatusElement = document.getElementById('game-status'); |
| const levelCompleteModal = document.getElementById('level-complete-modal'); |
| const gameOverModal = document.getElementById('game-over-modal'); |
| const nextLevelButton = document.getElementById('next-level'); |
| const playAgainButton = document.getElementById('play-again'); |
| const levelTimeElement = document.getElementById('level-time'); |
| const levelScoreElement = document.getElementById('level-score'); |
| const finalLevelElement = document.getElementById('final-level'); |
| const finalScoreElement = document.getElementById('final-score'); |
| |
| |
| const tileTypes = [ |
| '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '9m', |
| '1s', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', |
| '1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', |
| 'ew', 'sw', 'ww', 'nw', |
| 'wd', 'gd', 'rd' |
| ]; |
| |
| |
| function initGame() { |
| state.level = 1; |
| state.score = 0; |
| state.time = 0; |
| state.matchedPairs = 0; |
| state.gameActive = true; |
| |
| clearInterval(state.timerInterval); |
| startTimer(); |
| |
| updateUI(); |
| createBoard(); |
| } |
| |
| |
| function createBoard() { |
| boardElement.innerHTML = ''; |
| state.board = []; |
| state.selectedTiles = []; |
| state.matchedPairs = 0; |
| |
| |
| const pairs = Math.min(4 + state.level, 32); |
| state.totalPairs = pairs; |
| |
| |
| let tiles = []; |
| const availableTypes = [...tileTypes].sort(() => 0.5 - Math.random()).slice(0, pairs); |
| |
| availableTypes.forEach(type => { |
| tiles.push(type, type); |
| }); |
| |
| |
| tiles = shuffleArray(tiles); |
| |
| |
| const cols = 8; |
| const rows = Math.ceil(tiles.length / cols); |
| |
| for (let i = 0; i < rows; i++) { |
| const row = []; |
| for (let j = 0; j < cols; j++) { |
| const index = i * cols + j; |
| if (index < tiles.length) { |
| row.push({ |
| type: tiles[index], |
| flipped: false, |
| matched: false, |
| row: i, |
| col: j |
| }); |
| } else { |
| row.push(null); |
| } |
| } |
| state.board.push(row); |
| } |
| |
| |
| renderBoard(); |
| } |
| |
| |
| function renderBoard() { |
| boardElement.innerHTML = ''; |
| |
| |
| boardElement.className = `grid gap-3 mx-auto`; |
| boardElement.style.gridTemplateColumns = `repeat(8, minmax(0, 1fr))`; |
| |
| state.board.forEach((row, rowIndex) => { |
| row.forEach((tile, colIndex) => { |
| if (!tile) return; |
| |
| const tileElement = document.createElement('div'); |
| tileElement.className = `tile aspect-square cursor-pointer transition-all duration-300 ${tile.matched ? 'opacity-0' : ''}`; |
| |
| tileElement.innerHTML = ` |
| <div class="tile-inner ${tile.flipped ? 'flipped' : ''}"> |
| <div class="tile-back flex items-center justify-center"> |
| <i class="fas fa-dragon text-2xl"></i> |
| </div> |
| <div class="tile-front"> |
| <span class="text-3xl font-bold">${getTileSymbol(tile.type)}</span> |
| </div> |
| </div> |
| `; |
| |
| tileElement.addEventListener('click', () => handleTileClick(tile)); |
| |
| if (!tile.matched) { |
| tileElement.classList.add('tile-hover'); |
| } |
| |
| boardElement.appendChild(tileElement); |
| }); |
| }); |
| } |
| |
| |
| function handleTileClick(tile) { |
| if (!state.gameActive || tile.matched || tile.flipped || state.selectedTiles.length >= 2) { |
| return; |
| } |
| |
| |
| tile.flipped = true; |
| state.selectedTiles.push(tile); |
| |
| |
| if (state.soundEnabled) { |
| playSound('flip'); |
| } |
| |
| renderBoard(); |
| |
| |
| if (state.selectedTiles.length === 2) { |
| const [tile1, tile2] = state.selectedTiles; |
| |
| if (tile1.type === tile2.type) { |
| |
| tile1.matched = true; |
| tile2.matched = true; |
| state.matchedPairs++; |
| state.score += 50 * state.level; |
| |
| |
| if (state.soundEnabled) { |
| playSound('match'); |
| } |
| |
| |
| if (state.matchedPairs === state.totalPairs) { |
| levelComplete(); |
| } |
| |
| |
| setTimeout(() => { |
| state.selectedTiles = []; |
| renderBoard(); |
| }, 500); |
| } else { |
| |
| setTimeout(() => { |
| tile1.flipped = false; |
| tile2.flipped = false; |
| state.selectedTiles = []; |
| renderBoard(); |
| |
| |
| if (state.soundEnabled) { |
| playSound('mismatch'); |
| } |
| }, 1000); |
| } |
| } |
| |
| updateUI(); |
| } |
| |
| |
| function levelComplete() { |
| state.gameActive = false; |
| clearInterval(state.timerInterval); |
| |
| |
| const timeBonus = Math.max(0, 300 - state.time); |
| state.score += timeBonus; |
| |
| |
| levelTimeElement.textContent = formatTime(state.time); |
| levelScoreElement.textContent = `+${timeBonus}`; |
| levelCompleteModal.classList.remove('hidden'); |
| |
| updateUI(); |
| } |
| |
| |
| function nextLevel() { |
| state.level++; |
| state.time = 0; |
| state.gameActive = true; |
| |
| levelCompleteModal.classList.add('hidden'); |
| startTimer(); |
| createBoard(); |
| updateUI(); |
| |
| |
| gameStatusElement.classList.remove('hidden'); |
| gameStatusElement.querySelector('p').textContent = `Level ${state.level}!`; |
| |
| setTimeout(() => { |
| gameStatusElement.classList.add('hidden'); |
| }, 2000); |
| } |
| |
| |
| function gameOver() { |
| state.gameActive = false; |
| clearInterval(state.timerInterval); |
| |
| |
| finalLevelElement.textContent = state.level; |
| finalScoreElement.textContent = state.score; |
| |
| |
| gameOverModal.classList.remove('hidden'); |
| } |
| |
| |
| function startTimer() { |
| state.time = 0; |
| updateTimerDisplay(); |
| |
| state.timerInterval = setInterval(() => { |
| state.time++; |
| updateTimerDisplay(); |
| |
| |
| if (state.time >= 300) { |
| gameOver(); |
| } |
| }, 1000); |
| } |
| |
| |
| function updateTimerDisplay() { |
| timerElement.textContent = formatTime(state.time); |
| } |
| |
| |
| function formatTime(seconds) { |
| const mins = Math.floor(seconds / 60).toString().padStart(2, '0'); |
| const secs = (seconds % 60).toString().padStart(2, '0'); |
| return `${mins}:${secs}`; |
| } |
| |
| |
| function updateUI() { |
| levelElement.textContent = `Level ${state.level}`; |
| scoreElement.textContent = state.score; |
| } |
| |
| |
| function getTileSymbol(type) { |
| const symbols = { |
| '1m': '一', '2m': '二', '3m': '三', '4m': '四', '5m': '五', |
| '6m': '六', '7m': '七', '8m': '八', '9m': '九', |
| '1s': '1', '2s': '2', '3s': '3', '4s': '4', '5s': '5', |
| '6s': '6', '7s': '7', '8s': '8', '9s': '9', |
| '1p': '❶', '2p': '❷', '3p': '❸', '4p': '❹', '5p': '❺', |
| '6p': '❻', '7p': '❼', '8p': '❽', '9p': '❾', |
| 'ew': '東', 'sw': '南', 'ww': '西', 'nw': '北', |
| 'wd': '白', 'gd': '發', 'rd': '中' |
| }; |
| return symbols[type] || type; |
| } |
| |
| |
| function shuffleArray(array) { |
| const newArray = [...array]; |
| for (let i = newArray.length - 1; i > 0; i--) { |
| const j = Math.floor(Math.random() * (i + 1)); |
| [newArray[i], newArray[j]] = [newArray[j], newArray[i]]; |
| } |
| return newArray; |
| } |
| |
| |
| function playSound(type) { |
| |
| console.log(`Playing ${type} sound`); |
| } |
| |
| |
| function provideHint() { |
| if (!state.gameActive || state.matchedPairs === state.totalPairs) { |
| return; |
| } |
| |
| |
| const unflippedTiles = []; |
| state.board.forEach(row => { |
| row.forEach(tile => { |
| if (tile && !tile.flipped && !tile.matched) { |
| unflippedTiles.push(tile); |
| } |
| }); |
| }); |
| |
| if (unflippedTiles.length < 2) return; |
| |
| |
| const tileCount = {}; |
| let hintTile1 = null; |
| let hintTile2 = null; |
| |
| for (const tile of unflippedTiles) { |
| if (tileCount[tile.type]) { |
| hintTile1 = tileCount[tile.type]; |
| hintTile2 = tile; |
| break; |
| } |
| tileCount[tile.type] = tile; |
| } |
| |
| if (hintTile1 && hintTile2) { |
| |
| const tileElements = boardElement.querySelectorAll('.tile'); |
| |
| tileElements.forEach((element, index) => { |
| const row = Math.floor(index / 8); |
| const col = index % 8; |
| const tile = state.board[row]?.[col]; |
| |
| if (tile === hintTile1 || tile === hintTile2) { |
| element.classList.add('ring-4', 'ring-yellow-400', 'ring-opacity-75'); |
| |
| |
| setTimeout(() => { |
| element.classList.remove('ring-4', 'ring-yellow-400', 'ring-opacity-75'); |
| }, 2000); |
| } |
| }); |
| |
| |
| state.score = Math.max(0, state.score - 25); |
| updateUI(); |
| |
| |
| if (state.soundEnabled) { |
| playSound('hint'); |
| } |
| } |
| } |
| |
| |
| newGameButton.addEventListener('click', initGame); |
| hintButton.addEventListener('click', provideHint); |
| soundToggleButton.addEventListener('click', () => { |
| state.soundEnabled = !state.soundEnabled; |
| soundToggleButton.innerHTML = state.soundEnabled |
| ? '<i class="fas fa-volume-up mr-2"></i> Sound On' |
| : '<i class="fas fa-volume-mute mr-2"></i> Sound Off'; |
| }); |
| nextLevelButton.addEventListener('click', nextLevel); |
| playAgainButton.addEventListener('click', () => { |
| gameOverModal.classList.add('hidden'); |
| initGame(); |
| }); |
| |
| |
| initGame(); |
| }); |
| </script> |
| </body> |
| </html> |