likes
comments
collection
share

TS实现简易的井字棋

作者站长头像
站长
· 阅读数 14

前言

TS的类型系统比较灵活,可以支持类型间的编程,由于过于灵活的编程技巧,所以也戏称TS的类型体操。

TS是图灵完备的,内部提供了条件语句(extends),递归,字符串,类型推断的运算,这使得我们可以用TS编写更灵活的类型。我们可以编写普通的类型工具,同时,我们也可以去编写如斐波那契数列,简单编译器,中国象棋,类型系统这样的复杂类型。

那么,下面,我们就用TS来简单实现一个井子棋吧。

效果:TS Playground 井字棋

仓库代码:TypeScript-Note/TicTacToe.ts at main · hua-bang/TypeScript-Note

TS实现简易的井字棋

基本思路

思路并非最佳方案,仅供参考哈。

规则: 两个玩家,一個打圈(◯),一個打叉(✗),轮流在3乘3的格上打自己的符号,最先以横、直、斜连成一线则为胜。实际上井字棋就是一个3*3的二维数组吧。

那么我们思考下需要实现的功能点

  1. 棋盘上棋子的分布情况。
  1. 棋盘的UI如何表现出来
  1. 判断棋盘结束(待更新)
  1. 下棋规则的实现

解决了以上的问题,基本我们就能实现一个简单的井子棋。

棋子的分布情况

整个棋盘,我们可以图上加上索引,抽象成一个3*3的二维数组。

我们就可以以如下的类型来进行描述(这里使用'❌' | '⭕️' | '🔲'符号, 其中'🔲'表示空白。)

type Chess = [
    ['🔲', '🔲', '🔲'], 
    ['🔲', '⭕️', '🔲'], 
    ['🔲', '🔲', '🔲']
]

举个例子

TS实现简易的井字棋

上图我们就可以以下的进行描述

type Chess = [
    ['❌', '⭕️', '⭕️'], 
    ['🔲', '❌', '⭕️'], 
    ['⭕️', '⭕️', '❌']
]

这里,我们就可以用二维数组描述棋子的分布情况了。

棋盘的UI展示

虽然说我们上方实现了一个二维数组,感觉也能体现出UI,但是由于TS,描述类型的时候,会将数组放在一行,会显示的不直观。

TS实现简易的井字棋

所以最后决定使用索引类型去展示,如下。

TS实现简易的井字棋

所以我们可以实现一个类型,将上方的棋子,转述成上方的类型就可以了。

判断棋盘结束(待更新)

目前的一个方案,是去穷举所有可能的情况,来进行判断。(但方案不是很简洁,需要我们去把所有情况列出来,这不是很优雅,所以后续会想新方案来进行更新)

ps:文中仅列举一种方式。

下面是伪代码

type WinResult = 
    [['⭕️', '❌' | '🔲', '❌' | '🔲'], ['❌' | '🔲', '⭕️', '❌' | '🔲'], ['❌' | '🔲', '❌' | '🔲', '⭕️']]
  | // 其他情况,就不列举了

type res = Chess extends WinResult ? true : false;

下棋规则

下棋规则主要分为

  1. 检查棋盘是否结束。
  1. 检查下棋的位置是否合法。
  1. 更新棋子的分布情况。
  1. 根据新棋子生成新棋盘

我们实现一个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来描述

TS实现简易的井字棋

type BoardLine = [SignType, SignType, SignType];

整体 - BoardLines

接着,我们去定义井字棋的三行的类型。

TS实现简易的井字棋

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: "🔲 🔲 🔲";
}

以上就简单实现了局面

TS实现简易的井字棋

整个棋盘类型的生成

棋盘上带有多种类型,使得我们后面的操作更为简单。

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>

TS实现简易的井字棋

更新棋子的分布情况

实际上,下棋就是更新棋子的分布情况。

  1. 根据位置和下棋的符号,确定新的棋子分布情况
  1. 根据棋子的分布情况生成新的棋盘。
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> 

总体效果

初始化棋盘

TS实现简易的井字棋

进行走棋

TS实现简易的井字棋

走法不合理的情况

TS实现简易的井字棋

走棋子结束的情况

TS实现简易的井字棋

总结

上方实现了一个简单的井字棋,这样体现了TypeScript类型编程的灵活之处。

仓库代码:TypeScript-Note/TicTacToe.ts at main · hua-bang/TypeScript-Note

当然,上方的实现并不完美,还有很多地方是可以挑细节问题的,并且当初编写的时候,没有考虑效率问题。

但想要表达的是:

  1. 我们确实能通过类型编程,去实现一些我们的想法吧。 这个过程其实还是蛮有趣的,同时也许你会加深对类型编程的理解。
  1. 很多时候,其实类型体操对我们的日常开发的帮助不大,甚至当我们编写很复杂的类型的时候,反而会提供理解成本。所以还是理性的对待类型编程和类型体操,我们使用他的目的更多是为了让开发更为规范,提效

参考资料