Skip to content

Commit 7daefd8

Browse files
committed
Update SnakeCard.jsx
Added ability for phones to play snake with D-Pad
1 parent f11a22f commit 7daefd8

File tree

1 file changed

+135
-4
lines changed

1 file changed

+135
-4
lines changed

src/components/SnakeCard.jsx

Lines changed: 135 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Card, Button, ButtonGroup, Badge } from "react-bootstrap";
55
/**
66
* <SnakeCard /> — drop this inside any React-Bootstrap layout.
77
* Arrow keys / WASD to move. Space = pause. R = reset.
8+
* On phones: swipe on the board or use the D-pad to steer.
89
*/
910
export default function SnakeCard() {
1011
// Tunables
@@ -28,6 +29,10 @@ export default function SnakeCard() {
2829
const snakeRef = useRef([]);
2930
const foodRef = useRef({ x: 5, y: 5 });
3031

32+
// Touch swipe tracking
33+
const touchStartRef = useRef({ x: 0, y: 0, active: false });
34+
const SWIPE_THRESH = 24; // px movement needed to register a swipe
35+
3136
const randomEmptyCell = useCallback(() => {
3237
const taken = new Set(snakeRef.current.map(p => `${p.x},${p.y}`));
3338
let x, y;
@@ -96,7 +101,6 @@ export default function SnakeCard() {
96101
snake.forEach((p, idx) => {
97102
const r = idx === 0 ? 3 : 2;
98103
ctx.beginPath();
99-
// roundRect is widely supported in modern browsers
100104
ctx.roundRect(p.x * TILE + 1, p.y * TILE + 1, TILE - 2, TILE - 2, r);
101105
ctx.fill();
102106
});
@@ -181,6 +185,64 @@ export default function SnakeCard() {
181185
return () => window.removeEventListener("keydown", onKey);
182186
}, [draw, resetGame]);
183187

188+
// Touch (swipe) controls on the canvas
189+
useEffect(() => {
190+
const canvas = canvasRef.current;
191+
if (!canvas) return;
192+
193+
const onTouchStart = (e) => {
194+
if (!e.touches || e.touches.length === 0) return;
195+
const t = e.touches[0];
196+
touchStartRef.current = { x: t.clientX, y: t.clientY, active: true };
197+
// prevent scroll/bounce
198+
e.preventDefault();
199+
};
200+
201+
const onTouchMove = (e) => {
202+
// prevent page scroll while swiping on the board
203+
if (touchStartRef.current.active) e.preventDefault();
204+
};
205+
206+
const onTouchEnd = (e) => {
207+
const start = touchStartRef.current;
208+
if (!start.active) return;
209+
210+
// last touch point (or changedTouches[0])
211+
const t = (e.changedTouches && e.changedTouches[0]) || null;
212+
if (!t) { touchStartRef.current.active = false; return; }
213+
214+
const dx = t.clientX - start.x;
215+
const dy = t.clientY - start.y;
216+
217+
// Ignore micro-movements
218+
if (Math.max(Math.abs(dx), Math.abs(dy)) >= SWIPE_THRESH) {
219+
const cur = dirRef.current;
220+
// Horizontal swipe
221+
if (Math.abs(dx) > Math.abs(dy)) {
222+
const next = dx > 0 ? { x: 1, y: 0 } : { x: -1, y: 0 };
223+
if (!(next.x === -cur.x && next.y === -cur.y)) pendingDirRef.current = next;
224+
} else {
225+
const next = dy > 0 ? { x: 0, y: 1 } : { x: 0, y: -1 };
226+
if (!(next.x === -cur.x && next.y === -cur.y)) pendingDirRef.current = next;
227+
}
228+
}
229+
230+
touchStartRef.current.active = false;
231+
e.preventDefault();
232+
};
233+
234+
// Use non-passive to allow preventDefault
235+
canvas.addEventListener("touchstart", onTouchStart, { passive: false });
236+
canvas.addEventListener("touchmove", onTouchMove, { passive: false });
237+
canvas.addEventListener("touchend", onTouchEnd, { passive: false });
238+
239+
return () => {
240+
canvas.removeEventListener("touchstart", onTouchStart);
241+
canvas.removeEventListener("touchmove", onTouchMove);
242+
canvas.removeEventListener("touchend", onTouchEnd);
243+
};
244+
}, []);
245+
184246
// Loop start/stop
185247
useEffect(() => {
186248
if (running) {
@@ -206,6 +268,14 @@ export default function SnakeCard() {
206268
stepMsRef.current = ms;
207269
};
208270

271+
// helper for D-pad taps
272+
const nudgeDir = (next) => {
273+
const cur = dirRef.current;
274+
if (!(next.x === -cur.x && next.y === -cur.y)) {
275+
pendingDirRef.current = next;
276+
}
277+
};
278+
209279
return (
210280
<Card className="h-100 shadow-sm">
211281
<Card.Header className="d-flex justify-content-between align-items-center">
@@ -215,12 +285,18 @@ export default function SnakeCard() {
215285
<Badge bg="success" title="Best on this browser">High Score!: {high}</Badge>
216286
</div>
217287
</Card.Header>
288+
218289
<Card.Body className="d-flex flex-column">
219-
<div className="ratio ratio-1x1 border rounded" style={{ overflow: "hidden" }}>
290+
{/* Board */}
291+
<div
292+
className="ratio ratio-1x1 border rounded"
293+
style={{ overflow: "hidden", touchAction: "none" }} // critical for mobile swipes
294+
>
220295
<canvas ref={canvasRef} style={{ width: "100%", height: "100%", display: "block" }} />
221296
</div>
222297

223-
<div className="d-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
298+
{/* Desktop controls */}
299+
<div className="d-none d-sm-flex justify-content-between align-items-center mt-3 flex-wrap gap-2">
224300
<ButtonGroup>
225301
<Button variant={running ? "warning" : "primary"} onClick={() => setRunning(p => !p)}>
226302
{running ? "Pause (Space)" : "Start (Space)"}
@@ -237,7 +313,62 @@ export default function SnakeCard() {
237313
</ButtonGroup>
238314
</div>
239315

240-
<small className="text-muted mt-2">
316+
{/* Mobile controls (visible on xs only) */}
317+
<div className="d-flex d-sm-none flex-column gap-2 mt-3">
318+
<div className="d-flex justify-content-center gap-2">
319+
<Button size="sm" variant="primary" onClick={() => setRunning(p => !p)}>
320+
{running ? "Pause" : "Start"}
321+
</Button>
322+
<Button size="sm" variant="outline-secondary" onClick={() => { resetGame(); draw(); }}>
323+
Reset
324+
</Button>
325+
<Button size="sm" variant="outline-success" onClick={() => setSpeed("Fast", 80)}>
326+
FastMode 🔥
327+
</Button>
328+
</div>
329+
330+
{/* D-pad */}
331+
<div className="d-flex justify-content-center">
332+
<div className="d-grid" style={{ gridTemplateColumns: "56px 56px 56px", gap: "8px" }} aria-label="Directional pad">
333+
<div />
334+
<Button
335+
size="sm"
336+
className="fw-bold"
337+
onClick={() => nudgeDir({ x: 0, y: -1 })}
338+
aria-label="Up"
339+
></Button>
340+
<div />
341+
<Button
342+
size="sm"
343+
className="fw-bold"
344+
onClick={() => nudgeDir({ x: -1, y: 0 })}
345+
aria-label="Left"
346+
></Button>
347+
<div />
348+
<Button
349+
size="sm"
350+
className="fw-bold"
351+
onClick={() => nudgeDir({ x: 1, y: 0 })}
352+
aria-label="Right"
353+
></Button>
354+
<div />
355+
<Button
356+
size="sm"
357+
className="fw-bold"
358+
onClick={() => nudgeDir({ x: 0, y: 1 })}
359+
aria-label="Down"
360+
></Button>
361+
<div />
362+
</div>
363+
</div>
364+
365+
<small className="text-muted text-center">
366+
Tip: swipe on the board to steer.
367+
</small>
368+
</div>
369+
370+
{/* Shared hint (desktop already has separate control strip) */}
371+
<small className="text-muted mt-2 d-none d-sm-block">
241372
Controls: Arrow keys / WASD. Space to pause/resume. R to restart.
242373
</small>
243374
</Card.Body>

0 commit comments

Comments
 (0)