Skip to content

Commit 159ffec

Browse files
authored
Merge pull request #13 from acrosman/bug/fix-piggie-end-game
Update end game modal for fast piggie
2 parents 373b999 + 9d65ceb commit 159ffec

4 files changed

Lines changed: 127 additions & 85 deletions

File tree

app/games/fast-piggie/index.js

Lines changed: 38 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ let _feedbackEl = null;
191191
let _flashEl = null;
192192
let _instructionsEl = null;
193193
let _gameAreaEl = null;
194+
let _endPanelEl = null;
195+
let _playAgainBtn = null;
196+
let _returnToMenuBtn = null;
197+
let _finalScoreEl = null;
198+
let _finalHighScoreEl = null;
194199

195200
// Game state
196201
let _images = null; // [commonImage, outlierImage]
@@ -443,6 +448,18 @@ function _getCorrectWedgeIndex(round) {
443448
return outlierWedgeIndex;
444449
}
445450

451+
/**
452+
* Show the end-game panel with the latest score summary.
453+
* @param {number} score
454+
* @param {number} highScore
455+
*/
456+
function _showEndPanel(score, highScore) {
457+
if (_gameAreaEl) _gameAreaEl.hidden = true;
458+
if (_endPanelEl) _endPanelEl.hidden = false;
459+
if (_finalScoreEl) _finalScoreEl.textContent = String(score);
460+
if (_finalHighScoreEl) _finalHighScoreEl.textContent = String(highScore);
461+
}
462+
446463
/**
447464
* Resolves the round after a wedge is selected, updates state and feedback.
448465
* @param {number} wedge
@@ -537,14 +554,19 @@ export default {
537554
init(container) {
538555
_instructionsEl = container.querySelector('#fp-instructions');
539556
_gameAreaEl = container.querySelector('#fp-game-area');
557+
_endPanelEl = container.querySelector('#fp-end-panel');
540558
_startBtn = container.querySelector('#fp-start-btn');
541559
_canvas = container.querySelector('#fp-canvas');
542560
_ctx = _canvas.getContext('2d');
543561
_stopBtn = container.querySelector('#fp-stop-btn');
562+
_playAgainBtn = container.querySelector('#fp-play-again-btn');
563+
_returnToMenuBtn = container.querySelector('#fp-return-btn');
544564
_scoreEl = container.querySelector('#fp-score');
545565
_roundEl = container.querySelector('#fp-round-count');
546566
_feedbackEl = container.querySelector('#fp-feedback');
547567
_flashEl = container.querySelector('#fp-flash');
568+
_finalScoreEl = container.querySelector('#fp-final-score');
569+
_finalHighScoreEl = container.querySelector('#fp-final-high-score');
548570

549571
// Pre-load images
550572
const base = new URL('../fast-piggie/images/', import.meta.url).href;
@@ -564,6 +586,15 @@ export default {
564586
_canvas.addEventListener('mouseleave', _handleMouseLeave);
565587
_canvas.addEventListener('keydown', _handleKeydown);
566588
_stopBtn.addEventListener('click', () => this.stop());
589+
if (_playAgainBtn) {
590+
_playAgainBtn.addEventListener('click', () => {
591+
this.reset();
592+
this.start();
593+
});
594+
}
595+
if (_returnToMenuBtn) {
596+
_returnToMenuBtn.addEventListener('click', () => _returnToMainMenu());
597+
}
567598
},
568599

569600
/**
@@ -572,6 +603,7 @@ export default {
572603
start() {
573604
if (_instructionsEl) _instructionsEl.hidden = true;
574605
if (_gameAreaEl) _gameAreaEl.hidden = false;
606+
if (_endPanelEl) _endPanelEl.hidden = true;
575607
game.startGame();
576608
_updateStats();
577609
_runRound();
@@ -590,8 +622,7 @@ export default {
590622
const result = game.stopGame();
591623

592624
let highScore = result.score;
593-
let previousHigh = 0;
594-
let sessionsPlayed = 1;
625+
let bestStats = game.getBestStats();
595626
// Return a promise for test compatibility
596627
return (async () => {
597628
try {
@@ -617,20 +648,18 @@ export default {
617648
} catch {
618649
// If load fails, still proceed to save with defaults
619650
}
620-
previousHigh = gameEntry.highScore;
621651
// Only update highScore if the new score is higher
622652
highScore = Math.max(gameEntry.highScore || 0, result.score);
623-
sessionsPlayed = (gameEntry.sessionsPlayed || 0) + 1;
624653
// Get best stats from game logic
625-
const bestStats = game.getBestStats();
654+
bestStats = game.getBestStats();
626655
const updated = {
627656
...existing,
628657
games: {
629658
...existing.games,
630659
'fast-piggie': {
631660
...gameEntry,
632661
highScore,
633-
sessionsPlayed,
662+
sessionsPlayed: (gameEntry.sessionsPlayed || 0) + 1,
634663
lastPlayed: new Date().toISOString(),
635664
maxLevel:
636665
typeof bestStats.maxScore === 'number'
@@ -660,13 +689,8 @@ export default {
660689
// Swallow all errors from progress load/save
661690
}
662691

663-
// Show accessible summary modal (skip in test env)
664-
if (typeof document !== 'undefined' &&
665-
document.body &&
666-
!document.body.classList.contains('jest-testing')
667-
) {
668-
_showSummaryModal(result.score, previousHigh, highScore);
669-
} else if (_feedbackEl) {
692+
_showEndPanel(result.score, highScore);
693+
if (_feedbackEl) {
670694
_feedbackEl.textContent = `Game over! Final score: ${result.score} in ${result.roundsPlayed} rounds.`;
671695
}
672696
_stopBtn.hidden = true;
@@ -695,78 +719,14 @@ export default {
695719
_stopBtn.hidden = false;
696720
if (_instructionsEl) _instructionsEl.hidden = false;
697721
if (_gameAreaEl) _gameAreaEl.hidden = true;
722+
if (_endPanelEl) _endPanelEl.hidden = true;
698723
},
699724
};
700725

701-
/**
702-
* Show an accessible summary modal with current and best score, and return button.
703-
* @param {number} score
704-
* @param {number} previousHigh
705-
* @param {number} highScore
706-
*/
707-
function _showSummaryModal(score, previousHigh, highScore) {
708-
// Remove any existing modal
709-
const oldModal = document.getElementById('fp-summary-modal');
710-
if (oldModal) oldModal.remove();
711-
712-
// Get best stats for this session
713-
const bestStats = game.getBestStats();
714-
715-
const modal = document.createElement('div');
716-
modal.id = 'fp-summary-modal';
717-
modal.className = 'fp-modal';
718-
modal.setAttribute('role', 'dialog');
719-
modal.setAttribute('aria-modal', 'true');
720-
modal.setAttribute('aria-labelledby', 'fp-summary-title');
721-
modal.setAttribute('tabindex', '-1');
722-
723-
modal.innerHTML = `
724-
<div class="fp-modal-content">
725-
<h2 id="fp-summary-title">Game Over</h2>
726-
<p>Your score: <strong>${score}</strong></p>
727-
<p>Personal best: <strong>${highScore}</strong></p>
728-
<hr />
729-
<h3>Session Bests</h3>
730-
<ul>
731-
<li>Max score: <strong>${bestStats.maxScore}</strong></li>
732-
<li>Most rounds: <strong>${bestStats.mostRounds}</strong></li>
733-
<li>Most guinea pigs in a round: <strong>${bestStats.mostGuineaPigs}</strong></li>
734-
<li>Top speed (ms): <strong>${bestStats.topSpeedMs !== null ? bestStats.topSpeedMs : '—'}</strong></li>
735-
</ul>
736-
<button id="fp-return-btn" class="fp-btn fp-btn--primary">Return to Main Menu</button>
737-
</div>
738-
`;
739-
740-
// Trap focus inside modal
741-
modal.addEventListener('keydown', (e) => {
742-
if (e.key === 'Tab') {
743-
const focusable = modal.querySelectorAll('button');
744-
if (focusable.length) {
745-
e.preventDefault();
746-
focusable[0].focus();
747-
}
748-
}
749-
if (e.key === 'Escape') {
750-
_returnToMainMenu();
751-
}
752-
});
753-
754-
// Return button handler
755-
modal.querySelector('#fp-return-btn').addEventListener('click', _returnToMainMenu);
756-
757-
// Add modal to DOM and focus
758-
document.body.appendChild(modal);
759-
setTimeout(() => {
760-
modal.focus();
761-
}, 0);
762-
}
763-
764726
/**
765727
* Return to the main game selection screen, removing modal and resetting UI.
766728
*/
767729
function _returnToMainMenu() {
768-
const modal = document.getElementById('fp-summary-modal');
769-
if (modal) modal.remove();
770730
// Dispatch a custom event to notify the app shell to return to main menu
771731
if (typeof window !== 'undefined') {
772732
window.dispatchEvent(new CustomEvent('bsx:return-to-main-menu'));

app/games/fast-piggie/interface.html

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,18 @@ <h3>How to Play</h3>
5252

5353
</div>
5454

55-
</section>
55+
<div id="fp-end-panel" class="fp-end-panel" hidden>
56+
<h3>Game Over!</h3>
57+
<p>Final Score: <strong id="fp-final-score">0</strong></p>
58+
<p>Personal Best: <strong id="fp-final-high-score">0</strong></p>
59+
<div class="fp-end-panel__actions">
60+
<button id="fp-play-again-btn" type="button" class="fp-btn fp-btn--primary">
61+
Play Again
62+
</button>
63+
<button id="fp-return-btn" type="button" class="fp-btn fp-btn--secondary">
64+
Return to Menu
65+
</button>
66+
</div>
67+
</div>
68+
69+
</section>

app/games/fast-piggie/style.css

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
align-items: center;
66
gap: 1rem;
77
padding: 1.5rem;
8-
background-color: #f8f9fa; /* matches app body background */
9-
color: #212529; /* ~14.5:1 contrast on #f8f9fa */
8+
background-color: #f8f9fa;
9+
/* matches app body background */
10+
color: #212529;
11+
/* ~14.5:1 contrast on #f8f9fa */
1012
}
1113

1214
.fast-piggie h2 {
@@ -42,7 +44,8 @@
4244
/* Intrinsic size is 500×500; CSS keeps it responsive */
4345
width: 100%;
4446
height: 100%;
45-
border-radius: 50%; /* visual hint that it's a circle game */
47+
border-radius: 50%;
48+
/* visual hint that it's a circle game */
4649
background-color: #ffffff;
4750
cursor: crosshair;
4851
}
@@ -74,6 +77,7 @@
7477
background-color: #28a745;
7578
opacity: 0.55;
7679
}
80+
7781
100% {
7882
background-color: #28a745;
7983
opacity: 0;
@@ -85,6 +89,7 @@
8589
background-color: #dc3545;
8690
opacity: 0.55;
8791
}
92+
8893
100% {
8994
background-color: #dc3545;
9095
opacity: 0;
@@ -106,8 +111,10 @@
106111
border: none;
107112
border-radius: 4px;
108113
cursor: pointer;
109-
background-color: #005fcc; /* primary blue */
110-
color: #ffffff; /* 7.3:1 contrast on #005fcc */
114+
background-color: #005fcc;
115+
/* primary blue */
116+
color: #ffffff;
117+
/* 7.3:1 contrast on #005fcc */
111118
transition: background-color 0.15s ease;
112119
}
113120

