xxxxxxxxxx
276
//
// Random Transactions
//
// by Philip, February 21, 2021
//
// People wander around and make transactions when they run into each other.
//
// The size (area) of a person is proportional to their wealth.
//
// The Gini Index is constantly computed and displayed.
//
let people = 500; // How many people in the simulation?
let balance_initial = 3400.0; // How much money does everyone start with?
// 3400 is the median cash balance for USA
let transactions_per_day = 2; // How many transactions correspond to an average person's day?
let adjust_radius = 0.0; //0.05; // How big are the people relative to their wealth?
let min_radius = 10;
let days = 0; // How many days have gone by?
let frame = 0;
let days_last_frame = 0;
let transactions = 0;
let v_min = 10;
let v_max = 50;
let r_min = 5;
let atoms = [];
let repeat = false;
let collisions = true;
let moneySupply = balance_initial * people;
let averageBalance = moneySupply / people;
let numRich = 0;
let numPoor = 0;
let giniIndex = 0;
let blue = "";
let red = "";
let green = "";
function maybeTrade(i, j) {
//
// Two people meet, and maybe they make a transaction
//
let richer = j;
let poorer = i;
if (atoms[i].balance >= atoms[j].balance) {
richer = i;
poorer = j;
}
// Randomly choose whether richer person buys or sells:
let buy = (random() > 0.5);
let buyer = (buy) ? richer : poorer;
let seller = (buy) ? poorer : richer;
let amount = 0;
let method = 0;
if (method == 1) {
//
// Method 1: Transaction amount is fixed fraction of buyer's balance
//
amount = atoms[buyer].balance * 0.05;
}else if (method == 2) {
//
// Method 2: Transaction amount is fixed fraction of poorer person's balance
//
amount = atoms[poorer].balance * 0.05;
} else if (method == 3) {
//
// Method 3: Transaction is a fixed amount, and only happens if buyer can afford it.
//
amount = 0.01 * averageBalance;
} else if (method == 4) {
//
// random transfers
//
amount = atoms[buyer].balance * random(1.0);
}
if (atoms[buyer].balance >= amount) {
atoms[buyer].balance -= amount ;
atoms[seller].balance += amount;
transactions++;
}
}
function setup() {
createCanvas(800, 600);
noStroke();
blue = color(0, 0, 255);
red = color(255, 0, 0);
green = color(0, 255, 0);
for (let i = 0; i < people; i++) {
let r = r_min;
let c = blue;
let v = createVector(v_min + random(-1.0, 1.0) * (v_max - v_min),
v_min + random(-1.0, 1.0) * (v_max - v_min));
let p = createVector(random(width), random(height));
atoms[i] = new atom(p, v, r, c, atoms, i);
}
}
function draw() {
frame++;
background(128);
let last_transactions = transactions;
for (let i = 0; i < people; i++) {
atoms[i].update();
atoms[i].display();
}
days_last_frame = (transactions - last_transactions) / people / transactions_per_day;
days += days_last_frame;
noStroke();
strokeWeight(1);
updateStats(frame);
}
function computeGiniIndex() {
//
// Uses discrete formula found at:
// https://en.wikipedia.org/wiki/Gini_coefficient#Calculation
//
averageBalance = 0;
for (let i = 0; i < people; i++) {
averageBalance += atoms[i].balance;
if (atoms[i].balance < 0.0) {
fill(0);
noStroke();
text("Error: negative balance!", width/2, height/2);
}
}
averageBalance /= people;
let sumOfDifferences = 0;
for (let i = 0; i < people; i++) {
for (let j = 0; j < people; j++) {
sumOfDifferences += abs(atoms[i].balance - atoms[j].balance);
}
}
return sumOfDifferences/(2 * people * people * averageBalance);
}
function updateStats(frame) {
numRich = 0;
numPoor = 0;
moneySupply = 0;
for (let i = 0; i < people; i++) {
if (atoms[i].balance > averageBalance * 2) numRich++;
else if (atoms[i].balance < averageBalance / 2) numPoor++;
moneySupply += atoms[i].balance;
}
if (frame % 100 == 0) {
// every once in a while, re-compute the giniIndex
giniIndex = computeGiniIndex();
}
noStroke();
fill(128,128,128,220);
rect(0, height - 20, width, 20);
fill(0);
noStroke();
fill(green);
text(round(numRich/people * 100)+"%", 10, height - 5);
fill(red);
text(round(numPoor/people * 100)+"%", 40, height - 5);
fill(blue);
text(round((people - numPoor - numRich)/people * 100)+"%", 70, height - 5);
fill (0);
text("Gini Index: " + round(giniIndex * 100,0), 150, height - 5);
let stats = "Money: " + round(moneySupply) +
" kXs: " + round(transactions / 1000,0) +
" Days: " + round(days);
text(stats, width - (stats.length * 6) , height - 5);
}
// atom class
function atom(p, v, r, c, _others, _id) {
this.p = p.copy();
this.v = v.copy();
this.r = r;
this.c = c;
this.others = _others;
this.id = _id;
this.dt = 1.0/60.0;
this.balance = balance_initial * random(1.0);
this.update = function() {
if (collisions) {
// Check for collisions and add restoring forces
let collisionForces = createVector(0,0,0);
for (let i = 0; i < this.others.length; i++) {
if (i != this.id) {
let d = p5.Vector.sub(this.p, this.others[i].p);
let penetration = ((this.r + this.others[i].r)) - d.mag();
if (penetration > 0) {
d.normalize();
// Elastic component - restitution (note: does not conserve energy)
collisionForces.add(d.mult(10 * penetration));
// When in collision, after first few secs, there may be trade
maybeTrade(this.id, i);
}
}
}
this.v.add(collisionForces.mult(1.0));
}
if (this.v.mag() > v_max) this.v.mult(v_max / this.v.mag());
this.p.add(p5.Vector.mult(this.v, this.dt));
// Your radius corresponds to your fraction of the money supply
this.r = max(sqrt(this.balance / moneySupply * width * height * adjust_radius), min_radius);
let average_balance = moneySupply / people;
if (this.balance < average_balance / 2) this.c = red;
else if (this.balance > average_balance * 2) this.c = green;
else this.c = blue;
// Edge conditions
if (repeat) {
if (this.p.x > width) this.p.x -= width;
if (this.p.x < 0) this.p.x += width;
if (this.p.y > height) this.p.y -= height;
if (this.p.y < 0) this.p.y += height;
} else {
if (this.p.x >= width) {
this.v.x *= -1.0;
this.p.x = width - 1;
}
if (this.p.x < 0) {
this.v.x *= -1.0;
this.p.x = 0.0;
}
if (this.p.y >= height) {
this.v.y *= -1.0;
this.p.y = height - 1;
}
if (this.p.y < 0) {
this.v.y *= -1.0;
this.p.y = 0.0;
}
}
}
this.display = function() {
stroke(0);
//strokeWeight(1);
noStroke();
fill(this.c);
ellipse(this.p.x, this.p.y, this.r * 2, this.r * 2);
}
};
function keyTyped() {
}
function mousePressed() {
enable_dividend = !enable_dividend;
}