欢迎光临散文网 会员登陆 & 注册

日麻折腾笔记Java篇(2)-向听数和牌效率

2020-11-01 21:58 作者:天羽ちよこ  | 我要投稿

思考

向听数比较通俗易懂的解释是还差几张牌听牌,用程序能理解的话就是:

1. 将手牌的n张牌替换成任意一张牌,能够满足听牌的n的最小值
2. 此时,手牌就是n向听

向听数可以由下面的公式快速算出(暂时不考虑七对和国士):

向听数=8-2x(顺子数量+刻子数量)-对子数量-搭子数量

所以只需要算出顺子、刻子、对子以及搭子的数量即可

我在这里将对子单独从搭子中列出来,本文以及本系列文章所说的“搭子”均不包含“对子”

但是要注意的是,很多牌形是有多种拆分方式的,诸如下面的示例

1234s

可以认为是123的顺子加上孤张4,也可以认为是1的孤张加上234的顺子,还可以认为是1234两个搭子……


我完善了上一章中的代码,将对子、刻子、顺子等概念抽象成类,每种拆分情况都由这四种组成,部分示例代码如下

package io.****.mj.util.analyze;


import io.****.mj.util.HandPai;

import java.util.ArrayList;

import java.util.List;


/**

 * 手牌分解的每种组合

 * 每种组合都由a个顺子、b个刻子、c个对子、d个搭子、e个孤张组合而成

 */

public class SplitCondition {

    private List<Duizi> duiziList = new ArrayList<>();

    private List<Shunzi> shunziList = new ArrayList<>();

    private List<Dazi> daziList = new ArrayList<>();

    private List<Kezi> keziList = new ArrayList<>();

    private List<HandPai> guzhangList = new ArrayList<>();


    public int xiangTing() {

        return 8 - 2 * (keziList.size() + shunziList.size()) - daziList.size() - duiziList.size();

    }

    public void addDuizi(Duizi duizi) {

        duiziList.add(duizi);

    }

    public void addShunzi(Shunzi shunzi) {

        shunziList.add(shunzi);

    }

    public void addDazi(Dazi dazi) {

        daziList.add(dazi);

    }

    public void addKezi(Kezi kezi) {

        keziList.add(kezi);

    }

    public void addGuzhang(HandPai guzhang) {

        guzhangList.add(guzhang);

    }

    public List<Duizi> getDuiziList() {

        return duiziList;

    }

    public List<Shunzi> getShunziList() {

        return shunziList;

    }

    public List<Dazi> getDaziList() {

        return daziList;

    }

    public List<Kezi> getKeziList() {

        return keziList;

    }

    public List<HandPai> getGuzhangList() {

        return guzhangList;

    }


    public void setGuzhangList(List<HandPai> guzhangList) {

        this.guzhangList = guzhangList;

    }

    public SplitCondition copy() {

        SplitCondition condition = new SplitCondition();

        condition.duiziList = new ArrayList<>(getDuiziList());

        condition.daziList = new ArrayList<>(getDaziList());

        condition.keziList = new ArrayList<>(getKeziList());

        condition.shunziList = new ArrayList<>(getShunziList());

        return condition;

    }

    @Override

    public String toString() {

        return "[" +

                duiziList +

                shunziList +

                daziList +

                keziList +

                guzhangList + "]";

    }

}

package io.****.mj.util.analyze;


import io.****.mj.util.HandAnalyze;

import io.****.mj.util.HandPai;

import io.****.mj.util.Pai;


import java.util.ArrayList;

import java.util.Arrays;

import java.util.List;

import java.util.stream.Collectors;


public class PaiSplitter {

    public static List<SplitCondition> split(String str) {

        List<HandPai> handPais = HandAnalyze.strToHandPai(str);

        List<SplitCondition> conditions = new ArrayList<>();

        return split(handPais, conditions, new SplitCondition(), 0);

    }


    private static List<SplitCondition> split(List<HandPai> handPais, List<SplitCondition> conditions, SplitCondition condition, int depth) {

        if (handPais.size() <= 1) {

            conditions.add(condition);

            return conditions;

        }

        HandPai current = null;

        HandPai next = null;

        HandPai nextNext = null;

        current = handPais.get(0);

        next = handPais.get(1);

        if (0 != handPais.size() - 2) {

            nextNext = handPais.get(2);

        }

        // 处理对子

        if (current.getPai() == next.getPai()) {

            handleDuizi(handPais, conditions, condition.copy(), current, next, depth + 1);

        }

        // 处理刻子

        if (nextNext != null) {

            if (current.getPai() == next.getPai() && next.getPai() == nextNext.getPai()) {

                handleKezi(handPais, conditions, condition.copy(), current, next, nextNext, depth + 1);

            }

        }

        Pai currentPlusOne = current.getPai().next(false);

        if (currentPlusOne != null) {

            List<HandPai> filtered = handPais.stream().filter(p -> p.getPai() == currentPlusOne).collect(Collectors.toList());

            if (filtered.size() > 0) {

                HandPai second = filtered.get(0);

                handleDazi(handPais, conditions, condition.copy(), current, second, depth + 1);

                Pai currentPlusTwo = currentPlusOne.next(false);

                filtered = handPais.stream().filter(p -> p.getPai() == currentPlusTwo).collect(Collectors.toList());

                if (filtered.size() > 0) {

                    HandPai third = filtered.get(0);

                    handleShunzi(handPais, conditions, condition.copy(), current, second, third, depth + 1);

                }

            }

        }

        handleGuzhang(handPais, conditions, condition.copy(), current, depth + 1);

        return conditions;

    }



