./exe/gol/readme.txt
This is an interface into John Conway's "Game of Life". This is probably the most famous of the cellular automatons - basicly games that can be played on a sheet of graph paper, marking certain squares as "on" and others as "off", and the game progressing in steps based on the current position of the board.
This particular game progresses by the following rules:
Any live cell with fewer than two live neighbours dies, as if by underpopulation.
Any live cell with two or three live neighbours lives on to the next generation.
Any live cell with more than three live neighbours dies, as if by overpopulation.
Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
Or, equivalently:
Any live cell with two or three live neighbours survives.
Any dead cell with three live neighbours becomes a live cell.
All other live cells die in the next generation. Similarly, all other dead cells stay dead.
You can play this game yourself on graph paper, but you will find it's rather hard to keep track of what's happening, especially because the entire grid needs to change "at once" each generation, and not square by square. It's much more fun to let a computer, which can do millions of calculations pre seoond, to do the counting for us. Then we can really see the grid come to life.
There is no real goal to the game, other than to search out starting positions that have interesting lives.
this combination, for example:
░░░░░░░░░
░░●░░░░░░
░░●░●●●░░
░░●░░░░░░
░░░░░░░░░
evolves in a very interesting way. Can you find any other interesting patterns? Click on squares to turn them on/off. The counter at the bottom right of the board counts how many generations have passed since you last clicked. You can make a generation pass by pressing the leftmost button, or you can play it as an animation with the play button. The square pauses the game, and the back button returns you to your last set up. The final button clears the screen. You can alwalys just refresh the page to reset it as well.
Additionally, for speed there are also keyboard controls:
[n] next generation
[p] play
[s] stop
[x] clear
[r] reset to last setup
[?] randomize
Oh, and one more important detail: I've altered the game a little here. The trouble is in how to deal with the edges of the board. There are several ways to answer this question: you can treat it as if your board is just a section of an infinite board stretching out in all directions, so if you send out a glider, it will just go on out of the edge and on to infinity. Another way is to treat the squares outside of the board as if they do not exist at all, as if there is a wall around the boundary of the grid. In this universe, a glider will hit the wall and turn into a 2x2 square. There is another way, which is to topologically identify the grid as a torus, by linking the right edge to the left edge, and the top edge to the bottom edge. In this world, a spaceship the leaves the left edge, will fly in seamlessly from the left edge. (like the old asteroids game) This is the way that I have chosen for this current application. I've found that there is more opportunity for life in this kind of universe, though of course this is not quite the "classic game of life" in which you can do crazy things like set up Turing Machines that operate things like digital clocks and even the game of life itself. In order to mod this torroidal universe into that infinite one, you will have to adjust the count neighbors function, and add methods to dynamically resize the grid based on where the action is. This would be a good project.
In this current version, you can change the size of the grid through url parameters, to do this you have to take your mouse and keyboard, and add to the URL of the page : ?rows=100&cols=200 no spaces!. so the URL would look something like:
https://boris-volkov.github.io/games/exe/gol/main.html?rows=100&cols=200
Load the page with this new URL, and the grid will now be 100 rows by 200 columns.
Okay, that's all you need to know. Now have fun with it. As always, the code is below, and you should take a look under the hood to see how all this works.
./exe/gol/main.html
<html>
<head>
<link href="style.css" rel="stylesheet"></style>
</head>
<body>
<div class="control_bar">
<div id="step">next</div>
<div id="play">play</div>
<div id="stop">stop</div>
<div id="reset">reset</div>
<div id="clear">clear</div>
<div id="randomize">random</div>
<div id="gen_display">00000</div>
</div>
<div class="outer">
<canvas id="canvas"></canvas>
</div>
<script src="./game.js"></script>
</body>
</html>
./exe/gol/style.css
body {
cursor: crosshair;
background-color: #123;
}
div.outer{
margin-top: 5px;
display:flex;
flex-direction: column;
justify-content: center;
/*overflow: auto;*/
}
div.control_bar{
margin: auto;
width: 95vw;
position: sticky;
top: 0;
display: flex;
flex-direction: row;
align-content: stretch;
}
.control_bar div {
text-align : center;
background-color: rgb(35,40,50);
flex : 1;
color: #678;
color: coral;
font-family: Courier new;
padding-bottom: 10px;
padding-top: 10px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
opacity: 0.8;
}
.control_bar div:active {
background-color: #358;
}
.control_bar div:hover {
background-color: #245;
}
.control_bar div:first-child{
border-bottom-left-radius: 20px;
}
.control_bar div:last-child{
border-bottom-right-radius: 20px;
}
canvas{
border: 1px solid rgb(35,40,50);
display: block;
margin:auto;
}
./exe/gol/game.js
// calling all the elements from the html document for the bottom control bar.
// there they are just <div>'s but here we turn them into buttons
const step_button = document.querySelector("#step");
const play_button = document.querySelector("#play");
const stop_button = document.querySelector("#stop");
const reset_button = document.querySelector("#reset");
const clear_button = document.querySelector("#clear");
const gen_display = document.querySelector("#gen_display");
const random_button = document.querySelector("#randomize");
let paused = true;// game starts off paused.
let interval = 100; // milliseconds per generation
let generations = 0; // generation counter, resets when adding new pieces
let trail = 1;
// initialize display
const canvas = document.querySelector("#canvas");
let cell_width = 16; // pixels on the display
// get url params for grid size, or set default 64
const urlParams = new URLSearchParams(window.location.search);
var num_rows = parseInt( urlParams.get('rows'));
if (isNaN(num_rows)) { num_rows = 64; }
var num_cols = parseInt( urlParams.get('cols'));
if (isNaN(num_cols)) { num_cols = 64; }
// initialize grids
let grid = new Array(num_rows);
for (let i = 0; i < num_rows; i++){
grid[i] = new Array(num_cols);
for (let j = 0; j < num_cols; j++)
grid[i][j] = new Uint8Array(4).fill(0);
// 4 entries: current, temp, undo, neighbor count
} // storing relevant things closer in memory for performance
// match graphics context to grid size
canvas.height = grid.length * cell_width;
canvas.width = grid[0].length * cell_width;
const c = canvas.getContext("2d");
// this c is important! it is your messenger to the screen.
// it is similar to the turtle in that you give it commands
// to draw things on the screen. Here, rectangles and circles.
// game logic
function next_generation(){
generations++;
print_generations();
if (trail)
clear_transparent();
else
clear();
count_neighbors();
for (let row = 0; row < grid.length; row++){ // make new generation
for (let col = 0; col < grid[0].length; col++){
count = grid[row][col][3];
if (grid[row][col][0] === 1){ // alive
if (count < 2)
grid[row][col][1] = 0;
else if (count <= 3)
grid[row][col][1] = 1;
else
grid[row][col][1] = 0;
} else { // if dead
if (count === 3)
grid[row][col][1] = 1;
else
grid[row][col][1] = 0;
}
}
}
c.fillStyle = "#bfc";
for (let row = 0; row < grid.length; row++){ // write temp to current state
for (let col = 0; col < grid[0].length; col++){
if (grid[row][col][0] = grid[row][col][1] === 1){
// light up the living cells
c.beginPath();
c.arc(Math.round(col*cell_width + cell_width/2),
Math.round(row*cell_width + cell_width/2),
Math.round(cell_width/3), 0, 2*Math.PI, false);
c.fill();
}
grid[row][col][3] = 0; // reset neighbor count for next time
// there's probably a better place to do this
}
}
}
step_button.onclick = next_generation;
function count_neighbors() {
for (let row = 0; row < num_rows; row++){
let up = (row === 0) ? num_rows-1 : row - 1;
let down = (row === num_rows-1) ? 0 : row + 1;
for (let col = 0; col < num_cols; col++){
if (grid[row][col][0] == 0) // only do the work for living cells
continue;
let left = (col === 0) ? num_cols-1 : col - 1;
let right = (col === num_cols-1) ? 0 : col + 1;
grid[up][left][3] ++;
grid[up][right][3] ++;
grid[up][col][3] ++;
grid[row][left][3] ++;
grid[row][right][3] ++;
grid[down][left][3] ++;
grid[down][col][3] ++;
grid[down][right][3]++;
}
}
}
function print_generations(){
gen_display.innerHTML = ('00000'+generations.toString()).slice(-5);
}
// not on the page yet
function print_count(){
live_counter.innerHTML = living;
}
c.strokeStyle = "#123";
c.lineWidth = 4;
function stroke_grid() {
for (let row = 0; row < grid.length; row++){
c.beginPath();
c.moveTo(0, row*cell_width);
c.lineTo(canvas.width, row*cell_width);
c.stroke();
}
for (let col = 0; col < grid[0].length; col++){
c.beginPath();
c.moveTo(col*cell_width, 0);
c.lineTo(col*cell_width, canvas.height);
c.stroke();
}
}
function clear() {
c.fillStyle = "rgba(32,45,55,1)";
c.fillRect(0, 0, canvas.width, canvas.height);
if (num_rows < 256 && num_cols < 256) // for performance
stroke_grid();
}
// the transparent fill is what gives the afterglow effect
// this is suprisingly the main bottleneck for the whole program
// this may be a good candidate for web workers to fill the canvas
// in tiles?
let opacity = 0.7;
function clear_transparent() {
c.fillStyle = "rgba(32,45,55," + opacity + ")";
c.fillRect(0, 0, canvas.width, canvas.height);
if (num_rows < 256 && num_cols < 256) // for performance
stroke_grid();
}
function raise_opacity() {
opacity += 0.05;
if (opacity > 1)
opacity = 1;
}
function lower_opacity() {
opacity -= 0.05;
if (opacity <0)
opacity = 0;
}
// drawing the circles that are alive
function grid_to_canvas() {
c.fillStyle = "#bfc";
for (let row = 0; row < grid.length; row++){
for (let col = 0; col < grid[0].length; col++){
if (grid[row][col][0] === 1){
c.beginPath();
c.arc(Math.round(col*cell_width + cell_width/2),
Math.round(row*cell_width + cell_width/2),
Math.round(cell_width/3), 0, 2*Math.PI, false);
c.fill();
}
}
}
}
// initial condition of the screen
function init(){
c.fillStyle = "#123";
c.fillRect(0,0, canvas.width, canvas.height);
clear();
}
// utility functions
function clear_grid(){
generations = 0;
print_generations();
stop();
for (let row = 0; row < grid.length; row++){
for (let col = 0; col < grid[0].length; col++){
grid[row][col][0]= 0;
}
}
clear();
}
clear_button.onclick = clear_grid;
// state you return to in reset
function save_grid(){
for (let row = 0; row < grid.length; row++){
for (let col = 0; col < grid[0].length; col++){
grid[row][col][2] = grid[row][col][0];
}
}
}
function reset_grid(){
generations = 0;
print_generations();
stop();
for (let row = 0; row < grid.length; row++){
for (let col = 0; col < grid[0].length; col++){
// write undo slot into the current gen slot
grid[row][col][0] = grid[row][col][2];
}
}
clear();
grid_to_canvas();
}
reset_button.onclick = reset_grid;
function randomize(){
stop();
generations = 0;
print_generations();
for (let row = 0; row < grid.length; row++){
for (let col = 0; col < grid[0].length; col++){
if (Math.random() < 0.2)
grid[row][col][0] = 1;
else
grid[row][col][0] = 0;
}
}
clear();
grid_to_canvas();
save_grid();
}
random_button.onclick = randomize;
let id;
function stop(){
if (!paused)
clearInterval(id);
paused = true;
}
stop_button.onclick = stop;
function play(){
if (paused)
id = setInterval(next_generation, interval);
paused = false;
}
play_button.onclick = play;
function faster(){
if (interval <= 5)// 200 fps max?
return;// not sure what the limit should be yet
interval -= 5;
if (paused)
return;
stop();
play();
}
function slower(){
interval += 5;
if (paused)
return;
stop();
play();
}
document.onkeypress = (e) => {
if (e.key === 's'){
stop();
}
if (e.key === 'p'){
play();
}
if (e.key === 'n'){
next_generation();
}
if (e.key === 'x'){
clear_grid();
}
if (e.key === 'r'){
reset_grid();
}
if (e.key === '+'){
faster();
}
if (e.key === '-'){
slower();
}
if (e.key === '?'){
randomize();
}
if (e.key === '['){
raise_opacity();
}
if (e.key === ']'){
lower_opacity();
}
if (e.key === 't'){
trail ^= 1; // toggle 0 <-> 1
}
grid_to_canvas();
}
canvas.onclick = (event) => {
generations = 0;
print_generations();
stop();
bb = canvas.getBoundingClientRect();
let x = (event.clientX-bb.left)*(canvas.width/bb.width);
let y = (event.clientY-bb.top)*(canvas.height/bb.height);
let col = Math.floor(x/cell_width);
let row = Math.floor(y/cell_width);
grid[row][col][0] ^= 1;
save_grid();
clear();
grid_to_canvas();
}
init();