diff options
Diffstat (limited to 'client/index.js')
| -rw-r--r-- | client/index.js | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/client/index.js b/client/index.js new file mode 100644 index 0000000..644b1ea --- /dev/null +++ b/client/index.js @@ -0,0 +1,511 @@ +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 + '#008000', // team 1 + '#000080', // 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 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, + 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, + 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', + } +} + +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 === '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() + if (agent === selected_agent) { + ctx.strokeStyle = '#000' + ctx.strokeRect((x + Size + 1) * Px, y * Px, Px, Px) + ctx.strokeRect(x * Px, y * Px, Px, Px) + } + 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() + } + }) + stats.forEach(stat => stat()) + if (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(){ + clearTimeout(stepTimeout) + reset() + step() +} +function pause(){ + paused = !paused + if (!paused) { + clearTimeout(stepTimeout) + step() + } +} +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] + // console.log(new_sim, sim) + console.log(sim.name) + reset() +} +function pick_view(new_view){ + view = new_view + console.log('viewing', new_view + ':', views[new_view].name) +} + +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: 10, + max: 1000, + step: 10, + value: 200, + }, true, reset) + nx.L = Slider(parent, 'governmental_legitimacy', 'Governmental legitimacy', { + min: 0, + max: 1, + step: 0.01, + value: 0.9, + }, 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.1, + step: 0.002, + value: 0.05 + }, false, play) + + const stats_el = document.querySelector('#stats') + 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(stats_el, '~ ~ ~', () => ''), + Statistic(stats_el, 'Selected Agent #', () => selected_agent && selected_agent.id), + Statistic(stats_el, 'Position', () => selected_agent && (selected_agent.x + ',' + selected_agent.y)), + Statistic(stats_el, 'Hardship', () => selected_agent && selected_agent.H.toFixed(2)), + Statistic(stats_el, 'Risk tolerance', () => selected_agent && selected_agent.R.toFixed(2)), + Statistic(stats_el, 'Grievance', () => selected_agent && selected_agent.G.toFixed(2)), + Statistic(stats_el, 'Score', () => selected_agent && selected_agent.GN.toFixed(2)), + Statistic(stats_el, 'Cop / Active ratio', () => selected_agent && selected_agent.CAv.toFixed(2)), + Statistic(stats_el, 'Arrest probability', () => selected_agent && selected_agent.P.toFixed(2)), + Statistic(stats_el, 'Net risk', () => selected_agent && selected_agent.NetR.toFixed(2)), + Statistic(stats_el, 'Active', () => selected_agent && yesno(selected_agent.active)), + Statistic(stats_el, 'Age', () => selected_agent && selected_agent.Age), + Statistic(stats_el, 'In Jail?', () => selected_agent && yesno(selected_agent.J)), + Statistic(stats_el, 'Dead?', () => selected_agent && yesno(selected_agent.dead)), + Statistic(stats_el, 'Jail term', () => selected_agent && (selected_agent.J ? 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() + }) + + document.querySelector('.loading').classList.remove('loading') + rescale() + play() +} +function yesno(x) { return x ? 'Yes' : 'No' } +build() |
