掘金 后端 ( ) • 2024-04-23 15:10

1. 简介

这个idea插件呢就是 xechat-idea当然也有vscode版本的,vscode可以搜索xechat-vscode,博主本人有相关的笔记链接

说明一下,vscode版本的可以直接在vscode插件市场下载,但是登录的话原有的官方服务器不可用了。现存的有两个地址可用。可用服务器列表的前两个。
其中第二个服务器需要前方qq群754126966找邪恶鼓励师申请token ,下边的登录命令以第一个服务器为例

  • 查询服务器列表
#showServer
  • 登录服务器
#login {你的名字} -h lesscoding.net -p 1024 
或者 
#login {你的名字} -s 0

image.png

image.png

  • 打开麻将
#play 12

image.png

可以邀请同服务器好友同玩,也可直接开始和ai玩

image.png

点击free之后会将游戏页面独立出来一个窗口,该窗口可以调节透明度,摸鱼的时候不容易被发现

2. 开发过程

1. 什么是责任链

我的理解就是每个处理器只处理自己该处理的东西,除非遇到特殊条件,否则就交给下一个处理器来处理

2. 麻将游戏开发。

首先我这里现在只实现了最基本的 平胡七小对 胡牌判断,其他的胡牌类型都写在枚举里边了,有时间的话可以再写
然后 麻将氛围 1-9万 1-9饼 1-9条 东南西北 中发白
判断顺子的时候需要判断牌的数值是否连续,前边三种万饼条只要数字连续就可以,后边的两种只能组成刻子或者是杠。所以它们的id要隔开。于是就有了下方的这个类

1. 麻将枚举类

package cn.xeblog.plugin.game.mahjong.enums;


import cn.hutool.core.util.StrUtil;
import lombok.Getter;

import java.util.Arrays;


/**
 * @author eleven
 * @date 2024/3/20 8:38
 * @apiNote
 */
@Getter
public enum MahjongEnum {
    //🀇
    YI_WAN(1, "\uD83C\uDC07", "一万"),
    //🀈
    ER_WAN(2, "\uD83C\uDC08", "二万"),
    //🀉
    SAN_WAN(3, "\uD83C\uDC09", "三万"),
    //🀊
    SI_WAN(4, "\uD83C\uDC0A", "四万"),
    //🀋
    WU_WAN(5, "\uD83C\uDC0B", "五万"),
    //🀌
    LIU_WAN(6, "\uD83C\uDC0C","六万"),
    //🀍
    QI_WAN(7, "\uD83C\uDC0D", "七万"),
    //🀎
    BA_WAN(8, "\uD83C\uDC0E", "八万"),
    //🀏
    JIU_WAN(9, "\uD83C\uDC0F", "九万"),

    //🀐
    YI_TIAO(11, "\uD83C\uDC10", "一条"),
    //🀑
    ER_TIAO(12, "\uD83C\uDC11", "二条"),
    //🀒
    SAN_TIAO(13, "\uD83C\uDC12", "三条"),
    //🀓
    SI_TIAO(14, "\uD83C\uDC13", "四条"),
    //🀔
    WU_TIAO(15, "\uD83C\uDC14", "五条"),
    //🀕
    LIU_TIAO(16, "\uD83C\uDC15", "六条"),
    //🀖
    QI_TIAO(17, "\uD83C\uDC16", "七条"),
    //🀗
    BA_TIAO(18, "\uD83C\uDC17", "八条"),
    //🀘
    JIU_TIAO(19, "\uD83C\uDC18", "九条"),

    // 🀙
    YI_BING(21, "\uD83C\uDC19", "一饼"),
    //🀚
    ER_BING(22, "\uD83C\uDC1A", "二饼"),
    //🀛
    SAN_BING(23, "\uD83C\uDC1B", "三饼"),
    //🀜
    SI_BING(24, "\uD83C\uDC1C", "四饼"),
    //🀝
    WU_BING(25, "\uD83C\uDC1D", "五饼"),
    //🀞
    LIU_BING(26, "\uD83C\uDC1E", "六饼"),
    //🀟
    QI_BING(27, "\uD83C\uDC1F", "七饼"),
    //🀠
    BA_BING(28, "\uD83C\uDC20", "八饼"),
    //🀡
    JIU_BING(29, "\uD83C\uDC21", "九饼"),

