135 lines
4.1 KiB
TypeScript
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);
|
|
}
|