update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View File

@@ -1,152 +1,22 @@
// 2048游戏核心逻辑
class Game2048 {
constructor() {
this.size = 4;
this.grid = [];
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.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();
}
// 依据分数计算权重0.1 ~ 0.95
calculateWeightByScore(score) {
const w = score / 4000; // 4000分约接近满权重
return Math.max(0.1, Math.min(0.95, w));
}
// 按权重偏向生成0~10的随机整数权重越高越偏向更大值
biasedRandomInt(maxInclusive, weight) {
const rand = Math.random();
const biased = Math.pow(rand, 1 - weight); // weight越大biased越接近1
const val = Math.floor(biased * (maxInclusive + 1));
return Math.max(0, Math.min(maxInclusive, val));
}
// 附加结束信息到界面
appendEndInfo(text, type = 'info') {
const message = document.getElementById('game-message');
if (!message) return;
const info = document.createElement('div');
info.style.marginTop = '10px';
info.style.fontSize = '16px';
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#776e65');
info.textContent = text;
message.appendChild(info);
}
// 游戏结束时尝试给当前登录账户加“萌芽币”
async tryAwardCoinsOnGameOver() {
try {
const token = localStorage.getItem('token');
if (!token) {
this.appendEndInfo('未登录,无法获得萌芽币');
return;
}
let email = null;
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const userObj = JSON.parse(userStr);
email = userObj && (userObj.email || userObj['邮箱']);
}
} catch (e) {
// 忽略解析错误
}
if (!email) {
this.appendEndInfo('未找到账户信息email无法加币', 'error');
return;
}
// 根据分数计算权重与概率
const weight = this.calculateWeightByScore(this.score);
let awardProbability = weight; // 默认用权重作为概率
let guaranteed = false;
// 分数≥500时必定触发奖励
if (this.score >= 500) {
awardProbability = 1;
guaranteed = true;
}
const roll = Math.random();
if (roll > awardProbability) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 生成0~10随机萌芽币数量权重越高越偏向更大值
let coins = this.biasedRandomInt(5, weight);
// 保底至少 1 个仅当分数≥500时
if (guaranteed) {
coins = Math.max(1, coins);
}
coins = Math.max(0, Math.min(10, coins));
if (coins <= 0) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 后端 API base URL从父窗口ENV_CONFIG获取回退到本地默认
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
? window.parent.ENV_CONFIG.API_URL
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ email, amount: coins })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
this.appendEndInfo(`加币失败:${msg}`, 'error');
return;
}
const data = await resp.json();
if (data && data.success) {
const newCoins = data.data && data.data.new_coins;
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
} else {
const msg = (data && (data.message || data.error)) || '未知错误';
this.appendEndInfo(`加币失败:${msg}`, 'error');
}
} catch (e) {
console.error('加币流程发生错误:', e);
this.appendEndInfo('加币失败:网络或系统错误', 'error');
}
}
initializeGrid() {
this.grid = [];
for (let i = 0; i < this.size; i++) {
@@ -156,468 +26,216 @@ class Game2048 {
}
}
}
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 (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 cell = 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);
this.grid[cell.x][cell.y] = value;
this.createTileElement(cell.x, cell.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.style.left = `${y * 25}%`;
tile.style.top = `${x * 25}%`;
tile.dataset.x = x;
tile.dataset.y = y;
tile.dataset.value = value;
container.appendChild(tile);
// 移除动画类
setTimeout(() => {
tile.classList.remove('tile-new');
}, 200);
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]);
}
if (this.grid[i][j] !== 0) this.createTileElement(i, j, this.grid[i][j]);
}
}
// 更新分数
document.getElementById('score').textContent = this.score;
// 更新统计数据显示
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;
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();
this.showEndScreen('你赢了!🎉');
} else if (this.isGameOver()) {
this.gameOver = true;
this.showGameOver();
this.showEndScreen('游戏结束!');
}
}
}
moveLeft() {
for (let i = 0; i < this.size; i++) {
const row = this.grid[i].filter(val => val !== 0);
const row = this.grid[i].filter(v => v !== 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++;
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;
row[j + 1] = 0; merged[j] = true;
}
}
const newRow = row.filter(val => val !== 0);
while (newRow.length < this.size) {
newRow.push(0);
}
const newRow = row.filter(v => v !== 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;
}
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 row = this.grid[i].filter(v => v !== 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++;
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;
row[j - 1] = 0; merged[j] = true;
}
}
const newRow = row.filter(val => val !== 0);
while (newRow.length < this.size) {
newRow.unshift(0);
}
const newRow = row.filter(v => v !== 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;
}
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 col = [];
for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) col.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;
for (let i = 0; i < col.length - 1; i++) {
if (col[i] === col[i + 1] && !merged[i] && !merged[i + 1]) {
col[i] *= 2; this.score += col[i]; this.stats.mergeCount++;
this.stats.maxTile = Math.max(this.stats.maxTile, col[i]);
col[i + 1] = 0; merged[i] = true;
}
}
const newColumn = column.filter(val => val !== 0);
while (newColumn.length < this.size) {
newColumn.push(0);
}
const newCol = col.filter(v => v !== 0);
while (newCol.length < this.size) newCol.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];
if (this.grid[i][j] !== newCol[i]) this.moved = true;
this.grid[i][j] = newCol[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 col = [];
for (let i = 0; i < this.size; i++) { if (this.grid[i][j] !== 0) col.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;
for (let i = col.length - 1; i > 0; i--) {
if (col[i] === col[i - 1] && !merged[i] && !merged[i - 1]) {
col[i] *= 2; this.score += col[i]; this.stats.mergeCount++;
this.stats.maxTile = Math.max(this.stats.maxTile, col[i]);
col[i - 1] = 0; merged[i] = true;
}
}
const newColumn = column.filter(val => val !== 0);
while (newColumn.length < this.size) {
newColumn.unshift(0);
}
const newCol = col.filter(v => v !== 0);
while (newCol.length < this.size) newCol.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];
if (this.grid[i][j] !== newCol[i]) this.moved = true;
this.grid[i][j] = newCol[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;
}
}
}
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 i = 0; i < this.size; i++)
for (let j = 0; j < this.size; j++) {
if (this.grid[i][j] === 0) {
if (this.grid[i][j] === 0) return false;
const c = this.grid[i][j];
if ((i > 0 && this.grid[i-1][j] === c) || (i < this.size-1 && this.grid[i+1][j] === c) ||
(j > 0 && this.grid[i][j-1] === c) || (j < this.size-1 && this.grid[i][j+1] === c))
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() {
showEndScreen(text) {
const message = document.getElementById('game-message');
message.className = 'game-message game-won';
message.className = 'game-message ' + (this.gameWon ? 'game-won' : 'game-over');
message.style.display = 'flex';
message.querySelector('p').textContent = '你赢了!';
// 胜利也尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 显示最终统计
setTimeout(() => {
if (window.gameStats) {
window.gameStats.showFinalStats();
}
}, 1000);
message.querySelector('p').innerHTML =
`${text}<br><span style="font-size:16px;opacity:0.8;">` +
`得分 ${this.score} · 步数 ${this.stats.moves} · ` +
`最大方块 ${this.stats.maxTile} · 用时 ${this.stats.gameTime}秒</span>`;
}
showGameOver() {
const message = document.getElementById('game-message');
message.className = 'game-message game-over';
message.style.display = 'flex';
message.querySelector('p').textContent = '游戏结束!';
// 渲染排行榜
try {
this.renderLeaderboard();
} catch (e) {
console.error('渲染排行榜时发生错误:', e);
}
// 尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 显示最终统计
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.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();
}
startTimer() {
this.stats.startTime = Date.now();
if (this.timerInterval) {
clearInterval(this.timerInterval);
}
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);
}
// 构建并渲染排行榜
renderLeaderboard() {
const container = document.getElementById('leaderboard');
if (!container) return;
// 生成当前玩家数据
const today = this.formatDate(new Date());
const currentPlayer = {
"名称": "我",
"账号": "guest-local",
"分数": this.score,
"时间": today,
_current: true
};
// 合并并排序数据(分数由高到低)
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
const merged = [...baseData.map(d => ({...d})), currentPlayer]
.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
// 计算当前玩家排名
const currentIndex = merged.findIndex(d => d._current);
const rank = currentIndex >= 0 ? currentIndex + 1 : '-';
// 仅展示前10条
const topN = merged.slice(0, 10);
// 生成 HTML
const summaryHtml = `
<div class="leaderboard-summary">
<span>本局分数:<strong>${this.score}</strong></span>
<span>用时:<strong>${this.stats.gameTime}</strong> 秒</span>
<span>你的排名:<strong>${rank}</strong></span>
</div>
`;
const headerHtml = `
<div class="leaderboard-header">
<div class="leaderboard-col rank">排名</div>
<div class="leaderboard-col name">名称</div>
<div class="leaderboard-col score">分数</div>
<div class="leaderboard-col time">日期</div>
</div>
`;
const rowsHtml = topN.map((d, i) => {
const isCurrent = !!d._current;
const rowClass = `leaderboard-row${isCurrent ? ' current' : ''}`;
return `
<div class="${rowClass}">
<div class="leaderboard-col rank">${i + 1}</div>
<div class="leaderboard-col name">${this.escapeHtml(d["名称"] || '未知')}</div>
<div class="leaderboard-col score">${d["分数"] ?? 0}</div>
<div class="leaderboard-col time">${this.escapeHtml(d["时间"] || '-')}</div>
</div>
`;
}).join('');
container.innerHTML = `
<div class="leaderboard-title">排行榜</div>
${summaryHtml}
<div class="leaderboard-table">
${headerHtml}
<div class="leaderboard-body">${rowsHtml}</div>
</div>
`;
}
// 工具:日期格式化 YYYY-MM-DD
formatDate(date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
// 工具:简单转义以避免 XSS
escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\"/g, '&quot;')
.replace(/'/g, '&#039;');
}
bindEvents() {
// 重试按钮
document.getElementById('retry-btn').addEventListener('click', () => {
this.restart();
});
document.getElementById('retry-btn').addEventListener('click', () => this.restart());
}
}
// 游戏实例
let game;
// 页面加载完成后初始化游戏
document.addEventListener('DOMContentLoaded', () => {
game = new Game2048();
// 导出游戏实例供其他模块使用
window.game2048 = game;
});
});

View File

@@ -1,20 +0,0 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -22,8 +22,6 @@
<div class="game-container">
<div class="game-message" id="game-message">
<p></p>
<!-- 排行榜容器:游戏结束后动态填充 -->
<div id="leaderboard" class="leaderboard" aria-live="polite"></div>
<div class="lower">
<a class="retry-button" id="retry-btn">重新开始</a>
</div>
@@ -66,7 +64,6 @@
<script src="gamedata.js"></script>
<script src="game-logic.js"></script>
<script src="controls.js"></script>
</body>

View File

