Full Code of epidemian/snake for AI

master 319a58d45acc cached
5 files
19.6 KB
5.6k tokens
23 symbols
1 requests
Download .txt
Repository: epidemian/snake
Branch: master
Commit: 319a58d45acc
Files: 5
Total size: 19.6 KB

Directory structure:
gitextract_eh2l0vp_/

├── README.md
├── UNLICENSE
├── index.html
├── snake.js
└── style.css

================================================
FILE CONTENTS
================================================

================================================
FILE: README.md
================================================
# URL Snake

Play the classic snake game on a URL!

<https://demian.ferrei.ro/snake>

This is how the game should look:

![Pro level gameplay](gameplay.gif)

Note that the game might be unplayable on some browsers for different reasons, like the browser not showing the full URL, or not allowing it to change so frequently, or escaping the Braille characters used to display the game.

Although this game is kind of a joke, bug reports, ideas and pull requests are always [welcome](https://github.com/epidemian/snake/issues)!


================================================
FILE: UNLICENSE
================================================
This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>URL Snake!</title>
<meta name=description content="Play the classic snake game on the browser URL."/>
<meta name=viewport content="width=device-width, initial-scale=1.0"/>
<link rel=canonical href="/snake/">
<link rel="shortcut icon" href=favicon.ico />
<link rel=stylesheet href=style.css>
<script data-goatcounter="https://demian.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
<script>
  if ('ontouchstart' in document.documentElement) {
    document.documentElement.classList.add('touch')
  }
</script>
</head>
<noscript>⚠ Sorry, this game requires JavaScript.</noscript>
<div id=url-container class=invisible>URL: <span id=url></span></div>
<div class=expandable>
  <button class="help-toggle expand-btn" aria-label=Help>?</button>
  <div class="expandable-content hidden">
    <span class=no-touch-only>Use the arrow keys or WASD to control the snake on the URL.</span>
    <span class=touch-only>Use the arrows to control the snake on the URL.</span>
    <a id=reveal-url href=#>Click here</a> if you can't see the page URL<span id=url-escaping-note class=invisible> or if it looks messed up with <span id=replacement-char-description></span></span>.&nbsp;<button class=collapse-btn aria-label=Hide>〈</button>
  </div>
</div>
<div id=max-score-container class="expandable hidden">
  <button class="high-score-toggle expand-btn" aria-label="Max score">!</button>
  <div class="expandable-content hidden">
    Your highest score is <span id=max-score-points></span>!
    <span id=max-score-grid></span><a id=share href=#><i class=icon-share></i>Share</a>&nbsp;<button class=collapse-btn aria-label=Hide>〈</button><span id=share-note class=invisible></span>
  </div>
</div>
<div class="controls touch-only">
  <button id=up>▲&#xFE0E;</button>
  <button id=left>◀&#xFE0E;</button>
  <button id=down>▼&#xFE0E;</button>
  <button id=right>▶&#xFE0E;</button>
</div>
<footer>
  Made with (⠠⠇) by <a href=https://twitter.com/epidemian>@epidemian</a> |
  <a href=https://github.com/epidemian/snake>Code</a>
</footer>
<script src=snake.js></script>


================================================
FILE: snake.js
================================================
'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 < INITIAL_SNAKE_LENGTH; 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 < GRID_WIDTH; 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 < grid.length; 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;

  $('#max-score-points').textContent = maxScorePoints;
  $('#max-score-grid').textContent = maxScoreGrid;
  $('#max-score-container').classList.remove('hidden');

  $('#share').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 < candidates.length; 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 < totalPixels; 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();


================================================
FILE: style.css
================================================
/* Reset */
html, body, h1, p {
  margin: 0;
  padding: 0;
}

* {
  box-sizing: border-box;
}

html {
  height: 100%;
  font-family: Arial, sans-serif;
  color: #222;
  line-height: 1.5;
}

body {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  padding: 10px;
}

a {
  color: #3473ee;
}

.controls {
  display: grid;
  grid-template-areas: ".    up   ."
                       "left down right";
  /* Limit the size of the controls by the viewport size. */
  width: 75vmin;
  height: 50vmin;
  margin: 10px auto;
}

#up { grid-area: up; }
#down { grid-area: down; }
#left { grid-area: left; }
#right { grid-area: right; }

.controls button {
  color: #888;
  font-size: 8vmin;
  background: none;
  border: 2px solid;
  margin: 2px;
  border-radius: 5vmin;
}

.controls button:focus {
  outline: none;
}

@media (min-width: 10cm) and (min-height: 10cm) {
  /* Avoid controls getting too big on larger touch devices. */
  .controls {
    position: absolute;
    width: 7.5cm;
    height: 5cm;
    bottom: 1.5cm;
    right: 1.5cm;
  }
  .controls button {
    font-size: 0.8cm;
    border-radius: 0.5cm;
  }
}

#url,
#max-score-grid {
  background: #8883;
  padding: 2px;
  border-radius: 3px;
}

#share {
  display: inline-block;
}

footer {
  margin-top: auto;
  font-size: 0.9rem;
}

.invisible {
  display: none !important;
}

.hidden {
  visibility: hidden;
}

