Skip to content

Commit e8e710d

Browse files
committed
puzzle-15
1 parent 1ece2b7 commit e8e710d

File tree

2 files changed

+183
-0
lines changed

2 files changed

+183
-0
lines changed

public/static/duck.jpg

98.7 KB
Loading

ui/games/Puzzle15.tsx

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { Grid, Box, Flex, Button } from '@chakra-ui/react';
2+
import { motion } from 'framer-motion';
3+
import React, { useState, useEffect, useCallback, useRef } from 'react';
4+
5+
const getPossibleMoves = (emptyIndex: number): Array<number> => {
6+
7+
const moves: Array<number> = [];
8+
const row = Math.floor(emptyIndex / 4);
9+
const col = emptyIndex % 4;
10+
11+
if (row > 0) {
12+
// Move tile from above into the empty space
13+
moves.push((row - 1) * 4 + col);
14+
}
15+
if (row < 3) {
16+
// Move tile from below into the empty space
17+
moves.push((row + 1) * 4 + col);
18+
}
19+
if (col > 0) {
20+
// Move tile from the left into the empty space
21+
moves.push(row * 4 + (col - 1));
22+
}
23+
if (col < 3) {
24+
// Move tile from the right into the empty space
25+
moves.push(row * 4 + (col + 1));
26+
}
27+
28+
return moves;
29+
};
30+
31+
const shuffleBoard = (initialBoard: Array<number>): Array<number> => {
32+
const board = initialBoard.slice(); // Create a copy of the board
33+
let emptyIndex = board.indexOf(15);
34+
let lastMoveIndex = -1;
35+
36+
for (let i = 0; i < 100; i++) {
37+
let possibleMoves = getPossibleMoves(emptyIndex);
38+
39+
// Prevent immediate reversal of the last move
40+
if (lastMoveIndex !== -1) {
41+
possibleMoves = possibleMoves.filter(index => index !== lastMoveIndex);
42+
}
43+
44+
// Randomly select a tile to move into the empty space
45+
const moveIndex = possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
46+
47+
// Swap the selected tile with the empty space
48+
[ board[emptyIndex], board[moveIndex] ] = [ board[moveIndex], board[emptyIndex] ];
49+
50+
// Update indices for the next iteration
51+
lastMoveIndex = emptyIndex;
52+
emptyIndex = moveIndex;
53+
}
54+
55+
return board;
56+
};
57+
58+
const Puzzle15 = () => {
59+
const [ tiles, setTiles ] = useState<Array<number>>(Array.from({ length: 16 }, (_, i) => i));
60+
const [ isWon, setIsWon ] = useState(false);
61+
const [ image, setImage ] = useState<HTMLImageElement | null>(null);
62+
const canvasRefs = useRef<Array<(HTMLCanvasElement | null)>>([]);
63+
64+
const initializeGame = useCallback(() => {
65+
const newTiles = shuffleBoard(tiles);
66+
setTiles(newTiles);
67+
setIsWon(false);
68+
// eslint-disable-next-line react-hooks/exhaustive-deps
69+
}, []);
70+
71+
useEffect(() => {
72+
initializeGame();
73+
}, [ initializeGame ]);
74+
75+
useEffect(() => {
76+
const img = new Image();
77+
img.src = '/static/duck.jpg';
78+
img.onload = () => setImage(img);
79+
}, []);
80+
81+
useEffect(() => {
82+
if (image) {
83+
tiles.forEach((tile, index) => {
84+
const canvas = canvasRefs.current[index];
85+
if (canvas) {
86+
const ctx = canvas.getContext('2d');
87+
if (ctx) {
88+
const tileSize = image.width / 4;
89+
const srcX = (tile % 4) * tileSize;
90+
const srcY = Math.floor(tile / 4) * tileSize;
91+
ctx.drawImage(
92+
image,
93+
srcX,
94+
srcY,
95+
tileSize,
96+
tileSize,
97+
0,
98+
0,
99+
canvas.width,
100+
canvas.height,
101+
);
102+
}
103+
}
104+
});
105+
}
106+
}, [ tiles, image ]);
107+
108+
const isAdjacent = React.useCallback((index1: number, index2: number) => {
109+
const row1 = Math.floor(index1 / 4);
110+
const col1 = index1 % 4;
111+
const row2 = Math.floor(index2 / 4);
112+
const col2 = index2 % 4;
113+
return Math.abs(row1 - row2) + Math.abs(col1 - col2) === 1;
114+
}, []);
115+
116+
const checkWinCondition = useCallback((currentTiles: Array<number>) => {
117+
setIsWon(currentTiles.every((tile, index) => tile === index));
118+
}, []);
119+
120+
const moveTile = useCallback((index: number) => {
121+
const emptyIndex = tiles.indexOf(15);
122+
if (isAdjacent(index, emptyIndex)) {
123+
const newTiles = [ ...tiles ];
124+
[ newTiles[index], newTiles[emptyIndex] ] = [ newTiles[emptyIndex], newTiles[index] ];
125+
setTiles(newTiles);
126+
checkWinCondition(newTiles);
127+
}
128+
}, [ tiles, isAdjacent, checkWinCondition ]);
129+
130+
const handleTileClick = useCallback((index: number) => () => {
131+
if (!isWon) {
132+
moveTile(index);
133+
}
134+
}, [ isWon, moveTile ]);
135+
136+
return (
137+
<Flex flexDirection="column" alignItems="center" justifyContent="center">
138+
<Grid templateColumns="repeat(4, 1fr)" w="400px" h="400px">
139+
{ tiles.map((tile, index) => (
140+
<motion.div
141+
key={ tile }
142+
layout
143+
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
144+
onClick={ handleTileClick(index) }
145+
>
146+
<Box position="relative">
147+
<canvas
148+
ref={ (el) => (canvasRefs.current[index] = el) }
149+
width="100"
150+
height="100"
151+
style={{
152+
display: tile !== 15 ? 'block' : 'none',
153+
border: '1px solid gray',
154+
}}
155+
/>
156+
<Box
157+
position="absolute"
158+
top="0"
159+
left="0"
160+
right="0"
161+
bottom="0"
162+
display="flex"
163+
alignItems="center"
164+
justifyContent="center"
165+
fontSize="3xl"
166+
fontWeight="bold"
167+
color="white"
168+
opacity="0"
169+
_hover={{ opacity: 0.3 }}
170+
transition="opacity 0.2s"
171+
>
172+
{ tile !== 15 && tile + 1 }
173+
</Box>
174+
</Box>
175+
</motion.div>
176+
)) }
177+
</Grid>
178+
{ isWon && <Button mt={ 10 }>Claim NFT</Button> }
179+
</Flex>
180+
);
181+
};
182+
183+
export default Puzzle15;

0 commit comments

Comments
 (0)