    //🀀
    DONG_FENG(31, "\uD83C\uDC00", "东风"),
    //🀁
    NAN_FENG(33, "\uD83C\uDC01", "南风"),
    //🀂
    XI_FENG(35, "\uD83C\uDC02", "西风"),
    //🀃
    BEI_FENG(37, "\uD83C\uDC03", "北风"),
    //🀄
    HONG_ZHONG(41, "\uD83C\uDC04", "中"),
    //🀅
    FA_CAI(43, "\uD83C\uDC05", "发"),
    //🀆
    BAI_BAN(45, "\uD83C\uDC06", "白"),

    UNKNOWN(-9527, "\uD83C\uDC22", "暗杠")
    ;

    private final Integer id;

    private final String value;

    private final String tipsText;

    MahjongEnum(Integer id, String value, String tipsText) {
        this.id = id;
        this.value =value;
        this.tipsText =tipsText;
    }

    public static MahjongEnum getById(Integer searchId) {
        return Arrays.stream(MahjongEnum.values())
                .filter(item -> item.getId() != null && item.getId().equals(searchId))
                .findFirst()
                .orElse(UNKNOWN);
    }

    @Override
    public String toString() {
        return StrUtil.format("[{}:{}:{}]", id, value, tipsText);
    }
}

2. 接着我们写一个判断刻子,顺子,杠,的工具类

这里边的刻子就是有三个一样的,就是四个一样的,顺子就是三个连着的。
一开始的时候没想好怎么写判断顺子,但是后来给枚举加上id之后,只需要判断有没有三个id连续在一起就行了
判断是否能听牌就更加简单粗暴了,如果当前玩家的手牌不能赢,那么就把摸得牌替换完其他所有牌,如果能够胡的话那就是能够听牌。
例如你摸了一个发财没胡,那我就把这个牌替换成1-9万1-9条1-9饼东南西北中白,只要有一个能胡,那就是听牌了。

3. 胡牌类型枚举

从腾讯麻将里找的规则 完整代码地址

@Getter
public enum HuType {
    TIAN_HU(0, 88, "天胡", "", "庄家发完手牌后直接胡牌"),
    DI_HU(1, 88, "地胡", "", "发完手牌后,非庄家第一次摸牌就自摸胡牌。如在第一次摸牌前有任意玩家吃碰杠则不算地胡"),
    SHO_SAN_YAO(2, 88, "十三幺",
            values(DONG_FENG, NAN_FENG, XI_FENG, BEI_FENG) +
                    values(HONG_ZHONG, FA_CAI, BAI_BAN) +
                    values(YI_WAN, JIU_WAN) +
                    values(YI_TIAO, JIU_TIAO) +
                    values(YI_BING, JIU_BING) +
                    values(YI_WAN),
            "由3种序数牌的一、九牌,7种字牌及其中一对作将组成的胡牌。不计五门齐、不求人、单钓将、门前清、全带么");  
}

4. 摸牌类型枚举

package cn.xeblog.plugin.game.mahjong.enums;

/**
 * @author eleven
 * @date 2024/3/20 11:24
 * @apiNote
 */
public enum TouchType {
    // 摸
    TOUCH,
    // 吃
    EAT,
    // 碰
    BUMP,
    // 杠
    BAR,
    // 测试听牌
    TEST
    ;
}

3. 责任链开发

1. 创建一个责任链的处理对象

package cn.xeblog.plugin.game.mahjong.model.dto;

import cn.hutool.core.collection.CollUtil;
import cn.xeblog.plugin.game.mahjong.enums.MahjongEnum;
import cn.xeblog.plugin.game.mahjong.enums.TouchType;
import cn.xeblog.plugin.game.mahjong.utils.MahjongUtils;
import groovy.transform.builder.Builder;
import lombok.Data;

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

/**
 * @author eleven
 * @date 2024/3/20 9:06
 * @apiNote
 */
