用react实现水排序,来收集月饼啦!

530 阅读7分钟

成品

gif.gif

代码

分析

细看游戏不难发现,难度不一样,颜色和水管数量就不一样。相同的是,都有两个空管,每个管有四滴水,用一个二维数组来体现:

let data = [  
    ['red', 'red', 'red', 'red'],  
    ['green', 'green', 'green', 'green'],  
    .  
    .  
    .  
    ['', '', '', ''],  
    ['', '', '', ''],  
]  

而过关条件就是,所有瓶子中的水的颜色是一样的,即二维数组中每个一维数组的所有值都相等,用一个方法去校验

const isFinished = (array) => {  
    for (let j = 0; j < array.length; j++) {  
        let arr = array[j]  
        let firstElement = arr[0]  
        for (let i = 1; i < arr.length; i++) {  
            if (arr[i] !== firstElement) {  
                return false  
            }  
        }  
    }  
    return true  
}  

开始

关卡

首先设计难度和关卡,计划总共300关,最低难度:3个水管+2个空水管;最高难度:12个水管+2个空水管,生成数据:

// 关卡难度配置  
// 以难度为属性,关卡为值,起始关卡用逗号隔开  
const allLevelSetting = {  
    3: '1,1',  
    4: '2,3',  
    5: '4,10',  
    6: '11,20',  
    7: '21,35',  
    8: '36,55',  
    9: '56,80',  
    10: '81,120',  
    11: '121,200',  
    12: '201,300',  
}  
  
// 生成关卡数据  
const gameLevelSetting = () => {  
    let res = []  
    for (let l = 1; l < 301; l++) {  
        for (let le in allLevelSetting) {  
            let range = allLevelSetting[le].split(',')  
            if (l >= range[0] && l <= range[1]) {  
                res.push({  
                    name: l,  
                    level: le,  
                })  
            }  
        }  
    }  
    setGameLevels(res)  
}  

关卡页面很简单,点击关卡,将难度等级传递给游戏页面。注:只有通关了上一关,才可以进行下一关的游戏

限制解除了,尽情的玩耍吧

const handleLevelClick = (level) => {  
// if (level.name > nowLevel) {  
// antd.message.warning('请解锁上一关')  
// return false  
// }  
// if (level.name < nowLevel) {  
// return false  
// }  
props.handleLevelClick(level)  
}  

gif.gif

游戏

根据难度生成游戏数据, 难度level,即装水的水管数量,即颜色数量,从colorsMap中取出对应数量的颜色并填充,这里先生成一维数组,因为需要打乱后,再分别注入每个水管中(切割成二维数组),之后填充两个空水管即可。

.  
.  
.  
const [gameData, setGameData] = useState([])  
.  
.  
.  
const initGames = () => {  
    let arr = []  
    for (let i = 0; i < props.level; i++) {  
        arr.push(...Array(4).fill(colorsMap[i]))  
    }  
    arr = shuffle(arr)  
    for (let i = 0; i < 2; i++) {  
        arr.push(...Array(4).fill(''))  
    }  
    arr = splitArray(arr, 4)  
    setGameData(arr)  
}  

开始游戏

第一次点击:标记被点击的水管在数组中的位置,如果水管已完成,取消标记。

第二次点击:判断第一次点击的水管中的水,能否倒入到第二个水管中,有两个条件,满足任意一个即可。

  1. 第二个水管是空的。
  2. 第二个水管的最上层水的颜色和第一个水管的最上层的颜色一样,并且第二个水管有空位

然后,在确认可以倒入多少水,并倒入,至此一次倒水的动作完成,取消水管的选中。这里便是整个游戏最关键的逻辑,下面我们来实现它:

