import { useEffect, useMemo, useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { cn } from "@/lib/utils"; // Statements const STATEMENTS = [ { id: "S", label: "At least one of us is a knave." }, { id: "SNEG", label: "Both of us are knights." }, { id: "AK", label: "A is a knight" }, { id: "AN", label: "A is a knave" }, { id: "BK", label: "B is a knight" }, { id: "BN", label: "B is a knave" }, ] as const; type StatementId = typeof STATEMENTS[number]["id"]; const META: Record = { S: { group: "S", neg: false }, SNEG: { group: "S", neg: true }, AK: { group: "A", neg: false }, AN: { group: "A", neg: true }, BK: { group: "B", neg: false }, BN: { group: "B", neg: true }, }; const KnightsAndKnavesI = () => { const [inArena, setInArena] = useState([]); const [showExplain, setShowExplain] = useState(false); // SEO basics useEffect(() => { const title = "Knights and Knaves – I (Logic Puzzle)"; document.title = title; const desc = "Drag statements into the arena to test consistency in a classic Knights and Knaves puzzle."; let meta = document.querySelector('meta[name="description"]'); if (!meta) { meta = document.createElement("meta"); meta.setAttribute("name", "description"); document.head.appendChild(meta); } meta.setAttribute("content", desc); // canonical let link = document.querySelector('link[rel="canonical"]'); if (!link) { link = document.createElement("link"); link.setAttribute("rel", "canonical"); document.head.appendChild(link); } link.setAttribute("href", window.location.href); }, []); const available = useMemo( () => STATEMENTS.filter(s => !inArena.includes(s.id)), [inArena] ); function onDragStart(e: React.DragEvent, id: StatementId) { e.dataTransfer.setData("text/plain", id); e.dataTransfer.effectAllowed = "move"; } function onArenaDrop(e: React.DragEvent) { e.preventDefault(); const id = e.dataTransfer.getData("text/plain") as StatementId; if (id && !inArena.includes(id)) setInArena(prev => [...prev, id]); } function onArenaDragOver(e: React.DragEvent) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; } function removeFromArena(id: StatementId) { setInArena(prev => prev.filter(x => x !== id)); } function reset() { setInArena([]); } // Consistency checker via brute force over assignments (with A's utterance semantics) const consistentWith = (idsArr: StatementId[]) => { const ids = new Set(idsArr); for (const aKnight of [true, false]) { for (const bKnight of [true, false]) { // role constraints if (ids.has("AK") && !aKnight) continue; if (ids.has("AN") && aKnight) continue; if (ids.has("BK") && !bKnight) continue; if (ids.has("BN") && bKnight) continue; if (ids.has("SNEG") && !(aKnight && bKnight)) continue; // Content of A's statement: S = "At least one of us is a knave." const Scontent = (!aKnight) || (!bKnight); // Implicit linkage: if A is declared knight/knave, his statement's content must match if (ids.has("AK") && !Scontent) continue; if (ids.has("AN") && Scontent) continue; // If token S is explicitly included, also enforce truthfulness parity if (ids.has("S")) { if (aKnight !== Scontent) continue; } return true; } } return idsArr.length <= 1; }; const isConsistent = useMemo(() => consistentWith(inArena), [inArena]); const arenaClasses = cn( "relative rounded-lg border transition-colors min-h-[320px] grid place-items-center p-6", isConsistent ? "bg-surface border-outline" : "bg-destructive/10 border-destructive" ); // Partition remaining statements into compatible vs incompatible under current worldview const partitions = useMemo(() => { const remaining = STATEMENTS.filter(s => !inArena.includes(s.id)); const incompatible = remaining .filter(s => !consistentWith([...inArena, s.id])) .map(s => s.id); const compatible = remaining .filter(s => !incompatible.includes(s.id)) .map(s => s.id); return { compatible, incompatible }; }, [inArena]); function generateExplanation(idsArr: StatementId[]): string[] { const ids = new Set(idsArr); const lines: string[] = []; // Direct role contradictions if (ids.has("AK") && ids.has("AN")) lines.push("A cannot be both a knight and a knave."); if (ids.has("BK") && ids.has("BN")) lines.push("B cannot be both a knight and a knave."); // A's utterance linkage if (ids.has("S")) { const forcedA = ids.has("AK") ? true : ids.has("AN") ? false : null; const forcedB = ids.has("BK") ? true : ids.has("BN") ? false : null; if (forcedA === true && forcedB === true) { lines.push("If A and B are both knights, then A’s statement “at least one of us is a knave” is false; but a knight cannot assert a falsehood."); } if (forcedA === false) { lines.push("If A is a knave, his statement must be false. “At least one of us is a knave” being false means neither is a knave (both are knights), contradicting “A is a knave” and any claim that B is a knave."); } if (forcedB === true && forcedA === null) { lines.push("B is a knight. If A were a knight, his statement would be false; if A were a knave, his statement would be true. Either way, A’s truthfulness would not match his statement, so no assignment works."); } // Enumerate remaining candidate assignments for clarity if (lines.length === 0) { const aVals = forcedA === null ? [true, false] : [forcedA]; const bVals = forcedB === null ? [true, false] : [forcedB]; for (const aKnight of aVals) { for (const bKnight of bVals) { const content = (!aKnight) || (!bKnight); const ok = (aKnight === content); if (!ok) { const contentTruth = content ? "true" : "false"; lines.push(`If A is ${aKnight ? "a knight" : "a knave"} and B is ${bKnight ? "a knight" : "a knave"}, then the content “at least one of us is a knave” is ${contentTruth}, which a ${aKnight ? "knight must tell" : "knave must not tell"}.`); } } } } } if (lines.length === 0) lines.push("The selected set cannot be satisfied by any assignment of A and B."); return lines; } const explanation = useMemo(() => (isConsistent ? [] : generateExplanation(inArena)), [inArena, isConsistent]); useEffect(() => { if (isConsistent) setShowExplain(false); }, [isConsistent]); return (

Knights and Knaves – I

Knights always tell the truth, knaves always lie. A says: “At least one of us is a knave.” Drag statements into the arena to test whether they can all be true together.

{/* Available statements */} Potential Truths {partitions.compatible.map(id => { const s = STATEMENTS.find(x => x.id === id)!; const meta = META[s.id]; const groupClass = meta.group === "A" ? "logic-a" : meta.group === "B" ? "logic-b" : "logic-s"; const styleClass = meta.neg ? "logic-dotted" : "logic-solid"; const classes = cn( "cursor-move select-none rounded-md bg-surface-variant px-3 py-2 text-sm text-foreground shadow-card border-2 logic-border", groupClass, styleClass, "ring-1 ring-success/30" ); return (
onDragStart(e, s.id)} className={classes} aria-label={`Drag: ${s.label}`} role="button" title="Compatible with current worldview" > {s.label}
); })}
{/* Arena */}

