-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
195 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Money Distribution Simulation</title> | ||
<style> | ||
body { | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
font-family: Arial, sans-serif; | ||
} | ||
canvas { | ||
border: 1px solid black; | ||
} | ||
#controls { | ||
margin: 10px 0; | ||
} | ||
#gini { | ||
margin-top: 10px; | ||
font-size: 18px; | ||
font-weight: bold; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<h1 style="margin: 0 auto">5% exchange ("realistic") VBI (1% donation cap)</h1> | ||
<div id="controls"> | ||
<button id="playPause">Play/Pause</button> | ||
<button id="reset">Reset</button> | ||
</div> | ||
<div id="gini">Gini Coefficient: 0.000</div> | ||
<canvas id="simulationCanvas" width="1600" height="650"></canvas> | ||
|
||
<script> | ||
const canvas = document.getElementById('simulationCanvas'); | ||
const ctx = canvas.getContext('2d'); | ||
const playPauseBtn = document.getElementById('playPause'); | ||
const resetBtn = document.getElementById('reset'); | ||
const giniDisplay = document.getElementById('gini'); | ||
|
||
const numIndividuals = 400; | ||
const EPOCH_LENGTH = 200; | ||
const TRANSACTION_CHANCE = 0.10; // 10% chance of transaction | ||
let individuals = []; | ||
let isRunning = false; | ||
let animationId; | ||
let roundCount = 0; | ||
let wealthiestIndividual = null; | ||
|
||
function gaussianRandom(mean = 0.5, stdev = 0.15) { | ||
const u = 1 - Math.random(); | ||
const v = Math.random(); | ||
const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); | ||
return Math.min(Math.max(z * stdev + mean, 0), 1); | ||
} | ||
|
||
function initializeSimulation() { | ||
individuals = Array(numIndividuals).fill().map((_, i) => ({ | ||
id: i, | ||
wealth: 100, | ||
moneySkill: gaussianRandom(), | ||
generosity: gaussianRandom() | ||
})); | ||
roundCount = 0; | ||
wealthiestIndividual = null; | ||
} | ||
|
||
function runSimulation() { | ||
// Sort individuals by wealth | ||
individuals.sort((a, b) => a.wealth - b.wealth); | ||
// Calculate total wealth | ||
const totalWealth = individuals.reduce((sum, individual) => sum + individual.wealth, 0) | ||
const maxDonation = totalWealth * .01 | ||
const medianIndex = Math.floor(numIndividuals / 2); | ||
|
||
for (let i = 0; i < numIndividuals; i++) { | ||
// Random transaction | ||
if (Math.random() < TRANSACTION_CHANCE) { | ||
const giver = i; | ||
let receiver; | ||
do { | ||
receiver = Math.floor(Math.random() * numIndividuals); | ||
} while (receiver === giver); | ||
|
||
const amount = Math.floor(individuals[giver].wealth * 0.05); | ||
if (amount > 0) { | ||
individuals[giver].wealth -= amount; | ||
individuals[receiver].wealth += amount; | ||
} | ||
} | ||
|
||
// Apply investment based on money skill and 10% of wealth | ||
const investment = 0.1 * individuals[i].wealth; | ||
individuals[i].wealth += individuals[i].moneySkill * investment; | ||
|
||
// Voluntary Basic Income (VBI) | ||
if (i >= medianIndex) { // Top 50% of wealthy individuals | ||
const vbi = Math.min(maxDonation, Math.floor(0.1 * individuals[i].wealth * individuals[i].generosity)) | ||
if (vbi > 0) { | ||
const receiver = Math.floor(Math.random() * medianIndex); // Random individual from bottom 50% | ||
individuals[i].wealth -= vbi; | ||
individuals[receiver].wealth += vbi; | ||
} | ||
} | ||
} | ||
|
||
individuals.sort((a, b) => a.wealth - b.wealth) | ||
|
||
roundCount++; | ||
|
||
if (roundCount % EPOCH_LENGTH === 0 || wealthiestIndividual === null) { | ||
wealthiestIndividual = individuals[individuals.length - 1]; | ||
} else { | ||
wealthiestIndividual = individuals.find(ind => ind.id === wealthiestIndividual.id); | ||
} | ||
} | ||
|
||
function calculateGini() { | ||
const totalWealth = individuals.reduce((sum, ind) => sum + ind.wealth, 0); | ||
const lorenzPoints = []; | ||
let sumSoFar = 0; | ||
for (let i = 0; i < numIndividuals; i++) { | ||
sumSoFar += individuals[i].wealth; | ||
lorenzPoints.push(sumSoFar / totalWealth); | ||
} | ||
|
||
let areaUnderLorenz = 0; | ||
for (let i = 0; i < numIndividuals; i++) { | ||
areaUnderLorenz += lorenzPoints[i] / numIndividuals; | ||
} | ||
|
||
const gini = (0.5 - areaUnderLorenz) / 0.5; | ||
return gini; | ||
} | ||
|
||
function drawBarGraph() { | ||
ctx.clearRect(0, 0, canvas.width, canvas.height); | ||
const barWidth = canvas.width / numIndividuals; | ||
const maxMoney = Math.max(...individuals.map(ind => ind.wealth)); | ||
|
||
for (let i = 0; i < numIndividuals; i++) { | ||
const barHeight = (individuals[i].wealth / maxMoney) * (canvas.height - 50); | ||
ctx.fillStyle = `hsl(${(individuals[i].wealth / maxMoney) * 120}, 100%, 50%)`; | ||
ctx.fillRect(i * barWidth, canvas.height - 50 - barHeight, barWidth, barHeight); | ||
} | ||
|
||
// Draw arrow pointing to wealthiest individual | ||
if (wealthiestIndividual) { | ||
const wealthiestIndex = individuals.findIndex(ind => ind.id === wealthiestIndividual.id); | ||
const arrowX = (wealthiestIndex + 0.5) * barWidth; | ||
ctx.beginPath(); | ||
ctx.moveTo(arrowX, canvas.height - 40); | ||
ctx.lineTo(arrowX - 10, canvas.height - 20); | ||
ctx.lineTo(arrowX + 10, canvas.height - 20); | ||
ctx.closePath(); | ||
ctx.fillStyle = 'red'; | ||
ctx.fill(); | ||
} | ||
|
||
const gini = calculateGini(); | ||
const currentEpoch = Math.floor(roundCount / EPOCH_LENGTH); | ||
giniDisplay.textContent = `Gini Coefficient: ${gini.toFixed(3)} | Round: ${roundCount} | Epoch: ${currentEpoch}`; | ||
} | ||
|
||
function animate() { | ||
if (isRunning) { | ||
runSimulation(); | ||
drawBarGraph(); | ||
animationId = requestAnimationFrame(animate); | ||
} | ||
} | ||
|
||
playPauseBtn.addEventListener('click', () => { | ||
isRunning = !isRunning; | ||
if (isRunning) { | ||
animate(); | ||
} else { | ||
cancelAnimationFrame(animationId); | ||
} | ||
}); | ||
|
||
resetBtn.addEventListener('click', () => { | ||
isRunning = false; | ||
cancelAnimationFrame(animationId); | ||
initializeSimulation(); | ||
drawBarGraph(); | ||
}); | ||
|
||
initializeSimulation(); | ||
drawBarGraph(); | ||
</script> | ||
</body> | ||
</html> |