酷炫贪吃蛇-ai-demo
script dog · v1.0.0 · 独立窗口 · 7 次安装
简介
暂无简介
脚本源码
(() => {
const rootId = 'gsh-neon-snake-root';
const oldRoot = document.getElementById(rootId);
if (oldRoot) oldRoot.remove();
if (!document.body) {
document.documentElement.appendChild(document.createElement('body'));
}
const css = document.createElement('style');
css.textContent = `
#${rootId} {
position: fixed;
inset: 0;
z-index: 2147483647;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
box-sizing: border-box;
font-family: "SF Mono", "Cascadia Mono", "Fira Code", Consolas, monospace;
color: #e2e8f0;
background:
radial-gradient(circle at 50% 0%, rgba(34,211,238,.16), transparent 38%),
radial-gradient(circle at 50% 100%, rgba(232,121,249,.10), transparent 42%),
#020617;
overflow: hidden;
}
#${rootId} * { box-sizing: border-box; }
#${rootId} .game-shell {
width: 100%;
max-width: 820px;
display: flex;
flex-direction: column;
gap: 14px;
align-items: center;
}
#${rootId} .hud {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
padding: 10px 14px;
border: 1px solid rgba(34,211,238,.24);
border-radius: 14px;
background: rgba(2,6,23,.45);
box-shadow: 0 0 28px rgba(34,211,238,.08), inset 0 0 24px rgba(34,211,238,.04);
backdrop-filter: blur(8px);
}
#${rootId} .title {
font-size: 18px;
font-weight: 900;
letter-spacing: .22em;
color: #22d3ee;
text-shadow: 0 0 10px rgba(34,211,238,.75), 0 0 28px rgba(34,211,238,.25);
}
#${rootId} .stats {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
font-size: 13px;
letter-spacing: .08em;
}
#${rootId} .pill {
padding: 7px 10px;
border-radius: 999px;
border: 1px solid rgba(148,163,184,.22);
background: rgba(15,23,42,.45);
color: #94a3b8;
white-space: nowrap;
}
#${rootId} .pill b {
color: #f472b6;
font-weight: 800;
text-shadow: 0 0 8px rgba(244,114,182,.55);
}
#${rootId} .btn {
appearance: none;
border: 1px solid rgba(34,211,238,.45);
background: rgba(34,211,238,.08);
color: #e0f2fe;
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
font: inherit;
font-weight: 700;
letter-spacing: .12em;
text-transform: uppercase;
box-shadow: 0 0 18px rgba(34,211,238,.12);
transition: .18s ease;
}
#${rootId} .btn:hover {
color: #fff;
background: rgba(34,211,238,.16);
box-shadow: 0 0 26px rgba(34,211,238,.28);
}
#${rootId} .btn:active { transform: scale(.96); }
#${rootId} .canvas-wrap {
position: relative;
border-radius: 18px;
overflow: hidden;
border: 1px solid rgba(34,211,238,.32);
background: rgba(2,6,23,.35);
box-shadow:
0 0 50px rgba(34,211,238,.13),
inset 0 0 45px rgba(34,211,238,.05);
}
#${rootId} canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none;
user-select: none;
}
#${rootId} .overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity .18s ease;
background:
radial-gradient(circle at center, rgba(2,6,23,.45), rgba(2,6,23,.82)),
rgba(2,6,23,.25);
backdrop-filter: blur(3px);
}
#${rootId} .overlay.show {
opacity: 1;
pointer-events: auto;
}
#${rootId} .panel {
width: min(86%, 560px);
text-align: center;
padding: 28px;
border-radius: 16px;
border: 1px solid rgba(34,211,238,.25);
background: rgba(2,6,23,.42);
box-shadow: 0 0 50px rgba(34,211,238,.12);
}
#${rootId} .panel h2 {
margin: 0 0 10px;
font-size: clamp(26px, 5vw, 42px);
letter-spacing: .16em;
color: #22d3ee;
text-shadow: 0 0 18px rgba(34,211,238,.75), 0 0 40px rgba(34,211,238,.25);
}
#${rootId} .panel p {
margin: 0 0 22px;
color: #94a3b8;
line-height: 1.65;
font-size: 14px;
}
#${rootId} .hint {
margin-top: 14px;
color: #64748b;
font-size: 12px;
letter-spacing: .08em;
}
@media (max-width: 640px) {
#${rootId} .hud { padding: 8px; }
#${rootId} .title { font-size: 15px; }
#${rootId} .stats { font-size: 12px; }
#${rootId} .panel { padding: 20px; }
}
`;
(document.head || rootId.parentNode || document.body).appendChild(css);
const root = document.createElement('div');
root.id = rootId;
root.innerHTML = `
<div class="game-shell">
<div class="hud">
<div class="title">NEON SNAKE</div>
<div class="stats">
<span class="pill">SCORE <b id="gsh-score">0000</b></span>
<span class="pill">BEST <b id="gsh-best">0000</b></span>
<button id="gsh-pause" class="btn">Pause</button>
</div>
</div>
<div class="canvas-wrap" id="gsh-canvas-wrap">
<canvas id="gsh-canvas"></canvas>
<div id="gsh-overlay" class="overlay show">
<div class="panel">
<h2 id="gsh-overlay-title">READY</h2>
<p id="gsh-overlay-text">WASD / 方向键移动,空格暂停。</p>
<button id="gsh-action" class="btn">Start</button>
<div class="hint">点击按钮或按任意方向键开始</div>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(root);
window.__GSH_SNAKE_ACTIVE__ = root;
const scoreEl = root.querySelector('#gsh-score');
const bestEl = root.querySelector('#gsh-best');
const pauseBtn = root.querySelector('#gsh-pause');
const overlay = root.querySelector('#gsh-overlay');
const overlayTitle = root.querySelector('#gsh-overlay-title');
const overlayText = root.querySelector('#gsh-overlay-text');
const actionBtn = root.querySelector('#gsh-action');
const canvas = root.querySelector('#gsh-canvas');
const ctx = canvas.getContext('2d');
const COLS = 21;
const ROWS = 21;
let cell = 20;
let canvasSizeCss = COLS * cell;
let state = 'ready';
let score = 0;
let best = 0;
let food = { x: 15, y: 10 };
let snake = [];
let dir = { x: 1, y: 0 };
let pendingDirs = [];
let acc = 0;
let lastTime = performance.now();
let audioCtx = null;
const storageKey = 'gsh-neon-snake-best';
try {
best = Number(localStorage.getItem(storageKey) || 0) || 0;
} catch (_) {}
function isActive() {
return root.isConnected && window.__GSH_SNAKE_ACTIVE__ === root;
}
function updateHud() {
scoreEl.textContent = String(score).padStart(4, '0');
bestEl.textContent = String(best).padStart(4, '0');
pauseBtn.textContent = state === 'paused' ? 'Resume' : 'Pause';
}
function showOverlay(title, text, button) {
overlayTitle.textContent = title;
overlayText.textContent = text;
actionBtn.textContent = button;
overlay.classList.add('show');
}
function hideOverlay() {
overlay.classList.remove('show');
}
function ensureAudio() {
try {
if (!audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return;
audioCtx = new AudioContextClass();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
} catch (_) {}
}
function tone(freq, duration = 0.08, type = 'sine', volume = 0.045) {
try {
ensureAudio();
if (!audioCtx) return;
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, now);
gain.gain.setValueAtTime(volume, now);
gain.gain.exponentialRampToValueAtTime(0.0001, now + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(now);
osc.stop(now + duration);
} catch (_) {}
}
function playEat() {
tone(680, 0.07, 'triangle', 0.05);
setTimeout(() => tone(980, 0.06, 'triangle', 0.04), 35);
}
function playDeath() {
tone(280, 0.10, 'sawtooth', 0.055);
setTimeout(() => tone(170, 0.18, 'sawtooth', 0.045), 90);
}
function playStart() {
tone(520, 0.06, 'square', 0.03);
setTimeout(() => tone(820, 0.08, 'square', 0.03), 60);
}
function resize() {
if (!isActive()) return;
const availW = Math.max(220, window.innerWidth - 72);
const availH = Math.max(220, window.innerHeight - 170);
const nextCell = Math.max(9, Math.floor(Math.min(availW / COLS, availH / ROWS)));
cell = nextCell;
canvasSizeCss = COLS * cell;
const dpr = window.devicePixelRatio || 1;
canvas.style.width = canvasSizeCss + 'px';
canvas.style.height = canvasSizeCss + 'px';
canvas.width = Math.floor(canvasSizeCss * dpr);
canvas.height = Math.floor(canvasSizeCss * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.imageSmoothingEnabled = true;
}
function spawnFood() {
if (snake.length >= COLS * ROWS) {
return false;
}
let pos;
let guard = 0;
do {
pos = {
x: Math.floor(Math.random() * COLS),
y: Math.floor(Math.random() * ROWS)
};
guard++;
} while (snake.some(seg => seg.x === pos.x && seg.y === pos.y) && guard < 5000);
if (guard >= 5000) {
outer:
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (!snake.some(seg => seg.x === x && seg.y === y)) {
pos = { x, y };
break outer;
}
}
}
}
food = pos;
return true;
}
function resetGame() {
snake = [
{ x: 10, y: 10 },
{ x: 9, y: 10 },
{ x: 8, y: 10 }
];
dir = { x: 1, y: 0 };
pendingDirs = [];
score = 0;
acc = 0;
spawnFood();
updateHud();
}
function startGame() {
ensureAudio();
resetGame();
state = 'playing';
hideOverlay();
updateHud();
playStart();
}
function togglePause() {
if (state === 'playing') {
state = 'paused';
showOverlay('PAUSED', '游戏已暂停,继续你的霓虹蛇阵。', 'Resume');
updateHud();
} else if (state === 'paused') {
state = 'playing';
hideOverlay();
updateHud();
} else if (state === 'ready' || state === 'over') {
startGame();
}
}
function gameOver(win = false) {
state = 'over';
pendingDirs = [];
if (score > best) {
best = score;
try {
localStorage.setItem(storageKey, String(best));
} catch (_) {}
}
updateHud();
if (win) {
showOverlay('PERFECT', '蛇已经占满整个棋盘,霓虹传说达成。', 'Play Again');
} else {
showOverlay('GAME OVER', `本局得分:${score}。最高分:${best}。`, 'Play Again');
}
playDeath();
}
function queueDir(next) {
if (!isActive()) return;
if (state === 'over') return;
if (state === 'ready') {
startGame();
}
if (state !== 'playing') return;
if (next.x === -dir.x && next.y === -dir.y) return;
const last = pendingDirs[pendingDirs.length - 1];
if (last && last.x === next.x && last.y === next.y) return;
if (last && next.x === -last.x && next.y === -last.y) return;
pendingDirs.push(next);
if (pendingDirs.length > 4) pendingDirs.shift();
}
function move() {
while (pendingDirs.length) {
const next = pendingDirs.shift();
if (next.x === -dir.x && next.y === -dir.y) continue;
dir = next;
break;
}
const head = {
x: snake[0].x + dir.x,
y: snake[0].y + dir.y
};
if (head.x < 0 || head.y < 0 || head.x >= COLS || head.y >= ROWS) {
gameOver(false);
return;
}
const willEat = food && head.x === food.x && head.y === food.y;
const checkBody = willEat ? snake : snake.slice(0, -1);
if (checkBody.some(seg => seg.x === head.x && seg.y === head.y)) {
gameOver(false);
return;
}
snake.unshift(head);
if (willEat) {
score += 10;
updateHud();
playEat();
if (!spawnFood()) {
gameOver(true);
}
} else {
snake.pop();
}
}
function roundRect(x, y, w, h, r) {
const radius = Math.max(0, Math.min(r, w / 2, h / 2));
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + w, y, x + w, y + h, radius);
ctx.arcTo(x + w, y + h, x, y + h, radius);
ctx.arcTo(x, y + h, x, y, radius);
ctx.arcTo(x, y, x + w, y, radius);
ctx.closePath();
}
function draw(now) {
if (!isActive() || !ctx) return;
const width = canvasSizeCss;
const height = ROWS * cell;
const bg = ctx.createRadialGradient(
width / 2,
height / 2,
0,
width / 2,
height / 2,
Math.max(width, height) * 0.75
);
bg.addColorStop(0, '#030b1f');
bg.addColorStop(1, '#01030b');
ctx.fillStyle = bg;
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = 'rgba(34,211,238,.075)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 1; i < COLS; i++) {
const x = i * cell;
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let j = 1; j < ROWS; j++) {
const y = j * cell;
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.stroke();
if (food) {
const pulse = 0.5 + 0.5 * Math.sin(now / 220);
const fx = food.x * cell + cell / 2;
const fy = food.y * cell + cell / 2;
const r = cell * 0.22 + pulse * cell * 0.08;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.shadowBlur = 14 + pulse * 18;
ctx.shadowColor = '#f472b6';
ctx.fillStyle = '#f472b6';
ctx.beginPath();
ctx.arc(fx, fy, r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 8;
ctx.shadowColor = '#fff';
ctx.fillStyle = 'rgba(255,255,255,.72)';
ctx.beginPath();
ctx.arc(fx, fy, Math.max(1, r * 0.22), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
ctx.save();
ctx.globalCompositeOperation = 'lighter';
for (let i = snake.length - 1; i >= 0; i--) {
const seg = snake[i];
const ratio = snake.length > 1 ? i / (snake.length - 1) : 0;
const hue = 175 + i * 2.2 + Math.sin(now / 900) * 10 - ratio * 40;
const light = 62 - ratio * 16;
const alpha = 1;
const color = `hsla(${hue}, 100%, ${light}%, ${alpha})`;
const pad = cell * 0.11;
const size = cell - pad * 2;
const x = seg.x * cell + pad;
const y = seg.y * cell + pad;
const radius = Math.max(2, size * (i === 0 ? 0.42 : 0.26));
ctx.shadowBlur = i === 0 ? 22 : 10;
ctx.shadowColor = color;
ctx.fillStyle = color;
roundRect(x, y, size, size, radius);
ctx.fill();
if (i === 0) {
const cx = seg.x * cell + cell / 2;
const cy = seg.y * cell + cell / 2;
const eyeOffset = cell * 0.17;
const eyeR = Math.max(1.6, cell * 0.065);
let ex = 0;
let ey = 0;
if (dir.x !== 0) ex = dir.x * eyeOffset;
if (dir.y !== 0) ey = dir.y * eyeOffset;
const vertical = dir.y !== 0;
const leftX = cx - (vertical ? eyeOffset : 0);
const rightX = cx + (vertical ? eyeOffset : 0);
const topY = cy - (vertical ? 0 : eyeOffset);
const bottomY = cy + (vertical ? 0 : eyeOffset);
ctx.shadowBlur = 12;
ctx.shadowColor = '#fff';
ctx.fillStyle = 'rgba(255,255,255,.95)';
if (vertical) {
ctx.beginPath();
ctx.arc(leftX + ex, cy + ey, eyeR, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(rightX + ex, cy + ey, eyeR, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(cx + ex, topY + ey, eyeR, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(cx + ex, bottomY + ey, eyeR, 0, Math.PI * 2);
ctx.fill();
}
}
}
ctx.restore();
}
function frame(now) {
if (!isActive()) return;
const dt = Math.min(120, now - lastTime);
lastTime = now;
if (state === 'playing') {
acc += dt;
const interval = Math.max(68, 138 - Math.floor(score / 10) * 7);
while (acc >= interval) {
acc -= interval;
move();
if (state !== 'playing') break;
}
} else {
acc = 0;
}
draw(now);
requestAnimationFrame(frame);
}
const dirMap = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
w: { x: 0, y: -1 },
s: { x: 0, y: 1 },
a: { x: -1, y: 0 },
d: { x: 1, y: 0 },
W: { x: 0, y: -1 },
S: { x: 0, y: 1 },
A: { x: -1, y: 0 },
D: { x: 1, y: 0 }
};
window.addEventListener('resize', resize);
document.addEventListener('keydown', (e) => {
if (!isActive()) return;
const keyDir = dirMap[e.key];
const code = e.code || '';
if (keyDir) {
if (state === 'over') {
startGame();
} else {
queueDir(keyDir);
}
e.preventDefault();
return;
}
if (code === 'Space' || e.key === ' ') {
if (state === 'ready') startGame();
else if (state === 'over') startGame();
else togglePause();
e.preventDefault();
return;
}
if (code === 'Enter' || e.key === 'Enter') {
if (state === 'ready' || state === 'over' || state === 'paused') {
startGame();
}
e.preventDefault();
return;
}
if (e.key === 'r' || e.key === 'R') {
startGame();
e.preventDefault();
}
});
pauseBtn.addEventListener('click', togglePause);
actionBtn.addEventListener('click', () => {
if (state === 'paused') {
state = 'playing';
hideOverlay();
updateHud();
} else {
startGame();
}
});
let touchX = 0;
let touchY = 0;
let touching = false;
canvas.addEventListener('pointerdown', (e) => {
if (!isActive()) return;
touching = true;
touchX = e.clientX;
touchY = e.clientY;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('pointermove', (e) => {
if (!isActive() || !touching) return;
e.preventDefault();
}, { passive: false });
canvas.addEventListener('pointerup', (e) => {
if (!isActive() || !touching) return;
touching = false;
const dx = e.clientX - touchX;
const dy = e.clientY - touchY;
if (Math.abs(dx) < 18 && Math.abs(dy) < 18) return;
if (Math.abs(dx) > Math.abs(dy)) {
queueDir(dx > 0 ? { x: 1, y: 0 } : { x: -1, y: 0 });
} else {
queueDir(dy > 0 ? { x: 0, y: 1 } : { x: 0, y: -1 });
}
e.preventDefault();
}, { passive: false });
canvas.addEventListener('pointercancel', () => {
touching = false;
});
resize();
resetGame();
showOverlay('READY', 'WASD / 方向键移动,空格暂停,手机可滑动控制。', 'Start');
updateHud();
requestAnimationFrame(frame);
})();