Add explanation for inconsistency
Add an "Explain" button next to "Reset" in the Knights and Knaves puzzle. When clicked, this button displays an explanation for any inconsistencies found in the arena below it.
This commit is contained in:
parent
954110c6a2
commit
44dd1497f6
1 changed files with 71 additions and 5 deletions
|
|
@ -15,7 +15,7 @@ type StatementId = typeof STATEMENTS[number]["id"];
|
||||||
|
|
||||||
const KnightsAndKnavesI = () => {
|
const KnightsAndKnavesI = () => {
|
||||||
const [inArena, setInArena] = useState<StatementId[]>([]);
|
const [inArena, setInArena] = useState<StatementId[]>([]);
|
||||||
|
const [showExplain, setShowExplain] = useState(false);
|
||||||
// SEO basics
|
// SEO basics
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const title = "Knights and Knaves – I (Logic Puzzle)";
|
const title = "Knights and Knaves – I (Logic Puzzle)";
|
||||||
|
|
@ -100,6 +100,54 @@ const KnightsAndKnavesI = () => {
|
||||||
isConsistent ? "bg-surface border-outline" : "bg-destructive/10 border-destructive"
|
isConsistent ? "bg-surface border-outline" : "bg-destructive/10 border-destructive"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<header className="space-y-2">
|
<header className="space-y-2">
|
||||||
|
|
@ -145,11 +193,19 @@ const KnightsAndKnavesI = () => {
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<p className={cn("text-sm font-medium", isConsistent ? "text-success" : "text-destructive")}
|
<p className={cn("text-sm font-medium", isConsistent ? "text-success" : "text-destructive")}
|
||||||
>{isConsistent ? "Consistent" : "Incompatible set"}</p>
|
>{isConsistent ? "Consistent" : "Incompatible set"}</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{!isConsistent && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowExplain((v) => !v)}
|
||||||
|
className="text-destructive hover:text-destructive/80 text-sm"
|
||||||
|
>Explain</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={reset}
|
onClick={reset}
|
||||||
className="text-muted-foreground hover:text-foreground text-sm"
|
className="text-muted-foreground hover:text-foreground text-sm"
|
||||||
>Reset</button>
|
>Reset</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{inArena.length === 0 ? (
|
{inArena.length === 0 ? (
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
|
|
@ -179,6 +235,16 @@ const KnightsAndKnavesI = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showExplain && !isConsistent && (
|
||||||
|
<div className="mt-4 rounded-md border border-outline bg-surface p-4">
|
||||||
|
<h2 className="text-sm font-semibold mb-2">Why inconsistent?</h2>
|
||||||
|
<ul className="list-disc pl-5 space-y-1 text-sm text-foreground">
|
||||||
|
{explanation.map((line, i) => (
|
||||||
|
<li key={i}>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue