import React, { createContext, useContext, useRef, useState } from 'react'; import { Howl } from 'howler'; import { IMG, PLAYER_DEF, DESC, patchCells, initCells } from '../constants'; // ── Context ────────────────────────────────────────────────────────────────── const GameContext = createContext(null); export const useGame = () => useContext(GameContext); // ── Provider ───────────────────────────────────────────────────────────────── export const GameProvider = ({ children }) => { // Refs that are read inside async callbacks (stay in sync with state via sync* helpers) const webPlayerRef = useRef(null); const activePlayerRef = useRef(false); const bombSelectedRef = useRef(false); const connectionLostRef = useRef(false); const redRef = useRef({ ...PLAYER_DEF }); const blueRef = useRef({ ...PLAYER_DEF }); const lastClickedRef = useRef({ red: null, blue: null }); const endRef = useRef(false); const sounds = useRef({ click: new Howl({ src: ['/sound/click.mp3'] }), bomb: new Howl({ src: ['/sound/bomb.mp3'] }), mine: new Howl({ src: ['/sound/mine.mp3'] }), warning: new Howl({ src: ['/sound/warning.mp3'] }), won: new Howl({ src: ['/sound/won.mp3'] }), }); // Display state const [webPlayer, setWebPlayer] = useState(null); const [activePlayer, setActivePlayer] = useState(false); const [overlay, setOverlay] = useState(true); const [overlayTitle, setOverlayTitle] = useState(''); const [overlaySubTitle, setOverlaySubTitle] = useState(''); const [mines, setMines] = useState(51); const [bombSelected, setBombSelected] = useState(false); const [foundMines, setFoundMines] = useState(false); const [red, setRed] = useState({ ...PLAYER_DEF }); const [blue, setBlue] = useState({ ...PLAYER_DEF }); const [cells, setCells] = useState(initCells); const [gridReady, setGridReady] = useState(false); const [connectionLost, setConnectionLost] = useState(false); // ── Sync helpers (keep ref + state in lockstep) ────────────────────────── const syncWebPlayer = v => { webPlayerRef.current = v; setWebPlayer(v); }; const syncActivePlayer = v => { activePlayerRef.current = v; setActivePlayer(v); }; const syncBombSelected = v => { bombSelectedRef.current = v; setBombSelected(v); }; const syncConnLost = v => { connectionLostRef.current = v; setConnectionLost(v); }; const syncRed = fn => { const n = fn(redRef.current); redRef.current = n; setRed(n); }; const syncBlue = fn => { const n = fn(blueRef.current); blueRef.current = n; setBlue(n); }; // ── Overlay ─────────────────────────────────────────────────────────────── const showOverlay = (title, sub) => { setOverlay(true); setOverlayTitle(title); setOverlaySubTitle(sub); }; const hideOverlay = () => setOverlay(false); // ── Cell helpers ────────────────────────────────────────────────────────── const applyRevealedCell = (cell, player, isMainCell = false) => { const { row, col, value } = cell; setCells(prev => { if (prev[row][col].active) return prev; const patch = 'm' === value ? { currentImage: `${IMG}bg-flag-${player}-outbg.png`, currentObj: 'm', active: true } : { currentImage: value, currentObj: value, active: true }; if (isMainCell) { patch.lastClickedRed = 'red' === player; patch.lastClickedBlue = 'blue' === player; } return patchCells(prev, [{ row, col, ...patch }]); }); if (isMainCell) lastClickedRef.current = { ...lastClickedRef.current, [player]: [row, col] }; }; const showLeftMines = (leftMines = []) => { if (!leftMines.length) return; setCells(prev => { const patches = leftMines .filter(({ row, col }) => !prev[row][col].active) .map(({ row, col }) => ({ row, col, currentImage: IMG + 'bg-left-mine-outbg.png' })); return patches.length ? patchCells(prev, patches) : prev; }); }; // ── Game logic ──────────────────────────────────────────────────────────── const changePlayer = () => { const wasColor = activePlayerRef.current ? 'blue' : 'red'; const nextColor = activePlayerRef.current ? 'red' : 'blue'; const nextVal = activePlayerRef.current ? 0 : 1; const desc = wasColor === webPlayerRef.current ? DESC.buddy : DESC.you; syncActivePlayer(nextVal); syncRed(p => ({ ...p, active: 'red' === nextColor, desc: 'red' === nextColor ? desc : '' })); syncBlue(p => ({ ...p, active: 'blue' === nextColor, desc: 'blue' === nextColor ? desc : '' })); }; const applyStep = stepData => { const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData; if (isBomb) { sounds.current.bomb.play(); } else if (0 < minesFound) { const cur = ('red' === player ? redRef : blueRef).current.mines; sounds.current[20 < cur + minesFound ? 'warning' : 'mine'].play(); } else { sounds.current.click.play(); } const lc = lastClickedRef.current[player]; setCells(prev => { let next = prev; if (lc) next = patchCells(next, [{ row: lc[0], col: lc[1], lastClickedRed: false, lastClickedBlue: false }]); revealedCells.forEach(({ row, col, value }, idx) => { if (next[row][col].active) return; const patch = 'm' === value ? { currentImage: `${IMG}bg-flag-${player}-outbg.png`, currentObj: 'm', active: true } : { currentImage: value, currentObj: value, active: true }; if (0 === idx) { patch.lastClickedRed = 'red' === player; patch.lastClickedBlue = 'blue' === player; } next = patchCells(next, [{ row, col, ...patch }]); }); if (isBomb) next = next.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c)); return next; }); if (0 < revealedCells.length) { lastClickedRef.current = { ...lastClickedRef.current, [player]: [revealedCells[0].row, revealedCells[0].col] }; } if (0 < minesFound) { setMines(51 - rp - bp); setFoundMines(true); setTimeout(() => setFoundMines(false), 500); syncRed(p => ({ ...p, mines: 'red' === player ? rp : p.mines })); syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines })); } syncRed(p => ({ ...p, enabledBomb: rp <= bp })); syncBlue(p => ({ ...p, enabledBomb: bp <= rp })); if (isBomb || 0 === minesFound) changePlayer(); if (isBomb) { syncBombSelected(false); syncRed(p => 'red' === player ? { ...p, haveBomb: false } : p); syncBlue(p => 'blue' === player ? { ...p, haveBomb: false } : p); } }; const makeGameEndIfItEnds = (bluePoints, redPoints, resign = false, leftMines = []) => { const redWins = 25 < redPoints; const blueWins = 25 < bluePoints; if (redWins || blueWins || resign) { sounds.current.won.play(); if (!resign) showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', 'Play again!'); showLeftMines(leftMines); syncActivePlayer(false); syncRed(p => ({ ...p, desc: '' })); syncBlue(p => ({ ...p, desc: '' })); } }; const resignProcess = color => { const wp = webPlayerRef.current; showOverlay( color === wp ? 'You have been give up' : 'Your opponent has been resigned', color === wp ? 'You LOSE!' : 'You WIN!', ); endRef.current = true; makeGameEndIfItEnds(0, 0, true); }; const onBombToggle = () => { const next = !bombSelectedRef.current; syncBombSelected(next); if (!next) { setCells(prev => prev.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c))); } }; // ── Context value ───────────────────────────────────────────────────────── const value = { // State (for rendering) webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle, mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost, // Refs (needed by useServerComm for async-safe reads) webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef, // Setters needed by useServerComm setCells, setGridReady, syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue, // Game logic called by useServerComm showOverlay, hideOverlay, applyRevealedCell, applyStep, makeGameEndIfItEnds, resignProcess, // UI action (bomb toggle is pure state, no server call) onBombToggle, }; return {children}; };