diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..76efb07
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.vscode
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..78d4464
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,28 @@
+BSD 3-Clause License
+
+Copyright (c) 2022, Leonardo Ciocari
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..85fcb13
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# ssmg
+Simple Seedable Map Generator
diff --git a/dist/ssmg.mjs b/dist/ssmg.mjs
new file mode 100644
index 0000000..ecfc367
--- /dev/null
+++ b/dist/ssmg.mjs
@@ -0,0 +1,117 @@
+var C = Object.defineProperty;
+var O = (i, t, s) => t in i ? C(i, t, { enumerable: !0, configurable: !0, writable: !0, value: s }) : i[t] = s;
+var o = (i, t, s) => (O(i, typeof t != "symbol" ? t + "" : t, s), s);
+class w {
+ constructor(t) {
+ o(this, "N");
+ o(this, "M");
+ o(this, "MATRIX_A");
+ o(this, "UPPER_MASK");
+ o(this, "LOWER_MASK");
+ o(this, "mt");
+ o(this, "mti");
+ o(this, "seed");
+ this.N = 624, this.M = 397, this.MATRIX_A = 2567483615, this.UPPER_MASK = 2147483648, this.LOWER_MASK = 2147483647, this.mt = new Array(this.N), this.mti = this.N + 1, this.seed = t || new Date().getTime(), this.init_genrand(this.seed);
+ }
+ init_genrand(t) {
+ for (this.mt[0] = t >>> 0, this.mti = 1; this.mti < this.N; this.mti++) {
+ const s = this.mt[this.mti - 1] ^ this.mt[this.mti - 1] >>> 30;
+ this.mt[this.mti] = (((s & 4294901760) >>> 16) * 1812433253 << 16) + (s & 65535) * 1812433253 + this.mti, this.mt[this.mti] >>>= 0;
+ }
+ }
+ random() {
+ let t;
+ const s = new Array(0, this.MATRIX_A);
+ if (this.mti >= this.N) {
+ let e;
+ for (this.mti == this.N + 1 && this.init_genrand(5489), e = 0; e < this.N - this.M; e++)
+ t = this.mt[e] & this.UPPER_MASK | this.mt[e + 1] & this.LOWER_MASK, this.mt[e] = this.mt[e + this.M] ^ t >>> 1 ^ s[t & 1];
+ for (; e < this.N - 1; e++)
+ t = this.mt[e] & this.UPPER_MASK | this.mt[e + 1] & this.LOWER_MASK, this.mt[e] = this.mt[e + (this.M - this.N)] ^ t >>> 1 ^ s[t & 1];
+ t = this.mt[this.N - 1] & this.UPPER_MASK | this.mt[0] & this.LOWER_MASK, this.mt[this.N - 1] = this.mt[this.M - 1] ^ t >>> 1 ^ s[t & 1], this.mti = 0;
+ }
+ return t = this.mt[this.mti++], t ^= t >>> 11, t ^= t << 7 & 2636928640, t ^= t << 15 & 4022730752, t ^= t >>> 18, (t >>> 0) * (1 / 4294967296);
+ }
+}
+var I = /* @__PURE__ */ ((i) => (i[i.Grass = 0] = "Grass", i[i.Sand = 1] = "Sand", i[i.Shore = 2] = "Shore", i[i.Ocean = 3] = "Ocean", i[i.Stone = 4] = "Stone", i[i.Tree = 5] = "Tree", i))(I || {});
+class L {
+ constructor({
+ width: t = 100,
+ height: s = 100,
+ smoothLevel: e = 25,
+ elevationMin: f = 0,
+ elevationMax: d = 1e3,
+ percentageStone: g = 0.3,
+ percentageGrass: m = 0.3,
+ percentageSand: a = 0.03,
+ percentageShore: n = 0.08,
+ seed: N = void 0
+ }) {
+ o(this, "tilemap");
+ o(this, "metadata");
+ o(this, "seed");
+ const A = new w(N), S = () => A.random();
+ this.seed = A.seed, this.tilemap = this.generateHeightMap(t, s, f, d, S);
+ for (let h = 0; h < e; h++)
+ this.smoothMap(this.tilemap);
+ let c = d, u = f;
+ for (let h = 0; h < this.tilemap.length; h++)
+ for (let r = 0; r < this.tilemap[h].length; r++) {
+ const l = this.tilemap[h][r];
+ l < c && (c = l), l > u && (u = l);
+ }
+ const M = u - c;
+ this.metadata = {
+ elevationMin: c,
+ elevationMax: u,
+ elevationDelta: M,
+ tilesCount: {
+ stone: 0,
+ grass: 0,
+ sand: 0,
+ shore: 0,
+ ocean: 0,
+ tree: 0
+ }
+ };
+ for (let h = 0; h < this.tilemap.length; h++)
+ for (let r = 0; r < this.tilemap[h].length; r++) {
+ const l = this.tilemap[h][r], R = u - M * g, _ = R - M * m, P = _ - M * a, E = P - M * n, K = c;
+ l >= R ? (this.tilemap[h][r] = 4, this.metadata.tilesCount.stone++) : l >= _ ? this.getRandInt(0, 20, S) === 0 ? (this.tilemap[h][r] = 5, this.metadata.tilesCount.tree++) : (this.tilemap[h][r] = 0, this.metadata.tilesCount.grass++) : l >= P ? (this.tilemap[h][r] = 1, this.metadata.tilesCount.sand++) : l >= E ? (this.tilemap[h][r] = 2, this.metadata.tilesCount.shore++) : l >= K && (this.tilemap[h][r] = 3, this.metadata.tilesCount.ocean++);
+ }
+ }
+ getRandInt(t, s, e) {
+ return s = Math.floor(s), Math.floor(e() * (s - t + 1)) + Math.ceil(t);
+ }
+ generateHeightMap(t, s, e, f, d) {
+ let g = [], m = [];
+ for (let a = 0; a < s; a++) {
+ for (let n = 0; n < t; n++)
+ m.push(this.getRandInt(e, f, d));
+ g.push(m), m = [];
+ }
+ return g;
+ }
+ smoothMap(t) {
+ for (let s = 0; s < t.length; s++)
+ for (let e = 0; e < t[s].length; e++)
+ t[s][e] = this.sumSurroundingValues(t, e, s, !0) / 9;
+ }
+ sumSurroundingValues(t, s, e, f) {
+ let d = 0;
+ for (let g = -1; g <= 1; g++)
+ for (let m = -1; m <= 1; m++) {
+ let a = s + m, n = e + g;
+ if (f)
+ n < 0 && (n += t.length), n > t.length - 1 && (n -= t.length), a < 0 && (a += t[n].length), a > t[n].length - 1 && (a -= t[n].length);
+ else if (n < 0 || n > t.length - 1 || a < 0 || a > t[n].length - 1)
+ continue;
+ d += t[n][a];
+ }
+ return d;
+ }
+}
+export {
+ L as Map,
+ I as mapTileType
+};
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a160ee1
--- /dev/null
+++ b/index.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+ SSMG - Simple Seedable Map Generator
+
+
+
+
+ SSMG - Simple Seedable Map Generator
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..579ae19
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,730 @@
+{
+ "name": "ssmg",
+ "version": "1.0.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "ssmg",
+ "version": "1.0.0",
+ "license": "BSD-3-Clause",
+ "devDependencies": {
+ "typescript": "^4.9.4",
+ "vite": "^4.0.0",
+ "vitest": "^0.25.6"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.16.4",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/chai-subset": {
+ "version": "1.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.11.13",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.8.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chai": {
+ "version": "4.3.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^4.1.2",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.16.4",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.16.4",
+ "@esbuild/android-arm64": "0.16.4",
+ "@esbuild/android-x64": "0.16.4",
+ "@esbuild/darwin-arm64": "0.16.4",
+ "@esbuild/darwin-x64": "0.16.4",
+ "@esbuild/freebsd-arm64": "0.16.4",
+ "@esbuild/freebsd-x64": "0.16.4",
+ "@esbuild/linux-arm": "0.16.4",
+ "@esbuild/linux-arm64": "0.16.4",
+ "@esbuild/linux-ia32": "0.16.4",
+ "@esbuild/linux-loong64": "0.16.4",
+ "@esbuild/linux-mips64el": "0.16.4",
+ "@esbuild/linux-ppc64": "0.16.4",
+ "@esbuild/linux-riscv64": "0.16.4",
+ "@esbuild/linux-s390x": "0.16.4",
+ "@esbuild/linux-x64": "0.16.4",
+ "@esbuild/netbsd-x64": "0.16.4",
+ "@esbuild/openbsd-x64": "0.16.4",
+ "@esbuild/sunos-x64": "0.16.4",
+ "@esbuild/win32-arm64": "0.16.4",
+ "@esbuild/win32-ia32": "0.16.4",
+ "@esbuild/win32-x64": "0.16.4"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.11.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "0.4.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.4.19",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.7.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-literal": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.8.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinypool": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.16.3",
+ "postcss": "^8.4.19",
+ "resolve": "^1.22.1",
+ "rollup": "^3.7.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "0.25.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^4.3.3",
+ "@types/chai-subset": "^1.3.3",
+ "@types/node": "*",
+ "acorn": "^8.8.0",
+ "acorn-walk": "^8.2.0",
+ "chai": "^4.3.6",
+ "debug": "^4.3.4",
+ "local-pkg": "^0.4.2",
+ "source-map": "^0.6.1",
+ "strip-literal": "^1.0.0",
+ "tinybench": "^2.3.1",
+ "tinypool": "^0.3.0",
+ "tinyspy": "^1.0.2",
+ "vite": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": ">=v14.16.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@vitest/browser": "*",
+ "@vitest/ui": "*",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ }
+ },
+ "dependencies": {
+ "@esbuild/linux-x64": {
+ "version": "0.16.4",
+ "dev": true,
+ "optional": true
+ },
+ "@types/chai": {
+ "version": "4.3.4",
+ "dev": true
+ },
+ "@types/chai-subset": {
+ "version": "1.3.3",
+ "dev": true,
+ "requires": {
+ "@types/chai": "*"
+ }
+ },
+ "@types/node": {
+ "version": "18.11.13",
+ "dev": true
+ },
+ "acorn": {
+ "version": "8.8.1",
+ "dev": true
+ },
+ "acorn-walk": {
+ "version": "8.2.0",
+ "dev": true
+ },
+ "assertion-error": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "chai": {
+ "version": "4.3.7",
+ "dev": true,
+ "requires": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^4.1.2",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ }
+ },
+ "check-error": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "deep-eql": {
+ "version": "4.1.3",
+ "dev": true,
+ "requires": {
+ "type-detect": "^4.0.0"
+ }
+ },
+ "esbuild": {
+ "version": "0.16.4",
+ "dev": true,
+ "requires": {
+ "@esbuild/android-arm": "0.16.4",
+ "@esbuild/android-arm64": "0.16.4",
+ "@esbuild/android-x64": "0.16.4",
+ "@esbuild/darwin-arm64": "0.16.4",
+ "@esbuild/darwin-x64": "0.16.4",
+ "@esbuild/freebsd-arm64": "0.16.4",
+ "@esbuild/freebsd-x64": "0.16.4",
+ "@esbuild/linux-arm": "0.16.4",
+ "@esbuild/linux-arm64": "0.16.4",
+ "@esbuild/linux-ia32": "0.16.4",
+ "@esbuild/linux-loong64": "0.16.4",
+ "@esbuild/linux-mips64el": "0.16.4",
+ "@esbuild/linux-ppc64": "0.16.4",
+ "@esbuild/linux-riscv64": "0.16.4",
+ "@esbuild/linux-s390x": "0.16.4",
+ "@esbuild/linux-x64": "0.16.4",
+ "@esbuild/netbsd-x64": "0.16.4",
+ "@esbuild/openbsd-x64": "0.16.4",
+ "@esbuild/sunos-x64": "0.16.4",
+ "@esbuild/win32-arm64": "0.16.4",
+ "@esbuild/win32-ia32": "0.16.4",
+ "@esbuild/win32-x64": "0.16.4"
+ }
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "get-func-name": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "is-core-module": {
+ "version": "2.11.0",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "local-pkg": {
+ "version": "0.4.2",
+ "dev": true
+ },
+ "loupe": {
+ "version": "2.3.6",
+ "dev": true,
+ "requires": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "nanoid": {
+ "version": "3.3.4",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "dev": true
+ },
+ "pathval": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "postcss": {
+ "version": "8.4.19",
+ "dev": true,
+ "requires": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "resolve": {
+ "version": "1.22.1",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "rollup": {
+ "version": "3.7.2",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "dev": true
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "strip-literal": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.8.1"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "tinybench": {
+ "version": "2.3.1",
+ "dev": true
+ },
+ "tinypool": {
+ "version": "0.3.0",
+ "dev": true
+ },
+ "tinyspy": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "dev": true
+ },
+ "typescript": {
+ "version": "4.9.4",
+ "dev": true
+ },
+ "vite": {
+ "version": "4.0.0",
+ "dev": true,
+ "requires": {
+ "esbuild": "^0.16.3",
+ "fsevents": "~2.3.2",
+ "postcss": "^8.4.19",
+ "resolve": "^1.22.1",
+ "rollup": "^3.7.0"
+ }
+ },
+ "vitest": {
+ "version": "0.25.7",
+ "dev": true,
+ "requires": {
+ "@types/chai": "^4.3.3",
+ "@types/chai-subset": "^1.3.3",
+ "@types/node": "*",
+ "acorn": "^8.8.0",
+ "acorn-walk": "^8.2.0",
+ "chai": "^4.3.6",
+ "debug": "^4.3.4",
+ "local-pkg": "^0.4.2",
+ "source-map": "^0.6.1",
+ "strip-literal": "^1.0.0",
+ "tinybench": "^2.3.1",
+ "tinypool": "^0.3.0",
+ "tinyspy": "^1.0.2",
+ "vite": "^3.0.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c7f1eca
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "ssmg",
+ "description": "Simple Seedable Map Generator",
+ "version": "1.0.3",
+ "license": "BSD-3-Clause",
+ "author": "Leonardo Ciocari",
+ "type": "module",
+ "main": "dist/ssmg.mjs",
+ "keywords": [
+ "map",
+ "terrain",
+ "world",
+ "generator",
+ "tilemap"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/themrleon/ssmg"
+ },
+ "scripts": {
+ "demo": "vite --host",
+ "test": "vitest",
+ "build": "tsc && vite build"
+ },
+ "files": [
+ "/dist"
+ ],
+ "devDependencies": {
+ "typescript": "^4.9.4",
+ "vite": "^4.0.0",
+ "vitest": "^0.25.6"
+ }
+}
diff --git a/src/demo.ts b/src/demo.ts
new file mode 100644
index 0000000..f4cfe11
--- /dev/null
+++ b/src/demo.ts
@@ -0,0 +1,52 @@
+import { Map, mapOptions, mapTileType } from './map'
+
+const ctx: any = (document.getElementById('canvas') as HTMLCanvasElement).getContext('2d');
+const tileColor = {
+ [mapTileType.Grass]: 'green',
+ [mapTileType.Sand]: 'yellow',
+ [mapTileType.Shore]: 'cyan',
+ [mapTileType.Ocean]: 'blue',
+ [mapTileType.Stone]: 'gray',
+ [mapTileType.Tree]: 'brown',
+}
+updateMap();
+
+document.querySelector('#button').addEventListener("click", function () {
+ updateMap();
+});
+
+function getElementValueAsNumber(id: string) {
+ return Number(document.querySelector(id)?.value)
+}
+
+function updateMap() {
+ const width: number = getElementValueAsNumber('#width') || 100;
+ const height: number = getElementValueAsNumber('#height') || 100;
+ const smoothLevel: number = getElementValueAsNumber('#smoothLevel') || 25;
+ const elevationMax: number = getElementValueAsNumber('#elevationMax') || 500;
+ const elevationMin: number = getElementValueAsNumber('#elevationMin') || 0;
+ const tilePixelSize: number = getElementValueAsNumber('#tilePixelSize') || 2;
+ const percentageGrass: number = getElementValueAsNumber('#percentageGrass') || 0.3;
+ const percentageStone: number = getElementValueAsNumber('#percentageStone') || 0.3;
+ const percentageSand: number = getElementValueAsNumber('#percentageSand') || 0.03;
+ const percentageShore: number = getElementValueAsNumber('#percentageShore') || 0.08;
+ const seed: number = getElementValueAsNumber('#seed');
+ const options: mapOptions = { width, height, smoothLevel, elevationMax, elevationMin, percentageGrass, percentageStone, percentageSand, percentageShore, seed };
+ const map = new Map(options);
+ (document.getElementById('json-input') as HTMLElement).innerHTML = `${JSON.stringify(options, null, 2)}`;
+ (document.getElementById('json-output') as HTMLElement).innerHTML = `${JSON.stringify({ metadata: map.metadata, seed: map.seed, tilemap: "Check browser's console!" }, null, 2)}`
+
+ console.log('--------------------------------');
+ console.log('Map input object', options);
+ console.log('Map output object\n', map);
+
+ ctx.canvas.width = width * tilePixelSize;
+ ctx.canvas.height = height * tilePixelSize;
+
+ for (let y = 0; y < map.tilemap.length; y++) {
+ for (let x = 0; x < map.tilemap[0].length; x++) {
+ ctx.fillStyle = tileColor[map.tilemap[y][x] as mapTileType];
+ ctx.fillRect(x * tilePixelSize, y * tilePixelSize, tilePixelSize, tilePixelSize)
+ }
+ }
+}
diff --git a/src/map.test.ts b/src/map.test.ts
new file mode 100644
index 0000000..73258b1
--- /dev/null
+++ b/src/map.test.ts
@@ -0,0 +1,70 @@
+import { describe, expect, it, vitest } from 'vitest'
+import { Map } from './map'
+
+describe('Map generation tests', () => {
+ it('Should accept just width and height', () => {
+ expect(new Map({ width: 10, height: 10 })).toBeDefined()
+ })
+
+ it('Should return random map if no seed specified', () => {
+ const map = new Map({ width: 10, height: 10 })
+ // If both maps are created at exact same timestamp they would be equal!
+ vitest
+ .useFakeTimers()
+ .setSystemTime(new Date('2022-01-01'));
+ expect(
+ map
+ ).not.toMatchObject(
+ new Map({ width: 10, height: 10 })
+ );
+ })
+
+ it('Should return same map for same seed', () => {
+ expect(
+ new Map({ width: 10, height: 10, seed: 123 })
+ ).toMatchObject(
+ new Map({ width: 10, height: 10, seed: 123 })
+ );
+ })
+
+ it('Should return map with the requested dimensions', () => {
+ const map = new Map({ width: 100, height: 100 })
+ expect(map.tilemap.length).toEqual(100)
+ expect(map.tilemap[0].length).toEqual(100)
+ })
+
+ it('Should generate known map scenario', () => {
+ expect(
+ new Map({ width: 100, height: 100, seed: 456 }).metadata
+ ).toMatchObject({
+ "elevationMin": 465.55372231640536,
+ "elevationMax": 539.1652840841607,
+ "elevationDelta": 73.61156176775535,
+ "tilesCount": { "stone": 1242, "grass": 5956, "sand": 513, "shore": 1042, "ocean": 976, "tree": 271 }
+ })
+ })
+
+ it('Should respect elevation constraints', () => {
+ const map = new Map({ width: 10, height: 10, elevationMax: 10, elevationMin: 0 })
+ expect(map.metadata.elevationMin).toBeGreaterThanOrEqual(0)
+ expect(map.metadata.elevationMax).toBeLessThanOrEqual(10)
+ })
+
+ it('Should have the correct elevation delta calculation', () => {
+ const map = new Map({ width: 10, height: 10, elevationMax: 10, elevationMin: 0 })
+ expect(map.metadata.elevationDelta).toEqual(map.metadata.elevationMax - map.metadata.elevationMin)
+ })
+
+ it('Should generate mostly ocean map', () => {
+ const map = new Map({ width: 100, height: 100, elevationMax: 5, elevationMin: 0, percentageStone: 0 })
+ expect(map.metadata.tilesCount.ocean).toBeGreaterThan(map.metadata.tilesCount.grass)
+ expect(map.metadata.tilesCount.ocean).toBeGreaterThan(map.metadata.tilesCount.sand)
+ expect(map.metadata.tilesCount.ocean).toBeGreaterThan(map.metadata.tilesCount.shore)
+ expect(map.metadata.tilesCount.ocean).toBeGreaterThan(map.metadata.tilesCount.tree)
+ })
+
+ it('Should generate ocean map', () => {
+ const map = new Map({ width: 100, height: 100, percentageStone: -1 })
+ expect(map.metadata.tilesCount.ocean).toBeGreaterThan(1000)
+ })
+})
diff --git a/src/map.ts b/src/map.ts
new file mode 100644
index 0000000..32b2d0a
--- /dev/null
+++ b/src/map.ts
@@ -0,0 +1,256 @@
+// Extracted and modified from https://github.com/goddtriffin/Oasis
+/*
+BSD 3-Clause License
+
+Copyright (c) 2018, Todd Griffin
+Copyright (c) 2022, Leonardo Ciocari
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+import { MersenneTwister } from "./rng";
+
+export enum mapTileType {
+ Grass,
+ Sand,
+ Shore,
+ Ocean,
+ Stone,
+ Tree
+}
+
+export interface mapOptions {
+ width: number;
+ height: number;
+ smoothLevel?: number;
+ elevationMin?: number;
+ elevationMax?: number;
+ percentageStone?: number;
+ percentageGrass?: number;
+ percentageSand?: number;
+ percentageShore?: number;
+ seed?: number;
+}
+
+export interface mapMetadata {
+ elevationMin: number;
+ elevationMax: number;
+ elevationDelta: number;
+ tilesCount: {
+ stone: number;
+ grass: number;
+ sand: number;
+ shore: number;
+ ocean: number;
+ tree: number;
+ }
+}
+
+export class Map {
+ tilemap: number[][];
+ metadata: mapMetadata;
+ seed: number;
+
+ /**
+ * Generates a map based on the following options:
+ * @param width Width of the map
+ * @param height Height of the map
+ * @param smoothLevel How smooth the map will be, low values = smoother, high values = sharper
+ * @param elevationMin Minimal elevation of the map, used for the ocean calculation
+ * @param elevationMax Maximum elevation of the map, used for the ocean calculation
+ * @param percentageGrass Percentage of the grass to be generated, float between 0 and 1
+ * @param percentageStone Percentage of the stone to be generated, float between 0 and 1
+ * @param percentageSand Percentage of the sand to be generated, float between 0 and 1
+ * @param percentageShore Percentage of the shore to be generated, float between 0 and 1
+ */
+ constructor({
+ width = 100,
+ height = 100,
+ smoothLevel = 25,
+ elevationMin = 0,
+ elevationMax = 1000,
+ percentageStone = 0.30,
+ percentageGrass = 0.30,
+ percentageSand = 0.03,
+ percentageShore = 0.08,
+ seed = 0
+ }: mapOptions) {
+ const mersenneTwister = new MersenneTwister(seed)
+ const rng = () => mersenneTwister.random();
+ this.seed = mersenneTwister.seed;
+ this.tilemap = this.generateHeightMap(width, height, elevationMin, elevationMax, rng);
+
+ for (let i = 0; i < smoothLevel; i++) {
+ this.smoothMap(this.tilemap);
+ }
+
+ let minHeight = elevationMax;
+ let maxHeight = elevationMin;
+ for (let y = 0; y < this.tilemap.length; y++) {
+ for (let x = 0; x < this.tilemap[y].length; x++) {
+ const height = this.tilemap[y][x];
+ if (height < minHeight) {
+ minHeight = height;
+ }
+ if (height > maxHeight) {
+ maxHeight = height;
+ }
+ }
+ }
+
+ const deltaHeight = maxHeight - minHeight;
+ this.metadata = {
+ elevationMin: minHeight,
+ elevationMax: maxHeight,
+ elevationDelta: deltaHeight,
+ tilesCount: {
+ stone: 0,
+ grass: 0,
+ sand: 0,
+ shore: 0,
+ ocean: 0,
+ tree: 0,
+ }
+ }
+
+ for (let y = 0; y < this.tilemap.length; y++) {
+ for (let x = 0; x < this.tilemap[y].length; x++) {
+ const height = this.tilemap[y][x];
+ const stonePercentage = maxHeight - (deltaHeight * percentageStone);
+ const grassPercentage = stonePercentage - (deltaHeight * percentageGrass);
+ const sandPercentage = grassPercentage - (deltaHeight * percentageSand);
+ const shorePercentage = sandPercentage - (deltaHeight * percentageShore);
+ const oceanPercentage = minHeight;
+
+ if (height >= stonePercentage) {
+ this.tilemap[y][x] = mapTileType.Stone;
+ this.metadata.tilesCount.stone++;
+ } else if (height >= grassPercentage) {
+ if (this.getRandInt(0, 20, rng) === 0) {
+ this.tilemap[y][x] = mapTileType.Tree;
+ this.metadata.tilesCount.tree++;
+ } else {
+ this.tilemap[y][x] = mapTileType.Grass;
+ this.metadata.tilesCount.grass++;
+ }
+ } else if (height >= sandPercentage) {
+ this.tilemap[y][x] = mapTileType.Sand;
+ this.metadata.tilesCount.sand++;
+ } else if (height >= shorePercentage) {
+ this.tilemap[y][x] = mapTileType.Shore;
+ this.metadata.tilesCount.shore++;
+ } else if (height >= oceanPercentage) {
+ this.tilemap[y][x] = mapTileType.Ocean;
+ this.metadata.tilesCount.ocean++;
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates random height map based on the following options:
+ * @param width Width of the map
+ * @param height Height of the map
+ * @param min Minimum height (inclusive)
+ * @param max Maximum height (inclusive)
+ * @param rng Random number generator function
+ * @returns A random integer number
+ */
+ private getRandInt(min: number, max: number, rng: any): number {
+ max = Math.floor(max);
+ return Math.floor(rng() * (max - min + 1)) + Math.ceil(min);
+ }
+
+ /**
+ * Generates random height map based on the following options:
+ * @param width Width of the map
+ * @param height Height of the map
+ * @param min Minimum height (inclusive)
+ * @param max Maximum height (inclusive)
+ * @param rng Random number generator function
+ * @returns 2D matrix of tile heights
+ */
+ private generateHeightMap(width: number, height: number, min: number, max: number, rng: any): number[][] {
+ let map: number[][] = [];
+ let column = [];
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ column.push(this.getRandInt(min, max, rng));
+ }
+ map.push(column);
+ column = []
+ }
+ return map;
+ }
+
+ /**
+ * Smooths a given map by setting all points' values as the average of all surrounding point values (including the one being looked at)
+ * @param map Map object
+ */
+ private smoothMap(map: number[][]): void {
+ for (let y = 0; y < map.length; y++) {
+ for (let x = 0; x < map[y].length; x++) {
+ map[y][x] = this.sumSurroundingValues(map, x, y, true) / 9;
+ }
+ }
+ }
+
+ /**
+ * Returns the sum of all the values in the 3x3 grid surrounding the given point in the map
+ * @param map Map object
+ * @param x x coordinate of the point in the map
+ * @param y y coordinate of the point in the map
+ * @param worldWrap Wrap points outside the bounds of the map back into it ie: (-1,-1) will become (map.width-1, map.height-1)
+ * @returns The sum of the surrounding values
+ */
+ private sumSurroundingValues(map: number[][], x: number, y: number, worldWrap: boolean): number {
+ let sum: number = 0;
+ for (let deltaY = -1; deltaY <= 1; deltaY++) {
+ for (let deltaX = -1; deltaX <= 1; deltaX++) {
+ let sumX = x + deltaX;
+ let sumY = y + deltaY;
+ if (worldWrap) {
+ if (sumY < 0) {
+ sumY += map.length;
+ }
+ if (sumY > map.length - 1) {
+ sumY -= map.length;
+ }
+ if (sumX < 0) {
+ sumX += map[sumY].length;
+ }
+ if (sumX > map[sumY].length - 1) {
+ sumX -= map[sumY].length;
+ }
+ } else if (sumY < 0 || sumY > map.length - 1 || sumX < 0 || sumX > map[sumY].length - 1) {
+ continue;
+ }
+ sum += map[sumY][sumX];
+ }
+ }
+ return sum;
+ }
+}
diff --git a/src/rng.test.ts b/src/rng.test.ts
new file mode 100644
index 0000000..d267adc
--- /dev/null
+++ b/src/rng.test.ts
@@ -0,0 +1,35 @@
+import { MersenneTwister } from './rng';
+import { describe, expect, it, vitest } from 'vitest'
+
+describe('RNG tests', () => {
+ it('Should always return same results for same seed', () => {
+ expect(new MersenneTwister(1234).random()).toEqual(0.19151945016346872);
+ expect(new MersenneTwister(1234).random()).toEqual(0.19151945016346872);
+ })
+
+ it('Should always return different results for different seeds', () => {
+ expect(new MersenneTwister(1234).random()).toEqual(0.19151945016346872);
+ expect(new MersenneTwister(12345).random()).not.toEqual(0.19151945016346872);
+ })
+
+ it('Should always return the same sequence for same seed', () => {
+ const rng1 = new MersenneTwister(1234)
+ const rng2 = new MersenneTwister(1234)
+ const f1 = rng1.random()
+ expect(f1).toEqual(rng2.random());
+ expect(f1).not.toEqual(rng2.random());
+ rng1.random() // Catch up with rng 2
+ expect(rng1.random()).toEqual(rng2.random());
+ expect(rng1.random()).toEqual(rng2.random());
+ })
+
+ it('Should always return different results when no seed specified', () => {
+ const rng1 = new MersenneTwister();
+ // If both rngs are created at exact same timestamp they would be equal!
+ vitest
+ .useFakeTimers()
+ .setSystemTime(new Date('2022-01-01'));
+ const rng2 = new MersenneTwister();
+ expect(rng1.random()).not.toEqual(rng2.random());
+ })
+})
diff --git a/src/rng.ts b/src/rng.ts
new file mode 100644
index 0000000..66e38da
--- /dev/null
+++ b/src/rng.ts
@@ -0,0 +1,89 @@
+// Extracted and modified from https://gist.github.com/banksean/300494
+/*
+ A C-program for MT19937, with initialization improved 2002/1/26.
+ Coded by Takuji Nishimura and Makoto Matsumoto.
+
+ Copyright (c) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura
+ Copyright (c) 2009, Sean McCullough
+ Copyright (c) 2022, Leonardo Ciocari
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. The names of its contributors may not be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+export class MersenneTwister {
+ N: number = 624;
+ M: number = 397;
+ MATRIX_A: number = 0x9908b0df;
+ UPPER_MASK: number = 0x80000000;
+ LOWER_MASK: number = 0x7fffffff;
+ mt: number[] = new Array(this.N);
+ mti: number = this.N + 1;
+ seed: number;
+
+ constructor(seed?: number) {
+ this.seed = seed ? seed : new Date().getTime();
+ this.init_genrand(this.seed);
+ }
+
+ private init_genrand(s: number) {
+ this.mt[0] = s >>> 0;
+ for (this.mti = 1; this.mti < this.N; this.mti++) {
+ const s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
+ this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253) + this.mti;
+ this.mt[this.mti] >>>= 0;
+ }
+ }
+
+ random(): number {
+ let y;
+ const mag01 = new Array(0x0, this.MATRIX_A);
+ if (this.mti >= this.N) {
+ let kk;
+ if (this.mti === this.N + 1) {
+ this.init_genrand(5489);
+ }
+ for (kk = 0; kk < this.N - this.M; kk++) {
+ y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
+ this.mt[kk] = this.mt[kk + this.M] ^ (y >>> 1) ^ mag01[y & 0x1];
+ }
+ for (; kk < this.N - 1; kk++) {
+ y = (this.mt[kk] & this.UPPER_MASK) | (this.mt[kk + 1] & this.LOWER_MASK);
+ this.mt[kk] = this.mt[kk + (this.M - this.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
+ }
+ y = (this.mt[this.N - 1] & this.UPPER_MASK) | (this.mt[0] & this.LOWER_MASK);
+ this.mt[this.N - 1] = this.mt[this.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];
+ this.mti = 0;
+ }
+ y = this.mt[this.mti++];
+ y ^= (y >>> 11);
+ y ^= (y << 7) & 0x9d2c5680;
+ y ^= (y << 15) & 0xefc60000;
+ y ^= (y >>> 18);
+ return (y >>> 0) * (1.0 / 4294967296.0);
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..eac16d1
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..15e9bcb
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,12 @@
+import { resolve } from 'path'
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'src/map.ts'),
+ formats: ['es'],
+ fileName: () => 'ssmg.mjs',
+ }
+ },
+})