@Data
@Builder
public class MahjongRequest {
    // 摸得牌, 可能是 吃碰杠,或者自己摸的
    private MahjongEnum touchMahjong;
    // 玩家的手牌
    private List<MahjongEnum> handMahjong;
    // 当前轮次,判断天胡,地胡,人胡
    private Integer round;

    /**
     * 0 摸排 1吃牌 2碰牌 3杠牌
     */
    private TouchType touchType;
    // 是否胡牌
    private Boolean win;
    // 输出最后的胡牌类型和番数
    private StringBuilder stringBuilder;
    /**
     * 庄家
     */
    private Boolean banker;
    
    private Long score;
    // 玩家的刻子,顺子,杠,暗杠
    private MahjongGroup group;
    
    // 当前玩家手牌可以听牌的列表
    private List<MahjongEnum> tingList;



    public StringBuilder getStringBuilder() {
        if (stringBuilder == null) {
            stringBuilder = new StringBuilder();
        }
        return stringBuilder;
    }

    public void addScore(Long addScore) {
        Long defaultScore = Optional.ofNullable(score).orElse(0L);
        this.score = defaultScore + addScore;
    }

    public Boolean getWin() {
        return win != null && win;
    }

    public Boolean getBanker() {
        return banker != null && banker;
    }

    public List<MahjongEnum> getAllHandMahjong() {
        List<MahjongEnum> result = new ArrayList<>();
        List<MahjongEnum> handMahjong = getHandMahjong();
        if (CollUtil.isNotEmpty(handMahjong)) {
            result.addAll(handMahjong);
        }
        if (null != touchMahjong) {
            result.add(touchMahjong);
        }
        return MahjongUtils.sortById(result);
    }

    public Integer getRound() {
        return Optional.ofNullable(round).orElse(0);
    }

    public MahjongGroup getGroup() {
        return Optional.ofNullable(group).orElse(new MahjongGroup());
    }
}

2. 首先创建一个抽象类

这个抽象类定义了下一个处理器是谁,公共的处理方法 handler,定义了一个抽象方法 isValidWin用来给让每一个实现来判断是否符合自己的胡牌逻辑

package cn.xeblog.plugin.game.mahjong.handler;

import cn.xeblog.plugin.game.mahjong.model.dto.MahjongRequest;
import lombok.Data;

/**
 * @author eleven
 * @date 2024/3/20 9:04
 * @apiNote
 */
@Data
public abstract class HuHandler {

    private HuHandler nextHandler;

    public void handle(MahjongRequest request) {
        if (nextHandler != null) {
            nextHandler.handle(request);
        }
    }

    public abstract boolean isValidWin(MahjongRequest request);

}

3. 创建一个责任链的构造器

抄的别人的,好像是固定写法

package cn.xeblog.plugin.game.mahjong.handler;

import cn.xeblog.plugin.game.mahjong.handler.common.*;

import java.util.Arrays;

/**
 * @author eleven
 * @date 2024/3/20 13:40
 * @apiNote
 */
public class HuHandlerBuilder {
    private HuHandler head;

    private HuHandler tail;

    public HuHandlerBuilder addHandler(HuHandler handler) {
        if (this.head == null) {
            this.head = this.tail = handler;
            return this;
        }
        this.tail.setNextHandler(handler);
        this.tail = handler;
        return this;
    }

    public HuHandlerBuilder addHandler(HuHandler... handlers) {
        if (handlers.length != 0) {
            Arrays.stream(handlers).forEach(this::addHandler);
        }
        return this;
    }

    public HuHandler build() {
        return this.head;
    }

    public static HuHandler fastCommonBuild() {
        return new HuHandlerBuilder().addHandler(
                new PingHuHandler(),
                new SevenPairsHuHandler(),
                new SelfTouchHuHandler(),
                new ShiSanYaoHandler(),
                new TianHuHandler(),
                new DiHuHandler(),
                new RenHuHandler()
        ).build();
    }

}

4. 创建一个平胡处理类

具体逻辑自己看看吧,反正就那么回事

package cn.xeblog.plugin.game.mahjong.handler.common;

