「井字棋」小游戏 - Rax 进阶重构

1,494 阅读4分钟

前言

在之前实现的 Rax.js+Ts+ESlint「旅游官网」实战项目 的基础上,利用 React 官方项目例子 GoBang 井字棋小游戏 来进一步熟悉 Rax 框架。

井字棋小游戏例子在官方文档中实例已经写得很清楚详细,本文就不稍加介绍了嘿,主要是将该小游戏以 Rax 进行重构,并且使用 function 组件和 hooks 的方式进行项目案例的更新替代。

1.gif

实现功能效果如下:

  1. tic-tac-toe(三连棋)游戏的所有功能
  2. 能够判定玩家何时获胜
  3. 能够记录游戏进程
  4. 允许玩家查看游戏的历史记录,也可以查看任意一个历史版本的游戏棋盘状态

而本篇文章的目标是进阶优化完善升级小游戏项目的功能点:

  1. 在游戏历史记录列表显示每一步棋的坐标,格式为 (列号, 行号)。
  2. 在历史记录列表中加粗显示当前选择的项目。
  3. 使用两个循环来渲染出棋盘的格子,而不是在代码里写死(hardcode)。
  4. 添加一个可以升序或降序显示历史记录的按钮。
  5. 每当有人获胜时,高亮显示连成一线的 3 颗棋子。
  6. 当无人获胜时,显示一个平局的消息。

项目

Gobang 小游戏嵌入旅游项目

2.gif

重点:利用 Rax 的路由进行跳转。

路由配置

/src/app.json

{
  "routes": [
    {
      "path": "/",
      "source": "pages/Home/index"
    },
    {
      "path": "/gobang",
      "source": "pages/Gobang/index"
    }
  ],
  "window": {
    "title": "柃木🎈"
  }
}

旅游导航

/src/components/Header.tsx

import { history } from 'rax-app';

// ...代码省略
// 在 {/* navMenu */} 中加入 Gobang 导航
<div className={styles.navLink} key="gobang" onClick={() => history.push('gobang')}>
  Gobang
</div>

Gobang 导航

Gobang 返回旅游页面

/src/pages/Gobang/index.tsx

import { history as router } from 'rax-app';

// ... 省略

// 添加返回旅游页面代码
<div style={{ textAlign: 'center' }} className="button buttonPrimary buttonBig buttonNoRound" onClick={() => router.push('/')}>
          返回旅游页面
</div>

添加棋子坐标

3.gif

重点:利用棋盘点击事件上的 i 来确定当前棋子坐标。

Gobang 页面

/src/pages/Gobang/index.tsx

// 给 history 对象状态添加 nowKey 属性,记录当前坐标。
const [history, setHistory] = useState([
  {
    squares: Array(9).fill(null),
    nowKey: '',
  },
]);

// 在 handleClick 事件中根据 `i` 计算出当前棋子的坐标,并调用 effect 事件更新记录。
const handleClick = (i: string | number) => {
  const nowKey = [(+i % 3) + 1, Math.floor((+i / 3)) + 1].join(',');
  // ...省略代码
  const historyTemp = [...curHistory, { squares, nowKey }];
  setHistory(historyTemp);
}

// 在游戏历史记录列表显示每一步棋的坐标
const moves = history.map((step, move) => {
  const desc = move ? `Go to move #${move} \n Chess position: (${step.nowKey})` : 'Go to game start';
  // ...省略代码
});

需要更新一个 li 的外边距以及 li > button 的宽高样式。

显示当前选择的历史项目

4.gif

重点:历史项目上样式的 focus,伪元素和伪类是可以直接套用添加。

Gobang 样式

/src/pages/Gobang/index.module.css

li > button {
  font-size: 16px;
  width: 160px;
  height: 100%;
  position: relative;
}
li > button:focus:before {
  content: "";
  position: absolute;
  top: -4px;
  left: -4px;
  right: -4px;
  bottom: -4px;
  border: 4px solid gold;
  transition: all 0.5s;
  animation: clipPath 3s infinite linear;
}
@keyframes clipPath {
  0%,
  100% {
    clip-path: inset(0 0 95% 0);
  }

  25% {
    clip-path: inset(0 95% 0 0);
  }
  50% {
    clip-path: inset(95% 0 0 0);
  }
  75% {
    clip-path: inset(0 0 0 95%);
  }
}

循环渲染格子

5.gif

重点:Array.map() 和对应的 key 值

Board 棋盘

/src/components/Board/index.tsx

