├── .github └── workflows │ └── docs.yml ├── .gitignore ├── README.md ├── cmd ├── gojs │ └── main.go └── golcli │ └── main.go ├── docs ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ ├── wasm_exec.d.ts │ └── wasm_exec.js └── tsconfig.json ├── go.mod ├── go.sum └── pkg └── gol ├── gameoflife.go ├── lattice.go ├── topology.go └── topology_test.go /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Github Page 2 | 3 | # Only run if docs/ has changed 4 | on: 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - docs/** 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | deploy-gh-page: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | path: docs 23 | - name: Deploy Github Page 24 | run: npm run deploy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary 9 | *.test 10 | cmd/golcli/golcli 11 | cmd/gojs/main.wasm 12 | docs/public/main.wasm 13 | 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | .idea/ 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go-L 2 | Game of Life simulation 3 | 4 | I originally wrote a Game of Life model in C, [GOL](https://github.com/Jeadie/GOL/). But I wanted to convert it into Golang for a few reasons: 5 | - Learn Go 6 | - Consider more novel topologies based on border rules (see #border-topologies). 7 | - Extend GOL simulation for generic update rules. 8 | - Extend GOL lattice for integer values (i.e. not just 1/0) 9 | - Run physical system simulations based on applications of cellular automaton: 10 | - Computational fluid dynamics 11 | - Population dynamics 12 | - Ising Models 13 | - Other interesting cellular automaton: Rule 90, Langton's ant 14 | - Boolean binary logic rules (i.e. consider the 1 cell + 4 neighbours as a 5 bit input - 1 bit output to a logic circuit) 15 | - Discretise and extend to 2D my previous research in [rho signalling in cell-cell junctions during collective cell migration](https://github.com/Jeadie/UQ-Winter-Research-Project-2017) 16 | 17 | 18 | ## Usage 19 | Current usage simply displays the simulation onto the terminal. Usage: 20 | ```bash 21 | > Go-L --help 22 | Usage of Go-L: 23 | -aliveratio float 24 | The fraction of squares that start as alive, assigned at random. Domain: [0.0, 1.0]. (default 0.8) 25 | -gridsize uint 26 | Length of square grid to define game on. (default 20) 27 | -iterations uint 28 | Max number of iterations to simulate game of life. If stable solution, will exit early. (default 100) 29 | -topology string 30 | Specify the topology of the grid (as a fundamental topology from a parallelograms). Valid parameters: BORDERED, TORUS, KLEIN_BOTTLE, PROJECTIVE_PLANE, SPHERE. (default "BORDERED") 31 | -updatedelay uint 32 | Additional period delay between updating rounds of the game, in milliseconds. Does not take into account processing time. (default 200) 33 | -updaterule uint 34 | Specify the number associated with the update rule to use. Default to Conway's Game of Life. (default 1994975360) 35 | ``` 36 | 37 | ### Border Topologies 38 | In redefining how the border conditions work, we can simulate GOL as if it was played on a variety of manifolds. This is most clearly seen when looking at the fundamental polygons derived from a square (or parallelogram). When considering the neighbours of a cell on the border of the lattice, a fundamental polygon helps show where the neighbouring values should be. 39 | | Sphere | Real Projective Plane | Klein Bottle | Torus | 40 | | -- | -- | -- | -- | 41 | | ![Sphere](https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/SphereAsSquare.svg/240px-SphereAsSquare.svg.png)| ![Real Projective Plane](https://upload.wikimedia.org/wikipedia/commons/thumb/9/9b/ProjectivePlaneAsSquare.svg/240px-ProjectivePlaneAsSquare.svg.png)| ![Klein Bottle](https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/KleinBottleAsSquare.svg/240px-KleinBottleAsSquare.svg.png)| ![Torus](https://upload.wikimedia.org/wikipedia/commons/thumb/f/f2/TorusAsSquare.svg/240px-TorusAsSquare.svg.png) | 42 | 43 | 44 | Consider a square lattice of size 5, coordinates indexed in $[0,4] \times [0,4]$. For a standard coordinate, say $(2,2)$, its neighbours are: ${(1,2),(3,2),(2,1),(2,3)}$. These are all within standard bounds. Now consider the point $(4,2)$ with neighbours: ${(3,2),(5,2),(4,1),(4,3)}$. What value should we use for $(5,2)$? The topology dictates how lattice-border neighbours get selected. For a bordered topology, there is nothing outside of lattice, therefore index it the null value (or 0 in GOL rules). For a sphere, the $(5,2)$ becomes $(2,0)$ given its equivalence relation (on an $n$ square lattice): 45 | 46 | $$ 47 | \displaylines{ 48 | (x, 0) \backsim (n, n-x), \quad x\in [0, n] \\ 49 | (x, n) \backsim (0, n-x), \quad x\in [0, n] 50 | } 51 | $$ 52 | 53 | Or for a torus 54 | 55 | $$ 56 | \displaylines{ 57 | (x, 0) \backsim (x, n), \quad x\in [0, n] \\ 58 | (0, y) \backsim (n, y), \quad y\in [0, n] 59 | } 60 | $$ 61 | 62 | or a real projective plane 63 | 64 | $$ 65 | \displaylines{ 66 | (0, y) \backsim (n, n-y), \quad y\in [0, n] \\ 67 | (x, 0) \backsim (n-x, n), \quad x\in [0, n] 68 | } 69 | $$ 70 | 71 | and lastly a klein bottle 72 | 73 | $$ 74 | \displaylines{ 75 | (0, y) \backsim (n, y), \quad y\in [0, n] \\ 76 | (x, 0) \backsim (n-x, n), \quad x\in [0, n] 77 | } 78 | $$ 79 | 80 | For update rules that consider 2nd degree neighbours (i.e. $(6,2)$ ), the mapping gets a bit more complicated. 81 | 82 | 83 | ### More on Game of Life 84 | A cellular automaton designed by mathematician John Conway showing how complex emergent behaviour can arise from simple 85 | rules. In his case, at each future timestamp, a cell will be updated, based on its four neighbouring cells: 86 | 1. A live cell will survive if only 2 or 3 of its neighbours are alive 87 | 2. A dead cell with 3 alive neighbours will become alive 88 | 3. All other cells die. 89 | 90 | ### Update Rules 91 | Conway's Game of Life is but one 2D cellular automata that depends only on its 4 direct neighbours. One can conceive of 92 | other update rules. If one considers the five relevant cells: left, cell, right, up & down, there are then $2^5=32$ possible states to consider. An update rule can be defined as follows. 93 | 1. Define an ordered set on the 32 possible states $ \{ s_i \}_{i=0}^{32} $ 94 | 2. Create a 32 digit binary number, $B$, where $B_i = 1$ iff the update rule maps the cell with state $S_i$ to 1. 95 | 3. All update rules can be then indexed from this, $U_B : \{0,1\}^5 \to \{ 0, 1\}$ 96 | 97 | #### Update Rules: 1D example 98 | Consider a simple 1D case of: left, cell, right. There are 8 states with a natural indexing: $111, 110, 101, 100, 011, 010, 001, 000$. An example update rule $U_{177}$ updates the cell to 1 in the following cases: 111, 101, 100, 000 (all other cases to 0). With the binary expansion $177 = 10110001b$, this can be expressed simply below: 99 | 100 | | 111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 | 101 | | --- | --- | --- | --- | --- | --- | --- | --- | 102 | | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 103 | 104 | #### 2D Implementation 105 | In this repo, we consider a similar binary representation for cells: left, cell, right, up, down and a natural indexing $11111, 11110, 11101, ..., 00001, 00000$. This creates $2^5=32$ possible states and therefore $2^{32} = 4294967296$ possible update rules (conveniently fitting in a 32-bit integer). 106 | 107 | We can now consider the update rule in Conway's Game of Life: 108 | 1. Alive and 3 neighbours: 01111, 10111, 11101, 11110 ([15, 23, 29, 30]) 109 | 2. Alive and 2 neighbours: 00111, 01101, 01110, 10101, 10110, 11100 ([7, 13, 14, 21, 22, 28]) 110 | 3. Dead and 3 neighbours: 01011, 10011, 11001, 11010 ([11, 19, 25, 26]) 111 | 112 | Which gives a binary number with 1's at positions: [7, 11, 13, 14, 15, 19, 21, 22, 23, 25, 26, 28, 29, 30] 113 | or in binary: 01110110111010001110100010000000 114 | or in base 10: 1994975360 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /cmd/gojs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Jeadie/Go-L/pkg/gol" 6 | //"github.com/teamortix/golang-wasm/wasm" 7 | ) 8 | 9 | // Run a cellular automaton simulation. Return type is mapping of iteration index to grid values. 10 | func Run(params gol.InputParameters) map[int][][]uint { 11 | 12 | l := gol.ConstructUintLatticeFromInput(params) 13 | output := make(map[int][][]uint, params.Iterations) 14 | 15 | for i := uint(0); i < params.Iterations; i++ { 16 | output[int(i)] = l.MakeGrid() 17 | if !l.SingleIteration() {break} 18 | } 19 | return output 20 | } 21 | 22 | // This exports an add function. 23 | // It takes in two 32-bit integer values 24 | // And returns a 32-bit integer value. 25 | // To make this function callable from JavaScript, 26 | // we need to add the: "export add" comment above the function 27 | //export add 28 | func add(x int, y int) int { 29 | return x + y 30 | } 31 | 32 | 33 | func main() { 34 | //wasm.Expose("run", Run) 35 | //wasm.Ready() 36 | fmt.Println("Hello browser, I am from Golang world!") 37 | } 38 | -------------------------------------------------------------------------------- /cmd/golcli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "github.com/Jeadie/Go-L/pkg/gol" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type LatticeProcessor func(*gol.Lattice[uint]) error 13 | 14 | func parseArguments() (*gol.InputParameters, error) { 15 | params := gol.InputParameters{} 16 | 17 | flag.UintVar(¶ms.Iterations, "iterations", 100, "Max number of iterations to simulate game of life. If stable solution, will exit early.") 18 | flag.UintVar(¶ms.GridSize, "gridsize", 20, "Length of square grid to define game on.") 19 | flag.Float64Var(¶ms.AliveRatio, "aliveratio", 0.8, "The fraction of squares that start as alive, assigned at random. Domain: [0.0, 1.0].") 20 | flag.UintVar(¶ms.UpdateDelay, "updatedelay", 200, "Additional period delay between updating rounds of the game, in milliseconds. Does not take into account processing time.") 21 | flag.StringVar(¶ms.Topology, "topology", gol.DefaultTopology, "Specify the topology of the grid (as a fundamental topology from a parallelograms). Valid parameters: BORDERED, TORUS, KLEIN_BOTTLE, PROJECTIVE_PLANE, SPHERE.") 22 | flag.UintVar(¶ms.UpdateFunctionNumber, "updaterule", gol.ConwaysGameOfLifeUpdateRuleNumber, "Specify the number associated with the update rule to use. Default to Conway's Game of Life.") 23 | flag.Parse() 24 | 25 | if gol.IsValidTopology(params.Topology) { 26 | return ¶ms, nil 27 | } 28 | return nil, errors.New(fmt.Sprintf("Invalid topology specified %s. Topology must be one of %s", params.Topology, gol.ALLOWED_TOPOLOGIES)) 29 | } 30 | 31 | func main() { 32 | // Parse cli args 33 | params, err := parseArguments() 34 | if err != nil { 35 | fmt.Println(err) 36 | return 37 | } 38 | 39 | // Setup Lattice 40 | l := gol.ConstructUintLatticeFromInput(*params) 41 | 42 | processors := []LatticeProcessor{Print} 43 | 44 | fmt.Print("\033[H\033[2J") 45 | for i := uint(0); i < params.Iterations; i++ { 46 | if !l.SingleIteration() {break} 47 | runProcessors(l, processors) 48 | } 49 | } 50 | 51 | func Print(l *gol.Lattice[uint]) error { 52 | for i := 0; i 0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "gh-pages": "^4.0.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeadie/Go-L/60e4d9bb249fa2583ef48d0b38ceaefa3b88ddf7/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /docs/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeadie/Go-L/60e4d9bb249fa2583ef48d0b38ceaefa3b88ddf7/docs/public/logo192.png -------------------------------------------------------------------------------- /docs/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jeadie/Go-L/60e4d9bb249fa2583ef48d0b38ceaefa3b88ddf7/docs/public/logo512.png -------------------------------------------------------------------------------- /docs/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /docs/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | import Go from "./wasm_exec" 5 | 6 | function App() { 7 | 8 | const go = new Go(); 9 | 10 | const wasmBrowserInstantiate = async (wasmModuleUrl: string, importObject) => { 11 | let response = undefined; 12 | 13 | // Check if the browser supports streaming instantiation 14 | if (WebAssembly.instantiateStreaming) { 15 | // Fetch the module, and instantiate it as it is downloading 16 | response = await WebAssembly.instantiateStreaming( 17 | fetch(wasmModuleUrl), 18 | importObject 19 | ); 20 | } else { 21 | // Fallback to using fetch to download the entire module 22 | // And then instantiate the module 23 | const fetchAndInstantiateTask = async () => { 24 | const wasmArrayBuffer = await fetch(wasmModuleUrl).then(response => 25 | response.arrayBuffer() 26 | ); 27 | return WebAssembly.instantiate(wasmArrayBuffer, importObject); 28 | }; 29 | 30 | response = await fetchAndInstantiateTask(); 31 | } 32 | 33 | return response; 34 | }; 35 | 36 | const runWasmAdd = async (x: number, y: number) => { 37 | // Get the importObject from the go instance. 38 | const importObject = go.importObject; 39 | 40 | // Instantiate our wasm module 41 | const wasmModule = await wasmBrowserInstantiate("../main.wasm", importObject); 42 | 43 | // Allow the wasm_exec go instance, bootstrap and execute our wasm module 44 | go.run(wasmModule.instance); 45 | 46 | // Call the Add function export from wasm, save the result 47 | const addResult = wasmModule.instance.exports.add(x, y); 48 | 49 | // Set the result onto the body 50 | document.body.textContent = `Hello World! addResult: ${addResult}`; 51 | }; 52 | 53 | runWasmAdd(24, 53); 54 | runWasmAdd(24, 3); 55 | runWasmAdd(4, 3); 56 | 57 | return ( 58 |
59 |
60 | logo 61 |

