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

华容道组件

使用技术:React + ts

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

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

demo试玩及文档

组件源码

该组件跟我之前开发的 滑块拼图组件 是类似的。

使用描述

引入 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)
相关推荐
崔庆才丨静觅2 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅24 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
剪刀石头布啊2 小时前
生成随机数,Math.random的使用
前端