【算法】麻将听牌算法,借助StreamAPI,使用线程池完成寻找听牌,判断胡牌 - Go语言中文社区

【算法】麻将听牌算法,借助StreamAPI,使用线程池完成寻找听牌,判断胡牌


效果

判断胡牌
Java听牌算法判断胡牌
Java听牌算法判断胡牌

寻找听牌
Java听牌算法寻找听牌

多牌型的寻找胡牌
Java听牌算法寻找听牌

这次的听牌算法采用了Java8的StreamAPI新特性进行处理牌组集合,比传统的集合遍历性能上提高了不少,而且还支持并行,这为多线程提供了很好的支持,而且牌组的编码采用了集合工厂,底层省去了修改的功能,创建后不能修改只能遍历,而且赋值时还很方便

项目源码,jar文件,请前往github获取,https://github.com/ZDG-Kinlon/MahjongCheckHuUtil

牌组类

Deck.java
只是为了对牌组进行转换,方便计算和输出的切换

package cn.mahjong;

import java.util.ArrayList;
import java.util.List;

public class Deck {
    //使用集合工厂创建牌组的编号集合
    public static final List<Integer> DECK_NO = List.of(
            11, 12, 13, 14, 15, 16, 17, 18, 19,
            21, 22, 23, 24, 25, 26, 27, 28, 29,
            31, 32, 33, 34, 35, 36, 37, 38, 39,
            41, 42, 43, 44, 45, 46, 47
    );

    //使用集合工厂创建牌组的名称集合
    private static final List<String> DECK_STR = List.of(
            "一万", "二万", "三万", "四万", "五万", "六万", "七万", "八万", "九万",
            "一筒", "二筒", "三筒", "四筒", "五筒", "六筒", "七筒", "八筒", "九筒",
            "一条", "二条", "三条", "四条", "五条", "六条", "七条", "八条", "九条",
            "东风", "西风", "南风", "北风", "红中", "白板", "发财"
    );

    /**
     * 将牌编号转换为对应的牌名称
     * @param num
     * @return
     * @throws IndexOutOfBoundsException
     */
    public static String getDeckByStr(int num)
      throws IndexOutOfBoundsException {
        return DECK_STR.get(DECK_NO.indexOf(num));
    }

    /**
     * 将牌名称转换为对应的牌编号
     * @param str
     * @return
     * @throws IndexOutOfBoundsException
     */
    public synchronized static Integer getDeckByNO(String str)
      throws IndexOutOfBoundsException {
        return DECK_NO.get(DECK_STR.indexOf(str));
    }

    /**
     * 将牌集合名称集合转换为牌编号集合
     * @param stringList
     * @return
     * @throws IndexOutOfBoundsException
     */
    public static List<Integer> toDeckNO(List<String> stringList)
      throws IndexOutOfBoundsException {
        List<Integer> out = new ArrayList<>();
        stringList.forEach(s -> out.add(getDeckByNO(s)));
        return out;
    }

    /**
     * 将牌集合编号转换为牌名称集合
     * @param intList
     * @return
     * @throws IndexOutOfBoundsException
     */
    public static List<String> toDeckStr(List<Integer> intList)
      throws IndexOutOfBoundsException {
        List<String> out = new ArrayList<>();
        intList.forEach(s -> out.add(getDeckByStr(s)));
        return out;
    }
}

判断胡牌的类

CheckHu.java
借助StreamAPI对待测牌进行处理,先判断七对子,国士无双这种特殊牌型,如果不成立只能检测是否为3n*2的牌型

3n的牌型有刻牌和链牌两种,刻牌是三张相同的牌,链牌是连续的同色牌

因为n最大只能为4,所以在拆牌的时候最多使用4层嵌套,总共16种组合,如果牌型为胡牌,绝对有一种能拆解到最后一张不剩

拆牌时如果测试牌拆除失败,将会回滚还原,不影响下一次拆除,因为编号没有10,20,30,40号牌,间接做到了区别花色的作用,避免出现拆除链牌时的跨色问题

