frontend leaderboard
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
ce663003a5532ef56bbe6fad85d05d173ba680c2- Parents
-
fbe7527 - Tree
d1432aa
ce66300
ce663003a5532ef56bbe6fad85d05d173ba680c2fbe7527
d1432aa| Status | File | + | - |
|---|---|---|---|
| M |
frontend/src/components/Game.tsx
|
28 | 3 |
| A |
frontend/src/components/GameOverModal.tsx
|
208 | 0 |
| M |
frontend/src/hooks/useCommandExecution.ts
|
47 | 63 |
| M |
frontend/src/lib/api.ts
|
60 | 32 |
frontend/src/components/Game.tsxmodified@@ -1,11 +1,12 @@ | |||
| 1 | "use client"; | 1 | "use client"; |
| 2 | 2 | ||
| 3 | -import React, { useEffect, useCallback } from 'react'; | 3 | +import React, { useEffect, useCallback, useState } from 'react'; |
| 4 | import TreeVisualizer from './TreeVisualizer'; | 4 | import TreeVisualizer from './TreeVisualizer'; |
| 5 | import Terminal from './Terminal'; | 5 | import Terminal from './Terminal'; |
| 6 | import HelpModals from './HelpModals'; | 6 | import HelpModals from './HelpModals'; |
| 7 | import GameStatus from './GameStatus'; | 7 | import GameStatus from './GameStatus'; |
| 8 | -import { gameApi, CommandResponse } from '@/lib/api'; | 8 | +import GameOverModal from './GameOverModal'; |
| 9 | +import { gameApi, CommandResponse, GameStats } from '@/lib/api'; | ||
| 9 | 10 | ||
| 10 | // Import our custom hooks | 11 | // Import our custom hooks |
| 11 | import { useGameState } from '@/hooks/useGameState'; | 12 | import { useGameState } from '@/hooks/useGameState'; |
@@ -45,6 +46,10 @@ const Game: React.FC = () => { | |||
| 45 | 46 | ||
| 46 | const { updateTreeDataToShowMole, removeMoleFromTree } = useTreeUtils(); | 47 | const { updateTreeDataToShowMole, removeMoleFromTree } = useTreeUtils(); |
| 47 | 48 | ||
| 49 | + // Game over modal state | ||
| 50 | + const [showGameOver, setShowGameOver] = useState(false); | ||
| 51 | + const [finalStats, setFinalStats] = useState<GameStats | null>(null); | ||
| 52 | + | ||
| 48 | // Handle mole kill animation and updates | 53 | // Handle mole kill animation and updates |
| 49 | const handleMoleKilled = useCallback((response: CommandResponse) => { | 54 | const handleMoleKilled = useCallback((response: CommandResponse) => { |
| 50 | if (!gameState.tree) return; | 55 | if (!gameState.tree) return; |
@@ -92,7 +97,11 @@ const Game: React.FC = () => { | |||
| 92 | gameState.sessionId, | 97 | gameState.sessionId, |
| 93 | updatePlayerLocation, | 98 | updatePlayerLocation, |
| 94 | handleMoleKilled, | 99 | handleMoleKilled, |
| 95 | - updateTreeData | 100 | + updateTreeData, |
| 101 | + (stats) => { // Add this callback for game completion | ||
| 102 | + setFinalStats(stats); | ||
| 103 | + setShowGameOver(true); | ||
| 104 | + } | ||
| 96 | ); | 105 | ); |
| 97 | 106 | ||
| 98 | // Wrap executeCommand to clear the command input | 107 | // Wrap executeCommand to clear the command input |
@@ -185,6 +194,13 @@ const Game: React.FC = () => { | |||
| 185 | executeCommand(`cd ${path}`); | 194 | executeCommand(`cd ${path}`); |
| 186 | }, [executeCommand]); | 195 | }, [executeCommand]); |
| 187 | 196 | ||
| 197 | + // Handler for starting a new game from the modal | ||
| 198 | + const handleNewGameFromModal = () => { | ||
| 199 | + setShowGameOver(false); | ||
| 200 | + setFinalStats(null); | ||
| 201 | + initializeGame(); | ||
| 202 | + }; | ||
| 203 | + | ||
| 188 | // Start game on mount | 204 | // Start game on mount |
| 189 | useEffect(() => { | 205 | useEffect(() => { |
| 190 | initializeGame(); | 206 | initializeGame(); |
@@ -290,6 +306,15 @@ const Game: React.FC = () => { | |||
| 290 | {/* Help Modals */} | 306 | {/* Help Modals */} |
| 291 | <HelpModals {...helpModals} /> | 307 | <HelpModals {...helpModals} /> |
| 292 | 308 | ||
| 309 | + {/* Game Over Modal */} | ||
| 310 | + <GameOverModal | ||
| 311 | + isOpen={showGameOver} | ||
| 312 | + gameStats={finalStats} | ||
| 313 | + sessionId={gameState.sessionId} | ||
| 314 | + onClose={() => setShowGameOver(false)} | ||
| 315 | + onNewGame={handleNewGameFromModal} | ||
| 316 | + /> | ||
| 317 | + | ||
| 293 | {/* Bottom Game Bar */} | 318 | {/* Bottom Game Bar */} |
| 294 | <div className="absolute bottom-0 left-0 right-0 bg-slate-800/90 backdrop-blur-sm border-t border-slate-700 p-3 z-20"> | 319 | <div className="absolute bottom-0 left-0 right-0 bg-slate-800/90 backdrop-blur-sm border-t border-slate-700 p-3 z-20"> |
| 295 | <div className="max-w-7xl mx-auto flex justify-between items-center px-4"> | 320 | <div className="max-w-7xl mx-auto flex justify-between items-center px-4"> |
frontend/src/components/GameOverModal.tsxadded@@ -0,0 +1,208 @@ | |||
| 1 | +import React, { useState, useEffect } from 'react'; | ||
| 2 | +import { GameStats, LeaderboardEntry, gameApi } from '@/lib/api'; | ||
| 3 | + | ||
| 4 | +interface GameOverModalProps { | ||
| 5 | + isOpen: boolean; | ||
| 6 | + gameStats: GameStats | null; | ||
| 7 | + sessionId: number | null; | ||
| 8 | + onClose: () => void; | ||
| 9 | + onNewGame: () => void; | ||
| 10 | +} | ||
| 11 | + | ||
| 12 | +const GameOverModal: React.FC<GameOverModalProps> = ({ | ||
| 13 | + isOpen, | ||
| 14 | + gameStats, | ||
| 15 | + sessionId, | ||
| 16 | + onClose, | ||
| 17 | + onNewGame, | ||
| 18 | +}) => { | ||
| 19 | + const [playerName, setPlayerName] = useState(''); | ||
| 20 | + const [submitted, setSubmitted] = useState(false); | ||
| 21 | + const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]); | ||
| 22 | + const [leaderboardPosition, setLeaderboardPosition] = useState<number | null>(null); | ||
| 23 | + const [showLeaderboard, setShowLeaderboard] = useState(false); | ||
| 24 | + const [loading, setLoading] = useState(false); | ||
| 25 | + | ||
| 26 | + useEffect(() => { | ||
| 27 | + if (isOpen && !submitted) { | ||
| 28 | + // Load leaderboard when modal opens | ||
| 29 | + loadLeaderboard(); | ||
| 30 | + } | ||
| 31 | + }, [isOpen, submitted]); | ||
| 32 | + | ||
| 33 | + const loadLeaderboard = async () => { | ||
| 34 | + try { | ||
| 35 | + const response = await gameApi.getLeaderboard(); | ||
| 36 | + setLeaderboard(response.leaderboard); | ||
| 37 | + } catch (error) { | ||
| 38 | + console.error('Failed to load leaderboard:', error); | ||
| 39 | + } | ||
| 40 | + }; | ||
| 41 | + | ||
| 42 | + const handleSubmit = async (e: React.FormEvent | React.MouseEvent) => { | ||
| 43 | + e.preventDefault(); | ||
| 44 | + if (!sessionId || submitted) return; | ||
| 45 | + | ||
| 46 | + setLoading(true); | ||
| 47 | + try { | ||
| 48 | + const response = await gameApi.savePlayerName( | ||
| 49 | + sessionId, | ||
| 50 | + playerName.trim() || 'Anonymous' | ||
| 51 | + ); | ||
| 52 | + setLeaderboardPosition(response.leaderboard_position); | ||
| 53 | + setSubmitted(true); | ||
| 54 | + await loadLeaderboard(); | ||
| 55 | + setShowLeaderboard(true); | ||
| 56 | + } catch (error) { | ||
| 57 | + console.error('Failed to save score:', error); | ||
| 58 | + } finally { | ||
| 59 | + setLoading(false); | ||
| 60 | + } | ||
| 61 | + }; | ||
| 62 | + | ||
| 63 | + if (!isOpen || !gameStats) return null; | ||
| 64 | + | ||
| 65 | + return ( | ||
| 66 | + <div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50"> | ||
| 67 | + <div className="bg-gray-900 border-2 border-green-500 rounded-lg p-8 max-w-2xl w-full mx-4 shadow-2xl"> | ||
| 68 | + {/* Header */} | ||
| 69 | + <h2 className="text-3xl font-bold text-green-400 mb-6 text-center font-terminal"> | ||
| 70 | + GAME OVER | ||
| 71 | + </h2> | ||
| 72 | + | ||
| 73 | + {/* Score Display */} | ||
| 74 | + {!showLeaderboard && ( | ||
| 75 | + <> | ||
| 76 | + <div className="bg-black/50 border border-green-500/50 rounded-lg p-6 mb-6"> | ||
| 77 | + <div className="grid grid-cols-2 gap-4 text-green-300 font-terminal"> | ||
| 78 | + <div> | ||
| 79 | + <span className="text-gray-500">Final Score:</span> | ||
| 80 | + <div className="text-2xl font-bold text-green-400">{gameStats.score}</div> | ||
| 81 | + </div> | ||
| 82 | + <div> | ||
| 83 | + <span className="text-gray-500">Moles Killed:</span> | ||
| 84 | + <div className="text-2xl font-bold">{gameStats.moles_killed}</div> | ||
| 85 | + </div> | ||
| 86 | + <div> | ||
| 87 | + <span className="text-gray-500">Moles Escaped:</span> | ||
| 88 | + <div className="text-xl text-red-400">{gameStats.moles_escaped}</div> | ||
| 89 | + </div> | ||
| 90 | + <div> | ||
| 91 | + <span className="text-gray-500">Commands Used:</span> | ||
| 92 | + <div className="text-xl">{gameStats.commands_used}</div> | ||
| 93 | + </div> | ||
| 94 | + <div className="col-span-2"> | ||
| 95 | + <span className="text-gray-500">Time Played:</span> | ||
| 96 | + <div className="text-xl">{gameStats.time_taken}</div> | ||
| 97 | + </div> | ||
| 98 | + </div> | ||
| 99 | + </div> | ||
| 100 | + | ||
| 101 | + {/* Name Input */} | ||
| 102 | + {!submitted ? ( | ||
| 103 | + <div className="mb-6"> | ||
| 104 | + <label className="block text-green-400 mb-2 font-terminal"> | ||
| 105 | + Enter your name for the leaderboard: | ||
| 106 | + </label> | ||
| 107 | + <div className="flex gap-2"> | ||
| 108 | + <input | ||
| 109 | + type="text" | ||
| 110 | + value={playerName} | ||
| 111 | + onChange={(e) => setPlayerName(e.target.value)} | ||
| 112 | + onKeyDown={(e) => { | ||
| 113 | + if (e.key === 'Enter') { | ||
| 114 | + e.preventDefault(); | ||
| 115 | + handleSubmit(e); | ||
| 116 | + } | ||
| 117 | + }} | ||
| 118 | + placeholder="Anonymous" | ||
| 119 | + maxLength={20} | ||
| 120 | + className="flex-1 bg-black border border-green-500 text-green-300 px-4 py-2 rounded font-terminal focus:outline-none focus:border-green-400" | ||
| 121 | + autoFocus | ||
| 122 | + disabled={loading} | ||
| 123 | + /> | ||
| 124 | + <button | ||
| 125 | + onClick={handleSubmit} | ||
| 126 | + disabled={loading} | ||
| 127 | + className="px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded font-terminal transition disabled:opacity-50" | ||
| 128 | + > | ||
| 129 | + {loading ? 'Saving...' : 'Submit'} | ||
| 130 | + </button> | ||
| 131 | + </div> | ||
| 132 | + </div> | ||
| 133 | + ) : ( | ||
| 134 | + <div className="text-center mb-6"> | ||
| 135 | + <p className="text-green-400 font-terminal text-lg"> | ||
| 136 | + Score saved! You ranked #{leaderboardPosition} | ||
| 137 | + </p> | ||
| 138 | + <button | ||
| 139 | + onClick={() => setShowLeaderboard(true)} | ||
| 140 | + className="mt-2 text-green-300 hover:text-green-200 underline font-terminal" | ||
| 141 | + > | ||
| 142 | + View Leaderboard | ||
| 143 | + </button> | ||
| 144 | + </div> | ||
| 145 | + )} | ||
| 146 | + </> | ||
| 147 | + )} | ||
| 148 | + | ||
| 149 | + {/* Leaderboard */} | ||
| 150 | + {showLeaderboard && ( | ||
| 151 | + <div className="bg-black/50 border border-green-500/50 rounded-lg p-4 mb-6 max-h-96 overflow-y-auto"> | ||
| 152 | + <h3 className="text-xl font-bold text-green-400 mb-4 font-terminal">Top Scores</h3> | ||
| 153 | + <div className="space-y-2"> | ||
| 154 | + {leaderboard.map((entry) => ( | ||
| 155 | + <div | ||
| 156 | + key={`${entry.player_name}-${entry.completed_at}`} | ||
| 157 | + className={`flex justify-between items-center p-2 rounded ${ | ||
| 158 | + entry.score === gameStats.score && submitted | ||
| 159 | + ? 'bg-green-900/50 border border-green-500' | ||
| 160 | + : 'hover:bg-gray-800/50' | ||
| 161 | + }`} | ||
| 162 | + > | ||
| 163 | + <div className="flex items-center gap-4 font-terminal"> | ||
| 164 | + <span className="text-green-500 font-bold w-8">#{entry.rank}</span> | ||
| 165 | + <span className="text-green-300">{entry.player_name}</span> | ||
| 166 | + </div> | ||
| 167 | + <div className="flex items-center gap-6 text-sm font-terminal"> | ||
| 168 | + <span className="text-green-400">{entry.score} pts</span> | ||
| 169 | + <span className="text-gray-500">{entry.moles_killed} moles</span> | ||
| 170 | + <span className="text-gray-600">{entry.commands_used} cmds</span> | ||
| 171 | + </div> | ||
| 172 | + </div> | ||
| 173 | + ))} | ||
| 174 | + </div> | ||
| 175 | + </div> | ||
| 176 | + )} | ||
| 177 | + | ||
| 178 | + {/* Action Buttons */} | ||
| 179 | + <div className="flex justify-center gap-4"> | ||
| 180 | + <button | ||
| 181 | + onClick={onNewGame} | ||
| 182 | + className="px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg font-terminal transition transform hover:scale-105" | ||
| 183 | + > | ||
| 184 | + New Game | ||
| 185 | + </button> | ||
| 186 | + {!showLeaderboard && submitted && ( | ||
| 187 | + <button | ||
| 188 | + onClick={() => setShowLeaderboard(true)} | ||
| 189 | + className="px-6 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-terminal transition" | ||
| 190 | + > | ||
| 191 | + View Leaderboard | ||
| 192 | + </button> | ||
| 193 | + )} | ||
| 194 | + {showLeaderboard && ( | ||
| 195 | + <button | ||
| 196 | + onClick={() => setShowLeaderboard(false)} | ||
| 197 | + className="px-6 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-terminal transition" | ||
| 198 | + > | ||
| 199 | + Back to Score | ||
| 200 | + </button> | ||
| 201 | + )} | ||
| 202 | + </div> | ||
| 203 | + </div> | ||
| 204 | + </div> | ||
| 205 | + ); | ||
| 206 | +}; | ||
| 207 | + | ||
| 208 | +export default GameOverModal; | ||
frontend/src/hooks/useCommandExecution.tsmodified@@ -1,10 +1,6 @@ | |||
| 1 | +// src/hooks/useCommandExecution.ts | ||
| 1 | import { useState, useCallback } from 'react'; | 2 | import { useState, useCallback } from 'react'; |
| 2 | -import { gameApi, CommandResponse, TreeNode } from '@/lib/api'; | 3 | +import { gameApi, CommandResponse, TreeNode, GameStats } from '@/lib/api'; |
| 3 | - | ||
| 4 | -interface CommandExecutionState { | ||
| 5 | - executing: boolean; | ||
| 6 | - commandHistory: CommandHistoryEntry[]; | ||
| 7 | -} | ||
| 8 | 4 | ||
| 9 | interface CommandHistoryEntry { | 5 | interface CommandHistoryEntry { |
| 10 | command: string; | 6 | command: string; |
@@ -13,99 +9,87 @@ interface CommandHistoryEntry { | |||
| 13 | } | 9 | } |
| 14 | 10 | ||
| 15 | export const useCommandExecution = ( | 11 | export const useCommandExecution = ( |
| 16 | - gameTreeId: number | null, | 12 | + treeId: number | null, |
| 17 | sessionId: number | null, | 13 | sessionId: number | null, |
| 18 | - onLocationChange?: (newPath: string) => void, | 14 | + onLocationChange: (newLocation: string) => void, |
| 19 | - onMoleKilled?: (response: CommandResponse) => void, | 15 | + onMoleKilled: (response: CommandResponse) => void, |
| 20 | - onTreeUpdate?: (updater: (tree: TreeNode) => TreeNode) => void | 16 | + onTreeUpdate: (updater: (tree: TreeNode) => TreeNode) => void, |
| 17 | + onGameComplete?: (stats: GameStats) => void | ||
| 21 | ) => { | 18 | ) => { |
| 22 | - const [state, setState] = useState<CommandExecutionState>({ | 19 | + const [executing, setExecuting] = useState(false); |
| 23 | - executing: false, | 20 | + const [commandHistory, setCommandHistory] = useState<CommandHistoryEntry[]>([]); |
| 24 | - commandHistory: [], | ||
| 25 | - }); | ||
| 26 | 21 | ||
| 27 | const addToHistory = useCallback((entry: CommandHistoryEntry) => { | 22 | const addToHistory = useCallback((entry: CommandHistoryEntry) => { |
| 28 | - setState(prev => ({ | 23 | + setCommandHistory(prev => [...prev, entry]); |
| 29 | - ...prev, | ||
| 30 | - commandHistory: [...prev.commandHistory, entry], | ||
| 31 | - })); | ||
| 32 | }, []); | 24 | }, []); |
| 33 | 25 | ||
| 34 | const clearHistory = useCallback(() => { | 26 | const clearHistory = useCallback(() => { |
| 35 | - setState(prev => ({ | 27 | + setCommandHistory([]); |
| 36 | - ...prev, | ||
| 37 | - commandHistory: [], | ||
| 38 | - })); | ||
| 39 | }, []); | 28 | }, []); |
| 40 | 29 | ||
| 41 | const executeCommand = useCallback(async (cmd: string) => { | 30 | const executeCommand = useCallback(async (cmd: string) => { |
| 42 | - if (!gameTreeId || !cmd.trim() || state.executing) return; | 31 | + if (!treeId || !cmd.trim() || executing) return; |
| 43 | 32 | ||
| 44 | - setState(prev => ({ ...prev, executing: true })); | 33 | + setExecuting(true); |
| 45 | - | ||
| 46 | try { | 34 | try { |
| 47 | - const response = await gameApi.executeCommand( | 35 | + const response = await gameApi.executeCommand(treeId, cmd, sessionId || undefined); |
| 48 | - gameTreeId, | ||
| 49 | - cmd, | ||
| 50 | - sessionId || undefined | ||
| 51 | - ); | ||
| 52 | - | ||
| 53 | - // Build output with timer warnings | ||
| 54 | - let fullOutput = response.output; | ||
| 55 | 36 | ||
| 56 | - // Add timer warnings if present | 37 | + // Build output with timer warnings |
| 38 | + let output = response.output; | ||
| 57 | if (response.timer_warnings && response.timer_warnings.length > 0) { | 39 | if (response.timer_warnings && response.timer_warnings.length > 0) { |
| 58 | - const warnings = response.timer_warnings.map(w => | 40 | + const warnings = response.timer_warnings.map(w => `⚠️ ${w.level}: ${w.message}`).join('\n'); |
| 59 | - `⚠️ ${w.level}: ${w.message}` | 41 | + output = warnings + (output ? '\n' + output : ''); |
| 60 | - ).join('\n'); | ||
| 61 | - fullOutput = warnings + (fullOutput ? '\n' + fullOutput : ''); | ||
| 62 | } | 42 | } |
| 63 | - | 43 | + |
| 64 | - // Add to command history | ||
| 65 | addToHistory({ | 44 | addToHistory({ |
| 66 | command: cmd, | 45 | command: cmd, |
| 67 | - output: fullOutput, | 46 | + output, |
| 68 | success: response.success, | 47 | success: response.success, |
| 69 | }); | 48 | }); |
| 70 | 49 | ||
| 71 | - // Handle location change | 50 | + // Update player location if it changed |
| 72 | - if (response.current_path && onLocationChange) { | 51 | + if (response.current_path) { |
| 73 | onLocationChange(response.current_path); | 52 | onLocationChange(response.current_path); |
| 74 | } | 53 | } |
| 75 | 54 | ||
| 76 | - // Handle mole spawning | 55 | + // Handle mole killed |
| 77 | - if (response.mole_spawned && onMoleKilled) { | 56 | + if (response.mole_spawned) { |
| 78 | - // Format the output to include timer info on new line | ||
| 79 | - if (response.timer_reason && !response.output.includes('New mole detected')) { | ||
| 80 | - response.output += `\nNew mole detected ${response.timer_reason}!`; | ||
| 81 | - } | ||
| 82 | onMoleKilled(response); | 57 | onMoleKilled(response); |
| 83 | } | 58 | } |
| 84 | 59 | ||
| 85 | - // Legacy: Handle game won | 60 | + // Handle game completion |
| 86 | - if (response.game_won && !response.mole_spawned && onTreeUpdate) { | 61 | + if (response.game_completed && response.final_stats && onGameComplete) { |
| 87 | - onTreeUpdate((tree) => ({ | 62 | + onGameComplete(response.final_stats); |
| 88 | - ...tree, | ||
| 89 | - has_mole: true, | ||
| 90 | - })); | ||
| 91 | } | 63 | } |
| 92 | 64 | ||
| 93 | - return response; | 65 | + // Handle mole location updates in tree |
| 94 | - } catch { | 66 | + if (response.new_mole_location) { |
| 67 | + onTreeUpdate((tree) => { | ||
| 68 | + const updateMoleInTree = (node: TreeNode, molePath: string): TreeNode => { | ||
| 69 | + return { | ||
| 70 | + ...node, | ||
| 71 | + has_mole: node.path === molePath, | ||
| 72 | + children: node.children.map(child => updateMoleInTree(child, molePath)) | ||
| 73 | + }; | ||
| 74 | + }; | ||
| 75 | + return updateMoleInTree(tree, response.new_mole_location); | ||
| 76 | + }); | ||
| 77 | + } | ||
| 78 | + } catch (error) { | ||
| 79 | + console.error('Command execution failed:', error); | ||
| 95 | addToHistory({ | 80 | addToHistory({ |
| 96 | command: cmd, | 81 | command: cmd, |
| 97 | - output: 'Error: Failed to execute command. Check your connection.', | 82 | + output: 'Error: Failed to execute command', |
| 98 | success: false, | 83 | success: false, |
| 99 | }); | 84 | }); |
| 100 | - return null; | ||
| 101 | } finally { | 85 | } finally { |
| 102 | - setState(prev => ({ ...prev, executing: false })); | 86 | + setExecuting(false); |
| 103 | } | 87 | } |
| 104 | - }, [gameTreeId, sessionId, state.executing, addToHistory, onLocationChange, onMoleKilled, onTreeUpdate]); | 88 | + }, [treeId, sessionId, executing, onLocationChange, onMoleKilled, onTreeUpdate, onGameComplete, addToHistory]); |
| 105 | 89 | ||
| 106 | return { | 90 | return { |
| 107 | - executing: state.executing, | 91 | + executing, |
| 108 | - commandHistory: state.commandHistory, | 92 | + commandHistory, |
| 109 | executeCommand, | 93 | executeCommand, |
| 110 | addToHistory, | 94 | addToHistory, |
| 111 | clearHistory, | 95 | clearHistory, |
frontend/src/lib/api.tsmodified@@ -39,6 +39,20 @@ export interface MoleDirection { | |||
| 39 | angle: number; | 39 | angle: number; |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | +export interface TimerWarning { | ||
| 43 | + level: string; | ||
| 44 | + message: string; | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +export interface GameStats { | ||
| 48 | + score: number; | ||
| 49 | + moles_killed: number; | ||
| 50 | + moles_escaped: number; | ||
| 51 | + commands_used: number; | ||
| 52 | + time_taken: string; | ||
| 53 | + directories_visited: number; | ||
| 54 | +} | ||
| 55 | + | ||
| 42 | export interface CommandResponse { | 56 | export interface CommandResponse { |
| 43 | command: string; | 57 | command: string; |
| 44 | success: boolean; | 58 | success: boolean; |
@@ -50,6 +64,14 @@ export interface CommandResponse { | |||
| 50 | score?: number; | 64 | score?: number; |
| 51 | moles_killed?: number; | 65 | moles_killed?: number; |
| 52 | new_mole_location?: string; | 66 | new_mole_location?: string; |
| 67 | + timer_remaining?: number; | ||
| 68 | + timer_warnings?: TimerWarning[]; | ||
| 69 | + new_timer?: number; | ||
| 70 | + timer_reason?: string; | ||
| 71 | + timer_distance?: number; | ||
| 72 | + game_completed?: boolean; | ||
| 73 | + session_completed?: boolean; | ||
| 74 | + final_stats?: GameStats; | ||
| 53 | } | 75 | } |
| 54 | 76 | ||
| 55 | export interface GameCreationResponse { | 77 | export interface GameCreationResponse { |
@@ -57,6 +79,9 @@ export interface GameCreationResponse { | |||
| 57 | session_id: number; | 79 | session_id: number; |
| 58 | mole_hint: string; | 80 | mole_hint: string; |
| 59 | home_directory: string; | 81 | home_directory: string; |
| 82 | + initial_timer?: number; | ||
| 83 | + timer_reason?: string; | ||
| 84 | + timer_distance?: number; | ||
| 60 | } | 85 | } |
| 61 | 86 | ||
| 62 | export interface HintResponse { | 87 | export interface HintResponse { |
@@ -95,29 +120,6 @@ export interface CommandReferenceResponse { | |||
| 95 | special_paths: SpecialPath[]; | 120 | special_paths: SpecialPath[]; |
| 96 | } | 121 | } |
| 97 | 122 | ||
| 98 | -export interface TimerWarning { | ||
| 99 | - level: string; | ||
| 100 | - message: string; | ||
| 101 | -} | ||
| 102 | - | ||
| 103 | -export interface CommandResponse { | ||
| 104 | - command: string; | ||
| 105 | - success: boolean; | ||
| 106 | - output: string; | ||
| 107 | - current_path: string; | ||
| 108 | - game_won?: boolean; | ||
| 109 | - mole_spawned?: boolean; | ||
| 110 | - mole_direction?: MoleDirection | null; | ||
| 111 | - score?: number; | ||
| 112 | - moles_killed?: number; | ||
| 113 | - new_mole_location?: string; | ||
| 114 | - timer_remaining?: number; | ||
| 115 | - timer_warnings?: TimerWarning[]; | ||
| 116 | - new_timer?: number; | ||
| 117 | - timer_reason?: string; | ||
| 118 | - timer_distance?: number; | ||
| 119 | -} | ||
| 120 | - | ||
| 121 | export interface TimerStatusResponse { | 123 | export interface TimerStatusResponse { |
| 122 | remaining: number; | 124 | remaining: number; |
| 123 | total: number; | 125 | total: number; |
@@ -148,14 +150,27 @@ export interface CheckTimerResponse { | |||
| 148 | message?: string; | 150 | message?: string; |
| 149 | } | 151 | } |
| 150 | 152 | ||
| 151 | -export interface GameCreationResponse { | 153 | +export interface LeaderboardEntry { |
| 152 | - tree: FileSystemTree; | 154 | + rank: number; |
| 153 | - session_id: number; | 155 | + player_name: string; |
| 154 | - mole_hint: string; | 156 | + score: number; |
| 155 | - home_directory: string; | 157 | + moles_killed: number; |
| 156 | - initial_timer?: number; | 158 | + moles_escaped: number; |
| 157 | - timer_reason?: string; | 159 | + commands_used: number; |
| 158 | - timer_distance?: number; | 160 | + time_taken: string; |
| 161 | + completed_at: string; | ||
| 162 | +} | ||
| 163 | + | ||
| 164 | +export interface LeaderboardResponse { | ||
| 165 | + leaderboard: LeaderboardEntry[]; | ||
| 166 | + total_games: number; | ||
| 167 | +} | ||
| 168 | + | ||
| 169 | +export interface SavePlayerNameResponse { | ||
| 170 | + success: boolean; | ||
| 171 | + player_name: string; | ||
| 172 | + score: number; | ||
| 173 | + leaderboard_position: number; | ||
| 159 | } | 174 | } |
| 160 | 175 | ||
| 161 | export const gameApi = { | 176 | export const gameApi = { |
@@ -200,7 +215,7 @@ export const gameApi = { | |||
| 200 | return response.data; | 215 | return response.data; |
| 201 | }, | 216 | }, |
| 202 | 217 | ||
| 203 | - getTimerStatus: async (treeId: number): Promise<TimerStatusResponse> => { | 218 | + getTimerStatus: async (treeId: number): Promise<TimerStatusResponse> => { |
| 204 | const response = await api.get(`/trees/filesystem-trees/${treeId}/timer_status/`); | 219 | const response = await api.get(`/trees/filesystem-trees/${treeId}/timer_status/`); |
| 205 | return response.data; | 220 | return response.data; |
| 206 | }, | 221 | }, |
@@ -211,4 +226,17 @@ export const gameApi = { | |||
| 211 | const response = await api.get(url + params); | 226 | const response = await api.get(url + params); |
| 212 | return response.data; | 227 | return response.data; |
| 213 | }, | 228 | }, |
| 229 | + | ||
| 230 | + savePlayerName: async (sessionId: number, playerName: string): Promise<SavePlayerNameResponse> => { | ||
| 231 | + const response = await api.post('/trees/game-sessions/save_player_name/', { | ||
| 232 | + session_id: sessionId, | ||
| 233 | + player_name: playerName, | ||
| 234 | + }); | ||
| 235 | + return response.data; | ||
| 236 | + }, | ||
| 237 | + | ||
| 238 | + getLeaderboard: async (): Promise<LeaderboardResponse> => { | ||
| 239 | + const response = await api.get('/trees/game-sessions/leaderboard/'); | ||
| 240 | + return response.data; | ||
| 241 | + }, | ||
| 214 | }; | 242 | }; |