{isConsistent ? "Consistent" : "Incompatible set"}

{!isConsistent && ( )}
{inArena.length === 0 ? (

Drag any statement here. Tokens turn green in the arena. If two statements clash, the arena turns red.

) : (
{inArena.map(id => { const s = STATEMENTS.find(x => x.id === id)!; return (
removeFromArena(id)} role="button" tabIndex={0} onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && removeFromArena(id)} > {s.label} ×
); })}
)}
{showExplain && !isConsistent && (

Why inconsistent?

    {explanation.map((line, i) => (
  • {line}
  • ))}
)} {partitions.incompatible.length > 0 && ( Incompatible with current worldview {partitions.incompatible.map(id => { const s = STATEMENTS.find(x => x.id === id)!; const meta = META[id]; const groupClass = meta.group === "A" ? "logic-a" : meta.group === "B" ? "logic-b" : "logic-s"; const styleClass = meta.neg ? "logic-dotted" : "logic-solid"; const classes = cn( "select-none rounded-md bg-surface-variant px-3 py-2 text-sm text-foreground shadow-card border-2 logic-border", groupClass, styleClass, "opacity-60 cursor-not-allowed bg-destructive/10 ring-1 ring-destructive/40" ); return (
{s.label}
); })}
)}
); }; export default KnightsAndKnavesI;