storytellurx-v1/app/game-engine/game-state.ts
2025-06-11 05:15:50 +02:00

135 lines
4.1 KiB
TypeScript

import type { AssistantSchema, GameState } from "./game-schemas.ts";
import type { NarrativeMessage } from "../game-engine/narrative-provider.tsx";
import { deepMerge } from "@std/collections/deep-merge";
import unique from "@bureaudouble/outils/unique.ts";
import { parsePartialJson } from "ai";
export const useGetAssistantTurn = (
message?: NarrativeMessage,
): AssistantSchema | undefined => {
const res = ((message?.role === "assistant" &&
message.parts.filter((part) => part.type === "data-assistant")
.at(0)
?.data) || undefined) as AssistantSchema | undefined;
return res;
};
export const applyFullGameState = (
v: NarrativeMessage,
data: AssistantSchema | undefined,
prev: NarrativeMessage[],
) =>
(v.role === "assistant" && data)
? ({
...v,
parts: [...v.parts, {
type: "data-assistant",
data: {
...data,
fullGameState: deepGameStateMerge(
getGameState([...prev]),
data?.gameStateDiff ?? {},
),
},
}],
})
: v;
export const addFullGameStateSync = (
messages: NarrativeMessage[],
) => {
return messages.reduce((prev, v) => {
const data = v.role === "assistant"
? JSON.parse(v.parts.find((v) => v.type === "text")?.text ?? "{}") ?? {}
: null;
return [...prev, applyFullGameState(v, data, prev)] as NarrativeMessage[];
}, [] as NarrativeMessage[]);
};
export const addFullGameStateAsync = async (
messages: NarrativeMessage[],
) => {
const resolvedDataPromises = messages
.filter(unique((v) => v.id))
.map(async (message) => {
if (message.role === "assistant") {
const text = message.parts.find((v) => v.type === "text")?.text;
const data = text ? await parsePartialJson(text) : null;
return {
id: message.id,
data: data?.value as AssistantSchema | undefined,
};
}
return { id: message.id, data: null };
});
const resolvedPairs = await Promise.all(resolvedDataPromises);
const resolvedDataMap = new Map(
resolvedPairs.filter((p) => p.data).map((p) => [p.id, p.data!]),
);
return messages
.filter(unique((v) => v.id))
.reduce((prev, v) => {
const data = resolvedDataMap.get(v.id);
const finalMessage = applyFullGameState(v, data, prev);
return [...prev, finalMessage] as NarrativeMessage[];
}, [] as NarrativeMessage[]);
};
export const getGameState = (
messages: NarrativeMessage[],
): GameState => computeGameStateFromMessages(messages);
export const deepGameStateMerge = (
prevState: Readonly<GameState>,
diffState: Partial<Readonly<GameState>>,
): GameState => {
const newGameState = deepMerge(
prevState,
diffState,
{ arrays: "merge" },
);
if (diffState.toolkitItemsUsed ?? newGameState.toolkitItemsUsed) {
newGameState.toolkitItemsUsed = [
...new Set(diffState.toolkitItemsUsed ?? newGameState.toolkitItemsUsed),
];
}
if (diffState.userSexualOrgans ?? newGameState.userSexualOrgans) {
newGameState.userSexualOrgans = [
...new Set(diffState.userSexualOrgans ?? newGameState.userSexualOrgans),
];
}
if (newGameState.sharedKnowledge) {
newGameState.sharedKnowledge = [
...new Set(newGameState.sharedKnowledge ?? []),
];
}
if (newGameState.userSoftLimits) {
newGameState.userSoftLimits = [
...new Set(newGameState.userSoftLimits ?? []),
];
}
return newGameState;
};
/**
* Computes the current game state by replaying the history of messages.
* This is a pure function, safe to run on the client.
* @param messages - The array of messages for the session.
* @param defaultGameState - The default game state.
* @returns The computed GameState.
*/
export function computeGameStateFromMessages(
messages: NarrativeMessage[],
): GameState {
return messages.reduce((computedState: GameState, message) => {
if (message.role === "assistant") {
const turn = useGetAssistantTurn(message);
computedState = deepGameStateMerge(
computedState,
turn?.fullGameState ?? {},
);
}
return computedState;
}, {} as GameState);
}