summaryrefslogtreecommitdiff
path: root/client/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'client/index.js')
-rw-r--r--client/index.js511
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()