├── requirements.txt ├── .gitignore ├── run.sh ├── screenshot.png ├── animated-screenshot.gif ├── style.css ├── deploy.sh ├── .gcloudignore ├── index.html ├── LICENSE ├── sketch.js ├── gol.sol ├── README.md └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask===1.0.2 2 | web3==5.17.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | env.yaml 3 | __pycache__/* 4 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | set -x 2 | FLASK_ENV=development FLASK_APP=main flask run -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nialloc/GameOfLife/HEAD/screenshot.png -------------------------------------------------------------------------------- /animated-screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nialloc/GameOfLife/HEAD/animated-screenshot.gif -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | canvas { 6 | display: block; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | gcloud functions deploy gameoflife \ 2 | --runtime python38 \ 3 | --trigger-http \ 4 | --allow-unauthenticated \ 5 | --env-vars-file env.yaml -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | run.sh 2 | deploy.sh 3 | .env 4 | gol.sol 5 | index.html 6 | LICENSE 7 | README.md 8 | run.sh 9 | screenshot.png 10 | sketch.js 11 | style.css 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 nialloc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /sketch.js: -------------------------------------------------------------------------------- 1 | let w 2 | let columns 3 | let rows 4 | let board 5 | let button 6 | let timer = 3 7 | 8 | 9 | let gdata 10 | 11 | // using ngrok for testing purposes 12 | //let server_name = 'https://76a64b6ad96e.ngrok.io' 13 | server_name = 'https://us-central1-crypto-174821.cloudfunctions.net/gameoflife' 14 | 15 | function setup() { 16 | createCanvas(800, 640); 17 | w = 12; // width of cells in pixels 18 | columns = 32 19 | rows = 32 20 | 21 | board = new Array(columns); 22 | for (let i = 0; i < columns; i++) { 23 | board[i] = new Array(rows); 24 | } 25 | for (let i = 0; i < rows; i++) { 26 | for (let j = 0; j < columns; j++) { 27 | board[i][j] = 0 28 | } 29 | } 30 | 31 | button_start = createButton('start new pattern'); 32 | button_start.position(400, 0); 33 | button_start.mousePressed(function() { 34 | // send cell pattern to server 35 | send_cells() 36 | }); 37 | 38 | 39 | button_getdata = createButton('get data') 40 | button_getdata.position(400,30) 41 | button_getdata.mousePressed(function(){ 42 | request_data() 43 | }) 44 | 45 | 46 | 47 | button_random = createButton('Random'); 48 | button_random.position(400, 120); 49 | button_random.mousePressed(function() { 50 | for (let i = 0; i < rows; i++) { 51 | for (let j = 0; j < columns; j++) { 52 | board[i][j] = random([0, 1]) 53 | } 54 | } 55 | 56 | 57 | }); 58 | 59 | button_clear = createButton('Clear'); 60 | button_clear.position(400, 150); 61 | button_clear.mousePressed(function() { 62 | console.log('button_clear') 63 | clear_board() 64 | }) 65 | 66 | button_gliders = createButton('Gliders') 67 | button_gliders.position(400, 180) 68 | button_gliders.mousePressed(function() { 69 | 70 | clear_board() 71 | for (let i = 5; i < 20; i += 4) { 72 | board[i + 1][i + 1] = 1 73 | board[i + 2][i + 2] = 1 74 | board[i + 3][i + 2] = 1 75 | board[i + 1][i + 3] = 1 76 | board[i + 2][i + 3] = 1 77 | } 78 | 79 | }) 80 | 81 | 82 | setInterval(timerGetData, 30 * 1000); 83 | setInterval(timerStep, 60 * 1000); 84 | 85 | loadJSON(server_name + '/data', cbData, 'json') 86 | 87 | } 88 | 89 | function timerGetData() { 90 | loadJSON(server_name + '/data', cbData, 'json'); 91 | } 92 | function timerStep() { 93 | loadJSON(server_name + '/step', cbStep, 'json'); 94 | } 95 | 96 | function request_data(){ 97 | loadJSON(server_name + '/data', cbData, 'json') 98 | } 99 | 100 | function send_cells() { 101 | let cells = [] 102 | for (let i = 0; i < rows; i++) { 103 | for (let j = 0; j < columns; j++) { 104 | cells.push(board[i][j]) 105 | } 106 | } 107 | 108 | httpPost(server_name + '/setcells', 109 | "json", { 110 | 'cells': cells 111 | },cbData) 112 | 113 | } 114 | 115 | 116 | function clear_board() { 117 | for (let i = 0; i < rows; i++) { 118 | for (let j = 0; j < columns; j++) { 119 | board[i][j] = 0 120 | } 121 | } 122 | } 123 | 124 | 125 | 126 | function cbStep(data){ 127 | console.log('cbStep',data) 128 | loadJSON(server_name + '/data', cbData, 'json'); 129 | } 130 | 131 | function cbSetCellsError(response) { 132 | console.log("cbSetCellsError", response) 133 | } 134 | 135 | function draw() { 136 | background(255); 137 | 138 | for (let i = 0; i < rows; i++) { 139 | for (let j = 0; j < columns; j++) { 140 | if ((board[i][j] == 1)) { 141 | fill(0); 142 | } else { 143 | fill(255); 144 | } 145 | stroke(0); 146 | rect(i * w, j * w, w - 1, w - 1); 147 | } 148 | } 149 | 150 | 151 | fill(color(0, 0, 255)); 152 | textSize(16) 153 | let posx = 400 154 | let posy = 220 155 | if (gdata) { 156 | text(`Network: ${gdata.network}`,posx,posy) 157 | 158 | s = `Sender Address: ${gdata.caller_address} 159 | Balance ${gdata.caller_balance} eth 160 | Latest Block ${gdata.block} 161 | Last Transaction Block ${gdata.myblock} 162 | Next Step won't be until at least Block ${gdata.target}` 163 | text(s,posx,posy+20) 164 | 165 | } 166 | 167 | text(`status: ${status}`,10,400) 168 | 169 | } 170 | 171 | // allow user to select their own cells.. 172 | function mousePressed(event) { 173 | 174 | let i = int(mouseX / w) 175 | let j = int(mouseY / w) 176 | if (i >= 0 && i < rows && j >= 0 && j < columns) { 177 | if (board[i][j]) { 178 | board[i][j] = 0 179 | } else { 180 | board[i][j] = 1 181 | } 182 | } 183 | 184 | } 185 | 186 | function cbData(data) { 187 | 188 | console.log(data) 189 | 190 | if (data.cells) { 191 | let cells = data.cells 192 | 193 | let index = 0 194 | for (let i = 0; i < rows; i++) { 195 | for (let j = 0; j < columns; j++) { 196 | board[i][j] = cells[index++] 197 | 198 | } 199 | } 200 | } 201 | 202 | // store this is a global variable 203 | gdata = data 204 | 205 | if (data.status) { 206 | status = data.status 207 | } 208 | 209 | } -------------------------------------------------------------------------------- /gol.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.4; 3 | 4 | contract GameOfLife { 5 | int16 constant rows = 32; 6 | int16 constant cols = 32; 7 | uint256[] public cells; 8 | uint256[] newcells; 9 | int public iterations; 10 | uint public myblock; 11 | 12 | event log(string text); 13 | event Cells(uint256[] cells); 14 | 15 | constructor () { 16 | // a bit for each cell - stored in an array of 256 bit numbers 17 | // for default of 32 x 32 this is 4 x 256 bit numbers 18 | cells = new uint256[](uint16(rows * cols) / 256); 19 | 20 | newcells = cells; 21 | 22 | // set up a few 'sliders' 23 | for (int16 i=0; i<20; i +=6) { 24 | int16 pos; 25 | pos = ((i+1) + (i+1)*cols) ; 26 | set(pos,1); 27 | pos = ((i+2) + (i+2)*cols) ; 28 | set(pos,1); 29 | pos = ((i+3) + (i+2)*cols) ; 30 | set(pos,1); 31 | pos = ((i+1) + (i+3)*cols) ; 32 | set(pos,1); 33 | pos = ((i+2) + (i+3)*cols) ; 34 | set(pos,1); 35 | } 36 | 37 | cells = newcells; 38 | } 39 | 40 | 41 | // get for a particular row/col return the cell state (1 alive, 0 dead) 42 | function get(int32 pos) view internal returns (int) { 43 | 44 | if (pos<0) { 45 | pos += rows*cols; 46 | } 47 | if (pos >= rows*cols) { 48 | pos -= rows*cols; 49 | } 50 | // make sure pos always is a valid value 51 | pos = pos % int32(rows*cols); 52 | // the linear array is held in 4 x 256 unsigned integers 53 | uint32 i = uint32(pos) / 256; 54 | uint32 j = uint32(pos) % 256; 55 | // if the j-th bit set? 56 | if ((cells[i] >> j) & 0x01 == 1 ) { 57 | return 1; 58 | } else { 59 | return 0; 60 | } 61 | } 62 | function set(int16 pos, int8 value) internal { 63 | // the linear array is held in 4 x 256 unsigned integers 64 | uint32 i = uint32(pos) / 256; 65 | uint32 j = uint32(pos) % 256; 66 | // if value is 1 then set the j-th bit 67 | if (value>0){ 68 | newcells[i] |= (1 << j); // set this bit 69 | } else { 70 | newcells[i] &= ~(1 << j); // turn off this bit 71 | } 72 | 73 | } 74 | 75 | // Overpopulation: if a living cell is surrounded by more than three living cells, it dies. 76 | // Stasis: if a living cell is surrounded by two or three living cells, it survives. 77 | // Underpopulation: if a living cell is surrounded by fewer than two living cells, it dies. 78 | // Reproduction: if a dead cell is surrounded by exactly three cells, it becomes a live cell. 79 | 80 | // step - perform a single step of the Conway's game of life 81 | function step() public { 82 | 83 | newcells = new uint256[](cells.length); 84 | // skip the edge row/col to avoid checking for out of bounds 85 | // skip some more to reduce the amount of gas needed. 86 | for (int16 row=4; row
7 |
8 | This application would be able to run 'forever' - so long as there was some funds in an Ethereum account that could be used to run each 'step'.
9 |
10 | However, the cost of Ethereum (and therefore 'gas') used to run smart contracts is so high that it would cost (in March 2021) over $80 just to register the smart contract, and to run a single 'step' of the game would cost over $2,000! No doubt that the code could be made more efficient and consume less resources, but hey that's just too much work for a concept app, so I have simply registered the [contract](https://kovan.etherscan.io/address/0x51B92cef4C0847EF552e4129a28d817c26a4A053) on the Kovan test network instead, and use some 'fake' Ethereum to run the system.
11 | The app is the same, but it just points to the 'Kovan' test network instead of the Ethereum mainnet.
12 |
13 | You can see it in action [here](https://nialloc.github.io/GameOfLife/)
14 |
15 | The application consists of three parts:
16 | * Front end Javascript application using the [p5js](https://p5js.org/) library which runs in your browser
17 | * A Python Flask app implemented as a google cloud function
18 | * An Ethereum smart contract written in Solidity which runs on the (Kovan) Ethereum blockchain
19 |
20 | ### p5js - client application
21 | There are just three files in the app: a very simple `index.html` to host the javascript application in [sketch.js](https://nialloc.github.io/GameOfLife/sketch.js) along with a simple `style.css` stylesheet. When started, the app requests the 32x32 grid from the blockchain (via the flask app). Every minute or so the app will trigger a 'step' in the smart contract. There are a couple of other buttons that will create a random selection on the screen, clear the screen, and add a few [gliders](https://en.wikipedia.org/wiki/Glider_(Conway%27s_Life)). You can also use the mouse to select/deselect individual cells. Press the `start new pattern` button to send your pattern to the smart contract.
22 |
23 | ### Python Flask google cloud function
24 | This started as a bog standard [Flask](https://flask.palletsprojects.com/en/1.1.x/) application, but I converted it into a google cloud function to avoid the hassle of having to host it somewhere.
25 | Most of the functionality here could have also been included in the browser-based javascript application, but because of my greater familiarity with python and my uncertainty about how to secure the private key needed to sign the solidity transactions I left it running on the server side.
26 | The ```main.py``` needs some environment variables for it to work. These are configured as part of the deployment script when pushing the flask app to the google cloud service:
27 | ```
28 | gcloud functions deploy gameoflife \
29 | --runtime python38 \
30 | --trigger-http \
31 | --allow-unauthenticated \
32 | --env-vars-file env.yaml
33 | ```
34 | ```
35 | env.yaml:
36 | ---
37 | network_name: Kovan
38 | HTTPProvider: 'https://kovan.infura.io/v3/PUT-YOUR-INFURA-KEY-HERE'
39 | contract_address: '0x51B92cef4C0847EF552e4129a28d817c26a4A053'
40 | private_key: 'PUT-THE-PRIVATE-KEY-OF-YOUR-ACCOUNT-HERE'
41 | chain_id: '42'
42 | ```
43 | ### Ethereum smart contract
44 | The smart contract was written in Solidity. I used VS Code with a [solidity extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity) that highlighted any syntax errors.
45 | The testing of the contract was done with the [Truffle/Ganache](https://www.trufflesuite.com/ganache) suite of applications, and to get it onto the blockchain I simply used the [remix](http://remix.ethereum.org) online tool with the [metamask](https://metamask.io/) browser extension.
46 |
47 |
48 | I decided that a 32 x 32 cell structure would be big enough the showcase how the game works. In order to reduce the size requirements of data to be stored on the block chain, I used an array of 4 x 256 bit unsigned integers and used this as a bit field. There are three entry points in the contract (apart from the constructor): setCells(), getCells() and step()
49 |
50 |
51 | #### Step
52 | The step function mainly consists of loop iterating through the cells, creating/removing cells according to the rules of the game.
53 | I didn't make much effort to reduce the amount of work done in order to run a single cell, so I did run foul of one specific issue - the amount of gas consumed. At times it would exceed the limits of even the test networks, and so I ignored some of the cells on the edge of the whole cell universe.
54 | It would then take about 11M gas to process it, which is just below the 12M limit for the Kovan network.
55 |
56 | ```
57 | for (int16 row=4; row