import Nexus from 'nexusui' import keys from './lib/keys' import { requestAudioContext, choice, shuffle, mod, randint, norm, dataURItoBlob, update_value_on_change, update_radio_value_on_change, build_options, Slider, Statistic } from './lib/util' const canvas = document.querySelector('canvas') const ctx = canvas.getContext('2d') const nx = window.nx = {} const COP_TEAM = 0, EMPTY_CELL = -1 const types = { SIM_REBELLION: 0, SIM_GENOCIDE: 1 } const inactive_colors = [ '#000000', // cops '#00d020', // team 1 '#0000f0', // team 2 '#008080', // team 3 '#00ff80', // team 4 '#0080ff', // team 5 ] const active_colors = [ '#000000', '#800000', // team 1 '#800080', // team 2 '#808080', // team 3 '#80ff80', // team 4 '#8080ff', // team 5 ] const team_names = [ 'Cops', 'Green', 'Blue', '#3', '#4', '#5', ] const neighbor_idxs = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1], ] const simulations = { rebellion: { id: types.SIM_REBELLION, name: 'Civil Violence Model I: Generalized Rebellion Against Central Authority', teams: 1, init: () => { document.querySelector('.governmental_legitimacy').innerText = 'Governmental Legitimacy' }, update: (agent, N, T) => { if (agent.J > 0) return const is_active = agent.GN > T if (is_active && !agent.active) { agent.log.push('went active') } else if (!is_active && agent.active) { agent.log.push('went inactive') } agent.active = is_active move(agent, N) }, color: (agent) => { return agent.active ? active_colors[agent.team] : inactive_colors[agent.team] }, }, genocide: { id: types.SIM_GENOCIDE, name: 'Civil Violence Model II: Inter-Group Violence', teams: 2, init: () => { document.querySelector('.governmental_legitimacy').innerText = 'Empathy' }, update: (agent, N, T) => { if (agent.dead) return if (agent.Age-- <= 0) { if (agent.J > 0) { kill(agent, 'died in jail') died.in_jail += 1 } else { kill(agent, 'died of old age') died.old_age += 1 } return } agent.GN > T && N.some(n => { if (n.team !== EMPTY_CELL && n.team !== COP_TEAM && n.team !== agent.team) { kill(n, 'died of murder') agent.active = true agent.log.push('killed someone') died.murder += 1 return true } return false }) if (Math.random() < nx.Rep.value) { birth(agent, N) } move(agent, N) }, color: (agent) => { return inactive_colors[agent.team] }, } } const views = { G: { value: 'G', name: 'Grievance', }, CAv: { value: 'CAv', name: 'Cop-to-Active ratio' }, P: { value: 'P', name: 'Arrest probability', }, NetR: { value: 'NetR', name: 'Net risk of being arrested', }, GN: { value: 'GN', name: 'Distance from threshold', }, id: { value: 'id', name: 'Agent ID', }, } const Px = 10 const Margin = 1 let agents = [] let board = [] let stats = [] let sim = simulations.rebellion let view = 'G' let paused = false let stepTimeout let agent_id = 0 let selected_agent let time = 0 let died = {} function step(){ stepTimeout = setTimeout(step, nx.Rate.value) if (paused) return time += 1 const T = nx.T.value shuffle(agents).forEach(agent => { if (agent.J > 1) { agent.J -= 1 return } const N = neighbors(agent) if (agent.J === 1) { // check to see if we can get out of jail (if our previous square was free) if (!board[agent.y][agent.x]) { agent.J = 0 board[agent.y][agent.x] = agent agent.log.push('left jail') } } if (agent.team === COP_TEAM) { update_cop(agent, N) } else { calculate_net_risk(agent, N) sim.update(agent, N, T) } }) if (sim.id === types.SIM_GENOCIDE) { agents = agents.filter(agent => !agent.dead) } draw() } function calculate_net_risk(agent, N) { agent.CAv = get_cav(N) // grievance equals hardship times illegitimacy agent.G = agent.H * (1 - nx.L.value) // arrest probability is logarithmically proportional to number of cops/active nearby agent.P = 1 - Math.exp(-nx.k.value * agent.CAv) // net risk of being arrested equals agent's risk aversion times arrest probability agent.NetR = agent.R * agent.P * Math.pow(nx.J.value, nx.alpha.value) // if G - NetR is above the threshold, go active agent.GN = agent.G - agent.NetR } function get_cav(N){ let C = 0, A = 1 N.forEach(n => { if (n.team === EMPTY_CELL) return if (n.team === COP_TEAM) { C += 1 } else { A += 1 } }) return C / A } function update_cop(agent, N){ N.some(n => { if (n.team !== COP_TEAM && n.active) { jail(n) return true } return false }) move(agent, N) } function jail(agent){ agent.J = randint(nx.J.value) agent.active = false board[agent.y][agent.x] = null agent.log.push('sent to jail') } function kill(agent, cause_of_death){ agent.dead = true agent.Age = 0 board[agent.y][agent.x] = null agent.log.push(cause_of_death) } function birth(agent, N){ N.some(n => { if (n.team === EMPTY_CELL) { let { y, x } = n board[y][x] = add_agent(y, x, agent) agent.log.push('gave birth') return true } return false }) } function add_agent(y, x, a){ if (agents.length >= nx.Size.value * nx.Size.value) return const agent = { id: agent_id++, team: a ? a.team : randint(sim.teams) + 1, H: a ? a.H : Math.random(), R: Math.random(), J: 0, Age: randint(nx.MaxAge.value) + 1, active: false, G: 0, CAv: 0, P: 0, NetR: 0, GN: 0, log: [], y, x, } agents.push(agent) return agent } function move(agent, N){ if (agent.dead || agent.J) return N.some(n => { if (n.team === EMPTY_CELL) { let { y, x } = n board[agent.y][agent.x] = null board[y][x] = agent agent.y = y agent.x = x return true } return false }) } function view_color(agent){ let x, H, S, L if (view === 'id') { x = agent.id / agent_id H = x * 360 S = 1 L = 0.5 } else if (view === 'GN') { x = agent[view] H = agent.active ? 0 : 180 S = 1-x L = 1-x } else { x = agent[view] H = agent.active ? 0 : 100 S = 1 L = 1-x * 2 } const color = 'hsl(' + [ ((H)|0), ((S * 100)|0) + '%', ((L * 100)|0) + '%', ].join(',') + ')' return color } function draw(){ const Size = nx.Size.value ctx.clearRect(0, 0, (Size + 1) * Px * 2, Size * Px) stride((y, x) => { const agent = board[y][x] if (! agent) { ctx.fillStyle = '#ffffff' ctx.fillRect((x + Size + 1) * Px, y * Px, Px - Margin, Px - Margin) ctx.fillRect(x * Px, y * Px, Px - Margin, Px - Margin) } else if (agent.team === COP_TEAM) { ctx.fillStyle = '#000000' ctx.fillRect((x + Size + 1) * Px, y * Px, Px - Margin, Px - Margin) ctx.fillRect(x * Px, y * Px, Px - Margin, Px - Margin) } else { ctx.save() ctx.fillStyle = view_color(agent) ctx.fillRect((x + Size + 1) * Px, y * Px, Px - Margin, Px - Margin) ctx.fillStyle = sim.color(agent) ctx.fillRect(x * Px, y * Px, Px - Margin, Px - Margin) ctx.save() } }) if (selected_agent && !selected_agent.dead && !selected_agent.J) { const { y, x } = selected_agent ctx.strokeStyle = '#000000' ctx.lineWidth = 1 ctx.strokeRect((x + Size + 1) * Px, y * Px, Px, Px) ctx.strokeRect(x * Px, y * Px, Px, Px) } stats.forEach(stat => stat()) if (selected_agent && selected_agent.dead) { observe_random_agent() } } function observe_random_agent(){ let agent do { agent = choice(agents) } while (agent.dead || agent.team === COP_TEAM) selected_agent = agent } function reset(){ agents = [] board = [] agent_id = 0 time = 0 const Pop = nx.Pop.value const Cops = nx.Cops.value board = stride((y, x) => { if (Math.random() < Pop) { return add_agent(y, x) } return null }) for (let i = 0, len = agents.length * Cops; i < len; i++) { agents[randint(agents.length)].team = COP_TEAM } observe_random_agent(agents) died = { old_age: 0, in_jail: 0, murder: 0 } } function restart(){ rescale() play() } function play(){ paused = false clearTimeout(stepTimeout) reset() step() } function pause(){ paused = !paused if (!paused) { document.querySelector('#pause').innerText = 'Pause' clearTimeout(stepTimeout) step() } else { document.querySelector('#pause').innerText = 'Paused' clearTimeout(stepTimeout) } } function stride(fn){ let A = [] let Size = nx.Size.value let i = 0 for (let y = 0; y < Size; y++) { A[y] = [] for (let x = 0; x < Size; x++) { A[y][x] = fn(y, x, i) } } return A } function rescale(){ if (! nx.Size) return canvas.width = (nx.Size.value + 1) * Px * 2 canvas.height = nx.Size.value * Px } function get(y, x){ const Size = nx.Size.value y = mod(y, Size) x = mod(x, Size) return board[y][x] || { y, x, team: EMPTY_CELL } } function neighbors(agent){ const { y, x } = agent return shuffle(neighbor_idxs.map(pair => get(y + pair[0], x + pair[1]))) } function pick_simulation(new_sim){ sim = simulations[new_sim] sim.init() console.log(sim.name) reset() } function pick_view(new_view){ view = new_view console.log('viewing', new_view + ':', views[new_view].name) } function select_from_event(e){ const Size = nx.Size.value const bounds = canvas.getBoundingClientRect() let y = (((e.pageY - bounds.top) / bounds.height) * Size)|0 let x = (((e.pageX - bounds.left) / bounds.width) * (2 * Size + 1))|0 console.log(x, y) if (x >= Size) { x %= Size } selected_agent = board[y][x] draw() return board[y][x] } function build() { build_options(document.querySelector('#sim'), simulations, pick_simulation) build_options(document.querySelector('#view'), views, pick_view) const parent = document.querySelector('#options') nx.Size = Slider(parent, 'world_size', 'World size', { min: 10, max: 200, step: 1, value: 40, }, true, restart) nx.Rate = Slider(parent, 'rate', 'Frame rate', { min: 100, max: 5000, step: 10, value: 200, }, true, reset) nx.L = Slider(parent, 'governmental_legitimacy', 'Governmental legitimacy', { min: 0, max: 1, step: 0.01, value: 0.89, }, false) nx.T = Slider(parent, 'activation_threshold', 'Grievance threshold', { min: 0, max: 1, step: 0.01, value: 0.1, }, false) nx.Rep = Slider(parent, 'birth_rate', 'Birth rate', { min: 0, max: 1, step: 0.001, value: 0.05, }, false) nx.MaxAge = Slider(parent, 'max_age', 'Max age', { min: 0, max: 1000, step: 1, value: 200, }, true) nx.J = Slider(parent, 'max_jail_term', 'Maximum jail term', { min: 1, max: 100, step: 1, value: 30, }, true) nx.alpha = Slider(parent, 'jail_deterrent', 'Jail deterrent', { min: 0, max: 2, step: 0.01, value: 0, }, false) nx.k = Slider(parent, 'arrest_constant', 'Arrest probability constant', { min: 1, max: 10, step: 0.01, value: 2.3, }, false) nx.Pop = Slider(parent, 'population_density', 'Population density', { min: 0, max: 1, step: 0.01, value: 0.7 }, false, play) nx.Cops = Slider(parent, 'cop_density', 'Cop density', { min: 0, max: 0.3, step: 0.002, value: 0.05 }, false, play) const stats_el = document.querySelector('#stats') const agent_el = document.querySelector('#agent') const log_el = document.querySelector('#log') stats = [ Statistic(stats_el, 'Time', () => time), Statistic(stats_el, 'Agents', () => agents.length), Statistic(stats_el, 'Cops', () => agents.filter(a => a.team === COP_TEAM).length), Statistic(stats_el, 'Team #1', () => agents.filter(a => a.team === 1).length), Statistic(stats_el, 'Team #2', () => agents.filter(a => a.team === 2).length), Statistic(stats_el, 'Quiescent', () => agents.filter(a => !a.active).length), Statistic(stats_el, 'Active', () => agents.filter(a => a.active).length), Statistic(stats_el, 'Jailed', () => agents.filter(a => a.J > 0).length), Statistic(stats_el, 'Died of old age', () => died.old_age), Statistic(stats_el, 'Died in jail', () => died.in_jail), Statistic(stats_el, 'Murder rate', () => died.murder), Statistic(agent_el, 'Selected Agent #', () => selected_agent ? selected_agent.id : 'None'), Statistic(agent_el, 'Team', () => selected_agent && team_names[selected_agent.team]), Statistic(agent_el, 'Position', () => selected_agent && (selected_agent.x + ',' + selected_agent.y)), Statistic(agent_el, 'Hardship', () => selected_agent && selected_agent.H.toFixed(2)), Statistic(agent_el, 'Risk tolerance', () => selected_agent && selected_agent.R.toFixed(2)), Statistic(agent_el, 'Grievance', () => selected_agent && selected_agent.G.toFixed(2)), Statistic(agent_el, 'Score', () => selected_agent && selected_agent.GN.toFixed(2)), Statistic(agent_el, 'Cop / Active ratio', () => selected_agent && selected_agent.CAv.toFixed(2)), Statistic(agent_el, 'Arrest probability', () => selected_agent && selected_agent.P.toFixed(2)), Statistic(agent_el, 'Net risk', () => selected_agent && selected_agent.NetR.toFixed(2)), Statistic(agent_el, 'Active', () => selected_agent && yesno(selected_agent.active)), Statistic(agent_el, 'Age', () => selected_agent && selected_agent.Age), Statistic(agent_el, 'Dead?', () => selected_agent && yesno(selected_agent.dead)), Statistic(agent_el, 'In Jail?', () => selected_agent && yesno(selected_agent.J)), Statistic(agent_el, 'Jail term', () => selected_agent && selected_agent.J), Statistic(log_el, 'Log', () => selected_agent && (selected_agent.log.join('\n'))), ] const restart_button = document.querySelector('button#restart') restart_button.addEventListener('click', play) const pause_button = document.querySelector('button#pause') pause_button.addEventListener('click', pause) const observe_button = document.querySelector('button#observe') observe_button.addEventListener('click', () => { observe_random_agent() draw() }) let clicked = false canvas.addEventListener('mousemove', e => { if (clicked) return select_from_event(e) }) canvas.addEventListener('mousedown', e => { const found = select_from_event(e) if (found) { clicked = true } }) document.querySelector('.loading').classList.remove('loading') rescale() play() } function yesno(x) { return x ? 'Yes' : 'No' } build()