diff --git a/public/tide-pool-world.js b/public/tide-pool-world.js index 22da2e7..5322cb1 100644 --- a/public/tide-pool-world.js +++ b/public/tide-pool-world.js @@ -19,7 +19,7 @@ export const SPECIES = { tab: { code: 'TB', name: 'Tab', kind: 'swimmer', speed: 1.05, max: 28, start: 12, min: 5, burn: .007, radius: .14 }, pylon: { code: 'PY', name: 'Pylon', kind: 'sessile', max: 10, start: 4, min: 2, burn: .0025, radius: .3 }, collector: { code: 'CL', name: 'Collector', kind: 'walker', speed: .95, max: 8, start: 3, min: 1, burn: .0035, radius: .38, core: true }, - mason: { code: 'MS', name: 'Mason', kind: 'walker', speed: .58, max: 8, start: 3, min: 1, burn: .0045, radius: .4, core: true }, + mason: { code: 'MS', name: 'Mason', kind: 'walker', speed: .58, max: 8, start: 3, min: 2, burn: .0045, radius: .4, core: true }, breaker: { code: 'BR', name: 'Breaker', kind: 'walker', speed: .75, max: 6, start: 2, min: 1, burn: .0042, radius: .48, core: true }, borer: { code: 'BO', name: 'Borer', kind: 'walker', speed: .32, max: 8, start: 4, min: 2, burn: .004, radius: .34 }, }; @@ -465,10 +465,13 @@ function breed(world, c) { if (!canBreed(world, c.sp)) return null; const S = SPECIES[c.sp]; let at = null; - for (let i = 0; i < 10 && !at; i++) { - const a = world.random() * Math.PI * 2, r = S.kind === 'sessile' ? 1.2 + world.random() * 1.4 : .5; + for (let i = 0; i < 16 && !at; i++) { + // A pylon's bud sometimes drifts far, like a spore, and settles in other standing water. + const far = S.kind === 'sessile' && world.random() < .45; + const a = world.random() * Math.PI * 2, r = S.kind === 'sessile' ? (far ? 5 + world.random() * 14 : 1.2 + world.random() * 1.4) : .5; const x = c.x + Math.cos(a) * r, z = c.z + Math.sin(a) * r; - const ok = S.kind === 'walker' ? onShelf(world, x, z) : depthAt(world, x, z) > (S.kind === 'sessile' ? .25 : .1); + const ok = S.kind === 'walker' ? onShelf(world, x, z) : + S.kind === 'sessile' ? depthAt(world, x, z) > .25 && world.S[cellAt(x, z)] > TIDE_LOW + .25 : depthAt(world, x, z) > .1; const clear = S.kind !== 'sessile' || world.creatures.every(o => o.sp !== 'pylon' || distance(o, { x, z }) > 1); if (ok && clear) at = { x, z }; } @@ -479,6 +482,7 @@ function breed(world, c) { child.angle = c.angle; child.heading = c.heading + (world.random() - .5) * 2; if (c.sp === 'tab') { child.vx = -c.vz; child.vz = c.vx; } if (c.sp === 'borer') child.home = settle(world, c) || child.home; + if (c.sp === 'collector' || c.sp === 'mason') child.home = roomy(world, child, 4, 12, c.sp === 'mason') || child.home; event(world, 'birth', c, null, label(child)); return child; } @@ -514,12 +518,15 @@ function think(world, c) { } function graze(world, c, efficiency = 1) { + // Good film, not too far, and not already crowded by others of the same kind. A few samples look much further afield. let best = null, score = -Infinity; - for (let k = 0; k < 14; k++) { - const r = k < 8 ? 3 : 9; + for (let k = 0; k < 16; k++) { + const r = k < 8 ? 3 : k < 13 ? 9 : 18; const x = c.x + (world.random() - .5) * r * 2, z = c.z + (world.random() - .5) * r * 2; if (!onShelf(world, x, z)) continue; - const s = world.film[cellAt(x, z)] - Math.hypot(x - c.x, z - c.z) * .04; + let crowd = 0; + for (const o of world.creatures) if (o !== c && o.sp === c.sp && Math.abs(o.x - x) < 2.5 && Math.abs(o.z - z) < 2.5) crowd++; + const s = world.film[cellAt(x, z)] - Math.hypot(x - c.x, z - c.z) * .025 - crowd * .12; if (s > score) { score = s; best = { x, z }; } } assign(world, c, 'graze', null, { at: best || { x: c.x, z: c.z }, patience: 14 }); @@ -616,14 +623,16 @@ function thinkMason(world, c) { const spot = ringSpot(world, c); if (spot) return assign(world, c, 'build', null, { at: spot, patience: 50, reach: .45 }); // The home is finished: start another nearby, on ground that is neither built nor deep. - const next = { x: c.home.x + (world.random() - .5) * 6, z: c.home.z + (world.random() - .5) * 6 }; - c.home = onShelf(world, next.x, next.z) ? next : { x: c.x, z: c.z }; + c.home = roomy(world, c, 3, 12, true) || { x: c.x, z: c.z }; event(world, 'home', c); return idle(world, c); } - if (c.energy < .4) return graze(world, c); - if (c.energy > .8 && canBreed(world, 'mason')) { - const source = brassSource(world, c, world.random() < .5); + // Once hungry, a mason keeps feeding until it is properly full, which leaves it strong enough to breed. + if (c.energy < .4) c.hungry = true; + if (c.energy > .85) c.hungry = false; + if (c.hungry) return graze(world, c); + if (c.energy > .7 && canBreed(world, 'mason')) { + const source = brassSource(world, c, world.random() < .55); if (source) return assign(world, c, source.steal ? 'steal' : 'fetch', source.object, { use: 'breed' }); } if (c.energy > .45 && ringSpot(world, c)) { @@ -641,7 +650,7 @@ function thinkBreaker(world, c) { const source = brassSource(world, c, true); if (source) return assign(world, c, source.steal ? 'steal' : 'fetch', source.object, { use: 'breed' }); } - if (c.energy < .92) { + if (c.energy < .7) { const target = nearest(c, world.creatures.filter(s => s.sp === 'scraper' && s.shell === null), 28); if (target) { // Prey inside a wall: break the wall down first. @@ -682,6 +691,23 @@ function nearbyWater(world, c, radius = 14) { } return best; } +// Somewhere to settle: on the shelf, clear of others of the same kind, and near water other than the crowded first pool when possible. +function roomy(world, c, r0, r1, wantWater) { + let best = null, score = -Infinity; + for (let k = 0; k < 24; k++) { + const a = world.random() * Math.PI * 2, r = r0 + world.random() * (r1 - r0); + const x = c.x + Math.cos(a) * r, z = c.z + Math.sin(a) * r; + if (!onShelf(world, x, z) || z > 10) continue; + let near = 99; + for (const o of world.creatures) if (o !== c && o.sp === c.sp && o.home) near = Math.min(near, distance(o.home, { x, z })); + let wet = 0; + for (let n = 0; n < 6; n++) { const b = n / 6 * Math.PI * 2; if (depthAt(world, x + Math.cos(b) * 2, z + Math.sin(b) * 2) > .15) wet++; } + const first = bodyAt(world, x, z) === 'I'; + const s = Math.min(near, 8) + (wantWater ? wet * .8 : 0) - (first ? 4 : 0) - (depthAt(world, x, z) > .5 ? 3 : 0); + if (s > score) { score = s; best = { x, z }; } + } + return best; +} // A new burrow site: dry shelf a fair walk from any standing water, so its burrow starts a pool of its own. function settle(world, c) { for (let k = 0; k < 30; k++) { @@ -953,6 +979,12 @@ function moveSwimmer(world, c, dt) { } if (best > depth) { ax += bx * 2.5 * worry; az += bz * 2.5 * worry; } } + // At high water the school roams the flooded shelf along a slowly turning heading. + const high = tideOf(world.time); + if (high > .55 && depth > .15) { + c.heading += (world.random() - .5) * dt * 1.2; + ax += Math.cos(c.heading) * 1.4 * (high - .55) * 2.2; az += Math.sin(c.heading) * 1.4 * (high - .55) * 2.2; + } const a = (world.random() - .5) * 2.2; ax += Math.cos(c.phase * .3 + a) * .5; az += Math.sin(c.phase * .3 + a) * .5; c.vx += ax * dt; c.vz += az * dt; diff --git a/public/tide-pool.js b/public/tide-pool.js index 323a921..4de7a05 100644 --- a/public/tide-pool.js +++ b/public/tide-pool.js @@ -1,7 +1,7 @@ import { createWorld, advanceWorld, offerObject, SPECIES, SPECIES_ORDER, CELL, COLS, ROWS, X0, Z0, X1, Z1, START_POOL, waterLevel, daylight, heightAt, surfaceAt, depthAt, onShelf, bodyAt, tideOf, tideRising, label, goalText, describeWorld, creatureById, -} from './tide-pool-world.js?v=8'; +} from './tide-pool-world.js?v=9'; const root = document.querySelector('[data-tide-pool]'); const status = document.querySelector('#tide-status'); diff --git a/scripts/tide-pool.test.mjs b/scripts/tide-pool.test.mjs index 3eaf822..6e08270 100644 --- a/scripts/tide-pool.test.mjs +++ b/scripts/tide-pool.test.mjs @@ -166,8 +166,11 @@ test('pylons fold shut when the water is disturbed and reopen once it is still', dropObject(world, 'pebble', pylon.x + .3, pylon.z); run(world, 1); assert.ok(pylon.open < .3, `open ${pylon.open}`); + // Leave it alone: no one else nearby to disturb the water. + world.creatures = [pylon]; + world.ripples = []; run(world, 15); - assert.ok(pylon.open > .6 || depthAt(world, pylon.x, pylon.z) < WET); + assert.ok(pylon.open > .6 || depthAt(world, pylon.x, pylon.z) < WET, `open ${pylon.open}`); }); test('a runaway shelf stays bounded: no ground below the digging floor and all water finite', () => { diff --git a/src/components/tide-pool.tsx b/src/components/tide-pool.tsx index bef8bf1..d613b3a 100644 --- a/src/components/tide-pool.tsx +++ b/src/components/tide-pool.tsx @@ -138,7 +138,7 @@ export function TidePoolContent() {

Rendered with Three.js. Software license.

- + ); }