From 6bc22aa89a3b4c728c0aa8f4fb92aee4f5a26bcf Mon Sep 17 00:00:00 2001 From: Pim de Wit Date: Thu, 16 Apr 2026 14:59:30 +0100 Subject: [PATCH 1/2] feat(go): add useAudio hook --- .../v2/src/components/SoundEffects.tsx | 13 ++--- .../go/visualizer/v2/src/hooks/useAudio.ts | 48 +++++++++++++++++++ 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts diff --git a/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/components/SoundEffects.tsx b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/components/SoundEffects.tsx index b18ae0b37..9dd18f897 100644 --- a/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/components/SoundEffects.tsx +++ b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/components/SoundEffects.tsx @@ -1,18 +1,16 @@ import { useEffect, useRef } from 'react'; -import { Howl } from 'howler'; import useGameStore from '../stores/useGameStore'; import usePreferences from '../stores/usePreferences'; +import useAudio from '../hooks/useAudio'; import placeSoundUrl from '../assets/audio/go-stone-placing.mp3'; import captureSoundUrl from '../assets/audio/go-stone-removal.mp3'; -const placeSound = new Howl({ src: [placeSoundUrl], volume: 0.5 }); -const captureSound = new Howl({ src: [captureSoundUrl], volume: 0.5 }); - const THROTTLE_MS = 150; export default function SoundEffects() { const game = useGameStore((state) => state.game); const soundEnabled = usePreferences((state) => state.soundEnabled); + const sounds = useAudio({ place: placeSoundUrl, capture: captureSoundUrl }); const prevRef = useRef({ move: 0, captures: 0 }); const lastPlayedRef = useRef(0); @@ -29,14 +27,13 @@ export default function SoundEffects() { if (!soundEnabled || (!placed && !captured)) return; - // Prevent audio spam when scrubbing quickly (e.g. holding arrow keys) const now = performance.now(); if (now - lastPlayedRef.current < THROTTLE_MS) return; lastPlayedRef.current = now; - if (placed) placeSound.play(); - if (captured) captureSound.play(); - }, [game, soundEnabled]); + if (placed) sounds.place.play(); + if (captured) sounds.capture.play(); + }, [game, soundEnabled, sounds]); return null; } diff --git a/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts new file mode 100644 index 000000000..908a68fbd --- /dev/null +++ b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts @@ -0,0 +1,48 @@ +import { useEffect, useRef } from 'react'; +import { Howl, Howler } from 'howler'; + +type SoundConfig = { src: string; volume?: number }; +type SoundMap = Record; +type Sounds = Record; + +function createSounds(map: SoundMap): Sounds { + const entries = Object.entries(map).map(([key, value]) => { + const config = typeof value === 'string' ? { src: value } : value; + return [key, new Howl({ src: [config.src], volume: config.volume ?? 0.5 })]; + }); + return Object.fromEntries(entries) as Sounds; +} + +export default function useAudio(map: SoundMap): Sounds { + const soundsRef = useRef>(createSounds(map)); + + useEffect(() => { + function cleanup() { + window.removeEventListener('pointerdown', resume); + window.removeEventListener('keydown', resume); + } + + function resume() { + Howler.unload(); + soundsRef.current = createSounds(map); + cleanup(); + } + + const handleVisibilityChange = () => { + if (document.visibilityState !== 'visible') return; + const state = Howler.ctx?.state as string; + if (state !== 'interrupted' && state !== 'suspended') return; + + window.addEventListener('pointerdown', resume); + window.addEventListener('keydown', resume); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + cleanup(); + }; + }, []); + + return soundsRef.current; +} From 383c133991e4d50bd58a388f768414f7a06a204c Mon Sep 17 00:00:00 2001 From: Pim de Wit Date: Thu, 16 Apr 2026 16:34:01 +0100 Subject: [PATCH 2/2] state instead of ref --- .../games/go/visualizer/v2/src/hooks/useAudio.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts index 908a68fbd..695f83b4a 100644 --- a/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts +++ b/kaggle_environments/envs/open_spiel_env/games/go/visualizer/v2/src/hooks/useAudio.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Howl, Howler } from 'howler'; type SoundConfig = { src: string; volume?: number }; @@ -14,7 +14,8 @@ function createSounds(map: SoundMap): Sounds { } export default function useAudio(map: SoundMap): Sounds { - const soundsRef = useRef>(createSounds(map)); + const mapRef = useRef(map); + const [sounds, setSounds] = useState(() => createSounds(map)); useEffect(() => { function cleanup() { @@ -24,7 +25,7 @@ export default function useAudio(map: SoundMap): Sounds function resume() { Howler.unload(); - soundsRef.current = createSounds(map); + setSounds(createSounds(mapRef.current)); cleanup(); } @@ -44,5 +45,5 @@ export default function useAudio(map: SoundMap): Sounds }; }, []); - return soundsRef.current; + return sounds; }