Changes
This commit is contained in:
parent
9ab07e0421
commit
2922027ee7
3 changed files with 462 additions and 0 deletions
|
|
@ -62,6 +62,7 @@ import CubeColoringPage from './pages/CubeColoringPage';
|
|||
import PresentsPuzzlePage from './pages/PresentsPuzzlePage';
|
||||
import FerrersRogersRamanujanPage from './pages/FerrersRogersRamanujanPage';
|
||||
import DominoRetilingPuzzlePage from './pages/DominoRetilingPuzzlePage';
|
||||
import RentDivisionPuzzlePage from './pages/RentDivisionPuzzlePage';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
|
|
@ -93,6 +94,7 @@ const App = () => (
|
|||
<Route path="/themes/discrete-math/uncertainty/presents-puzzle" element={<PresentsPuzzlePage />} />
|
||||
<Route path="/themes/discrete-math/counting/ferrers-rogers-ramanujan" element={<FerrersRogersRamanujanPage />} />
|
||||
<Route path="/themes/social-choice" element={<SocialChoice />} />
|
||||
<Route path="/themes/social-choice/rent-division" element={<RentDivisionPuzzlePage />} />
|
||||
<Route path="/themes/advanced-algorithms" element={<AdvancedAlgorithms />} />
|
||||
<Route path="/themes/data-structures" element={<DataStructures />} />
|
||||
<Route path="/themes/games" element={<Games />} />
|
||||
|
|
|
|||
423
src/components/RentDivisionPuzzle.tsx
Normal file
423
src/components/RentDivisionPuzzle.tsx
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
import { useState, useMemo } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
const AGENTS = ["Alice", "Bob", "Charlie", "Dana"];
|
||||
const ROOM_COLORS = [
|
||||
"hsl(142, 70%, 45%)", // green
|
||||
"hsl(0, 70%, 50%)", // red
|
||||
"hsl(210, 70%, 50%)", // blue
|
||||
"hsl(45, 80%, 50%)", // gold/yellow
|
||||
];
|
||||
|
||||
interface RentDivisionPuzzleProps {
|
||||
onComplete?: (completed: boolean) => void;
|
||||
}
|
||||
|
||||
const RentDivisionPuzzle = ({ onComplete }: RentDivisionPuzzleProps) => {
|
||||
// V-matrix: valuations[agent][room]
|
||||
const [valuations, setValuations] = useState<number[][]>(() =>
|
||||
Array.from({ length: 4 }, () =>
|
||||
Array.from({ length: 4 }, () => Math.floor(Math.random() * 10) + 1)
|
||||
)
|
||||
);
|
||||
|
||||
// Rents for each room
|
||||
const [rents, setRents] = useState<number[]>([5, 5, 5, 5]);
|
||||
|
||||
// Allocation: assignment[agent] = room index (-1 = unassigned)
|
||||
const [assignment, setAssignment] = useState<number[]>([-1, -1, -1, -1]);
|
||||
|
||||
const totalRent = useMemo(() => rents.reduce((a, b) => a + b, 0), [rents]);
|
||||
|
||||
// Compute U-matrix: utility[agent][room] = valuation - rent
|
||||
const utilities = useMemo(() => {
|
||||
return valuations.map((agentVals) =>
|
||||
agentVals.map((v, roomIdx) => v - rents[roomIdx])
|
||||
);
|
||||
}, [valuations, rents]);
|
||||
|
||||
// Find envy relationships
|
||||
const envyInfo = useMemo(() => {
|
||||
const envies: { from: number; to: number; fromUtil: number; toUtil: number }[] = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (assignment[i] === -1) continue;
|
||||
const myRoom = assignment[i];
|
||||
const myUtility = utilities[i][myRoom];
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if (i === j || assignment[j] === -1) continue;
|
||||
const theirRoom = assignment[j];
|
||||
const theirRoomUtility = utilities[i][theirRoom];
|
||||
|
||||
if (theirRoomUtility > myUtility) {
|
||||
envies.push({
|
||||
from: i,
|
||||
to: j,
|
||||
fromUtil: myUtility,
|
||||
toUtil: theirRoomUtility,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return envies;
|
||||
}, [assignment, utilities]);
|
||||
|
||||
const isEnvyFree = envyInfo.length === 0 && assignment.every((a) => a !== -1);
|
||||
const allAssigned = assignment.every((a) => a !== -1);
|
||||
|
||||
const handleValuationChange = (agent: number, room: number, value: string) => {
|
||||
const num = parseInt(value) || 0;
|
||||
setValuations((prev) => {
|
||||
const next = prev.map((row) => [...row]);
|
||||
next[agent][room] = Math.max(0, Math.min(99, num));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleRentChange = (room: number, value: string) => {
|
||||
const num = parseInt(value) || 0;
|
||||
setRents((prev) => {
|
||||
const next = [...prev];
|
||||
next[room] = Math.max(0, Math.min(99, num));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const handleAssignmentChange = (agent: number, room: string) => {
|
||||
const roomIdx = room === "none" ? -1 : parseInt(room);
|
||||
setAssignment((prev) => {
|
||||
const next = [...prev];
|
||||
// If another agent has this room, unassign them
|
||||
if (roomIdx !== -1) {
|
||||
const existing = next.indexOf(roomIdx);
|
||||
if (existing !== -1 && existing !== agent) {
|
||||
next[existing] = -1;
|
||||
}
|
||||
}
|
||||
next[agent] = roomIdx;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const randomizeValuations = () => {
|
||||
setValuations(
|
||||
Array.from({ length: 4 }, () =>
|
||||
Array.from({ length: 4 }, () => Math.floor(Math.random() * 10) + 1)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const resetAll = () => {
|
||||
randomizeValuations();
|
||||
setRents([5, 5, 5, 5]);
|
||||
setAssignment([-1, -1, -1, -1]);
|
||||
};
|
||||
|
||||
const getAgentColor = (agentIdx: number) => {
|
||||
const room = assignment[agentIdx];
|
||||
return room === -1 ? "hsl(0, 0%, 60%)" : ROOM_COLORS[room];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="outline" size="sm" onClick={randomizeValuations}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Randomize Values
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={resetAll}>
|
||||
Reset All
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Agents & Rooms Visual */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">Agents & Rooms</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-8 justify-center">
|
||||
{/* Agents Column */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-muted-foreground text-center mb-2">
|
||||
Agents
|
||||
</div>
|
||||
{AGENTS.map((name, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg border-2 flex items-center justify-center text-xs font-medium"
|
||||
style={{
|
||||
borderColor: getAgentColor(idx),
|
||||
backgroundColor: `${getAgentColor(idx)}20`,
|
||||
}}
|
||||
>
|
||||
{name.slice(0, 2)}
|
||||
</div>
|
||||
<Select
|
||||
value={assignment[idx] === -1 ? "none" : String(assignment[idx])}
|
||||
onValueChange={(v) => handleAssignmentChange(idx, v)}
|
||||
>
|
||||
<SelectTrigger className="w-24 h-8 text-xs">
|
||||
<SelectValue placeholder="Room" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">None</SelectItem>
|
||||
{[0, 1, 2, 3].map((r) => (
|
||||
<SelectItem key={r} value={String(r)}>
|
||||
Room {r + 1}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Rooms Column */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium text-muted-foreground text-center mb-2">
|
||||
Rooms
|
||||
</div>
|
||||
{[0, 1, 2, 3].map((roomIdx) => (
|
||||
<div key={roomIdx} className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg border-2 flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
borderColor: ROOM_COLORS[roomIdx],
|
||||
backgroundColor: `${ROOM_COLORS[roomIdx]}30`,
|
||||
}}
|
||||
>
|
||||
R{roomIdx + 1}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-muted-foreground">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
value={rents[roomIdx]}
|
||||
onChange={(e) => handleRentChange(roomIdx, e.target.value)}
|
||||
className="w-14 h-8 text-xs text-center"
|
||||
min={0}
|
||||
max={99}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="text-sm font-medium pt-2 border-t">
|
||||
Total: ${totalRent}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* V-Matrix (Valuations) */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">
|
||||
V-Matrix <span className="text-sm font-normal text-muted-foreground">(Valuations)</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1"></th>
|
||||
{[1, 2, 3, 4].map((r) => (
|
||||
<th
|
||||
key={r}
|
||||
className="p-1 text-center font-medium"
|
||||
style={{ color: ROOM_COLORS[r - 1] }}
|
||||
>
|
||||
R{r}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{AGENTS.map((name, agentIdx) => (
|
||||
<tr key={agentIdx}>
|
||||
<td
|
||||
className="p-1 font-medium text-xs"
|
||||
style={{ color: getAgentColor(agentIdx) }}
|
||||
>
|
||||
{name}
|
||||
</td>
|
||||
{[0, 1, 2, 3].map((roomIdx) => {
|
||||
const isAssigned = assignment[agentIdx] === roomIdx;
|
||||
return (
|
||||
<td key={roomIdx} className="p-1">
|
||||
<Input
|
||||
type="number"
|
||||
value={valuations[agentIdx][roomIdx]}
|
||||
onChange={(e) =>
|
||||
handleValuationChange(agentIdx, roomIdx, e.target.value)
|
||||
}
|
||||
className={`w-12 h-8 text-xs text-center ${
|
||||
isAssigned ? "ring-2 ring-primary bg-primary/10" : ""
|
||||
}`}
|
||||
min={0}
|
||||
max={99}
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-3">
|
||||
v<sub>ij</sub> = Agent i's value for Room j
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* U-Matrix (Utilities) */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg">
|
||||
U-Matrix <span className="text-sm font-normal text-muted-foreground">(Utilities)</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1"></th>
|
||||
{[1, 2, 3, 4].map((r) => (
|
||||
<th
|
||||
key={r}
|
||||
className="p-1 text-center font-medium"
|
||||
style={{ color: ROOM_COLORS[r - 1] }}
|
||||
>
|
||||
R{r}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{AGENTS.map((name, agentIdx) => (
|
||||
<tr key={agentIdx}>
|
||||
<td
|
||||
className="p-1 font-medium text-xs"
|
||||
style={{ color: getAgentColor(agentIdx) }}
|
||||
>
|
||||
{name}
|
||||
</td>
|
||||
{[0, 1, 2, 3].map((roomIdx) => {
|
||||
const util = utilities[agentIdx][roomIdx];
|
||||
const isAssigned = assignment[agentIdx] === roomIdx;
|
||||
const isEnvied = envyInfo.some(
|
||||
(e) => e.from === agentIdx && assignment[e.to] === roomIdx
|
||||
);
|
||||
return (
|
||||
<td key={roomIdx} className="p-1">
|
||||
<div
|
||||
className={`w-12 h-8 flex items-center justify-center text-xs font-mono rounded border ${
|
||||
isAssigned
|
||||
? "bg-primary/20 border-primary font-bold"
|
||||
: isEnvied
|
||||
? "bg-destructive/20 border-destructive text-destructive"
|
||||
: "bg-muted/50 border-border"
|
||||
}`}
|
||||
>
|
||||
{util}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-3">
|
||||
u<sub>ij</sub> = v<sub>ij</sub> − r<sub>j</sub>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Status / Envy Display */}
|
||||
<Card className={allAssigned ? (isEnvyFree ? "border-green-500/50 bg-green-500/5" : "border-destructive/50 bg-destructive/5") : ""}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg">
|
||||
{!allAssigned
|
||||
? "Assign all agents to rooms"
|
||||
: isEnvyFree
|
||||
? "✓ Envy-Free Allocation!"
|
||||
: "Envy Detected"}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!allAssigned ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Each agent must be assigned to exactly one room.
|
||||
</p>
|
||||
) : isEnvyFree ? (
|
||||
<p className="text-sm text-green-600 dark:text-green-400">
|
||||
No agent prefers another agent's room at the current prices. This is an envy-free allocation!
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{envyInfo.map((envy, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-sm p-2 rounded bg-destructive/10 border border-destructive/20"
|
||||
>
|
||||
<span className="font-medium" style={{ color: getAgentColor(envy.from) }}>
|
||||
{AGENTS[envy.from]}
|
||||
</span>{" "}
|
||||
envies{" "}
|
||||
<span className="font-medium" style={{ color: getAgentColor(envy.to) }}>
|
||||
{AGENTS[envy.to]}
|
||||
</span>
|
||||
: utility {envy.fromUtil} < {envy.toUtil}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Explanation */}
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-lg">How It Works</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
<strong>Problem:</strong> n agents share n rooms and must divide a total rent R.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Goal:</strong> Find an assignment σ and rents r₁,...,rₙ (summing to R) such that
|
||||
no agent envies another—i.e., each agent's utility for their own room is at least as high
|
||||
as for any other room.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Utility:</strong> u<sub>ij</sub> = v<sub>ij</sub> − r<sub>j</sub> (value minus rent).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Envy-Free:</strong> Agent i is envy-free if u<sub>i,σ(i)</sub> ≥ u<sub>ij</sub> for all j.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RentDivisionPuzzle;
|
||||
37
src/pages/RentDivisionPuzzlePage.tsx
Normal file
37
src/pages/RentDivisionPuzzlePage.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { useState } from "react";
|
||||
import RentDivisionPuzzle from "@/components/RentDivisionPuzzle";
|
||||
import SocialShare from "@/components/SocialShare";
|
||||
|
||||
const RentDivisionPuzzlePage = () => {
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-h-screen transition-colors duration-500 p-6 ${
|
||||
isCompleted ? "bg-green-50/50 dark:bg-green-950/20" : "bg-background"
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-6xl mx-auto space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-3xl font-bold text-foreground">
|
||||
Envy-Free Rent Division
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Explore fair division by assigning rooms and setting rents. Can you find an
|
||||
allocation where no one envies another?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<RentDivisionPuzzle onComplete={setIsCompleted} />
|
||||
|
||||
<SocialShare
|
||||
title="Envy-Free Rent Division - Interactive Fair Division"
|
||||
description="Explore fair division by assigning rooms and setting rents to achieve envy-free allocations."
|
||||
url="https://lovable.dev/themes/social-choice/rent-division"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RentDivisionPuzzlePage;
|
||||
Loading…
Add table
Add a link
Reference in a new issue