使用 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)
相关推荐
mldong8 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排8 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60619 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角10 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!10 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰10 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi12 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen12 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马13 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu13 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss