<html>
  <head>
    <title>ProcGen Island</title>
  </head>
  <body>
    <script>
      "use strict";
      (() => {
        // lib/html.ts
        function h(name, props, ...children) {
          const element = Object.assign(document.createElement(name), props);
          element.append(...children);
          return element;
        }
        function canvas2d(props) {
          const canvas = h("canvas", props);
          const cx = canvas.getContext("2d");
          if (!cx) {
            throw new Error("2D rendering context not supported");
          }
          return [canvas, cx];
        }

        // lib/prng.ts
        function mulberry32(state) {
          return function () {
            let t = (state += 1831565813);
            t = Math.imul(t ^ (t >>> 15), t | 1);
            t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
            return (t ^ (t >>> 14)) >>> 0;
          };
        }

        // island.ts
        var BLOWUP = 4;
        var WIDTH = 240;
        var HEIGHT = 135;
        var DEFAULT_SEED = 128;
        function dim(width, height) {
          return function xy(x, y) {
            return (
              (((x % width) + width) % width) +
              width * (((y % height) + height) % height)
            );
          };
        }
        var IslandGrid = class {
          constructor(width, height, seed) {
            this.width = width;
            this.height = height;
            this.data = Array(width * height).fill(0);
            this.rng = mulberry32(seed);
            this.xy = dim(width, height);
            this.basePos = this.data.length >> 1;
            this.lobePos1 = this.xy(
              (WIDTH >> 1) + (this.rng() % 48) - 24,
              (HEIGHT >> 1) + (this.rng() % 48) - 24
            );
            this.lobePos2 = this.xy(
              (WIDTH >> 1) + (this.rng() % 48) - 24,
              (HEIGHT >> 1) + (this.rng() % 48) - 24
            );
          }
          data;
          rng;
          xy;
          basePos;
          lobePos1;
          lobePos2;
          done = false;
          get(x, y) {
            return this.data[this.xy(x, y)];
          }
          set(x, y, tile) {
            this.data[this.xy(x, y)] = tile;
            console.log(x, y, this.xy(x, y), this.data[this.xy(x, y)]);
          }
          floodSearch(startPos, shouldExpand) {
            const len = this.data.length;
            const width = this.width;
            const seen = new Uint8Array(len);
            const hitPositions = [];
            function enqueue(pos) {
              if (!seen[pos]) {
                hitPositions.push(pos);
                seen[pos] = 1;
              }
            }
            enqueue(startPos);
            for (let i = 0; i < hitPositions.length; i++) {
              const pos = hitPositions[i];
              if (shouldExpand(this.data[pos])) {
                enqueue((pos - width) % len);
                enqueue((pos - 1) % len);
                enqueue((pos + 1) % len);
                enqueue((pos + width) % len);
              }
            }
            return hitPositions;
          }
          drop(pos) {
            const lowerNeighbors = [];
            const check = (adjPos) => {
              if (this.data[adjPos] < this.data[pos]) {
                lowerNeighbors.push(adjPos);
              }
            };
            check((pos - this.width) % this.data.length);
            check((pos - 1) % this.data.length);
            check((pos + 1) % this.data.length);
            check((pos + this.width) % this.data.length);
            if (lowerNeighbors.length > 0) {
              const downhill =
                lowerNeighbors[this.rng() % lowerNeighbors.length];
              return this.drop(downhill);
            }
            check((pos - this.width - 1) % this.data.length);
            check((pos - this.width + 1) % this.data.length);
            check((pos + this.width - 1) % this.data.length);
            check((pos + this.width + 1) % this.data.length);
            if (lowerNeighbors.length > 0) {
              const downhill =
                lowerNeighbors[this.rng() % lowerNeighbors.length];
              return this.drop(downhill);
            }
            const newValue = ++this.data[pos];
            if (newValue == 9) {
              this.done = true;
            }
          }
          dropWithin(tiles) {
            if (tiles.length > 0) {
              this.drop(tiles[this.rng() % tiles.length]);
            }
          }
          step() {
            const lowlandTiles1 = this.floodSearch(
              this.lobePos1,
              (tile) => tile > 0
            );
            const lowlandTiles2 = this.floodSearch(
              this.lobePos2,
              (tile) => tile > 0
            );
            const shoreTiles1 = lowlandTiles1.filter(
              (pos) => this.data[pos] == 0
            );
            this.dropWithin(shoreTiles1);
            const shoreTiles2 = lowlandTiles2.filter(
              (pos) => this.data[pos] == 0
            );
            this.dropWithin(shoreTiles2);
            const beachTiles = lowlandTiles1.filter(
              (pos) => this.data[pos] == 1
            );
            this.dropWithin(beachTiles);
            const forestLobe = this.floodSearch(
              this.lobePos1,
              (tile) => tile > 1
            );
            const forestTiles = forestLobe.filter((pos) => this.data[pos] == 2);
            this.dropWithin(forestTiles);
            this.dropWithin(forestTiles);
            this.dropWithin(forestTiles);
            const mountainTiles = this.floodSearch(
              this.basePos,
              (tile) => tile > 4
            );
            this.dropWithin(mountainTiles);
          }
        };
        function renderIslands(islands, cx) {
          for (let y = 0; y < islands.height; y++) {
            for (let x = 0; x < islands.width; x++) {
              const tile = islands.data[islands.xy(x, y)];
              switch (tile) {
                case 0:
                  cx.fillStyle = "blue";
                  break;
                case 1:
                  cx.fillStyle = "yellow";
                  break;
                case 2:
                  cx.fillStyle = "#00ff00";
                  break;
                case 3:
                  cx.fillStyle = "#008800";
                  break;
                case 4:
                case 5:
                case 6:
                case 7:
                case 8:
                  cx.fillStyle = "#666666";
                  break;
                default:
                  cx.fillStyle = "#88aaff";
                  break;
              }
              cx.fillRect(x, y, 1, 1);
            }
          }
        }
        function IslandApplet() {
          let timerId;
          let islands = new IslandGrid(WIDTH, HEIGHT, DEFAULT_SEED);
          const [canvas, cx] = canvas2d({
            width: WIDTH * BLOWUP,
            height: HEIGHT * BLOWUP,
          });
          cx.scale(BLOWUP, BLOWUP);
          const seedInput = h("input", {
            type: "number",
            valueAsNumber: DEFAULT_SEED,
          });
          const seedLabel = h("label", {}, "Seed:", seedInput);
          const generateButton = h(
            "button",
            {
              onclick: () => {
                clearInterval(timerId);
                islands = new IslandGrid(
                  WIDTH,
                  HEIGHT,
                  seedInput.valueAsNumber
                );
                timerId = setInterval(function tick() {
                  islands.step();
                  islands.step();
                  islands.step();
                  if (islands.done) {
                    clearInterval(timerId);
                  }
                  renderIslands(islands, cx);
                }, 1e3 / 30);
              },
            },
            "Generate"
          );
          renderIslands(islands, cx);
          return [canvas, seedLabel, generateButton];
        }
        globalThis.IslandApplet = IslandApplet;
      })();

      document.body.append(...IslandApplet());
    </script>
  </body>
</html>