Civil Violence
This implementation of Joshua Epstein's Civil Violence Model simulates outbreaks of unrest against a central authority by agents questioning its legitimacy.
In this visualization, cops are represented as blue pixels. Civilians are represented by gray, red, or white pixels. Every turn, some civilians will go from inactive (gray) to active (red), depending on their own threshold and the exogenous legitimacy of the government. However, a cop who is next to an active civilian will arrest them (turning them white and unable to move) for a fixed number of turns set by the jail term. By updating parameters for legitimacy, jail term, % full, and % Cops, different dynamics will arise, from a peaceful population to periodic outbursts to near-constant anarchy.
import { Environment, CanvasRenderer, Terrain, utils, Colors } from "flocc"; utils.seed(1); /* ----- PARAMETERS ----- */ const PERCENT_FULL = 0.7; const PERCENT_COPS = 0.05; const LEGITIMACY = 0.4; const JAIL_TERM = 10; /* ----- SETUP ----- */ const scale = 5; const [width, height] = [100, 100]; const environment = new Environment({ width, height }); const renderer = new CanvasRenderer(environment, { width: width * scale, height: height * scale }); renderer.mount("#container"); const terrain = new Terrain(width, height, { scale, async: true }); environment.use(terrain); const hardship = new Terrain(width, height, { scale, grayscale: true }); const riskAversion = new Terrain(width, height, { scale, grayscale: true }); const jailTerms = new Terrain(width, height, { scale, grayscale: true }); const EMPTY = Colors.BLACK; const COP = Colors.BLUE; const CIVILIAN = { r: 127, g: 127, b: 127, a: 255 }; const ACTIVE = Colors.RED; const ARRESTED = Colors.WHITE; function match(p1, p2) { return p1.r === p2.r && p1.g === p2.g && p1.b === p2.b && p1.a === p2.a; } function getRandomOpenCell() { const [x, y] = [utils.random(0, width), utils.random(0, height)]; if (!match(terrain.sample(x, y), EMPTY)) return getRandomOpenCell(); return { x, y }; } function grievance(x, y) { return (hardship.sample(x, y) / 255) * (1 - LEGITIMACY); } function netRisk(x, y) { return (riskAversion.sample(x, y) / 255) * arrestProbability(x, y); } function arrestProbability(x, y) { const k = 2.302585; const visible = terrain.neighbors(x, y); let cops = 0; let actives = 1; // count self for (let i = 0; i < visible.length; i++) { if (match(visible[i], COP)) { cops++; } else if (match(visible[i], ACTIVE)) { actives++; } } const copsToActives = cops / actives; const result = 1 - Math.pow(Math.E, -k * copsToActives); return result; } function swap(x1, y1, x2, y2) { const [t, h, r, j] = [ terrain.sample(x1, y1), hardship.sample(x1, y1), riskAversion.sample(x1, y1), jailTerms.sample(x1, y1) ]; terrain.set(x1, y1, terrain.sample(x2, y2)); hardship.set(x1, y1, hardship.sample(x2, y2)); riskAversion.set(x1, y1, riskAversion.sample(x2, y2)); jailTerms.set(x1, y1, jailTerms.sample(x2, y2)); terrain.set(x2, y2, t); hardship.set(x2, y2, h); riskAversion.set(x2, y2, r); jailTerms.set(x2, y2, j); } function tickCivilian(x, y) { const here = terrain.sample(x, y); const jt = jailTerms.sample(x, y); const threshold = 0.5; let arrested = match(here, ARRESTED); let active = false; if (!arrested) { active = grievance(x, y) - netRisk(x, y) > threshold; } else if (arrested) { if (jt === 0) { terrain.set(x, y, CIVILIAN); } else { jailTerms.set(x, y, jt - 1); return; } } if (active) terrain.set(x, y, ACTIVE); for (let dy of utils.shuffle([-1, 0, 1])) { for (let dx of utils.shuffle([-1, 0, 1])) { if (dx === 0 && dy === 0) continue; const p = terrain.sample(x + dx, y + dy); if (!match(p, EMPTY)) continue; return swap(x, y, x + dx, y + dy); } } } function tickCop(x, y) { const actives = []; for (let dy = -1; dy < 1; dy++) { for (let dx = -1; dx < 1; dx++) { if (match(terrain.sample(x + dx, y + dy), ACTIVE)) actives.push({ x: x + dx, y: y + dy }); } } if (actives.length > 0) { const randomArrest = utils.sample(actives); terrain.set(randomArrest.x, randomArrest.y, ARRESTED); jailTerms.set(randomArrest.x, randomArrest.y, JAIL_TERM); } for (let dy of utils.shuffle([-1, 0, 1])) { for (let dx of utils.shuffle([-1, 0, 1])) { if (dx === 0 && dy === 0) continue; const p = terrain.sample(x + dx, y + dy); if (!match(p, EMPTY)) continue; return swap(x, y, x + dx, y + dy); } } } function setup() { terrain.init((x, y) => { const r = utils.uniform(); if (r > PERCENT_FULL) { return EMPTY; } else if (r > (1 - PERCENT_COPS) * PERCENT_FULL) { return COP; } else { hardship.set(x, y, utils.random(0, 255)); riskAversion.set(x, y, utils.random(0, 255)); return CIVILIAN; } }); terrain.addRule((x, y) => { const here = terrain.sample(x, y); if (match(here, EMPTY)) return; if (match(here, COP)) return tickCop(x, y); return tickCivilian(x, y); }); } function run() { environment.tick({ randomizeOrder: true }); setTimeout(run, 100); } setup(); run();