// ==UserScript==
// @name GitHub Contributions Snake
// @namespace http://tampermonkey.net/
// @version 2024-07-19
// @description turn github contribs to snake game
// @author GV3Dev
// @match https://github.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant none
// @license MIT
// ==/UserScript==
class SnakeElem {
constructor() {
this.body = [{x: 0, y: 0}];
this.length = 1;
this.foodEaten = 0;
this.direction = "right";
}
createSnake(cells, columns) {
for (let i = 0; i < this.length; i++) {
const pos = this.body[i];
makeGreen(cells[pos.y * columns + pos.x], 4);
}
}
move(direction, cells, columns, rows, food) {
const head = { ...this.body[0] };
switch (direction) {
case "left":
if (this.direction !== "right") this.direction = "left";
break;
case "right":
if (this.direction !== "left") this.direction = "right";
break;
case "down":
if (this.direction !== "up") this.direction = "down";
break;
case "up":
if (this.direction !== "down") this.direction = "up";
break;
}
switch (this.direction) {
case "left":
head.x = (head.x - 1 + columns) % columns;
break;
case "right":
head.x = (head.x + 1) % columns;
break;
case "down":
head.y = (head.y + 1) % rows;
break;
case "up":
head.y = (head.y - 1 + rows) % rows;
break;
}
this.body.unshift(head);
if (head.x === food.position.x && head.y === food.position.y) {
this.foodEaten++;
this.length += 2;
food.getEaten(cells, this, columns, rows);
} else {
const tail = this.body.pop();
cells[tail.y * columns + tail.x].setAttribute("data-level", "0");
}
this.createSnake(cells, columns);
}
}
class FoodElem {
constructor() {
this.position = null;
}
getEaten(cells, snake, columns, rows) {
cells[this.position.y * columns + this.position.x].style.backgroundColor = "#161b22";
this.createFood(cells, columns, rows, snake);
}
createFood(cells, columns, rows, snake) {
let randCell;
let x, y;
do {
randCell = Math.floor(Math.random() * cells.length);
y = Math.floor(randCell / columns);
x = randCell % columns;
} while (cells[randCell].getAttribute("data-level") !== "0" || snake.body.some(part => part.x === x && part.y === y));
cells[y * columns + x].style.backgroundColor = "red";
this.position = {x, y};
}
}
const main = () => {
const keepCheck = setInterval(() => {
const contribBoard = document.querySelector(".js-yearly-contributions");
if (contribBoard !== null) {
clearInterval(keepCheck);
let cells = contribBoard.querySelectorAll(`.ContributionCalendar-day`);
let columns = contribBoard.querySelector(`.js-calendar-graph-table tbody`).firstElementChild.children.length;
let rows = Math.floor(cells.length / columns+1);
cleanGrid();
const snake = new SnakeElem();
const food = new FoodElem();
food.createFood(cells, columns, rows, snake);
snake.createSnake(cells, columns);
appendControls(snake, cells, columns, rows, food);
}
}, 100);
}
const appendControls = (snake, cells, columns, rows, food) => {
window.addEventListener("keydown", (evt) => {
if (["ArrowUp", "ArrowRight", "ArrowLeft", "ArrowDown"].includes(evt.code)) {
evt.preventDefault();
}
if (evt.code === "ArrowUp") {
snake.move("up", cells, columns, rows, food);
} else if (evt.code === "ArrowRight") {
snake.move("right", cells, columns, rows, food);
} else if (evt.code === "ArrowLeft") {
snake.move("left", cells, columns, rows, food);
} else if (evt.code === "ArrowDown") {
snake.move("down", cells, columns, rows, food);
}
});
}
function makeGreen(elem, level) {
elem.setAttribute("data-level", `${level}`);
}
function cleanGrid() {
const contribBoard = document.querySelector(".js-yearly-contributions");
let cells = contribBoard.querySelectorAll(`.ContributionCalendar-day`);
cells.forEach((cell) => { cell.setAttribute("data-level", "0"); });
}
main();