    private static void handleGuzhang(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, int depth) {

        currentHandPai = new ArrayList<>(currentHandPai);

        condition.addGuzhang(first);

        currentHandPai.remove(first);

        split(currentHandPai, conditions, condition, depth);

    }


    private static void handleDazi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second, int depth) {

        currentHandPai = new ArrayList<>(currentHandPai);

        Dazi dazi = new Dazi(Arrays.asList(first, second));

        condition.addDazi(dazi);

        currentHandPai.remove(first);

        currentHandPai.remove(second);

        split(currentHandPai, conditions, condition, depth);

    }


    private static void handleDuizi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second, int depth) {

        currentHandPai = new ArrayList<>(currentHandPai);

        Duizi duizi = new Duizi(Arrays.asList(first, second));

        condition.addDuizi(duizi);

        currentHandPai.remove(first);

        currentHandPai.remove(second);

        split(currentHandPai, conditions, condition, depth);

    }


    private static void handleShunzi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second,

                                     HandPai third, int depth) {

        currentHandPai = new ArrayList<>(currentHandPai);

        Shunzi shunzi = new Shunzi(Arrays.asList(first, second, third));

        condition.addShunzi(shunzi);

        currentHandPai.remove(first);

        currentHandPai.remove(second);

        currentHandPai.remove(third);

        split(currentHandPai, conditions, condition, depth);

    }


    private static void handleKezi(List<HandPai> currentHandPai, List<SplitCondition> conditions, SplitCondition condition, HandPai first, HandPai second,

                                   HandPai third, int depth) {

        currentHandPai = new ArrayList<>(currentHandPai);

        Kezi kezi = new Kezi(Arrays.asList(first, second, third));

        condition.addKezi(kezi);

        currentHandPai.remove(first);

        currentHandPai.remove(second);

        currentHandPai.remove(third);

        split(currentHandPai, conditions, condition, depth);

    }

}


相关测试代码

@Test
public void splitDuizi() {
   List<SplitCondition> split = PaiSplitter.split("11223344556677s");
   Assert.assertTrue(split.stream().anyMatch(c -> c.getDuiziList().size() == 7));
}

@Test
public void testHe() {
   List<SplitCondition> split = PaiSplitter.split("11223344556677s");
   Assert.assertTrue(split.stream().mapToInt(SplitCondition::xiangTing)
           .min().getAsInt() < 1);
}

@Test
public void testKeziAndShunzi() {
   List<SplitCondition> split = PaiSplitter.split("11155s134m579p122z");
   Assert.assertEquals(3, split.stream().mapToInt(SplitCondition::xiangTing)
           .min().getAsInt());
}


牌效率是什么

经营自己的手牌,以达成和牌为目标
我们的任何操作(切牌、碰、吃等)都是为了最终能够和牌服务的,也就是提高和了率。

在只考虑自摸的情况下,我们可以快速的计算出打哪张牌能够使向听数前进、有效牌和改良牌变多。

有效牌

能够减少向听数的牌

通过向听数的计算公式8-2*(面子数)-对子数-搭子数,我们发现,能够减少向听数的牌有以下几种:

  1. 能够与搭子组成一个顺子,此时面子数量+1,搭子数量-1,向听数+1

  2. 能够与对子组成一个刻子,此时面子数量+1,对子数量-1,向听数+1

  3. 能够与孤张组成一个对子,此时对子数量+1,向听数+1

  4. 能够与孤张组成一个搭子,此时搭子数量+1,向听数+1

这样一来就很清晰了,我们来编码,计算以下两种情况

  1. 在向听数减少的情况下,打出哪张牌后的有效牌最多

  2. 在打出任何牌都不能导致向听数减少的情况下,打出哪张牌后的有效牌最多(也就是指改良)

编码

我们需要有个工具来记录当前玩家视角内牌的数量情况:

/**

 * 对局中牌出现数量的计数器

 */

public class PaiCounter {

    private Map<Pai, AtomicInteger> paiCount = new HashMap<>();


