diff --git a/src/components/KnightsAndKnavesI.tsx b/src/components/KnightsAndKnavesI.tsx index a612f63..e2ca97d 100644 --- a/src/components/KnightsAndKnavesI.tsx +++ b/src/components/KnightsAndKnavesI.tsx @@ -78,39 +78,55 @@ const KnightsAndKnavesI = () => { setInArena([]); } - // Consistency checker via brute force over assignments - const isConsistent = useMemo(() => { - // Try all assignments for A, B - const ids = new Set(inArena); - + // 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 from dragged tokens + // 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; - // A's utterance if included: S = "At least one is a knave" + // 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")) { - const S = (!aKnight) || (!bKnight); - // Knights tell truth, knaves lie - if (aKnight !== S) continue; + if (aKnight !== Scontent) continue; } - // If we got here, this assignment fits all dragged statements return true; } } - return inArena.length <= 1; // single or empty always treated as consistent for UX - }, [inArena]); + 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[] = []; @@ -173,18 +189,19 @@ const KnightsAndKnavesI = () => { {/* Available statements */} - Statements + Potential Truths - {available.map(s => { + {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", + "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 + styleClass, + "ring-1 ring-success/30" ); return (
{ className={classes} aria-label={`Drag: ${s.label}`} role="button" + title="Compatible with current worldview" > {s.label}
@@ -267,6 +285,37 @@ const KnightsAndKnavesI = () => { )} + {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} +
+ ); + })} +
+
+ )}