Skip to content

Instantly share code, notes, and snippets.

@patrickisgreat
Last active August 27, 2025 22:13
Show Gist options
  • Select an option

  • Save patrickisgreat/e12bfcee2ae92dc039a5f615d0204768 to your computer and use it in GitHub Desktop.

Select an option

Save patrickisgreat/e12bfcee2ae92dc039a5f615d0204768 to your computer and use it in GitHub Desktop.
Trivia decision tree
// ---------- 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