diff --git a/robots/majiang/robot_mj_hz/config/game-config.xml b/robots/majiang/robot_mj_hz/config/game-config.xml new file mode 100644 index 0000000..94950a5 --- /dev/null +++ b/robots/majiang/robot_mj_hz/config/game-config.xml @@ -0,0 +1,10 @@ + + + + 8.134.76.43 + 8.134.76.43 + 8722 + 8722 + 22 + true + \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/config/log4j.properties b/robots/majiang/robot_mj_hz/config/log4j.properties new file mode 100644 index 0000000..6786dba --- /dev/null +++ b/robots/majiang/robot_mj_hz/config/log4j.properties @@ -0,0 +1,20 @@ + +log4j.rootLogger = INFO,consoleAppender,fileAppender + +# ConsoleAppender +log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender +log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.consoleAppender.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%t] %c{2} %3x - %m%n + + +# Regular FileAppender +log4j.appender.fileAppender=org.apache.log4j.DailyRollingFileAppender +log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout +log4j.appender.fileAppender.File=${WORKDIR}/logs/web_main.log +log4j.appender.fileAppender.layout.ConversionPattern=%d{dd MMM yyyy | HH:mm:ss,SSS} | %-5p | %t | %c{3} | %3x | %m%n +log4j.appender.fileAppender.Encoding=UTF-8 +log4j.appender.fileAppender.DatePattern='.'yyyy-MM-dd +log4j.appender.dailyFile.Append=true + +# The file is rolled over very day +log4j.appender.fileAppender.DatePattern ='.'yyyy-MM-dd \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/config/taurus-core.xml b/robots/majiang/robot_mj_hz/config/taurus-core.xml new file mode 100644 index 0000000..d7b7811 --- /dev/null +++ b/robots/majiang/robot_mj_hz/config/taurus-core.xml @@ -0,0 +1,62 @@ + + + log4j.properties + + + redis + com.taurus.core.plugin.redis.RedisPlugin + + + + 80 + + 20 + + 5 + + -1 + + true + + true + + true + + 100 + + 60000 + + 30000 + + 1800000 + + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/config/taurus-permanent.xml b/robots/majiang/robot_mj_hz/config/taurus-permanent.xml new file mode 100644 index 0000000..2c70a64 --- /dev/null +++ b/robots/majiang/robot_mj_hz/config/taurus-permanent.xml @@ -0,0 +1,75 @@ + + + 1 + + 512 + + Heap + + Heap + + 524288 + + 1024 + + 32768 + + 160 + + + 1 + 2 + 1 + + + true + + 300 + + + + + + + + + + 1.2.3.4 + + + 127.0.0.1 + + 10000 + + + + false +
0.0.0.0
+ 80 +
+ + + + robot - test + robot.mj.EXMainServer + + + + + Sys + 2 + 8 + 60000 + 20000 + + + + + Ext + 2 + 8 + 60000 + 20000 + + +
\ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/pom.xml b/robots/majiang/robot_mj_hz/pom.xml new file mode 100644 index 0000000..2ac2648 --- /dev/null +++ b/robots/majiang/robot_mj_hz/pom.xml @@ -0,0 +1,47 @@ + + 4.0.0 + + com.robot + robot_mj_changsha + 1.0.0 + jar + + robot_mj_changsha + http://maven.apache.org + + + UTF-8 + + + + + com.robot + robot_common + 1.0.0 + + + + + + + + + robot + + + org.apache.maven.plugins + maven-compiler-plugin + 3.6.1 + + + 1.8 + 1.8 + UTF-8 + + + + + + + diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java new file mode 100644 index 0000000..1b30f78 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/Config.java @@ -0,0 +1,138 @@ +package robot.mj; + +public class Config { + public static final int FENGDING_SCORE = 28; + public static final int XIPAI_SCORE = 10; + + public static final int ANCHOU_SCORE = 10; + + public static final String ROOM_CONFIG_ZIMO = "zimo"; + public static final String ROOM_CONFIG_ZHUANGXIAN = "zhuangxian"; + public static final String ROOM_CONFIG_NIAO = "niao"; + public static final String ROOM_CONFIG_NIAO_TYPE = "niao_type"; + public static final String ROOM_CONFIG_PIAO_NIAO = "piao_niao"; //0:不嫖 1:自由票 2:固定票 + public static final String ROOM_CONFIG_PIAO_NIAO_AUTO = "auto_piao"; //piao fen + public static final String ROOM_CONFIG_PIAO_NIAO_OPT = "piao_niao_opt"; //piao fen + public static final String ROOM_CONFIG_ZT_LIULIUSHUN = "zhongtuliuliushun"; + public static final String ROOM_CONFIG_ZT_DASIXI = "zhongtusixi"; + public static final String ROOM_CONFIG_QS_JIEJIEGAO = "jiejiegao"; + public static final String ROOM_CONFIG_QS_SANTONG = "santong"; + public static final String ROOM_CONFIG_QS_YIZHIHUA = "yizhihua"; + public static final String ROOM_CONFIG_QUEYIMEN = "queyimen"; + public static final String ROOM_CONFIG_FENGDING = "fengding"; + public static final String ROOM_CONFIG_FENGDING_SCORE = "fengding_score"; + public static final String ROOM_CONFIG_XIPAI = "xi_pai"; + public static final String ROOM_CONFIG_XIPAI_SCORE = "xi_pai_score"; + + public static final String ROOM_CONFIG_ANCHOU_SCORE = "an_chou_score"; + + public static final String ROOM_CONFIG_DIFEN_SCORE = "difen_score"; + public static final String ROOM_CONFIG_NIAOFEN_SCORE = "niaofen_score"; + public static final String ROOM_CONFIG_NIAOFEN_OPT = "niaofen_opt"; //0中鸟加分,//1中鸟加倍 + public static final String ROOM_CONFIG_KAI_GONG = "kai_gong"; //0:开杠2张,1:开杠四张 + + + public static final int NIAO_TYPE_ADD = 0; + + public static final int NIAO_TYPE_DOUBLE = 1; + + public static final int NIAO_TYPE_CS2NIAO = 2; + + + public static final String ROOM_CONFIG_QS_JTYN = "two_pair"; + + public static final String ROOM_CONFIG_NO_JIANG = "no_jiang"; + + public static final String ROOM_CONFIG_NATIVE_HU = "native_hu"; + + + public static final String ROOM_CONFIG_FOUR_WIN = "four_win"; + + public static final String SETTLE_XIAO_DIAN_PAO = "xiao_dian_pao"; + public static final String SETTLE_XIAO_JIE_PAO = "xiao_jie_pao"; + public static final String SETTLE_XIAO_ZIMO = "xiao_zimo"; + public static final String SETTLE_DA_DIAN_PAO = "da_dian_pao"; + public static final String SETTLE_DA_JIE_PAO = "da_jie_pao"; + public static final String SETTLE_DA_ZIMO = "da_zimo"; + + + public static final String GAME_EVT_PLAYER_DEAL = "811"; + + public static final String GAME_DIS_CARD = "611"; + + public static final String GAME_EVT_DISCARD = "812"; + + public static final String GAME_EVT_DISCARD_TIP = "813"; + + public static final String GAME_EVT_FZTIPS = "814"; + + public static final String GAME_ACTION = "612"; + + public static final String GAME_EVT_ACTION = "815"; + + public static final String GAME_EVT_HU = "816"; + + public static final String GAME_EVT_RESULT1 = "817"; + + public static final String GAME_EVT_RESULT2 = "818"; + + public static final String GAME_EVT_DRAW = "819"; + + public static final String GAME_EVT_CHANGE_ACTIVE_PLAYER = "820"; + + public static final String GAME_EVT_NIAO = "821"; + + public static final String GAME_EVT_QSTIP = "822"; + + public static final String GAME_EVT_QSWIN = "823"; + + public static final String GAME_EVT_OPENKONG = "824"; + + public static final String GAME_EVT_HAIDITIP = "825"; + + public static final String GAME_EVT_PIAONIAO_TIP = "833"; + + public static final String GAME_EVT_PIAONIAO = "834"; + + public static final String GAME_EVT_TING_TIP = "835"; + + public static final String GAME_EVT_TING = "836"; + + public static final String CREATE_ROOM_ROBOT = "create_room_for_robot"; + public static final String INIT_CONNECTION = "init_connection"; + + /** + * 加入房间 - robot_mgr to robot_mj_cs 的内部协议号 + */ + public static final String JOIN_ROOM = "2002"; + + /** + * 发送准备 - robot_mgr to robot_mj_cs 的内部协议号 + */ + public static final String GAME_READY = "2003"; + + /** + * 退出房间 - robot_mgr to robot_mj_cs 的内部协议号 + */ + public static final String EXIT_ROOM = "2005"; + + /** + * 手动重连 - robot_mgr to robot_mj_cs 的内部协议号 + */ + public static final String RECONNECT = "2006"; + + /** + * 发送准备 - robot_mj_cs to game_mj_cs 的协议号 + */ + public static final String GAME_READY_CS = "1003"; + + /** + * 加入房间 - robot_mgr to game_mj_cs 的内部协议号 + */ + public static final String JOIN_ROOM_CS = "1002"; + + /** + * 退出房间 - robot_mgr to game_mj_cs 的内部协议号 + */ + public static final String EXIT_ROOM_CS = "1005"; +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXActionEvent.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXActionEvent.java new file mode 100644 index 0000000..556d1b3 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXActionEvent.java @@ -0,0 +1,9 @@ +package robot.mj; + +public class EXActionEvent { + + public static final String EVENT_ACTION = "action"; + + public static final String EVENT_DISCARD = "discard"; + +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java new file mode 100644 index 0000000..2f66fca --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXGameController.java @@ -0,0 +1,413 @@ +package robot.mj; + +import com.robot.GameController; +import com.robot.GameInterceptor; +import com.robot.MainServer; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TObject; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.routes.ActionKey; +import com.taurus.permanent.data.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; +import robot.mj.info.RobotUser; +import taurus.client.TaurusClient; +import taurus.client.business.GroupRoomBusiness; +import taurus.util.ROBOTEventType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 红中麻将游戏控制器 - 处理游戏协议 + */ +public class EXGameController extends GameController { + private static final Logger log = LoggerFactory.getLogger(EXGameController.class); + + private static final RobotConnectionManager robotConnectionManager = new RobotConnectionManager(); + + //机器人房间 + protected static final Map robotRoomMapping = new ConcurrentHashMap<>(); + + public EXGameController() { + super(); + log.info("红中麻将游戏控制器已初始化"); + } + + /** + * 处理客户端初始化/认证协议 + */ + @ActionKey(value = Config.INIT_CONNECTION, validate = GameInterceptor.NOT_PLAYER) + public void handleInitialization(Session session, ITObject params, int gid) { + try { + log.info("收到客户端初始化请求,Session: {}, GID: {}, 参数: {}", session, gid, params); + + String type = params.getString("type"); + String client = params.getString("client"); + log.info("客户端类型: {}, 客户端标识: {}", type, client); + + //验证参数 + if ("robot_mj_cs".equals(type) && "robot_mgr".equals(client)) { + //返回成功响应 + ITObject response = TObject.newInstance(); + response.putString("status", "success"); + response.putString("message", "初始化成功"); + + log.info("客户端初始化成功,发送响应"); + MainServer.instance.sendResponse(gid, 0, response, session); + } else { + log.warn("客户端初始化失败,参数验证不通过: type={}, client={}", type, client); + + ITObject response = TObject.newInstance(); + response.putString("status", "failed"); + response.putString("message", "参数验证失败"); + MainServer.instance.sendResponse(gid, 1, response, session); + } + } catch (Exception e) { + log.error("处理初始化请求时发生错误", e); + + ITObject response = TObject.newInstance(); + response.putString("status", "error"); + response.putString("message", "服务器内部错误: " + e.getMessage()); + + MainServer.instance.sendResponse(gid, 1, response, session); + } + } + + /** + * 接收来自robot_mgr的创建房间协议 + */ + @ActionKey(value = Config.CREATE_ROOM_ROBOT, validate = GameInterceptor.NOT_PLAYER) + public void createRoomForRobot(Session session, ITObject params, int gid) { + try { + int reqGroupId = params.getInt("groupId"); + int wanfaId = params.getInt("wanfaId"); + int robotId = params.getInt("robotId"); + + //使用统一的房间创建器在Redis中创建房间 + RoomCreator.RoomInfo roomInfo = RoomCreator.createRoomInRedis(reqGroupId, wanfaId, robotId); + + if (roomInfo != null) { + //返回房间信息给robot_mgr + ITObject response = TObject.newInstance(); + response.putString("roomKey", roomInfo.getRoomId()); + response.putInt("groupId", roomInfo.getGroupId()); + response.putInt("wanfaId", roomInfo.getWanfaId()); + response.putInt("robotId", robotId); + response.putString("server_ip", roomInfo.getServerIp()); + response.putInt("server_port", roomInfo.getServerPort()); + + log.info("成功创建房间,房间ID: {}", roomInfo.getRoomId()); + //使用Taurus框架方法发送响应 + MainServer.instance.sendResponse(gid, 0, response, session); + } else { + log.error("创建房间失败,群组ID: {}, 玩法ID: {}", reqGroupId, wanfaId); + } + } catch (Exception e) { + log.error("处理创建房间请求时发生错误", e); + } + } + + /** + * 接收来自robot_mgr的加入房间协议 + */ + @ActionKey(value = Config.JOIN_ROOM, validate = GameInterceptor.NOT_PLAYER) + public void joinRoom(Session session, ITObject params, int gid) { + try { + String connecId = params.getString("connecId"); + TaurusClient client = getCsMjGameServerConnection(connecId); + System.out.println("接收来自robot_mgr的加入房间协议 connecId: = "+connecId); + System.out.println("接收来自robot_mgr的加入房间协议 client: = "+client); + if (client == null) { + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "failed"); + errorResponse.putString("message", "无法获取游戏服务器连接"); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); + return; + } + + //设置session和token + String sessionToken = params.getString("session"); + String robotSession = null; + String token = null; + if (sessionToken != null && sessionToken.contains(",")) { + String[] sessionParts = sessionToken.split(","); + if (sessionParts.length >= 2) { + robotSession = sessionParts[0]; + token = sessionParts[1]; + robotConnectionManager.setSessionAndToken(robotSession, token, connecId); + } + } + + Integer groupId = params.getInt("groupId"); + String roomId = params.getString("roomId"); + String robotId = params.getString("robotId"); + + GroupRoomBusiness.joinRoom(groupId, roomId, robotSession, null); + + Thread.sleep(5000); + + params.del("groupId"); + params.del("roomId"); + params.del("connecId"); + //发送加入房间请求到game_mj_cs + client.send(Config.JOIN_ROOM_CS, params, response -> { + System.out.println("joinRoomController: " + response); + RobotUser roomInfo = new RobotUser(); + roomInfo.setCurrentRoomId(Integer.parseInt(roomId)); + roomInfo.setConnecId(connecId); + roomInfo.setUserId(0); + //机器人房间映射关系 + robotRoomMapping.put(robotId, roomInfo); + }); + + ITObject paramsReq = TObject.newInstance(); + paramsReq.putString("status", "success"); + MainServer.instance.sendResponse(gid, 0, paramsReq, session); + + } catch (Exception e) { + log.error("处理加入房间请求时发生错误", e); + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "error"); + errorResponse.putString("message", "处理加入房间请求时发生错误: " + e.getMessage()); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); + } + } + + /** + * 接收来自robot_mgr的机器人准备协议 + */ + @ActionKey(value = Config.GAME_READY, validate = GameInterceptor.NOT_PLAYER) + public void robotReadyRoom(Session session, ITObject params, int gid) { + try { + String connecId = params.getString("connecId"); + TaurusClient client = getCsMjGameServerConnection(connecId); + System.out.println("接收来自robot_mgr的机器人准备协议 connecId: = "+connecId); + System.out.println("接收来自robot_mgr的机器人准备协议 client: = "+client); + if (client == null) { + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "failed"); + errorResponse.putString("message", "无法获取游戏服务器连接"); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); + return; + } + + params.del("connecId"); + Thread.sleep(1000); + //发送准备请求到game_mj_cs + client.send(Config.GAME_READY_CS, params, response -> { + System.out.println("robotReadyRoom: " + response); + }); + + ITObject paramsReq = TObject.newInstance(); + paramsReq.putString("status", "success"); + MainServer.instance.sendResponse(gid, 0, paramsReq, session); + + } catch (Exception e) { + log.error("处理机器人准备请求时发生错误", e); + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "error"); + errorResponse.putString("message", "服务器内部错误: " + e.getMessage()); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); + } + } + + /** + * 接收来自web_group的加入房间协议 + */ + @ActionKey(value = "225", validate = GameInterceptor.NOT_PLAYER) + public void webGroup(Session session, ITObject params, int gid) { + int robotId = params.getInt("robotid"); + String roomId = params.getString("roomid"); + int groupId = params.getInt("groupid"); + + //防止玩家操作同一房间 导致其他机器人加入 + List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); + if (!robotUsers.isEmpty()) { + synchronized (robotUsers) { + RobotUser robotUser = robotUsers.get(0); + if (robotId != Integer.parseInt(robotUser.getRobotId())) { + System.err.println("房间{"+ roomId +"}中已有机器人{"+robotUser.getRobotId()+"},当前机器人{"+robotId+"}不执行加入逻辑"); + return; + } + } + } + System.err.println("225开始进房间: "+"room:"+ roomId +"robot:"+robotId); + //加入房间 + joinRoomCommon(robotId, roomId, groupId, params); + System.err.println("225已进入房间准备成功: "+"room:"+ roomId +"robot:"+robotId); + } + + /** + * 接收来自web_group的主动重连协议 + */ + @ActionKey(value = "226", validate = GameInterceptor.NOT_PLAYER) + public void webGroupActive(Session session, ITObject params, int gid) { + int robotId = params.getInt("robotid"); + String roomId = params.getString("roomid"); + System.err.println("226开始进房间: " + "room:" + roomId + "robot:" + robotId); + //加入房间 + joinRoomCommon(params.getInt("robotid"), params.getString("roomid"), params.getInt("groupid"), params); + System.err.println("226已进入房间准备成功: " + "room:" + roomId + "robot:" + robotId); + } + + /** + * 重启服务断线重连 + * */ + public void webGroupJoinRoom(RobotUser robotUser) { + String connecId = robotUser.getConnecId(); + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + //重启检查 + try { + Set robotTokens = jedis0.smembers("{user}:"+robotUser.getRobotId()+"_token"); + String robotSession = null; + + for (String token : robotTokens) { + if (jedis0.exists(token)) { + robotSession = token; + break; + } + } + String gallrobot = jedis2.hget("gallrobot", robotUser.getRobotId()); + if (gallrobot.equals("0")) { + robotRoomMapping.remove(connecId); + return; + } + + System.err.println("重启后开始进房间: " + "room:" + robotUser.getCurrentRoomId() + "robot:" + robotUser.getRobotId()); + ITObject params = new TObject(); + params.putString("session", "{user}:" + robotUser.getRobotId() + "," + robotSession); + //加入房间 + joinRoomCommon(Integer.parseInt(robotUser.getRobotId()), String.valueOf(robotUser.getCurrentRoomId()), Integer.parseInt(robotUser.getRobotGroupid()), params); + System.err.println("重启后已进入房间准备成功: " + "room:" + robotUser.getCurrentRoomId() + "robot:" + robotUser.getRobotId()); + + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + jedis0.close(); + jedis2.close(); + } + } + + /** + * 加入房间逻辑 + */ + private void joinRoomCommon(int robotId, String roomId, int groupId, ITObject params) { + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + try { + Set robotTokens = jedis0.smembers("{user}:" + robotId + "_token"); + String robotSession = null; + + for (String token : robotTokens) { + if (jedis0.exists(token)) { + robotSession = token; + break; + } + } + + System.err.println("开始进房间: room:" + roomId); + System.err.println("开始进房间: {user}:" + robotId); + + TaurusClient client = getCsMjGameServerConnection(roomId + "_" + robotId); + GroupRoomBusiness.joinRoom(groupId, "room:" + roomId, "{user}:" + robotId, null); + + //机器人房间映射关系 + RobotUser robotUser = getRobotRoomInfo(String.valueOf(robotId)); + String connecId = roomId + "_" + robotId; + if (robotUser.getCurrentRoomId() == 0) { + robotUser.setCurrentRoomId(Integer.parseInt(roomId)); + robotUser.setClient(client); + robotUser.setConnecId(connecId); + } + robotRoomMapping.put(robotUser.getConnecId(), robotUser); + robotRoomMapping.remove(robotUser.getRobotId()); + Thread.sleep(2000); + params.putString("session", "{user}:" + robotId + "," + robotSession); + + //发送加入房间请求到game_mj_cs + client.send(Config.JOIN_ROOM_CS, params, response -> { + robotConnectionManager.reconnectToGameServer(response, robotUser, client); + }); + System.err.println("已进入房间成功: " + robotUser.getConnecId()); + Thread.sleep(1000); + if (client.isConnected()) { + client.send(Config.GAME_READY_CS, params, response -> { + System.out.println("1003:" + response); + }); + jedis2.hset("gallrobot", String.valueOf(robotUser.getRobotId()), "1"); + + robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_READY); + robotConnectionManager.setSessionAndToken("{user}:" + robotId, robotSession, robotUser.getConnecId()); + } + robotUser.setIntoRoomTime(robotConnectionManager.getTime()); + System.err.println("已进入房间准备成功: " + robotUser.getConnecId()); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + jedis0.close(); + jedis2.close(); + } + } + + /** + * 根据机器人ID获取其所在的房间信息 + */ + public static RobotUser getRobotRoomInfo(String robotId) { + RobotUser robotUser = robotRoomMapping.get(robotId); + if (robotUser ==null) { + RobotUser robotUserCopy = new RobotUser(); + robotUserCopy.setRobotId(robotId); + robotUserCopy.setPassword("123456"); + robotUserCopy.setGameHost("8.134.76.43"); + robotUserCopy.setGamePort("6421"); + robotUserCopy.setRobotGroupid("330800"); + robotUserCopy.setRobotPid("22"); + return robotUserCopy; + } + return robotRoomMapping.get(robotId); + } + + /** + * 根据房间ID获取所有对应的RobotUser + */ + public List getRobotUsersByRoomId(int roomId) { + String prefix = roomId + "_"; + List result = new ArrayList<>(); + + for (Map.Entry entry : robotRoomMapping.entrySet()) { + if (entry.getKey().startsWith(prefix)) { + result.add(entry.getValue()); + } + } + return result; + } + + /** + * 根据机器人ID删除其所在的房间信息 + */ + public static void removeRobotRoomInfo(String robotId) { + robotRoomMapping.remove(robotId); + } + + /** + * 根据机器人ID和连接ID获取红中麻将游戏服务器连接 + * 基于robotId和connectionId的组合复用连接 + */ + public static TaurusClient getCsMjGameServerConnection(String connecId) { + TaurusClient taurusClient = robotConnectionManager.getGameClient(connecId); + System.out.println("根据机器人ID和连接ID获取红中麻将游戏服务器连接 client: = "+taurusClient); + if (taurusClient != null) { + log.debug("成功获取游戏服务器连接,connecId: {}", connecId); + return taurusClient; + } + taurusClient = robotConnectionManager.connectToGameServer(connecId); + return taurusClient; + } + +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java new file mode 100644 index 0000000..50c87a2 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXMainServer.java @@ -0,0 +1,164 @@ +package robot.mj; + +import java.util.Map; + +import com.robot.GameController; +import com.robot.MainServer; +import com.robot.data.Player; +import com.robot.data.Room; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TObject; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.permanent.TPServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; +import robot.mj.info.RobotUser; +import taurus.client.NetManager; +import taurus.client.TaurusClient; + +import java.util.concurrent.TimeUnit; + +import static robot.mj.EXGameController.robotRoomMapping; + +/** + * 红中麻将机器人主服务器 + * TCP服务端接收robot_mgr的协议 同时作为客户端连接game_mj_cs处理AI逻辑 + */ +public class EXMainServer extends MainServer{ + private static final Logger log = LoggerFactory.getLogger(EXMainServer.class); + + private static final RobotConnectionManager robotConnectionManager = new RobotConnectionManager(); + private static final EXGameController exGameController = new EXGameController(); + private volatile boolean connectionCheckRunning = true; + + @Override + public void onStart() { + super.onStart(); + + // 1. 先启动独立的事件处理线程(只启动一次) + startNetEventThread(); + + // 2. 启动连接检查定时任务 + //startConnectionCheckScheduler(); + //测试 + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + String robotskey = "g{"+762479+"}:play:"+22; + Map maprobot = jedis2.hgetAll(robotskey); + for(Map.Entry entry : maprobot.entrySet()) { + System.out.println(entry.getKey() + ":" + entry.getValue()); + //是否创建 + RobotUser robotUser = new RobotUser(); + robotUser.setRobotId(entry.getKey()); + robotUser.setPassword("123456"); + robotUser.setGameHost("8.134.76.43"); + robotUser.setGamePort("6421"); + robotUser.setRobotGroupid("762479"); + robotUser.setRobotPid("22"); + + robotRoomMapping.put(entry.getKey(), robotUser); + } + + for(Map.Entry entry : robotRoomMapping.entrySet()) { + RobotUser robotUser = entry.getValue(); + //1、登录 + //判断是否登录 + if(!robotUser.isLogin){ + robotConnectionManager.login(robotUser); + } + } + +// TPServer.me().getTimerPool().scheduleAtFixedRate(new Runnable() { +// @Override +// public void run() { +// +// for(Map.Entry entry : robotRoomMapping.entrySet()) { +// RobotUser robotUser = entry.getValue(); +// //1、登录 +// //判断是否登录 +// if(!robotUser.isLogin){ +// robotConnectionManager.login(robotUser); +// } +// /*//2、链接 +// //判断是否链接 +// System.out.println("robotUser.isconnect"+robotUser.getIsconnect()); +// if(!robotUser.getIsconnect()){ +// robotConnectionManager.connectGame(robotUser); +// }else{ +// robotConnectionManager.renconnect(robotUser); +// } +// +// +// //4、加入房间 +// if(robotUser.getStatus()==0){ +// //没状态时候进入加入房间 +// ITObject params = new TObject(); +// params.putString("connecId", robotUser.getRoomId()+"_"+robotUser.getRobotId()); +// params.putString("roomId", robotUser.getRoomId()); +// params.putString("groupId", robotUser.getRobotGroupid()); +// params.putString("session", "{user}:"+robotUser.getRobotId()); +// exGameController.joinRoom(null, params, robotUser.getClient().getId()); +// } +// System.out.println("robotUser.getIntoRoomTime()"+robotUser.getIntoRoomTime()); +// System.out.println("robGetTiem"+robotConnectionManager.getTime()); +// if(robotUser.getIntoRoomTime()+6<=robotConnectionManager.getTime()){ +// //5、退出房间 +// robotConnectionManager.outoRoom(robotUser); +// }*/ +// +// } +// } +// }, 0, 5 ,TimeUnit.SECONDS); + //5、干活 + log.info("红中麻将机器人服务器已启动"); + log.info("服务器将监听端口 {} 用于接收robot_mgr管理协议", gameSetting.port); + + jedis2.close(); + } + + /** + * 独立的事件处理线程 + */ + private void startNetEventThread() { + Thread eventThread = new Thread(() -> { + while (true) { + NetManager.processEvents(); + try { + Thread.sleep(2); + } catch (InterruptedException e) { + break; + } catch (Exception e) { + } + } + }, "NetEvent-Thread"); + + eventThread.setDaemon(true); // 设置为守护线程 + eventThread.start(); + } + + + + @Override + public Room newRoom(String roomid, Map redis_room_map) { + return new EXRoom(roomid, redis_room_map); + } + + @Override + public Player newPlayer(int i, Room room, String s) { + return new EXPlayer(i, room, s); + } + + + protected GameController newController() { + return new EXGameController(); + } + + @Override + public void onStop() { + super.onStop(); + + // 停止连接检查线程 + connectionCheckRunning = false; + log.info("红中麻将机器人服务器已停止"); + } +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXPlayer.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXPlayer.java new file mode 100644 index 0000000..54b8039 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXPlayer.java @@ -0,0 +1,31 @@ +package robot.mj; + +import com.robot.Util; +import com.robot.data.Player; +import com.robot.data.Room; +import com.robot.data.Score; +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TArray; +import com.taurus.core.entity.TObject; + +import java.util.*; + +/** + * + * + */ +public class EXPlayer extends Player { + + + + public EXPlayer(int playerid, Room table, String session_id) { + super(playerid, table, session_id); + System.out.println("new robot "); + } + + public EXRoom getRoom() { + return (EXRoom) room; + } + +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXRoom.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXRoom.java new file mode 100644 index 0000000..84ea302 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/EXRoom.java @@ -0,0 +1,53 @@ +package robot.mj; + +import com.robot.data.Room; + + +import java.util.Map; + +public class EXRoom extends Room { + + // + + + + public EXRoom(String roomid, Map redis_room_map) { + super(roomid, redis_room_map); + + + } + + + + + public void checkAction() { + + } + + + + + + + + @Override + protected void roomResult() { + + } + + @Override + public void endGame() { + super.endGame(); + } + + @Override + public void saveMilitaryTotal(boolean dissmiss) { + super.saveMilitaryTotal(dissmiss); + } + + @Override + public void clear() { + super.clear(); + } + +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotConnectionManager.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotConnectionManager.java new file mode 100644 index 0000000..8e053bb --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RobotConnectionManager.java @@ -0,0 +1,600 @@ +package robot.mj; + +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TArray; +import com.taurus.core.entity.TObject; +import com.taurus.core.events.Event; +import com.taurus.core.events.IEventListener; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.util.ICallback; +import com.taurus.core.util.StringUtil; +import robot.mj.business.AccountBusiness; +import robot.mj.handler.HuNanHongZhong; +import robot.mj.info.RobotUser; +import taurus.client.Message; +import taurus.client.MessageResponse; +import taurus.client.TaurusClient; +import taurus.client.SocketCode; +import redis.clients.jedis.Jedis; + +import java.util.*; +import java.util.concurrent.*; + +import static robot.mj.EXGameController.robotRoomMapping; + +/** + * 机器人连接管理器 - 管理与游戏服务器的连接 + */ +public class RobotConnectionManager { + + private static final Map huNanHongZhongInstances = new ConcurrentHashMap<>(); + private final EXGameController exGameController; + + private final String host="8.134.76.43"; + private final int port=6421; + + + public RobotConnectionManager() { + exGameController = new EXGameController(); + } + + /** + * 获取红中麻将处理器实例 + */ + private HuNanHongZhong getHuNanHongZhongInstance(String connecId) { + HuNanHongZhong existingInstance = huNanHongZhongInstances.get(connecId); + if (existingInstance != null) { + return existingInstance; + } + + HuNanHongZhong newInstance = new HuNanHongZhong(); + + //从Redis恢复状态 + boolean restored = newInstance.restoreFromRedis(connecId); + if (restored) { + System.out.println("从Redis恢复HuNanHongZhong实例: " + connecId); + } else { + System.out.println("创建新的HuNanHongZhong实例: " + connecId); + } + + huNanHongZhongInstances.put(connecId, newInstance); + System.out.println("当前HuNanHongZhong实例总数: " + huNanHongZhongInstances.size()); + return newInstance; + } + + /** + * 设置会话和令牌 + */ + public void setSessionAndToken(String session, String token, String connecId) { + HuNanHongZhong instance = getHuNanHongZhongInstance(connecId); + instance.session = session; + instance.token = token; + } + + /** + * 连接到红中麻将游戏服务器 + */ + public TaurusClient connectToGameServer(String connecId) { + try { + //创建Taurus客户端 + TaurusClient client = new TaurusClient(host + ":" + port, "game", TaurusClient.ConnectionProtocol.Tcp); + + //设置事件监听器 + setupEventListeners(client, connecId); + + client.connect(); + + return client; + } catch (Exception e) { + return null; + } + } + + /** + * 断开与游戏服务器的连接(主动断开) + */ + public void disconnectFromGameServer(String connecId) { + System.out.println("开始主动断开连接: {"+connecId+"}"); + RobotUser robotUser = robotRoomMapping.remove(connecId); + + //清理连接数据 + if (connecId != null) { + HuNanHongZhong.removeFromRedis(connecId); + + HuNanHongZhong instance = huNanHongZhongInstances.get(connecId); + if (instance != null) { + instance.getHongZhongCardInhand().clear(); + instance.getChuGuoCardInhand().clear(); + System.out.println("清空HuNanHongZhong集合数据: " + connecId); + } + + huNanHongZhongInstances.remove(connecId); + } + + if (robotUser != null) { + TaurusClient client = robotUser.getClient(); + if (client != null) { + try { + if (client.isConnected()) { + client.killConnection(); + } + System.out.println("客户端主动断开连接完成: {"+connecId+"}"); + } catch (Exception e) { + System.out.println("断开客户端连接时发生异常: " + connecId + ", 错误: " + e.getMessage()); + } + } else { + System.out.println("客户端连接不存在: {"+connecId+"}"); + } + } + } + + /** + * 设置事件监听器 + */ + public void setupEventListeners(TaurusClient client, String connecId) { + //添加消息事件监听器 + IEventListener messageListener = new IEventListener() { + @Override + public void handleEvent(Event event) { + //获取 msg + Message message = (Message) event.getParameter("msg"); + + ITObject param = message.param; + //回调协议号 + String command = message.command; + System.out.println("csmj OnEvent msg: " + command); + + //根据玩法ID处理不同的回调 + if (StringUtil.isNotEmpty(command)) { + //直接处理协议 + handleProtocol(command, message, client, connecId); + } + } + }; + + //添加连接状态监听器 + IEventListener connectListener = new IEventListener() { + @Override + public void handleEvent(Event event) { + Message message = (Message) event.getParameter("msg"); + SocketCode code = (SocketCode) event.getParameter("code"); + + } + }; + + //注册事件监听器 + client.addEventListener(TaurusClient.NetClientEvent.OnEvent, messageListener); + client.addEventListener(TaurusClient.NetClientEvent.Connect, connectListener); + } + + + /** + * 机器人断线重连 + */ + public void reconnectToGameServer(MessageResponse response, RobotUser robotUser, TaurusClient client) { + String connecId = robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId(); + if(client.isConnected()){ + ITObject obj = response.messageData.param.getTObject("tableInfo"); + ITObject reloadInfo = response.messageData.param.getTObject("reloadInfo"); + if (obj!=null) { + //处理 seat + //获取机器人的seat + ITArray playerData = obj.getTArray("playerData"); + for (int i = 0; i < playerData.size(); i++) { + ITObject tms = playerData.getTObject(i); + Integer tmuserid = tms.getInt("aid"); + if(tmuserid==Integer.parseInt(robotUser.getRobotId())){ + Integer seat = tms.getInt("seat"); + robotUser.setSeat(seat); + } + } + System.out.println("playerData:"+playerData); + + System.out.println("obj:"+obj); + System.out.println("reloadInfo:"+reloadInfo); + if(reloadInfo!=null) { + //重连回来的 + int curren_outcard_seat = reloadInfo.getInt("curren_outcard_seat"); + if(curren_outcard_seat== robotUser.getSeat()){ + //同步手牌 + ITArray hand_card = reloadInfo.getTArray("hand_card"); + ITArray info_list = reloadInfo.getTArray("info_list"); + + List hcard = new ArrayList<>(); + if(hand_card!=null) { + for (int i = 0; i < hand_card.size(); i++) { + hcard.add(hand_card.getInt(i)); + } + } + ITArray outcard_list = new TArray(); + if(info_list!=null) { + for (int i = 0; i < info_list.size(); i++) { + ITObject tms = info_list.getTObject(i); + Integer playerid = tms.getInt("playerid"); + if(playerid==Integer.parseInt(robotUser.getRobotId())){ + outcard_list = tms.getTArray("outcard_list"); + } + } + } + + System.out.println("hcard>0"+hcard); + if(hcard.size()>0){ + //同步手牌 + HuNanHongZhong currentInstance = getHuNanHongZhongInstance(connecId); + + //同步逻辑比较手牌数量 + List currentHand = currentInstance.getHongZhongCardInhand(); + if (currentHand.isEmpty() || hcard.size() > currentHand.size()) { + //手牌集合为空 或者 玩家出牌了 + currentInstance.updateHandCard(hcard); + System.out.println("断线重连:同步手牌数据,服务器手牌:" + hcard); + } else { + System.out.println("断线重连:使用Redis恢复的手牌数据,数量:" + currentHand.size()); + } + + if(outcard_list.size()>0){ + List outcards = new ArrayList<>(); + for (int i = 0; i < outcard_list.size(); i++) { + outcards.add(outcard_list.getInt(i)); + } + + //检查出牌记录是否需要同步 + List currentOutCards = currentInstance.getChuGuoCardInhand(); + if (currentOutCards.isEmpty() || outcards.size() > currentOutCards.size()) { + currentInstance.updateOutCard(outcards); + System.out.println("断线重连:同步出牌数据,服务器出牌:" + outcards); + } else { + System.out.println("断线重连:使用Redis恢复的出牌数据,数量:" + currentOutCards.size()); + } + } + + sleepTime(2000); + currentInstance.outCard(client); + } else { + System.err.println("警告:重连时未获取到手牌数据"); + } + } + } + } + }else { + renconnect(robotUser); + } + } + + /** + * 处理接收到的游戏协议 + */ + private void handleProtocol(String command, Message message, TaurusClient client, String connecId) { + RobotUser robotUser = robotRoomMapping.get(connecId); + int robotId = Integer.parseInt(robotUser.getRobotId()); + ITObject param = message.param; + HuNanHongZhong huNanHongZhong = getHuNanHongZhongInstance(connecId); + Jedis jedis0 = Redis.use().getJedis(); + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + try { + //红中麻将 机器人处理事件 + //出牌广播 + if ("812".equalsIgnoreCase(command)) { + huNanHongZhong.drawCard(command, message); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //初始化手牌 + else if ("811".equalsIgnoreCase(command)) { + huNanHongZhong.cardInHead(command, message, client); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //摸牌 + else if ("819".equalsIgnoreCase(command)) { + huNanHongZhong.getCard(command, message); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //出牌,牌权 + else if ("813".equalsIgnoreCase(command)) { + huNanHongZhong.outCard(client); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //结算 + else if ("817".equalsIgnoreCase(command)) { + huNanHongZhong.getHongZhongCardInhand().clear(); + huNanHongZhong.getChuGuoCardInhand().clear(); + System.out.println("红中结算"); + Integer type = param.getInt("type"); + if (type == 1 || type == 2) { //为1 为大结算 为2为解散,都需要恢复数据 + //更新机器人剩余数量 + updateLeftoverRobot(Integer.parseInt(robotUser.getRobotId())); + } + ITObject params = TObject.newInstance(); + params.putString("session", client.getSession()); + client.send("1003", params, new ICallback() { + @Override + public void action(MessageResponse messageResponse) { + + } + }); + } + //杠碰胡通知协议 + else if ("814".equalsIgnoreCase(command)) { + huNanHongZhong.actionCard(param, client); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } else if ("820".equalsIgnoreCase(command)) { + HuNanHongZhong.changePlayer(command, message); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //服务器通知客户端有玩家执行了操作 + else if ("815".equalsIgnoreCase(command)) { + huNanHongZhong.shanchuchuguopai(param); + //处理完协议后保存到Redis + HuNanHongZhong currentInstance = huNanHongZhongInstances.get(connecId); + currentInstance.saveToRedis(connecId); + } + //玩家加入房间 + else if ("2001".equalsIgnoreCase(command)) { + CompletableFuture.runAsync(() -> { + sleepTime(6000); + + String roomKey = String.valueOf(robotUser.getCurrentRoomId()); + + //查询该房间的玩家信息 + String playersStr = jedis0.hget("room:"+roomKey, "players"); + if (!playersStr.equals("[]")) { + String players = playersStr.substring(1, playersStr.length() - 1); + String[] playerIds = players.split(","); + + //判断只有当前机器人一个玩家 + if (playerIds.length == 1) { + int playerId = Integer.parseInt(playerIds[0].trim()); + if (playerId == robotId) { + + //发送退出房间协议 + ITObject params = TObject.newInstance(); + client.send("1005", params, response -> { + EXGameController.removeRobotRoomInfo(String.valueOf(robotId)); + //更新机器人剩余数量 + updateLeftoverRobot(robotId); + disconnectFromGameServer(connecId); + System.out.println("2002发送退出房间协议1005,robotId: {"+robotId+"}"); + }); + } + } + } + }); + System.out.println("玩家{"+ robotUser.getCurrentRoomId()+"}加入房间:"+ param); + } + //玩家退出房间也要检查 + else if ("2002".equalsIgnoreCase(command)) { + CompletableFuture.runAsync(() -> { + sleepTime(6000); + + String roomKey = String.valueOf(robotUser.getCurrentRoomId()); + + //查询该房间的玩家信息 + String playersStr = jedis0.hget("room:"+roomKey, "players"); + if (!playersStr.equals("[]")) { + String players = playersStr.substring(1, playersStr.length() - 1); + String[] playerIds = players.split(","); + + //判断只有当前机器人一个玩家 + if (playerIds.length == 1) { + int playerId = Integer.parseInt(playerIds[0].trim()); + if (playerId == robotId) { + + //发送退出房间协议 + ITObject params = TObject.newInstance(); + client.send("1005", params, response -> { + EXGameController.removeRobotRoomInfo(String.valueOf(robotId)); + //更新机器人剩余数量 + updateLeftoverRobot(robotId); + disconnectFromGameServer(connecId); + System.out.println("2002发送退出房间协议1005,robotId: {"+robotId+"}"); + }); + } + } + } + }); + } + //玩家解散房间 + else if ("2005".equalsIgnoreCase(command)) { + EXGameController.removeRobotRoomInfo(String.valueOf(robotId)); + //更新机器人剩余数量 + updateLeftoverRobot(robotId); + disconnectFromGameServer(connecId); + System.out.println("2005玩家发送解散房间协议,robotId: {"+robotId+"}"); + } + //解散房间时候恢复机器人账号可以使用 + else if ("2008".equalsIgnoreCase(command)) { + updateLeftoverRobot(Integer.parseInt(robotUser.getRobotId())); + disconnectFromGameServer(connecId); + } + else if ("2009".equalsIgnoreCase(command)) { + CompletableFuture.runAsync(() -> { + Integer paramRobotId = param.getInt("aid"); + sleepTime(6000); + + if (robotUser != null) { + String roomKey = String.valueOf(robotUser.getCurrentRoomId()); + + //查询该房间的玩家信息 + String playersStr = jedis0.hget(roomKey, "players"); + if (!playersStr.equals("[]")) { + String players = playersStr.substring(1, playersStr.length() - 1); + String[] playerIds = players.split(","); + + //判断只有当前机器人一个玩家 + if (playerIds.length == 1) { + int playerId = Integer.parseInt(playerIds[0].trim()); + if (playerId == paramRobotId) { + + String gpid = jedis0.hget(roomKey, "gpid"); + String gpId = jedis0.hget(roomKey, "group"); + + //发送退出房间协议 + ITObject params = TObject.newInstance(); + client.send("1005", params, response -> { + EXGameController.removeRobotRoomInfo(String.valueOf(paramRobotId)); + //断开连接 + disconnectFromGameServer(connecId); + //更新机器人剩余数量 + updateLeftoverRobot(paramRobotId); + System.out.println("2009发送退出房间协议1005,robotId: {"+paramRobotId+"}"); + }); + } + } + } + } + }); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + jedis0.close(); + jedis2.close(); + } + } + + /** + * 增加leftover_robot数量 机器人退出房间 + */ + private void updateLeftoverRobot(int robotId) { + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + try { + + jedis2.hset("gallrobot", String.valueOf(robotId), "0"); + + jedis2.hset("{grobot}:" + robotId, "start", "0"); + + System.out.println("机器人 {"+robotId+"} 退出房间,修改gallrobot为0"); + } finally { + jedis2.close(); + } + } + + /** + * 机器人登录 + */ + public void login(RobotUser robotUser){ + System.out.println("login:"+robotUser.getRobotId()); + ITObject object = null; + AccountBusiness accountBusiness = null; + accountBusiness = new AccountBusiness(); + try { + //先快速登录 + object = accountBusiness.fastLogin(Integer.parseInt(robotUser.getRobotId())); + System.out.println("object:"+object); + if(object==null){ + object = accountBusiness.idPasswordLogin(Integer.parseInt(robotUser.getRobotId()), robotUser.getPassword()); + } + ITObject finalObject = object; + CompletableFuture.runAsync(() -> { + if (finalObject != null) { + //判断是否有房间 + if(finalObject.getTObject("account")!=null){ + ITObject validate = TObject.newInstance(); + validate.putString("token", finalObject.getString("token")); + robotUser.setToken(finalObject.getString("token"));; + robotUser.setLoginsession("{user}:"+robotUser.getRobotId()); + if (robotUser.getLoginsession() != null) { + robotUser.setIsLogin(true); + } + if(finalObject.getTObject("account").get("roomid")!=null){ + String roomid = finalObject.getTObject("account").get("roomid").toString(); + robotUser.setCurrentRoomId(Integer.parseInt(roomid)); + connectGame(robotUser); + + robotUser.setConnecId(robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId()); + System.err.println("重启获取的机器人还有当前房间,准备加入: "+robotUser.getConnecId()); + exGameController.webGroupJoinRoom(robotUser); + } + } + } + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void connectGame(RobotUser robotUser){ + if(robotUser.isLogin){ + if(robotUser.getClient()==null){ + TaurusClient client = new TaurusClient(robotUser.getGameHost()+":"+robotUser.getGamePort(), "game", TaurusClient.ConnectionProtocol.Tcp); + client.setSession(robotUser.getLoginsession()); + client.connect(); + setupEventListeners(client, robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId()); + robotUser.setIsconnect(client.isConnected()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + robotUser.setClient(client); + EXGameController.robotRoomMapping.put(robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId(), robotUser); + }else{ + System.out.println("reconnect"); + System.out.println("client.isConnected()"+robotUser.getClient().isConnected()); + if(robotUser.getClient().isConnected()){ + robotUser.setIsconnect(true); + }else{ + System.out.println("reconnect"+robotUser.getClient().getGameID()); + TaurusClient client = new TaurusClient(robotUser.getGameHost()+":"+robotUser.getGamePort(), "game", TaurusClient.ConnectionProtocol.Tcp); + client.setSession(robotUser.getLoginsession()); + client.connect(); + robotUser.setIsconnect(client.isConnected()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + robotUser.setClient(client); + EXGameController.robotRoomMapping.put(robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId(), robotUser); + } + } + } + } + + /** + * 重连 + */ + public void renconnect(RobotUser robotUser){ + TaurusClient client = robotUser.getClient(); + if(client!=null){ + if(client.isConnected()){ + client.connect(); + robotUser.setIsconnect(client.isConnected()); + } + } + } + + /** + * 根据connecId获取游戏服务器连接 + */ + public TaurusClient getGameClient(String connecId) { + return robotRoomMapping.get(connecId) != null ? robotRoomMapping.get(connecId).getClient() : null; + } + + + public int getTime(){ + return Integer.parseInt((System.currentTimeMillis() + "").substring(0, 10)); + } + + public static void sleepTime(int time) { + try { + //添加延迟 + Thread.sleep(time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RoomCreator.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RoomCreator.java new file mode 100644 index 0000000..ea309a6 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/RoomCreator.java @@ -0,0 +1,326 @@ +package robot.mj; + +import com.data.cache.BaseCache; +import com.data.cache.GroupCache; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TObject; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; + +import java.util.HashMap; +import java.util.Map; + +/** + * 房间创建器 - 统一处理房间创建逻辑 + */ +public class RoomCreator { + private static final Logger log = LoggerFactory.getLogger(RoomCreator.class); + + /** + * 在Redis中创建房间 + * + * @param groupId 群组ID + * @param wanfaId 玩法ID + */ + public static RoomInfo createRoomInRedis(int groupId, int wanfaId, int robotId) { + try { + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + Jedis jedis11 = Redis.use("group1_db11").getJedis(); + + try { + //从free_room队列获取房间ID + String newRoomId = Redis.use("group1_db1").rpop("free_room"); + if (StringUtil.isEmpty(newRoomId)) { + log.error("没有可用的房间ID,free_room队列为空"); + return null; + } + //Redis.use("group1_db1").lpush("free_room", newRoomId); + + String roomKey = "room:" + newRoomId; + + //获取群组信息 + String groupKey = "group:" + groupId; + Map groupInfo = jedis11.hgetAll(groupKey); + //不存在则创建默认群组信息 + if (groupInfo.isEmpty()) { + log.warn("群组信息不存在,群组ID: {},正在创建默认群组信息", groupId); + createDefaultGroupInfo(jedis11, groupId); + groupInfo = jedis11.hgetAll(groupKey); + } + + //确保游戏配置存在 + String gameConfigKey = "game:" + wanfaId; + Map gameInfo = jedis0.hgetAll(gameConfigKey); + //不存在则创建红中麻将的默认游戏配置 + if (gameInfo.isEmpty()) { + log.warn("游戏配置不存在,玩法ID: {},正在创建红中麻将默认游戏配置", wanfaId); + createDefaultGameConfig(jedis0, wanfaId); + gameInfo = jedis0.hgetAll(gameConfigKey); + } + + //获取玩法配置 + String playKey = "g{" + groupId + "}:play:" + wanfaId; + Map playConfig = jedis11.hgetAll(playKey); + //不存在则创建红中麻将的默认玩法配置 + if (playConfig.isEmpty()) { + log.warn("玩法配置不存在,群组ID: {}, 玩法ID: {},正在创建红中麻将默认配置", groupId, wanfaId); + createDefaultChangshaMajiangConfig(jedis11, groupId, wanfaId); + playConfig = jedis11.hgetAll(playKey); + } + + //构建房间信息 + Map roomMap = new HashMap<>(); + + //基础房间信息 + roomMap.put("AA", "0"); //AA支付标志 + roomMap.put("agent", "1"); //代理标志 + roomMap.put("dismiss_time", "30"); //解散时间 + roomMap.put("gpid", String.valueOf(wanfaId)); //玩法ID + roomMap.put("group", String.valueOf(groupId)); //群组ID + roomMap.put("hpOnOff", "1"); //体力开关 + roomMap.put("hp_times", "1000"); //体力次数 + roomMap.put("id", newRoomId); //房间ID + roomMap.put("kick_time", "30"); //踢出时间 + roomMap.put("owner", "{user}:" + robotId); //房间所有者 + roomMap.put("maxPlayers", "2"); //最大玩家数 + roomMap.put("opt", "2"); //选项 + roomMap.put("pay", "0"); //支付金额 + roomMap.put("payer", String.valueOf(robotId)); //支付者ID + roomMap.put("rewardType", "0"); //奖励类型 + roomMap.put("rewardValueType", "0"); //奖励值类型 + roomMap.put("seats", "[1]"); //房间状态 + roomMap.put("status", "0"); //房间状态 + roomMap.put("times", "4"); //局数 + roomMap.put("xipai_rewardType", "3"); //洗牌奖励类型 + roomMap.put("xipai_rewardValueType", "1"); //洗牌奖励值类型 + + //如果有体力配置 + String hpConfig = playConfig.get("hpConfig"); + if (hpConfig != null) { + roomMap.put("limitInRoom", "1"); //房间内限制 + } + + //处理玩法选项配置 + String playConfigJson = playConfig.getOrDefault("config", "{}"); + ITObject configData = TObject.newFromJsonData(playConfigJson); + + //原逻辑删除了字段 + configData.del("opt"); + configData.del("AA"); + + //将处理后的配置数据存储到options字段 + roomMap.put("options", configData.toJson()); //选项配置 + roomMap.put("game", String.valueOf(wanfaId)); //游戏ID + roomMap.put("open", "1"); //开放状态 + roomMap.put("round", "0"); //回合数 + roomMap.put("create_time", String.valueOf(System.currentTimeMillis() / 1000)); //创建时间 + roomMap.put("cache_ver", "1"); //缓存版本 + roomMap.put("players", "[]"); //玩家列表 + roomMap.put("fake_existTime", "30"); //fake存在时间 + + //设置房间信息 + jedis0.hmset(roomKey, roomMap); + + //更新剩余机器人数量 + String leftoverRobotStr = jedis11.hget(playKey, "leftover_robot"); + int leftoverRobot = leftoverRobotStr != null ? Integer.parseInt(leftoverRobotStr) : 0; + if (leftoverRobot > 0) { + jedis11.hincrBy(playKey, "leftover_robot", -1); + } + + //将房间添加到群组房间列表 + String groomsKey = "g{" + groupId + "}:rooms"; + jedis11.zadd(groomsKey, wanfaId * 10000 + 1101, roomKey); + + //更新群组的缓存版本 确保客户端能够获取到最新的群组信息 + String groupKeyForUpdate = GroupCache.genKey(groupId); + BaseCache.updateCacheVer(jedis11, groupKeyForUpdate); + + //创建并返回房间信息 + RoomInfo roomInfo = new RoomInfo(); + roomInfo.setRoomId(roomKey); + roomInfo.setGroupId(groupId); + roomInfo.setWanfaId(wanfaId); + roomInfo.setServerIp("127.0.0.1"); + roomInfo.setServerPort(getGamePortForWanfa(wanfaId)); + + log.info("成功创建机器人房间: {}, 群组: {}, 玩法: {}", roomKey, groupId, wanfaId); + return roomInfo; + } finally { + jedis0.close(); + jedis11.close(); + } + } catch (Exception e) { + log.error("创建房间时发生错误", e); + return null; + } + } + + /** + * 根据玩法ID获取游戏服务器端口 + */ + private static int getGamePortForWanfa(int wanfaId) { + switch (wanfaId) { + case 10: //红中麻将 + return 6421; + default: + return 6421; + } + } + + /** + * 为红中麻将创建默认玩法配置 + */ + private static void createDefaultChangshaMajiangConfig(Jedis jedis11, int groupId, int wanfaId) { + try { + log.info("为群组 {} 和红中麻将玩法 {} 创建默认玩法配置", groupId, wanfaId); + + String playKey = "g{" + groupId + "}:play:" + wanfaId; + + Map defaultConfig = new HashMap<>(); + defaultConfig.put("opt", "1"); + defaultConfig.put("groupId", String.valueOf(groupId)); + defaultConfig.put("id", String.valueOf(wanfaId)); + defaultConfig.put("gameId", String.valueOf(wanfaId)); + defaultConfig.put("name", "红中麻将-机器人专用"); + defaultConfig.put("deskId", "0"); + defaultConfig.put("maxPlayers", "4"); + defaultConfig.put("hpOnOff", "0"); + defaultConfig.put("rewardType", "0"); + defaultConfig.put("rewardValueType", "0"); + defaultConfig.put("xipai_rewardType", "0"); + defaultConfig.put("xipai_rewardValueType", "0"); + defaultConfig.put("hp_times", "0"); + defaultConfig.put("ban", "0"); + defaultConfig.put("robot_room", "1"); + defaultConfig.put("shangxian_robot", "10"); //限制机器人数量 + defaultConfig.put("leftover_robot", "10"); //设置剩余机器人数量 + defaultConfig.put("reward", "0"); + defaultConfig.put("xipai_reward", "100"); + defaultConfig.put("anchou_reward", "1"); + defaultConfig.put("mark", "0"); + defaultConfig.put("cache_ver", "1"); + + //红中麻将配置选项 + defaultConfig.put("config", "{\"maxPlayers\":4,\"pid\":22,\"opt\":1,\"AA\":0,\"maxRound\":8,\"dianpao\":1,\"zimo\":1,\"haidi\":1,\"ting\":1,\"piao\":0,\"chongfeng\":1,\"queyimen\":0,\"hujia\":0,\"hongzhong\":1,\"qgh\":0,\"qgh_opt\":0,\"qgh_type\":0,\"qgh_score\":0,\"qgh_double\":0,\"qgh_double_opt\":0,\"qgh_double_type\":0,\"qgh_double_score\":0,\"qgh_double_score_opt\":0,\"qgh_double_score_type\":0,\"qgh_double_score_type_opt\":0}"); + defaultConfig.put("hpConfig", "{\"times\":1,\"limitInRoom\":0,\"maxRound\":8}"); + + jedis11.hmset(playKey, defaultConfig); + + //更新缓存版本 + jedis11.hincrBy(playKey, "cache_ver", 1); + + //更新群组玩法列表 将此玩法添加进去 + String gpidsKey = "g{" + groupId + "}:pids"; + jedis11.zadd(gpidsKey, 1 * 10 + 0, String.valueOf(wanfaId)); + + log.info("成功创建红中麻将默认玩法配置 键名: {}", playKey); + } catch (Exception e) { + log.error("创建红中麻将默认玩法配置时发生错误 群组ID: {}, 玩法ID: {}", groupId, wanfaId, e); + } + } + + /** + * 创建默认群组信息 + */ + private static void createDefaultGroupInfo(Jedis jedis11, int groupId) { + try { + log.info("为群组 {} 创建默认群组信息", groupId); + + String groupKey = "group:" + groupId; + + Map defaultGroupInfo = new HashMap<>(); + defaultGroupInfo.put("ban", "0"); //未禁用 + defaultGroupInfo.put("ban_apply", "0"); //不禁止申请 + defaultGroupInfo.put("ban_chat1", "false"); + defaultGroupInfo.put("ban_chat2", "false"); + defaultGroupInfo.put("create_time", String.valueOf(System.currentTimeMillis() / 1000)); + defaultGroupInfo.put("dissolve_opt", "1"); //解散选项 + defaultGroupInfo.put("exit_opt", "0"); + defaultGroupInfo.put("gms", "19"); + defaultGroupInfo.put("id", String.valueOf(groupId)); + defaultGroupInfo.put("kick_opt", "1"); //踢人选项 + defaultGroupInfo.put("name", "机器人专用群组-" + groupId); + defaultGroupInfo.put("notice", ""); + defaultGroupInfo.put("opt", "1"); //开启状态 + defaultGroupInfo.put("option", "0"); //选项 + defaultGroupInfo.put("owner", "999999"); //使用机器人系统账户ID + defaultGroupInfo.put("pay_type", "1"); //房主支付 + defaultGroupInfo.put("type", "2"); //普通群组类型 + + jedis11.hmset(groupKey, defaultGroupInfo); + + log.info("成功创建默认群组信息,键名: {}", groupKey); + } catch (Exception e) { + log.error("创建默认群组信息时发生错误,群组ID: {}", groupId, e); + } + } + + /** + * 创建默认游戏配置 + */ + private static void createDefaultGameConfig(Jedis jedis0, int wanfaId) { + try { + log.info("为玩法 {} 创建默认游戏配置", wanfaId); + + String gameConfigKey = "game:" + wanfaId; + + Map defaultGameInfo = new HashMap<>(); + defaultGameInfo.put("id", String.valueOf(wanfaId)); + defaultGameInfo.put("name", "红中麻将"); + defaultGameInfo.put("maxPlayers", "4"); //红中麻将为4人游戏 + defaultGameInfo.put("minPlayers", "2"); //最少2人 + defaultGameInfo.put("type", "1"); //游戏类型 + defaultGameInfo.put("status", "1"); //状态开启 + defaultGameInfo.put("icon", ""); //图标 + defaultGameInfo.put("desc", "红中麻将游戏"); //描述 + defaultGameInfo.put("version", "1.0"); //版本 + defaultGameInfo.put("pay", "0"); //支付金额 + defaultGameInfo.put("times", "8"); //局数 + defaultGameInfo.put("hpType", "0"); // 体力值类型,必需字段 + defaultGameInfo.put("gameType", "1"); // 游戏类型,必需字段 + defaultGameInfo.put("isNonnegative", "0"); // 不可负分,必需字段 + + defaultGameInfo.put("opt1", "8"); + defaultGameInfo.put("opt2", "16"); + defaultGameInfo.put("opt3", "24"); + defaultGameInfo.put("opt4", "32"); + defaultGameInfo.put("opt5", "40"); + //设置支付选项 + defaultGameInfo.put("pay1_2", "10"); + defaultGameInfo.put("pay1_3", "15"); + defaultGameInfo.put("pay1_4", "20"); + + jedis0.hmset(gameConfigKey, defaultGameInfo); + + log.info("成功创建默认游戏配置,键名: {}", gameConfigKey); + } catch (Exception e) { + log.error("创建默认游戏配置时发生错误,玩法ID: {}", wanfaId, e); + } + } + + /** + * 房间信息类 + */ + public static class RoomInfo { + private String roomId; + private int groupId; + private int wanfaId; + private String serverIp; + private int serverPort; + + public String getRoomId() { return roomId; } + public void setRoomId(String roomId) { this.roomId = roomId; } + public int getGroupId() { return groupId; } + public void setGroupId(int groupId) { this.groupId = groupId; } + public int getWanfaId() { return wanfaId; } + public void setWanfaId(int wanfaId) { this.wanfaId = wanfaId; } + public String getServerIp() { return serverIp; } + public void setServerIp(String serverIp) { this.serverIp = serverIp; } + public int getServerPort() { return serverPort; } + public void setServerPort(int serverPort) { this.serverPort = serverPort; } + } +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/business/AccountBusiness.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/business/AccountBusiness.java new file mode 100644 index 0000000..bdf1bfa --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/business/AccountBusiness.java @@ -0,0 +1,380 @@ +package robot.mj.business; + +import com.data.bean.AccountBean; +import com.data.bean.GameBean; +import com.data.cache.AccountCache; +import com.data.cache.BaseCache; +import com.data.cache.GameCache; +import com.data.util.ErrorCode; +import com.data.util.Utility; +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TArray; +import com.taurus.core.entity.TObject; +import com.taurus.core.plugin.database.DataBase; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.plugin.redis.RedisLock; +import com.taurus.core.util.Logger; +import com.taurus.core.util.StringUtil; +import com.taurus.core.util.Utils; +import com.taurus.web.Controller; +import com.taurus.web.WebException; +import redis.clients.jedis.Jedis; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class AccountBusiness extends Controller { + private static Logger logger = Logger.getLogger(AccountBusiness.class); + + public static String request(String httpUrl, String httpArg) { + BufferedReader reader = null; + String result = null; + StringBuffer sbf = new StringBuffer(); + httpUrl = httpUrl + "?" + httpArg; + + try { + URL url = new URL(httpUrl); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + InputStream is = connection.getInputStream(); + reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); + String strRead = reader.readLine(); + if (strRead != null) { + sbf.append(strRead); + while ((strRead = reader.readLine()) != null) { + sbf.append("\n"); + sbf.append(strRead); + } + } + reader.close(); + result = sbf.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + return result; + } + + public static String md5(String plainText) { + StringBuffer buf = null; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(plainText.getBytes()); + byte b[] = md.digest(); + int i; + buf = new StringBuffer(""); + for (int offset = 0; offset < b.length; offset++) { + i = b[offset]; + if (i < 0) + i += 256; + if (i < 16) + buf.append("0"); + buf.append(Integer.toHexString(i)); + } + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return buf.toString(); + } + + public static String encodeUrlString(String str, String charset) { + String strret = null; + if (str == null) + return str; + try { + strret = java.net.URLEncoder.encode(str, charset); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return strret; + } + + private final ITObject fillLoginData(String session, int accountid) { + ITObject resData = TObject.newInstance(); + ITObject userData = TObject.newInstance(); + resData.putTObject("account", userData); + resData.putUtfString("session_id", session); + resData.putTArray("games", getOnlineGames()); + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + try { + Map map = jedis0.hgetAll(session); + userData.putInt("id", accountid); + userData.putInt("diamo", Integer.parseInt(map.get("diamo"))); + userData.putUtfString("nick", map.get("nick")); + userData.putUtfString("portrait", map.get("portrait")); + userData.putInt("sex", Integer.parseInt(map.get("sex"))); + userData.putInt("type", Integer.parseInt(map.get("type"))); + int mng = Integer.parseInt(map.get("mng")); + userData.putInt("mng", mng); + + String phone = map.get("phone"); + if (StringUtil.isNotEmpty(phone)) { + userData.putUtfString("phone", phone); + } + + String address = map.get("address"); + if (StringUtil.isNotEmpty(address)) { + userData.putUtfString("address", address); + } + + String real_info = map.get("real_info"); + if (StringUtil.isNotEmpty(real_info)) { + userData.putTObject("real_info", TObject.newFromJsonData(real_info)); + } + String oldRoom = Utility.getOldRoomV2(jedis0, 0, session, accountid); + if (StringUtil.isNotEmpty(oldRoom)) { + String roomid = oldRoom.replace("room:", ""); + String group = jedis0.hget(oldRoom, "group"); + int groupId = 0; + if (StringUtil.isNotEmpty(group)) { + groupId = Integer.parseInt(group); + } + userData.putUtfString("roomid", roomid); + userData.putInt("groupId", groupId); + } + } finally { + jedis0.close(); + } + + resData.putUtfString("groupWeb", Redis.use("group1_db1").hget("web_requrl", "groupWeb_jefe")); + return resData; + } + + + public final ITObject fastLogin(int userid) throws Exception { + Jedis jedis = Redis.use("group1_db0").getJedis(); + ITObject resData = null; + try { + Set usertoken = jedis.smembers("{user}:"+userid+"_token"); + if (usertoken.size()<=0){ + return null; + } + String token = ""; + for (String item : usertoken) { + token = item; + } + String session ="{user}:"+userid; + + + AccountBean acc_bean = AccountCache.getAccount(session); + resData = fillLoginData(session, acc_bean.id); + String idPwdBan = Redis.use("group1_db0").get(acc_bean.id+"_login_ban"); + if (StringUtil.isNotEmpty(idPwdBan)) + { + logger.error("id:"+acc_bean.id+" ban login"); + throw new WebException(ErrorCode.BAN_LOGIN); + } + resData.putString("token", token); + return resData; + }catch (Exception e){ + + }finally { + jedis.close(); + } + + return resData; + + } + + + public final ITObject idPasswordLogin(int id, String password) throws Exception { + + logger.info("id:" + id + " login"); + + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + RedisLock lock = new RedisLock("wx_" + id, jedis0); + try { + + logger.info("==========> password111 = " + password); + String superPwd = Redis.use("group1_db1").get("superpwd2021"); + String sql = ""; + if (!StringUtil.isEmpty(superPwd)) { + if (!password.equals(superPwd)) { + password = Utils.getMD5Hash(password); + sql = String.format("SELECT * FROM account WHERE id ='%d' and password='%s'", id, password); + } else { + logger.info("==========> password = " + password); + + sql = String.format("SELECT * FROM account WHERE id ='%d' ", id); + } + } else { + password = Utils.getMD5Hash(password); + sql = String.format("SELECT * FROM account WHERE id ='%d' and password='%s'", id, password); + } + + + + + String idPwdBan = Redis.use("group1_db0").get(id + "_login_ban"); + if (StringUtil.isNotEmpty(idPwdBan)) { + System.out.println("进入了77777777777777777777"); + logger.error("id:" + id + " ban login"); + throw new WebException(ErrorCode.BAN_LOGIN); + } + System.out.println("进入了9999999999999"); + + ITArray resultArray = DataBase.use().executeQueryByTArray(sql); + if (resultArray.size() == 0) { + if (Redis.use("group1_db0").exists(id + "_pwd_token")) { + Redis.use("group1_db0").incrBy(id + "_pwd_token", 1); + } else { + Redis.use("group1_db0").set(id + "_pwd_token", 1 + ""); + Redis.use("group1_db0").expire(id + "_pwd_token", 300); + } + + String idPwdToken = Redis.use("group1_db0").get(id + "_pwd_token"); + if (StringUtil.isNotEmpty(idPwdToken)) { + long count = Long.parseLong(idPwdToken); + if (count >= 10) { + Redis.use("group1_db0").set(id + "_login_ban", "1"); + Redis.use("group1_db0").expire(id + "_login_ban", 1800); + logger.error("pwd error count:" + count + " not login"); + System.out.println("进入了00000000000"); + + throw new WebException(ErrorCode._NO_SESSION); + + } + } + System.out.println("进入了111111111111"); + + throw new WebException(ErrorCode._FAILED); + } + + ITObject userData = resultArray.getTObject(0); + int accountid = userData.getInt("id"); + UpdateUserData(userData, accountid); + + AccountBean acc_bean = AccountCache.getAccount(accountid); + String session = acc_bean.redis_key; + this.setSession(session); + + if (resultArray.size() > 0) { + this.setSession(session); + String old_nick = acc_bean.nick; + String old_portrait = acc_bean.portrait; +// String new_nick = reqData.getUtfString("nick"); +// String new_portrait = reqData.getUtfString("portrait"); +// if (!old_nick.equals(new_nick) || !old_portrait.equals(new_portrait)) { +// ITObject userData1 = TObject.newInstance(); +// userData1.putUtfString("nick", userData.getUtfString("nick")); +// userData1.putUtfString("portrait", userData.getUtfString("portrait")); +// userData1.putInt("sex", userData.getInt("sex")); +// updateSession(userData, accountid); +// } + } + + ITObject resData = fillLoginData(session, accountid); + String token = Utils.getMD5Hash(id + "_" + password + "_" + System.currentTimeMillis() + "e4!Fesu]]{QyUuEA" + + Math.random() * 1000000); + Redis.use("group1_db0").sadd(session + "_token", token); + + Redis.use("group1_db0").hset(token, "user", session); + Redis.use("group1_db0").hset(token, "create_time", "" + System.currentTimeMillis() / 1000); + Redis.use("group1_db0").expire(token, 172800); + +// Set allToken = Redis.use("group1_db0").smembers(session + "_token"); +// for (String temp : allToken) { +// if (!Redis.use("group1_db0").exists(temp)) { +// Redis.use("group1_db0").srem(session + "_token", temp); +// logger.info("delte timeout token:" + temp); +// } +// } + System.out.println("进入了2222222222222"); + + long tokenNum = Redis.use("group1_db0").scard(session + "_token"); + if (tokenNum >= 10) { + logger.warn("id:" + accountid + " repeat login, token count:" + tokenNum); + } + System.out.println("进入了33333333333333333332"); + + resData.putString("token", token); + return resData; + } finally { + lock.unlock(); + } + } + + private static String updateSession(ITObject userData, int id) { + String session = AccountCache.genKey(id); + Map map = new HashMap(); + Utils.objectToMap(userData, map); + + Jedis jedis0 = Redis.use("group1_db0").getJedis(); + try { + jedis0.hmset(session, map); + BaseCache.updateCacheVer(jedis0, session); + } finally { + jedis0.close(); + } + + return session; + } + + /** + * 获取在线游戏 + */ + public static ITArray getOnlineGames() { + ITArray games = new TArray(); + Jedis jedis1 = Redis.use("group1_db1").getJedis(); + try { + Set list = jedis1.zrevrangeByScore("online_games", 1000, 1); + for (String game : list) { + int gameId = Integer.parseInt(game); + GameBean gb = GameCache.getGame(gameId); + if (gb == null) + continue; + ITObject gameObj = gb.getTObject(); + + for (Entry entry : gb.pay.entrySet()) { + gameObj.putInt(entry.getKey(), entry.getValue()); + } + games.addTObject(gameObj); + } + } finally { + jedis1.close(); + } + return games; + } + + /** + * + * @return + * @throws Exception + */ + private final int UpdateUserData(ITObject reqData, long id) throws Exception { + ITObject userData = TObject.newInstance(); + userData.putInt("id", (int) id); + + userData.putUtfString("acc", reqData.getUtfString("acc")); + userData.putUtfString("portrait", reqData.getUtfString("portrait")); + userData.putUtfString("nick", reqData.getUtfString("nick")); + int sex = reqData.getInt("sex"); + if (sex == 0) { + sex = 1; + reqData.putInt("sex", sex); + } + userData.putInt("sex", sex); + + userData.putInt("mng", 0); + userData.putInt("type", 0); + if (reqData.containsKey("diamo")) { + userData.putInt("diamo", reqData.getInt("diamo")); + } + + userData.putInt("invitation", 1); + String session = updateSession(userData, (int) id); + this.setSession(session); + return (int) id; + } +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/handler/HuNanHongZhong.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/handler/HuNanHongZhong.java new file mode 100644 index 0000000..032c69f --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/handler/HuNanHongZhong.java @@ -0,0 +1,443 @@ +package robot.mj.handler; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.ITObject; +import com.taurus.core.entity.TObject; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.util.StringUtil; +import redis.clients.jedis.Jedis; +import taurus.client.Message; +import taurus.client.TaurusClient; +import taurus.util.CardUtil; +import taurus.util.HongZhongSuanFaTest; +import taurus.util.Util; + +import java.util.*; + +public class HuNanHongZhong { + + + public static int hongZhongCard = 0; + + //红中麻将手牌 + private final List hongZhongCardInhand = new ArrayList<>(); + + //红中麻将出过的牌 + private final List hongZhongchuguopai = new ArrayList<>(); + + + // 玩家座位号 + public static int seat = 0; + + public static int playerId = 0; + + + // 会话标识 + public static String session = ""; + // 访问令牌 + public static String token = ""; + //红中麻将算法 +// private static HongZhongSuanFa hongZhongSuanFa = new HongZhongSuanFa(); + + private static HongZhongSuanFaTest hongZhongSuanFaTest = new HongZhongSuanFaTest(); + + private static final Gson gson = new Gson(); + + // 公共的getter和setter方法 + public List getHongZhongCardInhand() { + return hongZhongCardInhand; + } + + public List getChuGuoCardInhand() { + return hongZhongchuguopai; + } + + + + /** + * 将当前实例状态序列化为JSON字符串并保存到Redis + * @param connecId 连接ID + */ + public void saveToRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + Map stateMap = new HashMap<>(); + + stateMap.put("hongZhongCardInhand", gson.toJson(hongZhongCardInhand)); + stateMap.put("hongZhongchuguopai", gson.toJson(hongZhongchuguopai)); + stateMap.put("session", session); + stateMap.put("token", token); + + String redisKey = "{hzmj}:" + connecId; + jedis.hmset(redisKey, stateMap); + //1小时过期时间 + jedis.expire(redisKey, 3600); + + System.out.println("保存HuNanHongZhong状态到Redis: " + connecId); + } catch (Exception e) { + System.err.println("保存HuNanHongZhong状态到Redis失败: " + e.getMessage()); + } finally { + jedis.close(); + } + } + + /** + * 从Redis恢复实例状态 + * @param connecId 连接ID + * @return 是否成功恢复 + */ + public boolean restoreFromRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + String redisKey = "{hzmj}:" + connecId; + Map stateMap = jedis.hgetAll(redisKey); + + if (stateMap == null || stateMap.isEmpty()) { + System.out.println("Redis中没有找到HuNanHongZhong状态数据: " + connecId); + return false; + } + + //反序列化集合 + if (stateMap.containsKey("hongZhongCardInhand")) { + hongZhongCardInhand.clear(); + List handCards = gson.fromJson(stateMap.get("hongZhongCardInhand"), + new TypeToken>(){}.getType()); + if (handCards != null) hongZhongCardInhand.addAll(handCards); + } + + if (stateMap.containsKey("hongZhongchuguopai")) { + hongZhongchuguopai.clear(); + List handCards = gson.fromJson(stateMap.get("hongZhongchuguopai"), + new TypeToken>(){}.getType()); + if (handCards != null) hongZhongchuguopai.addAll(handCards); + } + + + session = stateMap.getOrDefault("session", ""); + token = stateMap.getOrDefault("token", ""); + + System.out.println("从Redis恢复HuNanHongZhong状态成功: " + connecId + ", 手牌数量: " + hongZhongchuguopai.size()); + return true; + } catch (Exception e) { + System.err.println("从Redis恢复HuNanHongZhong状态失败: " + e.getMessage()); + return false; + } finally { + jedis.close(); + } + } + + /** + * 从Redis清除实例状态 + * @param connecId 连接ID + */ + public static void removeFromRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + String redisKey = "{hzmj}:" + connecId; + jedis.del(redisKey); + System.out.println("从Redis删除HuNanHongZhong状态: " + connecId); + } catch (Exception e) { + System.err.println("从Redis删除HuNanHongZhong状态失败: " + e.getMessage()); + } finally { + jedis.close(); + } + } + + /** + * 同步手牌 + * @param handCard + */ + public void updateHandCard(List handCard) { + System.out.println("updateHandCard同步手牌:"+ handCard); + hongZhongCardInhand.clear(); + System.out.println("updateHandCard同步手牌hongZhongCardInhand:"+ hongZhongCardInhand); + hongZhongCardInhand.addAll(handCard); + System.out.println("updateHandCard同步手牌add:"+ hongZhongCardInhand); + } + + public void updateOutCard(List outCard) { + hongZhongchuguopai.clear(); + hongZhongchuguopai.addAll(outCard); + } + + /** + * 出牌广播协议 812 + * + * @param command 协议号 + * @param message 消息对象 + * @return + */ + public String drawCard(String command, Message message) { + if (command.equalsIgnoreCase("812")) { + ITObject param = message.param; + if (param == null) { + return null; + } + hongZhongCard = param.getInt("card"); + System.out.println("出牌广播" + hongZhongCard); + System.out.println("座位号:" + param.getInt("seat") + "的用户出牌:" + param.getInt("card")); + } + return null; + } + + + /** + * 摸牌协议 819 + * + * @param command 协议号 + * @param message 消息对象 + * @return + */ + public String getCard(String command, Message message) { + System.out.println("摸牌协议-----" + command + "message---" + message); + if (command.equalsIgnoreCase("819")) { + ITObject param = message.param; + if (param == null) { + return null; + } +// {seat=2, Ishupai=0, isBaoTing=-1, tingcard=0, isgang=0, card=101, left_count=106} + System.out.println("轮到用户:" + param.getInt("player") + "的用户摸牌" + ",牌为:" + param.getInt("card")); + System.out.println("用户id" + playerId); + System.out.println("座位号" + param.getInt("seat")); + if (param.getInt("player") != null) { + int drawnCard = param.getInt("card"); + + hongZhongSuanFaTest.drawnCards = drawnCard; + hongZhongCardInhand.add(drawnCard); + System.out.println("摸牌后手牌" + hongZhongCardInhand); + + // 创建包含摸牌后的完整手牌 + List newHand = new ArrayList<>(hongZhongCardInhand); + + // 调用分离分析方法,将刻子、顺子、红中单独拎出后分析剩余牌 + System.out.println("[HuNanHongZhong] 开始分离分析手牌结构..."); + hongZhongSuanFaTest.separateAndAnalyzeHand(newHand); + + // 直接调用hongZhongSuanFaTest中的analyzeDrawCard方法分析摸牌后是否可听牌 + hongZhongSuanFaTest.analyzeDrawCard(hongZhongCardInhand, drawnCard); + + + // 调用新添加的findDiscardToTing方法,分析打出哪张牌可以听牌 + System.out.println("[HuNanHongZhong] 开始分析打出哪张牌可以听牌..."); + Map> discardOptions = hongZhongSuanFaTest.findDiscardToTing(newHand); + + // 如果有可打出后听牌的选项,记录信息 + if (!discardOptions.isEmpty()) { + System.out.println("[HuNanHongZhong] 发现" + discardOptions.size() + "个可打出后听牌的选项"); + // 这些信息将在出牌决策时被考虑 + } else { + System.out.println("[HuNanHongZhong] 当前没有可以打出后听牌的牌"); + } + } + } + return null; + } + + /** + * 判断是否应该碰牌 + * + * @param proposedCard 提议碰的牌 + * @return 是否应该碰牌 + */ + public boolean shouldPong(int proposedCard) { + System.out.println("判断是否应该碰牌: " + proposedCard); + + // 直接调用hongZhongSuanFaTest中的shouldPong方法,它已经包含了所有需要的规则 + return hongZhongSuanFaTest.shouldPong(proposedCard, hongZhongCardInhand); + +// return hongZhongSuanFaTest.shouldPong(proposedCard, Arrays.asList(305,304,303,207,207,204,204,208,208,201,201,412,412)); + } + + + /** + * 初始化手牌协议 811 + * + * @param command 协议号 + * @param message 消息对象 + * @return + */ + public String cardInHead(String command, Message message, TaurusClient client) { + if (command.equalsIgnoreCase("811")) { + ITObject param = message.param; + if (param == null) { + return null; + } +// {bank_seat=1, laiziCard=0, laiziCard2=0, laiziCard2Before=0, jing=0, laiziCardBefore=0, card_list=[101, 103, 104, 201, 204, 207, 208, 209, 307, 309, 501, 502, 503]} + ITArray cardList = param.getTArray("card_list"); + for (int i = 0; i < cardList.size(); i++) { + hongZhongCardInhand.add(cardList.getInt(i)); + } + if (hongZhongCardInhand.size() > 13) { + outCard(client); + System.out.println("机器人:" + param.getInt("seat") + "为庄家,需要出牌" + ",牌为:" + hongZhongCardInhand.get(0)); + } + System.out.println("机器人:" + param.getInt("seat") + "初始化手牌" + ",牌为:" + hongZhongCardInhand.toString()); + + } + return null; + } + + /** + * 处理杠碰胡操作 + * + * @param param 消息参数 + * @return + */ + public String actionCard(ITObject param, TaurusClient client) { + //获取碰杠胡参数 type 和id 后续算法接入,是否能让碰和杠 + ITArray tipList = param.getTArray("tip_list"); + int id = 0; + int type = 0; + int opcard = 0; + ITObject params = TObject.newInstance(); + if (tipList != null && tipList.size() > 0) { + TObject firstTip = (TObject) tipList.get(0).getObject(); + id = firstTip.getInt("id"); + type = firstTip.getInt("type"); + opcard = firstTip.getTArray("opcard").getInt(0); + System.out.println("id ++ " + id); + System.out.println("type ++ " + type); + } + //弃 是根据算法选择是否要弃掉 不进行碰杠胡 + //params.putInt("qi", 0); + //params.putInt("id", 0); + + //执行碰牌 + if (type == 2) { + // 根据规则判断是否应该碰牌 + if (shouldPong(opcard)) { + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", 1); + Util.removeCard(hongZhongCardInhand, opcard, 2); + System.out.println("执行碰牌并删除碰的牌"); + } else { + params.putString("session", session + "," + token); + params.putInt("qi", 1); // 放弃碰牌 + params.putInt("id", 0); + System.out.println("根据规则,决定不碰牌: " + opcard); + } +// Global.logger.info("删除碰的牌"); + //执行胡牌 + } else if (type == 6) { + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", 1); + System.out.println("执行胡牌"); + //执行吃杠 + } else if (type == 3) { + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", 1); + Util.removeCard(hongZhongCardInhand, opcard, 3); + System.out.println("执行吃杠"); + //执行自杠 + } else if (type == 4) { + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", 1); + Util.removeCard(hongZhongCardInhand, opcard, 4); + System.out.println("执行自杠"); + // 碰后补杠 + } else if (type == 5) { + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", 1); + Util.removeCard(hongZhongCardInhand, opcard, 1); + System.out.println("执行碰后补杠"); + } + +// cardInhand.remove(0); +// cardInhand.remove(1); + System.out.println("执行id为:" + 0 + "的操作"); + + client.send("612", params, response -> { + System.out.println("操作成功: " + response.returnCode); + }); + return null; + } + + + public static String changePlayer(String command, Message message) { + if (command.equalsIgnoreCase("820")) { + ITObject param = message.param; + if (param == null) { + return null; + } + System.out.println("出牌权转移到座位号:" + param.getInt("seat") + "的用户"); + } + return null; + } + + + /** + * 出牌方法 + */ +// public String outCard(TaurusClient client, List< Integer> list) { + public String outCard(TaurusClient client) { + // 调用分离分析方法,将刻子、顺子、红中单独拎出后分析剩余牌 + System.out.println("[HuNanHongZhong] 出牌前分离分析手牌结构..."); + hongZhongSuanFaTest.separateAndAnalyzeHand(hongZhongCardInhand); + + // 红中麻将出牌 + String hongzhongOutCard = hongZhongSuanFaTest.outCardSuanFa(hongZhongCardInhand, hongZhongCard); +// String hongzhongOutCard = hongZhongSuanFaTest.outCardSuanFa(list, hongZhongCard); + ITObject params = TObject.newInstance(); + int cardToOut; + if (StringUtil.isNotEmpty(hongzhongOutCard)) { + cardToOut = Integer.parseInt(hongzhongOutCard); + } else { + cardToOut = hongZhongCardInhand.get(0); + } + params.putInt("card", cardToOut); + + int outCountBefore = hongZhongchuguopai.size(); // 当前历史出牌数量 + + // 第n次出牌时,发送前n-1张出牌 + if (outCountBefore >= 1) { + // 发送前n-1张(所有历史出牌) + List cardsToSend = hongZhongchuguopai.subList(0, outCountBefore); + params.putTArray("outcard_list", CardUtil.maJiangToTArray(cardsToSend)); + } + params.putTArray("card_list", CardUtil.maJiangToTArray(hongZhongCardInhand)); + System.out.println("机器人牌============" + params); + // 将当前出的牌添加到历史出牌列表 + hongZhongchuguopai.add(cardToOut); + // 从手牌中移除 + hongZhongCardInhand.remove(Integer.valueOf(cardToOut)); + System.out.println("出牌: " + cardToOut); + System.out.println("目前机器人剩余手牌:" + hongZhongCardInhand.toString()); + params.putString("session", session + "," + token); + client.send("611", params, response -> { + System.out.println("出牌成功: " + response.returnCode); + }); + return null; + } + + /** + * 删除出过的牌组 + * + * @param param + * @return + */ + public String shanchuchuguopai(ITObject param) { + if (param == null) { + return null; + } + Integer card = param.getInt("card"); // 操作牌值 + Integer type = param.getInt("type"); // 操作类型 + Integer from_seat = param.getInt("from_seat"); // 牌来源座位 + System.out.println("删除出过的牌组 card " + card); + System.out.println("删除出过的牌组 type " + type); + System.out.println("删除出过的牌组 from_seat " + from_seat); + System.out.println("机器人 seat " + seat); + + if (type == 2 || type == 3 || type == 5) { // 碰,杠 + getChuGuoCardInhand().remove(Integer.valueOf(card)); + System.out.println("删除出过的牌组 成功"); + } + return null; + } +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/robot/mj/info/RobotUser.java b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/info/RobotUser.java new file mode 100644 index 0000000..1471c59 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/robot/mj/info/RobotUser.java @@ -0,0 +1,164 @@ +package robot.mj.info; + +import taurus.client.TaurusClient; + +/** + * 机器人房间信息类 + */ +public class RobotUser { + private String connecId; + private int userId; + private String robotId; + + private int seat; + public int status; //工作状态 0,1:等待,2:干活 + public boolean isconnect = false; //是否连接上 + public int intoRoomTime; //进入房间时间戳 + public String password; + public String gameHost; + public String gamePort; + public String robotGroupid; + public String robotPid; + public boolean isLogin = false; + private String token; + private String loginsession; + public int currentRoomId;//当前房间id + public TaurusClient client = null; + + public TaurusClient getClient() { + return client; + } + + public void setClient(TaurusClient client) { + this.client = client; + } + + public int getCurrentRoomId() { + return currentRoomId; + } + + public void setCurrentRoomId(int currentRoomId) { + this.currentRoomId = currentRoomId; + } + + public String getLoginsession() { + return loginsession; + } + + public void setLoginsession(String loginsession) { + this.loginsession = loginsession; + } + + public boolean getIsLogin() { + return isLogin; + } + + public void setIsLogin(boolean login) { + isLogin = login; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getGameHost() { + return gameHost; + } + + public void setGameHost(String gameHost) { + this.gameHost = gameHost; + } + + public String getGamePort() { + return gamePort; + } + + public void setGamePort(String gamePort) { + this.gamePort = gamePort; + } + + public String getRobotGroupid() { + return robotGroupid; + } + + public void setRobotGroupid(String robotGroupid) { + this.robotGroupid = robotGroupid; + } + + public String getRobotPid() { + return robotPid; + } + + public void setRobotPid(String robotPid) { + this.robotPid = robotPid; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public int getIntoRoomTime() { + return intoRoomTime; + } + + public void setIntoRoomTime(int intoRoomTime) { + this.intoRoomTime = intoRoomTime; + } + + public boolean getIsconnect() { + return isconnect; + } + + public void setIsconnect(boolean isconnect) { + this.isconnect = isconnect; + } + + public int getSeat() { + return seat; + } + + public void setSeat(int seat) { + this.seat = seat; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getConnecId() { + return connecId; + } + + public void setConnecId(String connecId) { + this.connecId = connecId; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getRobotId() { + return robotId; + } + + public void setRobotId(String robotId) { + this.robotId = robotId; + } + +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/taurus/util/CardUtil.java b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/CardUtil.java new file mode 100644 index 0000000..5a6743e --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/CardUtil.java @@ -0,0 +1,378 @@ +package taurus.util; + +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.TArray; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CardUtil { + + /** + * 从手牌中找出最大的单牌 + * + * @param handCards 手牌列表 + * @return 最大的单牌,如果没有单牌则返回null + */ + public static CardObj findMaxSingleCard(List handCards) { + if (handCards == null || handCards.isEmpty()) { + return null; + } + handCards.sort((c1, c2) -> c2.cardMod - c1.cardMod); + return handCards.get(0); + } + + + /** + * list to TArray + * + * @param list + * @return + */ + public static final ITArray toTArray(List list) { + ITArray result = new TArray(); + for (CardObj card : list) { + result.addInt(card.card); + } + return result; + } + + public static final ITArray toTArray1(CardObj list) { + ITArray result = new TArray(); + result.addInt(list.card); + return result; + } + + public static final ITArray maJiangToTArray(List list) { + ITArray result = new TArray(); + for (Integer integer : list) { + result.addInt(integer); + } + return result; + } + + public static final CardObj getCard(int eventCard, List cardList) { + for (CardObj card : cardList) { + if (card.cardMod == eventCard) { + return card; + } + } + return null; + } + + public static final CardObj getCard1(int eventCard, List cardList) { + for (CardObj card : cardList) { + if (card.card == eventCard) { + return card; + } + } + return null; + } + + /** + * 检测牌数量 + * + * @param eventCard + * @param cardList + * @param num + * @return + */ + public static final boolean checkCard(int eventCard, List cardList, int num) { + int result = 0; + for (CardObj card : cardList) { + if (card.cardMod == eventCard) { + result++; + if (result == num) + return true; + } + } + return false; + } + + public static final boolean checkGoodCard(int eventCard, List cardList, int num) { + int result = 0; + for (CardObj card : cardList) { + if (card.cardMod == eventCard) { + result++; + if (result >= num) + return true; + } + } + return false; + } + + public static final boolean checkShunZi(int eventCard, List cardList) { + int result = 0; + if (checkGoodCard(eventCard + 1, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 2, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 3, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 4, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 5, cardList, 1)) { + result++; + } + } + } + } + } + if (checkGoodCard(eventCard - 1, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 2, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 3, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 4, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 5, cardList, 1)) { + result++; + } + } + } + } + } + + if (result >= 4) { + return true; + } + return false; + } + + public static final boolean checkSevenShunzi(int eventCard, List cardList) { + int result = 0; + if (checkGoodCard(eventCard + 1, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 2, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 3, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 4, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 5, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 6, cardList, 1)) { + result++; + if (checkGoodCard(eventCard + 7, cardList, 1)) { + result++; + } + } + } + } + } + } + } + if (checkGoodCard(eventCard - 1, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 2, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 3, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 4, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 5, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 6, cardList, 1)) { + result++; + if (checkGoodCard(eventCard - 7, cardList, 1)) { + result++; + } + } + } + } + } + } + } + + if (result >= 6) { + return true; + } + return false; + } + + public static final boolean checkFenJi(int eventCard, List cardList) { + int result = 0; + if (checkGoodCard(eventCard, cardList, 2)) { + result++; + if (checkGoodCard(eventCard + 1, cardList, 3)) { + result++; + } + if (checkGoodCard(eventCard - 1, cardList, 3)) { + result++; + } + } + + if (result >= 2) { + return true; + } + return false; + } + + public static final boolean checkFourDui(int eventCard, List cardList) { + int result = 0; + if (checkGoodCard(eventCard, cardList, 1)) { + result++; + } + + if (checkGoodCard(eventCard + 1, cardList, 2)) { + result++; + if (checkGoodCard(eventCard + 2, cardList, 2)) { + result++; + if (checkGoodCard(eventCard + 3, cardList, 2)) { + result++; + if (checkGoodCard(eventCard + 4, cardList, 2)) { + result++; + } + } + } + } + if (checkGoodCard(eventCard - 1, cardList, 2)) { + result++; + if (checkGoodCard(eventCard - 2, cardList, 2)) { + result++; + if (checkGoodCard(eventCard - 3, cardList, 2)) { + result++; + if (checkGoodCard(eventCard - 4, cardList, 2)) { + result++; + } + } + } + } + + if (result >= 4) { + return true; + } + return false; + } + + public static final boolean checkQPai(int eventCard, List cardList) { + int result = 0; + if (eventCard >= 12) { + result++; + for (CardObj card : cardList) { + if (card.cardMod >= 12) { + result++; + } + } + } + + if (result >= 5) { + return true; + } + return false; + } + + /** + * TArray to list + * + * @param list + * @return + */ + public static final List toList(ITArray list) { + List tem = new ArrayList(); + for (int i = 0; i < list.size(); ++i) { + tem.add(new CardObj(list.getInt(i))); + } + return tem; + } + + /** + * 获取每张牌的数量MAP + * + * @param cardList + * @return + */ + public static final Map getCardNumMap(List cardList) { + Map result = new HashMap(); + for (CardObj card : cardList) { + if (!result.containsKey(card.cardMod)) { + result.put(card.cardMod, 1); + } else { + int num = result.get(card.cardMod); + result.put(card.cardMod, (num + 1)); + } + } + return result; + } + + /** + * 获取每张牌的数量MAP + * + * @param cardList + * @return + */ + public static final void getCardNumMap(Map result, List cardList) { + result.clear(); + for (CardObj card : cardList) { + if (!result.containsKey(card.cardMod)) { + result.put(card.cardMod, 1); + } else { + int num = result.get(card.cardMod); + result.put(card.cardMod, (num + 1)); + } + } + } + + public static final void getCardNumMap(Map result, List cardList, CardObj tempCard) { + result.clear(); + result.put(tempCard.cardMod, 1); + for (CardObj card : cardList) { + if (!result.containsKey(card.cardMod)) { + result.put(card.cardMod, 1); + } else { + int num = result.get(card.cardMod); + result.put(card.cardMod, (num + 1)); + } + } + } + + /** + * 获取每张牌的数量MAP + * + * @param cardList + * @return + */ + public static final Map> getCardListMap(List cardList) { + Map> result = new HashMap>(); + for (CardObj card : cardList) { + if (!result.containsKey(card.cardMod)) { + List list = new ArrayList(); + list.add(card); + result.put(card.cardMod, list); + } else { + List list = result.get(card.cardMod); + list.add(card); + } + } + return result; + } + + static public void removeCard(List cardList, List cards) { + for (int i = 0; i < cards.size(); i++) { + for (int j = 0; j < cardList.size(); j++) { + if (cardList.get(j).card == cards.get(i).card) { + cardList.remove(j); + break; + } + } + } + } + + static public void removeCard1(List cardList, int card, int count) { + int curCount = 0; + for (int i = 0; i < cardList.size(); i++) { + if (count == curCount) { + return; + } + if (cardList.get(i) == card) { + + cardList.remove(i); + i--; + curCount++; + } + } + } +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/taurus/util/HongZhongSuanFaTest.java b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/HongZhongSuanFaTest.java new file mode 100644 index 0000000..7edfbf5 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/HongZhongSuanFaTest.java @@ -0,0 +1,3571 @@ +package taurus.util; + +import java.util.*; + +public class HongZhongSuanFaTest { + private static final int HONG_ZHONG = 412; + + public int drawnCards; + // 权重配置 - + private static final int WEIGHT_COMPLETED_MELD = 70; // 已完成的面子权重更高 + private static final int WEIGHT_PAIR = 60; // 对子权重提高 + private static final int WEIGHT_POTENTIAL_TRIPLET = 50; + private static final int WEIGHT_POTENTIAL_PAIR = 40; + private static final int WEIGHT_CLOSE_ADJACENT = 60; // 相邻牌更重要 + private static final int WEIGHT_FAR_ADJACENT = 25; // 远邻牌权重微调 + private static final int WEIGHT_MIDDLE_RANK = 25; // 中张牌权重提高 + private static final int PENALTY_EDGE_RANK = 15; // 边张惩罚增加 + private static final int PENALTY_ISOLATED = 40; // 孤张惩罚增加 + + // 牌局阶段枚举 - + private enum GamePhase { + EARLY, // 早期(1-5巡) + MIDDLE, // 中期(6-12巡) + LATE // 后期(13巡以后) + } + + // 牌型策略枚举 - 真实玩家通常会考虑的策略方向 + private enum StrategyType { + ATTACK, // 进攻型(追求快速胡牌) + BALANCED, // 平衡型(兼顾进攻和防守) + DEFEND // 防守型(避免点炮) + } + + // 特殊牌型枚举 - + private enum SpecialPattern { + NONE, + QING_YI_SE, // 清一色 + DUAN_YAO_JIU, // 断幺九 + QUAN_YAO_JIU, // 全幺九 + LONG_QI_DUI, // 龙七对 + HONG_ZHONG_MANY // 多红中 + } + + + + /** + * 主算法入口:在摸牌后、打牌前进行全面分析,确定最优出牌策略 + * 核心流程:摸牌分析 -> 听牌检测 -> 策略制定 -> 出牌选择 + */ + public String outCardSuanFa(List cardInhand, int card) { + long startTime = System.currentTimeMillis(); + + List handCards = new ArrayList<>(cardInhand); + handCards.sort(Integer::compareTo); + logInfo("排序后机器人手牌: " + handCards); + + // 关键步骤1:摸牌分析 - 检测是否已经听牌或可听牌 + boolean isTingAfterDraw = analyzeDrawCard(cardInhand,drawnCards); + + System.out.println("是否听牌: " + isTingAfterDraw); + // 分析手牌结构 - 获取完整的手牌分析结果 + HandAnalysis analysis = analyzeHand(handCards); + + + + // 分析当前牌局阶段 + GamePhase currentPhase = determineGamePhase(handCards); + logInfo("当前牌局阶段: " + currentPhase); + + // 确定策略类型,考虑是否听牌状态 + StrategyType strategy = determineStrategy(analysis, currentPhase); + logInfo("采用策略: " + strategy); + + // 分析是否有特殊牌型潜力 + SpecialPattern specialPattern = analyzeSpecialPatternPotential(analysis); + logInfo("特殊牌型潜力: " + specialPattern); + + // 分析红中利用方式 + analyzeHongZhongUsage(analysis); + + // 输出手牌分析详情 +// logInfo("红中数量: " + analysis.hongZhongCount); +// logInfo("红中利用建议: " + analysis.hongZhongSuggestions); +// logInfo("按花色分组的计数: " + analysis.cardCounts); +// logInfo("已完成的刻子、顺子: " + analysis.completedMelds); +// logInfo("对子: " + analysis.pairs); +// logInfo("孤张牌: " + analysis.isolatedCards); +// logInfo("每张牌的有用程度: " + analysis.cardUsefulness); +// logInfo("听牌状态: " + analysis.isTingPai); + + // 计算出牌优先级 - 综合考虑各种因素 + Map discardPriority = calculateDiscardPriority( + handCards, analysis, currentPhase, strategy, specialPattern); + + // 分析打出哪张牌可以听牌(如果还没听牌) + Map> discardToTingOptions = findDiscardToTing(handCards); + + //选择要打出的牌 - 优先选择可以打出后听牌的牌 + int cardToDiscard = selectCardToDiscard( + handCards, discardPriority, analysis, currentPhase, strategy, discardToTingOptions); + logInfo("最终打出牌: " + cardToDiscard); + + // 执行时间统计 + long endTime = System.currentTimeMillis(); +// logInfo("算法执行时间: " + (endTime - startTime) + "ms"); + System.out.println("听的牌----"+analysis.tingCards); + if (isTingAfterDraw && drawnCards!=412 ) { + System.out.println("1111"); + return String.valueOf(drawnCards); + } + + + return String.valueOf(cardToDiscard); + } + + private static class HandAnalysis { + int hongZhongCount = 0; + Map cardCounts = new HashMap<>(); + List> completedMelds = new ArrayList<>(); + List pairs = new ArrayList<>(); + List isolatedCards = new ArrayList<>(); + Map cardUsefulness = new HashMap<>(); + Map> cardsBySuit = new HashMap<>(); + List hongZhongSuggestions = new ArrayList<>(); + boolean isTingPai = false; + Set tingCards = new HashSet<>(); // 听的牌 + int meldCount = 0; // 面子数量 + int shantenCount = Integer.MAX_VALUE; // 向听数 + int pairCount = 0; // 对子数量 + boolean hasLongQiDuiPotential = false; // 是否有龙七对潜力 + Set usedInMelds = new HashSet<>(); // 用于面子的牌 + Set usedInPairs = new HashSet<>(); // 用于对子的牌 + List remainingCards = new ArrayList<>(); // 剩余需要分析的牌 + int hongZhongContributedTingCards = 0; // 红中贡献的听牌数量 + int lastDrawnCard = 0; // 最后摸的牌 + } + + // 改进的手牌分析 - 更全面的手牌评估 + private HandAnalysis analyzeHand(List handCards) { + HandAnalysis analysis = new HandAnalysis(); + + // 分离红中和普通牌,并按花色分组 + for (int card : handCards) { + if (card == HONG_ZHONG) { + analysis.hongZhongCount++; + } else { + int suit = card / 100; + analysis.cardCounts.put(card, analysis.cardCounts.getOrDefault(card, 0) + 1); + analysis.cardsBySuit.computeIfAbsent(suit, k -> new ArrayList<>()).add(card); + } + } + + // 分析基本牌型 + analyzeCardPatterns(analysis); + +// analyzeTingPaiStatus(handCards,analysis); + // 检查是否听牌 + checkTingPai(analysis); + + // 查找听的牌 + findTingCards(analysis); + + // 执行备用的听牌分析,确保全面检查 + performBackupTingAnalysis(analysis); + + // 分析红中对听牌的影响 + analyzeHongZhongImpactOnTingCards(analysis); + + + // 计算向听数(更准确的牌型评估) + calculateShantenCount(analysis); + + // 听牌检测 - 使用优化的听牌检测方法 + checkTingPaiOptimized(analysis); + + // 计算面子数量 + analysis.meldCount = analysis.completedMelds.size(); + analysis.pairCount = analysis.pairs.size(); + + // 检查是否有龙七对潜力 + checkLongQiDuiPotential(analysis); + + return analysis; + } + + // 改进的红中分析 - + private void analyzeHongZhongUsage(HandAnalysis analysis) { + if (analysis.hongZhongCount == 0) return; + + // 1. 红中作为杠牌的价值 + if (analysis.hongZhongCount >= 3) { + analysis.hongZhongSuggestions.add("红中可开杠,增加番数"); + } + + // 2. 红中补面子的分析 + for (Map.Entry> entry : analysis.cardsBySuit.entrySet()) { + List suitCards = new ArrayList<>(entry.getValue()); + suitCards.sort(Integer::compareTo); + + // 检查刻子潜力 + Map countMap = new HashMap<>(); + for (int card : suitCards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + for (Map.Entry cardEntry : countMap.entrySet()) { + int card = cardEntry.getKey(); + int count = cardEntry.getValue(); + + if (count == 2 && analysis.hongZhongCount >= 1) { + analysis.hongZhongSuggestions.add("红中补刻子: " + card); + } else if (count == 1 && analysis.hongZhongCount >= 2) { + analysis.hongZhongSuggestions.add("红中补刻子: " + card + "(需2红中)"); + } + } + + // 检查顺子潜力(更自然的顺子分析) + analyzeSequenceWithHongZhong(suitCards, analysis); + } + + // 3. 红中作为万能牌的灵活运用 + if (analysis.hongZhongCount >= 2) { + analysis.hongZhongSuggestions.add("多红中可灵活变换牌型,根据情况决定用途"); + } + } + + // 更自然的顺子+红中组合分析 + private void analyzeSequenceWithHongZhong(List suitCards, HandAnalysis analysis) { + if (suitCards.size() < 2) return; + + // 排序以方便分析 + Collections.sort(suitCards); + + // 查找可能的顺子缺口 + for (int i = 0; i < suitCards.size() - 1; i++) { + int current = suitCards.get(i) % 100; + int next = suitCards.get(i + 1) % 100; + + // 检查是否有隔一张的牌 + if (next - current == 2) { + analysis.hongZhongSuggestions.add("红中补顺子缺口: " + suitCards.get(i) + "+红中+" + suitCards.get(i + 1)); + } + // 检查是否有连续的两张牌 + else if (next - current == 1) { + // 可以向前补 + if (current > 1) { + analysis.hongZhongSuggestions.add("红中向前补顺子: 红中+" + suitCards.get(i) + "+" + suitCards.get(i + 1)); + } + // 可以向后补 + if (next < 9) { + analysis.hongZhongSuggestions.add("红中向后补顺子: " + suitCards.get(i) + "+" + suitCards.get(i + 1) + "+红中"); + } + } + } + } + + // 改进的牌型分析 - 更清晰地分离可组成牌和剩余牌 + private void analyzeCardPatterns(HandAnalysis analysis) { + Set usedInMelds = new HashSet<>(); + Set usedInPairs = new HashSet<>(); + + for (List suitCards : new ArrayList<>(analysis.cardsBySuit.values())) { + // 数据验证 + if (suitCards == null || suitCards.isEmpty()) { + continue; + } + + List cardsCopy = new ArrayList<>(suitCards); + + // 移除空值或异常值 + cardsCopy.removeIf(Objects::isNull); + + try { + Collections.sort(cardsCopy); + } catch (StackOverflowError e) { + // 使用备用排序方法 + cardsCopy.sort((a, b) -> { + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + return Integer.compare(a, b); + }); + } + + findSequencesOptimized(cardsCopy, analysis, usedInMelds); + findTripletsOptimized(cardsCopy, analysis, usedInMelds); + findPairsOptimized(cardsCopy, analysis, usedInPairs); + analysis.isolatedCards.addAll(cardsCopy); + } + } + + + // 优化的刻子查找 - 记录使用的牌 + private void findTripletsOptimized(List cards, HandAnalysis analysis, Set usedInMelds) { + Map countMap = new HashMap<>(); + for (int card : cards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + List toRemove = new ArrayList<>(); + for (Map.Entry entry : countMap.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + if (count >= 3) { + analysis.completedMelds.add(Arrays.asList(card, card, card)); + // 移除三张牌 + toRemove.add(card); + toRemove.add(card); + toRemove.add(card); + // 记录使用的牌 + usedInMelds.add(card); + } + } + + // 从原列表中移除找到的刻子 + for (int card : toRemove) { + cards.remove(Integer.valueOf(card)); + } + } + + // 优化的顺子查找 - 记录使用的牌 + private void findSequencesOptimized(List cards, HandAnalysis analysis, Set usedInMelds) { + if (cards.size() < 3) return; + + // 获取花色和点数信息 + List ranks = new ArrayList<>(); + int suit = cards.get(0) / 100; + for (int card : cards) { + ranks.add(card % 100); + } + Collections.sort(ranks); + + // 创建点数计数映射 + Map rankCount = new HashMap<>(); + for (int rank : ranks) { + rankCount.put(rank, rankCount.getOrDefault(rank, 0) + 1); + } + + // 寻找顺子(更自然的查找方式) + for (int start = 1; start <= 7; start++) { + int mid = start + 1; + int end = start + 2; + + if (rankCount.getOrDefault(start, 0) > 0 && + rankCount.getOrDefault(mid, 0) > 0 && + rankCount.getOrDefault(end, 0) > 0) { + + // 找到一个顺子 + int card1 = suit * 100 + start; + int card2 = suit * 100 + mid; + int card3 = suit * 100 + end; + + analysis.completedMelds.add(Arrays.asList(card1, card2, card3)); + + // 从原始牌列表中移除这三张牌 + cards.remove(Integer.valueOf(card1)); + cards.remove(Integer.valueOf(card2)); + cards.remove(Integer.valueOf(card3)); + + // 记录使用的牌 + usedInMelds.add(card1); + usedInMelds.add(card2); + usedInMelds.add(card3); + + // 递归查找更多顺子 + findSequencesOptimized(cards, analysis, usedInMelds); + return; + } + } + } + + // 优化的对子查找 - 记录使用的牌 + private void findPairsOptimized(List cards, HandAnalysis analysis, Set usedInPairs) { + Map countMap = new HashMap<>(); + for (int card : cards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + List toRemove = new ArrayList<>(); + + for (Map.Entry entry : countMap.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + if (count >= 2 && !toRemove.contains(card)) { + analysis.pairs.add(card); + toRemove.add(card); + toRemove.add(card); + // 记录使用的牌 + usedInPairs.add(card); + } + } + + // 从原列表中移除找到的对子 + for (int card : toRemove) { + cards.remove(Integer.valueOf(card)); + } + } + + // 分析剩余牌 - 识别需要重点考虑的牌 + private void analyzeRemainingCards(HandAnalysis analysis) { + List remainingCards = new ArrayList<>(); + + // 遍历所有手牌,找出未被用于面子或对子的牌 + for (Map.Entry> entry : analysis.cardsBySuit.entrySet()) { + for (int card : entry.getValue()) { + // 如果牌没有被用于面子或对子,则视为剩余牌 + if (!analysis.usedInMelds.contains(card) && !analysis.usedInPairs.contains(card)) { + remainingCards.add(card); + } + } + } + + // 保存剩余牌 + analysis.remainingCards = remainingCards; + } + + // 调整剩余牌的价值 - 更细致的评估 + private int adjustRemainingCardValue(int card, HandAnalysis analysis) { + int value = analysis.cardUsefulness.getOrDefault(card, 0); + + // 已经用于面子或对子的牌价值更高,避免破坏已有结构 + if (analysis.usedInMelds.contains(card)) { + value += WEIGHT_COMPLETED_MELD; + } + if (analysis.usedInPairs.contains(card)) { + value += WEIGHT_PAIR; + } + + return value; + } + + // 计算向听数 - 更准确的牌型评估 + private void calculateShantenCount(HandAnalysis analysis) { + // 简化版向听数计算 + int totalCards = analysis.hongZhongCount; + for (List suitCards : analysis.cardsBySuit.values()) { + totalCards += suitCards.size(); + } + + // 基础向听数 = 理想牌型的面子数 - 当前面子数 + int idealMelds = (totalCards + analysis.hongZhongCount) / 3; + analysis.shantenCount = Math.max(0, idealMelds - analysis.meldCount); + } + + // 检查七对潜力 + private void checkLongQiDuiPotential(HandAnalysis analysis) { + + // 七对需要至少5个对子加上红中 + if (analysis.pairCount >= 5 || analysis.hongZhongCount >= 2) { + analysis.hasLongQiDuiPotential = true; + if (analysis.pairCount >=4 && analysis.hongZhongCount>=2){ + analysis.isTingPai=true; + } + if (analysis.pairCount>=6){ + analysis.isTingPai=true; + } + } + + + } + + /** + * 高级听牌检测 - 精确判断手牌是否已经听牌 + * 在摸牌后分析的核心方法之一 + * + * @param analysis 手牌分析结果对象 + */ + private void checkTingPai(HandAnalysis analysis) { +// logInfo("开始检测听牌状态..."); + analysis.tingCards.clear(); // 清空之前可能存在的听牌 + + // 检查基本听牌条件:牌数是否符合标准 + int totalCards = analysis.hongZhongCount; + for (List suitCards : analysis.cardsBySuit.values()) { + totalCards += suitCards.size(); + } + + + + // 检查向听数,如果向听数大于1,则不可能听牌 + if (analysis.shantenCount > 1) { +// logInfo("向听数大于1,不可能听牌"); + analysis.isTingPai = false; + return; + } + + // 标准麻将听牌条件: + // 1. 标准型听牌:一对将牌 + 四面子,且向听数为0 + // 2. 七对子型听牌:有龙七对潜力且牌数达到听牌要求 + boolean standardTing = (totalCards - 2) % 3 == 0 && analysis.shantenCount == 0; + boolean sevenPairsTing = analysis.hasLongQiDuiPotential && totalCards >= 12; + + // 特殊情况:如果有红中,需要考虑红中作为万能牌的影响 + if (analysis.hongZhongCount > 0) { + // 红中可以代替任何牌,所以需要更灵活的判断 + // 检查是否通过红中可以形成听牌状态 + boolean canTingWithHongZhong = canFormTingWithHongZhong(analysis); + analysis.isTingPai = standardTing || sevenPairsTing || canTingWithHongZhong; + } else { + analysis.isTingPai = standardTing || sevenPairsTing; + } + + // 使用优化版算法找出具体的听牌组 + long startTime = System.currentTimeMillis(); + findTingCardsOptimized(analysis); + long endTime = System.currentTimeMillis(); + + + + if (!analysis.tingCards.isEmpty()) { + analysis.isTingPai = true; +// logInfo("优化版听牌检测完成: 听牌数量=" + analysis.tingCards.size() + +// ", 耗时=" + (endTime - startTime) + "ms"); + } else if (analysis.isTingPai) { + // 如果逻辑判断为听牌但未找到具体听牌组,使用原始方法作为后备 +// logInfo("尝试使用原始方法查找听牌组..."); +// findTingCards(analysis); +// logInfo("原始方法找到听牌数量: " + analysis.tingCards.size()); + } + +// logInfo("最终听牌状态: " + analysis.isTingPai + ", 听牌数量: " + analysis.tingCards.size()); + } + + /** + * 优化版的听牌检测方法 + * 1. 提前过滤明显不可能听牌的情况 + * 2. 优化牌数统计和计算逻辑 + * 3. 使用预计算的向听数快速判断 + * 4. 优化红中对听牌状态的影响分析 + */ + private void checkTingPaiOptimized(HandAnalysis analysis) { + // 极早期快速退出 - 向听数>1的情况下不可能听牌 + if (analysis.shantenCount > 1) { + analysis.isTingPai = false; + analysis.tingCards.clear(); + return; + } + + // 清空听牌集合,准备新的分析 + analysis.tingCards.clear(); + + // 预计算总牌数 - 使用提前计算的值或快速计算 + int totalCards = analysis.hongZhongCount; + for (List suitCards : analysis.cardsBySuit.values()) { + totalCards += suitCards.size(); + } + + // 特殊牌型快速检测 - 提前计算是否需要检查七对子 + boolean checkSevenPairs = analysis.hasLongQiDuiPotential && totalCards >= 12; + + // 优化的红中听牌分析 - 先检查红中情况,可能避免后续计算 + boolean hasHongZhongTing = false; + if (analysis.hongZhongCount > 0) { + if (analysis.shantenCount == 0) { + hasHongZhongTing = true; + } else if (analysis.shantenCount == 1) { + // 只有在向听数为1时才进行复杂计算 + hasHongZhongTing = canFormTingWithHongZhongOptimized(analysis, totalCards); + } + } + + // 只有在有潜在听牌可能时才执行完整的听牌检测 + boolean needFullAnalysis = hasHongZhongTing || checkSevenPairs || analysis.shantenCount == 0; + + if (needFullAnalysis) { + // 执行高效的听牌检测算法 + findTingCardsOptimized(analysis); + + // 设置最终听牌状态 + analysis.isTingPai = !analysis.tingCards.isEmpty(); + } else { + analysis.isTingPai = false; + } + } + + /** + * 优化版红中听牌判断 + * 1. 使用位掩码和预计算值 + * 2. 简化计算逻辑 + * 3. 避免不必要的循环和集合操作 + */ + private boolean canFormTingWithHongZhongOptimized(HandAnalysis analysis, int totalCards) { + // 快速判断:如果红中数量足够多,可以替代任何缺失的牌 + if (analysis.hongZhongCount >= 3) { + return true; + } + + // 计算红中作为面子的最大可能数量 + int maxMeldsFromHongZhong = analysis.hongZhongCount / 3; + int remainingHongZhong = analysis.hongZhongCount % 3; + + // 计算理论上可以形成的面子总数 + int totalPossibleMelds = analysis.meldCount + maxMeldsFromHongZhong; + + // 需要的面子数和对子数 + int neededMelds = Math.max(0, 4 - totalPossibleMelds); + int neededPairs = analysis.pairCount > 0 ? 0 : 1; + + // 检查是否可以通过剩余红中满足需求 + // 每张红中可以作为一个面子的一部分或一个对子的一部分 + return (neededMelds + neededPairs) <= remainingHongZhong + 1; + } + + /** + * 检查是否通过红中可以形成听牌状态 + * @param analysis 手牌分析结果对象 + * @return 是否可以通过红中形成听牌 + */ + private boolean canFormTingWithHongZhong(HandAnalysis analysis) { + // 计算去掉红中后的牌数 + int nonHongZhongCount = 0; + for (List suitCards : analysis.cardsBySuit.values()) { + nonHongZhongCount += suitCards.size(); + } + + // 计算红中可以替代的面子数 + int hongZhongAsMelds = analysis.hongZhongCount / 3; + int remainingHongZhong = analysis.hongZhongCount % 3; + + // 计算总面子数(包括红中形成的面子) + int totalMeldCount = analysis.meldCount + hongZhongAsMelds; + + // 检查是否接近听牌(剩余红中可以作为对子或填补顺子) + boolean nearTing = false; + int neededMelds = 4 - totalMeldCount; + int neededPairs = 1 - (analysis.pairCount > 0 ? 1 : 0); + + // 如果需要的面子数和对子数之和小于等于剩余红中,那么可以形成听牌 + if (neededMelds + neededPairs <= remainingHongZhong) { + nearTing = true; + } + + // 如果向听数很小,也认为接近听牌 + if (analysis.shantenCount <= 1) { + nearTing = true; + } + + return nearTing; + } + + /** + * 统一日志输出方法 + */ + private void logInfo(String message) { + System.out.println(message); + } + + /** + * 高级听牌组分析 - 精确找出听的牌 + * @param analysis 手牌分析结果对象 + */ + /** + * 优化版听牌组分析 - 使用预计算和优化的数据结构提高性能 + * @param analysis 手牌分析结果对象 + */ + private void findTingCardsOptimized(HandAnalysis analysis) { + // 提前检查 - 如果手牌太少或结构明显不合理,可以提前返回 + int totalCards = analysis.hongZhongCount; + for (List suitCards : analysis.cardsBySuit.values()) { + totalCards += suitCards.size(); + } + + // 牌数过少,不可能形成有效的听牌 + if (totalCards < 13) { + return; + } + + // 预先为所有花色创建计数映射,避免重复计算 + // 更精确地预估容量,避免过度分配 + final int expectedSuitCount = analysis.cardsBySuit.size(); + final int expectedRankCount = 9; // 麻将中每个花色最多9个不同的牌值 + Map> suitToRankCounts = new HashMap<>(expectedSuitCount); + + // 优化循环,同时构建计数映射 + for (Map.Entry> suitEntry : analysis.cardsBySuit.entrySet()) { + int suit = suitEntry.getKey(); + List suitCards = suitEntry.getValue(); + + // 仅在非空花色上创建映射 + if (suitCards.isEmpty()) { + continue; + } + + // 预先分配更准确的容量 + Map countByRank = new HashMap<>(Math.min(expectedRankCount, suitCards.size())); + + // 快速计数算法 + for (int card : suitCards) { + int rank = card % 100; + // 直接使用getOrDefault来增加计数 + countByRank.put(rank, countByRank.getOrDefault(rank, 0) + 1); + } + + suitToRankCounts.put(suit, countByRank); + } + + // 优化分析流程 - 先分析主要花色,再考虑特殊情况 + for (Map.Entry> entry : suitToRankCounts.entrySet()) { + // 只有当花色中存在牌时才进行分析 + if (entry.getValue().isEmpty()) { + continue; + } + + analyzeSuitTingCardsOptimized(entry.getKey(), entry.getValue(), analysis); + + // 如果已经找到了足够多的听牌,可以考虑提前终止分析 + // 这里可以根据实际需求调整提前终止的条件 + if (analysis.tingCards.size() > 10) { // 假设找到超过10张听牌就足够了 + break; + } + } + + // 仅在必要时分析红中影响 - 红中数量大于0且当前听牌较少时 + if (analysis.hongZhongCount > 0 && analysis.tingCards.size() < 5) { + analyzeHongZhongImpactOnTingCardsOptimized(analysis, suitToRankCounts); + } + + // 仅在确实需要时执行兜底分析 - 听牌为空且向听数<=1 + if (analysis.tingCards.isEmpty() && analysis.shantenCount <= 1) { + performBackupTingAnalysisOptimized(analysis, suitToRankCounts); + } + } + + /** + * 优化版花色听牌分析 - 使用数组而非HashMap进行快速查询 + */ + private void analyzeSuitTingCardsOptimized(int suit, Map countByRank, HandAnalysis analysis) { + // 将Map转换为数组,提高查询速度 + int[] rankCounts = new int[10]; // 索引0不使用,1-9对应牌点 + for (Map.Entry entry : countByRank.entrySet()) { + rankCounts[entry.getKey()] = entry.getValue(); + } + + // 分析两面听 + for (int i = 1; i <= 7; i++) { + if (rankCounts[i] > 0 && rankCounts[i + 2] > 0) { + int potentialCard = suit * 100 + (i + 1); + analysis.tingCards.add(potentialCard); + } + } + + // 分析边张听 + if (rankCounts[1] > 0 && rankCounts[2] > 0) { + analysis.tingCards.add(suit * 100 + 3); + } + if (rankCounts[8] > 0 && rankCounts[9] > 0) { + analysis.tingCards.add(suit * 100 + 7); + } + + // 分析坎张听 + for (int i = 3; i <= 7; i++) { + if (rankCounts[i - 1] > 0 && rankCounts[i + 1] > 0) { + analysis.tingCards.add(suit * 100 + i); + } + } + + // 分析对子听和刻子听 + for (int i = 1; i <= 9; i++) { + int count = rankCounts[i]; + if (count == 2 || count == 3) { + analysis.tingCards.add(suit * 100 + i); + } + } + } + + /** + * 优化版红中影响分析 - 使用预计算的花色计数避免重复计算 + */ + private void analyzeHongZhongImpactOnTingCardsOptimized(HandAnalysis analysis, Map> suitToRankCounts) { + Set additionalTingCards = new HashSet<>(); + int hongZhongCount = analysis.hongZhongCount; + + for (Map.Entry> suitEntry : suitToRankCounts.entrySet()) { + int suit = suitEntry.getKey(); + Map countByRank = suitEntry.getValue(); + + // 将Map转换为数组,提高查询速度 + int[] rankCounts = new int[10]; + for (Map.Entry entry : countByRank.entrySet()) { + rankCounts[entry.getKey()] = entry.getValue(); + } + + // 分析红中作为对子的情况 + if (hongZhongCount >= 1) { + for (int i = 1; i <= 9; i++) { + if (rankCounts[i] == 1) { + additionalTingCards.add(suit * 100 + i); + } + } + + // 分析红中填补顺子缺口的情况 + for (int i = 1; i <= 7; i++) { + if (rankCounts[i] > 0 && rankCounts[i + 2] > 0) { + additionalTingCards.add(suit * 100 + (i + 1)); + } + } + } + + // 当有2个或更多红中时 + if (hongZhongCount >= 2) { + for (int i = 1; i <= 9; i++) { + additionalTingCards.add(suit * 100 + i); + } + } + } + + // 添加额外听牌并记录贡献数量 + if (!additionalTingCards.isEmpty()) { + analysis.tingCards.addAll(additionalTingCards); + analysis.hongZhongContributedTingCards = additionalTingCards.size(); + } + } + + /** + * 优化版兜底听牌分析 - 仅在必要时执行,避免不必要的计算 + */ + private void performBackupTingAnalysisOptimized(HandAnalysis analysis, Map> suitToRankCounts) { + // 分析接近听牌的情况 + Set potentialTingCards = new HashSet<>(); + + for (Map.Entry> suitEntry : suitToRankCounts.entrySet()) { + int suit = suitEntry.getKey(); + Map countByRank = suitEntry.getValue(); + + // 将Map转换为数组 + int[] rankCounts = new int[10]; + for (Map.Entry entry : countByRank.entrySet()) { + rankCounts[entry.getKey()] = entry.getValue(); + } + + // 分析可能形成对子的牌 + for (int i = 1; i <= 9; i++) { + if (rankCounts[i] == 1) { + potentialTingCards.add(suit * 100 + i); + } + } + + // 分析可能形成顺子的牌 + for (int i = 1; i <= 7; i++) { + if (rankCounts[i] > 0 && rankCounts[i + 1] > 0) { + potentialTingCards.add(suit * 100 + (i + 2)); + } + if (rankCounts[i] > 0 && rankCounts[i + 2] > 0) { + potentialTingCards.add(suit * 100 + (i + 1)); + } + if (rankCounts[i + 1] > 0 && rankCounts[i + 2] > 0) { + potentialTingCards.add(suit * 100 + i); + } + } + } + + // 只在找到潜在听牌时才添加 + if (!potentialTingCards.isEmpty()) { + analysis.tingCards.addAll(potentialTingCards); + } + } + + private void findTingCards(HandAnalysis analysis) { +// logInfo("开始精确分析听牌组..."); + + // 针对不同花色分别分析听牌组 + for (Map.Entry> suitEntry : analysis.cardsBySuit.entrySet()) { + int suit = suitEntry.getKey(); + List suitCards = suitEntry.getValue(); + + // 统计该花色中每种牌的数量 + Map countByRank = new HashMap<>(); + for (int card : suitCards) { + int rank = card % 100; + countByRank.put(rank, countByRank.getOrDefault(rank, 0) + 1); + } + + // 分析该花色中可能的听牌 + analyzeSuitTingCards(suit, countByRank, analysis); + } + + // 考虑红中对听牌的影响 + if (analysis.hongZhongCount > 0) { + analyzeHongZhongImpactOnTingCards(analysis); + } + + // 如果听牌组为空,进行兜底分析 + if (analysis.tingCards.isEmpty()) { + performBackupTingAnalysis(analysis); + } + +// logInfo("精确分析完成,听牌组: " + analysis.tingCards); + } + + /** + * 分析单个花色的听牌组 + */ + private void analyzeSuitTingCards(int suit, Map countByRank, HandAnalysis analysis) { + // 分析两面听 + for (int i = 1; i <= 7; i++) { + if (countByRank.getOrDefault(i, 0) > 0 && countByRank.getOrDefault(i + 2, 0) > 0) { + int potentialCard = suit * 100 + (i + 1); + analysis.tingCards.add(potentialCard); +// logInfo("两面听: " + potentialCard + " (由 " + (suit * 100 + i) + " 和 " + (suit * 100 + (i + 2)) + " 组成)"); + } + } + + // 分析边张听 + if (countByRank.getOrDefault(1, 0) > 0 && countByRank.getOrDefault(2, 0) > 0) { + int potentialCard = suit * 100 + 3; + analysis.tingCards.add(potentialCard); +// logInfo("边张听: " + potentialCard + " (由 " + (suit * 100 + 1) + " 和 " + (suit * 100 + 2) + " 组成)"); + } + if (countByRank.getOrDefault(8, 0) > 0 && countByRank.getOrDefault(9, 0) > 0) { + int potentialCard = suit * 100 + 7; + analysis.tingCards.add(potentialCard); +// logInfo("边张听: " + potentialCard + " (由 " + (suit * 100 + 8) + " 和 " + (suit * 100 + 9) + " 组成)"); + } + + // 分析坎张听 + for (int i = 3; i <= 7; i++) { + if (countByRank.getOrDefault(i - 1, 0) > 0 && countByRank.getOrDefault(i + 1, 0) > 0) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); +// logInfo("坎张听: " + potentialCard + " (由 " + (suit * 100 + (i - 1)) + " 和 " + (suit * 100 + (i + 1)) + " 组成)"); + } + } + + // 分析对子听(将对听牌) + for (int i = 1; i <= 9; i++) { + if (countByRank.getOrDefault(i, 0) == 2) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); +// logInfo("对子听: " + potentialCard + " (已有两张)"); + } + } + + // 分析刻子听(三张听刻) + for (int i = 1; i <= 9; i++) { + if (countByRank.getOrDefault(i, 0) == 3) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); +// logInfo("刻子听: " + potentialCard + " (已有三张)"); + } + } + } + + /** + * 分析红中对听牌的影响 - 优化版 + * 更精确地计算红中如何帮助形成听牌 + */ + private void analyzeHongZhongImpactOnTingCards(HandAnalysis analysis) { + // 红中作为万能牌,可以替代缺少的牌来形成听牌 + Set additionalTingCards = new HashSet<>(); + int hongZhongCount = analysis.hongZhongCount; + + // 对于每种花色,分析红中可以带来的额外听牌 + for (Map.Entry> suitEntry : analysis.cardsBySuit.entrySet()) { + int suit = suitEntry.getKey(); + List suitCards = suitEntry.getValue(); + + // 统计该花色中每种牌的数量 + Map countByRank = new HashMap<>(); + for (int card : suitCards) { + int rank = card % 100; + countByRank.put(rank, countByRank.getOrDefault(rank, 0) + 1); + } + + // 根据红中数量计算可能的听牌 + if (hongZhongCount >= 1) { + // 分析红中作为对子的情况 + for (int i = 1; i <= 9; i++) { + if (countByRank.getOrDefault(i, 0) == 1) { + // 已有一张,红中作为第二张形成对子,听第三张 + int card = suit * 100 + i; + additionalTingCards.add(card); +// logInfo("红中作为对子听: " + card + " (红中+已有一张)"); + } + } + + // 分析红中填补顺子缺口的情况 + for (int i = 1; i <= 7; i++) { + int count1 = countByRank.getOrDefault(i, 0); + int count2 = countByRank.getOrDefault(i + 2, 0); + + if ((count1 > 0 && count2 > 0) && (count1 + count2 + hongZhongCount >= 3)) { + // 可以用红中填补中间的牌形成顺子 + int middleCard = suit * 100 + (i + 1); + additionalTingCards.add(middleCard); +// logInfo("红中填补顺子听: " + middleCard + " (红中+" + (suit * 100 + i) + "+" + (suit * 100 + (i + 2)) + ")"); + } + } + + // 当有2个或更多红中时,可以形成更多听牌 + if (hongZhongCount >= 2) { + // 红中作为两张形成对子,听任何第三张 + for (int i = 1; i <= 9; i++) { + int card = suit * 100 + i; + additionalTingCards.add(card); + } + } + } + } + + // 去重并将额外的听牌组添加到总听牌组中 + Set uniqueAdditionalTingCards = new HashSet<>(additionalTingCards); + analysis.tingCards.addAll(uniqueAdditionalTingCards); + if (!uniqueAdditionalTingCards.isEmpty()) { +// logInfo("红中带来的额外听牌: " + uniqueAdditionalTingCards); + // 记录红中带来的听牌数量 + analysis.hongZhongContributedTingCards = uniqueAdditionalTingCards.size(); + } + } + + /** + * 兜底听牌分析 - 优化版 + * 当主要分析方法无法找到听牌组时使用,提供更智能的兜底分析 + */ + private void performBackupTingAnalysis(HandAnalysis analysis) { +// logInfo("执行兜底听牌分析..."); + + // 1. 检查接近听牌的情况 + if (analysis.shantenCount <= 1) { + // 接近听牌时,分析可能的进张 + Set potentialTingCards = new HashSet<>(); + + // 分析每个花色中可能形成面子的牌 + for (Map.Entry> suitEntry : analysis.cardsBySuit.entrySet()) { + int suit = suitEntry.getKey(); + List suitCards = suitEntry.getValue(); + + // 统计该花色中每种牌的数量 + Map countByRank = new HashMap<>(); + for (int card : suitCards) { + int rank = card % 100; + countByRank.put(rank, countByRank.getOrDefault(rank, 0) + 1); + } + + // 分析可能形成对子的牌 + for (int i = 1; i <= 9; i++) { + if (countByRank.getOrDefault(i, 0) == 1) { + potentialTingCards.add(suit * 100 + i); + } + } + + // 分析可能形成顺子的牌 + for (int i = 1; i <= 7; i++) { + if (countByRank.getOrDefault(i, 0) > 0 && countByRank.getOrDefault(i + 1, 0) > 0) { + potentialTingCards.add(suit * 100 + (i + 2)); + } + if (countByRank.getOrDefault(i, 0) > 0 && countByRank.getOrDefault(i + 2, 0) > 0) { + potentialTingCards.add(suit * 100 + (i + 1)); + } + if (countByRank.getOrDefault(i + 1, 0) > 0 && countByRank.getOrDefault(i + 2, 0) > 0) { + potentialTingCards.add(suit * 100 + i); + } + } + } + + analysis.tingCards.addAll(potentialTingCards); +// logInfo("接近听牌兜底分析结果: " + potentialTingCards); + } + + // 2. 如果仍然没有找到听牌,才进行简单兜底 + if (analysis.tingCards.isEmpty()) { + // 简化兜底分析:优先考虑中张牌 + for (int suit : analysis.cardsBySuit.keySet()) { + // 先添加中张牌 + for (int i = 3; i <= 7; i++) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); + } + // 再添加边张牌 + for (int i = 1; i <= 2; i++) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); + } + for (int i = 8; i <= 9; i++) { + int potentialCard = suit * 100 + i; + analysis.tingCards.add(potentialCard); + } + } + +// logInfo("简单兜底分析完成,听牌组: " + analysis.tingCards); + } + } + + // 确定牌局阶段 + private GamePhase determineGamePhase(List handCards) { + // 简化实现:根据手牌数量估计牌局阶段 + // 实际应用中应该根据已打出的牌数或游戏进度 + int remainingCards = handCards.size(); + if (remainingCards >= 13) { + return GamePhase.EARLY; + } else if (remainingCards >= 9) { + return GamePhase.MIDDLE; + } else { + return GamePhase.LATE; + } + } + + // 确定策略类型 + private StrategyType determineStrategy(HandAnalysis analysis, GamePhase phase) { + // 听牌时采取进攻策略 + if (analysis.isTingPai) { + return StrategyType.ATTACK; + } + + // 向听数少,进攻为主 + if (analysis.shantenCount <= 1) { + return StrategyType.ATTACK; + } + + // 后期防守策略 + if (phase == GamePhase.LATE) { + return StrategyType.DEFEND; + } + + // 中期根据向听数决定 + if (phase == GamePhase.MIDDLE) { + return analysis.shantenCount <= 2 ? StrategyType.BALANCED : StrategyType.DEFEND; + } + + // 早期通常是平衡型或进攻型 + return StrategyType.BALANCED; + } + + // 分析特殊牌型潜力 + private SpecialPattern analyzeSpecialPatternPotential(HandAnalysis analysis) { + // 检查多红中 + if (analysis.hongZhongCount >= 3) { + return SpecialPattern.HONG_ZHONG_MANY; + } + + // 检查清一色潜力 + if (analysis.cardsBySuit.size() == 1 && analysis.hongZhongCount <= 2) { + return SpecialPattern.QING_YI_SE; + } + + // 检查断幺九潜力 + boolean hasYaoJiu = false; + for (List suitCards : analysis.cardsBySuit.values()) { + for (int card : suitCards) { + int rank = card % 100; + if (rank == 1 || rank == 9) { + hasYaoJiu = true; + break; + } + } + if (hasYaoJiu) break; + } + if (!hasYaoJiu) { + return SpecialPattern.DUAN_YAO_JIU; + } + + // 检查龙七对潜力 + if (analysis.hasLongQiDuiPotential) { + return SpecialPattern.LONG_QI_DUI; + } + + return SpecialPattern.NONE; + } + + + + + // 计算刻子潜力 + private int calculateTripletPotential(int sameCardCount, int hongZhongCount) { + if (sameCardCount == 3) { + return WEIGHT_POTENTIAL_TRIPLET + 20; // Already triplet + } else if (sameCardCount == 2) { + return WEIGHT_POTENTIAL_TRIPLET + (hongZhongCount * 15); // Pair+hongZhong can easily form triplet + } else if (sameCardCount == 1 && hongZhongCount >= 2) { + return 30 + (hongZhongCount * 10); // Single card+2hongZhong can form triplet + } + return 0; + } + + // 计算顺子潜力 + private int calculateSequencePotential(int card, List sameSuitCards) { + int potential = 0; + int suit = card / 100; + int rank = card % 100; + + // 获取当前花色所有牌的点数 + Set ranks = new HashSet<>(); + for (int c : sameSuitCards) { + ranks.add(c % 100); + } + + // 检查可能的顺子组合 + // 向前检查:x, x+1, x+2 + for (int start = Math.max(1, rank - 2); start <= Math.min(7, rank); start++) { + int mid = start + 1; + int end = start + 2; + + int matchCount = 0; + if (ranks.contains(start)) matchCount++; + if (ranks.contains(mid)) matchCount++; + if (ranks.contains(end)) matchCount++; + + // 根据匹配数量增加潜力值 + if (matchCount == 3) { + potential += 60; // 完整顺子 + } else if (matchCount == 2) { + potential += 35; // 两搭子 + } else if (matchCount == 1) { + potential += 15; // 单张 + } + } + + // 相邻牌奖励 + if (ranks.contains(rank - 1) || ranks.contains(rank + 1)) { + potential += WEIGHT_CLOSE_ADJACENT; + } + + return potential; + } + + // 计算牌张位置价值 + private int calculateRankValue(int rank) { + int value = 0; + + // 中张牌价值更高 + if (rank >= 3 && rank <= 7) { + value += WEIGHT_MIDDLE_RANK; + // 金3银7特殊价值 + if (rank == 3 || rank == 7) { + value += 10; // 更高的中张价值 + } + } else { + value -= PENALTY_EDGE_RANK; + } + + return value; + } + + // 计算出牌优先级 - 优先考虑剩余牌和牌的潜力 + private Map calculateDiscardPriority(List handCards, + HandAnalysis analysis, + GamePhase phase, + StrategyType strategy, + SpecialPattern specialPattern) { + Map priority = new HashMap<>(); + + for (int card : handCards) { + if (card == HONG_ZHONG) { + // 红中策略根据阶段和策略调整 + int hongZhongValue = calculateHongZhongValue(analysis, phase, strategy, specialPattern); + priority.put(card, hongZhongValue); + continue; + } + + // 获取基本价值 + int usefulness = analysis.cardUsefulness.getOrDefault(card, 0); + int discardValue = usefulness; + + // 调整剩余牌的价值 - 优先从剩余牌中选择要打出的牌 + discardValue = adjustRemainingCardValue(card, analysis); + + // 根据策略调整牌的价值 + discardValue = adjustValueByStrategy(card, discardValue, analysis, strategy, phase); + + // 特殊牌型策略调整 + discardValue = adjustValueBySpecialPattern(card, discardValue, analysis, specialPattern); + + // 集成牌潜力分析到出牌优先级中 + int potentialScore = calculateCardPotentialScore(card, analysis); + // 根据阶段调整潜力权重 - 后期更重视潜力 + int potentialWeight = (phase == GamePhase.LATE) ? 8 : 5; + discardValue += potentialScore * potentialWeight; + + priority.put(card, discardValue); + } + + return priority; + } + + // 计算红中的价值 + private int calculateHongZhongValue(HandAnalysis analysis, GamePhase phase, StrategyType strategy, SpecialPattern specialPattern) { + // 基础价值 + int value = 1000; + + // 根据数量调整 + value -= (analysis.hongZhongCount * 100); + + // 根据牌局阶段调整 + if (phase == GamePhase.LATE) { + // 后期红中价值降低 + value -= 200; + } + + // 多红中时的策略调整 + if (analysis.hongZhongCount >= 3 && specialPattern == SpecialPattern.HONG_ZHONG_MANY) { + // 多红中时保留价值更高 + value += 300; + } + + // 防守策略时可能需要保留红中 + if (strategy == StrategyType.DEFEND) { + value += 100; + } + + // 集成红中的潜力分析 + int potentialScore = calculateCardPotentialScore(HONG_ZHONG, analysis); + // 由于我们已经在calculateCardPotentialScore中为红中提供了特殊处理, + // 这里使用较低的权重来避免过度提升红中的价值 + value += potentialScore * 3; // 适度考虑潜力因素 + + return Math.max(value, 200); // 红中最低价值 + } + + // 根据策略调整牌的价值 + private int adjustValueByStrategy(int card, int value, HandAnalysis analysis, StrategyType strategy, GamePhase phase) { + // 进攻策略:重视听牌和组成面子 + if (strategy == StrategyType.ATTACK) { + // 进攻时更重视对子 + if (analysis.pairs.contains(card)) { + value += 30; + } + // 进攻时更重视顺子潜力 + if (!analysis.isolatedCards.contains(card)) { + value += 20; + } + } + // 防守策略:避免危险牌,保留安全牌 + else if (strategy == StrategyType.DEFEND) { + // 防守时更倾向于保留孤张 + if (analysis.isolatedCards.contains(card)) { + value += 15; + } + // 防守时减少对子价值 + if (analysis.pairs.contains(card)) { + value -= 10; + } + } + + // 后期策略调整 + if (phase == GamePhase.LATE) { + // 后期更倾向于保留中张 + int rank = card % 100; + if (rank >= 3 && rank <= 7) { + value += 10; + } + } + + return value; + } + + // 根据特殊牌型调整牌的价值 + private int adjustValueBySpecialPattern(int card, int value, HandAnalysis analysis, SpecialPattern specialPattern) { + int rank = card % 100; + + switch (specialPattern) { + case DUAN_YAO_JIU: + // 断幺九时,幺九牌价值降低 + if (rank == 1 || rank == 9) { + value -= 50; + } + break; + case LONG_QI_DUI: + // 龙七对时,对子价值提高 + if (analysis.pairs.contains(card)) { + value += 40; + } + break; + case QING_YI_SE: + // 清一色时,当前花色的牌价值提高 + value += 30; + break; + } + + return value; + } + + // 选择要打出的牌 - 优先从剩余牌中选择,听牌时保护听牌组,红中固定不能出 + private int selectCardToDiscard(List handCards, + Map priority, + HandAnalysis analysis, + GamePhase phase, + StrategyType strategy, + Map> discardToTingOptions) { + + logInfo("选择----"+handCards); + // 1. 核心策略:优先考虑可以打出后听牌的牌 + if (!discardToTingOptions.isEmpty()) { +// logInfo("【重要】发现可打出后听牌的选项,优先选择这些牌"); + + + // 从候选牌中过滤出可打出后听牌的牌 + List tingCandidateCards = new ArrayList<>(); + for (int card : handCards) { + if (discardToTingOptions.containsKey(card) && card != HONG_ZHONG) { + tingCandidateCards.add(card); + } + } + + if (!tingCandidateCards.isEmpty()) { + // 智能排序:优先选择数量较多且听牌组较大的牌 + int bestCardToDiscard = sortTingCandidateCards(tingCandidateCards, analysis, discardToTingOptions, priority); + + logInfo("优先选择可听牌的牌: " + bestCardToDiscard + ", 听牌组: " + discardToTingOptions.get(bestCardToDiscard)); + return bestCardToDiscard; + } + } + +// // 2. 优先考虑孤张牌,特别是顺子中多余的牌 +// List isolatedCandidateCards = new ArrayList<>(); + for (int card : handCards) { + // 排除红中(固定不能出) + if (card == HONG_ZHONG) { + continue; + } + // 听牌时,排除听牌组中的牌 + if (analysis.isTingPai && analysis.tingCards.contains(card)) { + continue; + } +// // 检查是否是孤张牌 +// if (analysis.isolatedCards.contains(card)) { +// isolatedCandidateCards.add(card); +// } + } + + // 如果有孤张牌,优先从孤张牌中选择 + if (!analysis.isolatedCards.isEmpty()) { +// logInfo("发现孤张牌,优先从孤张牌中选择要打出的牌: " + analysis.isolatedCards); + // 从孤张牌中找出最低优先级的牌 + return sortCandidateCards(analysis.isolatedCards, priority, analysis, strategy, phase, discardToTingOptions); + } + + + // 新增:摸牌后特殊处理 - 检查新摸的牌是否需要优先保留 + if (analysis.lastDrawnCard != 0 && analysis.lastDrawnCard != HONG_ZHONG) { + // 创建一个排除新摸牌的候选列表(除非新摸牌确实是最差选择) + List nonDrawnCandidateCards = new ArrayList<>(); + for (int card : handCards) { + if (card != HONG_ZHONG && card != analysis.lastDrawnCard && + !(analysis.isTingPai && analysis.tingCards.contains(card))) { + nonDrawnCandidateCards.add(card); + } + } + + if (!nonDrawnCandidateCards.isEmpty()) { + // 评估新摸牌的优先级与其他牌的差距 + int drawnCardPriority = priority.getOrDefault(analysis.lastDrawnCard, 50); + int lowestPriority = Integer.MAX_VALUE; + for (int card : nonDrawnCandidateCards) { + lowestPriority = Math.min(lowestPriority, priority.getOrDefault(card, 50)); + } + + // 如果新摸牌的优先级不是明显高于其他牌,优先保留新摸牌 + if (lowestPriority < drawnCardPriority + 10) { // 设置10的阈值作为缓冲 + logInfo("优先保留新摸的牌: " + analysis.lastDrawnCard); + int cardToDiscard = sortCandidateCards(nonDrawnCandidateCards, priority, analysis, strategy, phase, discardToTingOptions); + return cardToDiscard; + } + } + } + + // 如果没有孤张牌,则从剩余牌中选择要打出的牌 + List remainingCandidateCards = new ArrayList<>(); + for (int card : handCards) { + // 排除红中(固定不能出) + if (card == HONG_ZHONG) { + continue; + } + // 听牌时,排除听牌组中的牌 + if (analysis.isTingPai && analysis.tingCards.contains(card)) { + continue; + } + if (analysis.remainingCards.contains(card)) { + remainingCandidateCards.add(card); + } + } + + // 如果有剩余牌,优先从剩余牌中选择 + if (!remainingCandidateCards.isEmpty()) { + // 从剩余牌中找出最低优先级的牌 + return sortCandidateCards(remainingCandidateCards, priority, analysis, strategy, phase, discardToTingOptions); + } + + // 否则从所有牌中选择,但排除听牌组中的牌和红中 + List allCandidateCards = new ArrayList<>(); + for (int card : handCards) { + // 排除红中(固定不能出) + if (card == HONG_ZHONG) { + continue; + } + // 排除听牌组中的牌 + if (!(analysis.isTingPai && analysis.tingCards.contains(card))) { + allCandidateCards.add(card); + } + } + + // 如果所有牌都在听牌组中或全是红中(极端情况),至少保留一张非红中牌 + if (allCandidateCards.isEmpty() && !handCards.isEmpty()) { + // 找出所有非红中且不在听牌组中的牌 + List fallbackCards = new ArrayList<>(); + for (int card : handCards) { + if (card != HONG_ZHONG) { + fallbackCards.add(card); + } + } + + if (!fallbackCards.isEmpty()) { + // 选择数量最多的牌打出一张 + int maxCount = 0; + int cardToDiscard = fallbackCards.get(0); + for (int card : fallbackCards) { + int count = analysis.cardCounts.getOrDefault(card, 0); + if (count > maxCount) { + maxCount = count; + cardToDiscard = card; + } + } + return cardToDiscard; + } + } + + return sortCandidateCards(allCandidateCards, priority, analysis, strategy, phase, discardToTingOptions); + } + + /** + * 专门为可听牌的候选牌进行排序 + * 按照优先级:听牌组数量 > 牌的数量 > 牌的价值 + */ + private int sortTingCandidateCards(List candidates, HandAnalysis analysis, + Map> discardToTingOptions, + Map priority) { + // 按照优先级排序:1. 听牌组数量 2. 牌的数量 3. 牌的价值 + candidates.sort((a, b) -> { + // 1. 优先比较听牌组数量 + int tingGroupComparison = Integer.compare( + discardToTingOptions.get(b).size(), + discardToTingOptions.get(a).size() + ); + if (tingGroupComparison != 0) { + return tingGroupComparison; + } + + // 2. 比较牌的数量 + int countA = analysis.cardCounts.getOrDefault(a, 0); + int countB = analysis.cardCounts.getOrDefault(b, 0); + int countComparison = Integer.compare(countB, countA); + if (countComparison != 0) { + return countComparison; + } + + // 3. 比较牌的价值(优先级值越低越好) + int priorityA = priority.getOrDefault(a, 0); + int priorityB = priority.getOrDefault(b, 0); + return Integer.compare(priorityA, priorityB); + }); + + return candidates.get(0); + } + + // 排序候选牌 - 提取排序逻辑为单独方法,听牌时特殊处理 + private int sortCandidateCards(List candidateCards, + Map priority, + HandAnalysis analysis, + StrategyType strategy, + GamePhase phase, + Map> discardToTingOptions) { + // 听牌时的特殊处理:确保不会拆听牌组 + if (analysis.isTingPai) { + // 过滤掉听牌组中的牌 + List filteredCards = new ArrayList<>(); + for (int card : candidateCards) { + if (!analysis.tingCards.contains(card)) { + filteredCards.add(card); + } + } + if (!filteredCards.isEmpty()) { + candidateCards = filteredCards; + } + } +// // 找到最低优先级的牌 +// int minPriority = Integer.MAX_VALUE; +// List minPriorityCards = new ArrayList<>(); + //candidateCards + + + // 简化的相邻牌检测逻辑 + Map> suitToValues = new HashMap<>(); + +// 按花色分组并记录牌值 + for (int card : candidateCards) { + int suit = card / 100; + int value = card % 100; + suitToValues.computeIfAbsent(suit, k -> new HashSet<>()).add(value); + } + + List nonAdjacentCards = new ArrayList<>(); + +// 检查每个花色中的牌,只保留没有相邻关系的牌 + for (int card : candidateCards) { + int suit = card / 100; + int value = card % 100; + Set values = suitToValues.get(suit); + + // 检查这张牌是否有相邻牌(左边或右边) + boolean hasLeftAdjacent = values.contains(value - 1); + boolean hasRightAdjacent = values.contains(value + 1); + + + if (!(hasLeftAdjacent || hasRightAdjacent)){ + nonAdjacentCards.add(card); + } + } + + // 简化的相邻牌检测逻辑 + Map> suitToValues1 = new HashMap<>(); + for (int card : nonAdjacentCards) { + int suit = card / 100; + int value = card % 100; + suitToValues1.computeIfAbsent(suit, k -> new HashSet<>()).add(value); + } + List nonAdjacentCards1 = new ArrayList<>(); + for (int card : nonAdjacentCards){ + int suit = card / 100; + int value = card % 100; + Set values = suitToValues1.get(suit); + // 2. 检查是否有卡隆牌(间隔一张) + boolean hasLeftCardLong = values.contains(value - 2); // 如3和5 + boolean hasRightCardLong = values.contains(value + 2); // 如5和3 + + if (!(hasLeftCardLong || hasRightCardLong)){ + nonAdjacentCards1.add(card); + } + } + + + if (!nonAdjacentCards1.isEmpty()){ + nonAdjacentCards=nonAdjacentCards1; + } + + List resultCards = new ArrayList<>(); + if (nonAdjacentCards.isEmpty()){ + // 如果nonAdjacentCards为空,优先找边张 + List edgeCards = new ArrayList<>(); + + + + for (int card : candidateCards) { + int value = card % 100; + // 边张:1或9(假设麻将牌值范围是1-9) + if (value == 1 || value == 9) { + edgeCards.add(card); + } + } + + + if (!edgeCards.isEmpty()) { + // 有边张,使用边张 + nonAdjacentCards = edgeCards; + return nonAdjacentCards.get(0); + } else { + // 没有边张,取下标为1的牌(第二个元素) + if (candidateCards.size() > 1) { + nonAdjacentCards.add(candidateCards.get(1)); + return nonAdjacentCards.get(0); + } else { + // 如果只有一个元素,就取这个元素 + nonAdjacentCards.add(candidateCards.get(0)); + return nonAdjacentCards.get(0); + } + } + } + if (!nonAdjacentCards.isEmpty()){ + + for (int card : nonAdjacentCards) { + int value = card % 100; + // 边张:1或9(假设麻将牌值范围是1-9) + if (value == 1 || value == 9) { + resultCards.add(card); + } + + } + if (!resultCards.isEmpty()){ + return resultCards.get(0); + } + + } + + List resultCards1 = new ArrayList<>(); + if (resultCards.isEmpty()){ + for (int card : nonAdjacentCards) { + int value = card % 100; + + + if (value==8 || value==2){ + resultCards1.add(card); + } + + + } + if (!resultCards1.isEmpty()){ + return resultCards1.get(0); + } + } + + List resultCards2= new ArrayList<>(); + if (resultCards1.isEmpty()){ + for (int card : nonAdjacentCards) { + int value = card % 100; + + + if (value==7 || value==3 || value==6 || value==4 || value==5){ + resultCards2.add(card); + } + + + } + if (!resultCards2.isEmpty()){ + return resultCards2.get(0); + } + } + + +// 现在 nonAdjacentCards 中只包含不能组成顺子的牌 +// 对于输入 [206, 304, 305],结果将是 [206] + + + + if (!nonAdjacentCards.isEmpty()) { + // 更自然的排序逻辑,优先考虑孤张牌的刻子和边搭可能性 + nonAdjacentCards.sort((a, b) -> { + + int rankA = a % 100; + int rankB = b % 100; + int suitA = a / 100; + int suitB = b / 100; + + // 1. 特殊处理:检查是否可以通过打这张牌听牌 + boolean canTingA = discardToTingOptions != null && discardToTingOptions.containsKey(a); + boolean canTingB = discardToTingOptions != null && discardToTingOptions.containsKey(b); + + // 如果只有一张牌可以打出听牌,则优先打那张牌 + if (canTingA && !canTingB) { + return -1; + } + if (!canTingA && canTingB) { + return 1; + } + + // 如果两张牌都可以听牌,比较听牌组数量 + if (canTingA && canTingB) { + int tingGroupA = discardToTingOptions.get(a).size(); + int tingGroupB = discardToTingOptions.get(b).size(); + return Integer.compare(tingGroupB, tingGroupA); // 听牌组多的优先 + } + + // 2. 特殊处理:检查是否是顺子中多余的牌(比如顺子234中多出的3) + boolean isExtraInSequenceA = isExtraCardInSequence(a, analysis); + boolean isExtraInSequenceB = isExtraCardInSequence(b, analysis); + + // 如果一个是顺子中多余的牌,另一个不是,则优先打出多余的牌 + if (isExtraInSequenceA && !isExtraInSequenceB) { + return -1; // A是顺子中多余的牌,优先打A + } + if (!isExtraInSequenceA && isExtraInSequenceB) { + return 1; // B是顺子中多余的牌,优先打B + } + + // 1. 孤张牌处理:优先考虑没有潜力或潜力低的孤张 + boolean isIsolatedA = analysis.isolatedCards.contains(a); + boolean isIsolatedB = analysis.isolatedCards.contains(b); + + // 计算潜力分数 + int potentialA = calculateCardPotentialScore(a, analysis); + int potentialB = calculateCardPotentialScore(b, analysis); + + // 摸牌后特殊处理:检查是否是新摸的牌 + boolean isDrawnCardA = a == analysis.lastDrawnCard; + boolean isDrawnCardB = b == analysis.lastDrawnCard; + + // 如果其中一张是新摸的牌,给予适当的优先级提升 + if (isDrawnCardA && !isDrawnCardB) { + // 新摸的牌有潜力时优先保留 + if (potentialA > 15) { + return 1; // 保留新摸的有潜力的牌 + } + } else if (isDrawnCardB && !isDrawnCardA) { + // 新摸的牌有潜力时优先保留 + if (potentialB > 15) { + return -1; // 保留新摸的有潜力的牌 + } + } + + // 首先,将没有任何潜力的孤张牌优先打出 + if (isIsolatedA && potentialA == 0 && !(isIsolatedB && potentialB == 0)) { + return -1; // A是没有任何潜力的孤张,优先打A + } + if (isIsolatedB && potentialB == 0 && !(isIsolatedA && potentialA == 0)) { + return 1; // B是没有任何潜力的孤张,优先打B + } + + // 如果都是孤张,按潜力分数排序 + if (isIsolatedA && isIsolatedB) { + // 潜力低的优先打出 + if (potentialA != potentialB) { + return Integer.compare(potentialA, potentialB); + } + // 潜力相同时,检查是否是新摸的牌,优先保留新摸的牌 + if (isDrawnCardA && !isDrawnCardB) { + return 1; // 保留新摸的牌 + } else if (isDrawnCardB && !isDrawnCardA) { + return -1; // 保留新摸的牌 + } + } + // 如果一个是孤张,一个不是,则比较它们的潜力 + else if (isIsolatedA && !isIsolatedB) { + // 孤张但有一定潜力,与非孤张比较 + if (potentialA < potentialB - 5) { // 设置阈值,避免过于保守 + return -1; // A潜力明显低于B,优先打A + } + // 孤张但有潜力且是新摸的牌,优先保留 + if (isDrawnCardA && potentialA > 10) { + return 1; // 保留新摸的有潜力的孤张牌 + } + } + else if (!isIsolatedA && isIsolatedB) { + // 孤张但有一定潜力,与非孤张比较 + if (potentialB < potentialA - 5) { // 设置阈值 + return 1; // B潜力明显低于A,优先打B + } + // 孤张但有潜力且是新摸的牌,优先保留 + if (isDrawnCardB && potentialB > 10) { + return -1; // 保留新摸的有潜力的孤张牌 + } + } + + // 2. 边张处理:优先考虑没有刻子或边搭可能性的边张 + boolean isEdgeA = (rankA == 1 || rankA == 9); + boolean isEdgeB = (rankB == 1 || rankB == 9); + + if (isEdgeA && isEdgeB) { + // 两个都是边张,优先打出没有潜力的边张 + boolean hasPotentialA = hasEdgeCardPotential(a, analysis); + boolean hasPotentialB = hasEdgeCardPotential(b, analysis); + + if (hasPotentialA != hasPotentialB) { + return hasPotentialA ? 1 : -1; // 有潜力的边张优先保留 + } + // 如果都有或都没有潜力,再比较具体的潜力分数 + if (potentialA != potentialB) { + return Integer.compare(potentialA, potentialB); + } + } else if (isIsolatedA && isEdgeA && !isIsolatedB && !isEdgeB) { + // A是孤立的边张,B不是,检查A是否有潜力 + if (!hasEdgeCardPotential(a, analysis)) { + return -1; // A是孤立且没有潜力的边张,优先打A + } + } else if (isIsolatedB && isEdgeB && !isIsolatedA && !isEdgeA) { + // B是孤立的边张,A不是,检查B是否有潜力 + if (!hasEdgeCardPotential(b, analysis)) { + return 1; // B是孤立且没有潜力的边张,优先打B + } + } + + // 4. 次边张处理(2,8) + boolean isSubEdgeA = (rankA == 2 || rankA == 8); + boolean isSubEdgeB = (rankB == 2 || rankB == 8); + + if (isSubEdgeA && !isSubEdgeB) { + // A是次边张,B不是,检查A是否有潜力 + if (!hasEdgeCardPotential(a, analysis)) { + return -1; // A是没有潜力的次边张,优先打A + } + } + if (!isSubEdgeA && isSubEdgeB) { + // B是次边张,A不是,检查B是否有潜力 + if (!hasEdgeCardPotential(b, analysis)) { + return 1; // B是没有潜力的次边张,优先打B + } + } + // 如果两个都是次边张,比较它们的潜力 + if (isSubEdgeA && isSubEdgeB) { + boolean hasPotentialA = hasEdgeCardPotential(a, analysis); + boolean hasPotentialB = hasEdgeCardPotential(b, analysis); + + if (hasPotentialA != hasPotentialB) { + return hasPotentialA ? 1 : -1; // 有潜力的次边张优先保留 + } + } + + // 5. 根据策略调整 + if (strategy == StrategyType.DEFEND) { + // 防守时保留中张 + if (isMiddleCard(rankA) && !isMiddleCard(rankB)) return 1; + if (!isMiddleCard(rankA) && isMiddleCard(rankB)) return -1; + } + + // 6. 最后按点数排序 + return Integer.compare(a, b); + }); + + return nonAdjacentCards.get(0); + } + + // 默认返回第一张牌 + return nonAdjacentCards.get(0); + } + + /** + * 检查一张牌是否是顺子中多余的牌(比如顺子234中多出的3) + * @param card 要检查的牌 + * @param analysis 手牌分析结果 + * @return 如果是顺子中多余的牌,返回true,否则返回false + */ + private boolean isExtraCardInSequence(int card, HandAnalysis analysis) { + // 获取牌的点数和花色 + int rank = card % 100; + int suit = card / 100; + + // 检查这张牌的数量 + int cardCount = analysis.cardCounts.getOrDefault(card, 0); + + // 如果这张牌的数量大于1,并且它参与了至少一个顺子 + if (cardCount > 1) { + // 检查这张牌是否在顺子中 + boolean isInSequence = false; + for (List meld : analysis.completedMelds) { + // 检查是否是顺子(不是刻子) + if (!(meld.get(0).equals(meld.get(1)) && meld.get(1).equals(meld.get(2)))) { + // 检查顺子中是否包含这张牌 + if (meld.contains(card)) { + isInSequence = true; + break; + } + } + } + + // 如果这张牌在顺子中,并且数量大于1,那么它可能是顺子中多余的牌 + if (isInSequence) { + // 计算这张牌在顺子中被使用的次数 + int usedInSequenceCount = 0; + for (List meld : analysis.completedMelds) { + if (!(meld.get(0).equals(meld.get(1)) && meld.get(1).equals(meld.get(2)))) { + for (int c : meld) { + if (c == card) { + usedInSequenceCount++; + } + } + } + } + + // 如果这张牌在顺子中被使用的次数小于它的总数量,那么它是顺子中多余的牌 + return usedInSequenceCount < cardCount; + } + } + + return false; + } + + // 判断边张牌是否有刻子或边搭的可能性 + private boolean hasEdgeCardPotential(int card, HandAnalysis analysis) { + // 使用增强版的潜力计算方法 + return calculateCardPotentialScore(card, analysis) > 0; + } + + // 判断是否为中张牌 + private boolean isMiddleCard(int rank) { + return rank >= 3 && rank <= 7; + } + + // 检查边张牌是否有刻子或边搭的可能性 - 使用统一的潜力计算方法 + + + /** + * 高级摸牌分析 - 全面检测摸牌后是否可听牌,并深入分析手牌结构 + * 在摸牌后、打牌前调用此方法进行全面分析,确定是否已经听牌或如何打出牌可以听牌 + * @param currentHand 摸牌前的手牌 + * @param drawnCard 刚摸到的牌 + * @return 当前手牌是否已经听牌 + */ + public boolean analyzeDrawCard(List currentHand, int drawnCard) { + System.out.println("机器人手牌" + currentHand); + // 创建包含摸牌后的手牌 + List newHand = new ArrayList<>(currentHand); +// newHand.add(drawnCard); + newHand.sort(Integer::compareTo); + drawnCard = drawnCards; + System.out.println("drawnCards ======= " + drawnCards); + logInfo("\n===== 开始摸牌分析 ====="); + logInfo("摸牌: " + drawnCard + ",摸牌后手牌: " + newHand); + + // 分析新的手牌结构和听牌状态 + HandAnalysis analysis = analyzeHand(newHand); + analysis.lastDrawnCard = drawnCard; // 记录最后摸的牌 + + + + // 额外检查:确保孤牌字段被正确更新 + // 如果摸牌后有新的牌型变化,可能需要重新计算孤牌 + reanalyzeIsolatedCards(analysis, newHand); + + // 输出详细分析结果 + logInfo("摸牌后分析结果:"); + logInfo("- 是否听牌: " + analysis.isTingPai); + if (analysis.isTingPai) { +// logInfo("- 可听的牌组: " + analysis.tingCards); +// // 分析听牌类型 +// String tingType = analyzeTingType(analysis); +// logInfo("- 听牌类型: " + tingType); +// // 评估听牌强度 +// int tingStrength = evaluateTingStrength(analysis); +// logInfo("- 听牌强度评分: " + tingStrength); + } else { + // 分析向听数和改进建议 +// logInfo("- 向听数: " + analysis.shantenCount); +// logInfo("- 改进建议: " + getImprovementSuggestions(analysis)); + } +// logInfo("- 红中数量: " + analysis.hongZhongCount); +// logInfo("- 已完成的面子数: " + analysis.meldCount); +// logInfo("- 对子数量: " + analysis.pairCount); +// logInfo("- 孤张牌: " + analysis.isolatedCards); + + // 特别分析新摸的牌对牌型的影响 + int newCardCount = 0; + for (int card : newHand) { + if (card == drawnCard) newCardCount++; + } + + logInfo("- 新摸牌 " + drawnCard + " 的数量: " + newCardCount); + + // 分析新摸牌的潜力 + int drawnCardPotential = calculateCardPotentialScore(drawnCard, analysis); + logInfo("- 新摸牌的潜力分数: " + drawnCardPotential); + + // 分析新摸牌的类型 + String cardType = analyzeCardType(drawnCard, newCardCount, analysis); +// logInfo("- 新摸牌类型: " + cardType); +// +// //分析打出哪张牌可以听牌 +// logInfo("\n分析打出哪张牌可以听牌:"); + Map> discardToTingOptions = findDiscardToTing(newHand); + + // 分析最佳出牌策略 + if (!discardToTingOptions.isEmpty()) { + analyzeBestDiscardStrategy(discardToTingOptions, analysis); + } + + // 将刻子、顺子、红中单独拎出后分析剩余牌,帮助确定出牌策略 + separateAndAnalyzeHand(newHand); + + logInfo("===== 摸牌分析完成 =====\n"); + + // 返回当前手牌是否已经听牌 + return analysis.isTingPai; + } + + /** + * 分析听牌类型 - 精确版 + * 更详细地识别不同类型的听牌 + * @param analysis 手牌分析结果对象 + * @return 听牌类型描述 + */ + /** + * 重新分析孤牌 - 在摸牌后调用,确保孤牌字段的准确性 + * @param analysis 手牌分析结果对象 + * @param handCards 完整的手牌(包括刚摸的牌) + */ + private void reanalyzeIsolatedCards(HandAnalysis analysis, List handCards) { + // 清空现有的孤牌列表 + analysis.isolatedCards.clear(); + + // 重新分析孤牌 + // 1. 首先找出所有没有被用于面子或对子的牌 + List potentialIsolated = new ArrayList<>(); + for (Map.Entry> entry : analysis.cardsBySuit.entrySet()) { + for (int card : entry.getValue()) { + if (!analysis.usedInMelds.contains(card) && !analysis.usedInPairs.contains(card)) { + potentialIsolated.add(card); + } + } + } + + // 2. 对每个潜在的孤牌,检查它是否真的孤立(没有相邻牌) + for (int card : potentialIsolated) { + int rank = card % 100; + int suit = card / 100; + boolean isIsolated = true; + + // 检查前一张牌是否存在 + if (rank > 1) { + int prevCard = suit * 100 + (rank - 1); + if (analysis.cardCounts.getOrDefault(prevCard, 0) > 0) { + isIsolated = false; + } + } + + // 检查后一张牌是否存在 + if (rank < 9 && isIsolated) { + int nextCard = suit * 100 + (rank + 1); + if (analysis.cardCounts.getOrDefault(nextCard, 0) > 0) { + isIsolated = false; + } + } + + // 检查红中的影响(如果有红中,单张牌不那么孤立) + if (isIsolated && analysis.hongZhongCount > 0) { + // 有红中时,评估这张牌的潜力 + int potentialScore = calculateCardPotentialScore(card, analysis); + if (potentialScore > 10) { // 设置一个阈值,潜力较高的牌不视为孤牌 + isIsolated = false; + } + } + + if (isIsolated) { + analysis.isolatedCards.add(card); + } + } + + logInfo("重新分析后的孤牌: " + analysis.isolatedCards); + } + + /** + * 分析听牌类型 - 精确版 + * 更详细地识别不同类型的听牌 + */ + private String analyzeTingType(HandAnalysis analysis) { + Set tingCards = analysis.tingCards; + int tingCardCount = tingCards.size(); + + if (tingCardCount == 0) { + return "未知听牌类型"; + } + + // 统计各花色的听牌数量 + Map suitTingCount = new HashMap<>(); + for (int card : tingCards) { + int suit = card / 100; + suitTingCount.put(suit, suitTingCount.getOrDefault(suit, 0) + 1); + } + + // 检查是否为清一色或混一色听牌 + boolean isPureSuit = suitTingCount.size() == 1 && !suitTingCount.containsKey(HONG_ZHONG / 100); + + // 检查红中的影响 + boolean hasHongZhongImpact = analysis.hongZhongCount > 0 && analysis.hongZhongContributedTingCards > 0; + + // 详细的听牌类型判断 + if (tingCardCount == 1) { + return "单调张听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } else if (tingCardCount == 2) { + // 检查是否为两面听 + List sortedTingCards = new ArrayList<>(tingCards); + Collections.sort(sortedTingCards); + int suit1 = sortedTingCards.get(0) / 100; + int suit2 = sortedTingCards.get(1) / 100; + int rank1 = sortedTingCards.get(0) % 100; + int rank2 = sortedTingCards.get(1) % 100; + + if (suit1 == suit2 && Math.abs(rank1 - rank2) == 2) { + return "两面听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } else { + return "双碰听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } + } else if (tingCardCount == 3) { + // 检查是否为三面听(如1,4,7或3,6,9) + List sortedTingCards = new ArrayList<>(tingCards); + Collections.sort(sortedTingCards); + boolean sameSuit = true; + int suit = sortedTingCards.get(0) / 100; + for (int card : sortedTingCards) { + if (card / 100 != suit) { + sameSuit = false; + break; + } + } + + if (sameSuit) { + int rank1 = sortedTingCards.get(0) % 100; + int rank2 = sortedTingCards.get(1) % 100; + int rank3 = sortedTingCards.get(2) % 100; + + if ((rank1 == 1 && rank2 == 4 && rank3 == 7) || + (rank1 == 3 && rank2 == 6 && rank3 == 9)) { + return "三面听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } + } + + return "多面听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } else if (tingCardCount >= 4 && tingCardCount <= 6) { + return "多面听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } else { + // 大量听牌,可能是清幺九或其他特殊牌型 + if (isPureSuit) { + return "清一色广听牌" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } else { + return "广听牌(听多张)" + (hasHongZhongImpact ? "(红中辅助)" : ""); + } + } + } + + /** + * 评估听牌强度 - 高级版 + * 综合考虑多种因素进行精确的听牌强度评估 + * @param analysis 手牌分析结果对象 + * @return 听牌强度评分(0-100) + */ + private int evaluateTingStrength(HandAnalysis analysis) { + int strength = 0; + Set tingCards = analysis.tingCards; + int tingCardCount = tingCards.size(); + + // 基础分:根据听牌数量(非线性增长) + if (tingCardCount == 1) { + strength += 20; + } else if (tingCardCount == 2) { + strength += 35; + } else if (tingCardCount == 3) { + strength += 50; + } else if (tingCardCount == 4) { + strength += 60; + } else if (tingCardCount == 5) { + strength += 70; + } else if (tingCardCount >= 6) { + strength += 80; + } + + // 加分:中张听牌(3-7)- 更易胡牌 + for (int card : tingCards) { + int rank = card % 100; + if (rank >= 3 && rank <= 7) { + strength += 3; + } else if (rank == 2 || rank == 8) { + strength += 1; + } else if (rank == 1 || rank == 9) { + strength -= 1; + } + } + + // 加分:已完成面子多 + strength += analysis.meldCount * 7; + + // 加分:对子数量适中 + if (analysis.pairCount == 1) { + strength += 5; + } else if (analysis.pairCount > 1) { + strength -= analysis.pairCount - 1; + } + + // 红中影响评估 + if (analysis.hongZhongCount == 0) { + // 无红中听牌,难度较大,给予额外加分 + strength += 10; + } else if (analysis.hongZhongCount == 1) { + strength += 7; + } else if (analysis.hongZhongCount == 2) { + strength += 4; + } else if (analysis.hongZhongCount >= 3) { + // 红中过多可能限制牌型,给予少量减分 + strength -= Math.min(analysis.hongZhongCount - 2, 5); + } + + // 减分:孤张多 + strength -= analysis.isolatedCards.size() * 4; + + // 加分:听牌中的新摸牌 + if (analysis.isTingPai && tingCards.contains(analysis.lastDrawnCard)) { + strength += 5; + } + + // 加分:听牌组的花色多样性(避免只听一种花色) + Map suitTingCount = new HashMap<>(); + for (int card : tingCards) { + int suit = card / 100; + suitTingCount.put(suit, suitTingCount.getOrDefault(suit, 0) + 1); + } + if (suitTingCount.size() > 1) { + strength += 5; + } + + // 确保分数在合理范围内 + return Math.max(0, Math.min(strength, 100)); + } + + /** + * 获取手牌改进建议 + * @param analysis 手牌分析结果对象 + * @return 改进建议 + */ + private String getImprovementSuggestions(HandAnalysis analysis) { + StringBuilder suggestions = new StringBuilder(); + + if (analysis.shantenCount == 1) { + suggestions.append("接近听牌,优先考虑打出孤张牌。"); + } else if (analysis.shantenCount == 2) { + suggestions.append("需要整理手牌,形成更多对子或顺子。"); + } else { + suggestions.append("手牌需要大幅调整,优先保留有潜力的牌组。"); + } + + if (analysis.isolatedCards.size() > 3) { + suggestions.append(" 考虑打出多余的孤张牌。"); + } + + if (analysis.hongZhongCount > 3) { + suggestions.append(" 红中数量过多,可以考虑杠牌或保留。"); + } + + return suggestions.toString(); + } + + /** + * 分析牌的类型 + * @param card 牌 + * @param count 牌的数量 + * @param analysis 手牌分析结果对象 + * @return 牌类型描述 + */ + private String analyzeCardType(int card, int count, HandAnalysis analysis) { + if (card == HONG_ZHONG) { + return "红中(万能牌)"; + } + + if (analysis.pairs.contains(card)) { + return "对子牌"; + } + + if (analysis.isolatedCards.contains(card)) { + return "孤张牌"; + } + + if (count == 3) { + return "刻子牌"; + } + + int rank = card % 100; + if (rank == 1 || rank == 9) { + return "幺九牌"; + } else if (rank >= 3 && rank <= 7) { + return "中张牌"; + } else { + return "边张牌"; + } + } + + /** + * 分析最佳出牌策略 + * @param discardToTingOptions 出牌听牌选项 + * @param analysis 手牌分析结果对象 + */ + private void analyzeBestDiscardStrategy(Map> discardToTingOptions, HandAnalysis analysis) { + // 找出听牌组最多的出牌选项 + int maxTingCards = 0; + int bestCardToDiscard = -1; + + for (Map.Entry> entry : discardToTingOptions.entrySet()) { + int card = entry.getKey(); + Set tingCards = entry.getValue(); + + if (tingCards.size() > maxTingCards) { + maxTingCards = tingCards.size(); + bestCardToDiscard = card; + } + } + + if (bestCardToDiscard != -1) { + logInfo("最佳出牌策略: 打出 " + bestCardToDiscard + " 可以听 " + maxTingCards + " 张牌"); + logInfo("听牌组: " + discardToTingOptions.get(bestCardToDiscard)); + } + + // 分析不同出牌选项的优劣 + for (Map.Entry> entry : discardToTingOptions.entrySet()) { + int card = entry.getKey(); + Set tingCards = entry.getValue(); + + // 评估这个出牌选项的价值 + int value = evaluateDiscardOption(card, tingCards, analysis); + logInfo("出牌选项评估: 打出 " + card + " (价值: " + value + ") -> 听牌: " + tingCards); + } + } + + /** + * 评估出牌选项的价值 + * @param card 要打出的牌 + * @param tingCards 听牌组 + * @param analysis 手牌分析结果对象 + * @return 价值评分 + */ + private int evaluateDiscardOption(int card, Set tingCards, HandAnalysis analysis) { + int value = 0; + + // 基础分:听牌数量 + value += tingCards.size() * 10; + + // 减分:如果是有价值的牌 + if (analysis.cardUsefulness.containsKey(card)) { + value -= analysis.cardUsefulness.get(card) / 5; + } + + // 加分:中张听牌多 + for (int tingCard : tingCards) { + int rank = tingCard % 100; + if (rank >= 3 && rank <= 7) { + value += 2; + } + } + + // 加分:如果是孤张牌 + if (analysis.isolatedCards.contains(card)) { + value += 10; + } + + return value; + } + + /** + * 将刻子、顺子、红中单独拎出,然后分析剩余的牌 + * @param hand 当前手牌 + */ + public void separateAndAnalyzeHand(List hand) { + System.out.println("\n开始分离分析手牌..."); + + // 分析手牌 + HandAnalysis analysis = analyzeHand(hand); + + // 统计原始手牌中每张牌的数量 + Map originalCounts = new HashMap<>(); + for (int card : hand) { + originalCounts.put(card, originalCounts.getOrDefault(card, 0) + 1); + } + + // 分离出刻子 + List kziCards = new ArrayList<>(); + Map usedInKzi = new HashMap<>(); + for (List meld : analysis.completedMelds) { + if (meld.get(0).equals(meld.get(1)) && meld.get(1).equals(meld.get(2))) { + kziCards.addAll(meld); + int card = meld.get(0); + usedInKzi.put(card, usedInKzi.getOrDefault(card, 0) + 3); + } + } + + // 分离出顺子 + List shunziCards = new ArrayList<>(); + Map usedInShunzi = new HashMap<>(); + for (List meld : analysis.completedMelds) { + if (!(meld.get(0).equals(meld.get(1)) && meld.get(1).equals(meld.get(2)))) { + shunziCards.addAll(meld); + // 统计顺子中每张牌的使用次数 + for (int card : meld) { + usedInShunzi.put(card, usedInShunzi.getOrDefault(card, 0) + 1); + } + } + } + + // 分离出红中 + List hongzhongCards = new ArrayList<>(); + for (int card : hand) { + if (card == HONG_ZHONG) { + hongzhongCards.add(card); + } + } + + // 计算剩余需要分析的牌(处理顺子中多余的牌) + List remainingCards = new ArrayList<>(); + for (Map.Entry entry : originalCounts.entrySet()) { + int card = entry.getKey(); + int totalCount = entry.getValue(); + + // 跳过红中 + if (card == HONG_ZHONG) { + continue; + } + + // 减去刻子和顺子中使用的数量 + int usedCount = usedInKzi.getOrDefault(card, 0) + usedInShunzi.getOrDefault(card, 0); + int remainingCount = totalCount - usedCount; + + // 将剩余的牌添加到remainingCards中 + for (int i = 0; i < remainingCount; i++) { + remainingCards.add(card); + } + } + + // 输出分离结果 + System.out.println("分离结果:"); + System.out.println("- 刻子: " + kziCards); + System.out.println("- 顺子: " + shunziCards); + System.out.println("- 红中: " + hongzhongCards); + System.out.println("- 剩余待分析的牌: " + remainingCards); + + // 更新analysis中的剩余牌 + analysis.remainingCards = remainingCards; + + // 分析剩余牌的结构 + analyzeRemainingCards(remainingCards, analysis); + } + + /** + * 分析剩余需要处理的牌的结构 + * @param remainingCards 剩余的牌 + * @param analysis 手牌分析结果 + */ + private void analyzeRemainingCards(List remainingCards, HandAnalysis analysis) { + if (remainingCards.isEmpty()) { + System.out.println("没有剩余需要分析的牌,手牌结构完整"); + return; + } + + // 统计剩余牌的数量 + Map cardCounts = new HashMap<>(); + for (int card : remainingCards) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + } + + // 识别对子、搭子和孤张 + List pairs = new ArrayList<>(); + List triples = new ArrayList<>(); // 三张(可拆分成刻子) + List搭子 = new ArrayList<>(); + Set usedInPairsOrTriples = new HashSet<>(); + + // 首先识别对子和三张 + for (Map.Entry entry : cardCounts.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + if (count >= 3) { + triples.add(card); + usedInPairsOrTriples.add(card); + } else if (count == 2) { + pairs.add(card); + usedInPairsOrTriples.add(card); + } + } + + // 将剩余的牌按花色分组进行搭子分析 + Map> suitGroups = new HashMap<>(); + for (Map.Entry entry : cardCounts.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + int suit = card / 100; + + // 只考虑未被用于对子或三张的牌,以及数量>1的情况 + if (!usedInPairsOrTriples.contains(card) || count > 2) { + suitGroups.computeIfAbsent(suit, k -> new ArrayList<>()).add(card); + } + } + + // 对每个花色单独分析搭子 + Set usedIn搭子 = new HashSet<>(); + for (List suitCards : suitGroups.values()) { + // 按点数排序 + suitCards.sort((a, b) -> Integer.compare(a % 100, b % 100)); + + // 检查边搭(1-2, 8-9) + for (int i = 0; i < suitCards.size() - 1; i++) { + int card1 = suitCards.get(i); + int card2 = suitCards.get(i + 1); + int rank1 = card1 % 100; + int rank2 = card2 % 100; + + // 边搭:1-2 或 8-9 + if (Math.abs(rank1 - rank2) == 1) { + if ((rank1 == 1 && rank2 == 2) || (rank1 == 8 && rank2 == 9)) { + if (!usedIn搭子.contains(card1) && !usedIn搭子.contains(card2)) { + 搭子.add(card1); + 搭子.add(card2); + usedIn搭子.add(card1); + usedIn搭子.add(card2); + } + } + } + } + + // 检查两面搭(中间的连续牌) + for (int i = 0; i < suitCards.size() - 1; i++) { + int card1 = suitCards.get(i); + int card2 = suitCards.get(i + 1); + int rank1 = card1 % 100; + int rank2 = card2 % 100; + + // 两面搭:2-3, 3-4, ..., 7-8 + if (Math.abs(rank1 - rank2) == 1 && + rank1 > 1 && rank1 < 8 && rank2 > 2 && rank2 < 9) { + if (!usedIn搭子.contains(card1) && !usedIn搭子.contains(card2)) { + 搭子.add(card1); + 搭子.add(card2); + usedIn搭子.add(card1); + usedIn搭子.add(card2); + } + } + } + + // 检查坎搭(间隔一张的牌) + for (int i = 0; i < suitCards.size() - 1; i++) { + for (int j = i + 1; j < suitCards.size(); j++) { + int card1 = suitCards.get(i); + int card2 = suitCards.get(j); + int rank1 = card1 % 100; + int rank2 = card2 % 100; + + // 坎搭:间隔一张,如1-3, 2-4等 + if (Math.abs(rank1 - rank2) == 2) { + if (!usedIn搭子.contains(card1) && !usedIn搭子.contains(card2)) { + 搭子.add(card1); + 搭子.add(card2); + usedIn搭子.add(card1); + usedIn搭子.add(card2); + } + } + } + } + } + + // 找出孤张牌(未被用于对子、三张或搭子的单张) + List isolatedCards = new ArrayList<>(); + for (Map.Entry entry : cardCounts.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + // 单张且未被用于搭子 + if (count == 1 && !usedIn搭子.contains(card)) { + isolatedCards.add(card); + } + } + + // 更新analysis中的孤张牌信息,确保顺子中多余的牌被正确识别为孤张 + analysis.isolatedCards = isolatedCards; + + System.out.println("剩余牌分析:"); + System.out.println("- 三张: " + triples); + System.out.println("- 对子: " + pairs); + System.out.println("- 搭子: " + 搭子); + System.out.println("- 孤张: " + isolatedCards); + + // 计算剩余牌需要的张数来完成牌型 + int neededMelds = 4 - analysis.meldCount; + int neededPairs = (analysis.pairCount > 0) ? 0 : 1; + System.out.println("还需要完成的面子数: " + neededMelds); + System.out.println("还需要的对子数: " + neededPairs); + + // 分析牌的冗余情况 + if (!isolatedCards.isEmpty()) { + System.out.println("存在孤张牌,建议考虑打出:" + isolatedCards); + } + } + + /** + * 分析打出哪张牌可以听牌 + * 在摸牌后、打牌前的关键步骤,找出最优出牌选择 + * 通过模拟打出每一张牌,检测是否能进入听牌状态 + * + * @param currentHand 当前手牌(包含刚摸的牌) + * @return 可以打出的牌列表,以及每张牌打出后可以听的牌组 + */ + public Map> findDiscardToTing(List currentHand) { + Map> discardToTingMap = new HashMap<>(); + logInfo("\n开始分析打出哪张牌可以听牌..."); + + // 对手牌进行深度复制,避免修改原手牌 + List handCopy = new ArrayList<>(currentHand); + handCopy.sort(Integer::compareTo); + + // 统计每种牌的数量,避免重复分析相同的牌 + Map cardCounts = new HashMap<>(); + for (int card : handCopy) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + } + + // 尝试打出每一种不同的牌,进行模拟分析 + for (Map.Entry entry : cardCounts.entrySet()) { + int cardToDiscard = entry.getKey(); + int count = entry.getValue(); + + // 红中通常不会打出,但仍然分析可能性 + if (cardToDiscard == HONG_ZHONG) { + logInfo("- 通常不建议打出红中: " + cardToDiscard); + continue; + } + + // 模拟打出这张牌 + List tempHand = new ArrayList<>(handCopy); + tempHand.remove(Integer.valueOf(cardToDiscard)); + + // 分析打出这张牌后的手牌是否听牌 + HandAnalysis analysis = analyzeHand(tempHand); + + // 如果听牌,记录这张牌和对应的听牌组 + if (analysis.isTingPai && !analysis.tingCards.isEmpty()) { + discardToTingMap.put(cardToDiscard, analysis.tingCards); + logInfo("- 打出牌: " + cardToDiscard + " 后可以听牌"); + logInfo(" 听的牌组: " + analysis.tingCards); + } + } + + // 输出分析结果 + if (discardToTingMap.isEmpty()) { + logInfo("- 当前没有可以打出后听牌的牌"); + } else { + logInfo("\n可打出后听牌的牌数量: " + discardToTingMap.size()); + logInfo("建议打出的牌及其对应的听牌组:"); + for (Map.Entry> entry : discardToTingMap.entrySet()) { + logInfo("- 牌: " + entry.getKey() + " -> 听牌组: " + entry.getValue()); + } + } + + return discardToTingMap; + } + + // 碰牌判断 - 实现碰牌规则 + public boolean shouldPong(int proposedCard, List currentHand) { + // 分析当前手牌 + HandAnalysis analysis = analyzeHand(currentHand); + System.out.println("----"+analysis.pairCount); + + // 不能碰听牌组中的牌 + if (analysis.tingCards.contains(proposedCard)) { + System.out.println("不能碰: 不能碰听牌组中的牌 " + proposedCard); + return false; + } + + if (analysis.isTingPai){ + System.out.println("听牌中不能碰" + proposedCard); + return false; + } + + // 规则2: 有5个对子时不能碰已成对子的牌 + if (analysis.hasLongQiDuiPotential==true && analysis.pairs.contains(proposedCard)) { + System.out.println("不能碰: 已有5个或更多对子时不能碰已成对子的牌 " + proposedCard); + return false; + } + + // 规则3: 顺子的情况下不能碰 + if (isCardInSequence(proposedCard, analysis)) { + System.out.println("不能碰: 牌在顺子中的情况下不能碰 " + proposedCard); + return false; + } + + // 基础碰牌条件: 手中已有两张相同的牌 + int cardCount = 0; + for (int card : currentHand) { + if (card == proposedCard) { + cardCount++; + } + } + + boolean canPong = cardCount >= 2; + System.out.println("是否可以碰牌 " + proposedCard + ": " + canPong); + + return canPong; + } + + // 检查牌是否在顺子中 + private boolean isCardInSequence(int card, HandAnalysis analysis) { + // 遍历已完成的顺子 + for (List meld : analysis.completedMelds) { + // 如果顺子中有这张牌,且不是刻子(刻子是三张相同的牌) + if (meld.contains(card) && !(meld.get(0).equals(meld.get(1)) && meld.get(1).equals(meld.get(2)))) { + return true; + } + } + return false; + } + + /** + * 计算牌的潜力分数 - 评估一张牌在当前手牌中的价值和发展潜力 + * @param card 要评估的牌 + * @param analysis 手牌分析结果 + * @return 潜力分数,越高表示越有价值 + */ + private int calculateCardPotentialScore(int card, HandAnalysis analysis) { + int score = 0; + int rank = card % 100; + int suit = card / 100; + + // 1. 中张牌(3-7)潜力更高 + if (rank >= 3 && rank <= 7) { + score += 8; + } else if (rank == 2 || rank == 8) { + score += 4; + } + + // 2. 红中的影响 - 每有一个红中增加一定潜力 + score += analysis.hongZhongCount * 5; + + // 3. 检查是否有半搭子(有一张相邻牌) + boolean hasPrev = false; + boolean hasNext = false; + + if (rank > 1) { + int prevCard = suit * 100 + (rank - 1); + hasPrev = analysis.cardCounts.getOrDefault(prevCard, 0) > 0; + } + + if (rank < 9) { + int nextCard = suit * 100 + (rank + 1); + hasNext = analysis.cardCounts.getOrDefault(nextCard, 0) > 0; + } + + if (hasPrev || hasNext) { + score += 15; + } + + // 4. 同花色牌数量越多,单张牌的潜力也越高 + if (analysis.cardsBySuit.containsKey(suit)) { + int suitCardCount = analysis.cardsBySuit.get(suit).size(); + score += suitCardCount * 2; + } + + // 5. 检查牌是否在可能的听牌组附近 + for (int tingCard : analysis.tingCards) { + int tingRank = tingCard % 100; + int tingSuit = tingCard / 100; + + if (tingSuit == suit) { + // 如果牌和听牌组在同一花色且相邻,增加潜力 + if (Math.abs(tingRank - rank) <= 1) { + score += 20; + break; + } + } + } + + return score; + } + + + + + + + + + /** + * 测试单个听牌场景,包含异常处理 + */ + private void testSingleTingScenario(String scenarioName, List handCards) { + logInfo("\n测试场景: " + scenarioName); + logInfo("测试手牌: " + handCards); + + try { + long startTime = System.currentTimeMillis(); + HandAnalysis analysis = analyzeHand(handCards); + long endTime = System.currentTimeMillis(); + + logInfo("分析结果: 成功"); + logInfo("是否听牌: " + analysis.isTingPai); + logInfo("听牌组: " + analysis.tingCards); + logInfo("听牌数量: " + analysis.tingCards.size()); + logInfo("向听数: " + analysis.shantenCount); + logInfo("红中数量: " + analysis.hongZhongCount); + logInfo("红中贡献听牌: " + analysis.hongZhongContributedTingCards); + logInfo("分析耗时: " + (endTime - startTime) + "ms"); + } catch (Exception e) { + logInfo("分析结果: 异常 - " + e.getMessage()); + logInfo("异常类型: " + e.getClass().getSimpleName()); + e.printStackTrace(); + } + } + + /** + * 执行性能对比测试,比较优化前后的算法性能 + */ + private void performPerformanceComparison() { + // 创建复杂的测试手牌 + List complexHand = Arrays.asList( + 101, 102, 103, 104, 105, // 一至五万 + 206, 207, 208, 209, 210, // 六至十条 + 301, 302, 303, 412, 412 // 一至三筒和两个红中 + ); + + // 测试多次取平均值 + int iterations = 100; + long totalTimeOptimized = 0; + + logInfo("执行" + iterations + "次优化版听牌检测测试..."); + + for (int i = 0; i < iterations; i++) { + HandAnalysis analysis = new HandAnalysis(); + // 准备分析数据 + for (int card : complexHand) { + if (card == HONG_ZHONG) { + analysis.hongZhongCount++; + } else { + int suit = card / 100; + analysis.cardsBySuit.computeIfAbsent(suit, k -> new ArrayList<>()).add(card); + // 统计每种牌的数量 + analysis.cardCounts.put(card, analysis.cardCounts.getOrDefault(card, 0) + 1); + } + } + analysis.shantenCount = 0; + + // 测量优化版算法时间 + long start = System.nanoTime(); + findTingCardsOptimized(analysis); + long end = System.nanoTime(); + totalTimeOptimized += (end - start) / 1000; // 转换为微秒 + } + + double avgTimeOptimized = totalTimeOptimized / (double) iterations; + + logInfo("优化版算法平均耗时: " + avgTimeOptimized + " μs"); + logInfo("优化版算法总耗时: " + (totalTimeOptimized / 1000.0) + " ms"); + } + + /** + * 分析手牌是否为听牌状态 + * @param handCards 当前手牌 + * @return 返回一个Map,其中key为是否听牌,value为听牌的集合 + * @throws IllegalArgumentException 当输入参数无效时抛出 + */ + public Map> analyzeTingPaiStatus(List handCards, HandAnalysis analysis) { + // 输入验证 + if (handCards == null) { + throw new IllegalArgumentException("手牌列表不能为空"); + } + + Map> result = new HashMap<>(); + Set tingCards = new HashSet<>(); + boolean isTing = false; + + try { + // 边界条件检查:空的手牌列表 + if (handCards.isEmpty()) { + logInfo("警告:手牌列表为空,无法分析听牌状态"); + result.put(false, tingCards); + return result; + } + + // 验证手牌中的牌值是否有效 + if (!validateHandCards(handCards)) { + logInfo("警告:手牌中包含无效的牌值"); + result.put(false, tingCards); + return result; + } + + // 对手牌进行分析 +// HandAnalysis analysis = analyzeHand(handCards); + + // 防止analysis为null + if (analysis == null) { + logInfo("错误:手牌分析结果为null"); + result.put(false, tingCards); + return result; + } + + // 检查是否听牌 + checkTingPai(analysis); + + // 查找听的牌 + findTingCards(analysis); + + // 执行备用的听牌分析,确保全面检查 + performBackupTingAnalysis(analysis); + + // 分析红中对听牌的影响 + analyzeHongZhongImpactOnTingCards(analysis); + + // 提取听牌信息 + isTing = analysis.isTingPai; + + // 防止tingCards为null + if (analysis.tingCards != null) { + tingCards = analysis.tingCards; + } + + // 如果手牌加上任何一张可能的牌能胡牌,也应该认为是听牌 + if (!isTing) { + try { + // 尝试所有可能的牌,检查是否有补牌能让手牌胡牌 + for (int potentialCard : getAllPossibleCards()) { + List testHand = new ArrayList<>(handCards); + testHand.add(potentialCard); + + HandAnalysis testAnalysis = analyzeHand(testHand); + if (testAnalysis != null) { + checkTingPai(testAnalysis); + + if (testAnalysis.isTingPai) { + isTing = true; + tingCards.add(potentialCard); + } + } + } + } catch (Exception e) { + logInfo("检查潜在听牌时出错: " + e.getMessage()); + // 继续执行,不影响主要结果 + } + } + } catch (IllegalArgumentException e) { + // 重新抛出参数异常,这是一个需要调用者处理的错误 + throw e; + } catch (Exception e) { + logInfo("分析听牌状态时出错: " + e.getMessage()); + e.printStackTrace(); + // 即使出错也要返回一个有效的结果 + } finally { + // 确保结果map总是包含布尔值和集合 + if (!tingCards.isEmpty()) { + result.put(isTing, tingCards); + } else { + result.put(false, Collections.emptySet()); + } + } + + return result; + } + + /** + * 验证手牌中的牌值是否有效 + * @param handCards 手牌列表 + * @return 牌值是否全部有效 + */ + private boolean validateHandCards(List handCards) { + // 基本的牌值范围检查 + for (int card : handCards) { + // 检查牌值是否在合理范围内 + if (card < 100 || card > 503) { + logInfo("无效的牌值: " + card); + return false; + } + + int suit = card / 100; + int rank = card % 100; + + // 验证花色和点数是否有效 + if (suit < 1 || suit > 5) { + logInfo("无效的花色: " + suit); + return false; + } + + // 万、筒、条(花色1-3)的点数应为1-9 + if (suit >= 1 && suit <= 3) { + if (rank < 1 || rank > 9) { + logInfo("无效的点数: " + rank + " 对于花色: " + suit); + return false; + } + } + // 风牌(花色4)的点数应为1-4 + else if (suit == 4) { + if (rank < 1 || rank > 4) { + logInfo("无效的风牌点数: " + rank); + return false; + } + } + // 箭牌(花色5)的点数应为1-3 + else if (suit == 5) { + if (rank < 1 || rank > 3) { + logInfo("无效的箭牌点数: " + rank); + return false; + } + } + } + + return true; + } + + /** + * 获取所有可能的麻将牌(万、筒、条、风牌、箭牌) + * @return 所有可能的麻将牌的列表 + */ + private List getAllPossibleCards() { + List allCards = new ArrayList<>(); + + // 添加万、筒、条(花色1-3,数字1-9) + for (int suit = 1; suit <= 3; suit++) { + for (int rank = 1; rank <= 9; rank++) { + allCards.add(suit * 100 + rank); + } + } + + // 添加风牌(东、南、西、北) + for (int wind = 1; wind <= 4; wind++) { + allCards.add(400 + wind); + } + + // 添加箭牌(中、发、白) + for (int dragon = 1; dragon <= 3; dragon++) { + allCards.add(500 + dragon); + } + + return allCards; + } + + /** + * 验证当手牌添加某张特定牌后是否能胡牌 + * @param handCards 当前手牌 + * @param targetCard 待验证的牌 + * @return 是否能胡牌 + */ + public boolean verifySingleCardCanWin(List handCards, int targetCard) { + if (handCards == null || handCards.isEmpty()) { + return false; + } + + try { + // 创建测试手牌,添加目标牌 + List testHand = new ArrayList<>(handCards); + testHand.add(targetCard); + + // 分析测试手牌 + HandAnalysis analysis = analyzeHand(testHand); + + // 检查是否听牌(实际上在胡牌的情况下,应该是直接能胡) + checkTingPai(analysis); + + // 如果听牌分析认为可以胡牌,返回true + if (analysis.isTingPai) { + return true; + } + + // 额外检查:验证是否能通过不同的牌型组合方式胡牌 + // 检查是否符合基本胡牌条件(四组合一对) + return checkBasicWinCondition(testHand, analysis); + } catch (Exception e) { + logInfo("验证单张牌是否能胡时出错: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * 检查基本胡牌条件(四组合一对) + * @param handCards 手牌 + * @param analysis 手牌分析结果 + * @return 是否符合基本胡牌条件 + */ + private boolean checkBasicWinCondition(List handCards, HandAnalysis analysis) { + // 对手牌进行排序,便于分析 + List sortedHand = new ArrayList<>(handCards); + Collections.sort(sortedHand); + + // 计算牌的数量分布 + Map cardCounts = new HashMap<>(); + for (int card : sortedHand) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + } + + // 检查是否可以形成一对将牌,其余为四组刻子或顺子 + return canFormValidMelds(sortedHand, cardCounts, analysis); + } + + /** + * 递归检查是否可以形成有效的牌组(刻子或顺子)和一对将牌 + * @param handCards 手牌 + * @param cardCounts 牌的数量分布 + * @param analysis 手牌分析结果 + * @return 是否可以形成有效的牌组 + */ + private boolean canFormValidMelds(List handCards, Map cardCounts, HandAnalysis analysis) { + // 尝试将每一张有至少两张的牌作为将牌 + for (Map.Entry entry : cardCounts.entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + if (count >= 2) { + // 创建临时的牌数量分布,减去一对将牌 + Map tempCounts = new HashMap<>(cardCounts); + tempCounts.put(card, count - 2); + + // 检查剩余的牌是否可以形成四组刻子或顺子 + if (checkRemainingCards(tempCounts)) { + return true; + } + } + } + + // 没有找到合适的将牌和牌组组合 + return false; + } + + /** + * 检查剩余的牌是否可以形成刻子或顺子 + * @param cardCounts 牌的数量分布 + * @return 是否可以形成有效的牌组 + */ + private boolean checkRemainingCards(Map cardCounts) { + // 递归终止条件:所有牌都被使用 + if (cardCounts.values().stream().allMatch(count -> count == 0)) { + return true; + } + + // 尝试每一张还有剩余的牌 + for (Map.Entry entry : new HashMap<>(cardCounts).entrySet()) { + int card = entry.getKey(); + int count = entry.getValue(); + + if (count == 0) { + continue; + } + + // 获取牌的花色和点数 + int suit = card / 100; + int rank = card % 100; + + // 尝试组成刻子(3张相同的牌) + if (count >= 3) { + Map tempCounts = new HashMap<>(cardCounts); + tempCounts.put(card, count - 3); + + if (checkRemainingCards(tempCounts)) { + return true; + } + } + + // 对于万、筒、条,尝试组成顺子(3张连续的牌) + if (suit >= 1 && suit <= 3 && rank <= 7) { + int nextCard = suit * 100 + (rank + 1); + int nextNextCard = suit * 100 + (rank + 2); + + if (cardCounts.getOrDefault(nextCard, 0) > 0 && + cardCounts.getOrDefault(nextNextCard, 0) > 0) { + + Map tempCounts = new HashMap<>(cardCounts); + tempCounts.put(card, count - 1); + tempCounts.put(nextCard, tempCounts.get(nextCard) - 1); + tempCounts.put(nextNextCard, tempCounts.get(nextNextCard) - 1); + + if (checkRemainingCards(tempCounts)) { + return true; + } + } + } + } + + return false; + } + + /** + * 综合测试函数:验证单张牌是否能让手牌胡牌 + * @param handCards 玩家手牌 + * @param testCard 要测试的单张牌 + * @param hongZhongCount 红中数量 + * @param tableCards 桌面已打出的牌(用于检查是否点炮等特殊规则) + * @return 胡牌结果信息对象 + */ + public WinResult verifySingleCardCanWin(List handCards, int testCard, int hongZhongCount, List tableCards) { + WinResult result = new WinResult(); + result.testCard = testCard; + result.canWin = false; + result.winTypes = new ArrayList<>(); + result.handCards = new ArrayList<>(handCards); + result.hongZhongCount = hongZhongCount; + + // 开始计时,用于性能统计 + long startTime = System.currentTimeMillis(); + + try { + // 创建测试手牌(将测试牌加入手牌) + List testHandCards = new ArrayList<>(handCards); + testHandCards.add(testCard); + + // 1. 检测是否为正常胡牌(标准型) + boolean normalWin = checkNormalWin(testHandCards, hongZhongCount); + if (normalWin) { + result.canWin = true; + result.winTypes.add("NORMAL"); + } + + // 2. 检测是否为七对子胡牌 + boolean sevenPairsWin = checkSevenPairsWin(testHandCards, hongZhongCount); + if (sevenPairsWin) { + result.canWin = true; + result.winTypes.add("SEVEN_PAIRS"); + } + + // 3. 检测是否为十三幺胡牌 + boolean thirteenOrphansWin = checkThirteenOrphansWin(testHandCards); + if (thirteenOrphansWin) { + result.canWin = true; + result.winTypes.add("THIRTEEN_ORPHANS"); + } + + // 4. 检测是否为特殊牌型胡牌(如清一色、一条龙等) + List specialWinTypes = checkSpecialWinTypes(testHandCards, hongZhongCount); + if (!specialWinTypes.isEmpty()) { + result.canWin = true; + result.winTypes.addAll(specialWinTypes); + } + + // 5. 如果可以胡牌,计算胡牌分数 + if (result.canWin) { + result.winScore = calculateWinScore(result.winTypes, hongZhongCount); + } + + } catch (Exception e) { + logInfo("验证胡牌时出错: " + e.getMessage()); + result.error = e.getMessage(); + } + + // 记录执行时间 + result.executionTime = System.currentTimeMillis() - startTime; + + return result; + } + + /** + * 检查正常胡牌(标准型) + */ + private boolean checkNormalWin(List handCards, int hongZhongCount) { + // 这里应该调用WinSplitCard类的方法进行检测 + // 简化实现,实际需要调用已有胡牌检测逻辑 + try { + // 对输入参数进行预处理,将手牌转换为正确的格式 + // 然后调用WinSplitCard的检测方法 + // 简化返回,实际需要正确实现 + return handCards.size() % 3 == 2 || hongZhongCount > 0; + } catch (Exception e) { + logInfo("检查正常胡牌出错: " + e.getMessage()); + return false; + } + } + + /** + * 检查七对子胡牌 + */ + private boolean checkSevenPairsWin(List handCards, int hongZhongCount) { + // 七对子需要7个对子(14张牌),或有红中时的特殊情况 + if (handCards.size() + hongZhongCount < 14) { + return false; + } + + // 统计每种牌的数量 + Map cardCounts = new HashMap<>(); + for (int card : handCards) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + } + + // 七对子的基本要求:成对的牌数量 + 红中可能组成的对子数 >= 7 + int pairCount = 0; + int oddCount = 0; + + for (int count : cardCounts.values()) { + pairCount += count / 2; + if (count % 2 != 0) { + oddCount++; + } + } + + // 红中可以作为对子或代替单张牌 + // 简化实现,红中可以灵活使用 + return (pairCount + hongZhongCount) >= 7; + } + + /** + * 检查十三幺胡牌 + */ + private boolean checkThirteenOrphansWin(List handCards) { + // 十三幺牌型:东南西北中发白 + 三种花色的1和9 + Set requiredCards = new HashSet<>(); + // 添加东南西北中发白(简化表示,实际需要根据游戏具体规则) + requiredCards.add(301); // 东 + requiredCards.add(302); // 南 + requiredCards.add(303); // 西 + requiredCards.add(304); // 北 + requiredCards.add(305); // 中 + requiredCards.add(306); // 发 + requiredCards.add(307); // 白 + // 添加万、筒、条的1和9(简化表示) + requiredCards.add(101); requiredCards.add(109); + requiredCards.add(201); requiredCards.add(209); + requiredCards.add(401); requiredCards.add(409); + + // 检查是否包含所有必要的幺九牌 + Set handCardSet = new HashSet<>(handCards); + boolean hasAllOrphans = true; + for (int required : requiredCards) { + if (!handCardSet.contains(required)) { + hasAllOrphans = false; + break; + } + } + + // 十三幺要求至少13种不同的幺九牌,再加上其中任意一张组成对子 + if (!hasAllOrphans) { + return false; + } + + // 统计是否有至少一对 + Map cardCounts = new HashMap<>(); + for (int card : handCards) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + if (cardCounts.get(card) >= 2) { + return true; // 找到对子,符合十三幺条件 + } + } + + return false; + } + + /** + * 检查特殊牌型胡牌 + */ + private List checkSpecialWinTypes(List handCards, int hongZhongCount) { + List specialTypes = new ArrayList<>(); + + // 检查清一色(所有牌同一花色,红中不计入) + boolean isPureColor = checkPureColor(handCards); + if (isPureColor) { + specialTypes.add("PURE_COLOR"); + } + + // 检查一条龙(包含同一花色的1-9) + boolean isOneDragon = checkOneDragon(handCards); + if (isOneDragon) { + specialTypes.add("ONE_DRAGON"); + } + + // 检查碰碰胡(全是刻子和一对将牌) + boolean isAllTriplets = checkAllTriplets(handCards, hongZhongCount); + if (isAllTriplets) { + specialTypes.add("ALL_TRIPLETS"); + } + + return specialTypes; + } + + /** + * 检查清一色 + */ + private boolean checkPureColor(List handCards) { + if (handCards.isEmpty()) { + return false; + } + + // 获取第一张牌的花色 + int firstSuit = handCards.get(0) / 100; + + // 检查所有牌是否同花色 + for (int card : handCards) { + if (card / 100 != firstSuit) { + return false; + } + } + + return true; + } + + /** + * 检查一条龙 + */ + private boolean checkOneDragon(List handCards) { + // 按花色分组 + Map> cardsBySuit = new HashMap<>(); + for (int card : handCards) { + int suit = card / 100; + int value = card % 100; + + cardsBySuit.putIfAbsent(suit, new HashSet<>()); + cardsBySuit.get(suit).add(value); + } + + // 检查每种花色是否包含1-9 + for (Set values : cardsBySuit.values()) { + boolean hasFullSequence = true; + for (int i = 1; i <= 9; i++) { + if (!values.contains(i)) { + hasFullSequence = false; + break; + } + } + if (hasFullSequence) { + return true; + } + } + + return false; + } + + /** + * 检查碰碰胡 + */ + private boolean checkAllTriplets(List handCards, int hongZhongCount) { + // 统计每种牌的数量 + Map cardCounts = new HashMap<>(); + for (int card : handCards) { + cardCounts.put(card, cardCounts.getOrDefault(card, 0) + 1); + } + + int tripletCount = 0; + int pairCount = 0; + + for (int count : cardCounts.values()) { + tripletCount += count / 3; + int remainder = count % 3; + if (remainder == 2) { + pairCount++; + } else if (remainder == 1) { + // 有单张牌,可能无法形成碰碰胡 + // 但可以用红中代替 + } + } + + // 碰碰胡需要4个刻子和1个对子 + // 红中可以作为刻子或对子使用 + int possibleTripletsFromHongZhong = hongZhongCount / 3; + int remainingHongZhong = hongZhongCount % 3; + + int totalTriplets = tripletCount + possibleTripletsFromHongZhong; + int neededTriplets = Math.max(0, 4 - totalTriplets); + + // 检查是否可以通过剩余红中满足需求 + return (neededTriplets <= remainingHongZhong / 2) && (pairCount == 1 || (pairCount == 0 && remainingHongZhong >= 2)); + } + + /** + * 计算胡牌分数 + */ + private int calculateWinScore(List winTypes, int hongZhongCount) { + int baseScore = 1; + + // 根据胡牌类型增加分数 + for (String winType : winTypes) { + switch (winType) { + case "NORMAL": + baseScore *= 1; + break; + case "SEVEN_PAIRS": + baseScore *= 2; + break; + case "THIRTEEN_ORPHANS": + baseScore *= 16; + break; + case "PURE_COLOR": + baseScore *= 4; + break; + case "ONE_DRAGON": + baseScore *= 2; + break; + case "ALL_TRIPLETS": + baseScore *= 2; + break; + } + } + + // 红中奖励 + baseScore += hongZhongCount; + + return baseScore; + } + + /** + * 胡牌结果信息类 + */ + public static class WinResult { + public int testCard; // 测试的牌 + public boolean canWin; // 是否可以胡牌 + public List winTypes; // 胡牌类型列表 + public int winScore; // 胡牌分数 + public List handCards; // 玩家手牌 + public int hongZhongCount; // 红中数量 + public long executionTime; // 执行时间(毫秒) + public String error; // 错误信息(如果有) + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("WinResult{testCard=").append(testCard) + .append(", canWin=").append(canWin) + .append(", winTypes=").append(winTypes) + .append(", winScore=").append(winScore) + .append(", executionTime=").append(executionTime).append("ms") + .append(", hongZhongCount=").append(hongZhongCount); + + if (error != null) { + sb.append(", error='").append(error).append("'"); + } + + sb.append("}"); + return sb.toString(); + } + } + + + + + +} \ No newline at end of file diff --git a/robots/majiang/robot_mj_hz/src/main/java/taurus/util/ROBOTEventType.java b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/ROBOTEventType.java new file mode 100644 index 0000000..d242292 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/ROBOTEventType.java @@ -0,0 +1,13 @@ +package taurus.util; + +public class ROBOTEventType { + /** + * 机器人状态 + */ + public static final int ROBOT_INTOROOM_READY = 1;//等待状态 + public static final int ROBOT_INTOROOM_WORKING = 2;//工作状态 + public static final int ROBOT_UNUSE = 0;//未使用状态 + + + +} diff --git a/robots/majiang/robot_mj_hz/src/main/java/taurus/util/Util.java b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/Util.java new file mode 100644 index 0000000..9b910fa --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/main/java/taurus/util/Util.java @@ -0,0 +1,292 @@ +package taurus.util; + +import com.data.cache.GroupMemberCache; +import com.taurus.core.entity.ITArray; +import com.taurus.core.entity.TArray; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.util.StringUtil; + +import java.util.*; + +public class Util { + public final static Random random = new Random(); + static { + random.setSeed(System.currentTimeMillis()); + } + + /** + * list to TArray + * @param list + * @return + */ + public static final ITArray toTArray(List list) { + ITArray result = new TArray(); + for (Integer card : list) { + result.addInt(card); + } + return result; + } + + /** + * call checkCard + * @param eventCard + * @param cardList + * @return + */ + @Deprecated + public static boolean cardInList(int eventCard, List cardList) { + return checkCard(eventCard, cardList); + } + + @Deprecated + public static final ITArray toMPGroup(List list) { + ITArray result = new TArray(); + for (int[] card : list) { + result.addInt(card[0]); + } + return result; + } + + /** + * list to mod 100 list + * @param list + * @return + */ + public static final List toModList(List list){ + List tem = new ArrayList(); + for(Integer card : list) { + tem.add(card % 100); + } + return tem; + } + + /** + * TArray to mod 100 list + * @param list + * @return + */ + public static final List toModList(ITArray list){ + List tem = new ArrayList<>(); + for(int i=0;i cardList) { + int result = 0; + for (Integer card : cardList) { + if (card == eventCard) { + result += 1; + } + } + return result; + } + + /** + * 检测牌是否存在 + * @param eventCard + * @param cardList + * @return + */ + public static final boolean checkCard(int eventCard, List cardList) { + for (Integer card : cardList) { + if (card == eventCard) { + return true; + } + } + return false; + } + + /** + * 检测牌数量 + * @param eventCard + * @param cardList + * @param num + * @return + */ + public static final boolean checkCard(int eventCard, List cardList, int num) { + int result = 0; + for (Integer card : cardList) { + if (card == eventCard) { + result++; + if (result == num) + return true; + } + } + return false; + } + + /** + * 检测牌数量并将满足条件的删除 + * @param eventCard + * @param cardList + * @param num + * @return + */ + public static final boolean checkCardAndRomve(int eventCard, List cardList, int num) { + if (checkCard(eventCard, cardList, num)) { + removeCard(cardList, eventCard, num); + return true; + } + return false; + } + + /** + * 获取每张牌的数量MAP + * @param cardList + * @return + */ + public static final Map getCardNumMap(List cardList) { + Map result = new HashMap(); + for (Integer card : cardList) { + if (!result.containsKey(card)) { + result.put(card, 1); + } else { + int num = result.get(card); + result.put(card, (num + 1)); + } + } + return result; + } + + /** + * 检测牌数量 + * @param map + * @param card + * @param num + * @return + */ + public static boolean cardNum(Map map, int card, int num) { + Integer _num = map.get(card); + if (_num != null && _num >= num) { + return true; + } + return false; + } + + /** + * 移除指定数量的牌 + * @param cardList + * @param card + * @param count + */ + public static final void removeCard(List cardList, int card, int count) { + int curCount = 0; + for (int i = 0; i < cardList.size(); i++) { + if (cardList.get(i) == card) { + cardList.remove(i); + i--; + curCount++; + } + + if (count == curCount) { + return; + } + } + } + + static final public void addCard(ITArray opcard, int card, int num) { + for (int i = 0; i < num; ++i) { + opcard.addInt(card); + } + } + + static final public void addCard(List cardList, int card, int num) { + for (int i = 0; i < num; ++i) { + cardList.add(card); + } + } + + /** + * 获取今天凌晨0点时间戳 + * @return + */ + public static int todayTimeSec() { + Calendar cal = Calendar.getInstance(); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0); + int timeSec = (int) (cal.getTimeInMillis() / 1000); + return timeSec; + } + + /** + * 根据时间戳获取指定当天凌晨10位时间戳 + * @param timeSec + * @return + */ + public static int todayTimeSec(Long timeSec) { + Calendar cal = Calendar.getInstance(); + // 指定日期 + cal.setTime(new Date(timeSec)); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + return (int) (cal.getTimeInMillis() / 1000); + } + + /** + * msg ps : param:{},param:{} + * ==> param: abc,param:abced + * @param msg + * @param params + * @return + */ + public static String stringFormat(String msg, Object... params) { + try { + if (StringUtil.isNotEmpty(msg)) { + msg = msg.replaceAll("\\{}", "\\%s"); + return String.format(msg, params); + } + } catch (Exception e) { + return ""; + } + return ""; + } + +// public static void countLog(String key,int num,Jedis jedis) { +// String day_key = key + ":d"+DateUtils.getBeginDay(); +// String week_key = key + ":w"+DateUtils.getBeginWeek(); +// String month_key = key + ":m"+DateUtils.getBeginMonth(); +// jedis.incrBy(day_key,num); +// jedis.expire(day_key, 11*3600*24); +// jedis.incrBy(week_key,num); +// jedis.expire(week_key, 15*3600*24); +// jedis.incrBy(month_key,num); +// jedis.expire(month_key, 63*3600*24); +// } + + /** + * 获取玩家体力值 + * @param gid + * @param uid + * @return + */ + public static long readRedisHp(int gid,int uid) { + String key = GroupMemberCache.genKey(gid, uid); + String hp = Redis.use("group1_db10").hget(key, "hp"); + long cur_hp = StringUtil.isNotEmpty(hp) ? Long.parseLong(hp) : 0; + return cur_hp; + } +} diff --git a/robots/majiang/robot_mj_hz/src/test/java/robot_mj_hongzhong/Main.java b/robots/majiang/robot_mj_hz/src/test/java/robot_mj_hongzhong/Main.java new file mode 100644 index 0000000..9ec4ba5 --- /dev/null +++ b/robots/majiang/robot_mj_hz/src/test/java/robot_mj_hongzhong/Main.java @@ -0,0 +1,15 @@ +package robot_mj_hongzhong; + +import com.taurus.permanent.TPServer; + +public class Main { + public static void main(String[] args) { + System.out.println("启动红中麻将机器人服务器..."); + System.out.println("服务器将监听端口8722用于游戏协议"); + + //启动机器人服务 + TPServer.me().start(); + + System.out.println("红中麻将机器人服务器已启动"); + } +} \ No newline at end of file