CancerSim / index.html
TeacherPuffy's picture
Update index.html
a498893 verified
<!DOCTYPE html>
<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>