const isAllElementsEqual = (arr) => {
  const firstElement = arr[0]
  if (firstElement === '') {
    return false
  }
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] !== firstElement) {
      return false
    }
  }
  return true
}
const findFirstNonNullValue = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] !== '') {
      return arr[i]
    }
  }
  return ''
}
const findLastNullKey = (arr) => {
  let key: any = ''
  for (let i = 0; i < 4; i++) {
    if (arr[i] === '') {
      key = i
    }
  }
  return key
}
const isFinished = (array) => {
  for (let j = 0; j < array.length; j++) {
    let arr = array[j]
    let firstElement = arr[0]
    for (let i = 1; i < arr.length; i++) {
      if (arr[i] !== firstElement) {
        return false
      }
    }
  }
  return true
}
const handleTubeClick = (tube, tubeKey) => {  
    if (tubeKey === selectKey) {  
        setSelectKey(null)  
        return false  
    }  
    // first click  
    if (selectKey === null) {  
        if (isAllElementsEqual(tube)) {  
            message.warning('换一个吧')  
            return false  
        }  
        setSelectKey(tubeKey)  
        return false  
    }  
    // second click  
    let firstTube = gameData[selectKey]  
    let color = findFirstNonNullValue(firstTube)  
    if (color === '') {  
        setSelectKey(null)  
        return false  
    }  
    let secondColor = findFirstNonNullValue(tube)  
    let secondNullLen = findLastNullKey(tube)  
    if (secondNullLen === '') {  
        setSelectKey(null)  
        return false  
    }  
    // second tube is empty  
    if (secondNullLen === 3) {  
        for (let squareKey = 0; squareKey < 4; squareKey++) {  
            let square = firstTube[squareKey]  
            if (square !== '' && square !== color) {  
                break  
            }  
            if (square === color) {  
                gameData[selectKey][squareKey] = ''  
                gameData[tubeKey][secondNullLen] = square  
                secondNullLen--  
            }  
        }  
        setGameData(gameData)  
        setSelectKey(null)  
        if (isFinished(gameData)) {  
            message.success('过关了')  
            handleSuccess()  
        }  
        return false  
    }  
    if (color === secondColor) {  
        for (let squareKey = 0; squareKey < 4; squareKey++) {  
            let square = firstTube[squareKey]  
            if (square !== '' && square !== color) {  
                break  
            }  
            if (secondNullLen < 0) {  
                break  
            }  
            if (square === color) {  
                gameData[selectKey][squareKey] = ''  
                gameData[tubeKey][secondNullLen] = square  
                secondNullLen--  
            }  
        }  
        setGameData(gameData)  
        setSelectKey(null)  
        if (isFinished(gameData)) {  
            message.success('过关了')  
            handleSuccess()  
        }  
        return false  
    }  
}

代码有点多,我一步步来解释,首先看handleTubeClick这个方法,接受了两个参数,第一个是当前选中的水管tube,第二个是当前选中水管的位置tubeKey

    if (tubeKey === selectKey) {
      setSelectKey(null)
      return false
    }

判断第二次点击是否和第一次点击一样,是一样就取消第一次点击,取消倒水。

    // first click
    if (selectKey === null) {
      if (isAllElementsEqual(tube)) {
        message.warning('换一个吧')
        return false
      }
      setSelectKey(tubeKey)
      return false
    }
const isAllElementsEqual = (arr) => {
  const firstElement = arr[0]
  if (firstElement === '') {
    return false
  }
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] !== firstElement) {
      return false
    }
  }
  return true
}

第一次点击判断是否已完成的水管,是的话,提示出来,取消倒水。

    // second click
    let firstTube = gameData[selectKey]
    let color = findFirstNonNullValue(firstTube)
    if (color === '') {
      setSelectKey(null)
      return false
    }
    let secondColor = findFirstNonNullValue(tube)
    let secondNullLen = findLastNullKey(tube)
    if (secondNullLen === '') {
      setSelectKey(null)
      return false
    }
const findFirstNonNullValue = (arr) => {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] !== '') {
      return arr[i]
    }
  }
  return ''
}
const findLastNullKey = (arr) => {
  let key: any = ''
  for (let i = 0; i < 4; i++) {
    if (arr[i] === '') {
      key = i
    }
  }
  return key
}

这里首先取出第一个水管中从上到下第一个不为空的颜色,再同样获取第二个水管的颜色和第二个水管的空余长度,先判断第二个水管是否有足够的空间。secondNullLen是从上tube[0]开始,往下tube[3],找到最后一个空位,当tube是空水管时,secondNullLen便等于3

    // second tube is empty
    if (secondNullLen === 3) {
      for (let squareKey = 0; squareKey < 4; squareKey++) {
        let square = firstTube[squareKey]
        if (square !== '' && square !== color) {
          break
        }
        if (square === color) {
          gameData[selectKey][squareKey] = ''
          gameData[tubeKey][secondNullLen] = square
          secondNullLen--
        }
      }
      setGameData(gameData)
      setSelectKey(null)
      if (isFinished(gameData)) {
        message.success('过关了')
        handleSuccess()
      }
      return false
    }
