diff --git a/README.md b/README.md index f5b6318..77e7d96 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # snk +![type definitions](https://img.shields.io/npm/types/typescript?style=flat-square) +![code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square) + ![](https://raw.githubusercontent.com/Platane/snk/output/github-contribution-grid-snake.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) diff --git a/packages/compute/generateGrid.ts b/packages/compute/generateGrid.ts index 380566a..6dfc8f9 100644 --- a/packages/compute/generateGrid.ts +++ b/packages/compute/generateGrid.ts @@ -3,9 +3,9 @@ import { Grid, Color } from "./grid"; const rand = (a: number, b: number) => Math.floor(Math.random() * (b - a)) + a; 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, height: number, options: { colors: Color[]; emptyP: number } = { diff --git a/packages/compute/snake.ts b/packages/compute/snake.ts index 8dfaaf4..2b7c399 100644 --- a/packages/compute/snake.ts +++ b/packages/compute/snake.ts @@ -1,7 +1,9 @@ import { Point } from "./point"; +export type Snake = Point[]; + export const snakeSelfCollideNext = ( - snake: Point[], + snake: Snake, direction: Point, options: { maxSnakeLength: number } ) => { @@ -14,11 +16,11 @@ export const snakeSelfCollideNext = ( return false; }; -export const snakeSelfCollide = (snake: Point[]) => { +export const snakeSelfCollide = (snake: Snake) => { for (let i = 1; i < snake.length; i++) if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true; return false; }; -export const copySnake = (x: Point[]) => x.map((p) => ({ ...p })); +export const copySnake = (x: Snake) => x.map((p) => ({ ...p })); diff --git a/packages/demo/index.ts b/packages/demo/index.ts index aba075c..55fcfdb 100644 --- a/packages/demo/index.ts +++ b/packages/demo/index.ts @@ -1,81 +1,89 @@ -// import { generateGrid } from "@snk/compute/generateGrid"; - -import { generateGrid } from "@snk/compute/generateGrid"; +import { generateRandomGrid } from "@snk/compute/generateGrid"; import { Color, copyGrid } from "@snk/compute/grid"; import { computeBestRun } from "@snk/compute"; import { step } from "@snk/compute/step"; 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 })); - -export const run = async () => { - const drawOptions = { - sizeBorderRadius: 2, - sizeCell: 16, - sizeDot: 12, - colorBorder: "#1b1f230a", - 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)); +const drawOptions = { + sizeBorderRadius: 2, + sizeCell: 16, + sizeDot: 12, + colorBorder: "#1b1f230a", + colorDots: { 1: "#9be9a8", 2: "#40c463", 3: "#30a14e", 4: "#216e39" }, + colorEmpty: "#ebedf0", + colorSnake: "purple", }; -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); diff --git a/packages/demo/package.json b/packages/demo/package.json index 5ceed03..6b5bf5f 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -2,7 +2,8 @@ "name": "@snk/demo", "version": "1.0.0", "dependencies": { - "@snk/compute": "1.0.0" + "@snk/compute": "1.0.0", + "@snk/draw": "1.0.0" }, "devDependencies": { "webpack": "4.43.0", diff --git a/packages/demo/webpack.config.ts b/packages/demo/webpack.config.ts index 8c5a97b..0f7d2ca 100644 --- a/packages/demo/webpack.config.ts +++ b/packages/demo/webpack.config.ts @@ -27,7 +27,6 @@ const config: Configuration = { ], }, plugins: [ - // game new HtmlWebpackPlugin({ title: "demo", filename: "index.html", @@ -39,9 +38,6 @@ const config: Configuration = { devtool: false, stats: "errors-only", - - // @ts-ignore - devServer: {}, }; export default config; diff --git a/packages/gif-creator/__tests__/createGif.spec.ts b/packages/gif-creator/__tests__/createGif.spec.ts index 0677884..67a4fb7 100644 --- a/packages/gif-creator/__tests__/createGif.spec.ts +++ b/packages/gif-creator/__tests__/createGif.spec.ts @@ -1,5 +1,5 @@ import { createGif } from ".."; -import { generateGrid } from "@snk/compute/generateGrid"; +import { generateRandomGrid } from "@snk/compute/generateGrid"; import { computeBestRun } from "@snk/compute"; const drawOptions = { @@ -17,7 +17,7 @@ const gameOptions = { maxSnakeLength: 5 }; const gifOptions = { delay: 200 }; 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 = [ { x: 4, y: -1 }, diff --git a/packages/gif-creator/__tests__/dev.ts b/packages/gif-creator/__tests__/dev.ts index ed1dc71..7817671 100644 --- a/packages/gif-creator/__tests__/dev.ts +++ b/packages/gif-creator/__tests__/dev.ts @@ -1,5 +1,5 @@ import { createGif } from ".."; -import { generateGrid } from "@snk/compute/generateGrid"; +import { generateRandomGrid } from "@snk/compute/generateGrid"; import { computeBestRun } from "@snk/compute"; const drawOptions = { @@ -16,7 +16,7 @@ const gameOptions = { maxSnakeLength: 5 }; 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 = [ { x: 4, y: -1 }, diff --git a/packages/gif-creator/index.ts b/packages/gif-creator/index.ts index 9fb4a9d..864dd9c 100644 --- a/packages/gif-creator/index.ts +++ b/packages/gif-creator/index.ts @@ -7,7 +7,6 @@ import { copySnake } from "@snk/compute/snake"; import { drawWorld } from "@snk/draw/drawWorld"; import { step } from "@snk/compute/step"; import * as tmp from "tmp"; -// @ts-ignore import * as execa from "execa"; export const createGif = async ( diff --git a/packages/github-user-contribution/__tests__/getGithubUserContribution.spec.ts b/packages/github-user-contribution/__tests__/getGithubUserContribution.spec.ts index d8eb2ae..3860701 100644 --- a/packages/github-user-contribution/__tests__/getGithubUserContribution.spec.ts +++ b/packages/github-user-contribution/__tests__/getGithubUserContribution.spec.ts @@ -3,7 +3,7 @@ import { getGithubUserContribution } from ".."; it("should get user contribution", async () => { const { cells, colorScheme } = await getGithubUserContribution("platane"); - expect(cells).toBeDefined(); + expect(cells.length).toBeGreaterThan(300); expect(colorScheme).toEqual([ "#ebedf0", "#9be9a8", diff --git a/packages/github-user-contribution/index.ts b/packages/github-user-contribution/index.ts index 5341ac5..1b93d91 100644 --- a/packages/github-user-contribution/index.ts +++ b/packages/github-user-contribution/index.ts @@ -1,26 +1,11 @@ -// import * as https from "https"; - -// @ts-ignore -// import * as cheerio from "cheerio"; - import { JSDOM } from "jsdom"; +/** + * get the contribution grid from a github user page + * + * @param userName + */ 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 colorScheme = Array.from( @@ -34,14 +19,14 @@ export const getGithubUserContribution = async (userName: string) => { dom.window.document.querySelectorAll(".js-calendar-graph-svg > g > g") ) .map((column, x) => - Array.from(column.querySelectorAll("rect")).map((element, y) => ({ - x, - y, - count: element.getAttribute("data-count"), - date: element.getAttribute("data-date"), - color: element.getAttribute("fill"), - k: colorScheme.indexOf(element.getAttribute("fill")!), - })) + Array.from(column.querySelectorAll("rect")).map((element, y) => { + const count = +element.getAttribute("data-count")!; + const date = element.getAttribute("data-date")!; + const color = element.getAttribute("fill")!; + const k = colorScheme.indexOf(color); + + return { x, y, count, date, color, k }; + }) ) .flat(); @@ -53,9 +38,3 @@ type ThenArg = T extends PromiseLike ? U : T; export type Cell = ThenArg< ReturnType >["cells"][number]; - -// "#ebedf0"; -// "#9be9a8"; -// "#40c463"; -// "#30a14e"; -// "#216e39";