Spaces:
Running
Running
<html> | |
<head> | |
<title>Cancer Game Theory</title> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<style> | |
body { | |
font-family: Arial, sans-serif; | |
margin: 20px; | |
background-color: #f0f8ff; | |
} | |
.header { | |
text-align: center; | |
padding: 20px; | |
background-color: #1e3799; | |
color: white; | |
border-radius: 10px; | |
margin-bottom: 20px; | |
} | |
.header h1 { | |
margin: 0; | |
font-size: 2.5em; | |
} | |
.rules { | |
background-color: #e8f4f8; | |
padding: 20px; | |
border-radius: 10px; | |
margin: 20px 0; | |
border: 2px solid #1e3799; | |
} | |
.rules h2 { | |
color: #1e3799; | |
margin-top: 0; | |
} | |
.rules ul { | |
line-height: 1.6; | |
list-style-type: none; | |
padding-left: 0; | |
} | |
canvas { | |
border: 2px solid #1e3799; | |
margin: 10px 0; | |
border-radius: 5px; | |
} | |
.controls { | |
margin: 10px 0; | |
padding: 15px; | |
border: 2px solid #1e3799; | |
border-radius: 5px; | |
background-color: white; | |
} | |
.param-group { | |
margin: 10px 0; | |
padding: 10px; | |
border-left: 4px solid #1e3799; | |
background-color: #f8f9fa; | |
} | |
.footer { | |
text-align: center; | |
margin-top: 20px; | |
padding: 10px; | |
color: #666; | |
font-size: 0.9em; | |
} | |
button { | |
background-color: #1e3799; | |
color: white; | |
border: none; | |
padding: 10px 20px; | |
border-radius: 5px; | |
cursor: pointer; | |
margin: 5px; | |
} | |
button:hover { | |
background-color: #0c2461; | |
} | |
.data-panel { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 20px; | |
margin-top: 20px; | |
} | |
.generations-table { | |
max-height: 300px; | |
overflow-y: auto; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th, td { | |
padding: 8px; | |
text-align: left; | |
border-bottom: 1px solid #ddd; | |
} | |
th { | |
background-color: #1e3799; | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="header"> | |
<h1>Cancer Game Theory Simulation</h1> | |
</div> | |
<div class="rules"> | |
<h2>Simulation Rules</h2> | |
<ul> | |
<li>Cancer cells die when surrounded by 3+ cells within <span id="deathProxDisplay">25</span>px</li> | |
<li>Healthy cells die when near 3+ cancer cells within 25px</li> | |
<li>Healthy cells gain defense from neighbors and move 20% faster</li> | |
<li>Cancer cells become vulnerable when isolated</li> | |
<li>Cells reproduce with population-based adjustments</li> | |
<li>Neural networks control movement decisions</li> | |
</ul> | |
</div> | |
<div class="controls"> | |
<h2>Simulation Parameters</h2> | |
<div class="param-group"> | |
<label>Death Proximity (px): <input type="number" id="deathProximity" value="25" min="1"></label> | |
<label>Healthy Death Threshold: <input type="number" id="healthyDeathThreshold" value="3" min="1"></label> | |
<label>Healthy Death Radius: <input type="number" id="healthyDeathRadius" value="25" min="1"></label> | |
<label>Hidden Dimension: <input type="number" id="hiddenDim" value="6" min="1"></label> | |
<label>Initial Healthy: <input type="number" id="initialHealthy" value="20" min="1"></label> | |
<label>Initial Cancer: <input type="number" id="initialCancer" value="5" min="1"></label> | |
<label>Mutation Rate: <input type="number" id="mutationRate" value="0.08" step="0.01" min="0"></label> | |
<label>Healthy Repro Rate: <input type="number" id="healthyRepro" value="3" min="0"></label> | |
<label>Cancer Repro Rate: <input type="number" id="cancerRepro" value="1" min="0"></label> | |
</div> | |
<button id="start">Start</button> | |
<button id="reset">Reset</button> | |
<div id="status"> | |
Generation: <span id="genCount">1</span> | | |
Healthy: <span id="healthyCount">0</span> | | |
Cancer: <span id="cancerCount">0</span> | |
</div> | |
<div class="data-panel"> | |
<div class="generations-table"> | |
<h3>Generation History (Every 10 gens)</h3> | |
<table id="generationTable"> | |
<thead> | |
<tr> | |
<th>Generation</th> | |
<th>Healthy</th> | |
<th>Cancer</th> | |
<th>Avg Speed</th> | |
</tr> | |
</thead> | |
<tbody id="tableBody"></tbody> | |
</table> | |
</div> | |
<div> | |
<h3>Population Trends</h3> | |
<canvas id="populationChart"></canvas> | |
</div> | |
</div> | |
</div> | |
<canvas id="simCanvas" width="800" height="500"></canvas> | |
<div class="footer"> | |
<p>Developed by Julian Herrera | For Biology LQHS</p> | |
<p>Simulation Purpose: Demonstrate evolutionary game theory in cancer biology</p> | |
</div> | |
<script> | |
const canvas = document.getElementById('simCanvas'); | |
const ctx = canvas.getContext('2d'); | |
let cells = []; | |
let animationId; | |
let generation = 1; | |
let frameCount = 0; | |
const cellRadius = 5; | |
let populationChart = null; | |
let generationsData = []; | |
let currentHiddenDim = 6; | |
const targetFPS = 60; | |
let lastFrame = 0; | |
function getNormal(mean = 0, std = 1) { | |
let u, v, s; | |
do { | |
u = Math.random() * 2 - 1; | |
v = Math.random() * 2 - 1; | |
s = u * u + v * v; | |
} while (s >= 1 || s === 0); | |
s = Math.sqrt(-2 * Math.log(s)/s); | |
return mean + std * u * s; | |
} | |
class NeuralNetwork { | |
constructor(parent = null) { | |
const inputSize = 8; | |
const outputSize = 2; | |
if(parent) { | |
this.weights1 = parent.weights1.map(row => | |
row.map(w => w + getNormal(0, parseFloat(document.getElementById('mutationRate').value))) | |
); | |
this.weights2 = parent.weights2.map(row => | |
row.map(w => w + getNormal(0, parseFloat(document.getElementById('mutationRate').value))) | |
); | |
} else { | |
this.weights1 = Array.from({length: inputSize}, () => | |
Array.from({length: currentHiddenDim}, () => getNormal(0, 1))); | |
this.weights2 = Array.from({length: currentHiddenDim}, () => | |
Array.from({length: outputSize}, () => getNormal(0, 1))); | |
} | |
} | |
activate(x) { | |
return x; | |
} | |
predict(inputs) { | |
const hidden = this.weights1[0].map((_, i) => | |
this.activate(inputs.reduce((sum, val, j) => sum + val * this.weights1[j][i], 0)) | |
); | |
return this.weights2[0].map((_, i) => | |
this.activate(hidden.reduce((sum, val, j) => sum + val * this.weights2[j][i], 0)) | |
); | |
} | |
} | |
class Cell { | |
constructor(type, parent = null) { | |
this.type = type; | |
this.brain = parent ? new NeuralNetwork(parent.brain) : new NeuralNetwork(); | |
this.x = parent ? | |
parent.x + (Math.random() * 40 - 20) : | |
Math.random() * canvas.width; | |
this.y = parent ? | |
parent.y + (Math.random() * 40 - 20) : | |
Math.random() * canvas.height; | |
this.speed = 0; | |
this.defense = type === 'healthy' ? Math.random() * 0.3 : 0; | |
} | |
getNearbyCells() { | |
return cells.filter(c => c !== this) | |
.map(c => ({ | |
dx: c.x - this.x, | |
dy: c.y - this.y, | |
dist: Math.hypot(c.x - this.x, c.y - this.y), | |
type: c.type | |
})).sort((a, b) => a.dist - b.dist).slice(0, 4); | |
} | |
update() { | |
const nearby = this.getNearbyCells(); | |
const inputs = []; | |
for(let i = 0; i < 4; i++) { | |
inputs.push(nearby[i] ? nearby[i].dist / 800 : 0); | |
inputs.push(nearby[i] ? (nearby[i].type === 'healthy' ? 0 : 1) : 0); | |
} | |
const [vx, vy] = this.brain.predict(inputs); | |
if(this.type === 'healthy') { | |
this.x += vx * 1.2; | |
this.y += vy * 1.2; | |
this.speed = Math.hypot(vx, vy) * 1.2; | |
} else { | |
this.x += vx; | |
this.y += vy; | |
this.speed = Math.hypot(vx, vy); | |
} | |
this.x = (this.x + canvas.width) % canvas.width; | |
this.y = (this.y + canvas.height) % canvas.height; | |
} | |
draw() { | |
const baseColor = this.type === 'healthy' ? '#00ff00' : '#ff0000'; | |
const defenseBoost = Math.min(this.defense * 100, 50); | |
ctx.fillStyle = this.type === 'healthy' | |
? `hsl(120, 100%, ${50 + defenseBoost}%)` | |
: baseColor; | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, cellRadius, 0, Math.PI * 2); | |
ctx.fill(); | |
} | |
} | |
function checkCollisions() { | |
const deathProximity = parseInt(document.getElementById('deathProximity').value); | |
const healthyDeathThreshold = parseInt(document.getElementById('healthyDeathThreshold').value); | |
const healthyDeathRadius = parseInt(document.getElementById('healthyDeathRadius').value); | |
const cellsCopy = [...cells]; | |
const cellsToRemove = new Set(); | |
const cellsToConvert = new Set(); | |
cellsCopy.forEach((cell) => { | |
if(cell.type === 'healthy') { | |
const healthyNeighbors = cellsCopy.filter(c => | |
c.type === 'healthy' && | |
Math.hypot(c.x - cell.x, c.y - cell.y) < 50 | |
); | |
const defenseBoost = Math.min(healthyNeighbors.length * 0.1, 0.5); | |
const nearbyCancer = cellsCopy.filter(c => | |
c.type === 'cancer' && | |
Math.hypot(c.x - cell.x, c.y - cell.y) < healthyDeathRadius | |
); | |
if(nearbyCancer.length >= healthyDeathThreshold && | |
Math.random() > (cell.defense + defenseBoost)) { | |
cellsToRemove.add(cell); | |
} | |
} | |
if(cell.type === 'cancer') { | |
const cancerNeighbors = cellsCopy.filter(c => | |
c.type === 'cancer' && | |
c !== cell && | |
Math.hypot(c.x - cell.x, c.y - cell.y) < 60 | |
); | |
const neighbors = cellsCopy.filter(c => | |
c !== cell && | |
Math.hypot(c.x - cell.x, c.y - cell.y) < deathProximity | |
); | |
if((neighbors.length >= 3) || (cancerNeighbors.length === 0 && Math.random() < 0.1)) { | |
cellsToRemove.add(cell); | |
} | |
cellsCopy.forEach((other) => { | |
if(other.type === 'healthy' && | |
Math.hypot(cell.x - other.x, cell.y - other.y) < cellRadius * 2 && | |
!cellsToConvert.has(other)) { | |
const resistance = other.defense + (Math.random() * 0.2); | |
if(resistance < 0.7) { | |
cellsToConvert.add(other); | |
} | |
} | |
}); | |
} | |
}); | |
cells = cells.filter(cell => !cellsToRemove.has(cell)); | |
cellsToConvert.forEach(cell => cell.type = 'cancer'); | |
} | |
function reproduceCells() { | |
const healthyReproRate = parseInt(document.getElementById('healthyRepro').value); | |
const healthyCells = cells.filter(c => c.type === 'healthy'); | |
const boost = Math.max(0, 3 - Math.floor(healthyCells.length / 5)); | |
const healthyCandidates = [...healthyCells].sort(() => Math.random() - 0.5) | |
.slice(0, healthyReproRate + boost); | |
healthyCandidates.forEach(cell => cells.push(new Cell('healthy', cell))); | |
const cancerReproRate = parseInt(document.getElementById('cancerRepro').value); | |
const cancerCells = cells.filter(c => c.type === 'cancer'); | |
const penalty = Math.floor(cancerCells.length / 10); | |
const cancerCandidates = [...cancerCells].sort(() => Math.random() - 0.5) | |
.slice(0, Math.max(0, cancerReproRate - penalty)); | |
cancerCandidates.forEach(cell => cells.push(new Cell('cancer', cell))); | |
} | |
function updateStatus() { | |
document.getElementById('genCount').textContent = generation; | |
document.getElementById('healthyCount').textContent = | |
cells.filter(c => c.type === 'healthy').length; | |
document.getElementById('cancerCount').textContent = | |
cells.filter(c => c.type === 'cancer').length; | |
} | |
function saveGenerationData() { | |
if(generation % 10 === 0) { | |
const healthy = cells.filter(c => c.type === 'healthy').length; | |
const cancer = cells.filter(c => c.type === 'cancer').length; | |
const speeds = cells.map(c => c.speed); | |
const avgSpeed = speeds.reduce((a,b) => a + b, 0) / speeds.length || 0; | |
generationsData.push({ | |
generation, | |
healthy, | |
cancer, | |
avgSpeed | |
}); | |
if(generationsData.length > 20) generationsData.shift(); | |
updateChart(); | |
updateTable(); | |
} | |
} | |
function updateChart() { | |
const ctx = document.getElementById('populationChart').getContext('2d'); | |
if(populationChart) { | |
populationChart.destroy(); | |
} | |
populationChart = new Chart(ctx, { | |
type: 'line', | |
data: { | |
labels: generationsData.map(d => d.generation), | |
datasets: [{ | |
label: 'Healthy Cells', | |
data: generationsData.map(d => d.healthy), | |
borderColor: '#00ff00', | |
tension: 0.1 | |
}, { | |
label: 'Cancer Cells', | |
data: generationsData.map(d => d.cancer), | |
borderColor: '#ff0000', | |
tension: 0.1 | |
}] | |
}, | |
options: { | |
responsive: true, | |
scales: { | |
y: { | |
beginAtZero: true | |
} | |
} | |
} | |
}); | |
} | |
function updateTable() { | |
const tableBody = document.getElementById('tableBody'); | |
tableBody.innerHTML = generationsData.map(d => ` | |
<tr> | |
<td>${d.generation}</td> | |
<td>${d.healthy}</td> | |
<td>${d.cancer}</td> | |
<td>${d.avgSpeed.toFixed(2)}</td> | |
</tr> | |
`).join(''); | |
} | |
function safeAnimate(timestamp) { | |
try { | |
const delta = timestamp - lastFrame; | |
if (delta >= 1000/targetFPS) { | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
frameCount++; | |
if(frameCount % 60 === 0) { | |
generation++; | |
reproduceCells(); | |
updateStatus(); | |
saveGenerationData(); | |
} | |
cells.forEach(cell => cell.update()); | |
checkCollisions(); | |
cells.forEach(cell => cell.draw()); | |
lastFrame = timestamp; | |
} | |
animationId = requestAnimationFrame(safeAnimate); | |
} catch (error) { | |
console.error('Simulation error:', error); | |
document.getElementById('status').innerHTML += ' [PAUSED DUE TO ERROR]'; | |
cancelAnimationFrame(animationId); | |
} | |
} | |
document.getElementById('start').addEventListener('click', () => { | |
if(!animationId) { | |
lastFrame = performance.now(); | |
animationId = requestAnimationFrame(safeAnimate); | |
} | |
}); | |
document.getElementById('reset').addEventListener('click', () => { | |
document.getElementById('deathProximity').value = 25; | |
document.getElementById('healthyDeathThreshold').value = 3; | |
document.getElementById('healthyDeathRadius').value = 25; | |
document.getElementById('hiddenDim').value = 6; | |
document.getElementById('initialHealthy').value = 20; | |
document.getElementById('initialCancer').value = 5; | |
document.getElementById('mutationRate').value = 0.08; | |
document.getElementById('healthyRepro').value = 3; | |
document.getElementById('cancerRepro').value = 1; | |
cancelAnimationFrame(animationId); | |
animationId = null; | |
generation = 1; | |
frameCount = 0; | |
cells = []; | |
generationsData = []; | |
currentHiddenDim = parseInt(document.getElementById('hiddenDim').value); | |
const initialHealthy = parseInt(document.getElementById('initialHealthy').value); | |
const initialCancer = parseInt(document.getElementById('initialCancer').value); | |
for(let i = 0; i < initialHealthy; i++) cells.push(new Cell('healthy')); | |
for(let i = 0; i < initialCancer; i++) cells.push(new Cell('cancer')); | |
updateStatus(); | |
updateChart(); | |
updateTable(); | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
cells.forEach(cell => cell.draw()); | |
}); | |
document.getElementById('deathProximity').addEventListener('input', function() { | |
document.getElementById('deathProxDisplay').textContent = this.value; | |
}); | |
document.getElementById('reset').click(); | |
</script> | |
</body> | |
</html> |