likes
comments
collection
share

使用 React 开发一个华容道组件

作者站长头像
站长
· 阅读数 22
使用 React 开发一个华容道组件

华容道组件

使用技术:React + ts

由曹操,五虎将和4个小卒组成的华容道小游戏。

整体由 4 * 5 的格子组成,曹操占4格,五虎将横或竖向占2格,卒占1格,两个空格,当曹操移动到最底下时,游戏获胜。

demo试玩及文档

组件源码

使用描述

引入 lhh-ui 组件库

npm i lhh-ui

导入其中的 HuarongRoad 组件。

import { HuarongRoad } from "lhh-ui"

HuarongRoad.Item 用作自定义各 item 中的 children 内容。

HuarongRoad.Item 传入的 index 用来标记其中的内容。

index描述
0曹操
1-5五虎将
6-10

item 的数量加起来不到 10 个时,组件会自动补充,所以其实不传 item 也是可以展示出华容道的。

// 这样就是采用组件自带的样式了
<HuarongRoad width={400}></HuarongRoad>

demo代码

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>
  )
}

组件主要源码简述

华容道位置结构

华容道位置信息采用一个二维数组保存,大体结构如下:

[
  [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格

大体描述如图所示:

使用 React 开发一个华容道组件

移动 item

我的描述可能不太好,查看 完整代码 会比较清晰。处理 item 移动的判断结构大致如下:

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 可以移动的方向

/** 方向 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次,上和右则不能移动

{
  1: 0,
  2: 0,
  3: 1,
  4: 3,
}
  • handleHeroDirectionVal

用于处理各 item 的方向问题,根据传入的 status 判断,记下来需要遍历该格子 上右下左 四个方向上下一个格子是否可以移动,下一格为空,则该方向上加一。

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 时,需要将对应数组中的值进行互换。

交换值的处理函数如下:

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])
}
// 格子的位置信息
export type GridPosition = {row: number, col: number}

export type onChangeGridParams = {
  p: GridPosition
  target: GridPosition
  /** 当前移动的方向是:1:上 2:右 3:下 4:左 */
  direction: Direction
  // 代表是哪个 item 
  index: number
}

当是小卒的时候,只有一个格比较好处理,直接调用下方函数就可。

exChangeVal(p.row, p.col, target.row, target.col)

顺带一提,数组交互值可以这样简便写。

const arr = [1,2];
[arr[0], arr[1]] = [arr[1], arr[0]];
console.log(arr) // [2, 1]

当是五虎将需要交换两个格时,需要判断当前滑动是正向(向右,向下)还是反向(向左,向上),然后判断是竖的还是横向排列的五虎将;然后区分好后,就可知道除交换本格外,还需交换下一格还是右一格。

const arr = [[0, 0]];
const arrMethod = isForward ? 'unshift' : 'push';
// state.heroesIndexs 中保存的是横向或竖向的五虎将
arr[arrMethod](state.heroesIndexs[index - 1] ? [1, 0] : [0, 1])
onExChangeVal(arr)

当是曹操时,跟五虎将的判断是类似的,只是多了两个格子的判断而已。

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)