Last active
July 10, 2022 21:14
-
-
Save michlbro/03af399c4e52652b36d74db4e77eff09 to your computer and use it in GitHub Desktop.
Conway's game of life module. Written in luau.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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