📄 Iowa Caucuses - README.txt
×

Iowa Caucuses

PoliticsSocial Science CanvasRendererNetworkTableRenderer

This model simulates the Iowa caucuses, a distinctive American political institution that blends voting with social deliberation. Unlike a simple ballot, the caucus involves multiple rounds: voters physically cluster with their preferred candidate, and candidates failing to reach a 15% viability threshold are eliminated, forcing their supporters to realign.

Voters and candidates are positioned on a two-dimensional ideological space (a political compass). Voters initially support the candidate nearest to their ideological position. Voters are also embedded in a social network—mostly ideological neighbors, with some cross-spectrum ties. When a voter's candidate becomes non-viable, they switch to a candidate supported by one of their network connections, simulating the persuasion and horse-trading that characterize real caucuses. This network structure allows initially marginal candidates to gain viability through social influence—a dynamic impossible in conventional voting models.

💻 iowa-caucuses.js - Interactive Editor
×
import { Agent, Environment, CanvasRenderer, Network, utils, TableRenderer } from "flocc";

utils.seed(1);

const colors = [
  "red",
  "orange",
  "yellow",
  "green",
  "cyan",
  "blue",
  "purple",
  "black"
];
const VOTERS = 500;
const CANDIDATES = colors.length;

const width = Math.min(window.innerWidth, window.innerHeight);
const height = Math.min(window.innerWidth, window.innerHeight);

const ui = (() => {
  return {
    drawAxes(compass, width, height) {
      const dpr = window.devicePixelRatio;
      const { context } = compass;
      context.save();
      context.beginPath();
      context.globalAlpha = 0.25;
      context.moveTo(dpr * 0, (dpr * height) / 2);
      context.lineTo(dpr * width, (dpr * height) / 2);
      context.moveTo((dpr * width) / 2, dpr * 0);
      context.lineTo((dpr * width) / 2, dpr * height);
      context.stroke();
      context.restore();
    },
    log(environment, showWinner) {
      const voteCount = document.getElementById("vote-count");
      const winner = showWinner ? "All remaining candidates over 15%" : "";
      voteCount.innerHTML = `<h3>Round ${environment.time - 1}: ${winner}</h3>`;
    }
  };
})();

const xy = () => {
  const a = utils.gaussian(Math.PI, Math.PI / 4);
  const r = utils.gaussian(0.2, 0.08, true);
  const x = r * Math.cos(a);
  const y = r * Math.sin(a);
  return { x, y };
};

const environment = new Environment();
const network = new Network();
environment.use(network);

['table', 'vote-count', 'compass'].forEach(id => {
  const el = document.createElement('div');
  el.id = id;
  document.getElementById('container').appendChild(el);
});

const compass = new CanvasRenderer(environment, {
  width,
  height,
  scale: width,
  origin: {
    x: -0.5,
    y: -0.5
  },
  connectionColor: "#bbb"
});
compass.mount("#compass");

const table = new TableRenderer(environment, {
  filter: (a) => a.get("type") === "candidate"
});
table.columns = ["color", "votes"];
table.mount("#table");

function getVoters() {
  return environment.getAgents().filter((a) => a.get("type") === "voter");
}

function getCandidates() {
  return environment.getAgents().filter((a) => a.get("type") === "candidate");
}

function tickVoter(agent) {
  const candidates = environment.memo(getCandidates);
  let candidate = agent.get("candidate");

  if (environment.time === 0) {
    return;
  }

  // voting for the first time
  if (!candidate) {
    candidate = utils.sample(
      candidates,
      candidates.map((c) => {
        const id = Math.max(0.5 - utils.distance(c, agent), 0.0001);
        return id ** 3;
      })
    );
  } else if (candidate && candidate.get("valid") === false) {
    const neighborCandidates = network
      .neighbors(agent)
      .map((a) => a.get("candidate"));
    candidate = utils.sample(neighborCandidates);
    if (!candidate) return;
  }

  candidate.increment("votes");
  agent.set("candidate", candidate);
  agent.set("color", candidate.get("color"));
}

function setup() {
  for (let i = 0; i < VOTERS; i++) {
    const { x, y } = xy();
    const voter = new Agent({
      x,
      y,
      color: "gray",
      size: 2,
      type: "voter"
    });
    voter.addRule(tickVoter);
    environment.addAgent(voter);
    network.addAgent(voter);
  }
  for (let i = 0; i < CANDIDATES; i++) {
    const { x, y } = xy();
    const candidate = new Agent({
      x,
      y,
      color: colors[i],
      size: 6,
      type: "candidate",
      votes: 0
    });
    environment.addAgent(candidate);
    network.addAgent(candidate);
  }
  const voters = getVoters();
  voters.forEach((voter) => {
    const neighbors = Array.from(voters);
    neighbors.sort(
      (a, b) => utils.distance(voter, a) - utils.distance(voter, b)
    );
    const r = utils.random(2, 7);
    for (let i = 0; i < r; i++) {
      network.connect(voter, neighbors[i]);
    }
  });
  // randomly rewire
  voters.forEach((voter) => {
    const connections = network.neighbors(voter);
    connections.forEach((connect) => {
      if (utils.uniform() < 0.05) {
        network.disconnect(voter, connect);
        network.connect(voter, utils.sample(voters));
      }
    });
  });
}

function draw() {
  environment.tick({ randomizeOrder: true });
  ui.drawAxes(compass, width, height);

  const candidates = environment.memo(getCandidates);
  const voters = environment.memo(getVoters);

  const serializedVotes = candidates.map((c) => c.get("votes")).join(",");
  if (serializedVotes === environment.get("serializedVotes")) {
    return ui.log(environment, true);
  } else {
    environment.set("serializedVotes", serializedVotes);
  }

  ui.log(environment);

  candidates.forEach((c) => {
    const { votes } = c.getData();
    const valid = votes / voters.length >= 0.15;
    c.set("valid", valid);
    // reset votes
    c.set("lastVotes", votes);
    c.set("votes", 0);
  });

  setTimeout(draw, 1500);
}

setup();
draw();

Edit the code on the left · See results on the right