235 lines
8.0 KiB
React
235 lines
8.0 KiB
React
|
|
import React, { useRef } from 'react';
|
||
|
|
import { Howl } from 'howler';
|
||
|
|
import GameContext from './GameContext';
|
||
|
|
import useGameRefs from '../hooks/useGameRefs';
|
||
|
|
import useGameState from '../hooks/useGameState';
|
||
|
|
import { DESC, IMAGES, patchCells } from '../utils/constants';
|
||
|
|
|
||
|
|
export const GameProvider = ({ children }) => {
|
||
|
|
const {
|
||
|
|
webPlayerRef,
|
||
|
|
activePlayerRef,
|
||
|
|
bombSelectedRef,
|
||
|
|
connectionLostRef,
|
||
|
|
redRef,
|
||
|
|
blueRef,
|
||
|
|
lastClickedRef,
|
||
|
|
endRef,
|
||
|
|
} = useGameRefs();
|
||
|
|
|
||
|
|
const {
|
||
|
|
webPlayer, setWebPlayer,
|
||
|
|
activePlayer, setActivePlayer,
|
||
|
|
overlay, setOverlay,
|
||
|
|
overlayTitle, setOverlayTitle,
|
||
|
|
overlaySubTitle, setOverlaySubTitle,
|
||
|
|
mines, setMines,
|
||
|
|
bombSelected, setBombSelected,
|
||
|
|
foundMines, setFoundMines,
|
||
|
|
red, setRed,
|
||
|
|
blue, setBlue,
|
||
|
|
cells, setCells,
|
||
|
|
gridReady, setGridReady,
|
||
|
|
connectionLost, setConnectionLost,
|
||
|
|
} = useGameState();
|
||
|
|
|
||
|
|
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'] }),
|
||
|
|
});
|
||
|
|
|
||
|
|
// ── 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: IMAGES.flag(player), 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: IMAGES.leftMine }));
|
||
|
|
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: IMAGES.flag(player), 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)));
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<GameContext.Provider
|
||
|
|
value={{
|
||
|
|
// State (for rendering)
|
||
|
|
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
||
|
|
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost,
|
||
|
|
// Setters needed by useServerComm
|
||
|
|
setCells, setGridReady,
|
||
|
|
// Refs (needed by useServerComm for async-safe reads)
|
||
|
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
||
|
|
// Sync helpers
|
||
|
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||
|
|
// Game logic called by useServerComm
|
||
|
|
showOverlay, hideOverlay,
|
||
|
|
applyRevealedCell, applyStep,
|
||
|
|
makeGameEndIfItEnds, resignProcess,
|
||
|
|
// UI action
|
||
|
|
onBombToggle,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{children}
|
||
|
|
</GameContext.Provider>
|
||
|
|
);
|
||
|
|
};
|