Last active
August 27, 2025 22:13
-
-
Save patrickisgreat/e12bfcee2ae92dc039a5f615d0204768 to your computer and use it in GitHub Desktop.
Trivia decision tree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ---------- Configuration ---------- | |
| const FRIEND_WEIGHTS = { | |
| Kim: 0.976, | |
| Ryan: 0.95, | |
| Kristen: 0.95, | |
| Taran: 0.85, | |
| Rachel: 0.75, | |
| Paul: 0.65, | |
| Hillary: 0.45, | |
| Pierce: 0.25, | |
| Price: 0.01, | |
| } as const; | |
| const DECISION_THRESHOLDS = { | |
| HIGH_WEIGHT_FRIEND: 0.90, | |
| DEFAULT_HIGH_FRIENDS_NEEDED: 2, | |
| PATRICK_YES_THRESHOLD: 1, | |
| PATRICK_NO_THRESHOLD: 3, | |
| DEFAULT_MAX_WORK_HOURS: 4, | |
| DEFAULT_MAX_DOG_CARE_HOURS: 2 | |
| } as const; | |
| // ---------- Types ---------- | |
| type EventType = "trivia" | "dinner" | "concert" | string; | |
| type TaskType = "work" | "dogCare" | "other"; | |
| type FriendName = keyof typeof FRIEND_WEIGHTS | string; | |
| interface Task { | |
| readonly type: TaskType; | |
| readonly hours: number; | |
| readonly blocking: boolean; | |
| readonly note?: string; | |
| } | |
| interface EventPreferences { | |
| readonly sarah?: boolean; | |
| readonly patrick?: boolean; | |
| readonly dogCanAttend: boolean; | |
| } | |
| interface EventPriorities { | |
| readonly requireDog: boolean; | |
| readonly maxWorkHours: number; | |
| readonly maxDogCareHours: number; | |
| } | |
| interface DecisionInput { | |
| readonly eventType: EventType; | |
| readonly preferences: EventPreferences; | |
| readonly attendees: readonly FriendName[]; | |
| readonly tasks: readonly Task[]; | |
| readonly priorities: EventPriorities; | |
| readonly customWeights?: Partial<Record<FriendName, number>>; | |
| } | |
| interface TaskSummary { | |
| readonly workHours: number; | |
| readonly dogCareHours: number; | |
| readonly hasBlockingWork: boolean; | |
| readonly hasBlockingDogCare: boolean; | |
| } | |
| interface DecisionContext { | |
| readonly input: DecisionInput; | |
| readonly weights: Record<FriendName, number>; | |
| readonly taskSummary: TaskSummary; | |
| readonly reasons: readonly string[]; | |
| } | |
| interface DecisionMetrics { | |
| readonly highWeightFriends: readonly FriendName[]; | |
| readonly requiredHighWeightFriends: number; | |
| readonly taskSummary: TaskSummary; | |
| } | |
| interface DecisionResult { | |
| readonly shouldGo: boolean; | |
| readonly reasons: readonly string[]; | |
| readonly metrics: DecisionMetrics; | |
| } | |
| interface DecisionResultParams { | |
| shouldGo: boolean; | |
| reason: string; | |
| context: DecisionContext; | |
| highWeightFriends: FriendName[]; | |
| requiredHighWeightFriends: number; | |
| } | |
| // Type for decision rules - functions that can block the event | |
| type DecisionRule = (context: DecisionContext) => DecisionResult | null; | |
| // ---------- Task Analysis Functions ---------- | |
| function normalizeTaskType(taskType: string): string { | |
| return taskType.toLowerCase().replace('_', ''); | |
| } | |
| function processTask(summary: TaskSummary, task: Task): TaskSummary { | |
| const taskType = normalizeTaskType(task.type); | |
| if (taskType === 'work') { | |
| return { | |
| ...summary, | |
| workHours: summary.workHours + task.hours, | |
| hasBlockingWork: summary.hasBlockingWork || task.blocking | |
| }; | |
| } | |
| if (taskType === 'dogcare' || taskType === 'dog') { | |
| return { | |
| ...summary, | |
| dogCareHours: summary.dogCareHours + task.hours, | |
| hasBlockingDogCare: summary.hasBlockingDogCare || task.blocking | |
| }; | |
| } | |
| return summary; | |
| } | |
| function createEmptyTaskSummary(): TaskSummary { | |
| return { | |
| workHours: 0, | |
| dogCareHours: 0, | |
| hasBlockingWork: false, | |
| hasBlockingDogCare: false | |
| }; | |
| } | |
| function analyzeTaskSummary(tasks: readonly Task[]): TaskSummary { | |
| return tasks.reduce(processTask, createEmptyTaskSummary()); | |
| } | |
| // ---------- Helper Functions ---------- | |
| function getFriendWeight(friend: FriendName, weights: Record<FriendName, number>): number { | |
| return weights[friend] ?? 0; | |
| } | |
| function getHighWeightFriends(attendees: readonly FriendName[], weights: Record<FriendName, number>): FriendName[] { | |
| return attendees.filter(friend => getFriendWeight(friend, weights) >= DECISION_THRESHOLDS.HIGH_WEIGHT_FRIEND); | |
| } | |
| function calculateRequiredHighWeightFriends(patrickPreference?: boolean): number { | |
| if (patrickPreference === true) return DECISION_THRESHOLDS.PATRICK_YES_THRESHOLD; | |
| if (patrickPreference === false) return DECISION_THRESHOLDS.PATRICK_NO_THRESHOLD; | |
| return DECISION_THRESHOLDS.DEFAULT_HIGH_FRIENDS_NEEDED; | |
| } | |
| function hasExcessiveTaskLoad(taskSummary: TaskSummary, priorities: EventPriorities): boolean { | |
| return taskSummary.workHours >= priorities.maxWorkHours || | |
| taskSummary.dogCareHours >= priorities.maxDogCareHours; | |
| } | |
| function getTaskLoadReasons(taskSummary: TaskSummary, priorities: EventPriorities): string[] { | |
| const reasons: string[] = []; | |
| if (taskSummary.workHours >= priorities.maxWorkHours) { | |
| reasons.push(`Work hours exceed limit (${taskSummary.workHours}h >= ${priorities.maxWorkHours}h)`); | |
| } | |
| if (taskSummary.dogCareHours >= priorities.maxDogCareHours) { | |
| reasons.push(`Dog care hours exceed limit (${taskSummary.dogCareHours}h >= ${priorities.maxDogCareHours}h)`); | |
| } | |
| return reasons; | |
| } | |
| function createWeights(customWeights: Partial<Record<FriendName, number>> = {}): Record<FriendName, number> { | |
| return { ...FRIEND_WEIGHTS, ...customWeights }; | |
| } | |
| // ---------- Result Creation Helpers ---------- | |
| function createDecisionResult(params: DecisionResultParams): DecisionResult { | |
| return { | |
| shouldGo: params.shouldGo, | |
| reasons: [...params.context.reasons, params.reason], | |
| metrics: { | |
| highWeightFriends: params.highWeightFriends, | |
| requiredHighWeightFriends: params.requiredHighWeightFriends, | |
| taskSummary: params.context.taskSummary | |
| } | |
| }; | |
| } | |
| function createDecisionContext(input: DecisionInput): DecisionContext { | |
| const taskSummary = analyzeTaskSummary(input.tasks); | |
| const taskLoadReasons = getTaskLoadReasons(taskSummary, input.priorities); | |
| return { | |
| input, | |
| weights: createWeights(input.customWeights), | |
| taskSummary, | |
| reasons: taskLoadReasons | |
| }; | |
| } | |
| // ---------- Decision Rule Functions ---------- | |
| function checkDogRequirement(context: DecisionContext): DecisionResult | null { | |
| const { requireDog } = context.input.priorities; | |
| const { dogCanAttend } = context.input.preferences; | |
| if (requireDog && !dogCanAttend) { | |
| return createDecisionResult({ | |
| shouldGo: false, | |
| reason: "Dog is required but cannot attend", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| return null; | |
| } | |
| function checkBlockingTasks(context: DecisionContext): DecisionResult | null { | |
| const { hasBlockingWork, hasBlockingDogCare } = context.taskSummary; | |
| if (hasBlockingWork || hasBlockingDogCare) { | |
| return createDecisionResult({ | |
| shouldGo: false, | |
| reason: "Blocking tasks prevent attendance", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| return null; | |
| } | |
| function checkSarahPreference(context: DecisionContext): DecisionResult | null { | |
| const sarah = context.input.preferences.sarah; | |
| const hasExcessiveTasks = hasExcessiveTaskLoad(context.taskSummary, context.input.priorities); | |
| if (sarah === true) { | |
| if (hasExcessiveTasks) { | |
| return createDecisionResult({ | |
| shouldGo: false, | |
| reason: "Sarah wants to go, but task load is too high", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| return createDecisionResult({ | |
| shouldGo: true, | |
| reason: "Sarah wants to go", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| if (sarah === false) { | |
| return createDecisionResult({ | |
| shouldGo: false, | |
| reason: "Sarah doesn't want to go", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| return null; // Sarah is undecided | |
| } | |
| function checkTaskLoadWithUndecidedSarah(context: DecisionContext): DecisionResult | null { | |
| // Only check this if Sarah is undecided (previous rule returned null) | |
| const hasExcessiveTasks = hasExcessiveTaskLoad(context.taskSummary, context.input.priorities); | |
| if (hasExcessiveTasks) { | |
| return createDecisionResult({ | |
| shouldGo: false, | |
| reason: "Task load too high while Sarah is undecided", | |
| context, | |
| highWeightFriends: [], | |
| requiredHighWeightFriends: 0 | |
| }); | |
| } | |
| return null; | |
| } | |
| // ---------- Friend-Based Decision Logic ---------- | |
| function decideBasedOnFriends(context: DecisionContext): DecisionResult { | |
| const highWeightFriends = getHighWeightFriends(context.input.attendees, context.weights); | |
| const requiredHighWeightFriends = calculateRequiredHighWeightFriends(context.input.preferences.patrick); | |
| const shouldGo = highWeightFriends.length >= requiredHighWeightFriends; | |
| const reason = shouldGo | |
| ? `Sufficient high-weight friends attending (${highWeightFriends.length} >= ${requiredHighWeightFriends})` | |
| : `Insufficient high-weight friends attending (${highWeightFriends.length} < ${requiredHighWeightFriends})`; | |
| return createDecisionResult({ | |
| shouldGo, | |
| reason, | |
| context, | |
| highWeightFriends, | |
| requiredHighWeightFriends | |
| }); | |
| } | |
| // ---------- Main Decision Pipeline ---------- | |
| const DECISION_RULES: DecisionRule[] = [ | |
| checkDogRequirement, | |
| checkBlockingTasks, | |
| checkSarahPreference, | |
| checkTaskLoadWithUndecidedSarah | |
| ]; | |
| function runDecisionPipeline(context: DecisionContext): DecisionResult | null { | |
| for (const rule of DECISION_RULES) { | |
| const result = rule(context); | |
| if (result !== null) { | |
| return result; | |
| } | |
| } | |
| return null; | |
| } | |
| // ---------- Public API ---------- | |
| export function decide(input: DecisionInput): DecisionResult { | |
| const context = createDecisionContext(input); | |
| // Try each rule in sequence - first match wins | |
| const pipelineResult = runDecisionPipeline(context); | |
| if (pipelineResult !== null) { | |
| return pipelineResult; | |
| } | |
| // If no rules triggered, decide based on friends | |
| return decideBasedOnFriends(context); | |
| } | |
| // ---------- Factory Functions ---------- | |
| export function createDecisionInput(params: { | |
| eventType: EventType; | |
| sarah?: boolean; | |
| patrick?: boolean; | |
| dogCanAttend?: boolean; | |
| attendees?: FriendName[]; | |
| tasks?: Partial<Task>[]; | |
| requireDog?: boolean; | |
| maxWorkHours?: number; | |
| maxDogCareHours?: number; | |
| customWeights?: Partial<Record<FriendName, number>>; | |
| }): DecisionInput { | |
| return { | |
| eventType: params.eventType, | |
| preferences: { | |
| sarah: params.sarah, | |
| patrick: params.patrick, | |
| dogCanAttend: params.dogCanAttend ?? false | |
| }, | |
| attendees: params.attendees ?? [], | |
| tasks: params.tasks?.map(task => ({ | |
| type: task.type ?? "other", | |
| hours: task.hours ?? 0, | |
| blocking: task.blocking ?? false, | |
| note: task.note | |
| })) ?? [], | |
| priorities: { | |
| requireDog: params.requireDog ?? false, | |
| maxWorkHours: params.maxWorkHours ?? DECISION_THRESHOLDS.DEFAULT_MAX_WORK_HOURS, | |
| maxDogCareHours: params.maxDogCareHours ?? DECISION_THRESHOLDS.DEFAULT_MAX_DOG_CARE_HOURS | |
| }, | |
| customWeights: params.customWeights | |
| }; | |
| } | |
| // ---------- Convenience Functions ---------- | |
| export function shouldWeGo(input: DecisionInput): boolean { | |
| return decide(input).shouldGo; | |
| } | |
| // ---------- Usage Example ---------- | |
| const exampleDecision = decide( | |
| createDecisionInput({ | |
| eventType: "trivia", | |
| sarah: undefined, | |
| patrick: true, | |
| dogCanAttend: false, | |
| attendees: ["Ryan", "Kristen", "Paul"], | |
| tasks: [ | |
| { type: "work", hours: 1.5 }, | |
| { type: "dogCare", hours: 0.5 } | |
| ], | |
| customWeights: { Paul: 0.7 } | |
| }) | |
| ); | |
| console.log(`Should go: ${exampleDecision.shouldGo}`); | |
| console.log(`Reasons: ${exampleDecision.reasons.join(", ")}`); | |
| console.log(`High-weight friends: ${exampleDecision.metrics.highWeightFriends.join(", ")}`); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment