Private
Public Access
1
0
Files
MineSeeker/assets/js/mine-seeker/grid/grid-control.js

302 lines
9.4 KiB
JavaScript
Raw Normal View History

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;