新年将至,如何教会别人玩四川麻将?——编个程序算一算

2,123 阅读18分钟

前言

程序来源于生活,程序改变生活,编程让生活变得更美好。

年关将至,相信很多同学已经结束了辛苦的一年的工作开启了假期模式,抵达家乡之后,很多亲朋好友聚在一起,大家可以打打麻将交流交流感情。不过,对于常年奔跑在工作中的我们来说,牌技着实拙计,别急,今天我们就编个程序帮忙算一算,哈哈哈,让我们这个春节都不吃亏,祝大家运气都旺旺旺。

成都麻将的规则

四川麻将的规则非常简单,各地有些许的不同,本文就以成都麻将的规则为例,编写一个算法根据手牌计算出听牌。

首先,成都麻将108张,分别是36张筒,36张条,36张万,每个玩家拿13张牌,作为起始游戏的玩家拿14张牌。

玩家需要至少定缺一门,比如,,花色,玩家的手牌不能超过2门,超过则称之为花猪,不能听牌。

然后,玩家可以有多种胡牌方式,一种比较简单的胡牌方式叫做 7对,就是手里面有6对对子,然后剩下的单的牌就是你听的牌

剩下的,也就是最复杂的胡牌计算公式了,这个公式叫做DD + m*ABC + n*XXX,其中,DD在四川麻将中称为将,然后ABC公差为1的等差数列XXX就是完全一样的3个牌,成都麻将称之为

胡牌的时候,系数mn可能为0。

因为或者的牌可以视为已经处理好的手牌,所以算法在做处理的时候,只需要考虑手牌是否能够满足公式即可。

下面,是几种听牌的方式:

七对: image.png 单钓(对子胡): image.png 清一色(一条龙): image.png 普通对子胡: image.png 平胡: image.png image.png

编写算法计算听牌

成都麻将有3种花色,因此可以用一个类型来表示:

type EntityCategory = "筒" | "条" | "万";

设计麻将类

class Entity {
  /**
   * 牌的类型,筒条万
   */
  type: EntityCategory;
  /**
   * 牌的内容,
   */
  size: number;

  constructor({ type, size }: { size?: number; type?: EntityCategory } = {}) {
    size && (this.size = size);
    type && (this.type = type);
  }
}

编写检测七对听牌方式的算法

七对胡牌类型的算法实现起来相对比较简单,因为我们只需要枚举手里面的牌即可。

七对必须要求玩家手里的牌张数为13,多少都不符合七对的类型。

如果玩家还是花猪,那么肯定是不能听牌的,玩家的牌,只有点数一样,花色一样,才能算的上对子,我们不关心这些对子,我们其实只关心那一张单的牌,因此,我的检测程序实现如下:

/**
 * 七对的胡牌类型,保证传入的参数是已经进行过排序的,找到7对的听牌
 */
export function sevenPairHandCalc(group: Entity[][]) {
  // 暂未定缺,还不可以进行胡牌
  if (group.length === 3) {
    return -1;
  }
  // 将牌型进行合并
  const list = group.reduce((l, c) => {
    return l.concat(c);
  });
  // 如果牌的内容不为13,说明肯定是不是7对
  if (list.length !== 13) {
    return -1;
  }
  let targetVal = -1;
  let i = 0;
  while (i < list.length) {
    // 说明当前是一对牌,花色一样,内容一样,可以直接跳过到下一对进行比较
    if (
      list[i + 1] &&
      list[i].size === list[i + 1].size &&
      list[i].type === list[i + 1].type
    ) {
      // 跳到下一对进行比较
      i += 2;
    }
    // 说明当前这个牌是单的,如果是符合预期的牌型的话,那么,这个就是最终听的牌,否则说明不是听牌
    else if (
      list[i + 1] &&
      (list[i].size !== list[i + 1].size || list[i].type !== list[i + 1].type)
    ) {
      if (targetVal === -1) {
        targetVal = list[i].size;
        i++;
      } else {
        return -1;
      }
    }
    // 若最后一个牌才是单的,之前的都是双的,则最后一个也是可以胡的牌
    else if (!list[i + 1] && targetVal === -1) {
      return list[i].size;
    }
  }
  // 返回找到的听牌
  return targetVal;
}

