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