Using TypeScript to Generate a Map with Cellular Automata
This is an example of how to use TypeScript in NodeJS and generate cellular automata on a map and render it with console or with a normal JavaScript node library.
As our sample node library we will be using blessed
to render the map in a colorful terminal prompt in addition to a standard console.log.
Setup
Our directory structure will look like:
build/
node_modules/
src/
main.ts
main-blessed.ts
map.ts
package-lock.json
tsconfig.json
The contents of our tsconfig.json
file includes a standard build process that takes all in src/
and outputs it to JS in build/
.
{
"compilerOptions": {
"outDir": "./build",
"allowJs": true,
"target": "es5",
"types": ["node"]
},
"include": ["./src/**/*"]
}
We are going to install two node packages:
npm install @types/node
so that the TypeScript compiler knows how to use NodeJS librariesnpm install blessed
so we can render- If you have not installed typescript do so globally with
npm install -g typescript
so we can compile withtsc
Map Module
We will start by defining the map module in TypeScript that includes the Map and Cell classes that we will use to simulate a map and generate cellular automata with.
In src/map.ts
// Cell class is a single cell in our map and has an
// index on our parent map as well as row, column and
// value
export class Cell {
index: number;
r: number;
c: number;
value: number = 0;
constructor(index: number, r: number, c: number) {
this.index = index;
this.r = r;
this.c = c;
}
}
// Represents a map of rows and columns and contains
// a set of Cells to represent values
// Can initialize them to empty or run cellular
// automata against them
export class Map {
rows: number;
columns: number;
cells: Cell[];
constructor(r: number, c: number) {
this.rows = r;
this.columns = c;
this.cells = [];
let i: number = 0;
for (let r: number = 0; r < this.rows; r++) {
for (let c: number = 0; c < this.columns; c++) {
this.cells.push(new Cell(i, r, c));
i++;
}
}
}
public clear() {
for (let r: number = 0; r < this.rows; r++) {
for (let c: number = 0; c < this.columns; c++) {
let index = r * this.columns + c;
this.cells[index].value = 0;
}
}
}
public toString() {
let p = '';
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.columns; c++) {
let index = r * this.columns + c;
if (this.cells[index].value == 0)
p += ' ';
else
p += this.cells[index].value + ' ';
}
p += "\n";
}
return p;
}
// Main entry point for running cellular automata
public cellify(aliveChance: number = 0.4, deathMax: number = 3, birthMax: number = 4, steps: number = 2): void {
this.cellInit(aliveChance)
for (let i: number = 0; i < steps; i++) {
this.cellSimulate(deathMax, birthMax);
}
}
private cellInit(aliveChance: number) {
for (let r: number = 0; r < this.rows; r++) {
for (let c: number = 0; c < this.columns; c++) {
let index = r * this.columns + c;
if (Math.random() < aliveChance)
this.cells[index].value = 1;
else
this.cells[index].value = 0;
}
}
}
private cellSimulate(deathMax: number, birthMax: number) {
for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.columns; c++) {
let index = r * this.columns + c;
let nbs = this.cellCountAliveNeighbors(r, c);
if (this.cells[index].value > 0) {
// See if it should die or stay solid
if (nbs < deathMax)
this.cells[index].value = 0;
else
this.cells[index].value = 1;
} else {
//See if it should become solid
if (nbs > birthMax)
this.cells[index].value = 1;
else
this.cells[index].value = 0;
}
}
}
}
private cellCountAliveNeighbors(r: number, c: number): number {
let count = 0;
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
let nbx = i+c;
let nby = j+r;
let nbindex = nby * this.columns + nbx;
if (i == 0 && j == 0)
continue;
//If it's at the edges, consider it to be solid (you can try removing the count = count + 1)
if (nbx < 0 || nby < 0 || nbx >= this.columns || nby >= this.rows)
count = count + 1;
else if (this.cells[nbindex].value == 1)
count = count + 1;
}
}
return count;
}
}
Main with Logging
Once we have our map module built we can import it into a script and log its output
In src/main/ts
:
import { Map } from './map';
let map = new Map(24, 24);
map.cellify();
console.log(map.toString());
Will output something like:
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Cool!
Main with Blessed
Now we can make this a little richer by rendering it in a pseudo terminal window with the package blessed
just to see how to use JS node modules in a TypeScript application.
In src/main-blessed.ts
:
import { Map } from './map';
import * as blessed from 'blessed';
let map = new Map(24, 24);
map.cellify();
// Create a screen object.
var screen = blessed.screen({
smartCSR: true
});
screen.title = 'my window title';
// Create a box perfectly centered horizontally and vertically.
var box = blessed.box({
top: 'center',
left: 'center',
width: map.columns * 2,
height: map.rows,
content: map.toString(),
tags: true,
border: {
type: 'bg',
ch: '#',
fg: 1,
bg: 'cyan',
},
style: {
fg: 'white',
bg: 'blue',
border: {
fg: '#FFF'
}
}
});
// Append our box to the screen.
screen.append(box);
// Quit on Escape, q, or Control-C.
screen.key(['escape', 'q', 'C-c'], function(ch, key) {
return process.exit(0);
});
box.key('enter', function(ch, key) {
map.cellify();
box.setContent(map.toString());
screen.render();
});
// Render the screen.
screen.render();
And now it will render the map in a blue box. Each time we press enter it will generate a new map.