@@ -126,7 +133,8 @@
126133
}
127134

128135
.fp-btn--secondary {
129-
background-color: #6c757d; /* muted grey; 4.6:1 contrast on white */
136+
background-color: #6c757d;
137+
/* muted grey; 4.6:1 contrast on white */
130138
color: #ffffff;
131139
}
132140

@@ -192,3 +200,36 @@
192200
padding: 0.65rem 1.5rem;
193201
font-size: 1.1rem;
194202
}
203+
204+
/* End-game panel */
205+
.fp-end-panel {
206+
max-width: 360px;
207+
background-color: #ffffff;
208+
border: 1px solid #dee2e6;
209+
border-radius: 8px;
210+
padding: 1.5rem 2rem;
211+
text-align: center;
212+
}
213+
214+
.fp-end-panel h3 {
215+
font-size: 1.5rem;
216+
font-weight: 700;
217+
margin: 0 0 1rem;
218+
}
219+
220+
.fp-end-panel p {
221+
font-size: 1.1rem;
222+
margin: 0 0 0.5rem;
223+
}
224+
225+
.fp-end-panel__actions {
226+
display: flex;
227+
gap: 0.75rem;
228+
flex-wrap: wrap;
229+
justify-content: center;
230+
margin-top: 1rem;
231+
}
232+
233+
.fp-end-panel .fp-btn {
234+
padding: 0.65rem 1.5rem;
235+
}

