diff --git a/README.md b/README.md
index 9f9f33b1295..0ee9934893d 100644
--- a/README.md
+++ b/README.md
@@ -179,3 +179,35 @@ Licenses for borrowed code can be found in `Settings -> Licenses` screen, and al
- Hypertile - tfernd - https://github.com/tfernd/HyperTile
- Initial Gradio script - posted on 4chan by an Anonymous user. Thank you Anonymous user.
- (You)
+
+# 贪吃蛇小游戏
+
+一个零依赖的浏览器小游戏,使用 HTML5 Canvas + 原生 JavaScript 构建。
+
+## 运行
+
+在本地启动一个静态服务器即可(推荐 Python 方式):
+
+```bash
+cd /workspace/game
+python3 -m http.server 8000
+```
+
+打开浏览器访问:`http://localhost:8000`
+
+如果你的环境有 Node.js,也可以:
+
+```bash
+npx --yes serve -l 8000 .
+```
+
+## 操作
+
+- 键盘:W/A/S/D 或 方向键 控制方向,空格 暂停/继续;在未开始或结束时按空格可重新开始。
+- 触控:滑动控制方向。
+
+## 玩法
+
+- 吃到食物加 1 分并变长,撞墙或撞到自己则游戏结束。
+- 每 5 分速度略微提升。
+- 最佳分数会自动保存在浏览器的 localStorage。
diff --git a/game/index.html b/game/index.html
new file mode 100644
index 00000000000..04f0b907bba
--- /dev/null
+++ b/game/index.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ 贪吃蛇 Snake
+
+
+
+
+
+
+
+
+
+
+
+
+
+
按开始
+
方向键 / WASD 控制,手机支持滑动。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/game/main.js b/game/main.js
new file mode 100644
index 00000000000..31c7e8846ac
--- /dev/null
+++ b/game/main.js
@@ -0,0 +1,351 @@
+'use strict';
+
+(function(){
+ const canvas = document.getElementById('game-canvas');
+ const ctx = canvas.getContext('2d');
+
+ const startButton = document.getElementById('start-btn');
+ const restartButton = document.getElementById('restart-btn');
+ const pauseButton = document.getElementById('pause-btn');
+ const overlay = document.getElementById('overlay');
+ const overlayTitle = document.getElementById('overlay-title');
+ const overlaySub = document.getElementById('overlay-sub');
+ const scoreEl = document.getElementById('score');
+ const bestScoreEl = document.getElementById('best-score');
+
+ const BEST_SCORE_KEY = 'snake_best_score_v1';
+
+ function loadBestScore(){
+ try{ return Number(localStorage.getItem(BEST_SCORE_KEY) || '0'); }
+ catch{ return 0; }
+ }
+ function saveBestScore(value){
+ try{ localStorage.setItem(BEST_SCORE_KEY, String(value)); }
+ catch{}
+ }
+
+ let bestScore = loadBestScore();
+ bestScoreEl.textContent = String(bestScore);
+
+ // Grid configuration
+ const GRID_SIZE = 24; // number of cells per side
+ let cellSize = Math.floor(canvas.width / GRID_SIZE);
+
+ function resizeCanvasToContainer(){
+ const parent = canvas.parentElement;
+ const rect = parent.getBoundingClientRect();
+ const size = Math.min(rect.width, 560); // clamp for readability
+ const devicePixelRatio = window.devicePixelRatio || 1;
+ const targetCssSize = Math.max(320, Math.floor(size));
+ canvas.style.width = targetCssSize + 'px';
+ canvas.style.height = targetCssSize + 'px';
+ canvas.width = Math.floor(targetCssSize * devicePixelRatio);
+ canvas.height = Math.floor(targetCssSize * devicePixelRatio);
+ cellSize = Math.floor(canvas.width / GRID_SIZE);
+ ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
+ }
+
+ window.addEventListener('resize', resizeCanvasToContainer);
+ resizeCanvasToContainer();
+
+ const Direction = Object.freeze({
+ Up: {x:0, y:-1, name:'Up'},
+ Down: {x:0, y:1, name:'Down'},
+ Left: {x:-1, y:0, name:'Left'},
+ Right: {x:1, y:0, name:'Right'},
+ });
+
+ function positionsEqual(a,b){ return a.x===b.x && a.y===b.y; }
+
+ function randomInt(min, max){ // inclusive
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+ }
+
+ function generateFoodPosition(snake){
+ const occupied = new Set(snake.map(p=> p.x + ':' + p.y));
+ while(true){
+ const pos = { x: randomInt(0, GRID_SIZE-1), y: randomInt(0, GRID_SIZE-1) };
+ if(!occupied.has(pos.x + ':' + pos.y)) return pos;
+ }
+ }
+
+ function willReverse(currentDir, nextDir){
+ return (currentDir.x + nextDir.x === 0 && currentDir.y + nextDir.y === 0);
+ }
+
+ const initialState = () => ({
+ snake: [
+ {x: 8, y: 12},
+ {x: 7, y: 12},
+ {x: 6, y: 12},
+ ],
+ direction: Direction.Right,
+ pendingDirections: [],
+ food: {x: 14, y:12},
+ score: 0,
+ isRunning: false,
+ isPaused: false,
+ isGameOver: false,
+ msPerStep: 120,
+ lastStepAt: 0,
+ accumulator: 0,
+ lastFrameTs: 0,
+ });
+
+ let state = initialState();
+
+ function setScore(newScore){
+ state.score = newScore;
+ scoreEl.textContent = String(newScore);
+ if(newScore > bestScore){
+ bestScore = newScore;
+ bestScoreEl.textContent = String(bestScore);
+ saveBestScore(bestScore);
+ }
+ }
+
+ function startGame(){
+ state = initialState();
+ state.isRunning = true;
+ overlay.hidden = true;
+ scoreEl.textContent = '0';
+ }
+
+ function restartGame(){
+ startGame();
+ }
+
+ function pauseOrResume(){
+ if(!state.isRunning || state.isGameOver){ return; }
+ state.isPaused = !state.isPaused;
+ overlay.hidden = state.isPaused ? false : true;
+ overlayTitle.textContent = state.isPaused ? '已暂停' : '';
+ overlaySub.textContent = state.isPaused ? '空格继续' : '';
+ }
+
+ function gameOver(){
+ state.isRunning = false;
+ state.isGameOver = true;
+ overlay.hidden = false;
+ overlayTitle.textContent = '游戏结束';
+ overlaySub.textContent = '点“重新开始”或按空格重来';
+ }
+
+ function step(deltaMs){
+ state.accumulator += deltaMs;
+ if(state.accumulator < state.msPerStep){ return; }
+ const steps = Math.floor(state.accumulator / state.msPerStep);
+ state.accumulator -= steps * state.msPerStep;
+
+ for(let i=0;i= GRID_SIZE || nextHead.y >= GRID_SIZE){
+ gameOver();
+ return;
+ }
+
+ // self collision
+ for(let k=0;k 70 && state.score % 5 === 0){
+ state.msPerStep -= 5;
+ }
+ state.food = generateFoodPosition(state.snake);
+ }else{
+ state.snake.pop();
+ }
+ }
+ }
+
+ function drawGrid(){
+ const size = canvas.width; // already scaled via DPR
+ ctx.fillStyle = '#0f1337';
+ ctx.fillRect(0, 0, size, size);
+
+ ctx.save();
+ ctx.globalAlpha = 0.12;
+ ctx.strokeStyle = '#7ba8ff';
+ ctx.lineWidth = 1;
+ for(let i=0;i<=GRID_SIZE;i++){
+ const p = i * cellSize;
+ ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, size); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(size, p); ctx.stroke();
+ }
+ ctx.restore();
+ }
+
+ function drawSnake(){
+ for(let i=0;i{
+ if(keyToDir.has(e.code)){
+ enqueueDirection(keyToDir.get(e.code));
+ e.preventDefault();
+ } else if(e.code === 'Space'){
+ if(!state.isRunning || state.isGameOver){
+ restartGame();
+ } else {
+ pauseOrResume();
+ }
+ e.preventDefault();
+ }
+ }, { passive: false });
+
+ // Touch controls (swipe)
+ let touchStart = null;
+ const SWIPE_THRESHOLD = 18; // px
+
+ canvas.addEventListener('touchstart', (e)=>{
+ if(e.touches.length === 1){
+ touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
+ }
+ }, { passive: true });
+ canvas.addEventListener('touchmove', (e)=>{
+ if(!touchStart || e.touches.length !== 1) return;
+ const dx = e.touches[0].clientX - touchStart.x;
+ const dy = e.touches[0].clientY - touchStart.y;
+ if(Math.abs(dx) < SWIPE_THRESHOLD && Math.abs(dy) < SWIPE_THRESHOLD){
+ return;
+ }
+ if(Math.abs(dx) > Math.abs(dy)){
+ enqueueDirection(dx > 0 ? Direction.Right : Direction.Left);
+ } else {
+ enqueueDirection(dy > 0 ? Direction.Down : Direction.Up);
+ }
+ touchStart = null;
+ }, { passive: true });
+
+ // Buttons
+ startButton.addEventListener('click', ()=>{
+ if(!state.isRunning){ startGame(); }
+ });
+ restartButton.addEventListener('click', ()=>{
+ restartGame();
+ });
+ pauseButton.addEventListener('click', ()=>{
+ pauseOrResume();
+ });
+
+ // Show initial overlay
+ overlay.hidden = false;
+ overlayTitle.textContent = '按开始';
+ overlaySub.textContent = '方向键 / WASD 控制,手机支持滑动。';
+})();
\ No newline at end of file
diff --git a/game/style.css b/game/style.css
new file mode 100644
index 00000000000..ab25c24f500
--- /dev/null
+++ b/game/style.css
@@ -0,0 +1,95 @@
+:root{
+ --bg: #0f1220;
+ --panel: #151935;
+ --panel-2: #1b214a;
+ --text: #e7ecff;
+ --muted: #9aa3c7;
+ --accent: #6dd6ff;
+ --accent-2: #7bffb5;
+ --danger: #ff6b6b;
+}
+*{ box-sizing: border-box; }
+html,body{ height:100%; }
+body{
+ margin:0;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Arial, "Apple Color Emoji", "Segoe UI Emoji";
+ background: radial-gradient(1200px 800px at 20% -10%, #1e2350, transparent),
+ radial-gradient(900px 600px at 120% 10%, #1a3a53, transparent),
+ var(--bg);
+ color: var(--text);
+}
+.container{
+ max-width: 720px;
+ margin: 24px auto;
+ padding: 16px;
+}
+.header{
+ display:flex;
+ align-items:center;
+ justify-content:space-between;
+ gap: 16px;
+}
+.header h1{
+ margin:0;
+ font-size: 28px;
+ letter-spacing: 0.5px;
+}
+.scores{ display:flex; gap: 12px; }
+.score{
+ background: linear-gradient(180deg, var(--panel), var(--panel-2));
+ padding:8px 12px;
+ border-radius: 10px;
+ border: 1px solid #232a5f;
+}
+.score span{ color: var(--muted); font-size: 12px; }
+.score strong{ display:block; font-size: 18px; margin-top:2px; }
+
+.stage{
+ position: relative;
+ margin-top:16px;
+ background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));
+ border: 1px solid #232a5f;
+ border-radius: 16px;
+ overflow: hidden;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.35);
+}
+canvas{
+ display:block;
+ width:100%;
+ height:auto;
+ image-rendering: pixelated;
+ background: repeating-linear-gradient(0deg, #0e1030, #0e1030 16px, #0f1337 16px, #0f1337 32px);
+}
+.overlay{
+ position:absolute; inset:0; display:grid; place-items:center;
+ backdrop-filter: blur(6px);
+ background: rgba(13,16,40,0.55);
+}
+.overlay-content{
+ text-align:center;
+ padding: 18px 20px;
+ background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02));
+ border: 1px solid #2a3270;
+ border-radius: 14px;
+}
+.overlay-actions{ margin-top: 12px; display:flex; justify-content:center; gap:10px; }
+
+.footer{ margin-top: 10px; color: var(--muted); }
+.hint{ margin: 8px 0 0; font-size: 12px; }
+.controls{ display:flex; gap:10px; }
+
+.btn{
+ appearance:none; border:none; cursor:pointer;
+ padding: 10px 14px; border-radius: 12px; color: var(--text);
+ background: linear-gradient(180deg, #1b2253, #161c43);
+ border: 1px solid #28327b;
+}
+.btn.primary{
+ background: linear-gradient(180deg, #1b4b7a, #173b5f);
+ border-color: #2a6aa5;
+}
+.btn:active{ transform: translateY(1px); }
+
+@media (max-width: 520px){
+ .header{ flex-direction: column; align-items:flex-start; }
+}
\ No newline at end of file