Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 38 additions & 78 deletions app/games/fast-piggie/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ let _feedbackEl = null;
let _flashEl = null;
let _instructionsEl = null;
let _gameAreaEl = null;
let _endPanelEl = null;
let _playAgainBtn = null;
let _returnToMenuBtn = null;
let _finalScoreEl = null;
let _finalHighScoreEl = null;

// Game state
let _images = null; // [commonImage, outlierImage]
Expand Down Expand Up @@ -443,6 +448,18 @@ function _getCorrectWedgeIndex(round) {
return outlierWedgeIndex;
}

/**
* Show the end-game panel with the latest score summary.
* @param {number} score
* @param {number} highScore
*/
function _showEndPanel(score, highScore) {
if (_gameAreaEl) _gameAreaEl.hidden = true;
if (_endPanelEl) _endPanelEl.hidden = false;
if (_finalScoreEl) _finalScoreEl.textContent = String(score);
if (_finalHighScoreEl) _finalHighScoreEl.textContent = String(highScore);
}

/**
* Resolves the round after a wedge is selected, updates state and feedback.
* @param {number} wedge
Expand Down Expand Up @@ -537,14 +554,19 @@ export default {
init(container) {
_instructionsEl = container.querySelector('#fp-instructions');
_gameAreaEl = container.querySelector('#fp-game-area');
_endPanelEl = container.querySelector('#fp-end-panel');
_startBtn = container.querySelector('#fp-start-btn');
_canvas = container.querySelector('#fp-canvas');
_ctx = _canvas.getContext('2d');
_stopBtn = container.querySelector('#fp-stop-btn');
_playAgainBtn = container.querySelector('#fp-play-again-btn');
_returnToMenuBtn = container.querySelector('#fp-return-btn');
_scoreEl = container.querySelector('#fp-score');
_roundEl = container.querySelector('#fp-round-count');
_feedbackEl = container.querySelector('#fp-feedback');
_flashEl = container.querySelector('#fp-flash');
_finalScoreEl = container.querySelector('#fp-final-score');
_finalHighScoreEl = container.querySelector('#fp-final-high-score');

// Pre-load images
const base = new URL('../fast-piggie/images/', import.meta.url).href;
Expand All @@ -564,6 +586,15 @@ export default {
_canvas.addEventListener('mouseleave', _handleMouseLeave);
_canvas.addEventListener('keydown', _handleKeydown);
_stopBtn.addEventListener('click', () => this.stop());
if (_playAgainBtn) {
_playAgainBtn.addEventListener('click', () => {
this.reset();
this.start();
});
}
if (_returnToMenuBtn) {
_returnToMenuBtn.addEventListener('click', () => _returnToMainMenu());
}
},

/**
Expand All @@ -572,6 +603,7 @@ export default {
start() {
if (_instructionsEl) _instructionsEl.hidden = true;
if (_gameAreaEl) _gameAreaEl.hidden = false;
if (_endPanelEl) _endPanelEl.hidden = true;
game.startGame();
_updateStats();
_runRound();
Expand All @@ -590,8 +622,7 @@ export default {
const result = game.stopGame();

let highScore = result.score;
let previousHigh = 0;
let sessionsPlayed = 1;
let bestStats = game.getBestStats();
// Return a promise for test compatibility
return (async () => {
try {
Expand All @@ -617,20 +648,18 @@ export default {
} catch {
// If load fails, still proceed to save with defaults
}
previousHigh = gameEntry.highScore;
// Only update highScore if the new score is higher
highScore = Math.max(gameEntry.highScore || 0, result.score);
sessionsPlayed = (gameEntry.sessionsPlayed || 0) + 1;
// Get best stats from game logic
const bestStats = game.getBestStats();
bestStats = game.getBestStats();
const updated = {
...existing,
games: {
...existing.games,
'fast-piggie': {
...gameEntry,
highScore,
sessionsPlayed,
sessionsPlayed: (gameEntry.sessionsPlayed || 0) + 1,
lastPlayed: new Date().toISOString(),
maxLevel:
typeof bestStats.maxScore === 'number'
Expand Down Expand Up @@ -660,13 +689,8 @@ export default {
// Swallow all errors from progress load/save
}

// Show accessible summary modal (skip in test env)
if (typeof document !== 'undefined' &&
document.body &&
!document.body.classList.contains('jest-testing')
) {
_showSummaryModal(result.score, previousHigh, highScore);
} else if (_feedbackEl) {
_showEndPanel(result.score, highScore);
if (_feedbackEl) {
_feedbackEl.textContent = `Game over! Final score: ${result.score} in ${result.roundsPlayed} rounds.`;
}
_stopBtn.hidden = true;
Expand Down Expand Up @@ -695,78 +719,14 @@ export default {
_stopBtn.hidden = false;
if (_instructionsEl) _instructionsEl.hidden = false;
if (_gameAreaEl) _gameAreaEl.hidden = true;
if (_endPanelEl) _endPanelEl.hidden = true;
},
};

/**
* Show an accessible summary modal with current and best score, and return button.
* @param {number} score
* @param {number} previousHigh
* @param {number} highScore
*/
function _showSummaryModal(score, previousHigh, highScore) {
// Remove any existing modal
const oldModal = document.getElementById('fp-summary-modal');
if (oldModal) oldModal.remove();

// Get best stats for this session
const bestStats = game.getBestStats();

const modal = document.createElement('div');
modal.id = 'fp-summary-modal';
modal.className = 'fp-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'fp-summary-title');
modal.setAttribute('tabindex', '-1');

modal.innerHTML = `
<div class="fp-modal-content">
<h2 id="fp-summary-title">Game Over</h2>
<p>Your score: <strong>${score}</strong></p>
<p>Personal best: <strong>${highScore}</strong></p>
<hr />
<h3>Session Bests</h3>
<ul>
<li>Max score: <strong>${bestStats.maxScore}</strong></li>
<li>Most rounds: <strong>${bestStats.mostRounds}</strong></li>
<li>Most guinea pigs in a round: <strong>${bestStats.mostGuineaPigs}</strong></li>
<li>Top speed (ms): <strong>${bestStats.topSpeedMs !== null ? bestStats.topSpeedMs : '—'}</strong></li>
</ul>
<button id="fp-return-btn" class="fp-btn fp-btn--primary">Return to Main Menu</button>
</div>
`;

// Trap focus inside modal
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
const focusable = modal.querySelectorAll('button');
if (focusable.length) {
e.preventDefault();
focusable[0].focus();
}
}
if (e.key === 'Escape') {
_returnToMainMenu();
}
});

