git / VitaMahjong.html
KEXEL's picture
1.1
e0eccaf verified
raw
history blame
24.5 kB
<!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 -->
<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>
<!-- Game Controls -->
<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>
<!-- Game Board -->
<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>
<!-- Game Status -->
<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>
<!-- Level Complete Modal -->
<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>
<!-- Game Over Modal -->
<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', () => {
// Game state
const state = {
board: [],
level: 1,
score: 0,
time: 0,
timerInterval: null,
selectedTiles: [],
matchedPairs: 0,
totalPairs: 0,
soundEnabled: true,
gameActive: false
};
// DOM elements
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');
// Tile types (Vita Mahjong style)
const tileTypes = [
'1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '9m', // Characters
'1s', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', // Bamboo
'1p', '2p', '3p', '4p', '5p', '6p', '7p', '8p', '9p', // Circles
'ew', 'sw', 'ww', 'nw', // Winds
'wd', 'gd', 'rd' // Dragons
];
// Initialize game
function initGame() {
state.level = 1;
state.score = 0;
state.time = 0;
state.matchedPairs = 0;
state.gameActive = true;
clearInterval(state.timerInterval);
startTimer();
updateUI();
createBoard();
}
// Create game board based on current level
function createBoard() {
boardElement.innerHTML = '';
state.board = [];
state.selectedTiles = [];
state.matchedPairs = 0;
// Determine number of pairs based on level
const pairs = Math.min(4 + state.level, 32); // Max 32 pairs (64 tiles)
state.totalPairs = pairs;
// Create array of tile pairs
let tiles = [];
const availableTypes = [...tileTypes].sort(() => 0.5 - Math.random()).slice(0, pairs);
availableTypes.forEach(type => {
tiles.push(type, type);
});
// Shuffle tiles
tiles = shuffleArray(tiles);
// Create board matrix (8 columns, rows as needed)
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); // Empty slot for uneven boards
}
}
state.board.push(row);
}
// Render tiles
renderBoard();
}
// Render the game board
function renderBoard() {
boardElement.innerHTML = '';
// Calculate grid rows based on board rows
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);
});
});
}
// Handle tile click
function handleTileClick(tile) {
if (!state.gameActive || tile.matched || tile.flipped || state.selectedTiles.length >= 2) {
return;
}
// Flip the tile
tile.flipped = true;
state.selectedTiles.push(tile);
// Play sound
if (state.soundEnabled) {
playSound('flip');
}
renderBoard();
// Check for match if two tiles are selected
if (state.selectedTiles.length === 2) {
const [tile1, tile2] = state.selectedTiles;
if (tile1.type === tile2.type) {
// Match found
tile1.matched = true;
tile2.matched = true;
state.matchedPairs++;
state.score += 50 * state.level;
// Play success sound
if (state.soundEnabled) {
playSound('match');
}
// Check if level is complete
if (state.matchedPairs === state.totalPairs) {
levelComplete();
}
// Clear selection after delay
setTimeout(() => {
state.selectedTiles = [];
renderBoard();
}, 500);
} else {
// No match
setTimeout(() => {
tile1.flipped = false;
tile2.flipped = false;
state.selectedTiles = [];
renderBoard();
// Play mismatch sound
if (state.soundEnabled) {
playSound('mismatch');
}
}, 1000);
}
}
updateUI();
}
// Level complete
function levelComplete() {
state.gameActive = false;
clearInterval(state.timerInterval);
// Calculate bonus points based on time
const timeBonus = Math.max(0, 300 - state.time);
state.score += timeBonus;
// Show level complete modal
levelTimeElement.textContent = formatTime(state.time);
levelScoreElement.textContent = `+${timeBonus}`;
levelCompleteModal.classList.remove('hidden');
updateUI();
}
// Next level
function nextLevel() {
state.level++;
state.time = 0;
state.gameActive = true;
levelCompleteModal.classList.add('hidden');
startTimer();
createBoard();
updateUI();
// Show level up message
gameStatusElement.classList.remove('hidden');
gameStatusElement.querySelector('p').textContent = `Level ${state.level}!`;
setTimeout(() => {
gameStatusElement.classList.add('hidden');
}, 2000);
}
// Game over
function gameOver() {
state.gameActive = false;
clearInterval(state.timerInterval);
// Update final stats
finalLevelElement.textContent = state.level;
finalScoreElement.textContent = state.score;
// Show game over modal
gameOverModal.classList.remove('hidden');
}
// Start timer
function startTimer() {
state.time = 0;
updateTimerDisplay();
state.timerInterval = setInterval(() => {
state.time++;
updateTimerDisplay();
// Game over if time exceeds limit (5 minutes)
if (state.time >= 300) {
gameOver();
}
}, 1000);
}
// Update timer display
function updateTimerDisplay() {
timerElement.textContent = formatTime(state.time);
}
// Format time as MM:SS
function formatTime(seconds) {
const mins = Math.floor(seconds / 60).toString().padStart(2, '0');
const secs = (seconds % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
}
// Update UI elements
function updateUI() {
levelElement.textContent = `Level ${state.level}`;
scoreElement.textContent = state.score;
}
// Get tile symbol for display
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;
}
// Shuffle array
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;
}
// Play sound
function playSound(type) {
// In a real implementation, you would play actual sound files
console.log(`Playing ${type} sound`);
}
// Provide hint
function provideHint() {
if (!state.gameActive || state.matchedPairs === state.totalPairs) {
return;
}
// Find all unflipped, unmatched tiles
const unflippedTiles = [];
state.board.forEach(row => {
row.forEach(tile => {
if (tile && !tile.flipped && !tile.matched) {
unflippedTiles.push(tile);
}
});
});
if (unflippedTiles.length < 2) return;
// Find a matching pair
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) {
// Highlight the hint tiles
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');
// Remove highlight after delay
setTimeout(() => {
element.classList.remove('ring-4', 'ring-yellow-400', 'ring-opacity-75');
}, 2000);
}
});
// Deduct points for using hint
state.score = Math.max(0, state.score - 25);
updateUI();
// Play hint sound
if (state.soundEnabled) {
playSound('hint');
}
}
}
// Event listeners
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();
});
// Initialize the game
initGame();
});
</script>
</body>
</html>