Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
92420d0
Make getNeighborhood order-agnostic
ParkerM Feb 14, 2022
603baef
WIP pixi work
ParkerM Feb 16, 2022
82dbea5
Extract renderer base class
ParkerM Feb 16, 2022
7fafd8f
Add MCell rule parser
ParkerM Feb 20, 2022
0f71ac8
Add list of WellKnownRules and default to GoL
ParkerM Feb 20, 2022
b1a87c9
WIP canvas renderer
ParkerM Feb 20, 2022
e74b62b
WIP canvas polish
ParkerM Feb 21, 2022
a32cb5b
RendererBase doc polish
ParkerM Apr 17, 2022
c241d22
Font nonsense
ParkerM Apr 17, 2022
ec59bcf
Implement some canvas rendering; restore index
ParkerM Apr 17, 2022
884cd68
Add click listeners to canvas renderer
ParkerM Apr 17, 2022
d6fb296
Make coord text optional
ParkerM Apr 18, 2022
14e813a
Polish + dependency bumps
ParkerM Apr 18, 2022
fa6d67a
Version bumps
ParkerM Oct 3, 2022
28aa3c5
wip css renderer
ParkerM Oct 7, 2022
a4f7d1a
Bump dependencies
ParkerM Apr 16, 2023
293162a
Fix dev cmd
ParkerM Jun 9, 2025
078e6a4
Upgrade deps to latest minor
ParkerM Jun 9, 2025
200b994
add jest types dep
ParkerM Jun 9, 2025
4878371
upgrade husky v8->v9
ParkerM Jun 9, 2025
5e8f86d
upgrade lint-staged
ParkerM Jun 9, 2025
e53cf45
upgrade prettier
ParkerM Jun 9, 2025
a4c3335
vite 4 -> 5
ParkerM Jun 9, 2025
416cbfd
vite 5 -> 6
ParkerM Jun 9, 2025
f4ea234
add test for cell.toString
ParkerM Jun 10, 2025
3872cb6
Add game state tests
ParkerM Jun 10, 2025
769255a
WIP get OOB cells
ParkerM Jun 10, 2025
4518914
Merge branch 'css-renderer' into merge-canvas-renderer
ParkerM Jun 10, 2025
28dfb2e
Move util classes to own files
ParkerM Oct 7, 2022
e8a7b32
CI version bumps
ParkerM Jun 10, 2025
2c3492a
turn off failing OOB test
ParkerM Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
permissions:
actions: read
contents: read
Expand All @@ -39,11 +39,11 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -69,6 +69,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'
34 changes: 17 additions & 17 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ on: push

jobs:
lint:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '22'
- name: Install dependencies
run: npm ci

- name: check prettier
run: npm run lint

build:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '22'
- name: Install dependencies
run: npm ci

Expand All @@ -33,19 +33,19 @@ jobs:

