zeroed-some/bashamole / ce66300

Browse files

frontend leaderboard

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ce663003a5532ef56bbe6fad85d05d173ba680c2
Parents
fbe7527
Tree
d1432aa

4 changed files

StatusFile+-
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 @@
11
 "use client";
22
 
3
-import React, { useEffect, useCallback } from 'react';
3
+import React, { useEffect, useCallback, useState } from 'react';
44
 import TreeVisualizer from './TreeVisualizer';
55
 import Terminal from './Terminal';
66
 import HelpModals from './HelpModals';
77
 import GameStatus from './GameStatus';
8
-import { gameApi, CommandResponse } from '@/lib/api';
8
+import GameOverModal from './GameOverModal';
9
+import { gameApi, CommandResponse, GameStats } from '@/lib/api';
910
 
1011
 // Import our custom hooks
1112
 import { useGameState } from '@/hooks/useGameState';
@@ -45,6 +46,10 @@ const Game: React.FC = () => {
4546
 
4647
   const { updateTreeDataToShowMole, removeMoleFromTree } = useTreeUtils();
4748
 
49
+  // Game over modal state
50
+  const [showGameOver, setShowGameOver] = useState(false);
51
+  const [finalStats, setFinalStats] = useState<GameStats | null>(null);
52
+
4853
   // Handle mole kill animation and updates
4954
   const handleMoleKilled = useCallback((response: CommandResponse) => {
5055
     if (!gameState.tree) return;
@@ -92,7 +97,11 @@ const Game: React.FC = () => {
9297
     gameState.sessionId,
9398
     updatePlayerLocation,
9499
     handleMoleKilled,
95
-    updateTreeData
100
+    updateTreeData,
101
+    (stats) => {  // Add this callback for game completion
102
+      setFinalStats(stats);
103
+      setShowGameOver(true);
104
+    }
96105
   );
97106
 
98107
   // Wrap executeCommand to clear the command input
@@ -185,6 +194,13 @@ const Game: React.FC = () => {
185194
     executeCommand(`cd ${path}`);
186195
   }, [executeCommand]);
187196
 
197
+  // Handler for starting a new game from the modal
198
+  const handleNewGameFromModal = () => {
199
+    setShowGameOver(false);
200
+    setFinalStats(null);
201
+    initializeGame();
202
+  };
203
+
188204
   // Start game on mount
189205
   useEffect(() => {
190206
     initializeGame();
@@ -290,6 +306,15 @@ const Game: React.FC = () => {
290306
       {/* Help Modals */}
291307
       <HelpModals {...helpModals} />
292308
 
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
+
293318
       {/* Bottom Game Bar */}
294319
       <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">
295320
         <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
12
 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';
84
 
95
 interface CommandHistoryEntry {
106
   command: string;
@@ -13,99 +9,87 @@ interface CommandHistoryEntry {
139
 }
1410
 
1511
 export const useCommandExecution = (
16
-  gameTreeId: number | null,
12
+  treeId: number | null,
1713
   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
2118
 ) => {
22
-  const [state, setState] = useState<CommandExecutionState>({
23
-    executing: false,
24
-    commandHistory: [],
25
-  });
19
+  const [executing, setExecuting] = useState(false);
20
+  const [commandHistory, setCommandHistory] = useState<CommandHistoryEntry[]>([]);
2621
 
2722
   const addToHistory = useCallback((entry: CommandHistoryEntry) => {
28
-    setState(prev => ({
29
-      ...prev,
30
-      commandHistory: [...prev.commandHistory, entry],
31
-    }));
23
+    setCommandHistory(prev => [...prev, entry]);
3224
   }, []);
3325
 
3426
   const clearHistory = useCallback(() => {
35
-    setState(prev => ({
36
-      ...prev,
37
-      commandHistory: [],
38
-    }));
27
+    setCommandHistory([]);
3928
   }, []);
4029
 
4130
   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;
4532
 
33
+    setExecuting(true);
4634
     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);
5236
       
5337
       // Build output with timer warnings
54
-      let fullOutput = response.output;
55
-      
56
-      // Add timer warnings if present
38
+      let output = response.output;
5739
       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 : '');
6242
       }
6343
       
64
-      // Add to command history
6544
       addToHistory({
6645
         command: cmd,
67
-        output: fullOutput,
46
+        output,
6847
         success: response.success,
6948
       });
7049
 
71
-      // Handle location change
72
-      if (response.current_path && onLocationChange) {
50
+      // Update player location if it changed
51
+      if (response.current_path) {
7352
         onLocationChange(response.current_path);
7453
       }
7554
 
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) {
8257
         onMoleKilled(response);
8358
       }
8459
 
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);
9163
       }
9264
 
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);
9580
       addToHistory({
9681
         command: cmd,
97
-        output: 'Error: Failed to execute command. Check your connection.',
82
+        output: 'Error: Failed to execute command',
9883
         success: false,
9984
       });
100
-      return null;
10185
     } finally {
102
-      setState(prev => ({ ...prev, executing: false }));
86
+      setExecuting(false);
10387
     }
104
-  }, [gameTreeId, sessionId, state.executing, addToHistory, onLocationChange, onMoleKilled, onTreeUpdate]);
88
+  }, [treeId, sessionId, executing, onLocationChange, onMoleKilled, onTreeUpdate, onGameComplete, addToHistory]);
10589
 
10690
   return {
107
-    executing: state.executing,
108
-    commandHistory: state.commandHistory,
91
+    executing,
92
+    commandHistory,
10993
     executeCommand,
11094
     addToHistory,
11195
     clearHistory,
frontend/src/lib/api.tsmodified
@@ -39,6 +39,20 @@ export interface MoleDirection {
3939
   angle: number;
4040
 }
4141
 
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
+
4256
 export interface CommandResponse {
4357
   command: string;
4458
   success: boolean;
@@ -50,6 +64,14 @@ export interface CommandResponse {
5064
   score?: number;
5165
   moles_killed?: number;
5266
   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;
5375
 }
5476
 
5577
 export interface GameCreationResponse {
@@ -57,6 +79,9 @@ export interface GameCreationResponse {
5779
   session_id: number;
5880
   mole_hint: string;
5981
   home_directory: string;
82
+  initial_timer?: number;
83
+  timer_reason?: string;
84
+  timer_distance?: number;
6085
 }
6186
 
6287
 export interface HintResponse {
@@ -95,29 +120,6 @@ export interface CommandReferenceResponse {
95120
   special_paths: SpecialPath[];
96121
 }
97122
 
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
-
121123
 export interface TimerStatusResponse {
122124
   remaining: number;
123125
   total: number;
@@ -148,14 +150,27 @@ export interface CheckTimerResponse {
148150
   message?: string;
149151
 }
150152
 
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;
159174
 }
160175
 
161176
 export const gameApi = {
@@ -211,4 +226,17 @@ export const gameApi = {
211226
     const response = await api.get(url + params);
212227
     return response.data;
213228
   },
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
+  },
214242
 };