Skip to content

Instantly share code, notes, and snippets.

@michlbro
Last active July 10, 2022 21:14
Show Gist options
  • Save michlbro/03af399c4e52652b36d74db4e77eff09 to your computer and use it in GitHub Desktop.
Save michlbro/03af399c4e52652b36d74db4e77eff09 to your computer and use it in GitHub Desktop.
Conway's game of life module. Written in luau.
local version = "0.0.2"
--[[
Conway's game of life.
Scripted by XxprofessgamerxX.
]]
--[[
API:
local conway_module = require(path_to_module)
local conway = conway_module.new(canvasSize: number, canvasPosition: vector3, partSize: number = 1) -> conway class
conways:createCanvas() -> folder (where life will be kept)
conway:addLife(worldPosition: Vector2, matrixPosition: Vector2, matrixTable: table) -- Three ways to set life of cell.
conway:removeLife(worldPosition: Vector2, matrixPosition: Vector2, matrixTable: table) -- Three ways to remove life of cell.
conway:addRandom()
conway:start()
conway:stop()
conway:clean() -- removes all life
conway:destroy() -- destroys class
conway:step() -- manual iteration
conway:speed(speed: number) -- per second
-- [WIP]
conways:moveCanvas(position: Vector3) - move alive cells too.
-- [UPDATES]:
0.0.1:
o Idk finished?
o works fine.
0.0.2:
o Optimised cell checking to allow a bigger canvas size. (radius cell check around live cells.)
]]
-- Types
local defaultConstructor: Constructor = {
Config = {
canvasSize = 10;
canvasPosition = Vector3.new(0,0,0);
partSize = 1;
}
}
export type Constructor = {
Config: {
canvasSize: number?;
canvasPosition: Vector3?;
partSize: number?;
}
}
export type MatrixTable = {
[number?]: {
[number?]: number?;
}
}
-- Enums
local runState = {
stop = 0;
start = 1;
step = 3;
speed = 4;
}
-- Create class
local conway = {}
conway.__index = conway
-- Services
local runService = game:GetService("RunService")
-- Internal Variables
local ConwayNumber: number = 0
-- External Variables
--[[
Construct conway class.
]]
function conway.new(canvasSize: number, canvasPosition: Vector3, partSize: number)
local self = setmetatable(defaultConstructor, conway)
-- Setup user inputed values
self.Config.canvasSize = canvasSize or defaultConstructor.Config.canvasSize
self.Config.canvasPosition = canvasPosition or defaultConstructor.Config.canvasPosition
self.Config.partSize = partSize or defaultConstructor.Config.partSize
--[[
Create internal life config.
Create a folder to hold life, keep it in the module while canvas does not exist yet.
Create life 2d matrix to hold cell states.
Create bindableEvent (start, stop, step, speed) - used to listen to start/stop events
Runtime:
executing: bool
runtime = function
]]
self._life = {
matrix = {};
setup = false;
}
self._life.lifeState = self._createEvent(self._life.folder)
self._life.folder = self._createFolder()
self._life.matrix = {
MainMatrix = self._createMatrix(self.Config.canvasSize);
liveCellsArray = {};
}
--[[REDO-DONE:
Cell Matrix:
MainMatrix = CanvasSize with cell state
liveCellsArray = Array of live cells
liveCellsArray = {
{x = 0;
y = 0;
}
}
]]
-- Runtime
self._runtime = {
executing = false;
runtime = self._runtimeFunction(self);
}
return self
end
--[[
Listen to lifeState (BindableEvent) to control: start, stop, speed and step.
If looping then use RunService.Heartbeat. (Can be controlled using speed).
]]
function conway._runtimeFunction(self)
local runtimeEvent: BindableEvent = self._life.lifeState
local runtime = nil
local function init(speed: number?)
local start = os.clock()
local connection = runService.Heartbeat:Connect(function(deltatime: number)
self._runtime.executing = true
if speed then
if (os.clock() + deltatime) - start < speed then
return
end
start = os.clock()
-- code
self._init(self)
return
end
-- code
self._init(self)
end)
return function()
connection:Disconnect()
connection = nil
self._runtime.executing = false
end
end
local function step()
self._runtime.executing = true
-- code
self._init(self)
self._runtime.executing = false
end
runtimeEvent.Event:Connect(function(state, speed: number?)
if state == runState.start then
if runtime then
return
else
runtime = init()
end
elseif state == runState.stop then
if runtime then
runtime()
runtime = nil
end
elseif state == runState.speed then
if runtime then
runtime()
runtime = nil
runtime = init(speed)
end
runtime = init(speed)
elseif state == runState.step then
if runtime then
runtime()
runtime = nil
step()
end
step()
end
end)
end
-- clone table
local function deepCopy(original)
local copy = {}
for k, v in pairs(original) do
if type(v) == "table" then
v = deepCopy(v)
end
copy[k] = v
end
return copy
end
--[[
Control the cell states.
Conways life rules:
If there are:
2 or 3 neighbour cells = lives/created
less than 2 neighbour cells = dies (underpopulation)
more than 3 neighbour cells = dies (overpopulation)
North = (x, y - 1)
East = (x + 1, y)
South = (x, y + 1)
West = (x - 1, y)
]]
--[[
REDO-DONE:
For optimisations:
Create a new array - position searched
For loop through liveCellsArray:
For live cells deepCopy a 5 radius around live cell.
for loop this range around live cell, if position already searched then skip or edit value then add it to position searched.
]]
function conway._init(self)
local radiusSearch = 4
-- matricies
local cellMatrix = self._life.matrix.MainMatrix
local liveCellsArray = self._life.matrix.liveCellsArray
local newLiveCellsArray = {}
local updatedCellsArray = {}
local positionsSearched = {} -- { [x] = [y] = true }
-- cell/canvas properties
local canvasSize = self.Config.canvasSize
local cellFolder = self._life.folder.cellParts
local canvasPosition = self.Config.canvasPosition
local cellSize = self.Config.partSize
local scaledCanvasSize = canvasSize * cellSize
-- For loop liveCellsArray
for i, position in pairs(liveCellsArray) do
-- create chunk around liveCell
local cellMatrixChunk = {}
local xStartPos, yStartPos = position.x - radiusSearch, position.y - radiusSearch
local xEndPos, yEndPos = position.x + radiusSearch, position.y + radiusSearch
for x = xStartPos, xEndPos do
if not cellMatrix[x] then continue end
cellMatrixChunk[x] = {}
for y = yStartPos, yEndPos do
if not cellMatrix[x][y] then continue end
cellMatrixChunk[x][y] = cellMatrix[x][y]
end
for x, yTable in pairs(cellMatrixChunk) do
if x == xStartPos or x == xEndPos then continue end
local xSeached = (positionsSearched[x]) and true or false
-- check west and east bounderies
local canEast = if ((x + 1) <= canvasSize) then true else false
local canWest = if ((x - 1) > 0) then true else false
for y, cellTable in pairs(yTable) do
if y == yStartPos or y == yEndPos then continue end
if (xSeached and positionsSearched[x][y]) then continue end
if not xSeached then positionsSearched[x] = {} end
positionsSearched[x][y] = true
-- check north and south bounderies
local canNorth = if ((y - 1) > 0) then true else false
local canSouth = if ((y + 1) <= canvasSize) then true else false
-- Calculate live cells and south/north-east/west
local liveCells = 0
local cellState = cellTable.cellState
-- north
liveCells += if (canNorth and cellMatrix[x][y - 1].cellState) then 1 else 0
-- north-east
liveCells += if (canNorth and canEast and cellMatrix[x + 1][y - 1].cellState) then 1 else 0
-- north-west
liveCells += if (canNorth and canWest and cellMatrix[x - 1][y - 1].cellState) then 1 else 0
-- south
liveCells += if (canSouth and cellMatrix[x][y + 1].cellState) then 1 else 0
-- south-east
liveCells += if (canSouth and canEast and cellMatrix[x + 1][y + 1].cellState) then 1 else 0
-- south-west
liveCells += if (canSouth and canWest and cellMatrix[x - 1][y + 1].cellState) then 1 else 0
-- west
liveCells += if (canWest and cellMatrix[x - 1][y].cellState) then 1 else 0
-- east
liveCells += if (canEast and cellMatrix[x + 1][y].cellState) then 1 else 0
if liveCells == 3 and not cellTable.cellState then
cellState = true
elseif liveCells < 2 or liveCells > 3 then
cellState = false
end
if not updatedCellsArray[x] then
updatedCellsArray[x] = {}
end
if cellState then
updatedCellsArray[x][y] = true
else
updatedCellsArray[x][y] = false
end
end
end
end
end
for x, yTable in pairs(updatedCellsArray) do
for y,cellState in pairs(yTable) do
if cellState then
if cellMatrix[x][y].cellPart then
continue
end
cellMatrix[x][y].cellState = true
table.insert(newLiveCellsArray, {x = x, y = y})
-- create part
local cell: Part = Instance.new("Part")
cell.Name = string.format("%s,%s", x, y)
cell.Anchored = true
cell.Parent = cellFolder
-- size
cell.Size = Vector3.one * cellSize
-- position
local xPos: number, yPos: number = 0, 0
xPos = ((-(scaledCanvasSize/2) + ((x - 1) * cellSize)) + canvasPosition.X) + cellSize/2
yPos = ((-(scaledCanvasSize/2) + ((y - 1) * cellSize)) + canvasPosition.Z) + cellSize/2
cell.Position = Vector3.new(xPos, canvasPosition.Y + (cellSize/2) + (cellFolder.Parent.Canvas.Size.Y/2), yPos)
-- set part to cell
cellMatrix[x][y].cellPart = cell
-- mataterial/colurs done later.
continue
end
cellMatrix[x][y].cellState = false
if cellMatrix[x][y].cellPart then
cellMatrix[x][y].cellPart:Destroy()
cellMatrix[x][y].cellPart = nil
end
end
end
end
--[[
Create bindable event, parent it to life folder. Use for listening to (start/stop events)
]]
function conway._createEvent(lifeFolder: Folder): BindableEvent
local bindableEvent: BindableEvent = Instance.new("BindableEvent")
bindableEvent.Name = "conwayState"
bindableEvent.Parent = lifeFolder
return bindableEvent
end
--[[
Create a life 2d matrix that will hold the cells state.
for x let it be x
for z let it be y
cellState = 0 (dead), 1(alive); cellPart = if alive then part here exists or nil.
]]
function conway._createMatrix(canvasSize: number): table
local cellMatrix = {}
for x = 1, canvasSize do
cellMatrix[x] = {}
for y = 1, canvasSize do
cellMatrix[x][y] = {cellState = false, cellPart = nil}
end
end
return cellMatrix
end
--[[
Create life folder, keep it parented to the module while canvas does not exist.
Name it "Conway[number]" based on how many conway lifes exist.
]]
function conway._createFolder(): Folder
local folder: Folder = Instance.new("Folder")
folder.Parent = script
folder.Name = string.format("Conway_%s", tostring(ConwayNumber + 1))
local lifeInstances: Folder = Instance.new("Folder")
lifeInstances.Name = "cellParts"
lifeInstances.Parent = folder
return folder
end
--[[
conway:createCanvas() -> Folder
Create canvas using position given/default.
Scale canvas based on part size.
Return a folder that will hold the life parts.
]]
function conway:createCanvas(): Folder
-- if canvas does not exist yet, then setup canvas using this command.
-- if canvas:createCanvas() is called, then the conway class is fully setup (true)
if self._life.setup then
return
else
self._life.setup = true
end
-- Canvas/life configs
local partSize: number = self.Config.partSize
local canvasSize: number = self.Config.canvasSize
local canvasPosition: Vector3 = self.Config.canvasPosition
-- Create canvas part, parent it to conway folder, then set folder to workspace.
local canvas: Part = Instance.new("Part")
canvas.Anchored = true
canvas.Parent = self._life.folder
canvas.Name = "Canvas"
self._life.folder.Parent = workspace
-- Set canvas size based on partSize, then position it
local xySize = 0
xySize = canvasSize * partSize
canvas.Size = Vector3.new(xySize, 1, xySize)
canvas.Position = canvasPosition
-- return life Folder
return self._life.folder
end
--[[
conway:addLife(position: Vector3, matrixPosition: Vector2, matrix: matrixTable)
Change cellState to true when life added to that cell.
Add part to position.
OPTIONAL:
Using matrixPosition add at certain cell e.g. Vector2.new(4, 3) -> matrix[4][3].cellState = true
Using matrix add life to a group of cells.
]]
function conway:addLife(worldPosition: Vector2, matrixPosition: Vector2, matrixTable: MatrixTable)
-- if conway is exectuing then don't edit.
if self._runtime.executing then
return
end
-- if conway setup not finished then don't edit.
if not self._life.setup then
return
end
local cellMatrix: table = self._life.matrix.MainMatrix
local liveCellsArray: table = self._life.matrix.liveCellsArray
local MatrixSize: number = self.Config.canvasSize
local cellFolder: Folder = self._life.folder.cellParts
local cellSize: number = self.Config.partSize
local canvasPosition: Vector3 = self.Config.canvasPosition
local relativeCanvasSize: number = cellSize * MatrixSize
local function addLife(x: number, y: number)
if cellMatrix[x] and cellMatrix[x][y] and not cellMatrix[y][x].cellState then
-- add life
cellMatrix[x][y].cellState = true
table.insert(liveCellsArray, {x = x, y = y})
-- create cell part.
local cell: Part = Instance.new("Part")
cell.Name = string.format("%s,%s", x, y)
cell.Anchored = true
cell.Parent = cellFolder
-- size
cell.Size = Vector3.new(cellSize, cellSize, cellSize)
-- position
local xPos: number, yPos: number = 0, 0
xPos = ((-(relativeCanvasSize/2) + ((x - 1) * cellSize)) + canvasPosition.X) + cellSize/2
yPos = ((-(relativeCanvasSize/2) + ((y - 1) * cellSize)) + canvasPosition.Z) + cellSize/2
cell.Position = Vector3.new(xPos, canvasPosition.Y + (cellSize/2) + (cellFolder.Parent.Canvas.Size.Y/2), yPos)
-- set part to cell
cellMatrix[x][y].cellPart = cell
-- mataterial/colurs done later.
end
end
-- matrixTable setup
if matrixTable then
for x, yTable in pairs(matrixTable) do
for y, cellState in pairs(yTable) do
if cellState then
addLife(x, y)
end
end
end
end
-- matrixPosition
if matrixPosition then
addLife(matrixPosition.X, matrixPosition.Y)
end
-- worldPosition
if worldPosition then
local xPos, yPos = math.floor(worldPosition.X), math.floor(worldPosition.Y)
addLife(xPos, yPos)
end
end
--[[
conway:removeLife(worldPosition: Vector2, matrixPosition: Vector2, matrixTable: MatrixTable).
Change cell state to false when life removed to the cell.
Remove part at position.
OPTIONAL:
Using matrixPosition remove life at cell. e.g. Vector2.new(4, 3) -> matrix[4, 3].cellState = false.
Using matrixTable remove life of a group of cells.
]]
function conway:removeLife(worldPosition: Vector2, matrixPosition: Vector2, matrixTable: MatrixTable)
-- if conway is exectuing then don't edit.
if self._runtime.executing then
return
end
-- if conway setup not finished then don't edit.
if not self._life.setup then
return
end
local cellMatrix: table = self._life.matrix.MainMatrix
local liveCellsArray: table = self._life.matrix.liveCellsArray
local function removeLife(x: number, y: number)
if cellMatrix[x] and cellMatrix[x][y] and cellMatrix[x][y].cellState and cellMatrix[x][y].cellPart then
-- remove life
cellMatrix[x][y].cellState = false
for _, position in pairs(liveCellsArray) do
if position.x == x and position.y == y then
position = nil
end
end
-- destroy cell part
cellMatrix[x][y].cellPart:Destroy()
end
end
-- matrixTable setup
if matrixTable then
for x, yTable in pairs(matrixTable) do
for y, cellState in pairs(yTable) do
if not cellState then
removeLife(x, y)
end
end
end
end
-- matrixPosition
if matrixPosition then
removeLife(matrixPosition.X, matrixPosition.Y)
end
-- worldPosition
if worldPosition then
local xPos, yPos = math.floor(worldPosition.X), math.floor(worldPosition.Y)
removeLife(xPos, yPos)
end
end
--[[
conway:addRandom()
Makes the cell alive or dead based on math.random(0,1). (0 dead, 1 alive)
]]
function conway:addRandom()
-- if conway is exectuing then don't edit.
if self._runtime.executing then
return
end
-- if conway setup not finished then don't edit.
if not self._life.setup then
return
end
local cellMatrix: table = self._life.matrix.MainMatrix
local liveCellsArray: table = self._life.matrix.liveCellsArray
local MatrixSize: number = self.Config.canvasSize
local cellFolder: Folder = self._life.folder.cellParts
local cellSize: number = self.Config.partSize
local canvasPosition: Vector3 = self.Config.canvasPosition
local relativeCanvasSize: number = cellSize * MatrixSize
local function addLife(x: number, y: number)
if cellMatrix[x] and cellMatrix[x][y] and not cellMatrix[x][y].cellState then
-- add life
cellMatrix[x][y].cellState = true
table.insert(liveCellsArray, {x = x, y = y})
-- create cell part.
local cell: Part = Instance.new("Part")
cell.Name = string.format("%s,%s", x, y)
cell.Anchored = true
cell.Parent = cellFolder
-- size
cell.Size = Vector3.one * cellSize
-- position
local xPos: number, yPos: number = 0, 0
xPos = ((-(relativeCanvasSize/2) + ((x - 1) * cellSize)) + canvasPosition.X) + cellSize/2
yPos = ((-(relativeCanvasSize/2) + ((y - 1) * cellSize)) + canvasPosition.Z) + cellSize/2
cell.Position = Vector3.new(xPos, canvasPosition.Y + (cellSize/2) + (cellFolder.Parent.Canvas.Size.Y/2), yPos)
-- set part to cell
cellMatrix[x][y].cellPart = cell
-- mataterial/colurs done later.
end
end
local function removeLife(x: number, y: number)
if cellMatrix[x] and cellMatrix[x][y] and cellMatrix[x][y].cellState and cellMatrix[x][y].cellPart then
-- remove life
cellMatrix[x][y].cellState = false
for _, position in pairs(liveCellsArray) do
if position.x == x and position.y == y then
position = nil
end
end
-- destroy cell part
cellMatrix[x][y].cellPart:Destroy()
end
end
for x, yTable in pairs(cellMatrix) do
for y, cell in pairs(yTable) do
local cellState = math.random(0,1)
if cellState == 0 then
removeLife(x, y)
continue
end
addLife(x, y)
end
end
end
-- Start simulation.
function conway:start()
-- if conway setup not finished then don't start.
if not self._life.setup then
return
end
self._life.lifeState:Fire(runState.start)
end
-- Stop simulation.
function conway:stop()
-- if conway setup not finished then don't stop/do anything.
if not self._life.setup then
return
end
self._life.lifeState:Fire(runState.stop)
end
-- Stops and destroy conway class.
function conway:destroy()
self:stop()
self._life.folder:Destroy()
self = nil
end
-- Step simulation.
function conway:step()
-- if conway setup not finished then don't step.
if not self._life.setup then
return
end
self._life.lifeState:Fire(runState.step)
end
-- Start simulation with variable speed.
function conway:speed(speed: number)
-- if conway setup not finished then don't start.
if not self._life.setup then
return
end
self._life.lifeState:Fire(runState.speed, speed)
end
-- Stop and clean canvas.
function conway:clean()
self:stop()
for x, yTable in pairs(self._life.matrix.MainMatrix) do
for y, cell in pairs(yTable) do
if cell.cellState then
cell.cellState = false
cell.cellPart:Destroy()
cell.cellPart = nil
end
end
end
self._life.matrix.liveCellsArray = {}
end
return conway
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment