892 lines
30 KiB
HTML
892 lines
30 KiB
HTML
<!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>
|