编写检测一般胡牌方式的检测算法

检测一般胡牌方式的算法相对也复杂一些,用计算机的视角来说,就是不断的尝试。

首先,我们对玩家的手牌按照类别和点数进行排序,这个操作就可以对应实际操作的时候我们理牌的那个过程。

玩家的手牌必须是对3取余之后为1,手牌多了少了都是相公(实际打牌的过程中,我们可能会因为自己的疏忽,而导致手牌多了或手牌不足无法胡牌的场景十有八九)

然后,还是跟之前聊到过的规则一样,玩家手牌必须最多只能持有2种花色,花猪是不能听牌的。

很容易想到的一个点,玩家能胡的牌,只能是手牌里面持有的花色1-9,所以,我们这一步可以先生成玩家手牌的可胡的所有牌,最后把这个集合送进玩家的手牌依次进行尝试。

然后,就是开始不断的枚举了。我们需要尝试将一张牌加入到玩家的手牌中,计算玩家的手牌是否可以满足这个公式DD + m*ABC + n*XXX,系数m和n可以为0

枚举,该怎么样来进行枚举呢?之前我们在规则那一小节提出的将对,不知道同学们还有没有印象。我们首先先找出玩家手里面有两张牌以上的牌,然后取两张,把剩余的牌划分成一个集合。

玩家手里面,可能有很多对牌,所以这个时候找出来的组合也可能有很多种。

在得到了除了将对以外的所有组合集合以后,事情看起来就变得简单了。

我们只需要取出一个集合,每次尝试从这个组合里面抵消AAAABC(公差为1的等差数列)这样的组合,若经过不断的抵消之后,最终剩余集合的元素为0,则认为玩家是可以胡这张牌的。

因为玩家至多持有2种花色,因此必须两种花色都满足这种组合方式才算听牌。

在计算胡牌的过程中,每次抵消一组牌的组合时,必须要优先消除AAA这样的牌,否则会造成错误的计算结果。

所以,这就是根据以上思路编写的算法:

/**
 * 普通胡牌的计算,保证传入的参数是已经进行过排序的
 */
export function normalHandCalc(group: Entity[][]) {
  // 尚未打缺,无法胡牌
  if (group.length === 3) {
    return [];
  }
  // 将不同的牌型进行合并
  const list = group.reduce((l, c) => {
    return l.concat(c);
  });
  // 相公,即手牌不足或者手牌多了,都无法胡牌
  if (list.length > 13 || list.length % 3 !== 1) {
    return [];
  }
  // 找出当前手牌能够可以胡的牌
  const selectTargetEntity: Entity[] = [];
  // 如果玩家不是清一色,那么,玩家可以胡牌的类型就是持有两种类型牌的其中之一
  const type1 = group[0][0].type;
  selectTargetEntity.push(...createTryTarget(type1));
  if (group.length === 2) {
    const type2 = group[1][0].type;
    selectTargetEntity.push(...createTryTarget(type2));
  }
  // 得到所有可以胡的牌的结果
  return selectTargetEntity.filter((v) => {
    return calcHand(list, v);
  });
}

function clone(obj: unknown) {
  return JSON.parse(JSON.stringify(obj));
}

/**
 * 计算胡牌
 * @param group 当前手牌
 * @param target 是否可以胡这个牌
 */