62 | Edit src/App.tsx and save to reload. 63 |

64 | 70 | Learn React 71 | 72 |
73 |
74 | ); 75 | } 76 | 77 | export default App; 78 | -------------------------------------------------------------------------------- /docs/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /docs/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /docs/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /docs/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /docs/src/wasm_exec.d.ts: -------------------------------------------------------------------------------- 1 | // wasm_exec.d.ts 2 | 3 | declare module "wasm_exec" { 4 | export default interface Go { 5 | go?: () => number | undefined; 6 | } 7 | } 8 | export default interface Go { 9 | go?: () => number | undefined; 10 | } 11 | -------------------------------------------------------------------------------- /docs/src/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | "use strict"; 6 | 7 | (() => { 8 | const enosys = () => { 9 | const err = new Error("not implemented"); 10 | err.code = "ENOSYS"; 11 | return err; 12 | }; 13 | 14 | if (!globalThis.fs) { 15 | let outputBuf = ""; 16 | globalThis.fs = { 17 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 18 | writeSync(fd, buf) { 19 | outputBuf += decoder.decode(buf); 20 | const nl = outputBuf.lastIndexOf("\n"); 21 | if (nl != -1) { 22 | console.log(outputBuf.substr(0, nl)); 23 | outputBuf = outputBuf.substr(nl + 1); 24 | } 25 | return buf.length; 26 | }, 27 | write(fd, buf, offset, length, position, callback) { 28 | if (offset !== 0 || length !== buf.length || position !== null) { 29 | callback(enosys()); 30 | return; 31 | } 32 | const n = this.writeSync(fd, buf); 33 | callback(null, n); 34 | }, 35 | chmod(path, mode, callback) { callback(enosys()); }, 36 | chown(path, uid, gid, callback) { callback(enosys()); }, 37 | close(fd, callback) { callback(enosys()); }, 38 | fchmod(fd, mode, callback) { callback(enosys()); }, 39 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 40 | fstat(fd, callback) { callback(enosys()); }, 41 | fsync(fd, callback) { callback(null); }, 42 | ftruncate(fd, length, callback) { callback(enosys()); }, 43 | lchown(path, uid, gid, callback) { callback(enosys()); }, 44 | link(path, link, callback) { callback(enosys()); }, 45 | lstat(path, callback) { callback(enosys()); }, 46 | mkdir(path, perm, callback) { callback(enosys()); }, 47 | open(path, flags, mode, callback) { callback(enosys()); }, 48 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 49 | readdir(path, callback) { callback(enosys()); }, 50 | readlink(path, callback) { callback(enosys()); }, 51 | rename(from, to, callback) { callback(enosys()); }, 52 | rmdir(path, callback) { callback(enosys()); }, 53 | stat(path, callback) { callback(enosys()); }, 54 | symlink(path, link, callback) { callback(enosys()); }, 55 | truncate(path, length, callback) { callback(enosys()); }, 56 | unlink(path, callback) { callback(enosys()); }, 57 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 58 | }; 59 | } 60 | 61 | if (!globalThis.process) { 62 | globalThis.process = { 63 | getuid() { return -1; }, 64 | getgid() { return -1; }, 65 | geteuid() { return -1; }, 66 | getegid() { return -1; }, 67 | getgroups() { throw enosys(); }, 68 | pid: -1, 69 | ppid: -1, 70 | umask() { throw enosys(); }, 71 | cwd() { throw enosys(); }, 72 | chdir() { throw enosys(); }, 73 | } 74 | } 75 | 76 | if (!globalThis.crypto) { 77 | throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); 78 | } 79 | 80 | if (!globalThis.performance) { 81 | throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); 82 | } 83 | 84 | if (!globalThis.TextEncoder) { 85 | throw new Error("globalThis.TextEncoder is not available, polyfill required"); 86 | } 87 | 88 | if (!globalThis.TextDecoder) { 89 | throw new Error("globalThis.TextDecoder is not available, polyfill required"); 90 | } 91 | 92 | const encoder = new TextEncoder("utf-8"); 93 | const decoder = new TextDecoder("utf-8"); 94 | 95 | globalThis.Go = class { 96 | constructor() { 97 | this.argv = ["js"]; 98 | this.env = {}; 99 | this.exit = (code) => { 100 | if (code !== 0) { 101 | console.warn("exit code:", code); 102 | } 103 | }; 104 | this._exitPromise = new Promise((resolve) => { 105 | this._resolveExitPromise = resolve; 106 | }); 107 | this._pendingEvent = null; 108 | this._scheduledTimeouts = new Map(); 109 | this._nextCallbackTimeoutID = 1; 110 | 111 | const setInt64 = (addr, v) => { 112 | this.mem.setUint32(addr + 0, v, true); 113 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 114 | } 115 | 116 | const getInt64 = (addr) => { 117 | const low = this.mem.getUint32(addr + 0, true); 118 | const high = this.mem.getInt32(addr + 4, true); 119 | return low + high * 4294967296; 120 | } 121 | 122 | const loadValue = (addr) => { 123 | const f = this.mem.getFloat64(addr, true); 124 | if (f === 0) { 125 | return undefined; 126 | } 127 | if (!isNaN(f)) { 128 | return f; 129 | } 130 | 131 | const id = this.mem.getUint32(addr, true); 132 | return this._values[id]; 133 | } 134 | 135 | const storeValue = (addr, v) => { 136 | const nanHead = 0x7FF80000; 137 | 138 | if (typeof v === "number" && v !== 0) { 139 | if (isNaN(v)) { 140 | this.mem.setUint32(addr + 4, nanHead, true); 141 | this.mem.setUint32(addr, 0, true); 142 | return; 143 | } 144 | this.mem.setFloat64(addr, v, true); 145 | return; 146 | } 147 | 148 | if (v === undefined) { 149 | this.mem.setFloat64(addr, 0, true); 150 | return; 151 | } 152 | 153 | let id = this._ids.get(v); 154 | if (id === undefined) { 155 | id = this._idPool.pop(); 156 | if (id === undefined) { 157 | id = this._values.length; 158 | } 159 | this._values[id] = v; 160 | this._goRefCounts[id] = 0; 161 | this._ids.set(v, id); 162 | } 163 | this._goRefCounts[id]++; 164 | let typeFlag = 0; 165 | switch (typeof v) { 166 | case "object": 167 | if (v !== null) { 168 | typeFlag = 1; 169 | } 170 | break; 171 | case "string": 172 | typeFlag = 2; 173 | break; 174 | case "symbol": 175 | typeFlag = 3; 176 | break; 177 | case "function": 178 | typeFlag = 4; 179 | break; 180 | } 181 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 182 | this.mem.setUint32(addr, id, true); 183 | } 184 | 185 | const loadSlice = (addr) => { 186 | const array = getInt64(addr + 0); 187 | const len = getInt64(addr + 8); 188 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 189 | } 190 | 191 | const loadSliceOfValues = (addr) => { 192 | const array = getInt64(addr + 0); 193 | const len = getInt64(addr + 8); 194 | const a = new Array(len); 195 | for (let i = 0; i < len; i++) { 196 | a[i] = loadValue(array + i * 8); 197 | } 198 | return a; 199 | } 200 | 201 | const loadString = (addr) => { 202 | const saddr = getInt64(addr + 0); 203 | const len = getInt64(addr + 8); 204 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 205 | } 206 | 207 | const timeOrigin = Date.now() - performance.now(); 208 | this.importObject = { 209 | go: { 210 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 211 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 212 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 213 | // This changes the SP, thus we have to update the SP used by the imported function. 214 | 215 | // func wasmExit(code int32) 216 | "runtime.wasmExit": (sp) => { 217 | sp >>>= 0; 218 | const code = this.mem.getInt32(sp + 8, true); 219 | this.exited = true; 220 | delete this._inst; 221 | delete this._values; 222 | delete this._goRefCounts; 223 | delete this._ids; 224 | delete this._idPool; 225 | this.exit(code); 226 | }, 227 | 228 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 229 | "runtime.wasmWrite": (sp) => { 230 | sp >>>= 0; 231 | const fd = getInt64(sp + 8); 232 | const p = getInt64(sp + 16); 233 | const n = this.mem.getInt32(sp + 24, true); 234 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 235 | }, 236 | 237 | // func resetMemoryDataView() 238 | "runtime.resetMemoryDataView": (sp) => { 239 | sp >>>= 0; 240 | this.mem = new DataView(this._inst.exports.mem.buffer); 241 | }, 242 | 243 | // func nanotime1() int64 244 | "runtime.nanotime1": (sp) => { 245 | sp >>>= 0; 246 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 247 | }, 248 | 249 | // func walltime() (sec int64, nsec int32) 250 | "runtime.walltime": (sp) => { 251 | sp >>>= 0; 252 | const msec = (new Date).getTime(); 253 | setInt64(sp + 8, msec / 1000); 254 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 255 | }, 256 | 257 | // func scheduleTimeoutEvent(delay int64) int32 258 | "runtime.scheduleTimeoutEvent": (sp) => { 259 | sp >>>= 0; 260 | const id = this._nextCallbackTimeoutID; 261 | this._nextCallbackTimeoutID++; 262 | this._scheduledTimeouts.set(id, setTimeout( 263 | () => { 264 | this._resume(); 265 | while (this._scheduledTimeouts.has(id)) { 266 | // for some reason Go failed to register the timeout event, log and try again 267 | // (temporary workaround for https://github.com/golang/go/issues/28975) 268 | console.warn("scheduleTimeoutEvent: missed timeout event"); 269 | this._resume(); 270 | } 271 | }, 272 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 273 | )); 274 | this.mem.setInt32(sp + 16, id, true); 275 | }, 276 | 277 | // func clearTimeoutEvent(id int32) 278 | "runtime.clearTimeoutEvent": (sp) => { 279 | sp >>>= 0; 280 | const id = this.mem.getInt32(sp + 8, true); 281 | clearTimeout(this._scheduledTimeouts.get(id)); 282 | this._scheduledTimeouts.delete(id); 283 | }, 284 | 285 | // func getRandomData(r []byte) 286 | "runtime.getRandomData": (sp) => { 287 | sp >>>= 0; 288 | crypto.getRandomValues(loadSlice(sp + 8)); 289 | }, 290 | 291 | // func finalizeRef(v ref) 292 | "syscall/js.finalizeRef": (sp) => { 293 | sp >>>= 0; 294 | const id = this.mem.getUint32(sp + 8, true); 295 | this._goRefCounts[id]--; 296 | if (this._goRefCounts[id] === 0) { 297 | const v = this._values[id]; 298 | this._values[id] = null; 299 | this._ids.delete(v); 300 | this._idPool.push(id); 301 | } 302 | }, 303 | 304 | // func stringVal(value string) ref 305 | "syscall/js.stringVal": (sp) => { 306 | sp >>>= 0; 307 | storeValue(sp + 24, loadString(sp + 8)); 308 | }, 309 | 310 | // func valueGet(v ref, p string) ref 311 | "syscall/js.valueGet": (sp) => { 312 | sp >>>= 0; 313 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 314 | sp = this._inst.exports.getsp() >>> 0; // see comment above 315 | storeValue(sp + 32, result); 316 | }, 317 | 318 | // func valueSet(v ref, p string, x ref) 319 | "syscall/js.valueSet": (sp) => { 320 | sp >>>= 0; 321 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 322 | }, 323 | 324 | // func valueDelete(v ref, p string) 325 | "syscall/js.valueDelete": (sp) => { 326 | sp >>>= 0; 327 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 328 | }, 329 | 330 | // func valueIndex(v ref, i int) ref 331 | "syscall/js.valueIndex": (sp) => { 332 | sp >>>= 0; 333 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 334 | }, 335 | 336 | // valueSetIndex(v ref, i int, x ref) 337 | "syscall/js.valueSetIndex": (sp) => { 338 | sp >>>= 0; 339 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 340 | }, 341 | 342 | // func valueCall(v ref, m string, args []ref) (ref, bool) 343 | "syscall/js.valueCall": (sp) => { 344 | sp >>>= 0; 345 | try { 346 | const v = loadValue(sp + 8); 347 | const m = Reflect.get(v, loadString(sp + 16)); 348 | const args = loadSliceOfValues(sp + 32); 349 | const result = Reflect.apply(m, v, args); 350 | sp = this._inst.exports.getsp() >>> 0; // see comment above 351 | storeValue(sp + 56, result); 352 | this.mem.setUint8(sp + 64, 1); 353 | } catch (err) { 354 | sp = this._inst.exports.getsp() >>> 0; // see comment above 355 | storeValue(sp + 56, err); 356 | this.mem.setUint8(sp + 64, 0); 357 | } 358 | }, 359 | 360 | // func valueInvoke(v ref, args []ref) (ref, bool) 361 | "syscall/js.valueInvoke": (sp) => { 362 | sp >>>= 0; 363 | try { 364 | const v = loadValue(sp + 8); 365 | const args = loadSliceOfValues(sp + 16); 366 | const result = Reflect.apply(v, undefined, args); 367 | sp = this._inst.exports.getsp() >>> 0; // see comment above 368 | storeValue(sp + 40, result); 369 | this.mem.setUint8(sp + 48, 1); 370 | } catch (err) { 371 | sp = this._inst.exports.getsp() >>> 0; // see comment above 372 | storeValue(sp + 40, err); 373 | this.mem.setUint8(sp + 48, 0); 374 | } 375 | }, 376 | 377 | // func valueNew(v ref, args []ref) (ref, bool) 378 | "syscall/js.valueNew": (sp) => { 379 | sp >>>= 0; 380 | try { 381 | const v = loadValue(sp + 8); 382 | const args = loadSliceOfValues(sp + 16); 383 | const result = Reflect.construct(v, args); 384 | sp = this._inst.exports.getsp() >>> 0; // see comment above 385 | storeValue(sp + 40, result); 386 | this.mem.setUint8(sp + 48, 1); 387 | } catch (err) { 388 | sp = this._inst.exports.getsp() >>> 0; // see comment above 389 | storeValue(sp + 40, err); 390 | this.mem.setUint8(sp + 48, 0); 391 | } 392 | }, 393 | 394 | // func valueLength(v ref) int 395 | "syscall/js.valueLength": (sp) => { 396 | sp >>>= 0; 397 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 398 | }, 399 | 400 | // valuePrepareString(v ref) (ref, int) 401 | "syscall/js.valuePrepareString": (sp) => { 402 | sp >>>= 0; 403 | const str = encoder.encode(String(loadValue(sp + 8))); 404 | storeValue(sp + 16, str); 405 | setInt64(sp + 24, str.length); 406 | }, 407 | 408 | // valueLoadString(v ref, b []byte) 409 | "syscall/js.valueLoadString": (sp) => { 410 | sp >>>= 0; 411 | const str = loadValue(sp + 8); 412 | loadSlice(sp + 16).set(str); 413 | }, 414 | 415 | // func valueInstanceOf(v ref, t ref) bool 416 | "syscall/js.valueInstanceOf": (sp) => { 417 | sp >>>= 0; 418 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 419 | }, 420 | 421 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 422 | "syscall/js.copyBytesToGo": (sp) => { 423 | sp >>>= 0; 424 | const dst = loadSlice(sp + 8); 425 | const src = loadValue(sp + 32); 426 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 427 | this.mem.setUint8(sp + 48, 0); 428 | return; 429 | } 430 | const toCopy = src.subarray(0, dst.length); 431 | dst.set(toCopy); 432 | setInt64(sp + 40, toCopy.length); 433 | this.mem.setUint8(sp + 48, 1); 434 | }, 435 | 436 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 437 | "syscall/js.copyBytesToJS": (sp) => { 438 | sp >>>= 0; 439 | const dst = loadValue(sp + 8); 440 | const src = loadSlice(sp + 16); 441 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 442 | this.mem.setUint8(sp + 48, 0); 443 | return; 444 | } 445 | const toCopy = src.subarray(0, dst.length); 446 | dst.set(toCopy); 447 | setInt64(sp + 40, toCopy.length); 448 | this.mem.setUint8(sp + 48, 1); 449 | }, 450 | 451 | "debug": (value) => { 452 | console.log(value); 453 | }, 454 | } 455 | }; 456 | } 457 | 458 | async run(instance) { 459 | if (!(instance instanceof WebAssembly.Instance)) { 460 | throw new Error("Go.run: WebAssembly.Instance expected"); 461 | } 462 | this._inst = instance; 463 | this.mem = new DataView(this._inst.exports.mem.buffer); 464 | this._values = [ // JS values that Go currently has references to, indexed by reference id 465 | NaN, 466 | 0, 467 | null, 468 | true, 469 | false, 470 | globalThis, 471 | this, 472 | ]; 473 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 474 | this._ids = new Map([ // mapping from JS values to reference ids 475 | [0, 1], 476 | [null, 2], 477 | [true, 3], 478 | [false, 4], 479 | [globalThis, 5], 480 | [this, 6], 481 | ]); 482 | this._idPool = []; // unused ids that have been garbage collected 483 | this.exited = false; // whether the Go program has exited 484 | 485 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 486 | let offset = 4096; 487 | 488 | const strPtr = (str) => { 489 | const ptr = offset; 490 | const bytes = encoder.encode(str + "\0"); 491 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 492 | offset += bytes.length; 493 | if (offset % 8 !== 0) { 494 | offset += 8 - (offset % 8); 495 | } 496 | return ptr; 497 | }; 498 | 499 | const argc = this.argv.length; 500 | 501 | const argvPtrs = []; 502 | this.argv.forEach((arg) => { 503 | argvPtrs.push(strPtr(arg)); 504 | }); 505 | argvPtrs.push(0); 506 | 507 | const keys = Object.keys(this.env).sort(); 508 | keys.forEach((key) => { 509 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 510 | }); 511 | argvPtrs.push(0); 512 | 513 | const argv = offset; 514 | argvPtrs.forEach((ptr) => { 515 | this.mem.setUint32(offset, ptr, true); 516 | this.mem.setUint32(offset + 4, 0, true); 517 | offset += 8; 518 | }); 519 | 520 | // The linker guarantees global data starts from at least wasmMinDataAddr. 521 | // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. 522 | const wasmMinDataAddr = 4096 + 8192; 523 | if (offset >= wasmMinDataAddr) { 524 | throw new Error("total length of command line and environment variables exceeds limit"); 525 | } 526 | 527 | this._inst.exports.run(argc, argv); 528 | if (this.exited) { 529 | this._resolveExitPromise(); 530 | } 531 | await this._exitPromise; 532 | } 533 | 534 | _resume() { 535 | if (this.exited) { 536 | throw new Error("Go program has already exited"); 537 | } 538 | this._inst.exports.resume(); 539 | if (this.exited) { 540 | this._resolveExitPromise(); 541 | } 542 | } 543 | 544 | _makeFuncWrapper(id) { 545 | const go = this; 546 | return function () { 547 | const event = { id: id, this: this, args: arguments }; 548 | go._pendingEvent = event; 549 | go._resume(); 550 | return event.result; 551 | }; 552 | } 553 | } 554 | })(); 555 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Jeadie/Go-L 2 | 3 | go 1.18 4 | 5 | require golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e 6 | require github.com/Jeadie/DateDiff v0.0.0-20220326045009-6a7a8d44b728 7 | require github.com/teamortix/golang-wasm/wasm v0.0.0-20220630082535-f3e518b544db // indirect -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 2 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 3 | github.com/Jeadie/DateDiff v0.0.0-20220326045009-6a7a8d44b728 h1:XdIwmtIDRwVhRLLanNfnM+nQP3tVdoyWYqBVwtNHEbA= 4 | github.com/Jeadie/DateDiff v0.0.0-20220326045009-6a7a8d44b728/go.mod h1:iZgMbqnBTrwxyQKXr+17N1z5LPDoCWWc32V+s8OEYsc= 5 | github.com/teamortix/golang-wasm/wasm v0.0.0-20220630082535-f3e518b544db h1:WjuAaAU0DojriAHY/TNV+Km18cImbaOu1K8j1PPrD1M= 6 | github.com/teamortix/golang-wasm/wasm v0.0.0-20220630082535-f3e518b544db/go.mod h1:nskvTyoGIaAsC+664SkRitVI1ft6dm1xerCr50YZsnY= -------------------------------------------------------------------------------- /pkg/gol/gameoflife.go: -------------------------------------------------------------------------------- 1 | package gol 2 | 3 | type UpdateRuleFn func([][]uint) uint 4 | 5 | 6 | type InputParameters struct { 7 | Iterations uint `wasm:"iterations" json:"iterations"` 8 | GridSize uint `wasm:"gridSize" json:"gridSize"` 9 | AliveRatio float64 `wasm:"aliveRatio" json:"aliveRatio"` 10 | UpdateDelay uint `json:"iterations"` // wasm does not support rendering, only outputting of data 11 | Topology string `wasm:"topology" json:"topology"` 12 | UpdateFunctionNumber uint `wasm:"updateFunctionNumber" json:"updateFunctionNumber" ` 13 | } 14 | type LatticeProcessor func(*Lattice[uint]) error 15 | const ConwaysGameOfLifeUpdateRuleNumber = 1994975360 16 | 17 | 18 | func ConstructUpdateRule(updateRuleNumber uint) UpdateRuleFn { 19 | if updateRuleNumber == ConwaysGameOfLifeUpdateRuleNumber { 20 | return CalculateGOLValue 21 | } else { 22 | return CreateUpdateRule(updateRuleNumber) 23 | } 24 | } 25 | 26 | func CalculateGOLValue(box [][]uint) uint { 27 | x, y := getMidpoint(box) 28 | neighbourCount := countOnesAround(box, x, y) 29 | if box[x][y] == 1 && (neighbourCount == 2 || neighbourCount == 3) { 30 | return 1 31 | } else if box[x][y] == 0 && neighbourCount == 3 { 32 | return 1 33 | } 34 | return 0 35 | } 36 | 37 | // CreateUpdateRule from a 32 bit update rule number. See README.md#2d-implementation for details. 38 | func CreateUpdateRule(updateRuleNumber uint) UpdateRuleFn { 39 | // Only contain states that are alive, but will also map to true 40 | // So for alive states: isAlive, found := aliveStates[s] == true, true 41 | // So for dead states: isAlive, found := aliveStates[s] == false, false 42 | aliveStates := make(map[uint]bool) 43 | 44 | // Check bit i of updateRuleNumber to add alive states to map 45 | for i := uint(0); i < 32; i++ { 46 | if updateRuleNumber & (1 << i) > 0 { 47 | aliveStates[i] = true 48 | } 49 | } 50 | 51 | return func(box [][]uint) uint { 52 | state := 16 * box[0][1] + 8*box[1][1] + 4* box[2][1] + 2* box[1][0] + box[1][2] 53 | 54 | if a, b := aliveStates[state]; a && b { 55 | return 1 56 | } 57 | return 0 58 | } 59 | } 60 | 61 | func countOnesAround(box [][]uint, x, y uint) uint { 62 | co := uint(0) 63 | 64 | a, b, c, d := box[x-1][y-1], box[x-1][y+1], box[x+1][y-1], box[x+1][y+1] 65 | 66 | if a < 2 { 67 | co += a 68 | } 69 | if b < 2 { 70 | co += b 71 | } 72 | if c < 2 { 73 | co += c 74 | } 75 | if d < 2 { 76 | co += d 77 | } 78 | 79 | return co 80 | } 81 | 82 | func getMidpoint(box [][]uint) (uint, uint) { 83 | first := uint(len(box) / 2) 84 | if len(box) > 0 { 85 | return first, uint(len(box[0]) / 2) 86 | } else { 87 | return first, uint(0) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/gol/lattice.go: -------------------------------------------------------------------------------- 1 | package gol 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | 7 | "golang.org/x/exp/constraints" 8 | ) 9 | 10 | // Node can be used in Lattice method signatures 11 | type Node constraints.Ordered 12 | type IntPair = [2]int 13 | 14 | type Lattice[T Node] struct { 15 | grid []T 16 | topologyFn TopologyTransformation 17 | n uint 18 | null T 19 | formatter func(x T) string 20 | updateRule func([][]T) T 21 | } 22 | 23 | func (l *Lattice[T]) GetFormattedValueAt(x, y int) string { 24 | return l.formatter(l.GetValue(x, y)) 25 | } 26 | 27 | // Size of the square grid lattice 28 | func (l *Lattice[T]) Size() uint { 29 | return l.n 30 | } 31 | 32 | // GetValue on lattice at coordinate (x, y) 33 | func (l *Lattice[T]) GetValue(x int, y int) T { 34 | nx, ny := l.topologyFn(x, y, 0, 0, int(l.n)) 35 | if nx == -1 || ny == -1 { 36 | return l.null 37 | } 38 | return l.grid[(nx*int(l.n))+ny] 39 | } 40 | 41 | // SetValue on lattice at coordinate (x, y) to v 42 | func (l *Lattice[T]) SetValue(x int, y int, v T) { 43 | nx, ny := l.topologyFn(x, y, 0, 0, int(l.n)) 44 | l.grid[(nx*int(l.n))+ny] = v 45 | } 46 | 47 | // GetValuesAround returns all values around a coordinate within an L1 distance of w. 48 | func (l *Lattice[T]) GetValuesAround(x int, y int, w int) [][]T { 49 | rows := make([][]T, 2*w+1) 50 | 51 | for i := 0; i < 2*w+1; i++ { 52 | rows[i] = make([]T, 2*w+1) 53 | for j := 0; j < 2*w+1; j++ { 54 | 55 | rows[i][j] = l.GetValue(x+i-w, y+j-w) 56 | } 57 | } 58 | return rows 59 | } 60 | 61 | // Copy a Lattice struct, and all its references, to a new instance (i.e. deep copy). 62 | func (l *Lattice[T]) Copy() *Lattice[T] { 63 | readG := make([]T, len(l.grid)) 64 | copy(readG, l.grid) 65 | return &Lattice[T]{ 66 | grid: readG, 67 | topologyFn: l.topologyFn, 68 | n: l.n, 69 | updateRule: l.updateRule, 70 | } 71 | } 72 | 73 | // GetLatticeCoordinates into a channel of all coordinates on the lattice. 74 | func (l *Lattice[T]) GetLatticeCoordinates() chan IntPair { 75 | out := make(chan IntPair) 76 | go func(out chan IntPair) { 77 | defer close(out) 78 | for i := 0; i < int(l.n); i++ { 79 | for j := 0; j < int(l.n); j++ { 80 | out <- [2]int{i, j} 81 | } 82 | } 83 | }(out) 84 | return out 85 | } 86 | 87 | // UpdatePair value within a lattice at a given co-ordinate. if `readL` is nil, 88 | // will read from referenced Lattice. This allows for parallel update of 89 | // lattice without conflict. 90 | func (l *Lattice[T]) UpdatePair(i IntPair, readL *Lattice[T]) bool { 91 | x, y := i[0], i[1] 92 | var box [][]T 93 | if readL != nil { 94 | box = readL.GetValuesAround(x, y, 1) 95 | } else { 96 | box = l.GetValuesAround(x, y, 1) 97 | } 98 | 99 | newV := l.updateRule(box) 100 | 101 | l.SetValue(x, y, newV) 102 | return newV != box[1][1] 103 | } 104 | 105 | // SingleIteration of applying an update rule to a lattice. Returns true if any 106 | // coordinate value in the lattice updated in the iteration. 107 | func (l *Lattice[T]) SingleIteration() bool { 108 | isUpdated := false 109 | readL := l.Copy() 110 | for i := range l.GetLatticeCoordinates() { 111 | updated := l.UpdatePair(i, readL) 112 | if updated { 113 | isUpdated = true 114 | } 115 | } 116 | return isUpdated 117 | } 118 | 119 | // MakeGrid converts and copies the internal grid representation to the standard 2D representation. 120 | func (l *Lattice[T]) MakeGrid() [][]T { 121 | result := make([][]T, l.Size()) 122 | 123 | for i := uint(0); i < l.Size(); i++ { 124 | result[i] = make([]T, l.Size()) 125 | copy(result[i],l.grid[i*l.Size():(i+1)*l.Size()]) 126 | } 127 | return result 128 | } 129 | 130 | // LatticeParams required to construct a Lattice with type uint (i.e. Lattice[uint]). 131 | type LatticeParams struct { 132 | GridSize uint 133 | AliveRatio float64 134 | Topology string 135 | } 136 | 137 | func ConstructUintLatticeFromInput(inputParams InputParameters) *Lattice[uint] { 138 | return ConstructUintLattice( 139 | LatticeParams{ 140 | GridSize: inputParams.GridSize, 141 | AliveRatio: inputParams.AliveRatio, 142 | Topology: inputParams.Topology, 143 | }, 144 | ConstructUpdateRule(inputParams.UpdateFunctionNumber), 145 | ) 146 | } 147 | 148 | func ConstructUintLattice(params LatticeParams, updateRule UpdateRuleFn) *Lattice[uint] { 149 | return &Lattice[uint]{ 150 | grid: ConstructUintGrid(params.GridSize, params.AliveRatio), 151 | topologyFn: GetTransformation(params.Topology), 152 | n: params.GridSize, 153 | null: uint(math.MaxUint), 154 | formatter: func(t uint) string { 155 | if t == 0 { 156 | return "-" 157 | } else { 158 | return "+" 159 | } 160 | }, 161 | updateRule: updateRule, 162 | } 163 | } 164 | 165 | // ConstructUintGrid of binary values on a given square size `n`. Probability of a value of 1 is binaryProb. 166 | func ConstructUintGrid(n uint, binaryProb float64) []uint { 167 | // Construct chan of 1/0s from decomposing uint64s. 168 | u := make(chan uint) 169 | go func(out chan uint, size uint) { 170 | for i := 0; i < int(size); i++ { 171 | if rand.Float64() < binaryProb { 172 | u <- 1 173 | } else { 174 | u <- 0 175 | } 176 | } 177 | }(u, n*n) 178 | 179 | rows := make([]uint, n*n) 180 | for i := 0; i < int(n*n); i++ { 181 | rows[i] = <-u 182 | } 183 | return rows 184 | } 185 | -------------------------------------------------------------------------------- /pkg/gol/topology.go: -------------------------------------------------------------------------------- 1 | package gol 2 | 3 | const ( 4 | Bordered string = "BORDERED" 5 | Torus = "TORUS" 6 | Sphere = "SPHERE" 7 | KleinBottle = "KLEIN_BOTTLE" 8 | ProjectivePlane = "PROJECTIVE_PLANE" 9 | ) 10 | 11 | var ALLOWED_TOPOLOGIES = []string{Bordered, Torus, KleinBottle, ProjectivePlane, Sphere} 12 | 13 | const DefaultTopology = Bordered 14 | 15 | // Returns true iff x is an allowed Topology. 16 | func IsValidTopology(x string) bool { 17 | for _, t := range ALLOWED_TOPOLOGIES { 18 | if t == x { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // Describes how co-ordinates get transformed onn the boundary conditions 26 | type TopologyTransformation func(x, y int, dx, dy int, n int) (int, int) 27 | 28 | // GetTransformation returns the transformation function for a given Topology. 29 | func GetTransformation(topology string) TopologyTransformation { 30 | switch topology { 31 | case Bordered: 32 | return TranslateBordered 33 | case Torus: 34 | return TranslateTorus 35 | case KleinBottle: 36 | return TranslateKleinBottle 37 | case ProjectivePlane: 38 | return TranslateProjectivePlane 39 | case Sphere: 40 | return TranslateSphere 41 | default: 42 | return GetTransformation(DefaultTopology) 43 | } 44 | } 45 | 46 | func TranslateBordered(x, y int, dx, dy int, n int) (int, int) { 47 | // Efficient non-boundary case return. 48 | if x+dx >= 0 && x+dx < n && y+dy >= 0 && y+dy < n { 49 | return x + dx, y + dy 50 | } 51 | 52 | return -1, -1 53 | } 54 | 55 | func TranslateTorus(x, y int, dx, dy int, n int) (int, int) { 56 | // Efficient non-boundary case return. 57 | if x+dx >= 0 && x+dx < n && y+dy >= 0 && y+dy < n { 58 | return x + dx, y + dy 59 | } 60 | 61 | return mod(x+dx, n), mod(y+dy, n) 62 | } 63 | 64 | func TranslateKleinBottle(x, y int, dx, dy int, n int) (int, int) { 65 | // Efficient non-boundary case return. 66 | if x+dx >= 0 && x+dx < n && y+dy >= 0 && y+dy < n { 67 | return x + dx, y + dy 68 | } 69 | 70 | ny := y + dy 71 | nx := x + dx 72 | 73 | // Top boundary reverse x co-ordinate 74 | if y+dy == n { 75 | nx = n - (x + dx) 76 | } 77 | 78 | if (y + dy) == n { 79 | ny = mod(n-(y+dy), n) 80 | } 81 | return mod(nx, n), mod(ny, n) 82 | } 83 | 84 | func TranslateProjectivePlane(x, y int, dx, dy int, n int) (int, int) { 85 | // Efficient non-boundary case return. 86 | if x+dx >= 0 && x+dx < n && y+dy >= 0 && y+dy < n { 87 | return x + dx, y + dy 88 | } 89 | 90 | nx := x + dx 91 | ny := y + dy 92 | 93 | // It appears (0, 5) & (5,0) cannot be mapped properly. 94 | // We shall map (0, 5) & (5,0) to (-1, -1) 95 | if (nx == 0 && ny == n) || (nx == n && ny == 0) { 96 | return -1, -1 97 | } 98 | 99 | // Border line conditions 100 | if ny >= n || ny < 0 { 101 | ny = mod(ny, n) 102 | nx = n - x 103 | } 104 | if nx >= n || nx < 0 { 105 | nx = mod(nx, n) 106 | ny = n - y 107 | } 108 | 109 | // Border line might require two mappings 110 | return TranslateProjectivePlane(nx, ny, 0, 0, n) // mod(nx, n), mod(ny, n) 111 | } 112 | 113 | func TranslateSphere(x, y int, dx, dy int, n int) (int, int) { 114 | // Efficient non-boundary case return. 115 | if x+dx >= 0 && x+dx < n && y+dy >= 0 && y+dy < n { 116 | return x + dx, y + dy 117 | } 118 | 119 | // 120 | // on border 121 | if x+dx == n || y+dy == n { 122 | return mod(y+dy, n), mod(x+dx, n) 123 | } 124 | 125 | // For boundary cases, points are reflected across straight line between (0,n-1) and (n-1, 0) 126 | x, y = reflectSphere(x+dx, y+dy, n) 127 | 128 | // on border 129 | if x == n || y == n { 130 | return mod(y, n), mod(x, n) 131 | } 132 | 133 | // Now reflect back into square. 134 | x, y = reflectSquare(x, y, n) 135 | // on border 136 | if x == n || y == n { 137 | return mod(y, n), mod(x, n) 138 | } 139 | return x, y 140 | } 141 | 142 | // Reflect the point (x,y) back into the square bound by (0, 0), (n, n) 143 | func reflectSquare(x, y, n int) (int, int) { 144 | return reflectSquareCoordinate(x, n), reflectSquareCoordinate(y, n) 145 | } 146 | 147 | func reflectSquareCoordinate(a, n int) int { 148 | if a < 0 { 149 | return -a 150 | } else if a > n { 151 | return n - (a - n) 152 | } 153 | return a 154 | } 155 | 156 | // Reflect point (x,y) across y=n-x 157 | func reflectSphere(x, y, n int) (int, int) { 158 | return n - y, n - x 159 | } 160 | 161 | // mod operation that handles negative number properly. 162 | func mod(d, m int) int { 163 | var res = d % m 164 | 165 | if res < 0 { 166 | return res + m 167 | } 168 | return res 169 | } 170 | -------------------------------------------------------------------------------- /pkg/gol/topology_test.go: -------------------------------------------------------------------------------- 1 | package gol 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type TopologyTestCase struct { 9 | x, y, dx, dy, n int 10 | expectedX, expectedY int 11 | } 12 | 13 | type TransformTestSet struct { 14 | tests []TopologyTestCase 15 | topology string 16 | } 17 | 18 | func RunCase(test TopologyTestCase, fn TopologyTransformation) error { 19 | nx, ny := fn(test.x, test.y, test.dx, test.dy, test.n) 20 | if nx != test.expectedX || ny != test.expectedY { 21 | return fmt.Errorf( 22 | "incorrect output for (%d+%d, %d+%d) on size %d. Expected (%d, %d), output (%d, %d)", 23 | test.x, test.dx, test.y, test.dy, test.n, test.expectedX, test.expectedY, nx, ny) 24 | } 25 | return nil 26 | } 27 | 28 | func TestTopologyTransformation(t *testing.T) { 29 | tests := []TransformTestSet{{ 30 | topology: Bordered, 31 | tests: []TopologyTestCase{ 32 | {x: 0, y: 0, dx: -1, dy: 0, n: 5, expectedX: -1, expectedY: -1}, 33 | {x: 0, y: 0, dx: 0, dy: -1, n: 5, expectedX: -1, expectedY: -1}, 34 | {x: 0, y: 0, dx: -1, dy: -1, n: 5, expectedX: -1, expectedY: -1}, 35 | {x: 0, y: 0, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 1}, 36 | }, 37 | }, { 38 | topology: Torus, 39 | tests: []TopologyTestCase{ 40 | {x: 0, y: 0, dx: -1, dy: 0, n: 5, expectedX: 4, expectedY: 0}, 41 | {x: 0, y: 0, dx: 0, dy: -1, n: 5, expectedX: 0, expectedY: 4}, 42 | {x: 0, y: 0, dx: -1, dy: -1, n: 5, expectedX: 4, expectedY: 4}, 43 | {x: 0, y: 0, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 1}, 44 | }, 45 | }, { 46 | topology: KleinBottle, 47 | tests: []TopologyTestCase{ 48 | // Boundary Conditions 49 | {x: 5, y: 1, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 1}, 50 | {x: 3, y: 5, dx: 0, dy: 0, n: 5, expectedX: 2, expectedY: 0}, 51 | 52 | // Corner conditions 53 | {x: 5, y: 0, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 0}, 54 | {x: 0, y: 5, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 0}, 55 | {x: 5, y: 5, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 0}, 56 | 57 | // Out of bound conditions 58 | {x: 0, y: 0, dx: -1, dy: 0, n: 5, expectedX: 4, expectedY: 0}, 59 | {x: 0, y: 0, dx: 0, dy: -1, n: 5, expectedX: 0, expectedY: 4}, 60 | {x: 0, y: 0, dx: -1, dy: -1, n: 5, expectedX: 4, expectedY: 4}, 61 | {x: 0, y: 0, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 1}, 62 | }, 63 | }, { 64 | topology: ProjectivePlane, 65 | tests: []TopologyTestCase{ 66 | 67 | // Boundary conditions 68 | {x: 3, y: 5, dx: 0, dy: 0, n: 5, expectedX: 2, expectedY: 0}, 69 | {x: 5, y: 3, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 2}, 70 | 71 | // Corner conditions 72 | {x: 5, y: 5, dx: 0, dy: 0, n: 5, expectedX: 0, expectedY: 0}, 73 | 74 | // These are unmappable, default to (-1, -1) 75 | {x: 5, y: 0, dx: 0, dy: 0, n: 5, expectedX: -1, expectedY: -1}, 76 | {x: 0, y: 5, dx: 0, dy: 0, n: 5, expectedX: -1, expectedY: -1}, 77 | 78 | // Out of bound conditions 79 | {x: 0, y: 0, dx: -1, dy: 0, n: 5, expectedX: -1, expectedY: -1}, 80 | {x: 0, y: 0, dx: 0, dy: -1, n: 5, expectedX: -1, expectedY: -1}, 81 | {x: 0, y: 0, dx: -1, dy: -1, n: 5, expectedX: -1, expectedY: -1}, 82 | {x: 0, y: 0, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 1}, 83 | }, 84 | }, { 85 | topology: Sphere, 86 | tests: []TopologyTestCase{ 87 | {x: 0, y: 0, dx: -1, dy: 0, n: 5, expectedX: 1, expectedY: 0}, 88 | {x: 0, y: 0, dx: 0, dy: -1, n: 5, expectedX: 0, expectedY: 1}, 89 | {x: 0, y: 0, dx: -1, dy: -1, n: 5, expectedX: 4, expectedY: 4}, 90 | 91 | {x: 3, y: 4, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 3}, 92 | {x: 4, y: 3, dx: 1, dy: 0, n: 5, expectedX: 3, expectedY: 0}, 93 | {x: 4, y: 4, dx: 1, dy: 1, n: 5, expectedX: 0, expectedY: 0}, 94 | 95 | {x: 0, y: 0, dx: 0, dy: 1, n: 5, expectedX: 0, expectedY: 1}, 96 | }, 97 | }, 98 | } 99 | 100 | for _, testCase := range tests { 101 | t.Run(testCase.topology, func(t *testing.T) { 102 | fn := GetTransformation(testCase.topology) 103 | for _, test := range testCase.tests { 104 | err := RunCase(test, fn) 105 | if err != nil { 106 | t.Errorf(err.Error()) 107 | } 108 | } 109 | }) 110 | } 111 | } 112 | --------------------------------------------------------------------------------