Optional replay completed.
+The JSON export and any later server submission now include replayGame and replay behavior-change metrics.
+ + ); + } + + return ( ++ 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 + +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.
+- Round {currentEvent.roundNumber} of {medicalEvents.length} + {mode === "replay" ? "Replay round" : "Round"} {currentEvent.roundNumber} of {medicalEvents.length}
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