🚿 clean up
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
# snk
|
# snk
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Generates a snake game from a github user contributions grid and output a screen capture as gif
|
Generates a snake game from a github user contributions grid and output a screen capture as gif
|
||||||
|
|
||||||
---
|
- [demo](https://platane.github.io/snk/index.html)
|
||||||
|
|
||||||
[demo](https://platane.github.io/snk/index.html)
|
- [github action](https://github.com/marketplace/actions/generate-snake-game-from-github-contribution-grid)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Grid, Color } from "./grid";
|
|||||||
const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a;
|
const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a;
|
||||||
|
|
||||||
export const generateEmptyGrid = (width: number, height: number) =>
|
export const generateEmptyGrid = (width: number, height: number) =>
|
||||||
generateGrid(width, height, { colors: [], emptyP: 1 });
|
generateRandomGrid(width, height, { colors: [], emptyP: 1 });
|
||||||
|
|
||||||
export const generateGrid = (
|
export const generateRandomGrid = (
|
||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
options: { colors: Color[]; emptyP: number } = {
|
options: { colors: Color[]; emptyP: number } = {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Point } from "./point";
|
import { Point } from "./point";
|
||||||
|
|
||||||
|
export type Snake = Point[];
|
||||||
|
|
||||||
export const snakeSelfCollideNext = (
|
export const snakeSelfCollideNext = (
|
||||||
snake: Point[],
|
snake: Snake,
|
||||||
direction: Point,
|
direction: Point,
|
||||||
options: { maxSnakeLength: number }
|
options: { maxSnakeLength: number }
|
||||||
) => {
|
) => {
|
||||||
@@ -14,11 +16,11 @@ export const snakeSelfCollideNext = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const snakeSelfCollide = (snake: Point[]) => {
|
export const snakeSelfCollide = (snake: Snake) => {
|
||||||
for (let i = 1; i < snake.length; i++)
|
for (let i = 1; i < snake.length; i++)
|
||||||
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
|
if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
|
export const copySnake = (x: Snake) => x.map((p) => ({ ...p }));
|
||||||
|
|||||||
@@ -1,81 +1,89 @@
|
|||||||
// import { generateGrid } from "@snk/compute/generateGrid";
|
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||||
|
|
||||||
import { generateGrid } from "@snk/compute/generateGrid";
|
|
||||||
import { Color, copyGrid } from "@snk/compute/grid";
|
import { Color, copyGrid } from "@snk/compute/grid";
|
||||||
import { computeBestRun } from "@snk/compute";
|
import { computeBestRun } from "@snk/compute";
|
||||||
import { step } from "@snk/compute/step";
|
import { step } from "@snk/compute/step";
|
||||||
import { drawWorld } from "@snk/draw/drawWorld";
|
import { drawWorld } from "@snk/draw/drawWorld";
|
||||||
import { Point } from "@snk/compute/point";
|
import { copySnake } from "@snk/compute/snake";
|
||||||
|
|
||||||
const copySnake = (x: Point[]) => x.map((p) => ({ ...p }));
|
const drawOptions = {
|
||||||
|
sizeBorderRadius: 2,
|
||||||
export const run = async () => {
|
sizeCell: 16,
|
||||||
const drawOptions = {
|
sizeDot: 12,
|
||||||
sizeBorderRadius: 2,
|
colorBorder: "#1b1f230a",
|
||||||
sizeCell: 16,
|
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
||||||
sizeDot: 12,
|
colorEmpty: "#ebedf0",
|
||||||
colorBorder: "#1b1f230a",
|
colorSnake: "purple",
|
||||||
colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" },
|
|
||||||
colorEmpty: "#ebedf0",
|
|
||||||
colorSnake: "purple",
|
|
||||||
};
|
|
||||||
|
|
||||||
const gameOptions = { maxSnakeLength: 5 };
|
|
||||||
|
|
||||||
const grid0 = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
|
||||||
|
|
||||||
const snake0 = [
|
|
||||||
{ x: 4, y: -1 },
|
|
||||||
{ x: 3, y: -1 },
|
|
||||||
{ x: 2, y: -1 },
|
|
||||||
{ x: 1, y: -1 },
|
|
||||||
{ x: 0, y: -1 },
|
|
||||||
];
|
|
||||||
const stack0: Color[] = [];
|
|
||||||
|
|
||||||
const chain = computeBestRun(grid0, snake0, gameOptions);
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
|
|
||||||
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
const ctx = canvas.getContext("2d")!;
|
|
||||||
|
|
||||||
const update = (n: number) => {
|
|
||||||
const snake = copySnake(snake0);
|
|
||||||
const stack = stack0.slice();
|
|
||||||
const grid = copyGrid(grid0);
|
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
|
|
||||||
|
|
||||||
ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
drawWorld(ctx, grid, snake, stack, drawOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const input: any = document.createElement("input");
|
|
||||||
input.type = "range";
|
|
||||||
input.style.width = "100%";
|
|
||||||
input.min = 0;
|
|
||||||
input.max = chain.length;
|
|
||||||
input.step = 1;
|
|
||||||
input.value = 0;
|
|
||||||
input.addEventListener("input", () => update(+input.value));
|
|
||||||
document.addEventListener("click", () => input.focus());
|
|
||||||
|
|
||||||
document.body.appendChild(input);
|
|
||||||
|
|
||||||
update(+input.value);
|
|
||||||
|
|
||||||
// while (chain.length) {
|
|
||||||
// await wait(100);
|
|
||||||
|
|
||||||
// step(grid, snake, stack, chain.shift()!, gameOptions);
|
|
||||||
|
|
||||||
// ctx.clearRect(0, 0, 9999, 9999);
|
|
||||||
// drawWorld(ctx, grid, snake, stack, options);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
run();
|
const gameOptions = { maxSnakeLength: 5 };
|
||||||
|
|
||||||
|
const grid0 = generateRandomGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
|
|
||||||
|
const snake0 = [
|
||||||
|
{ x: 4, y: -1 },
|
||||||
|
{ x: 3, y: -1 },
|
||||||
|
{ x: 2, y: -1 },
|
||||||
|
{ x: 1, y: -1 },
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
];
|
||||||
|
const stack0: Color[] = [];
|
||||||
|
|
||||||
|
const chain = computeBestRun(grid0, snake0, gameOptions);
|
||||||
|
|
||||||
|
//
|
||||||
|
// draw
|
||||||
|
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = drawOptions.sizeCell * (grid0.width + 4);
|
||||||
|
canvas.height = drawOptions.sizeCell * (grid0.height + 4) + 100;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
|
const update = (n: number) => {
|
||||||
|
const snake = copySnake(snake0);
|
||||||
|
const stack = stack0.slice();
|
||||||
|
const grid = copyGrid(grid0);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++) step(grid, snake, stack, chain[i], gameOptions);
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, 9999, 9999);
|
||||||
|
drawWorld(ctx, grid, snake, stack, drawOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// controls
|
||||||
|
|
||||||
|
const input: any = document.createElement("input");
|
||||||
|
input.type = "range";
|
||||||
|
input.style.width = "100%";
|
||||||
|
input.min = 0;
|
||||||
|
input.max = chain.length;
|
||||||
|
input.step = 1;
|
||||||
|
input.value = 0;
|
||||||
|
input.addEventListener("input", () => {
|
||||||
|
setAutoPlay(false);
|
||||||
|
update(+input.value);
|
||||||
|
});
|
||||||
|
document.addEventListener("click", () => input.focus());
|
||||||
|
|
||||||
|
document.body.appendChild(input);
|
||||||
|
|
||||||
|
const autoplayButton = document.createElement("button");
|
||||||
|
let cancel: any;
|
||||||
|
const loop = () => {
|
||||||
|
input.value = (1 + input.value) % +input.max;
|
||||||
|
update(+input.value);
|
||||||
|
cancelAnimationFrame(cancel);
|
||||||
|
cancel = requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
const setAutoPlay = (a: boolean) => {
|
||||||
|
autoplayButton.innerHTML = a ? "pause ⏸" : "play ▶";
|
||||||
|
if (a) loop();
|
||||||
|
else cancelAnimationFrame(cancel);
|
||||||
|
};
|
||||||
|
autoplayButton.addEventListener("click", () =>
|
||||||
|
setAutoPlay(autoplayButton.innerHTML === "pause ⏸")
|
||||||
|
);
|
||||||
|
|
||||||
|
setAutoPlay(true);
|
||||||
|
update(+input.value);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
"name": "@snk/demo",
|
"name": "@snk/demo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snk/compute": "1.0.0"
|
"@snk/compute": "1.0.0",
|
||||||
|
"@snk/draw": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack": "4.43.0",
|
"webpack": "4.43.0",
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ const config: Configuration = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
// game
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
title: "demo",
|
title: "demo",
|
||||||
filename: "index.html",
|
filename: "index.html",
|
||||||
@@ -39,9 +38,6 @@ const config: Configuration = {
|
|||||||
|
|
||||||
devtool: false,
|
devtool: false,
|
||||||
stats: "errors-only",
|
stats: "errors-only",
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
devServer: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createGif } from "..";
|
import { createGif } from "..";
|
||||||
import { generateGrid } from "@snk/compute/generateGrid";
|
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||||
import { computeBestRun } from "@snk/compute";
|
import { computeBestRun } from "@snk/compute";
|
||||||
|
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
@@ -17,7 +17,7 @@ const gameOptions = { maxSnakeLength: 5 };
|
|||||||
const gifOptions = { delay: 200 };
|
const gifOptions = { delay: 200 };
|
||||||
|
|
||||||
it("should generate gif", async () => {
|
it("should generate gif", async () => {
|
||||||
const grid = generateGrid(14, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
const grid = generateRandomGrid(14, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
|
|
||||||
const snake = [
|
const snake = [
|
||||||
{ x: 4, y: -1 },
|
{ x: 4, y: -1 },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createGif } from "..";
|
import { createGif } from "..";
|
||||||
import { generateGrid } from "@snk/compute/generateGrid";
|
import { generateRandomGrid } from "@snk/compute/generateGrid";
|
||||||
import { computeBestRun } from "@snk/compute";
|
import { computeBestRun } from "@snk/compute";
|
||||||
|
|
||||||
const drawOptions = {
|
const drawOptions = {
|
||||||
@@ -16,7 +16,7 @@ const gameOptions = { maxSnakeLength: 5 };
|
|||||||
|
|
||||||
const gifOptions = { delay: 20 };
|
const gifOptions = { delay: 20 };
|
||||||
|
|
||||||
const grid = generateGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
const grid = generateRandomGrid(42, 7, { colors: [1, 2, 3, 4], emptyP: 3 });
|
||||||
|
|
||||||
const snake = [
|
const snake = [
|
||||||
{ x: 4, y: -1 },
|
{ x: 4, y: -1 },
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { copySnake } from "@snk/compute/snake";
|
|||||||
import { drawWorld } from "@snk/draw/drawWorld";
|
import { drawWorld } from "@snk/draw/drawWorld";
|
||||||
import { step } from "@snk/compute/step";
|
import { step } from "@snk/compute/step";
|
||||||
import * as tmp from "tmp";
|
import * as tmp from "tmp";
|
||||||
// @ts-ignore
|
|
||||||
import * as execa from "execa";
|
import * as execa from "execa";
|
||||||
|
|
||||||
export const createGif = async (
|
export const createGif = async (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getGithubUserContribution } from "..";
|
|||||||
it("should get user contribution", async () => {
|
it("should get user contribution", async () => {
|
||||||
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
const { cells, colorScheme } = await getGithubUserContribution("platane");
|
||||||
|
|
||||||
expect(cells).toBeDefined();
|
expect(cells.length).toBeGreaterThan(300);
|
||||||
expect(colorScheme).toEqual([
|
expect(colorScheme).toEqual([
|
||||||
"#ebedf0",
|
"#ebedf0",
|
||||||
"#9be9a8",
|
"#9be9a8",
|
||||||
|
|||||||
@@ -1,26 +1,11 @@
|
|||||||
// import * as https from "https";
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
// import * as cheerio from "cheerio";
|
|
||||||
|
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the contribution grid from a github user page
|
||||||
|
*
|
||||||
|
* @param userName
|
||||||
|
*/
|
||||||
export const getGithubUserContribution = async (userName: string) => {
|
export const getGithubUserContribution = async (userName: string) => {
|
||||||
// const content: string = await new Promise((resolve, reject) => {
|
|
||||||
// const req = https.request(`https://github.com/${userName}`, (res) => {
|
|
||||||
// let data = "";
|
|
||||||
|
|
||||||
// res.on("error", reject);
|
|
||||||
// res.on("data", (chunk) => (data += chunk));
|
|
||||||
// res.on("end", () => resolve(data));
|
|
||||||
// });
|
|
||||||
|
|
||||||
// req.on("error", reject);
|
|
||||||
// req.end();
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const dom = new JSDOM(content);
|
|
||||||
|
|
||||||
const dom = await JSDOM.fromURL(`https://github.com/${userName}`);
|
const dom = await JSDOM.fromURL(`https://github.com/${userName}`);
|
||||||
|
|
||||||
const colorScheme = Array.from(
|
const colorScheme = Array.from(
|
||||||
@@ -34,14 +19,14 @@ export const getGithubUserContribution = async (userName: string) => {
|
|||||||
dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g")
|
dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g")
|
||||||
)
|
)
|
||||||
.map((column, x) =>
|
.map((column, x) =>
|
||||||
Array.from(column.querySelectorAll("rect")).map((element, y) => ({
|
Array.from(column.querySelectorAll("rect")).map((element, y) => {
|
||||||
x,
|
const count = +element.getAttribute("data-count")!;
|
||||||
y,
|
const date = element.getAttribute("data-date")!;
|
||||||
count: element.getAttribute("data-count"),
|
const color = element.getAttribute("fill")!;
|
||||||
date: element.getAttribute("data-date"),
|
const k = colorScheme.indexOf(color);
|
||||||
color: element.getAttribute("fill"),
|
|
||||||
k: colorScheme.indexOf(element.getAttribute("fill")!),
|
return { x, y, count, date, color, k };
|
||||||
}))
|
})
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
@@ -53,9 +38,3 @@ type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
|
|||||||
export type Cell = ThenArg<
|
export type Cell = ThenArg<
|
||||||
ReturnType<typeof getGithubUserContribution>
|
ReturnType<typeof getGithubUserContribution>
|
||||||
>["cells"][number];
|
>["cells"][number];
|
||||||
|
|
||||||
// "#ebedf0";
|
|
||||||
// "#9be9a8";
|
|
||||||
// "#40c463";
|
|
||||||
// "#30a14e";
|
|
||||||
// "#216e39";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user