function calcHand(list: Entity[], target: Entity) {
  // 将手牌进行克隆,因为要计算很多次,不能直接操作玩家的手牌
  const myList = clone(list) as Entity[];
  // 将候选听牌加入手牌
  myList.push(target);
  myList.sort((a, b) => {
    if (a.type != b.type) {
      return String(a.type).charCodeAt(0) - String(b.type).charCodeAt(0);
    } else {
      return a.size - b.size;
    }
  });
  // 从玩家的手牌里面选出一对,四川麻将称之为 将,然后剩余的牌,只要每3个能够组成AAA 或者ABC,ABC为等差数列,公差为1的组合,
  // 这个牌就是玩家可听的候选项。
  const map: Map<String, Entity[]> = new Map();
  myList.forEach((entity) => {
    // 以类型和内容作为Map的Key
    const key = entity.size + "" + entity.type;
    if (map.has(key)) {
      map.get(key)!.push(entity);
    } else {
      map.set(key, [entity]);
    }
  });
  // 过滤出大于2的牌,这些牌才有资格作为将对
  const pairOptions = [...map.values()].filter((v) => {
    return v.length >= 2;
  });
  // 计算出,去除将对之后的剩余的所有手牌的可能性
  const filterPairResultGroupList = pairOptions.map((v) => {
    // 仍然需要深克隆,因为需要多次处理
    const onceList = clone(myList) as Entity[];
    let counter = 0;
    while (counter < 2) {
      const idx = onceList.findIndex(
        (t) => t.size === v[0].size && t.type === v[0].type
      );
      if (idx >= 0) {
        onceList.splice(idx, 1);
        counter++;
      }
    }
    // 得到去除将对以后剩余的手牌
    return onceList;
  });
  // 只要有一个能满足条件,则认为可以胡
  return filterPairResultGroupList.some((list) => judge(list));
}

/**
 * 对手牌进行分组,然后再进行判断
 * @param list
 */
function judge(list: Entity[]) {
  const map: Map<String, Entity[]> = new Map();
  list.forEach((v) => {
    if (map.has(v.type)) {
      map.get(v.type)!.push(v);
    } else {
      map.set(v.type, [v]);
    }
  });
  const group = [...map.values()];
  // 分别对两组牌进行组合,如果都可以满足,则认为是可以胡牌的,玩家有可能只有一门花色的牌
  return calcOnce(group[0] || []) && calcOnce(group[1] || []);
}

/**
 * 计算出除了将对以外的牌,是否可以组成每3个能够组成AAA 或者 ABC,ABC为等差数列,公差为1的结果,只处理一种类型的牌型
 * @param list
 */
function calcOnce(list: Entity[]) {
  if (list.length === 0) {
    return true;
  }
  while (list.length >= 3) {
    let flag = true;
    // 如果当前牌能够组成AAA的组合
    if (
      list.length >= 3 &&
      list[0].size === list[1].size &&
      list[1].size === list[2].size
    ) {
      // 丢弃AAA,继续进行下一轮计算
      let counter = 3;
      while (counter > 0) {
        list.shift();
        counter--;
      }
      flag = false;
    } else {
      // 不能直接取前3进行比较,因为有可能出现122334这样的case,实际上可以组成123,234这样的组合
      let a = list[0].size;
      let b: number | null;
      let offsetB = 1;
      // 向后找到第一个比A大的数,B一定是可以找的到的
      while (offsetB < list.length && a === list[offsetB].size) {
        offsetB++;
      }
      if (offsetB >= list.length) {
        return false;
      }
      b = list[offsetB].size;
      let offsetC = offsetB + 1;
      // 向后找到第一个比C大的数,但是C不一定能够找到,比如113这样的场景,就只能找到AB,无法确定C
      while (offsetC < list.length && b === list[offsetC].size) {
        offsetC++;
      }
      let c: number | null = offsetC < list.length ? list[offsetC].size : null;
      // 如果ABC满足公差为1的等差数列,说明这三个牌也可以丢弃。
      if (typeof c === "number" && b - a === 1 && c - b === 1) {
        // 丢弃第一个牌
        list.shift();
        // 丢弃第二个牌,因为原来的牌少了一个,所以offset要减去1
        list.splice(offsetB - 1, 1);
        // 丢弃第三个牌,因为原来的牌少了两个个,所以offset要减去2
        list.splice(offsetC - 2, 1);
      } else {
        return false;
      }
      flag = false;
    }
    if (!flag) {
      list.sort((a, b) => {
        return a.size - b.size;
      });
    } else {
      // 说明无法组成AAA或者,ABC,公差为1的等差数列
      return false;
    }
  }
  // 如果不能消完,说明不能完全组成AAA,或者ABC这样的排列
  return list.length === 0;
}

