diff --git a/src/components/RulesOfInferencePlayground.tsx b/src/components/RulesOfInferencePlayground.tsx new file mode 100644 index 0000000..c6e0ba0 --- /dev/null +++ b/src/components/RulesOfInferencePlayground.tsx @@ -0,0 +1,331 @@ +import { useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useToast } from "@/components/ui/use-toast"; + +// Rules of Inference Playground (Example 1: Resolution) +// Layout: 3 columns (Premises | Notepad lines with rule margin | Rules) +// Below: pool of intermediate statements + distractors +// Interaction: drag premises/clauses onto lines, drag a rule to the small right margin for that line, +// then drag a conclusion from the pool to a new line. We validate the step. + +// Types +interface Line { + statement?: string; + rule?: string; // rule id placed in the margin for this line + status?: "pending" | "correct" | "incorrect"; +} + +type DragType = "premise" | "statement" | "rule"; + +interface RuleDef { + id: string; + label: string; + help?: string; +} + +interface ExampleDef { + id: string; + title: string; + premises: string[]; + conclusion: string; + rules: RuleDef[]; + pool: string[]; // candidate intermediate and target statements +} + +const EXAMPLES: ExampleDef[] = [ + { + id: "ex-resolution-1", + title: "(p ∧ q) ∨ r, r → s ⊢ p ∨ s", + premises: ["(p ∧ q) ∨ r", "r → s"], + conclusion: "p ∨ s", + rules: [ + { + id: "distribution", + label: "Distribution: (A ∧ B) ∨ C ≡ (A ∨ C) ∧ (B ∨ C)", + help: "From (p ∧ q) ∨ r you may derive p ∨ r or q ∨ r.", + }, + { + id: "implication-elim", + label: "→ elimination: (A → B) ≡ (¬A ∨ B)", + help: "From r → s you may derive ¬r ∨ s.", + }, + { + id: "resolution", + label: "Resolution", + help: "From (X ∨ r) and (¬r ∨ Y) derive (X ∨ Y).", + }, + ], + pool: [ + "p ∨ r", + "q ∨ r", + "¬r ∨ s", + "p ∨ s", // target + // distractors + "p", + "q", + "r", + "s", + "p ∨ q", + "¬p ∨ s", + ], + }, +]; + +// Small helper to attach drag payload +function onDragStart(e: React.DragEvent, type: DragType, value: string) { + e.dataTransfer.setData("text/plain", JSON.stringify({ type, value })); + e.dataTransfer.effectAllowed = "move"; +} + +function parseDragData(e: React.DragEvent): { type: DragType; value: string } | null { + try { + const raw = e.dataTransfer.getData("text/plain"); + return JSON.parse(raw); + } catch { + return null; + } +} + +function DraggableChip({ label, onDragStart: handle }: { label: string; onDragStart: (e: React.DragEvent) => void }) { + return ( +
+ {label} +
+ ); +} + +function DropZone({ children, onDrop, className }: { children?: React.ReactNode; onDrop: (e: React.DragEvent) => void; className?: string }) { + return ( +
e.preventDefault()} + onDrop={onDrop} + className={ + "min-h-12 rounded-md border border-dashed border-border bg-background/60 p-2 " + + (className ?? "") + } + > + {children} +
+ ); +} + +export default function RulesOfInferencePlayground() { + const { toast } = useToast(); + const [exampleId, setExampleId] = useState(EXAMPLES[0].id); + const example = useMemo(() => EXAMPLES.find((e) => e.id === exampleId)!, [exampleId]); + + const [lines, setLines] = useState(Array.from({ length: 6 }, () => ({ status: "pending" }))); + + useEffect(() => { + // Reset when example changes + setLines(Array.from({ length: 6 }, () => ({ status: "pending" }))); + }, [exampleId]); + + const previousStatements = (upto: number) => + lines.slice(0, upto).map((l) => l.statement).filter(Boolean) as string[]; + + const setLineStatement = (i: number, statement: string) => { + setLines((prev) => { + const next = [...prev]; + next[i] = { ...next[i], statement, status: validateLine(i, { ...next[i], statement }) }; + return next; + }); + }; + + const setLineRule = (i: number, rule: string) => { + setLines((prev) => { + const next = [...prev]; + next[i] = { ...next[i], rule, status: validateLine(i, { ...next[i], rule }) }; + return next; + }); + }; + + const validateLine = (i: number, candidate?: Line): Line["status"] => { + const l = candidate ?? lines[i]; + const stmt = l.statement?.trim(); + const rule = l.rule; + + // Premises allowed with no rule. + if (stmt && example.premises.includes(stmt) && !rule) return "correct"; + + if (!stmt || !rule) return "pending"; + + // Gather earlier statements + const prev = previousStatements(i); + + // Validation rules specific to this example + if (example.id === "ex-resolution-1") { + switch (rule) { + case "distribution": + if (stmt === "p ∨ r" || stmt === "q ∨ r") { + if (prev.includes("(p ∧ q) ∨ r")) return "correct"; + } + break; + case "implication-elim": + if (stmt === "¬r ∨ s" && prev.includes("r → s")) return "correct"; + break; + case "resolution": + if ( + stmt === "p ∨ s" && + prev.includes("p ∨ r") && + prev.includes("¬r ∨ s") + ) + return "correct"; + break; + } + } + + // Not validated + return "incorrect"; + }; + + const handleStatementDrop = (lineIdx: number, e: React.DragEvent) => { + const data = parseDragData(e); + if (!data) return; + if (data.type === "premise" || data.type === "statement") { + setLineStatement(lineIdx, data.value); + const status = validateLine(lineIdx, { ...lines[lineIdx], statement: data.value }); + if (status === "incorrect") + toast({ title: "Not quite", description: "That statement doesn’t follow from your chosen rule and prior lines.", variant: "destructive" }); + } + }; + + const handleRuleDrop = (lineIdx: number, e: React.DragEvent) => { + const data = parseDragData(e); + if (!data) return; + if (data.type === "rule") { + setLineRule(lineIdx, data.value); + } + }; + + const reset = () => setLines(Array.from({ length: 6 }, () => ({ status: "pending" }))); + + const reachedTarget = lines.some((l) => l.statement === example.conclusion && l.status === "correct"); + + return ( +
+ {/* Top controls */} +
+ +
+ {reachedTarget && ( + Nice! You derived the target. + )} + +
+
+ + {/* Arena */} +
+ {/* Left: Premises */} +
+

Premises

+
+ {example.premises.map((p) => ( + onDragStart(e, "premise", p)} + /> + ))} +
+
+ + {/* Center: Notepad lines with rule margin */} +
+
+
+ {Array.from({ length: 6 }).map((_, i) => { + const line = lines[i]; + const ringCls = + line.status === "correct" + ? "ring-1 ring-primary" + : line.status === "incorrect" + ? "ring-1 ring-destructive" + : ""; + return ( +
+ {/* statement area (left) */} + handleStatementDrop(i, e)} + className={"bg-background min-h-12 flex items-center " + ringCls} + > +
+ {line.statement ? ( + {line.statement} + ) : ( + Drop a statement or premise here + )} +
+
+ + {/* rule margin (right) */} + handleRuleDrop(i, e)}> +
+ {line.rule ? {line.rule} : Drop rule} +
+
+
+ ); + })} +
+ + {/* Target below last line */} +
+ Target conclusion: {example.conclusion} +
+
+
+ + {/* Right: Rules */} +
+