Java8引入的StreamAPI在对集合的处理上得到了很大的增强,可以把集合中的元素抽象为Excel表格中每一列的单元格,后面一系列的点方法就是位于表格第一行的筛选和统计功能,很方便的完成对集合的处理

Java9引入的集合工厂of()方法可以很快的生成不可改变只能查询的集合,效率提升了很多

package cn.mahjong;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;

public class CheckHu implements Callable<Map<Integer, Boolean>> {
    private Integer newDeck;//待测牌
    private List<Integer> checkDeckList;//待测牌集合
    private Map<Integer, Long> map;//每张牌的数量

    public CheckHu(List<Integer> checkDeckList) {
        this.checkDeckList = checkDeckList;
    }

    public CheckHu(List<Integer> checkDeckList, Integer newDeck) {
        this.newDeck = newDeck;
        this.checkDeckList = checkDeckList;
    }

    /**
     * 多线程返回值方法
     *
     * @return
     * @throws Exception
     */
    @Override
    public Map<Integer, Boolean> call() throws Exception {
        return Map.of(newDeck, isHu());
    }

    /**
     * 根据待测牌集合判断是否为胡牌牌型
     *
     * @return
     */
    public boolean isHu() {
        return init.getAsBoolean() && (isQiDuiZi.getAsBoolean() ||
           isGuoShiWuShuang.getAsBoolean() || is3n2.getAsBoolean());
    }

    //初始化方法
    private BooleanSupplier init = () -> {
        List<Integer> list = new ArrayList<>(this.checkDeckList);
        if (newDeck != null) list.add(newDeck);//如果是另一个待测牌存在,添加进待测集合中
        Collections.sort(list);//必须排序,链牌拆解的时候需要从小到大遍历
        this.checkDeckList = list;
        //使用流统计每张牌的数量
        this.map = list.stream()
          .collect(Collectors.groupingBy(integer -> integer, Collectors.counting()));          
        return !check4count();//判断单张牌是否超过4张
    };

    /**
     * 判断单张牌是否超过4张
     *
     * @return
     */
    private boolean check4count() {
    //使用流遍历出单张牌最多的张数
        return this.map.values().stream()
          .max(Comparator.comparing(count -> count)).get().intValue() > 4;
    }

    //使用流判断是否为七对子的Lambda表达式,先判断去重复后的牌的数量是否为7张,然后过滤每张牌的个数是为2张的结果的个数是否为7张,
    private BooleanSupplier isQiDuiZi = () -> this.map.keySet().size() == 7 && 
        this.map.values().stream().filter(count -> count == 2).count() == 7;

    //使用流判断是否为国士无双的Lambda表达式,先判断去重复后的牌的数量是否为13张,然后过滤每张牌必须为40以上的杂牌,或者40以下的边牌的结果的个数是否为13张
    private BooleanSupplier isGuoShiWuShuang = () -> this.map.keySet().size() == 13 && 
        this.map.keySet().stream()
            .filter(a -> a / 10 == 4 || a % 10 == 9 || a % 10 == 1).count() == 13;

