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( , ); } } } render() { /** Render the grid fields just one time in one party #12 */ this.state.renderGridFields && this.renderGridFields(); this.state.renderGridFields = false; return (

{this.state.overlayTitle}

{this.state.overlaySubTitle}

<> {this.state.gridFields}
); } } export default GridControl;