/* * OpenNash published solver source * Solver version: 2.5.0 * Scenario source: /opennash/scenarios/ai-pact-v1.1.json * License notice: This release does not state a separate code license. */ export const OPENNASH_SOLVER_VERSION = "2.5.0"; export const OPENNASH_RECEIPT_SCHEMA_VERSION = "opennash.run.v1"; export const OPENNASH_DRAFT_SCHEMA_VERSION = "opennash.draft.v2"; export const OPENNASH_DRAFT_MAX_ENCODED_LENGTH = 2_048; const EPSILON = 1e-9; export type Strategy = { id: string; label: string; incentive: string; }; export type Actor = { id: string; label: string; role: string; strategies: Strategy[]; }; export type Profile = string[]; export type ProvenanceGrade = "A" | "B" | "C"; export type PayoffEstimate = { point: number; low: number; high: number; grade: ProvenanceGrade; basis: string; sourceIds: string[]; }; export type PolicyInstrumentId = | "capacity-investment" | "market-access-condition" | "transition-transfer"; export type PolicyEdit = { instrument: PolicyInstrumentId; profileKey: string; actorId: string; delta: number; before: number; after: number; description: string; }; export type PayoffRecord = { values: number[]; estimates?: PayoffEstimate[]; rationale: string; adjustments?: string[]; }; export type Game = { actors: Actor[]; payoffs: Record; policyEdits?: PolicyEdit[]; }; export type SourceReference = { id: string; type: "authorial" | "calibrated" | "elicited"; title: string; citation: string; url: string | null; note: string; }; export type InstrumentDefinition = { maximum: number; description: string; }; export type InterventionEnvelope = { budget: number; step: number; instruments: { capacityInvestment: InstrumentDefinition; marketAccessCondition: InstrumentDefinition; transitionTransfer: InstrumentDefinition; }; costDefinition: string; }; export type WorldviewPanelSource = { id: string; label: string; summary: string; authorial: true; grade: "C"; sourceIds: string[]; payoffs: Record; }; export type WorldviewPanel = Omit & { game: Game; }; export type ScenarioSourceDocument = { schemaVersion: "opennash.scenario.v1"; scenarioVersion: string; solverVersion: string; id: string; number: string; title: string; shortTitle: string; sourcePath: string; publishedAt: string; modelClass: "two-player-two-strategy-simultaneous-one-shot"; status: "illustrative"; payoffUnits: string; aggregationRule: string; brief: string; stakes: string; interventionEnvelope: InterventionEnvelope; strategicVectors: Array<{ id: string; label: string; us: string; china: string; contest: string; }>; actors: Actor[]; cooperativeStrategies: Record; statusQuoProfile: string; concept: string; lesson: string; narratives: Record; sources: SourceReference[]; defaultPanelId: string; worldviewPanels: WorldviewPanelSource[]; }; export type Scenario = { schemaVersion: "opennash.scenario.v1"; scenarioVersion: string; solverVersion: string; sourcePath: string; publishedAt: string; modelClass: ScenarioSourceDocument["modelClass"]; status: ScenarioSourceDocument["status"]; payoffUnits: string; aggregationRule: string; id: string; number: string; title: string; shortTitle: string; brief: string; stakes: string; budget: number; interventionEnvelope: InterventionEnvelope; strategicVectors: ScenarioSourceDocument["strategicVectors"]; game: Game; defaultPanelId: string; worldviewPanels: WorldviewPanel[]; cooperativeStrategies: Record; statusQuoProfile: string; concept: string; lesson: string; narratives: Record; sources: SourceReference[]; }; export type PolicyConfig = { capacityActorId: string; capacityInvestment: number; marketAccessActorId: string; marketAccessCondition: number; transitionRecipientId: string; transitionTransfer: number; }; export type PolicyDraft = Partial; export type DeviationConstraint = { actorId: string; actorIndex: number; equilibriumProfileKey: string; deviationProfileKey: string; fromStrategyId: string; toStrategyId: string; equilibriumPayoff: number; deviationPayoff: number; deviationGain: number; slack: number; }; export type IncentiveMargin = { singleEntryTieMargin: number; jointLInfinityTieRadius: number; equilibriumProfileKey: string; bindingConstraints: DeviationConstraint[]; definition: string; }; export type PureEquilibrium = { profile: Profile; profileKey: string; kind: "strict" | "weak"; constraints: DeviationConstraint[]; incentiveMargin: IncentiveMargin; }; export type PureProfileBoundaryCandidate = { profile: Profile; profileKey: string; classification: "strict" | "weak" | "not-equilibrium"; boundaryEffect: "strict-to-weak" | "already-weak" | "not-equilibrium-to-weak"; radius: number; bindingConstraints: DeviationConstraint[]; }; export type PureEquilibriumBoundary = { metric: "payoff-entry-l-infinity"; radius: number; candidates: PureProfileBoundaryCandidate[]; definition: string; }; export type MixedEquilibrium = { kind: "interior"; probabilities: Record>; expectedPayoffs: number[]; }; export type MixedTie = { actorId: string; againstStrategyId: string | null; description: string; }; export type ProbabilityRange = { strategyId: string; minimum: number; maximum: number; }; export type MixedContinuum = { dimension: "line" | "area"; probabilityRanges: Record; description: string; }; export type MixedEquilibriumAnalysis = { status: "not-applicable" | "none" | "interior" | "degenerate"; interior: MixedEquilibrium[]; ties: MixedTie[]; continua: MixedContinuum[]; method: string; }; export type CoordinationThreshold = { actorId: string; actorLabel: string; otherActorId: string; otherActorLabel: string; cooperativeDeviationLoss: number; defensiveDeviationLoss: number; minimumOtherCooperationBelief: number | null; }; export type StagHuntAnalysis = { classification: "stag-hunt" | "not-stag-hunt" | "not-applicable"; cooperativeProfileKey: string | null; defensiveProfileKey: string | null; cooperativeProfileIsStrictEquilibrium: boolean; defensiveProfileIsStrictEquilibrium: boolean; cooperativeProfilePayoffDominates: boolean; payoffDominantProfileKey: string | null; riskDominance: "cooperative" | "defensive" | "tie" | "not-applicable"; riskDominantProfileKey: string | null; cooperativeDeviationLossProduct: number | null; defensiveDeviationLossProduct: number | null; thresholds: CoordinationThreshold[]; diagnosis: string; method: string; }; export type GameSolution = { profiles: Profile[]; pureNash: Profile[]; pureEquilibria: PureEquilibrium[]; referenceOutcome: Profile | null; mixedEquilibriumAnalysis: MixedEquilibriumAnalysis; pureEquilibriumBoundary: PureEquilibriumBoundary; paretoFrontier: Profile[]; maximumDeviationGains: Record; deviationConstraints: Record; weaklyDominatedStrategies: Record; }; export type CrossPanelProfileSurvival = { profileKey: string; equilibriumPanelIds: string[]; equilibriumInAllPanels: boolean; }; export type CrossPanelSurvivalReport = { method: string; panelResults: Array<{ panelId: string; panelLabel: string; authorial: boolean; pureEquilibria: Array<{ profileKey: string; kind: "strict" | "weak" }>; mixedStatus: MixedEquilibriumAnalysis["status"]; }>; profiles: CrossPanelProfileSurvival[]; commonPureEquilibriumKeys: string[]; }; export type ScenarioRun = { status: "solved"; scenarioId: string; scenarioVersion: string; solverVersion: string; panelId: string; policy: PolicyConfig; game: Game; solution: GameSolution; referenceOutcome: Profile | null; profile: Profile | null; profileKey: string | null; policyCost: number; headline: string; narrative: string; selectionReason: string; coordinationAnalysis: StagHuntAnalysis; crossPanelSurvival: CrossPanelSurvivalReport; }; export type RobustnessOptions = { panelId?: string; samples?: number; seed?: number | string; }; export type SensitivityResult = { rank: number; inputId: string; profileKey: string; actorId: string; correlation: number; absoluteCorrelation: number; }; export type RobustnessReport = { panelId: string; seed: number; samples: number; generator: string; uncertaintyModel: string; sensitivityMethod: string; sensitivityMetric: string; sensitivityStatus: "estimated" | "not-estimable"; referenceProfileKey: string | null; referenceEquilibriumSurvivalRate: number | null; profileEquilibriumRates: Array<{ profileKey: string; count: number; rate: number }>; pureEquilibriumStructureFrequencies: Array<{ signature: string; count: number; rate: number }>; sensitivityRanking: SensitivityResult[]; }; export type CanonicalPanelSnapshot = { id: string; label: string; summary: string; authorial: boolean; grade: ProvenanceGrade; sourceIds: string[]; game: Game; }; export type CanonicalRunPayload = { schemaVersion: typeof OPENNASH_RECEIPT_SCHEMA_VERSION; scenario: { schemaVersion: Scenario["schemaVersion"]; id: string; version: string; sourcePath: string; publishedAt: string; modelClass: Scenario["modelClass"]; status: Scenario["status"]; payoffUnits: string; aggregationRule: string; panel: CanonicalPanelSnapshot; worldviewPanels: CanonicalPanelSnapshot[]; interventionEnvelope: InterventionEnvelope; cooperativeStrategies: Record; narratives: Record; sources: SourceReference[]; }; solver: { version: string }; policy: PolicyConfig; transformedGame: Game; result: Omit & { crossPanelSurvival: CrossPanelSurvivalReport; }; robustness: RobustnessReport | null; }; export function profileKey(profile: Profile): string { return profile.join("|"); } export function validateScenarioSourceDocument(document: ScenarioSourceDocument): string[] { const errors: string[] = []; if (document.schemaVersion !== "opennash.scenario.v1") errors.push("schemaVersion must be opennash.scenario.v1."); if (!document.scenarioVersion) errors.push("scenarioVersion is required."); if (document.solverVersion !== OPENNASH_SOLVER_VERSION) { errors.push(`solverVersion must be ${OPENNASH_SOLVER_VERSION}.`); } if (document.actors.length !== 2) errors.push("A published OpenNash 2x2 scenario must contain exactly two actors."); document.actors.forEach((actor) => { if (actor.strategies.length !== 2) errors.push(`${actor.id} must contain exactly two strategies.`); }); if (document.worldviewPanels.length !== 2) errors.push("Scenario version 1.1 must publish exactly two worldview panels."); if (!document.worldviewPanels.some((panel) => panel.id === document.defaultPanelId)) { errors.push("defaultPanelId must reference a published worldview panel."); } if (document.interventionEnvelope.budget !== 8) errors.push("The intervention budget must be 8."); if (document.interventionEnvelope.step !== 0.5) errors.push("The intervention input step must be 0.5."); if (!document.payoffUnits.trim()) errors.push("payoffUnits is required."); if (!document.aggregationRule.trim()) errors.push("aggregationRule is required."); const actorIds = new Set(document.actors.map((actor) => actor.id)); if (actorIds.size !== document.actors.length) errors.push("Actor IDs must be unique."); const sourceIds = new Set(document.sources.map((source) => source.id)); const expectedProfiles = document.actors.length === 2 ? document.actors[0].strategies.flatMap((row) => document.actors[1].strategies.map((column) => `${row.id}|${column.id}`)) : []; Object.entries(document.cooperativeStrategies).forEach(([actorId, strategyId]) => { const actor = document.actors.find((entry) => entry.id === actorId); if (!actorIds.has(actorId) || !actor?.strategies.some((strategy) => strategy.id === strategyId)) { errors.push(`cooperativeStrategies contains an invalid reference for ${actorId}.`); } }); document.actors.forEach((actor) => { if (!document.cooperativeStrategies[actor.id]) errors.push(`cooperativeStrategies is missing ${actor.id}.`); if (new Set(actor.strategies.map((strategy) => strategy.id)).size !== actor.strategies.length) { errors.push(`${actor.id} strategy IDs must be unique.`); } }); if (expectedProfiles.length > 0 && !expectedProfiles.includes(document.statusQuoProfile)) { errors.push("statusQuoProfile must reference a declared strategy profile."); } document.worldviewPanels.forEach((panel) => { if (!panel.authorial || panel.grade !== "C") errors.push(`${panel.id} must be explicitly authorial with grade C.`); const actualProfiles = Object.keys(panel.payoffs).sort(); if (canonicalStringify(actualProfiles) !== canonicalStringify([...expectedProfiles].sort())) { errors.push(`${panel.id} must contain exactly the four declared strategy profiles.`); } panel.sourceIds.forEach((sourceId) => { if (!sourceIds.has(sourceId)) errors.push(`${panel.id} references unknown source ${sourceId}.`); }); Object.entries(panel.payoffs).forEach(([key, record]) => { if (!record.rationale.trim()) errors.push(`${panel.id}:${key} requires a rationale.`); if (record.values.length !== document.actors.length) errors.push(`${panel.id}:${key} must contain one payoff per actor.`); record.values.forEach((value, actorIndex) => { const prefix = `${panel.id}:${key}:${document.actors[actorIndex]?.id ?? actorIndex}`; if (![value.low, value.point, value.high].every(Number.isFinite)) errors.push(`${prefix} payoff bounds must be finite.`); if (value.low > value.point || value.point > value.high) errors.push(`${prefix} must satisfy low <= point <= high.`); if (value.grade !== "C") errors.push(`${prefix} must use provenance grade C.`); if (!value.basis.trim()) errors.push(`${prefix} requires a basis.`); if (value.sourceIds.length === 0) errors.push(`${prefix} requires at least one source ID.`); value.sourceIds.forEach((sourceId) => { if (!sourceIds.has(sourceId)) errors.push(`${prefix} references unknown source ${sourceId}.`); }); }); }); }); return errors; } export function assertValidScenarioSourceDocument(document: ScenarioSourceDocument): void { const errors = validateScenarioSourceDocument(document); if (errors.length > 0) throw new Error(`Invalid OpenNash scenario source:\n- ${errors.join("\n- ")}`); } export function enumerateProfiles(actors: Actor[]): Profile[] { return actors.reduce( (profiles, actor) => profiles.flatMap((profile) => actor.strategies.map((strategy) => [...profile, strategy.id])), [[]], ); } function payoffFor(game: Game, profile: Profile): PayoffRecord { const record = game.payoffs[profileKey(profile)]; if (!record) throw new Error(`Missing payoff for ${profileKey(profile)}`); if (record.values.length !== game.actors.length) { throw new Error(`Payoff ${profileKey(profile)} must contain one value for each actor.`); } return record; } function copyGame(game: Game): Game { return { actors: game.actors.map((actor) => ({ ...actor, strategies: actor.strategies.map((strategy) => ({ ...strategy })), })), payoffs: Object.fromEntries( Object.entries(game.payoffs).map(([key, record]) => [key, { values: [...record.values], estimates: record.estimates?.map((estimate) => ({ ...estimate, sourceIds: [...estimate.sourceIds], })), rationale: record.rationale, adjustments: record.adjustments ? [...record.adjustments] : undefined, }]), ), policyEdits: game.policyEdits?.map((edit) => ({ ...edit })), }; } function resolvePanel(scenario: Scenario, panelId = scenario.defaultPanelId): WorldviewPanel { const panel = scenario.worldviewPanels.find((entry) => entry.id === panelId); if (!panel) throw new Error(`Unknown OpenNash worldview panel: ${panelId}`); return panel; } function deviationConstraintsFor(game: Game, profile: Profile): DeviationConstraint[] { const key = profileKey(profile); const current = payoffFor(game, profile).values; const constraints: DeviationConstraint[] = []; game.actors.forEach((actor, actorIndex) => { actor.strategies.forEach((strategy) => { if (strategy.id === profile[actorIndex]) return; const deviation = [...profile]; deviation[actorIndex] = strategy.id; const deviationKey = profileKey(deviation); const deviationPayoff = payoffFor(game, deviation).values[actorIndex]; const gain = deviationPayoff - current[actorIndex]; constraints.push({ actorId: actor.id, actorIndex, equilibriumProfileKey: key, deviationProfileKey: deviationKey, fromStrategyId: profile[actorIndex], toStrategyId: strategy.id, equilibriumPayoff: current[actorIndex], deviationPayoff, deviationGain: gain, slack: current[actorIndex] - deviationPayoff, }); }); }); return constraints; } function incentiveMarginForEquilibrium(profile: Profile, constraints: DeviationConstraint[]): IncentiveMargin { const minimumSlack = Math.min(...constraints.map((constraint) => constraint.slack)); const bindingConstraints = constraints.filter( (constraint) => Math.abs(constraint.slack - minimumSlack) <= EPSILON, ); return { singleEntryTieMargin: Math.max(0, minimumSlack), jointLInfinityTieRadius: Math.max(0, minimumSlack / 2), equilibriumProfileKey: profileKey(profile), bindingConstraints, definition: "The single-entry tie margin is the minimum payoff slack across this equilibrium's best-response constraints. Every constraint at that minimum is reported. An equal one-entry edit creates a tie. The joint L-infinity tie radius is half the slack because the equilibrium payoff can fall while the deviation payoff rises by the same bounded amount. These local values do not measure the nearest boundary elsewhere in the pure-profile classification.", }; } function nearestPureEquilibriumBoundary( profiles: Profile[], constraintsByProfile: Record, ): PureEquilibriumBoundary { const allCandidates = profiles.map((profile): PureProfileBoundaryCandidate => { const key = profileKey(profile); const constraints = constraintsByProfile[key]; const strict = constraints.every((constraint) => constraint.slack > EPSILON); const equilibrium = constraints.every((constraint) => constraint.slack >= -EPSILON); if (equilibrium && !strict) { return { profile, profileKey: key, classification: "weak", boundaryEffect: "already-weak", radius: 0, bindingConstraints: constraints.filter((constraint) => Math.abs(constraint.slack) <= EPSILON), }; } if (strict) { const threshold = Math.min(...constraints.map((constraint) => constraint.slack)); return { profile, profileKey: key, classification: "strict", boundaryEffect: "strict-to-weak", radius: threshold / 2, bindingConstraints: constraints.filter((constraint) => Math.abs(constraint.slack - threshold) <= EPSILON), }; } const threshold = Math.max(...constraints.map((constraint) => Math.max(0, -constraint.slack))); return { profile, profileKey: key, classification: "not-equilibrium", boundaryEffect: "not-equilibrium-to-weak", radius: threshold / 2, bindingConstraints: constraints.filter( (constraint) => Math.abs(Math.max(0, -constraint.slack) - threshold) <= EPSILON, ), }; }); const radius = Math.min(...allCandidates.map((candidate) => candidate.radius)); return { metric: "payoff-entry-l-infinity", radius, candidates: allCandidates.filter((candidate) => Math.abs(candidate.radius - radius) <= EPSILON), definition: "Exact payoff-entry L-infinity distance to the nearest strict, weak, or non-equilibrium classification boundary among pure profiles. The metric checks all pure profiles. It does not measure behavioral validity or changes in mixed-strategy probabilities.", }; } function detectWeaklyDominatedStrategies(game: Game, profiles: Profile[]): Record { const weaklyDominated: Record = {}; game.actors.forEach((actor, actorIndex) => { weaklyDominated[actor.id] = actor.strategies .filter((candidate) => actor.strategies.some((alternative) => { if (candidate.id === alternative.id) return false; let neverWorse = true; let sometimesBetter = false; profiles .filter((profile) => profile[actorIndex] === candidate.id) .forEach((candidateProfile) => { const alternativeProfile = [...candidateProfile]; alternativeProfile[actorIndex] = alternative.id; const candidateValue = payoffFor(game, candidateProfile).values[actorIndex]; const alternativeValue = payoffFor(game, alternativeProfile).values[actorIndex]; if (alternativeValue < candidateValue - EPSILON) neverWorse = false; if (alternativeValue > candidateValue + EPSILON) sometimesBetter = true; }); return neverWorse && sometimesBetter; }), ) .map((strategy) => strategy.id); }); return weaklyDominated; } function isZero(value: number): boolean { return Math.abs(value) <= EPSILON; } function rootOfLinear(atZero: number, atOne: number): number | null { const change = atOne - atZero; if (isZero(change)) return null; return -atZero / change; } function inequalityInterval(atZero: number, atOne: number, relation: "gte" | "lte"): [number, number] | null { const accepts = (value: number) => relation === "gte" ? value >= -EPSILON : value <= EPSILON; const zeroAccepted = accepts(atZero); const oneAccepted = accepts(atOne); if (zeroAccepted && oneAccepted) return [0, 1]; if (!zeroAccepted && !oneAccepted) return null; const root = rootOfLinear(atZero, atOne); if (root === null || root < -EPSILON || root > 1 + EPSILON) return null; const boundedRoot = Math.max(0, Math.min(1, root)); return zeroAccepted ? [0, boundedRoot] : [boundedRoot, 1]; } function analyzeMixedEquilibria(game: Game): MixedEquilibriumAnalysis { const method = "For a 2x2 game, solve each actor's linear indifference equation. Report an interior mixed equilibrium only when both probabilities are strictly between 0 and 1. Inspect endpoint ties and all-mixture indifference separately to identify degenerate equilibrium continua."; if (game.actors.length !== 2 || game.actors.some((actor) => actor.strategies.length !== 2)) { return { status: "not-applicable", interior: [], ties: [], continua: [], method }; } const [rowActor, columnActor] = game.actors; const [row0, row1] = rowActor.strategies; const [column0, column1] = columnActor.strategies; const value = (rowStrategy: string, columnStrategy: string, actorIndex: number) => payoffFor(game, [rowStrategy, columnStrategy]).values[actorIndex]; const rowDifferenceAtColumn0 = value(row0.id, column0.id, 0) - value(row1.id, column0.id, 0); const rowDifferenceAtColumn1 = value(row0.id, column1.id, 0) - value(row1.id, column1.id, 0); const columnDifferenceAtRow0 = value(row0.id, column0.id, 1) - value(row0.id, column1.id, 1); const columnDifferenceAtRow1 = value(row1.id, column0.id, 1) - value(row1.id, column1.id, 1); const rowAlwaysIndifferent = isZero(rowDifferenceAtColumn0) && isZero(rowDifferenceAtColumn1); const columnAlwaysIndifferent = isZero(columnDifferenceAtRow0) && isZero(columnDifferenceAtRow1); const q = rootOfLinear(rowDifferenceAtColumn1, rowDifferenceAtColumn0); const p = rootOfLinear(columnDifferenceAtRow1, columnDifferenceAtRow0); const ties: MixedTie[] = []; if (rowAlwaysIndifferent) { ties.push({ actorId: rowActor.id, againstStrategyId: null, description: `${rowActor.label} is indifferent between both strategies against every ${columnActor.label} mixture.` }); } else { if (isZero(rowDifferenceAtColumn0)) ties.push({ actorId: rowActor.id, againstStrategyId: column0.id, description: `${rowActor.label} is tied when ${columnActor.label} uses ${column0.label}.` }); if (isZero(rowDifferenceAtColumn1)) ties.push({ actorId: rowActor.id, againstStrategyId: column1.id, description: `${rowActor.label} is tied when ${columnActor.label} uses ${column1.label}.` }); } if (columnAlwaysIndifferent) { ties.push({ actorId: columnActor.id, againstStrategyId: null, description: `${columnActor.label} is indifferent between both strategies against every ${rowActor.label} mixture.` }); } else { if (isZero(columnDifferenceAtRow0)) ties.push({ actorId: columnActor.id, againstStrategyId: row0.id, description: `${columnActor.label} is tied when ${rowActor.label} uses ${row0.label}.` }); if (isZero(columnDifferenceAtRow1)) ties.push({ actorId: columnActor.id, againstStrategyId: row1.id, description: `${columnActor.label} is tied when ${rowActor.label} uses ${row1.label}.` }); } const continua: MixedContinuum[] = []; const addContinuum = (continuum: MixedContinuum) => { const signature = canonicalStringify(continuum.probabilityRanges); if (!continua.some((candidate) => canonicalStringify(candidate.probabilityRanges) === signature)) continua.push(continuum); }; const range = (strategyId: string, minimum: number, maximum: number): ProbabilityRange => ({ strategyId, minimum, maximum }); if (rowAlwaysIndifferent && columnAlwaysIndifferent) { addContinuum({ dimension: "area", probabilityRanges: { [rowActor.id]: range(row0.id, 0, 1), [columnActor.id]: range(column0.id, 0, 1), }, description: "Both actors are indifferent for every opponent mixture, so every mixed-strategy pair is a Nash equilibrium.", }); } else { if (rowAlwaysIndifferent && p !== null && p >= -EPSILON && p <= 1 + EPSILON) { const boundedP = Math.max(0, Math.min(1, p)); addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, boundedP, boundedP), [columnActor.id]: range(column0.id, 0, 1), }, description: `${rowActor.label} uses its probability that makes ${columnActor.label} indifferent; ${columnActor.label} can use any mixture.`, }); } if (columnAlwaysIndifferent && q !== null && q >= -EPSILON && q <= 1 + EPSILON) { const boundedQ = Math.max(0, Math.min(1, q)); addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, 0, 1), [columnActor.id]: range(column0.id, boundedQ, boundedQ), }, description: `${columnActor.label} uses its probability that makes ${rowActor.label} indifferent; ${rowActor.label} can use any mixture.`, }); } if (isZero(rowDifferenceAtColumn1)) { const interval = inequalityInterval(columnDifferenceAtRow1, columnDifferenceAtRow0, "lte"); if (interval && interval[1] - interval[0] > EPSILON) { addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, interval[0], interval[1]), [columnActor.id]: range(column0.id, 0, 0), }, description: `${columnActor.label} uses ${column1.label}; ${rowActor.label} can mix across the stated interval while both remain best responding.`, }); } } if (isZero(rowDifferenceAtColumn0)) { const interval = inequalityInterval(columnDifferenceAtRow1, columnDifferenceAtRow0, "gte"); if (interval && interval[1] - interval[0] > EPSILON) { addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, interval[0], interval[1]), [columnActor.id]: range(column0.id, 1, 1), }, description: `${columnActor.label} uses ${column0.label}; ${rowActor.label} can mix across the stated interval while both remain best responding.`, }); } } if (isZero(columnDifferenceAtRow1)) { const interval = inequalityInterval(rowDifferenceAtColumn1, rowDifferenceAtColumn0, "lte"); if (interval && interval[1] - interval[0] > EPSILON) { addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, 0, 0), [columnActor.id]: range(column0.id, interval[0], interval[1]), }, description: `${rowActor.label} uses ${row1.label}; ${columnActor.label} can mix across the stated interval while both remain best responding.`, }); } } if (isZero(columnDifferenceAtRow0)) { const interval = inequalityInterval(rowDifferenceAtColumn1, rowDifferenceAtColumn0, "gte"); if (interval && interval[1] - interval[0] > EPSILON) { addContinuum({ dimension: "line", probabilityRanges: { [rowActor.id]: range(row0.id, 1, 1), [columnActor.id]: range(column0.id, interval[0], interval[1]), }, description: `${rowActor.label} uses ${row0.label}; ${columnActor.label} can mix across the stated interval while both remain best responding.`, }); } } } if (ties.length > 0 || continua.length > 0) { return { status: "degenerate", interior: [], ties, continua, method }; } if (p !== null && q !== null && p > EPSILON && p < 1 - EPSILON && q > EPSILON && q < 1 - EPSILON) { const rowExpected = p * q * value(row0.id, column0.id, 0) + p * (1 - q) * value(row0.id, column1.id, 0) + (1 - p) * q * value(row1.id, column0.id, 0) + (1 - p) * (1 - q) * value(row1.id, column1.id, 0); const columnExpected = p * q * value(row0.id, column0.id, 1) + p * (1 - q) * value(row0.id, column1.id, 1) + (1 - p) * q * value(row1.id, column0.id, 1) + (1 - p) * (1 - q) * value(row1.id, column1.id, 1); return { status: "interior", interior: [{ kind: "interior", probabilities: { [rowActor.id]: { [row0.id]: p, [row1.id]: 1 - p }, [columnActor.id]: { [column0.id]: q, [column1.id]: 1 - q }, }, expectedPayoffs: [rowExpected, columnExpected], }], ties: [], continua: [], method, }; } return { status: "none", interior: [], ties: [], continua: [], method }; } export function analyzeStagHunt( game: Game, cooperativeStrategies: Record, ): StagHuntAnalysis { const method = "The stag-hunt lens requires two actors with two strategies each. Mutual cooperation and mutual defense must both be strict Nash equilibria, and mutual cooperation must give both actors a strictly higher payoff than mutual defense. Risk dominance compares the products of the actors' unilateral-deviation losses at the two equilibria. Each cooperation threshold is the other actor's cooperation probability that makes the named actor indifferent."; if (game.actors.length !== 2 || game.actors.some((actor) => actor.strategies.length !== 2)) { return { classification: "not-applicable", cooperativeProfileKey: null, defensiveProfileKey: null, cooperativeProfileIsStrictEquilibrium: false, defensiveProfileIsStrictEquilibrium: false, cooperativeProfilePayoffDominates: false, payoffDominantProfileKey: null, riskDominance: "not-applicable", riskDominantProfileKey: null, cooperativeDeviationLossProduct: null, defensiveDeviationLossProduct: null, thresholds: [], diagnosis: "The stag-hunt lens applies only to a two-actor, two-strategy game with one declared cooperative strategy for each actor.", method, }; } const cooperativeProfile = game.actors.map((actor) => cooperativeStrategies[actor.id]); const declaredStrategiesAreValid = game.actors.every((actor, actorIndex) => actor.strategies.some((strategy) => strategy.id === cooperativeProfile[actorIndex]), ); if (!declaredStrategiesAreValid) { return { classification: "not-applicable", cooperativeProfileKey: null, defensiveProfileKey: null, cooperativeProfileIsStrictEquilibrium: false, defensiveProfileIsStrictEquilibrium: false, cooperativeProfilePayoffDominates: false, payoffDominantProfileKey: null, riskDominance: "not-applicable", riskDominantProfileKey: null, cooperativeDeviationLossProduct: null, defensiveDeviationLossProduct: null, thresholds: [], diagnosis: "The stag-hunt lens requires one valid declared cooperative strategy for each actor.", method, }; } const defensiveProfile = game.actors.map((actor, actorIndex) => actor.strategies.find((strategy) => strategy.id !== cooperativeProfile[actorIndex])!.id, ); const cooperativeKey = profileKey(cooperativeProfile); const defensiveKey = profileKey(defensiveProfile); const cooperativeValues = payoffFor(game, cooperativeProfile).values; const defensiveValues = payoffFor(game, defensiveProfile).values; const thresholds = game.actors.map((actor, actorIndex): CoordinationThreshold => { const otherActorIndex = actorIndex === 0 ? 1 : 0; const cooperativeDeviation = [...cooperativeProfile]; cooperativeDeviation[actorIndex] = defensiveProfile[actorIndex]; const defensiveDeviation = [...defensiveProfile]; defensiveDeviation[actorIndex] = cooperativeProfile[actorIndex]; const cooperativeDeviationLoss = cooperativeValues[actorIndex] - payoffFor(game, cooperativeDeviation).values[actorIndex]; const defensiveDeviationLoss = defensiveValues[actorIndex] - payoffFor(game, defensiveDeviation).values[actorIndex]; const denominator = cooperativeDeviationLoss + defensiveDeviationLoss; const minimumOtherCooperationBelief = cooperativeDeviationLoss > EPSILON && defensiveDeviationLoss > EPSILON && denominator > EPSILON ? defensiveDeviationLoss / denominator : null; return { actorId: actor.id, actorLabel: actor.label, otherActorId: game.actors[otherActorIndex].id, otherActorLabel: game.actors[otherActorIndex].label, cooperativeDeviationLoss, defensiveDeviationLoss, minimumOtherCooperationBelief, }; }); const cooperativeProfileIsStrictEquilibrium = thresholds.every( (entry) => entry.cooperativeDeviationLoss > EPSILON, ); const defensiveProfileIsStrictEquilibrium = thresholds.every( (entry) => entry.defensiveDeviationLoss > EPSILON, ); const cooperativeProfilePayoffDominates = cooperativeValues.every( (value, actorIndex) => value > defensiveValues[actorIndex] + EPSILON, ); const classification = cooperativeProfileIsStrictEquilibrium && defensiveProfileIsStrictEquilibrium && cooperativeProfilePayoffDominates ? "stag-hunt" : "not-stag-hunt"; const cooperativeDeviationLossProduct = classification === "stag-hunt" ? thresholds.reduce((product, entry) => product * entry.cooperativeDeviationLoss, 1) : null; const defensiveDeviationLossProduct = classification === "stag-hunt" ? thresholds.reduce((product, entry) => product * entry.defensiveDeviationLoss, 1) : null; let riskDominance: StagHuntAnalysis["riskDominance"] = "not-applicable"; let riskDominantProfileKey: string | null = null; if (classification === "stag-hunt" && cooperativeDeviationLossProduct !== null && defensiveDeviationLossProduct !== null) { if (Math.abs(cooperativeDeviationLossProduct - defensiveDeviationLossProduct) <= EPSILON) { riskDominance = "tie"; } else if (cooperativeDeviationLossProduct > defensiveDeviationLossProduct) { riskDominance = "cooperative"; riskDominantProfileKey = cooperativeKey; } else { riskDominance = "defensive"; riskDominantProfileKey = defensiveKey; } } const failedConditions: string[] = []; if (!cooperativeProfileIsStrictEquilibrium) { const actorLabels = thresholds .filter((entry) => entry.cooperativeDeviationLoss <= EPSILON) .map((entry) => entry.actorLabel) .join(" and "); failedConditions.push(`${actorLabels} can gain or tie by leaving mutual cooperation alone.`); } if (!defensiveProfileIsStrictEquilibrium) { const actorLabels = thresholds .filter((entry) => entry.defensiveDeviationLoss <= EPSILON) .map((entry) => entry.actorLabel) .join(" and "); failedConditions.push(`${actorLabels} can gain or tie by leaving mutual defense alone.`); } if (!cooperativeProfilePayoffDominates) { failedConditions.push("Mutual cooperation does not give both actors a strictly higher payoff than mutual defense."); } const diagnosis = classification === "stag-hunt" ? riskDominance === "tie" ? "The submitted matrix is a stag hunt. Mutual cooperation is payoff-dominant. Neither strict equilibrium is risk-dominant under the deviation-loss product test." : `The submitted matrix is a stag hunt. Mutual cooperation is payoff-dominant. The ${riskDominance} equilibrium is risk-dominant under the deviation-loss product test.` : `The submitted matrix is not a stag hunt. ${failedConditions.join(" ")}`; return { classification, cooperativeProfileKey: cooperativeKey, defensiveProfileKey: defensiveKey, cooperativeProfileIsStrictEquilibrium, defensiveProfileIsStrictEquilibrium, cooperativeProfilePayoffDominates, payoffDominantProfileKey: cooperativeProfilePayoffDominates ? cooperativeKey : null, riskDominance, riskDominantProfileKey, cooperativeDeviationLossProduct, defensiveDeviationLossProduct, thresholds, diagnosis, method, }; } export function solveGame(game: Game): GameSolution { const profiles = enumerateProfiles(game.actors); if (profiles.length === 0) throw new Error("A game must contain at least one strategy profile."); Object.keys(game.payoffs).forEach((key) => { if (!profiles.some((profile) => profileKey(profile) === key)) throw new Error(`Unexpected payoff profile: ${key}`); }); profiles.forEach((profile) => payoffFor(game, profile)); const deviationConstraints = Object.fromEntries( profiles.map((profile) => [profileKey(profile), deviationConstraintsFor(game, profile)]), ); const maximumDeviationGains = Object.fromEntries( profiles.map((profile) => { const constraints = deviationConstraints[profileKey(profile)]; const maxGain = constraints.length > 0 ? Math.max(...constraints.map((constraint) => constraint.deviationGain)) : 0; return [profileKey(profile), maxGain]; }), ); const pureNash = profiles.filter((profile) => maximumDeviationGains[profileKey(profile)] <= EPSILON); const pureEquilibria = pureNash.map((profile): PureEquilibrium => { const constraints = deviationConstraints[profileKey(profile)]; return { profile, profileKey: profileKey(profile), kind: constraints.every((constraint) => constraint.deviationGain < -EPSILON) ? "strict" : "weak", constraints, incentiveMargin: incentiveMarginForEquilibrium(profile, constraints), }; }); const paretoFrontier = profiles.filter((profile) => { const current = payoffFor(game, profile).values; return !profiles.some((other) => { if (profileKey(other) === profileKey(profile)) return false; const candidate = payoffFor(game, other).values; return candidate.every((value, index) => value >= current[index] - EPSILON) && candidate.some((value, index) => value > current[index] + EPSILON); }); }); const mixedEquilibriumAnalysis = analyzeMixedEquilibria(game); const hasAdditionalMixedEquilibria = mixedEquilibriumAnalysis.interior.length > 0 || mixedEquilibriumAnalysis.continua.length > 0; return { profiles, pureNash, pureEquilibria, referenceOutcome: pureNash.length === 1 && !hasAdditionalMixedEquilibria ? [...pureNash[0]] : null, mixedEquilibriumAnalysis, pureEquilibriumBoundary: nearestPureEquilibriumBoundary(profiles, deviationConstraints), paretoFrontier, maximumDeviationGains, deviationConstraints, weaklyDominatedStrategies: detectWeaklyDominatedStrategies(game, profiles), }; } export function defaultPolicy(scenario: Scenario): PolicyConfig { const firstActor = scenario.game.actors[0]; const secondActor = scenario.game.actors[1] ?? firstActor; return { capacityActorId: firstActor.id, capacityInvestment: 0, marketAccessActorId: secondActor.id, marketAccessCondition: 0, transitionRecipientId: secondActor.id, transitionTransfer: 0, }; } function quantize(value: unknown, minimum: number, maximum: number, step: number): number { const numeric = Number(value); const finite = Number.isFinite(numeric) ? numeric : minimum; const clamped = Math.min(maximum, Math.max(minimum, finite)); const stepped = Math.round(clamped / step) * step; return Number(stepped.toFixed(8)); } export function normalizePolicy(scenario: Scenario, candidate: PolicyDraft): PolicyConfig { const defaults = defaultPolicy(scenario); const actorIds = new Set(scenario.game.actors.map((actor) => actor.id)); const envelope = scenario.interventionEnvelope; return { capacityActorId: actorIds.has(candidate.capacityActorId ?? "") ? candidate.capacityActorId! : defaults.capacityActorId, capacityInvestment: quantize( candidate.capacityInvestment, 0, envelope.instruments.capacityInvestment.maximum, envelope.step, ), marketAccessActorId: actorIds.has(candidate.marketAccessActorId ?? "") ? candidate.marketAccessActorId! : defaults.marketAccessActorId, marketAccessCondition: quantize( candidate.marketAccessCondition, 0, envelope.instruments.marketAccessCondition.maximum, envelope.step, ), transitionRecipientId: actorIds.has(candidate.transitionRecipientId ?? "") ? candidate.transitionRecipientId! : defaults.transitionRecipientId, transitionTransfer: quantize( candidate.transitionTransfer, 0, envelope.instruments.transitionTransfer.maximum, envelope.step, ), }; } export function calculatePolicyCost(policy: PolicyConfig): number { return Number((policy.capacityInvestment + policy.marketAccessCondition + policy.transitionTransfer).toFixed(8)); } export function isPolicyWithinEnvelope(scenario: Scenario, policy: PolicyConfig): boolean { return calculatePolicyCost(policy) <= scenario.interventionEnvelope.budget + EPSILON; } function adjustEstimate(estimate: PayoffEstimate | undefined, point: number, delta: number): PayoffEstimate | undefined { if (!estimate) return undefined; return { ...estimate, point, low: estimate.low + delta, high: estimate.high + delta, sourceIds: [...estimate.sourceIds], }; } export function applyPolicy(scenario: Scenario, candidatePolicy: PolicyDraft, panelId = scenario.defaultPanelId): Game { const policy = normalizePolicy(scenario, candidatePolicy); if (!isPolicyWithinEnvelope(scenario, policy)) { throw new RangeError(`OpenNash intervention cost ${calculatePolicyCost(policy)} exceeds the ${scenario.interventionEnvelope.budget}-unit envelope.`); } const game = copyGame(resolvePanel(scenario, panelId).game); const actors = game.actors; const capacityIndex = actors.findIndex((actor) => actor.id === policy.capacityActorId); const marketAccessIndex = actors.findIndex((actor) => actor.id === policy.marketAccessActorId); const recipientIndex = actors.findIndex((actor) => actor.id === policy.transitionRecipientId); const payerIndex = recipientIndex === 0 && actors.length > 1 ? 1 : 0; const edits: PolicyEdit[] = []; Object.entries(game.payoffs).forEach(([key, record]) => { const profile = key.split("|"); const adjustments: string[] = []; const applyEdit = (actorIndex: number, delta: number, instrument: PolicyInstrumentId, description: string) => { const before = record.values[actorIndex]; const after = before + delta; record.values[actorIndex] = after; if (record.estimates) record.estimates[actorIndex] = adjustEstimate(record.estimates[actorIndex], after, delta)!; adjustments.push(description); edits.push({ instrument, profileKey: key, actorId: actors[actorIndex].id, delta, before, after, description, }); }; if (capacityIndex >= 0 && policy.capacityInvestment > 0 && profile[capacityIndex] === scenario.cooperativeStrategies[actors[capacityIndex].id]) { applyEdit( capacityIndex, policy.capacityInvestment, "capacity-investment", `${actors[capacityIndex].label} receives a ${policy.capacityInvestment.toFixed(1)} capacity-investment reward for its cooperative strategy.`, ); } if (marketAccessIndex >= 0 && policy.marketAccessCondition > 0 && profile[marketAccessIndex] !== scenario.cooperativeStrategies[actors[marketAccessIndex].id]) { applyEdit( marketAccessIndex, -policy.marketAccessCondition, "market-access-condition", `${actors[marketAccessIndex].label} loses ${policy.marketAccessCondition.toFixed(1)} payoff units under the direct market-access condition when it uses its adversarial strategy.`, ); } const allCooperate = actors.every( (actor, index) => profile[index] === scenario.cooperativeStrategies[actor.id], ); if (allCooperate && recipientIndex >= 0 && payerIndex !== recipientIndex && policy.transitionTransfer > 0) { applyEdit( payerIndex, -policy.transitionTransfer, "transition-transfer", `${actors[payerIndex].label} pays a ${policy.transitionTransfer.toFixed(1)} transition transfer in the mutual-cooperation cell.`, ); applyEdit( recipientIndex, policy.transitionTransfer, "transition-transfer", `${actors[recipientIndex].label} receives a ${policy.transitionTransfer.toFixed(1)} transition transfer in the mutual-cooperation cell.`, ); } record.adjustments = adjustments; }); game.policyEdits = edits; return game; } function outcomeNarrative(scenario: Scenario, profile: Profile): { title: string; copy: string } { const exact = scenario.narratives[profileKey(profile)]; if (exact) return exact; return { title: "One reference equilibrium.", copy: "The transformed one-shot game has one pure-strategy Nash equilibrium.", }; } export function analyzeCrossPanelSurvival(scenario: Scenario, candidatePolicy: PolicyDraft): CrossPanelSurvivalReport { const policy = normalizePolicy(scenario, candidatePolicy); const panelResults = scenario.worldviewPanels.map((panel) => { const solution = solveGame(applyPolicy(scenario, policy, panel.id)); return { panelId: panel.id, panelLabel: panel.label, authorial: panel.authorial, pureEquilibria: solution.pureEquilibria.map((equilibrium) => ({ profileKey: equilibrium.profileKey, kind: equilibrium.kind, })), mixedStatus: solution.mixedEquilibriumAnalysis.status, }; }); const profileKeys = enumerateProfiles(scenario.game.actors).map(profileKey); const profiles = profileKeys.map((key) => { const equilibriumPanelIds = panelResults .filter((panel) => panel.pureEquilibria.some((equilibrium) => equilibrium.profileKey === key)) .map((panel) => panel.panelId); return { profileKey: key, equilibriumPanelIds, equilibriumInAllPanels: equilibriumPanelIds.length === panelResults.length, }; }); return { method: "Apply the same normalized one-shot intervention to every published worldview panel, solve each panel independently, and report the intersection of their pure-strategy equilibrium profile keys.", panelResults, profiles, commonPureEquilibriumKeys: profiles.filter((entry) => entry.equilibriumInAllPanels).map((entry) => entry.profileKey), }; } export function runScenario( scenario: Scenario, candidatePolicy: PolicyDraft, options: { panelId?: string } = {}, ): ScenarioRun { const panelId = options.panelId ?? scenario.defaultPanelId; const policy = normalizePolicy(scenario, candidatePolicy); const game = applyPolicy(scenario, policy, panelId); const solution = solveGame(game); const coordinationAnalysis = analyzeStagHunt(game, scenario.cooperativeStrategies); const referenceOutcome = solution.referenceOutcome; const policyCost = calculatePolicyCost(policy); let headline: string; let narrative: string; let selectionReason: string; if (referenceOutcome) { const copy = outcomeNarrative(scenario, referenceOutcome); headline = copy.title; narrative = copy.copy; selectionReason = "The complete equilibrium set contains exactly one pure-strategy Nash equilibrium, so OpenNash reports it as the unique reference outcome."; } else if (solution.pureEquilibria.length > 1) { headline = `${solution.pureEquilibria.length} pure equilibria remain.`; narrative = "OpenNash does not select among multiple equilibria. Inspect every equilibrium and state an external selection assumption before using one as a reference."; selectionReason = "No reference outcome: the transformed game has multiple pure-strategy Nash equilibria."; } else if (solution.pureEquilibria.length === 1) { headline = "The equilibrium set is not a singleton."; narrative = "The game has one pure equilibrium and an additional mixed equilibrium or equilibrium continuum. OpenNash does not elevate the pure profile over the other equilibria."; selectionReason = "No reference outcome: the complete equilibrium set contains more than one equilibrium."; } else { headline = "No pure-strategy reference outcome."; narrative = solution.mixedEquilibriumAnalysis.status === "interior" ? "The game has an interior mixed-strategy equilibrium. OpenNash does not replace it with a pure profile selected by a payoff sum." : "The game has no pure-strategy equilibrium. Inspect the mixed-strategy and degeneracy report."; selectionReason = "No reference outcome: the transformed game has no pure-strategy Nash equilibrium."; } return { status: "solved", scenarioId: scenario.id, scenarioVersion: scenario.scenarioVersion, solverVersion: OPENNASH_SOLVER_VERSION, panelId, policy, game, solution, referenceOutcome, profile: referenceOutcome, profileKey: referenceOutcome ? profileKey(referenceOutcome) : null, policyCost, headline, narrative, selectionReason, coordinationAnalysis, crossPanelSurvival: analyzeCrossPanelSurvival(scenario, policy), }; } function seedToUint32(seed: number | string): number { if (typeof seed === "number" && Number.isFinite(seed)) return Math.trunc(seed) >>> 0; const text = String(seed); let hash = 2166136261; for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return hash >>> 0; } function mulberry32(seed: number): () => number { let state = seed >>> 0; return () => { state += 0x6d2b79f5; let value = state; value = Math.imul(value ^ (value >>> 15), value | 1); value ^= value + Math.imul(value ^ (value >>> 7), value | 61); return ((value ^ (value >>> 14)) >>> 0) / 4294967296; }; } function correlation( samples: number, sumX: number, sumX2: number, sumY: number, sumY2: number, sumXY: number, ): number { const numerator = samples * sumXY - sumX * sumY; const denominator = Math.sqrt((samples * sumX2 - sumX * sumX) * (samples * sumY2 - sumY * sumY)); return denominator <= EPSILON ? 0 : numerator / denominator; } export function analyzeRobustness( scenario: Scenario, candidatePolicy: PolicyDraft, options: RobustnessOptions = {}, ): RobustnessReport { const panelId = options.panelId ?? scenario.defaultPanelId; const samples = Math.max(10, Math.min(20_000, Math.floor(options.samples ?? 2_000))); const policy = normalizePolicy(scenario, candidatePolicy); const seed = seedToUint32(options.seed ?? canonicalStringify({ scenarioId: scenario.id, scenarioVersion: scenario.scenarioVersion, panelId, policy, algorithm: "opennash-monte-carlo.mulberry32.v1", })); const random = mulberry32(seed); const pointGame = applyPolicy(scenario, policy, panelId); const pointSolution = solveGame(pointGame); const referenceProfileKey = pointSolution.referenceOutcome ? profileKey(pointSolution.referenceOutcome) : null; const profiles = pointSolution.profiles.map(profileKey); const profileCounts = new Map(profiles.map((key) => [key, 0])); const structures = new Map(); const inputs = pointSolution.profiles.flatMap((profile) => { const key = profileKey(profile); const record = payoffFor(pointGame, profile); return pointGame.actors.map((actor, actorIndex) => { const estimate = record.estimates?.[actorIndex]; return { inputId: `${key}:${actor.id}`, profileKey: key, actorId: actor.id, low: estimate?.low ?? record.values[actorIndex], high: estimate?.high ?? record.values[actorIndex], sumX: 0, sumX2: 0, sumXY: 0, }; }); }); let sumY = 0; let sumY2 = 0; let referenceSurvivals = 0; for (let sampleIndex = 0; sampleIndex < samples; sampleIndex += 1) { const sampledGame = copyGame(pointGame); const sampledValues = new Map(); inputs.forEach((input) => { const value = input.low === input.high ? input.low : input.low + random() * (input.high - input.low); sampledValues.set(input.inputId, value); const actorIndex = sampledGame.actors.findIndex((actor) => actor.id === input.actorId); sampledGame.payoffs[input.profileKey].values[actorIndex] = value; }); const solution = solveGame(sampledGame); const equilibriumKeys = solution.pureEquilibria.map((equilibrium) => equilibrium.profileKey).sort(); equilibriumKeys.forEach((key) => profileCounts.set(key, (profileCounts.get(key) ?? 0) + 1)); const signature = equilibriumKeys.length > 0 ? equilibriumKeys.join(";") : "no-pure-equilibrium"; structures.set(signature, (structures.get(signature) ?? 0) + 1); const referenceSurvives = referenceProfileKey !== null && equilibriumKeys.includes(referenceProfileKey); if (referenceSurvives) referenceSurvivals += 1; const y = referenceProfileKey === null ? equilibriumKeys.length : referenceSurvives ? 1 : 0; sumY += y; sumY2 += y * y; inputs.forEach((input) => { const x = sampledValues.get(input.inputId)!; input.sumX += x; input.sumX2 += x * x; input.sumXY += x * y; }); } const sensitivityEstimable = samples * sumY2 - sumY * sumY > EPSILON; const ranked = sensitivityEstimable ? inputs .map((input) => { const value = correlation(samples, input.sumX, input.sumX2, sumY, sumY2, input.sumXY); return { inputId: input.inputId, profileKey: input.profileKey, actorId: input.actorId, correlation: value, absoluteCorrelation: Math.abs(value), }; }) .sort((left, right) => right.absoluteCorrelation - left.absoluteCorrelation || compareCodeUnitStrings(left.inputId, right.inputId)) .map((entry, index) => ({ ...entry, rank: index + 1 })) : []; return { panelId, seed, samples, generator: "Mulberry32 32-bit pseudorandom number generator; OpenNash implementation version 1.", uncertaintyModel: "Sample every published payoff independently from its uniform [low, high] range. Vary all payoff inputs together in each seeded draw.", sensitivityMethod: "Rank inputs by absolute Pearson correlation between the jointly sampled payoff and the sample-level sensitivity metric. This is a global screening statistic, not a Saltelli or Sobol variance decomposition.", sensitivityMetric: referenceProfileKey === null ? "Number of pure-strategy Nash equilibria in the sampled game." : `Indicator that the point-game reference profile ${referenceProfileKey} remains a pure-strategy Nash equilibrium.`, sensitivityStatus: sensitivityEstimable ? "estimated" : "not-estimable", referenceProfileKey, referenceEquilibriumSurvivalRate: referenceProfileKey === null ? null : referenceSurvivals / samples, profileEquilibriumRates: profiles.map((key) => ({ profileKey: key, count: profileCounts.get(key) ?? 0, rate: (profileCounts.get(key) ?? 0) / samples, })), pureEquilibriumStructureFrequencies: [...structures.entries()] .map(([signature, count]) => ({ signature, count, rate: count / samples })) .sort((left, right) => right.count - left.count || compareCodeUnitStrings(left.signature, right.signature)), sensitivityRanking: ranked, }; } export function payoffRecord(game: Game, profile: Profile): PayoffRecord { return payoffFor(game, profile); } export function strategyLabel(actor: Actor, strategyId: string): string { return actor.strategies.find((strategy) => strategy.id === strategyId)?.label ?? strategyId; } export function encodeGameState(scenarioId: string, policy: PolicyConfig, panelId?: string): string { return btoa(JSON.stringify({ schemaVersion: OPENNASH_DRAFT_SCHEMA_VERSION, scenarioId, panelId, policy, })); } export function decodeGameState(value: string): { scenarioId: string; panelId?: string; policy: PolicyDraft } | null { try { if (value.length > OPENNASH_DRAFT_MAX_ENCODED_LENGTH) return null; const decoded = JSON.parse(atob(value)); if ( !decoded || decoded.schemaVersion !== OPENNASH_DRAFT_SCHEMA_VERSION || typeof decoded.scenarioId !== "string" || !decoded.policy || typeof decoded.policy !== "object" || Array.isArray(decoded.policy) ) return null; return { scenarioId: decoded.scenarioId, panelId: typeof decoded.panelId === "string" ? decoded.panelId : undefined, policy: decoded.policy, }; } catch { return null; } } function canonicalize(value: unknown): unknown { if (value === null || typeof value === "string" || typeof value === "boolean") return value; if (typeof value === "number") { if (!Number.isFinite(value)) throw new TypeError("Canonical OpenNash payloads cannot contain non-finite numbers."); return Object.is(value, -0) ? 0 : value; } if (Array.isArray(value)) return value.map(canonicalize); if (typeof value === "object") { return Object.fromEntries( Object.entries(value as Record) .filter(([, entry]) => entry !== undefined) .sort(([left], [right]) => compareCodeUnitStrings(left, right)) .map(([key, entry]) => [key, canonicalize(entry)]), ); } throw new TypeError(`Unsupported canonical payload value: ${typeof value}`); } function compareCodeUnitStrings(left: string, right: string): number { return left < right ? -1 : left > right ? 1 : 0; } export function canonicalStringify(value: unknown): string { return JSON.stringify(canonicalize(value)); } export async function hashCanonicalPayload(value: unknown): Promise { const bytes = new TextEncoder().encode(canonicalStringify(value)); const digest = await crypto.subtle.digest("SHA-256", bytes); return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); } export function buildCanonicalRunPayload( scenario: Scenario, candidatePolicy: PolicyDraft, options: { panelId?: string; robustness?: false | Omit } = {}, ): CanonicalRunPayload { const panelId = options.panelId ?? scenario.defaultPanelId; const panel = resolvePanel(scenario, panelId); const policy = normalizePolicy(scenario, candidatePolicy); const run = runScenario(scenario, policy, { panelId }); const robustness = options.robustness === false ? null : analyzeRobustness(scenario, policy, { panelId, ...options.robustness }); const { game: transformedGame, policy: runPolicy, crossPanelSurvival, ...result } = run; if (canonicalStringify(runPolicy) !== canonicalStringify(policy)) { throw new Error("OpenNash run policy does not match the canonical normalized policy."); } const panelSnapshot = (entry: WorldviewPanel): CanonicalPanelSnapshot => ({ id: entry.id, label: entry.label, summary: entry.summary, authorial: entry.authorial, grade: entry.grade, sourceIds: [...entry.sourceIds], game: copyGame(entry.game), }); return { schemaVersion: OPENNASH_RECEIPT_SCHEMA_VERSION, scenario: { schemaVersion: scenario.schemaVersion, id: scenario.id, version: scenario.scenarioVersion, sourcePath: scenario.sourcePath, publishedAt: scenario.publishedAt, modelClass: scenario.modelClass, status: scenario.status, payoffUnits: scenario.payoffUnits, aggregationRule: scenario.aggregationRule, panel: panelSnapshot(panel), worldviewPanels: scenario.worldviewPanels.map(panelSnapshot), interventionEnvelope: { ...scenario.interventionEnvelope, instruments: { capacityInvestment: { ...scenario.interventionEnvelope.instruments.capacityInvestment }, marketAccessCondition: { ...scenario.interventionEnvelope.instruments.marketAccessCondition }, transitionTransfer: { ...scenario.interventionEnvelope.instruments.transitionTransfer }, }, }, cooperativeStrategies: { ...scenario.cooperativeStrategies }, narratives: Object.fromEntries( Object.entries(scenario.narratives).map(([key, narrative]) => [key, { ...narrative }]), ), sources: scenario.sources.map((source) => ({ ...source })), }, solver: { version: OPENNASH_SOLVER_VERSION }, policy, transformedGame, result: { ...result, crossPanelSurvival }, robustness, }; }