refactor; new mole; stable
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
1d765cf63964d7c81a1c97ab8a34e7ceee2c44c3- Parents
-
2edb0eb - Tree
fc662f5
1d765cf
1d765cf63964d7c81a1c97ab8a34e7ceee2c44c32edb0eb
fc662f5| Status | File | + | - |
|---|---|---|---|
| M |
frontend/public/mole.svg
|
8 | 11 |
| M |
frontend/src/components/Game.tsx
|
23 | 9 |
| M |
frontend/src/components/TreeVisualizer.tsx
|
137 | 74 |
frontend/public/mole.svgmodified@@ -1,11 +1,8 @@ | ||
| 1 | -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 2 | - <ellipse cx="12" cy="14" rx="6" ry="5" fill="#8B4513"/> | |
| 3 | - <circle cx="12" cy="13" r="4" fill="#D2691E"/> | |
| 4 | - <circle cx="10" cy="12" r="1" fill="#000000"/> | |
| 5 | - <circle cx="14" cy="12" r="1" fill="#000000"/> | |
| 6 | - <ellipse cx="12" cy="15" rx="1.5" ry="1" fill="#FFB6C1"/> | |
| 7 | - <path d="M6 13C6 13 5 12 4 12" stroke="#000000" stroke-width="0.5" stroke-linecap="round"/> | |
| 8 | - <path d="M18 13C18 13 19 12 20 12" stroke="#000000" stroke-width="0.5" stroke-linecap="round"/> | |
| 9 | - <circle cx="8" cy="17" r="2" fill="#8B4513"/> | |
| 10 | - <circle cx="16" cy="17" r="2" fill="#8B4513"/> | |
| 11 | -</svg> | |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | |
| 2 | +<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg"> | |
| 3 | + <path d="m119.28 874.44c0.81641 28.594 11.742 55.977 30.84 77.277-4.082 14.672-3.0234 30.297 3 44.281-1.7578 2.8789-3.207 5.9375-4.3203 9.1211-8.5156 26.387-5.0977 55.18 9.3594 78.84 2.2031 3.4336 6 5.5117 10.078 5.5195 1.1953 0.17969 2.4102 0.17969 3.6016 0 18.086-6.1406 33.758-17.863 44.762-33.48 4.8008 2.6406 9.8398 5.2812 15.238 7.8008-0.67188 18.098 4.1836 35.969 13.922 51.238 2.2031 3.4336 6 5.5156 10.078 5.5195 1.1914 0.18359 2.4062 0.18359 3.6016 0 17.273-5.6328 32.426-16.387 43.438-30.84 5.6406 1.4414 11.281 2.8789 16.559 3.9609h0.003906c-1.1172 18.824 3.7539 37.516 13.918 53.398 2.2031 3.4375 6 5.5156 10.082 5.5234 1.1914 0.17969 2.4062 0.17969 3.5977 0 26.18-9.125 47.137-29.121 57.48-54.84 1.0391-3.168 1.8047-6.4219 2.2812-9.7227 5.5-2.8594 10.586-6.4531 15.121-10.68 10.801 1.9219 21.719 3.7188 32.879 5.2812l5.3984 0.71875c12.48 1.6797 25.078 3.1211 37.801 4.3203l10.199 0.96094c12 0.96094 23.16 1.8008 35.039 2.3984l11.16 0.60156c15.121 0 30.238 1.0781 45.602 1.0781 15.359 0 30.48 0 45.602-1.0781l11.16-0.60156c12 0 24-1.4414 35.039-2.3984l10.199-0.96094c12.719-1.1992 25.32-2.6406 37.801-4.3203l5.3984-0.71875c11.16-1.5586 22.078-3.3594 32.879-5.2812 4.5352 4.2266 9.6211 7.8203 15.121 10.68 0.47656 3.3008 1.2422 6.5547 2.2812 9.7227 10.484 25.492 31.422 45.25 57.48 54.238 1.1914 0.17969 2.4062 0.17969 3.5977 0 4.082-0.007812 7.8789-2.0859 10.082-5.5195 10.172-15.926 15.043-34.66 13.918-53.52 5.2812-0.96094 10.922-2.3984 16.559-3.8398h0.003906c11.012 14.449 26.164 25.207 43.438 30.84 1.1953 0.17969 2.4102 0.17969 3.6016 0 4.0781-0.007813 7.875-2.0859 10.078-5.5195 9.7383-15.27 14.594-33.145 13.922-51.242 5.3984-2.5195 10.441-5.1602 15.238-7.8008 11.168 15.215 26.82 26.547 44.762 32.402 1.1914 0.17969 2.4062 0.17969 3.6016 0 4.0781-0.007812 7.875-2.0859 10.078-5.5195 14.457-23.664 17.875-52.453 9.3594-78.84-1.0898-3.1953-2.5391-6.2539-4.3203-9.1211 5.9141-13.844 6.9727-29.281 3-43.801 19.098-21.301 30.023-48.684 30.84-77.281 0-66.359-64.078-127.44-175.8-168l0.003906-65.52 61.801 36h-0.003906c1.8203 1.0781 3.8867 1.6602 6 1.6797 5.4648 0.039062 10.266-3.6172 11.68-8.8984 1.4102-5.2812-0.92578-10.848-5.6797-13.539l-73.797-42.961v-87.961h115.08c6.6289 0 12-5.3711 12-12 0-6.625-5.3711-12-12-12h-115.08v-79.438l72.84-33.238v-0.003906c6.0273-2.75 8.6875-9.8672 5.9375-15.898s-9.8672-8.6914-15.898-5.9414l-62.879 28.68v-41.039c0-80.871-32.129-158.43-89.312-215.61-57.184-57.184-134.74-89.312-215.61-89.312s-158.43 32.129-215.61 89.312c-57.184 57.184-89.312 134.74-89.312 215.61v41.039l-62.879-28.68c-6.0312-2.75-13.148-0.089843-15.898 5.9414s-0.089843 13.148 5.9375 15.898l72.84 33.238v79.441h-115.08c-6.6289 0-12 5.375-12 12 0 6.6289 5.3711 12 12 12h114.96v87.961l-73.801 42.719c-4.7539 2.6953-7.0898 8.2617-5.6758 13.543 1.4102 5.2773 6.2109 8.9375 11.676 8.8984 2.1133-0.023437 4.1836-0.60156 6-1.6797l61.801-36v66.359c-111.6 41.16-175.68 102.24-175.68 168.6zm24 0c0-53.398 56.398-106.08 151.8-142.68l-0.003906 150.6c-23.945-4.0508-48.473-3.0312-72 3-24.824 5.8164-46.672 20.496-61.438 41.281-11.363-15.086-17.777-33.32-18.359-52.199zm63.48 150.96v-0.003906c-6.6602 15.398-18.332 28.086-33.121 36-6.332-15.66-7.1367-33.016-2.2812-49.199 0.60156-1.8008 6.2383-16.199 18.719-16.199 2.2539 0.023438 4.4844 0.42578 6.6016 1.1992 16.441 6.4805 11.281 24.48 10.082 28.199zm87.48 31.559h-0.003907c-6.5352 15.434-18.191 28.145-33 36-6.3398-15.664-7.1445-33.02-2.2773-49.199 1.0781-3 6.7188-16.199 18.719-16.199v-0.003906c2.2539 0.023437 4.4844 0.42969 6.6016 1.2031 16.559 6.3594 11.277 24.477 9.957 28.199zm87.602 31.441-0.003906-0.003906c-6.5938 15.398-18.234 28.094-33 36-6.3438-15.617-7.1484-32.938-2.2812-49.078 1.0781-3.1211 6.7188-16.32 18.719-16.32h0.003906c2.25 0.023438 4.4844 0.42578 6.5977 1.1992 16.441 6.4805 11.281 24.48 9.9609 28.199zm218.16-20.402c-54.512 0.058594-108.93-4.3555-162.72-13.199 0.96094-2.0391 2.0391-4.0781 2.8789-6.2383v-0.003906c7.2305-21.223 6.2461-44.387-2.7617-64.918 25.562-88.32 89.16-151.2 162.6-151.2s137.04 62.879 162.6 150.72c-9.0078 20.531-9.9922 43.695-2.7617 64.918 0.83984 2.1602 1.9219 4.1992 2.8789 6.2383v0.003906c-53.773 9.0039-108.2 13.582-162.72 13.68zm251.16 57.359c-14.766-7.9062-26.406-20.602-33-36-1.3203-3.7188-6.4805-21.719 9.9609-27.719s24 12 25.32 15c4.8164 15.863 4.0078 32.902-2.2812 48.238zm87.602-31.559c-14.781-7.8945-26.422-20.594-33-36-1.3203-3.8398-6.4805-21.84 9.9609-27.719 16.441-5.8789 24 12 25.32 15l-0.003907-0.003906c4.8164 15.863 4.0117 32.902-2.2773 48.242zm31.922-60h-0.003906c0.070312 0.51562 0.070312 1.043 0 1.5586l-6.8398 3.6016c-0.20703-0.90234-0.48828-1.7852-0.83984-2.6406-2.6797-7.1367-6.7578-13.664-12-19.199-5.3945-5.9961-12.5-10.188-20.355-12.012s-16.082-1.1914-23.566 1.8125c-11.059 4.375-19.863 13.059-24.391 24.059s-4.3828 23.367 0.39062 34.258l1.0781 2.6406-7.5586 1.5586 0.003906 0.003906c-0.16797-0.49609-0.36719-0.97656-0.60156-1.4414-2.7461-7.1055-6.8164-13.621-12-19.199-5.4219-5.9922-12.551-10.18-20.422-12.004-7.8711-1.8203-16.113-1.1914-23.617 1.8047-10.707 3.3945-19.445 11.215-24 21.477-6.3281-5.1602-11.102-11.973-13.801-19.68-7.1133-23.73-1.9883-49.445 13.68-68.637 19.871-26.266 47.168-45.957 78.359-56.523 19.234-7.0234 39.523-10.719 60-10.918 12.156-0.046874 24.262 1.5273 36 4.6797 24.277 4.8125 44.617 21.289 54.359 44.039 2.7891 7.6836 3.4102 15.988 1.8008 24-9.9805-4.9023-21.566-5.3828-31.918-1.3203-10.875 4.457-19.512 13.082-23.977 23.953s-4.3867 23.074 0.21484 33.887zm55.68 28.32h-0.003906c-14.789-7.918-26.461-20.605-33.121-36-1.1992-3.7188-6.3594-21.719 10.078-27.719 16.441-6 24 12 25.32 15l0.003906-0.003906c4.6875 15.961 3.8867 33.031-2.2812 48.48zm-121.44-330.12c96 36.602 151.8 89.281 151.8 142.68h-0.003906c-0.47656 18.84-6.7617 37.07-18 52.199-14.766-20.785-36.613-35.465-61.438-41.277-23.527-6.0312-48.055-7.0547-72-3zm-585.84-106.2 76.922-44.879c5.4258-3.4531 7.168-10.566 3.9531-16.133-3.2188-5.5703-10.254-7.6094-15.953-4.6289l-65.398 37.922v-74.043h68.16c6.625 0 12-5.3711 12-12 0-6.625-5.375-12-12-12h-67.684v-68.039l68.16 31.199c1.5898 0.69922 3.3047 1.0703 5.043 1.082 4.7148-0.011719 8.9844-2.7812 10.918-7.082 2.7031-6.0312 0.023437-13.113-6-15.84l-78.121-36v-52.199c0-74.504 29.598-145.96 82.281-198.64 52.684-52.684 124.14-82.281 198.64-82.281s145.96 29.598 198.64 82.281c52.684 52.684 82.281 124.14 82.281 198.64v51.961l-78.121 36c-6.0234 2.7266-8.7031 9.8086-6 15.84 1.9336 4.3008 6.2031 7.0703 10.918 7.0781 1.7383-0.011718 3.4531-0.37891 5.043-1.0781l68.16-30.961v68.52h-68.16c-6.6289 0-12 5.375-12 12 0 6.6289 5.3711 12 12 12h68.16v74.039l-64.922-38.398c-5.6992-2.9805-12.734-0.94141-15.953 4.6289-3.2148 5.5664-1.4727 12.68 3.9531 16.133l77.398 44.879v262.2c-4.8008 1.4414-9.7188 3-14.52 4.6797-4.8008 1.6797-8.7617 3.3594-13.078 5.2812v-111.36c0-6.6289-5.375-12-12-12-6.6289 0-12 5.3711-12 12v123.24c-19.547 11.555-36.695 26.746-50.52 44.762-32.402-87-99.961-146.16-179.28-146.16s-146.88 59.16-178.8 146.16c-22.531-28.855-52.965-50.535-87.598-62.402-4.8008-1.6797-9.7188-3.2383-14.52-4.6797zm6.3594 288.96h0.003906c31.191 10.566 58.488 30.254 78.359 56.52 15.668 19.191 20.793 44.906 13.68 68.641-2.6992 7.7031-7.4727 14.516-13.801 19.68-5.8477-12.375-17.391-21.098-30.891-23.348-13.496-2.25-27.242 2.2578-36.789 12.066-5.2344 5.7227-9.3086 12.406-12 19.68-0.24609 0.54297-0.44531 1.1055-0.60156 1.6797l-7.5586-1.6797 1.0781-2.6406h0.003906c4.7734-10.891 4.918-23.258 0.39062-34.258s-13.332-19.688-24.391-24.062c-7.4805-3.0391-15.715-3.6875-23.578-1.8633-7.8633 1.8281-14.969 6.0391-20.344 12.062-5.2422 5.5352-9.3203 12.066-12 19.203-0.35156 0.85547-0.63281 1.7383-0.83984 2.6367l-6.8398-3.6016v0.003907c0.12109-0.54688 0.32422-1.0742 0.60156-1.5586 4.7773-10.91 4.9219-23.297 0.39844-34.312-4.5234-11.02-13.332-19.727-24.398-24.129-10.355-4.0625-21.938-3.582-31.922 1.3203-1.6094-8.0156-0.98438-16.316 1.8008-24 9.7422-22.75 30.082-39.23 54.359-44.039 11.738-3.1523 23.844-4.7266 36-4.6836 20.219 0.20703 40.262 3.8164 59.281 10.684z"/> | |
| 4 | + <path d="m596.4 403.56c-4.5586 0-111.72 1.3203-111.72 72 0 26.641 13.68 82.68 33.359 134.64-6.5859 0.64844-13.238 0.20312-19.68-1.3203-34.922-8.6406-50.762-49.68-50.879-50.16-2.3203-6.2305-9.25-9.3984-15.48-7.0781s-9.3984 9.25-7.0781 15.48c0.83984 2.1602 20.16 53.16 67.441 65.039h-0.003906c11.449 2.7266 23.34 3.0547 34.922 0.96094 5.2812 12 10.801 24 16.441 34.559h-0.003906c-7.3086 12.992-9.7578 28.164-6.9023 42.797 2.8555 14.633 10.828 27.773 22.484 37.062 11.66 9.293 26.246 14.129 41.148 13.645 14.902-0.48047 29.145-6.2539 40.176-16.281 11.035-10.027 18.137-23.656 20.039-38.441 1.8984-14.789-1.5273-29.77-9.6641-42.262 5.2812-10.078 10.32-21 15.121-32.398 7.1445 1.7812 14.477 2.707 21.84 2.7578 6.6328-0.003906 13.238-0.8125 19.68-2.3984 47.281-12 66.602-62.879 67.441-65.039h-0.003906c2.3203-6.2305-0.84766-13.16-7.0781-15.48s-13.16 0.84766-15.48 7.0781c0 0-15.961 41.281-50.879 49.922-8.6953 1.9258-17.707 1.9258-26.402 0 19.441-51.359 32.762-106.8 32.762-133.32 0-70.441-106.92-71.762-111.6-71.762zm1.9219 333.36v0.003906c-10.312 0-20.203-4.0977-27.492-11.391-7.293-7.2891-11.387-17.18-11.387-27.492s4.0938-20.199 11.387-27.492c7.2891-7.2891 17.18-11.387 27.492-11.387s20.199 4.0977 27.492 11.387c7.293 7.293 11.387 17.18 11.387 27.492-0.03125 10.305-4.1367 20.172-11.422 27.457s-17.156 11.391-27.457 11.426zm34.68-91.199c-10.715-7.2109-23.418-10.891-36.328-10.523-12.91 0.36328-25.383 4.7578-35.672 12.566-25.32-51.719-52.32-130.8-52.32-172.44 0-46.559 87-48 87.719-48s87.84 1.1992 87.84 48c-0.23828 40.68-26.398 118.56-51.238 170.4z"/> | |
| 5 | + <path d="m756 180c2.1992 2 5.0664 3.1133 8.0391 3.1211 4.9766 0.035156 9.457-3 11.266-7.6328 1.8125-4.6328 0.57812-9.9023-3.1055-13.25-7.6094-6.9062-15.625-13.359-24-19.316-2.5938-1.8477-5.8125-2.5859-8.9531-2.0586-3.1406 0.52734-5.9414 2.2812-7.7852 4.875-3.8438 5.4023-2.582 12.898 2.8203 16.742 7.5898 5.3906 14.844 11.242 21.719 17.52z"/> | |
| 6 | + <path d="m483.48 157.32c29.781-18.684 63.441-30.305 98.41-33.965 34.965-3.6602 70.305 0.73438 103.31 12.844 6.2305 2.2891 13.133-0.91016 15.422-7.1406 2.2852-6.2266-0.91016-13.133-7.1406-15.418-36.426-13.34-75.418-18.176-114-14.141-38.582 4.0352-75.727 16.832-108.61 37.422-60 36.602-99.48 94.199-106.32 153.96v-0.003906c-0.37109 3.168 0.53516 6.3555 2.5156 8.8516 1.9805 2.5 4.875 4.1094 8.0469 4.4688h1.4414-0.003906c6.1445 0.039062 11.324-4.5703 12-10.68 5.8828-52.32 41.402-103.32 94.922-136.2z"/> | |
| 7 | + <path d="m841.32 748.68c3.1836 0 6.2344-1.2656 8.4844-3.5156s3.5156-5.3008 3.5156-8.4844v-51c0-6.6289-5.3711-12-12-12-6.6289 0-12 5.3711-12 12v51c0 3.1836 1.2656 6.2344 3.5156 8.4844s5.3008 3.5156 8.4844 3.5156z"/> | |
| 8 | +</svg> | |
frontend/src/components/Game.tsxmodified@@ -29,7 +29,8 @@ const Game: React.FC = () => { | ||
| 29 | 29 | const [executing, setExecuting] = useState(false); |
| 30 | 30 | const [showHints, setShowHints] = useState(false); |
| 31 | 31 | const [hints, setHints] = useState<string[]>([]); |
| 32 | - const [terminalMinimized, setTerminalMinimized] = useState(false); | |
| 32 | + const [terminalMinimized, setTerminalMinimized] = useState(true); | |
| 33 | + const [hasPlayedIntro, setHasPlayedIntro] = useState(false); | |
| 33 | 34 | |
| 34 | 35 | const terminalRef = useRef<HTMLDivElement>(null); |
| 35 | 36 | const inputRef = useRef<HTMLInputElement>(null); |
@@ -60,13 +61,14 @@ const Game: React.FC = () => { | ||
| 60 | 61 | error: null, |
| 61 | 62 | }); |
| 62 | 63 | setCommandHistory([{ |
| 63 | - command: '🎮 Game started!', | |
| 64 | + command: 'Hunt started!', | |
| 64 | 65 | output: response.mole_hint + '\nType "help" for available commands.', |
| 65 | 66 | success: true, |
| 66 | 67 | }]); |
| 67 | 68 | setHints([]); |
| 68 | 69 | setShowHints(false); |
| 69 | - setTerminalMinimized(false); | |
| 70 | + setTerminalMinimized(true); // Keep terminal minimized on new game | |
| 71 | + setHasPlayedIntro(false); // Reset intro for new game | |
| 70 | 72 | } catch (error) { |
| 71 | 73 | setGameState({ |
| 72 | 74 | ...gameState, |
@@ -176,6 +178,17 @@ const Game: React.FC = () => { | ||
| 176 | 178 | startNewGame(); |
| 177 | 179 | }, []); |
| 178 | 180 | |
| 181 | + // Mark intro as played after first render | |
| 182 | + useEffect(() => { | |
| 183 | + if (gameState.tree && !hasPlayedIntro) { | |
| 184 | + // Set timeout to mark intro as played after animation completes | |
| 185 | + const timer = setTimeout(() => { | |
| 186 | + setHasPlayedIntro(true); | |
| 187 | + }, 6500); // Total intro duration | |
| 188 | + return () => clearTimeout(timer); | |
| 189 | + } | |
| 190 | + }, [gameState.tree, hasPlayedIntro]); | |
| 191 | + | |
| 179 | 192 | if (gameState.loading) { |
| 180 | 193 | return ( |
| 181 | 194 | <div className="flex items-center justify-center min-h-screen bg-gray-900 text-white"> |
@@ -207,7 +220,7 @@ const Game: React.FC = () => { | ||
| 207 | 220 | return ( |
| 208 | 221 | <div className="flex items-center justify-center min-h-screen bg-gray-900 text-white"> |
| 209 | 222 | <div className="text-center"> |
| 210 | - <h1 className="text-4xl font-bold mb-4">🐭 Bashamole</h1> | |
| 223 | + <h1 className="text-4xl font-bold mb-4">Bashamole</h1> | |
| 211 | 224 | <p className="text-gray-400 mb-8">Hunt the mole in the Unix filesystem!</p> |
| 212 | 225 | <button |
| 213 | 226 | onClick={startNewGame} |
@@ -228,6 +241,7 @@ const Game: React.FC = () => { | ||
| 228 | 241 | treeData={gameState.tree.tree_data} |
| 229 | 242 | playerLocation={gameState.tree.player_location} |
| 230 | 243 | onNodeClick={handleNodeClick} |
| 244 | + playIntro={!hasPlayedIntro} | |
| 231 | 245 | /> |
| 232 | 246 | </div> |
| 233 | 247 | |
@@ -256,7 +270,7 @@ const Game: React.FC = () => { | ||
| 256 | 270 | {commandHistory.map((entry, index) => ( |
| 257 | 271 | <div key={index} className="mb-2"> |
| 258 | 272 | <div className="text-gray-400"> |
| 259 | - {entry.command.startsWith('🎮') ? ( | |
| 273 | + {entry.command.startsWith('Game started!') ? ( | |
| 260 | 274 | <span className="text-yellow-400">{entry.command}</span> |
| 261 | 275 | ) : ( |
| 262 | 276 | <>$ {entry.command}</> |
@@ -306,7 +320,7 @@ const Game: React.FC = () => { | ||
| 306 | 320 | > |
| 307 | 321 | ✕ |
| 308 | 322 | </button> |
| 309 | - <h3 className="text-yellow-400 font-bold mb-2">💡 Hints:</h3> | |
| 323 | + <h3 className="text-yellow-400 font-bold mb-2">Hints:</h3> | |
| 310 | 324 | {hints.map((hint, index) => ( |
| 311 | 325 | <p key={index} className="text-yellow-200 text-sm">{hint}</p> |
| 312 | 326 | ))} |
@@ -317,7 +331,7 @@ const Game: React.FC = () => { | ||
| 317 | 331 | <div className="absolute bottom-0 left-0 right-0 bg-gray-900/90 backdrop-blur-sm border-t border-gray-700 p-4 z-20"> |
| 318 | 332 | <div className="max-w-7xl mx-auto flex justify-between items-center"> |
| 319 | 333 | <div className="flex items-center gap-4"> |
| 320 | - <h1 className="text-2xl font-bold">🐭 Bashamole</h1> | |
| 334 | + <h1 className="text-2xl font-bold">Bashamole</h1> | |
| 321 | 335 | <div className="text-sm text-gray-400"> |
| 322 | 336 | Location: <span className="font-mono text-blue-400">{gameState.tree.player_location}</span> |
| 323 | 337 | </div> |
@@ -326,7 +340,7 @@ const Game: React.FC = () => { | ||
| 326 | 340 | <div className="flex items-center gap-3"> |
| 327 | 341 | {gameState.tree.is_completed ? ( |
| 328 | 342 | <div className="text-green-400 font-bold animate-pulse"> |
| 329 | - 🎉 You found the mole! | |
| 343 | + You found the mole! | |
| 330 | 344 | </div> |
| 331 | 345 | ) : ( |
| 332 | 346 | <> |
@@ -334,7 +348,7 @@ const Game: React.FC = () => { | ||
| 334 | 348 | onClick={getHints} |
| 335 | 349 | className="px-3 py-1.5 bg-yellow-600 text-white text-sm rounded hover:bg-yellow-700 transition" |
| 336 | 350 | > |
| 337 | - Get Hint 💡 | |
| 351 | + Get Hint | |
| 338 | 352 | </button> |
| 339 | 353 | <div className="text-xs text-gray-500"> |
| 340 | 354 | Click nodes or use terminal |
frontend/src/components/TreeVisualizer.tsxmodified@@ -9,19 +9,28 @@ interface TreeVisualizerProps { | ||
| 9 | 9 | treeData: TreeNode; |
| 10 | 10 | playerLocation: string; |
| 11 | 11 | onNodeClick?: (path: string) => void; |
| 12 | + playIntro?: boolean; | |
| 12 | 13 | } |
| 13 | 14 | |
| 14 | 15 | const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ |
| 15 | 16 | treeData, |
| 16 | 17 | playerLocation, |
| 17 | 18 | onNodeClick, |
| 19 | + playIntro = true, | |
| 18 | 20 | }) => { |
| 19 | 21 | const svgRef = useRef<SVGSVGElement>(null); |
| 20 | 22 | const containerRef = useRef<HTMLDivElement>(null); |
| 23 | + const previousLocationRef = useRef<string | null>(null); | |
| 21 | 24 | |
| 22 | 25 | useEffect(() => { |
| 23 | 26 | if (!treeData || !svgRef.current || !containerRef.current) return; |
| 24 | 27 | |
| 28 | + const isNavigation = previousLocationRef.current !== null && | |
| 29 | + previousLocationRef.current !== playerLocation && | |
| 30 | + !playIntro; | |
| 31 | + | |
| 32 | + previousLocationRef.current = playerLocation; | |
| 33 | + | |
| 25 | 34 | // Clear previous render |
| 26 | 35 | d3.select(svgRef.current).selectAll('*').remove(); |
| 27 | 36 | |
@@ -29,9 +38,21 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 29 | 38 | const containerWidth = containerRef.current.clientWidth; |
| 30 | 39 | const containerHeight = containerRef.current.clientHeight; |
| 31 | 40 | |
| 32 | - // Set up dimensions with dynamic sizing | |
| 33 | - const margin = { top: 100, right: 50, bottom: 100, left: 50 }; | |
| 34 | - const width = Math.max(containerWidth, 1600); | |
| 41 | + // Create hierarchy and calculate dimensions | |
| 42 | + const root = d3.hierarchy(treeData); | |
| 43 | + | |
| 44 | + // Calculate the maximum number of nodes at any depth | |
| 45 | + const levelCounts: { [key: number]: number } = {}; | |
| 46 | + root.each(d => { | |
| 47 | + levelCounts[d.depth] = (levelCounts[d.depth] || 0) + 1; | |
| 48 | + }); | |
| 49 | + const maxNodesAtLevel = Math.max(...Object.values(levelCounts)); | |
| 50 | + | |
| 51 | + // Dynamic width based on tree structure | |
| 52 | + const nodeSpacing = 120; // Increased from 80 for better spacing | |
| 53 | + const dynamicWidth = Math.max(maxNodesAtLevel * nodeSpacing, containerWidth * 2); // Increased multiplier | |
| 54 | + const margin = { top: 100, right: 150, bottom: 100, left: 150 }; // Increased margins | |
| 55 | + const width = dynamicWidth; | |
| 35 | 56 | const height = Math.max(containerHeight, 1200); |
| 36 | 57 | |
| 37 | 58 | const svg = d3 |
@@ -70,29 +91,50 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 70 | 91 | |
| 71 | 92 | const g = svg |
| 72 | 93 | .append('g') |
| 73 | - .attr('transform', `translate(${width / 2},${margin.top})`); | |
| 94 | + .attr('transform', `translate(${margin.left},${margin.top})`); | |
| 74 | 95 | |
| 75 | - // Create tree layout - vertical orientation with more spacing | |
| 96 | + // Create tree layout - vertical orientation with better spacing | |
| 76 | 97 | const treeLayout = d3 |
| 77 | 98 | .tree<TreeNode>() |
| 78 | 99 | .size([width - margin.left - margin.right, height - margin.top - margin.bottom]) |
| 79 | 100 | .separation((a, b) => { |
| 101 | + // Special handling for directories with many children | |
| 102 | + const aParentChildCount = a.parent ? (a.parent.children?.length || 0) : 0; | |
| 103 | + const bParentChildCount = b.parent ? (b.parent.children?.length || 0) : 0; | |
| 104 | + | |
| 105 | + // If nodes share a parent with many children (like home dirs), give more space | |
| 106 | + if (a.parent === b.parent && aParentChildCount > 3) { | |
| 107 | + const aIsLeaf = !a.children || a.children.length === 0; | |
| 108 | + const bIsLeaf = !b.children || b.children.length === 0; | |
| 109 | + | |
| 110 | + if (aIsLeaf && bIsLeaf) { | |
| 111 | + // Extra space for leaf nodes in crowded directories | |
| 112 | + return 2.5; | |
| 113 | + } | |
| 114 | + return 2; | |
| 115 | + } | |
| 116 | + | |
| 117 | + // Base separation on node depth | |
| 118 | + if (a.depth === 0 || b.depth === 0) return 4; | |
| 119 | + if (a.depth === 1 || b.depth === 1) return 3; | |
| 120 | + | |
| 80 | 121 | const aIsLeaf = !a.children || a.children.length === 0; |
| 81 | 122 | const bIsLeaf = !b.children || b.children.length === 0; |
| 82 | 123 | |
| 83 | - // Much more spacing for leaf nodes to prevent crowding | |
| 84 | 124 | if (aIsLeaf && bIsLeaf) { |
| 85 | - return 3; // Increased from 1.5 | |
| 86 | - } | |
| 87 | - if (aIsLeaf || bIsLeaf) { | |
| 88 | - return 2.5; // Mixed leaf/non-leaf | |
| 125 | + return 1.5; | |
| 89 | 126 | } |
| 90 | - return a.parent === b.parent ? 2 : 2.5; // Increased general spacing | |
| 127 | + return a.parent === b.parent ? 1.5 : 2; | |
| 91 | 128 | }); |
| 92 | 129 | |
| 93 | - // Create hierarchy | |
| 94 | - const root = d3.hierarchy(treeData); | |
| 130 | + // Apply tree layout | |
| 95 | 131 | const treeNodes = treeLayout(root); |
| 132 | + | |
| 133 | + // Adjust the x-coordinate to center the root | |
| 134 | + const rootX = width / 2; | |
| 135 | + treeNodes.each(d => { | |
| 136 | + d.x = d.x + (rootX - root.x); | |
| 137 | + }); | |
| 96 | 138 | |
| 97 | 139 | // Create gradient for links |
| 98 | 140 | const linkGradient = defs |
@@ -154,9 +196,9 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 154 | 196 | node |
| 155 | 197 | .append('circle') |
| 156 | 198 | .attr('r', d => { |
| 157 | - if (d.data.path === '/') return 14; | |
| 158 | - if (d.data.path === playerLocation) return 11; | |
| 159 | - return 9; | |
| 199 | + if (d.data.path === '/') return 16; | |
| 200 | + if (d.data.path === playerLocation) return 13; | |
| 201 | + return 11; | |
| 160 | 202 | }) |
| 161 | 203 | .style('fill', d => { |
| 162 | 204 | if (d.data.path === playerLocation) return '#3B82F6'; |
@@ -177,14 +219,14 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 177 | 219 | d3.select(this) |
| 178 | 220 | .transition() |
| 179 | 221 | .duration(200) |
| 180 | - .attr('r', d.data.path === '/' ? 16 : 12) | |
| 222 | + .attr('r', d.data.path === '/' ? 18 : 14) | |
| 181 | 223 | .style('stroke-width', 3); |
| 182 | 224 | }) |
| 183 | 225 | .on('mouseout', function(event, d) { |
| 184 | 226 | d3.select(this) |
| 185 | 227 | .transition() |
| 186 | 228 | .duration(200) |
| 187 | - .attr('r', d.data.path === '/' ? 14 : d.data.path === playerLocation ? 11 : 9) | |
| 229 | + .attr('r', d.data.path === '/' ? 16 : d.data.path === playerLocation ? 13 : 11) | |
| 188 | 230 | .style('stroke-width', 2); |
| 189 | 231 | }) |
| 190 | 232 | .on('click', (event, d) => { |
@@ -203,64 +245,39 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 203 | 245 | .append('text') |
| 204 | 246 | .attr('dy', d => d.children ? -20 : 25) |
| 205 | 247 | .attr('text-anchor', 'middle') |
| 206 | - .style('font-size', '12px') | |
| 207 | - .style('font-weight', d => d.data.path === playerLocation ? '600' : '400') | |
| 248 | + .style('font-size', '14px') | |
| 249 | + .style('font-weight', d => d.data.path === playerLocation ? '700' : '500') | |
| 208 | 250 | .style('fill', d => d.data.path === playerLocation ? '#93C5FD' : '#E5E7EB') |
| 209 | 251 | .style('text-shadow', '0 0 4px rgba(0,0,0,0.8)') |
| 210 | 252 | .text(d => d.data.name || '/') |
| 211 | 253 | .style('pointer-events', 'none'); |
| 212 | 254 | |
| 213 | - // Add player indicator with SVG | |
| 255 | + // Add player indicator with SVG overlaid on node | |
| 214 | 256 | const playerNode = treeNodes.descendants().find(d => d.data.path === playerLocation); |
| 215 | 257 | if (playerNode) { |
| 216 | 258 | const playerGroup = node |
| 217 | 259 | .filter(d => d.data.path === playerLocation) |
| 218 | - .append('g') | |
| 219 | - .attr('transform', 'translate(0, -30)'); | |
| 260 | + .append('g'); | |
| 220 | 261 | |
| 221 | - // Add pulsing animation | |
| 222 | - playerGroup | |
| 223 | - .append('circle') | |
| 224 | - .attr('r', 15) | |
| 225 | - .style('fill', 'none') | |
| 226 | - .style('stroke', '#3B82F6') | |
| 227 | - .style('stroke-width', 2) | |
| 228 | - .style('opacity', 0) | |
| 229 | - .append('animate') | |
| 230 | - .attr('attributeName', 'r') | |
| 231 | - .attr('from', '15') | |
| 232 | - .attr('to', '25') | |
| 233 | - .attr('dur', '2s') | |
| 234 | - .attr('repeatCount', 'indefinite'); | |
| 235 | - | |
| 236 | - playerGroup | |
| 237 | - .select('circle') | |
| 238 | - .append('animate') | |
| 239 | - .attr('attributeName', 'opacity') | |
| 240 | - .attr('from', '0.8') | |
| 241 | - .attr('to', '0') | |
| 242 | - .attr('dur', '2s') | |
| 243 | - .attr('repeatCount', 'indefinite'); | |
| 244 | - | |
| 245 | - // Add player icon | |
| 262 | + // Add player SVG directly on the node | |
| 246 | 263 | playerGroup |
| 247 | 264 | .append('image') |
| 248 | 265 | .attr('xlink:href', '/player.svg') |
| 249 | - .attr('width', 24) | |
| 250 | - .attr('height', 24) | |
| 251 | - .attr('x', -12) | |
| 252 | - .attr('y', -12); | |
| 266 | + .attr('width', 20) | |
| 267 | + .attr('height', 20) | |
| 268 | + .attr('x', -10) | |
| 269 | + .attr('y', -10) | |
| 270 | + .style('pointer-events', 'none'); | |
| 253 | 271 | } |
| 254 | 272 | |
| 255 | - // Add mole indicator with SVG if game is won | |
| 273 | + // Add mole indicator with SVG overlaid if game is won | |
| 256 | 274 | const moleNode = treeNodes.descendants().find(d => d.data.has_mole); |
| 257 | 275 | if (moleNode) { |
| 258 | 276 | const moleGroup = node |
| 259 | 277 | .filter(d => d.data.has_mole) |
| 260 | - .append('g') | |
| 261 | - .attr('transform', 'translate(0, -30)'); | |
| 278 | + .append('g'); | |
| 262 | 279 | |
| 263 | - // Add celebration animation | |
| 280 | + // Add celebration animation ring | |
| 264 | 281 | moleGroup |
| 265 | 282 | .append('circle') |
| 266 | 283 | .attr('r', 15) |
@@ -284,41 +301,87 @@ const TreeVisualizer: React.FC<TreeVisualizerProps> = ({ | ||
| 284 | 301 | .attr('dur', '1s') |
| 285 | 302 | .attr('repeatCount', 'indefinite'); |
| 286 | 303 | |
| 287 | - // Add mole icon | |
| 304 | + // Add mole SVG directly on the node | |
| 288 | 305 | moleGroup |
| 289 | 306 | .append('image') |
| 290 | 307 | .attr('xlink:href', '/mole.svg') |
| 291 | - .attr('width', 24) | |
| 292 | - .attr('height', 24) | |
| 293 | - .attr('x', -12) | |
| 294 | - .attr('y', -12); | |
| 308 | + .attr('width', 20) | |
| 309 | + .attr('height', 20) | |
| 310 | + .attr('x', -10) | |
| 311 | + .attr('y', -10) | |
| 312 | + .style('pointer-events', 'none'); | |
| 295 | 313 | } |
| 296 | 314 | |
| 297 | 315 | // Add zoom and pan behavior |
| 298 | 316 | const zoom = d3.zoom<SVGSVGElement, unknown>() |
| 299 | - .scaleExtent([0.3, 2]) | |
| 317 | + .scaleExtent([0.1, 3]) | |
| 300 | 318 | .on('zoom', (event) => { |
| 301 | 319 | g.attr('transform', event.transform); |
| 302 | 320 | }); |
| 303 | 321 | |
| 322 | + // Apply zoom behavior immediately | |
| 304 | 323 | svg.call(zoom); |
| 305 | 324 | |
| 306 | - // Center on player location with animation | |
| 307 | - if (playerNode) { | |
| 308 | - const scale = 0.8; | |
| 309 | - const x = width / 2 - playerNode.x * scale; | |
| 310 | - const y = containerHeight / 2 - playerNode.y * scale - margin.top; | |
| 325 | + // Helper function to create zoom transform for centering on a node | |
| 326 | + const getZoomTransform = (node: d3.HierarchyPointNode<TreeNode>, scale: number, offsetX: number = 0, offsetY: number = 0) => { | |
| 327 | + const viewBoxCenterX = width / 2; | |
| 328 | + const viewBoxCenterY = height / 2; | |
| 311 | 329 | |
| 312 | - svg | |
| 330 | + // Calculate translation to center the node with optional offset | |
| 331 | + const translateX = viewBoxCenterX - (node.x + margin.left) * scale + (width * offsetX); | |
| 332 | + const translateY = viewBoxCenterY - (node.y + margin.top) * scale + (height * offsetY); | |
| 333 | + | |
| 334 | + return d3.zoomIdentity.translate(translateX, translateY).scale(scale); | |
| 335 | + }; | |
| 336 | + | |
| 337 | + // Animated intro sequence using zoom transitions | |
| 338 | + if (playIntro && playerNode) { | |
| 339 | + // Start zoomed in on root | |
| 340 | + const rootTransform = getZoomTransform(treeNodes, 3); | |
| 341 | + svg.call(zoom.transform, rootTransform); | |
| 342 | + | |
| 343 | + // Calculate full tree view | |
| 344 | + const allNodes = treeNodes.descendants(); | |
| 345 | + const xExtent = d3.extent(allNodes, d => d.x) as [number, number]; | |
| 346 | + const yExtent = d3.extent(allNodes, d => d.y) as [number, number]; | |
| 347 | + | |
| 348 | + const treeWidth = xExtent[1] - xExtent[0] + 200; | |
| 349 | + const treeHeight = yExtent[1] - yExtent[0] + 200; | |
| 350 | + | |
| 351 | + const scaleX = (width - margin.left - margin.right) / treeWidth; | |
| 352 | + const scaleY = (height - margin.top - margin.bottom) / treeHeight; | |
| 353 | + const fullTreeScale = Math.min(scaleX, scaleY, 0.8); | |
| 354 | + | |
| 355 | + const treeCenterX = (xExtent[0] + xExtent[1]) / 2; | |
| 356 | + const treeCenterY = (yExtent[0] + yExtent[1]) / 2; | |
| 357 | + const treeCenter = { x: treeCenterX, y: treeCenterY } as d3.HierarchyPointNode<TreeNode>; | |
| 358 | + const fullTreeTransform = getZoomTransform(treeCenter, fullTreeScale); | |
| 359 | + | |
| 360 | + // Final player position - nudged 10% left and 15% down | |
| 361 | + const playerTransform = getZoomTransform(playerNode, 3, 0.1, 0.2); | |
| 362 | + | |
| 363 | + // Animate using zoom transitions | |
| 364 | + svg.transition() | |
| 365 | + .duration(1000) | |
| 366 | + .call(zoom.transform, rootTransform) | |
| 367 | + .transition() | |
| 368 | + .duration(2000) | |
| 369 | + .ease(d3.easeCubicInOut) | |
| 370 | + .call(zoom.transform, fullTreeTransform) | |
| 371 | + .transition() | |
| 372 | + .duration(1000) | |
| 373 | + .call(zoom.transform, fullTreeTransform) | |
| 313 | 374 | .transition() |
| 314 | - .duration(750) | |
| 315 | - .call( | |
| 316 | - zoom.transform as any, | |
| 317 | - d3.zoomIdentity.translate(x, y).scale(scale) | |
| 318 | - ); | |
| 375 | + .duration(1500) | |
| 376 | + .ease(d3.easeCubicInOut) | |
| 377 | + .call(zoom.transform, playerTransform); | |
| 378 | + } else if (playerNode) { | |
| 379 | + // No intro: directly position on player - nudged 10% left and 15% down | |
| 380 | + const playerTransform = getZoomTransform(playerNode, 3, 0.1, 0.2); | |
| 381 | + svg.call(zoom.transform, playerTransform); | |
| 319 | 382 | } |
| 320 | 383 | |
| 321 | - }, [treeData, playerLocation, onNodeClick]); | |
| 384 | + }, [treeData, playerLocation, onNodeClick, playIntro]); | |
| 322 | 385 | |
| 323 | 386 | return ( |
| 324 | 387 | <div ref={containerRef} className="w-full h-full"> |