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> |