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 | 1 | "use client"; |
| 2 | 2 | |
| 3 | -import React, { useEffect, useCallback } from 'react'; | |
| 3 | +import React, { useEffect, useCallback, useState } from 'react'; | |
| 4 | 4 | import TreeVisualizer from './TreeVisualizer'; |
| 5 | 5 | import Terminal from './Terminal'; |
| 6 | 6 | import HelpModals from './HelpModals'; |
| 7 | 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 | 11 | // Import our custom hooks |
| 11 | 12 | import { useGameState } from '@/hooks/useGameState'; |
@@ -45,6 +46,10 @@ const Game: React.FC = () => { | ||
| 45 | 46 | |
| 46 | 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 | 53 | // Handle mole kill animation and updates |
| 49 | 54 | const handleMoleKilled = useCallback((response: CommandResponse) => { |
| 50 | 55 | if (!gameState.tree) return; |
@@ -92,7 +97,11 @@ const Game: React.FC = () => { | ||
| 92 | 97 | gameState.sessionId, |
| 93 | 98 | updatePlayerLocation, |
| 94 | 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 | 107 | // Wrap executeCommand to clear the command input |
@@ -185,6 +194,13 @@ const Game: React.FC = () => { | ||
| 185 | 194 | executeCommand(`cd ${path}`); |
| 186 | 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 | 204 | // Start game on mount |
| 189 | 205 | useEffect(() => { |
| 190 | 206 | initializeGame(); |
@@ -290,6 +306,15 @@ const Game: React.FC = () => { | ||
| 290 | 306 | {/* Help Modals */} |
| 291 | 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 | 318 | {/* Bottom Game Bar */} |
| 294 | 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 | 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 | 2 | import { useState, useCallback } from 'react'; |
| 2 | -import { gameApi, CommandResponse, TreeNode } from '@/lib/api'; | |
| 3 | - | |
| 4 | -interface CommandExecutionState { | |
| 5 | - executing: boolean; | |
| 6 | - commandHistory: CommandHistoryEntry[]; | |
| 7 | -} | |
| 3 | +import { gameApi, CommandResponse, TreeNode, GameStats } from '@/lib/api'; | |
| 8 | 4 | |
| 9 | 5 | interface CommandHistoryEntry { |
| 10 | 6 | command: string; |
@@ -13,99 +9,87 @@ interface CommandHistoryEntry { | ||
| 13 | 9 | } |
| 14 | 10 | |
| 15 | 11 | export const useCommandExecution = ( |
| 16 | - gameTreeId: number | null, | |
| 12 | + treeId: number | null, | |
| 17 | 13 | sessionId: number | null, |
| 18 | - onLocationChange?: (newPath: string) => void, | |
| 19 | - onMoleKilled?: (response: CommandResponse) => void, | |
| 20 | - onTreeUpdate?: (updater: (tree: TreeNode) => TreeNode) => void | |
| 14 | + onLocationChange: (newLocation: string) => void, | |
| 15 | + onMoleKilled: (response: CommandResponse) => void, | |
| 16 | + onTreeUpdate: (updater: (tree: TreeNode) => TreeNode) => void, | |
| 17 | + onGameComplete?: (stats: GameStats) => void | |
| 21 | 18 | ) => { |
| 22 | - const [state, setState] = useState<CommandExecutionState>({ | |
| 23 | - executing: false, | |
| 24 | - commandHistory: [], | |
| 25 | - }); | |
| 19 | + const [executing, setExecuting] = useState(false); | |
| 20 | + const [commandHistory, setCommandHistory] = useState<CommandHistoryEntry[]>([]); | |
| 26 | 21 | |
| 27 | 22 | const addToHistory = useCallback((entry: CommandHistoryEntry) => { |
| 28 | - setState(prev => ({ | |
| 29 | - ...prev, | |
| 30 | - commandHistory: [...prev.commandHistory, entry], | |
| 31 | - })); | |
| 23 | + setCommandHistory(prev => [...prev, entry]); | |
| 32 | 24 | }, []); |
| 33 | 25 | |
| 34 | 26 | const clearHistory = useCallback(() => { |
| 35 | - setState(prev => ({ | |
| 36 | - ...prev, | |
| 37 | - commandHistory: [], | |
| 38 | - })); | |
| 27 | + setCommandHistory([]); | |
| 39 | 28 | }, []); |
| 40 | 29 | |
| 41 | 30 | const executeCommand = useCallback(async (cmd: string) => { |
| 42 | - if (!gameTreeId || !cmd.trim() || state.executing) return; | |
| 43 | - | |
| 44 | - setState(prev => ({ ...prev, executing: true })); | |
| 31 | + if (!treeId || !cmd.trim() || executing) return; | |
| 45 | 32 | |
| 33 | + setExecuting(true); | |
| 46 | 34 | try { |
| 47 | - const response = await gameApi.executeCommand( | |
| 48 | - gameTreeId, | |
| 49 | - cmd, | |
| 50 | - sessionId || undefined | |
| 51 | - ); | |
| 35 | + const response = await gameApi.executeCommand(treeId, cmd, sessionId || undefined); | |
| 52 | 36 | |
| 53 | 37 | // Build output with timer warnings |
| 54 | - let fullOutput = response.output; | |
| 55 | - | |
| 56 | - // Add timer warnings if present | |
| 38 | + let output = response.output; | |
| 57 | 39 | if (response.timer_warnings && response.timer_warnings.length > 0) { |
| 58 | - const warnings = response.timer_warnings.map(w => | |
| 59 | - `⚠️ ${w.level}: ${w.message}` | |
| 60 | - ).join('\n'); | |
| 61 | - fullOutput = warnings + (fullOutput ? '\n' + fullOutput : ''); | |
| 40 | + const warnings = response.timer_warnings.map(w => `⚠️ ${w.level}: ${w.message}`).join('\n'); | |
| 41 | + output = warnings + (output ? '\n' + output : ''); | |
| 62 | 42 | } |
| 63 | 43 | |
| 64 | - // Add to command history | |
| 65 | 44 | addToHistory({ |
| 66 | 45 | command: cmd, |
| 67 | - output: fullOutput, | |
| 46 | + output, | |
| 68 | 47 | success: response.success, |
| 69 | 48 | }); |
| 70 | 49 | |
| 71 | - // Handle location change | |
| 72 | - if (response.current_path && onLocationChange) { | |
| 50 | + // Update player location if it changed | |
| 51 | + if (response.current_path) { | |
| 73 | 52 | onLocationChange(response.current_path); |
| 74 | 53 | } |
| 75 | 54 | |
| 76 | - // Handle mole spawning | |
| 77 | - if (response.mole_spawned && onMoleKilled) { | |
| 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 | - } | |
| 55 | + // Handle mole killed | |
| 56 | + if (response.mole_spawned) { | |
| 82 | 57 | onMoleKilled(response); |
| 83 | 58 | } |
| 84 | 59 | |
| 85 | - // Legacy: Handle game won | |
| 86 | - if (response.game_won && !response.mole_spawned && onTreeUpdate) { | |
| 87 | - onTreeUpdate((tree) => ({ | |
| 88 | - ...tree, | |
| 89 | - has_mole: true, | |
| 90 | - })); | |
| 60 | + // Handle game completion | |
| 61 | + if (response.game_completed && response.final_stats && onGameComplete) { | |
| 62 | + onGameComplete(response.final_stats); | |
| 91 | 63 | } |
| 92 | 64 | |
| 93 | - return response; | |
| 94 | - } catch { | |
| 65 | + // Handle mole location updates in tree | |
| 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 | 80 | addToHistory({ |
| 96 | 81 | command: cmd, |
| 97 | - output: 'Error: Failed to execute command. Check your connection.', | |
| 82 | + output: 'Error: Failed to execute command', | |
| 98 | 83 | success: false, |
| 99 | 84 | }); |
| 100 | - return null; | |
| 101 | 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 | 90 | return { |
| 107 | - executing: state.executing, | |
| 108 | - commandHistory: state.commandHistory, | |
| 91 | + executing, | |
| 92 | + commandHistory, | |
| 109 | 93 | executeCommand, |
| 110 | 94 | addToHistory, |
| 111 | 95 | clearHistory, |
frontend/src/lib/api.tsmodified@@ -39,6 +39,20 @@ export interface MoleDirection { | ||
| 39 | 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 | 56 | export interface CommandResponse { |
| 43 | 57 | command: string; |
| 44 | 58 | success: boolean; |
@@ -50,6 +64,14 @@ export interface CommandResponse { | ||
| 50 | 64 | score?: number; |
| 51 | 65 | moles_killed?: number; |
| 52 | 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 | 77 | export interface GameCreationResponse { |
@@ -57,6 +79,9 @@ export interface GameCreationResponse { | ||
| 57 | 79 | session_id: number; |
| 58 | 80 | mole_hint: string; |
| 59 | 81 | home_directory: string; |
| 82 | + initial_timer?: number; | |
| 83 | + timer_reason?: string; | |
| 84 | + timer_distance?: number; | |
| 60 | 85 | } |
| 61 | 86 | |
| 62 | 87 | export interface HintResponse { |
@@ -95,29 +120,6 @@ export interface CommandReferenceResponse { | ||
| 95 | 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 | 123 | export interface TimerStatusResponse { |
| 122 | 124 | remaining: number; |
| 123 | 125 | total: number; |
@@ -148,14 +150,27 @@ export interface CheckTimerResponse { | ||
| 148 | 150 | message?: string; |
| 149 | 151 | } |
| 150 | 152 | |
| 151 | -export interface GameCreationResponse { | |
| 152 | - tree: FileSystemTree; | |
| 153 | - session_id: number; | |
| 154 | - mole_hint: string; | |
| 155 | - home_directory: string; | |
| 156 | - initial_timer?: number; | |
| 157 | - timer_reason?: string; | |
| 158 | - timer_distance?: number; | |
| 153 | +export interface LeaderboardEntry { | |
| 154 | + rank: number; | |
| 155 | + player_name: string; | |
| 156 | + score: number; | |
| 157 | + moles_killed: number; | |
| 158 | + moles_escaped: number; | |
| 159 | + commands_used: 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 | 176 | export const gameApi = { |
@@ -211,4 +226,17 @@ export const gameApi = { | ||
| 211 | 226 | const response = await api.get(url + params); |
| 212 | 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 | }; |