/**
 * 根据牌型,生成可能听牌的候选项
 * @param type
 * @returns
 */
function createTryTarget(type: EntityCategory) {
  const selectTargetEntity: Entity[] = [];
  for (let i = 1; i <= 9; i++) {
    selectTargetEntity.push(
      new Entity({
        type,
        size: i,
      })
    );
  }
  return selectTargetEntity;
}

计算胡牌的番数算法

本来文章是没有打算编写番数的计算规则的,但是评论区有小伙伴提到了,我觉得是一个Good idea,所以也尝试着实现这个番数的计算规则。

计算胡牌的番数不算特别难写,但是也不会特别简单。

在编写这个算法之前,我们需要考虑一些问题,什么加番策略是互斥的,什么加番策略是可以叠加的,在明白了这个问题之后,这个算法的判断就非常简单了。

胡牌类型的番数奖励主要有几类,它们是互斥的:

  • 七对,加两番
  • 大单吊(假设规则允许),加两番
  • 带幺(即手牌只能是111,123,789,999,将对也能只能是11或者99,可以有2个花色),加2番
  • 普通对子胡,加一番。

清一色,加两番,是可以叠加的。

手牌里面,每拥有4个一样花色一样点数的牌,加一番,成都麻将称之为,是可以叠加的。

偶然的加番因素也是可以叠加的,比如杠上花,抢杠,被别人杠上炮。

牌序因素,比如最后一张牌胡了(自己摸到了最后一张牌,或者别人摸了最后一张牌,打出来给我们胡了,俗称海底捞月),加一番。

其它特色因素,也是可以叠加的,比如自摸加翻。

以下就是根据这个分析编写的番数计算方法:

type WinBonus = "杠上花" | "杠上炮" | "抢杠" | "NULL";

/**
 * 胡牌收益计算规则
 */
class ProfitCalculator {
  /**
   * 自摸,根据各地的规则决定是否加翻,可以自行配置
   */
  winBySelf() {
    return 1;
  }

  /**
   * 海底捞月胡,假设加一翻,可根据规则修改配置,可以自行配置
   * @returns
   */
  winByLastEntity() {
    return 1;
  }

  /**
   * 胡牌的时候的加倍
   * @param type 胡牌的加倍类型
   * @param isWinBySelf 是否是自摸
   * @returns
   */
  getWinBonus(type: WinBonus, isWinBySelf: boolean, isLastEntity: boolean) {
    const lastExp = isLastEntity ? this.winByLastEntity() : 0;
    switch (type) {
      case "杠上炮":
      case "抢杠":
        return 1 + lastExp;
      case "杠上花":
        // 杠上花本来是2翻,如果实行自摸加翻,则杠上花是3翻
        return 2 + this.winBySelf() + lastExp;
      default:
        return lastExp + (isWinBySelf ? this.winBySelf() : 0);
    }
  }

  /**
   * 胡牌的类型
   * @param handGroup 手牌
   * @param assetGroup 碰杠的牌
   */
  getWinGameTypeExp(handGroup: Entity[][], assetGroup: Entity[][]) {
    // 七对
    if (this.isSevenPair(handGroup)) {
      return 2;
    }
    // 大单调
    else if (this.isBigSingleFishing(handGroup)) {
      return 2;
    }
    // 带幺
    else if (this.isDaiYao(handGroup, assetGroup)) {
      return 2;
    }
    // 普通对子胡
    else if (this.isStrictPair(handGroup)) {
      return 1;
    }
    // 平胡
    else {
      return 0;
    }
  }

