<html>
  <head>
    <title>ProcGen Island</title>
  </head>
  <body>
    <script>
      "use strict";
      (() => {
        // lib/prng.ts
        var UINT_MAX = 4294967295;
        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/data.ts
        var WATER = 0;
        var BEACH = 1;
        var LIGHT_FOREST = 2;
        var DENSE_FOREST = 3;
        var MOUNTAIN = 4;
        var ICECAP = 7;

        // island/generators.ts
        var SMALL_MOUNTAIN = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const edgeTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= BEACH
          );
          islands.dropWithin(edgeTiles);
          const mountainTiles = islands.floodSearch(
            basePos,
            (tile) => tile > MOUNTAIN
          );
          islands.dropWithin(mountainTiles);
          return true;
        };
        var BIG_MOUNTAIN = (islands, basePos) => () => {
          const mountainTiles = islands.floodSearch(
            basePos,
            (tile) => tile > MOUNTAIN
          );
          islands.dropWithin(mountainTiles);
          return mountainTiles.some((pos) => islands.data[pos] == ICECAP);
        };
        var SMALL_BEACH = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const shoreTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= WATER
          );
          islands.dropWithin(shoreTiles);
          return true;
        };
        var BIG_BEACH = (islands, basePos) => {
          let dropped = 0;
          return () => {
            const islandTiles = islands.floodSearch(
              basePos,
              (tile) => tile > WATER
            );
            const shoreTiles = islandTiles.filter(
              (pos) => islands.data[pos] <= WATER
            );
            islands.dropWithin(shoreTiles);
            dropped++;
            const landTiles = islandTiles.filter(
              (pos) => islands.data[pos] > WATER
            );
            return landTiles.length > dropped;
          };
        };
        var SCATTERED_FOREST = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const shoreTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= WATER
          );
          islands.dropWithin(shoreTiles);
          const beachTiles = islandTiles.filter(
            (pos) => islands.data[pos] == BEACH
          );
          islands.dropWithin(beachTiles);
          islands.dropWithin(beachTiles);
          const forestLobe = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const forestTiles = forestLobe.filter(
            (pos) => islands.data[pos] == LIGHT_FOREST
          );
          islands.dropWithin(forestTiles);
          return true;
        };
        var CONTIGUOUS_FOREST = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const shoreTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= WATER
          );
          islands.dropWithin(shoreTiles);
          const beachTiles = islandTiles.filter(
            (pos) => islands.data[pos] == BEACH
          );
          islands.dropWithin(beachTiles);
          const forestLobe = islands.floodSearch(
            basePos,
            (tile) => tile > BEACH
          );
          const forestTiles = forestLobe.filter(
            (pos) => islands.data[pos] == LIGHT_FOREST
          );
          islands.dropWithin(forestTiles);
          islands.dropWithin(forestTiles);
          return true;
        };
        var HILLY_FOREST = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const shoreTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= WATER
          );
          islands.dropWithin(shoreTiles);
          const beachTiles = islandTiles.filter(
            (pos) => islands.data[pos] == BEACH
          );
          islands.dropWithin(beachTiles);
          const centralForest = islands.floodSearch(
            basePos,
            (tile) => tile > BEACH
          );
          islands.dropWithin(centralForest);
          const edgeTiles = centralForest.filter(
            (pos) => islands.data[pos] == LIGHT_FOREST
          );
          islands.dropWithin(edgeTiles);
          const hillTiles = centralForest.filter(
            (pos) => islands.data[pos] >= MOUNTAIN
          );
          return hillTiles.length > 10;
        };
        var ERODED_BEACH = (islands, basePos) => () => {
          const islandTiles = islands.floodSearch(
            basePos,
            (tile) => tile > WATER
          );
          const shoreTiles = islandTiles.filter(
            (pos) => islands.data[pos] <= WATER
          );
          islands.dropWithin(shoreTiles);
          if (islandTiles.length > 1) {
            const erodePos = islandTiles[islands.rng() % islandTiles.length];
            islands.data[erodePos] = Math.max(
              islands.data[erodePos] - 1,
              WATER
            );
          }
          return true;
        };
        var NO_ISLAND = (islands, basePos) => () => {
          return true;
        };
        var SINKHOLE_BURNOUT = 1500;
        var SINKHOLE = (islands, basePos) => {
          let ticks = 0;
          return () => {
            if (ticks++ < SINKHOLE_BURNOUT) {
              const sunk = islands.floodSearch(basePos, (tile) => tile < 0);
              islands.sinkhole(islands.choose(sunk));
            }
            return true;
          };
        };
        var WIDE_SINKHOLE = (islands, basePos) => {
          let ticks = 0;
          return () => {
            if (ticks++ < SINKHOLE_BURNOUT) {
              const sunk = islands.floodSearch(basePos, (tile) => tile < 0);
              const sunkEdge = sunk.filter((pos) => islands.data[pos] >= WATER);
              if (sunkEdge.length > 0) {
                islands.sinkhole(islands.choose(sunkEdge));
              } else {
                islands.sinkhole(islands.choose(sunk));
              }
            }
            return true;
          };
        };
        var BIG_ISLANDS = [BIG_MOUNTAIN, BIG_BEACH, HILLY_FOREST];
        var ROCKY_ISLANDS = [SMALL_MOUNTAIN, BIG_MOUNTAIN, HILLY_FOREST];
        var GREEN_ISLANDS = [SCATTERED_FOREST, CONTIGUOUS_FOREST, HILLY_FOREST];
        var SMALL_ISLANDS = [SMALL_BEACH, SCATTERED_FOREST, ERODED_BEACH];
        var ALL_ISLANDS = [
          SMALL_MOUNTAIN,
          BIG_MOUNTAIN,
          SMALL_BEACH,
          BIG_BEACH,
          SCATTERED_FOREST,
          CONTIGUOUS_FOREST,
          HILLY_FOREST,
          ERODED_BEACH,
        ];
        var VOIDS = [NO_ISLAND, SINKHOLE, WIDE_SINKHOLE];

        // island/grid.ts
        var IslandGrid = class {
          constructor(width, height, seed) {
            this.width = width;
            this.height = height;
            this.data = Array(width * height).fill(WATER);
            this.rng = mulberry32(seed);
            const islandBag = this.shuffle([
              this.choose(BIG_ISLANDS),
              this.choose(ROCKY_ISLANDS),
              this.choose(GREEN_ISLANDS),
              this.choose(SMALL_ISLANDS),
              this.choose(SMALL_ISLANDS),
              this.choose(ALL_ISLANDS),
              this.choose(ALL_ISLANDS),
              this.choose(VOIDS),
              this.choose(VOIDS),
              this.choose(VOIDS),
              this.choose(VOIDS),
              NO_ISLAND,
              NO_ISLAND,
            ]);
            const islandCount = islandBag.length;
            const spacing = (Math.PI * 2) / islandCount;
            const rootX = width / 2;
            const rootY = height / 2;
            const xScale = width / 4;
            const yScale = height / 4;
            for (let i = 0; i < islandCount; i++) {
              const rScale = (this.rng() / UINT_MAX) * 2 - 1;
              const y = rootY + Math.sin(spacing * i) * yScale * rScale;
              const x = rootX + Math.cos(spacing * i) * xScale * rScale;
              this.generators.push(islandBag[i](this, this.xy(x | 0, y | 0)));
            }
          }
          data;
          rng;
          generators = [];
          done = false;
          xy(x, y) {
            return (
              (((x % this.width) + this.width) % this.width) +
              this.width * (((y % this.height) + this.height) % this.height)
            );
          }
          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);
              }
            };
            this.forCardinals(pos, check);
            if (lowerNeighbors.length > 0) {
              const downhill =
                lowerNeighbors[this.rng() % lowerNeighbors.length];
              return this.drop(downhill);
            }
            this.forDiagonals(pos, check);
            if (lowerNeighbors.length > 0) {
              const downhill =
                lowerNeighbors[this.rng() % lowerNeighbors.length];
              return this.drop(downhill);
            }
            ++this.data[pos];
          }
          sinkhole(pos) {
            const higherNeighbors = [];
            this.forNeighbors(pos, (adjPos) => {
              if (this.data[adjPos] > this.data[pos]) {
                higherNeighbors.push(adjPos);
              }
            });
            if (higherNeighbors.length > 0) {
              const uphill =
                higherNeighbors[this.rng() % higherNeighbors.length];
              return this.sinkhole(uphill);
            }
            this.data[pos] = Math.max(this.data[pos] - 1, -3);
          }
          forCardinals(pos, action) {
            action((pos - this.width) % this.data.length);
            action((pos - 1) % this.data.length);
            action((pos + 1) % this.data.length);
            action((pos + this.width) % this.data.length);
          }
          forDiagonals(pos, action) {
            action((pos - this.width - 1) % this.data.length);
            action((pos - this.width + 1) % this.data.length);
            action((pos + this.width - 1) % this.data.length);
            action((pos + this.width + 1) % this.data.length);
          }
          forNeighbors(pos, action) {
            this.forCardinals(pos, action);
            this.forDiagonals(pos, action);
          }
          deepenWater() {
            for (let i = 0; i < this.data.length; i++) {
              if (this.data[i] == WATER) {
                let isShore = false;
                this.forNeighbors(i, (adjPos) => {
                  if (this.data[adjPos] > WATER) {
                    isShore = true;
                  }
                });
                if (!isShore) {
                  this.data[i] = WATER - 1;
                }
              }
            }
          }
          choose(list) {
            if (list.length == 0) {
              throw new Error("Picking from empty list");
            }
            return list[this.rng() % list.length];
          }
          shuffle(list) {
            const shuffled = list.slice();
            for (let i = 0; i < shuffled.length - 1; i++) {
              const swapIndex = (this.rng() % (shuffled.length - i)) + i;
              [shuffled[i], shuffled[swapIndex]] = [
                shuffled[swapIndex],
                shuffled[i],
              ];
            }
            return shuffled;
          }
          dropWithin(tiles) {
            if (tiles.length > 0) {
              this.drop(this.choose(tiles));
            }
          }
          step() {
            this.done = this.generators
              .map((generator) => generator())
              .every((done) => done);
          }
        };

        // island/render.ts
        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 -3:
                case -2:
                case -1:
                  cx.fillStyle = "#000088";
                  break;
                case WATER:
                  cx.fillStyle = "blue";
                  break;
                case BEACH:
                  cx.fillStyle = "yellow";
                  break;
                case LIGHT_FOREST:
                  cx.fillStyle = "#00ff00";
                  break;
                case DENSE_FOREST:
                  cx.fillStyle = "#008800";
                  break;
                case MOUNTAIN:
                case MOUNTAIN + 1:
                case MOUNTAIN + 2:
                  cx.fillStyle = "#666666";
                  break;
                default:
                  cx.fillStyle = "#88aaff";
                  break;
              }
              cx.fillRect(x, y, 1, 1);
            }
          }
        }

        // 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];
        }

        // island.ts
        var BLOWUP = 4;
        var WIDTH = 240;
        var HEIGHT = 135;
        var DEFAULT_SEED = 128;
        function IslandApplet() {
          let timerId;
          let ticks = 0;
          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);
                ticks = 0;
                islands = new IslandGrid(
                  WIDTH,
                  HEIGHT,
                  seedInput.valueAsNumber
                );
                timerId = setInterval(function tick() {
                  islands.step();
                  islands.step();
                  islands.step();
                  ticks += 3;
                  if (islands.done) {
                    clearInterval(timerId);
                    islands.deepenWater();
                  }
                  renderIslands(islands, cx);
                }, 1e3 / 30);
              },
            },
            "Generate"
          );
          renderIslands(islands, cx);
          return [canvas, seedLabel, generateButton];
        }
        globalThis.IslandApplet = IslandApplet;
      })();

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