Skip to content

Commit

Permalink
Merge pull request #1 from sondregj/development
Browse files Browse the repository at this point in the history
💡 Rule functions
  • Loading branch information
sondregj authored Oct 2, 2019
2 parents c87a78f + 6b4bb07 commit 3e0710b
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 105 deletions.
80 changes: 68 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
<h1 align="center">
<span style="font-size: 100px;">🧫</span>
<span style="font-size: 128px;">🧫</span>

<br>
<br>
Conway

Conway

</h1>

<h4 align="center">Conway's Game of Life</h4>
<h4 align="center">Cellular Automata</h4>

<p align="center">
<a href="https://travis-ci.org/sondregj/conway">
<img alt="Travis Build Status" src="https://img.shields.io/travis/sondregj/conway.svg?style=flat-square">
</a>

<a href="https://npmjs.com/conway">
<img alt="npm (latest)" src="https://img.shields.io/npm/v/conway/latest.svg?style=flat-square">
<img alt="npm (latest)" src="https://img.shields.io/npm/v/@sondregj/conway/latest.svg?style=flat-square">
</a>

<a href="https://npmjs.com/conway">
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/min/conway.svg?style=flat-square">
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/min/@sondregj/conway.svg?style=flat-square">
</a>

<a href="https://github.com/sondregj/conway">
Expand All @@ -33,16 +32,73 @@
</a>
</p>

A simple Game of Life implementation, in TypeScript.
A simple cellular automaton implementation, in TypeScript.

## Usage

Install by running `npm install @sondregj/conway`

```javascript
import { advance } from '@sondregj/conway'

const world = {
cells: [
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: true }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

const day1 = advance(world)
```

You can also define custom rule functions.

```javascript
import { advance } from '@sondregj/conway'

const world = {
cells: [
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: true }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

const rules = (board, cell, x, y) => !cell.alive

const day1 = advance(world, rules)
```

A convenience function for initializing boards is also included.

```javascript
import { initializeBoard, advance } from '@sondregj/conway'

const board = initializeBoard(30, 30, { random: true })
const genesis: Board = initializeBoard(64, 64, { random: true })

const day1 = advance(genesis)
```

The following TypeScript types are included.

```typescript
import { Board, BoardTick, Cell, RuleFunction } from '@sondregj/conway'

const world: Board = {
cells: [
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: true }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

const cell: Cell = { alive: true }

const advance: BoardTick = (board: Board): Board => board

const nextStep = advance(board)
const rules: RuleFunction = (board: Board, cell: Cell, x: number, y: number): boolean =>
true
```

## License
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ module.exports = {
"ts",
"js"
],
testRegex: "(/test/unit/.*|(\\.|/)(test|spec))\\.[jt]sx?$",
testRegex: "(/test/.*|(\\.|/)(test|spec))\\.[jt]sx?$",
}
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@sondregj/conway",
"version": "0.1.0",
"description": "Conway's Game of Life in TypeScript",
"version": "1.0.0",
"description": "Cellular Automata in TypeScript",
"author": "Sondre Gjellestad <[email protected]> (https://sondregjellestad.space)",
"homepage": "https://github.com/sondregj/conway#readme",
"repository": {
Expand Down Expand Up @@ -38,6 +38,7 @@
"dist/**/*"
],
"keywords": [
"game-of-life", "conway"
"game-of-life",
"conway"
]
}
11 changes: 11 additions & 0 deletions src/advance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { BoardTick, RuleFunction } from './types'

import { conwayRules } from './rules'

export const advance: BoardTick = (board, rules: RuleFunction = conwayRules) => ({
cells: board.cells.map((row, yIndex) =>
row
.map((cell, xIndex) => rules(board, cell, xIndex, yIndex))
.map(alive => ({ alive })),
),
})
82 changes: 5 additions & 77 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,7 @@
import { Board, BoardTick, Cell } from './types'
export { advance } from './advance'

interface BoardInitializerConfig {
random?: boolean
}
export { initializeBoard } from './initializer'
export { conwayRules } from './rules'
export { neighborCount } from './utils'