Rules

+
+ {example.rules.map((r) => ( +
+ onDragStart(e, "rule", r.id)} + /> + {r.help && ( +

{r.help}

+ )} +
+ ))} +
+
+
+ + {/* Pool */} +
+

Intermediate statements

+
+ {example.pool.map((s) => ( + onDragStart(e, "statement", s)} /> + ))} +
+
+
+ ); +} diff --git a/src/pages/theme-pages/discrete-math/Foundations.tsx b/src/pages/theme-pages/discrete-math/Foundations.tsx index 1cdf8c0..afadca3 100644 --- a/src/pages/theme-pages/discrete-math/Foundations.tsx +++ b/src/pages/theme-pages/discrete-math/Foundations.tsx @@ -9,6 +9,7 @@ import BalancedTernaryGame from "@/components/BalancedTernaryGame"; import ZeckendorfGame from "@/components/ZeckendorfGame"; import ZeckendorfSearchTrick from "@/components/ZeckendorfSearchTrick"; import KnightsAndKnavesI from "@/components/KnightsAndKnavesI"; +import RulesOfInferencePlayground from "@/components/RulesOfInferencePlayground"; interface Interactive { id: string; title: string; @@ -52,6 +53,13 @@ const interactives: Interactive[] = [{ tags: ["logic", "puzzle", "knights", "knaves", "truth-tellers"], component: KnightsAndKnavesI, greenScreenPath: "/themes/discrete-math/foundations" +}, { + id: "rules-of-inference", + title: "Rules of Inference Playground", + description: "Practice applying resolution and equivalence rules to derive conclusions step by step.", + tags: ["logic", "proofs", "inference", "resolution", "deduction"], + component: RulesOfInferencePlayground, + greenScreenPath: "/themes/discrete-math/foundations" }]; const Foundations = () => { const [searchTerm, setSearchTerm] = useState("");