成品
代码
分析
细看游戏不难发现,难度不一样,颜色和水管数量就不一样。相同的是,都有两个空管,每个管有四滴水,用一个二维数组来体现:
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)
}
游戏
根据难度生成游戏数据, 难度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)
}
开始游戏
第一次点击:标记被点击的水管在数组中的位置,如果水管已完成,取消标记。
第二次点击:判断第一次点击的水管中的水,能否倒入到第二个水管中,有两个条件,满足任意一个即可。
- 第二个水管是空的。
- 第二个水管的最上层水的颜色和第一个水管的最上层的颜色一样,并且第二个水管有空位
然后,在确认可以倒入多少水,并倒入,至此一次倒水的动作完成,取消水管的选中。这里便是整个游戏最关键的逻辑,下面我们来实现它:
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)
}
结束语
结束啦,看看你能收集多少月饼吧!