  /**
   * 计算出玩家最终牌型的 勾 数
   * @param handGroup
   * @param assetGroup
   */
  getHasWholeEntityCounter(handGroup: Entity[][], assetGroup: Entity[][]) {
    // 手牌和碰杠的所有牌均参与计算
    const list = [...handGroup, ...assetGroup].reduce((total, arr) => {
      return total.concat(arr);
    });
    const map: Map<String, Entity[]> = new Map();
    list.forEach((entity) => {
      const key = entity.size + entity.type;
      if (map.has(key)) {
        map.get(key)!.push(entity);
      } else {
        map.set(key, [entity]);
      }
    });
    // 找出所有的拥有完全4张的牌
    return [...map.values()].filter((v) => v.length === 4).length;
  }

  /**
   * 计算胡牌的番数
   * @param handGroup 手牌
   * @param assetGroup 资产牌,即碰或者杠下去之后的结果
   */
  calcProfit(
    handGroup: Entity[][],
    assetGroup: Entity[][],
    type: WinBonus,
    isWinBySelf: boolean,
    isLastEntity: boolean
  ) {
    // 胡牌类型的番数,已处理互斥
    const normalExp = this.getWinGameTypeExp(handGroup, assetGroup);
    // 清一色的番数为2,可叠加
    const sameColorExp = this.isSameColor(handGroup, assetGroup) ? 2 : 0;
    // 拥有同4个一样的花色同样的点数的组数,每组算一勾,可叠加
    const wholeCounter = this.getHasWholeEntityCounter(handGroup, assetGroup);
    // 胡牌时候的奖励番数,可叠加
    const winBonus = this.getWinBonus(type, isWinBySelf, isLastEntity);
    // 总番数
    const totalExp = normalExp + sameColorExp + wholeCounter + winBonus;
    return 2 ** totalExp;
  }

  /**
   * 是否是大单调
   * @param handGroup 玩家手牌
   */
  isBigSingleFishing(handGroup: Entity[][]) {
    // 大单调只剩最后一张牌,算上胡了的牌,总共只能是2张
    return handGroup.length === 1 && handGroup[0].length === 2;
  }

  /**
   * 是否是带幺
   */
  isDaiYao(handGroup: Entity[][], assetGroup: Entity[][]) {
    // TODO: 未实现
    return false;
  }

  /**
   * 判断一类花色的牌是否符合对子胡的牌型
   */
  _judge(list: Entity[]): boolean {
    if (list.length === 0) {
      return true;
    }
    // 排序
    list.sort((a, b) => {
      return a.size - b.size;
    });
    // 拷贝一下源数据,避免对其它计算结果造成影响
    list = JSON.parse(JSON.stringify(list)) as Entity[];
    // 是否已经消费了对子
    let costPair = false;
    while (list.length >= 2) {
      // 如果还没有消费过对子,此时还剩下两张牌,说明最后的2张牌可以成为对子胡的 将
      if (!costPair && list.length === 2 && list[0].size === list[1].size) {
        return true;
      }
      // 正常case,直接消费掉3个牌
      if (list[0].size === list[1].size && list[1].size === list[2].size) {
        let counter = 3;
        // 扔掉3个牌,继续下一轮判断
        while (counter > 0) {
          list.shift();
          counter--;
        }
      } else if (
        list[0].size === list[1].size &&
        !costPair &&
        list[1].size !== list[2].size
      ) {
        // 此时恰好出现AA BBB这样的场景
        costPair = true;
        // 扔掉前面两个,继续进行下一轮判断,但是需要消费掉costPair
        let counter = 2;
        while (counter > 0) {
          list.shift();
          counter--;
        }
      } else {
        // 其它情况均不是对子胡
        return false;
      }
    }
    return true;
  }

  /**
   * 是否是对子胡
   * @param handGroup
   */
  isStrictPair(handGroup: Entity[][]) {
    return this._judge(handGroup[0]) || this._judge(handGroup[1] || []);
  }

