diff --git a/assets/js/mine-seeker/components/user/User.jsx b/assets/js/mine-seeker/components/user/User.jsx
index eb55adc..5411933 100644
--- a/assets/js/mine-seeker/components/user/User.jsx
+++ b/assets/js/mine-seeker/components/user/User.jsx
@@ -1,34 +1,38 @@
import React, { memo } from 'react';
-
-const SRC = '/images/';
+import { IMAGES } from '../../utils/constants';
const User = memo(function User({
- color, webPlayer,
- name, desc, active, mines, haveBomb, enabledBomb,
+ color,
+ webPlayer,
+ name,
+ desc,
+ active,
+ mines,
+ haveBomb,
+ enabledBomb,
onClickBombSelector,
}) {
const buzzClass = 'bomb-container'
+ (active && color === webPlayer && haveBomb && enabledBomb ? ' buzz' : '');
- const bombImg = haveBomb
- ? SRC + (enabledBomb && active ? 'bg-bomb-outbg.png' : 'bg-bomb-disabled-outbg.png')
- : SRC + 'bg-bomb-exploded-outbg.png';
-
return (
{color}
- {active &&

}
-

+ {active &&
})
}
+
{name}
{desc}
-

+
{mines}
-
+
+ {haveBomb &&

}
+ {!haveBomb &&

}
+
diff --git a/assets/js/mine-seeker/components/user/UserControl.jsx b/assets/js/mine-seeker/components/user/UserControl.jsx
index 3b591b6..4c3cbb8 100644
--- a/assets/js/mine-seeker/components/user/UserControl.jsx
+++ b/assets/js/mine-seeker/components/user/UserControl.jsx
@@ -1,7 +1,9 @@
import React from 'react';
+import { useGame } from '../../contexts/GameContext';
import User from './User';
-const UserControl = ({ webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle, resign }) => {
+const UserControl = ({ resign }) => {
+ const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame();
const activeColor = activePlayer ? 'blue' : 'red';
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
diff --git a/assets/js/mine-seeker/contexts/GameContext.jsx b/assets/js/mine-seeker/contexts/GameContext.jsx
index 3c8f4a3..b8ac5a4 100644
--- a/assets/js/mine-seeker/contexts/GameContext.jsx
+++ b/assets/js/mine-seeker/contexts/GameContext.jsx
@@ -1,233 +1,7 @@
-import React, { createContext, useContext, useRef, useState } from 'react';
-import { Howl } from 'howler';
-import { IMG, PLAYER_DEF, DESC, patchCells, initCells } from '../constants';
-
-// ── Context ──────────────────────────────────────────────────────────────────
+import { createContext, useContext } from 'react';
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};
-};
+export default GameContext;
diff --git a/assets/js/mine-seeker/contexts/GameProvider.jsx b/assets/js/mine-seeker/contexts/GameProvider.jsx
new file mode 100644
index 0000000..d13bd1d
--- /dev/null
+++ b/assets/js/mine-seeker/contexts/GameProvider.jsx
@@ -0,0 +1,234 @@
+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 (
+
+ {children}
+
+ );
+};
diff --git a/assets/js/mine-seeker/hooks/useGameRefs.jsx b/assets/js/mine-seeker/hooks/useGameRefs.jsx
new file mode 100644
index 0000000..6112f6f
--- /dev/null
+++ b/assets/js/mine-seeker/hooks/useGameRefs.jsx
@@ -0,0 +1,26 @@
+import { useRef } from 'react';
+import { PLAYER_DEF } from '../utils/constants';
+
+const useGameRefs = () => {
+ 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);
+
+ return {
+ webPlayerRef,
+ activePlayerRef,
+ bombSelectedRef,
+ connectionLostRef,
+ redRef,
+ blueRef,
+ lastClickedRef,
+ endRef,
+ };
+};
+
+export default useGameRefs;
diff --git a/assets/js/mine-seeker/hooks/useGameState.jsx b/assets/js/mine-seeker/hooks/useGameState.jsx
new file mode 100644
index 0000000..74fc0f2
--- /dev/null
+++ b/assets/js/mine-seeker/hooks/useGameState.jsx
@@ -0,0 +1,36 @@
+import { useState } from 'react';
+import { initCells, PLAYER_DEF } from '../utils/constants';
+
+const useGameState = () => {
+ 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);
+
+ return {
+ 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,
+ };
+};
+
+export default useGameState;
diff --git a/assets/js/mine-seeker/hooks/useServerComm.js b/assets/js/mine-seeker/hooks/useServerCommunication.jsx
similarity index 77%
rename from assets/js/mine-seeker/hooks/useServerComm.js
rename to assets/js/mine-seeker/hooks/useServerCommunication.jsx
index 55731cc..1ead01c 100644
--- a/assets/js/mine-seeker/hooks/useServerComm.js
+++ b/assets/js/mine-seeker/hooks/useServerCommunication.jsx
@@ -1,26 +1,22 @@
import React, { useEffect, useRef } from 'react';
-import { useQuery, useMutation } from '@tanstack/react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
import { useGame } from '../contexts/GameContext';
-import { DESC } from '../constants';
+import { DESC } from '../utils/constants';
-/**
- * Handles all server communication: SSE (Mercure), REST calls, and the
- * initialization lifecycle. Exposes only the UI-facing callbacks the
- * component needs: onClick, resign.
- */
-const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
+/** Handles all server communication: SSE (Mercure), REST calls, and the initialization lifecycle. */
+const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
const {
- // Async-safe refs
+ /** Async-safe refs */
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
- // State setters
+ /** State setters */
setGridReady,
- // Sync helpers
+ /** Sync helpers */
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
- // Game logic
+ /** Game logic */
showOverlay, hideOverlay,
applyRevealedCell, applyStep,
makeGameEndIfItEnds, resignProcess,
- // Current cells snapshot (for active-check in onClick)
+ /** Current cells snapshot (for active-check in onClick) */
cells,
} = useGame();
@@ -28,7 +24,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
const rpcUsersRef = useRef(null);
const stepCacheRef = useRef([]);
- // ── Helpers ───────────────────────────────────────────────────────────────
+ /** HELPERS */
const correctGridSize = () => {
let $f = $('#mine-wrapper .grid');
@@ -38,7 +34,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
$('#mine-wrapper .grid .field-wrapper .field').height($f.width()).css('line-height', ($f.width() - 2) + 'px');
};
- // ── REST mutations / queries ──────────────────────────────────────────────
+ /** REST mutations / queries */
const connectQuery = useQuery({
queryKey: ['game-connect', gameAssoc],
@@ -72,7 +68,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
}).then(r => r.json()),
});
- // ── Game-start helpers (triggered by server events) ───────────────────────
+ /** Game-start helpers (triggered by server events) */
const wInit = (revealedCells = []) => {
setGridReady(true);
@@ -91,17 +87,20 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
const makeGameStart = payload => {
syncActivePlayer(1);
- syncRed(p => ({ ...p, name: payload.users.red || payload.users.redAnon || p.name }));
+ syncRed(p => ({
+ ...p,
+ name: payload.users.red || payload.users.redAnon || p.name,
+ }));
syncBlue(p => ({
...p,
- name: payload.users.blue || payload.users.blueAnon || p.name,
- desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
+ name: payload.users.blue || payload.users.blueAnon || p.name,
+ desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
active: true,
}));
hideOverlay();
};
- // ── Mercure / SSE message handlers ───────────────────────────────────────
+ /** Mercure / SSE message handlers */
const wSubscribe = (payload, rpcUsers = null) => {
isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
@@ -174,17 +173,26 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
es.onmessage = e => handleMercureMessage(JSON.parse(e.data));
es.onopen = () => {
isEnvDev && console.info('SSE opened');
- if (connectionLostRef.current) { isEnvDev && console.info('SSE reconnected'); joinMutation.mutate(); }
+ if (connectionLostRef.current) {
+ isEnvDev && console.info('SSE reconnected');
+ joinMutation.mutate();
+ }
+ };
+ es.onerror = () => {
+ isEnvDev && console.error('SSE error');
+ syncConnLost(true);
};
- es.onerror = () => { isEnvDev && console.error('SSE error'); syncConnLost(true); };
eventSourceRef.current = es;
};
- // ── Initialization ────────────────────────────────────────────────────────
+ /** Initialization */
useEffect(() => {
(async () => {
- if (connectionLostRef.current) { openEventSource(); return; }
+ if (connectionLostRef.current) {
+ openEventSource();
+ return;
+ }
try {
if (gameInherited) {
const serverData = await connectQuery.refetch().then(r => {
@@ -219,7 +227,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- // ── UI-facing callbacks ───────────────────────────────────────────────────
+ /** UI-facing callbacks */
const onClick = async coords => {
const activeColor = activePlayerRef.current ? 'blue' : 'red';
@@ -230,7 +238,10 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null };
- if (connectionLostRef.current) { stepCacheRef.current.push(dataPack); return; }
+ if (connectionLostRef.current) {
+ stepCacheRef.current.push(dataPack);
+ return;
+ }
try {
const result = await stepMutation.mutateAsync(dataPack);
@@ -261,4 +272,4 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
return { onClick, resign };
};
-export default useServerComm;
+export default useServerCommunication;
diff --git a/assets/js/mine-seeker/constants.js b/assets/js/mine-seeker/utils/constants.jsx
similarity index 74%
rename from assets/js/mine-seeker/constants.js
rename to assets/js/mine-seeker/utils/constants.jsx
index 368eccf..a7a3143 100644
--- a/assets/js/mine-seeker/constants.js
+++ b/assets/js/mine-seeker/utils/constants.jsx
@@ -10,6 +10,21 @@ export const WAVES = {
3: 'bg-wave-2-outbg.png',
};
+export const IMAGES = {
+ target: `${IMG}bg-target-outbg.png`,
+ bomb: `${IMG}bg-bomb-outbg.png`,
+ bombDisabled: `${IMG}bg-bomb-disabled-outbg.png`,
+ bombExploded: `${IMG}bg-bomb-exploded-outbg.png`,
+ bombEmpty: `${IMG}bg-bomb-empty-outbg.png`,
+ leftMine: `${IMG}bg-left-mine-outbg.png`,
+ cursor: color => `${IMG}bg-cursor-${color}-outbg.png`,
+ figure: color => `${IMG}bg-figure-${color}-outbg.png`,
+ flag: player => `${IMG}bg-flag-${player}-outbg.png`,
+ last: color => `${IMG}bg-last-${color}-outbg.png`,
+ wave: n => `${IMG}${WAVES[n]}`,
+ bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
+};
+
export const PLAYER_DEF = {
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
};
@@ -53,7 +68,7 @@ export const patchCells = (prev, patches) => {
export const initCells = () =>
Array.from({ length: ROWS }, () =>
Array.from({ length: COLS }, () => ({
- currentImage: IMG + WAVES[Math.floor(Math.random() * 3) + 1],
+ currentImage: IMAGES.wave(Math.floor(Math.random() * 3) + 1),
currentObj: 'w',
active: false,
lastClickedRed: false,
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 17325ce..4cc297c 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -7,12 +7,6 @@ export default [
{
ignores: ['node_modules/**', 'vendor/**', 'var/**', 'public/build/**'],
},
- {
- files: ['**/*.jsx'],
- rules: {
- 'react/jsx-uses-vars': 'error',
- },
- },
{
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx}'],
plugins: {
@@ -111,4 +105,17 @@ export default [
}],
},
},
+ {
+ files: ['**/*.jsx'],
+ rules: {
+ 'react/jsx-uses-vars': 'error',
+ 'no-unused-vars': ['error', {
+ varsIgnorePattern: '^React$',
+ args: 'after-used',
+ caughtErrors: 'none',
+ ignoreRestSiblings: false,
+ reportUsedIgnorePattern: false,
+ }],
+ },
+ },
];