// Return button handler
modal.querySelector('#fp-return-btn').addEventListener('click', _returnToMainMenu);

// Add modal to DOM and focus
document.body.appendChild(modal);
setTimeout(() => {
modal.focus();
}, 0);
}

/**
* Return to the main game selection screen, removing modal and resetting UI.
*/
function _returnToMainMenu() {
const modal = document.getElementById('fp-summary-modal');
if (modal) modal.remove();
// Dispatch a custom event to notify the app shell to return to main menu
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('bsx:return-to-main-menu'));
Expand Down
16 changes: 15 additions & 1 deletion app/games/fast-piggie/interface.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,18 @@ <h3>How to Play</h3>

</div>

</section>
<div id="fp-end-panel" class="fp-end-panel" hidden>
<h3>Game Over!</h3>
<p>Final Score: <strong id="fp-final-score">0</strong></p>
<p>Personal Best: <strong id="fp-final-high-score">0</strong></p>
<div class="fp-end-panel__actions">
<button id="fp-play-again-btn" type="button" class="fp-btn fp-btn--primary">
Play Again
</button>
<button id="fp-return-btn" type="button" class="fp-btn fp-btn--secondary">
Return to Menu
</button>
</div>
</div>

</section>
53 changes: 47 additions & 6 deletions app/games/fast-piggie/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
align-items: center;
gap: 1rem;
padding: 1.5rem;
background-color: #f8f9fa; /* matches app body background */
color: #212529; /* ~14.5:1 contrast on #f8f9fa */
background-color: #f8f9fa;
/* matches app body background */
color: #212529;
/* ~14.5:1 contrast on #f8f9fa */
}

