The following is a rough outline of how all implementations are written.
Language constraints may force specific implementations to vary from what is below.
For example, if a language does not support UInt32, then it may use Int32 instead.
Or if a language does not natively support classes, structs may be used.
But for the most part, each implementation will be similar to the following.
Identifier names below are canonical spellings; each implementation adapts their
casing to its language's dominant convention (e.g. alive_neighbours becomes
aliveNeighbours in camelCase languages, alive-neighbours in Clojure/Lisp).
The _f helper keeps its name everywhere (f in Fortran, Gleam, and R, where
leading underscores are illegal).
- play.ext
- world.ext
- cell.ext
class Play {
private const WORLD_WIDTH: UInt32
private const WORLD_HEIGHT: UInt32
// ANSI escape sequences, extracted from the render loop for readability.
private const CLEAR_SCREEN: String // begin synchronized update, cursor home, clear screen
private const SHOW_SCREEN: String // end synchronized update
public static run(): void
private static _f(value: Float64): Float64 // Or Double
}
class World {
public var tick: UInt32
private var width: UInt32
private var height: UInt32
private var cells: Map[String, Cell]
private const DIRECTIONS: Array[Array[Int32, Int32]]
public initialize(width: UInt32, height: UInt32): void
public tick(): void // Or `dotick()` if `tick` clashes with variable name
public render(): string
private make_key(x: UInt32, y: UInt32): string
private cell_at(x: UInt32, y: UInt32): Cell
private populate_cells(): void
private add_cell(x: UInt32, y: UInt32, alive: Bool = false): bool
private prepopulate_neighbours(): void
private class LocationOccupied inherits Exception
}
The make_key function should demonstrate several different ways of making the desired output. Comment out all but the fastest. Different approaches include:
- String Interpolation
"${x}-${y}"
- String Concatenation
x + "-" + y
- Array & Join
[x, "-", y].join
- Other
Some languages might support other approaches, such as writing to a stack buffer.
The render function should demonstrate several different ways of making the desired output. Comment out all but the fastest. Different approaches include:
- String Concatenation
rendering = ""
rendering << cell.to_char()
rendering
- Append Strings to Array and Join
rendering = []
rendering << cell.to_char()
rendering.join
- String Builder (if the language has a built-in one)
render_size = width * height + height
rendering = StringBuilder.new(render_size)
rendering << cell.to_char()
rendering.to_s
- Simulate String Builder (Preallocate U8 Array, Insert by Index)
render_size = width * height + height
rendering = Array.new(render_size)
idx = 0
rendering[idx] = cell.to_char()
String(rendering)
For each cell, store its in-bounds neighbours so they aren't recomputed every tick.
Normally cell.neighbours holds references to the neighbouring Cells. Languages
without pointers/shared references (immutable data) store the neighbours'
coordinate keys ("x-y") instead, refetched from world.cells when counting.
class Cell {
public var x: UInt32
public var y: UInt32
public var alive: Bool
public var next_state: Bool | Nil
public var neighbours: Array[Cell]
initialize(x: UInt32, y: UInt32, alive: Bool = false): void
public to_char(): Char // Or String
public alive_neighbours(): UInt32
}
The alive_neighbours function should demonstrate several different ways of calculating the desired output. Comment out all but the fastest. Different approaches include:
- Lamdba/Anonymous Function
neighbours.count(&:alive)
- Loop over neighbour array and increment counter
alive_neighbours = 0
for (neighbour in neighbours) {
if (neighbour.alive) {
alive_neighbours += 1
}
}
alive_neighbours
- Traditional for loop with index access
alive_neighbours = 0
count = neighbours.length
for (i = 0; i < count; i++) {
neighbour = neighbours[i]
if (neighbour.alive) {
alive_neighbours += 1
}
}
alive_neighbours