华容道组件
使用技术:React
+ ts
由曹操,五虎将和4个小卒组成的华容道小游戏。
整体由 4 * 5 的格子组成,曹操占4格,五虎将横或竖向占2格,卒占1格,两个空格,当曹操移动到最底下时,游戏获胜。
该组件跟我之前开发的 滑块拼图组件 是类似的。
使用描述
引入 lhh-ui
组件库
bash
npm i lhh-ui
导入其中的 HuarongRoad
组件。
js
import { HuarongRoad } from "lhh-ui"
HuarongRoad.Item
用作自定义各 item
中的 children
内容。
HuarongRoad.Item
传入的 index
用来标记其中的内容。
index | 描述 |
---|---|
0 | 曹操 |
1-5 | 五虎将 |
6-10 | 卒 |
当 item
的数量加起来不到 10 个时,组件会自动补充,所以其实不传 item
也是可以展示出华容道的。
js
// 这样就是采用组件自带的样式了
<HuarongRoad width={400}></HuarongRoad>
demo代码
js
import { HuarongRoad } from "lhh-ui"
import React from "react"
const list = ['曹操','张飞','赵云','马超','关羽','黄忠','卒','卒','卒','卒']
export default () => {
return (
<HuarongRoad
width={400}
onComplete={() => {
setTimeout(() => {alert('曹操跑了')}, 400);
}}
>
{list.map((name, index) => (
<HuarongRoad.Item key={name + index} index={index} style={{background: "#f1f1f1"}}>
<div>{name}</div>
</HuarongRoad.Item>
))}
</HuarongRoad>
)
}
组件主要源码简述
华容道位置结构
华容道位置信息采用一个二维数组保存,大体结构如下:
js
[
[21, 1, 1, 22],
[21, 1, 1, 22],
[23, 24, 24, 25],
[23, 31, 32, 25],
[33, 0, 0, 34],
]
对应 props
中的 locationArr
参数,ts 类型如下:
HeroesIndex
值 | 描述 | 占位 |
---|---|---|
1 | 曹操(boss) | 占4格 |
21 - 25 | 五虎将 | 横或竖向占2格 |
31 - 34 | 卒 | 占1格 |
大体描述如图所示:
移动 item
我的描述可能不太好,查看 完整代码 会比较清晰。处理 item
移动的判断结构大致如下:
js
const HuarongRoadItem = (comProps: HuarongRoadItemProps) => {
/** 当前可移动的方向 */
const moveDirection = useMemo(() => (
// gridArr 中保存的就是 item 的位置信息,rowNum 和 colNum 就是 item 处于的行列数
checkRoadDirection(gridArr, info.rowNum, info.colNum)
), [gridArr, info.rowNum, info.colNum])
const {info: _info, onTouchFn} = useTouchEvent({
onTouchStart() {
// ...
},
onTouchMove() {
// ...
},
onTouchEnd() {
// ...
},
isDisable: {
all: !moveDirection // 当为 0 的时候,就是无法触摸
},
isStopPropagation: true
})
return (
<div {...onTouchFn}></div>
)
}
这里的 useTouchEvent 是封装的一个兼容移动端和pc端的触摸钩子。
这里每个 item
都是记录左上角的第一个数据的位置的。比如:关羽的位置是:(2,1);黄忠的位置是:(2,3)
checkRoadDirection
用于检查华容道中 item
可以移动的方向
js
/** 方向 1:上 2:右 3:下 4:左 */
type Direction = 1 | 2 | 3 | 4
type CheckDirectionRes = {[key in Direction]: number} | 0
/** 检查华容道item可以移动的方向 */
export function checkRoadDirection(arr: HeroesIndex[][], row: number, col: number): CheckDirectionRes {
if(!arr?.length) return 0
const value = arr[row][col]
if(value > 30) { // 小兵
return handleHeroDirectionVal({arr, row, col, status: 4})
} else {
let status: HeroesStatus = 1
if(value > 20) { // 五虎将
status = arr[row][col + 1] === value ? 2 : 3
}
return handleHeroDirectionVal({arr, row, col, status})
}
}
最终返回 0
则表示无法移动,返回对象则表示各方向上可以移动多少次。比如下面的结构,代表可以向下移动一次或向左移动3次,上和右则不能移动
js
{
1: 0,
2: 0,
3: 1,
4: 3,
}
handleHeroDirectionVal
用于处理各 item
的方向问题,根据传入的 status
判断,记下来需要遍历该格子 上右下左
四个方向上下一个格子是否可以移动,下一格为空,则该方向上加一。
js
type HeroesStatus = 1 | 2 | 3 | 4
/**
* @param status 1: boss 2: 横着的英雄 3: 竖着的英雄 4: 卒
*/
function handleHeroDirectionVal({arr, row, col, status}: {
arr: HeroesIndex[][], row: number, col: number, status: HeroesStatus
}): CheckDirectionRes {
const colNext = status === 2 || status === 1
const rowNext = status === 3 || status === 1
// 上右下左四个位置组成的数组。
const checkArr: checkItem[] = [
{addRow: -1, addCol: 0, colNext},
{addRow: 0, addCol: 1, rowNext},
{addRow: 1, addCol: 0, colNext},
{addRow: 0, addCol: -1, rowNext},
]
const res: CheckDirectionRes = {1: 0, 2: 0, 3: 0, 4: 0}
// 检查下一个格子是否为空
const checkNextGrid = ({addRow, addCol, rowNext, colNext}: checkItem, i: number) => {
const isColNext = colNext ? arr[row + addRow]?.[col + addCol + 1] === 0 : true
const isRowNext = rowNext ? arr[row + addRow + 1]?.[col + addCol] === 0 : true
if(arr[row + addRow]?.[col + addCol] === 0 && isColNext && isRowNext) {
res[(i + 1) as Direction]++
checkNextGrid({
addRow: addRow += checkArr[i].addRow,
addCol: addCol += checkArr[i].addCol,
rowNext,
colNext
}, i)
}
}
for(let i = 0; i < checkArr.length; i++) {
let {addRow, addCol, ...p} = checkArr[i]
if(i === 1 && colNext) addCol++;
if(i === 2 && rowNext) addRow++;
checkNextGrid({addRow, addCol, ...p}, i)
}
return Object.values(res).some(v => v) ? res : 0
}
type checkItem = {
addRow: number
addCol: number
rowNext?: boolean
colNext?: boolean
}
交换值的判断
每次移动一个 item
时,需要将对应数组中的值进行互换。
交换值的处理函数如下:
js
const onChangeGrid = ({p, target, direction, index}: onChangeGridParams) => {
function exChangeVal(row: number, col: number, row2: number, col2: number) {
[gridArr[row][col], gridArr[row2][col2]] = [gridArr[row2][col2], gridArr[row][col]];
}
// 遍历交换值
function onExChangeVal(arr: number[][]) {
arr.forEach(v => {
exChangeVal(p.row + v[0], p.col + v[1], target.row + v[0], target.col + v[1])
})
}
const isForward = direction === 2 || direction === 3 // 代表是正向
if(index < 1) { // boss
// ...
} else if(index <= 5) { // 五虎将
// ...
} else { // 小卒
// ...
}
setGridArr([...gridArr])
}
ts
// 格子的位置信息
export type GridPosition = {row: number, col: number}
export type onChangeGridParams = {
p: GridPosition
target: GridPosition
/** 当前移动的方向是:1:上 2:右 3:下 4:左 */
direction: Direction
// 代表是哪个 item
index: number
}
当是小卒的时候,只有一个格比较好处理,直接调用下方函数就可。
js
exChangeVal(p.row, p.col, target.row, target.col)
顺带一提,数组交互值可以这样简便写。
js
const arr = [1,2];
[arr[0], arr[1]] = [arr[1], arr[0]];
console.log(arr) // [2, 1]
当是五虎将需要交换两个格时,需要判断当前滑动是正向(向右,向下)还是反向(向左,向上),然后判断是竖的还是横向排列的五虎将;然后区分好后,就可知道除交换本格外,还需交换下一格还是右一格。
js
const arr = [[0, 0]];
const arrMethod = isForward ? 'unshift' : 'push';
// state.heroesIndexs 中保存的是横向或竖向的五虎将
arr[arrMethod](state.heroesIndexs[index - 1] ? [1, 0] : [0, 1])
onExChangeVal(arr)
当是曹操时,跟五虎将的判断是类似的,只是多了两个格子的判断而已。
js
const isVertical = direction === 3 || direction === 1 // 表示是向上或向下
const arr = [
[0, 0],
isVertical ? [0, 1] : [1, 0],
];
const arrMethod = isForward ? 'unshift' : 'push';
arr[arrMethod]([1, 1])
arr[arrMethod](isVertical ? [1, 0] : [0, 1])
onExChangeVal(arr)