  /**
   * 是否是清一色
   */
  isSameColor(handGroup: Entity[][], assetGroup: Entity[][]) {
    // 只有一组牌,并且碰或者杠下去的牌必须也是同样的
    return (
      handGroup.length === 1 &&
      assetGroup.length === 1 &&
      handGroup[0][0].type === assetGroup[0][0].type
    );
  }

  /**
   * 判断玩家的牌是否是七对
   * @param handGroup 玩家手牌
   * @returns
   */
  isSevenPair(handGroup: Entity[][]) {
    const list = handGroup.reduce((total, arr) => {
      return total.concat(arr);
    });
    if (list.length !== 14) {
      return false;
    }
    let val = 1;
    // 两个数字成对出现,做两次异或的效果等于没有做
    for (let i = 0; i < list.length; i++) {
      const temp =
        list[i].type === "万" ? 10000 : list[i].type === "筒" ? 1000 : 0;
      const num = list[i].size + temp;
      val ^= num;
    }
    // 如果最终的结果是1,说明玩家的手牌全部是成对出现的
    return val === 1;
  }
}

关于七对的判断依据,如果有同学不太明白的,可以参考这道题: 136. 只出现一次的数字 - 力扣(LeetCode)

关于“带幺”的计算逻辑,我没有实现,有兴趣的读者可以自行实现,如果实现有问题的同学可以在评论区或私信我。

测试用例

把七对的胡牌类型和一般的胡牌类型的结果求并集再去重之后,就是玩家最终可以胡牌的结果。 以下是我编写的测试用例,感兴趣的同学可以贡献测试用例来校验我的算法是否正确,哈哈哈。

import { normalHandCalc, sevenPairHandCalc } from "./Calculator";
import { Entity } from "./Entity";
import { groupBy } from "lodash";

function createHand(code: string) {
  const list = code.split("").map((v) => {
    const en = new Entity();
    en.size = +v;
    if (/\d/.test(v)) {
      en.size = +v;
      en.type = "万";
    } else if (/[a-z]/.test(v)) {
      en.size = v.charCodeAt(0) - 96;
      en.type = "条";
    } else {
      en.size = v.charCodeAt(0) - 64;
      en.type = "筒";
    }
    return en;
  });
  const group = Object.values(groupBy(list, (v) => v.type));
  return group;
}

describe("calc seven pairs", () => {
  it("1123344556677", () => {
    const hand = createHand("1123344556677");
    const target = sevenPairHandCalc(hand);
    expect(target).toBe(2);
  });

  it("1223344556677", () => {
    const hand = createHand("1223344556677");
    const idx = sevenPairHandCalc(hand);
    expect(idx).toBe(1);
  });

  it("1122334455667", () => {
    const hand = createHand("1122334455667");
    const idx = sevenPairHandCalc(hand);
    expect(idx).toBe(7);
  });

  it("1111223344557", () => {
    const hand = createHand("1111223344557");
    const idx = sevenPairHandCalc(hand);
    expect(idx).toBe(7);
  });

  it("1111222244557", () => {
    const hand = createHand("1111222244557");
    const idx = sevenPairHandCalc(hand);
    expect(idx).toBe(7);
  });

  it("1233445567788", () => {
    const list = "1233445567788".split("").map((v) => {
      const en = new Entity();
      en.size = +v;
      return en;
    });
    const idx = sevenPairHandCalc([list]);
    expect(idx).toBe(-1);
  });

  it("1A23344556677", () => {
    const list = "1A23344556677".split("").map((v) => {
      const en = new Entity();
      if (/\d/.test(v)) {
        en.size = +v;
        en.type = "万";
      } else {
        en.size = v.charCodeAt(0) - 64;
        en.type = "筒";
      }
      return en;
    });
    const idx = sevenPairHandCalc([list]);
    expect(idx).toBe(-1);
  });
});

