From 09afd296244284e023734818281801a3e6c56ea0 Mon Sep 17 00:00:00 2001 From: Gator Date: Sat, 1 Jan 2022 23:11:38 +0100 Subject: [PATCH] [wip] game of life, 1D to 2d --- index.html | 98 -------------------------------- main.js | 135 --------------------------------------------- src/js/automata.js | 82 +++++++++++++++++++++++---- src/js/main.js | 74 ++++++++++++++++++------- style.css | 62 --------------------- 5 files changed, 123 insertions(+), 328 deletions(-) delete mode 100644 index.html delete mode 100644 main.js delete mode 100644 style.css diff --git a/index.html b/index.html deleted file mode 100644 index 16ab6c2..0000000 --- a/index.html +++ /dev/null @@ -1,98 +0,0 @@ - - - Cellular Automaton Explorer - - - - -

Cellular Automaton Explorer

- - - - - - -
-
- -
- - - diff --git a/main.js b/main.js deleted file mode 100644 index 2de9879..0000000 --- a/main.js +++ /dev/null @@ -1,135 +0,0 @@ -let drawing = 1; - -const form = Array.from(document.forms.rules.elements); -const canvas = document.querySelector('#canvas'); -const ctx = canvas.getContext('2d'); -const main = document.querySelector('#main'); -const dead = document.querySelector('#dead'); -const live = document.querySelector('#live'); -const startBtn = document.querySelector('#start'); -const resetBtn = document.querySelector('#reset'); -const stopBtn = document.querySelector('#stop'); -const cellSize = document.querySelector('#cellSize'); -const loop = document.querySelector('#loop'); -const menuRow = document.querySelectorAll('.menu-row'); - -canvas.width = main.offsetWidth * 0.9; -canvas.height = main.offsetHeight * 0.9; - -// TODO: Hide iterator inside -function evolve(state, acc, rules) { - const [x, y, z, ...xs] = state; - if (!xs.length) { - return acc[acc.length - 1] + acc + acc[0]; - } - - const rule = x + y + z; - const newAcc = acc.concat(rules[rule]); - return evolve(y + z + xs.join(''), newAcc, rules); -} - -function getRules() { - const rules = {}; - - form.reduce((_, i) => { - if (i !== undefined - && i.type === 'checkbox') { - if (i.checked) rules[i.name] = '1'; - else rules[i.name] = '0'; - } - return rules; - }, rules); - - return rules; -} - -function getDrawingValues(state, acc) { - const cellDim = cellSize.value; - - return Object.keys(state).map( - (key) => { - const fillStyle = state[key] === '1' ? live.value : dead.value; - return { - move: [key * cellDim, acc * cellDim], - fill: [key * cellDim, acc * cellDim, cellDim, cellDim], - fillStyle, - }; - }, - ); -} - -function getRandomInt(min, max) { - const minVal = Math.ceil(min); - const maxVal = Math.floor(max); - // The maximum is exclusive and the minimum is inclusive - return Math.floor(Math.random() * (maxVal - minVal) + minVal); -} - -async function sleep(ms) { - await new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function draw(state, acc) { - if (drawing === 0) { - return; - } - - const position = acc * cellSize.value; - if (position >= canvas.height && !loop.checked) return; - - const rules = getRules(); - const newState = evolve(state, '', rules); - const line = getDrawingValues(newState, acc); - - line.map((cell) => { - ctx.moveTo(...cell.move); - ctx.fillRect(...cell.fill); - ctx.fillStyle = cell.fillStyle; - return cell; - }); - - await sleep(40); - - const newAcc = () => { - if (position >= canvas.height && loop.checked) return 0; - return acc; - }; - - await draw(newState, newAcc() + 1); -} - -function reset() { - drawing = 0; - ctx.clearRect(0, 0, canvas.width, canvas.height); -} - -startBtn.addEventListener('click', async () => { - reset(); - - await sleep(60); - - drawing = 1; - - const initialState = [...Array(canvas.width)].map( - () => getRandomInt(0, 2).toString(), - ).join(''); - - draw(initialState, 1); -}); - -resetBtn.addEventListener('click', async () => { - reset(); -}); - -stopBtn.addEventListener('click', async () => { - drawing = 0; -}); - -menuRow.forEach((elem) => { - elem.querySelector('h2').addEventListener('click', async (e) => { - // ugly - const menuDisplay = e.currentTarget.parentNode.querySelector('.menu-row-content').style; - if (menuDisplay.display !== 'none') menuDisplay.setProperty('display', 'none'); - else menuDisplay.setProperty('display', 'block'); - }); -}); diff --git a/src/js/automata.js b/src/js/automata.js index 588e83e..b9548c9 100644 --- a/src/js/automata.js +++ b/src/js/automata.js @@ -1,13 +1,59 @@ -// TODO: Hide accumulator inside -function evolve(state, acc, rules) { - const [x, y, z, ...xs] = state; - if (!xs.length) { - return acc[acc.length - 1] + acc + acc[0]; +// get the next evolution of a 1D CA +function evolve(state, rules) { + function evolveAcc(s, acc) { + const [x, y, z, ...xs] = s; + if (!xs.length) { + return acc[acc.length - 1] + acc + acc[0]; + } + + const rule = x + y + z; + const newAcc = acc.concat(rules[rule]); + return evolveAcc(y + z + xs.join(''), newAcc); + } + return evolveAcc(state, ''); +} + +// create a 2D board from a 1D initial state +function createBoard(state, rules, height) { + function createBoardAcc(s, h, acc) { + if (h === 0) return acc; + const newState = evolve(state, rules); + const newAcc = acc.concat(state); + return createBoardAcc(newState, h - 1, newAcc); + } + return createBoardAcc(state, height, []).map( + (row) => row.split('')) +} + +function getCellNeighbors(board, cellCoordinates) { + const [x, y] = cellCoordinates; + const rowLength = board[0].length; // caca? + + // handles board edges where the cell is missing neighbors + function getCell(xx, yy) { + // handles negative index and index bigger than its array length + function guard(index, array) { + if (index > array.length - 1) return 0; + if (index < 0) return array.length - 1; + return index; + } + + const safeX = guard(xx, board); + const safeY = guard(yy, rowLength); + return board[safeX][safeY]; } - const rule = x + y + z; - const newAcc = acc.concat(rules[rule]); - return evolve(y + z + xs.join(''), newAcc, rules); + // the current cell is not included in the result + return [ + getCell(x - 1, y - 1), getCell(x, y - 1), + getCell(x + 1, y - 1), getCell(x - 1, y), + getCell(x + 1, y), getCell(x - 1, y + 1), + getCell(x, y + 1), getCell(x + 1, y - 1), + ]; +} + +function getNeighborsSum(cells) { + return cells.reduce((cell, acc) => cell + acc, 0); } function conwayRules(cell, neighbors) { @@ -21,11 +67,23 @@ function conwayRules(cell, neighbors) { return cell; } +function evolve2d(board, rulesFn) { + return board.map((row, x) => row.map((cell, y) => { + const neighbors = getCellNeighbors(board, [x, y]); + const sum = getNeighborsSum(neighbors); + return rulesFn(cell, sum); + })); +} + function getDrawingValues(state, acc, cell) { const d = cell.dimension; return Object.keys(state).map( (key) => { - const fillStyle = state[key] === '1' ? cell.liveColor : cell.deadColor; + const fillStyle = (() => { + if (state[key] === '1') return cell.liveColor; + return cell.deadColor; + })(); + return { move: [key * d, acc * d], fill: [key * d, acc * d, d, d], @@ -35,18 +93,18 @@ function getDrawingValues(state, acc, cell) { ); } -function initialState1D(width, initFn, args) { +function initialState1d(width, initFn, args) { return [...Array(width)].map( () => initFn(...args).toString(), ).join(''); } -function initialState2D(width, height, initFn, args) { +function initialState2d(width, height, initFn, args) { return [...Array(height)].map( () => [...Array(width)].map(() => initFn(...args)), ); } export { - evolve, getDrawingValues, initialState1D, initialState2D, + getDrawingValues, initialState1d, initialState2d, evolve, evolve2d, conwayRules, createBoard, }; diff --git a/src/js/main.js b/src/js/main.js index aea920d..19d8a01 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1,5 +1,7 @@ -import { getRandomInt, sleep } from './common.js'; -import { evolve, getDrawingValues, initialState1D } from './automata.js'; +import { getRandomInt, sleep, hexToRgb } from './common.js'; +import { + evolve, evolve2d, getDrawingValues, initialState1d, conwayRules, createBoard, +} from './automata.js'; let drawing = 1; @@ -10,6 +12,7 @@ const main = document.querySelector('#main'); const dead = document.querySelector('#dead'); const live = document.querySelector('#live'); const startBtn = document.querySelector('#start'); +const startBtn2d = document.querySelector('#start2d'); const resetBtn = document.querySelector('#reset'); const stopBtn = document.querySelector('#stop'); const cellSize = document.querySelector('#cellSize'); @@ -39,41 +42,70 @@ function reset() { ctx.clearRect(0, 0, canvas.width, canvas.height); } +// TODO: Hide accumulator inside maybe? async function draw(state, acc) { if (drawing === 0) { return; } - const cell = { - dimension: cellSize.value, + const cellProperties = { + dimensions: cellSize.value, liveColor: live.value, deadColor: dead.value, }; - const position = acc * cell.dimension; - if (position >= canvas.height && !loop.checked) return; + // const position = acc * cell.dimension; + // if (position >= canvas.height && !loop.checked) return; const rules = getRules(); - const newState = evolve(state, '', rules); - const line = getDrawingValues(newState, acc, cell); + const board = createBoard(state, rules, canvas.height); + // const newState = evolve(state, rules); + // const line = getDrawingValues(newState, acc, cell); - line.map((c) => { - ctx.moveTo(...c.move); - ctx.fillRect(...c.fill); - ctx.fillStyle = c.fillStyle; - return c; + board.reduce((row, prevRow, x) => { + const d = cellProperties.dimensions; + row.reduce((cell) => { + ctx.moveTo(x * d, y * d); + ctx.fillRect(x * d, x * d, d, d); + ctx.fillStyle = (() => { + if (cell === '1') return cellProperties.liveColor; + return cellProperties.deadColor; + })(); + }) }); - await sleep(40); + // await sleep(40); - const newAcc = () => { - if (position >= canvas.height && loop.checked) return 0; - return acc; - }; + // const newAcc = (() => { + // if (position >= canvas.height && loop.checked) return 0; + // return acc; + // })(); - await draw(newState, newAcc() + 1); + // await draw(newState, newAcc + 1); } +async function draw2d(board, acc) { + if (drawing === 0) { + + } +} + +// Listeners + +startBtn2d.addEventListener('click', async () => { + reset(); + + await sleep(60); + + drawing = 1; + + const initialState = initialState2d(canvas.width, canvas.height, getRandomInt, [0, 2]); + + const nextState = evolve2d(initialState, conwayRules); + + draw2d(initialState, 0); +}); + startBtn.addEventListener('click', async () => { reset(); @@ -81,9 +113,9 @@ startBtn.addEventListener('click', async () => { drawing = 1; - const initialState = initialState1D(canvas.width, getRandomInt, [0, 2]); + const initialState = initialState1d(canvas.width, getRandomInt, [0, 2]); - draw(initialState, 1); + draw(initialState, 0); }); resetBtn.addEventListener('click', async () => { diff --git a/style.css b/style.css deleted file mode 100644 index 299fa3c..0000000 --- a/style.css +++ /dev/null @@ -1,62 +0,0 @@ -* { - margin: 0; - padding: 0; -} - -body { - background: black; - color: white; - display: flex; - font-family: Courier New; -} - -canvas { - background: #110812; - margin: 20px 0 0 0; -} - -h1, h2 { - font-weight: bold; -} - -h1 { - font-size: large; -} - -.menu-row h2 { - font-size: medium; - margin-left: 10px; - padding: 10px; - cursor: pointer; - border: 2px solid darkgrey; -} - -sidebar { - flex: auto; - padding: 10px; -} - -input[type="button"] { - min-width: 60px; - padding: 5px; - font-weight: bold; -} - -.form-field { - display: flex; - margin: 10px; -} - -.menu-row { - flex: 1; - margin: 5px 0; -} - -label, .form-field label { - margin-right: 10px; - font-weight: bold; -} - -#main { - flex: 4; -}