“我正在参加掘金社区游戏创意投稿大赛个人赛,详情请看:游戏创意投稿大赛”
代码仓库,走过路过点一个 Star ✨
在移动之前
我们的目的是移动这个棋子,实际上我们是对于这个棋盘的状态进行一个处理,让 React 去重新渲染出来就行,如果说要考虑性能的问题,我是不相信一个吃我大几百兆内存的现代浏览器连几百个 Dom 节点渲染都会卡的,回到上一句话,我们对于棋盘的处理实际上就是变成了对这个二维数组进行处理。
移动这些棋子
在不考虑是否正确移动的情况下,第一次点击会选中一个棋子,第二次我们会去选中一个目标格子,然后我们对数组中这两个值进行处理,把原来的格子更新为空,把目标格子上的棋子更新为我们之前选中的棋子。
// Before
// -> ' +------------------------+
// 8 | r n b q k b n r |
// 7 | p p p p . p p p |
// 6 | . . . . . . . . |
// 5 | . . . . p . . . |
// 4 | . . . . P P . . |
// 3 | . . . . . . . . |
// 2 | P P P P . . P P |
// 1 | R N B Q K B N R |
// +------------------------+
// a b c d e f g h'
// After
// -> ' +------------------------+
// 8 | r n b q k b n r |
// 7 | p p . p . p p p |
// 6 | . . . . . . . . |
// 5 | . . p . p . . . |
// 4 | . . . . P P . . |
// 3 | . . . . . . . . |
// 2 | P P P P . . P P |
// 1 | R N B Q K B N R |
// +------------------------+
// a b c d e f g h'
最终这个棋子的移动问题就变成了数组处理的问题。
export const initMap = [
[BLACK.rock, BLACK.knight, BLACK.bishop, BLACK.queen, BLACK.king, BLACK.bishop, BLACK.knight, BLACK.rock],
[BLACK.pawn, BLACK.pawn, BLACK.pawn, BLACK.pawn, BLACK.pawn, BLACK.pawn, BLACK.pawn, BLACK.pawn],
[FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank,],
[FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank,],
[FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank,],
[FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank, FUNC.blank,],
[WHITE.pawn, WHITE.pawn, WHITE.pawn, WHITE.pawn, WHITE.pawn, WHITE.pawn, WHITE.pawn, WHITE.pawn],
[WHITE.rock, WHITE.knight, WHITE.bishop, WHITE.queen, WHITE.king, WHITE.bishop, WHITE.knight, WHITE.rock],
];
interface CellProps {
col: number;
row: number;
}
export const move = (board: string[][], startPos: CellProps, endPos: CellProps) => {
const startPiece = board[startPos.row][startPos.col]
if (isEqual(startPos, endPos)) {
return board;
}
return board.map((row, rowIndex) => {
return row.map((col, colIndex) => {
if (rowIndex === endPos.row && colIndex === endPos.col) {
return startPiece;
} if (rowIndex === startPos.row && colIndex === startPos.col) {
return FUNC.blank;
} else {
return col;
}
})
})
}
写上一些测试是一个好的习惯
你应该听说过 TDD(Test Driven Development),BDD(Behavior Driven Development)这些单词,你可能并没有真正意义上写过几个测试,也许你会把测试这个动作后置,你先写出了实现的代码,然后才编写测试,这样其实你陷入了一个陷阱,你会为了通过测试,而使得你故意去忽略掉了一些 edge cases,这样其实是违反了 TDD 的原则的,而 TDD 是需要你先定义好这个函数的边界,写出这个测试,把可能出现的情况想透彻,然后再去编写你的函数。So called test driven development. 因此你需要前置这个动作,虽然在一开始你会很难受,会觉得这样我功能都来不及写,还写测试。在你最后修改整个项目代码的时候,你就知道当时这些测试是多么的明智,多么痛的领悟,如果你的项目有详尽的测试,那么你在修改你功能函数的时候,你会更加自信。而不会担心下面这种极端的情况出现。
这里写上一个针对这个 move 函数的测试,其实测试并不难写,测试的本质就是我期望这输入在各种情况下得到什么样的输出?只要牢记这个问题,你就能写出测试。
这里我们使用 create-react-app 自带的 jest 框架进行一个简单的测试,你只需要 npm run test 就可以运行测试了,create-react-app 还很贴心的加上了 watch 模式,所以我可以边写边保存边看结果,而不需要去命令行中重复的去运行这个命令。这里对于如何配置不会多描述,官方文档上面写的非常详细,各个环境,各个语言都有很详细的描述,足够 cover 运行起一个简单的测试。
这个 move函数在src/operations/index.ts 中, 首先我们在相同的目录下面新建一个文件叫做index.test.ts在按照文档中配置完成之后,实际上用 create-react-app 的话你不需要做很多额外的配置,如果你是使用 TypeScript 的话则需要加上 "@types/jest": "^27.4.0",这个 deps就可以了。
那么接下来我们就可以写测试了,回到刚才提到的,我期望这输入在各种情况下得到什么样的输出?所以 Jest 对应的 DSL 也是非常直接
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
expect(sum(1, 2)).toBe(3);其实这里就非常直观了,我期望 1 + 2 = 3 。
回到我们的 move函数,我们期望什么?
test('board: from [0][0] to [0][0] did not move', () => {});
test('board: from [0][0] to [0][3] did move', () => {});
那么上面的期望就是从 [0 , 0] 移动到 [0, 0],这是不是没有移动?是的这个没有移动。那从 [0, 0] 移动到 [0, 3] 这个是不是移动了?没错,就是这么简单。那接下来我们需要实现这个测试
import { move } from './index';
test('board: from [0][0] to [0][0] did not move', () => {
const startPos = { col: 0, row: 0 }
const endPos = { col: 0, row: 0 }
const newBoard = move(initMap, startPos, endPos);
expect(newBoard).toEqual(initMap);
});
test('board: from [0][0] to [0][3] did move', () => {
const startPos = { col: 0, row: 0 }
const endPos = { col: 0, row: 3 }
const newBoard = move(initMap, startPos, endPos);
expect(newBoard[startPos.row][startPos.col]).toBe(FUNC.blank);
expect(newBoard[endPos.row][endPos.col]).toBe(initMap[startPos.row][startPos.col]);
});
那这样两个测试就写好啦。
使用 Scenario
仔细看上面两个测试,我都写了board: from ...那这种情况下,可以看出这两个测试是同属一个测试场景的,那么我们可以把以上代码改造成如下的形式
describe('moves on board', () => {
const startPos = { col: 0, row: 0 }
const endPosA = { col: 0, row: 0 }
const endPosB = { col: 0, row: 3 }
test('from [0][0] to [0][0] did not move', () => {
const newBoard = move(initMap, startPos, endPosA);
expect(newBoard).toEqual(initMap);
});
test('from [0][0] to [0][3] did move', () => {
const newBoard = move(initMap, startPos, endPosB);
expect(newBoard[startPos.row][startPos.col]).toBe(FUNC.blank);
expect(newBoard[endPosB.row][endPosB.col]).toBe(initMap[startPos.row][startPos.col]);
});
})
最终你可能得到以下的输出,因为我还写了其他的测试,所以最终结果会变成这样。
PASS src/operations/index.test.ts
get pieces
✓ white pieces (2 ms)
✓ black pieces (1 ms)
✓ empty grid
get grid axis
✓ grid axis (1 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 2.113 s
Ran all test suites.
Watch Usage: Press w to show more.