import cn.hutool.core.collection.CollUtil;
import cn.xeblog.plugin.game.mahjong.enums.MahjongEnum;
import cn.xeblog.plugin.game.mahjong.handler.HuHandler;
import cn.xeblog.plugin.game.mahjong.model.dto.MahjongGroup;
import cn.xeblog.plugin.game.mahjong.model.dto.MahjongRequest;
import cn.xeblog.plugin.game.mahjong.utils.MahjongUtils;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author eleven
 * @date 2024/3/20 9:07
 * @apiNote
 */
public class PingHuHandler extends HuHandler {
    @Override
    public void handle(MahjongRequest request) {
        boolean validWin = isValidWin(request);
        if (validWin) {
            request.setWin(true);
            request.addScore(1L);
            request.getStringBuilder().append(" 平胡(1)");
        }
        super.handle(request);
    }

    @Override
    public boolean isValidWin(MahjongRequest request) {
        List<MahjongEnum> handMahjong = request.getAllHandMahjong();
        Map<Integer, List<MahjongEnum>> map = MahjongUtils.getCountMap(handMahjong);
        return valid4442(map, request) || valid33332(map, request) || validCommon(handMahjong, request);
    }

    private boolean validCommon(List<MahjongEnum> handMahjong, MahjongRequest request) {
        Set<MahjongEnum> gang;
        //MahjongGroup group = request.getGroup();
        MahjongGroup group = new MahjongGroup();
        //List<MahjongEnum> pairs = getGroupAndPairsV1(handMahjong, group);
        List<MahjongEnum> pairs = getGroupAndPairsV2(handMahjong, group);
        if (pairs == null) return false;
        request.setGroup(group);
        int aaaOrAbcNum = group.getGang().size() +
                group.getKeZi().size() +
                group.getShunZi().size();

        return aaaOrAbcNum == 4 && (CollUtil.size(pairs) == 1 || CollUtil.isEmpty(handMahjong));

    }

    private List<MahjongEnum> getGroupAndPairsV2(List<MahjongEnum> handMahjong, MahjongGroup group) {
        List<MahjongEnum> pairs = getGroupAndPairsV1(handMahjong, group);
        Map<Integer, List<MahjongEnum>> map = handMahjong.stream()
                .collect(Collectors.groupingBy(MahjongEnum::getId));
        if (CollUtil.isNotEmpty(pairs)) {
            boolean doWhileFlag = pairs.stream().anyMatch(item -> map.get(item.getId()).size() > 2);
            while (pairs.size() > 1 && doWhileFlag) {
                getGroupAndPairsV1(handMahjong, group);
            }
        }
        return pairs;
    }

    @Nullable
    private static List<MahjongEnum> getGroupAndPairsV1(List<MahjongEnum> handMahjong, MahjongGroup group) {
        Set<MahjongEnum> gang;
        while (CollUtil.isNotEmpty(gang = MahjongUtils.getGang(handMahjong))) {
            group.addGang(gang);
            handMahjong.removeAll(gang);
        }
        List<MahjongEnum> pairs = MahjongUtils.getPairs(handMahjong);
        if (CollUtil.isEmpty(pairs)) {
            return null;
        }
        Set<MahjongEnum> keZi;
        while (CollUtil.isNotEmpty(keZi = MahjongUtils.getAAA(handMahjong))) {
            group.addKeZi(keZi);
            handMahjong.removeAll(keZi);
        }

        List<MahjongEnum> shunZi;
        while (CollUtil.isNotEmpty(shunZi = MahjongUtils.getABC(handMahjong))) {
            group.addShunZi(shunZi);
            shunZi.forEach(handMahjong::remove);

        }
        pairs = MahjongUtils.getPairs(handMahjong);

        if (CollUtil.isNotEmpty(pairs)) {
            group.setPairs(pairs.get(0));
        }
        return pairs;
    }

    /**
     * 校验三组四个一样的和一个对子
     *
     * @param map 分组数据
     * @return boolean
     */
    public boolean valid4442(Map<Integer, List<MahjongEnum>> map, MahjongRequest request) {
        return validSame(map, request);
    }

