角色选择 · 吃吃乐
by MysticLion44695 lines25.1 KB
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>角色选择 · 吃吃乐</title>
<style>
* { touch-action: none; user-select: none; box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
font-family: Arial, sans-serif;
padding: 4px 6px 6px;
height: 100vh;
max-height: 100vh;
}
/* ----- 顶部:分数 + 最高分 ----- */
.top-area {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
margin-bottom: 2px;
gap: 0px;
}
#score {
color: #eee;
font-size: 20px;
font-weight: bold;
line-height: 1.2;
}
#highScore {
color: #888;
font-size: 13px;
font-weight: normal;
line-height: 1.2;
}
/* ----- 设置区域(角色+速度 合并为一行,紧凑) ----- */
.settings-row {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
flex-shrink: 0;
margin-bottom: 2px;
flex-wrap: nowrap; /* 强制一行 */
}
.settings-label {
color: #888;
font-size: 11px;
font-weight: bold;
margin-right: 0px;
flex-shrink: 0;
}
.role-wrap, .speed-wrap {
display: flex;
align-items: center;
background: #16213e;
padding: 1px 4px;
border-radius: 20px;
border: 1px solid #0f3460;
gap: 1px;
flex-shrink: 1;
overflow: hidden;
}
.role-btn {
padding: 0px 4px;
border-radius: 16px;
border: none;
font-size: 15px;
background: transparent;
cursor: pointer;
opacity: 0.4;
transition: all 0.2s;
line-height: 1.6;
}
.role-btn.active {
opacity: 1;
background: #e94560;
box-shadow: 0 0 6px #e94560aa;
transform: scale(1.04);
}
.role-btn:active { transform: scale(0.9); }
.speed-btn {
padding: 0px 5px;
border-radius: 16px;
border: none;
font-size: 11px;
font-weight: bold;
color: #888;
background: transparent;
cursor: pointer;
transition: all 0.2s;
line-height: 1.8;
white-space: nowrap;
}
.speed-btn.active {
color: #fff;
background: #e94560;
box-shadow: 0 0 6px #e94560aa;
}
.speed-btn:active { transform: scale(0.92); }
/* ----- 画布(大幅放大) ----- */
.canvas-wrap {
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 0;
max-height: 55vh;
}
canvas {
background: #1a1a2e;
border: 3px solid #0f3460;
border-radius: 12px;
width: min(88vw, 420px);
height: min(88vw, 420px);
max-width: 420px;
max-height: 420px;
touch-action: none;
display: block;
}
/* ----- 方向键(放大) ----- */
.dpad {
display: grid;
grid-template-columns: 62px 62px 62px;
grid-template-rows: 62px 62px 62px;
gap: 6px;
margin-top: 4px;
flex-shrink: 0;
}
.dpad-btn {
border: none;
border-radius: 16px;
font-size: 24px;
font-weight: bold;
color: white;
background: #2d4059;
box-shadow: 0 4px 0 #111;
cursor: pointer;
touch-action: manipulation;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.05s linear;
width: 100%;
height: 100%;
}
.dpad-btn:active {
transform: translateY(4px);
box-shadow: none;
}
.dpad-btn.up { grid-column: 2; grid-row: 1; }
.dpad-btn.left { grid-column: 1; grid-row: 2; }
.dpad-btn.center { grid-column: 2; grid-row: 2; background: #f39c12; box-shadow: 0 4px 0 #b86d0e; font-size: 18px; }
.dpad-btn.right { grid-column: 3; grid-row: 2; }
.dpad-btn.down { grid-column: 2; grid-row: 3; }
/* ----- 功能按钮 ----- */
.action-row {
display: flex;
gap: 8px;
margin-top: 4px;
flex-shrink: 0;
flex-wrap: wrap;
justify-content: center;
}
.action-row button {
padding: 4px 12px;
font-size: 13px;
font-weight: bold;
border-radius: 30px;
border: none;
color: white;
background: #2d4059;
box-shadow: 0 3px 0 #111;
cursor: pointer;
min-width: 58px;
}
.action-row button:active { transform: translateY(3px); box-shadow: none; }
#saveBtn { background: #2980b9; box-shadow: 0 3px 0 #1a5276; }
#loadBtn { background: #8e44ad; box-shadow: 0 3px 0 #5b2d6e; }
#restartBtn { background: #e74c3c; box-shadow: 0 3px 0 #922b21; }
.status-tip {
color: #666;
font-size: 11px;
margin-top: 2px;
flex-shrink: 0;
text-align: center;
line-height: 1.2;
}
/* ----- 小屏适配 ----- */
@media (max-width: 400px) {
.dpad { grid-template-columns: 52px 52px 52px; grid-template-rows: 52px 52px 52px; gap: 5px; }
.dpad-btn { font-size: 20px; border-radius: 14px; }
.dpad-btn.center { font-size: 16px; }
canvas { width: min(85vw, 350px); height: min(85vw, 350px); }
.action-row button { padding: 3px 10px; font-size: 12px; min-width: 50px; }
.role-btn { font-size: 13px; padding: 0px 3px; }
.speed-btn { font-size: 10px; padding: 0px 4px; }
#score { font-size: 17px; }
#highScore { font-size: 12px; }
.settings-label { font-size: 10px; }
.role-wrap, .speed-wrap { padding: 1px 3px; gap: 1px; }
}
@media (max-width: 350px) {
.dpad { grid-template-columns: 44px 44px 44px; grid-template-rows: 44px 44px 44px; gap: 4px; }
.dpad-btn { font-size: 17px; border-radius: 12px; }
.dpad-btn.center { font-size: 13px; }
canvas { width: min(80vw, 300px); height: min(80vw, 300px); }
.speed-btn { font-size: 9px; padding: 0px 3px; }
.role-btn { font-size: 12px; }
}
</style>
</head>
<body>
<!-- 顶部:分数 + 最高分 -->
<div class="top-area">
<div id="score">🧀 0</div>
<div id="highScore">🏆 最高: 0</div>
</div>
<!-- 设置区域(角色 + 速度 合并为一行,紧凑) -->
<div class="settings-row">
<span class="settings-label">🎭</span>
<div class="role-wrap">
<button class="role-btn active" data-role="mouse">🐭</button>
<button class="role-btn" data-role="snake">🐍</button>
<button class="role-btn" data-role="sheep">🐑</button>
</div>
<span class="settings-label" style="margin-left:2px;">⚡</span>
<div class="speed-wrap">
<!-- 新增极慢(400ms) -->
<button class="speed-btn" data-speed="400">🐢极慢</button>
<button class="speed-btn" data-speed="200">🐑慢</button>
<button class="speed-btn active" data-speed="130">🐭中</button>
<button class="speed-btn" data-speed="70">🐍快</button>
</div>
</div>
<!-- 画布 -->
<div class="canvas-wrap">
<canvas id="gameCanvas" width="400" height="400"></canvas>
</div>
<!-- 方向键 -->
<div class="dpad">
<button class="dpad-btn up" id="btnUp">▲</button>
<button class="dpad-btn left" id="btnLeft">◀</button>
<button class="dpad-btn center" id="pauseBtn">⏸️</button>
<button class="dpad-btn right" id="btnRight">▶</button>
<button class="dpad-btn down" id="btnDown">▼</button>
</div>
<!-- 功能按钮 -->
<div class="action-row">
<button id="saveBtn">💾 保存</button>
<button id="loadBtn">📂 读取</button>
<button id="restartBtn">🔄 重启</button>
</div>
<div class="status-tip" id="statusTip">💡 保存后,下次打开点“读取”继续</div>
<script>
// ===================== 游戏脚本(同前,增加极慢速度400ms) =====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const gridSize = 20;
const tileCount = canvas.width / gridSize;
let currentRole = 'mouse';
const roleData = {
mouse: { head: '🐭', body: '🧀', food: '🧀', scoreIcon: '🧀' },
snake: { head: '🐍', body: '💣', food: '💣', scoreIcon: '💣' },
sheep: { head: '🐑', body: '🧋', food: '🧋', scoreIcon: '🧋' }
};
let snake, food, direction, nextDirection, score, gameOverFlag, gameInterval;
let currentSpeed = 130; // 默认中速
let isPaused = false;
// ========== 最高分管理 ==========
const HIGH_SCORE_KEY = 'snakeHighScore';
function getHighScore() {
return parseInt(localStorage.getItem(HIGH_SCORE_KEY)) || 0;
}
function setHighScore(val) {
localStorage.setItem(HIGH_SCORE_KEY, String(val));
}
function updateHighScoreDisplay() {
document.getElementById('highScore').innerText = '🏆 最高: ' + getHighScore();
}
// ========== 绘制函数 ==========
function drawFoodEmoji(ctx, x, y, size, emoji) {
const cx = x + size / 2;
const cy = y + size / 2;
ctx.font = `${size * 0.85}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, cx, cy + 2);
}
function drawBody(ctx, x, y, size, emoji) {
const cx = x + size / 2;
const cy = y + size / 2;
ctx.font = `${size * 0.8}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, cx, cy + 2);
}
// ========== 游戏核心 ==========
function initGame() {
snake = [{ x: 10, y: 10 }];
direction = { x: 0, y: 0 };
nextDirection = { x: 0, y: 0 };
score = 0;
gameOverFlag = false;
isPaused = false;
document.getElementById('pauseBtn').innerText = '⏸️';
updateScoreDisplay();
updateHighScoreDisplay();
placeFood();
clearGameInterval();
startGameInterval();
drawGame();
}
function updateScoreDisplay() {
const icon = roleData[currentRole].scoreIcon;
document.getElementById('score').innerText = icon + ' ' + score;
}
function startGameInterval() {
if (gameInterval) clearInterval(gameInterval);
gameInterval = setInterval(gameLoop, currentSpeed);
}
function clearGameInterval() {
if (gameInterval) {
clearInterval(gameInterval);
gameInterval = null;
}
}
function gameLoop() {
if (isPaused) {
drawGame();
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⏸ Paused', canvas.width/2, canvas.height/2 - 10);
ctx.font = '16px Arial';
ctx.fillStyle = '#aaa';
ctx.fillText('Press "Continue" below', canvas.width/2, canvas.height/2 + 40);
return;
}
if (gameOverFlag) return;
direction = { ...nextDirection };
if (direction.x === 0 && direction.y === 0) return;
const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
score++;
updateScoreDisplay();
placeFood();
} else {
snake.pop();
}
if (head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount) {
gameOver(); return;
}
for (let i = 1; i < snake.length; i++) {
if (snake[i].x === head.x && snake[i].y === head.y) {
gameOver(); return;
}
}
drawGame();
}
function drawGame() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= tileCount; i++) {
ctx.beginPath();
ctx.moveTo(i * gridSize, 0);
ctx.lineTo(i * gridSize, canvas.height);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * gridSize);
ctx.lineTo(canvas.width, i * gridSize);
ctx.stroke();
}
const foodEmoji = roleData[currentRole].food;
drawFoodEmoji(ctx, food.x * gridSize, food.y * gridSize, gridSize, foodEmoji);
const bodyEmoji = roleData[currentRole].body;
const headEmoji = roleData[currentRole].head;
snake.forEach((part, index) => {
const cx = part.x * gridSize + gridSize/2;
const cy = part.y * gridSize + gridSize/2;
if (index === 0) {
ctx.font = `${gridSize * 0.9}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(headEmoji, cx, cy + 1);
} else {
drawBody(ctx, part.x * gridSize, part.y * gridSize, gridSize, bodyEmoji);
}
});
}
function placeFood() {
let newFood, valid;
do {
valid = true;
newFood = { x: Math.floor(Math.random() * tileCount), y: Math.floor(Math.random() * tileCount) };
for (let part of snake) {
if (part.x === newFood.x && part.y === newFood.y) { valid = false; break; }
}
} while (!valid);
food = newFood;
}
// ========== 游戏结束(更新最高分) ==========
function gameOver() {
if (gameOverFlag) return;
gameOverFlag = true;
clearGameInterval();
const currentHigh = getHighScore();
if (score > currentHigh) {
setHighScore(score);
updateHighScoreDisplay();
}
drawGame();
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const headEmoji = roleData[currentRole].head;
ctx.fillStyle = '#ff6b6b';
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(headEmoji + ' Game Over', canvas.width/2, canvas.height/2 - 20);
ctx.fillStyle = '#eee';
ctx.font = '20px Arial';
ctx.fillText('Score: ' + score + ' 最高: ' + getHighScore(), canvas.width/2, canvas.height/2 + 40);
setTimeout(() => {
alert('Game Over! Score: ' + score + ' 最高: ' + getHighScore());
}, 50);
}
// ========== 切换角色 ==========
function setRole(role) {
if (role === currentRole) return;
currentRole = role;
document.querySelectorAll('.role-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.role === role);
});
updateScoreDisplay();
placeFood();
drawGame();
}
// ========== 速度切换 ==========
function setSpeed(speed) {
currentSpeed = speed;
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.classList.remove('active');
if (parseInt(btn.dataset.speed) === speed) btn.classList.add('active');
});
if (isPaused) {
isPaused = false;
document.getElementById('pauseBtn').innerText = '⏸️';
}
if (!gameOverFlag) {
clearGameInterval();
startGameInterval();
}
}
// ========== 暂停 ==========
function togglePause() {
if (gameOverFlag) {
alert('Game has ended, please click "Restart"');
return;
}
isPaused = !isPaused;
document.getElementById('pauseBtn').innerText = isPaused ? '▶️' : '⏸️';
if (isPaused) {
clearGameInterval();
drawGame();
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#fff';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('⏸ Paused', canvas.width/2, canvas.height/2 - 10);
ctx.font = '16px Arial';
ctx.fillStyle = '#aaa';
ctx.fillText('Press "Continue" below', canvas.width/2, canvas.height/2 + 40);
} else {
if (!gameOverFlag) startGameInterval();
}
}
// ========== 方向控制 ==========
function setDirection(newDir) {
if (gameOverFlag || isPaused) return;
if ((direction.x !== 0 && newDir.x !== 0) || (direction.y !== 0 && newDir.y !== 0)) return;
if (direction.x === 0 && direction.y === 0) { nextDirection = newDir; return; }
if (direction.x === -newDir.x && direction.y === -newDir.y) return;
nextDirection = newDir;
}
// ========== 保存 & 读取 ==========
function saveGame() {
const state = {
snake: snake,
food: food,
direction: direction,
nextDirection: nextDirection,
score: score,
currentSpeed: currentSpeed,
gameOverFlag: gameOverFlag,
isPaused: false,
role: currentRole
};
try {
localStorage.setItem('snakeGameSave', JSON.stringify(state));
document.getElementById('statusTip').innerText = '✅ Game saved!';
setTimeout(() => {
document.getElementById('statusTip').innerText = '💡 保存后,下次打开点“读取”继续';
}, 3000);
} catch (e) {
alert('Save failed');
}
}
function loadGame() {
const data = localStorage.getItem('snakeGameSave');
if (!data) {
alert('No save file found');
return;
}
try {
const state = JSON.parse(data);
snake = state.snake;
food = state.food;
direction = state.direction;
nextDirection = state.nextDirection;
score = state.score;
currentSpeed = state.currentSpeed;
gameOverFlag = state.gameOverFlag;
if (state.role) {
currentRole = state.role;
document.querySelectorAll('.role-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.role === currentRole);
});
}
updateScoreDisplay();
updateHighScoreDisplay();
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.classList.remove('active');
if (parseInt(btn.dataset.speed) === currentSpeed) btn.classList.add('active');
});
clearGameInterval();
isPaused = false;
document.getElementById('pauseBtn').innerText = '⏸️';
if (!gameOverFlag) {
startGameInterval();
} else {
drawGame();
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const headEmoji = roleData[currentRole].head;
ctx.fillStyle = '#ff6b6b';
ctx.font = 'bold 32px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(headEmoji + ' Game Over', canvas.width/2, canvas.height/2 - 20);
ctx.fillStyle = '#eee';
ctx.font = '20px Arial';
ctx.fillText('Score: ' + score + ' 最高: ' + getHighScore(), canvas.width/2, canvas.height/2 + 40);
}
drawGame();
document.getElementById('statusTip').innerText = '✅ Load successful!';
setTimeout(() => {
document.getElementById('statusTip').innerText = '💡 保存后,下次打开点“读取”继续';
}, 3000);
} catch (e) {
alert('Load failed');
}
}
// ========== 触摸滑动 ==========
let touchStartX = 0, touchStartY = 0;
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
}, { passive: false });
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (!touchStartX || !touchStartY) return;
const touch = e.touches[0];
const deltaX = touch.clientX - touchStartX;
const deltaY = touch.clientY - touchStartY;
if (Math.abs(deltaX) < 20 && Math.abs(deltaY) < 20) return;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
setDirection({ x: deltaX > 0 ? 1 : -1, y: 0 });
} else {
setDirection({ x: 0, y: deltaY > 0 ? 1 : -1 });
}
touchStartX = touch.clientX;
touchStartY = touch.clientY;
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
touchStartX = 0;
touchStartY = 0;
}, { passive: false });
// ========== 按钮绑定 ==========
document.getElementById('btnUp').addEventListener('click', () => setDirection({ x: 0, y: -1 }));
document.getElementById('btnDown').addEventListener('click', () => setDirection({ x: 0, y: 1 }));
document.getElementById('btnLeft').addEventListener('click', () => setDirection({ x: -1, y: 0 }));
document.getElementById('btnRight').addEventListener('click', () => setDirection({ x: 1, y: 0 }));
document.getElementById('pauseBtn').addEventListener('click', togglePause);
document.getElementById('saveBtn').addEventListener('click', saveGame);
document.getElementById('loadBtn').addEventListener('click', loadGame);
document.getElementById('restartBtn').addEventListener('click', () => {
if (isPaused) {
isPaused = false;
document.getElementById('pauseBtn').innerText = '⏸️';
}
initGame();
});
document.querySelectorAll('.role-btn').forEach(btn => {
btn.addEventListener('click', function() {
setRole(this.dataset.role);
});
});
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', function() {
setSpeed(parseInt(this.dataset.speed));
});
});
// ========== 启动 ==========
initGame();
</script>
</body>
</html>Game Source: 角色选择 · 吃吃乐
Creator: MysticLion44
Libraries: none
Complexity: complex (695 lines, 25.1 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-mysticlion44-mr1v0yl6" to link back to the original. Then publish at arcadelab.ai/publish.