const isFinished = (array) => {
  for (let j = 0; j < array.length; j++) {
    let arr = array[j]
    let firstElement = arr[0]
    for (let i = 1; i < arr.length; i++) {
      if (arr[i] !== firstElement) {
        return false
      }
    }
  }
  return true
}

当第二个水管是空水管的时候,直接往里面倒水,从第一个水管的最上层(firstTube[0])开始, 一直到出现另一个颜色位置,第二个水管从下(gameData[tubeKey][3])往上接收水滴。

    if (color === secondColor) {
      for (let squareKey = 0; squareKey < 4; squareKey++) {
        let square = firstTube[squareKey]
        if (square !== '' && square !== color) {
          break
        }
        if (secondNullLen < 0) {
          break
        }
        if (square === color) {
          gameData[selectKey][squareKey] = ''
          gameData[tubeKey][secondNullLen] = square
          secondNullLen--
        }
      }
      setGameData(gameData)
      setSelectKey(null)
      if (isFinished(gameData)) {
        message.success('过关了')
        handleSuccess()
      }
      return false
    }

当第二个水管不为空时, 判断第二个水管的最上层颜色与第一个水管的最上层颜色是否相等,与空水管不同的是,倒水的时候需要判断第二个水管是否有足够的空间。

每次倒水完成后需要判断,游戏是否结束。到这里,简单的倒水游戏就完成了。接下来,进行一些动画上的调整。

动画

首先固定所有的水管位置,最多14个水管,分成两排,再根据水管的高度与宽度,偏移对应的位置,生成的结果如下

const tubePositionMap = [
  { 'top': 0, 'left': 24 },
  { 'top': 0, 'left': 76 },
  { 'top': 0, 'left': 128 },
  { 'top': 0, 'left': 180 },
  { 'top': 0, 'left': 232 },
  { 'top': 0, 'left': 284 },
  { 'top': 0, 'left': 336 },
  { 'top': 190, 'left': 24 },
  { 'top': 190, 'left': 76 },
  { 'top': 190, 'left': 128 },
  { 'top': 190, 'left': 180 },
  { 'top': 190, 'left': 232 },
  { 'top': 190, 'left': 284 },
  { 'top': 190, 'left': 336 }
]

水管的位置确定之后,在倒水的时候根据tubeKey找到第二个水管的位置,将第一个水管旋转90度secondStyle.transform = 'rotate(90deg)',再将top和left调到合适的位置,执行时间1秒secondStyle.transition = '1s linear',然后1.5秒后恢复第一个水管的位置。

  const animation = (first: any, second: any) => {
    let firstStyle = { ...tubePosition[first] }
    let secondStyle = { ...tubePosition[second] }
    secondStyle.top -= 110
    secondStyle.left -= 96
    secondStyle.transform = 'rotate(90deg)'
    secondStyle.transition = '1s linear'
    tubePosition[first] = secondStyle
    setTubePosition([...tubePosition])
    setTimeout(function () {
      tubePosition[first] = firstStyle
      setTubePosition([...tubePosition])
    }, 1500)
  }

回退

回退就是将每一次倒水完成后的数据记录到一个历史记录historyData的数组中,点击回退就弹出historyData中最后一个元素替换gameData。这里我给回退设置了一个限制,最多回退5步。

const [historyData, setHistoryData] = useState<any[]>([])

const history = (arr: any[]) => {
    let nowData = JSON.parse(JSON.stringify(arr))
    let nowHisData = JSON.parse(JSON.stringify(historyData))
    if (nowHisData.length > 4) {
      nowHisData.shift()
    }
    nowHisData.push(nowData)
    setHistoryData(nowHisData)
}

const handleRevoke = () => {
	let nowHisData = JSON.parse(JSON.stringify(historyData))
	let data = JSON.parse(JSON.stringify(nowHisData[nowHisData.length - 1]))
	setGameData(data)
	nowHisData.pop()
	setHistoryData(nowHisData)
}

结束语

结束啦,看看你能收集多少月饼吧!