百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

地址栏也能打游戏!程序员用不到400行代码,在浏览器地址栏复活《贪吃蛇》,网友惊叹:怎么想出这个点子的?

myzbx 2025-10-14 01:59 18 浏览

整理 | 苏宓

出品 | CSDN(ID:CSDNnews)

打开浏览器的时候,你有没有想过,地址栏也能玩游戏?大多数人肯定没这么想过——毕竟它平时的功能也就那么简单:输入网址、回车、加载网页。但一些程序员总能做些让人意想不到的事。

最近,一位开发者就把经典的《贪吃蛇》搬进了地址栏里。没错,就是小时候大家都玩过的像素版贪吃蛇,现在竟然能在地址栏里动起来。

400 行不到的 JavaScript 代码,把「贪吃蛇」塞到地址栏中

这个项目名叫 URL Snake,出自开发者 Demian Ferreiro 之手。

简单来看,他用了不到 400 行 JavaScript 代码,就在一个原本只能显示文字的地方“造出”了这款游戏。

话不多说,「Talk is Cheap,Show me the code」,完整代码如下:

'use strict';var GRID_WIDTH = 40;var SNAKE_CELL = 1;var FOOD_CELL = 2;var UP = {x: 0, y: -1};var DOWN = {x: 0, y: 1};var LEFT = {x: -1, y: 0};var RIGHT = {x: 1, y: 0};var INITIAL_SNAKE_LENGTH = 4;var BRAILLE_SPACE = '\u2800';var grid;var snake;var currentDirection;var moveQueue;var hasMoved;var gamePaused = false;var urlRevealed = false;var whitespaceReplacementChar;function main {  detectBrowserUrlWhitespaceEscaping;  cleanUrl;  setupEventHandlers;  drawMaxScore;  initUrlRevealed;  startGame;  var lastFrameTime = Date.now;  window.requestAnimationFrame(function frameHandler {    var now = Date.now;    if (!gamePaused && now - lastFrameTime >= tickTime) {      updateWorld;      drawWorld;      lastFrameTime = now;    }    window.requestAnimationFrame(frameHandler);  });}function detectBrowserUrlWhitespaceEscaping {  // Write two Braille whitespace characters to the hash because Firefox doesn't  // escape single WS chars between words.  history.replaceState(null, null, '#' + BRAILLE_SPACE + BRAILLE_SPACE)  if (location.hash.indexOf(BRAILLE_SPACE) == -1) {    console.warn('Browser is escaping whitespace characters on URL')    var replacementData = pickWhitespaceReplacementChar;    whitespaceReplacementChar = replacementData[0];    $('#url-escaping-note').classList.remove('invisible');    $('#replacement-char-description').textContent = replacementData[1];  }}function cleanUrl {  // In order to have the most space for the game, shown on the URL hash,  // remove all query string parameters and trailing / from the URL.  history.replaceState(null, null, location.pathname.replace(/\b\/$/, ''));}function setupEventHandlers {  var directionsByKey = {    // Arrows    37: LEFT, 38: UP, 39: RIGHT, 40: DOWN,    // WASD    87: UP, 65: LEFT, 83: DOWN, 68: RIGHT,    // hjkl    75: UP, 72: LEFT, 74: DOWN, 76: RIGHT  };  document.onkeydown = function (event) {    var key = event.keyCode;    if (key in directionsByKey) {      changeDirection(directionsByKey[key]);    }  };  // Use touchstart instead of mousedown because these arrows are only shown on  // touch devices, and also because there is a delay between touchstart and  // mousedown on those devices, and the game should respond ASAP.  $('#up').ontouchstart = function  { changeDirection(UP) };  $('#down').ontouchstart = function  { changeDirection(DOWN) };  $('#left').ontouchstart = function  { changeDirection(LEFT) };  $('#right').ontouchstart = function  { changeDirection(RIGHT) };  window.onblur = function pauseGame {    gamePaused = true;    window.history.replaceState(null, null, location.hash + '[paused]');  };  window.onfocus = function unpauseGame {    gamePaused = false;    drawWorld;  };  $('#reveal-url').onclick = function (e) {    e.preventDefault;    setUrlRevealed(!urlRevealed);  };  document.querySelectorAll('.expandable').forEach(function (expandable) {    var expand = expandable.querySelector('.expand-btn');    var collapse = expandable.querySelector('.collapse-btn');    var content = expandable.querySelector('.expandable-content');    expand.onclick = collapse.onclick = function  {      expand.classList.remove('hidden');      content.classList.remove('hidden');      expandable.classList.toggle('expanded');    };    // Hide the expand button or the content when the animation ends so those    // elements are not interactive anymore.    // Surely there's a way to do this with CSS animations more directly.    expandable.ontransitionend = function  {      var expanded = expandable.classList.contains('expanded');      expand.classList.toggle('hidden', expanded);      content.classList.toggle('hidden', !expanded);    };  });}function initUrlRevealed {  setUrlRevealed(Boolean(localStorage.urlRevealed));}// Some browsers don't display the page URL, either partially (e.g. Safari) or// entirely (e.g. mobile in-app web-views). To make the game playable in such// cases, the player can choose to "reveal" the URL within the page body.function setUrlRevealed(value) {  urlRevealed = value;  $('#url-container').classList.toggle('invisible', !urlRevealed);  if (urlRevealed) {    localStorage.urlRevealed = 'y';  } else {    delete localStorage.urlRevealed;  }}function startGame {  grid = new Array(GRID_WIDTH * 4);  snake = ;  for (var x = 0; x     var y = 2;    snake.unshift({x: x, y: y});    setCellAt(x, y, SNAKE_CELL);  }  currentDirection = RIGHT;  moveQueue = ;  hasMoved = false;  dropFood;}function updateWorld {  if (moveQueue.length) {    currentDirection = moveQueue.pop;  }  var head = snake[0];  var tail = snake[snake.length - 1];  var newX = head.x + currentDirection.x;  var newY = head.y + currentDirection.y;  var outOfBounds = newX 0 || newX >= GRID_WIDTH || newY 0 || newY >= 4;  var collidesWithSelf = cellAt(newX, newY) === SNAKE_CELL    && !(newX === tail.x && newY === tail.y);  if (outOfBounds || collidesWithSelf) {    endGame;    startGame;    return;  }  var eatsFood = cellAt(newX, newY) === FOOD_CELL;  if (!eatsFood) {    snake.pop;    setCellAt(tail.x, tail.y, null);  }  // Advance head after tail so it can occupy the same cell on next tick.  setCellAt(newX, newY, SNAKE_CELL);  snake.unshift({x: newX, y: newY});  if (eatsFood) {    dropFood;  }}function endGame {  var score = currentScore;  var maxScore = parseInt(localStorage.maxScore || 0);  if (score > 0 && score > maxScore && hasMoved) {    localStorage.maxScore = score;    localStorage.maxScoreGrid = gridString;    drawMaxScore;    showMaxScore;  }}function drawWorld {  var hash = '#|' + gridString + '|[score:' + currentScore() + ']';  if (urlRevealed) {    // Use the original game representation on the on-DOM view, as there are no    // escaping issues there.    $('#url').textContent = location.href.replace(/#.*$/, '') + hash;  }  // Modern browsers escape whitespace characters on the address bar URL for  // security reasons. In case this browser does that, replace the empty Braille  // character with a non-whitespace (and hopefully non-intrusive) symbol.  if (whitespaceReplacementChar) {    hash = hash.replace(/\u2800/g, whitespaceReplacementChar);  }  history.replaceState(null, null, hash);  // Some browsers have a rate limit on history.replaceState calls, resulting  // in the URL not updating at all for a couple of seconds. In those cases,  // location.hash is updated directly, which is unfortunate, as it causes a new  // navigation entry to be created each time, effectively hijacking the user's  // back button.  if (decodeURIComponent(location.hash) !== hash) {    console.warn(      'history.replaceState throttling detected. Using location.hash fallback'    );    location.hash = hash;  }}function gridString {  var str = '';  for (var x = 0; x 2) {    // Unicode Braille patterns are 256 code points going from 0x2800 to 0x28FF.    // They follow a binary pattern where the bits are, from least significant    // to most:     // So, for example, 147 (10010011) corresponds to     var n = 0      | bitAt(x, 0) 0      | bitAt(x, 1) 1      | bitAt(x, 2) 2      | bitAt(x + 1, 0) 3      | bitAt(x + 1, 1) 4      | bitAt(x + 1, 2) 5      | bitAt(x, 3) 6      | bitAt(x + 1, 3) 7;    str += String.fromCharCode(0x2800 + n);  }  return str;}function tickTime {  // Game speed increases as snake grows.  var start = 125;  var end = 75;  return start + snake.length * (end - start) / grid.length;}function currentScore {  return snake.length - INITIAL_SNAKE_LENGTH;}function cellAt(x, y) {  return grid[x % GRID_WIDTH + y * GRID_WIDTH];}function bitAt(x, y) {  return cellAt(x, y) ? 1 : 0;}function setCellAt(x, y, cellType) {  grid[x % GRID_WIDTH + y * GRID_WIDTH] = cellType;}function dropFood {  var emptyCells = grid.length - snake.length;  if (emptyCells === 0) {    return;  }  var dropCounter = Math.floor(Math.random * emptyCells);  for (var i = 0; i     if (grid[i] === SNAKE_CELL) {      continue;    }    if (dropCounter === 0) {      grid[i] = FOOD_CELL;      break;    }    dropCounter--;  }}function changeDirection(newDir) {  var lastDir = moveQueue[0] || currentDirection;  var opposite = newDir.x + lastDir.x === 0 && newDir.y + lastDir.y === 0;  if (!opposite) {    // Process moves in a queue to prevent multiple direction changes per tick.    moveQueue.unshift(newDir);  }  hasMoved = true;}function drawMaxScore {  var maxScore = localStorage.maxScore;  if (maxScore == null) {    return;  }  var maxScorePoints = maxScore == 1 ? '1 point' : maxScore + ' points'  var maxScoreGrid = localStorage.maxScoreGrid;  $('-score-points').textContent = maxScorePoints;  $('-score-grid').textContent = maxScoreGrid;  $('-score-container').classList.remove('hidden');  $('').onclick = function (e) {    e.preventDefault;    shareScore(maxScorePoints, maxScoreGrid);  };}// Expands the high score details if collapsed. Only done when beating the// highest score, to grab the player's attention.function showMaxScore {  if ($('#max-score-container.expanded')) return  $('#max-score-container .expand-btn').click;}function shareScore(scorePoints, grid) {  var message = '|' + grid + '| Got ' + scorePoints +    ' playing this stupid snake game on the browser URL!';  var url = $('link[rel=canonical]').href;  if (navigator.share) {    navigator.share({text: message, url: url});  } else {    navigator.clipboard.writeText(message + '\n' + url)      .then(function  { showShareNote('copied to clipboard') })      .catch(function  { showShareNote('clipboard write failed') })  }}function showShareNote(message) {  var note = $("#share-note");  note.textContent = message;  note.classList.remove("invisible");  setTimeout(function  { note.classList.add("invisible") }, 1000);}// Super hacky function to pick a suitable character to replace the empty// Braille character (u+2800) when the browser escapes whitespace on the URL.// We want to pick a character that's close in width to the empty Braille symbol// —so the game doesn't stutter horizontally—, and also pick something that's// not too visually noisy. So we actually measure how wide and how "dark" some// candidate characters are when rendered by the browser (using a canvas) and// pick the first that passes both criteria.function pickWhitespaceReplacementChar {  var candidates = [    // U+0ADF is part of the Gujarati Unicode blocks, but it doesn't have an    // associated glyph. For some reason, Chrome renders is as totally blank and    // almost the same size as the Braille empty character, but it doesn't    // escape it on the address bar URL, so this is the perfect replacement    // character. This behavior of Chrome is probably a bug, and might be    // changed at any time, and in other browsers like Firefox this character is    // rendered with an ugly "undefined" glyph, so it'll get filtered out by the    // width or the "blankness" check in either of those cases.    ['', 'strange symbols'],    // U+27CB Mathematical Rising Diagonal, not a great replacement for    // whitespace, but is close to the correct size and blank enough.    ['', 'some weird slashes']  ];  var N = 5;  var canvas = document.createElement('canvas');  var ctx = canvas.getContext('2d');  ctx.font = '30px system-ui';  var targetWidth = ctx.measureText(BRAILLE_SPACE.repeat(N)).width;  for (var i = 0; i     var char = candidates[i][0];    var str = char.repeat(N);    var width = ctx.measureText(str).width;    var similarWidth = Math.abs(targetWidth - width) / targetWidth 0.1;    ctx.clearRect(0, 0, canvas.width, canvas.height);    ctx.fillText(str, 0, 30);    var pixelData = ctx.getImageData(0, 0, width, 30).data;    var totalPixels = pixelData.length / 4;    var coloredPixels = 0;    for (var j = 0; j       var alpha = pixelData[j * 4 + 3];      if (alpha != 0) {        coloredPixels++;      }    }    var notTooDark = coloredPixels / totalPixels 0.15;    if (similarWidth && notTooDark) {      return candidates[i];    }  }  // Fallback to a safe U+2591 Light Shade.  return ['', 'some kind of "fog"'];}var $ = document.querySelector.bind(document);main;

听起来有点疯狂,但真的能玩,而且画面也不是乱闪的乱码。

在 Chrome 浏览器上打开,界面如下所示:你能清晰地看到一条由密密麻麻的盲文符号组成的“蛇”在地址栏里爬动,即「长的点」代表贪吃蛇,「单个点」是食物,吃掉小点点代表的食物,身体一点点变长。

整个画面虽然简陋,但加上浏览器实时更新 URL 的那种“闪动”,它像极了 DOS 年代的小游戏,简洁、直接、充满旧时代的技术感,也引发了一波回忆潮。

从操作上看,游戏支持「↑↓←→」方向键或 WASD 控制蛇移动。随着吃掉的“食物”增多,速度也会慢慢提升,难度上升。你需要反应足够快才能避免撞墙或自咬。虽然画面高度只有 4 行,但可玩性依然不错。

为了感兴趣的小伙伴能上手体验,Demian Ferreiro 将项目代码在 GitHub 上开源了:
https://github.com/epidemian/snake

试玩地址:
http://demian.ferrei.ro/snake

游戏原理

其实从技术上讲,要在浏览器那条显得有些狭窄的地址栏里面塞进一个小游戏,说简单不简单,说难也确实挺有门道。毕竟那地方既不能嵌入 Canvas 或 SVG,也没有图形 API 可以用,几乎不可能画出像样的画面。

好在 Ferreiro 向来不是一个墨守成规的极客,正如上图所示,他想出了一个让人意想不到的办法——用 Unicode 字符“画”出游戏画面。可以说,这波操作把“极简主义”玩到了极致。

至于为什么 Ferreiro 会想到这个离谱的项目,他自己也记不太清了。他在 Hacker News 上提到,灵感可能来源于 Unicode 的盲文字符(Braille)系统。他发现一个有趣的规律:

每个盲文字符都是 2×4 的点阵,每个点只有两种状态——亮或不亮。8 个点组合起来,正好对应一个字节,总共 256 种组合,而且 Unicode 把这些组合全都映射成编码点。

Ferreiro 兴奋地说:“这不就是展示字节级动画潜力的完美载体吗?”

于是,他把这个思路用在了 URL 栏里:用一串盲文字符拼出一块虚拟的“游戏屏幕”,每一帧都重新生成字符,更新蛇的形状和位置。

这个版本的《贪吃蛇》在一个 40×4 的“像素格”上运行,用了 requestAnimationFrame 来驱动动画,让一串串盲文字符在地址栏中滑动起来。虽然只有四行高,但蛇一旦上下移动,玩家就得迅速反应,否则分分钟撞墙。

玩这个游戏时,其实就是浏览器不断修改地址栏内容,用不同的 Unicode 符号“刷出”画面。它有点像早年程序员在命令行窗口里做 ASCII 动画,只不过这次空间更狭小,也更有创意——一条蛇,硬是在一行网址里“活”了过来。

副作用——打开浏览器的“历史记录”,网友:“天塌了”

玩着玩着,很多人会注意到一个奇怪的副作用:浏览器里的历史记录会被这个网址疯狂「刷屏」。

也不用太担心,正如上文所述,因为每一次蛇的移动都意味着地址栏内容发生了变化,浏览器就会记录一次新“访问”。短短一局游戏下来,你的历史记录可能已经塞满几百个“URL Snake”的痕迹。

Chrome 用户可以靠批量删除功能一次清掉,但如果你用的是其他浏览器,那就只能慢慢手动清理。

此外,游戏的画面空间非常有限。只有四行“像素”的高度,让上下移动变得特别危险。稍微操作迟一点就容易撞上自己。再加上地址栏本身不是为显示图形而生,盲文字符的显示效果也会受不同系统和字体影响,在某些浏览器里可能略显错位。换句话说,这并不是一款“完美”的游戏,而更像是一场炫技实验。

“这个项目本身带着点玩笑性质,但也不妨可以继续探索”

很多人好奇 Ferreiro 为什么要这么折腾?做一个普通网页游戏不是更简单吗?

其实,这种项目的意义不在“实用”,而在“创意”和“挑战”。对开发者来说,URL Snake 就像一场极限运动。它验证了一个问题——“我们能不能在完全不合适的环境里做出游戏?” 这种逆向思维带有一点黑客精神,也让人想起早期互联网的自由氛围:没人告诉你什么能做、什么不能做。

Ferreiro 在发布时也说过,这个项目本身带着点玩笑性质,但他觉得有趣的地方就在于:地址栏是网页中最被忽视的部分,它几乎没有被用作创意表达的空间。而他想让大家重新注意到这一点。

他也表示愿意继续改进,欢迎大家在 GitHub 上提交 bug、提意见、甚至直接拉个 PR 一起完善。

最后

看到这样一款游戏的诞生,HN 上网友也纷纷表达了自己的看法:

  • CobrastanJorji:太棒了。我喜欢人们用非常富有创意的方式让事物以奇怪的方式变得互动。百分百的黑客精神。干得好。

  • system2:对于普通人来说,这可能看起来没什么,但对我来说这太疯狂了。你们这些人到底是怎么想出这些点子的……

甚至有人期待,什么时候能在地址栏里面玩 DOOM 游戏?

其实说到底,URL Snake 不只是一个小游戏,更像是一场创意实验。它证明了即使在最“不适合”的环境里,也依然可以找到代码表达的可能。它没有酷炫的图形,也没有复杂的关卡,却让人看到了编程的另一种浪漫:在规则之外寻找惊喜。

相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...