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

+
+
+
+ Map Options +
+
+ + 5 + Pixels per tile* +
+
+ + 100 + Width +
+
+ + 100 + Height +
+
+ + 25 + smoothLevel +
+
+ + 500 + elevationMax +
+
+ + 0 + elevationMin +
+
+ + 0.3 + percentageGrass +
+
+ + 0.3 + percentageStone +
+
+ + 0.03 + percentageSand +
+
+ + 0.07 + percentageShore +
+
+ +

+ *Not part of the library, it's just a convenience here! +
+
+
+
+ Input +
+

+        
+
+
+ Output +
+

+        
+
+
+ + + + 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', + } + }, +})