    //使用流判断是否为3n+2牌型的Lambda表达式
    private BooleanSupplier is3n2 = () -> {
        int count = checkDeckList.size();//判断待测牌的张数是否满足3n+2,且必须在2至14张之内
        if (count >= 2 && count <= 14 && (count - 2) % 3 == 0) {
            List<Integer> duiDeckList = new ArrayList<>();//使用流遍历找出可能为将牌(2)的所有可能,不是单张牌就添加
            this.map.forEach((a, b) -> {
                if (b > 1) duiDeckList.add(a);
            });
            for (Integer a : duiDeckList) {//遍历将牌
                List<Integer> testList = new LinkedList<>(this.checkDeckList);
                testList.remove(a);//去除两张将牌,然后根据刻牌和链牌进行3n拆解
                testList.remove(a);
                if (_A(_A(_A(_A(testList)))).size() == 0) return true;//刻 刻 刻 刻
                if (_A(_A(_A(_B(testList)))).size() == 0) return true;//链 刻 刻 刻
                if (_A(_A(_B(_A(testList)))).size() == 0) return true;//刻 链 刻 刻
                if (_A(_A(_B(_B(testList)))).size() == 0) return true;//链 链 刻 刻
                if (_A(_B(_A(_A(testList)))).size() == 0) return true;//刻 刻 链 刻
                if (_A(_B(_A(_B(testList)))).size() == 0) return true;//链 刻 链 刻
                if (_A(_B(_B(_A(testList)))).size() == 0) return true;//刻 链 链 刻
                if (_A(_B(_B(_B(testList)))).size() == 0) return true;//链 链 链 刻
                if (_B(_A(_A(_A(testList)))).size() == 0) return true;//刻 刻 刻 链
                if (_B(_A(_A(_B(testList)))).size() == 0) return true;//链 刻 刻 链
                if (_B(_A(_B(_A(testList)))).size() == 0) return true;//刻 链 刻 链
                if (_B(_A(_B(_B(testList)))).size() == 0) return true;//链 链 刻 链
                if (_B(_B(_A(_A(testList)))).size() == 0) return true;//刻 刻 链 链
                if (_B(_B(_A(_B(testList)))).size() == 0) return true;//链 刻 链 链
                if (_B(_B(_B(_A(testList)))).size() == 0) return true;//刻 链 链 链
                if (_B(_B(_B(_B(testList)))).size() == 0) return true;//链 链 链 链
            }
        }
        return false;//最多只有4组3n组合,如果都尝试过没有能拆解完毕,则不满足3n+2牌型
    };

    /**
     * 刻牌拆解方法
     *
     * @param list
     * @return
     */
    private List<Integer> _A(List<Integer> list) {
        for (Integer integer : list) {//遍历每张牌
            List<Integer> testList = new LinkedList<>(list);//创建测试牌集合
            //尝试拆解,如果拆解成功,返回拆解后的牌集合
            if (breakDeckList(testList, integer, integer, integer)) return testList;
        }
        return list;//拆解失败,返回原先传入的待测牌组
    }

    /**
     * 链牌拆解方法
     *
     * @param list
     * @return
     */
    private List<Integer> _B(List<Integer> list) {
        for (Integer integer : list) {//遍历每张牌
            if (integer / 10 == 4) continue;//杂牌无法构成链牌
            List<Integer> testList = new LinkedList<>(list);//创建测试牌集合
            //尝试拆解,如果拆解成功,返回拆解后的牌集合
            if (breakDeckList(testList, integer, integer + 1, integer + 2)) return testList;
        }
        return list;
    }

    /**
     * 拆解牌的方法
     *
     * @param list
     * @param i1
     * @param i2
     * @param i3
     * @return
     */
    private boolean breakDeckList(List<Integer> list, Integer i1, Integer i2, Integer i3) {
        //尝试删除传入的三张牌,有一张删除失败返回false
        return list.remove(i1) && list.remove(i2) && list.remove(i3);
    }
}

寻找听牌的类

FindHu.java
使用线程池,遍历所有牌的可能,与待测牌组合为完整的牌型,然后交给线程去执行,最后返回结果是true的的牌编号的集合

