- Add WASD and Vim hjkl keyboard mappings to input handler - Add touch/swipe input with 10px threshold and dominant-axis detection - Refactor all component dimensions to CSS custom properties - Add responsive breakpoint at 520px (500px→280px container, scaled fonts) - Add 31 new tests (21 input-handler + 10 swipe), 90 total passing
6.1 KiB
Story 4.2: Touch & Swipe Input
Status: done
Story
As a mobile player, I want to swipe on the game board to slide tiles, so that I can play the game naturally on a touchscreen.
Acceptance Criteria
- Given the player touches the game grid and swipes, when the swipe distance exceeds 10px minimum threshold, then the dominant axis (larger delta between horizontal and vertical) determines the direction and the corresponding move is executed
- Given the player touches and lifts with less than 10px movement, when the touch event completes, then no move is triggered (prevents accidental swipes)
- Given a diagonal swipe, when the touch event completes, then the axis with the larger delta wins (e.g., deltaX=30, deltaY=15 → horizontal → Left or Right based on sign)
- Given an animation is in progress on mobile, when the player swipes again, then the input is queued and executes after the current animation completes (same queuing as keyboard)
Tasks / Subtasks
- Task 1: Add swipe detection functions to input-handler.js (AC: #1, #2, #3)
- Export
getDirectionFromSwipe(startX, startY, endX, endY)— returns direction string or null - Implement 10px minimum threshold: if both |deltaX| and |deltaY| < 10, return null
- Implement dominant-axis detection: compare |deltaX| vs |deltaY|, use larger axis
- For horizontal: deltaX < 0 → LEFT, deltaX > 0 → RIGHT
- For vertical: deltaY < 0 → UP, deltaY > 0 → DOWN
- Export
- Task 2: Add unit tests for swipe detection (AC: #1, #2, #3)
- Test swipe right (deltaX=50, deltaY=5) → 'right'
- Test swipe left (deltaX=-50, deltaY=5) → 'left'
- Test swipe up (deltaX=5, deltaY=-50) → 'up'
- Test swipe down (deltaX=5, deltaY=50) → 'down'
- Test below threshold (deltaX=3, deltaY=5) → null
- Test diagonal dominant axis (deltaX=30, deltaY=15) → 'right'
- Test exact threshold (deltaX=10, deltaY=0) → 'right'
- Test zero movement (0, 0) → null
- Task 3: Wire touch events in App.svelte (AC: #1, #4)
- Import
getDirectionFromSwipefrom input-handler.js - Add
handleTouchStart— store start coordinates from event.touches[0] - Add
handleTouchEnd— compute direction via changedTouches[0], feed into executeMove with same animation queuing - Attach ontouchstart and ontouchend on the game container div
- Call
event.preventDefault()in touchend when a direction is detected to prevent scroll
- Import
- Task 4: Verify no regressions (AC: #4)
- All 90 tests pass (was 80, +10 new swipe tests)
- Keyboard input unchanged — same handleKeydown path
- Animation queuing works for both keyboard and touch (same isAnimating/queuedDirection path in handleTouchEnd)
Dev Notes
Architecture Pattern
Per architecture doc, input-handler.js translates DOM events to direction strings — no game logic. Touch detection follows the same pattern as keyboard: pure functions that convert raw event data to a direction string.
The touch wiring in App.svelte follows the same pattern as keyboard:
handleTouchStartcaptures touch coordinates (likehandleKeydowncaptures key)handleTouchEndcomputes direction viagetDirectionFromSwipe()(likegetDirectionFromKey())- Same
isAnimatingcheck → queue orexecuteMove(direction)— identical pipeline
Current input-handler.js exports
export function getDirectionFromKey(key) // existing — returns direction string or null
New exports to add:
export function getDirectionFromSwipe(startX, startY, endX, endY) // returns direction string or null
App.svelte Touch Wiring
Touch handlers go on the game container div (the <div class="relative"> at line 110):
<div class="relative"
ontouchstart={handleTouchStart}
ontouchend={handleTouchEnd}
>
handleTouchStart and handleTouchEnd are defined in App.svelte's <script> block (not in input-handler.js) because they need access to component state (isAnimating, queuedDirection, executeMove). Only the pure direction calculation is in input-handler.js.
Touch Event Details
TouchEvent.touches[0].clientX/clientYfor start positionTouchEvent.changedTouches[0].clientX/clientYfor end position (touches array is empty on touchend)- Must use
changedTouchesin touchend, NOTtouches event.preventDefault()in touchend to prevent page scroll on successful swipe
Previous Story Learnings (4.1)
- input-handler.js pattern works well: pure functions, easy to test with Vitest
- Test file already exists at
src/lib/input-handler.test.js— add new describe blocks for swipe tests - 80 tests currently passing across 4 test files
FRs Covered
- FR17: Touch/swipe input for mobile with 10px minimum threshold and dominant-axis detection
References
- [Source: _bmad-output/planning-artifacts/epics.md — Epic 4, Story 4.2]
- [Source: _bmad-output/planning-artifacts/architecture.md — Input Processing pattern]
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md — UX-DR17 non-blocking animation, UX-DR18 touch targets]
- [Source: src/lib/input-handler.js — current implementation with keyboard mappings]
- [Source: src/App.svelte:42-88 — executeMove + handleKeydown + animation queuing]
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
Completion Notes List
- Added
getDirectionFromSwipe()to input-handler.js: 10px threshold, dominant-axis detection, returns direction or null - Added 10 unit tests for swipe detection: 4 cardinal, 3 threshold, 3 dominant axis
- Wired handleTouchStart/handleTouchEnd in App.svelte on game container div
- Uses changedTouches[0] in touchend (not touches), preventDefault on valid swipe
- Touch input feeds into same executeMove + animation queuing pipeline as keyboard
- All 90 tests pass, zero regressions
File List
- src/lib/input-handler.js (modified — added getDirectionFromSwipe export)
- src/lib/input-handler.test.js (modified — added 10 swipe detection tests)
- src/App.svelte (modified — added touch handlers, imported getDirectionFromSwipe)