返回 CodeWhale
pet-native.ts
根目录 / pet / src / core / pet-native.ts
1 import { PetWorld, type PetInteraction, type PetSegment } from './pet-world.js';
2 import { compilePetTelemetry, decodePetJSONL, PetLiveTape } from './pet-telemetry.js';
3 import { ARCH_OF, digest, layout, PetSim } from './pet-sim.js';
4 import { renderPetPCM, type PetVoice } from './pet-audio.js';
5 import { PetEngineTelemetry } from './pet-engine.js';
6
7 /** Synchronous native boundary: JSON state and directly transferable PCM.
8 * Native hosts share the actual world / score implementation, not a rewrite. */
9 export class PetNative {
10 private world: PetWorld;
11 private engine = new PetEngineTelemetry();
12 private engineTick = 0;
13 private segment?: PetSegment;
14 private liveTape = new PetLiveTape();
15 private stillProjection?: { key: string; points: number[][]; style: PetSim['frame'] };
16 constructor(pointsJSON: string, tapeJSONL = '', interactionsJSON = '[]', live = false, expressionVersion: 1 | 2 = 2) {
17 const points = JSON.parse(pointsJSON) as [number, number][];
18 if (!Array.isArray(points) || points.length !== 980 || points.some(p => !Array.isArray(p) || p.length !== 2 || !p.every(n => Number.isFinite(n) && Math.abs(n) <= 1)))
19 throw new Error('Invalid native whale body.');
20 this.world = new PetWorld(points, live ? compilePetTelemetry([]) : decodePetJSONL(tapeJSONL), JSON.parse(interactionsJSON) as PetInteraction[], expressionVersion, true);
21 }
22 step(dt: number, motion: boolean): string { this.world.step(dt, { motion, sensitivity: 1 }); return this.snapshot(); }
23 snapshot(): string { return JSON.stringify({ ...this.world.frame, voices: this.world.voices, digest: digest(this.world.sim) }); }
24 /** View-only projection. Display cadence and accessibility preferences never
25 * advance the owner, consume randomness, or change its score/checkpoint. */
26 presentation(): string {
27 const { sim, frame } = this.world;
28 const state = { ...frame.state, roamX: 0, roamY: 0, flip: 1, lit: frame.behaviour === 'doze' ? .18 : 1 };
29 const key = JSON.stringify([state, frame.pod]);
30 if (this.stillProjection?.key !== key) {
31 const still = new PetSim(sim.p.map(p => [p.hx, p.hy]), 0xC0FFEE, sim.expressionVersion);
32 const peers = frame.pod.filter(p => p.present);
33 still.step(1 / 30, state, { motion: false, sensitivity: 1,
34 podSlots: peers.length >= 3 ? peers.map(p => [[0, 2, 4, 1, 3, 5][p.slot], p.phase] as const) : undefined });
35 this.stillProjection = { key, points: still.p.map(p => [p.x, p.y]), style: still.frame };
36 }
37 return JSON.stringify({ ...frame, digest: digest(sim), style: sim.frame, activity: this.engine.activity(frame.timeMs),
38 points: sim.p.map(p => [p.x, p.y]),
39 still: { points: this.stillProjection.points, style: this.stillProjection.style, state } });
40 }
41 /** Losing a producer invalidates outstanding coverage, never the creature. */
42 disconnectEngine(): void { this.engine = new PetEngineTelemetry(); this.world.voices = []; }
43 interact(kind: PetInteraction['kind'], x: number, y: number): void { this.world.interact(kind, x, y); }
44 interactions(): string { return JSON.stringify(this.world.interactions); }
45 accept(packet: string): void { this.world.acceptTelemetry(JSON.parse(packet)); }
46 acceptLiveTail(text: string): boolean {
47 const packet = this.liveTape.readTail(text); if (!packet) return false;
48 this.world.acceptTelemetry(packet); return true;
49 }
50 resetLiveInput(): number { this.liveTape.reset(); return this.resumeEngine(); }
51 recording(withCheckpoint = false): string { return JSON.stringify(this.world.recording(withCheckpoint)); }
52 needsSegment(): boolean { return this.world.needsSegment; }
53 prepareSegment(): string { this.segment = this.world.prepareSegment(); return JSON.stringify(this.segment.recording); }
54 commitSegment(): void { if (!this.segment) throw new Error('No pet segment was prepared.'); this.segment.commit(); this.segment = undefined; }
55 checkpoint(): string { return JSON.stringify(this.world.checkpoint()); }
56 recordingChunk(index: number, completed = false): string | null { return this.world.recordingChunk(index, completed); }
57 restoreCheckpoint(text: string): void {
58 if (text.length > 512 * 1024) throw new Error('Pet checkpoint exceeds its size limit.');
59 this.restoreRecording(JSON.stringify({ ...this.world.recording(false), checkpoint: JSON.parse(text) }));
60 }
61 restoreRecording(text: string): void {
62 if (text.length > 8 * 1024 * 1024) throw new Error('Native habitat exceeds 8 MiB.');
63 const points = this.world.sim.p.map(p => [p.hx, p.hy] as [number, number]);
64 this.world = PetWorld.fromRecording(points, JSON.parse(text));
65 this.liveTape.reset();
66 this.segment = undefined; this.engineTick = Math.round(this.world.frame.timeMs * 30 / 1000); this.engine = new PetEngineTelemetry();
67 }
68 /** Resume a live host at the first unrecorded bucket. Keep every accepted
69 * interval, but never present its last observed frame as current evidence. */
70 resumeEngine(): number {
71 this.world.resumeObservation();
72 this.engineTick = Math.round(this.world.frame.timeMs * 30 / 1000);
73 this.engine = new PetEngineTelemetry();
74 this.world.voices = [];
75 return this.world.frame.timeMs;
76 }
77 observeEngine(metadataJSON: string, timeMs: number): void { this.engine.observe(JSON.parse(metadataJSON), timeMs); }
78 observeEngineBatch(metadataJSON: string, timeMs: number): void {
79 const events: unknown = JSON.parse(metadataJSON);
80 if (!Array.isArray(events) || events.length > 64) throw new Error('Invalid Engine batch.');
81 const next = this.engine.clone();
82 for (const event of events) next.observe(event, timeMs);
83 this.engine = next;
84 }
85 advanceEngine(timeMs: number, motion: boolean, waiting: boolean): void {
86 const target = Math.floor(timeMs * 30 / 1000 + 1e-8);
87 if (!Number.isFinite(timeMs) || target < this.engineTick || target - this.engineTick > 300) throw new Error('Engine pet clock jump.');
88 this.engine.confirmWaiting(timeMs, waiting);
89 const voices: PetVoice[] = [];
90 while (this.engineTick < target) {
91 if ((this.engineTick + 1) % 12 === 0) this.world.acceptTelemetry(this.engine.bucket(Math.floor(this.engineTick / 12)));
92 this.world.step(1 / 30, { motion, sensitivity: 1 });
93 voices.push(...this.world.voices);
94 this.engineTick++;
95 }
96 this.world.voices = voices;
97 }
98 terminal(width: number, height: number): string {
99 if (![width, height].every(n => Number.isSafeInteger(n) && n >= 1 && n <= 512)) throw new Error('Invalid pet raster size.');
100 const { sim, frame } = this.world, l = layout(width * 2, height * 4, frame.state);
101 const cells = Array(width * height).fill(0), bits = [[1, 8], [2, 16], [4, 32], [64, 128]];
102 sim.p.forEach((p, i) => {
103 if (frame.state.observed < .92 && i % 2 === 1) return;
104 const x = Math.round(l.ox + p.x * l.scale * l.flipX), y = Math.round(l.oy + p.y * l.scale);
105 if (x >= 0 && x < width * 2 && y >= 0 && y < height * 4) cells[Math.floor(y / 4) * width + Math.floor(x / 2)] |= bits[y % 4][x % 2];
106 });
107 return JSON.stringify({ width, height, cells, timeMs: frame.timeMs, channel: frame.state.channel, arch: ARCH_OF[frame.state.channel],
108 hollow: frame.state.observed < .92, dozing: frame.behaviour === 'doze', lit: frame.state.lit });
109 }
110 pcmChannels(voicesJSON: string, startSample: number, length: number, rate: number): [Float32Array, Float32Array] {
111 const p = renderPetPCM(JSON.parse(voicesJSON) as PetVoice[], startSample, length, rate);
112 return [p.left, p.right];
113 }
114 pcm(voicesJSON: string, startSample: number, length: number, rate: number): string {
115 return JSON.stringify(this.pcmChannels(voicesJSON, startSample, length, rate).map(channel => Array.from(channel)));
116 }
117 }
118
118 lines TYPESCRIPT