302 lines
9.4 KiB
JavaScript
302 lines
9.4 KiB
JavaScript
import React from 'react';
|
|
import GridField from './grid-field';
|
|
import UserControl from '../user/user-control';
|
|
import { Howl } from 'howler';
|
|
|
|
class GridControl extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
let 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'] });
|
|
|
|
this.state = {
|
|
env: props.env,
|
|
webPlayer: null,
|
|
grid: null,
|
|
desc: null,
|
|
renderGridFields: false,
|
|
gridFields: [],
|
|
bombFieldCache: [],
|
|
overlay: false,
|
|
overlayTitle: '',
|
|
overlaySubTitle: '',
|
|
sound: {
|
|
click: click,
|
|
bomb: bomb,
|
|
mine: mine,
|
|
warning: warning,
|
|
won: won,
|
|
},
|
|
lastClicked: {
|
|
red: null,
|
|
blue: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
refString(row, col) {
|
|
return 'gridField_' + row + '_' + col;
|
|
}
|
|
|
|
checkFieldHasBeenNeverClicked(row, col) {
|
|
return !this.refs[this.refString(row, col)].state.active;
|
|
}
|
|
|
|
getBombRadius(row, col) {
|
|
let isBombTargetCenter = 1 < row && row < this.state.grid.length - 2 && 1 < col && col < this.state.grid[row].length - 2;
|
|
|
|
if (!isBombTargetCenter) {
|
|
col = 2 > col ? 2 : col;
|
|
row = 2 > row ? 2 : row;
|
|
row = row > this.state.grid.length - 3 ? this.state.grid.length - 3 : row;
|
|
col = col > this.state.grid[0].length - 3 ? this.state.grid[0].length - 3 : col;
|
|
}
|
|
|
|
return [
|
|
[row, col], [row - 2, col - 2], [row - 2, col], [row - 2, col + 2], [row, col - 2], [row, col + 2],
|
|
[row + 2, col - 2], [row + 2, col], [row + 2, col + 2], [row - 2, col + 1], [row - 2, col - 1],
|
|
[row - 1, col - 2], [row - 1, col - 1], [row - 1, col], [row - 1, col + 1], [row - 1, col + 2],
|
|
[row, col - 1], [row, col + 1], [row + 1, col - 2], [row + 1, col - 1], [row + 1, col],
|
|
[row + 1, col + 1], [row + 1, col + 2], [row + 2, col - 1], [row + 2, col + 1],
|
|
];
|
|
}
|
|
|
|
getBombFieldRadius() {
|
|
return [
|
|
[null, null], [0, 0], [1, 0], [2, 0], [0, 1], [2, 1], [0, 2], [1, 2], [2, 2],
|
|
[null, null], [null, null], [null, null], [null, null], [null, null], [null, null], [null, null],
|
|
[null, null], [null, null], [null, null], [null, null], [null, null], [null, null], [null, null],
|
|
[null, null], [null, null],
|
|
];
|
|
}
|
|
|
|
bombClear() {
|
|
if (this.state.bombFieldCache.length) {
|
|
for (var i = 0, j = this.state.bombFieldCache.length; i < j; i++) {
|
|
var cacheItem = this.state.bombFieldCache[i];
|
|
this.refs[this.refString(cacheItem[0], cacheItem[1])]
|
|
.setState({ bombTargetArea: null });
|
|
}
|
|
this.state.bombFieldCache = [];
|
|
}
|
|
}
|
|
|
|
bombCreate(row, col) {
|
|
var bombFieldSymbols = this.getBombFieldRadius(),
|
|
bombFields = this.getBombRadius(row, col);
|
|
|
|
for (var i = 0, j = bombFields.length; i < j; i++) {
|
|
this.state.bombFieldCache.push(bombFields[i]);
|
|
|
|
this.refs[this.refString(bombFields[i][0], bombFields[i][1])]
|
|
.setState({ bombTargetArea: bombFieldSymbols[i] });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show unrevealed mines at the end of the game.
|
|
* leftMines comes from the server and contains only cells not yet revealed.
|
|
*/
|
|
showLeftMines(leftMines = []) {
|
|
for (let mine of leftMines) {
|
|
let currentField = this.refs[this.refString(mine.row, mine.col)];
|
|
if (currentField && !currentField.state.active) {
|
|
currentField.setState({
|
|
currentImage: currentField.state.icons.root + currentField.state.icons.left,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/** set __ACTIVE__ player in the UserControl */
|
|
changePlayer() {
|
|
var userControl = this.refs.userControl,
|
|
activePlayer = userControl.state.activePlayer ? 'blue' : 'red',
|
|
inactivePlayer = userControl.state.activePlayer ? 'red' : 'blue';
|
|
|
|
userControl.setState({ activePlayer: userControl.state.activePlayer ? 0 : 1 });
|
|
|
|
userControl.refs[activePlayer].setState({ active: false, desc: '' });
|
|
userControl.refs[inactivePlayer].setState({
|
|
active: true,
|
|
desc: activePlayer === this.state.webPlayer
|
|
? this.state.desc.buddy
|
|
: this.state.desc.you,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Apply a single revealed cell to the grid UI.
|
|
* Called both for live steps and when reconstructing the board for a joining player.
|
|
*
|
|
* @param {object} cell - { row, col, value } value is 'm' | 0-8
|
|
* @param {string} player - 'red' | 'blue'
|
|
* @param {boolean} isMainCell - true for the primary clicked cell (sets last-clicked highlight)
|
|
*/
|
|
applyRevealedCell(cell, player, isMainCell = false) {
|
|
let ref = this.refs[this.refString(cell.row, cell.col)];
|
|
if (!ref || ref.state.active) {
|
|
return;
|
|
}
|
|
|
|
if ('m' === cell.value) {
|
|
ref.setState({
|
|
currentImage: ref.state.icons.root + ref.state.icons.flag[player],
|
|
currentObj: 'm',
|
|
active: true,
|
|
});
|
|
} else {
|
|
ref.setState({
|
|
currentImage: cell.value,
|
|
currentObj: cell.value,
|
|
active: true,
|
|
});
|
|
}
|
|
|
|
if (isMainCell) {
|
|
this.state.lastClicked[player] = [cell.row, cell.col];
|
|
ref.setState({
|
|
lastClickedRed: 'red' === player,
|
|
lastClickedBlue: 'blue' === player,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a full step response from the server.
|
|
* stepData shape: { coords, player, bomb, revealedCells, minesFound, redPoints, bluePoints, resign, gameOver,
|
|
* leftMines }
|
|
*/
|
|
applyStep(stepData) {
|
|
let player = stepData.player;
|
|
let isBomb = stepData.bomb;
|
|
let minesFound = stepData.minesFound || 0;
|
|
let revealedCells = stepData.revealedCells || [];
|
|
|
|
// Reset previous last-clicked highlight for this player
|
|
let lastClicked = this.state.lastClicked[player];
|
|
if (lastClicked) {
|
|
let lastRef = this.refs[this.refString(lastClicked[0], lastClicked[1])];
|
|
if (lastRef) {
|
|
lastRef.setState({ lastClickedRed: false, lastClickedBlue: false });
|
|
}
|
|
}
|
|
|
|
// Sound
|
|
if (isBomb) {
|
|
this.state.sound.bomb.play();
|
|
} else if (0 < minesFound) {
|
|
let currentMines = this.refs.userControl.refs[player].state.mines;
|
|
this.state.sound[20 < (currentMines + minesFound) ? 'warning' : 'mine'].play();
|
|
} else {
|
|
this.state.sound.click.play();
|
|
}
|
|
|
|
// Apply each revealed cell; first cell gets the last-clicked highlight
|
|
revealedCells.forEach((cell, idx) => {
|
|
this.applyRevealedCell(cell, player, 0 === idx);
|
|
});
|
|
|
|
// Update scores
|
|
let userControl = this.refs.userControl;
|
|
let inactivePlayer = 'red' === player ? 'blue' : 'red';
|
|
|
|
if (0 < minesFound) {
|
|
userControl.setState({
|
|
mines: 51 - stepData.redPoints - stepData.bluePoints,
|
|
foundMines: true,
|
|
}, () => {
|
|
setTimeout(() => userControl.setState({ foundMines: false }), 500);
|
|
userControl.refs[player].setState({
|
|
mines: 'red' === player ? stepData.redPoints : stepData.bluePoints,
|
|
});
|
|
});
|
|
}
|
|
|
|
// Bomb-enabled status: a player may use their bomb when their score <= opponent's
|
|
userControl.refs.red.setState({ enabledBomb: stepData.redPoints <= stepData.bluePoints });
|
|
userControl.refs.blue.setState({ enabledBomb: stepData.bluePoints <= stepData.redPoints });
|
|
|
|
// Change active player: always after a bomb, or when no mine was found on a normal click
|
|
if (isBomb || 0 === minesFound) {
|
|
this.changePlayer();
|
|
}
|
|
|
|
// Clean up bomb state
|
|
if (isBomb) {
|
|
userControl.setState({ bombSelected: false });
|
|
userControl.refs[player].setState({ haveBomb: false });
|
|
this.bombClear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On Hover when you want to drop BOMB — show target area overlay
|
|
*/
|
|
onHoverGridField(coords) {
|
|
if (this.refs.userControl.state.bombSelected) {
|
|
var activePlayer = this.refs.userControl.state.activePlayer ? 'blue' : 'red';
|
|
|
|
if (activePlayer === this.state.webPlayer) {
|
|
this.bombClear();
|
|
this.bombCreate(coords[0], coords[1]);
|
|
} else {
|
|
this.refs.userControl.setState({ bombSelected: false });
|
|
}
|
|
}
|
|
}
|
|
|
|
renderGridFields() {
|
|
for (let i = 0, j = this.state.grid.length; i < j; i++) {
|
|
for (let k = 0, l = this.state.grid[i].length; k < l; k++) {
|
|
this.state.gridFields.push(
|
|
<GridField
|
|
row={i}
|
|
col={k}
|
|
ref={this.refString(i, k)}
|
|
key={this.refString(i, k)}
|
|
handleHoverOn={this.onHoverGridField.bind(this, [i, k])}
|
|
onClick={this.props.onClick.bind(null, [i, k])}
|
|
/>,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
render() {
|
|
/** Render the grid fields just one time in one party #12 */
|
|
this.state.renderGridFields && this.renderGridFields();
|
|
this.state.renderGridFields = false;
|
|
|
|
return (
|
|
<div className="game-wrapper">
|
|
<div className={`game-overlay ${this.state.overlay ? '' : ' hide'}`}>
|
|
<div className="game-overlay-window">
|
|
<h1>{this.state.overlayTitle}</h1>
|
|
<h2>{this.state.overlaySubTitle}</h2>
|
|
</div>
|
|
</div>
|
|
<UserControl
|
|
ref="userControl"
|
|
resign={this.props.resign}
|
|
webPlayer={this.state.webPlayer}
|
|
bombClear={this.bombClear.bind(this)}
|
|
/>
|
|
<div className="grid-container">
|
|
<div className="grid">
|
|
<>
|
|
{this.state.gridFields}
|
|
</>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default GridControl;
|