    public PaiCounter() {

        Pai.ALL.forEach(p -> {

            paiCount.put(p, new AtomicInteger(0));

        });

    }


    public PaiCounter(Map<Pai, AtomicInteger> paiCount) {

        this.paiCount = paiCount;

    }

    /**

     * 查询当前玩家视角内某张牌已经出现的数量

     * @param pai 需要查询的牌

     * @return 数量

     */

    public int getCount(Pai pai) {

        return paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).get();

    }

    /**

     * 玩家新摸到、对手打出、或者吃碰杠等操作,导致当前玩家视角内出现新的牌,请调用此方法计数

     *

     * @param pai 新出现的牌

     */

    public void addCount(Pai pai) {

        paiCount.computeIfAbsent(pai, p -> new AtomicInteger(0)).incrementAndGet();

    }

}


计算当前手牌打出哪张后的有效进张

/**
* 计算有效牌的工具类
*/
public class EffectSelector {
   public static List<EffectCondition> calculate(String pais, PaiCounter counter) {
       List<HandPai> handPais = HandAnalyze.strToHandPai(pais);
       handPais.forEach(p -> {
           counter.addCount(p.getPai());
       });
       return handPais.stream()
               .map(p -> {
                   EffectCondition condition = new EffectCondition();
                   condition.setThro(p);
                   List<HandPai> tmp = new ArrayList<>(handPais);
                   tmp.remove(p);
                   List<SplitCondition> split = PaiSplitter.split(tmp, 0);
                   // 过滤出向听数最少的
                   int min = split.stream().mapToInt(SplitCondition::xiangTing).min().getAsInt();
                   split = split.stream()
                           .filter(d -> d.xiangTing() == min)
                           .collect(Collectors.toList());
                   Map<Pai, Integer> collect = calculate(split)
                           .stream().map(a -> new AbstractMap.SimpleEntry<>(a, 4 - counter.getCount(a)))
                           .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
                   condition.setEffectivePais(new TreeMap<>(collect));
                   return condition;
               }).sorted((o1, o2) -> o2.getEffectivePais().values().stream().mapToInt(d -> d).sum()
                       - o1.getEffectivePais().values().stream().mapToInt(d -> d).sum()).collect(Collectors.toList());
   }

   public static List<Pai> calculate(List<SplitCondition> conditions) {
       return conditions.stream().map(d -> {
           List<Pai> daziData = d.getDaziList().stream()
                   .flatMap(m -> {
                       List<HandPai> tmp = m.getData();
                       Collections.sort(tmp);
                       HandPai first = m.getData().get(0);
                       HandPai second = m.getData().get(1);
                       List<Pai> res = new ArrayList<>();
                       if (first.getPai().getNumber() == second.getPai().getNumber() - 1) {
                           res.add(first.getPai().prev(false));
                           res.add(second.getPai().next(false));
                       } else {
                           res.add(first.getPai().next(false));
                       }
                       return res.stream().filter(Objects::nonNull);
                   }).collect(Collectors.toList());
           List<Pai> duiziData = d.getDuiziList()
                   .stream()
                   .map(a -> a.getData().get(0).getPai()).collect(Collectors.toList());
           List<Pai> guzhangData = d.getGuzhangList()
                   .stream()
                   .flatMap(a -> {
                       if (a.getPai().getType() == Type.z)
                           return Stream.of(a.getPai());
                       else
                           return Stream.of(a.getPai(), a.getPai().next(false), a.getPai().prev(false));
                   })
                   .filter(Objects::nonNull).collect(Collectors.toList());
           return Stream.of(daziData, duiziData)
                   .flatMap(List::stream);
       }).flatMap(d -> d).distinct().collect(Collectors.toList());
   }
}


测试代码

EffectSelector
       .calculate("34677m67p22577s27z", new PaiCounter())
       .forEach(System.out::println);

输出如下

打=2z 向听数=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

打=7z 向听数=3 有效牌=30 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

打=7m 向听数=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

打=7m 向听数=3 有效牌=28 {2m=4, 5m=4, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

打=7s 向听数=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}

打=7s 向听数=3 有效牌=28 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 6s=4}

打=6m 向听数=3 有效牌=26 {2m=4, 5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

打=5s 向听数=3 有效牌=26 {2m=4, 5m=4, 7m=2, 8m=4, 5p=4, 8p=4, 2s=2, 7s=2}

打=3m 向听数=3 有效牌=22 {5m=4, 7m=2, 5p=4, 8p=4, 2s=2, 6s=4, 7s=2}

天凤牌理

我们用天凤牌理来验证一下我们的程序是否正确:


可以看到是一致的


日麻折腾笔记Java篇(2)-向听数和牌效率的评论 (共 条)

分享到微博请遵守国家法律