describe("calc normal list", () => {
  it("case where is 122334BBCDEFG", () => {
    const hand = createHand("122334BBCDEFG");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(3);
    expect(results[0].type).toBe("筒");
    expect(results[1].type).toBe("筒");
    expect(results[2].type).toBe("筒");
    expect(results[0].size).toBe(2);
    expect(results[1].size).toBe(5);
    expect(results[2].size).toBe(8);
  });

  it("case only 1", () => {
    const hand = createHand("1");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(1);
    expect(results[0].type).toBe("万");
  });

  it("case normal case", () => {
    const hand = createHand("1114567");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(2);
    expect(results[0].type).toBe("万");
    expect(results[1].type).toBe("万");
    expect(results[0].size).toBe(4);
    expect(results[1].size).toBe(7);
  });

  it("case two pair", () => {
    const hand = createHand("11AA");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(2);
    expect(results[0].type).toBe("万");
    expect(results[1].type).toBe("筒");
    expect(results[0].size).toBe(1);
    expect(results[1].size).toBe(1);
  });

  it("case six target mahjong", () => {
    const hand = createHand("1112345678");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(6);
    expect(results[0].type).toBe("万");
    expect(results[1].type).toBe("万");
    expect(results[2].type).toBe("万");
    expect(results[3].type).toBe("万");
    expect(results[4].type).toBe("万");
    expect(results[5].type).toBe("万");
    expect(results[0].size).toBe(2);
    expect(results[1].size).toBe(3);
    expect(results[2].size).toBe(5);
    expect(results[3].size).toBe(6);
    expect(results[4].size).toBe(8);
    expect(results[5].size).toBe(9);
  });

  it("case test clear three pattern is correct", () => {
    const hand = createHand("11123456AA");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(4);
    expect(results[0].type).toBe("万");
    expect(results[1].type).toBe("万");
    expect(results[2].type).toBe("万");
    expect(results[3].type).toBe("筒");
    expect(results[0].size).toBe(1);
    expect(results[1].size).toBe(4);
    expect(results[2].size).toBe(7);
    expect(results[3].size).toBe(1);
  });

  it("case every target in some type", () => {
    const hand = createHand("1112345678999");
    const results = normalHandCalc(hand);
    expect(results.length).toBe(9);
    expect(results[0].type).toBe("万");
    expect(results[1].type).toBe("万");
    expect(results[2].type).toBe("万");
    expect(results[3].type).toBe("万");
    expect(results[4].type).toBe("万");
    expect(results[5].type).toBe("万");
    expect(results[6].type).toBe("万");
    expect(results[7].type).toBe("万");
    expect(results[8].type).toBe("万");
    expect(results[0].size).toBe(1);
    expect(results[1].size).toBe(2);
    expect(results[2].size).toBe(3);
    expect(results[3].size).toBe(4);
    expect(results[4].size).toBe(5);
    expect(results[5].size).toBe(6);
    expect(results[6].size).toBe(7);
    expect(results[7].size).toBe(8);
    expect(results[8].size).toBe(9);
  });
});

结语

实际上这个算法还有很多改进的空间,这个就交给有兴趣的同学去自行实现吧,哈哈哈。

其实在实际生活中有很多例子可以用计算机帮助我们解决问题,在遇到的时候大家可以充分的发挥自己的头脑。

最后说一些警惕的话,可以看出,计算机处理麻将的规则是很简单的,并且生成的牌序可以把它记录下来,分成4份,然后可以准确的知道每个玩家面前的牌堆的排列,在发牌之后进而就能根据顺序和初始化的拿牌的位置进而分析出玩家的起始手牌,然后就可以通过分析牌序得到之后可能出现的所有牌,如果其中有玩家碰或者杠影响了牌序,能够立刻进行重新计算。如果参与网络对战,一旦这些信息被第三方外挂抓取到那你的信息将会是被别人看的一览无余了,所以珍爱生命,远离网络赌博

最后,祝大家新年假期里都能玩的开心,过的快乐!Happy New Year!

对于本文阐述的内容有任何疑问的同学可以在评论区留言或私信我。

如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。