Compare commits

..

No commits in common. "gh-pages" and "master" have entirely different histories.

23 changed files with 7762 additions and 435 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
.github
.jenkins
.git
pkg
target
**/dist
**/node_modules

72
.gitea/workflows/test.yml Normal file
View File

@ -0,0 +1,72 @@
name: test and deploy
on:
push:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.sheep-ghoul.ts.net
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
- name: Build Rust for WASM
run: wasm-pack build
- name: Test WASM in-browser
run: wasm-pack test --firefox --chrome --headless
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Install Node Modules
run: npm ci
working-directory: ./www
- name: Build Js
run: npm run build --if-present
working-directory: ./www
package:
runs-on: ubuntu-latest
needs: [build] # for ignoring bad builds
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
with:
github-server-url: https://gitea.sheep-ghoul.ts.net
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: gitea.sheep-ghoul.ts.net
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & Push Container
uses: docker/build-push-action@v2
with:
push: true
tags: gitea.sheep-ghoul.ts.net/sarsoo/game-of-life:latest
context: .

73
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,73 @@
name: test and deploy
on:
push:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- name: Install wasm-pack
uses: jetli/wasm-pack-action@v0.3.0
- name: Build Rust for WASM
run: wasm-pack build
- name: Test WASM in-browser
run: wasm-pack test --firefox --headless
- name: Install Node
uses: actions/setup-node@v2
with:
node-version: 18
- name: Install Node Modules
run: npm ci
working-directory: ./www
- name: Build Js
run: npm run build --if-present
working-directory: ./www
- name: Move CNAME file to Staging Directory
run: mv CNAME www/dist
- name: Deploy To Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./www/dist
package:
runs-on: ubuntu-latest
needs: [build] # for ignoring bad builds
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build & Push Container
uses: docker/build-push-action@v2
with:
push: true
tags: sarsoo/game-of-life:latest

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log

22
.jenkins/jenkinsfile Normal file
View File

@ -0,0 +1,22 @@
pipeline {
agent any
stages {
stage('Deploy') {
when { branch 'master' }
steps {
script {
docker.withRegistry('https://registry.sarsoo.xyz', 'git-registry-creds') {
docker.build("sarsoo/game-of-life:latest").push()
}
}
}
}
}
post {
always {
cleanWs()
}
}
}

View File

48
Cargo.toml Normal file
View File

@ -0,0 +1,48 @@
[package]
name = "gameoflife"
version = "0.1.0"
authors = ["aj <andrewjpack@gmail.com>"]
edition = "2018"
repository = "https://github.com/Sarsoo/game-of-life"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook",
"random_init"
]
random_init = ["rand", "rand_pcg"]
[dependencies]
wasm-bindgen = "0.2.74"
rand = {version = "0.8.4", optional = true }
rand_pcg = {version = "0.3.1", optional = true }
# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
# code size when deploying.
console_error_panic_hook = { version = "0.1.6", optional = true }
[dependencies.web-sys]
version = "0.3.45"
features = [
"console",
]
[dependencies.getrandom]
version = "*"
features = ["js"]
[dev-dependencies]
wasm-bindgen-test = "0.3.24"
[profile.release]
# Tell `rustc` to optimize for small code size.
opt-level = "s"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM rust:1.78 AS rust-build
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
COPY . /gof
WORKDIR /gof
RUN wasm-pack build --release
RUN cargo doc --no-deps --document-private-items
FROM node:22-alpine AS js-build
COPY . /gof
WORKDIR /gof
COPY --from=rust-build /gof/pkg /gof/pkg
WORKDIR /gof/www
RUN npm ci
RUN npm run build --if-present
COPY --from=rust-build /gof/target/doc /gof/www/dist/
FROM nginx:alpine-slim
COPY --from=js-build /gof/www/dist /usr/share/nginx/html/

22
README.md Normal file
View File

