Initial commit from WSL migration

This commit is contained in:
Rocky
2026-04-09 13:42:10 +02:00
commit d91d1fe571
41 changed files with 20181 additions and 0 deletions

View File

@@ -0,0 +1,619 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>物种进化模拟器</title>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body {
background:#080810;
color:#aaa;
font-family:'Courier New',monospace;
display:flex;
flex-direction:column;
align-items:center;
padding:10px;
min-height:100vh;
}
h1 {
color:#ccc;
font-size:13px;
letter-spacing:6px;
text-transform:uppercase;
margin-bottom:8px;
opacity:0.6;
}
.layout {
display:flex;
gap:8px;
align-items:flex-start;
}
.left { display:flex; flex-direction:column; gap:5px; }
#game { border:1px solid #1a1a28; cursor:crosshair; display:block; }
#graph { border:1px solid #1a1a28; display:block; }
.sidebar {
width:185px;
display:flex;
flex-direction:column;
gap:5px;
}
.card {
background:#0c0c18;
border:1px solid #1a1a28;
border-radius:3px;
padding:7px;
}
.card-title {
font-size:8px;
letter-spacing:3px;
text-transform:uppercase;
color:#333;
margin-bottom:5px;
}
.big-num {
font-size:26px;
color:#ddd;
font-weight:bold;
text-align:center;
letter-spacing:2px;
}
.big-label {
font-size:8px;
color:#2a2a3a;
text-align:center;
letter-spacing:3px;
text-transform:uppercase;
}
.stat-row {
display:flex;
justify-content:space-between;
font-size:9px;
color:#3a3a4a;
margin-top:2px;
}
.stat-row span:last-child { color:#666; }
button {
display:block;
width:100%;
background:#0e0e1c;
color:#666;
border:1px solid #222233;
padding:4px 7px;
font-family:'Courier New',monospace;
font-size:10px;
border-radius:2px;
cursor:pointer;
margin-bottom:3px;
text-align:left;
transition:all 0.1s;
}
button:hover { background:#161624; color:#aaa; border-color:#334; }
button:active { background:#1e1e30; }
.sp-item {
display:flex;
align-items:center;
gap:4px;
padding:3px;
border-radius:2px;
cursor:pointer;
border:1px solid transparent;
margin-bottom:2px;
}
.sp-item:hover { background:#0e0e1c; }
.sp-item.active { border-color:#223; background:#0e0e1c; }
.sp-dot { width:9px; height:9px; border-radius:50%; flex-shrink:0; }
.sp-name { font-size:9px; flex:1; color:#888; }
.sp-pop { font-size:9px; color:#444; min-width:28px; text-align:right; }
.sp-gene { font-size:8px; color:#2a2a3a; }
.events-box {
height:66px;
overflow-y:auto;
font-size:8px;
line-height:1.7;
}
.ev { color:#2a2a3a; border-bottom:1px solid #0e0e18; padding:0; }
.ev-birth { color:#2a5a3a; }
.ev-extinct { color:#5a2a2a; }
.ev-mutate { color:#5a4a2a; }
input[type=range] { width:100%; margin:1px 0 5px; accent-color:#334; }
input[type=text] {
width:100%;
background:#06060e;
border:1px solid #1a1a28;
color:#666;
padding:3px 4px;
font-family:'Courier New',monospace;
font-size:9px;
border-radius:2px;
margin-bottom:3px;
}
input[type=text]:focus { outline:none; border-color:#334; color:#999; }
label { font-size:9px; color:#333; display:block; margin-bottom:1px; }
.gene-display {
background:#06060e;
border:1px solid #1a1a28;
border-radius:2px;
padding:3px 5px;
font-size:9px;
color:#444;
margin-top:3px;
word-break:break-all;
min-height:16px;
}
.copy-btn {
background:#0a0a16;
color:#445;
border:1px solid #1a1a28;
padding:2px 5px;
font-size:8px;
cursor:pointer;
border-radius:2px;
width:auto;
display:inline-block;
margin:2px 0 0;
}
.copy-btn:hover { color:#88a; border-color:#446; }
</style>
</head>
<body>
<h1>🧬 物种进化模拟器</h1>
<div class="layout">
<div class="left">
<canvas id="game" width="720" height="480"></canvas>
<canvas id="graph" width="720" height="88"></canvas>
</div>
<div class="sidebar">
<!-- Stats -->
<div class="card">
<div class="big-num" id="genNum">0</div>
<div class="big-label">Generation</div>
<div class="stat-row"><span>活细胞</span><span id="aliveNum">0</span></div>
<div class="stat-row"><span>现存物种</span><span id="spAlive">0</span></div>
<div class="stat-row"><span>已灭绝</span><span id="spDead">0</span></div>
</div>
<!-- Controls -->
<div class="card">
<div class="card-title">控制</div>
<button id="btnPlay">▶ 开始</button>
<button id="btnStep">⏭ 单步</button>
<button id="btnReset">↺ 重置</button>
<button id="btnMeteor">☄ 陨石打击</button>
<label>速度 <span id="spdVal">10</span> fps</label>
<input type="range" id="spdSlider" min="1" max="30" value="10">
</div>
<!-- Species list / draw -->
<div class="card">
<div class="card-title">物种(点击切换画笔)</div>
<div id="spList"></div>
<button id="btnErase" style="margin-top:2px">✕ 橡皮擦</button>
<button id="btnAddRandom">+ 加入随机物种</button>
<div class="card-title" style="margin-top:5px">当前物种基因码</div>
<div class="gene-display" id="geneDisplay"></div>
<button class="copy-btn" id="btnCopy">📋 复制基因码</button>
</div>
<!-- Import -->
<div class="card">
<div class="card-title">导入基因码</div>
<input type="text" id="geneInput" placeholder="色相-出生-存min-存max-变异率">
<button id="btnImport">↓ 导入物种</button>
</div>
<!-- Log -->
<div class="card">
<div class="card-title">进化日志</div>
<div class="events-box" id="evLog"></div>
</div>
</div>
</div>
<script>
// ===== CONSTANTS =====
const COLS = 120, ROWS = 80, CS = 6;
const MAX_SP = 14, HIST = 300;
// ===== STATE =====
let grid, sp, nextId, gen, running, raf, lastT, spd;
let drawSid = 1, mdown = false;
// ===== HELPERS =====
const clamp = (v,lo,hi) => Math.max(lo,Math.min(hi,v));
const rnd = (lo,hi) => lo + Math.floor(Math.random()*(hi-lo+1));
// ===== SPECIES =====
function addSp(opts={}) {
if (Object.keys(sp).length >= MAX_SP) return null;
const id = nextId++;
let birth = opts.birth ?? rnd(2,3);
let smin = opts.smin ?? rnd(1,2);
let smax = opts.smax ?? rnd(smin+1, smin+2);
let mut = opts.mut ?? (0.5 + Math.random()*2.5);
smin = clamp(smin, 0, 7);
smax = clamp(smax, smin+1, 8);
birth = clamp(birth, 1, 6);
mut = clamp(mut, 0.1, 15);
sp[id] = {
id,
name: opts.name ?? `物种${id}`,
hue: opts.hue ?? Math.random()*360,
birth, smin, smax, mut,
pop:0, hist:[], dead:false,
parent: opts.parent ?? null,
born: gen,
};
log(`🌱 ${sp[id].name} B${birth}/S${smin}-${smax} mut${mut.toFixed(1)}`, 'birth');
return id;
}
function geneCode(id) {
const s = sp[id];
if (!s) return '';
return `${Math.round(s.hue)}-${s.birth}-${s.smin}-${s.smax}-${Math.round(s.mut*10)}`;
}
function parseGene(str) {
const p = str.trim().split('-');
if (p.length < 5) return null;
const g = { hue:+p[0], birth:+p[1], smin:+p[2], smax:+p[3], mut:+p[4]/10 };
if ([g.hue,g.birth,g.smin,g.smax,g.mut].some(isNaN)) return null;
return g;
}
// ===== GRID =====
const mkGrid = () =>
Array.from({length:ROWS}, () =>
Array.from({length:COLS}, () => ({sid:0, age:0}))
);
function nbrs(x, y) {
const r = [];
for (let dy=-1; dy<=1; dy++)
for (let dx=-1; dx<=1; dx++) {
if (!dx && !dy) continue;
r.push(grid[(y+dy+ROWS)%ROWS][(x+dx+COLS)%COLS]);
}
return r;
}
// ===== CORE SIMULATION =====
// Birth rule: a dead cell is born as species S if it has exactly S.birth neighbors of species S
// Survival rule: a live cell of species S survives if it has S.smin..S.smax neighbors of same species
// Multi-species: if multiple species qualify for an empty cell, one is chosen randomly
// Mutation: on birth, small chance to produce a new sub-species (color drift + 1 gene ±1)
function tick(x, y) {
const cell = grid[y][x];
const ns = nbrs(x, y);
// Count neighbors per species
const cnt = {};
for (const n of ns) if (n.sid > 0) cnt[n.sid] = (cnt[n.sid]||0)+1;
if (cell.sid === 0) {
// --- BIRTH ---
const cands = [];
for (const [sid, c] of Object.entries(cnt)) {
const s = sp[+sid];
if (s && !s.dead && c === s.birth) cands.push(+sid);
}
if (!cands.length) return {sid:0, age:0};
const winner = cands[Math.floor(Math.random()*cands.length)];
return { sid: tryMutate(winner), age: 0 };
} else {
// --- SURVIVE ---
const s = sp[cell.sid];
if (!s) return {sid:0, age:0};
const same = cnt[cell.sid] || 0;
if (same >= s.smin && same <= s.smax)
return { sid: cell.sid, age: cell.age+1 };
return {sid:0, age:0};
}
}
function tryMutate(sid) {
const s = sp[sid];
if (!s || Math.random()*100 > s.mut) return sid;
if (Object.keys(sp).length >= MAX_SP) return sid;
const g = Math.floor(Math.random()*4);
const opts = {
name: s.name + "'",
hue: (s.hue + (Math.random()>0.5?22:-22)+360)%360,
birth: s.birth,
smin: s.smin,
smax: s.smax,
mut: clamp(s.mut*(0.6+Math.random()*0.8), 0.1, 15),
parent: sid,
};
if (g===0) opts.birth = clamp(s.birth + (Math.random()>0.5?1:-1), 1, 6);
if (g===1) opts.smin = clamp(s.smin + (Math.random()>0.5?1:-1), 0, s.smax-1);
if (g===2) opts.smax = clamp(s.smax + (Math.random()>0.5?1:-1), s.smin+1, 8);
// g===3: only mut changes
const nid = addSp(opts);
if (nid) log(`🔬 Gen${gen}: ${s.name}${sp[nid].name}`, 'mutate');
return nid ?? sid;
}
function step() {
const ng = Array.from({length:ROWS}, (_,y) =>
Array.from({length:COLS}, (_,x) => tick(x,y))
);
grid = ng;
gen++;
countPop();
checkExtinct();
}
function countPop() {
for (const id in sp) sp[id].pop = 0;
for (let y=0; y<ROWS; y++)
for (let x=0; x<COLS; x++) {
const sid = grid[y][x].sid;
if (sid && sp[sid]) sp[sid].pop++;
}
for (const id in sp) {
// Resurrection check
if (sp[id].dead && sp[id].pop > 0) {
sp[id].dead = false;
log(`${sp[id].name} 复活`, 'birth');
}
sp[id].hist.push(sp[id].pop);
if (sp[id].hist.length > HIST) sp[id].hist.shift();
}
}
function checkExtinct() {
for (const id in sp) {
const s = sp[id];
if (!s.dead && s.pop===0 && s.hist.length>8) {
s.dead = true;
log(`💀 Gen${gen}: ${s.name} 灭绝`, 'extinct');
}
}
}
// ===== RENDER =====
const gc = document.getElementById('game');
const gx = gc.getContext('2d');
const pc = document.getElementById('graph');
const px = pc.getContext('2d');
function draw() {
gx.fillStyle = '#060610';
gx.fillRect(0, 0, gc.width, gc.height);
for (let y=0; y<ROWS; y++) {
for (let x=0; x<COLS; x++) {
const {sid, age} = grid[y][x];
if (!sid || !sp[sid]) continue;
const h = sp[sid].hue;
// Age: young cells bright, old cells deeper
const l = clamp(65 - age*0.35, 30, 65);
gx.fillStyle = `hsl(${h},72%,${l}%)`;
gx.fillRect(x*CS, y*CS, CS-1, CS-1);
}
}
}
function drawGraph() {
const W = pc.width, H = pc.height;
px.fillStyle = '#050510';
px.fillRect(0, 0, W, H);
// Subtle grid
px.strokeStyle = '#0e0e1e'; px.lineWidth=1;
for (let i=1; i<4; i++) {
px.beginPath(); px.moveTo(0,H*i/4); px.lineTo(W,H*i/4); px.stroke();
}
// Find max for scale
let mx = 80;
for (const id in sp)
for (const v of sp[id].hist) if (v>mx) mx=v;
// Draw per species
for (const id in sp) {
const s = sp[id];
const h = s.hist;
if (h.length < 2) continue;
const alive = !s.dead;
px.strokeStyle = `hsl(${s.hue},70%,${alive?48:20}%)`;
px.lineWidth = alive ? 1.5 : 0.5;
px.beginPath();
for (let i=0; i<h.length; i++) {
const bx = ((HIST - h.length + i) / HIST) * W;
const by = H - (h[i]/mx)*H*0.9 - 2;
i===0 ? px.moveTo(bx,by) : px.lineTo(bx,by);
}
px.stroke();
}
// Gen label
px.fillStyle = '#222';
px.font = '9px monospace';
px.fillText(`Gen ${gen}`, 4, H-3);
}
function updateUI() {
document.getElementById('genNum').textContent = gen;
let totalCells=0, alive=0, dead=0;
for (const id in sp) {
totalCells += sp[id].pop;
if (sp[id].dead) dead++;
else if (sp[id].pop > 0) alive++;
}
document.getElementById('aliveNum').textContent = totalCells.toLocaleString();
document.getElementById('spAlive').textContent = alive;
document.getElementById('spDead').textContent = dead;
// Gene display
const gd = document.getElementById('geneDisplay');
if (drawSid > 0 && sp[drawSid]) gd.textContent = geneCode(drawSid);
else gd.textContent = '—';
// Species list
const list = document.getElementById('spList');
list.innerHTML = '';
const sorted = Object.values(sp).sort((a,b) => b.pop - a.pop);
for (const s of sorted) {
const div = document.createElement('div');
div.className = 'sp-item' + (drawSid===s.id?' active':'');
div.style.opacity = s.dead ? '0.3' : '1';
div.innerHTML = `
<div class="sp-dot" style="background:hsl(${s.hue},72%,50%)"></div>
<div style="flex:1">
<div class="sp-name">${s.name}</div>
<div class="sp-gene">B${s.birth}/S${s.smin}-${s.smax} m${s.mut.toFixed(1)}</div>
</div>
<div class="sp-pop">${s.pop}</div>
`;
div.addEventListener('click', () => { drawSid = s.id; updateUI(); });
list.appendChild(div);
}
}
// ===== GAME LOOP =====
function loop(t) {
if (!running) return;
if (t - lastT >= 1000/spd) {
step(); draw(); drawGraph(); updateUI();
lastT = t;
}
raf = requestAnimationFrame(loop);
}
// ===== INIT =====
function init() {
running = false;
if (raf) cancelAnimationFrame(raf);
document.getElementById('btnPlay').textContent = '▶ 开始';
document.getElementById('evLog').innerHTML = '';
grid = mkGrid();
sp = {};
nextId = 1;
gen = 0;
// 4 preset species with distinct personalities
addSp({ name:'红霸', hue:0, birth:3, smin:2, smax:3, mut:1.0 }); // Conway classic
addSp({ name:'蓝潮', hue:210, birth:3, smin:2, smax:4, mut:1.5 }); // More tolerant
addSp({ name:'绿芽', hue:130, birth:2, smin:1, smax:3, mut:2.5 }); // Aggressive spreader
addSp({ name:'金堡', hue:45, birth:3, smin:3, smax:5, mut:0.4 }); // Cluster fortress
// Place each in a quadrant
const seeds = [
{sid:1, cx:20, cy:20}, {sid:2, cx:100, cy:20},
{sid:3, cx:20, cy:60}, {sid:4, cx:100, cy:60},
];
for (const {sid, cx, cy} of seeds)
for (let i=0; i<45; i++) {
const x = clamp(cx + rnd(-10,10), 0, COLS-1);
const y = clamp(cy + rnd(-8,8), 0, ROWS-1);
grid[y][x] = { sid, age:0 };
}
drawSid = 1;
draw(); drawGraph(); updateUI();
}
// ===== MOUSE =====
function gridPos(e) {
const r = gc.getBoundingClientRect();
return {
x: clamp(Math.floor((e.clientX-r.left)*(COLS/r.width)), 0, COLS-1),
y: clamp(Math.floor((e.clientY-r.top) *(ROWS/r.height)), 0, ROWS-1),
};
}
function paint(x, y) {
const br = 2;
for (let dy=-br; dy<=br; dy++)
for (let dx=-br; dx<=br; dx++) {
if (dx*dx+dy*dy > br*br+1) continue;
const nx=clamp(x+dx,0,COLS-1), ny=clamp(y+dy,0,ROWS-1);
grid[ny][nx] = drawSid===0 ? {sid:0,age:0} : {sid:drawSid,age:0};
}
if (!running) { draw(); updateUI(); }
}
gc.addEventListener('mousedown', e => { mdown=true; const p=gridPos(e); paint(p.x,p.y); });
gc.addEventListener('mousemove', e => { if(mdown){ const p=gridPos(e); paint(p.x,p.y); }});
gc.addEventListener('mouseup', () => mdown=false);
gc.addEventListener('mouseleave', () => mdown=false);
gc.addEventListener('contextmenu',e => e.preventDefault());
// ===== BUTTONS =====
document.getElementById('btnPlay').addEventListener('click', () => {
running = !running;
document.getElementById('btnPlay').textContent = running ? '⏸ 暂停' : '▶ 开始';
if (running) { lastT=0; raf=requestAnimationFrame(loop); }
});
document.getElementById('btnStep').addEventListener('click', () => {
if (running) return;
step(); draw(); drawGraph(); updateUI();
});
document.getElementById('btnReset').addEventListener('click', init);
document.getElementById('btnMeteor').addEventListener('click', () => {
const cx=rnd(12,COLS-12), cy=rnd(8,ROWS-8), r=rnd(7,13);
for (let dy=-r; dy<=r; dy++)
for (let dx=-r; dx<=r; dx++)
if (dx*dx+dy*dy<=r*r)
grid[clamp(cy+dy,0,ROWS-1)][clamp(cx+dx,0,COLS-1)] = {sid:0,age:0};
log(`☄ Gen${gen}: 陨石 [${cx},${cy}] r=${r}`, 'extinct');
if (!running) { draw(); updateUI(); }
});
document.getElementById('spdSlider').addEventListener('input', e => {
spd = +e.target.value;
document.getElementById('spdVal').textContent = spd;
});
document.getElementById('btnErase').addEventListener('click', () => { drawSid=0; updateUI(); });
document.getElementById('btnAddRandom').addEventListener('click', () => {
const id = addSp();
if (id) { drawSid=id; updateUI(); }
});
document.getElementById('btnCopy').addEventListener('click', () => {
if (drawSid > 0 && sp[drawSid]) {
const code = geneCode(drawSid);
navigator.clipboard.writeText(code).then(() => {
const btn = document.getElementById('btnCopy');
btn.textContent = '✓ 已复制';
setTimeout(() => btn.textContent = '📋 复制基因码', 1200);
});
}
});
document.getElementById('btnImport').addEventListener('click', () => {
const inp = document.getElementById('geneInput');
const g = parseGene(inp.value);
if (!g) { alert('格式错误\n正确格式: 色相-出生-存min-存max-变异率\n例如: 270-3-2-3-15'); return; }
const id = addSp(g);
if (id) { drawSid=id; inp.value=''; updateUI(); }
});
// ===== LOG =====
function log(msg, type='') {
const box = document.getElementById('evLog');
const d = document.createElement('div');
d.className = `ev ev-${type}`;
d.textContent = msg;
box.insertBefore(d, box.firstChild);
while(box.children.length > 40) box.removeChild(box.lastChild);
}
// ===== START =====
spd = 10;
init();
</script>
</body>
</html>