mirror of
https://github.com/tiennm99/try-bmad.git
synced 2026-08-05 08:23:46 +00:00
feat: add header, scores, keyboard input, and overlays (Stories 1.4-1.6)
ScoreBoard shows current/best score. Keyboard arrow keys control gameplay via input-handler module. GameMessage overlay shows win/game over states with Keep Going, Try Again, and New Game actions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,9 +47,9 @@ development_status:
|
||||
1-1-project-scaffold-and-dev-environment: review
|
||||
1-2-game-logic-module: review
|
||||
1-3-game-board-and-tile-rendering: review
|
||||
1-4-game-header-score-display-and-new-game-button: backlog
|
||||
1-5-keyboard-input-and-interactive-gameplay: backlog
|
||||
1-6-win-and-game-over-overlays: backlog
|
||||
1-4-game-header-score-display-and-new-game-button: review
|
||||
1-5-keyboard-input-and-interactive-gameplay: review
|
||||
1-6-win-and-game-over-overlays: review
|
||||
epic-1-retrospective: optional
|
||||
|
||||
# Epic 2: Save Progress & Keep Playing
|
||||
|
||||
+54
-3
@@ -1,7 +1,10 @@
|
||||
<script>
|
||||
import Grid from './components/Grid.svelte';
|
||||
import { initGame } from './lib/game-logic.js';
|
||||
import ScoreBoard from './components/ScoreBoard.svelte';
|
||||
import GameMessage from './components/GameMessage.svelte';
|
||||
import { initGame, move, isGameOver } from './lib/game-logic.js';
|
||||
import { GRID_SIZE } from './lib/constants.js';
|
||||
import { getDirectionFromKey } from './lib/input-handler.js';
|
||||
|
||||
let gameState = $state(initGame());
|
||||
|
||||
@@ -17,8 +20,56 @@
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
let overlayType = $derived.by(() => {
|
||||
if (gameState.won && !gameState.keepPlaying) return 'win';
|
||||
if (isGameOver(gameState)) return 'gameover';
|
||||
return null;
|
||||
});
|
||||
|
||||
function handleNewGame() {
|
||||
gameState = initGame();
|
||||
}
|
||||
|
||||
function handleKeepGoing() {
|
||||
gameState = { ...gameState, keepPlaying: true, won: true };
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
const direction = getDirectionFromKey(event.key);
|
||||
if (!direction) return;
|
||||
event.preventDefault();
|
||||
|
||||
if (overlayType === 'gameover') return;
|
||||
|
||||
const newState = move(gameState, direction);
|
||||
if (newState !== gameState) {
|
||||
gameState = newState;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="max-w-[500px] mx-auto pt-10">
|
||||
<Grid {tiles} />
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<main class="max-w-[500px] mx-auto px-2 pt-6" role="application">
|
||||
<header>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h1 class="text-[80px] font-bold leading-none" style="color: #776e65;">2048</h1>
|
||||
<ScoreBoard score={gameState.score} bestScore={0} />
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="text-[18px]" style="color: #776e65;">Join the numbers and get to the <strong>2048 tile!</strong></p>
|
||||
<button
|
||||
class="font-bold text-[18px] rounded-[3px] px-4 py-2 min-h-[44px] min-w-[44px] cursor-pointer"
|
||||
style="background: #8f7a66; color: #f9f6f2;"
|
||||
onclick={handleNewGame}
|
||||
>
|
||||
New Game
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="relative">
|
||||
<Grid {tiles} />
|
||||
<GameMessage type={overlayType} onKeepGoing={handleKeepGoing} onNewGame={handleNewGame} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script>
|
||||
let { type = null, onKeepGoing = () => {}, onNewGame = () => {} } = $props();
|
||||
|
||||
let message = $derived(type === 'win' ? 'You win!' : 'Game over!');
|
||||
let bgColor = $derived(type === 'win' ? 'rgba(237, 194, 46, 0.5)' : 'rgba(238, 228, 218, 0.73)');
|
||||
</script>
|
||||
|
||||
{#if type}
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center z-10 rounded-[6px]"
|
||||
style="background: {bgColor};"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-label={message}
|
||||
>
|
||||
<p class="text-[60px] font-bold mb-4" style="color: #776e65;">{message}</p>
|
||||
{#if type === 'win'}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="font-bold text-[18px] rounded-[3px] px-5 py-3 min-h-[44px] cursor-pointer"
|
||||
style="background: #8f7a66; color: #f9f6f2;"
|
||||
onclick={onKeepGoing}
|
||||
>
|
||||
Keep going
|
||||
</button>
|
||||
<button
|
||||
class="font-bold text-[18px] rounded-[3px] px-5 py-3 min-h-[44px] cursor-pointer"
|
||||
style="background: #8f7a66; color: #f9f6f2;"
|
||||
onclick={onNewGame}
|
||||
>
|
||||
New Game
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="font-bold text-[18px] rounded-[3px] px-5 py-3 min-h-[44px] cursor-pointer"
|
||||
style="background: #8f7a66; color: #f9f6f2;"
|
||||
onclick={onNewGame}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let { score = 0, bestScore = 0 } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col items-center rounded-[3px] px-6 py-2 min-w-[80px]" style="background: #bbada0;">
|
||||
<span class="uppercase text-[13px] font-bold" style="color: #eee4da;">Score</span>
|
||||
<span class="text-[25px] font-bold" style="color: #f9f6f2;" aria-live="polite">{score}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center rounded-[3px] px-6 py-2 min-w-[80px]" style="background: #bbada0;">
|
||||
<span class="uppercase text-[13px] font-bold" style="color: #eee4da;">Best</span>
|
||||
<span class="text-[25px] font-bold" style="color: #f9f6f2;">{bestScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { DIRECTIONS } from './constants.js';
|
||||
|
||||
const KEY_MAP = {
|
||||
ArrowUp: DIRECTIONS.UP,
|
||||
ArrowDown: DIRECTIONS.DOWN,
|
||||
ArrowLeft: DIRECTIONS.LEFT,
|
||||
ArrowRight: DIRECTIONS.RIGHT,
|
||||
};
|
||||
|
||||
export function getDirectionFromKey(key) {
|
||||
return KEY_MAP[key] || null;
|
||||
}
|
||||
Reference in New Issue
Block a user