diff --git a/connect4-ui/app/layout.tsx b/connect4-ui/app/layout.tsx
index 5a0de8d..1db4ada 100644
--- a/connect4-ui/app/layout.tsx
+++ b/connect4-ui/app/layout.tsx
@@ -3,6 +3,7 @@ import "./globals.css";
import Celebration from "@/components/Celebration";
import Nav from "@/components/Nav";
import { ConnectionProvider } from "@/lib/connection";
+import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
export const metadata: Metadata = {
title: "Connect4 Moderator",
@@ -16,6 +17,17 @@ export default function RootLayout({
}) {
return (
+
+ {CHIP_DROP_SOUND_PATHS.map((path) => (
+
+ ))}
+
diff --git a/connect4-ui/components/Board.tsx b/connect4-ui/components/Board.tsx
index 9ae69a3..678bd2d 100644
--- a/connect4-ui/components/Board.tsx
+++ b/connect4-ui/components/Board.tsx
@@ -3,6 +3,8 @@
import type { CSSProperties } from "react";
import { useEffect, useRef, useState } from "react";
import type { BoardState } from "@/lib/protocol";
+import { CHIP_DROP_SOUND_PATHS } from "@/lib/sfx";
+const CHIP_DROP_ANIMATION_MS = 450;
interface BoardProps {
board: BoardState;
@@ -30,10 +32,27 @@ export default function Board({
} | null>(null);
const isFirstRender = useRef(true);
const animationTimeoutRef = useRef(null);
+ const chipDropSoundsRef = useRef([]);
const canInteract = !disabled && !!onColumnClick;
const lastMoveColumn = lastMove?.column ?? null;
const lastMoveRow = lastMove?.row ?? null;
+ useEffect(() => {
+ chipDropSoundsRef.current = CHIP_DROP_SOUND_PATHS.map((path) => {
+ const sound = new Audio(path);
+ sound.preload = "auto";
+ return sound;
+ });
+
+ return () => {
+ chipDropSoundsRef.current.forEach((sound) => {
+ sound.pause();
+ sound.src = "";
+ });
+ chipDropSoundsRef.current = [];
+ };
+ }, []);
+
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
@@ -49,11 +68,26 @@ export default function Board({
return;
}
+ const prefersReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)",
+ ).matches;
+ const dropDurationMs = prefersReducedMotion ? 0 : CHIP_DROP_ANIMATION_MS;
+
setAnimatingMove({ column: lastMoveColumn, row: lastMoveRow });
animationTimeoutRef.current = window.setTimeout(() => {
+ const sounds = chipDropSoundsRef.current;
+ const sound = sounds[Math.floor(Math.random() * sounds.length)];
+
+ if (sound) {
+ sound.currentTime = 0;
+ void sound.play().catch(() => {
+ // Ignore autoplay failures until the user has interacted.
+ });
+ }
+
setAnimatingMove(null);
animationTimeoutRef.current = null;
- }, 450);
+ }, dropDurationMs);
return () => {
if (animationTimeoutRef.current !== null) {
@@ -192,7 +226,8 @@ export default function Board({