Add Chessboard Repaint Puzzle - a challenging parity puzzle
This commit is contained in:
parent
d9c0c88333
commit
f8fd9ac4d8
5 changed files with 411 additions and 2 deletions
|
|
@ -33,6 +33,7 @@ import NimGamePage from "./pages/NimGamePage";
|
||||||
import AssistedNimGamePage from "./pages/AssistedNimGamePage";
|
import AssistedNimGamePage from "./pages/AssistedNimGamePage";
|
||||||
import GoldCoinGamePage from "./pages/GoldCoinGamePage";
|
import GoldCoinGamePage from "./pages/GoldCoinGamePage";
|
||||||
import PlateSwapPuzzlePage from "./pages/PlateSwapPuzzlePage";
|
import PlateSwapPuzzlePage from "./pages/PlateSwapPuzzlePage";
|
||||||
|
import ChessboardRepaintPuzzlePage from "./pages/ChessboardRepaintPuzzlePage";
|
||||||
import Foundations from "./pages/theme-pages/discrete-math/Foundations";
|
import Foundations from "./pages/theme-pages/discrete-math/Foundations";
|
||||||
import Proofs from "./pages/theme-pages/discrete-math/Proofs";
|
import Proofs from "./pages/theme-pages/discrete-math/Proofs";
|
||||||
import Counting from "./pages/theme-pages/discrete-math/Counting";
|
import Counting from "./pages/theme-pages/discrete-math/Counting";
|
||||||
|
|
@ -92,6 +93,7 @@ const App = () => (
|
||||||
<Route path="/games/assisted-nim" element={<AssistedNimGamePage />} />
|
<Route path="/games/assisted-nim" element={<AssistedNimGamePage />} />
|
||||||
<Route path="/games/gold-coin" element={<GoldCoinGamePage />} />
|
<Route path="/games/gold-coin" element={<GoldCoinGamePage />} />
|
||||||
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
<Route path="/puzzles/plate-swap" element={<PlateSwapPuzzlePage />} />
|
||||||
|
<Route path="/puzzles/chessboard-repaint" element={<ChessboardRepaintPuzzlePage />} />
|
||||||
|
|
||||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
|
|
|
||||||
376
src/components/ChessboardRepaintPuzzle.tsx
Normal file
376
src/components/ChessboardRepaintPuzzle.tsx
Normal file
|
|
@ -0,0 +1,376 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
|
import { RefreshCw, RotateCcw, BookOpen, Trophy, CheckCircle, ArrowLeft, ExternalLink } from 'lucide-react';
|
||||||
|
import SocialShare from './SocialShare';
|
||||||
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface ChessboardRepaintPuzzleProps {
|
||||||
|
showSocialShare?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChessboardRepaintPuzzle: React.FC<ChessboardRepaintPuzzleProps> = ({ showSocialShare = false }) => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const isFromPuzzles = searchParams.get('from') === 'puzzles';
|
||||||
|
|
||||||
|
// Initialize 8x8 chessboard with alternating colors
|
||||||
|
const initializeBoard = (): boolean[][] => {
|
||||||
|
const board: boolean[][] = [];
|
||||||
|
for (let row = 0; row < 8; row++) {
|
||||||
|
board[row] = [];
|
||||||
|
for (let col = 0; col < 8; col++) {
|
||||||
|
// true = black, false = white
|
||||||
|
board[row][col] = (row + col) % 2 === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return board;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [board, setBoard] = useState<boolean[][]>(initializeBoard);
|
||||||
|
const [moves, setMoves] = useState(0);
|
||||||
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
|
const [showRules, setShowRules] = useState(false);
|
||||||
|
const [showVictory, setShowVictory] = useState(false);
|
||||||
|
|
||||||
|
const resetPuzzle = () => {
|
||||||
|
setBoard(initializeBoard());
|
||||||
|
setMoves(0);
|
||||||
|
setIsComplete(false);
|
||||||
|
setShowVictory(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const countBlackSquares = (board: boolean[][]): number => {
|
||||||
|
return board.flat().filter(square => square).length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const repaintRow = (rowIndex: number) => {
|
||||||
|
const newBoard = board.map(row => [...row]);
|
||||||
|
for (let col = 0; col < 8; col++) {
|
||||||
|
newBoard[rowIndex][col] = !newBoard[rowIndex][col];
|
||||||
|
}
|
||||||
|
setBoard(newBoard);
|
||||||
|
setMoves(moves + 1);
|
||||||
|
|
||||||
|
const blackCount = countBlackSquares(newBoard);
|
||||||
|
if (blackCount === 1) {
|
||||||
|
setIsComplete(true);
|
||||||
|
setShowVictory(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const repaintColumn = (colIndex: number) => {
|
||||||
|
const newBoard = board.map(row => [...row]);
|
||||||
|
for (let row = 0; row < 8; row++) {
|
||||||
|
newBoard[row][colIndex] = !newBoard[row][colIndex];
|
||||||
|
}
|
||||||
|
setBoard(newBoard);
|
||||||
|
setMoves(moves + 1);
|
||||||
|
|
||||||
|
const blackCount = countBlackSquares(newBoard);
|
||||||
|
if (blackCount === 1) {
|
||||||
|
setIsComplete(true);
|
||||||
|
setShowVictory(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const repaint2x2Square = (startRow: number, startCol: number) => {
|
||||||
|
const newBoard = board.map(row => [...row]);
|
||||||
|
for (let row = startRow; row < startRow + 2 && row < 8; row++) {
|
||||||
|
for (let col = startCol; col < startCol + 2 && col < 8; col++) {
|
||||||
|
newBoard[row][col] = !newBoard[row][col];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setBoard(newBoard);
|
||||||
|
setMoves(moves + 1);
|
||||||
|
|
||||||
|
const blackCount = countBlackSquares(newBoard);
|
||||||
|
if (blackCount === 1) {
|
||||||
|
setIsComplete(true);
|
||||||
|
setShowVictory(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSquare = (row: number, col: number) => {
|
||||||
|
const isBlack = board[row][col];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${row}-${col}`}
|
||||||
|
className={`
|
||||||
|
w-12 h-12 border border-gray-300 flex items-center justify-center cursor-pointer
|
||||||
|
transition-all duration-200 hover:scale-105
|
||||||
|
${isBlack ? 'bg-gray-800' : 'bg-gray-100'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const render2x2Button = (startRow: number, startCol: number) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={`2x2-${startRow}-${startCol}`}
|
||||||
|
onClick={() => repaint2x2Square(startRow, startCol)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-6 h-6 p-0 text-xs"
|
||||||
|
disabled={isComplete}
|
||||||
|
>
|
||||||
|
2×2
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||||
|
{/* Back to Puzzles button - only show when coming from puzzles */}
|
||||||
|
{isFromPuzzles && (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/themes/puzzles')}
|
||||||
|
className="text-primary hover:text-primary/80 font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Go back to Puzzles
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="text-center pb-6">
|
||||||
|
<CardTitle className="flex items-center justify-center gap-3 text-3xl">
|
||||||
|
<RefreshCw className="w-8 h-8 text-blue-500" />
|
||||||
|
Chessboard Repaint Puzzle
|
||||||
|
<RefreshCw className="w-8 h-8 text-blue-500" />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-lg max-w-2xl mx-auto">
|
||||||
|
Repaint rows, columns, or 2×2 squares to achieve exactly one black square!
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-8">
|
||||||
|
{/* Game Controls */}
|
||||||
|
<div className="flex justify-center items-center gap-4">
|
||||||
|
<Button onClick={() => setShowRules(true)} variant="outline" size="lg" className="px-8">
|
||||||
|
<BookOpen className="w-4 h-4 mr-2" />
|
||||||
|
Rules
|
||||||
|
</Button>
|
||||||
|
<Button onClick={resetPuzzle} variant="outline" size="lg" className="px-8">
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
Reset Puzzle
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold text-lg">Moves:</span>
|
||||||
|
<Badge className="text-lg px-4 py-2 bg-blue-600 text-white">
|
||||||
|
{moves}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-semibold text-lg">Black Squares:</span>
|
||||||
|
<Badge className={`text-lg px-4 py-2 ${countBlackSquares(board) === 1 ? 'bg-green-600' : 'bg-gray-600'} text-white`}>
|
||||||
|
{countBlackSquares(board)}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chessboard */}
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="relative">
|
||||||
|
{/* Row buttons */}
|
||||||
|
<div className="absolute -left-16 top-0 h-full flex flex-col justify-center gap-1">
|
||||||
|
{Array.from({ length: 8 }, (_, i) => (
|
||||||
|
<Button
|
||||||
|
key={`row-${i}`}
|
||||||
|
onClick={() => repaintRow(i)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-12 h-6 text-xs"
|
||||||
|
disabled={isComplete}
|
||||||
|
>
|
||||||
|
Row {i + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column buttons */}
|
||||||
|
<div className="absolute -top-16 left-0 w-full flex justify-center gap-1">
|
||||||
|
{Array.from({ length: 8 }, (_, i) => (
|
||||||
|
<Button
|
||||||
|
key={`col-${i}`}
|
||||||
|
onClick={() => repaintColumn(i)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-12 h-6 text-xs"
|
||||||
|
disabled={isComplete}
|
||||||
|
>
|
||||||
|
Col {i + 1}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2x2 square buttons */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
{Array.from({ length: 7 }, (_, row) =>
|
||||||
|
Array.from({ length: 7 }, (_, col) => (
|
||||||
|
<div
|
||||||
|
key={`2x2-${row}-${col}`}
|
||||||
|
className="absolute pointer-events-auto"
|
||||||
|
style={{
|
||||||
|
left: `${col * 48 + 24}px`,
|
||||||
|
top: `${row * 48 + 24}px`,
|
||||||
|
transform: 'translate(-50%, -50%)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{render2x2Button(row, col)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chessboard grid */}
|
||||||
|
<div className="grid grid-cols-8 gap-0 border-2 border-gray-400">
|
||||||
|
{board.map((row, rowIndex) =>
|
||||||
|
row.map((square, colIndex) => renderSquare(rowIndex, colIndex))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Instructions */}
|
||||||
|
<div className="text-center mt-8">
|
||||||
|
<div className="inline-block bg-gray-50 px-6 py-3 rounded-lg border">
|
||||||
|
<p className="text-sm text-gray-700 font-medium">
|
||||||
|
{isComplete ? (
|
||||||
|
"🎉 Congratulations! You achieved exactly one black square!"
|
||||||
|
) : (
|
||||||
|
"Click row/column buttons or 2×2 square buttons to repaint. Goal: exactly one black square!"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="space-y-3 mt-8">
|
||||||
|
<h3 className="font-semibold text-lg text-center">Legend</h3>
|
||||||
|
<div className="flex flex-wrap justify-center gap-6 text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-5 h-5 bg-gray-100 border border-gray-300"></div>
|
||||||
|
<span>White square</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-5 h-5 bg-gray-800 border border-gray-300"></div>
|
||||||
|
<span>Black square</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" className="w-6 h-6 p-0 text-xs">2×2</Button>
|
||||||
|
<span>Repaint 2×2 square</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Rules Dialog */}
|
||||||
|
<Dialog open={showRules} onOpenChange={setShowRules}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<BookOpen className="w-5 h-5" />
|
||||||
|
Chessboard Repaint Puzzle Rules
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-6 text-sm">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2 text-lg">Objective</h4>
|
||||||
|
<p className="text-gray-700">Repaint the chessboard to achieve exactly one black square.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2 text-lg">Initial Setup</h4>
|
||||||
|
<p className="text-gray-700">You start with a standard 8×8 chessboard with alternating black and white squares.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2 text-lg">How to Play</h4>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 ml-4 text-gray-700">
|
||||||
|
<li><strong>Repaint a row:</strong> Click any "Row X" button to flip all squares in that row (black becomes white, white becomes black).</li>
|
||||||
|
<li><strong>Repaint a column:</strong> Click any "Col X" button to flip all squares in that column.</li>
|
||||||
|
<li><strong>Repaint a 2×2 square:</strong> Click any "2×2" button to flip all four squares in that 2×2 region.</li>
|
||||||
|
<li><strong>Goal:</strong> Achieve exactly one black square on the entire board.</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2 text-lg">Rules</h4>
|
||||||
|
<ul className="list-disc list-inside space-y-2 ml-4 text-gray-700">
|
||||||
|
<li>You can repaint any row, any column, or any 2×2 square</li>
|
||||||
|
<li>Each click counts as one move</li>
|
||||||
|
<li>The puzzle is solved when exactly one square is black</li>
|
||||||
|
<li>Try to solve it in as few moves as possible!</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2 text-lg">Strategy Tips</h4>
|
||||||
|
<ul className="list-disc list-inside space-y-2 ml-4 text-gray-700">
|
||||||
|
<li>Think about parity - how do different operations affect the total number of black squares?</li>
|
||||||
|
<li>Consider the effect of each operation on the overall pattern</li>
|
||||||
|
<li>Remember that flipping a row or column affects 8 squares, while flipping a 2×2 affects 4 squares</li>
|
||||||
|
<li>Look for patterns in how the operations interact with each other</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Victory Dialog */}
|
||||||
|
<Dialog open={showVictory} onOpenChange={setShowVictory}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center justify-center gap-2">
|
||||||
|
<Trophy className="w-6 h-6 text-yellow-500" />
|
||||||
|
Puzzle Solved!
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-center space-y-6">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<CheckCircle className="w-10 h-10 text-green-500" />
|
||||||
|
<span className="text-2xl font-bold text-green-600">
|
||||||
|
Congratulations!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 text-lg">
|
||||||
|
You solved the puzzle in {moves} moves!
|
||||||
|
</p>
|
||||||
|
{moves > 5 && (
|
||||||
|
<p className="text-blue-600 font-medium">
|
||||||
|
Can you solve it in fewer moves?
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Button onClick={resetPuzzle} className="w-full" size="lg">
|
||||||
|
Play Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Attribution */}
|
||||||
|
<div className="pt-4 border-t border-outline">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
This puzzle explores the fascinating concept of parity and how different operations affect the overall state of a system.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social Share Section */}
|
||||||
|
{showSocialShare && (
|
||||||
|
<SocialShare
|
||||||
|
title="Chessboard Repaint Puzzle"
|
||||||
|
description="Can you repaint a chessboard to achieve exactly one black square? A challenging puzzle about parity and operations!"
|
||||||
|
url={window.location.href}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChessboardRepaintPuzzle;
|
||||||
|
|
@ -91,6 +91,15 @@ const allInteractives: Interactive[] = [
|
||||||
theme: 'Puzzles',
|
theme: 'Puzzles',
|
||||||
dateAdded: '2024-12-19'
|
dateAdded: '2024-12-19'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'chessboard-repaint',
|
||||||
|
title: 'Chessboard Repaint Puzzle',
|
||||||
|
description: 'Repaint rows, columns, or 2×2 squares to achieve exactly one black square. A puzzle about parity and operations!',
|
||||||
|
tags: ['logic', 'parity', 'strategy', 'puzzle', 'chessboard', 'operations'],
|
||||||
|
path: '/puzzles/chessboard-repaint',
|
||||||
|
theme: 'Puzzles',
|
||||||
|
dateAdded: '2024-12-19'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'knights-puzzle',
|
id: 'knights-puzzle',
|
||||||
title: 'Knights Exchange Puzzle',
|
title: 'Knights Exchange Puzzle',
|
||||||
|
|
|
||||||
12
src/pages/ChessboardRepaintPuzzlePage.tsx
Normal file
12
src/pages/ChessboardRepaintPuzzlePage.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ChessboardRepaintPuzzle from '@/components/ChessboardRepaintPuzzle';
|
||||||
|
|
||||||
|
const ChessboardRepaintPuzzlePage = () => {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background">
|
||||||
|
<ChessboardRepaintPuzzle showSocialShare={true} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChessboardRepaintPuzzlePage;
|
||||||
|
|
@ -16,6 +16,16 @@ const Puzzles = () => {
|
||||||
duration: "10-30 min",
|
duration: "10-30 min",
|
||||||
participants: "1 player"
|
participants: "1 player"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "chessboard-repaint",
|
||||||
|
title: "Chessboard Repaint Puzzle",
|
||||||
|
description: "Repaint rows, columns, or 2×2 squares to achieve exactly one black square. A puzzle about parity and operations!",
|
||||||
|
path: "/puzzles/chessboard-repaint",
|
||||||
|
tags: ["Logic", "Parity", "Operations"],
|
||||||
|
difficulty: "Advanced" as const,
|
||||||
|
duration: "15-45 min",
|
||||||
|
participants: "1 player"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "knights-puzzle",
|
id: "knights-puzzle",
|
||||||
title: "Knights Exchange Puzzle",
|
title: "Knights Exchange Puzzle",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue