This is my code repo https://github.com/540376482yzb/2048game_react
This is my live demo https://objective-fermat-0343a3.netlify.com/
The current state of the game only goes left and right.
However it generates new number on left arrow key pressed when no move was made. The identified issue is this.state.gridData
is flipped before I run this.diffGrid(grid)
therefore it will always return true and added new number.
The suspects on this issue is :
// issue ====> flipGrid function is mutating this.state.gridData
flipGrid(grid) {
return grid.map(row => row.reverse())
}
Or
//slide left
if (e.keyCode === 37) {
//issue ===> state is flipped on left arrow key pressed
copyGrid = this.flipGrid(copyGrid).map(row => this.slideAndCombine(row))
copyGrid = this.flipGrid(copyGrid)
}
Can someone tell me where did I do wrong to cause the state mutation?
import React from 'react'
import './App.css'
import Grid from './Grid'
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
gridData: [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
}
}
initGame() {
let grid = [...this.state.gridData]
grid = this.addNumber(grid)
grid = this.addNumber(grid)
this.setState({
gridData: grid
})
}
addNumber(grid) {
const availableSpot = []
grid.map((rowData, x) =>
rowData.map((data, y) => {
if (!data) availableSpot.push({ x, y })
})
)
const randomSpot = availableSpot[Math.floor(Math.random() * availableSpot.length)]
grid[randomSpot.x][randomSpot.y] = Math.random() < 0.2 ? 4 : 2
return grid
}
slide(row) {
const newRow = row.filter(data => data)
const zerosArr = Array(4 - newRow.length).fill(0)
return [...zerosArr, ...newRow]
}
combine(row) {
let a, b
for (let i = 3; i > 0; i--) {
a = row[i]
b = row[i - 1]
if (a === b) {
row[i] = a + b
row[i - 1] = 0
}
}
return row
}
slideAndCombine(row) {
row = this.slide(row)
row = this.combine(row)
return row
}
diffGrid(grid) {
let isDiff = false
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid.length; j++) {
if (grid[i][j] != this.state.gridData[i][j]) {
isDiff = true
}
}
}
return isDiff
}
// issue ====> flipGrid function is mutating this.state.gridData
flipGrid(grid) {
return grid.map(row => row.reverse())
}
componentDidMount() {
this.initGame()
let copyGrid = [...this.state.gridData]
window.addEventListener('keyup', e => {
if (e.keyCode === 37 || 38 || 39 || 40) {
//slide right
if (e.keyCode === 39) {
copyGrid = copyGrid.map(row => this.slideAndCombine(row))
}
//slide left
if (e.keyCode === 37) {
//issue ===> state is flipped on left arrow key pressed
copyGrid = this.flipGrid(copyGrid).map(row => this.slideAndCombine(row))
copyGrid = this.flipGrid(copyGrid)
}
// Line 89 issue==>>>>>> gridData in the state
console.table(this.state.gridData)
// diffGrid compares copyGrid with this.state.gridData
if (this.diffGrid(copyGrid)) {
copyGrid = this.addNumber(copyGrid)
//deepCopy of gridData
console.table(copyGrid)
this.setState({
gridData: copyGrid
})
}
}
})
}
render() {
// Line 103 ===>>>> gridData in the state
console.table(this.state.gridData)
return (
<div className="App">
<main className="centerGrid" id="game">
<Grid gridData={this.state.gridData} />
</main>
</div>
)
}
}
export default App
row.reverse() will return the reversed array, but it will also mutate the row array. Have a look at the MDN docs for reverse. So, because map doesn't copy the elements before iterating over them, your flipGrid function will mutate the grid passed to it.
To ensure you don't mutate the original grid you could do the following:
flipGrid(grid) {
const gridCopy = grid.slice()
return gridCopy.map(row => row.reverse())
}
or a bit more concisely:
flipGrid(grid) {
return grid.slice().map(row => row.reverse())
}
or if you wanted to use the spread operator:
flipGrid(grid) {
return [...grid].map(row => row.reverse())
}
As for why this.state.gridData is mutated: array spreading is actually a shallow copy, so gridCopy is still referencing this.state.gridData's row arrays, because of this you are still mutating this.state.gridData when you mutate with flipGrid. If you stop mutating entirely this wouldn't matter, but it is better to deep copy at the beginning of the event handler rather than in componentDidMount so this can't happen in the first place.
you can either use lodash's cloneDeep function or map over gridCopy and shallow copy those also (using the spread operator or slice)
window.addEventListener('keyup', e => {
if (e.keyCode === 37 || 38 || 39 || 40) {
let gridCopy = this.state.gridData.map((row) => row.slice())
etc...
}
}