export const initializeBoard = (
width: number,
height: number,
config: BoardInitializerConfig = {},
): Board => {
const { random } = config

const cells: Cell[][] = []

for (let y = 0; y < height; y++) {
cells[y] = []

for (let x = 0; x < width; x++) {
cells[y][x] = {
alive: random ? Math.random() > 0.5 : false,
}
}
}

return { width, height, cells }
}

export const neighborCount = (board: Board, x: number, y: number): number => {
const targets = [
[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],
]
.filter(
([xIndex, yIndex]) =>
Math.min(xIndex, yIndex) >= 0 &&
xIndex < board.width &&
yIndex < board.height,
)
.map(([xIndex, yIndex]) => ({ x: xIndex, y: yIndex }))

return targets
.map(target => board.cells[target.y][target.x])
.reduce((sum, cell) => (cell.alive ? sum + 1 : sum), 0)
}

export const advance: BoardTick = board => {
const { width, height } = board

const cells = board.cells.map((row, yIndex) =>
row.map((cell, xIndex) => {
const neighbors: number = neighborCount(board, xIndex, yIndex)

if (!cell.alive && neighbors === 3) {
return { alive: true }
}

if (neighbors < 2) {
return { alive: false }
}

if (neighbors > 3) {
return { alive: false }
}

return { ...cell }
}),
)

return { width, height, cells }
}

export { Board, BoardTick, Cell } from './types'
export { Board, BoardTick, Cell, RuleFunction } from './types'
27 changes: 27 additions & 0 deletions src/initializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Board, Cell } from './types'

interface BoardInitializerConfig {
random?: boolean
}

export const initializeBoard = (
width: number,
height: number,
config: BoardInitializerConfig = {},
): Board => {
const { random } = config

const cells: Cell[][] = []

for (let y = 0; y < height; y++) {
cells[y] = []

for (let x = 0; x < width; x++) {
cells[y][x] = {
alive: random ? Math.random() > 0.5 : false,
}
}
}

return { cells }
}
20 changes: 20 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { RuleFunction } from './types'
import { neighborCount } from './utils'

export const conwayRules: RuleFunction = (board, cell, x, y) => {
const neighbors = neighborCount(board, x, y)

if (!cell.alive && neighbors === 3) {
return true
}

if (neighbors < 2) {
return false
}

if (neighbors > 3) {
return false
}

return cell.alive
}
9 changes: 3 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// Data types
export interface Cell {
alive: boolean
}

export interface Board {
width: number
height: number

cells: Cell[][]
}

// Transform functions
export type BoardTick = (board: Board) => Board
export type BoardTick = (board: Board, ...args: any[]) => Board

export type RuleFunction = (board: Board, cell: Cell, x: number, y: number) => boolean
23 changes: 23 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Board } from './types'

export const neighborCount = (board: Board, x: number, y: number): number => {
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],
]
.filter(
([xIndex, yIndex]) =>
Math.min(xIndex, yIndex) >= 0 &&
xIndex < board.cells[0].length &&
yIndex < board.cells.length,
)
.map(([xIndex, yIndex]) => ({ x: xIndex, y: yIndex }))
.map(target => board.cells[target.y][target.x])
.reduce((sum, cell) => (cell.alive ? sum + 1 : sum), 0)
}
23 changes: 23 additions & 0 deletions test/advance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { advance, Board } from '../src'

test('Advance function works as expected', () => {
const world: Board = {
cells: [
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: true }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

const day1 = advance(world)

const expected: Board = {
cells: [
[{ alive: false }, { alive: true }, { alive: false }],
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

expect(day1).toStrictEqual(expected)
})
15 changes: 15 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Board, neighborCount } from '../src'

test('Neighbor count function works as expected', () => {
const world: Board = {
cells: [
[{ alive: false }, { alive: false }, { alive: true }],
[{ alive: true }, { alive: false }, { alive: true }],
[{ alive: false }, { alive: true }, { alive: true }],
],
}

const count = neighborCount(world, 1, 1)

expect(count).toStrictEqual(5)
})
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["es2019", "dom"],
"lib": ["es2019"],
"module": "commonjs",
"target": "es6",
"declaration": true,
Expand Down

0 comments on commit 3e0710b

Please sign in to comment.