    /**
     * 校验四组三个一样的和一个对子
     *
     * @param map
     * @return
     */
    public boolean valid33332(Map<Integer, List<MahjongEnum>> map, MahjongRequest request) {
        // todo 现在只校验了四组三个一样
        return validSame(map, request);
    }


    public boolean validSame(Map<Integer, List<MahjongEnum>> map, MahjongRequest request) {
        MahjongGroup group = request.getGroup();
        for (List<MahjongEnum> value : map.values()) {
            int size = value.size();
            if (size == 4) {
                group.addGang(Set.copyOf(value));
            }
            if (size == 3) {
                group.addKeZi(Set.copyOf(value));
            }
            if (size == 2) {
                group.setPairs(value.get(0));
            }
        }
        return group.getPairs() != null &&
                (group.getGang().size() == 4 ||
                        group.getKeZi().size() == 4);
    }
}

5. 创建一个七小对处理器

package cn.xeblog.plugin.game.mahjong.handler.common;

import cn.xeblog.plugin.game.mahjong.enums.MahjongEnum;
import cn.xeblog.plugin.game.mahjong.handler.HuHandler;
import cn.xeblog.plugin.game.mahjong.model.dto.MahjongRequest;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author eleven
 * @date 2024/3/20 10:12
 * @apiNote 七小对
 */
public class SevenPairsHuHandler extends HuHandler {

    @Override
    public void handle(MahjongRequest request) {
        boolean validWin = isValidWin(request);
        if (validWin) {
            request.setWin(true);
            request.getStringBuilder().append("七小对");
            request.addScore(2L);
        }
        super.handle(request);
    }

    @Override
    public boolean isValidWin(MahjongRequest request) {
        List<MahjongEnum> handMahjong = request.getAllHandMahjong();
        Map<Integer, List<MahjongEnum>> map = handMahjong.stream()
                .collect(Collectors.groupingBy(MahjongEnum::getId));
        AtomicInteger count = new AtomicInteger();
        map.forEach((k, v) -> {
            if (v.size() % 2 == 0) {
                count.getAndIncrement();
            }
        });
        return count.get() == 7;
    }
}

6. 更多类型可以参考HuType这个枚举类自己写

7. 测试

/**
 * @author eleven
 * @date 2024/3/20 11:08
 * @apiNote
 */
public class MahjongTest {


    /**
     * 七小对
     */
    @Test
    public void sevenPairs() {
        List<MahjongEnum> mahjongEnums = MahjongUtils.wan();
        mahjongEnums.remove(BA_WAN);
        mahjongEnums.remove(JIU_WAN);
        List<MahjongEnum> handMahjong = new ArrayList<>(13);
        handMahjong.addAll(mahjongEnums);
        handMahjong.addAll(mahjongEnums);
        handMahjong.remove(0);
        MahjongRequest request = new MahjongRequest();
        request.setHandMahjong(handMahjong);
        request.setTouchMahjong(MahjongEnum.YI_WAN);
        request.setTouchType(TouchType.TOUCH);
        request.setWin(false);
        printMahjong(request.getAllHandMahjong());
        HuHandler build = HuHandlerBuilder.fastCommonBuild();
        build.handle(request);
        printResponse(request);
    }