.fast-piggie h2 {
Expand Down Expand Up @@ -42,7 +44,8 @@
/* Intrinsic size is 500×500; CSS keeps it responsive */
width: 100%;
height: 100%;
border-radius: 50%; /* visual hint that it's a circle game */
border-radius: 50%;
/* visual hint that it's a circle game */
background-color: #ffffff;
cursor: crosshair;
}
Expand Down Expand Up @@ -74,6 +77,7 @@
background-color: #28a745;
opacity: 0.55;
}

100% {
background-color: #28a745;
opacity: 0;
Expand All @@ -85,6 +89,7 @@
background-color: #dc3545;
opacity: 0.55;
}

100% {
background-color: #dc3545;
opacity: 0;
Expand All @@ -106,8 +111,10 @@
border: none;
border-radius: 4px;
cursor: pointer;
background-color: #005fcc; /* primary blue */
color: #ffffff; /* 7.3:1 contrast on #005fcc */
background-color: #005fcc;
/* primary blue */
color: #ffffff;
/* 7.3:1 contrast on #005fcc */
transition: background-color 0.15s ease;
}

Expand All @@ -126,7 +133,8 @@
}

.fp-btn--secondary {
background-color: #6c757d; /* muted grey; 4.6:1 contrast on white */
background-color: #6c757d;
/* muted grey; 4.6:1 contrast on white */
color: #ffffff;
}

Expand Down Expand Up @@ -192,3 +200,36 @@
padding: 0.65rem 1.5rem;
font-size: 1.1rem;
}

/* End-game panel */
.fp-end-panel {
max-width: 360px;
background-color: #ffffff;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1.5rem 2rem;
text-align: center;
}

.fp-end-panel h3 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 1rem;
}

.fp-end-panel p {
font-size: 1.1rem;
margin: 0 0 0.5rem;
}

.fp-end-panel__actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
margin-top: 1rem;
}

.fp-end-panel .fp-btn {
padding: 0.65rem 1.5rem;
}
27 changes: 27 additions & 0 deletions app/games/fast-piggie/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,15 @@ function buildContainer() {
<button id="fp-stop-btn" class="fp-btn fp-btn--secondary">End Game</button>
</div>
</div>
<div id="fp-end-panel" class="fp-end-panel" hidden>
<h3>Game Over!</h3>
<p>Your score: <strong id="fp-final-score">0</strong></p>
<p>Personal best: <strong id="fp-final-high-score">0</strong></p>
<div class="fp-end-panel__actions">
<button id="fp-play-again-btn" type="button" class="fp-btn fp-btn--primary">Play Again</button>
<button id="fp-return-btn" type="button" class="fp-btn fp-btn--secondary">Return to Menu</button>
</div>
</div>
</section>
`;
return div;
Expand Down Expand Up @@ -364,6 +373,18 @@ describe('stop()', () => {
const btn = container.querySelector('#fp-stop-btn');
expect(btn.hidden).toBe(true);
});

it('shows #fp-end-panel', async () => {
await plugin.stop();
const endPanel = container.querySelector('#fp-end-panel');
expect(endPanel.hidden).toBe(false);
});

it('writes the final score into #fp-final-score', async () => {
await plugin.stop();
const finalScore = container.querySelector('#fp-final-score');
expect(finalScore.textContent).toBe('3');
});
});

// ===========================================================================
Expand Down Expand Up @@ -414,6 +435,12 @@ describe('reset()', () => {
const gameArea = container.querySelector('#fp-game-area');
expect(gameArea.hidden).toBe(true);
});

it('hides #fp-end-panel', () => {
plugin.reset();
const endPanel = container.querySelector('#fp-end-panel');
expect(endPanel.hidden).toBe(true);
});
});

// ===========================================================================
Expand Down
Loading