← Mike Higgins Home
Custom Code Experiment •

Faction Mosaic 1

Source Code View HTML / CSS / JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
  <title>Faction Mosaic 1</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
      user-select: none;
    }

    :root {
      color-scheme: light dark;
      background-color: light-dark(#eaeef3, #0a0c12);
    }

    body {
      margin: 0;
      padding: 0;
      overflow: hidden;
      background-color: inherit;
      height: 100vh;
      width: 100vw;
      position: fixed;
      top: 0;
      left: 0;
    }

    canvas {
      display: block;
      cursor: pointer;
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  </style>
</head>
<body>
  <script>
    (function(){
      let FactionCount = 4;
      const simSize = 100;
      const stability = 12;
      const mutations = 3;
      const mutationRate = 60;
      const maxFPS = 24;

      const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
      const idx2coord = (index, width = simSize) => [index % width, Math.floor(index / width)];

      const initPalette = (length, hueOffset = randomInt(0, 240)) => {
        return Array.from({ length }, (_, i) => {
          const lightness = Math.round(48 + (i / length) * 28);
          const chroma = 62 + Math.floor(Math.random() * 12);
          const hue = (hueOffset + (360 / length) * i) % 360;
          return `oklch(${lightness}% ${chroma}% ${hue}deg / 0.85)`;
        });
      };

      const initWins = (length) => Array.from({ length }, (_, i) => (i - 1 + length) % length);

      const NeighborOffsets = [
        [-1, -1], [0, -1], [1, -1],
        [-1, 0], [1, 0],
        [-1, 1], [0, 1], [1, 1]
      ];
      const OffsetToIndex = [
        -1 - simSize, 0 - simSize, 1 - simSize,
        -1, 1,
        -1 + simSize, simSize, 1 + simSize
      ];

      const getNeighbors = (index, array) => {
        const [u, v] = idx2coord(index);
        const neighbors = new Array(NeighborOffsets.length);
        for (let k = 0; k < NeighborOffsets.length; k++) {
          const dx = NeighborOffsets[k][0];
          const dy = NeighborOffsets[k][1];
          const nx = (u + dx + simSize) % simSize;
          const ny = (v + dy + simSize) % simSize;
          neighbors[k] = array[nx + ny * simSize];
        }
        return neighbors;
      };

      let grid = Array.from({ length: simSize ** 2 }, () => randomInt(0, FactionCount - 1));
      let Palette = initPalette(FactionCount);
      let Wins = initWins(FactionCount);

      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');
      document.body.appendChild(canvas);

      function resizeCanvasToDisplay() {
        const w = window.innerWidth;
        const h = window.innerHeight;
        canvas.width = w;
        canvas.height = h;
        draw();
      }

      window.addEventListener('resize', () => {
        resizeCanvasToDisplay();
      });

      const randomize = () => {
        FactionCount = randomInt(3, 5);
        grid = Array.from({ length: simSize ** 2 }, () => randomInt(0, FactionCount - 1));
        Palette = initPalette(FactionCount);
        Wins = initWins(FactionCount);
        draw();
      };

      let interval = setInterval(() => randomize(), 10000);

      canvas.addEventListener('click', () => {
        randomize();
        if (interval) {
          clearInterval(interval);
          interval = null;
        }
      });

      const step = () => {
        const scores = new Array(grid.length);
        for (let i = 0; i < grid.length; i++) {
          const cell = grid[i];
          const neighbors = getNeighbors(i, grid);
          let total = 0;
          for (let n = 0; n < neighbors.length; n++) {
            const nb = neighbors[n];
            if (cell === nb) {
              if (randomInt(0, stability) === 0) {
                total += randomInt(-4, 6);
              }
            } else {
              total += (Wins[cell] === nb) ? 1 : -1;
            }
          }
          scores[i] = total;
        }

        const newGrid = new Array(grid.length);
        for (let i = 0; i < grid.length; i++) {
          if (scores[i] >= 0) {
            newGrid[i] = grid[i];
          } else {
            const neighborScores = getNeighbors(i, scores);
            let bestScore = -Infinity;
            let bestIdx = 0;
            for (let k = 0; k < neighborScores.length; k++) {
              if (neighborScores[k] > bestScore) {
                bestScore = neighborScores[k];
                bestIdx = k;
              }
            }
            const neighborIndex = (i + OffsetToIndex[bestIdx] + grid.length) % grid.length;
            newGrid[i] = grid[neighborIndex];
          }
        }
        grid = newGrid;

        if (randomInt(0, mutationRate) === 0) {
          for (let m = 0; m < mutations; m++) {
            const center = randomInt(0, grid.length - 1);
            const newColor = randomInt(0, FactionCount - 1);
            for (let k = 0; k < OffsetToIndex.length; k++) {
              const idx = (grid.length + center + OffsetToIndex[k]) % grid.length;
              grid[idx] = newColor;
            }
            grid[center] = newColor;
          }
        }
      };

      const draw = () => {
        if (!ctx) return;
        const w = canvas.width;
        const h = canvas.height;
        if (w === 0 || h === 0) return;

        const cellW = w / simSize;
        const cellH = h / simSize;
        ctx.clearRect(0, 0, w, h);

        for (let i = 0; i < grid.length; i++) {
          const faction = grid[i];
          const [col, row] = idx2coord(i);
          const x = col * cellW;
          const y = row * cellH;

          const baseRadius = Math.min(cellW, cellH) * 0.48;
          const radiusJitter = (Math.random() * 4) - 1.2;
          const radius = Math.max(2.5, baseRadius + radiusJitter);

          const offsetX = Math.random() * cellW;
          const offsetY = Math.random() * cellH;

          ctx.fillStyle = Palette[faction];
          ctx.beginPath();
          ctx.arc(x + offsetX, y + offsetY, radius, 0, Math.PI * 2);
          ctx.fill();
        }
      };

      let lastTimestamp = 0;
      const animationLoop = (timestamp) => {
        requestAnimationFrame(animationLoop);
        if (timestamp - lastTimestamp >= 1000 / maxFPS) {
          lastTimestamp = timestamp;
          step();
          draw();
        }
      };

      resizeCanvasToDisplay();
      draw();
      animationLoop(-999);

      window.addEventListener('orientationchange', () => {
        setTimeout(() => {
          resizeCanvasToDisplay();
          draw();
        }, 30);
      });
    })();
  </script>
</body>
</html>