TS实现简易的井字棋
前言
TS
的类型系统比较灵活,可以支持类型间的编程,由于过于灵活的编程技巧,所以也戏称TS
的类型体操。
TS
是图灵完备的,内部提供了条件语句(extends),递归,字符串,类型推断的运算,这使得我们可以用TS
编写更灵活的类型。我们可以编写普通的类型工具,同时,我们也可以去编写如斐波那契数列
,简单编译器
,中国象棋
,类型系统
这样的复杂类型。
那么,下面,我们就用TS
来简单实现一个井子棋吧。
仓库代码:TypeScript-Note/TicTacToe.ts at main · hua-bang/TypeScript-Note
基本思路
思路并非最佳方案,仅供参考哈。
规则: 两个玩家,一個打圈(◯),一個打叉(✗),轮流在3乘3的格上打自己的符号,最先以横、直、斜连成一线则为胜。实际上井字棋就是一个3*3的二维数组吧。
那么我们思考下需要实现的功能点
- 棋盘上棋子的分布情况。
- 棋盘的UI如何表现出来
- 判断棋盘结束(待更新)
- 下棋规则的实现
解决了以上的问题,基本我们就能实现一个简单的井子棋。
棋子的分布情况
整个棋盘,我们可以图上加上索引,抽象成一个3*3的二维数组。
我们就可以以如下的类型来进行描述(这里使用'❌' | '⭕️' | '🔲'符号, 其中'🔲'表示空白。)
type Chess = [
['🔲', '🔲', '🔲'],
['🔲', '⭕️', '🔲'],
['🔲', '🔲', '🔲']
]
举个例子
上图我们就可以以下的进行描述
type Chess = [
['❌', '⭕️', '⭕️'],
['🔲', '❌', '⭕️'],
['⭕️', '⭕️', '❌']
]
这里,我们就可以用二维数组描述棋子的分布情况了。
棋盘的UI展示
虽然说我们上方实现了一个二维数组,感觉也能体现出UI,但是由于TS
,描述类型的时候,会将数组放在一行,会显示的不直观。
所以最后决定使用索引类型去展示,如下。
所以我们可以实现一个类型,将上方的棋子,转述成上方的类型就可以了。
判断棋盘结束(待更新)
目前的一个方案,是去穷举所有可能的情况,来进行判断。(但方案不是很简洁,需要我们去把所有情况列出来,这不是很优雅,所以后续会想新方案来进行更新)
ps:文中仅列举一种方式。
下面是伪代码
type WinResult =
[['⭕️', '❌' | '🔲', '❌' | '🔲'], ['❌' | '🔲', '⭕️', '❌' | '🔲'], ['❌' | '🔲', '❌' | '🔲', '⭕️']]
| // 其他情况,就不列举了
type res = Chess extends WinResult ? true : false;
下棋规则
下棋规则主要分为
- 检查棋盘是否结束。
- 检查下棋的位置是否合法。
- 更新棋子的分布情况。
- 根据新棋子生成新棋盘
我们实现一个Draw
类型,去进行以下操作即可
检查棋盘是否结束
直接使用上方棋盘的逻辑。
下棋是否合法
实现一个CheckDrawStepLegal,检测是否合法(即下棋的位置是否为空),不合法抛出错误。
// 伪代码
type CheckDrawStepLegal<T> = T extends Condition ? 走棋逻辑 : '错误信息';
更新棋子的分布情况
将对应的空位置进行更新即可。
举个例子
// 原先的chess
type prevChess = [
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲']
];
Draw<1, 1, '⭕️', 带有该棋子的棋盘>;
type nextChess = [
['🔲', '🔲', '🔲'],
['🔲', '⭕️', '🔲'],
['🔲', '🔲', '🔲']
]
具体实现
!!! 由于部分代码其实还有点hack,并且该思路不是一个完美的解决方案,不建议深入,仅供参考。
棋子和棋盘
棋子
首先,我们定义棋子类型,这里我们分两类棋子( '❌' | '⭕️'),同时,我们要支持空棋子。
type SignType = '❌' | '⭕️' | '🔲';
棋盘
接着,我们就定义整个棋盘吧
这里我们希望这上面能存放以下的数据
interface Board {
ui: UI界面;
lines: 目前的整个棋子分布,即上方的BoardLines类型。
prevPlayer: 上一步的棋手;
nextPlayer: 下一步的棋手;
isEnd: 是否结束;
winner: 胜利者;
}
棋子的分布情况
其次,我们需要描述棋子的分布情况。
行 - BoardLine
首先定义每一行,是由三个棋子组合而成的。
我们使用BoardLine
来描述
type BoardLine = [SignType, SignType, SignType];
整体 - BoardLines
接着,我们去定义井字棋的三行的类型。
type KeyOfBoardLines = '0' | '1' | '2';
type BoardLines = {
[key in KeyOfBoardLines]: BoardLine;
};
棋盘的UI展示
实质上就是将棋子的分布情况,用索引表示出来,同时类似于数组的Join
来进行展示。
type Join<T extends string[], U extends string> = T extends [infer First, ...infer Rest]
? Rest extends []
? First
: First extends string
? Rest extends string[]
? `${First}${U}${Join<Rest, U>}`
: never
: never
: '';
type TransformBoardLinesToUI<Lines extends BoardLines> = {
[key in keyof Lines as key extends '0' | '1' | '2' ? key : never]: Lines[key] extends BoardLine ? Join<Lines[key], ' '> : [];
};
type Chess = [
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲']
];
type UI = {
0: "🔲 🔲 🔲";
1: "🔲 🔲 🔲";
2: "🔲 🔲 🔲";
}
以上就简单实现了局面
整个棋盘类型的生成
棋盘上带有多种类型,使得我们后面的操作更为简单。
type EmptyBoardLines = [
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲'],
['🔲', '🔲', '🔲']
];
type Board<Lines extends BoardLines = EmptyBoardLines, player extends SignType = '⭕️'> = {
ui: TransformBoardLinesToUI<Lines>;
lines: Lines;
prevPlayer: player;
nextPlayer: player extends '⭕️' ? '❌' : '⭕️';
isEnd: Lines extends WinResult ? true : false;
winner: Lines extends WinResult ? player : 'none';
};
下棋规则
检查棋盘是否结束
type Board<Lines extends BoardLines = EmptyBoardLines, player extends SignType = '⭕️'> = {
// 判断穷举的情况是否由当前棋盘
isEnd: Lines extends WinResult ? true : false;
winner: Lines extends WinResult ? player : 'none';
};
下棋是否合法
这里,我们只需要确定是否走到空位置即可。
type CheckDrawStepLegal<X extends KeyOfBoardLines, Y extends KeyOfBoardLine, Lines extends BoardLines> = Lines[X][Y] extends '❌' | '⭕️' ? false : true;
type GenerateBoardError<B extends Board<BoardLines, SignType>> = '走法不合理,请重新走棋。'
type Res = CheckDrawStepLegal<X, Y, B['lines']> extends true
? Board<ReplaceBoardLinesItem<X, Y, B['lines'], Sign>, Sign>
: GenerateBoardError<B>
更新棋子的分布情况
实际上,下棋就是更新棋子的分布情况。
- 根据位置和下棋的符号,确定新的棋子分布情况
- 根据棋子的分布情况生成新的棋盘。
type ReplaceBoardLineItem<Index extends KeyOfBoardLine, Line extends BoardLine, Sign extends SignType> = ReplaceItemInArr<Index, Line, Sign>;
type ReplaceBoardLinesItem<X extends KeyOfBoardLines, Y extends KeyOfBoardLine, Lines extends BoardLines, Sign extends SignType> = Lines extends Array<BoardLine> ? ReplaceItemInArr<X, Lines, ReplaceBoardLineItem<Y, Lines[X], Sign>> : EmptyBoardLines;
// 新的棋子的分布情况
type NextBoardLines = ReplaceBoardLinesItem<X, Y, '原先的棋子', Sign>, Sign;、
// 新的棋盘
type NextBoard = Board<NextBoardLines, Sign>
总体效果
初始化棋盘
进行走棋
走法不合理的情况
走棋子结束的情况
总结
上方实现了一个简单的井字棋,这样体现了TypeScript
类型编程的灵活之处。
仓库代码:TypeScript-Note/TicTacToe.ts at main · hua-bang/TypeScript-Note
当然,上方的实现并不完美,还有很多地方是可以挑细节问题的,并且当初编写的时候,没有考虑效率问题。
但想要表达的是:
- 我们确实能通过类型编程,去实现一些我们的想法吧。 这个过程其实还是蛮有趣的,同时也许你会加深对类型编程的理解。
- 很多时候,其实类型体操对我们的日常开发的帮助不大,甚至当我们编写很复杂的类型的时候,反而会提供理解成本。所以还是理性的对待类型编程和类型体操,我们使用他的目的更多是为了让开发更为规范,提效。
参考资料
转载自:https://juejin.cn/post/7128621293011730469