Refactor interactive elements

Change "Statements" to "Potential Truths". Add a new zone below the arena called "Incompatible with current worldview". When a statement conflicts with the current setup, it will be moved to this zone, and it will not be possible to drag statements from this zone.
This commit is contained in:
gpt-engineer-app[bot] 2025-08-08 07:29:51 +00:00
parent 4b1940859a
commit 81f6873c4d

View file

@ -78,39 +78,55 @@ const KnightsAndKnavesI = () => {
setInArena([]); setInArena([]);
} }
// Consistency checker via brute force over assignments // Consistency checker via brute force over assignments (with A's utterance semantics)
const isConsistent = useMemo(() => { const consistentWith = (idsArr: StatementId[]) => {
// Try all assignments for A, B const ids = new Set(idsArr);
const ids = new Set(inArena);
for (const aKnight of [true, false]) { for (const aKnight of [true, false]) {
for (const bKnight 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("AK") && !aKnight) continue;
if (ids.has("AN") && aKnight) continue; if (ids.has("AN") && aKnight) continue;
if (ids.has("BK") && !bKnight) continue; if (ids.has("BK") && !bKnight) continue;
if (ids.has("BN") && bKnight) continue; if (ids.has("BN") && bKnight) continue;
if (ids.has("SNEG") && !(aKnight && 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")) { if (ids.has("S")) {
const S = (!aKnight) || (!bKnight); if (aKnight !== Scontent) continue;
// Knights tell truth, knaves lie
if (aKnight !== S) continue;
} }
// If we got here, this assignment fits all dragged statements
return true; return true;
} }
} }
return inArena.length <= 1; // single or empty always treated as consistent for UX return idsArr.length <= 1;
}, [inArena]); };
const isConsistent = useMemo(() => consistentWith(inArena), [inArena]);
const arenaClasses = cn( const arenaClasses = cn(
"relative rounded-lg border transition-colors min-h-[320px] grid place-items-center p-6", "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" 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[] { function generateExplanation(idsArr: StatementId[]): string[] {
const ids = new Set(idsArr); const ids = new Set(idsArr);
const lines: string[] = []; const lines: string[] = [];
@ -173,18 +189,19 @@ const KnightsAndKnavesI = () => {
{/* Available statements */} {/* Available statements */}
<Card className="lg:col-span-1"> <Card className="lg:col-span-1">
<CardHeader> <CardHeader>
<CardTitle className="text-xl">Statements</CardTitle> <CardTitle className="text-xl">Potential Truths</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-wrap gap-3"> <CardContent className="flex flex-wrap gap-3">
{available.map(s => { {partitions.compatible.map(id => {
const s = STATEMENTS.find(x => x.id === id)!;
const meta = META[s.id]; const meta = META[s.id];
const groupClass = meta.group === "A" ? "logic-a" : meta.group === "B" ? "logic-b" : "logic-s"; const groupClass = meta.group === "A" ? "logic-a" : meta.group === "B" ? "logic-b" : "logic-s";
const styleClass = meta.neg ? "logic-dotted" : "logic-solid"; const styleClass = meta.neg ? "logic-dotted" : "logic-solid";
const classes = cn( const classes = cn(
"cursor-move select-none rounded-md bg-surface-variant px-3 py-2 text-sm text-foreground shadow-card border-2", "cursor-move select-none rounded-md bg-surface-variant px-3 py-2 text-sm text-foreground shadow-card border-2 logic-border",
"logic-border",
groupClass, groupClass,
styleClass styleClass,
"ring-1 ring-success/30"
); );
return ( return (
<div <div
@ -194,6 +211,7 @@ const KnightsAndKnavesI = () => {
className={classes} className={classes}
aria-label={`Drag: ${s.label}`} aria-label={`Drag: ${s.label}`}
role="button" role="button"
title="Compatible with current worldview"
> >
{s.label} {s.label}
</div> </div>
@ -267,6 +285,37 @@ const KnightsAndKnavesI = () => {
</ul> </ul>
</div> </div>
)} )}
{partitions.incompatible.length > 0 && (
<Card className="mt-4">
<CardHeader>
<CardTitle className="text-xl">Incompatible with current worldview</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-3">
{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 (
<div
key={id}
className={classes}
aria-disabled="true"
title="Conflicts with current worldview"
>
{s.label}
</div>
);
})}
</CardContent>
</Card>
)}
</div> </div>
</div> </div>
</section> </section>