- name: Upload build artifacts
if: ${{ github.ref == 'refs/heads/main' }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: dist

test:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '16'
node-version: '22'
- name: Install dependencies
run: npm ci

Expand All @@ -57,7 +57,7 @@ jobs:

- name: Upload coverage artifacts
if: ${{ github.ref == 'refs/heads/main' }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage
Expand All @@ -67,16 +67,16 @@ jobs:
needs:
- build
- test
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- name: Download build artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: build-artifacts
path: public
- name: Download coverage artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v4
with:
name: coverage-report
path: public/coverage
Expand Down
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged
17 changes: 15 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<title>Game of Life RxJS</title>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Farsan&display=swap"
rel="stylesheet"
/>
<!-- <style >-->
<!-- @import url('https://fonts.googleapis.com/css2?family=BenchNine:wght@300&display=swap');-->
<!-- @import url('https://fonts.googleapis.com/css2?family=BenchNine:wght@300&family=Stint+Ultra+Condensed&display=swap');-->
<!-- </style>-->
<link rel="stylesheet" type="text/css" href="/style.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script type="module" src="/main.js"></script>
Expand All @@ -14,7 +24,10 @@
<header class="header">I'm alive!</header>

<div class="game-container">
<div id="game-div"></div>
<div id="game-div">
<canvas id="game-canvas" width="60" height="30"></canvas>
<canvas id="grid-canvas"></canvas>
</div>
</div>

<div class="panel controls">
Expand Down
32 changes: 28 additions & 4 deletions lib/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
takeUntil,
zip,
} from 'rxjs';
import { State } from './util.js';
import { Rule, WellKnownRules } from './rule.js';
import { State } from './state.js';

class Cell {
/**
Expand Down Expand Up @@ -49,19 +50,34 @@ class Cell {
*/
#posY;

/**
* The neighbor rules for this cell.
* @member {Rule}
*/
#rule;

/**
* @param {Observable<void>} ticker - the game "clock" that emits whenever state should update
* @param {number} x
* @param {number} y
* @param {Subject<[number, number, State]>} changeEmitter
* @param {State} state - initial state, defaults to false
* @param {Rule} rule - the neighbor rules for this cell (defaults to B3/S23 for Game of Life)
*/
constructor(ticker, x, y, changeEmitter = null, state = State.DEAD) {
constructor(
ticker,
x,
y,
changeEmitter = null,
state = State.DEAD,
rule = WellKnownRules.GAME_OF_LIFE,
) {
this.#ticker = ticker;
this.#posX = x;
this.#posY = y;
this.changeEmitter = changeEmitter;
this.#stateSubject = new BehaviorSubject(state);
this.#rule = rule;
}

/** @returns {State} whether this cell is alive or dead */
Expand Down Expand Up @@ -122,9 +138,17 @@ class Cell {
* @returns {State}
*/
#nextState(livingNeighborCount) {
if (this.state.isAlive && [2, 3].includes(livingNeighborCount))
if (
this.state.isAlive &&
this.#rule.survivalCounts.includes(livingNeighborCount)
)
return State.ALIVE;
if (!(this.state.isDead && livingNeighborCount === 3)) {
if (
!(
this.state.isDead &&
this.#rule.birthCounts.includes(livingNeighborCount)
)
) {
return State.DEAD;
}
return State.ALIVE;
Expand Down
103 changes: 62 additions & 41 deletions lib/cell.spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Subject } from 'rxjs';
import { Cell } from './cell.js';
import { State } from './util.js';

import { State } from './state.js';

describe('Cells', () => {
/** @type {Subject<void>} */
let ticker;

/** @type {Subject<number, number, State>} */
/** @type {Subject<[number, number, State]>} */
let changeEmitter;

/** @type {Subject<void>} */
Expand Down Expand Up @@ -41,62 +42,82 @@ describe('Cells', () => {
ticker = new Subject();
changeEmitter = new Subject();
stopSignal = new Subject();
cell = new Cell(ticker.asObservable(), 1, 1, changeEmitter);
});

it('Any live cell with fewer than two live neighbours dies, as if by underpopulation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.DEAD]);
describe('Game of Life', () => {
beforeEach(() => {
cell = new Cell(ticker.asObservable(), 1, 1, changeEmitter);
});

it('Any live cell with fewer than two live neighbours dies, as if by underpopulation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.DEAD]);

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.DEAD, done),
cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.DEAD, done),
});
ticker.next(void 0);
stopSignal.next(void 0);
});
ticker.next(void 0);
stopSignal.next(void 0);
});

it('Any live cell with two live neighbours lives on to the next generation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.ALIVE]);
it('Any live cell with two live neighbours lives on to the next generation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.ALIVE]);

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
});
ticker.next(void 0);
stopSignal.next(void 0);
});
ticker.next(void 0);
stopSignal.next(void 0);
});

it('Any live cell with three live neighbours lives on to the next generation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.ALIVE, State.ALIVE]);
it('Any live cell with three live neighbours lives on to the next generation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.ALIVE, State.ALIVE]);

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
});
ticker.next(void 0);
stopSignal.next(void 0);
});

it('Any live cell with more than three live neighbours dies, as if by overpopulation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [
State.ALIVE,
State.ALIVE,
State.ALIVE,
State.ALIVE,
]);

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.DEAD, done),
});
ticker.next(void 0);
stopSignal.next(void 0);
});
ticker.next(void 0);
stopSignal.next(void 0);
});

it('Any live cell with more than three live neighbours dies, as if by overpopulation', (done) => {
cell.state = State.ALIVE;
setupNeighbors(cell, [State.ALIVE, State.ALIVE, State.ALIVE, State.ALIVE]);
it('Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction', (done) => {
cell.state = State.DEAD;
setupNeighbors(cell, [State.ALIVE, State.ALIVE, State.ALIVE]);

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.DEAD, done),
cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
});
ticker.next(void 0);
stopSignal.next(void 0);
});
ticker.next(void 0);
stopSignal.next(void 0);
});

it('Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction', (done) => {
cell.state = State.DEAD;
setupNeighbors(cell, [State.ALIVE, State.ALIVE, State.ALIVE]);
describe('Overrides and defaults', () => {
beforeEach(() => {
cell = new Cell(ticker.asObservable(), 1, 1, changeEmitter);
});

cell.listenUntil(stopSignal).subscribe({
next: expectNext(State.ALIVE, done),
test('Cell.toString', () => {
expect(cell.toString()).toEqual('Cell(1,1): dead');
});
ticker.next(void 0);
stopSignal.next(void 0);
});
});

Expand Down
Loading