package cn.mahjong;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FindHu implements Callable<Map<List<Integer>, List<Integer>>> {
    private List<Integer> integerList;//待寻找的牌组集合

    public FindHu(List<Integer> integerList) {
        this.integerList = integerList;
    }

    /**
     * 获取听牌集合
     * @return
     */
    public List<Integer> findHu() {
        List<CheckHu> checkHuList = new ArrayList<>();//创建对象集合
        List<Integer> allDeck = Deck.DECK_NO;//获取所有的牌的编号
        //遍历所有牌的编号,传入对象中
        allDeck.forEach(integer -> checkHuList.add(new CheckHu(this.integerList, integer)));
        //创建线程池,每张牌为一个线程
        ExecutorService threadPool = Executors.newFixedThreadPool(allDeck.size());
        List<Future<Map<Integer, Boolean>>> futures = new ArrayList<>();//创建线程执行结果的集合
        try {
            //递交对象任务到线程池,自动调用call方法,返回结果到集合中
            checkHuList.forEach(checkHu -> futures.add(threadPool.submit(checkHu)));
        } finally {
            threadPool.shutdown();//关闭线程池
        }
        List<Integer> out = new ArrayList<>();//创建返回结果的集合
        futures.forEach(f -> {
            try {
                f.get().forEach((a, b) -> {
                    if (b) out.add(a);//遍历每一个线程的结果,将结果为true对应的牌的编号传入返回集合
                });
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        return out;//返回结果
    }

    /**
     * 多线程遍历多个待测牌的方法
     * @return
     * @throws Exception
     */
    @Override
    public Map<List<Integer>, List<Integer>> call() throws Exception {
        return Map.of(this.integerList, findHu());//key=待测牌集合,value=听牌集合
    }
}

寻找多个牌组的听牌的类

FindHus.java
同样使用了线程池,对传入的每一个牌组调用寻找听牌的类的方法,多任务同时执行,为了避免线程池泛滥,默认设置最多为4个,建议根据实际硬件情况进行调整

package cn.mahjong;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FindHus {
    private List<List<Integer>> lists;

    public FindHus(List<List<Integer>> lists) {
        this.lists = lists;
    }

    /**
     * 获取待测牌组集合的听牌集合
     * @return
     */
    public Map<List<Integer>, List<Integer>> findHu() {
        ExecutorService threadPool = Executors.newFixedThreadPool(4);//创建线程池
        List<FindHu> findHus = new ArrayList<>();//创建对象
        lists.forEach(list -> findHus.add(new FindHu(list)));//遍历每个传入的待测牌组集合,写入对应的对象中
        List<Future<Map<List<Integer>, List<Integer>>>> futures = new ArrayList<>();//创建每个线程返回结果的集合
        try {
            findHus.forEach(findHu -> futures.add(threadPool.submit(findHu)));//传入对象到线程池中,自动执行call方法,返回结果到集合中
        } finally {
            threadPool.shutdown();//关闭线程池
        }
        Map<List<Integer>, List<Integer>> out = new HashMap<>();//创建返回结果的集合
        futures.forEach(f -> {
            try {
                f.get().forEach(out::put);//遍历传入的待测牌集合,将听牌集合结果写入
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        });
        return out;
    }
}

测试示例

判断胡牌方法1

public void checkDemo1() {
    try {
        //1.创建待测牌组,可以不按顺序
        List<String> stringList = List.of("一万", "八万", "一万", "六万", "六万", "一万", "七万", "四万", "五万", "二万", "三万", "六万", "六万", "七万");
        //2.将牌组编号化,如果不存在会抛出异常IndexOutOfBoundsException
        List<Integer> integerList = Deck.toDeckNO(stringList);
        //3.初始化对象
        CheckHu checkHu = new CheckHu(integerList);
        //4.调用方法开始判断
        boolean result = checkHu.isHu();
        //5.输出结果,是胡牌=true
        System.out.println(result);
    } catch (IndexOutOfBoundsException e) {
        e.printStackTrace();
    }
}

判断胡牌方法2

public void checkDemo2() {
    try {
        //1.创建待测牌组,可以不按顺序
        List<String> stringList = List.of("一万", "八万", "一万", "六万", "六万", "一万", "七万", "五万", "二万", "三万", "六万", "六万", "七万");
        //2.创建起到的牌
        String checkDeck = "四万";
        //3.将牌组编号化,如果不存在会抛出异常IndexOutOfBoundsException
        List<Integer> integerList = Deck.toDeckNO(stringList);
        Integer checkDeckNO = Deck.getDeckByNO(checkDeck);
        //4.初始化对象,待测牌组,起到的牌
        CheckHu checkHu = new CheckHu(integerList, checkDeckNO);
        //5.调用方法开始判断
        boolean result = checkHu.isHu();
        //6.输出结果,是胡牌=true
        System.out.println(result);
    } catch (IndexOutOfBoundsException e) {
        e.printStackTrace();
    }
}

寻找听牌方法

public void findDemo1() {
    try {
        //1.创建待测牌组,可以不按顺序
        List<String> stringList = List.of("一万", "一万", "六万", "六万", "七万", "一万", "二万", "五万", "六万", "三万", "四万", "六万", "八万");
        //2.将牌组编号化,如果不存在会抛出异常IndexOutOfBoundsException
        List<Integer> integerList = Deck.toDeckNO(stringList);
        //3.创建对象
        FindHu findHu = new FindHu(integerList);
        //4.调用方法开始寻找
        List<Integer> result = findHu.findHu();
        //5.转换为字符串牌组,如果不存在会抛出异常IndexOutOfBoundsException
        List<String> out = Deck.toDeckStr(result);
        //6.输出结果
        out.forEach(str -> System.out.print(str + "t"));
    } catch (IndexOutOfBoundsException e) {
        e.printStackTrace();
    }
}

寻找多牌组的听牌

public void findDemo2() {
    try {
        //1.创建待测牌组,可以不按顺序,需要转换为编号牌集合
        List<List<Integer>> lists = List.of(
                //七对子,听1
                Deck.toDeckNO(List.of("一万", "一万", "三万", "三万", "六条", "六条", "五筒", "五筒", "东风", "东风", "南风", "南风", "七万")),
                //国士无双,听13
                Deck.toDeckNO(List.of("一万", "九万", "一筒", "九筒", "一条", "九条", "东风", "西风", "南风", "北风", "红中", "白板", "发财")),
                //13听9
                Deck.toDeckNO(List.of("一万", "一万", "一万", "二万", "三万", "四万", "五万", "六万", "七万", "八万", "九万", "九万", "九万")),
                //13听8
                Deck.toDeckNO(List.of("一万", "一万", "一万", "二万", "三万", "四万", "五万", "六万", "六万", "六万", "六万", "七万", "八万")),
                //10听8
                Deck.toDeckNO(List.of("三万", "三万", "三万", "四万", "五万", "六万", "七万", "八万", "八万", "八万")),
                //13听7
                Deck.toDeckNO(List.of("二万", "二万", "二万", "三万", "四万", "四万", "四万", "四万", "五万", "六万", "七万", "七万", "七万")),
                //10听7
                Deck.toDeckNO(List.of("一万", "一万", "一万", "二万", "三万", "四万", "五万", "六万", "六万", "六万")),
                //10听6
                Deck.toDeckNO(List.of("三万", "三万", "三万", "四万", "四万", "四万", "五万", "六万", "七万", "八万")),
                //7听5
                Deck.toDeckNO(List.of("三万", "三万", "三万", "四万", "五万", "五万", "五万")),
                //10听4
                Deck.toDeckNO(List.of("三万", "三万", "四万", "四万", "五万", "五万", "六万", "六万", "七万", "七万")),
                //7听4
                Deck.toDeckNO(List.of("三万", "三万", "三万", "四万", "四万", "四万", "五万")),
                //4听3
                Deck.toDeckNO(List.of("三万", "三万", "三万", "二万"))
        );
        //2.创建对象,传入待测牌组集合
        FindHus findHus = new FindHus(lists);
        findHus.setnThreads(16);//设置线程池中线程的个数,默认为4
        //3.调用方法开始寻找
        Map<List<Integer>, List<Integer>> result = findHus.findHu();
        //4.对结果进行排序处理,转换为字符串牌组,并输出
        result.forEach((a, b) -> {
            List<String> out1 = Deck.toDeckStr(a);
            List<String> out2 = Deck.toDeckStr(b);
            System.out.print("待测牌:");
            out1.forEach(e -> System.out.print(e + "t"));
            System.out.print("n胡牌:");
            out2.forEach(e -> System.out.print(e + "t"));
            System.out.println("n");
        });
    } catch (IndexOutOfBoundsException e) {
        e.printStackTrace();
    }
}
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/a4019069/article/details/79415964
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 23:09:29
  • 阅读 ( 962 )
  • 分类:算法

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