    /**
     * 前两种情况能够正常判断,后边两种情况判断出错。
     * 所以需要对特殊牌型进行特殊处理
     */
    @Test
    public void getPairs() {
        List<MahjongEnum> handMahjong = new ArrayList<>();
        Collections.addAll(handMahjong,
                YI_BING, YI_BING,
                YI_BING, ER_BING, SAN_BING,
                YI_WAN, ER_WAN, SAN_WAN,
                SI_WAN, WU_WAN, LIU_WAN,
                QI_WAN, BA_WAN, JIU_WAN
        );
        printMahjong(handMahjong);
        MahjongUtils.getPairs(handMahjong)
                .forEach(System.out::println);

        handMahjong.clear();
        Collections.addAll(handMahjong,
                YI_BING, YI_BING,
                YI_TIAO, ER_TIAO, SAN_TIAO,
                YI_WAN, ER_WAN, SAN_WAN,
                SI_WAN, WU_WAN, LIU_WAN,
                QI_WAN, BA_WAN, JIU_WAN
        );
        printMahjong(handMahjong);
        MahjongUtils.getPairs(handMahjong)
                .forEach(System.out::println);

        handMahjong.clear();
        Collections.addAll(handMahjong,
                YI_TIAO, YI_TIAO, YI_TIAO, YI_TIAO,
                YI_WAN, YI_WAN, YI_WAN, YI_WAN,
                YI_TIAO, YI_TIAO, YI_TIAO, YI_TIAO,
                FA_CAI, FA_CAI
        );
        printMahjong(handMahjong);
        // todo 判断错误应该为 FA_CAI
        MahjongUtils.getPairs(handMahjong)
                .forEach(System.out::println);

        handMahjong.clear();
        Collections.addAll(handMahjong,
                YI_TIAO, YI_TIAO, YI_TIAO,
                YI_WAN, YI_WAN, YI_WAN,
                YI_TIAO, YI_TIAO, YI_TIAO,
                DONG_FENG, DONG_FENG, DONG_FENG,
                FA_CAI, FA_CAI
        );
        printMahjong(handMahjong);
        MahjongUtils.getPairs(handMahjong)
                .forEach(System.out::println);
    }

    @Test
    public void shiSanYao() {
        MahjongRequest request = new MahjongRequest();
        List<MahjongEnum> handMahjong = new ArrayList<>(14);
        Collections.addAll(handMahjong,
                DONG_FENG, NAN_FENG, XI_FENG, BEI_FENG,
                HONG_ZHONG, FA_CAI, BAI_BAN,
                YI_WAN, JIU_WAN,
                YI_TIAO, JIU_TIAO,
                YI_BING, JIU_BING);
        request.setHandMahjong(handMahjong);
        request.setTouchMahjong(JIU_BING);
        request.setRound(0);
        request.setBanker(true);
        request.setTouchType(TouchType.TOUCH);
        printMahjong(request.getAllHandMahjong());
        HuHandlerBuilder.fastCommonBuild().handle(request);
        printResponse(request);
    }
    
    @Test
    public void pingHuTest() {
        List<MahjongEnum> handMahjong = new ArrayList<>();
        Collections.addAll(handMahjong,
                YI_WAN, YI_WAN, YI_WAN, YI_WAN,
                YI_BING, YI_BING,
                YI_BING, ER_BING, SAN_BING,
                SI_WAN, SI_WAN, SI_WAN,
                BAI_BAN, BAI_BAN, BAI_BAN
        );
        MahjongRequest request = new MahjongRequest();
        request.setHandMahjong(handMahjong);
        HuHandler huHandler = HuHandlerBuilder.fastCommonBuild();
        huHandler.handle(request);
        printMahjong(request.getAllHandMahjong());
        printResponse(request);
    }

    @Test
    public void tingTest() {
        List<MahjongEnum> list = List.of(SAN_WAN, SI_WAN, WU_WAN,
                SAN_TIAO, ER_TIAO,
                WU_WAN, LIU_WAN, QI_WAN,
                SAN_BING, SI_BING, WU_BING,
                BAI_BAN, BAI_BAN);
        System.out.println(MahjongUtils.getTingList(list));
    }

    public MahjongRequest getReq(MahjongEnum... enums) {
        MahjongRequest request = new MahjongRequest();
        request.setHandMahjong(List.of(enums));
        return request;
    }

    private void printResponse(MahjongRequest request) {
        System.out.println(StrUtil.format("当前回合{},身份:{},得分{}\n胡牌类型{}\n组合{}\n",
                request.getRound(),
                request.getBanker() ? "庄" : "闲",
                request.getScore(),
                request.getStringBuilder(),
                request.getGroup()));
    }


    private void printMahjong(List<MahjongEnum> list) {
        System.out.println("===============当前牌型============");
        List<List<MahjongEnum>> partition = Lists.partition(list, 4);
        partition.forEach(item -> {
                    item.forEach(it -> System.out.print(it + "\t"));
                    System.out.println();
                }
        );
        System.out.println("==================================");
    }
}