@ -0,0 +1,22 @@
Game of Life
===============
![gof-ci](https://github.com/sarsoo/game-of-life/actions/workflows/test.yml/badge.svg)
## [Try it Out!](https://sarsoo.github.io/game-of-life/)
WASM-based game of life following the [Rust WASM book](https://rustwasm.github.io/docs/book/introduction.html) tutorial.
Rust WASM module for game logic with a JS frontend for rendering and processing user input.
## Building
1. Setup a Rust + wasm-pack environment and a Node environment
2. Build the Rust library into a WASM module
- `wasm-pack build`
3. Move to the Js workspace
- `cd www`
4. Install the Js dependencies
- `npm install`
5. Build the Js frontend with Rust WASM module
- `npm run build`

393
bootstrap.js vendored
View File

@ -1,393 +0,0 @@
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./bootstrap.js":
/*!**********************!*\
!*** ./bootstrap.js ***!
\**********************/
/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
eval("// A dependency graph that contains any wasm must all be imported\n// asynchronously. This `bootstrap.js` file does the single async import, so\n// that no one else needs to worry about it again.\n__webpack_require__.e(/*! import() */ \"index_js\").then(__webpack_require__.bind(__webpack_require__, /*! ./index.js */ \"./index.js\"))\n .catch(e => console.error(\"Error importing `index.js`:\", e));\n\n\n//# sourceURL=webpack://game-of-life-web/./bootstrap.js?");
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ id: moduleId,
/******/ loaded: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = __webpack_modules__;
/******/
/************************************************************************/
/******/ /* webpack/runtime/async module */
/******/ (() => {
/******/ var webpackThen = typeof Symbol === "function" ? Symbol("webpack then") : "__webpack_then__";
/******/ var webpackExports = typeof Symbol === "function" ? Symbol("webpack exports") : "__webpack_exports__";
/******/ var webpackError = typeof Symbol === "function" ? Symbol("webpack error") : "__webpack_error__";
/******/ var completeQueue = (queue) => {
/******/ if(queue) {
/******/ queue.forEach((fn) => (fn.r--));
/******/ queue.forEach((fn) => (fn.r-- ? fn.r++ : fn()));
/******/ }
/******/ }
/******/ var completeFunction = (fn) => (!--fn.r && fn());
/******/ var queueFunction = (queue, fn) => (queue ? queue.push(fn) : completeFunction(fn));
/******/ var wrapDeps = (deps) => (deps.map((dep) => {
/******/ if(dep !== null && typeof dep === "object") {
/******/ if(dep[webpackThen]) return dep;
/******/ if(dep.then) {
/******/ var queue = [];
/******/ dep.then((r) => {
/******/ obj[webpackExports] = r;
/******/ completeQueue(queue);
/******/ queue = 0;
/******/ }, (e) => {
/******/ obj[webpackError] = e;
/******/ completeQueue(queue);
/******/ queue = 0;
/******/ });
/******/ var obj = {};
/******/ obj[webpackThen] = (fn, reject) => (queueFunction(queue, fn), dep['catch'](reject));
/******/ return obj;
/******/ }
/******/ }
/******/ var ret = {};
/******/ ret[webpackThen] = (fn) => (completeFunction(fn));
/******/ ret[webpackExports] = dep;
/******/ return ret;
/******/ }));
/******/ __webpack_require__.a = (module, body, hasAwait) => {
/******/ var queue = hasAwait && [];
/******/ var exports = module.exports;
/******/ var currentDeps;
/******/ var outerResolve;
/******/ var reject;
/******/ var isEvaluating = true;
/******/ var nested = false;
/******/ var whenAll = (deps, onResolve, onReject) => {
/******/ if (nested) return;
/******/ nested = true;
/******/ onResolve.r += deps.length;
/******/ deps.map((dep, i) => (dep[webpackThen](onResolve, onReject)));
/******/ nested = false;
/******/ };
/******/ var promise = new Promise((resolve, rej) => {
/******/ reject = rej;
/******/ outerResolve = () => (resolve(exports), completeQueue(queue), queue = 0);
/******/ });
/******/ promise[webpackExports] = exports;
/******/ promise[webpackThen] = (fn, rejectFn) => {
/******/ if (isEvaluating) { return completeFunction(fn); }
/******/ if (currentDeps) whenAll(currentDeps, fn, rejectFn);
/******/ queueFunction(queue, fn);
/******/ promise['catch'](rejectFn);
/******/ };
/******/ module.exports = promise;
/******/ body((deps) => {
/******/ currentDeps = wrapDeps(deps);
/******/ var fn;
/******/ var getResult = () => (currentDeps.map((d) => {
/******/ if(d[webpackError]) throw d[webpackError];
/******/ return d[webpackExports];
/******/ }))
/******/ var promise = new Promise((resolve, reject) => {
/******/ fn = () => (resolve(getResult));
/******/ fn.r = 0;
/******/ whenAll(currentDeps, fn, reject);
/******/ });
/******/ return fn.r ? promise : getResult();
/******/ }, (err) => (err && reject(promise[webpackError] = err), outerResolve()));
/******/ isEvaluating = false;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/ensure chunk */
/******/ (() => {
/******/ __webpack_require__.f = {};
/******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = (chunkId) => {
/******/ return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
/******/ __webpack_require__.f[key](chunkId, promises);
/******/ return promises;
/******/ }, []));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/get javascript chunk filename */
/******/ (() => {
/******/ // This function allow to reference async chunks
/******/ __webpack_require__.u = (chunkId) => {
/******/ // return url for filenames based on template
/******/ return "" + chunkId + ".bootstrap.js";
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/global */
/******/ (() => {
/******/ __webpack_require__.g = (function() {
/******/ if (typeof globalThis === 'object') return globalThis;
/******/ try {
/******/ return this || new Function('return this')();
/******/ } catch (e) {
/******/ if (typeof window === 'object') return window;
/******/ }
/******/ })();
/******/ })();
/******/
/******/ /* webpack/runtime/harmony module decorator */
/******/ (() => {
/******/ __webpack_require__.hmd = (module) => {
/******/ module = Object.create(module);
/******/ if (!module.children) module.children = [];
/******/ Object.defineProperty(module, 'exports', {
/******/ enumerable: true,
/******/ set: () => {
/******/ throw new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);
/******/ }
/******/ });
/******/ return module;
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/load script */
/******/ (() => {
/******/ var inProgress = {};
/******/ var dataWebpackPrefix = "game-of-life-web:";
/******/ // loadScript function to load a script via script tag
/******/ __webpack_require__.l = (url, done, key, chunkId) => {
/******/ if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ var script, needAttach;
/******/ if(key !== undefined) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ for(var i = 0; i < scripts.length; i++) {
/******/ var s = scripts[i];
/******/ if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ }
/******/ }
/******/ if(!script) {
/******/ needAttach = true;
/******/ script = document.createElement('script');
/******/
/******/ script.charset = 'utf-8';
/******/ script.timeout = 120;
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/ script.src = url;
/******/ }
/******/ inProgress[url] = [done];
/******/ var onScriptComplete = (prev, event) => {
/******/ // avoid mem leaks in IE.
/******/ script.onerror = script.onload = null;
/******/ clearTimeout(timeout);
/******/ var doneFns = inProgress[url];
/******/ delete inProgress[url];
/******/ script.parentNode && script.parentNode.removeChild(script);
/******/ doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ if(prev) return prev(event);
/******/ }
/******/ ;
/******/ var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ script.onload = onScriptComplete.bind(null, script.onload);
/******/ needAttach && document.head.appendChild(script);
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/wasm loading */
/******/ (() => {
/******/ __webpack_require__.v = (exports, wasmModuleId, wasmModuleHash, importsObj) => {
/******/ var req = fetch(__webpack_require__.p + "" + wasmModuleHash + ".module.wasm");
/******/ if (typeof WebAssembly.instantiateStreaming === 'function') {
/******/ return WebAssembly.instantiateStreaming(req, importsObj)
/******/ .then((res) => (Object.assign(exports, res.instance.exports)));
/******/ }
/******/ return req
/******/ .then((x) => (x.arrayBuffer()))
/******/ .then((bytes) => (WebAssembly.instantiate(bytes, importsObj)))
/******/ .then((res) => (Object.assign(exports, res.instance.exports)));
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/publicPath */
/******/ (() => {
/******/ var scriptUrl;
/******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";
/******/ var document = __webpack_require__.g.document;
/******/ if (!scriptUrl && document) {
/******/ if (document.currentScript)
/******/ scriptUrl = document.currentScript.src
/******/ if (!scriptUrl) {
/******/ var scripts = document.getElementsByTagName("script");
/******/ if(scripts.length) scriptUrl = scripts[scripts.length - 1].src
/******/ }
/******/ }
/******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
/******/ __webpack_require__.p = scriptUrl;
/******/ })();
/******/
/******/ /* webpack/runtime/jsonp chunk loading */
/******/ (() => {
/******/ // no baseURI
/******/
/******/ // object to store loaded and loading chunks
/******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
/******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
/******/ var installedChunks = {
/******/ "main": 0
/******/ };
/******/
/******/ __webpack_require__.f.j = (chunkId, promises) => {
/******/ // JSONP chunk loading for javascript
/******/ var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined;
/******/ if(installedChunkData !== 0) { // 0 means "already installed".
/******/
/******/ // a Promise means "currently loading".
/******/ if(installedChunkData) {
/******/ promises.push(installedChunkData[2]);
/******/ } else {
/******/ if(true) { // all chunks have JS
/******/ // setup Promise in chunk cache
/******/ var promise = new Promise((resolve, reject) => (installedChunkData = installedChunks[chunkId] = [resolve, reject]));
/******/ promises.push(installedChunkData[2] = promise);
/******/
/******/ // start chunk loading
/******/ var url = __webpack_require__.p + __webpack_require__.u(chunkId);
/******/ // create error before stack unwound to get useful stacktrace later
/******/ var error = new Error();
/******/ var loadingEnded = (event) => {
/******/ if(__webpack_require__.o(installedChunks, chunkId)) {
/******/ installedChunkData = installedChunks[chunkId];
/******/ if(installedChunkData !== 0) installedChunks[chunkId] = undefined;
/******/ if(installedChunkData) {
/******/ var errorType = event && (event.type === 'load' ? 'missing' : event.type);
/******/ var realSrc = event && event.target && event.target.src;
/******/ error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
/******/ error.name = 'ChunkLoadError';
/******/ error.type = errorType;
/******/ error.request = realSrc;
/******/ installedChunkData[1](error);
/******/ }
/******/ }
/******/ };
/******/ __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
/******/ } else installedChunks[chunkId] = 0;
/******/ }
/******/ }
/******/ };
/******/
/******/ // no prefetching
/******/
/******/ // no preloaded
/******/
/******/ // no HMR
/******/
/******/ // no HMR manifest
/******/
/******/ // no on chunks loaded
/******/
/******/ // install a JSONP callback for chunk loading
/******/ var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
/******/ var [chunkIds, moreModules, runtime] = data;
/******/ // add "moreModules" to the modules object,
/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0;
/******/ if(chunkIds.some((id) => (installedChunks[id] !== 0))) {
/******/ for(moduleId in moreModules) {
/******/ if(__webpack_require__.o(moreModules, moduleId)) {
/******/ __webpack_require__.m[moduleId] = moreModules[moduleId];
/******/ }
/******/ }
/******/ if(runtime) var result = runtime(__webpack_require__);
/******/ }
/******/ if(parentChunkLoadingFunction) parentChunkLoadingFunction(data);
/******/ for(;i < chunkIds.length; i++) {
/******/ chunkId = chunkIds[i];
/******/ if(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {
/******/ installedChunks[chunkId][0]();
/******/ }
/******/ installedChunks[chunkId] = 0;
/******/ }
/******/
/******/ }
/******/
/******/ var chunkLoadingGlobal = self["webpackChunkgame_of_life_web"] = self["webpackChunkgame_of_life_web"] || [];
/******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
/******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
/******/ })();
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = __webpack_require__("./bootstrap.js");
/******/
/******/ })()
;

Binary file not shown.

File diff suppressed because one or more lines are too long

188
src/lib.rs Normal file
View File

@ -0,0 +1,188 @@
mod utils;
use std::cmp::Ordering;
use std::fmt;
use wasm_bindgen::prelude::*;
use rand::prelude::*;
use rand_pcg::Pcg64Mcg;
mod time;
macro_rules! log {
( $( $t:tt )* ) => {
web_sys::console::log_1(&format!( $( $t )* ).into());
}
}
#[wasm_bindgen]
pub fn init_game() {
log!("initialising wasm");
utils::set_panic_hook();
#[cfg(feature = "random_init")]
log!("random layout enabled");
}
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
rng: Pcg64Mcg,
rand_threshold: u32,
}
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
#[cfg(not(feature = "random_init"))]
fn populate_cell(i: u32) -> Cell {
if i % 2 == 0 || i % 7 == 0 {
Cell::Alive
} else {
Cell::Dead
}
}
#[cfg(feature = "random_init")]
fn populate_cell(i: u32, rng: &mut Pcg64Mcg, threshold: u32) -> Cell {
match rng.gen_range(0..101).cmp(&threshold) {
Ordering::Less => Cell::Alive,
Ordering::Greater => Cell::Dead,
Ordering::Equal => Cell::Dead,
}
}
}
#[wasm_bindgen] // public methods exported to js
impl Universe {
pub fn tick(&mut self) {
// log!("ticking");
// let _timer = time::Timer::new("Universe::tick"); // will stop when dropped
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
let cell = self.cells[idx];
let live_neighbors = self.live_neighbor_count(row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
pub fn new(width: u32, height: u32, rand_threshold: u32, seed: f64) -> Universe {
log!("Generating new board {}x{}", width, height);
let mut rng = Pcg64Mcg::seed_from_u64(seed as u64);
let cells = (0..width * height)
.map(|i| {
#[cfg(not(feature = "random_init"))]
return Universe::populate_cell(i);
#[cfg(feature = "random_init")]
return Universe::populate_cell(i, &mut rng, rand_threshold);
})
.collect();
Universe {
width,
height,
cells,
rng,
rand_threshold,
}
}
pub fn render(&self) -> String {
self.to_string()
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn cells(&self) -> *const Cell {
self.cells.as_ptr()
}
pub fn reset(&mut self) {
self.cells = (0..self.width * self.height)
.map(|i| {
#[cfg(not(feature = "random_init"))]
return Universe::populate_cell(i);
#[cfg(feature = "random_init")]
return Universe::populate_cell(i, &mut self.rng, self.rand_threshold);
})
.collect();
}
}
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for line in self.cells.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '◻' } else { '◼' };
write!(f, "{}", symbol)?;
}
write!(f, "\n")?;
}
Ok(())
}
}

19
src/time.rs Normal file
View File

@ -0,0 +1,19 @@
extern crate web_sys;
use web_sys::console;
pub struct Timer<'a> {
name: &'a str,
}
impl<'a> Timer<'a> {
pub fn new(name: &'a str) -> Timer<'a> {
console::time_with_label(name);
Timer { name }
}
}
impl<'a> Drop for Timer<'a> {
fn drop(&mut self) {
console::time_end_with_label(self.name);
}
}

10
src/utils.rs Normal file
View File

@ -0,0 +1,10 @@
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}

32
tests/web.rs Normal file
View File

@ -0,0 +1,32 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]
extern crate wasm_bindgen_test;
// use wasm_bindgen_test::*;
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
wasm_bindgen_test_configure!(run_in_browser);
extern crate gameoflife;
use gameoflife::Universe;
#[wasm_bindgen_test]
fn get_width() {
let uni = Universe::new(10, 15, 1, 1.0);
assert_eq!(uni.width(), 10);
}
#[wasm_bindgen_test]
fn get_height() {
let uni = Universe::new(10, 15, 1, 1.0);
assert_eq!(uni.height(), 15);
}
#[wasm_bindgen_test]
fn get_cells() {
let uni = Universe::new(10, 15, 1, 1.0);
uni.cells();
}

2
www/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

5
www/bootstrap.js vendored Normal file
View File

@ -0,0 +1,5 @@
// A dependency graph that contains any wasm must all be imported
// asynchronously. This `bootstrap.js` file does the single async import, so
// that no one else needs to worry about it again.
import("./index.js")
.catch(e => console.error("Error importing `index.js`:", e));

236
www/index.js Normal file
View File

@ -0,0 +1,236 @@
import { Universe, Cell, init_game } from "gameoflife";
import { memory } from "gameoflife/gameoflife_bg.wasm";
// let PLAY = true;
// let PLAY = false;
init_game();
const randSlider = document.getElementById("randThreshold");
const randSliderLabel = document.getElementById("randThreshold-label");
const CELL_SIZE = 4; // px
const GRID_COLOR = "#BBBBBB";
const DEAD_COLOR = "#FFFFFF";
const ALIVE_COLOR = "#FF55AA";
let universe = Universe.new(100, 100, randSlider.value, new Date().getTime() / 1000);
let width = universe.width();
let height = universe.height();
let play = false;
const canvas = document.getElementById("game-of-life-canvas");
canvas.height = (CELL_SIZE + 1) * height + 1;
canvas.width = (CELL_SIZE + 1) * width + 1;
const ctx = canvas.getContext('2d');
/**
* Draw grid onto canvas prior to painting cells
*/
const drawGrid = () => {
ctx.beginPath();
ctx.strokeStyle = GRID_COLOR;
// Vertical lines.
for (let i = 0; i <= width; i++) {
ctx.moveTo(i * (CELL_SIZE + 1) + 1, 0);
ctx.lineTo(i * (CELL_SIZE + 1) + 1, (CELL_SIZE + 1) * height + 1);
}
// Horizontal lines.
for (let j = 0; j <= height; j++) {
ctx.moveTo(0, j * (CELL_SIZE + 1) + 1);
ctx.lineTo((CELL_SIZE + 1) * width + 1, j * (CELL_SIZE + 1) + 1);
}
ctx.stroke();
};
/**
* Get linear index from row and column indices
* @param {*} row Row index
* @param {*} column Column index
* @returns Linear index
*/
const getIndex = (row, column) => {
return row * width + column;
};
/**
* Paint alive cells onto grid
*/
const drawCells = () => {
const cellsPtr = universe.cells();
const cells = new Uint8Array(memory.buffer, cellsPtr, width * height);
ctx.beginPath();
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const idx = getIndex(row, col);
ctx.fillStyle = cells[idx] === Cell.Dead
? DEAD_COLOR
: ALIVE_COLOR;
ctx.fillRect(
col * (CELL_SIZE + 1) + 1,
row * (CELL_SIZE + 1) + 1,
CELL_SIZE,
CELL_SIZE
);
}
}
ctx.stroke();
};
/**
* Single frame/step of game, tick universe, refresh UI
*/
const renderSingle = () => {
// fps.render(); //new
universe.tick();
drawGrid();
drawCells();
}
/**
* Start interval timer to periodically iterate frames
*/
const start = () => {
if(loop != null) clearInterval(loop);
loop = setInterval(renderSingle, frameInterval);
}
/**
* Clear interval timer to stop animation loop
*/
const stop = () => {
if(loop != null) clearInterval(loop);
loop = null;
}
var frameInterval = 50;
// var loop = setInterval(renderSingle, frameInterval);
var loop = null;
// SLIDERS
const frameSlider = document.getElementById("frameRate");
const frameSliderLabel = document.getElementById("frameRate-label");
/**
* Handler for frame interval slider change, stop, change interval, start
*/
const onFrameSlider = () => {
stop();
frameInterval = frameSlider.value;
frameSliderLabel.innerHTML = `Frame Interval: ${frameSlider.value}ms`;
if(play) start();
}
frameSlider.onchange = onFrameSlider;
frameSlider.value = 100;
/**
* Handler for random threshold slider change, get a new universe with new threshold
*/
const onRandSlider = () => {
stop();
universe = Universe.new(width, height, randSlider.value, new Date().getTime() / 1000);
refreshCanvas();
randSliderLabel.innerHTML = `Random Threshold: ${randSlider.value}%`;
if(play) start();
}
randSlider.onchange = onRandSlider;
randSlider.value = 50;
/**
* Refresh existing canvas, calculate dimensions and draw
*/
const refreshCanvas = () => {
canvas.width = (CELL_SIZE + 1) * width + 1;
canvas.height = (CELL_SIZE + 1) * height + 1;
drawGrid();
drawCells();
}
// INPUT BOXES
const widthBox = document.getElementById("width");
/**
* Handler for width input box change, get a new universe of given size
*/
const onWidth = () => {
// PLAY = false;
width = widthBox.value;
universe = Universe.new(width, height, randSlider.value, new Date().getTime() / 1000);
refreshCanvas();
// PLAY = true;
// requestAnimationFrame(renderLoop);
}
widthBox.onchange = onWidth;
widthBox.value = 100;
const heightBox = document.getElementById("height");
/**
* Handler for height input box change, get a new universe of given size
*/
const onHeight = () => {
// PLAY = false;
height = heightBox.value;
universe = Universe.new(width, height, randSlider.value, new Date().getTime() / 1000);
refreshCanvas();
// PLAY = true;
// requestAnimationFrame(renderLoop);
}
heightBox.onchange = onHeight;
heightBox.value = 100;
// BUTTONS
/**
* Click handler for step button, make single move
*/
const onPlay = () => {
play = !play;
// console.log("play: " + play);
if(play) {
playButton.classList.remove("btn-success");
playButton.classList.add("btn-danger");
playButton.innerText = "Stop";
start();
}else {
playButton.classList.add("btn-success");
playButton.classList.remove("btn-danger");
playButton.innerText = "Play";
stop();
}
}
const playButton = document.getElementById("play");
playButton.onclick = onPlay;
/**
* Click handler for step button, make single move
*/
const onStep = () => {
console.log("stepping");
renderSingle();
}
document.getElementById("step").onclick = onStep;
/**
* Click handler for reset button, generate a new universe and refresh the canvas
*/
const onReset = () => {
universe = Universe.new(width, height, randSlider.value, new Date().getTime() / 1000);
refreshCanvas();
}
document.getElementById("reset").onclick = onReset;
drawGrid();
drawCells();

6940
www/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
www/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "game-of-life-web",
"version": "0.1.0",
"description": "Rust wasm-based game-of-life",
"main": "index.js",
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "webpack serve --config webpack.config.js --progress"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Sarsoo/game-of-life.git"
},
"keywords": [
"webassembly",
"wasm",
"rust",
"webpack"
],
"author": "sarsoo",
"bugs": {
"url": "https://github.com/Sarsoo/game-of-life/issues"
},
"homepage": "https://github.com/Sarsoo/game-of-life#readme",
"dependencies": {
"gameoflife": "file:../pkg"
},
"devDependencies": {
"copy-webpack-plugin": "^9.0.0",
"webpack": "^5.40.0",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^4.7.4"
}
}

23
www/webpack.config.js Normal file
View File

@ -0,0 +1,23 @@
const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path');
module.exports = {
entry: "./bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
mode: "development",
experiments: {
asyncWebAssembly: true
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "index.html")
}
]
})
],
};