:root.touch .no-touch-only,
:root:not(.touch) .touch-only {
  display: none;
}

.expand-btn,
.collapse-btn {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-weight: bold;
  cursor: pointer;
  width: 1rem;
}

.expand-btn,
.expandable {
  transition: transform, opacity;
  transition-duration: .4s;
}

.expandable {
  display: inline-block;
  position: relative;
  height: 1.5rem;
  transform: translateX(-100%);
  /* Clear body padding so it doesn't show on the left of the expand-btn */
  padding-right: 10px;
}

.expand-btn {
  position: absolute;
  right: 0;
  top: 0;
  transform: translateX(100%);
  opacity: 1;
}

.help-toggle {
  color: #0bc3ff;;
}

.high-score-toggle {
  color: #ff8c0b;
}

.collapse-btn {
  color: #aaa;
}

.expandable.expanded {
  height: auto;
  transform: none;
}

.expandable.expanded .expand-btn {
  opacity: 0;
}

#share-note {
  padding-left: 0.5rem;
}

#share-note.invisible {
  /* Only animate fading out. */
  transition: opacity 0.4s ease-in-out, display 0s ease-out 0.4s;
  transition-behavior: allow-discrete;
  opacity: 0;
  /* Hide with display:none to avoid messing up the position of the collapse button. */
  display: none;
}

@media (prefers-color-scheme: dark) {
  html {
    background: #222;
    color: #eee;
  }
}

/* Icon font styles copied from Fontello. */
@font-face {
  font-family: 'icons';
  src: url('icons.woff2?40046441') format('woff2');
  font-weight: normal;
  font-style: normal;
}
[class^="icon-"]:before, [class*=" icon-"]:before {
  font-family: "icons";
  font-style: normal;
  font-weight: normal;
  speak: none;
  display: inline-block;
  text-decoration: inherit;
  width: 1em;
  margin-right: .2em;
  text-align: center;
  /* For safety - reset parent styles, that can break glyph codes*/
  font-variant: normal;
  text-transform: none;
  /* Animation center compensation - margins should be symmetric */
  /* remove if not needed */
  margin-left: .2em;
  /* Font smoothing. That was taken from TWBS */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-share:before { content: '\e811'; }
Download .txt
gitextract_eh2l0vp_/

├── README.md
├── UNLICENSE
├── index.html
├── snake.js
└── style.css
Download .txt
SYMBOL INDEX (23 symbols across 1 files)

FILE: snake.js
  function main (line 22) | function main() {
  function detectBrowserUrlWhitespaceEscaping (line 42) | function detectBrowserUrlWhitespaceEscaping() {
  function cleanUrl (line 55) | function cleanUrl() {
  function setupEventHandlers (line 61) | function setupEventHandlers() {
  function initUrlRevealed (line 121) | function initUrlRevealed() {
  function setUrlRevealed (line 128) | function setUrlRevealed(value) {
  function startGame (line 138) | function startGame() {
  function updateWorld (line 152) | function updateWorld() {
  function endGame (line 187) | function endGame() {
  function drawWorld (line 198) | function drawWorld() {
  function gridString (line 229) | function gridString() {
  function tickTime (line 250) | function tickTime() {
  function currentScore (line 257) | function currentScore() {
  function cellAt (line 261) | function cellAt(x, y) {
  function bitAt (line 265) | function bitAt(x, y) {
  function setCellAt (line 269) | function setCellAt(x, y, cellType) {
  function dropFood (line 273) | function dropFood() {
  function changeDirection (line 291) | function changeDirection(newDir) {
  function drawMaxScore (line 301) | function drawMaxScore() {
  function showMaxScore (line 322) | function showMaxScore() {
  function shareScore (line 327) | function shareScore(scorePoints, grid) {
  function showShareNote (line 340) | function showShareNote(message) {
  function pickWhitespaceReplacementChar (line 354) | function pickWhitespaceReplacementChar() {
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (21K chars).
[
  {
    "path": "README.md",
    "chars": 526,
    "preview": "# URL Snake\n\nPlay the classic snake game on a URL!\n\n<https://demian.ferrei.ro/snake>\n\nThis is how the game should look:\n"
  },
  {
    "path": "UNLICENSE",
    "chars": 1211,
    "preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
  },
  {
    "path": "index.html",
    "chars": 2129,
    "preview": "<!DOCTYPE html>\n<html lang=en>\n<head>\n<meta charset=utf-8>\n<title>URL Snake!</title>\n<meta name=description content=\"Pla"
  },
  {
    "path": "snake.js",
    "chars": 12661,
    "preview": "'use strict';\n\nvar GRID_WIDTH = 40;\nvar SNAKE_CELL = 1;\nvar FOOD_CELL = 2;\nvar UP = {x: 0, y: -1};\nvar DOWN = {x: 0, y: "
  },
  {
    "path": "style.css",
    "chars": 3541,
    "preview": "/* Reset */\nhtml, body, h1, p {\n  margin: 0;\n  padding: 0;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nhtml {\n  height: 100%;\n  "
  }
]

About this extraction

This page contains the full source code of the epidemian/snake GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (19.6 KB), approximately 5.6k tokens, and a symbol index with 23 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!