Skip to content

Commit d81801f

Browse files
committed
feat: NQueens Viasualizer
1 parent c11f79c commit d81801f

File tree

4 files changed

+392
-1
lines changed

4 files changed

+392
-1
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
export function nQueensVisualizerSteps(N) {
2+
const steps = [];
3+
const board = Array.from({ length: N }, () => Array(N).fill(0));
4+
const solutions = [];
5+
const cols = Array(N).fill(false);
6+
const diag1 = Array(2 * N).fill(false);
7+
const diag2 = Array(2 * N).fill(false);
8+
const stack = [];
9+
10+
function cloneBoard(b) {
11+
return b.map(r => [...r]);
12+
}
13+
14+
function solve(row) {
15+
if (row === N) {
16+
steps.push({
17+
type: "solution",
18+
board: cloneBoard(board),
19+
message: `Found valid solution!`,
20+
stack: [...stack],
21+
safe: true,
22+
solutionCount: solutions.length + 1
23+
});
24+
solutions.push(cloneBoard(board));
25+
return;
26+
}
27+
28+
for (let col = 0; col < N; col++) {
29+
steps.push({
30+
type: "try",
31+
board: cloneBoard(board),
32+
row,
33+
col,
34+
message: `Trying to place Queen at (${row}, ${col})`,
35+
safe: null,
36+
stack: [...stack]
37+
});
38+
39+
if (!cols[col] && !diag1[row - col + N] && !diag2[row + col]) {
40+
steps.push({
41+
type: "check",
42+
board: cloneBoard(board),
43+
row,
44+
col,
45+
safe: true,
46+
message: `Position (${row}, ${col}) is safe.`,
47+
stack: [...stack]
48+
});
49+
50+
board[row][col] = 1;
51+
cols[col] = diag1[row - col + N] = diag2[row + col] = true;
52+
stack.push({ row, col });
53+
54+
steps.push({
55+
type: "place",
56+
board: cloneBoard(board),
57+
row,
58+
col,
59+
message: `Placed Queen at (${row}, ${col}). Moving to next row.`,
60+
safe: true,
61+
stack: [...stack]
62+
});
63+
64+
solve(row + 1);
65+
66+
board[row][col] = 0;
67+
cols[col] = diag1[row - col + N] = diag2[row + col] = false;
68+
stack.pop();
69+
70+
steps.push({
71+
type: "remove",
72+
board: cloneBoard(board),
73+
row,
74+
col,
75+
message: `Backtracking: Removed Queen from (${row}, ${col}).`,
76+
safe: false,
77+
stack: [...stack]
78+
});
79+
} else {
80+
steps.push({
81+
type: "check",
82+
board: cloneBoard(board),
83+
row,
84+
col,
85+
safe: false,
86+
message: `Conflict at (${row}, ${col}). Cannot place Queen here.`,
87+
stack: [...stack]
88+
});
89+
}
90+
}
91+
}
92+
93+
solve(0);
94+
95+
return {
96+
steps,
97+
solutions,
98+
solutionCount: solutions.length,
99+
solvable: solutions.length > 0,
100+
};
101+
}
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import React, { useState, useEffect, useRef } from "react";
2+
import { nQueensVisualizerSteps } from "../../algorithms/Recursion/NQueens";
3+
4+
const DEFAULT_N = 5;
5+
const MIN_N = 4;
6+
const MAX_N = 10;
7+
const DEFAULT_SPEED = 500;
8+
9+
function Chessboard({ board, step, N }) {
10+
if (
11+
!board ||
12+
board.length !== N ||
13+
board.some((row) => !row || row.length !== N)
14+
) {
15+
return (
16+
<div className="text-red-400 text-center p-10">
17+
Loading chessboard...
18+
</div>
19+
);
20+
}
21+
const BOARD_SIZE = 440;
22+
const cellSize = BOARD_SIZE / N;
23+
24+
return (
25+
<div
26+
className="grid mx-auto border-[6px] border-gray-700 rounded-xl shadow-lg overflow-hidden bg-gray-800"
27+
style={{
28+
width: BOARD_SIZE,
29+
height: BOARD_SIZE,
30+
gridTemplateRows: `repeat(${N}, ${cellSize}px)`,
31+
gridTemplateColumns: `repeat(${N}, ${cellSize}px)`,
32+
}}
33+
>
34+
{Array.from({ length: N }).map((_, row) =>
35+
Array.from({ length: N }).map((_, col) => {
36+
const cell = board[row][col];
37+
const isQueen = cell === 1;
38+
39+
let color = (row + col) % 2 === 0 ? "bg-gray-100" : "bg-gray-400";
40+
41+
if (step) {
42+
if (step.type === "try" && step.row === row && step.col === col)
43+
color = "bg-blue-300";
44+
else if (step.type === "check" && step.row === row && step.col === col)
45+
color = step.safe === false ? "bg-yellow-300" : "bg-green-300";
46+
else if (step.type === "place" && step.row === row && step.col === col)
47+
color = "bg-green-500";
48+
else if (step.type === "remove" && step.row === row && step.col === col)
49+
color = "bg-gray-500";
50+
else if (step.type === "solution" && board[row][col] === 1)
51+
color = "bg-lime-400";
52+
}
53+
54+
return (
55+
<div
56+
key={`${row}-${col}`}
57+
className={`flex items-center justify-center border border-gray-600 ${color}`}
58+
>
59+
{isQueen && (
60+
<span
61+
className="text-gray-800"
62+
style={{ fontSize: `${cellSize * 0.7}px` }}
63+
>
64+
65+
</span>
66+
)}
67+
</div>
68+
);
69+
})
70+
)}
71+
</div>
72+
);
73+
}
74+
75+
function StackTrace({ stack }) {
76+
return (
77+
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700 mb-3 shadow-sm w-100">
78+
<h3 className="text-indigo-400 font-semibold text-lg mb-2">
79+
Recursion Stack
80+
</h3>
81+
{stack.length === 0 ? (
82+
<div className="text-gray-400 text-sm italic">Stack empty</div>
83+
) : (
84+
<div className="flex flex-wrap gap-2">
85+
{stack.map((pos, idx) => (
86+
<span
87+
key={idx}
88+
className="px-3 py-1 bg-indigo-600 text-white rounded-md text-sm"
89+
>
90+
Row-{pos.row}, Col-{pos.col}
91+
</span>
92+
))}
93+
</div>
94+
)}
95+
</div>
96+
);
97+
}
98+
99+
function Legend({ color, text, symbol }) {
100+
return (
101+
<div className="flex items-center gap-2 mb-1">
102+
<div
103+
className={`w-5 h-5 rounded border border-gray-600 flex items-center justify-center ${color}`}
104+
>
105+
{symbol}
106+
</div>
107+
<span className="text-gray-300 text-sm">{text}</span>
108+
</div>
109+
);
110+
}
111+
112+
export default function NQueensVisualizer() {
113+
const [N, setN] = useState(DEFAULT_N);
114+
const [speed, setSpeed] = useState(DEFAULT_SPEED);
115+
const [steps, setSteps] = useState([]);
116+
const [currentStepIdx, setCurrentStepIdx] = useState(0);
117+
const [isPlaying, setIsPlaying] = useState(false);
118+
const [solutionCount, setSolutionCount] = useState(0);
119+
const [solvable, setSolvable] = useState(null);
120+
const timerRef = useRef(null);
121+
122+
const runSolver = () => {
123+
const result = nQueensVisualizerSteps(N);
124+
setSteps(result.steps);
125+
setSolutionCount(result.solutionCount);
126+
setSolvable(result.solvable);
127+
setCurrentStepIdx(0);
128+
setIsPlaying(false);
129+
};
130+
131+
useEffect(() => {
132+
setSteps([]);
133+
setCurrentStepIdx(0);
134+
setSolvable(null);
135+
setSolutionCount(0);
136+
runSolver();
137+
}, [N]);
138+
139+
useEffect(() => {
140+
if (isPlaying && currentStepIdx < steps.length - 1) {
141+
timerRef.current = setTimeout(() => {
142+
setCurrentStepIdx((idx) => idx + 1);
143+
}, speed);
144+
} else {
145+
clearTimeout(timerRef.current);
146+
setIsPlaying(false);
147+
}
148+
return () => clearTimeout(timerRef.current);
149+
}, [isPlaying, currentStepIdx, steps, speed]);
150+
151+
const step = steps[currentStepIdx] || {};
152+
const board = step.board || Array.from({ length: N }, () => Array(N).fill(0));
153+
154+
return (
155+
<div className="p-6 min-h-screen bg-gray-900 text-gray-100 font-sans">
156+
<div className="max-w-7xl mx-auto">
157+
<h1 className="text-4xl font-bold mb-6 text-center text-indigo-400">
158+
♛ N-Queens Visualizer
159+
</h1>
160+
161+
{/* Controls */}
162+
<div className="flex flex-wrap gap-6 mb-10 p-6 rounded-lg bg-gray-800 border border-gray-700 justify-center items-center">
163+
<div>
164+
<label className="text-gray-300 text-sm">Board Size (N):</label>
165+
<input
166+
type="number"
167+
min={MIN_N}
168+
max={MAX_N}
169+
value={N}
170+
onChange={(e) =>
171+
setN(Math.max(MIN_N, Math.min(MAX_N, Number(e.target.value))))
172+
}
173+
className="w-20 mt-1 p-2 rounded-md bg-gray-700 text-white border border-gray-600"
174+
/>
175+
</div>
176+
177+
<div>
178+
<label className="text-gray-300 text-sm">Speed (ms):</label>
179+
<input
180+
type="number"
181+
min={100}
182+
max={2000}
183+
value={speed}
184+
onChange={(e) =>
185+
setSpeed(Math.max(100, Math.min(2000, Number(e.target.value))))
186+
}
187+
className="w-20 mt-1 p-2 rounded-md bg-gray-700 text-white border border-gray-600"
188+
/>
189+
</div>
190+
191+
<button
192+
onClick={runSolver}
193+
className="bg-indigo-600 hover:bg-indigo-700 text-white font-medium px-6 py-2 rounded-md"
194+
>
195+
Start
196+
</button>
197+
<button
198+
onClick={() => setIsPlaying((p) => !p)}
199+
className={`px-5 py-2 rounded-md font-medium ${
200+
isPlaying ? "bg-red-600" : "bg-green-600"
201+
} text-white`}
202+
>
203+
{isPlaying ? "Pause" : "Play"}
204+
</button>
205+
<button
206+
onClick={() => setCurrentStepIdx((idx) => Math.max(0, idx - 1))}
207+
disabled={currentStepIdx === 0}
208+
className={`px-4 py-2 rounded-md font-medium ${
209+
currentStepIdx > 0
210+
? "bg-gray-700 hover:bg-gray-600 text-white"
211+
: "bg-gray-800 text-gray-500"
212+
}`}
213+
>
214+
Prev
215+
</button>
216+
<button
217+
onClick={() =>
218+
setCurrentStepIdx((idx) => Math.min(steps.length - 1, idx + 1))
219+
}
220+
disabled={currentStepIdx === steps.length - 1}
221+
className={`px-4 py-2 rounded-md font-medium ${
222+
currentStepIdx < steps.length - 1
223+
? "bg-gray-700 hover:bg-gray-600 text-white"
224+
: "bg-gray-800 text-gray-500"
225+
}`}
226+
>
227+
Next
228+
</button>
229+
</div>
230+
231+
{/* Main Layout*/}
232+
<div className="flex flex-col lg:flex-row gap-8 items-start justify-center">
233+
{/*Chessboard*/}
234+
<div className="flex flex-col items-center ml-16">
235+
<Chessboard board={board} step={step} N={N} />
236+
<div className="mt-4 text-center text-gray-300 text-sm">
237+
Step {currentStepIdx + 1}/{steps.length} • Solutions Found:{" "}
238+
<b className="text-green-400">{solutionCount}</b>
239+
{solvable === false && (
240+
<p className="text-red-400 font-medium mt-1">
241+
No solution for N={N}
242+
</p>
243+
)}
244+
</div>
245+
</div>
246+
247+
<div className="flex-1 flex flex-col gap-5 ml-26">
248+
<div className="bg-gray-800 p-4 rounded-lg border border-gray-700 w-100">
249+
<h3 className="text-indigo-400 font-semibold mb-2 text-lg">
250+
Step Explanation
251+
</h3>
252+
<p className="text-gray-200 text-base leading-relaxed">
253+
{step.message || "Start solving N-Queens..."}
254+
</p>
255+
</div>
256+
257+
<StackTrace stack={step.stack || []} />
258+
259+
<div className="bg-gray-800 border border-gray-700 rounded-lg p-4 shadow-sm w-100">
260+
<h3 className="text-indigo-400 font-semibold mb-3 text-lg">
261+
Color Meanings
262+
</h3>
263+
<div className="grid grid-cols-2 gap-2">
264+
<Legend color="bg-blue-300" text="Trying this cell" />
265+
<Legend color="bg-yellow-300" text="Unsafe position" />
266+
<Legend color="bg-green-300" text="Safe position" />
267+
<Legend color="bg-green-500" text="Queen placed" symbol="♛" />
268+
<Legend color="bg-gray-500" text="Queen removed (backtrack)" />
269+
<Legend color="bg-lime-400" text="Final solution cell" symbol="♛" />
270+
</div>
271+
</div>
272+
</div>
273+
</div>
274+
</div>
275+
</div>
276+
);
277+
}

src/pages/Recursion/NQueens.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import React from "react";
2+
import NQueensVisualizer from "../../components/recursion/NQueensVisualizer";
3+
4+
export default function NQueens() {
5+
return <NQueensVisualizer />;
6+
}

0 commit comments

Comments
 (0)