@@ -237,91 +237,6 @@ body {
transition: all 0.3s ease;
}
/* 排行榜样式(与 2048 主题一致) */
.leaderboard {
width: 100%;
max-width: 440px;
background: rgba(250, 248, 239, 0.95); /* #faf8ef */
border-radius: 12px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
color: #776e65;
}
.leaderboard-title {
font-size: 22px;
font-weight: 700;
color: #8f7a66;
margin-bottom: 8px;
letter-spacing: 0.5px;
}
.leaderboard-summary {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-size: 14px;
color: #8f7a66;
margin-bottom: 10px;
}
.leaderboard-summary strong {
color: #8f7a66;
}
.leaderboard-table {
border: 1px solid rgba(187, 173, 160, 0.3); /* #bbada0 */
border-radius: 10px;
overflow: hidden;
background: rgba(238, 228, 218, 0.4); /* #eee4da */
}
.leaderboard-header,
.leaderboard-row {
display: grid;
grid-template-columns: 64px 1fr 90px 120px; /* 排名/名称/分数/日期 */
align-items: center;
}
.leaderboard-header {
background: #eee4da;
color: #776e65;
font-weight: 700;
padding: 8px 10px;
border-bottom: 1px solid rgba(187, 173, 160, 0.3);
}
.leaderboard-body {
max-height: 220px;
overflow-y: auto;
background: rgba(238, 228, 218, 0.25);
}
.leaderboard-row {
padding: 8px 10px;
border-top: 1px solid rgba(187, 173, 160, 0.15);
}
.leaderboard-row:nth-child(odd) {
background: rgba(238, 228, 218, 0.22);
}
.leaderboard-row.current {
background: #f3e9d4;
box-shadow: inset 0 0 0 2px rgba(143, 122, 102, 0.35);
}
.leaderboard-col.rank {
text-align: center;
font-weight: 700;
color: #8f7a66;
}
.leaderboard-col.score {
text-align: right;
font-weight: 700;
}
.leaderboard-col.time {
text-align: right;
color: #776e65;
}
.retry-button:hover {
background: #9f8a76;
transform: translateY(-2px);

View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -0,0 +1,4 @@
{
"account_id": "3fdbaad92364222635c5c1c41ff1af8b",
"project_name": "floppy-bird"
}

View File

@@ -0,0 +1,6 @@
{
"account": {
"id": "3fdbaad92364222635c5c1c41ff1af8b",
"name": "shumengya"
}
}

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,51 @@
<img src="screencap.png" align="right" width="250">
# [play floppy bird](https://nebezb.com/floppybird/)
If you missed the Flappy Bird hype, here's your chance to try the best vintage knockoff.
**Features**
* 🎉 good ol' div's for all the objects and graphics,
* 🖥 scales perfectly on almost any screen, both mobile and desktop,
* 💩 unoptimized, laggy, and not nearly as fast as a canvas implementation,
* 👷‍♂️ a typescript version that does almost nothing better over at [ts-floppybird](https://github.com/nebez/ts-floppybird)!
Enjoy.
https://nebezb.com/floppybird/ (or play [**easy mode**](https://nebezb.com/floppybird/?easy))
### Clones
* https://wanderingstan.github.io/handybird/
* **[@wanderinstan](https://github.com/wanderingstan)** enables hand gestures to play using doppler effect and a microphone
* http://www.hhcc.com/404
* **[Hill Holiday](http://www.hhcc.com/)** using it for their 404
* http://heart-work.se/duvchi
* promotional campaign for an album release
* https://www.progressivewebflap.com/
* **[@jsonthor](https://twitter.com/jsonthor)** lets you take floppy bird with you as a progressive web app
* https://github.com/rukmal/FlappyLeapBird
* **[Rukmal](http://rukmal.me/)** integrates the LeapMotion Controller
* http://chrisbeaumont.github.io/floppybird/
* **[@chrisbeaumont](https://github.com/chrisbeaumont)** puts the bird on auto-pilot
* http://www.lobe.io/flappy-math-saga/
* **[@tikwid](https://github.com/tikwid)** teaches you math
* http://dota2.cyborgmatt.com/flappydota/
* flappy dota
* http://labs.aylien.com/flappy-bird/
* **[@mdibaiee/flappy-es](https://github.com/mdibaiee/flappy-es)** brings skynet to floppy bird
* https://emu.edu/gaming-hub/flappy-huxman-game/
* university celebrates 100 years by putting President Susan Huxman on a floppy bird body
* https://www.docker.com/blog/creating-the-kubecon-flappy-dock-extension/
* a Docker-themed fork that was turned into a Docker Extension for KubeCon EU 2022 ([source available here](https://github.com/mikesir87/floppybird))
* http://flappydragon.attim.in/
* **[@iarunava/flappydragon](https://github.com/iarunava/flappydragon)** redesign flappy bird for Game of Thrones.
### Notice
The assets powering the visual element of the game have all been extracted directly from the Flappy Bird android game. I do not own the assets, nor do I have explicit permission to use them from their creator. They are the work and copyright of original creator Dong Nguyen and .GEARS games (http://www.dotgears.com/).
I took this Tweet (https://twitter.com/dongatory/status/431060041009856512 / http://i.imgur.com/AcyWyqf.png) by Dong Nguyen, the creator of the game, as an open invitation to reuse the game concept and assets in an open source project. There is no intention to steal the game, monetize it, or claim it as my own.
If the copyright holder would like for the assets to be removed, please open an issue to start the conversation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -0,0 +1,374 @@
@-webkit-keyframes animLand {
0% { background-position: 0px 0px; }
100% { background-position: -335px 0px; }
}
@-moz-keyframes animLand {
0% { background-position: 0px 0px; }
100% { background-position: -335px 0px; }
}
@-o-keyframes animLand {
0% { background-position: 0px 0px; }
100% { background-position: -335px 0px; }
}
@keyframes animLand {
0% { background-position: 0px 0px; }
100% { background-position: -335px 0px; }
}
@-webkit-keyframes animSky {
0% { background-position: 0px 100%; }
100% { background-position: -275px 100%; }
}
@-moz-keyframes animSky {
0% { background-position: 0px 100%; }
100% { background-position: -275px 100%; }
}
@-o-keyframes animSky {
0% { background-position: 0px 100%; }
100% { background-position: -275px 100%; }
}
@keyframes animSky {
0% { background-position: 0px 100%; }
100% { background-position: -275px 100%; }
}
@-webkit-keyframes animBird {
from { background-position: 0px 0px; }
to { background-position: 0px -96px; }
}
@-moz-keyframes animBird {
from { background-position: 0px 0px; }
to { background-position: 0px -96px; }
}
@-o-keyframes animBird {
from { background-position: 0px 0px; }
to { background-position: 0px -96px; }
}
@keyframes animBird {
from { background-position: 0px 0px; }
to { background-position: 0px -96px; }
}
@-webkit-keyframes animPipe {
0% { left: 900px; }
100% { left: -100px; }
}
@-moz-keyframes animPipe {
0% { left: 900px; }
100% { left: -100px; }
}
@-o-keyframes animPipe {
0% { left: 900px; }
100% { left: -100px; }
}
@keyframes animPipe {
0% { left: 900px; }
100% { left: -100px; }
}
@-webkit-keyframes animCeiling {
0% { background-position: 0px 0px; }
100% { background-position: -63px 0px; }
}
@-moz-keyframes animCeiling {
0% { background-position: 0px 0px; }
100% { background-position: -63px 0px; }
}
@-o-keyframes animCeiling {
0% { background-position: 0px 0px; }
100% { background-position: -63px 0px; }
}
@keyframes animCeiling {
0% { background-position: 0px 0px; }
100% { background-position: -63px 0px; }
}
*,
*:before,
*:after
{
/* border box */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
/* gpu acceleration */
-webkit-transition: translate3d(0,0,0);
/* select disable */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
html,
body
{
height: 100%;
overflow: hidden;
font-family: monospace;
font-size: 12px;
color: #fff;
}
#gamecontainer
{
position: relative;
width: 100%;
height: 100%;
min-height: 525px;
}
/*
Screen - Game
*/
#gamescreen
{
position: absolute;
width: 100%;
height: 100%;
}
#sky
{
position: absolute;
top: 0;
width: 100%;
height: 80%;
background-image: url('../assets/sky.png');
background-repeat: repeat-x;
background-position: 0px 100%;
background-color: #4ec0ca;
-webkit-animation: animSky 7s linear infinite;
animation: animSky 7s linear infinite;
}
#flyarea
{
position: absolute;
bottom: 0;
height: 420px;
width: 100%;
}
#ceiling
{
position: absolute;
top: -16px;
height: 16px;
width: 100%;
background-image: url('../assets/ceiling.png');
background-repeat: repeat-x;
-webkit-animation: animCeiling 481ms linear infinite;
animation: animCeiling 481ms linear infinite;
}
#land
{
position: absolute;
bottom: 0;
width: 100%;
height: 20%;
background-image: url('../assets/land.png');
background-repeat: repeat-x;
background-position: 0px 0px;
background-color: #ded895;
-webkit-animation: animLand 2516ms linear infinite;
animation: animLand 2516ms linear infinite;
}
#bigscore
{
position: absolute;
top: 20px;
left: 150px;
z-index: 100;
}
#bigscore img
{
display: inline-block;
padding: 1px;
}
#splash
{
position: absolute;
opacity: 0;
top: 75px;
left: 65px;
width: 188px;
height: 170px;
background-image: url('../assets/splash.png');
background-repeat: no-repeat;
}
#scoreboard
{
position: absolute;
display: none;
opacity: 0;
top: 64px;
left: 43px;
width: 236px;
height: 280px;
background-image: url('../assets/scoreboard.png');
background-repeat: no-repeat;
z-index: 1000;
}
#medal
{
position: absolute;
opacity: 0;
top: 114px;
left: 32px;
width: 44px;
height: 44px;
}
#currentscore
{
position: absolute;
top: 105px;
left: 107px;
width: 104px;
height: 14px;
text-align: right;
}
#currentscore img
{
padding-left: 2px;
}
#highscore
{
position: absolute;
top: 147px;
left: 107px;
width: 104px;
height: 14px;
text-align: right;
}
#highscore img
{
padding-left: 2px;
}
#replay
{
position: absolute;
opacity: 0;
top: 205px;
left: 61px;
height: 115px;
width: 70px;
cursor: pointer;
}
.boundingbox
{
position: absolute;
display: none;
top: 0;
left: 0;
width: 0;
height: 0;
border: 1px solid red;
}
#player
{
left: 60px;
top: 200px;
}
.bird
{
position: absolute;
width: 34px;
height: 24px;
background-image: url('../assets/bird.png');
-webkit-animation: animBird 300ms steps(4) infinite;
animation: animBird 300ms steps(4) infinite;
}
.pipe
{
position: absolute;
left: -100px;
width: 52px;
height: 100%;
z-index: 10;
-webkit-animation: animPipe 7500ms linear;
animation: animPipe 7500ms linear;
}
.pipe_upper
{
position: absolute;
top: 0;
width: 52px;
background-image: url('../assets/pipe.png');
background-repeat: repeat-y;
background-position: center;
}
.pipe_upper:after
{
content: "";
position: absolute;
bottom: 0;
width: 52px;
height: 26px;
background-image: url('../assets/pipe-down.png');
}
.pipe_lower
{
position: absolute;
bottom: 0;
width: 52px;
background-image: url('../assets/pipe.png');
background-repeat: repeat-y;
background-position: center;
}
.pipe_lower:after
{
content: "";
position: absolute;
top: 0;
width: 52px;
height: 26px;
background-image: url('../assets/pipe-up.png');
}
#footer
{
position: absolute;
bottom: 3px;
left: 3px;
}
#footer a,
#footer a:link,
#footer a:visited,
#footer a:hover,
#footer a:active
{
display: block;
padding: 2px;
text-decoration: none;
color: #fff;
}

View File

@@ -0,0 +1,2 @@
/* html5doctor.com Reset v1.6.1 - http://cssreset.com */
html,body,div,span,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,abbr,address,cite,code,del,dfn,em,img,ins,kbd,q,samp,small,strong,sub,sup,var,b,i,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,figcaption,figure,footer,header,hgroup,menu,nav,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}nav ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}a{margin:0;padding:0;font-size:100%;vertical-align:baseline;background:transparent}ins{background-color:#ff9;color:#000;text-decoration:none}mark{background-color:#ff9;color:#000;font-style:italic;font-weight:bold}del{text-decoration:line-through}abbr[title],dfn[title]{border-bottom:1px dotted;cursor:help}table{border-collapse:collapse;border-spacing:0}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}input,select{vertical-align:middle}

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Floppy Bird</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="author" content="Nebez Briefkani" />
<meta name="description" content="play floppy bird. a remake of popular game flappy bird built in html/css/js" />
<meta name="keywords" content="flappybird,flappy,bird,floppybird,floppy,html,html5,css,css3,js,javascript,jquery,github,nebez,briefkani,nebezb,open,source,opensource" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<!-- Open Graph tags -->
<meta property="og:title" content="Floppy Bird" />
<meta property="og:description" content="play floppy bird. a remake of popular game flappy bird built in html/css/js" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://nebezb.com/floppybird/assets/thumb.png" />
<meta property="og:url" content="https://nebezb.com/floppybird/" />
<meta property="og:site_name" content="Floppy Bird" />
<!-- Style sheets -->
<link href="css/reset.css" rel="stylesheet">
<link href="css/main.css" rel="stylesheet">
</head>
<body>
<div id="gamecontainer">
<div id="gamescreen">
<div id="sky" class="animated">
<div id="flyarea">
<div id="ceiling" class="animated"></div>
<!-- This is the flying and pipe area container -->
<div id="player" class="bird animated"></div>
<div id="bigscore"></div>
<div id="splash"></div>
<div id="scoreboard">
<div id="medal"></div>
<div id="currentscore"></div>
<div id="highscore"></div>
<div id="replay"><img src="assets/replay.png" alt="replay"></div>
</div>
<!-- Pipes go here! -->
</div>
</div>
<div id="land" class="animated"><div id="debug"></div></div>
</div>
</div>
<div id="footer">
<a href="https://www.dotgears.com/">original game/concept/art by dong nguyen</a>
<a href="https://nebezb.com/">recreated by nebez briefkani</a>
<a href="https://github.com/nebez/floppybird/">view github project</a>
</div>
<div class="boundingbox" id="playerbox"></div>
<div class="boundingbox" id="pipebox"></div>
<script src="js/jquery.min.js"></script>
<script src="js/jquery.transit.min.js"></script>
<script src="js/buzz.min.js"></script>
<script src="js/main.js"></script>
<script>
function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
if (!inIframe() && window.location.hostname == 'nebezb.com') {
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-48047334-1', 'auto');
ga('send', 'pageview');
}
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,482 @@
var debugmode = false;
var states = Object.freeze({
SplashScreen: 0,
GameScreen: 1,
ScoreScreen: 2
});
var currentstate;
var gravity = 0.25;
var velocity = 0;
var position = 180;
var rotation = 0;
var jump = -4.6;
var flyArea = $("#flyarea").height();
var score = 0;
var highscore = 0;
var pipeheight = 90;
var pipewidth = 52;
var pipes = new Array();
var replayclickable = false;
//sounds
var volume = 30;
var soundJump = new buzz.sound("assets/sounds/sfx_wing.ogg");
var soundScore = new buzz.sound("assets/sounds/sfx_point.ogg");
var soundHit = new buzz.sound("assets/sounds/sfx_hit.ogg");
var soundDie = new buzz.sound("assets/sounds/sfx_die.ogg");
var soundSwoosh = new buzz.sound("assets/sounds/sfx_swooshing.ogg");
buzz.all().setVolume(volume);
//loops
var loopGameloop;
var loopPipeloop;
$(document).ready(function() {
if(window.location.search == "?debug")
debugmode = true;
if(window.location.search == "?easy")
pipeheight = 200;
//get the highscore
var savedscore = getCookie("highscore");
if(savedscore != "")
highscore = parseInt(savedscore);
//start with the splash screen
showSplash();
});
function getCookie(cname)
{
var name = cname + "=";
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++)
{
var c = ca[i].trim();
if (c.indexOf(name)==0) return c.substring(name.length,c.length);
}
return "";
}
function setCookie(cname,cvalue,exdays)
{
var d = new Date();
d.setTime(d.getTime()+(exdays*24*60*60*1000));
var expires = "expires="+d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires;
}
function showSplash()
{
currentstate = states.SplashScreen;
//set the defaults (again)
velocity = 0;
position = 180;
rotation = 0;
score = 0;
//update the player in preparation for the next game
$("#player").css({ y: 0, x: 0 });
updatePlayer($("#player"));
soundSwoosh.stop();
soundSwoosh.play();
//clear out all the pipes if there are any
$(".pipe").remove();
pipes = new Array();
//make everything animated again
$(".animated").css('animation-play-state', 'running');
$(".animated").css('-webkit-animation-play-state', 'running');
//fade in the splash
$("#splash").transition({ opacity: 1 }, 2000, 'ease');
}
function startGame()
{
currentstate = states.GameScreen;
//fade out the splash
$("#splash").stop();
$("#splash").transition({ opacity: 0 }, 500, 'ease');
//update the big score
setBigScore();
//debug mode?
if(debugmode)
{
//show the bounding boxes
$(".boundingbox").show();
}
//start up our loops
var updaterate = 1000.0 / 60.0 ; //60 times a second
loopGameloop = setInterval(gameloop, updaterate);
loopPipeloop = setInterval(updatePipes, 1400);
//jump from the start!
playerJump();
}
function updatePlayer(player)
{
//rotation
rotation = Math.min((velocity / 10) * 90, 90);
//apply rotation and position
$(player).css({ rotate: rotation, top: position });
}
function gameloop() {
var player = $("#player");
//update the player speed/position
velocity += gravity;
position += velocity;
//update the player
updatePlayer(player);
//create the bounding box
var box = document.getElementById('player').getBoundingClientRect();
var origwidth = 34.0;
var origheight = 24.0;
var boxwidth = origwidth - (Math.sin(Math.abs(rotation) / 90) * 8);
var boxheight = (origheight + box.height) / 2;
var boxleft = ((box.width - boxwidth) / 2) + box.left;
var boxtop = ((box.height - boxheight) / 2) + box.top;
var boxright = boxleft + boxwidth;
var boxbottom = boxtop + boxheight;
//if we're in debug mode, draw the bounding box
if(debugmode)
{
var boundingbox = $("#playerbox");
boundingbox.css('left', boxleft);
boundingbox.css('top', boxtop);
boundingbox.css('height', boxheight);
boundingbox.css('width', boxwidth);
}
//did we hit the ground?
if(box.bottom >= $("#land").offset().top)
{
playerDead();
return;
}
//have they tried to escape through the ceiling? :o
var ceiling = $("#ceiling");
if(boxtop <= (ceiling.offset().top + ceiling.height()))
position = 0;
//we can't go any further without a pipe
if(pipes[0] == null)
return;
//determine the bounding box of the next pipes inner area
var nextpipe = pipes[0];
var nextpipeupper = nextpipe.children(".pipe_upper");
var pipetop = nextpipeupper.offset().top + nextpipeupper.height();
var pipeleft = nextpipeupper.offset().left - 2; // for some reason it starts at the inner pipes offset, not the outer pipes.
var piperight = pipeleft + pipewidth;
var pipebottom = pipetop + pipeheight;
if(debugmode)
{
var boundingbox = $("#pipebox");
boundingbox.css('left', pipeleft);
boundingbox.css('top', pipetop);
boundingbox.css('height', pipeheight);
boundingbox.css('width', pipewidth);
}
//have we gotten inside the pipe yet?
if(boxright > pipeleft)
{
//we're within the pipe, have we passed between upper and lower pipes?
if(boxtop > pipetop && boxbottom < pipebottom)
{
//yeah! we're within bounds
}
else
{
//no! we touched the pipe
playerDead();
return;
}
}
//have we passed the imminent danger?
if(boxleft > piperight)
{
//yes, remove it
pipes.splice(0, 1);
//and score a point
playerScore();
}
}
//Handle space bar
$(document).keydown(function(e){
//space bar!
if(e.keyCode == 32)
{
//in ScoreScreen, hitting space should click the "replay" button. else it's just a regular spacebar hit
if(currentstate == states.ScoreScreen)
$("#replay").click();
else
screenClick();
}
});
//Handle mouse down OR touch start
if("ontouchstart" in window)
$(document).on("touchstart", screenClick);
else
$(document).on("mousedown", screenClick);
function screenClick()
{
if(currentstate == states.GameScreen)
{
playerJump();
}
else if(currentstate == states.SplashScreen)
{
startGame();
}
}
function playerJump()
{
velocity = jump;
//play jump sound
soundJump.stop();
soundJump.play();
}
function setBigScore(erase)
{
var elemscore = $("#bigscore");
elemscore.empty();
if(erase)
return;
var digits = score.toString().split('');
for(var i = 0; i < digits.length; i++)
elemscore.append("<img src='assets/font_big_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}
function setSmallScore()
{
var elemscore = $("#currentscore");
elemscore.empty();
var digits = score.toString().split('');
for(var i = 0; i < digits.length; i++)
elemscore.append("<img src='assets/font_small_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}
function setHighScore()
{
var elemscore = $("#highscore");
elemscore.empty();
var digits = highscore.toString().split('');
for(var i = 0; i < digits.length; i++)
elemscore.append("<img src='assets/font_small_" + digits[i] + ".png' alt='" + digits[i] + "'>");
}
function setMedal()
{
var elemmedal = $("#medal");
elemmedal.empty();
if(score < 10)
//signal that no medal has been won
return false;
if(score >= 10)
medal = "bronze";
if(score >= 20)
medal = "silver";
if(score >= 30)
medal = "gold";
if(score >= 40)
medal = "platinum";
elemmedal.append('<img src="assets/medal_' + medal +'.png" alt="' + medal +'">');
//signal that a medal has been won
return true;
}
function playerDead()
{
//stop animating everything!
$(".animated").css('animation-play-state', 'paused');
$(".animated").css('-webkit-animation-play-state', 'paused');
//drop the bird to the floor
var playerbottom = $("#player").position().top + $("#player").width(); //we use width because he'll be rotated 90 deg
var floor = flyArea;
var movey = Math.max(0, floor - playerbottom);
$("#player").transition({ y: movey + 'px', rotate: 90}, 1000, 'easeInOutCubic');
//it's time to change states. as of now we're considered ScoreScreen to disable left click/flying
currentstate = states.ScoreScreen;
//destroy our gameloops
clearInterval(loopGameloop);
clearInterval(loopPipeloop);
loopGameloop = null;
loopPipeloop = null;
//mobile browsers don't support buzz bindOnce event
if(isIncompatible.any())
{
//skip right to showing score
showScore();
}
else
{
//play the hit sound (then the dead sound) and then show score
soundHit.play().bindOnce("ended", function() {
soundDie.play().bindOnce("ended", function() {
showScore();
});
});
}
}
function showScore()
{
//unhide us
$("#scoreboard").css("display", "block");
//remove the big score
setBigScore(true);
//have they beaten their high score?
if(score > highscore)
{
//yeah!
highscore = score;
//save it!
setCookie("highscore", highscore, 999);
}
//update the scoreboard
setSmallScore();
setHighScore();
var wonmedal = setMedal();
//SWOOSH!
soundSwoosh.stop();
soundSwoosh.play();
//show the scoreboard
$("#scoreboard").css({ y: '40px', opacity: 0 }); //move it down so we can slide it up
$("#replay").css({ y: '40px', opacity: 0 });
$("#scoreboard").transition({ y: '0px', opacity: 1}, 600, 'ease', function() {
//When the animation is done, animate in the replay button and SWOOSH!
soundSwoosh.stop();
soundSwoosh.play();
$("#replay").transition({ y: '0px', opacity: 1}, 600, 'ease');
//also animate in the MEDAL! WOO!
if(wonmedal)
{
$("#medal").css({ scale: 2, opacity: 0 });
$("#medal").transition({ opacity: 1, scale: 1 }, 1200, 'ease');
}
});
//make the replay button clickable
replayclickable = true;
}
$("#replay").click(function() {
//make sure we can only click once
if(!replayclickable)
return;
else
replayclickable = false;
//SWOOSH!
soundSwoosh.stop();
soundSwoosh.play();
//fade out the scoreboard
$("#scoreboard").transition({ y: '-40px', opacity: 0}, 1000, 'ease', function() {
//when that's done, display us back to nothing
$("#scoreboard").css("display", "none");
//start the game over!
showSplash();
});
});
function playerScore()
{
score += 1;
//play score sound
soundScore.stop();
soundScore.play();
setBigScore();
}
function updatePipes()
{
//Do any pipes need removal?
$(".pipe").filter(function() { return $(this).position().left <= -100; }).remove()
//add a new pipe (top height + bottom height + pipeheight == flyArea) and put it in our tracker
var padding = 80;
var constraint = flyArea - pipeheight - (padding * 2); //double padding (for top and bottom)
var topheight = Math.floor((Math.random()*constraint) + padding); //add lower padding
var bottomheight = (flyArea - pipeheight) - topheight;
var newpipe = $('<div class="pipe animated"><div class="pipe_upper" style="height: ' + topheight + 'px;"></div><div class="pipe_lower" style="height: ' + bottomheight + 'px;"></div></div>');
$("#flyarea").append(newpipe);
pipes.push(newpipe);
}
var isIncompatible = {
Android: function() {
return navigator.userAgent.match(/Android/i);
},
BlackBerry: function() {
return navigator.userAgent.match(/BlackBerry/i);
},
iOS: function() {
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
Opera: function() {
return navigator.userAgent.match(/Opera Mini/i);
},
Safari: function() {
return (navigator.userAgent.match(/OS X.*Safari/) && ! navigator.userAgent.match(/Chrome/));
},
Windows: function() {
return navigator.userAgent.match(/IEMobile/i);
},
any: function() {
return (isIncompatible.Android() || isIncompatible.BlackBerry() || isIncompatible.iOS() || isIncompatible.Opera() || isIncompatible.Safari() || isIncompatible.Windows());
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "none", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es5", "es6", "dom"], /* Specify library files to be included in the compilation. */
"allowJs": false, /* Allow javascript files to be compiled. */
"checkJs": false, /* Report errors in .js files. */
"declaration": false, /* Generates corresponding '.d.ts' file. */
"declarationMap": false, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "./js/", /* Concatenate and emit output to single file. */
"removeComments": true, /* Do not emit comments to output. */
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */
},
"include": [
"ts/game.ts",
"ts/howler.d.ts"
]
}

View File

@@ -0,0 +1,11 @@
# 排除所有文件和目录
*
# 允许 index.html
!index.html
!favicon.ico
# 允许的目录(及其内容)
!remote_files/
!js/
!css/

View File

@@ -0,0 +1,4 @@
{
"account_id": "3fdbaad92364222635c5c1c41ff1af8b",
"project_name": "web-cube"
}

View File

@@ -0,0 +1,6 @@
{
"account": {
"id": "3fdbaad92364222635c5c1c41ff1af8b",
"name": "shumengya"
}
}

View File

@@ -0,0 +1,16 @@
FROM lipanski/docker-static-website:2.6.0
# 静态文件路径 /home/static
COPY . /home/static/
ENTRYPOINT ["/busybox-httpd", "-f", "-v"]
CMD [ "-p", "5146" ]
# 暴露端口
EXPOSE 5146
LABEL 镜像制作者="https://space.bilibili.com/17547201"
LABEL GitHub主页="https://github.com/Firfr/"
LABEL Gitee主页="https://gitee.com/firfe/"
# docker build -t firfe/h5cube:2025.09.17 .

View File

@@ -0,0 +1,71 @@
## 汉化&修改
3D魔方可以在浏览器中操作复原魔方不过没有自动复原的功能。
这个项目的原码时网上搜到的我做了含糊额和制作了docker镜像。
- 我汉化和构建docker镜像的
- GitHub仓库 https://github.com/Firfr/h5cube
- Gitee仓库 https://gitee.com/firfe/h5cube
欢迎关注我B站账号 [秦曱凧](https://space.bilibili.com/17547201) (读作 qín yuē zhēng)
有需要帮忙部署这个项目的朋友,一杯奶茶,即可程远程帮你部署,需要可联系。
微信号 `E-0_0-`
闲鱼搜索用户 `明月人间`
或者邮箱 `firfe163@163.com`
如果这个项目有帮到你。欢迎start。
有其他的项目的汉化需求欢迎提issue。或其他方式联系通知。
### 镜像
从阿里云或华为云镜像仓库拉取镜像,注意填写镜像标签,镜像仓库中没有`latest`标签
容器内部端口`5146`,可通过设置启动参数的值来指定监听端口。
```bash
swr.cn-north-4.myhuaweicloud.com/firfe/h5cube:2025.09.17
```
### docker run 命令部署
```bash
docker run -d \
--name h5cube \
--network bridge \
--restart always \
--log-opt max-size=1m \
--log-opt max-file=1 \
-p 5146:5146 \
swr.cn-north-4.myhuaweicloud.com/firfe/h5cube:2025.09.17
```
在命令最后追加`-p 端口`自定义端口
### compose 文件部署 👍推荐
```yaml
#version: '3'
name: h5cube
services:
h5cube:
container_name: h5cube
image: swr.cn-north-4.myhuaweicloud.com/firfe/h5cube:2025.09.17
network_mode: bridge
restart: always
logging:
options:
max-size: 1m
max-file: '1'
ports:
- 5146:5146
# 指定端口
# command: ["-p", "自定义端口"]
```
### 效果截图
| ![设置](图片/设置.jpg) | ![首页](图片/首页.jpg) |
|-|-|

View File

@@ -0,0 +1,344 @@
*, *:before, *:after {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
box-sizing: border-box;
cursor: inherit;
margin: 0;
padding: 0;
outline: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
font-style: inherit;
text-transform: uppercase;
}
*:focus {
outline: none;
}
html {
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
overflow: hidden;
height: 100%;
}
body {
font-family: "BungeeFont", sans-serif;
font-weight: normal;
font-style: normal;
line-height: 1;
cursor: default;
overflow: hidden;
height: 100%;
font-size: 5rem;
}
.icon {
display: inline-block;
font-size: inherit;
overflow: visible;
vertical-align: -0.125em;
preserveAspectRatio: none;
}
.range {
position: relative;
width: 14em;
z-index: 1;
opacity: 0;
}
.range:not(:last-child) {
margin-bottom: 2em;
}
.range__label {
position: relative;
font-size: 0.9em;
line-height: 0.75em;
padding-bottom: 0.5em;
z-index: 2;
}
.range__track {
position: relative;
height: 1em;
margin-left: 0.5em;
margin-right: 0.5em;
z-index: 3;
}
.range__track-line {
position: absolute;
background: rgba(0, 0, 0, 0.2);
height: 2px;
top: 50%;
margin-top: -1px;
left: -0.5em;
right: -0.5em;
transform-origin: left center;
}
.range__handle {
position: absolute;
width: 0;
height: 0;
top: 50%;
left: 0;
cursor: pointer;
z-index: 1;
}
.range__handle div {
transition: background 500ms ease;
position: absolute;
left: 0;
top: 0;
width: 0.9em;
height: 0.9em;
border-radius: 0.2em;
margin-left: -0.45em;
margin-top: -0.45em;
background: #41aac8;
border-bottom: 2px solid rgba(0, 0, 0, 0.2);
}
.range.is-active .range__handle div {
transform: scale(1.25);
}
.range__handle:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 3em;
height: 3em;
margin-left: -1.5em;
margin-top: -1.5em;
}
.range__list {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
position: relative;
padding-top: 0.5em;
font-size: 0.55em;
color: rgba(0, 0, 0, 0.5);
z-index: 1;
}
.stats {
position: relative;
width: 14em;
z-index: 1;
display: flex;
justify-content: space-between;
opacity: 0;
}
.stats:not(:last-child) {
margin-bottom: 1.5em;
}
.stats i {
display: inline-block;
color: rgba(0, 0, 0, 0.5);
font-size: 0.9em;
}
.stats b {
display: inline-block;
font-size: 0.9em;
}
.text {
position: absolute;
left: 0;
right: 0;
text-align: center;
line-height: 0.75;
perspective: 100rem;
opacity: 0;
}
.text i {
display: inline-block;
opacity: 0;
white-space: pre-wrap;
}
.text--title {
bottom: 75%;
font-size: 4.4em;
height: 1.2em;
}
.text--title span {
display: block;
}
.text--title span:first-child {
font-size: 0.5em;
margin-bottom: 0.2em;
}
.text--note {
top: 87%;
font-size: 1em;
}
.text--timer {
bottom: 78%;
font-size: 3.5em;
line-height: 1;
}
.text--complete, .text--best-time {
font-size: 1.5em;
top: 83%;
line-height: 1em;
}
.btn {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-color: transparent;
border-radius: 0;
border-width: 0;
position: absolute;
pointer-events: none;
font-size: 1.2em;
color: rgba(0, 0, 0, 0.25);
opacity: 0;
}
.btn:after {
position: absolute;
content: "";
width: 3em;
height: 3em;
left: 50%;
top: 50%;
margin-left: -1.5em;
margin-top: -1.5em;
border-radius: 100%;
}
.btn--bl {
bottom: 0.8em;
left: 0.8em;
}
.btn--br {
bottom: 0.8em;
right: 0.8em;
}
.btn--bc {
bottom: 0.8em;
left: calc(50% - 0.5em);
}
.btn--pwa {
transition: color 500ms ease;
color: inherit;
height: 1em;
}
.btn--pwa svg {
font-size: 0.6em;
margin: 0.35em 0;
}
.btn svg {
display: block;
}
.ui {
pointer-events: none;
color: #070d15;
}
.ui, .ui__background, .ui__game, .ui__texts, .ui__prefs, .ui__stats, .ui__buttons {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
.ui__background {
z-index: 1;
transition: background 500ms ease;
background: #d1d5db;
}
.ui__background:after {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
content: "";
background-image: linear-gradient(to bottom, white 50%, rgba(255, 255, 255, 0) 100%);
}
.ui__game {
pointer-events: all;
z-index: 2;
}
.ui__game canvas {
display: block;
width: 100%;
height: 100%;
}
.ui__texts {
z-index: 3;
}
.ui__prefs, .ui__stats {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
overflow: hidden;
z-index: 4;
}
.ui__buttons {
z-index: 5;
}
.ui__notification {
transition: transform 500ms ease, opacity 500ms ease;
font-family: sans-serif;
position: absolute;
left: 50%;
bottom: 0.6em;
padding: 0.6em;
margin-left: -9.4em;
width: 18.8em;
z-index: 6;
background: rgba(17, 17, 17, 0.9);
border-radius: 0.8em;
display: flex;
align-items: center;
flex-flow: row nowrap;
color: #fff;
user-select: none;
opacity: 0;
pointer-events: none;
transform: translateY(100%);
}
.ui__notification.is-active {
opacity: 1;
pointer-events: all;
transform: none;
}
.ui__notification * {
text-transform: none;
}
.ui__notification-icon {
background-size: 100% 100%;
left: 0.6em;
top: 0.6em;
width: 2.8em;
height: 2.8em;
border-radius: 0.5em;
background: #fff;
margin-right: 0.6em;
display: block;
}
.ui__notification-text {
font-size: 0.75em;
line-height: 1.4em;
}
.ui__notification-text .icon {
color: #4f82fd;
font-size: 1.1em;
}
.ui__notification-text b {
font-weight: 700;
}
.btn--stats {
visibility: hidden;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@@ -0,0 +1,86 @@
<!DOCTYPE html>
<html lang="zh" >
<head>
<meta charset="UTF-8">
<title>网页魔方</title>
<meta name="viewport" content="width=device-width,height=device-height,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="ui">
<div class="ui__background"></div>
<div class="ui__game"></div>
<div class="ui__texts">
<h1 class="text text--title">
<span>魔方</span>
<span>游戏</span>
</h1>
<div class="text text--note">
双击魔方即可开始
</div>
<div class="text text--timer">
0:00
</div>
<div class="text text--complete">
<span>完成!</span>
</div>
<div class="text text--best-time">
<icon trophy></icon>
<span>最佳时间!</span>
</div>
</div>
<div class="ui__prefs">
<range name="flip" title="翻转类型" list="快速,平滑,弹跳"></range>
<range name="scramble" title="打乱步数" list="20,25,30"></range>
<range name="fov" title="摄像机角度" list="正交,透视"></range>
<range name="theme" title="配色方案" list="经典,埃尔诺,尘埃,迷彩,彩虹"></range>
</div>
<div class="ui__stats">
<div class="stats" name="total-solves">
<i>总解决次数:</i><b>-</b>
</div>
<div class="stats" name="best-time">
<i>最佳时间:</i><b>-</b>
</div>
<div class="stats" name="worst-time">
<i>最差时间:</i><b>-</b>
</div>
<div class="stats" name="average-5">
<i>5次平均:</i><b>-</b>
</div>
<div class="stats" name="average-12">
<i>12次平均:</i><b>-</b>
</div>
<div class="stats" name="average-25">
<i>25次平均:</i><b>-</b>
</div>
</div>
<div class="ui__buttons">
<button class="btn btn--bl btn--stats">
<icon trophy></icon>
</button>
<button class="btn btn--bl btn--prefs">
<icon settings></icon>
</button>
<button class="btn btn--bl btn--back">
<icon back></icon>
</button>
<button class="btn btn--br btn--pwa">
</button>
</div>
</div>
<script src='js/three.min.js'></script>
<script src="js/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,24 @@
# 源码修改说明
这里记录除了汉化之外的代码修改
## [index.html](index.html)
- 2 `en` => `zh`
- 21 `THE` => `魔方`
- 22 `CUBE` => `游戏`
- 31 `Complete` => `完成`
- 35 `Best Time` => `最佳时间`
- 40 `Flip Type` => `翻转类型`
- 40 `Swift&nbsp;,Smooth,Bounce` => `快速,平滑,弹跳`
- 41 `Scramble Length` => `打乱步数`
- 42 `Camera Angle` => `摄像机角度`
- 42 `Ortographic,Perspective` => `正交,透视`
- 43 `Color Scheme` => `配色方案`
- 43 `Cube,Erno,Dust,Camo,Rain` => `经典,埃尔诺,尘埃,迷彩,彩虹`
- 48 `Total solves` => `总解决次数`
- 51 `Best time` => `最佳时间`
- 54 `Worst time` => `最差时间`
- 57 `Average of 5` => `5次平均`
- 60 `Average of 12` => `12次平均`
- 63 `Average of 25` => `25次平均`

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

View File

@@ -1,96 +0,0 @@
// 游戏结束排行榜展示
const gameStats = {
showStats({ score, playTime }) {
// 将毫秒转为 mm:ss
const formatDuration = (ms) => {
const totalSec = Math.max(0, Math.floor(ms / 1000));
const m = String(Math.floor(totalSec / 60)).padStart(2, '0');
const s = String(totalSec % 60).padStart(2, '0');
return `${m}:${s}`;
};
// 构造排行榜数据(模拟),将当前成绩与 gamedata.js 合并
const todayStr = (() => {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
})();
// 当前玩家信息(可根据实际项目替换为真实用户)
const currentEntry = {
名称: localStorage.getItem('tetris_player_name') || '我',
账号: localStorage.getItem('tetris_player_account') || 'guest@local',
分数: score,
时间: formatDuration(playTime), // 排行榜展示“游戏时长”
isCurrent: true,
};
// 注意:在浏览器中,使用 const 声明的全局变量不会挂载到 window 上
// 因此这里直接使用 playerdata而不是 window.playerdata
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
// 为基础数据模拟“游戏时长”mm:ss以满足展示需求
const simulateDuration = (scoreVal) => {
const sec = Math.max(30, Math.min(30 * 60, Math.round((Number(scoreVal) || 0) * 1.2)));
return formatDuration(sec * 1000);
};
const merged = [...baseData.map((d) => ({
...d,
// 使用已有分数推导一个模拟时长
时间: simulateDuration(d.分数),
isCurrent: false,
})), currentEntry]
.sort((a, b) => (b.分数 || 0) - (a.分数 || 0));
// 3) 渲染排行榜取前10
const tbody = document.getElementById('leaderboardBody');
tbody.innerHTML = '';
const topN = merged.slice(0, 10);
topN.forEach((item, idx) => {
const tr = document.createElement('tr');
if (item.isCurrent) {
tr.classList.add('current-row');
}
const rankCell = document.createElement('td');
const nameCell = document.createElement('td');
const scoreCell = document.createElement('td');
const timeCell = document.createElement('td');
const rankBadge = document.createElement('span');
rankBadge.className = 'rank-badge';
rankBadge.textContent = String(idx + 1);
rankCell.appendChild(rankBadge);
nameCell.textContent = item.名称 || '未知';
scoreCell.textContent = item.分数 || 0;
timeCell.textContent = item.时间 || formatDuration(playTime);
tr.appendChild(rankCell);
tr.appendChild(nameCell);
tr.appendChild(scoreCell);
tr.appendChild(timeCell);
tbody.appendChild(tr);
});
// 4) 展示排行榜界面
const statsEl = document.getElementById('gameStats');
statsEl.style.display = 'flex';
// 5) 再玩一次按钮
const playAgainBtn = document.getElementById('playAgainBtn');
if (playAgainBtn) {
playAgainBtn.onclick = () => {
statsEl.style.display = 'none';
if (window.game && typeof window.game.restart === 'function') {
window.game.restart();
}
};
}
},
};
// 暴露到全局
window.gameStats = gameStats;

View File

@@ -1,20 +0,0 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -43,7 +43,6 @@
</div>
</div>
<!-- 手机端触摸控制 -->
<div class="mobile-controls">
<div class="mobile-controls-left">
<button class="control-btn" id="rotateBtn"></button>
@@ -58,35 +57,15 @@
</div>
</div>
<!-- 游戏结束统计界面 -->
<div class="game-stats" id="gameStats">
<div class="stats-content">
<h2>游戏结束排行榜</h2>
<!-- 排行榜 -->
<div class="leaderboard" id="leaderboard">
<div class="leaderboard-title">本局排行榜</div>
<div class="leaderboard-wrap">
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>名称</th>
<th>分数</th>
<th>游戏时长</th>
</tr>
</thead>
<tbody id="leaderboardBody"></tbody>
</table>
</div>
<div class="leaderboard-tip">仅显示前10名“游戏时长”为模拟数据已与您的成绩合并</div>
</div>
<h2>游戏结束</h2>
<div class="end-summary" id="endSummary"></div>
<button class="game-btn" id="playAgainBtn">再玩一次</button>
</div>
</div>
<script src="tetris.js"></script>
<script src="game-controls.js"></script>
<script src="gamedata.js"></script>
<script src="game-stats.js"></script>
</body>
</html>

View File

@@ -319,82 +319,13 @@ body {
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.3);
}
/* 排行榜样式 */
.leaderboard {
background: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
.end-summary {
margin: 16px 0 24px;
font-size: 1.05rem;
line-height: 1.8;
}
.end-summary strong {
color: #2e7d32;
border: 1px solid rgba(46, 125, 50, 0.3);
border-radius: 16px;
box-shadow: 0 6px 18px rgba(46, 125, 50, 0.25);
padding: 16px;
margin-bottom: 20px;
}
.leaderboard-title {
font-weight: 700;
font-size: 1.2rem;
margin-bottom: 12px;
background: linear-gradient(135deg, #4caf50 0%, #8bc34a 50%, #cddc39 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.leaderboard-wrap {
max-height: 260px;
overflow: auto;
border-radius: 12px;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table thead tr {
background: linear-gradient(135deg, #66bb6a 0%, #8bc34a 100%);
color: #fff;
}
.leaderboard-table th,
.leaderboard-table td {
text-align: left;
padding: 10px 12px;
border-bottom: 1px solid rgba(46, 125, 50, 0.15);
font-size: 0.95rem;
}
.leaderboard-table tbody tr {
background: linear-gradient(135deg, rgba(46,125,50,0.08) 0%, rgba(46,125,50,0.03) 100%);
transition: background 0.2s ease, transform 0.2s ease;
}
.leaderboard-table tbody tr:hover {
background: linear-gradient(135deg, rgba(46,125,50,0.12) 0%, rgba(46,125,50,0.06) 100%);
transform: translateY(-1px);
}
.rank-badge {
display: inline-block;
min-width: 32px;
text-align: center;
padding: 4px 8px;
border-radius: 12px;
background: linear-gradient(45deg, #66bb6a, #8bc34a);
color: #fff;
font-weight: 700;
}
.current-row {
outline: 2px solid rgba(76, 175, 80, 0.7);
box-shadow: 0 0 0 4px rgba(76, 175, 80, 0.15) inset;
}
.leaderboard-tip {
margin-top: 10px;
font-size: 0.85rem;
color: #388e3c;
opacity: 0.85;
}
/* 响应式设计 */
@@ -457,14 +388,6 @@ body {
width: 95%;
}
.leaderboard-wrap {
max-height: 200px;
}
.leaderboard-table th,
.leaderboard-table td {
padding: 8px 10px;
font-size: 0.9rem;
}
}
@media (max-width: 480px) {
@@ -536,39 +459,3 @@ body {
.pulse {
animation: pulse 2s infinite;
}
/* 摘要卡片 */
.leaderboard-summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.summary-item {
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
color: #fff;
padding: 12px;
border-radius: 10px;
border: 1px solid rgba(46, 125, 50, 0.3);
box-shadow: 0 4px 8px rgba(46, 125, 50, 0.2);
}
.summary-label {
display: block;
font-size: 0.9rem;
opacity: 0.9;
}
.summary-value {
display: block;
font-size: 1.3rem;
font-weight: 700;
margin-top: 4px;
}
@media (max-width: 768px) {
.leaderboard-summary {
grid-template-columns: 1fr;
gap: 10px;
}
}

View File

@@ -402,15 +402,28 @@ class TetrisGame {
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
});
const playMs = Date.now() - this.gameStartTime;
const sec = Math.floor(playMs / 1000);
const m = String(Math.floor(sec / 60)).padStart(2, '0');
const s = String(sec % 60).padStart(2, '0');
const summary = document.getElementById('endSummary');
if (summary) {
summary.innerHTML =
`<p>得分 <strong>${this.score}</strong> · 等级 <strong>${this.level}</strong></p>` +
`<p>消除 <strong>${this.lines}</strong> 行 · 用时 <strong>${m}:${s}</strong></p>`;
}
const statsEl = document.getElementById('gameStats');
if (statsEl) statsEl.style.display = 'flex';
const btn = document.getElementById('playAgainBtn');
if (btn) {
btn.onclick = () => {
statsEl.style.display = 'none';
if (window.game && typeof window.game.restart === 'function') window.game.restart();
};
}
}
gameLoop(currentTime = 0) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,413 +0,0 @@
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);
// 渲染排行榜
renderLeaderboard();
// 显示游戏结束弹窗
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 formatDateYYYYMMDD() {
var d = new Date();
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
return y + '-' + m + '-' + day;
}
function escapeHtml(str) {
if (typeof str !== 'string') return str;
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderLeaderboard(){
var nowStr = formatDateYYYYMMDD();
// 当前玩家数据(模拟)
var me = {
"名称": "我",
"账号": "guest",
"分数": myScore,
"时间": nowStr,
"__isMe": true
};
// 合并现有数据与当前玩家
var data = (typeof playerdata !== 'undefined' && Array.isArray(playerdata))
? playerdata.slice() : [];
data.push(me);
// 按分数降序排序
data.sort(function(a, b){
return (b["分数"] || 0) - (a["分数"] || 0);
});
var tbody = document.getElementById('leaderboard-body');
if (!tbody) return;
tbody.innerHTML = '';
var myRank = -1;
for (var i = 0; i < data.length; i++){
var row = data[i];
var tr = document.createElement('tr');
if (row.__isMe){
myRank = i + 1;
tr.className = 'leaderboard-row-me';
}
tr.innerHTML =
'<td>' + (i + 1) + '</td>' +
'<td>' + escapeHtml(row["名称"] || '') + '</td>' +
'<td>' + (row["分数"] || 0) + '</td>' +
'<td>' + escapeHtml(row["时间"] || '') + '</td>';
// 只展示前10名
if (i < 10) tbody.appendChild(tr);
}
// 更新我的数据摘要
var rankEl = document.getElementById('my-rank');
var scoreEl = document.getElementById('my-score');
var timeEl = document.getElementById('my-time');
if (rankEl) rankEl.textContent = myRank > 0 ? myRank : '-';
if (scoreEl) scoreEl.textContent = myScore;
if (timeEl) timeEl.textContent = nowStr;
}
// 处理触摸事件
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
}
}
}

View File

@@ -1,20 +0,0 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -1,334 +1,102 @@
<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: linear-gradient(135deg, #a8e6cf 0%, #dcedc8 50%, #f1f8e9 100%);
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: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(46,125,50,0.3);
padding: 20px;
position: relative;
border: 1px solid rgba(129,199,132,0.2);
}
.game-title {
font-size: 24px;
font-weight: bold;
color: #1b5e20;
margin-bottom: 10px;
text-align: center;
text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
}
.score-display {
position: relative;
width: 300px;
height: 60px;
background: linear-gradient(135deg, #2e7d32 0%, #388e3c 100%);
border-radius: 8px 8px 0 0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(46,125,50,0.3);
}
.game-container {
position: relative;
width: 300px;
height: 600px;
border: 3px solid #2e7d32;
border-top: none;
border-radius: 0 0 8px 8px;
overflow: hidden;
background: white;
box-shadow: 0 4px 12px rgba(46,125,50,0.2);
}
.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: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
box-shadow: 0 4px 12px rgba(76,175,80,0.3);
}
.start-btn:hover {
background: linear-gradient(45deg, #4caf50, #388e3c);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(76,175,80,0.4);
}
.pause-btn {
background: linear-gradient(45deg, #81c784, #66bb6a);
color: white;
box-shadow: 0 4px 12px rgba(129,199,132,0.3);
}
.pause-btn:hover {
background: linear-gradient(45deg, #66bb6a, #4caf50);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(129,199,132,0.4);
}
.instructions {
text-align: center;
color: #2e7d32;
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: linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%);
padding: 30px;
border-radius: 15px;
text-align: center;
box-shadow: 0 20px 40px rgba(46,125,50,0.3);
max-width: 300px;
width: 90%;
border: 1px solid rgba(129,199,132,0.3);
}
.modal-title {
font-size: 24px;
font-weight: bold;
color: #c62828;
margin-bottom: 15px;
text-shadow: 1px 1px 2px rgba(255,255,255,0.5);
}
.final-score, .final-speed {
font-size: 18px;
margin: 15px 0;
color: #1b5e20;
}
.final-speed {
color: #2e7d32;
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: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
box-shadow: 0 4px 12px rgba(76,175,80,0.3);
}
.restart-btn:hover {
background: linear-gradient(45deg, #4caf50, #388e3c);
box-shadow: 0 6px 16px rgba(76,175,80,0.4);
}
/* 排行榜样式 */
.leaderboard {
margin-top: 15px;
background: rgba(255,255,255,0.6);
border: 1px solid rgba(129,199,132,0.3);
border-radius: 10px;
overflow: hidden;
}
.leaderboard-title {
background: linear-gradient(45deg, #66bb6a, #4caf50);
color: white;
font-weight: bold;
font-size: 16px;
padding: 8px 12px;
text-align: left;
box-shadow: inset 0 -1px 0 rgba(255,255,255,0.2);
}
.leaderboard-meta {
color: #2e7d32;
font-size: 13px;
padding: 8px 12px;
border-bottom: 1px solid rgba(129,199,132,0.2);
display: flex;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
}
.leaderboard-table {
width: 100%;
border-collapse: collapse;
}
.leaderboard-table th, .leaderboard-table td {
padding: 8px 6px;
font-size: 13px;
border-bottom: 1px solid rgba(129,199,132,0.2);
color: #1b5e20;
text-align: center;
}
.leaderboard-table th {
background: rgba(129,199,132,0.2);
font-weight: bold;
color: #1b5e20;
}
.leaderboard-row-me {
background: rgba(198,40,40,0.08);
border-left: 3px solid #c62828;
}
.leaderboard-table tr:nth-child(even) {
background: rgba(129,199,132,0.1);
}
/* 移动端适配 */
@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>
<!-- 排行榜区域 -->
<div class="leaderboard">
<div class="leaderboard-title">排行榜</div>
<div class="leaderboard-meta">
<span>我的排名:第 <span id="my-rank">-</span></span>
<span>我的分数:<span id="my-score">0</span></span>
<span>时间:<span id="my-time">--</span></span>
</div>
<table class="leaderboard-table">
<thead>
<tr>
<th>排名</th>
<th>名称</th>
<th>分数</th>
<th>时间</th>
</tr>
</thead>
<tbody id="leaderboard-body"></tbody>
</table>
</div>
<button id="restart-btn" class="modal-btn restart-btn">重新开始</button>
</div>
</div>
<audio id="music" src="MUSIC.mp3" loop></audio>
<script src="gamedata.js"></script>
<script src="game.js"></script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>别踩白方块</title>
<link rel="icon"
href="favicon.png">
<script>
(function (doc, win) {
var docEl = doc.documentElement
var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'
function recalc() {
var designWidth = 750
var clientWidth = docEl.clientWidth
if (!clientWidth || clientWidth > designWidth) {
clientWidth = designWidth
}
docEl.style.fontSize = (100 * clientWidth / designWidth) + 'px'
}
if (!doc.addEventListener) return
win.addEventListener(resizeEvt, recalc, false)
doc.addEventListener('DOMContentLoaded', recalc, false)
recalc()
})(document, window)
</script>
<link rel="stylesheet" href="./styles/basic.css?v=201902102023">
</head>
<body>
<div class="modal white-bg" id="init-modal">
<div class="init-modal-content">
<!--<p class="title">别踩白方块</p>-->
<div class="modal-btn" id="classics-btn">经典</div>
<div class="modal-btn" id="topspeed-btn">极速</div>
<div class="modal-btn disable" id="arcade-btn">街机</div>
<div class="modal-btn" id="history-btn">历史记录</div>
<a target="_blank" href="https://github.com/QiShaoXuan/dont-touch-white" class="modal-btn">原项目仓库</a>
<a target="_blank" href="https://github.com/Firfr/dont-touch-white" class="modal-btn" style="background: #ebf4f4;">镜像制作仓库</a>
</div>
</div>
<div class="modal dim-bg" id="score-modal" style="display: none;">
<div class="modal-content">
<p class="title">游戏结束</p>
<p class="content">得分:<span id="score"></span></p>
<p class="content">历史最高分:<span id="history-score"></span></p>
<div class="modal-btn" id="topspeed-reset" data-modal="#score-modal">重新开始</div>
<div class="modal-btn back-btn" data-modal="#score-modal">返回首页</div>
</div>
</div>
<div class="modal dim-bg" id="coding-modal" style="display: none;">
<div class="modal-content">
<p class="title">正在开发 ...</p>
<div class="modal-btn modal-close-btn" data-modal="#coding-modal">关闭</div>
</div>
</div>
<div class="modal dim-bg" id="history-modal" style="display: none;">
<div class="modal-content">
<p class="title">历史记录</p>
<p class="content">极速模式:<span id="history-topspeed-score"></span></p>
<p class="content">经典模式:<span id="history-classics-score">--</span></p>
<div class="modal-btn back-btn" data-modal="#history-modal">返回首页</div>
</div>
</div>
<div class="topspeed-container hide">
<div class="score-container topspeed">0</div>
<div class="container topspeed" id="topspeed-container"></div>
</div>
<div class="classics-container">
<div class="score-container classics">
<div><span id="remaining-time">60</span>''</div>
<!--<span id="classics-score">0</span>-->
</div>
<div class="container classics" id="classics-container"></div>
</div>
<!--<div class="toggle-btn"></div>-->
</body>
<script src="./scripts/topspeed.js?v=201902102023"></script>
<script src="./scripts/classics.js?v=201902102023"></script>
<script src="./scripts/index.js?v=201902102023"></script>
<script>
// const btn = document.querySelector('.toggle-btn')
//
// btn.addEventListener('click', function () {
// if (topspeed.status == 0) {
// topspeed.start()
// } else {
// topspeed.pause()
// }
// })
</script>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
"use strict";var _createClass=function(){function n(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(t,e,i){return e&&n(t.prototype,e),i&&n(t,i),t}}();function _classCallCheck(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var Classics=function(){function e(t){_classCallCheck(this,e),this.container=document.querySelector(t.container),this.scoreContainer=document.querySelector(t.scoreContainer),this.timeContainer=document.querySelector(t.timeContainer),this.overModal=document.querySelector(t.over.modal),this.scoreSpan=document.querySelector(t.over.score),this.historyscoreSpan=document.querySelector(t.over.historyScore),this.containerHeight=this.container.getClientRects()[0].height,this.bodyHeight=document.body.getClientRects()[0].height,this.frame=null,this.status=0,this.score=0,this.second=30,this.historyScore=localStorage.getItem("donttouchwhiteClassics")?Number(localStorage.getItem("donttouchwhiteClassics")):0}return _createClass(e,[{key:"init",value:function(){var i=this;this.score=0,this.container.innerHTML="",this.timeContainer.innerText=this.second,this.container.onclick=function(t){t.stopPropagation();var e=[].indexOf.call(t.target.parentNode.parentNode.querySelectorAll(t.target.tagName),t.target.parentNode);t.target.classList.contains("cube")&&(t.target.classList.contains("black")&&5===e&&(i.updateScore(),i.animate()),t.target.classList.contains("black")||i.gameover())}}},{key:"updateScore",value:function(){this.score+=1}},{key:"timeout",value:function(t,e){var i=this,n=(e-.1).toFixed(1);0!==this.status&&(0<=n?setTimeout(function(){t.innerText=n,i.timeout(t,n)},100):this.gameover())}},{key:"animate",value:function(){this.container.appendChild(this.setRow()),this.container.removeChild(this.container.firstElementChild)}},{key:"gameover",value:function(){this.status=0,this.overModal.style.display="flex",this.score>this.historyScore&&(this.updateHistoryScore(this.score),this.historyScore=this.score),this.scoreSpan.innerHTML=this.score,this.historyscoreSpan.innerHTML=this.historyScore}},{key:"updateHistoryScore",value:function(t){localStorage.setItem("donttouchwhiteClassics",t)}},{key:"start",value:function(){this.status=1,this.init();for(var t=0;t<7;t++)this.container.appendChild(this.setRow());this.timeout(this.timeContainer,this.second)}},{key:"setRow",value:function(){var t=document.createElement("div");return t.innerHTML='<div class="row">'+this.setCube(this.getRandom())+"</div>",t.firstChild}},{key:"setCube",value:function(t){for(var e=1<arguments.length&&void 0!==arguments[1]?arguments[1]:4,i="",n=0;n<e;n++)i+='<div class="cube '+(n===t?"black":"")+'"></div>';return i}},{key:"getRandom",value:function(){return parseInt(4*Math.random(),10)}}]),e}();

View File

@@ -0,0 +1 @@
"use strict";function $(e){return 1<arguments.length&&void 0!==arguments[1]&&arguments[1]?document.querySelectorAll(e):document.querySelector(e)}var gameType="";function initGame(e){switch(gameType=e){case"topspeed":$(".topspeed-container").classList.remove("hide"),$(".classics-container").classList.add("hide");break;case"classics":$(".topspeed-container").classList.add("hide"),$(".classics-container").classList.remove("hide")}}var topspeed=new Topspeed({container:"#topspeed-container",scoreContainer:".score-container.topspeed",over:{modal:"#score-modal",score:"#score",historyScore:"#history-score"}}),classics=new Classics({container:"#classics-container",timeContainer:"#remaining-time",scoreContainer:"#classics-score",over:{modal:"#score-modal",score:"#score",historyScore:"#history-score"}}),topSpeedBtn=$("#topspeed-btn"),classicsBtn=$("#classics-btn"),disableBtns=$(".modal-btn.disable",!0),closeBtns=$(".modal-close-btn",!0),backBtns=$(".back-btn",!0),resetBtn=$("#topspeed-reset"),historyBtn=$("#history-btn"),initModal=$("#init-modal");disableBtns.forEach(function(e){e.addEventListener("click",function(){$("#coding-modal").style.display="flex"})}),closeBtns.forEach(function(e){e.addEventListener("click",function(){$(this.dataset.modal).style.display="none"})}),topSpeedBtn.addEventListener("click",function(){initGame("topspeed"),initModal.style.display="none",topspeed.start()}),classicsBtn.addEventListener("click",function(){initGame("classics"),initModal.style.display="none",classics.start()}),backBtns.forEach(function(e){e.addEventListener("click",function(){$(this.dataset.modal).style.display="none",initModal.style.display="flex"})}),resetBtn.addEventListener("click",function(){switch($(this.dataset.modal).style.display="none",gameType){case"topspeed":topspeed.start();break;case"classics":classics.start()}}),historyBtn.addEventListener("click",function(){$("#history-modal").style.display="flex",$("#history-topspeed-score").innerHTML=topspeed.historyScore,$("#history-classics-score").innerHTML=classics.historyScore});

View File

@@ -0,0 +1 @@
"use strict";

View File

@@ -0,0 +1 @@
"use strict";var _createClass=function(){function s(e,t){for(var i=0;i<t.length;i++){var s=t[i];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}return function(e,t,i){return t&&s(e.prototype,t),i&&s(e,i),e}}();function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var Topspeed=function(){function t(e){_classCallCheck(this,t),this.container=document.querySelector(e.container),this.scoreContainer=document.querySelector(e.scoreContainer),this.overModal=document.querySelector(e.over.modal),this.scoreSpan=document.querySelector(e.over.score),this.historyscoreSpan=document.querySelector(e.over.historyScore),this.containerHeight=this.container.getClientRects()[0].height,this.bodyHeight=document.body.getClientRects()[0].height,this.frame=null,this.step=4,this.status=0,this.score=0,this.historyScore=localStorage.getItem("donttouchwhiteTopspeed")?Number(localStorage.getItem("donttouchwhiteTopspeed")):0,this.increaseBasic=6,this.lastIncrease=0}return _createClass(t,[{key:"init",value:function(){var t=this;this.container.innerHTML="",this.container.appendChild(this.setRow()),this.step=3,this.increaseBasic=6,this.lastIncrease=0,this.score=0,this.scoreContainer.innerHTML=this.score,this.container.onclick=function(e){e.stopPropagation(),e.target.classList.contains("cube")&&(e.target.classList.contains("black")?t.isFirstLine(e.target.parentNode)&&(e.target.classList.remove("black"),e.target.classList.add("toGray"),t.updateScore(),t.checkIncreaseDifficulty()):e.target.classList.contains("toGray")||t.gameover())}}},{key:"isFirstLine",value:function(e){return!e.previousElementSibling||null===e.previousElementSibling.querySelector(".cube.black")}},{key:"updateScore",value:function(){this.score+=1,this.scoreContainer.innerHTML=this.score}},{key:"checkIncreaseDifficulty",value:function(){this.score-this.lastIncrease===this.increaseBasic&&(this.lastIncrease=this.score,this.increaseBasic+=1,this.step+=.5)}},{key:"start",value:function(){this.status=1,this.init(),this.animateTopspeed()}},{key:"animateTopspeed",value:function(){var i=this;this.checkToAppend();var e=this.container.querySelectorAll(".row"),t=this;e.forEach(function(e){var t=Number(e.dataset.y);e.style.transform="translateY("+(t+i.step)+"px)",e.dataset.y=t+i.step}),this.frame=requestAnimationFrame(function(){t.animateTopspeed()}),this.checkBlackToBottom(),this.checkToRemove()}},{key:"checkBlackToBottom",value:function(){var e=this.container.firstElementChild;Number(e.dataset.y)>this.bodyHeight&&1===[].filter.call(e.childNodes,function(e){return e.classList.contains("black")}).length&&this.gameover()}},{key:"gameover",value:function(){this.pause(),this.overModal.style.display="flex",this.score>this.historyScore&&(this.updateHistoryScore(this.score),this.historyScore=this.score),this.scoreSpan.innerHTML=this.score,this.historyscoreSpan.innerHTML=this.historyScore}},{key:"updateHistoryScore",value:function(e){localStorage.setItem("donttouchwhiteTopspeed",e)}},{key:"pause",value:function(){this.status=0,cancelAnimationFrame(this.frame)}},{key:"checkToAppend",value:function(){var e=this.container.lastElementChild;Number(e.dataset.y)+this.step>=this.containerHeight&&this.container.appendChild(this.setRow())}},{key:"checkToRemove",value:function(){var e=this.container.firstElementChild;Number(e.dataset.y)>this.bodyHeight+this.containerHeight&&this.container.removeChild(e)}},{key:"setRow",value:function(){var e=document.createElement("div");return e.innerHTML='<div class="row" data-y="0">'+this.setCube(this.getRandom())+"</div>",e.firstChild}},{key:"setCube",value:function(e){for(var t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:4,i="",s=0;s<t;s++)i+='<div class="cube '+(s===e?"black":"")+'"></div>';return i}},{key:"getRandom",value:function(){return parseInt(4*Math.random(),10)}}]),t}();

View File

@@ -0,0 +1,36 @@
@charset "UTF-8";
body,
html {
height: 100%
}
body {
font-family: '微软雅黑', -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: .24rem;
width: 100%;
overflow: hidden
}
* {
padding: 0;
margin: 0;
list-style: none;
text-decoration: none;
box-sizing: border-box;
border: 0;
outline: 0;
font-style: normal
}
a {
-webkit-tap-highlight-color: transparent
}
input {
outline: 0
}
textarea {
resize: none
}

View File

@@ -0,0 +1,281 @@
@charset "UTF-8";
body,
html {
height: 100%
}
body {
font-family: '微软雅黑', -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 24px;
width: 100%;
overflow: hidden
}
* {
padding: 0;
margin: 0;
list-style: none;
text-decoration: none;
box-sizing: border-box;
border: 0;
outline: 0;
font-style: normal
}
a {
-webkit-tap-highlight-color: transparent
}
input {
outline: 0
}
textarea {
resize: none
}
body {
position: relative;
max-width: 7.5rem;
margin: 0 auto;
background: #fff
}
@media screen and (min-width:7.5rem) {
html {
background: #444
}
}
* {
-webkit-tap-highlight-color: transparent
}
.topspeed-container {
width: 100%;
height: 100%;
background: #fff;
position: relative;
z-index: 10
}
.topspeed-container.hide {
visibility: hidden;
z-index: -1
}
.container.topspeed {
height: 24%;
width: 100%;
position: absolute;
left: 0;
bottom: 100%;
z-index: 2
}
.container.topspeed .row {
display: flex;
align-items: flex-start;
justify-content: flex-start;
border-bottom: 1px solid #333;
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 1
}
.classics-container.hide {
visibility: hidden;
z-index: -1
}
.container.classics {
height: 100%;
width: 100%;
position: absolute;
left: 0;
bottom: 0;
z-index: 2
}
.container.classics .row {
display: flex;
align-items: flex-start;
justify-content: flex-start;
border-bottom: 1px solid #333;
height: 24%;
width: 100%;
z-index: 1;
position: absolute;
left: 0;
bottom: 0;
transition: all .1s
}
.container.classics .row:nth-of-type(2) {
border-bottom: 0
}
.container.classics .row:nth-of-type(1) {
transform: translateY(100%)
}
.container.classics .row:nth-of-type(2) {
transform: translateY(0%)
}
.container.classics .row:nth-of-type(3) {
transform: translateY(-100%)
}
.container.classics .row:nth-of-type(4) {
transform: translateY(-200%)
}
.container.classics .row:nth-of-type(5) {
transform: translateY(-300%)
}
.container.classics .row:nth-of-type(6) {
transform: translateY(-400%)
}
.container.classics .row:nth-of-type(7) {
transform: translateY(-500%)
}
.cube {
width: 25%;
height: 100%;
cursor: pointer;
background: #fff;
transition: all .3s
}
.cube:not(:last-of-type) {
border-right: 1px solid #333
}
.cube.black {
background: #333
}
.cube.toGray {
background: #ddd
}
.toggle-btn {
width: 1rem;
height: 1rem;
border-radius: 100%;
background: red;
position: fixed;
right: .2rem;
bottom: .2rem;
z-index: 100000
}
.score-container {
position: absolute;
top: .15rem;
left: 0;
width: 100%;
text-align: center;
font-size: .54rem;
color: #cd4545;
z-index: 999;
font-weight: 700
}
.score-container.classics {
display: flex;
justify-content: center;
align-items: center
}
.modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: flex;
justify-content: center;
align-items: center
}
.modal.white-bg {
background: #fff
}
.modal.dim-bg {
background: rgba(225, 225, 225, .7)
}
.modal a {
color: #00adb5;
display: block
}
.modal .init-modal-content {
height: 100%;
width: 100%;
padding: .6rem 0;
text-align: center;
line-height: 1.9rem
}
.modal .init-modal-content .title {
font-size: .64rem
}
.modal .init-modal-content .modal-btn {
font-size: .54rem;
height: 1.9rem
}
.modal .init-modal-content .modal-btn:nth-child(odd) {
background: #333;
color: #fff
}
.modal .modal-content {
width: 80%;
padding: .15rem;
border: 1px solid #333;
text-align: center;
border-radius: 4px;
background: #fff
}
.modal .modal-content .title {
font-size: .44rem;
margin-bottom: .25rem
}
.modal .modal-content .content {
font-size: .34rem;
line-height: 1.5
}
.modal .modal-content .content:last-of-type {
margin-bottom: .15rem
}
.modal .modal-content .modal-btn {
width: 80%;
padding: .1rem;
margin: 0 auto .15rem;
font-size: .32rem;
border: 1px solid #333;
cursor: pointer
}
.modal .modal-content .modal-btn.disable {
background: #ddd;
border-color: #999
}

View File

@@ -1,20 +0,0 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1232,
"时间":"2025-09-08"
},
{
"名称":"柚大青",
"账号":"2143323382@qq.com",
"分数":132,
"时间":"2025-09-21"
},
{
"名称":"牛马",
"账号":"2973419538@qq.com",
"分数":876,
"时间":"2025-09-25"
}
]

View File

@@ -0,0 +1,140 @@
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const tileSize = 40;
const levels = [
[
'##########',
'# #',
'# @ #',
'# #',
'# $ #',
'# #',
'# . #',
'# #',
'##########'
],
[
'##########',
'# . #',
'# $ #',
'# @ #',
'# #',
'##########'
],
[
'##########',
'# #',
'# @ $. #',
'# #',
'# . #',
'# #',
'##########'
]
];
let currentLevel = 0;
let player = { x: 0, y: 0 };
let boxes = [];
let goals = [];
function loadLevel(level) {
boxes = [];
goals = [];
for (let y = 0; y < level.length; y++) {
for (let x = 0; x < level[y].length; x++) {
const char = level[y][x];
if (char === '@') {
player = { x, y };
} else if (char === '$') {
boxes.push({ x, y });
} else if (char === '.') {
goals.push({ x, y });
}
}
}
drawLevel(level);
}
function drawLevel(level) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < level.length; y++) {
for (let x = 0; x < level[y].length; x++) {
const char = level[y][x];
if (char === '#') {
ctx.fillStyle = '#000';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
} else if (char === '.') {
ctx.fillStyle = '#0f0';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
}
drawPlayer();
drawBoxes();
}
function drawPlayer() {
ctx.fillStyle = '#00f';
ctx.fillRect(player.x * tileSize, player.y * tileSize, tileSize, tileSize);
}
function drawBoxes() {
ctx.fillStyle = '#f00';
boxes.forEach(box => {
ctx.fillRect(box.x * tileSize, box.y * tileSize, tileSize, tileSize);
});
}
function movePlayer(dx, dy) {
const newX = player.x + dx;
const newY = player.y + dy;
if (levels[currentLevel][newY][newX] === '#') return;
const boxIndex = boxes.findIndex(box => box.x === newX && box.y === newY);
if (boxIndex >= 0) {
const box = boxes[boxIndex];
const newBoxX = box.x + dx;
const newBoxY = box.y + dy;
if (levels[currentLevel][newBoxY][newBoxX] === '#' || boxes.some(b => b.x === newBoxX && b.y === newBoxY)) return;
box.x = newBoxX;
box.y = newBoxY;
}
player.x = newX;
player.y = newY;
checkLevelComplete();
drawLevel(levels[currentLevel]);
}
function checkLevelComplete() {
if (boxes.every(box => goals.some(goal => goal.x === box.x && goal.y === box.y))) {
currentLevel++;
if (currentLevel < levels.length) {
loadLevel(levels[currentLevel]);
} else {
alert('恭喜你完成了所有关卡!');
currentLevel = 0;
loadLevel(levels[currentLevel]);
}
}
}
window.addEventListener('keydown', (e) => {
switch (e.key) {
case 'ArrowUp':
movePlayer(0, -1);
break;
case 'ArrowDown':
movePlayer(0, 1);
break;
case 'ArrowLeft':
movePlayer(-1, 0);
break;
case 'ArrowRight':
movePlayer(1, 0);
break;
}
});
loadLevel(levels[currentLevel]);

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>推箱子游戏</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
canvas {
border: 1px solid #000;
background-color: #fff;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="600" height="400"></canvas>
<script src="game.js"></script>
</body>
</html>

View File

@@ -1,5 +0,0 @@
1.把jscsshtml分开储存每个功能单独分成每个模块避免单个文件过大问题
2.网页适配手机端和电脑端,优先优化手机竖屏游玩体验,所有游戏都是手机竖屏游戏
3.游戏都是无尽模式,玩到后期越来越困难,游戏玩法尽可能丰富多样
4.电脑端可以有键盘快捷键操作
5.最后结束游戏要统计显示玩家获得的最终游戏数据,给玩家成就感

View File

@@ -2,520 +2,12 @@ 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 = 6.5; // 初始速度 (10 * 0.65)
this.gameOver = false;
this.startTime = Date.now();
this.foodEaten = 0;
// 特殊食物
this.specialFood = null;
this.specialFoodTimer = 0;
this.specialFoodDuration = 5000; // 5秒
this.init();
}
// 根据分数计算权重(权重越高,越容易触发且数量偏大)
calculateWeightByScore(score) {
const w = score / 100; // 1000分趋近高权重
return Math.max(0.1, Math.min(0.95, w));
}
// 权重偏向的随机整数weight越大越偏向更大值
biasedRandomInt(maxInclusive, weight) {
const r = Math.random();
const biased = Math.pow(r, 1 - weight);
const val = Math.floor(biased * (maxInclusive + 1));
return Math.max(0, Math.min(maxInclusive, val));
}
// 在排行榜弹层追加结束信息
appendEndInfo(text, type = 'info') {
const summary = document.getElementById('leaderboardSummary');
if (!summary) return;
const info = document.createElement('div');
info.style.marginTop = '8px';
info.style.fontSize = '14px';
info.style.color = type === 'error' ? '#d9534f' : (type === 'success' ? '#28a745' : '#333');
info.textContent = text;
summary.appendChild(info);
}
// 游戏结束后尝试加“萌芽币”
async tryAwardCoinsOnGameOver() {
try {
const token = localStorage.getItem('token');
if (!token) {
this.appendEndInfo('未登录,无法获得萌芽币');
return;
}
let email = null;
try {
const userStr = localStorage.getItem('user');
if (userStr) {
const userObj = JSON.parse(userStr);
email = userObj && (userObj.email || userObj['邮箱']);
}
} catch (_) {}
if (!email) {
this.appendEndInfo('未找到账户信息email无法加币', 'error');
return;
}
const weight = this.calculateWeightByScore(this.score);
let coins = 0;
let guaranteed = false;
// 得分大于400必定触发获得1-5个萌芽币
if (this.score > 5) {
guaranteed = true;
coins = Math.floor(Math.random() * 5) + 1; // 1~5
} else {
// 使用权重作为概率
const roll = Math.random();
if (roll > weight) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
// 生成0~10随机数量权重越高越偏向更大
coins = this.biasedRandomInt(10, weight);
coins = Math.max(0, Math.min(10, coins));
if (coins <= 0) {
this.appendEndInfo('本局未获得萌芽币');
return;
}
}
// 后端 API base优先父窗口ENV_CONFIG
const apiBase = (window.parent && window.parent.ENV_CONFIG && window.parent.ENV_CONFIG.API_URL)
? window.parent.ENV_CONFIG.API_URL
: ((window.ENV_CONFIG && window.ENV_CONFIG.API_URL) ? window.ENV_CONFIG.API_URL : 'http://127.0.0.1:5002');
const resp = await fetch(`${apiBase}/api/user/add-coins`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ email, amount: coins })
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
const msg = err && (err.message || err.error) ? (err.message || err.error) : `请求失败(${resp.status})`;
this.appendEndInfo(`加币失败:${msg}`, 'error');
return;
}
const data = await resp.json();
if (data && data.success) {
const newCoins = data.data && data.data.new_coins;
this.appendEndInfo(`恭喜获得 ${coins} 个萌芽币!当前余额:${newCoins}`, 'success');
} else {
const msg = (data && (data.message || data.error)) || '未知错误';
this.appendEndInfo(`加币失败:${msg}`, 'error');
}
} catch (e) {
console.error('加币流程发生错误:', e);
this.appendEndInfo('加币失败:网络或系统错误', 'error');
}
}
init() {
this.generateFood();
this.gameLoop();
// 监听键盘事件
document.addEventListener('keydown', (e) => {
if (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(13, 6.5 + this.level * 0.65); // 速度上限13 (20 * 0.65)
}
}
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);
}
}
changeDirection(dx, dy) {
if (this.gameOver) return;
// 防止180度转弯
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) {
return;
}
this.dx = dx;
this.dy = dy;
}
// 工具:格式化日期为 YYYY-MM-DD
formatDate(date = new Date()) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
showGameOver() {
// 构建并展示排行榜弹层
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
const overlay = document.getElementById('leaderboardOverlay');
const listEl = document.getElementById('leaderboardList');
const lbScore = document.getElementById('lbScore');
const lbLength = document.getElementById('lbLength');
const lbLevel = document.getElementById('lbLevel');
const lbGameTime = document.getElementById('lbGameTime');
const lbRank = document.getElementById('lbRank');
if (!overlay || !listEl) {
console.warn('排行榜容器不存在');
return;
}
// 汇总当前玩家数据
lbScore.textContent = this.score;
lbLength.textContent = this.snake.length;
lbLevel.textContent = this.level;
lbGameTime.textContent = `${gameTime}`;
const currentEntry = {
"名称": localStorage.getItem('snakePlayerName') || '我',
"账号": localStorage.getItem('snakePlayerAccount') || 'guest@local',
"分数": this.score,
"时间": this.formatDate(new Date()),
__isCurrent: true,
__duration: gameTime
};
// 合并并排序数据(使用 gamedata.js 中的 playerdata
const baseData = (typeof playerdata !== 'undefined' && Array.isArray(playerdata)) ? playerdata : [];
const merged = [...baseData, currentEntry];
merged.sort((a, b) => (b["分数"] || 0) - (a["分数"] || 0));
const playerIndex = merged.findIndex(e => e.__isCurrent);
lbRank.textContent = playerIndex >= 0 ? `#${playerIndex + 1}` : '—';
// 生成排行榜TOP 10
const topList = merged.slice(0, 10).map((entry, idx) => {
const isCurrent = !!entry.__isCurrent;
const name = entry["名称"] ?? '未知玩家';
const score = entry["分数"] ?? 0;
const dateStr = entry["时间"] ?? '';
const timeStr = isCurrent ? `时长:${entry.__duration}` : `时间:${dateStr}`;
return `
<div class="leaderboard-item ${isCurrent ? 'current-player' : ''}">
<span class="rank">#${idx + 1}</span>
<span class="player-name">${name}</span>
<span class="player-score">${score}分</span>
<span class="player-time">${timeStr}</span>
</div>
`;
}).join('');
listEl.innerHTML = topList;
overlay.style.display = 'flex';
// 结束时尝试加币异步不阻塞UI
this.tryAwardCoinsOnGameOver();
// 触发游戏结束事件(供统计模块使用)
const gameOverEvent = new CustomEvent('gameOver', {
detail: {
score: this.score,
length: this.snake.length,
level: this.level,
gameTime: gameTime,
foodEaten: this.foodEaten
}
});
document.dispatchEvent(gameOverEvent);
// 绑定重新开始按钮
const restartBtn = document.getElementById('leaderboardRestartBtn');
if (restartBtn) {
restartBtn.onclick = () => {
overlay.style.display = 'none';
this.restart();
};
}
}
restart() {
this.snake = [
{x: 10, y: 10},
{x: 9, y: 10},
{x: 8, y: 10}
];
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;
@@ -525,21 +17,170 @@ class SnakeGame {
this.startTime = Date.now();
this.foodEaten = 0;
this.specialFood = null;
// 隐藏排行榜弹层(若可见)
const overlay = document.getElementById('leaderboardOverlay');
if (overlay) overlay.style.display = 'none';
this.specialFoodTimer = 0;
this.specialFoodDuration = 5000;
this.init();
}
init() {
this.generateFood();
this.updateUI();
this.gameLoop();
document.addEventListener('keydown', (e) => {
if (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;
if (Math.random() < 0.1 && !this.specialFood) this.generateSpecialFood();
}
generateSpecialFood() {
let f;
do { f = { x: Math.floor(Math.random() * this.tileCount), y: Math.floor(Math.random() * this.tileCountY), type:'special', value:5 }; }
while (this.isPositionOccupied(f));
this.specialFood = f;
this.specialFoodTimer = Date.now();
}
isPositionOccupied(pos) {
for (let s of this.snake) if (s.x === pos.x && s.y === pos.y) return true;
if (this.food && this.food.x === pos.x && this.food.y === pos.y) return true;
if (this.specialFood && this.specialFood.x === pos.x && this.specialFood.y === pos.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;
this.updateUI();
}
checkCollision(head) {
if (head.x < 0 || head.x >= this.tileCount || head.y < 0 || head.y >= this.tileCountY) return true;
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() {
const newLevel = Math.floor(this.foodEaten / 5) + 1;
if (newLevel > this.level) { this.level = newLevel; this.gameSpeed = Math.min(13, 6.5 + this.level * 0.65); }
}
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((seg, i) => {
this.ctx.fillStyle = i === 0 ? '#4CAF50' : `hsl(120,80%,${60 - (i/this.snake.length)*100}%)`;
this.ctx.fillRect(seg.x * this.gridSize, seg.y * this.gridSize, this.gridSize, this.gridSize);
this.ctx.strokeStyle = '#2E7D32';
this.ctx.strokeRect(seg.x * this.gridSize, seg.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 alpha = 0.5 + 0.5 * Math.sin((Date.now() - this.specialFoodTimer) / 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);
}
changeDirection(dx, dy) {
if (this.gameOver) return;
if ((this.dx !== 0 && dx !== 0) || (this.dy !== 0 && dy !== 0)) return;
this.dx = dx; this.dy = dy;
}
showGameOver() {
const gameTime = Math.floor((Date.now() - this.startTime) / 1000);
const overlay = document.getElementById('gameOverOverlay');
const summary = document.getElementById('endSummary');
if (summary) {
summary.innerHTML =
`<p>得分 <strong>${this.score}</strong> · 长度 <strong>${this.snake.length}</strong> · 等级 <strong>${this.level}</strong></p>` +
`<p>用时 <strong>${gameTime}</strong> 秒 · 吃掉 <strong>${this.foodEaten}</strong> 个食物</p>`;
}
if (overlay) overlay.style.display = 'flex';
const btn = document.getElementById('gameOverRestartBtn');
if (btn) btn.onclick = () => { overlay.style.display = 'none'; this.restart(); };
}
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 = 6.5; this.gameOver = false; this.startTime = Date.now();
this.foodEaten = 0; this.specialFood = null;
const overlay = document.getElementById('gameOverOverlay');
if (overlay) overlay.style.display = 'none';
this.generateFood(); this.updateUI(); this.gameLoop();
}
}
// 全局游戏实例
let game;
document.addEventListener('DOMContentLoaded', () => {
game = new SnakeGame();
});
document.addEventListener('DOMContentLoaded', () => { game = new SnakeGame(); });

View File

@@ -1,268 +0,0 @@
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));
}
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('统计信息已清除');
}
}
// 原游戏结束界面已移除,保留统计模块以便响应 'gameOver' 事件
// 初始化统计模块
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);

View File

@@ -1,50 +0,0 @@
const playerdata = [
{
"名称":"树萌芽",
"账号":"3205788256@qq.com",
"分数":1568,
"时间":"2025-09-08"
},
{
"名称":"风行者",
"账号":"4456723190@qq.com",
"分数":1987,
"时间":"2025-09-30"
},
{
"名称":"月光骑士",
"账号":"5832197462@qq.com",
"分数":876,
"时间":"2025-10-02"
},
{
"名称":"星河",
"账号":"6724981532@qq.com",
"分数":1345,
"时间":"2025-10-05"
},
{
"名称":"雷霆",
"账号":"7891234567@qq.com",
"分数":2105,
"时间":"2025-10-08"
},
{
"名称":"火焰猫",
"账号":"8912345678@qq.com",
"分数":654,
"时间":"2025-10-10"
},
{
"名称":"冰雪女王",
"账号":"9123456789@qq.com",
"分数":1789,
"时间":"2025-10-12"
},
{
"名称":"😊",
"账号":"1125234890@qq.com",
"分数":1432,
"时间":"2025-10-15"
}
]

View File

@@ -26,37 +26,17 @@
<button id="restartBtn" class="control-btn">重新开始</button>
</div>
</div>
</div>
<!-- 游戏结束排行榜弹层(替换旧的游戏结束界面) -->
<div id="leaderboardOverlay" class="modal">
<div id="gameOverOverlay" class="modal">
<div class="modal-content">
<h2>游戏结束排行榜</h2>
<div class="leaderboard-summary" id="leaderboardSummary">
<p>
分数: <span id="lbScore">0</span>
|长度: <span id="lbLength">3</span>
|等级: <span id="lbLevel">1</span>
</p>
<p>
游戏时长: <span id="lbGameTime">0秒</span>
|你的排名: <span id="lbRank"></span>
</p>
</div>
<div class="leaderboard">
<h3>TOP 10</h3>
<div id="leaderboardList" class="leaderboard-list"></div>
</div>
<button id="leaderboardRestartBtn" class="restart-btn">重新开始</button>
<h2>游戏结束</h2>
<div class="end-summary" id="endSummary"></div>
<button id="gameOverRestartBtn" class="restart-btn">重新开始</button>
</div>
</div>
<script src="gamedata.js"></script>
<script src="game-core.js"></script>
<script src="game-controls.js"></script>
<script src="game-stats.js"></script>
</body>
</html>
</html>

View File

@@ -175,85 +175,13 @@ body {
box-shadow: 0 8px 16px rgba(46, 125, 50, 0.3);
}
/* 排行榜样式 */
.leaderboard {
margin: 20px 0;
padding: 15px;
background: rgba(255, 255, 255, 0.3);
border-radius: 15px;
border: 1px solid rgba(46, 125, 50, 0.2);
}
.leaderboard-summary {
margin: 10px 0 15px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 12px;
color: #1b5e20;
text-align: center;
border: 1px solid rgba(46, 125, 50, 0.2);
}
.leaderboard-summary p {
margin: 6px 0;
}
.leaderboard h3 {
color: #1b5e20;
margin-bottom: 15px;
font-size: 1.3rem;
.end-summary {
margin: 16px 0;
font-size: 1rem;
line-height: 1.8;
text-align: center;
}
.leaderboard-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.leaderboard-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 10px;
font-size: 0.9rem;
transition: all 0.2s ease;
}
.leaderboard-item.current-player {
background: linear-gradient(135deg, #ffeb3b 0%, #fff176 100%);
font-weight: bold;
border: 2px solid #f57f17;
}
.leaderboard-item .rank {
font-weight: bold;
min-width: 30px;
text-align: left;
}
.leaderboard-item .player-name {
flex: 1;
text-align: left;
margin-left: 10px;
color: #2e7d32;
}
.leaderboard-item .player-score {
font-weight: bold;
color: #1b5e20;
min-width: 50px;
text-align: right;
}
.leaderboard-item .player-time {
color: #4a5568;
font-size: 0.8rem;
min-width: 80px;
text-align: right;
}
.end-summary strong { color: #2e7d32; }
/* 手机端优化 */
@media (max-width: 768px) {