// initiate ranges interface...
document
.querySelectorAll('input[type=range]')
.forEach(r => r.oninput =_=> r.closest('label').dataset.val=r.value)
const
action = { run: false, cycle: 0 }
, size = { cols: 0, rows:0 }
, onAction = function* () { while(action.run) yield }
, delay = ms => new Promise(r => setTimeout(r, ms))
, isAlive = (r,c) => tbl.rows[r].cells[c].classList.contains('life')
, adjCellsCount = (row,col) =>
{
let res = 0
for ( let r = Math.max(0, row-1); r < Math.min(size.rows, row+2); ++r )
for ( let c = Math.max(0, col-1); c < Math.min(size.cols, col+2); ++c )
if ((r!==row || c!==col) && isAlive(r,c) ) res++
return res
}
, StopGridAction =_=>
{
btClear.disabled = btGo.disabled = action.run = false
btStop.disabled = !btGo.disabled
tbl.className = 'onBorder'
}
cycles.onclick =_=> // reset cycles count
{
cycles.textContent = action.cycle = 0
}
addPoints.onclick =_=> // create new life points
{
let
freePos = [...tbl.querySelectorAll('td:not([class="life"]')]
, ptN = Math.min( freePos.length, Points.valueAsNumber )
;
if (!ptN) return
for (let i = freePos.length -1; i > 0; i--) // shuffle Array
{
let j = Math.floor(Math.random() * (i + 1));
[freePos[i], freePos[j]] = [freePos[j], freePos[i]]
}
for (;ptN--;) freePos[ptN].className = 'life'
}
btDrawGrid.onclick = _ => // draw Grid Button
{
StopGridAction()
tbl.innerHTML = '' // clear Grid
tbl.style.setProperty('--sz', cellSz.value + 'px')
size.cols = nCols.valueAsNumber
size.rows = nRows.valueAsNumber
for ( let r = 0; r < size.rows; ++r) // construct Grid
{
let nRow = tbl.insertRow()
for ( let c = 0; c < size.cols; ++c) nRow.insertCell()
}
addPoints.click()
cycles.click()
}
tbl.onclick = ({target}) => // add or remove points on Grid
{
if(!target.matches('td')) return
target.className = target.classList.contains('life') ? '' : 'life';
}
// initial setup...
btStop.disabled = true
nCols.value = 20
nRows.value = 12
cellSz.value = 16
Points.value = 24
btDrawGrid.click()
addPoints.click()
// main sections...
btStop.onclick = StopGridAction // stop Conway's Game of Life action
btClear.onclick =_=> // clear Grid
{
tbl.querySelectorAll('td').forEach(el=>el.className = '')
}
btGo.onclick =_=> // Start button -> see Conway's Game of Life action
{
gridOptions.open = false
btClear.disabled = btGo.disabled = action.run = true
btStop.disabled = !btGo.disabled
tbl.className = ''
loopLifes()
}
async function loopLifes()
{
for await (x of onAction())
{
cycles.textContent = (++action.cycle).toLocaleString()
await lifeCycle()
await delay (300)
tbl.querySelectorAll('td.die').forEach(el=>el.className = '')
tbl.querySelectorAll('td.born').forEach(el=>el.className = 'life')
await delay (800)
}
btStop.click()
}
function lifeCycle()
{
return new Promise((successCallback) =>
{
let cLives = 0
for ( let r = 0; r < size.rows; ++r)
{
if ( !action.run ) break
for ( let c = 0; c < size.cols; ++c)
{
if ( !action.run ) break
let aroundLife = adjCellsCount(r,c)
if (isAlive(r,c))
{
if ( aroundLife < 2 || aroundLife > 3 )
tbl.rows[r].cells[c].classList.add('die')
else
++cLives
}
else
{
if (aroundLife === 3 )
{
tbl.rows[r].cells[c].className = 'born'
++cLives
} } } }
action.run &= Boolean(cLives)
successCallback()
})
}
table {
border-collapse : collapse;
margin : 1em;
background-color : white;
}
table tbody {
--sz : 12px;
}
table tbody td {
width : var(--sz);
height : var(--sz);
border : 1px solid transparent;
position : relative;
cursor : pointer;
}
table tbody td:hover {
background-color : #28608371;
}
table tbody:hover td,
table tbody.onBorder td {
border-color : #d3d3d3cc;
}
.born::before,
.die::before,
.life::before {
position : absolute;
content : ' ';
top : 0;
left : 0;
width : 100%;
height : 100%;
display : inline-block;
border-radius : 50%;
}
.born::before { background-color : lightseagreen; }
.die::before { background-color : #f0808080 !important; }
.life::before { background-color : #286083; }
body {
font-family : Arial, Helvetica, sans-serif;
font-size : 16px;
}
label {
font-size : 12px;
}
label input {
vertical-align : middle;
width : 300px;
}
label::after {
content : ' ' attr(data-val);
}
label::before {
display : inline-block;
width : 4.5em;
content : attr(data-lib);
}
button {
margin-top : .4em;
min-width : 4em;
text-transform : capitalize;
}
details {
position : absolute;
top : 1.7em;
right : .5em;
width : 7em;
padding : .4em .6em .3em .6em;
background : #286083e5;
z-index : 100;
color : white;
border-radius : .4em;
}
details[open] {
width : 24em;
}
details label:first-of-type {
display: inline-block;
margin-top: 1.2em;
}
summary {
padding-right : .2em;
text-align : right;
direction : rtl;
}
summary,
button:not(:disabled) {
cursor : pointer;
}
details button {
float: right;
}
mark {
position : absolute;
top : .4em;
right : .5em;
width : 7.6em;
padding : 0 .3em;
text-align : right;
background : none;
cursor : pointer;
color : darkgrey;
border-radius : .2em;
font-style : oblique;
}
mark:hover {
background : #286083a2;
color : white;
}
<h3> Conway's Game of Life </h3>
<hr>
<details id="gridOptions">
<summary>Grid options</summary>
<label for="nCols" data-val="20" data-lib="Cols :" ><input type="range" id="nCols" min="6" max="60" value="20"></label> <br>
<label for="nRows" data-val="12" data-lib="Rows :"><input type="range" id="nRows" min="6" max="60" value="12"></label> <br>
<label for="cellSz" data-val="16" data-lib="size :"><input type="range" id="cellSz" min="6" max="40" value="16" style="width:100px;"></label><small>px</small> <br>
<label for="Points" data-val="24" data-lib="Point(s) :"><input type="range" id="Points" min="1" max="50" value="24" style="width:240px;"></label><br>
<button id="btDrawGrid"> draw grid </button>
</details>
<mark id="cycles">0</mark>
<button id="btGo"> go </button>
<button id="btStop"> stop </button>
<button id="btClear"> clear </button>
<button id="addPoints"> Add Point(s) </button>
<table><tbody id="tbl" class="onBorder"></tbody></table>