Skip to content

Commit bb9c5cc

Browse files
DIodideclaudeAmmaar-Alam
authored
Add session win tally for private matches (#14)
* Add session win tally for private match play-again sessions Track and display a per-player win counter across play-again cycles in private matches. The winner of each race (fastest completion time) gets their count incremented. The tally is shown as a gold badge above player names in the lobby, race status bar, and results screen. Wins carry over through play-again and reset when leaving the session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix private session win tally ranking * Clear stale inactivity warning on ready * Harden session win review fixes --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Ammaar Alam <ammaar@princeton.edu>
1 parent 537080e commit bb9c5cc

10 files changed

Lines changed: 413 additions & 36 deletions

File tree

client/src/components/PlayerStatusBar.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,19 @@
203203
display: block;
204204
}
205205

206+
.session-wins-badge {
207+
display: inline-block;
208+
font-size: 0.7rem;
209+
font-weight: 700;
210+
color: #FFD700;
211+
background: rgba(245, 128, 37, 0.15);
212+
border: 1px solid rgba(245, 128, 37, 0.3);
213+
border-radius: 10px;
214+
padding: 0.05rem 0.45rem;
215+
line-height: 1.3;
216+
white-space: nowrap;
217+
}
218+
206219
.player-name {
207220
font-weight: 600;
208221
color: var(--text-color);

client/src/components/PlayerStatusBar.jsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ function PlayerStatusBar({
1313
countdownActive = false,
1414
waitingForMinimumPlayers = false,
1515
readinessSummary = null,
16-
readinessDetail = null
16+
readinessDetail = null,
17+
sessionWins = null
1718
}) {
1819
const [enlargedAvatar, setEnlargedAvatar] = useState(null);
1920
const { authenticated, user } = useAuth();
@@ -232,6 +233,11 @@ function PlayerStatusBar({
232233
/>
233234
</div>
234235
<div className="player-text">
236+
{sessionWins && sessionWins[player.netid] > 0 && (
237+
<span className="session-wins-badge">
238+
{sessionWins[player.netid]} {sessionWins[player.netid] === 1 ? 'win' : 'wins'}
239+
</span>
240+
)}
235241
<span className="player-name">{player.netid}</span>
236242
{/* Determine the title to display */}
237243
{(() => {

client/src/components/Results.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,19 @@
210210
align-items: flex-start;
211211
}
212212

213+
.results-wins {
214+
display: inline-block;
215+
font-size: 0.75rem;
216+
font-weight: 700;
217+
color: #FFD700;
218+
background: rgba(245, 128, 37, 0.15);
219+
border: 1px solid rgba(245, 128, 37, 0.3);
220+
border-radius: 10px;
221+
padding: 0.1rem 0.55rem;
222+
margin-bottom: 0.35rem;
223+
white-space: nowrap;
224+
}
225+
213226
.winner-header {
214227
display: flex;
215228
align-items: center;

client/src/components/Results.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import ProfileModal from './ProfileModal.jsx';
1313
function Results({ onShowLeaderboard }) {
1414
const navigate = useNavigate();
1515
const { raceState, typingState, resetRace, joinPublicRace, playAgain } = useRace();
16+
const sessionWins = raceState.sessionWins || {};
1617
const { isRunning, endTutorial } = useTutorial();
1718
const { user } = useAuth();
1819
// State for profile modal
@@ -251,6 +252,11 @@ function Results({ onShowLeaderboard }) {
251252
/>
252253
</div>
253254
<div className="winner-details">
255+
{raceState.type === 'private' && sessionWins[winner.netid] > 0 && (
256+
<div className="session-wins-badge results-wins">
257+
{sessionWins[winner.netid]} {sessionWins[winner.netid] === 1 ? 'win' : 'wins'}
258+
</div>
259+
)}
254260
<div className="winner-header">
255261
<div className="winner-trophy"><i className="bi bi-trophy"></i></div>
256262
<div className="winner-netid">{winner.netid}</div>
@@ -302,6 +308,11 @@ function Results({ onShowLeaderboard }) {
302308
/>
303309
</div>
304310
<div className="result-text">
311+
{raceState.type === 'private' && sessionWins[result.netid] > 0 && (
312+
<span className="session-wins-badge results-wins">
313+
{sessionWins[result.netid]} {sessionWins[result.netid] === 1 ? 'win' : 'wins'}
314+
</span>
315+
)}
305316
<div className="result-netid">{result.netid}</div>
306317
{(() => {
307318
const titlesList = resultTitlesMap[result.netid] || [];

client/src/context/RaceContext.jsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ export const RaceProvider = ({ children }) => {
9999
testDuration: 15,
100100
// Add other potential settings here
101101
},
102-
countdown: null // Track countdown seconds
102+
countdown: null, // Track countdown seconds
103+
sessionWins: {} // Win tally per player across play-again sessions { netid: count }
103104
});
104105

105106
// Explicit state for TestConfigurator to avoid passing setRaceState
@@ -196,6 +197,14 @@ export const RaceProvider = ({ children }) => {
196197
redirectToHome: false
197198
});
198199

200+
const clearInactivityWarning = useCallback(() => {
201+
setInactivityState(prev => (
202+
prev.warning
203+
? { ...prev, warning: false, warningMessage: '' }
204+
: prev
205+
));
206+
}, []);
207+
199208
// Update session storage when inactivity state changes
200209
useEffect(() => {
201210
saveInactivityState(inactivityState);
@@ -286,11 +295,20 @@ export const RaceProvider = ({ children }) => {
286295
hostNetId: data.hostNetId || null, // Explicitly store hostNetId
287296
snippet: data.snippet ? { ...data.snippet, text: sanitizeSnippetText(data.snippet.text) } : null,
288297
settings: data.settings || prev.settings, // Store settings from server
289-
players: data.players || []
298+
players: data.players || [],
299+
sessionWins: data.sessionWins || {}
290300
}));
291301
};
292302

293303
const handlePlayersUpdate = (data) => {
304+
const currentUserReady = Boolean(
305+
user?.netid && data.players?.some(player => player.netid === user.netid && player.ready)
306+
);
307+
308+
if (currentUserReady) {
309+
clearInactivityWarning();
310+
}
311+
294312
setRaceState(prev => {
295313
// For quick-match public races, if the race is already in progress, we want to keep
296314
// any players previously marked as disconnected even if they are no longer in the
@@ -358,6 +376,8 @@ export const RaceProvider = ({ children }) => {
358376
});
359377

360378
if (shouldResetTyping) {
379+
clearInactivityWarning();
380+
361381
// Reset typing state
362382
setTypingState({
363383
input: '',
@@ -419,7 +439,8 @@ export const RaceProvider = ({ children }) => {
419439
return {
420440
...prev,
421441
inProgress: false,
422-
completed: true
442+
completed: true,
443+
sessionWins: data.sessionWins || {}
423444
};
424445
});
425446
};
@@ -600,7 +621,8 @@ export const RaceProvider = ({ children }) => {
600621
},
601622
snippetFilters: data.settings?.snippetFilters || { difficulty: 'all', type: 'all', department: 'all' },
602623
settings: data.settings || { testMode: 'snippet', testDuration: 15 },
603-
countdown: null
624+
countdown: null,
625+
sessionWins: data.sessionWins || {}
604626
});
605627
};
606628

@@ -653,7 +675,7 @@ export const RaceProvider = ({ children }) => {
653675
socket.off('snippetNotFound', handleSnippetNotFound); // Cleanup snippet not found listener
654676
};
655677
// Add raceState.snippet?.id to dependency array to reset typing state on snippet change
656-
}, [socket, connected, raceState.type, raceState.manuallyStarted, raceState.snippet?.id, resetAnticheatState]);
678+
}, [socket, connected, raceState.type, raceState.manuallyStarted, raceState.snippet?.id, resetAnticheatState, clearInactivityWarning, user?.netid]);
657679

658680
// Methods for race actions
659681
const joinPracticeMode = () => {
@@ -696,6 +718,7 @@ export const RaceProvider = ({ children }) => {
696718

697719
const setPlayerReady = () => {
698720
if (!socket || !connected) return;
721+
clearInactivityWarning();
699722
// console.log('Setting player ready...');
700723
socket.emit('player:ready');
701724
};
@@ -1075,7 +1098,8 @@ export const RaceProvider = ({ children }) => {
10751098
testMode: 'snippet',
10761099
testDuration: 15,
10771100
},
1078-
countdown: null
1101+
countdown: null,
1102+
sessionWins: {}
10791103
});
10801104

10811105
setTypingState({
@@ -1088,7 +1112,7 @@ export const RaceProvider = ({ children }) => {
10881112
accuracy: 0,
10891113
lockedPosition: 0
10901114
});
1091-
1115+
10921116
// Clear race state from session storage
10931117
sessionStorage.removeItem('raceState');
10941118
};

client/src/pages/Lobby.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,19 @@
443443
gap: 1rem;
444444
}
445445

446+
.lobby-wins {
447+
display: inline-block;
448+
font-size: 0.7rem;
449+
font-weight: 700;
450+
color: #FFD700;
451+
background: rgba(245, 128, 37, 0.15);
452+
border: 1px solid rgba(245, 128, 37, 0.3);
453+
border-radius: 10px;
454+
padding: 0.1rem 0.5rem;
455+
margin: 0.4rem auto 0;
456+
white-space: nowrap;
457+
}
458+
446459
.lobby-page .player-card {
447460
background: linear-gradient(135deg, var(--container-color) 0%, rgba(245, 128, 37, 0.05) 100%);
448461
border-radius: 10px;

client/src/pages/Lobby.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,11 @@ function Lobby() {
310310
<div className="player-grid">
311311
{raceState.players?.map(player => (
312312
<div key={player.netid} className="player-card">
313+
{raceState.sessionWins?.[player.netid] > 0 && (
314+
<span className="session-wins-badge lobby-wins">
315+
{raceState.sessionWins[player.netid]} {raceState.sessionWins[player.netid] === 1 ? 'win' : 'wins'}
316+
</span>
317+
)}
313318
<ProfileWidget
314319
// Pass user object including avg_wpm fetched from server
315320
user={{ netid: player.netid, avatar_url: player.avatar_url, avg_wpm: player.avg_wpm }}

client/src/pages/Race.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ function Race() {
212212
players={players}
213213
isRaceInProgress={raceState.inProgress}
214214
currentUser={window.user}
215+
sessionWins={raceState.type === 'private' ? raceState.sessionWins : null}
215216
onReadyClick={setPlayerReady}
216217
countdownActive={countdownActive}
217218
waitingForMinimumPlayers={shouldShowLobbyStatus && waitingForMinimumPlayers}

0 commit comments

Comments
 (0)