session types: shared decoderConfigDigest vectors from both decoder presets
Review round 1, N2. The gameboy-decoder-config-v1 form (channels as ordered arrays), vectors for raw mode and the Pokemon Red macro group computed by flysim's legacy_profile_identity test from gameboy_decoder_config_with_macros and reproduced by @flybrain/session-types from the oracle's gameboyDecoderConfig. The example composition now carries the real macros-mode digest and channel set.
This commit is contained in:
parent
a6e1623698
commit
2f8ad4914a
10 changed files with 584 additions and 13 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -3708,6 +3708,7 @@
|
|||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@flybrain/brain": "*",
|
||||
"@types/node": "22.17.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.9.2"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"typecheck": "tsc -p tsconfig.json --pretty false"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flybrain/brain": "*",
|
||||
"@types/node": "22.17.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.9.2"
|
||||
|
|
|
|||
|
|
@ -486,3 +486,75 @@ export function compositionDeclarationDigest(composition: LegacyGameboyCompositi
|
|||
if (!isDigest(digest)) fail('digest');
|
||||
return digest;
|
||||
}
|
||||
|
||||
// The decoder configuration digest ------------------------------------------------------------
|
||||
|
||||
/** The name of the canonical decoder-configuration form (legacy-gameboy-v1 section 12). */
|
||||
export const DECODER_CONFIG_FORM = 'gameboy-decoder-config-v1';
|
||||
|
||||
interface GroupLike {
|
||||
channels: Record<string, string>;
|
||||
decisionMs: number;
|
||||
holdMs: number;
|
||||
hysteresis: number;
|
||||
fatigueGain: number;
|
||||
fatigueDecay: number;
|
||||
blockedFatigue: number;
|
||||
blockedMs: number;
|
||||
}
|
||||
|
||||
/** Structurally the oracle's `DecoderConfig` (`packages/brain/src/readout/decoder.ts`). */
|
||||
export interface DecoderConfigLike {
|
||||
exclusive?: GroupLike;
|
||||
macros?: GroupLike;
|
||||
pulses: {
|
||||
channel: string;
|
||||
role: string;
|
||||
holdMs: number;
|
||||
cooldownMs: number;
|
||||
threshold: number;
|
||||
boot?: { cooldownMs: number; threshold: number };
|
||||
throttleGroup?: string;
|
||||
}[];
|
||||
clearLockoutMs: number;
|
||||
}
|
||||
|
||||
function groupForm(group: GroupLike | undefined): unknown {
|
||||
if (!group) return null;
|
||||
return {
|
||||
// Channel order breaks argmax ties, so it is part of the identity: an array, not a map,
|
||||
// because canonical JSON sorts object keys.
|
||||
channels: Object.entries(group.channels).map(([channel, role]) => ({ channel, role })),
|
||||
decisionMs: group.decisionMs,
|
||||
holdMs: group.holdMs,
|
||||
hysteresis: group.hysteresis,
|
||||
fatigueGain: group.fatigueGain,
|
||||
fatigueDecay: group.fatigueDecay,
|
||||
blockedFatigue: group.blockedFatigue,
|
||||
blockedMs: group.blockedMs,
|
||||
};
|
||||
}
|
||||
|
||||
/** The canonical form `decoderConfigDigest` is taken over. */
|
||||
export function decoderConfigForm(config: DecoderConfigLike): unknown {
|
||||
return {
|
||||
form: DECODER_CONFIG_FORM,
|
||||
exclusive: groupForm(config.exclusive),
|
||||
macros: groupForm(config.macros),
|
||||
pulses: config.pulses.map((pulse) => ({
|
||||
channel: pulse.channel,
|
||||
role: pulse.role,
|
||||
holdMs: pulse.holdMs,
|
||||
cooldownMs: pulse.cooldownMs,
|
||||
threshold: pulse.threshold,
|
||||
boot: pulse.boot ? { cooldownMs: pulse.boot.cooldownMs, threshold: pulse.boot.threshold } : null,
|
||||
throttleGroup: pulse.throttleGroup ?? null,
|
||||
})),
|
||||
clearLockoutMs: config.clearLockoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
/** `LegacyGameboyComposition.decoderConfigDigest`: SHA-256 of the canonical form. */
|
||||
export function decoderConfigDigest(config: DecoderConfigLike): Digest {
|
||||
return digestOf(decoderConfigForm(config));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,3 +144,33 @@ test('a rollback request and the extension methods check what they must', () =>
|
|||
validateAgentRollbackResultAgainstScope(result, newEpoch);
|
||||
assert.throws(() => validateAgentRollbackResultAgainstScope(result, { ...newEpoch, step: '4100' }));
|
||||
});
|
||||
|
||||
test('the decoderConfigDigest vectors are what the TypeScript oracle preset computes', async () => {
|
||||
const { gameboyDecoderConfig } = await import('@flybrain/brain');
|
||||
const file = fixtures.load('gameboy-decoder-config.json') as Record<string, any>;
|
||||
const cases = file.cases as Record<string, any>[];
|
||||
assert.deepEqual(
|
||||
cases.map((item) => item.name),
|
||||
['raw', 'macros'],
|
||||
);
|
||||
for (const item of cases) {
|
||||
const form = gameboy.decoderConfigForm(gameboyDecoderConfig(item.macroChannels as string[]));
|
||||
assert.deepEqual(form, item.form, `${item.name}: the oracle's form is the Rust twin's`);
|
||||
assert.equal(canonicalize(form), item.canonical, `${item.name}: canonical bytes`);
|
||||
assert.equal(
|
||||
gameboy.decoderConfigDigest(gameboyDecoderConfig(item.macroChannels as string[])),
|
||||
item.digest,
|
||||
`${item.name}: digest`,
|
||||
);
|
||||
}
|
||||
const reversed = [...(cases[1]!.macroChannels as string[])].reverse();
|
||||
assert.notEqual(
|
||||
gameboy.decoderConfigDigest(gameboyDecoderConfig(reversed)),
|
||||
cases[1]!.digest,
|
||||
'channel order is identity',
|
||||
);
|
||||
// The example composition declares the real macros-mode digest and channel set.
|
||||
const example = legacy().composition.example;
|
||||
assert.equal(example.decoderConfigDigest, cases[1]!.digest);
|
||||
assert.deepEqual(example.executor.macroChannels, cases[1]!.macroChannels);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ once and holds both languages to it.
|
|||
| `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest |
|
||||
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
|
||||
| `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses |
|
||||
| `gameboy-decoder-config.json` | The `decoderConfigDigest` vectors; written and checked by `flysim`'s `legacy_profile_identity` test (`FLY_UPDATE_FIXTURES=1` rewrites), reproduced by `@flybrain/session-types` from the oracle preset |
|
||||
| `gameboy-legacy.json` | The legacy Game Boy extension set and digest, every registered `SchemaRef`, the legacy profile and its `AssetRef`, the frame clock, an example composition and its digest |
|
||||
|
||||
The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields
|
||||
|
|
|
|||
|
|
@ -34,10 +34,12 @@ pub fn derived() -> Vec<(String, String)> {
|
|||
]
|
||||
}
|
||||
|
||||
/// An example legacy composition. The ROM and decoder digests are placeholders -- the real
|
||||
/// ones are computed by the composition that runs, and no ROM identity belongs in a fixture --
|
||||
/// and the macro channels are a short excerpt of the Pokemon Red set. The compatibility
|
||||
/// string is today's, byte for byte, because its segments must agree with the declaration.
|
||||
/// An example legacy composition. The ROM digest is a placeholder -- the real one is computed
|
||||
/// by the composition that runs, and no ROM identity belongs in a fixture. The macro channels and
|
||||
/// the decoder digest are the real macros-mode vector of `gameboy-decoder-config.json`, which
|
||||
/// `flysim`'s `legacy_profile_identity` test computes from `gameboy_decoder_config_with_macros`
|
||||
/// and the TypeScript test from the oracle preset. The compatibility string is today's, byte for
|
||||
/// byte, because its segments must agree with the declaration.
|
||||
pub fn example_composition() -> LegacyComposition {
|
||||
let pokered = "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b";
|
||||
LegacyComposition {
|
||||
|
|
@ -53,12 +55,17 @@ pub fn example_composition() -> LegacyComposition {
|
|||
adapter: "pokered-unique8-v6".to_owned(),
|
||||
symbol_provenance: pokered.to_owned(),
|
||||
mode: "macros".to_owned(),
|
||||
macro_channels: ["macro_go_objective", "macro_talk", "macro_next", "macro_move_1"]
|
||||
macro_channels: decoder_vector("macros")["macroChannels"]
|
||||
.as_array()
|
||||
.expect("macroChannels")
|
||||
.iter()
|
||||
.map(|c| (*c).to_owned())
|
||||
.map(|c| c.as_str().expect("channel").to_owned())
|
||||
.collect(),
|
||||
},
|
||||
decoder_config_digest: canonical::sha256_hex(b"placeholder: the effective DecoderConfig"),
|
||||
decoder_config_digest: decoder_vector("macros")["digest"]
|
||||
.as_str()
|
||||
.expect("digest")
|
||||
.to_owned(),
|
||||
environment: gameboy::EnvironmentDeclaration {
|
||||
slots: vec!["best".to_owned()],
|
||||
audio_sample_rate: 48_000,
|
||||
|
|
@ -72,6 +79,17 @@ pub fn example_composition() -> LegacyComposition {
|
|||
}
|
||||
}
|
||||
|
||||
/// One case of the (flysim-written) decoder-config vectors.
|
||||
fn decoder_vector(name: &str) -> Value {
|
||||
let file = fixtures::load("gameboy-decoder-config.json").expect("gameboy-decoder-config.json");
|
||||
fixtures::cases(&file)
|
||||
.expect("cases")
|
||||
.iter()
|
||||
.find(|c| c["name"] == Value::String(name.to_owned()))
|
||||
.unwrap_or_else(|| panic!("decoder vector {name}"))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The legacy Game Boy extension set, the profile document and its AssetRef, the clock
|
||||
/// vector and an example composition with its digest.
|
||||
fn gameboy_legacy() -> String {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,335 @@
|
|||
{
|
||||
"description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.",
|
||||
"cases": [
|
||||
{
|
||||
"name": "raw",
|
||||
"macroChannels": [],
|
||||
"form": {
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "up",
|
||||
"role": "command_0"
|
||||
},
|
||||
{
|
||||
"channel": "down",
|
||||
"role": "command_1"
|
||||
},
|
||||
{
|
||||
"channel": "left",
|
||||
"role": "command_2"
|
||||
},
|
||||
{
|
||||
"channel": "right",
|
||||
"role": "command_3"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"macros": null,
|
||||
"pulses": [
|
||||
{
|
||||
"channel": "a",
|
||||
"role": "command_4",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "b",
|
||||
"role": "command_5",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "start",
|
||||
"role": "command_6",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
},
|
||||
{
|
||||
"channel": "select",
|
||||
"role": "command_7",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
}
|
||||
],
|
||||
"clearLockoutMs": 480.0
|
||||
},
|
||||
"canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":null,\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}",
|
||||
"digest": "6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812"
|
||||
},
|
||||
{
|
||||
"name": "macros",
|
||||
"macroChannels": [
|
||||
"macro_go_objective",
|
||||
"macro_go_out",
|
||||
"macro_go_warp",
|
||||
"macro_go_route",
|
||||
"macro_go_item",
|
||||
"macro_go_npc",
|
||||
"macro_go_frontier",
|
||||
"macro_go_shop",
|
||||
"macro_go_heal",
|
||||
"macro_talk",
|
||||
"macro_menu",
|
||||
"macro_next",
|
||||
"macro_yes",
|
||||
"macro_no",
|
||||
"macro_close",
|
||||
"macro_confirm",
|
||||
"macro_back",
|
||||
"macro_move_1",
|
||||
"macro_move_2",
|
||||
"macro_move_3",
|
||||
"macro_move_4",
|
||||
"macro_switch",
|
||||
"macro_item",
|
||||
"macro_throw_ball",
|
||||
"macro_run",
|
||||
"macro_buy_potion",
|
||||
"macro_buy_ball",
|
||||
"macro_buy_antidote",
|
||||
"macro_buy_repel",
|
||||
"macro_heal",
|
||||
"macro_leave"
|
||||
],
|
||||
"form": {
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "up",
|
||||
"role": "command_0"
|
||||
},
|
||||
{
|
||||
"channel": "down",
|
||||
"role": "command_1"
|
||||
},
|
||||
{
|
||||
"channel": "left",
|
||||
"role": "command_2"
|
||||
},
|
||||
{
|
||||
"channel": "right",
|
||||
"role": "command_3"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"macros": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "macro_go_objective",
|
||||
"role": "macro_go_objective"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_out",
|
||||
"role": "macro_go_out"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_warp",
|
||||
"role": "macro_go_warp"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_route",
|
||||
"role": "macro_go_route"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_item",
|
||||
"role": "macro_go_item"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_npc",
|
||||
"role": "macro_go_npc"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_frontier",
|
||||
"role": "macro_go_frontier"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_shop",
|
||||
"role": "macro_go_shop"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_heal",
|
||||
"role": "macro_go_heal"
|
||||
},
|
||||
{
|
||||
"channel": "macro_talk",
|
||||
"role": "macro_talk"
|
||||
},
|
||||
{
|
||||
"channel": "macro_menu",
|
||||
"role": "macro_menu"
|
||||
},
|
||||
{
|
||||
"channel": "macro_next",
|
||||
"role": "macro_next"
|
||||
},
|
||||
{
|
||||
"channel": "macro_yes",
|
||||
"role": "macro_yes"
|
||||
},
|
||||
{
|
||||
"channel": "macro_no",
|
||||
"role": "macro_no"
|
||||
},
|
||||
{
|
||||
"channel": "macro_close",
|
||||
"role": "macro_close"
|
||||
},
|
||||
{
|
||||
"channel": "macro_confirm",
|
||||
"role": "macro_confirm"
|
||||
},
|
||||
{
|
||||
"channel": "macro_back",
|
||||
"role": "macro_back"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_1",
|
||||
"role": "macro_move_1"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_2",
|
||||
"role": "macro_move_2"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_3",
|
||||
"role": "macro_move_3"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_4",
|
||||
"role": "macro_move_4"
|
||||
},
|
||||
{
|
||||
"channel": "macro_switch",
|
||||
"role": "macro_switch"
|
||||
},
|
||||
{
|
||||
"channel": "macro_item",
|
||||
"role": "macro_item"
|
||||
},
|
||||
{
|
||||
"channel": "macro_throw_ball",
|
||||
"role": "macro_throw_ball"
|
||||
},
|
||||
{
|
||||
"channel": "macro_run",
|
||||
"role": "macro_run"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_potion",
|
||||
"role": "macro_buy_potion"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_ball",
|
||||
"role": "macro_buy_ball"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_antidote",
|
||||
"role": "macro_buy_antidote"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_repel",
|
||||
"role": "macro_buy_repel"
|
||||
},
|
||||
{
|
||||
"channel": "macro_heal",
|
||||
"role": "macro_heal"
|
||||
},
|
||||
{
|
||||
"channel": "macro_leave",
|
||||
"role": "macro_leave"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"pulses": [
|
||||
{
|
||||
"channel": "a",
|
||||
"role": "command_4",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "b",
|
||||
"role": "command_5",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "start",
|
||||
"role": "command_6",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
},
|
||||
{
|
||||
"channel": "select",
|
||||
"role": "command_7",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
}
|
||||
],
|
||||
"clearLockoutMs": 480.0
|
||||
},
|
||||
"canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"macro_go_objective\",\"role\":\"macro_go_objective\"},{\"channel\":\"macro_go_out\",\"role\":\"macro_go_out\"},{\"channel\":\"macro_go_warp\",\"role\":\"macro_go_warp\"},{\"channel\":\"macro_go_route\",\"role\":\"macro_go_route\"},{\"channel\":\"macro_go_item\",\"role\":\"macro_go_item\"},{\"channel\":\"macro_go_npc\",\"role\":\"macro_go_npc\"},{\"channel\":\"macro_go_frontier\",\"role\":\"macro_go_frontier\"},{\"channel\":\"macro_go_shop\",\"role\":\"macro_go_shop\"},{\"channel\":\"macro_go_heal\",\"role\":\"macro_go_heal\"},{\"channel\":\"macro_talk\",\"role\":\"macro_talk\"},{\"channel\":\"macro_menu\",\"role\":\"macro_menu\"},{\"channel\":\"macro_next\",\"role\":\"macro_next\"},{\"channel\":\"macro_yes\",\"role\":\"macro_yes\"},{\"channel\":\"macro_no\",\"role\":\"macro_no\"},{\"channel\":\"macro_close\",\"role\":\"macro_close\"},{\"channel\":\"macro_confirm\",\"role\":\"macro_confirm\"},{\"channel\":\"macro_back\",\"role\":\"macro_back\"},{\"channel\":\"macro_move_1\",\"role\":\"macro_move_1\"},{\"channel\":\"macro_move_2\",\"role\":\"macro_move_2\"},{\"channel\":\"macro_move_3\",\"role\":\"macro_move_3\"},{\"channel\":\"macro_move_4\",\"role\":\"macro_move_4\"},{\"channel\":\"macro_switch\",\"role\":\"macro_switch\"},{\"channel\":\"macro_item\",\"role\":\"macro_item\"},{\"channel\":\"macro_throw_ball\",\"role\":\"macro_throw_ball\"},{\"channel\":\"macro_run\",\"role\":\"macro_run\"},{\"channel\":\"macro_buy_potion\",\"role\":\"macro_buy_potion\"},{\"channel\":\"macro_buy_ball\",\"role\":\"macro_buy_ball\"},{\"channel\":\"macro_buy_antidote\",\"role\":\"macro_buy_antidote\"},{\"channel\":\"macro_buy_repel\",\"role\":\"macro_buy_repel\"},{\"channel\":\"macro_heal\",\"role\":\"macro_heal\"},{\"channel\":\"macro_leave\",\"role\":\"macro_leave\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}",
|
||||
"digest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"description": "The legacy Game Boy composition (legacy-gameboy-v1): registered payload schemas with their SchemaRef digests, the one legacy profile document and its AssetRef, the frame clock, and an example composition declaration with its digest.",
|
||||
"extensionSetDigest": "7bb9578d89b0a35a2914e699aad86d35ac8859d9ad4367632bd1fdfbd3e942ef",
|
||||
"extensionSetDigest": "a35823ecf607a218b0c9f9e7ec86585d355cb311f8d7a3ada8aa793e1c9fe47c",
|
||||
"extensionSet": {
|
||||
"contract": "fly-session-types/legacy-gameboy",
|
||||
"version": 1,
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"name": "decoderConfigDigest",
|
||||
"kind": "Digest",
|
||||
"required": true,
|
||||
"constraint": "SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)"
|
||||
"constraint": "SHA-256 of the canonical gameboy-decoder-config-v1 form of the effective decoder configuration"
|
||||
},
|
||||
{
|
||||
"name": "environment",
|
||||
|
|
@ -497,12 +497,39 @@
|
|||
"mode": "macros",
|
||||
"macroChannels": [
|
||||
"macro_go_objective",
|
||||
"macro_go_out",
|
||||
"macro_go_warp",
|
||||
"macro_go_route",
|
||||
"macro_go_item",
|
||||
"macro_go_npc",
|
||||
"macro_go_frontier",
|
||||
"macro_go_shop",
|
||||
"macro_go_heal",
|
||||
"macro_talk",
|
||||
"macro_menu",
|
||||
"macro_next",
|
||||
"macro_move_1"
|
||||
"macro_yes",
|
||||
"macro_no",
|
||||
"macro_close",
|
||||
"macro_confirm",
|
||||
"macro_back",
|
||||
"macro_move_1",
|
||||
"macro_move_2",
|
||||
"macro_move_3",
|
||||
"macro_move_4",
|
||||
"macro_switch",
|
||||
"macro_item",
|
||||
"macro_throw_ball",
|
||||
"macro_run",
|
||||
"macro_buy_potion",
|
||||
"macro_buy_ball",
|
||||
"macro_buy_antidote",
|
||||
"macro_buy_repel",
|
||||
"macro_heal",
|
||||
"macro_leave"
|
||||
]
|
||||
},
|
||||
"decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854",
|
||||
"decoderConfigDigest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52",
|
||||
"environment": {
|
||||
"extensions": [
|
||||
"gameboy-slots-v1"
|
||||
|
|
@ -535,7 +562,7 @@
|
|||
"checkpointFormatOfRecord": "FLYSIM01",
|
||||
"flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7",
|
||||
"digest": "77d8a88ff7fea51eb29b1ae75fc9cf8c17c184e6a1e5a585e86399f41fd9c9b0",
|
||||
"recipeLines": [
|
||||
"fly-session/composition-v1",
|
||||
"session=<sessionId>",
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ pub const DECLARATIONS: &[TypeSchema] = &[
|
|||
req(
|
||||
"decoderConfigDigest",
|
||||
"Digest",
|
||||
"SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)",
|
||||
"SHA-256 of the canonical gameboy-decoder-config-v1 form of the effective decoder configuration",
|
||||
),
|
||||
req(
|
||||
"environment",
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@ use std::sync::Arc;
|
|||
|
||||
use fly_session_types::gameboy;
|
||||
use fly_session_types::scalar::RationalNs;
|
||||
use fly_session_types::{canonical, fixtures};
|
||||
use flybrain_core::agent::{AgentConfig, DEFAULT_WARMUP_MS, GAMEBOY_MS_PER_FRAME, NeuralAgent};
|
||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
||||
use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTON_BITS, gameboy_decoder_config_with_macros};
|
||||
use flybrain_core::decoder::{DecoderConfig, ExclusiveGroup};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
fn the_profile_fingerprint_and_versions_are_the_ones_this_build_computes() {
|
||||
|
|
@ -56,3 +59,86 @@ fn the_profile_clock_warmup_and_joypad_are_the_service_defaults() {
|
|||
assert_eq!(*bit, 1 << index, "bit i is GAMEBOY_BUTTONS[i]");
|
||||
}
|
||||
}
|
||||
|
||||
fn group_form(group: Option<&ExclusiveGroup>) -> Value {
|
||||
match group {
|
||||
None => Value::Null,
|
||||
Some(g) => json!({
|
||||
"channels": g.channels.iter()
|
||||
.map(|(channel, role)| json!({"channel": channel, "role": role}))
|
||||
.collect::<Vec<_>>(),
|
||||
"decisionMs": g.decision_ms,
|
||||
"holdMs": g.hold_ms,
|
||||
"hysteresis": g.hysteresis,
|
||||
"fatigueGain": g.fatigue_gain,
|
||||
"fatigueDecay": g.fatigue_decay,
|
||||
"blockedFatigue": g.blocked_fatigue,
|
||||
"blockedMs": g.blocked_ms,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical decoder-configuration form of legacy-gameboy-v1 section 12, built from the
|
||||
/// Rust twin. `@flybrain/session-types` builds the same value from the TypeScript oracle's
|
||||
/// preset (`decoderConfigForm`), and both are held to `fixtures/gameboy-decoder-config.json`.
|
||||
fn decoder_config_form(config: &DecoderConfig) -> Value {
|
||||
json!({
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": group_form(config.exclusive.as_ref()),
|
||||
"macros": group_form(config.macros.as_ref()),
|
||||
"pulses": config.pulses.iter().map(|p| json!({
|
||||
"channel": p.channel,
|
||||
"role": p.role,
|
||||
"holdMs": p.hold_ms,
|
||||
"cooldownMs": p.cooldown_ms,
|
||||
"threshold": p.threshold,
|
||||
"boot": p.boot.map_or(Value::Null, |b| json!({"cooldownMs": b.cooldown_ms, "threshold": b.threshold})),
|
||||
"throttleGroup": p.throttle_group.clone().map_or(Value::Null, Value::String),
|
||||
})).collect::<Vec<_>>(),
|
||||
"clearLockoutMs": config.clear_lockout_ms,
|
||||
})
|
||||
}
|
||||
|
||||
/// The shared `decoderConfigDigest` vectors: raw mode and the Pokemon Red macro group, from
|
||||
/// `gameboy_decoder_config_with_macros`. `FLY_UPDATE_FIXTURES=1` rewrites the file; otherwise
|
||||
/// the checked-in values must be exactly what this build computes.
|
||||
#[test]
|
||||
fn the_decoder_config_digest_vectors_are_what_the_rust_preset_computes() {
|
||||
let pokered: Vec<&str> = flybrain_gb::macro_channels("pokemon-red");
|
||||
let cases: Vec<Value> = [("raw", Vec::new()), ("macros", pokered)]
|
||||
.into_iter()
|
||||
.map(|(name, channels)| {
|
||||
let form = decoder_config_form(&gameboy_decoder_config_with_macros(&channels));
|
||||
json!({
|
||||
"name": name,
|
||||
"macroChannels": channels,
|
||||
"form": form,
|
||||
"canonical": canonical::canonicalize(&form).expect("canonical"),
|
||||
"digest": canonical::digest_of(&form).expect("digest"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let file = json!({
|
||||
"description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.",
|
||||
"cases": cases,
|
||||
});
|
||||
let mut text = serde_json::to_string_pretty(&file).expect("json");
|
||||
text.push('\n');
|
||||
let path = fixtures::dir().join("gameboy-decoder-config.json");
|
||||
if std::env::var_os("FLY_UPDATE_FIXTURES").is_some() {
|
||||
std::fs::write(&path, &text).expect("write the fixture");
|
||||
}
|
||||
let found = std::fs::read_to_string(&path).expect("the checked-in fixture");
|
||||
assert_eq!(
|
||||
found, text,
|
||||
"gameboy-decoder-config.json is stale; rerun with FLY_UPDATE_FIXTURES=1"
|
||||
);
|
||||
// Channel order is identity: reversing the macro group moves the digest.
|
||||
let mut reversed: Vec<&str> = flybrain_gb::macro_channels("pokemon-red");
|
||||
reversed.reverse();
|
||||
let other = canonical::digest_of(&decoder_config_form(&gameboy_decoder_config_with_macros(
|
||||
&reversed,
|
||||
)))
|
||||
.expect("digest");
|
||||
assert_ne!(Some(other.as_str()), cases[1]["digest"].as_str());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue