1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
// Node adapter only. All rule evaluation, physics and persistent state live in Zig.
import { readFile } from 'node:fs/promises';
export const fields = [
'tick', 'y', 'vy', 'distance', 'obstacleX', 'passed', 'hits', 'jumpsUsed',
'running', 'revision', 'lastReason', 'lastSource', 'totalInputs',
'acceptedJumps', 'mode', 'impulse', 'enabled', 'grounded', 'obstacleHeight',
];
const signed = new Set(['y', 'vy', 'obstacleX']);
export async function loadCore() {
const binary = await readFile(new URL('./web/core.wasm', import.meta.url));
const module = await WebAssembly.compile(binary);
if (WebAssembly.Module.imports(module).length !== 0) throw new Error('Core must not import a host runtime.');
const instance = await WebAssembly.instantiate(module, {});
instance.exports.reset();
return instance.exports;
}
export function snapshot(core) {
const state = Object.fromEntries(fields.map((name, id) => {
const value = core.get(id);
return [name, signed.has(name) ? value : value >>> 0];
}));
return {
state,
decision: core.node_value(0) === -1 ? null : {
tick: core.decision_meta(0) >>> 0,
revision: core.decision_meta(1) >>> 0,
y: core.decision_meta(2),
jumps: core.decision_meta(3) >>> 0,
nodes: Array.from({ length: 6 }, (_, i) => core.node_value(i)),
},
history: Array.from({ length: core.history_count() }, (_, i) => ({
tick: core.history_get(i, 0) >>> 0,
kind: core.history_get(i, 1),
detail: core.history_get(i, 2) >>> 0,
revision: core.history_get(i, 3) >>> 0,
})),
};
}
export function storage(core) {
const keys = ['rules', 'state', 'decision', 'history', 'engine', 'graph', 'logicalTotal'];
return {
layoutBytes: Object.fromEntries(keys.map((key, i) => [key, core.bytes(i)])),
wasmLinearMemoryBytes: core.memory.buffer.byteLength,
note: 'Type-layout sizes; constant graph may be compiled into code. Not browser heap or RSS.',
};
}
export function parseCommand(line) {
const tokens = line.trim().split(/\s+/);
const name = tokens[0];
if (!name) return { op: 'status' };
if (['status', 'inspect', 'memory', 'reset', 'jump', 'help', 'quit'].includes(name)) {
if (tokens.length !== 1) throw new Error(`${name}: 인수가 필요하지 않습니다.`);
return { op: name };
}
if (name === 'step') {
if (tokens.length > 2) throw new Error('사용법: step [1..600]');
const count = tokens.length === 1 ? 1 : integer(tokens[1], 1, 600);
return { op: 'step', count };
}
if (name === 'rules') {
if (tokens.length !== 4 || !['ground', 'double'].includes(tokens[1]) || !['on', 'off'].includes(tokens[3])) {
throw new Error('사용법: rules ground|double 6..16 on|off');
}
return { op: 'rules', mode: tokens[1] === 'double' ? 1 : 0,
impulse: integer(tokens[2], 6, 16), enabled: tokens[3] === 'on' ? 1 : 0 };
}
throw new Error('알 수 없는 명령입니다. help를 입력하세요.');
}
function integer(text, min, max) {
if (!/^\d+$/.test(text)) throw new Error(`${min}..${max} 범위의 정수를 입력하세요.`);
const value = Number(text);
if (!Number.isSafeInteger(value) || value < min || value > max) {
throw new Error(`${min}..${max} 범위의 정수를 입력하세요.`);
}
return value;
}
export function execute(core, command) {
switch (command.op) {
case 'jump': core.input(2); break;
case 'step':
for (let n = 0; n < command.count; n += 1) core.step();
break;
case 'rules':
if (core.apply_rules(command.mode, command.impulse, command.enabled) !== 0) {
throw new Error('규칙 전체를 거절했습니다. 이전 규칙과 상태를 유지합니다.');
}
break;
case 'reset': core.reset(); break;
case 'status': case 'inspect': case 'memory': break;
default: throw new Error('실행할 수 없는 명령입니다.');
}
return snapshot(core);
}
|