// 省略代码
// 给格子添加 key 值
<Square key={i} value={props.squares[i]} onClick={() => props.onClick(i)} />;

 <View>
  // 使用两个循环来渲染出棋盘的格子
  {[0, 1, 2].map((v) => {
    return (
      <div key={v} className={styles.boardRow}>
        {[NaN, NaN, NaN].map((v2, i) => {
          return (renderSquare(v * 3 + i));
        })};
      </div>
    );
  })}
</View>
  • 注意不能使用 Array(3) 来初始化空数组,当空数组时不能被渲染出来
  • key 值不能是 index 索引
  • 注意传入方法 renderSquare 的值与外层循环的索引值的关系

排序历史记录

6.gif

重点:设置 sort 状态,并且调用 Array.reverse() 调转历史记录数组

Gobang 页面

/src/pages/Gobang/index.tsx

// 设置 sort 状态
const [sort, setSort] = useState(false);

// 判断是否需要调转历史记录数组
sort && moves.reverse();

// ... 省略代码
// 排序按钮
<div
  style={{ textAlign: 'center', width: '100px' }}
  className="button buttonPrimary buttonNoBig buttonRound"
  onClick={() => setSort(!sort)}
  >
  排序
</div>

标识获胜棋子

7.gif

重点:获胜时,获取到获胜棋子的编号,通过 prop 传递到棋子 Square 判断改变棋子样式

样式

/src/global.css

/* 设置获胜样式 */
.winner {
  background: firebrick !important;
  color: white;
}

注意需要 !important 否则优先级样式被覆盖。

获胜棋子编号

/src/utils/index.ts

// 判断获胜棋子方法中
export function calculateWinner(squares: string[]) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      // 更改这一步,返回一个对象,包含获取棋子的编号 square
      return { winner: squares[a], square: lines[i] };
    }
  }
  return null;
}

/src/pages/Gobang/index.tsx

更改判断 winner 的条件和传递获胜棋子数组

- if (calculateWinner(squares) || squares[i]) {
+ if (calculateWinner(squares)?.winner || squares[i]) {
      return;
}
  
// 当前棋盘
const current = history[stepNumber];
- const winner = calculateWinner(current.squares);
+ const calculate = calculateWinner(current.squares);
+ const winner = calculate?.winner;
+ let winSquare: number[] | undefined;
  
// 判断 winner 条件
if (winner) {
  status = `Winner: ${winner}`;
+ winSquare = calculate?.square;
// ... 省略代码

// 传递 props
<div className={styles.gameBoard}>
- <Board squares={current.squares} onClick={(i) => handleClick(i)} />
+ <Board squares={current.squares} winner={winSquare} onClick={(i) => handleClick(i)} />
</div>

棋盘判断获胜

/src/components/Board/index.tsx

- function Board(props: { squares: { [x: string]: any }; onClick: (arg0: any) => any }) {
+ function Board(props: { squares: { [x: string]: any }; winner?: number[], onClick: (arg0: any) => any }) {
  const renderSquare = (i: number) => {
    - return <Square key={i} value={props.squares[i]} onClick={() => props.onClick(i)} />;
    + const winner = props.winner?.includes(i);
    + return <Square key={i} winner={winner} value={props.squares[i]} onClick={() => props.onClick(i)} />;
  };
// ... 省略代码

棋格渲染

/src/components/Square/index.tsx

- function Square(props: { value: number, onClick: () => void; }) {
+ function Square(props: { value: number, winner?: boolean, onClick: () => void; }) {
  return (
- 	<button className={styles.square} onClick={() => props.onClick()}>
+ 	<button className={`${props.winner ? 'winner' : ''} ${styles.square}`} onClick={() => props.onClick()}>
      {props.value}
    </button>
  );
}

平局

8.gif

重点:很简单,判断一下历史条目的长度,改变显示内容

Gobang 页面

/src/pages/Gobang/index.tsx

if (winner) {
  status = `Winner: ${winner}`;
  winSquare = calculate?.square;
// 判断长度,显示平局内容。
} else if (moves.length === 10) {
  status = 'Draw, start over game';
} else {
  status = `Next player: ${xIsNext ? 'X' : 'O'}`;
}

项目总览

以上就是 Rax.js 重构 React 官方项目例子 GoBang 井字棋小游戏 并且实现进阶功能和优化项目的具体内容。下面简单进行项目总览:

9.gif

后言

目前个人对于 React.js 和 Rax.js 的基础知识学习暂时到这里,后边在实际项目中成长,争取学习后继续与小友们总结分享~
若学习过程有改进的地方,希望有大佬能给我指点一二。

项目仓库:githubgitee