diff --git a/app/replay-game/page.tsx b/app/replay-game/page.tsx new file mode 100644 index 0000000..a44c1fb --- /dev/null +++ b/app/replay-game/page.tsx @@ -0,0 +1,12 @@ +import { HiddenCostGame } from "@/components/HiddenCostGame"; +import { PageHeader } from "@/components/PageHeader"; +import { SiteShell } from "@/components/SiteShell"; + +export default function ReplayGamePage() { + return ( + + + + + ); +} diff --git a/components/ExportPanel.tsx b/components/ExportPanel.tsx index 8fb5208..7b92b0a 100644 --- a/components/ExportPanel.tsx +++ b/components/ExportPanel.tsx @@ -163,6 +163,8 @@ export function ExportPanel({ session: providedSession, title = "Research JSON e {completeness.isComplete ? "Export is complete and includes the background profile, game rounds, surveys, and computed metrics." : "Export is not complete yet. Continue the study flow to include all game rounds, surveys, and metrics."} + + +

Optional replay completed.

+

The JSON export and any later server submission now include replayGame and replay behavior-change metrics.

+ + ); + } + + return ( +
+

Optional second playthrough

+

+ You may play one more round sequence before submitting. No additional survey questions will be asked, and replay choices are saved separately from the first game. +

+ {session.serverSubmissionStatus === "submitted" ? ( +

+ This browser session has already been submitted once. If you complete the replay and want the server copy to include it, use Submit again after returning here. +

+ ) : ( +

+ If you want to add replay data, please do it before optional server submission. You can also skip this and submit normally. +

+ )} + + Play one more round sequence + +
+ ); +} + function normalizeSubmissionStatus(session: ResearchSession): ResearchSession { if (!isServerSubmissionEnabled) { return { diff --git a/components/HiddenCostGame.tsx b/components/HiddenCostGame.tsx index 79a8d16..0ff905b 100644 --- a/components/HiddenCostGame.tsx +++ b/components/HiddenCostGame.tsx @@ -6,14 +6,16 @@ import { Card } from "@/components/Card"; import { ROUND_INCOME_POINTS, createHiddenCostGameState, + createReplayGameState, formatPoints, getHealthAfterChoice, getPaidCost, getTreatmentCost, medicalEvents, } from "@/utils/game"; +import { isPostRevealSurveyComplete } from "@/utils/researchMetrics"; import { getStoredSession, saveStoredSession } from "@/utils/session"; -import type { GameChoice, HiddenCostGameState, ResearchSession } from "@/types/research"; +import type { GameChoice, HiddenCostGameState, ReplayGameState, ResearchSession } from "@/types/research"; const choiceLabels: Record = { "full-treatment": "Full treatment", @@ -21,14 +23,42 @@ const choiceLabels: Record = { "skip-treatment": "Skip treatment", }; -export function HiddenCostGame() { +export function HiddenCostGame({ mode = "primary" }: { mode?: "primary" | "replay" }) { const router = useRouter(); const [session, setSession] = useState(null); const [game, setGame] = useState(null); const [roundStartedAt, setRoundStartedAt] = useState(() => Date.now()); useEffect(() => { - const storedSession = getStoredSession("game"); + const storedSession = getStoredSession(mode === "replay" ? "individual-results" : "game"); + + if (mode === "replay") { + if (!storedSession.game?.completedAt || !isPostRevealSurveyComplete(storedSession.postRevealSurvey)) { + const redirectStage = storedSession.game?.completedAt ? "post-reveal" : "game"; + saveStoredSession({ ...storedSession, currentStage: redirectStage }); + router.replace(storedSession.game?.completedAt ? "/post-reveal-survey" : "/game"); + return; + } + + if (storedSession.replayGame?.completedAt) { + saveStoredSession({ ...storedSession, currentStage: "individual-results" }); + router.replace("/individual-results"); + return; + } + + const nextReplayGame = storedSession.replayGame ?? createReplayGameState(storedSession.game); + const nextSession = { + ...storedSession, + currentStage: "individual-results" as const, + replayGame: nextReplayGame, + }; + + saveStoredSession(nextSession); + setSession(nextSession); + setGame(nextReplayGame); + setRoundStartedAt(Date.now()); + return; + } if (!storedSession.participantProfile) { const backgroundSession: ResearchSession = { @@ -63,7 +93,7 @@ export function HiddenCostGame() { setSession(nextSession); setGame(nextGame); setRoundStartedAt(Date.now()); - }, [router]); + }, [mode, router]); const currentEvent = game ? medicalEvents[game.currentRoundIndex] : undefined; const actualFullCost = useMemo(() => { @@ -114,26 +144,39 @@ export function HiddenCostGame() { }, ]; const isComplete = updatedRounds.length === medicalEvents.length; - const updatedGame: HiddenCostGameState = { + const updatedGame: HiddenCostGameState | ReplayGameState = { ...game, financialPoints: scoreAfter, healthPoints: healthAfter, currentRoundIndex: isComplete ? game.currentRoundIndex : game.currentRoundIndex + 1, completedAt: isComplete ? timestamp : undefined, rounds: updatedRounds, + ...("replayId" in game + ? { + finalFinancialScore: scoreAfter, + finalHealthScore: healthAfter, + } + : {}), }; - const updatedSession: ResearchSession = { - ...session, - currentStage: isComplete ? "visible-results" : "game", - game: updatedGame, - }; + const updatedSession: ResearchSession = + mode === "replay" + ? { + ...session, + currentStage: "individual-results", + replayGame: updatedGame as ReplayGameState, + } + : { + ...session, + currentStage: isComplete ? "visible-results" : "game", + game: updatedGame, + }; saveStoredSession(updatedSession); setSession(updatedSession); setGame(updatedGame); if (isComplete) { - router.push("/visible-results"); + router.push(mode === "replay" ? "/individual-results" : "/visible-results"); return; } @@ -150,10 +193,17 @@ export function HiddenCostGame() { return ( + {mode === "replay" ? ( +
+

This is an optional second playthrough.

+

No additional survey questions will be asked. Your choices in this replay are saved separately from your first game.

+
+ ) : null} +

- Round {currentEvent.roundNumber} of {medicalEvents.length} + {mode === "replay" ? "Replay round" : "Round"} {currentEvent.roundNumber} of {medicalEvents.length}

{currentEvent.eventName}

diff --git a/components/ResultsTable.tsx b/components/ResultsTable.tsx index 87c8a8b..a39155a 100644 --- a/components/ResultsTable.tsx +++ b/components/ResultsTable.tsx @@ -138,6 +138,7 @@ function IndividualResults({ preRevealSurveyOriginal: session.preRevealSurveyOriginal, explanationFrameCondition: session.explanationFrameCondition, costVisibilityCondition: session.costVisibilityCondition, + replayGame: session.replayGame, }); const interpretations = buildParticipantInterpretation(computedMetrics); @@ -256,6 +257,22 @@ function MetricsGrid({ metrics }: { metrics: ComputedResearchMetrics }) { ["Cost visibility condition", metrics.costVisibilityCondition ?? "Not assigned"], ["Had any cost hint", metrics.hadAnyCostHint ? "Yes" : "No"], ["Had strong cost hint", metrics.hadStrongCostHint ? "Yes" : "No"], + ["Replay available", metrics.replayAvailable ? "Yes" : "No"], + ["Replay completed", metrics.replayCompleted ? "Yes" : "No"], + ["Replay assignment", metrics.replayAssignmentCondition ?? "Not played"], + ["Replay hidden profile", metrics.replayHiddenProfile ?? "Not played"], + ["Replay full treatment choices", metrics.replayFullTreatmentChoices ?? "Not played"], + ["Replay partial treatment choices", metrics.replayPartialTreatmentChoices ?? "Not played"], + ["Replay skipped treatment choices", metrics.replaySkippedTreatmentChoices ?? "Not played"], + ["Replay final financial score", metrics.replayFinalFinancialScore ?? "Not played"], + ["Replay final health score", metrics.replayFinalHealthScore ?? "Not played"], + ["Replay total treatment cost paid", metrics.replayTotalTreatmentCostPaid ?? "Not played"], + ["Replay care avoidance", metrics.replayCareAvoidance ?? "Not played"], + ["Behavior change: full treatment", metrics.behaviorChangeFullTreatment ?? "Not played"], + ["Behavior change: partial treatment", metrics.behaviorChangePartialTreatment ?? "Not played"], + ["Behavior change: skipped treatment", metrics.behaviorChangeSkippedTreatment ?? "Not played"], + ["Behavior change: care avoidance", metrics.behaviorChangeCareAvoidance ?? "Not played"], + ["Behavior change: cost burden", metrics.behaviorChangeCostBurden ?? "Not played"], ] as const; return ( diff --git a/lib/adminSubmissions.ts b/lib/adminSubmissions.ts index c882cee..fbfa56e 100644 --- a/lib/adminSubmissions.ts +++ b/lib/adminSubmissions.ts @@ -307,6 +307,22 @@ const CSV_COLUMNS: CsvColumn[] = [ payloadColumn("skipped_treatment_choices", ["gameSummary", "skippedTreatmentChoices"]), payloadColumn("burden", ["computedMetrics", "burden"]), payloadColumn("care_avoidance", ["computedMetrics", "careAvoidance"]), + payloadColumn("replay_available", ["computedMetrics", "replayAvailable"]), + payloadColumn("replay_completed", ["computedMetrics", "replayCompleted"]), + payloadColumn("replay_assignment_condition", ["computedMetrics", "replayAssignmentCondition"]), + payloadColumn("replay_hidden_profile", ["computedMetrics", "replayHiddenProfile"]), + payloadColumn("replay_full_treatment_choices", ["computedMetrics", "replayFullTreatmentChoices"]), + payloadColumn("replay_partial_treatment_choices", ["computedMetrics", "replayPartialTreatmentChoices"]), + payloadColumn("replay_skipped_treatment_choices", ["computedMetrics", "replaySkippedTreatmentChoices"]), + payloadColumn("replay_final_financial_score", ["computedMetrics", "replayFinalFinancialScore"]), + payloadColumn("replay_final_health_score", ["computedMetrics", "replayFinalHealthScore"]), + payloadColumn("replay_total_treatment_cost_paid", ["computedMetrics", "replayTotalTreatmentCostPaid"]), + payloadColumn("replay_care_avoidance", ["computedMetrics", "replayCareAvoidance"]), + payloadColumn("behavior_change_full_treatment", ["computedMetrics", "behaviorChangeFullTreatment"]), + payloadColumn("behavior_change_partial_treatment", ["computedMetrics", "behaviorChangePartialTreatment"]), + payloadColumn("behavior_change_skipped_treatment", ["computedMetrics", "behaviorChangeSkippedTreatment"]), + payloadColumn("behavior_change_care_avoidance", ["computedMetrics", "behaviorChangeCareAvoidance"]), + payloadColumn("behavior_change_cost_burden", ["computedMetrics", "behaviorChangeCostBurden"]), payloadColumn("metric_cost_visibility_condition", ["computedMetrics", "costVisibilityCondition"]), payloadColumn("had_any_cost_hint", ["computedMetrics", "hadAnyCostHint"]), payloadColumn("had_strong_cost_hint", ["computedMetrics", "hadStrongCostHint"]), diff --git a/lib/researchExportSchema.ts b/lib/researchExportSchema.ts index ca2355e..dcf1d6b 100644 --- a/lib/researchExportSchema.ts +++ b/lib/researchExportSchema.ts @@ -13,6 +13,7 @@ const revisionAccessConditionSchema = z.enum(["revision-unlocked", "revision-loc const revealTimingConditionNameSchema = z.enum(["immediate-reveal", "delayed-reveal"]); const explanationFrameConditionNameSchema = z.enum(["explain-to-self", "explain-to-other"]); const costVisibilityConditionNameSchema = z.enum(["no-cost-info", "partial-cost-hint", "full-cost-preview"]); +const replayAssignmentConditionSchema = z.enum(["same-hidden-profile", "switched-hidden-profile"]); export const revisionAccessSchema = z .object({ @@ -122,6 +123,24 @@ export const gameRoundSchema = z }) .passthrough(); +export const replayGameSchema = z + .object({ + replayId: z.string().min(1), + startedAt: isoDateStringSchema, + completedAt: isoDateStringSchema.optional(), + assignmentCondition: replayAssignmentConditionSchema, + displayedProfile: displayedProfileSchema, + hiddenProfile: hiddenProfileSchema, + treatmentCostMultiplier: z.number().positive(), + financialPoints: z.number(), + healthPoints: z.number(), + currentRoundIndex: z.number().int().nonnegative(), + rounds: z.array(gameRoundSchema), + finalFinancialScore: z.number(), + finalHealthScore: z.number(), + }) + .passthrough(); + export const preRevealSurveySchema = z .object({ primaryAttribution: z.string().min(1), @@ -177,6 +196,22 @@ export const computedMetricsSchema = z costVisibilityCondition: costVisibilityConditionNameSchema.nullable(), hadAnyCostHint: z.boolean(), hadStrongCostHint: z.boolean(), + replayAvailable: z.boolean(), + replayCompleted: z.boolean(), + replayAssignmentCondition: replayAssignmentConditionSchema.optional(), + replayHiddenProfile: hiddenProfileSchema.optional(), + replayFullTreatmentChoices: z.number().int().nonnegative().optional(), + replayPartialTreatmentChoices: z.number().int().nonnegative().optional(), + replaySkippedTreatmentChoices: z.number().int().nonnegative().optional(), + replayFinalFinancialScore: z.number().optional(), + replayFinalHealthScore: z.number().optional(), + replayTotalTreatmentCostPaid: z.number().nonnegative().optional(), + replayCareAvoidance: z.number().optional(), + behaviorChangeFullTreatment: z.number().optional(), + behaviorChangePartialTreatment: z.number().optional(), + behaviorChangeSkippedTreatment: z.number().optional(), + behaviorChangeCareAvoidance: z.number().optional(), + behaviorChangeCostBurden: z.number().optional(), attributionCategoryShift: z .object({ pre: z.string().min(1), @@ -228,6 +263,7 @@ export const researchExportSchema = z assignedProfile: assignedProfileSchema, gameSummary: gameSummarySchema, gameRounds: z.array(gameRoundSchema).min(1), + replayGame: replayGameSchema.optional(), preRevealSurvey: preRevealSurveySchema, preRevealSurveyOriginal: preRevealSurveySchema.optional(), preRevealSurveyRevisedAfterReveal: preRevealSurveySchema.optional(), diff --git a/sample-data/complete-research-export.example.json b/sample-data/complete-research-export.example.json index 2738d77..77c5445 100644 --- a/sample-data/complete-research-export.example.json +++ b/sample-data/complete-research-export.example.json @@ -1,6 +1,6 @@ { "exportVersion": "prototype-1.2", - "schemaVersion": "hidden-cost-game-research-schema-4", + "schemaVersion": "hidden-cost-game-research-schema-6", "sessionId": "hcg-example-session", "consentVersion": "pilot-consent-v1", "serverSubmissionStatus": "not_submitted", @@ -182,7 +182,12 @@ "pre": "individual choices", "post": "structural constraints" }, - "explanationFrame": "explain-to-self" + "explanationFrame": "explain-to-self", + "costVisibilityCondition": null, + "hadAnyCostHint": false, + "hadStrongCostHint": false, + "replayAvailable": true, + "replayCompleted": false }, "completeness": { "hasParticipantProfile": true, diff --git a/types/research.ts b/types/research.ts index 8ef5b8d..b7ace0e 100644 --- a/types/research.ts +++ b/types/research.ts @@ -40,6 +40,7 @@ export interface ParticipantBackground { } export type HiddenProfileMeaning = "High coverage" | "Low coverage"; +export type ReplayAssignmentCondition = "same-hidden-profile" | "switched-hidden-profile"; export type DisplayedProfile = "Profile A" | "Profile B"; export type GameChoice = "full-treatment" | "partial-treatment" | "skip-treatment"; export type MedicalRiskLevel = "low" | "medium" | "high"; @@ -89,6 +90,13 @@ export interface HiddenCostGameState { rounds: GameRoundData[]; } +export interface ReplayGameState extends HiddenCostGameState { + replayId: string; + assignmentCondition: ReplayAssignmentCondition; + finalFinancialScore: number; + finalHealthScore: number; +} + export type PreRevealSurveyAnswers = { primaryAttribution: string; individualResponsibility: number; @@ -213,6 +221,22 @@ export interface ComputedResearchMetrics { costVisibilityCondition: CostVisibilityConditionName | null; hadAnyCostHint: boolean; hadStrongCostHint: boolean; + replayAvailable: boolean; + replayCompleted: boolean; + replayAssignmentCondition?: ReplayAssignmentCondition; + replayHiddenProfile?: HiddenProfileMeaning; + replayFullTreatmentChoices?: number; + replayPartialTreatmentChoices?: number; + replaySkippedTreatmentChoices?: number; + replayFinalFinancialScore?: number; + replayFinalHealthScore?: number; + replayTotalTreatmentCostPaid?: number; + replayCareAvoidance?: number; + behaviorChangeFullTreatment?: number; + behaviorChangePartialTreatment?: number; + behaviorChangeSkippedTreatment?: number; + behaviorChangeCareAvoidance?: number; + behaviorChangeCostBurden?: number; } export interface ResearchExportAssignedProfile { @@ -246,6 +270,7 @@ export interface ResearchExport { assignedProfile: ResearchExportAssignedProfile; gameSummary: GameSummary; gameRounds: GameRoundData[]; + replayGame?: ReplayGameState; preRevealSurvey: PreRevealSurveyAnswers; preRevealSurveyOriginal?: PreRevealSurveyAnswers; preRevealSurveyRevisedAfterReveal?: PreRevealSurveyAnswers; @@ -274,6 +299,7 @@ export interface ResearchSession { participantProfile?: ParticipantProfile; responses: Record; game?: HiddenCostGameState; + replayGame?: ReplayGameState; preRevealSurvey?: PreRevealSurveyAnswers; preRevealSurveyOriginal?: PreRevealSurveyAnswers; preRevealSurveyRevisedAfterReveal?: PreRevealSurveyAnswers; diff --git a/utils/game.ts b/utils/game.ts index 0429ca0..0aabf8a 100644 --- a/utils/game.ts +++ b/utils/game.ts @@ -1,4 +1,4 @@ -import type { GameChoice, HiddenCostGameState, HiddenCostProfile, MedicalEvent } from "@/types/research"; +import type { GameChoice, HiddenCostGameState, HiddenCostProfile, MedicalEvent, ReplayGameState } from "@/types/research"; export const STARTING_FINANCIAL_POINTS = 100; export const STARTING_HEALTH_POINTS = 100; @@ -103,3 +103,33 @@ export function getHealthAfterChoice(choice: GameChoice, healthBefore: number): export function formatPoints(value: number): string { return Number.isInteger(value) ? value.toString() : value.toFixed(1); } + + +export function createReplayGameState(originalGame: HiddenCostGameState): ReplayGameState { + const assignmentCondition = Math.random() < 0.5 ? "same-hidden-profile" : "switched-hidden-profile"; + const hiddenProfile = assignmentCondition === "same-hidden-profile" ? originalGame.hiddenProfile : getOppositeHiddenProfile(originalGame.hiddenProfile); + const profile = getProfileForHiddenProfile(hiddenProfile); + + return { + replayId: `hcg-replay-${crypto.randomUUID()}`, + assignmentCondition, + displayedProfile: originalGame.displayedProfile, + hiddenProfile, + treatmentCostMultiplier: profile.treatmentCostMultiplier, + financialPoints: STARTING_FINANCIAL_POINTS, + healthPoints: STARTING_HEALTH_POINTS, + currentRoundIndex: 0, + startedAt: new Date().toISOString(), + rounds: [], + finalFinancialScore: STARTING_FINANCIAL_POINTS, + finalHealthScore: STARTING_HEALTH_POINTS, + }; +} + +export function getOppositeHiddenProfile(hiddenProfile: HiddenCostGameState["hiddenProfile"]): HiddenCostGameState["hiddenProfile"] { + return hiddenProfile === "High coverage" ? "Low coverage" : "High coverage"; +} + +export function getProfileForHiddenProfile(hiddenProfile: HiddenCostGameState["hiddenProfile"]): HiddenCostProfile { + return hiddenCostProfiles.find((profile) => profile.hiddenProfile === hiddenProfile) ?? hiddenCostProfiles[0]; +} diff --git a/utils/researchMetrics.ts b/utils/researchMetrics.ts index 42f65a8..f180634 100644 --- a/utils/researchMetrics.ts +++ b/utils/researchMetrics.ts @@ -13,7 +13,7 @@ import type { } from "@/types/research"; export const RESEARCH_EXPORT_VERSION = "prototype-1.2"; -export const RESEARCH_SCHEMA_VERSION = "hidden-cost-game-research-schema-5"; +export const RESEARCH_SCHEMA_VERSION = "hidden-cost-game-research-schema-6"; export const RESEARCH_CONSENT_VERSION = "pilot-consent-v1"; const choiceCountKeys: Record = { @@ -108,6 +108,7 @@ export function calculateComputedResearchMetrics({ preRevealCommitment, explanationFrameCondition, costVisibilityCondition, + replayGame, }: { game: HiddenCostGameState; preRevealSurvey: PreRevealSurveyAnswers; @@ -120,6 +121,7 @@ export function calculateComputedResearchMetrics({ preRevealCommitment?: ResearchSession["preRevealCommitment"]; explanationFrameCondition?: ResearchSession["explanationFrameCondition"]; costVisibilityCondition?: ResearchSession["costVisibilityCondition"]; + replayGame?: ResearchSession["replayGame"]; }): ComputedResearchMetrics { const summary = calculateGameSummary(game); const originalPreRevealSurvey = preRevealSurveyOriginal ?? preRevealSurvey; @@ -144,6 +146,11 @@ export function calculateComputedResearchMetrics({ ]; const hasRevisionComparison = revisionDeltas.every((delta): delta is number => typeof delta === "number"); const costVisibilityConditionName = costVisibilityCondition?.condition ?? null; + const originalCostBurden = roundMetric(summary.totalTreatmentCostPaid / Math.max(summary.totalIncome, 1)); + const replayCompleted = Boolean(replayGame?.completedAt); + const replaySummary = replayCompleted && replayGame ? calculateGameSummary(replayGame) : null; + const replayCostBurden = replaySummary ? roundMetric(replaySummary.totalTreatmentCostPaid / Math.max(replaySummary.totalIncome, 1)) : undefined; + const replayCareAvoidance = replaySummary ? replaySummary.skippedTreatmentChoices + 0.5 * replaySummary.partialTreatmentChoices : undefined; return { responsibilityShift: postRevealSurvey.revisedIndividualResponsibility - preRevealSurvey.individualResponsibility, @@ -154,7 +161,7 @@ export function calculateComputedResearchMetrics({ certaintyCorrection: preRevealSurvey.confidence - postRevealSurvey.initialJudgmentAccuracy, informationCaution: 8 - preRevealSurvey.informationSufficiency, perspectiveChange: postRevealSurvey.perspectiveChange, - burden: roundMetric(summary.totalTreatmentCostPaid / Math.max(summary.totalIncome, 1)), + burden: originalCostBurden, careAvoidance: summary.skippedTreatmentChoices + 0.5 * summary.partialTreatmentChoices, delayedReveal: revealTimingCondition?.condition === "delayed-reveal", ...(preRevealCommitment ? { standByInitialInterpretation: preRevealCommitment.standByInitialInterpretation } : {}), @@ -167,6 +174,26 @@ export function calculateComputedResearchMetrics({ costVisibilityCondition: costVisibilityConditionName, hadAnyCostHint: costVisibilityConditionName === "partial-cost-hint" || costVisibilityConditionName === "full-cost-preview", hadStrongCostHint: costVisibilityConditionName === "full-cost-preview", + replayAvailable: true, + replayCompleted, + ...(replaySummary && replayGame + ? { + replayAssignmentCondition: replayGame.assignmentCondition, + replayHiddenProfile: replayGame.hiddenProfile, + replayFullTreatmentChoices: replaySummary.fullTreatmentChoices, + replayPartialTreatmentChoices: replaySummary.partialTreatmentChoices, + replaySkippedTreatmentChoices: replaySummary.skippedTreatmentChoices, + replayFinalFinancialScore: replaySummary.finalFinancialScore, + replayFinalHealthScore: replaySummary.finalHealthScore, + replayTotalTreatmentCostPaid: replaySummary.totalTreatmentCostPaid, + replayCareAvoidance: replayCareAvoidance!, + behaviorChangeFullTreatment: replaySummary.fullTreatmentChoices - summary.fullTreatmentChoices, + behaviorChangePartialTreatment: replaySummary.partialTreatmentChoices - summary.partialTreatmentChoices, + behaviorChangeSkippedTreatment: replaySummary.skippedTreatmentChoices - summary.skippedTreatmentChoices, + behaviorChangeCareAvoidance: replayCareAvoidance! - (summary.skippedTreatmentChoices + 0.5 * summary.partialTreatmentChoices), + behaviorChangeCostBurden: replayCostBurden! - originalCostBurden, + } + : {}), attributionCategoryShift: { pre: preRevealSurvey.primaryAttribution, post: postRevealSurvey.revisedPrimaryAttribution, @@ -266,6 +293,7 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da preRevealCommitment: session.preRevealCommitment, explanationFrameCondition: session.explanationFrameCondition, costVisibilityCondition: session.costVisibilityCondition, + replayGame: session.replayGame, }); return { @@ -295,6 +323,7 @@ export function buildResearchExport(session: ResearchSession, createdAt = new Da }, gameSummary, gameRounds: session.game.rounds, + ...(session.replayGame ? { replayGame: session.replayGame } : {}), preRevealSurvey: session.preRevealSurvey, ...(session.preRevealSurveyOriginal ? { preRevealSurveyOriginal: session.preRevealSurveyOriginal } : {}), ...(session.preRevealSurveyRevisedAfterReveal ? { preRevealSurveyRevisedAfterReveal: session.preRevealSurveyRevisedAfterReveal } : {}), diff --git a/utils/session.ts b/utils/session.ts index 920efc1..610f662 100644 --- a/utils/session.ts +++ b/utils/session.ts @@ -65,6 +65,7 @@ function normalizeStoredSession(value: unknown, currentStage: StageId): Research participantProfile: candidate.participantProfile, responses: candidate.responses ?? {}, game: candidate.game, + replayGame: candidate.replayGame, preRevealSurvey: candidate.preRevealSurvey, preRevealSurveyOriginal: candidate.preRevealSurveyOriginal ?? (candidate.preRevealSurveyCompletedAt ? candidate.preRevealSurvey : undefined), preRevealSurveyRevisedAfterReveal: candidate.preRevealSurveyRevisedAfterReveal,