Files
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

892 lines
30 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>💣 扫雷 - PlayCanvas版</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>💣</text></svg>">
<!-- crypto.randomUUID 兼容性修复 -->
<script>
if (!crypto.randomUUID) {
crypto.randomUUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
}
</script>
<!-- Poki SDK -->
<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js" async></script>
<!-- PlayCanvas 1.66.0 -->
<script src="https://code.playcanvas.com/playcanvas-1.66.0.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
body {
overflow: hidden;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
touch-action: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
#application {
width: 100vw;
height: 100vh;
display: block;
}
#ui {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
}
/* 顶部信息面板 */
#info-panel {
position: fixed;
left: 20px;
right: 20px;
top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
color: white;
font-size: 18px;
z-index: 101;
pointer-events: auto;
}
.info-box {
background: rgba(0,0,0,0.6);
padding: 12px 20px;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
}
.info-label {
color: #aaa;
font-size: 12px;
margin-bottom: 4px;
}
.info-value {
color: #ffd700;
font-weight: bold;
font-size: 24px;
font-family: 'Courier New', monospace;
}
#smiley-btn {
width: 60px;
height: 60px;
font-size: 32px;
background: linear-gradient(145deg, #4a4a6a, #3a3a5a);
border: none;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: transform 0.1s;
pointer-events: auto;
}
#smiley-btn:active {
transform: scale(0.95);
}
/* 难度选择 */
#difficulty-panel {
position: fixed;
left: 20px;
right: 20px;
top: 100px;
display: flex;
justify-content: center;
gap: 10px;
z-index: 101;
pointer-events: auto;
}
.diff-btn {
padding: 8px 16px;
font-size: 14px;
background: rgba(255,255,255,0.1);
border: 2px solid rgba(255,255,255,0.3);
border-radius: 20px;
color: white;
cursor: pointer;
transition: all 0.2s;
}
.diff-btn.active {
background: rgba(0,212,255,0.3);
border-color: #00d4ff;
}
/* 菜单和结束画面 */
#menu, #gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0,0,0,0.9);
padding: 40px;
border-radius: 20px;
pointer-events: auto;
z-index: 200;
}
#gameOver { display: none; }
h1 {
color: #ffd700;
font-size: 36px;
margin-bottom: 20px;
}
h2 {
color: #ff6b6b;
font-size: 32px;
margin-bottom: 20px;
}
.btn {
padding: 15px 40px;
font-size: 20px;
background: linear-gradient(145deg, #00d4ff, #0099cc);
border: none;
border-radius: 30px;
color: white;
cursor: pointer;
margin: 10px;
box-shadow: 0 4px 15px rgba(0,212,255,0.4);
transition: transform 0.2s;
}
.btn:hover {
transform: scale(1.05);
}
.btn:active {
transform: scale(0.95);
}
/* PC端布局 */
@media (min-width: 901px) {
#info-panel {
left: 30px;
top: 30px;
right: auto;
flex-direction: column;
gap: 15px;
}
.info-box {
min-width: 120px;
padding: 15px 25px;
}
#difficulty-panel {
left: 30px;
top: 200px;
right: auto;
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<canvas id="application"></canvas>
<div id="ui">
<!-- 顶部信息栏 -->
<div id="info-panel">
<div class="info-box">
<span class="info-label">💣 地雷</span>
<span class="info-value" id="mine-count">010</span>
</div>
<button id="smiley-btn" onclick="restartGame()">😊</button>
<div class="info-box">
<span class="info-label">⏱️ 时间</span>
<span class="info-value" id="timer">000</span>
</div>
</div>
<!-- 难度选择 -->
<div id="difficulty-panel">
<button class="diff-btn active" onclick="setDifficulty('easy')">初级 9×9</button>
<button class="diff-btn" onclick="setDifficulty('medium')">中级 16×16</button>
<button class="diff-btn" onclick="setDifficulty('hard')">高级 16×30</button>
</div>
<!-- 菜单 -->
<div id="menu">
<h1>💣 扫雷</h1>
<p style="color: #ccc; margin-bottom: 20px; line-height: 1.6;">
找出所有地雷,不要踩到!<br>
左键翻开,右键标记 🚩
</p>
<button class="btn" onclick="startGame()">开始游戏</button>
</div>
<!-- 游戏结束 -->
<div id="gameOver">
<h2 id="result-text">游戏结束!</h2>
<p style="color: white; font-size: 20px; margin-bottom: 20px;">
用时: <span id="final-time" style="color: #ffd700;">0</span> 秒
</p>
<button class="btn" onclick="restartGame()">再玩一次</button>
</div>
</div>
<script>
// ==================== Poki SDK ====================
let pokiReady = false;
function initPoki() {
if (typeof PokiSDK !== 'undefined') {
return PokiSDK.init().then(() => {
console.log('✅ Poki SDK 已就绪');
pokiReady = true;
PokiSDK.gameLoadingStart();
setTimeout(() => PokiSDK.gameLoadingFinished(), 500);
}).catch(err => {
console.log('⚠️ Poki SDK 未就绪:', err);
return Promise.resolve();
});
}
return Promise.resolve();
}
// ==================== 游戏配置 ====================
const DIFFICULTY = {
easy: { rows: 9, cols: 9, mines: 10 },
medium: { rows: 16, cols: 16, mines: 40 },
hard: { rows: 16, cols: 30, mines: 99 }
};
// ==================== 爆炸效果系统 ====================
let explosions = [];
let screenShake = 0;
class Explosion {
constructor(x, y, size, delay = 0) {
this.x = x;
this.y = y;
this.size = size;
this.delay = delay;
this.age = 0;
this.maxAge = 30;
this.particles = [];
this.active = delay === 0;
// 创建粒子
const particleCount = 12 + Math.floor(Math.random() * 8);
for (let i = 0; i < particleCount; i++) {
const angle = (Math.PI * 2 * i) / particleCount + Math.random() * 0.5;
const speed = 3 + Math.random() * 4;
this.particles.push({
x: 0,
y: 0,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: 3 + Math.random() * 4,
life: 1.0,
color: this.getRandomFireColor()
});
}
}
getRandomFireColor() {
const colors = ['#ff0000', '#ff4400', '#ff8800', '#ffaa00', '#ffcc00', '#ffffff'];
return colors[Math.floor(Math.random() * colors.length)];
}
update() {
if (this.delay > 0) {
this.delay--;
if (this.delay === 0) this.active = true;
return false;
}
this.age++;
// 更新粒子
for (let p of this.particles) {
p.x += p.vx;
p.y += p.vy;
p.vx *= 0.95;
p.vy *= 0.95;
p.vy += 0.2; // 重力
p.life -= 0.03;
p.size *= 0.97;
}
return this.age < this.maxAge;
}
draw(ctx) {
if (!this.active) return;
const centerX = this.x;
const centerY = this.y;
const progress = this.age / this.maxAge;
// 绘制冲击波
const shockwaveSize = this.size * (0.5 + progress * 2);
const alpha = 1 - progress;
ctx.save();
ctx.globalCompositeOperation = 'screen';
// 外圈冲击波
const gradient = ctx.createRadialGradient(
centerX, centerY, 0,
centerX, centerY, shockwaveSize
);
gradient.addColorStop(0, `rgba(255, 100, 0, ${alpha * 0.8})`);
gradient.addColorStop(0.3, `rgba(255, 50, 0, ${alpha * 0.5})`);
gradient.addColorStop(0.7, `rgba(255, 0, 0, ${alpha * 0.2})`);
gradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(centerX, centerY, shockwaveSize, 0, Math.PI * 2);
ctx.fill();
// 内圈强光
const innerSize = this.size * (1 - progress * 0.5);
const innerGradient = ctx.createRadialGradient(
centerX, centerY, 0,
centerX, centerY, innerSize
);
innerGradient.addColorStop(0, `rgba(255, 255, 200, ${alpha})`);
innerGradient.addColorStop(0.4, `rgba(255, 200, 50, ${alpha * 0.8})`);
innerGradient.addColorStop(0.8, `rgba(255, 100, 0, ${alpha * 0.4})`);
innerGradient.addColorStop(1, 'rgba(255, 0, 0, 0)');
ctx.fillStyle = innerGradient;
ctx.beginPath();
ctx.arc(centerX, centerY, innerSize, 0, Math.PI * 2);
ctx.fill();
// 绘制粒子
for (let p of this.particles) {
if (p.life > 0 && p.size > 0.5) {
ctx.fillStyle = p.color;
ctx.globalAlpha = p.life * alpha;
ctx.beginPath();
ctx.arc(centerX + p.x, centerY + p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
}
// 闪光效果
if (this.age < 5) {
ctx.fillStyle = `rgba(255, 255, 255, ${1 - this.age / 5})`;
ctx.globalAlpha = 1;
ctx.beginPath();
ctx.arc(centerX, centerY, this.size * 0.3, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
}
function triggerExplosion(x, y, size, delay = 0) {
explosions.push(new Explosion(x, y, size, delay));
if (delay === 0) {
screenShake = 10;
}
}
function triggerChainExplosion(minePositions) {
minePositions.forEach((pos, index) => {
const x = boardOffsetX + pos.col * cellSize + cellSize / 2;
const y = boardOffsetY + pos.row * cellSize + cellSize / 2;
triggerExplosion(x, y, cellSize * 1.5, index * 3);
});
}
let currentDifficulty = 'easy';
let rows, cols, mineCount;
let grid = []; // -1=地雷, 0-8=周围地雷数
let mask = []; // 0=未翻开, 1=已翻开, 2=旗子, 3=问号
let gameRunning = false;
let gameStarted = false;
let firstClick = true;
let timer = 0;
let timerInterval = null;
let flagCount = 0;
// 渲染配置
let canvas, ctx;
let cellSize = 30;
let boardOffsetX = 0;
let boardOffsetY = 150; // 顶部留出信息栏空间
// 颜色配置(经典Windows扫雷风格)
const COLORS = {
closed: '#c0c0c0',
closedLight: '#ffffff',
closedDark: '#808080',
open: '#e0e0e0',
openDark: '#c0c0c0',
numbers: ['', '#0000ff', '#008000', '#ff0000', '#000080', '#800000', '#008080', '#000000', '#808080'],
mine: '#000000',
mineRed: '#ff0000',
flag: '#ff0000',
wrongFlag: '#ff0000',
bg: '#1a1a2e'
};
// ==================== 初始化 ====================
document.addEventListener('DOMContentLoaded', () => {
initPoki().then(() => {
initGame();
});
});
function initGame() {
canvas = document.getElementById('application');
ctx = canvas.getContext('2d');
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// 输入处理
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('contextmenu', e => e.preventDefault());
// 触摸支持
canvas.addEventListener('touchstart', onTouchStart, { passive: false });
setDifficulty('easy');
requestAnimationFrame(renderLoop);
}
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
calculateLayout();
}
function calculateLayout() {
const isMobile = window.innerWidth <= 900;
const padding = isMobile ? 20 : 40;
const topSpace = isMobile ? 140 : 100;
// 计算格子大小,使棋盘适应屏幕
const availableWidth = window.innerWidth - padding * 2;
const availableHeight = window.innerHeight - topSpace - padding;
const cellW = Math.floor(availableWidth / cols);
const cellH = Math.floor(availableHeight / rows);
cellSize = Math.min(cellW, cellH, 40); // 最大40px
// 计算棋盘居中位置
const boardWidth = cols * cellSize;
const boardHeight = rows * cellSize;
boardOffsetX = (window.innerWidth - boardWidth) / 2;
boardOffsetY = topSpace + (availableHeight - boardHeight) / 2;
}
function setDifficulty(diff) {
currentDifficulty = diff;
const config = DIFFICULTY[diff];
rows = config.rows;
cols = config.cols;
mineCount = config.mines;
// 更新UI
document.querySelectorAll('.diff-btn').forEach((btn, i) => {
btn.classList.toggle('active',
(diff === 'easy' && i === 0) ||
(diff === 'medium' && i === 1) ||
(diff === 'hard' && i === 2)
);
});
resetGame();
}
function resetGame() {
gameRunning = false;
gameStarted = false;
firstClick = true;
timer = 0;
flagCount = 0;
screenShake = 0;
explosions = [];
clearInterval(timerInterval);
updateTimerDisplay();
updateMineDisplay();
document.getElementById('smiley-btn').textContent = '😊';
// 初始化网格
grid = Array(rows).fill(null).map(() => Array(cols).fill(0));
mask = Array(rows).fill(null).map(() => Array(cols).fill(0));
calculateLayout();
}
function generateMines(firstR, firstC) {
let mines = 0;
const safeZone = new Set();
// 第一次点击的格子和周围8格是安全的
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const nr = firstR + dr;
const nc = firstC + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
safeZone.add(`${nr},${nc}`);
}
}
}
// 随机布雷
while (mines < mineCount) {
const r = Math.floor(Math.random() * rows);
const c = Math.floor(Math.random() * cols);
const key = `${r},${c}`;
if (!safeZone.has(key) && grid[r][c] !== -1) {
grid[r][c] = -1;
mines++;
// 周围格子数字+1
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
const nr = r + dr;
const nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] !== -1) {
grid[nr][nc]++;
}
}
}
}
}
}
// ==================== 输入处理 ====================
function onMouseDown(e) {
if (!gameRunning && gameStarted) return;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const col = Math.floor((x - boardOffsetX) / cellSize);
const row = Math.floor((y - boardOffsetY) / cellSize);
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
if (e.button === 0) {
// 左键
handleLeftClick(row, col);
} else if (e.button === 2) {
// 右键
handleRightClick(row, col);
}
}
function onTouchStart(e) {
e.preventDefault();
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
const col = Math.floor((x - boardOffsetX) / cellSize);
const row = Math.floor((y - boardOffsetY) / cellSize);
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
// 触摸使用左键逻辑
handleLeftClick(row, col);
}
function handleLeftClick(row, col) {
if (!gameRunning && gameStarted) return;
if (mask[row][col] === 1 || mask[row][col] === 2) return;
if (firstClick) {
firstClick = false;
gameStarted = true;
gameRunning = true;
generateMines(row, col);
startTimer();
if (pokiReady) PokiSDK.gameplayStart();
}
if (grid[row][col] === -1) {
// 踩到地雷
revealMine(row, col);
gameOver(false);
return;
}
// 翻开格子
revealCell(row, col);
// 检查胜利
checkWin();
}
function handleRightClick(row, col) {
if (!gameRunning) return;
if (mask[row][col] === 1) return;
// 循环:未翻开 -> 旗子 -> 未翻开
if (mask[row][col] === 0) {
mask[row][col] = 2;
flagCount++;
} else if (mask[row][col] === 2) {
mask[row][col] = 0;
flagCount--;
}
updateMineDisplay();
}
function revealCell(row, col) {
if (row < 0 || row >= rows || col < 0 || col >= cols) return;
if (mask[row][col] === 1) return;
mask[row][col] = 1;
// 如果是0,自动翻开周围
if (grid[row][col] === 0) {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr !== 0 || dc !== 0) {
revealCell(row + dr, col + dc);
}
}
}
}
}
function revealMine(row, col) {
// 显示所有地雷
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === -1) {
mask[r][c] = 1;
}
}
}
}
// ==================== 游戏状态 ====================
function startTimer() {
timerInterval = setInterval(() => {
timer++;
if (timer > 999) timer = 999;
updateTimerDisplay();
}, 1000);
}
function updateTimerDisplay() {
document.getElementById('timer').textContent = timer.toString().padStart(3, '0');
}
function updateMineDisplay() {
const remaining = mineCount - flagCount;
document.getElementById('mine-count').textContent = remaining.toString().padStart(3, '0');
}
function checkWin() {
let revealed = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (mask[r][c] === 1) revealed++;
}
}
if (revealed === rows * cols - mineCount) {
gameOver(true);
}
}
function gameOver(win) {
gameRunning = false;
clearInterval(timerInterval);
if (pokiReady) {
PokiSDK.gameplayStop();
if (win) PokiSDK.happyTime(0.5);
}
// 更新表情
document.getElementById('smiley-btn').textContent = win ? '😎' : '😵';
// 触发爆炸效果(失败时)
if (!win) {
const minePositions = [];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === -1) {
minePositions.push({ row: r, col: c });
}
}
}
triggerChainExplosion(minePositions);
}
// 显示结束画面
setTimeout(() => {
document.getElementById('result-text').textContent = win ? '恭喜你赢了!' : '游戏失败!';
document.getElementById('result-text').style.color = win ? '#4ecdc4' : '#ff6b6b';
document.getElementById('final-time').textContent = timer;
document.getElementById('gameOver').style.display = 'block';
}, win ? 300 : 2000); // 失败时延迟更久,让玩家看完爆炸
}
function startGame() {
document.getElementById('menu').style.display = 'none';
resetGame();
}
function restartGame() {
document.getElementById('gameOver').style.display = 'none';
resetGame();
}
// ==================== 渲染 ====================
function renderLoop() {
render();
requestAnimationFrame(renderLoop);
}
function render() {
ctx.save();
// 屏幕震动效果
if (screenShake > 0) {
const shakeX = (Math.random() - 0.5) * screenShake;
const shakeY = (Math.random() - 0.5) * screenShake;
ctx.translate(shakeX, shakeY);
screenShake *= 0.9;
if (screenShake < 0.5) screenShake = 0;
}
// 清空画布
ctx.fillStyle = COLORS.bg;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制棋盘背景
const boardWidth = cols * cellSize;
const boardHeight = rows * cellSize;
ctx.fillStyle = '#808080';
ctx.fillRect(boardOffsetX - 3, boardOffsetY - 3, boardWidth + 6, boardHeight + 6);
// 绘制格子
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
drawCell(r, c);
}
}
// 更新和绘制爆炸效果
explosions = explosions.filter(exp => {
const alive = exp.update();
if (alive) exp.draw(ctx);
return alive;
});
ctx.restore();
}
function drawCell(row, col) {
const x = boardOffsetX + col * cellSize;
const y = boardOffsetY + row * cellSize;
const size = cellSize - 1;
const m = mask[row][col];
const g = grid[row][col];
if (m === 0) {
// 未翻开 - 凸起效果
ctx.fillStyle = COLORS.closed;
ctx.fillRect(x, y, size, size);
// 高光
ctx.fillStyle = COLORS.closedLight;
ctx.fillRect(x, y, size, 2);
ctx.fillRect(x, y, 2, size);
// 阴影
ctx.fillStyle = COLORS.closedDark;
ctx.fillRect(x + size - 2, y, 2, size);
ctx.fillRect(x, y + size - 2, size, 2);
} else if (m === 1) {
// 已翻开
ctx.fillStyle = COLORS.open;
ctx.fillRect(x, y, size, size);
// 凹陷边框
ctx.fillStyle = COLORS.openDark;
ctx.fillRect(x, y, size, 1);
ctx.fillRect(x, y, 1, size);
if (g === -1) {
// 地雷
ctx.fillStyle = COLORS.mineRed;
ctx.beginPath();
ctx.arc(x + size/2, y + size/2, size/3, 0, Math.PI * 2);
ctx.fill();
// 引线
ctx.strokeStyle = COLORS.mine;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(x + size/2, y + size/2);
ctx.lineTo(x + size/2 + size/4, y + size/2 - size/4);
ctx.stroke();
} else if (g > 0) {
// 数字
ctx.fillStyle = COLORS.numbers[g];
ctx.font = `bold ${Math.floor(size * 0.7)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(g.toString(), x + size/2, y + size/2 + 2);
}
} else if (m === 2) {
// 旗子
ctx.fillStyle = COLORS.closed;
ctx.fillRect(x, y, size, size);
// 凸起效果
ctx.fillStyle = COLORS.closedLight;
ctx.fillRect(x, y, size, 2);
ctx.fillRect(x, y, 2, size);
ctx.fillStyle = COLORS.closedDark;
ctx.fillRect(x + size - 2, y, 2, size);
ctx.fillRect(x, y + size - 2, size, 2);
// 绘制旗子 🚩
ctx.font = `${Math.floor(size * 0.7)}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('🚩', x + size/2, y + size/2 + 2);
}
}
</script>
<!-- 登录认证 -->
<script src="../auth.js?v=1"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
GAME_AUTH.init('minesweeper', '扫雷');
});
</script>
</body>
</html>