优化结果
This commit is contained in:
537
InfoGenie-frontend/public/smallgame/2048/controls.js
vendored
Normal file
537
InfoGenie-frontend/public/smallgame/2048/controls.js
vendored
Normal file
@@ -0,0 +1,537 @@
|
||||
// 游戏控制模块 - 处理键盘和触摸输入
|
||||
class GameControls {
|
||||
constructor() {
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchEndX = 0;
|
||||
this.touchEndY = 0;
|
||||
this.minSwipeDistance = 30; // 最小滑动距离
|
||||
this.isGameActive = true;
|
||||
|
||||
this.initializeControls();
|
||||
}
|
||||
|
||||
initializeControls() {
|
||||
// 键盘控制
|
||||
this.initKeyboardControls();
|
||||
|
||||
// 触摸控制
|
||||
this.initTouchControls();
|
||||
|
||||
// 鼠标控制(用于电脑端测试)
|
||||
this.initMouseControls();
|
||||
|
||||
// 防止页面滚动
|
||||
this.preventScrolling();
|
||||
}
|
||||
|
||||
initKeyboardControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!this.isGameActive || !window.game2048) {
|
||||
console.log('Game not ready:', { isGameActive: this.isGameActive, game2048: !!window.game2048 });
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止默认行为
|
||||
const preventKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 'a', 's', 'd', 'W', 'A', 'S', 'D'];
|
||||
if (preventKeys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
case 'w':
|
||||
case 'W':
|
||||
window.game2048.move('up');
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 's':
|
||||
case 'S':
|
||||
window.game2048.move('down');
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case 'a':
|
||||
case 'A':
|
||||
window.game2048.move('left');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'd':
|
||||
case 'D':
|
||||
window.game2048.move('right');
|
||||
break;
|
||||
case 'r':
|
||||
case 'R':
|
||||
// R键重新开始游戏
|
||||
window.game2048.restart();
|
||||
break;
|
||||
case 'Escape':
|
||||
// ESC键暂停/继续游戏
|
||||
this.togglePause();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initTouchControls() {
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
|
||||
// 触摸开始
|
||||
gameContainer.addEventListener('touchstart', (e) => {
|
||||
if (!this.isGameActive) return;
|
||||
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
}, { passive: false });
|
||||
|
||||
// 触摸移动(可选:显示滑动方向提示)
|
||||
gameContainer.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
// 触摸结束
|
||||
gameContainer.addEventListener('touchend', (e) => {
|
||||
if (!this.isGameActive || !window.game2048) return;
|
||||
|
||||
e.preventDefault();
|
||||
const touch = e.changedTouches[0];
|
||||
this.touchEndX = touch.clientX;
|
||||
this.touchEndY = touch.clientY;
|
||||
|
||||
this.handleSwipe();
|
||||
}, { passive: false });
|
||||
|
||||
// 触摸取消
|
||||
gameContainer.addEventListener('touchcancel', (e) => {
|
||||
e.preventDefault();
|
||||
this.resetTouch();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
initMouseControls() {
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
let isMouseDown = false;
|
||||
let mouseStartX = 0;
|
||||
let mouseStartY = 0;
|
||||
|
||||
// 鼠标按下
|
||||
gameContainer.addEventListener('mousedown', (e) => {
|
||||
if (!this.isGameActive) return;
|
||||
|
||||
isMouseDown = true;
|
||||
mouseStartX = e.clientX;
|
||||
mouseStartY = e.clientY;
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// 鼠标释放
|
||||
gameContainer.addEventListener('mouseup', (e) => {
|
||||
if (!this.isGameActive || !isMouseDown || !window.game2048) return;
|
||||
|
||||
isMouseDown = false;
|
||||
const mouseEndX = e.clientX;
|
||||
const mouseEndY = e.clientY;
|
||||
|
||||
// 使用鼠标坐标模拟触摸
|
||||
this.touchStartX = mouseStartX;
|
||||
this.touchStartY = mouseStartY;
|
||||
this.touchEndX = mouseEndX;
|
||||
this.touchEndY = mouseEndY;
|
||||
|
||||
this.handleSwipe();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// 鼠标离开游戏区域
|
||||
gameContainer.addEventListener('mouseleave', () => {
|
||||
isMouseDown = false;
|
||||
});
|
||||
|
||||
// 防止右键菜单
|
||||
gameContainer.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
handleSwipe() {
|
||||
const deltaX = this.touchEndX - this.touchStartX;
|
||||
const deltaY = this.touchEndY - this.touchStartY;
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
// 检查是否达到最小滑动距离
|
||||
if (Math.max(absDeltaX, absDeltaY) < this.minSwipeDistance) {
|
||||
this.resetTouch();
|
||||
return;
|
||||
}
|
||||
|
||||
// 确定滑动方向
|
||||
let direction = null;
|
||||
|
||||
if (absDeltaX > absDeltaY) {
|
||||
// 水平滑动
|
||||
direction = deltaX > 0 ? 'right' : 'left';
|
||||
} else {
|
||||
// 垂直滑动
|
||||
direction = deltaY > 0 ? 'down' : 'up';
|
||||
}
|
||||
|
||||
// 执行移动
|
||||
if (direction && window.game2048) {
|
||||
window.game2048.move(direction);
|
||||
|
||||
// 添加触觉反馈(如果支持)
|
||||
this.addHapticFeedback();
|
||||
|
||||
// 添加视觉反馈
|
||||
this.addVisualFeedback(direction);
|
||||
}
|
||||
|
||||
this.resetTouch();
|
||||
}
|
||||
|
||||
resetTouch() {
|
||||
this.touchStartX = 0;
|
||||
this.touchStartY = 0;
|
||||
this.touchEndX = 0;
|
||||
this.touchEndY = 0;
|
||||
}
|
||||
|
||||
addHapticFeedback() {
|
||||
// 添加触觉反馈(仅在支持的设备上)
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(50); // 50ms的轻微震动
|
||||
}
|
||||
}
|
||||
|
||||
addVisualFeedback(direction) {
|
||||
// 添加方向指示的视觉反馈
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
const feedback = document.createElement('div');
|
||||
|
||||
feedback.className = 'swipe-feedback';
|
||||
feedback.textContent = this.getDirectionArrow(direction);
|
||||
|
||||
// 设置样式
|
||||
feedback.style.cssText = `
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 48px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
animation: swipeFeedback 0.3s ease-out;
|
||||
`;
|
||||
|
||||
gameContainer.appendChild(feedback);
|
||||
|
||||
// 添加动画样式(如果不存在)
|
||||
if (!document.getElementById('swipe-feedback-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'swipe-feedback-styles';
|
||||
style.textContent = `
|
||||
@keyframes swipeFeedback {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 移除反馈元素
|
||||
setTimeout(() => {
|
||||
if (feedback.parentNode) {
|
||||
feedback.remove();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
getDirectionArrow(direction) {
|
||||
const arrows = {
|
||||
'up': '↑',
|
||||
'down': '↓',
|
||||
'left': '←',
|
||||
'right': '→'
|
||||
};
|
||||
return arrows[direction] || '';
|
||||
}
|
||||
|
||||
preventScrolling() {
|
||||
// 防止页面滚动,特别是在移动设备上
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
// 只在游戏容器内防止滚动
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
if (gameContainer && gameContainer.contains(e.target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// 防止双击缩放
|
||||
document.addEventListener('touchstart', (e) => {
|
||||
if (e.touches.length > 1) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
// 防止长按选择文本
|
||||
document.addEventListener('selectstart', (e) => {
|
||||
const gameContainer = document.querySelector('.game-container');
|
||||
if (gameContainer && gameContainer.contains(e.target)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
this.isGameActive = !this.isGameActive;
|
||||
|
||||
const pauseOverlay = document.getElementById('pause-overlay') || this.createPauseOverlay();
|
||||
|
||||
if (this.isGameActive) {
|
||||
pauseOverlay.style.display = 'none';
|
||||
// 恢复计时器
|
||||
if (window.game2048 && window.game2048.stats.startTime) {
|
||||
const pausedTime = Date.now() - window.game2048.pauseStartTime;
|
||||
window.game2048.stats.startTime += pausedTime;
|
||||
}
|
||||
} else {
|
||||
pauseOverlay.style.display = 'flex';
|
||||
// 记录暂停时间
|
||||
if (window.game2048) {
|
||||
window.game2048.pauseStartTime = Date.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPauseOverlay() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'pause-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="pause-content">
|
||||
<h2>游戏暂停</h2>
|
||||
<p>按ESC键或点击继续游戏</p>
|
||||
<button class="resume-btn">继续游戏</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(5px);
|
||||
`;
|
||||
|
||||
const pauseContent = overlay.querySelector('.pause-content');
|
||||
pauseContent.style.cssText = `
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
`;
|
||||
|
||||
const resumeBtn = overlay.querySelector('.resume-btn');
|
||||
resumeBtn.style.cssText = `
|
||||
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
|
||||
// 继续游戏按钮事件
|
||||
resumeBtn.addEventListener('click', () => {
|
||||
this.togglePause();
|
||||
});
|
||||
|
||||
// 点击背景继续游戏
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) {
|
||||
this.togglePause();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
// 禁用控制(游戏结束时调用)
|
||||
disable() {
|
||||
this.isGameActive = false;
|
||||
}
|
||||
|
||||
// 启用控制(游戏开始时调用)
|
||||
enable() {
|
||||
this.isGameActive = true;
|
||||
}
|
||||
|
||||
// 显示控制提示
|
||||
showControlHints() {
|
||||
const hints = document.createElement('div');
|
||||
hints.className = 'control-hints';
|
||||
hints.innerHTML = `
|
||||
<div class="hint-content">
|
||||
<h3>操作说明</h3>
|
||||
<div class="hint-section">
|
||||
<h4>📱 手机操作</h4>
|
||||
<p>在游戏区域滑动手指移动方块</p>
|
||||
<div class="gesture-demo">
|
||||
<span>👆 上滑</span>
|
||||
<span>👇 下滑</span>
|
||||
<span>👈 左滑</span>
|
||||
<span>👉 右滑</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hint-section">
|
||||
<h4>⌨️ 键盘操作</h4>
|
||||
<div class="key-demo">
|
||||
<div class="key-row">
|
||||
<span class="key">↑</span>
|
||||
<span class="key">W</span>
|
||||
<span>上移</span>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<span class="key">↓</span>
|
||||
<span class="key">S</span>
|
||||
<span>下移</span>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<span class="key">←</span>
|
||||
<span class="key">A</span>
|
||||
<span>左移</span>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<span class="key">→</span>
|
||||
<span class="key">D</span>
|
||||
<span>右移</span>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<span class="key">R</span>
|
||||
<span>重新开始</span>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<span class="key">ESC</span>
|
||||
<span>暂停/继续</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="close-hints">知道了</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加样式
|
||||
hints.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(5px);
|
||||
`;
|
||||
|
||||
document.body.appendChild(hints);
|
||||
|
||||
// 关闭按钮事件
|
||||
hints.querySelector('.close-hints').addEventListener('click', () => {
|
||||
hints.remove();
|
||||
});
|
||||
|
||||
// 点击背景关闭
|
||||
hints.addEventListener('click', (e) => {
|
||||
if (e.target === hints) {
|
||||
hints.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局控制实例
|
||||
let gameControls;
|
||||
|
||||
// 页面加载完成后初始化控制
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 等待游戏对象初始化完成
|
||||
const initControls = () => {
|
||||
if (window.game2048) {
|
||||
gameControls = new GameControls();
|
||||
console.log('Game controls initialized successfully');
|
||||
|
||||
// 创建帮助按钮
|
||||
createHelpButton();
|
||||
} else {
|
||||
console.log('Waiting for game2048 to initialize...');
|
||||
setTimeout(initControls, 100);
|
||||
}
|
||||
};
|
||||
|
||||
initControls();
|
||||
});
|
||||
|
||||
// 创建帮助按钮函数
|
||||
function createHelpButton() {
|
||||
const helpBtn = document.createElement('button');
|
||||
helpBtn.textContent = '❓';
|
||||
helpBtn.title = '操作说明';
|
||||
helpBtn.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
`;
|
||||
|
||||
helpBtn.addEventListener('click', () => {
|
||||
gameControls.showControlHints();
|
||||
});
|
||||
|
||||
helpBtn.addEventListener('mouseenter', () => {
|
||||
helpBtn.style.transform = 'scale(1.1)';
|
||||
});
|
||||
|
||||
helpBtn.addEventListener('mouseleave', () => {
|
||||
helpBtn.style.transform = 'scale(1)';
|
||||
});
|
||||
|
||||
document.body.appendChild(helpBtn);
|
||||
}
|
||||
|
||||
// 导出控制实例
|
||||
window.gameControls = gameControls;
|
||||
424
InfoGenie-frontend/public/smallgame/2048/game-logic.js
Normal file
424
InfoGenie-frontend/public/smallgame/2048/game-logic.js
Normal file
@@ -0,0 +1,424 @@
|
||||
// 2048游戏核心逻辑
|
||||
class Game2048 {
|
||||
constructor() {
|
||||
this.size = 4;
|
||||
this.grid = [];
|
||||
this.score = 0;
|
||||
this.bestScore = parseInt(localStorage.getItem('2048-best-score')) || 0;
|
||||
this.gameWon = false;
|
||||
this.gameOver = false;
|
||||
this.moved = false;
|
||||
|
||||
// 游戏统计数据
|
||||
this.stats = {
|
||||
moves: 0,
|
||||
startTime: null,
|
||||
gameTime: 0,
|
||||
maxTile: 2,
|
||||
mergeCount: 0
|
||||
};
|
||||
|
||||
this.initializeGrid();
|
||||
this.updateDisplay();
|
||||
this.addRandomTile();
|
||||
this.addRandomTile();
|
||||
this.updateDisplay();
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 开始计时
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
initializeGrid() {
|
||||
this.grid = [];
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
this.grid[i] = [];
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
this.grid[i][j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRandomTile() {
|
||||
const emptyCells = [];
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] === 0) {
|
||||
emptyCells.push({x: i, y: j});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (emptyCells.length > 0) {
|
||||
const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
|
||||
const value = Math.random() < 0.9 ? 2 : 4;
|
||||
this.grid[randomCell.x][randomCell.y] = value;
|
||||
|
||||
// 创建新方块动画
|
||||
this.createTileElement(randomCell.x, randomCell.y, value, true);
|
||||
}
|
||||
}
|
||||
|
||||
createTileElement(x, y, value, isNew = false) {
|
||||
const container = document.getElementById('tile-container');
|
||||
const tile = document.createElement('div');
|
||||
tile.className = `tile tile-${value}`;
|
||||
if (isNew) tile.classList.add('tile-new');
|
||||
|
||||
tile.textContent = value;
|
||||
tile.style.left = `${y * (100/4)}%`;
|
||||
tile.style.top = `${x * (100/4)}%`;
|
||||
tile.dataset.x = x;
|
||||
tile.dataset.y = y;
|
||||
tile.dataset.value = value;
|
||||
|
||||
container.appendChild(tile);
|
||||
|
||||
// 移除动画类
|
||||
setTimeout(() => {
|
||||
tile.classList.remove('tile-new');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
// 清除所有方块
|
||||
const container = document.getElementById('tile-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
// 重新创建所有方块
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] !== 0) {
|
||||
this.createTileElement(i, j, this.grid[i][j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分数
|
||||
document.getElementById('score').textContent = this.score;
|
||||
document.getElementById('best-score').textContent = this.bestScore;
|
||||
|
||||
// 更新统计数据显示
|
||||
if (window.gameStats) {
|
||||
window.gameStats.updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
move(direction) {
|
||||
if (this.gameOver) return;
|
||||
|
||||
this.moved = false;
|
||||
const previousGrid = this.grid.map(row => [...row]);
|
||||
|
||||
switch (direction) {
|
||||
case 'up':
|
||||
this.moveUp();
|
||||
break;
|
||||
case 'down':
|
||||
this.moveDown();
|
||||
break;
|
||||
case 'left':
|
||||
this.moveLeft();
|
||||
break;
|
||||
case 'right':
|
||||
this.moveRight();
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.moved) {
|
||||
this.stats.moves++;
|
||||
this.addRandomTile();
|
||||
this.updateDisplay();
|
||||
|
||||
if (this.isGameWon() && !this.gameWon) {
|
||||
this.gameWon = true;
|
||||
this.showGameWon();
|
||||
} else if (this.isGameOver()) {
|
||||
this.gameOver = true;
|
||||
this.showGameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveLeft() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const row = this.grid[i].filter(val => val !== 0);
|
||||
const merged = [];
|
||||
|
||||
for (let j = 0; j < row.length - 1; j++) {
|
||||
if (row[j] === row[j + 1] && !merged[j] && !merged[j + 1]) {
|
||||
row[j] *= 2;
|
||||
this.score += row[j];
|
||||
this.stats.mergeCount++;
|
||||
this.stats.maxTile = Math.max(this.stats.maxTile, row[j]);
|
||||
row[j + 1] = 0;
|
||||
merged[j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newRow = row.filter(val => val !== 0);
|
||||
while (newRow.length < this.size) {
|
||||
newRow.push(0);
|
||||
}
|
||||
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] !== newRow[j]) {
|
||||
this.moved = true;
|
||||
}
|
||||
this.grid[i][j] = newRow[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveRight() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const row = this.grid[i].filter(val => val !== 0);
|
||||
const merged = [];
|
||||
|
||||
for (let j = row.length - 1; j > 0; j--) {
|
||||
if (row[j] === row[j - 1] && !merged[j] && !merged[j - 1]) {
|
||||
row[j] *= 2;
|
||||
this.score += row[j];
|
||||
this.stats.mergeCount++;
|
||||
this.stats.maxTile = Math.max(this.stats.maxTile, row[j]);
|
||||
row[j - 1] = 0;
|
||||
merged[j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newRow = row.filter(val => val !== 0);
|
||||
while (newRow.length < this.size) {
|
||||
newRow.unshift(0);
|
||||
}
|
||||
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] !== newRow[j]) {
|
||||
this.moved = true;
|
||||
}
|
||||
this.grid[i][j] = newRow[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveUp() {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
const column = [];
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
if (this.grid[i][j] !== 0) {
|
||||
column.push(this.grid[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
const merged = [];
|
||||
for (let i = 0; i < column.length - 1; i++) {
|
||||
if (column[i] === column[i + 1] && !merged[i] && !merged[i + 1]) {
|
||||
column[i] *= 2;
|
||||
this.score += column[i];
|
||||
this.stats.mergeCount++;
|
||||
this.stats.maxTile = Math.max(this.stats.maxTile, column[i]);
|
||||
column[i + 1] = 0;
|
||||
merged[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newColumn = column.filter(val => val !== 0);
|
||||
while (newColumn.length < this.size) {
|
||||
newColumn.push(0);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
if (this.grid[i][j] !== newColumn[i]) {
|
||||
this.moved = true;
|
||||
}
|
||||
this.grid[i][j] = newColumn[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveDown() {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
const column = [];
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
if (this.grid[i][j] !== 0) {
|
||||
column.push(this.grid[i][j]);
|
||||
}
|
||||
}
|
||||
|
||||
const merged = [];
|
||||
for (let i = column.length - 1; i > 0; i--) {
|
||||
if (column[i] === column[i - 1] && !merged[i] && !merged[i - 1]) {
|
||||
column[i] *= 2;
|
||||
this.score += column[i];
|
||||
this.stats.mergeCount++;
|
||||
this.stats.maxTile = Math.max(this.stats.maxTile, column[i]);
|
||||
column[i - 1] = 0;
|
||||
merged[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newColumn = column.filter(val => val !== 0);
|
||||
while (newColumn.length < this.size) {
|
||||
newColumn.unshift(0);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
if (this.grid[i][j] !== newColumn[i]) {
|
||||
this.moved = true;
|
||||
}
|
||||
this.grid[i][j] = newColumn[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isGameWon() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] === 2048) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isGameOver() {
|
||||
// 检查是否有空格
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
if (this.grid[i][j] === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以合并
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
for (let j = 0; j < this.size; j++) {
|
||||
const current = this.grid[i][j];
|
||||
if (
|
||||
(i > 0 && this.grid[i - 1][j] === current) ||
|
||||
(i < this.size - 1 && this.grid[i + 1][j] === current) ||
|
||||
(j > 0 && this.grid[i][j - 1] === current) ||
|
||||
(j < this.size - 1 && this.grid[i][j + 1] === current)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
showGameWon() {
|
||||
const message = document.getElementById('game-message');
|
||||
message.className = 'game-message game-won';
|
||||
message.style.display = 'flex';
|
||||
message.querySelector('p').textContent = '你赢了!';
|
||||
}
|
||||
|
||||
showGameOver() {
|
||||
const message = document.getElementById('game-message');
|
||||
message.className = 'game-message game-over';
|
||||
message.style.display = 'flex';
|
||||
message.querySelector('p').textContent = '游戏结束!';
|
||||
|
||||
// 显示最终统计
|
||||
setTimeout(() => {
|
||||
if (window.gameStats) {
|
||||
window.gameStats.showFinalStats();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.score = 0;
|
||||
this.gameWon = false;
|
||||
this.gameOver = false;
|
||||
this.moved = false;
|
||||
|
||||
// 重置统计数据
|
||||
this.stats = {
|
||||
moves: 0,
|
||||
startTime: null,
|
||||
gameTime: 0,
|
||||
maxTile: 2,
|
||||
mergeCount: 0
|
||||
};
|
||||
|
||||
this.initializeGrid();
|
||||
this.addRandomTile();
|
||||
this.addRandomTile();
|
||||
this.updateDisplay();
|
||||
|
||||
// 隐藏游戏消息
|
||||
document.getElementById('game-message').style.display = 'none';
|
||||
|
||||
// 重新开始计时
|
||||
this.startTimer();
|
||||
}
|
||||
|
||||
keepPlaying() {
|
||||
document.getElementById('game-message').style.display = 'none';
|
||||
}
|
||||
|
||||
startTimer() {
|
||||
this.stats.startTime = Date.now();
|
||||
|
||||
if (this.timerInterval) {
|
||||
clearInterval(this.timerInterval);
|
||||
}
|
||||
|
||||
this.timerInterval = setInterval(() => {
|
||||
if (!this.gameOver && this.stats.startTime) {
|
||||
this.stats.gameTime = Math.floor((Date.now() - this.stats.startTime) / 1000);
|
||||
if (window.gameStats) {
|
||||
window.gameStats.updateDisplay();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 重新开始按钮
|
||||
document.getElementById('restart-btn').addEventListener('click', () => {
|
||||
this.restart();
|
||||
});
|
||||
|
||||
// 继续游戏按钮
|
||||
document.getElementById('keep-playing').addEventListener('click', () => {
|
||||
this.keepPlaying();
|
||||
});
|
||||
|
||||
// 重试按钮
|
||||
document.getElementById('retry-btn').addEventListener('click', () => {
|
||||
this.restart();
|
||||
});
|
||||
}
|
||||
|
||||
updateBestScore() {
|
||||
if (this.score > this.bestScore) {
|
||||
this.bestScore = this.score;
|
||||
localStorage.setItem('2048-best-score', this.bestScore.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 游戏实例
|
||||
let game;
|
||||
|
||||
// 页面加载完成后初始化游戏
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
game = new Game2048();
|
||||
|
||||
// 监听分数变化以更新最高分
|
||||
const originalUpdateDisplay = game.updateDisplay.bind(game);
|
||||
game.updateDisplay = function() {
|
||||
originalUpdateDisplay();
|
||||
this.updateBestScore();
|
||||
};
|
||||
|
||||
// 导出游戏实例供其他模块使用
|
||||
window.game2048 = game;
|
||||
});
|
||||
165
InfoGenie-frontend/public/smallgame/2048/index.html
Normal file
165
InfoGenie-frontend/public/smallgame/2048/index.html
Normal file
@@ -0,0 +1,165 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>2048游戏</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1 class="title">2048</h1>
|
||||
<div class="score-container">
|
||||
<div class="score-box">
|
||||
<div class="score-label">分数</div>
|
||||
<div class="score" id="score">0</div>
|
||||
</div>
|
||||
<div class="score-box">
|
||||
<div class="score-label">最高分</div>
|
||||
<div class="score" id="best-score">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="game-intro">
|
||||
<p class="game-explanation">
|
||||
合并相同数字,达到<strong>2048</strong>!
|
||||
</p>
|
||||
<div class="restart-button" id="restart-btn">新游戏</div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<div class="game-message" id="game-message">
|
||||
<p></p>
|
||||
<div class="lower">
|
||||
<a class="keep-playing-button" id="keep-playing">继续游戏</a>
|
||||
<a class="retry-button" id="retry-btn">重新开始</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
<div class="grid-row">
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
<div class="grid-cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tile-container" id="tile-container">
|
||||
<!-- 动态生成的数字方块 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-stats" id="game-stats">
|
||||
<h3>游戏统计</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">移动次数:</span>
|
||||
<span class="stat-value" id="moves-count">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">游戏时间:</span>
|
||||
<span class="stat-value" id="game-time">00:00</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最大数字:</span>
|
||||
<span class="stat-value" id="max-tile">2</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">合并次数:</span>
|
||||
<span class="stat-value" id="merge-count">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls-hint">
|
||||
<p><strong>操作说明:</strong></p>
|
||||
<p>手机: 滑动屏幕移动方块</p>
|
||||
<p>电脑: 使用方向键 ↑↓←→ 或 WASD 键</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏结束统计弹窗 -->
|
||||
<div class="modal" id="stats-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>游戏结束</h2>
|
||||
<span class="close" id="close-modal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="final-score">
|
||||
<h3>最终得分: <span id="final-score">0</span></h3>
|
||||
</div>
|
||||
<div class="achievement-section">
|
||||
<h4>成就统计</h4>
|
||||
<div class="achievement-grid">
|
||||
<div class="achievement-item">
|
||||
<span class="achievement-icon">🎯</span>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">总移动次数</div>
|
||||
<div class="achievement-value" id="final-moves">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement-item">
|
||||
<span class="achievement-icon">⏱️</span>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">游戏时长</div>
|
||||
<div class="achievement-value" id="final-time">00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement-item">
|
||||
<span class="achievement-icon">🏆</span>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">最大数字</div>
|
||||
<div class="achievement-value" id="final-max-tile">2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement-item">
|
||||
<span class="achievement-icon">🔥</span>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">合并次数</div>
|
||||
<div class="achievement-value" id="final-merges">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement-item">
|
||||
<span class="achievement-icon">📊</span>
|
||||
<div class="achievement-info">
|
||||
<div class="achievement-title">平均每步得分</div>
|
||||
<div class="achievement-value" id="avg-score">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-primary" id="new-game-btn">开始新游戏</button>
|
||||
<button class="btn btn-secondary" id="share-btn">分享成绩</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="game-logic.js"></script>
|
||||
<script src="controls.js"></script>
|
||||
<script src="statistics.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
381
InfoGenie-frontend/public/smallgame/2048/statistics.js
Normal file
381
InfoGenie-frontend/public/smallgame/2048/statistics.js
Normal file
@@ -0,0 +1,381 @@
|
||||
// 游戏统计模块
|
||||
class GameStatistics {
|
||||
constructor() {
|
||||
this.achievements = {
|
||||
firstWin: false,
|
||||
speedRunner: false, // 5分钟内达到2048
|
||||
efficient: false, // 少于500步达到2048
|
||||
persistent: false, // 游戏时间超过30分钟
|
||||
merger: false, // 单局合并超过100次
|
||||
highScorer: false // 分数超过50000
|
||||
};
|
||||
|
||||
this.loadAchievements();
|
||||
this.initializeModal();
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
if (!window.game2048) return;
|
||||
|
||||
const game = window.game2048;
|
||||
|
||||
// 更新实时统计显示
|
||||
document.getElementById('moves-count').textContent = game.stats.moves;
|
||||
document.getElementById('game-time').textContent = this.formatTime(game.stats.gameTime);
|
||||
document.getElementById('max-tile').textContent = game.stats.maxTile;
|
||||
document.getElementById('merge-count').textContent = game.stats.mergeCount;
|
||||
}
|
||||
|
||||
formatTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
showFinalStats() {
|
||||
if (!window.game2048) return;
|
||||
|
||||
const game = window.game2048;
|
||||
const modal = document.getElementById('stats-modal');
|
||||
|
||||
// 更新最终统计数据
|
||||
document.getElementById('final-score').textContent = game.score;
|
||||
document.getElementById('final-moves').textContent = game.stats.moves;
|
||||
document.getElementById('final-time').textContent = this.formatTime(game.stats.gameTime);
|
||||
document.getElementById('final-max-tile').textContent = game.stats.maxTile;
|
||||
document.getElementById('final-merges').textContent = game.stats.mergeCount;
|
||||
|
||||
// 计算平均每步得分
|
||||
const avgScore = game.stats.moves > 0 ? Math.round(game.score / game.stats.moves) : 0;
|
||||
document.getElementById('avg-score').textContent = avgScore;
|
||||
|
||||
// 检查成就
|
||||
this.checkAchievements(game);
|
||||
|
||||
// 显示模态框
|
||||
modal.style.display = 'block';
|
||||
|
||||
// 添加动画效果
|
||||
setTimeout(() => {
|
||||
modal.querySelector('.modal-content').style.transform = 'scale(1)';
|
||||
}, 10);
|
||||
}
|
||||
|
||||
checkAchievements(game) {
|
||||
let newAchievements = [];
|
||||
|
||||
// 首次胜利
|
||||
if (game.gameWon && !this.achievements.firstWin) {
|
||||
this.achievements.firstWin = true;
|
||||
newAchievements.push('🏆 首次胜利!达到了2048!');
|
||||
}
|
||||
|
||||
// 速度跑者 - 5分钟内达到2048
|
||||
if (game.gameWon && game.stats.gameTime <= 300 && !this.achievements.speedRunner) {
|
||||
this.achievements.speedRunner = true;
|
||||
newAchievements.push('⚡ 速度跑者!5分钟内达到2048!');
|
||||
}
|
||||
|
||||
// 高效玩家 - 少于500步达到2048
|
||||
if (game.gameWon && game.stats.moves < 500 && !this.achievements.efficient) {
|
||||
this.achievements.efficient = true;
|
||||
newAchievements.push('🎯 高效玩家!少于500步达到2048!');
|
||||
}
|
||||
|
||||
// 坚持不懈 - 游戏时间超过30分钟
|
||||
if (game.stats.gameTime >= 1800 && !this.achievements.persistent) {
|
||||
this.achievements.persistent = true;
|
||||
newAchievements.push('⏰ 坚持不懈!游戏时间超过30分钟!');
|
||||
}
|
||||
|
||||
// 合并大师 - 单局合并超过100次
|
||||
if (game.stats.mergeCount >= 100 && !this.achievements.merger) {
|
||||
this.achievements.merger = true;
|
||||
newAchievements.push('🔥 合并大师!单局合并超过100次!');
|
||||
}
|
||||
|
||||
// 高分玩家 - 分数超过50000
|
||||
if (game.score >= 50000 && !this.achievements.highScorer) {
|
||||
this.achievements.highScorer = true;
|
||||
newAchievements.push('💎 高分玩家!分数超过50000!');
|
||||
}
|
||||
|
||||
// 保存成就
|
||||
if (newAchievements.length > 0) {
|
||||
this.saveAchievements();
|
||||
this.showAchievementNotifications(newAchievements);
|
||||
}
|
||||
}
|
||||
|
||||
showAchievementNotifications(achievements) {
|
||||
// 在成就区域显示新获得的成就
|
||||
const achievementSection = document.querySelector('.achievement-section');
|
||||
|
||||
achievements.forEach((achievement, index) => {
|
||||
setTimeout(() => {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'achievement-notification';
|
||||
notification.innerHTML = `
|
||||
<div class="achievement-popup">
|
||||
<span class="achievement-text">${achievement}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
achievementSection.appendChild(notification);
|
||||
|
||||
// 添加样式
|
||||
const popup = notification.querySelector('.achievement-popup');
|
||||
popup.style.cssText = `
|
||||
background: linear-gradient(45deg, #ff6b6b, #feca57);
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border-radius: 20px;
|
||||
margin: 5px 0;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
animation: achievementSlide 0.5s ease-out;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
`;
|
||||
|
||||
// 添加动画样式
|
||||
if (!document.getElementById('achievement-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'achievement-styles';
|
||||
style.textContent = `
|
||||
@keyframes achievementSlide {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// 3秒后移除通知
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 3000);
|
||||
}, index * 500);
|
||||
});
|
||||
}
|
||||
|
||||
saveAchievements() {
|
||||
localStorage.setItem('2048-achievements', JSON.stringify(this.achievements));
|
||||
}
|
||||
|
||||
loadAchievements() {
|
||||
const saved = localStorage.getItem('2048-achievements');
|
||||
if (saved) {
|
||||
this.achievements = { ...this.achievements, ...JSON.parse(saved) };
|
||||
}
|
||||
}
|
||||
|
||||
initializeModal() {
|
||||
const modal = document.getElementById('stats-modal');
|
||||
const closeBtn = document.getElementById('close-modal');
|
||||
const newGameBtn = document.getElementById('new-game-btn');
|
||||
const shareBtn = document.getElementById('share-btn');
|
||||
|
||||
// 关闭模态框
|
||||
closeBtn.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
// 点击模态框外部关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 新游戏按钮
|
||||
newGameBtn.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
if (window.game2048) {
|
||||
window.game2048.restart();
|
||||
}
|
||||
});
|
||||
|
||||
// 分享按钮
|
||||
shareBtn.addEventListener('click', () => {
|
||||
this.shareScore();
|
||||
});
|
||||
|
||||
// ESC键关闭模态框
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.style.display === 'block') {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
shareScore() {
|
||||
if (!window.game2048) return;
|
||||
|
||||
const game = window.game2048;
|
||||
const shareText = `我在2048游戏中获得了${game.score}分!\n` +
|
||||
`最大数字: ${game.stats.maxTile}\n` +
|
||||
`移动次数: ${game.stats.moves}\n` +
|
||||
`游戏时间: ${this.formatTime(game.stats.gameTime)}\n` +
|
||||
`来挑战一下吧!`;
|
||||
|
||||
// 尝试使用Web Share API
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
title: '2048游戏成绩',
|
||||
text: shareText,
|
||||
url: window.location.href
|
||||
}).catch(err => {
|
||||
console.log('分享失败:', err);
|
||||
this.fallbackShare(shareText);
|
||||
});
|
||||
} else {
|
||||
this.fallbackShare(shareText);
|
||||
}
|
||||
}
|
||||
|
||||
fallbackShare(text) {
|
||||
// 复制到剪贴板
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
this.showToast('成绩已复制到剪贴板!');
|
||||
}).catch(() => {
|
||||
this.showShareModal(text);
|
||||
});
|
||||
} else {
|
||||
this.showShareModal(text);
|
||||
}
|
||||
}
|
||||
|
||||
showShareModal(text) {
|
||||
// 创建分享文本显示框
|
||||
const shareModal = document.createElement('div');
|
||||
shareModal.innerHTML = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
z-index: 10000;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
">
|
||||
<h3>分享你的成绩</h3>
|
||||
<textarea readonly style="
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
resize: none;
|
||||
">${text}</textarea>
|
||||
<div>
|
||||
<button onclick="this.parentElement.parentElement.parentElement.remove()" style="
|
||||
background: #4ecdc4;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
">关闭</button>
|
||||
<button onclick="
|
||||
this.parentElement.previousElementSibling.select();
|
||||
document.execCommand('copy');
|
||||
alert('已复制到剪贴板!');
|
||||
" style="
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(shareModal);
|
||||
}
|
||||
|
||||
showToast(message) {
|
||||
const toast = document.createElement('div');
|
||||
toast.textContent = message;
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
z-index: 10000;
|
||||
font-weight: bold;
|
||||
animation: toastSlide 0.3s ease-out;
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 获取游戏统计摘要
|
||||
getStatsSummary() {
|
||||
if (!window.game2048) return null;
|
||||
|
||||
const game = window.game2048;
|
||||
return {
|
||||
score: game.score,
|
||||
bestScore: game.bestScore,
|
||||
moves: game.stats.moves,
|
||||
gameTime: game.stats.gameTime,
|
||||
maxTile: game.stats.maxTile,
|
||||
mergeCount: game.stats.mergeCount,
|
||||
achievements: this.achievements
|
||||
};
|
||||
}
|
||||
|
||||
// 重置所有统计数据
|
||||
resetAllStats() {
|
||||
this.achievements = {
|
||||
firstWin: false,
|
||||
speedRunner: false,
|
||||
efficient: false,
|
||||
persistent: false,
|
||||
merger: false,
|
||||
highScorer: false
|
||||
};
|
||||
|
||||
localStorage.removeItem('2048-achievements');
|
||||
localStorage.removeItem('2048-best-score');
|
||||
|
||||
this.showToast('所有统计数据已重置!');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建全局统计实例
|
||||
window.gameStats = new GameStatistics();
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 确保统计模块正确初始化
|
||||
if (!window.gameStats) {
|
||||
window.gameStats = new GameStatistics();
|
||||
}
|
||||
});
|
||||
611
InfoGenie-frontend/public/smallgame/2048/styles.css
Normal file
611
InfoGenie-frontend/public/smallgame/2048/styles.css
Normal file
@@ -0,0 +1,611 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #776e65;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.score-box {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 12px;
|
||||
color: #776e65;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* 游戏介绍区域 */
|
||||
.game-intro {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.game-explanation {
|
||||
margin-bottom: 15px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.restart-button {
|
||||
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.restart-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* 游戏容器 */
|
||||
.game-container {
|
||||
position: relative;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 网格样式 */
|
||||
.grid-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.grid-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
width: calc(25% - 6px);
|
||||
height: 80px;
|
||||
background: rgba(238, 228, 218, 0.35);
|
||||
border-radius: 8px;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 数字方块容器 */
|
||||
.tile-container {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
bottom: 15px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 数字方块样式 */
|
||||
.tile {
|
||||
position: absolute;
|
||||
width: calc(25% - 6px);
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 32px;
|
||||
transition: all 0.15s ease-in-out;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 不同数字的颜色 */
|
||||
.tile-2 { background: #eee4da; color: #776e65; }
|
||||
.tile-4 { background: #ede0c8; color: #776e65; }
|
||||
.tile-8 { background: #f2b179; color: #f9f6f2; }
|
||||
.tile-16 { background: #f59563; color: #f9f6f2; }
|
||||
.tile-32 { background: #f67c5f; color: #f9f6f2; }
|
||||
.tile-64 { background: #f65e3b; color: #f9f6f2; }
|
||||
.tile-128 { background: #edcf72; color: #f9f6f2; font-size: 28px; }
|
||||
.tile-256 { background: #edcc61; color: #f9f6f2; font-size: 28px; }
|
||||
.tile-512 { background: #edc850; color: #f9f6f2; font-size: 28px; }
|
||||
.tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 24px; }
|
||||
.tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 24px; box-shadow: 0 0 20px rgba(237, 194, 46, 0.5); }
|
||||
.tile-super { background: #3c3a32; color: #f9f6f2; font-size: 20px; }
|
||||
|
||||
/* 动画效果 */
|
||||
.tile-new {
|
||||
animation: appear 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.tile-merged {
|
||||
animation: pop 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 游戏消息 */
|
||||
.game-message {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.game-message.game-won {
|
||||
background: rgba(237, 194, 46, 0.9);
|
||||
color: #f9f6f2;
|
||||
}
|
||||
|
||||
.game-message.game-over {
|
||||
background: rgba(238, 228, 218, 0.9);
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
.game-message p {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lower {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.keep-playing-button,
|
||||
.retry-button {
|
||||
background: #8f7a66;
|
||||
color: #f9f6f2;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.keep-playing-button:hover,
|
||||
.retry-button:hover {
|
||||
background: #9f8a76;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 游戏统计 */
|
||||
.game-stats {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.game-stats h3 {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
color: #776e65;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background: rgba(238, 228, 218, 0.3);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #776e65;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: bold;
|
||||
color: #f67c5f;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 操作提示 */
|
||||
.controls-hint {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.controls-hint p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.controls-hint p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 5% auto;
|
||||
border-radius: 15px;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
animation: modalSlideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 15px 15px 0 0;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.final-score {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.final-score h3 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.achievement-section h4 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #776e65;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.achievement-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.achievement-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.achievement-item:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.achievement-icon {
|
||||
font-size: 32px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.achievement-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.achievement-title {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.achievement-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(45deg, #4ecdc4, #44a08d);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(45deg, #ff9a9e, #fecfef);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 手机端优化 */
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grid-cell,
|
||||
.tile {
|
||||
height: 70px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.tile-128,
|
||||
.tile-256,
|
||||
.tile-512 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tile-1024,
|
||||
.tile-2048 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tile-super {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.game-message p {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
margin: 10% auto;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.achievement-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 */
|
||||
@media (max-width: 360px) {
|
||||
.grid-cell,
|
||||
.tile {
|
||||
height: 60px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.tile-128,
|
||||
.tile-256,
|
||||
.tile-512 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tile-1024,
|
||||
.tile-2048 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tile-super {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 电脑端优化 */
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.grid-cell,
|
||||
.tile {
|
||||
height: 90px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.achievement-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
/* 触摸优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.restart-button,
|
||||
.keep-playing-button,
|
||||
.retry-button,
|
||||
.btn {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
279
InfoGenie-frontend/public/smallgame/俄罗斯方块/game-controls.js
Normal file
279
InfoGenie-frontend/public/smallgame/俄罗斯方块/game-controls.js
Normal file
@@ -0,0 +1,279 @@
|
||||
// 游戏控制模块
|
||||
class GameControls {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.keys = {};
|
||||
this.keyRepeatDelay = 150;
|
||||
this.keyRepeatInterval = 50;
|
||||
this.keyTimers = {};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupKeyboardControls();
|
||||
this.setupTouchControls();
|
||||
this.setupButtonControls();
|
||||
}
|
||||
|
||||
setupKeyboardControls() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
this.handleKeyDown(e);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
this.handleKeyUp(e);
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
const key = e.key;
|
||||
|
||||
// 防止重复触发
|
||||
if (this.keys[key]) return;
|
||||
this.keys[key] = true;
|
||||
|
||||
switch(key) {
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.game.moveLeft();
|
||||
this.startKeyRepeat('ArrowLeft', () => this.game.moveLeft());
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.game.moveRight();
|
||||
this.startKeyRepeat('ArrowRight', () => this.game.moveRight());
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.game.moveDown();
|
||||
this.startKeyRepeat('ArrowDown', () => this.game.moveDown());
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
e.preventDefault();
|
||||
if (this.game.gameRunning) {
|
||||
this.game.pause();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (!this.game.gameRunning) {
|
||||
this.game.start();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
case 'R':
|
||||
if (e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
this.game.restart();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
if (this.game.gameRunning) {
|
||||
this.game.pause();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyUp(e) {
|
||||
const key = e.key;
|
||||
this.keys[key] = false;
|
||||
this.stopKeyRepeat(key);
|
||||
}
|
||||
|
||||
startKeyRepeat(key, action) {
|
||||
this.stopKeyRepeat(key);
|
||||
|
||||
this.keyTimers[key] = setTimeout(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (this.keys[key]) {
|
||||
action();
|
||||
} else {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
}, this.keyRepeatInterval);
|
||||
|
||||
this.keyTimers[key] = intervalId;
|
||||
}, this.keyRepeatDelay);
|
||||
}
|
||||
|
||||
stopKeyRepeat(key) {
|
||||
if (this.keyTimers[key]) {
|
||||
clearTimeout(this.keyTimers[key]);
|
||||
clearInterval(this.keyTimers[key]);
|
||||
delete this.keyTimers[key];
|
||||
}
|
||||
}
|
||||
|
||||
setupTouchControls() {
|
||||
// 移动端触摸控制
|
||||
const leftBtn = document.getElementById('leftBtn');
|
||||
const rightBtn = document.getElementById('rightBtn');
|
||||
const downBtn = document.getElementById('downBtn');
|
||||
const rotateBtn = document.getElementById('rotateBtn');
|
||||
const dropBtn = document.getElementById('dropBtn');
|
||||
const pauseBtn = document.getElementById('pauseBtn');
|
||||
|
||||
// 左移
|
||||
this.addTouchEvents(leftBtn, () => this.game.moveLeft());
|
||||
|
||||
// 右移
|
||||
this.addTouchEvents(rightBtn, () => this.game.moveRight());
|
||||
|
||||
// 下移
|
||||
this.addTouchEvents(downBtn, () => this.game.moveDown());
|
||||
|
||||
// 旋转
|
||||
rotateBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
});
|
||||
|
||||
rotateBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
});
|
||||
|
||||
// 硬降
|
||||
dropBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.hardDrop();
|
||||
});
|
||||
|
||||
dropBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.hardDrop();
|
||||
});
|
||||
|
||||
// 暂停
|
||||
pauseBtn.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.pause();
|
||||
});
|
||||
|
||||
pauseBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.game.pause();
|
||||
});
|
||||
}
|
||||
|
||||
addTouchEvents(element, action) {
|
||||
let touchInterval;
|
||||
let touchTimeout;
|
||||
|
||||
const startAction = (e) => {
|
||||
e.preventDefault();
|
||||
action();
|
||||
|
||||
touchTimeout = setTimeout(() => {
|
||||
touchInterval = setInterval(action, this.keyRepeatInterval);
|
||||
}, this.keyRepeatDelay);
|
||||
};
|
||||
|
||||
const stopAction = (e) => {
|
||||
e.preventDefault();
|
||||
if (touchTimeout) {
|
||||
clearTimeout(touchTimeout);
|
||||
touchTimeout = null;
|
||||
}
|
||||
if (touchInterval) {
|
||||
clearInterval(touchInterval);
|
||||
touchInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener('touchstart', startAction);
|
||||
element.addEventListener('touchend', stopAction);
|
||||
element.addEventListener('touchcancel', stopAction);
|
||||
element.addEventListener('mousedown', startAction);
|
||||
element.addEventListener('mouseup', stopAction);
|
||||
element.addEventListener('mouseleave', stopAction);
|
||||
}
|
||||
|
||||
setupButtonControls() {
|
||||
const startBtn = document.getElementById('startBtn');
|
||||
const restartBtn = document.getElementById('restartBtn');
|
||||
|
||||
startBtn.addEventListener('click', () => {
|
||||
this.game.start();
|
||||
});
|
||||
|
||||
restartBtn.addEventListener('click', () => {
|
||||
this.game.restart();
|
||||
});
|
||||
}
|
||||
|
||||
// 游戏手势支持
|
||||
setupSwipeControls() {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let threshold = 50;
|
||||
|
||||
this.game.canvas.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
startX = touch.clientX;
|
||||
startY = touch.clientY;
|
||||
});
|
||||
|
||||
this.game.canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
this.game.canvas.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.changedTouches[0];
|
||||
const deltaX = touch.clientX - startX;
|
||||
const deltaY = touch.clientY - startY;
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// 水平滑动
|
||||
if (Math.abs(deltaX) > threshold) {
|
||||
if (deltaX > 0) {
|
||||
this.game.moveRight();
|
||||
} else {
|
||||
this.game.moveLeft();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 垂直滑动
|
||||
if (Math.abs(deltaY) > threshold) {
|
||||
if (deltaY > 0) {
|
||||
this.game.moveDown();
|
||||
} else {
|
||||
this.game.rotatePiece();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 双击旋转
|
||||
let lastTap = 0;
|
||||
this.game.canvas.addEventListener('touchend', (e) => {
|
||||
const currentTime = new Date().getTime();
|
||||
const tapLength = currentTime - lastTap;
|
||||
|
||||
if (tapLength < 500 && tapLength > 0) {
|
||||
e.preventDefault();
|
||||
this.game.rotatePiece();
|
||||
}
|
||||
lastTap = currentTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化控制系统
|
||||
const gameControls = new GameControls(game);
|
||||
338
InfoGenie-frontend/public/smallgame/俄罗斯方块/game-stats.js
Normal file
338
InfoGenie-frontend/public/smallgame/俄罗斯方块/game-stats.js
Normal file
@@ -0,0 +1,338 @@
|
||||
// 游戏统计和成就系统
|
||||
class GameStats {
|
||||
constructor() {
|
||||
this.achievements = [
|
||||
{
|
||||
id: 'first_game',
|
||||
name: '初次体验',
|
||||
description: '完成第一次游戏',
|
||||
condition: (stats) => true
|
||||
},
|
||||
{
|
||||
id: 'score_1000',
|
||||
name: '小试牛刀',
|
||||
description: '单局得分达到1000分',
|
||||
condition: (stats) => stats.score >= 1000
|
||||
},
|
||||
{
|
||||
id: 'score_5000',
|
||||
name: '游戏达人',
|
||||
description: '单局得分达到5000分',
|
||||
condition: (stats) => stats.score >= 5000
|
||||
},
|
||||
{
|
||||
id: 'score_10000',
|
||||
name: '方块大师',
|
||||
description: '单局得分达到10000分',
|
||||
condition: (stats) => stats.score >= 10000
|
||||
},
|
||||
{
|
||||
id: 'level_5',
|
||||
name: '步步高升',
|
||||
description: '达到第5级',
|
||||
condition: (stats) => stats.level >= 5
|
||||
},
|
||||
{
|
||||
id: 'level_10',
|
||||
name: '速度之王',
|
||||
description: '达到第10级',
|
||||
condition: (stats) => stats.level >= 10
|
||||
},
|
||||
{
|
||||
id: 'lines_50',
|
||||
name: '消除专家',
|
||||
description: '累计消除50行',
|
||||
condition: (stats) => stats.lines >= 50
|
||||
},
|
||||
{
|
||||
id: 'lines_100',
|
||||
name: '清理大师',
|
||||
description: '累计消除100行',
|
||||
condition: (stats) => stats.lines >= 100
|
||||
},
|
||||
{
|
||||
id: 'tetris',
|
||||
name: 'Tetris!',
|
||||
description: '一次消除4行',
|
||||
condition: (stats) => stats.maxCombo >= 4
|
||||
},
|
||||
{
|
||||
id: 'time_10min',
|
||||
name: '持久战士',
|
||||
description: '单局游戏时间超过10分钟',
|
||||
condition: (stats) => stats.playTime >= 600000
|
||||
},
|
||||
{
|
||||
id: 'efficiency',
|
||||
name: '效率专家',
|
||||
description: '平均每分钟得分超过500',
|
||||
condition: (stats) => stats.avgScore >= 500
|
||||
}
|
||||
];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const playAgainBtn = document.getElementById('playAgainBtn');
|
||||
playAgainBtn.addEventListener('click', () => {
|
||||
this.hideStats();
|
||||
game.restart();
|
||||
});
|
||||
}
|
||||
|
||||
showStats(gameData) {
|
||||
const playTimeMinutes = gameData.playTime / 60000;
|
||||
const avgScore = playTimeMinutes > 0 ? Math.round(gameData.score / playTimeMinutes) : 0;
|
||||
|
||||
const stats = {
|
||||
...gameData,
|
||||
avgScore: avgScore
|
||||
};
|
||||
|
||||
// 更新统计显示
|
||||
document.getElementById('finalScore').textContent = stats.score.toLocaleString();
|
||||
document.getElementById('finalLevel').textContent = stats.level;
|
||||
document.getElementById('finalLines').textContent = stats.lines;
|
||||
document.getElementById('playTime').textContent = this.formatTime(stats.playTime);
|
||||
document.getElementById('maxCombo').textContent = stats.maxCombo;
|
||||
document.getElementById('avgScore').textContent = stats.avgScore;
|
||||
|
||||
// 检查成就
|
||||
const achievement = this.checkAchievements(stats);
|
||||
this.displayAchievement(achievement);
|
||||
|
||||
// 显示统计界面
|
||||
document.getElementById('gameStats').style.display = 'flex';
|
||||
document.getElementById('gameStats').classList.add('fade-in');
|
||||
}
|
||||
|
||||
hideStats() {
|
||||
document.getElementById('gameStats').style.display = 'none';
|
||||
document.getElementById('gameStats').classList.remove('fade-in');
|
||||
}
|
||||
|
||||
formatTime(milliseconds) {
|
||||
const seconds = Math.floor(milliseconds / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
checkAchievements(stats) {
|
||||
// 获取已获得的成就
|
||||
const earnedAchievements = this.getEarnedAchievements();
|
||||
|
||||
// 检查新成就
|
||||
for (let achievement of this.achievements) {
|
||||
if (!earnedAchievements.includes(achievement.id) &&
|
||||
achievement.condition(stats)) {
|
||||
|
||||
// 保存新成就
|
||||
this.saveAchievement(achievement.id);
|
||||
return achievement;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
displayAchievement(achievement) {
|
||||
const achievementEl = document.getElementById('achievement');
|
||||
|
||||
if (achievement) {
|
||||
achievementEl.innerHTML = `
|
||||
🏆 <strong>成就解锁!</strong><br>
|
||||
<strong>${achievement.name}</strong><br>
|
||||
${achievement.description}
|
||||
`;
|
||||
achievementEl.classList.add('pulse');
|
||||
} else {
|
||||
// 显示随机鼓励话语
|
||||
const encouragements = [
|
||||
'继续努力,你会变得更强!',
|
||||
'每一次游戏都是进步的机会!',
|
||||
'方块世界需要你的智慧!',
|
||||
'熟能生巧,加油!',
|
||||
'下一局一定会更好!',
|
||||
'坚持就是胜利!',
|
||||
'你的反应速度在提升!',
|
||||
'策略思维正在增强!'
|
||||
];
|
||||
|
||||
const randomEncouragement = encouragements[Math.floor(Math.random() * encouragements.length)];
|
||||
achievementEl.innerHTML = `💪 ${randomEncouragement}`;
|
||||
achievementEl.classList.remove('pulse');
|
||||
}
|
||||
}
|
||||
|
||||
getEarnedAchievements() {
|
||||
const saved = localStorage.getItem('tetris_achievements');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
}
|
||||
|
||||
saveAchievement(achievementId) {
|
||||
const earned = this.getEarnedAchievements();
|
||||
if (!earned.includes(achievementId)) {
|
||||
earned.push(achievementId);
|
||||
localStorage.setItem('tetris_achievements', JSON.stringify(earned));
|
||||
}
|
||||
}
|
||||
|
||||
// 获取历史最佳记录
|
||||
getBestStats() {
|
||||
const saved = localStorage.getItem('tetris_best_stats');
|
||||
return saved ? JSON.parse(saved) : {
|
||||
score: 0,
|
||||
level: 0,
|
||||
lines: 0,
|
||||
maxCombo: 0
|
||||
};
|
||||
}
|
||||
|
||||
// 保存最佳记录
|
||||
saveBestStats(stats) {
|
||||
const best = this.getBestStats();
|
||||
let updated = false;
|
||||
|
||||
if (stats.score > best.score) {
|
||||
best.score = stats.score;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.level > best.level) {
|
||||
best.level = stats.level;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.lines > best.lines) {
|
||||
best.lines = stats.lines;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (stats.maxCombo > best.maxCombo) {
|
||||
best.maxCombo = stats.maxCombo;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
localStorage.setItem('tetris_best_stats', JSON.stringify(best));
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
// 显示排行榜
|
||||
showLeaderboard() {
|
||||
const best = this.getBestStats();
|
||||
const earned = this.getEarnedAchievements();
|
||||
|
||||
console.log('最佳记录:', best);
|
||||
console.log('已获得成就:', earned.length + '/' + this.achievements.length);
|
||||
}
|
||||
}
|
||||
|
||||
// 高级特效系统
|
||||
class GameEffects {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.particles = [];
|
||||
this.effects = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 创建特效canvas
|
||||
this.effectsCanvas = document.createElement('canvas');
|
||||
this.effectsCanvas.width = this.game.canvas.width;
|
||||
this.effectsCanvas.height = this.game.canvas.height;
|
||||
this.effectsCanvas.style.position = 'absolute';
|
||||
this.effectsCanvas.style.top = '0';
|
||||
this.effectsCanvas.style.left = '0';
|
||||
this.effectsCanvas.style.pointerEvents = 'none';
|
||||
this.effectsCanvas.style.zIndex = '10';
|
||||
|
||||
this.effectsCtx = this.effectsCanvas.getContext('2d');
|
||||
|
||||
// 将特效canvas添加到游戏板容器中
|
||||
this.game.canvas.parentElement.style.position = 'relative';
|
||||
this.game.canvas.parentElement.appendChild(this.effectsCanvas);
|
||||
}
|
||||
|
||||
// 行消除特效
|
||||
lineCleared(row) {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * this.game.canvas.width,
|
||||
y: row * this.game.CELL_SIZE + this.game.CELL_SIZE / 2,
|
||||
vx: (Math.random() - 0.5) * 10,
|
||||
vy: (Math.random() - 0.5) * 10,
|
||||
life: 1,
|
||||
decay: 0.02,
|
||||
color: `hsl(${Math.random() * 360}, 100%, 50%)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 方块锁定特效
|
||||
pieceLocked(piece) {
|
||||
const centerX = (piece.x + piece.matrix[0].length / 2) * this.game.CELL_SIZE;
|
||||
const centerY = (piece.y + piece.matrix.length / 2) * this.game.CELL_SIZE;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.particles.push({
|
||||
x: centerX,
|
||||
y: centerY,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
life: 0.8,
|
||||
decay: 0.03,
|
||||
color: piece.color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新特效
|
||||
update() {
|
||||
// 更新粒子
|
||||
for (let i = this.particles.length - 1; i >= 0; i--) {
|
||||
const particle = this.particles[i];
|
||||
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.life -= particle.decay;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
this.particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制特效
|
||||
draw() {
|
||||
this.effectsCtx.clearRect(0, 0, this.effectsCanvas.width, this.effectsCanvas.height);
|
||||
|
||||
// 绘制粒子
|
||||
for (let particle of this.particles) {
|
||||
this.effectsCtx.save();
|
||||
this.effectsCtx.globalAlpha = particle.life;
|
||||
this.effectsCtx.fillStyle = particle.color;
|
||||
this.effectsCtx.beginPath();
|
||||
this.effectsCtx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
|
||||
this.effectsCtx.fill();
|
||||
this.effectsCtx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建统计系统实例
|
||||
const gameStats = new GameStats();
|
||||
|
||||
// 在适当的地方创建特效系统
|
||||
// const gameEffects = new GameEffects(game);
|
||||
124
InfoGenie-frontend/public/smallgame/俄罗斯方块/index.html
Normal file
124
InfoGenie-frontend/public/smallgame/俄罗斯方块/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>俄罗斯方块</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<h1>俄罗斯方块</h1>
|
||||
<div class="score-board">
|
||||
<div class="score-item">
|
||||
<span class="label">分数:</span>
|
||||
<span id="score">0</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="label">等级:</span>
|
||||
<span id="level">1</span>
|
||||
</div>
|
||||
<div class="score-item">
|
||||
<span class="label">消除行数:</span>
|
||||
<span id="lines">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-main">
|
||||
<div class="game-board">
|
||||
<canvas id="gameCanvas" width="300" height="600"></canvas>
|
||||
<div class="game-overlay" id="gameOverlay">
|
||||
<div class="overlay-content">
|
||||
<h2 id="overlayTitle">游戏暂停</h2>
|
||||
<p id="overlayMessage">按空格键继续游戏</p>
|
||||
<button id="startBtn" class="game-btn">开始游戏</button>
|
||||
<button id="restartBtn" class="game-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-sidebar">
|
||||
<div class="next-piece">
|
||||
<h3>下一个</h3>
|
||||
<canvas id="nextCanvas" width="120" height="120"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="controls-info">
|
||||
<h3>操作说明</h3>
|
||||
<div class="control-item">
|
||||
<span class="key">←→</span>
|
||||
<span class="desc">移动</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">↓</span>
|
||||
<span class="desc">快速下降</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">↑</span>
|
||||
<span class="desc">旋转</span>
|
||||
</div>
|
||||
<div class="control-item">
|
||||
<span class="key">空格</span>
|
||||
<span class="desc">暂停/继续</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机端触摸控制 -->
|
||||
<div class="mobile-controls">
|
||||
<div class="mobile-controls-left">
|
||||
<button class="control-btn" id="rotateBtn">↻</button>
|
||||
<button class="control-btn" id="leftBtn">←</button>
|
||||
<button class="control-btn" id="downBtn">↓</button>
|
||||
</div>
|
||||
<div class="mobile-controls-right">
|
||||
<button class="control-btn" id="dropBtn">⇩</button>
|
||||
<button class="control-btn" id="rightBtn">→</button>
|
||||
<button class="control-btn" id="pauseBtn">⏸</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏结束统计界面 -->
|
||||
<div class="game-stats" id="gameStats">
|
||||
<div class="stats-content">
|
||||
<h2>游戏结束</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">最终分数</span>
|
||||
<span class="stat-value" id="finalScore">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">达到等级</span>
|
||||
<span class="stat-value" id="finalLevel">1</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">消除行数</span>
|
||||
<span class="stat-value" id="finalLines">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">游戏时长</span>
|
||||
<span class="stat-value" id="playTime">00:00</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">单次消除最大行数</span>
|
||||
<span class="stat-value" id="maxCombo">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">平均每分钟分数</span>
|
||||
<span class="stat-value" id="avgScore">0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="achievement" id="achievement"></div>
|
||||
<button class="game-btn" id="playAgainBtn">再玩一次</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="tetris.js"></script>
|
||||
<script src="game-controls.js"></script>
|
||||
<script src="game-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
459
InfoGenie-frontend/public/smallgame/俄罗斯方块/styles.css
Normal file
459
InfoGenie-frontend/public/smallgame/俄罗斯方块/styles.css
Normal file
@@ -0,0 +1,459 @@
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||||
color: white;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 游戏容器 */
|
||||
.game-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 游戏头部 */
|
||||
.game-header {
|
||||
text-align: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.score-board {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 8px 15px;
|
||||
border-radius: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.score-item .label {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.score-item span:last-child {
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
/* 游戏主体 */
|
||||
.game-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
display: block;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
}
|
||||
|
||||
/* 游戏覆盖层 */
|
||||
.game-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.overlay-content h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.overlay-content p {
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 游戏按钮 */
|
||||
.game-btn {
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin: 5px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.game-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.game-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.game-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.next-piece {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.next-piece h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
#nextCanvas {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.controls-info {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.controls-info h3 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.control-item .key {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.control-item .desc {
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 手机端控制 */
|
||||
.mobile-controls {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.mobile-controls-left {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mobile-controls-right {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(45deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
user-select: none;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.9);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* 游戏统计界面 */
|
||||
.game-stats {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
padding: 30px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
max-width: 90%;
|
||||
width: 400px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.stats-content h2 {
|
||||
margin-bottom: 25px;
|
||||
font-size: 2rem;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.achievement {
|
||||
background: linear-gradient(45deg, #f093fb, #f5576c);
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 25px;
|
||||
font-weight: bold;
|
||||
min-height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.game-container {
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.game-main {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.game-sidebar {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.mobile-controls {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 250px;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats-content {
|
||||
padding: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.game-header h1 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 200px;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.mobile-controls-left,
|
||||
.mobile-controls-right {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-controls-left {
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.mobile-controls-right {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.game-sidebar {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.next-piece,
|
||||
.controls-info {
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏类 */
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
521
InfoGenie-frontend/public/smallgame/俄罗斯方块/tetris.js
Normal file
521
InfoGenie-frontend/public/smallgame/俄罗斯方块/tetris.js
Normal file
@@ -0,0 +1,521 @@
|
||||
// 俄罗斯方块主游戏逻辑
|
||||
class TetrisGame {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('gameCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.nextCanvas = document.getElementById('nextCanvas');
|
||||
this.nextCtx = this.nextCanvas.getContext('2d');
|
||||
|
||||
// 游戏配置
|
||||
this.BOARD_WIDTH = 10;
|
||||
this.BOARD_HEIGHT = 20;
|
||||
this.CELL_SIZE = 30;
|
||||
|
||||
// 游戏状态
|
||||
this.board = [];
|
||||
this.currentPiece = null;
|
||||
this.nextPiece = null;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.lines = 0;
|
||||
this.dropTime = 0;
|
||||
this.dropInterval = 1000; // 初始下降间隔(毫秒)
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.lastTime = 0;
|
||||
|
||||
// 统计数据
|
||||
this.gameStartTime = 0;
|
||||
this.maxCombo = 0;
|
||||
this.currentCombo = 0;
|
||||
|
||||
// 方块类型定义
|
||||
this.pieces = {
|
||||
I: {
|
||||
color: '#00f5ff',
|
||||
matrix: [
|
||||
[0, 0, 0, 0],
|
||||
[1, 1, 1, 1],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]
|
||||
]
|
||||
},
|
||||
O: {
|
||||
color: '#ffff00',
|
||||
matrix: [
|
||||
[1, 1],
|
||||
[1, 1]
|
||||
]
|
||||
},
|
||||
T: {
|
||||
color: '#800080',
|
||||
matrix: [
|
||||
[0, 1, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
S: {
|
||||
color: '#00ff00',
|
||||
matrix: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 0],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
Z: {
|
||||
color: '#ff0000',
|
||||
matrix: [
|
||||
[1, 1, 0],
|
||||
[0, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
J: {
|
||||
color: '#0000ff',
|
||||
matrix: [
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
},
|
||||
L: {
|
||||
color: '#ffa500',
|
||||
matrix: [
|
||||
[0, 0, 1],
|
||||
[1, 1, 1],
|
||||
[0, 0, 0]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
this.pieceTypes = Object.keys(this.pieces);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initBoard();
|
||||
this.nextPiece = this.createPiece();
|
||||
this.spawnPiece();
|
||||
this.updateDisplay();
|
||||
this.showOverlay('游戏准备', '点击开始游戏按钮开始');
|
||||
}
|
||||
|
||||
initBoard() {
|
||||
this.board = [];
|
||||
for (let row = 0; row < this.BOARD_HEIGHT; row++) {
|
||||
this.board[row] = [];
|
||||
for (let col = 0; col < this.BOARD_WIDTH; col++) {
|
||||
this.board[row][col] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPiece() {
|
||||
const type = this.pieceTypes[Math.floor(Math.random() * this.pieceTypes.length)];
|
||||
const piece = this.pieces[type];
|
||||
return {
|
||||
type: type,
|
||||
color: piece.color,
|
||||
matrix: this.copyMatrix(piece.matrix),
|
||||
x: Math.floor(this.BOARD_WIDTH / 2) - Math.floor(piece.matrix[0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
copyMatrix(matrix) {
|
||||
return matrix.map(row => [...row]);
|
||||
}
|
||||
|
||||
spawnPiece() {
|
||||
this.currentPiece = this.nextPiece;
|
||||
this.nextPiece = this.createPiece();
|
||||
this.currentPiece.x = Math.floor(this.BOARD_WIDTH / 2) - Math.floor(this.currentPiece.matrix[0].length / 2);
|
||||
this.currentPiece.y = 0;
|
||||
|
||||
// 检查游戏结束
|
||||
if (this.collision(this.currentPiece)) {
|
||||
this.gameOver();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.drawNextPiece();
|
||||
return true;
|
||||
}
|
||||
|
||||
collision(piece, dx = 0, dy = 0) {
|
||||
const matrix = piece.matrix;
|
||||
const x = piece.x + dx;
|
||||
const y = piece.y + dy;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
const newX = x + col;
|
||||
const newY = y + row;
|
||||
|
||||
if (newX < 0 || newX >= this.BOARD_WIDTH ||
|
||||
newY >= this.BOARD_HEIGHT ||
|
||||
(newY >= 0 && this.board[newY][newX] !== 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
rotate(piece) {
|
||||
const matrix = piece.matrix;
|
||||
const N = matrix.length;
|
||||
const rotated = [];
|
||||
|
||||
// 创建旋转后的矩阵
|
||||
for (let i = 0; i < N; i++) {
|
||||
rotated[i] = [];
|
||||
for (let j = 0; j < N; j++) {
|
||||
rotated[i][j] = matrix[N - 1 - j][i];
|
||||
}
|
||||
}
|
||||
|
||||
return rotated;
|
||||
}
|
||||
|
||||
hardDrop() {
|
||||
while (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
this.score += 2; // 硬降给额外分数
|
||||
}
|
||||
this.lockPiece();
|
||||
}
|
||||
|
||||
lockPiece() {
|
||||
const matrix = this.currentPiece.matrix;
|
||||
const x = this.currentPiece.x;
|
||||
const y = this.currentPiece.y;
|
||||
|
||||
// 将方块锁定到游戏板上
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
if (y + row >= 0) {
|
||||
this.board[y + row][x + col] = this.currentPiece.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查并清除完整的行
|
||||
const linesCleared = this.clearLines();
|
||||
if (linesCleared > 0) {
|
||||
this.lines += linesCleared;
|
||||
this.currentCombo = linesCleared;
|
||||
this.maxCombo = Math.max(this.maxCombo, this.currentCombo);
|
||||
|
||||
// 计算分数 (基于消除行数和等级)
|
||||
const lineScores = [0, 40, 100, 300, 1200];
|
||||
this.score += lineScores[linesCleared] * this.level;
|
||||
|
||||
// 升级逻辑
|
||||
this.level = Math.floor(this.lines / 10) + 1;
|
||||
this.dropInterval = Math.max(50, 1000 - (this.level - 1) * 50);
|
||||
} else {
|
||||
this.currentCombo = 0;
|
||||
}
|
||||
|
||||
// 生成下一个方块
|
||||
this.spawnPiece();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
clearLines() {
|
||||
let linesCleared = 0;
|
||||
|
||||
for (let row = this.BOARD_HEIGHT - 1; row >= 0; row--) {
|
||||
if (this.board[row].every(cell => cell !== 0)) {
|
||||
// 移除完整的行
|
||||
this.board.splice(row, 1);
|
||||
// 在顶部添加新的空行
|
||||
this.board.unshift(new Array(this.BOARD_WIDTH).fill(0));
|
||||
linesCleared++;
|
||||
row++; // 重新检查当前行
|
||||
}
|
||||
}
|
||||
|
||||
return linesCleared;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!this.gameRunning || this.gamePaused) return;
|
||||
|
||||
this.dropTime += deltaTime;
|
||||
|
||||
if (this.dropTime >= this.dropInterval) {
|
||||
if (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
// 自然下降不加分
|
||||
} else {
|
||||
this.lockPiece();
|
||||
}
|
||||
this.dropTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 绘制游戏板
|
||||
this.drawBoard();
|
||||
|
||||
// 绘制当前方块
|
||||
if (this.currentPiece) {
|
||||
this.drawPiece(this.currentPiece, this.ctx);
|
||||
}
|
||||
|
||||
// 绘制网格
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
drawBoard() {
|
||||
for (let row = 0; row < this.BOARD_HEIGHT; row++) {
|
||||
for (let col = 0; col < this.BOARD_WIDTH; col++) {
|
||||
if (this.board[row][col] !== 0) {
|
||||
this.ctx.fillStyle = this.board[row][col];
|
||||
this.ctx.fillRect(
|
||||
col * this.CELL_SIZE,
|
||||
row * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
this.ctx.strokeStyle = '#333';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.strokeRect(
|
||||
col * this.CELL_SIZE,
|
||||
row * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawPiece(piece, context) {
|
||||
context.fillStyle = piece.color;
|
||||
const matrix = piece.matrix;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
context.fillRect(
|
||||
(piece.x + col) * this.CELL_SIZE,
|
||||
(piece.y + row) * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
context.strokeStyle = '#333';
|
||||
context.lineWidth = 1;
|
||||
context.strokeRect(
|
||||
(piece.x + col) * this.CELL_SIZE,
|
||||
(piece.y + row) * this.CELL_SIZE,
|
||||
this.CELL_SIZE,
|
||||
this.CELL_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawGrid() {
|
||||
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
this.ctx.lineWidth = 1;
|
||||
|
||||
// 垂直线
|
||||
for (let col = 0; col <= this.BOARD_WIDTH; col++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(col * this.CELL_SIZE, 0);
|
||||
this.ctx.lineTo(col * this.CELL_SIZE, this.canvas.height);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
// 水平线
|
||||
for (let row = 0; row <= this.BOARD_HEIGHT; row++) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(0, row * this.CELL_SIZE);
|
||||
this.ctx.lineTo(this.canvas.width, row * this.CELL_SIZE);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
drawNextPiece() {
|
||||
this.nextCtx.clearRect(0, 0, this.nextCanvas.width, this.nextCanvas.height);
|
||||
|
||||
if (this.nextPiece) {
|
||||
const size = 20;
|
||||
const matrix = this.nextPiece.matrix;
|
||||
const offsetX = (this.nextCanvas.width - matrix[0].length * size) / 2;
|
||||
const offsetY = (this.nextCanvas.height - matrix.length * size) / 2;
|
||||
|
||||
this.nextCtx.fillStyle = this.nextPiece.color;
|
||||
|
||||
for (let row = 0; row < matrix.length; row++) {
|
||||
for (let col = 0; col < matrix[row].length; col++) {
|
||||
if (matrix[row][col] !== 0) {
|
||||
this.nextCtx.fillRect(
|
||||
offsetX + col * size,
|
||||
offsetY + row * size,
|
||||
size,
|
||||
size
|
||||
);
|
||||
this.nextCtx.strokeStyle = '#333';
|
||||
this.nextCtx.lineWidth = 1;
|
||||
this.nextCtx.strokeRect(
|
||||
offsetX + col * size,
|
||||
offsetY + row * size,
|
||||
size,
|
||||
size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
document.getElementById('score').textContent = this.score;
|
||||
document.getElementById('level').textContent = this.level;
|
||||
document.getElementById('lines').textContent = this.lines;
|
||||
}
|
||||
|
||||
showOverlay(title, message) {
|
||||
document.getElementById('overlayTitle').textContent = title;
|
||||
document.getElementById('overlayMessage').textContent = message;
|
||||
document.getElementById('gameOverlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
hideOverlay() {
|
||||
document.getElementById('gameOverlay').style.display = 'none';
|
||||
}
|
||||
|
||||
start() {
|
||||
this.gameRunning = true;
|
||||
this.gamePaused = false;
|
||||
this.gameStartTime = Date.now();
|
||||
this.hideOverlay();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (!this.gameRunning) return;
|
||||
|
||||
this.gamePaused = !this.gamePaused;
|
||||
|
||||
if (this.gamePaused) {
|
||||
this.showOverlay('游戏暂停', '按空格键继续游戏');
|
||||
} else {
|
||||
this.hideOverlay();
|
||||
this.gameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.lines = 0;
|
||||
this.dropTime = 0;
|
||||
this.dropInterval = 1000;
|
||||
this.maxCombo = 0;
|
||||
this.currentCombo = 0;
|
||||
|
||||
this.init();
|
||||
this.start();
|
||||
}
|
||||
|
||||
gameOver() {
|
||||
this.gameRunning = false;
|
||||
this.gamePaused = false;
|
||||
|
||||
// 显示游戏统计
|
||||
gameStats.showStats({
|
||||
score: this.score,
|
||||
level: this.level,
|
||||
lines: this.lines,
|
||||
playTime: Date.now() - this.gameStartTime,
|
||||
maxCombo: this.maxCombo
|
||||
});
|
||||
}
|
||||
|
||||
gameLoop(currentTime = 0) {
|
||||
if (!this.gameRunning || this.gamePaused) return;
|
||||
|
||||
const deltaTime = currentTime - this.lastTime;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
this.update(deltaTime);
|
||||
this.draw();
|
||||
|
||||
requestAnimationFrame((time) => this.gameLoop(time));
|
||||
}
|
||||
|
||||
// 移动方法
|
||||
moveLeft() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, -1, 0)) {
|
||||
this.currentPiece.x--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveRight() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, 1, 0)) {
|
||||
this.currentPiece.x++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveDown() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
if (!this.collision(this.currentPiece, 0, 1)) {
|
||||
this.currentPiece.y++;
|
||||
this.score += 1; // 只有主动按下键才给软降分数
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rotatePiece() {
|
||||
if (this.gameRunning && !this.gamePaused && this.currentPiece) {
|
||||
const rotated = this.rotate(this.currentPiece);
|
||||
const originalMatrix = this.currentPiece.matrix;
|
||||
this.currentPiece.matrix = rotated;
|
||||
|
||||
// 检查旋转后是否会碰撞
|
||||
if (this.collision(this.currentPiece)) {
|
||||
// 尝试wall kick
|
||||
const kicks = [
|
||||
[-1, 0], [1, 0], [0, -1], [-2, 0], [2, 0]
|
||||
];
|
||||
|
||||
let canRotate = false;
|
||||
for (let kick of kicks) {
|
||||
if (!this.collision(this.currentPiece, kick[0], kick[1])) {
|
||||
this.currentPiece.x += kick[0];
|
||||
this.currentPiece.y += kick[1];
|
||||
canRotate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!canRotate) {
|
||||
this.currentPiece.matrix = originalMatrix;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建游戏实例
|
||||
const game = new TetrisGame();
|
||||
BIN
InfoGenie-frontend/public/smallgame/别踩白方块/MUSIC.mp3
Normal file
BIN
InfoGenie-frontend/public/smallgame/别踩白方块/MUSIC.mp3
Normal file
Binary file not shown.
339
InfoGenie-frontend/public/smallgame/别踩白方块/game.js
Normal file
339
InfoGenie-frontend/public/smallgame/别踩白方块/game.js
Normal file
@@ -0,0 +1,339 @@
|
||||
var c = document.getElementById("piano");
|
||||
var context = c.getContext("2d");
|
||||
var b = document.getElementById("background");
|
||||
var context_back = b.getContext("2d");
|
||||
var a = document.getElementById("score_bar");
|
||||
var context_score = a.getContext("2d");
|
||||
|
||||
|
||||
var numOfTiles = 5;
|
||||
var myScore = 0;
|
||||
var eachState = [false,false,false,false,false];
|
||||
var myTiles = [];
|
||||
|
||||
var intervalTmp;
|
||||
var geneTmp;
|
||||
var gameSpeed = 1; // 游戏速度倍数,初始为1倍
|
||||
var baseInterval = 5; // 基础更新间隔(毫秒)
|
||||
var baseGenerateInterval = 600; // 基础生成间隔(毫秒)
|
||||
|
||||
paintWindow();
|
||||
paintScoreBar();
|
||||
|
||||
// 添加全局鼠标和触摸事件监听
|
||||
c.addEventListener('click', function(e) {
|
||||
handleClick(e);
|
||||
});
|
||||
|
||||
c.addEventListener('touchstart', function(e) {
|
||||
e.preventDefault();
|
||||
handleTouch(e);
|
||||
});
|
||||
|
||||
document.getElementById('start_btn').addEventListener('click',function(e){
|
||||
var content = document.getElementById('start_btn');
|
||||
if(content.innerHTML == "开始游戏" || content.innerHTML == "继续游戏"){
|
||||
startGame();
|
||||
}
|
||||
else{
|
||||
pauseGame();
|
||||
}
|
||||
});
|
||||
|
||||
// 重新开始按钮事件
|
||||
document.getElementById('restart-btn').addEventListener('click', function(){
|
||||
restartGame();
|
||||
});
|
||||
|
||||
function startGame(){
|
||||
var content = document.getElementById('start_btn');
|
||||
updateGameSpeed();
|
||||
document.getElementById('music').play();
|
||||
content.innerHTML = "暂停游戏";
|
||||
content.className = "game-btn pause-btn";
|
||||
}
|
||||
|
||||
// 更新游戏速度
|
||||
function updateGameSpeed() {
|
||||
// 清除现有定时器
|
||||
if (intervalTmp) clearInterval(intervalTmp);
|
||||
if (geneTmp) clearInterval(geneTmp);
|
||||
|
||||
// 保持正常1倍速度,不加速
|
||||
gameSpeed = 1;
|
||||
|
||||
// 设置新的定时器,优化性能
|
||||
intervalTmp = setInterval(upDate, Math.max(baseInterval / gameSpeed, 3));
|
||||
geneTmp = setInterval(geneBlock, Math.max(baseGenerateInterval / gameSpeed, 200));
|
||||
}
|
||||
|
||||
function pauseGame(){
|
||||
var content = document.getElementById('start_btn');
|
||||
document.getElementById('music').pause();
|
||||
window.clearInterval(intervalTmp);
|
||||
window.clearInterval(geneTmp);
|
||||
content.innerHTML = "继续游戏";
|
||||
content.className = "game-btn start-btn";
|
||||
}
|
||||
|
||||
function gameOver(){
|
||||
document.getElementById('music').pause();
|
||||
window.clearInterval(intervalTmp);
|
||||
window.clearInterval(geneTmp);
|
||||
|
||||
// 显示最终得分和达到的最高速度
|
||||
document.getElementById('final-score-value').innerHTML = myScore;
|
||||
document.getElementById('final-speed-value').innerHTML = gameSpeed.toFixed(1);
|
||||
|
||||
// 显示游戏结束弹窗
|
||||
document.getElementById('game-over-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function restartGame(){
|
||||
// 重置游戏状态
|
||||
myScore = 0;
|
||||
gameSpeed = 1; // 重置游戏速度
|
||||
eachState = [false,false,false,false,false];
|
||||
myTiles = [];
|
||||
|
||||
// 清空画布
|
||||
context.clearRect(0,0,300,600);
|
||||
context_back.clearRect(0,0,300,600);
|
||||
context_score.clearRect(0,0,300,60);
|
||||
|
||||
// 重新绘制背景
|
||||
paintWindow();
|
||||
paintScoreBar();
|
||||
|
||||
// 隐藏弹窗
|
||||
document.getElementById('game-over-modal').style.display = 'none';
|
||||
|
||||
// 重置按钮状态
|
||||
var content = document.getElementById('start_btn');
|
||||
content.innerHTML = "开始游戏";
|
||||
content.className = "game-btn start-btn";
|
||||
}
|
||||
function paintScoreBar(){
|
||||
// 清空画布
|
||||
context_score.clearRect(0,0,300,60);
|
||||
|
||||
// 绘制黑色背景
|
||||
context_score.fillStyle = "#333";
|
||||
context_score.fillRect(0,0,300,60);
|
||||
|
||||
// 更新HTML显示
|
||||
document.getElementById('score-value').textContent = myScore;
|
||||
document.getElementById('speed-value').textContent = gameSpeed.toFixed(1);
|
||||
}
|
||||
function geneBlock(){
|
||||
var myRand = Math.floor(Math.random()*numOfTiles);
|
||||
var i;
|
||||
var flag = true;
|
||||
for( i = 0; i < numOfTiles; ++i){
|
||||
if(!eachState[i]){
|
||||
flag = false;
|
||||
}
|
||||
}
|
||||
if(flag)return;//if mytiles array didn't have false element, then return
|
||||
|
||||
while(eachState[myRand])
|
||||
myRand = Math.floor(Math.random()*numOfTiles);
|
||||
myTiles[myRand] = new Block(myRand);
|
||||
|
||||
}
|
||||
function paintWindow(){
|
||||
// 清空背景
|
||||
context_back.clearRect(0,0,300,600);
|
||||
|
||||
// 绘制白色背景
|
||||
context_back.fillStyle = "white";
|
||||
context_back.fillRect(0,0,300,600);
|
||||
|
||||
// 绘制分隔线
|
||||
context_back.strokeStyle = "#ddd";
|
||||
context_back.lineWidth = 2;
|
||||
|
||||
// 垂直分隔线
|
||||
context_back.beginPath();
|
||||
context_back.moveTo(75,0);
|
||||
context_back.lineTo(75,600);
|
||||
context_back.stroke();
|
||||
|
||||
context_back.beginPath();
|
||||
context_back.moveTo(150,0);
|
||||
context_back.lineTo(150,600);
|
||||
context_back.stroke();
|
||||
|
||||
context_back.beginPath();
|
||||
context_back.moveTo(225,0);
|
||||
context_back.lineTo(225,600);
|
||||
context_back.stroke();
|
||||
|
||||
// 可点击区域警戒线
|
||||
context_back.strokeStyle = "#ff4444";
|
||||
context_back.lineWidth = 3;
|
||||
context_back.beginPath();
|
||||
context_back.moveTo(0,250);
|
||||
context_back.lineTo(300,250);
|
||||
context_back.stroke();
|
||||
|
||||
// 底部失败线
|
||||
context_back.strokeStyle = "#ff4444";
|
||||
context_back.lineWidth = 3;
|
||||
context_back.beginPath();
|
||||
context_back.moveTo(0,470);
|
||||
context_back.lineTo(300,470);
|
||||
context_back.stroke();
|
||||
}
|
||||
function Block(index){
|
||||
if(!eachState[index])
|
||||
eachState[index] = true;
|
||||
|
||||
this.index = index;
|
||||
this.appearPos = Math.floor(Math.random()*4);
|
||||
|
||||
this.width = 75;
|
||||
this.height = 120;
|
||||
this.color = "black";
|
||||
switch(this.appearPos){
|
||||
case 0:
|
||||
this.x = 0;
|
||||
this.y = -120;
|
||||
break;
|
||||
case 1:
|
||||
this.x = 75;
|
||||
this.y = -120;
|
||||
break;
|
||||
case 2:
|
||||
this.x = 150;
|
||||
this.y = -120;
|
||||
break;
|
||||
case 3:
|
||||
this.x = 225;
|
||||
this.y = -120;
|
||||
break;
|
||||
}
|
||||
context.fillStyle = this.color;
|
||||
context.fillRect(this.x,this.y,this.width,this.height);
|
||||
this.live = true;
|
||||
|
||||
window.addEventListener('keydown',function(e){
|
||||
myTiles[index].keyCode = e.keyCode;
|
||||
});
|
||||
window.addEventListener('keyup',function(e){
|
||||
myTiles[index].keyCode = false;
|
||||
});
|
||||
}
|
||||
function move(index){
|
||||
if(myTiles[index].live){
|
||||
myTiles[index].y += Math.ceil(gameSpeed);
|
||||
// 绘制逻辑已移到upDate函数中,避免重复绘制
|
||||
}
|
||||
}
|
||||
function afterRight(index){
|
||||
myScore++;
|
||||
// 清除方块(在upDate中统一处理绘制)
|
||||
myTiles[index].live = false;
|
||||
eachState[index] = false;
|
||||
|
||||
// 立即更新得分显示
|
||||
paintScoreBar();
|
||||
|
||||
// 每次得分都更新游戏速度,实现平滑渐进加速
|
||||
updateGameSpeed();
|
||||
}
|
||||
// 处理鼠标点击事件
|
||||
function handleClick(e) {
|
||||
var rect = c.getBoundingClientRect();
|
||||
var x = e.clientX - rect.left;
|
||||
var y = e.clientY - rect.top;
|
||||
|
||||
checkHit(x, y);
|
||||
}
|
||||
|
||||
// 处理触摸事件
|
||||
function handleTouch(e) {
|
||||
var rect = c.getBoundingClientRect();
|
||||
var touch = e.touches[0];
|
||||
var x = touch.clientX - rect.left;
|
||||
var y = touch.clientY - rect.top;
|
||||
|
||||
checkHit(x, y);
|
||||
}
|
||||
|
||||
// 检查点击/触摸是否命中方块
|
||||
function checkHit(x, y) {
|
||||
// 检查是否点击到黑色方块
|
||||
for (var i = 0; i < numOfTiles; i++) {
|
||||
if (eachState[i] && myTiles[i].live) {
|
||||
// 检查点击位置是否在方块范围内
|
||||
if (x >= myTiles[i].x && x <= myTiles[i].x + 75 &&
|
||||
y >= myTiles[i].y && y <= myTiles[i].y + 120) {
|
||||
// 检查方块是否在可点击区域(提高到130像素以上)
|
||||
if (myTiles[i].y + 120 > 130) {
|
||||
afterRight(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有点击到任何黑色方块,且点击位置在游戏区域内,则游戏结束
|
||||
if (y > 130 && y < 600) {
|
||||
gameOver();
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function upDate(){//check keyCode whether correct
|
||||
var i;
|
||||
|
||||
// 清空整个游戏区域,避免重叠
|
||||
context.clearRect(0, 0, 300, 600);
|
||||
|
||||
// 移动并重绘所有活跃的方块
|
||||
for(i = 0; i < numOfTiles; ++i){
|
||||
if(eachState[i]){
|
||||
myTiles[i].y += Math.ceil(gameSpeed); // 使用整数移动,避免模糊
|
||||
context.fillStyle = "black";
|
||||
context.fillRect(myTiles[i].x, myTiles[i].y, 75, 120);
|
||||
}
|
||||
}
|
||||
for(i = 0; i < numOfTiles; ++i){
|
||||
if( eachState[i] ){
|
||||
if(myTiles[i].y < 470 && myTiles[i].y >350){
|
||||
switch(myTiles[i].keyCode){
|
||||
case 65: //A
|
||||
if(myTiles[i].x == 0)
|
||||
afterRight(i);
|
||||
break;
|
||||
case 83: //S
|
||||
if(myTiles[i].x == 75)
|
||||
afterRight(i);
|
||||
break;
|
||||
case 68: //D
|
||||
if(myTiles[i].x == 150)
|
||||
afterRight(i);
|
||||
break;
|
||||
case 70: //F
|
||||
if(myTiles[i].x == 225)
|
||||
afterRight(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(myTiles[i].y > 470){
|
||||
// 方块到达底部,游戏结束
|
||||
myTiles[i].live = false;
|
||||
eachState[i] = false;
|
||||
gameOver();
|
||||
return; // 立即退出,避免继续处理
|
||||
}
|
||||
}
|
||||
else{//not alive
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
251
InfoGenie-frontend/public/smallgame/别踩白方块/index.html
Normal file
251
InfoGenie-frontend/public/smallgame/别踩白方块/index.html
Normal file
@@ -0,0 +1,251 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>别踩白方块</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f5f5f5;
|
||||
font-family: 'Arial', 'Microsoft YaHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.game-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 60px;
|
||||
background: #333;
|
||||
border-radius: 8px 8px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 600px;
|
||||
border: 3px solid #333;
|
||||
border-top: none;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.game-btn {
|
||||
padding: 12px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
background: #45a049;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.pause-btn {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pause-btn:hover {
|
||||
background: #e68900;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 游戏结束弹窗 */
|
||||
.game-over-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
max-width: 300px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.final-score, .final-speed {
|
||||
font-size: 18px;
|
||||
margin: 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.final-speed {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
padding: 10px 25px;
|
||||
margin: 5px;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.game-wrapper {
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
width: 280px;
|
||||
height: 560px;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.game-container {
|
||||
width: 260px;
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-wrapper">
|
||||
<h1 class="game-title">别踩白方块</h1>
|
||||
|
||||
<div class="score-display">
|
||||
<canvas id="score_bar" width="300" height="60"></canvas>
|
||||
<div style="position: absolute; color: white; font-size: 14px; display: flex; justify-content: space-between; width: 260px; padding: 0 20px;">
|
||||
<span>得分: <span id="score-value">0</span></span>
|
||||
<span>速度: <span id="speed-value">1.0</span>x</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-container">
|
||||
<canvas id="background" width="300" height="600"></canvas>
|
||||
<canvas id="piano" width="300" height="600" style="position: absolute; top: 0; left: 0;"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="control-panel">
|
||||
<button id="start_btn" class="game-btn start-btn">开始游戏</button>
|
||||
<div class="instructions">
|
||||
<div>电脑端:使用 A S D F 键</div>
|
||||
<div>手机端:直接点击黑色方块</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 游戏结束弹窗 -->
|
||||
<div id="game-over-modal" class="game-over-modal">
|
||||
<div class="modal-content">
|
||||
<h2 class="modal-title">游戏结束</h2>
|
||||
<div class="final-score">最终得分: <span id="final-score-value">0</span></div>
|
||||
<div class="final-speed">最高速度: <span id="final-speed-value">1.0</span>x</div>
|
||||
<button id="restart-btn" class="modal-btn restart-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<audio id="music" src="MUSIC.mp3" loop></audio>
|
||||
<script src="game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
79
InfoGenie-frontend/public/smallgame/扫雷/css/style.css
Normal file
79
InfoGenie-frontend/public/smallgame/扫雷/css/style.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* 经典扫雷 - 手机竖屏优先 + 电脑端适配 */
|
||||
:root{
|
||||
--bg:#0f172a;
|
||||
--panel:#111827;
|
||||
--accent:#22d3ee;
|
||||
--accent-2:#60a5fa;
|
||||
--text:#e5e7eb;
|
||||
--muted:#94a3b8;
|
||||
--danger:#ef4444;
|
||||
--success:#22c55e;
|
||||
--warn:#f59e0b;
|
||||
--cell:#1f2937;
|
||||
--cell-hover:#273244;
|
||||
--flag:#fb7185;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%;}
|
||||
body{margin:0;background:linear-gradient(180deg,#0b1220,#0f172a);color:var(--text);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"PingFang SC","Microsoft Yahei",sans-serif;-webkit-tap-highlight-color:transparent}
|
||||
|
||||
.app{min-height:100dvh;display:flex;flex-direction:column;gap:12px;padding:12px;}
|
||||
.header{display:flex;flex-direction:column;gap:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:14px;padding:12px 12px 10px;backdrop-filter:blur(6px)}
|
||||
.title{margin:0;font-size:20px;letter-spacing:1px}
|
||||
.hud{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;align-items:center}
|
||||
.hud-item{display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:8px 6px}
|
||||
.hud-item .label{font-size:12px;color:var(--muted)}
|
||||
.hud-item .value{font-size:18px;font-weight:700;color:#fff}
|
||||
.btn{appearance:none;border:none;background:#1e293b;color:#fff;padding:10px 12px;border-radius:10px;cursor:pointer;outline:none;transition:.15s transform,.15s background;display:inline-flex;align-items:center;justify-content:center}
|
||||
.btn:active{transform:scale(.98)}
|
||||
.btn.primary{background:linear-gradient(90deg,var(--accent),var(--accent-2))}
|
||||
.btn.primary:active{filter:brightness(.95)}
|
||||
|
||||
.main{display:flex;flex-direction:column;gap:12px}
|
||||
.board-wrapper{display:flex;justify-content:center;align-items:center}
|
||||
.board{display:grid;gap:4px;touch-action:manipulation;user-select:none;background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:6px;width:100%;max-width:92vw}
|
||||
.cell{display:grid;place-items:center;background:var(--cell);border-radius:8px;border:1px solid rgba(255,255,255,0.06);font-weight:700;color:#9ca3af;box-shadow:inset 0 -1px 0 rgba(255,255,255,0.04);aspect-ratio:1/1;font-size:clamp(12px, 2.2vw, 18px)}
|
||||
.cell.revealed{background:#0b1220;color:#e5e7eb}
|
||||
.cell:hover{background:var(--cell-hover)}
|
||||
.cell.flag::after{content:"🚩"}
|
||||
.cell.mine.revealed{background:#3b0d0d;color:#fff}
|
||||
.cell.mine.revealed::after{content:"💣"}
|
||||
.cell[data-n="1"].revealed{color:#60a5fa}
|
||||
.cell[data-n="2"].revealed{color:#34d399}
|
||||
.cell[data-n="3"].revealed{color:#f87171}
|
||||
.cell[data-n="4"].revealed{color:#a78bfa}
|
||||
.cell[data-n="5"].revealed{color:#fbbf24}
|
||||
.cell[data-n="6"].revealed{color:#22d3ee}
|
||||
.cell[data-n="7"].revealed{color:#e879f9}
|
||||
.cell[data-n="8"].revealed{color:#cbd5e1}
|
||||
|
||||
.tips{font-size:12px;color:var(--muted);text-align:center}
|
||||
.toast{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);background:rgba(17,24,39,.95);border:1px solid rgba(255,255,255,.08);padding:10px 14px;border-radius:10px}
|
||||
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:grid;place-items:center;padding:14px}
|
||||
.modal{width:min(520px,92vw);background:linear-gradient(180deg,#0f172a,#0b1320);border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:16px 14px}
|
||||
.modal h2{margin:4px 0 8px;font-size:20px}
|
||||
.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:8px 0 14px}
|
||||
.stats .card{background:var(--panel);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:10px}
|
||||
.stats .card .k{font-size:12px;color:var(--muted)}
|
||||
.stats .card .v{font-size:18px;font-weight:700}
|
||||
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
|
||||
|
||||
/* 响应式:手机竖屏优先 */
|
||||
@media (min-width: 480px){
|
||||
.title{font-size:22px}
|
||||
}
|
||||
@media (min-width: 640px){
|
||||
.app{padding:18px}
|
||||
.hud{grid-template-columns:repeat(5,minmax(0,1fr))}
|
||||
}
|
||||
@media (min-width: 1024px){
|
||||
.board{ max-width: 420px; }
|
||||
.header{ padding:10px 10px 8px; }
|
||||
.hud-item{ padding:6px 4px; }
|
||||
.hud-item .value{ font-size:16px; }
|
||||
.title{ font-size:18px; }
|
||||
}
|
||||
@media (orientation: landscape) and (max-width: 900px){
|
||||
.board{transform:scale(.9)}
|
||||
}
|
||||
56
InfoGenie-frontend/public/smallgame/扫雷/index.html
Normal file
56
InfoGenie-frontend/public/smallgame/扫雷/index.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="theme-color" content="#2c3e50" />
|
||||
<title>经典扫雷(手机竖屏适配)</title>
|
||||
<meta name="description" content="经典Windows扫雷,手机竖屏与电脑端自适应,支持触摸与键盘操作,闯关无尽模式,难度逐步增加。" />
|
||||
|
||||
<link rel="preload" href="./css/style.css" as="style" />
|
||||
<link rel="stylesheet" href="./css/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<header class="header">
|
||||
<h1 class="title">经典扫雷</h1>
|
||||
<div class="hud">
|
||||
<div class="hud-item"><span class="label">关卡</span><span id="level" class="value">1</span></div>
|
||||
<div class="hud-item"><span class="label">雷数</span><span id="mines" class="value">0</span></div>
|
||||
<div class="hud-item"><span class="label">计时</span><span id="timer" class="value">00:00</span></div>
|
||||
<button id="btn-restart" class="btn primary" aria-label="重新开始">重开</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main">
|
||||
<section class="board-wrapper">
|
||||
<div id="board" class="board" role="grid" aria-label="扫雷棋盘"></div>
|
||||
</section>
|
||||
|
||||
<section class="tips">
|
||||
<p>
|
||||
手机:点开格子,长按插旗;电脑:左键开格,右键插旗;键盘:方向键移动,空格/回车开格,F 插旗。
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 关卡完成提示 -->
|
||||
<div id="toast-level" class="toast" aria-live="polite" style="display:none;"></div>
|
||||
|
||||
<!-- 结束统计弹窗 -->
|
||||
<div id="modal-overlay" class="modal-overlay" style="display:none;">
|
||||
<div class="modal" role="dialog" aria-labelledby="gameover-title" aria-modal="true">
|
||||
<h2 id="gameover-title">游戏结束</h2>
|
||||
<div id="stats" class="stats"></div>
|
||||
<div class="modal-actions">
|
||||
<button id="btn-retry" class="btn primary">重新开始</button>
|
||||
<button id="btn-close" class="btn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="./js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
273
InfoGenie-frontend/public/smallgame/扫雷/js/main.js
Normal file
273
InfoGenie-frontend/public/smallgame/扫雷/js/main.js
Normal file
@@ -0,0 +1,273 @@
|
||||
// 经典扫雷(手机竖屏适配 + 无尽模式 + 键盘操作)
|
||||
// 模块:状态、生成、交互、键盘、统计
|
||||
|
||||
class RNG {
|
||||
constructor(seed = Date.now()) { this.seed = seed >>> 0; }
|
||||
next() { // xorshift32
|
||||
let x = this.seed;
|
||||
x ^= x << 13; x ^= x >>> 17; x ^= x << 5;
|
||||
this.seed = x >>> 0; return this.seed / 0xffffffff;
|
||||
}
|
||||
range(min, max){ return Math.floor(this.next() * (max - min + 1)) + min; }
|
||||
}
|
||||
|
||||
const GameConfig = {
|
||||
start: { rows: 10, cols: 8, mineRatio: 0.12 }, // 竖屏优先:更多行
|
||||
levelStep(cfg){
|
||||
// 难度递增:逐步增加行列与雷密度,控制在移动端也能点击
|
||||
const next = { ...cfg };
|
||||
if (next.rows < 16) next.rows++;
|
||||
if (next.cols < 12) next.cols += (next.rows % 2 === 0 ? 1 : 0);
|
||||
next.mineRatio = Math.min(0.24, +(next.mineRatio + 0.02).toFixed(2));
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
const State = {
|
||||
level: 1,
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
mineCount: 0,
|
||||
revealed: 0,
|
||||
flags: 0,
|
||||
grid: [], // { mine, r, c, around, revealed, flag }
|
||||
timer: 0,
|
||||
timerId: null,
|
||||
startTs: 0,
|
||||
rng: new RNG(),
|
||||
stats: { opened:0, flagged:0, mistakes:0, time:0 }
|
||||
};
|
||||
|
||||
function el(sel){ return document.querySelector(sel); }
|
||||
function make(tag, cls){ const e = document.createElement(tag); if(cls) e.className = cls; return e; }
|
||||
function updateMinesHud(){ el('#mines').textContent = String(Math.max(0, State.mineCount - State.flags)); }
|
||||
|
||||
function startTimer(){
|
||||
State.startTs = Date.now();
|
||||
const timerEl = el('#timer');
|
||||
clearInterval(State.timerId);
|
||||
State.timerId = setInterval(() => {
|
||||
State.timer = Math.floor((Date.now() - State.startTs)/1000);
|
||||
const m = String(Math.floor(State.timer/60)).padStart(2,'0');
|
||||
const s = String(State.timer%60).padStart(2,'0');
|
||||
timerEl.textContent = `${m}:${s}`;
|
||||
}, 250);
|
||||
}
|
||||
function stopTimer(){ clearInterval(State.timerId); State.timerId = null; }
|
||||
|
||||
function setupBoard(cfg){
|
||||
State.rows = cfg.rows; State.cols = cfg.cols;
|
||||
const total = cfg.rows * cfg.cols;
|
||||
State.mineCount = Math.max(1, Math.floor(total * cfg.mineRatio));
|
||||
State.revealed = 0; State.flags = 0; State.grid = [];
|
||||
State.stats = { opened:0, flagged:0, mistakes:0, time:0 };
|
||||
|
||||
// 更新HUD
|
||||
el('#level').textContent = String(State.level);
|
||||
updateMinesHud();
|
||||
|
||||
const board = el('#board');
|
||||
board.innerHTML = '';
|
||||
board.style.gridTemplateColumns = `repeat(${State.cols}, 1fr)`;
|
||||
|
||||
// 生成空格子
|
||||
for(let r=0;r<State.rows;r++){
|
||||
State.grid[r] = [];
|
||||
for(let c=0;c<State.cols;c++){
|
||||
const cell = { mine:false, r, c, around:0, revealed:false, flag:false, el: null };
|
||||
const div = make('button','cell');
|
||||
div.type = 'button';
|
||||
div.setAttribute('role','gridcell');
|
||||
div.setAttribute('aria-label', `r${r} c${c}`);
|
||||
div.addEventListener('contextmenu', e=> e.preventDefault());
|
||||
// 触摸长按
|
||||
let pressTimer = null; let longPressed=false;
|
||||
div.addEventListener('touchstart', e => {
|
||||
longPressed=false;
|
||||
pressTimer = setTimeout(()=>{ longPressed=true; toggleFlag(cell); }, 420);
|
||||
}, {passive:true});
|
||||
div.addEventListener('touchend', e => { if(pressTimer){ clearTimeout(pressTimer); if(!longPressed) openCell(cell); } });
|
||||
// 鼠标
|
||||
div.addEventListener('mousedown', e => {
|
||||
if(e.button===2){ toggleFlag(cell); }
|
||||
else if(e.button===0){ if(cell.revealed && cell.around>0) chord(cell); else openCell(cell); }
|
||||
});
|
||||
cell.el = div;
|
||||
State.grid[r][c] = cell;
|
||||
board.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
// 随机埋雷
|
||||
let placed=0;
|
||||
const setMine = (r,c)=>{ if(!State.grid[r][c].mine){ State.grid[r][c].mine=true; placed++; } };
|
||||
while(placed < State.mineCount){ setMine(State.rng.range(0,State.rows-1), State.rng.range(0,State.cols-1)); }
|
||||
recomputeArounds();
|
||||
// 启动计时
|
||||
startTimer();
|
||||
}
|
||||
|
||||
function visitNeighbors(r,c, cb){
|
||||
for(let dr=-1; dr<=1; dr++){
|
||||
for(let dc=-1; dc<=1; dc++){
|
||||
if(dr===0 && dc===0) continue;
|
||||
const nr=r+dr, nc=c+dc;
|
||||
if(nr>=0 && nr<State.rows && nc>=0 && nc<State.cols) cb(nr,nc);
|
||||
}
|
||||
}
|
||||
}
|
||||
function countFlagsAround(r,c){ let n=0; visitNeighbors(r,c,(nr,nc)=>{ if(State.grid[nr][nc].flag) n++; }); return n; }
|
||||
function chord(cell){
|
||||
if(!cell.revealed || cell.around<=0) return;
|
||||
const flagged = countFlagsAround(cell.r, cell.c);
|
||||
if(flagged === cell.around){
|
||||
visitNeighbors(cell.r, cell.c, (nr,nc)=>{
|
||||
const ncell = State.grid[nr][nc];
|
||||
if(!ncell.revealed && !ncell.flag){ openCell(ncell); }
|
||||
});
|
||||
}
|
||||
}
|
||||
function recomputeArounds(){
|
||||
for(let r=0;r<State.rows;r++){
|
||||
for(let c=0;c<State.cols;c++){
|
||||
if(State.grid[r][c].mine){ State.grid[r][c].around = 0; continue; }
|
||||
let n=0; visitNeighbors(r,c,(nr,nc)=>{ if(State.grid[nr][nc].mine) n++; });
|
||||
State.grid[r][c].around = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
function safeFirstClick(badCell){
|
||||
// 移除当前雷并将其放到其他非雷位置
|
||||
badCell.mine = false;
|
||||
while(true){
|
||||
const r = State.rng.range(0, State.rows-1);
|
||||
const c = State.rng.range(0, State.cols-1);
|
||||
const target = State.grid[r][c];
|
||||
if(target!==badCell && !target.mine){ target.mine = true; break; }
|
||||
}
|
||||
recomputeArounds();
|
||||
}
|
||||
|
||||
function openCell(cell){
|
||||
if(cell.revealed || cell.flag) return;
|
||||
// 首次点击必定安全:若第一次就点到雷,则移动该雷并重算数字
|
||||
if(State.revealed===0 && cell.mine){
|
||||
safeFirstClick(cell);
|
||||
}
|
||||
cell.revealed = true; State.revealed++; State.stats.opened++;
|
||||
cell.el.classList.add('revealed');
|
||||
if(cell.mine){
|
||||
cell.el.classList.add('mine');
|
||||
endGame(false);
|
||||
return;
|
||||
}
|
||||
if(cell.around>0){ cell.el.dataset.n = cell.around; cell.el.textContent = cell.around; }
|
||||
else{
|
||||
// flood fill
|
||||
visitNeighbors(cell.r, cell.c, (nr,nc)=>{
|
||||
const ncell = State.grid[nr][nc];
|
||||
if(!ncell.revealed && !ncell.mine) openCell(ncell);
|
||||
});
|
||||
}
|
||||
checkWin();
|
||||
}
|
||||
|
||||
function toggleFlag(cell){
|
||||
if(cell.revealed) return;
|
||||
cell.flag = !cell.flag;
|
||||
State.flags += cell.flag ? 1 : -1;
|
||||
State.stats.flagged += cell.flag ? 1 : 0;
|
||||
cell.el.classList.toggle('flag', cell.flag);
|
||||
updateMinesHud();
|
||||
}
|
||||
|
||||
function checkWin(){
|
||||
const totalSafe = State.rows*State.cols - State.mineCount;
|
||||
if(State.revealed >= totalSafe){
|
||||
// 通关 -> 进入下一关
|
||||
showToast(`第 ${State.level} 关完成!`);
|
||||
stopTimer();
|
||||
setTimeout(()=>{
|
||||
State.level++;
|
||||
const nextCfg = GameConfig.levelStep({ rows: State.rows, cols: State.cols, mineRatio: State.mineCount/(State.rows*State.cols) });
|
||||
setupBoard(nextCfg);
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(msg){
|
||||
const t = el('#toast-level');
|
||||
t.textContent = msg; t.style.display='block';
|
||||
t.animate([
|
||||
{ transform:'translate(-50%, 20px)', opacity:0 },
|
||||
{ transform:'translate(-50%, 0)', opacity:1, offset:.2 },
|
||||
{ transform:'translate(-50%, 0)', opacity:1, offset:.8 },
|
||||
{ transform:'translate(-50%, 10px)', opacity:0 }
|
||||
], { duration:1200, easing:'ease' }).onfinish = ()=> t.style.display='none';
|
||||
}
|
||||
|
||||
function endGame(win){
|
||||
stopTimer();
|
||||
// 展示所有雷
|
||||
for(let r=0;r<State.rows;r++){
|
||||
for(let c=0;c<State.cols;c++){
|
||||
const cell = State.grid[r][c];
|
||||
if(cell.mine){ cell.el.classList.add('revealed','mine'); }
|
||||
}
|
||||
}
|
||||
State.stats.time = State.timer;
|
||||
const statsHtml = `
|
||||
<div class="stats">
|
||||
<div class="card"><div class="k">关卡</div><div class="v">${State.level}</div></div>
|
||||
<div class="card"><div class="k">总用时</div><div class="v">${formatTime(State.timer)}</div></div>
|
||||
<div class="card"><div class="k">开格</div><div class="v">${State.stats.opened}</div></div>
|
||||
<div class="card"><div class="k">插旗</div><div class="v">${State.stats.flagged}</div></div>
|
||||
</div>
|
||||
<p style="color:#94a3b8;margin:6px 0 10px">再接再厉,挑战更高难度!</p>
|
||||
`;
|
||||
el('#stats').innerHTML = statsHtml;
|
||||
el('#modal-overlay').style.display = 'grid';
|
||||
}
|
||||
|
||||
function formatTime(sec){ const m=Math.floor(sec/60), s=sec%60; return `${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; }
|
||||
|
||||
function bindUI(){
|
||||
el('#btn-restart').addEventListener('click', ()=> restart());
|
||||
el('#btn-retry').addEventListener('click', ()=> restart());
|
||||
el('#btn-close').addEventListener('click', ()=>{ el('#modal-overlay').style.display='none'; });
|
||||
}
|
||||
|
||||
function restart(){
|
||||
el('#modal-overlay').style.display='none';
|
||||
State.level = 1; setupBoard(GameConfig.start);
|
||||
}
|
||||
|
||||
// 键盘操作(电脑端)
|
||||
let kb = { r:0, c:0 };
|
||||
function bindKeyboard(){
|
||||
document.addEventListener('keydown', (e)=>{
|
||||
const key = e.key.toLowerCase();
|
||||
if(['arrowup','w'].includes(key)) move(-1,0);
|
||||
else if(['arrowdown','s'].includes(key)) move(1,0);
|
||||
else if(['arrowleft','a'].includes(key)) move(0,-1);
|
||||
else if(['arrowright','d'].includes(key)) move(0,1);
|
||||
else if(key==='f'){ toggleFlag(State.grid[kb.r][kb.c]); highlightFocus(); }
|
||||
else if(key===' ' || key==='enter'){ openCell(State.grid[kb.r][kb.c]); highlightFocus(); }
|
||||
});
|
||||
}
|
||||
function move(dr,dc){ kb.r = clamp(kb.r+dr,0,State.rows-1); kb.c = clamp(kb.c+dc,0,State.cols-1); highlightFocus(); }
|
||||
function clamp(v,min,max){ return Math.max(min, Math.min(max, v)); }
|
||||
function highlightFocus(){
|
||||
// 简单高亮当前聚焦格
|
||||
for(let r=0;r<State.rows;r++){
|
||||
for(let c=0;c<State.cols;c++){
|
||||
const el = State.grid[r][c].el;
|
||||
el.style.outline = (r===kb.r && c===kb.c) ? '2px solid rgba(96,165,250,.9)' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
bindUI(); bindKeyboard();
|
||||
setupBoard(GameConfig.start);
|
||||
highlightFocus();
|
||||
5
InfoGenie-frontend/public/smallgame/生成要求.txt
Normal file
5
InfoGenie-frontend/public/smallgame/生成要求.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
1.把js,css,html分开储存,每个功能单独分成每个模块,避免单个文件过大问题
|
||||
2.网页适配手机端和电脑端,优先优化手机竖屏游玩体验,所有游戏都是手机竖屏游戏
|
||||
3.游戏都是无尽模式,玩到后期越来越困难,游戏玩法尽可能丰富多样
|
||||
4.电脑端可以有键盘快捷键操作
|
||||
5.最后结束游戏要统计显示玩家获得的最终游戏数据,给玩家成就感
|
||||
130
InfoGenie-frontend/public/smallgame/贪吃蛇/game-controls.js
Normal file
130
InfoGenie-frontend/public/smallgame/贪吃蛇/game-controls.js
Normal file
@@ -0,0 +1,130 @@
|
||||
class GameControls {
|
||||
constructor(game) {
|
||||
this.game = game;
|
||||
this.initControls();
|
||||
this.initTouchControls();
|
||||
}
|
||||
|
||||
initControls() {
|
||||
// 方向按钮控制
|
||||
document.getElementById('upBtn').addEventListener('click', () => {
|
||||
this.game.changeDirection(0, -1);
|
||||
});
|
||||
|
||||
document.getElementById('downBtn').addEventListener('click', () => {
|
||||
this.game.changeDirection(0, 1);
|
||||
});
|
||||
|
||||
document.getElementById('leftBtn').addEventListener('click', () => {
|
||||
this.game.changeDirection(-1, 0);
|
||||
});
|
||||
|
||||
document.getElementById('rightBtn').addEventListener('click', () => {
|
||||
this.game.changeDirection(1, 0);
|
||||
});
|
||||
|
||||
// 暂停/继续按钮
|
||||
document.getElementById('pauseBtn').addEventListener('click', () => {
|
||||
this.game.togglePause();
|
||||
});
|
||||
|
||||
// 重新开始按钮
|
||||
document.getElementById('restartBtn').addEventListener('click', () => {
|
||||
this.game.restart();
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
this.initKeyboardShortcuts();
|
||||
}
|
||||
|
||||
initKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
switch(e.key) {
|
||||
case 'r':
|
||||
case 'R':
|
||||
if (this.game.gameOver) {
|
||||
this.game.restart();
|
||||
}
|
||||
break;
|
||||
case 'p':
|
||||
case 'P':
|
||||
this.game.togglePause();
|
||||
break;
|
||||
case 'Escape':
|
||||
if (this.game.gameOver) {
|
||||
document.getElementById('gameOverModal').style.display = 'none';
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initTouchControls() {
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
|
||||
canvas.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', (e) => {
|
||||
const touchEndX = e.changedTouches[0].clientX;
|
||||
const touchEndY = e.changedTouches[0].clientY;
|
||||
|
||||
const deltaX = touchEndX - touchStartX;
|
||||
const deltaY = touchEndY - touchStartY;
|
||||
const minSwipeDistance = 30;
|
||||
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||
// 水平滑动
|
||||
if (Math.abs(deltaX) > minSwipeDistance) {
|
||||
if (deltaX > 0) {
|
||||
this.game.changeDirection(1, 0); // 右滑
|
||||
} else {
|
||||
this.game.changeDirection(-1, 0); // 左滑
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 垂直滑动
|
||||
if (Math.abs(deltaY) > minSwipeDistance) {
|
||||
if (deltaY > 0) {
|
||||
this.game.changeDirection(0, 1); // 下滑
|
||||
} else {
|
||||
this.game.changeDirection(0, -1); // 上滑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
// 防止移动端页面滚动
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
if (e.target.closest('.game-container')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// 震动反馈(移动端)
|
||||
vibrate(duration = 50) {
|
||||
if ('vibrate' in navigator) {
|
||||
navigator.vibrate(duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化控制模块
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 等待游戏实例创建后初始化控制
|
||||
setTimeout(() => {
|
||||
new GameControls(game);
|
||||
}, 100);
|
||||
});
|
||||
366
InfoGenie-frontend/public/smallgame/贪吃蛇/game-core.js
Normal file
366
InfoGenie-frontend/public/smallgame/贪吃蛇/game-core.js
Normal file
@@ -0,0 +1,366 @@
|
||||
class SnakeGame {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('gameCanvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
|
||||
// 游戏配置
|
||||
this.gridSize = 20;
|
||||
this.tileCount = this.canvas.width / this.gridSize;
|
||||
this.tileCountY = this.canvas.height / this.gridSize;
|
||||
|
||||
// 蛇的初始状态
|
||||
this.snake = [
|
||||
{x: 10, y: 10},
|
||||
{x: 9, y: 10},
|
||||
{x: 8, y: 10}
|
||||
];
|
||||
|
||||
// 食物位置
|
||||
this.food = {x: 15, y: 15};
|
||||
|
||||
// 游戏状态
|
||||
this.dx = 1; // 初始向右移动
|
||||
this.dy = 0;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.gameSpeed = 10; // 初始速度
|
||||
this.isPaused = false;
|
||||
this.gameOver = false;
|
||||
this.startTime = Date.now();
|
||||
this.foodEaten = 0;
|
||||
|
||||
// 特殊食物
|
||||
this.specialFood = null;
|
||||
this.specialFoodTimer = 0;
|
||||
this.specialFoodDuration = 5000; // 5秒
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.generateFood();
|
||||
this.gameLoop();
|
||||
|
||||
// 监听键盘事件
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (this.isPaused || this.gameOver) return;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowUp':
|
||||
case 'w':
|
||||
case 'W':
|
||||
if (this.dy === 0) {
|
||||
this.dx = 0;
|
||||
this.dy = -1;
|
||||
}
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 's':
|
||||
case 'S':
|
||||
if (this.dy === 0) {
|
||||
this.dx = 0;
|
||||
this.dy = 1;
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case 'a':
|
||||
case 'A':
|
||||
if (this.dx === 0) {
|
||||
this.dx = -1;
|
||||
this.dy = 0;
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'd':
|
||||
case 'D':
|
||||
if (this.dx === 0) {
|
||||
this.dx = 1;
|
||||
this.dy = 0;
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
this.togglePause();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
generateFood() {
|
||||
// 生成普通食物
|
||||
let newFood;
|
||||
do {
|
||||
newFood = {
|
||||
x: Math.floor(Math.random() * this.tileCount),
|
||||
y: Math.floor(Math.random() * this.tileCountY)
|
||||
};
|
||||
} while (this.isPositionOccupied(newFood));
|
||||
|
||||
this.food = newFood;
|
||||
|
||||
// 有10%几率生成特殊食物
|
||||
if (Math.random() < 0.1 && !this.specialFood) {
|
||||
this.generateSpecialFood();
|
||||
}
|
||||
}
|
||||
|
||||
generateSpecialFood() {
|
||||
let newFood;
|
||||
do {
|
||||
newFood = {
|
||||
x: Math.floor(Math.random() * this.tileCount),
|
||||
y: Math.floor(Math.random() * this.tileCountY),
|
||||
type: 'special',
|
||||
value: 5 // 特殊食物价值5分
|
||||
};
|
||||
} while (this.isPositionOccupied(newFood));
|
||||
|
||||
this.specialFood = newFood;
|
||||
this.specialFoodTimer = Date.now();
|
||||
}
|
||||
|
||||
isPositionOccupied(position) {
|
||||
// 检查是否与蛇身重叠
|
||||
for (let segment of this.snake) {
|
||||
if (segment.x === position.x && segment.y === position.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否与普通食物重叠
|
||||
if (this.food && this.food.x === position.x && this.food.y === position.y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查是否与特殊食物重叠
|
||||
if (this.specialFood && this.specialFood.x === position.x && this.specialFood.y === position.y) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.isPaused || this.gameOver) return;
|
||||
|
||||
// 更新蛇头位置
|
||||
const head = {x: this.snake[0].x + this.dx, y: this.snake[0].y + this.dy};
|
||||
|
||||
// 检查游戏结束条件
|
||||
if (this.checkCollision(head)) {
|
||||
this.gameOver = true;
|
||||
this.showGameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
// 移动蛇
|
||||
this.snake.unshift(head);
|
||||
|
||||
// 检查是否吃到食物
|
||||
if (head.x === this.food.x && head.y === this.food.y) {
|
||||
this.score += 1;
|
||||
this.foodEaten++;
|
||||
this.generateFood();
|
||||
this.updateLevel();
|
||||
} else if (this.specialFood && head.x === this.specialFood.x && head.y === this.specialFood.y) {
|
||||
this.score += this.specialFood.value;
|
||||
this.foodEaten++;
|
||||
this.specialFood = null;
|
||||
this.generateFood();
|
||||
this.updateLevel();
|
||||
} else {
|
||||
this.snake.pop(); // 如果没有吃到食物,移除尾部
|
||||
}
|
||||
|
||||
// 检查特殊食物超时
|
||||
if (this.specialFood && Date.now() - this.specialFoodTimer > this.specialFoodDuration) {
|
||||
this.specialFood = null;
|
||||
}
|
||||
|
||||
// 更新UI
|
||||
this.updateUI();
|
||||
}
|
||||
|
||||
checkCollision(head) {
|
||||
// 检查撞墙
|
||||
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCountY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查撞到自己(从第4节开始检查,避免误判)
|
||||
for (let i = 4; i < this.snake.length; i++) {
|
||||
if (this.snake[i].x === head.x && this.snake[i].y === head.y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
updateLevel() {
|
||||
// 每吃5个食物升一级
|
||||
const newLevel = Math.floor(this.foodEaten / 5) + 1;
|
||||
if (newLevel > this.level) {
|
||||
this.level = newLevel;
|
||||
this.gameSpeed = Math.min(20, 10 + this.level); // 速度上限20
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
document.getElementById('score').textContent = this.score;
|
||||
document.getElementById('length').textContent = this.snake.length;
|
||||
document.getElementById('level').textContent = this.level;
|
||||
}
|
||||
|
||||
draw() {
|
||||
// 清空画布
|
||||
this.ctx.fillStyle = '#222';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// 绘制网格(背景)
|
||||
this.ctx.strokeStyle = '#333';
|
||||
this.ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < this.tileCount; x++) {
|
||||
for (let y = 0; y < this.tileCountY; y++) {
|
||||
this.ctx.strokeRect(x * this.gridSize, y * this.gridSize, this.gridSize, this.gridSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制蛇
|
||||
this.snake.forEach((segment, index) => {
|
||||
if (index === 0) {
|
||||
// 蛇头
|
||||
this.ctx.fillStyle = '#4CAF50';
|
||||
} else {
|
||||
// 蛇身,渐变颜色
|
||||
const gradient = (index / this.snake.length) * 100;
|
||||
this.ctx.fillStyle = `hsl(120, 80%, ${60 - gradient}%)`;
|
||||
}
|
||||
this.ctx.fillRect(segment.x * this.gridSize, segment.y * this.gridSize, this.gridSize, this.gridSize);
|
||||
|
||||
// 边框
|
||||
this.ctx.strokeStyle = '#2E7D32';
|
||||
this.ctx.strokeRect(segment.x * this.gridSize, segment.y * this.gridSize, this.gridSize, this.gridSize);
|
||||
});
|
||||
|
||||
// 绘制普通食物
|
||||
this.ctx.fillStyle = '#FF5252';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(
|
||||
this.food.x * this.gridSize + this.gridSize / 2,
|
||||
this.food.y * this.gridSize + this.gridSize / 2,
|
||||
this.gridSize / 2 - 2,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
this.ctx.fill();
|
||||
|
||||
// 绘制特殊食物(如果存在)
|
||||
if (this.specialFood) {
|
||||
this.ctx.fillStyle = '#FFD700';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(
|
||||
this.specialFood.x * this.gridSize + this.gridSize / 2,
|
||||
this.specialFood.y * this.gridSize + this.gridSize / 2,
|
||||
this.gridSize / 2 - 1,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
this.ctx.fill();
|
||||
|
||||
// 闪烁效果
|
||||
const time = Date.now() - this.specialFoodTimer;
|
||||
const alpha = 0.5 + 0.5 * Math.sin(time / 200);
|
||||
this.ctx.globalAlpha = alpha;
|
||||
this.ctx.fillStyle = '#FF6B00';
|
||||
this.ctx.fill();
|
||||
this.ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// 绘制暂停状态
|
||||
if (this.isPaused) {
|
||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
this.ctx.fillStyle = 'white';
|
||||
this.ctx.font = '24px Arial';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText('游戏暂停', this.canvas.width / 2, this.canvas.height / 2);
|
||||
}
|
||||
}
|
||||
|
||||
gameLoop() {
|
||||
this.update();
|
||||
this.draw();
|
||||
|
||||
if (!this.gameOver) {
|
||||
setTimeout(() => this.gameLoop(), 1000 / this.gameSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
togglePause() {
|
||||
this.isPaused = !this.isPaused;
|
||||
document.getElementById('pauseBtn').textContent = this.isPaused ? '继续' : '暂停';
|
||||
|
||||
if (!this.isPaused && !this.gameOver) {
|
||||
this.gameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
changeDirection(dx, dy) {
|
||||
if (this.isPaused || this.gameOver) return;
|
||||
|
||||
// 防止180度转弯
|
||||
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dx = dx;
|
||||
this.dy = dy;
|
||||
}
|
||||
|
||||
showGameOver() {
|
||||
const modal = document.getElementById('gameOverModal');
|
||||
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
|
||||
document.getElementById('finalScore').textContent = this.score;
|
||||
document.getElementById('finalLength').textContent = this.snake.length;
|
||||
document.getElementById('finalLevel').textContent = this.level;
|
||||
document.getElementById('gameTime').textContent = gameTime;
|
||||
document.getElementById('foodEaten').textContent = this.foodEaten;
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.snake = [
|
||||
{x: 10, y: 10},
|
||||
{x: 9, y: 10},
|
||||
{x: 8, y: 10}
|
||||
];
|
||||
this.dx = 1;
|
||||
this.dy = 0;
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.gameSpeed = 10;
|
||||
this.isPaused = false;
|
||||
this.gameOver = false;
|
||||
this.startTime = Date.now();
|
||||
this.foodEaten = 0;
|
||||
this.specialFood = null;
|
||||
|
||||
this.generateFood();
|
||||
this.updateUI();
|
||||
|
||||
document.getElementById('gameOverModal').style.display = 'none';
|
||||
document.getElementById('pauseBtn').textContent = '暂停';
|
||||
|
||||
this.gameLoop();
|
||||
}
|
||||
}
|
||||
|
||||
// 全局游戏实例
|
||||
let game;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
game = new SnakeGame();
|
||||
});
|
||||
295
InfoGenie-frontend/public/smallgame/贪吃蛇/game-stats.js
Normal file
295
InfoGenie-frontend/public/smallgame/贪吃蛇/game-stats.js
Normal file
@@ -0,0 +1,295 @@
|
||||
class GameStatistics {
|
||||
constructor() {
|
||||
this.highScores = JSON.parse(localStorage.getItem('snakeHighScores')) || [];
|
||||
this.sessionStats = {
|
||||
gamesPlayed: 0,
|
||||
totalScore: 0,
|
||||
maxLength: 0,
|
||||
maxLevel: 0,
|
||||
totalTime: 0
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// 恢复会话统计(如果存在)
|
||||
const savedSession = localStorage.getItem('snakeSessionStats');
|
||||
if (savedSession) {
|
||||
this.sessionStats = JSON.parse(savedSession);
|
||||
}
|
||||
|
||||
// 监听游戏事件
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// 监听自定义游戏事件
|
||||
document.addEventListener('gameOver', (e) => {
|
||||
this.handleGameOver(e.detail);
|
||||
});
|
||||
|
||||
document.addEventListener('foodEaten', (e) => {
|
||||
this.handleFoodEaten(e.detail);
|
||||
});
|
||||
|
||||
document.addEventListener('levelUp', (e) => {
|
||||
this.handleLevelUp(e.detail);
|
||||
});
|
||||
}
|
||||
|
||||
handleGameOver(gameData) {
|
||||
this.sessionStats.gamesPlayed++;
|
||||
this.sessionStats.totalScore += gameData.score;
|
||||
this.sessionStats.maxLength = Math.max(this.sessionStats.maxLength, gameData.length);
|
||||
this.sessionStats.maxLevel = Math.max(this.sessionStats.maxLevel, gameData.level);
|
||||
this.sessionStats.totalTime += gameData.gameTime;
|
||||
|
||||
// 保存会话统计
|
||||
localStorage.setItem('snakeSessionStats', JSON.stringify(this.sessionStats));
|
||||
|
||||
// 检查是否进入高分榜
|
||||
this.checkHighScore(gameData);
|
||||
|
||||
// 显示统计信息
|
||||
this.displaySessionStats();
|
||||
}
|
||||
|
||||
handleFoodEaten(foodData) {
|
||||
// 可以记录特殊食物统计等
|
||||
console.log('食物被吃掉:', foodData);
|
||||
}
|
||||
|
||||
handleLevelUp(levelData) {
|
||||
// 等级提升统计
|
||||
console.log('等级提升到:', levelData.level);
|
||||
}
|
||||
|
||||
checkHighScore(gameData) {
|
||||
const highScoreEntry = {
|
||||
score: gameData.score,
|
||||
length: gameData.length,
|
||||
level: gameData.level,
|
||||
time: gameData.gameTime,
|
||||
date: new Date().toLocaleDateString(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
// 添加到高分榜
|
||||
this.highScores.push(highScoreEntry);
|
||||
|
||||
// 按分数排序(降序)
|
||||
this.highScores.sort((a, b) => b.score - a.score);
|
||||
|
||||
// 只保留前10名
|
||||
this.highScores = this.highScores.slice(0, 10);
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('snakeHighScores', JSON.stringify(this.highScores));
|
||||
|
||||
// 显示高分榜
|
||||
this.displayHighScores();
|
||||
}
|
||||
|
||||
displaySessionStats() {
|
||||
const statsElement = document.createElement('div');
|
||||
statsElement.className = 'session-stats';
|
||||
statsElement.innerHTML = `
|
||||
<h3>本次会话统计</h3>
|
||||
<p>游戏次数: ${this.sessionStats.gamesPlayed}</p>
|
||||
<p>总得分: ${this.sessionStats.totalScore}</p>
|
||||
<p>最高长度: ${this.sessionStats.maxLength}</p>
|
||||
<p>最高等级: ${this.sessionStats.maxLevel}</p>
|
||||
<p>总游戏时间: ${Math.floor(this.sessionStats.totalTime / 60)}分钟</p>
|
||||
<p>平均得分: ${Math.round(this.sessionStats.totalScore / this.sessionStats.gamesPlayed)}</p>
|
||||
`;
|
||||
|
||||
// 添加到游戏结束模态框
|
||||
const statsContainer = document.querySelector('.stats');
|
||||
if (statsContainer && !document.querySelector('.session-stats')) {
|
||||
statsContainer.appendChild(statsElement);
|
||||
}
|
||||
}
|
||||
|
||||
displayHighScores() {
|
||||
const highScoresElement = document.createElement('div');
|
||||
highScoresElement.className = 'high-scores';
|
||||
|
||||
if (this.highScores.length > 0) {
|
||||
highScoresElement.innerHTML = `
|
||||
<h3>🏆 高分榜</h3>
|
||||
${this.highScores.map((score, index) => `
|
||||
<div class="score-item ${index === 0 ? 'first-place' : ''}">
|
||||
<span class="rank">${index + 1}.</span>
|
||||
<span class="score">${score.score}分</span>
|
||||
<span class="length">长度:${score.length}</span>
|
||||
<span class="date">${score.date}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
} else {
|
||||
highScoresElement.innerHTML = '<p>暂无高分记录</p>';
|
||||
}
|
||||
|
||||
// 添加到游戏结束模态框
|
||||
const modalContent = document.querySelector('.modal-content');
|
||||
if (modalContent && !document.querySelector('.high-scores')) {
|
||||
modalContent.appendChild(highScoresElement);
|
||||
}
|
||||
}
|
||||
|
||||
getAchievements(gameData) {
|
||||
const achievements = [];
|
||||
|
||||
if (gameData.score >= 100) achievements.push('百分达人');
|
||||
if (gameData.length >= 20) achievements.push('长蛇之王');
|
||||
if (gameData.level >= 5) achievements.push('等级大师');
|
||||
if (gameData.gameTime >= 300) achievements.push('持久战将');
|
||||
if (gameData.score >= 50 && gameData.gameTime <= 60) achievements.push('速通高手');
|
||||
|
||||
return achievements;
|
||||
}
|
||||
|
||||
// 工具方法:格式化时间
|
||||
formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}分${secs}秒`;
|
||||
}
|
||||
|
||||
// 清除统计
|
||||
clearStatistics() {
|
||||
this.highScores = [];
|
||||
this.sessionStats = {
|
||||
gamesPlayed: 0,
|
||||
totalScore: 0,
|
||||
maxLength: 0,
|
||||
maxLevel: 0,
|
||||
totalTime: 0
|
||||
};
|
||||
|
||||
localStorage.removeItem('snakeHighScores');
|
||||
localStorage.removeItem('snakeSessionStats');
|
||||
|
||||
console.log('统计信息已清除');
|
||||
}
|
||||
}
|
||||
|
||||
// 扩展游戏核心类,添加统计事件触发
|
||||
SnakeGame.prototype.showGameOver = function() {
|
||||
const modal = document.getElementById('gameOverModal');
|
||||
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
|
||||
document.getElementById('finalScore').textContent = this.score;
|
||||
document.getElementById('finalLength').textContent = this.snake.length;
|
||||
document.getElementById('finalLevel').textContent = this.level;
|
||||
document.getElementById('gameTime').textContent = gameTime;
|
||||
document.getElementById('foodEaten').textContent = this.foodEaten;
|
||||
|
||||
// 触发游戏结束事件
|
||||
const gameOverEvent = new CustomEvent('gameOver', {
|
||||
detail: {
|
||||
score: this.score,
|
||||
length: this.snake.length,
|
||||
level: this.level,
|
||||
gameTime: gameTime,
|
||||
foodEaten: this.foodEaten
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(gameOverEvent);
|
||||
|
||||
modal.style.display = 'flex';
|
||||
};
|
||||
|
||||
// 初始化统计模块
|
||||
let gameStats;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
gameStats = new GameStatistics();
|
||||
});
|
||||
|
||||
// 添加CSS样式
|
||||
const statsStyles = `
|
||||
.session-stats {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #fdfcfb 0%, #e2d1c3 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #d4a76a;
|
||||
}
|
||||
|
||||
.session-stats h3 {
|
||||
color: #8b4513;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.session-stats p {
|
||||
margin: 5px 0;
|
||||
color: #654321;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.high-scores {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #fff1eb 0%, #ace0f9 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #4682b4;
|
||||
}
|
||||
|
||||
.high-scores h3 {
|
||||
color: #2c5282;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #cbd5e0;
|
||||
}
|
||||
|
||||
.score-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.score-item.first-place {
|
||||
background: linear-gradient(135deg, #fceabb 0%, #f8b500 100%);
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
margin: -8px -8px 8px -8px;
|
||||
}
|
||||
|
||||
.rank {
|
||||
font-weight: bold;
|
||||
color: #2d3748;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-weight: bold;
|
||||
color: #e53e3e;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.length {
|
||||
color: #4a5568;
|
||||
font-size: 0.8rem;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #718096;
|
||||
font-size: 0.7rem;
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
// 注入样式
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = statsStyles;
|
||||
document.head.appendChild(styleSheet);
|
||||
61
InfoGenie-frontend/public/smallgame/贪吃蛇/index.html
Normal file
61
InfoGenie-frontend/public/smallgame/贪吃蛇/index.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!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>贪吃蛇游戏</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<h1>贪吃蛇</h1>
|
||||
<div class="score-board">
|
||||
<span>分数: <span id="score">0</span></span>
|
||||
<span>长度: <span id="length">3</span></span>
|
||||
<span>等级: <span id="level">1</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-canvas-container">
|
||||
<canvas id="gameCanvas" width="300" height="500"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="game-controls">
|
||||
<div class="control-row">
|
||||
<button id="upBtn" class="control-btn">↑</button>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<button id="leftBtn" class="control-btn">←</button>
|
||||
<button id="pauseBtn" class="control-btn">暂停</button>
|
||||
<button id="rightBtn" class="control-btn">→</button>
|
||||
</div>
|
||||
<div class="control-row">
|
||||
<button id="downBtn" class="control-btn">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-instructions">
|
||||
<p>使用方向键或触摸按钮控制蛇的方向</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="gameOverModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>游戏结束!</h2>
|
||||
<div class="stats">
|
||||
<p>最终分数: <span id="finalScore">0</span></p>
|
||||
<p>蛇的长度: <span id="finalLength">0</span></p>
|
||||
<p>达到等级: <span id="finalLevel">0</span></p>
|
||||
<p>游戏时间: <span id="gameTime">0</span>秒</p>
|
||||
<p>吃掉食物: <span id="foodEaten">0</span>个</p>
|
||||
</div>
|
||||
<button id="restartBtn" class="restart-btn">重新开始</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="game-core.js"></script>
|
||||
<script src="game-controls.js"></script>
|
||||
<script src="game-stats.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
258
InfoGenie-frontend/public/smallgame/贪吃蛇/styles.css
Normal file
258
InfoGenie-frontend/public/smallgame/贪吃蛇/styles.css
Normal file
@@ -0,0 +1,258 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||
max-width: 400px;
|
||||
width: 95%;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
color: #333;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.score-board {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
padding: 12px;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.score-board span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.game-canvas-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 3px solid #333;
|
||||
border-radius: 10px;
|
||||
background: #222;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 0 10px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.control-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
#pauseBtn {
|
||||
background: linear-gradient(135deg, #ff9a9e 0%, #fad0c4 100%);
|
||||
color: #333;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.game-instructions {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
color: #e74c3c;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stats p {
|
||||
margin: 10px 0;
|
||||
font-size: 1.1rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.stats span {
|
||||
font-weight: bold;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
border-radius: 25px;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 手机端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.game-container {
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
font-size: 0.9rem;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 1.3rem;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
#pauseBtn {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 280px;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕优化 */
|
||||
@media (max-width: 480px) {
|
||||
.game-header h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.score-board {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
font-size: 1.1rem;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 250px;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 防止文本选择 */
|
||||
.control-btn, .restart-btn {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.score-board {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
Reference in New Issue
Block a user