Compare commits
No commits in common. "gh-pages" and "master" have entirely different histories.
26 changed files with 14106 additions and 2 deletions
3
.babelrc
Normal file
3
.babelrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env"]
|
||||||
|
}
|
34
.github/workflows/main.yml
vendored
Normal file
34
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# This is a basic workflow to help you get started with Actions
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||||
|
# events but only for the master branch
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# This workflow contains a single job called "build"
|
||||||
|
build:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# Runs a single command using the runners shell
|
||||||
|
- name: Install Deps
|
||||||
|
run: npm install
|
||||||
|
- name: Test
|
||||||
|
run: npm run test
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
- name: Deploy
|
||||||
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
publish_dir: ./dist
|
0
.nojekyll → .gitignore
vendored
0
.nojekyll → .gitignore
vendored
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# Game of Life
|
||||||
|
*It's not really about the Game of Life*
|
||||||
|
|
||||||
|
A weekend project with the following goals:
|
||||||
|
- [x] implement webpack build with babel
|
||||||
|
- [x] develop a simple Conway's Game of Life app without framework reliance
|
||||||
|
- [x] seed Game of Life with GitHub contribution calendars just for fun
|
||||||
|
- [x] implement a GitHub Actions workflow
|
||||||
|
|
||||||
|
![Screenshot of Game of Life being seeded with creator's contribution calendar](./SeedScreenshot.png)
|
||||||
|
![Screenshot of same Game of Life running a few generations later](./RunningScreenshot.png)
|
BIN
RunningScreenshot.png
Normal file
BIN
RunningScreenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
BIN
SeedScreenshot.png
Normal file
BIN
SeedScreenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
File diff suppressed because one or more lines are too long
21
index.html
21
index.html
|
@ -1 +1,20 @@
|
||||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Game of Life</title></head><body><aside id="controls"><h1>Game of Life</h1></aside><main><p>Enter valid GitHub user handle above to seed with contribution calendar</p><p>Touch below to toggle seed cells alive or dead</p><canvas id="game-field" width="500" height="300"></canvas></main><script src="app.bundle.js"></script></body></html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Game of Life</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside id="controls">
|
||||||
|
<h1>Game of Life</h1>
|
||||||
|
<!-- insert form for fetch seed data -->
|
||||||
|
<!-- insert controls for animation -->
|
||||||
|
</aside>
|
||||||
|
<main>
|
||||||
|
<p>Enter valid GitHub user handle above to seed with contribution calendar</p>
|
||||||
|
<p>Touch below to toggle seed cells alive or dead</p>
|
||||||
|
<canvas id="game-field" width="500" height="300"></canvas>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
12659
package-lock.json
generated
Normal file
12659
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
41
package.json
Normal file
41
package.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "game-of-life",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homepage": "https://sorrelbri.github.io/game-of-life",
|
||||||
|
"description": "",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode=production --config webpack.prod.js",
|
||||||
|
"test": "jest",
|
||||||
|
"start": "webpack-dev-server --open --config webpack.dev.js",
|
||||||
|
"cleanup": "rm -rf node_modules/gh-pages/.cache",
|
||||||
|
"deploy": "npm run cleanup && gh-pages -d dist"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.9.6",
|
||||||
|
"@babel/preset-env": "^7.9.6",
|
||||||
|
"babel-jest": "^26.0.1",
|
||||||
|
"babel-loader": "^8.1.0",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"css-loader": "^3.5.3",
|
||||||
|
"gh-pages": "^2.2.0",
|
||||||
|
"html-loader": "^1.1.0",
|
||||||
|
"html-webpack-plugin": "^4.3.0",
|
||||||
|
"jest": "^26.0.1",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"webpack": "^4.43.0",
|
||||||
|
"webpack-cli": "^3.3.11",
|
||||||
|
"webpack-dev-server": "^3.11.0",
|
||||||
|
"webpack-merge": "^4.2.2"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"transform": {
|
||||||
|
".*": "<rootDir>/node_modules/babel-jest"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
66
src/components/Cell.js
Normal file
66
src/components/Cell.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
const { Stream } = require("../utils");
|
||||||
|
|
||||||
|
class Cell {
|
||||||
|
constructor(living = false, liveNeighbors = 0) {
|
||||||
|
this.living = living;
|
||||||
|
this.liveNeighbors = liveNeighbors;
|
||||||
|
}
|
||||||
|
toggleLiving() {
|
||||||
|
this.living = !this.living;
|
||||||
|
}
|
||||||
|
addLiveNeighbor() {
|
||||||
|
this.liveNeighbors++;
|
||||||
|
}
|
||||||
|
setLiving() {
|
||||||
|
if (this.living && this.liveNeighbors !== 2 && this.liveNeighbors !== 3) {
|
||||||
|
this.liveNeighbors = 0;
|
||||||
|
return (this.living = false);
|
||||||
|
}
|
||||||
|
if (this.liveNeighbors === 3) {
|
||||||
|
this.liveNeighbors = 0;
|
||||||
|
return (this.living = true);
|
||||||
|
}
|
||||||
|
this.liveNeighbors = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CellStream extends Stream {
|
||||||
|
constructor(head, next) {
|
||||||
|
super(head, next);
|
||||||
|
}
|
||||||
|
get living() {
|
||||||
|
return this.head.living;
|
||||||
|
}
|
||||||
|
get liveNeighbors() {
|
||||||
|
return this.head.liveNeighbors;
|
||||||
|
}
|
||||||
|
set liveNeighbors(liveNeighbors) {
|
||||||
|
this.head.liveNeighbors = liveNeighbors;
|
||||||
|
}
|
||||||
|
addLiveNeighbor() {
|
||||||
|
this.head.addLiveNeighbor();
|
||||||
|
}
|
||||||
|
setLiving() {
|
||||||
|
this.head.setLiving();
|
||||||
|
}
|
||||||
|
toggleLiving() {
|
||||||
|
this.head.toggleLiving();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellStream = (living = false, liveNeighbors = 0) => {
|
||||||
|
return new CellStream(new Cell(living, liveNeighbors), function () {
|
||||||
|
this.head.setLiving();
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// as a stream -> cellStream = Stream(Cell, () => Cell(cellStream.isLiving()))
|
||||||
|
// in this case GameField = { [x-y]: cellStream }
|
||||||
|
// communicating with neighbors = filter for (Boolean(Cell.living)) -> Cell neighbors.addLivingNeighbor
|
||||||
|
// controlling whether to call or not: filter for (Boolean(Cell.living) || Boolean(cell.liveNeighbors)) -> cellStream.next
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Cell,
|
||||||
|
cellStream,
|
||||||
|
};
|
104
src/components/Controls.js
Normal file
104
src/components/Controls.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
const html = require("./controls.html");
|
||||||
|
const css = require("../styles/controls.css");
|
||||||
|
const { getCalendar } = require("../utils/index");
|
||||||
|
|
||||||
|
const init = (gameField) => {
|
||||||
|
const controlsEl = document.getElementById("controls");
|
||||||
|
controlsEl.innerHTML += html;
|
||||||
|
const rateEl = document.getElementById("rate");
|
||||||
|
const forwardEl = document.getElementById("forward");
|
||||||
|
const playEl = document.getElementById("play");
|
||||||
|
const resetEl = document.getElementById("reset");
|
||||||
|
const clearEl = document.getElementById("clear");
|
||||||
|
const canvasEl = document.getElementById("game-field");
|
||||||
|
const calendarFormEl = document.getElementById("calendar-form");
|
||||||
|
const controls = {
|
||||||
|
interval: null,
|
||||||
|
play() {
|
||||||
|
const gameLoop = () => gameField.advance();
|
||||||
|
this.interval = setInterval(gameLoop, 1000 / this.rate);
|
||||||
|
},
|
||||||
|
pause() {
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.interval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
gameField = gameField.clear();
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.pause();
|
||||||
|
gameField = gameField.reset();
|
||||||
|
},
|
||||||
|
seed(weeks) {
|
||||||
|
if (weeks.length) {
|
||||||
|
gameField = gameField.seed(weeks);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forward() {
|
||||||
|
gameField.advance();
|
||||||
|
},
|
||||||
|
rate: 10,
|
||||||
|
updateField(x, y) {
|
||||||
|
gameField = gameField.toggleCell(x, y);
|
||||||
|
},
|
||||||
|
updateRate(rate) {
|
||||||
|
controls.rate = rate;
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rateEl.addEventListener("change", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
controls.updateRate(e.target.value);
|
||||||
|
});
|
||||||
|
forwardEl.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
controls.forward();
|
||||||
|
});
|
||||||
|
playEl.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (controls.interval) {
|
||||||
|
controls.pause();
|
||||||
|
return (forwardEl.disabled = false);
|
||||||
|
}
|
||||||
|
controls.play();
|
||||||
|
forwardEl.disabled = true;
|
||||||
|
});
|
||||||
|
resetEl.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
controls.reset();
|
||||||
|
});
|
||||||
|
clearEl.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
controls.clear();
|
||||||
|
});
|
||||||
|
canvasEl.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (controls.interval) return;
|
||||||
|
const { offsetX, offsetY } = e;
|
||||||
|
controls.updateField(offsetX, offsetY);
|
||||||
|
});
|
||||||
|
calendarFormEl.addEventListener("submit", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = e.target[0].value;
|
||||||
|
getCalendar(user)
|
||||||
|
.then((data) => {
|
||||||
|
return data.map((week) => {
|
||||||
|
return week.contributionDays.map((day) => day.contributionCount);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((weeks) => controls.seed(weeks))
|
||||||
|
.catch((e) => {
|
||||||
|
calendarFormEl.elements[0].value = "enter valid handle";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return controls;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
};
|
85
src/components/GameField.js
Normal file
85
src/components/GameField.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
const { cellStream } = require("./Cell");
|
||||||
|
const { Stream, getNeighbors } = require("../utils");
|
||||||
|
|
||||||
|
class GameField {
|
||||||
|
constructor({ fieldArray = [], fieldMap = {} }) {
|
||||||
|
// seed = [ [] ]
|
||||||
|
this.map = {};
|
||||||
|
fieldArray.forEach((subArray, majorIndex) =>
|
||||||
|
subArray.forEach((value, minorIndex) => {
|
||||||
|
if (value > 0) {
|
||||||
|
this.map[`${majorIndex},${minorIndex}`] = cellStream(true, 0);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Object.entries(fieldMap).forEach(
|
||||||
|
([key, [live, neighbors]]) =>
|
||||||
|
(this.map[key] = cellStream(live, neighbors))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FieldStream extends Stream {
|
||||||
|
constructor(head, next) {
|
||||||
|
super(head, next);
|
||||||
|
}
|
||||||
|
get map() {
|
||||||
|
return this.head.map;
|
||||||
|
}
|
||||||
|
addLiveNeighbor(key) {
|
||||||
|
if (this.map[key] === undefined) {
|
||||||
|
this.map[key] = cellStream(false);
|
||||||
|
}
|
||||||
|
this.map[key].addLiveNeighbor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seedMap = (map, [key, seed]) => {
|
||||||
|
map[key] = seed;
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
const isLiving = ([key, cell]) => cell.living === true;
|
||||||
|
const incrementLiveNeighbors = (field) => ([key]) =>
|
||||||
|
getNeighbors(key).forEach((neighbor) => field.addLiveNeighbor(neighbor));
|
||||||
|
const makeSeed = ([key, cell]) => [key, [cell.living, cell.liveNeighbors]];
|
||||||
|
const makeSeedNextGen = ([key, cell]) => {
|
||||||
|
cell.setLiving();
|
||||||
|
return [key, [cell.living, 0]];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldStream = ({ fieldArray, fieldMap }) => {
|
||||||
|
return new FieldStream(new GameField({ fieldArray, fieldMap }), function () {
|
||||||
|
// calculate liveNeighbors for all cells on first next call
|
||||||
|
Object.entries(this.map)
|
||||||
|
.filter(isLiving)
|
||||||
|
.forEach(incrementLiveNeighbors(this));
|
||||||
|
// generate seed for next Stream with liveNeighbors
|
||||||
|
const mapWithLiveNeighbors = Object.entries(this.map)
|
||||||
|
.map(makeSeed)
|
||||||
|
.reduce(seedMap, {});
|
||||||
|
// return next stream
|
||||||
|
return new FieldStream(
|
||||||
|
new GameField({ fieldMap: mapWithLiveNeighbors }),
|
||||||
|
function () {
|
||||||
|
// determine living cells for next generation
|
||||||
|
const nextGeneration = Object.entries(this.map)
|
||||||
|
.map(makeSeedNextGen)
|
||||||
|
.reduce(seedMap, {});
|
||||||
|
// seed next Stream
|
||||||
|
return fieldStream({ fieldMap: nextGeneration });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// wrapper for fieldStream
|
||||||
|
// -- .reset => instantiates new fieldStream
|
||||||
|
// -- .toggle(cell) => manually toggles cell state
|
||||||
|
|
||||||
|
// instantiate table (orientation of major and minor axis dependent on viewport)
|
||||||
|
// const gameFields = new Array(1).fill(new GameField({}));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
GameField,
|
||||||
|
fieldStream,
|
||||||
|
};
|
71
src/components/GameFieldTable.js
Normal file
71
src/components/GameFieldTable.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const css = require("../styles/gameField.css");
|
||||||
|
const { fieldStream } = require("./GameField");
|
||||||
|
|
||||||
|
const canvasEl = document.getElementById("game-field");
|
||||||
|
const canvas2D = canvasEl.getContext("2d");
|
||||||
|
canvas2D.fillStyle = "white";
|
||||||
|
|
||||||
|
const parseSeed = (seed) => {
|
||||||
|
if (seed && Array.isArray(seed)) {
|
||||||
|
return {
|
||||||
|
fieldArray: seed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return seed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fieldView = (seed) => {
|
||||||
|
seed = parseSeed(seed);
|
||||||
|
const field = fieldStream(seed);
|
||||||
|
const view = {
|
||||||
|
draw(x, y) {
|
||||||
|
const { scale, offset } = this.dimension;
|
||||||
|
canvas2D.fillRect(x * scale + offset, y * scale + offset, scale, scale);
|
||||||
|
},
|
||||||
|
dimension: { x0: 0, y0: 0, x1: 500, y1: 300, scale: 6, offset: 100 },
|
||||||
|
field,
|
||||||
|
updateView() {
|
||||||
|
canvas2D.clearRect(0, 0, this.dimension.x1, this.dimension.y1);
|
||||||
|
Object.entries(this.field.map)
|
||||||
|
.filter(([key, cell]) => cell.living)
|
||||||
|
.map(([key]) => key.split(","))
|
||||||
|
.forEach(([x, y]) => this.draw(x, y));
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
return fieldView({});
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
const newField = fieldView(seed);
|
||||||
|
newField.updateView();
|
||||||
|
return newField;
|
||||||
|
},
|
||||||
|
seed(seed) {
|
||||||
|
return fieldView(seed);
|
||||||
|
},
|
||||||
|
advance() {
|
||||||
|
this.field = this.field.next.next;
|
||||||
|
this.updateView();
|
||||||
|
},
|
||||||
|
toggleCell(x, y) {
|
||||||
|
const { scale, offset } = this.dimension;
|
||||||
|
x = Math.floor((x - offset) / scale);
|
||||||
|
y = Math.floor((y - offset) / scale);
|
||||||
|
const fieldMap = Object.entries(this.field.map)
|
||||||
|
.map(([key, cell]) => [key, cell.living])
|
||||||
|
.reduce((map, [key, living]) => {
|
||||||
|
map[key] = [living];
|
||||||
|
return map;
|
||||||
|
}, {});
|
||||||
|
fieldMap[`${x},${y}`] = !!fieldMap[`${x},${y}`]
|
||||||
|
? [!fieldMap[`${x},${y}`][0]]
|
||||||
|
: [true];
|
||||||
|
return new fieldView({ fieldMap });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
view.updateView();
|
||||||
|
return view;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fieldView,
|
||||||
|
};
|
17
src/components/controls.html
Normal file
17
src/components/controls.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="playControls">
|
||||||
|
<button id="clear">⏹️</button>
|
||||||
|
<button id="reset">🔄️</button>
|
||||||
|
<button id="play">⏯️</button>
|
||||||
|
<button id="forward">⏩️</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id="rate"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
value="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<form action="/" id="calendar-form">
|
||||||
|
<input type="text" value="GitHub handle"/>
|
||||||
|
<input type="submit" value="seed" />
|
||||||
|
</form>
|
7
src/index.js
Normal file
7
src/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import reset from "./styles/reset.css";
|
||||||
|
import css from "./styles/style.css";
|
||||||
|
const { fieldView } = require("./components/GameFieldTable");
|
||||||
|
const { init } = require("./components/Controls");
|
||||||
|
(() => console.log("hello world!"))();
|
||||||
|
window.game = fieldView([]);
|
||||||
|
window.controls = init(game);
|
51
src/styles/controls.css
Normal file
51
src/styles/controls.css
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
aside {
|
||||||
|
background: #944;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
position: float;
|
||||||
|
top: 0;
|
||||||
|
border-bottom: solid 1em #522;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside * {
|
||||||
|
background: inherit;
|
||||||
|
color: #dff;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 130%;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div.playControls {
|
||||||
|
min-width: 30vw;
|
||||||
|
display: grid;
|
||||||
|
border-radius: 1vh;
|
||||||
|
border: solid .5vh;
|
||||||
|
grid-template-rows: 2fr 1fr;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
gap: 1vh;
|
||||||
|
grid-template-areas: "buttons buttons buttons buttons" "slider slider slider slider";
|
||||||
|
padding: 1vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: .5vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
grid-area: slider;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin: 0.5em;
|
||||||
|
padding: 0.5em
|
||||||
|
}
|
7
src/styles/gameField.css
Normal file
7
src/styles/gameField.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
canvas {
|
||||||
|
background: #222;
|
||||||
|
color: white;
|
||||||
|
border: 0.5vmin solid #aaa;
|
||||||
|
fill: white;
|
||||||
|
margin: 0 3em 3em 3em;
|
||||||
|
}
|
361
src/styles/reset.css
Normal file
361
src/styles/reset.css
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
/* http://meyerweb.com/eric/tools/css/reset/
|
||||||
|
v2.0-modified | 20110126
|
||||||
|
License: none (public domain)
|
||||||
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=search]::-webkit-search-cancel-button,
|
||||||
|
input[type=search]::-webkit-search-decoration,
|
||||||
|
input[type=search]::-webkit-search-results-button,
|
||||||
|
input[type=search]::-webkit-search-results-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=search] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-box-sizing: content-box;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
vertical-align: top;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
video {
|
||||||
|
display: inline-block;
|
||||||
|
*display: inline;
|
||||||
|
*zoom: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 7/8/9, Firefox 3, and Safari 4.
|
||||||
|
* Known issue: no IE 6 support.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using
|
||||||
|
* `em` units.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `outline` inconsistency between Chrome and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: thin dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3.
|
||||||
|
* 2. Improve image quality when scaled in IE 7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
-ms-interpolation-mode: bicubic; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct margin displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited in IE 6/7/8/9.
|
||||||
|
* 2. Correct text not wrapping in Firefox 3.
|
||||||
|
* 3. Correct alignment displayed oddly in IE 6/7.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0;
|
||||||
|
white-space: normal; /* 2 */
|
||||||
|
*margin-left: -7px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct font size not being inherited in all browsers.
|
||||||
|
* 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5,
|
||||||
|
* and Chrome.
|
||||||
|
* 3. Improve appearance and consistency in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-size: 100%; /* 1 */
|
||||||
|
margin: 0; /* 2 */
|
||||||
|
vertical-align: baseline; /* 3 */
|
||||||
|
*vertical-align: middle; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 3+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+.
|
||||||
|
* Correct `select` style inheritance in Firefox 4+ and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
* 4. Remove inner spacing in IE 7 without affecting normal text inputs.
|
||||||
|
* Known issue: inner spacing remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
*overflow: visible; /* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address box sizing set to content-box in IE 8/9.
|
||||||
|
* 2. Remove excess padding in IE 8/9.
|
||||||
|
* 3. Remove excess padding in IE 7.
|
||||||
|
* Known issue: excess padding remains in IE 6.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
*height: 13px; /* 3 */
|
||||||
|
*width: 13px; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari 5 and Chrome
|
||||||
|
* on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 3+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Remove default vertical scrollbar in IE 6/7/8/9.
|
||||||
|
* 2. Improve readability and alignment in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto; /* 1 */
|
||||||
|
vertical-align: top; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: #b3d4fc;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chromeframe {
|
||||||
|
margin: 0.2em 0;
|
||||||
|
background: #ccc;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.2em 0;
|
||||||
|
}
|
27
src/styles/style.css
Normal file
27
src/styles/style.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
* {
|
||||||
|
background: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main, aside {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
main p {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
68
src/test/Cell.test.js
Normal file
68
src/test/Cell.test.js
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { Cell, cellStream } from "../components/Cell";
|
||||||
|
|
||||||
|
describe("Cell functionality", () => {
|
||||||
|
test("dispatch toggleLiving state should mark living cell dead", () => {
|
||||||
|
const cell = new Cell(true);
|
||||||
|
cell.toggleLiving();
|
||||||
|
expect(cell.living).toEqual(false);
|
||||||
|
});
|
||||||
|
test("dispatch toggleLiving state should mark dead cell living", () => {
|
||||||
|
const cell = new Cell();
|
||||||
|
cell.toggleLiving();
|
||||||
|
expect(cell.living).toEqual(true);
|
||||||
|
});
|
||||||
|
test("dispatch add Live Neighbor should increment live neighbors property", () => {
|
||||||
|
const cell = new Cell();
|
||||||
|
cell.addLiveNeighbor();
|
||||||
|
expect(cell.liveNeighbors).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const livingFromLivingStates = new Array(8)
|
||||||
|
.fill()
|
||||||
|
.map((_, i) => (i === 2 || i === 3 ? true : false));
|
||||||
|
|
||||||
|
livingFromLivingStates.forEach((state, liveNeighbors) => {
|
||||||
|
test(`dispatch setLiving on live cell with ${liveNeighbors} neighbors should result in living = ${state}`, () => {
|
||||||
|
const cell = new Cell(true);
|
||||||
|
new Array(liveNeighbors).fill().forEach((_) => cell.addLiveNeighbor());
|
||||||
|
cell.setLiving();
|
||||||
|
expect(cell.living).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const livingFromDeadStates = new Array(8)
|
||||||
|
.fill()
|
||||||
|
.map((_, i) => (i === 3 ? true : false));
|
||||||
|
|
||||||
|
livingFromDeadStates.forEach((state, liveNeighbors) => {
|
||||||
|
test(`dispatch setLiving on dead cell with ${liveNeighbors} neighbors should result in living = ${state}`, () => {
|
||||||
|
const cell = new Cell();
|
||||||
|
new Array(liveNeighbors).fill().forEach((_) => cell.addLiveNeighbor());
|
||||||
|
cell.setLiving();
|
||||||
|
expect(cell.living).toEqual(state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const cellStreamNextCalls = [
|
||||||
|
[2, false],
|
||||||
|
[3, true],
|
||||||
|
[0, false],
|
||||||
|
[5, false],
|
||||||
|
];
|
||||||
|
|
||||||
|
cellStreamNextCalls.forEach(([liveNeighbors, livingResult]) => {
|
||||||
|
test(`cellStream advances cell state for ${liveNeighbors} live Neighbors (from dead cell)`, () => {
|
||||||
|
const cell = cellStream(false, liveNeighbors).next;
|
||||||
|
expect(cell.living).toEqual(livingResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[[2, true], ...cellStreamNextCalls.slice(1)].forEach(
|
||||||
|
([liveNeighbors, livingResult]) => {
|
||||||
|
test(`cellStream advances cell state for ${liveNeighbors} live Neighbors (from living cell)`, () => {
|
||||||
|
const cell = cellStream(true, liveNeighbors).next;
|
||||||
|
expect(cell.living).toEqual(livingResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
368
src/test/GameField.test.js
Normal file
368
src/test/GameField.test.js
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
import { GameField, fieldStream } from "../components/GameField";
|
||||||
|
|
||||||
|
describe("Game Field seeds living Cells with array", () => {
|
||||||
|
const fieldArray = [
|
||||||
|
[0, 1, 0],
|
||||||
|
[1, 0, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const fieldMap = {
|
||||||
|
"0,0": [true, 0],
|
||||||
|
"0,2": [true, 0],
|
||||||
|
"1,1": [true, 0],
|
||||||
|
};
|
||||||
|
const gameArraySeed = new GameField({ fieldArray });
|
||||||
|
const gameMapSeed = new GameField({ fieldMap });
|
||||||
|
const streamArraySeed = fieldStream({ fieldArray });
|
||||||
|
const streamMapSeed = fieldStream({ fieldMap });
|
||||||
|
|
||||||
|
["0,1", "1,0", "1,2"].forEach((key) => {
|
||||||
|
test(`Array seed: ${key} should equal living Cell`, () => {
|
||||||
|
expect(gameArraySeed.map[key].living).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Map seed: ${key} should equal undefined`, () => {
|
||||||
|
expect(gameMapSeed.map[key]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Stream array seed: ${key} should equal living Cell`, () => {
|
||||||
|
expect(streamArraySeed.map[key].living).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Stream map seed: ${key} should equal undefined`, () => {
|
||||||
|
expect(streamMapSeed.map[key]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
["0,0", "0,2", "1,1"].forEach((key) => {
|
||||||
|
test(`Array seed: ${key} should equal undefined`, () => {
|
||||||
|
expect(gameArraySeed.map[key]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Map seed: ${key} should equal living Cell`, () => {
|
||||||
|
expect(gameMapSeed.map[key].living).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Stream array seed: ${key} should equal undefined`, () => {
|
||||||
|
expect(streamArraySeed.map[key]).toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`Stream map seed: ${key} should equal living Cell`, () => {
|
||||||
|
expect(streamMapSeed.map[key].living).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fieldStream.next calculates liveNeighbors", () => {
|
||||||
|
const fieldArray = [
|
||||||
|
[0, 1, 0],
|
||||||
|
[1, 0, 1],
|
||||||
|
];
|
||||||
|
const testStream = fieldStream({ fieldArray });
|
||||||
|
[
|
||||||
|
["-1,0", 1],
|
||||||
|
["-1,1", 1],
|
||||||
|
["-1,2", 1],
|
||||||
|
["0,-1", 1],
|
||||||
|
["0,0", 2],
|
||||||
|
["0,1", 2],
|
||||||
|
["0,2", 2],
|
||||||
|
["0,3", 1],
|
||||||
|
["1,-1", 1],
|
||||||
|
["1,0", 1],
|
||||||
|
["1,1", 3],
|
||||||
|
["1,2", 1],
|
||||||
|
["1,3", 1],
|
||||||
|
["2,-1", 1],
|
||||||
|
["2,0", 1],
|
||||||
|
["2,1", 2],
|
||||||
|
["2,2", 1],
|
||||||
|
["2,3", 1],
|
||||||
|
].forEach(([key, liveNeighbors]) => {
|
||||||
|
test(`after .next ${key} in 1st should have ${liveNeighbors} liveNeighbors`, () => {
|
||||||
|
expect(testStream.next.map[key].liveNeighbors).toEqual(liveNeighbors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldMap = {
|
||||||
|
"0,1": [true, 0],
|
||||||
|
"0,2": [true, 0],
|
||||||
|
"1,0": [true, 0],
|
||||||
|
"1,1": [true, 0],
|
||||||
|
"1,3": [true, 0],
|
||||||
|
"2,1": [true, 0],
|
||||||
|
"2,2": [true, 0],
|
||||||
|
};
|
||||||
|
const testStream2 = fieldStream({ fieldMap });
|
||||||
|
[
|
||||||
|
["-1,0", 1],
|
||||||
|
["-1,1", 2],
|
||||||
|
["-1,2", 2],
|
||||||
|
["-1,3", 1],
|
||||||
|
["0,-1", 1],
|
||||||
|
["0,0", 3],
|
||||||
|
["0,1", 3],
|
||||||
|
["0,2", 3],
|
||||||
|
["0,3", 2],
|
||||||
|
["0,4", 1],
|
||||||
|
["1,-1", 1],
|
||||||
|
["1,0", 3],
|
||||||
|
["1,1", 5],
|
||||||
|
["1,2", 6],
|
||||||
|
["1,3", 2],
|
||||||
|
["1,4", 1],
|
||||||
|
["2,-1", 1],
|
||||||
|
["2,0", 3],
|
||||||
|
["2,1", 3],
|
||||||
|
["2,2", 3],
|
||||||
|
["2,3", 2],
|
||||||
|
["2,4", 1],
|
||||||
|
["3,0", 1],
|
||||||
|
["3,1", 2],
|
||||||
|
["3,2", 2],
|
||||||
|
["3,3", 1],
|
||||||
|
].forEach(([key, liveNeighbors]) => {
|
||||||
|
test(`after .next ${key} in 2nd should have ${liveNeighbors} liveNeighbors`, () => {
|
||||||
|
expect(testStream2.next.map[key].liveNeighbors).toEqual(liveNeighbors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fieldStream.next tests still lifes", () => {
|
||||||
|
const blockArray = [
|
||||||
|
[1, 1],
|
||||||
|
[1, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
const streamBlock = fieldStream({ fieldArray: blockArray });
|
||||||
|
[
|
||||||
|
["-1,-1", false],
|
||||||
|
["-1,0", false],
|
||||||
|
["-1,1", false],
|
||||||
|
["-1,2", false],
|
||||||
|
["0,-1", false],
|
||||||
|
["0,0", true],
|
||||||
|
["0,1", true],
|
||||||
|
["0,2", false],
|
||||||
|
["1,-1", false],
|
||||||
|
["1,0", true],
|
||||||
|
["1,1", true],
|
||||||
|
["1,2", false],
|
||||||
|
["2,-1", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", false],
|
||||||
|
["2,2", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after one generation of Block, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamBlock.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const beehiveMap = {
|
||||||
|
"0,1": [true, 0],
|
||||||
|
"0,2": [true, 0],
|
||||||
|
"1,0": [true, 0],
|
||||||
|
"1,3": [true, 0],
|
||||||
|
"2,1": [true, 0],
|
||||||
|
"2,2": [true, 0],
|
||||||
|
};
|
||||||
|
const streamBeehive = fieldStream({ fieldMap: beehiveMap });
|
||||||
|
[
|
||||||
|
["-1,0", false],
|
||||||
|
["-1,1", false],
|
||||||
|
["-1,2", false],
|
||||||
|
["-1,3", false],
|
||||||
|
["0,-1", false],
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", true],
|
||||||
|
["0,2", true],
|
||||||
|
["0,3", false],
|
||||||
|
["0,4", false],
|
||||||
|
["1,-1", false],
|
||||||
|
["1,0", true],
|
||||||
|
["1,1", false],
|
||||||
|
["1,2", false],
|
||||||
|
["1,3", true],
|
||||||
|
["1,4", false],
|
||||||
|
["2,-1", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", true],
|
||||||
|
["2,2", true],
|
||||||
|
["2,3", false],
|
||||||
|
["2,4", false],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", false],
|
||||||
|
["3,2", false],
|
||||||
|
["3,3", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after one generation of Beehive, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamBeehive.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const boatArray = [
|
||||||
|
[1, 1, 0],
|
||||||
|
[1, 0, 1],
|
||||||
|
[0, 1, 0],
|
||||||
|
];
|
||||||
|
const streamBoat = fieldStream({ fieldArray: boatArray });
|
||||||
|
[
|
||||||
|
["-1,-1", false],
|
||||||
|
["-1,0", false],
|
||||||
|
["-1,1", false],
|
||||||
|
["-1,2", false],
|
||||||
|
["0,-1", false],
|
||||||
|
["0,0", true],
|
||||||
|
["0,1", true],
|
||||||
|
["0,2", false],
|
||||||
|
["1,-1", false],
|
||||||
|
["1,0", true],
|
||||||
|
["1,1", false],
|
||||||
|
["1,2", true],
|
||||||
|
["1,3", false],
|
||||||
|
["2,-1", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", true],
|
||||||
|
["2,2", false],
|
||||||
|
["2,3", false],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", false],
|
||||||
|
["3,2", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after one generation of Beehive, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamBoat.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fieldStream.next tests oscillators, spaceships", () => {
|
||||||
|
const blinkerArray = [
|
||||||
|
[0, 1, 0],
|
||||||
|
[0, 1, 0],
|
||||||
|
[0, 1, 0],
|
||||||
|
];
|
||||||
|
const streamBlinker = fieldStream({ fieldArray: blinkerArray });
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", false],
|
||||||
|
["0,2", false],
|
||||||
|
["1,0", true],
|
||||||
|
["1,1", true],
|
||||||
|
["1,2", true],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", false],
|
||||||
|
["2,2", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after one generation of blinker, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamBlinker.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", true],
|
||||||
|
["0,2", false],
|
||||||
|
["1,0", false],
|
||||||
|
["1,1", true],
|
||||||
|
["1,2", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", true],
|
||||||
|
["2,2", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after two generations of blinker, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamBlinker.next.next.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const gliderArray = [
|
||||||
|
[0, 1, 0],
|
||||||
|
[0, 0, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
];
|
||||||
|
const streamGlider = fieldStream({ fieldArray: gliderArray });
|
||||||
|
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", false],
|
||||||
|
["0,2", false],
|
||||||
|
["1,0", true],
|
||||||
|
["1,1", false],
|
||||||
|
["1,2", true],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", true],
|
||||||
|
["2,2", true],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", true],
|
||||||
|
["3,2", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after one generation of glider, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamGlider.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", false],
|
||||||
|
["0,2", false],
|
||||||
|
["1,0", false],
|
||||||
|
["1,1", false],
|
||||||
|
["1,2", true],
|
||||||
|
["2,0", true],
|
||||||
|
["2,1", false],
|
||||||
|
["2,2", true],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", true],
|
||||||
|
["3,2", true],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after two generations of glider, ${key} alive: ${live}`, () => {
|
||||||
|
expect(streamGlider.next.next.next.next.map[key].living).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", false],
|
||||||
|
["0,2", false],
|
||||||
|
["0,3", false],
|
||||||
|
["1,0", false],
|
||||||
|
["1,1", true],
|
||||||
|
["1,2", false],
|
||||||
|
["1,3", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", false],
|
||||||
|
["2,2", true],
|
||||||
|
["2,3", true],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", true],
|
||||||
|
["3,2", true],
|
||||||
|
["3,3", false],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after three generations of glider, ${key} alive: ${live}`, () => {
|
||||||
|
expect(
|
||||||
|
streamGlider.next.next.next.next.next.next.map[key].living
|
||||||
|
).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
[
|
||||||
|
["0,0", false],
|
||||||
|
["0,1", false],
|
||||||
|
["0,2", false],
|
||||||
|
["0,3", false],
|
||||||
|
["1,0", false],
|
||||||
|
["1,1", false],
|
||||||
|
["1,2", true],
|
||||||
|
["1,3", false],
|
||||||
|
["2,0", false],
|
||||||
|
["2,1", false],
|
||||||
|
["2,2", false],
|
||||||
|
["2,3", true],
|
||||||
|
["3,0", false],
|
||||||
|
["3,1", true],
|
||||||
|
["3,2", true],
|
||||||
|
["3,3", true],
|
||||||
|
].forEach(([key, live]) => {
|
||||||
|
test(`after four generations of glider, ${key} alive: ${live}`, () => {
|
||||||
|
expect(
|
||||||
|
streamGlider.next.next.next.next.next.next.next.next.map[key].living
|
||||||
|
).toEqual(live);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
47
src/utils/index.js
Normal file
47
src/utils/index.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
const options = {
|
||||||
|
method: "get",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCalendar = (user) => {
|
||||||
|
return fetch(
|
||||||
|
`https://appkeychain.herokuapp.com/git/calendar/${user}`,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((e) => e);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Stream {
|
||||||
|
constructor(head, next) {
|
||||||
|
this.head = head;
|
||||||
|
this.tail = next;
|
||||||
|
this.memo = false;
|
||||||
|
}
|
||||||
|
get next() {
|
||||||
|
if (!this.memo) {
|
||||||
|
this.tail = this.tail();
|
||||||
|
this.memo = true;
|
||||||
|
}
|
||||||
|
return this.tail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getNeighbors = (key) => {
|
||||||
|
const [x, y] = key.split(",").map((str) => parseInt(str));
|
||||||
|
return [
|
||||||
|
[x - 1, y - 1],
|
||||||
|
[x - 1, y],
|
||||||
|
[x - 1, y + 1],
|
||||||
|
[x, y - 1],
|
||||||
|
[x, y + 1],
|
||||||
|
[x + 1, y - 1],
|
||||||
|
[x + 1, y],
|
||||||
|
[x + 1, y + 1],
|
||||||
|
].map((arr) => arr.join(","));
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Stream,
|
||||||
|
getNeighbors,
|
||||||
|
getCalendar,
|
||||||
|
};
|
43
webpack.common.js
Normal file
43
webpack.common.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
const path = require("path");
|
||||||
|
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
app: "./src/index.js",
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: "index.html",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
filename: "[name].bundle.js",
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
chunkFilename: "[name].bundle.js",
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/i,
|
||||||
|
use: ["style-loader", "css-loader"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/i,
|
||||||
|
loader: "html-loader",
|
||||||
|
options: {
|
||||||
|
attributes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
10
webpack.dev.js
Normal file
10
webpack.dev.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
const common = require("./webpack.common.js");
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: "development",
|
||||||
|
devtool: "inline-source-map",
|
||||||
|
devServer: {
|
||||||
|
contentBase: "./dist",
|
||||||
|
},
|
||||||
|
});
|
6
webpack.prod.js
Normal file
6
webpack.prod.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
const merge = require("webpack-merge");
|
||||||
|
const common = require("./webpack.common.js");
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: "production",
|
||||||
|
});
|
Loading…
Reference in a new issue