app/games/fast-piggie/tests/index.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ function buildContainer() {
145145
<button id="fp-stop-btn" class="fp-btn fp-btn--secondary">End Game</button>
146146
</div>
147147
</div>
148+
<div id="fp-end-panel" class="fp-end-panel" hidden>
149+
<h3>Game Over!</h3>
150+
<p>Your score: <strong id="fp-final-score">0</strong></p>
151+
<p>Personal best: <strong id="fp-final-high-score">0</strong></p>
152+
<div class="fp-end-panel__actions">
153+
<button id="fp-play-again-btn" type="button" class="fp-btn fp-btn--primary">Play Again</button>
154+
<button id="fp-return-btn" type="button" class="fp-btn fp-btn--secondary">Return to Menu</button>
155+
</div>
156+
</div>
148157
</section>
149158
`;
150159
return div;
@@ -364,6 +373,18 @@ describe('stop()', () => {
364373
const btn = container.querySelector('#fp-stop-btn');
365374
expect(btn.hidden).toBe(true);
366375
});
376+
377+
it('shows #fp-end-panel', async () => {
378+
await plugin.stop();
379+
const endPanel = container.querySelector('#fp-end-panel');
380+
expect(endPanel.hidden).toBe(false);
381+
});
382+
383+
it('writes the final score into #fp-final-score', async () => {
384+
await plugin.stop();
385+
const finalScore = container.querySelector('#fp-final-score');
386+
expect(finalScore.textContent).toBe('3');
387+
});
367388
});
368389

369390
// ===========================================================================
@@ -414,6 +435,12 @@ describe('reset()', () => {
414435
const gameArea = container.querySelector('#fp-game-area');
415436
expect(gameArea.hidden).toBe(true);
416437
});
438+
439+
it('hides #fp-end-panel', () => {
440+
plugin.reset();
441+
const endPanel = container.querySelector('#fp-end-panel');
442+
expect(endPanel.hidden).toBe(true);
443+
});
417444
});
418445

419446
// ===========================================================================

0 commit comments

Comments
 (0)