mirror of
https://github.com/tiennm99/gsd-framework.git
synced 2026-06-07 16:11:59 +00:00
feat(04-04): implement restart() method with score preservation
- Added previousScore property to Game class - Implemented restart() method that stores previousScore, resets grid/score/state/UI - Added updatePreviousScoreDisplay() helper method - Added game:restart event type to GameEvents interface - Restart resets grid via initializeGrid(), resets score to 0, transitions state to IDLE - Previous score preserved and displayed, hidden if 0 - All tests passing for restart functionality Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -412,4 +412,114 @@ describe('Game', () => {
|
||||
expect(document.getElementById('previous-score-display')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restart() method', () => {
|
||||
let mockOverlay: any;
|
||||
let mockScoreDisplay: any;
|
||||
let mockPreviousScoreDisplay: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOverlay = { style: { display: '' }, textContent: '' };
|
||||
mockScoreDisplay = { style: {}, textContent: '' };
|
||||
mockPreviousScoreDisplay = { style: { display: 'none' }, textContent: '' };
|
||||
|
||||
vi.stubGlobal('document', {
|
||||
getElementById: vi.fn((id: string) => {
|
||||
if (id === 'game') return mockCanvas;
|
||||
if (id === 'score-display') return mockScoreDisplay;
|
||||
if (id === 'previous-score-display') return mockPreviousScoreDisplay;
|
||||
if (id === 'game-over-overlay') return mockOverlay;
|
||||
return null;
|
||||
}),
|
||||
});
|
||||
|
||||
game = new Game();
|
||||
});
|
||||
|
||||
it('should store current score as previousScore before reset', () => {
|
||||
// Set a score
|
||||
game['score'] = 500;
|
||||
|
||||
// Call restart
|
||||
game.restart();
|
||||
|
||||
// Verify previousScore was stored
|
||||
expect(game['previousScore']).toBe(500);
|
||||
});
|
||||
|
||||
it('should call gridManager.initializeGrid() to regenerate tiles', () => {
|
||||
const initializeGridSpy = vi.spyOn(game.gridManager, 'initializeGrid');
|
||||
|
||||
game.restart();
|
||||
|
||||
expect(initializeGridSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should reset score to 0 for new game', () => {
|
||||
// Set a score
|
||||
game['score'] = 500;
|
||||
|
||||
game.restart();
|
||||
|
||||
// Verify score is reset
|
||||
expect(game['score']).toBe(0);
|
||||
});
|
||||
|
||||
it('should call gameStateManager.reset() to transition to IDLE', () => {
|
||||
const resetSpy = vi.spyOn(game.gameStateManager, 'reset');
|
||||
|
||||
game.restart();
|
||||
|
||||
expect(resetSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should hide game over overlay', () => {
|
||||
// Show overlay first
|
||||
mockOverlay.style.display = 'flex';
|
||||
|
||||
game.restart();
|
||||
|
||||
// Verify overlay is hidden
|
||||
expect(mockOverlay.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('should update previous score display with preserved score', () => {
|
||||
// Set a score
|
||||
game['score'] = 500;
|
||||
|
||||
game.restart();
|
||||
|
||||
// Verify previous score display is updated
|
||||
expect(mockPreviousScoreDisplay.textContent).toBe('Previous: 500');
|
||||
expect(mockPreviousScoreDisplay.style.display).toBe('block');
|
||||
});
|
||||
|
||||
it('should hide previous score display if previousScore is 0', () => {
|
||||
// Score is already 0
|
||||
game['score'] = 0;
|
||||
|
||||
game.restart();
|
||||
|
||||
// Verify previous score display is hidden
|
||||
expect(mockPreviousScoreDisplay.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('should emit game:restart event', () => {
|
||||
const emitSpy = vi.spyOn(game.events, 'emit');
|
||||
|
||||
game.restart();
|
||||
|
||||
expect(emitSpy).toHaveBeenCalledWith('game:restart', undefined as never);
|
||||
});
|
||||
|
||||
it('should update current score display to 0', () => {
|
||||
// Set a score
|
||||
game['score'] = 500;
|
||||
|
||||
game.restart();
|
||||
|
||||
// Verify current score display shows 0
|
||||
expect(mockScoreDisplay.textContent).toBe('Score: 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { GridManager } from '../managers/GridManager';
|
||||
import { Renderer } from '../rendering/Renderer';
|
||||
import { MatchEngine } from '../matching/MatchEngine';
|
||||
import { GameStateManager, GameState } from '../state/GameStateManager';
|
||||
import { NoMovesDetector } from '../detection/NoMovesDetector';
|
||||
|
||||
export class Game {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
@@ -24,6 +25,7 @@ export class Game {
|
||||
readonly gameStateManager: GameStateManager;
|
||||
private resizeTimeout: number | undefined;
|
||||
private score = 0;
|
||||
private previousScore = 0;
|
||||
|
||||
constructor() {
|
||||
// Get canvas element
|
||||
@@ -81,6 +83,15 @@ export class Game {
|
||||
// Clear tiles from board after path animation completes
|
||||
setTimeout(() => {
|
||||
this.gridManager.clearTiles([tile1, tile2]);
|
||||
|
||||
// Check for no-moves condition after tiles cleared
|
||||
const grid = this.gridManager.getAllTiles();
|
||||
const hasValidMoves = NoMovesDetector.hasValidMoves(grid);
|
||||
|
||||
if (!hasValidMoves && this.gameStateManager.getState() !== GameState.GAME_OVER) {
|
||||
// No moves left - game over
|
||||
this.handleGameOver(false);
|
||||
}
|
||||
}, 300); // Wait for path animation (300ms)
|
||||
|
||||
// Update score immediately
|
||||
@@ -216,6 +227,11 @@ export class Game {
|
||||
* @param event - MouseEvent or TouchEvent
|
||||
*/
|
||||
private handleInput(event: MouseEvent | TouchEvent): void {
|
||||
// Block input if game is in GAME_OVER state
|
||||
if (!this.gameStateManager.canSelectTile()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
@@ -283,5 +299,71 @@ export class Game {
|
||||
|
||||
// Emit game:over event
|
||||
this.events.emit('game:over', { won });
|
||||
|
||||
// Show game over overlay
|
||||
this.showGameOverOverlay(won);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show game over overlay with win/lose message
|
||||
* @param won - Whether the player won (true) or lost (false)
|
||||
*/
|
||||
private showGameOverOverlay(won: boolean): void {
|
||||
const overlay = document.getElementById('game-over-overlay');
|
||||
const message = document.getElementById('game-over-message');
|
||||
|
||||
if (overlay && message) {
|
||||
message.textContent = won ? 'You Win!' : 'No moves left!';
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide game over overlay
|
||||
*/
|
||||
private hideGameOverOverlay(): void {
|
||||
const overlay = document.getElementById('game-over-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update previous score display in HTML overlay
|
||||
*/
|
||||
private updatePreviousScoreDisplay(): void {
|
||||
const previousScoreDisplay = document.getElementById('previous-score-display');
|
||||
if (previousScoreDisplay) {
|
||||
previousScoreDisplay.textContent = `Previous: ${this.previousScore}`;
|
||||
// Show the element if there's a previous score, hide if 0
|
||||
previousScoreDisplay.style.display = this.previousScore > 0 ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the game - reset grid, score, state, and UI
|
||||
*/
|
||||
restart(): void {
|
||||
// Store current score as previous score BEFORE reset
|
||||
this.previousScore = this.score;
|
||||
|
||||
// Reset grid to initial state
|
||||
this.gridManager.initializeGrid();
|
||||
|
||||
// Reset score to 0 for new game
|
||||
this.score = 0;
|
||||
|
||||
// Reset state machine to IDLE
|
||||
this.gameStateManager.reset();
|
||||
|
||||
// Hide game over overlay
|
||||
this.hideGameOverOverlay();
|
||||
|
||||
// Update score displays (current = 0, previous = preserved)
|
||||
this.updateScoreDisplay();
|
||||
this.updatePreviousScoreDisplay();
|
||||
|
||||
// Emit restart event for extensibility (future listeners can subscribe)
|
||||
this.events.emit('game:restart', undefined as never);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface StateChangeEvent {
|
||||
*/
|
||||
export interface GameEvents {
|
||||
'game:start': void;
|
||||
'game:restart': void;
|
||||
'game:tick': { deltaTime: number };
|
||||
'game:stateChange': StateChangeEvent;
|
||||
'tilesSelected': { tile1: Tile; tile2: Tile };
|
||||
|
||||
Reference in New Issue
Block a user