diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/Config.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/Config.java index b20d162..3c6a213 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/Config.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/Config.java @@ -12,26 +12,26 @@ public class Config { */ public static final String JOIN_ROOM_FLS = "1002"; - /** Web组加入房间协议 */ + /** Web 组加入房间协议 */ public static final String WEB_GROUP_JOIN_ROOM = "225"; - /** Web组主动重连协议 */ + /** Web 组主动重连协议 */ public static final String WEB_GROUP_ACTIVE_RECONNECT = "226"; //==================== 游戏服务器配置 ==================== /** 游戏服务器主机地址 */ - public static final String GAME_SERVER_HOST = "127.0.0.1"; + public static final String GAME_SERVER_HOST = "8.134.76.43"; /** 游戏服务器端口 */ - public static final String GAME_SERVER_PORT = "8971"; + public static final String GAME_SERVER_PORT = "8870"; /** 默认密码 */ public static final String DEFAULT_PASSWORD = "123456"; /** 默认PID */ - public static final String DEFAULT_PID = "107"; + public static final String DEFAULT_PID = "77"; /** 默认群组ID */ - public static final String DEFAULT_GROUP_ID = "426149"; + public static final String DEFAULT_GROUP_ID = "762479"; } \ No newline at end of file diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXGameController.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXGameController.java index 2728bd3..8b543ff 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXGameController.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXGameController.java @@ -2,16 +2,15 @@ package robot.zp; 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.core.util.Logger; import com.taurus.permanent.data.Session; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import robot.zp.info.RobotUser; -import robot.zp.thread.ThreadPoolConfig; import taurus.client.TaurusClient; import taurus.client.business.GroupRoomBusiness; import taurus.util.ROBOTEventType; @@ -20,7 +19,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -30,7 +28,7 @@ import static robot.zp.thread.ThreadPoolConfig.scheduleDelay; * 福禄寿游戏控制器 - 处理游戏协议 */ public class EXGameController extends GameController { - private static final Logger log = LoggerFactory.getLogger(EXGameController.class); + private static final Logger log = Logger.getLogger(EXGameController.class); private static final RobotConnectionManager robotConnectionManager = new RobotConnectionManager(); @@ -56,39 +54,59 @@ public class EXGameController extends GameController { int robotId = params.getInt("robotid"); String roomId = params.getString("roomid"); int groupId = params.getInt("groupid"); - - //检查Redis中该房间是否真的包含当前机器人 - if (!checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { - //Redis中不存在该机器人 清理本地可能的错误映射 - List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); - if (!robotUsers.isEmpty()) { - synchronized (robotUsers) { - RobotUser robotUser = robotUsers.get(0); - log.warn("房间{}中Redis未找到机器人{},但本地映射存在{},清理本地映射", roomId, robotId, robotUser.getRobotId()); - robotRoomMapping.remove(robotUser.getConnecId()); - robotRoomMapping.remove(robotUser.getRobotId()); - } - } - } else { - //Redis中存在该机器人 检查是否是不同机器人的冲突 - List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); - if (!robotUsers.isEmpty()) { - synchronized (robotUsers) { - RobotUser robotUser = robotUsers.get(0); - int existingRobotId = Integer.parseInt(robotUser.getRobotId()); - - if (robotId != existingRobotId) { - //不同机器人的冲突 - log.warn("房间{}中Redis已存在机器人{},当前机器人{}不执行加入逻辑", roomId, existingRobotId, robotId); + + String lockKey = "room_lock:" + roomId; + synchronized (lockKey.intern()) { + if (checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { + log.info("机器人{" + robotId + "}已在房间{" + roomId + "}中(Redis 中存在),直接允许加入"); + } else { + RobotUser existingRobotUser = getRobotRoomInfo(String.valueOf(robotId)); + if (existingRobotUser != null && existingRobotUser.getCurrentRoomId() == Integer.parseInt(roomId)) { + log.info("机器人{" + robotId + "}已在房间{" + roomId + "}中(本地映射存在),直接允许加入"); + } else { + if (isPlayerIdConflictInRoom(roomId, robotId)) { + log.warn("检测到机器人{" + robotId + "}与房间{" + roomId + "}中的真人玩家 ID 冲突,拒绝加入"); + ITObject errorResponse = TObject.newInstance(); + errorResponse.putString("status", "failed"); + errorResponse.putString("message", "机器人 ID 与房间内玩家冲突"); + MainServer.instance.sendResponse(gid, 1, errorResponse, session); return; } } } + //检查Redis中该房间是否真的包含当前机器人 + if (!checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { + //Redis中不存在该机器人 清理本地可能的错误映射 + List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); + if (!robotUsers.isEmpty()) { + synchronized (robotUsers) { + RobotUser robotUser = robotUsers.get(0); + log.warn("房间" + roomId + "中 Redis 未找到机器人" + robotId + ",但本地映射存在" + robotUser.getRobotId() + ",清理本地映射"); + robotRoomMapping.remove(robotUser.getConnecId()); + robotRoomMapping.remove(robotUser.getRobotId()); + } + } + } else { + //Redis中存在该机器人 检查是否是不同机器人的冲突 + List robotUsers = getRobotUsersByRoomId(Integer.parseInt(roomId)); + if (!robotUsers.isEmpty()) { + synchronized (robotUsers) { + RobotUser robotUser = robotUsers.get(0); + int existingRobotId = Integer.parseInt(robotUser.getRobotId()); + + if (robotId != existingRobotId) { + //不同机器人的冲突 + log.warn("房间" + roomId + "中 Redis 已存在机器人" + existingRobotId + ",当前机器人" + robotId + "不执行加入逻辑"); + return; + } + } + } + } } - log.info("225开始进房间: room:{} robot:{}", roomId, robotId); + log.info("225 开始进房间:room:" + roomId + " robot:" + robotId); //加入房间 joinRoomCommon(robotId, roomId, groupId, params); - log.info("225已进入房间准备成功: room:{} robot:{}", roomId, robotId); + log.info("225 已进入房间准备成功:room:" + roomId + " robot:" + robotId); } /** @@ -98,10 +116,10 @@ public class EXGameController extends GameController { public void webGroupActive(Session session, ITObject params, int gid) { int robotId = params.getInt("robotid"); String roomId = params.getString("roomid"); - log.info("226开始进房间: room:{} robot:{}", roomId, robotId); + log.info("226 开始进房间:room:" + roomId + " robot:" + robotId); //加入房间 joinRoomCommon(params.getInt("robotid"), params.getString("roomid"), params.getInt("groupid"), params); - log.info("226已进入房间准备成功: room:{} robot:{}", roomId, robotId); + log.info("226 已进入房间准备成功:room:" + roomId + " robot:" + robotId); } /** @@ -128,12 +146,12 @@ public class EXGameController extends GameController { return; } - log.info("重启后开始进房间: room:{} robot:{}", robotUser.getCurrentRoomId(), robotUser.getRobotId()); + log.info("重启后开始进房间: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); - log.info("重启后已进入房间准备成功: room:{} robot:{}", robotUser.getCurrentRoomId(), robotUser.getRobotId()); + log.info("重启后已进入房间准备成功:room:" + robotUser.getCurrentRoomId() + " robot:" + robotUser.getRobotId()); } catch (Exception e) { log.error("重启服务断线重连时发生错误", e); @@ -151,22 +169,22 @@ public class EXGameController extends GameController { Jedis jedis2 = Redis.use("group1_db2").getJedis(); try { Set robotTokens = jedis0.smembers("{user}:" + robotId + "_token"); - String robotSession = null; + String robotSession = robotTokens.stream().filter(jedis0::exists).findFirst().orElse(null); - for (String token : robotTokens) { - if (jedis0.exists(token)) { - robotSession = token; - break; - } - } + log.info("开始进房间:room:{"+roomId+"}"); + log.info("开始进房间:{user}:{"+robotId+"}"); - log.info("开始进房间: room:{}", roomId); - log.info("开始进房间: {user}:{}", robotId); + //建立 TCP 连接 + TaurusClient client = getFlsGameServerConnection(roomId + "_" + robotId); + if (client == null) { + log.error("机器人{"+robotId+"}连接游戏服务器失败,connecId:{"+roomId + "_" + robotId+"}"); + return; + } - TaurusClient client = getFlsGameServerConnection(roomId + "_" + robotId); - GroupRoomBusiness.joinRoom(groupId, "room:" + roomId, "{user}:" + robotId, null); + ITObject joinResult = GroupRoomBusiness.joinRoom(groupId, "room:" + roomId, "{user}:" + robotId, null); + log.info("GroupRoomBusiness.joinRoom 结果:robotId:{"+robotId+"}, roomId:{"+roomId+"}, result:{"+joinResult+"}"); - //机器人房间映射关系 + log.info("机器人{"+robotId+"}准备发送 JOIN_ROOM_CS(1002) 协议"); RobotUser robotUser = getRobotRoomInfo(String.valueOf(robotId)); String connecId = roomId + "_" + robotId; if (robotUser.getCurrentRoomId() == 0) { @@ -174,58 +192,115 @@ public class EXGameController extends GameController { robotUser.setClient(client); robotUser.setConnecId(connecId); } - - //先不放入映射 等确认加入成功后再放入 - //robotRoomMapping.put(robotUser.getConnecId(), robotUser); - robotRoomMapping.remove(robotUser.getRobotId()); - //非阻塞延迟替代Thread.sleep - scheduleDelay(() -> { - }, 2, TimeUnit.SECONDS); + //先不放入映射 等确认加入成功后再放入 + robotRoomMapping.remove(robotUser.getRobotId()); + params.putString("session", "{user}:" + robotId + "," + robotSession); - //发送加入房间请求到game_zp_fls + //发送 JOIN_ROOM_FLS(1002) client.send(Config.JOIN_ROOM_FLS, params, response -> { - //成功响应后才建立映射关系 - robotRoomMapping.put(robotUser.getConnecId(), robotUser); - robotConnectionManager.reconnectToGameServer(response, robotUser, client); + try { + log.info("JOIN_ROOM_FLS(1002) 响应:{"+response+"}"); + + //检查响应是否成功 + if (response == null || response.messageData == null || response.messageData.param == null) { + log.error("机器人{"+robotId+"}加入房间{"+roomId+"}失败:game_mj_cs 返回 null"); + cleanupFailedRobot(robotUser, connecId, roomId); + return; + } + + ITObject responseParam = response.messageData.param; + int responseCode = responseParam.containsKey("code") ? responseParam.getInt("code") : 0; + if (responseCode != 0) { + log.error("机器人{"+robotId+"}加入房间{"+roomId+"}失败:game_mj_cs 返回错误码{"+responseCode+"}, response:{"+response+"}"); + cleanupFailedRobot(robotUser, connecId, roomId); + return; + } + + //1002 响应成功后,添加短暂延迟等待服务器将机器人加入 Redis + scheduleDelay(() -> { + try { + //验证机器人是否真的进入了房间(检查 Redis) + if (!checkRobotInRoomRedis(roomId, String.valueOf(robotId))) { + log.error("机器人{"+robotId+"}加入房间{"+roomId+"}失败:1002 响应成功但 Redis 中未找到机器人,清理资源"); + cleanupFailedRobot(robotUser, connecId, roomId); + return; + } + + log.info("机器人{"+robotId+"}Redis 验证成功,建立映射关系"); + + //成功响应后才建立映射关系 + synchronized (robotRoomMapping) { + robotRoomMapping.put(robotUser.getConnecId(), robotUser); + } + + log.info("机器人{"+robotId+"}已成功加入房间{"+roomId+"},建立映射关系"); + + //发送准备协议 + scheduleDelay(() -> { + try { + if (client != null && client.isConnected()) { + client.send(Config.GAME_READY_FLS, params, readyResponse -> { + try { + log.info("GAME_READY 响应:{"+readyResponse+"}"); + + //设置准备状态 + robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_READY); + robotConnectionManager.setSessionAndToken("{user}:" + robotId, robotSession, robotUser.getConnecId()); + + //标记机器人为可用状态 + jedis2.hset("gallrobot", String.valueOf(robotUser.getRobotId()), "1"); + robotUser.setIntoRoomTime(robotConnectionManager.getTime()); + + log.info("机器人{"+robotId+"}准备成功,房间:{"+roomId+"}"); + } catch (Exception e) { + log.error("机器人{"+robotId+"}设置准备状态失败"+ e); + cleanupFailedRobot(robotUser, connecId, roomId); + } + }); + } + } catch (Exception e) { + log.error("机器人{"+robotId+"}发送准备协议失败"+ e); + cleanupFailedRobot(robotUser, connecId, roomId); + } + }, 1000, TimeUnit.MILLISECONDS); + } catch (Exception e) { + log.error("机器人{"+robotId+"}Redis 验证失败"+ e); + cleanupFailedRobot(robotUser, connecId, roomId); + } + }, 500, TimeUnit.MILLISECONDS); + + } catch (Exception e) { + log.error("处理 JOIN_ROOM_CS 响应时发生异常", e); + cleanupFailedRobot(robotUser, connecId, roomId); + } }); - log.info("已进入房间成功: {}", robotUser.getConnecId()); - Thread.sleep(1000); - if (client.isConnected()) { - client.send(Config.GAME_READY_FLS, params, response -> { - log.info("1003:{}", response); - }); - jedis2.hset("gallrobot", String.valueOf(robotUser.getRobotId()), "1"); + log.info("已进入房间成功:{"+robotUser.getConnecId()+"}"); - robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_READY); - robotConnectionManager.setSessionAndToken("{user}:" + robotId, robotSession, robotUser.getConnecId()); - } - //添加超时检查机制 + /*//添加超时检查机制(15 秒) CompletableFuture.runAsync(() -> { try { - //定时任务替代Thread.sleep + //定时任务替代 Thread.sleep scheduleDelay(() -> { - //15秒后还没有建立映射关系 加入可能失败 - if (robotRoomMapping.get(robotUser.getConnecId()) == null) { - log.warn("机器人{}加入房间{}超时,清理临时状态", robotId, roomId); - robotConnectionManager.disconnectFromGameServer(connecId); - } + //15 秒后还没有建立映射关系或状态不是准备状态,说明加入失败 + RobotUser currentUser = robotRoomMapping.get(connecId); + if (currentUser == null || currentUser.getStatus() != ROBOTEventType.ROBOT_INTOROOM_READY) { + log.warn("机器人" + robotId + "加入房间" + roomId + "超时(15 秒),清理临时状态"); + cleanupFailedRobot(robotUser, connecId, roomId); + } }, 15, TimeUnit.SECONDS); - //15秒后还没有建立映射关系 加入可能失败 - if (robotRoomMapping.get(robotUser.getConnecId()) == null) { - log.warn("机器人{}加入房间{}超时,清理临时状态", robotId, roomId); - robotConnectionManager.disconnectFromGameServer(connecId); - } } catch (Exception e) { - log.error("机器人加入房间超时", e); + log.error("机器人" + robotId + "加入房间超时检查异常", e); } - }, ThreadPoolConfig.getBusinessThreadPool());//指定自定义线程池 - robotUser.setIntoRoomTime(robotConnectionManager.getTime()); - log.info("已进入房间准备成功: {}", robotUser.getConnecId()); + }, ThreadPoolConfig.getBusinessThreadPool());*/ + log.info("已进入房间准备成功:{"+robotUser.getConnecId()+"}"); } catch (Exception e) { log.error("加入房间时发生错误", e); + //发生异常时清理资源 + String failedConnecId = roomId + "_" + robotId; + cleanupFailedRobot(null, failedConnecId, roomId); } finally { jedis0.close(); jedis2.close(); @@ -278,7 +353,7 @@ public class EXGameController extends GameController { lastAccessTime.remove(removedUser.getConnecId()); } - log.info("清理机器人房间信息: {}", robotId); + log.info("清理机器人房间信息:" + robotId); } /** @@ -304,7 +379,7 @@ public class EXGameController extends GameController { for (String connecId : expiredConnections) { RobotUser robotUser = robotRoomMapping.get(connecId); if (robotUser != null) { - log.info("清理超时连接: {}, 机器人ID: {}", connecId, robotUser.getRobotId()); + log.info("清理超时连接:" + connecId + ", 机器人 ID: " + robotUser.getRobotId()); robotConnectionManager.disconnectFromGameServer(connecId); } robotRoomMapping.remove(connecId); @@ -312,7 +387,7 @@ public class EXGameController extends GameController { } if (!expiredConnections.isEmpty()) { - log.info("本次清理了 {} 个超时连接", expiredConnections.size()); + log.info("本次清理了 " + expiredConnections.size() + " 个超时连接"); } } @@ -344,7 +419,7 @@ public class EXGameController extends GameController { } return false; } catch (Exception e) { - log.error("检查Redis房间玩家信息时发生错误,roomId: {}, robotId: {}", roomId, robotId, e); + log.error("检查 Redis 房间玩家信息时发生错误,roomId: " + roomId + ", robotId: " + robotId, e); return false; } finally { jedis.close(); @@ -357,13 +432,129 @@ public class EXGameController extends GameController { */ public static TaurusClient getFlsGameServerConnection(String connecId) { TaurusClient taurusClient = robotConnectionManager.getGameClient(connecId); - log.info("根据机器人ID和连接ID获取福禄寿游戏服务器连接 client: {}", taurusClient); + log.info("根据机器人 ID 和连接 ID 获取福禄寿游戏服务器连接 client: " + taurusClient); if (taurusClient != null) { - log.debug("成功获取游戏服务器连接,connecId: {}", connecId); + log.debug("成功获取游戏服务器连接,connecId: " + connecId); return taurusClient; } taurusClient = robotConnectionManager.connectToGameServer(connecId); return taurusClient; } + /** + * 检查机器人 ID 是否与房间内已有玩家冲突 + */ + private boolean isPlayerIdConflictInRoom(String roomId, int robotId) { + Jedis jedis = Redis.use().getJedis(); + Jedis jedis2 = Redis.use("group1_db2").getJedis(); + try { + //查询该房间的玩家信息 + String playersStr = jedis.hget("room:" + roomId, "players"); + if (playersStr == null || playersStr.equals("[]")) { + log.info("房间{"+roomId+"}为空,机器人{"+robotId+"}可以加入"); + return false; + } + + String players = playersStr.substring(1, playersStr.length() - 1); + String[] playerIds = players.split(","); + + //统计房间中的真人数量和机器人数量 + int realPlayerCount = 0; + int robotCount = 0; + boolean hasSameRobot = false; + + for (String playerIdStr : playerIds) { + try { + int playerId = Integer.parseInt(playerIdStr.trim()); + + String robotData = jedis2.hget("{robot}:" + playerId, "password"); + boolean isRobot = (robotData != null); + + if (isRobot) { + robotCount++; + //检查是否是相同的机器人 ID + if (playerId == robotId) { + hasSameRobot = true; + } + } else { + realPlayerCount++; + } + } catch (NumberFormatException e) { + log.error("解析玩家 ID 失败:"+playerIdStr); + } + } + + if (hasSameRobot) { + log.info("房间{"+roomId+"}中已有相同机器人{"+robotId+"},允许重复加入"); + return false; + } + + if (realPlayerCount >= 2) { + log.warn("房间{"+roomId+"}中已有{"+realPlayerCount+"}个真人玩家,拒绝机器人{"+robotId+"}加入"); + return true; + } + + if (realPlayerCount == 1) { + if (robotCount == 0) { + log.info("房间{"+roomId+"}中有 1 个真人玩家,允许第一个机器人{"+robotId+"}加入陪打"); + return false; + } + log.info("房间{"+roomId+"}中已有 1 个真人 +1 个机器人,拒绝额外机器人{"+robotId+"}"); + return true; + } + + if (robotCount >= 2) { + log.warn("房间{"+roomId+"}中已有{"+robotCount+"}个机器人,不再添加新机器人{"+robotId+"}"); + return true; + } + + log.info("房间{"+roomId+"}当前状态:真人{"+realPlayerCount+"}人,机器人{"+robotCount+"}个,允许机器人{"+robotId+"}加入"); + return false; + } catch (Exception e) { + log.error("检查房间人数时发生异常,roomId:{"+roomId+"}, robotId:{"+robotId+"}" + e); + return false; + } finally { + jedis.close(); + jedis2.close(); + } + } + + /** + * 清理加入失败的机器人资源 + */ + private void cleanupFailedRobot(RobotUser robotUser, String connecId, String roomId) { + try { + String robotId = robotUser != null ? robotUser.getRobotId() : "unknown"; + log.info("开始清理失败机器人资源:robotId:{"+robotId+"}, connecId:{"+connecId+"}, roomId:{"+roomId+"}"); + + // 清理映射关系 + if (robotUser != null) { + robotRoomMapping.remove(robotUser.getConnecId()); + robotRoomMapping.remove(robotUser.getRobotId()); + } else { + robotRoomMapping.remove(connecId); + if (connecId != null && connecId.contains("_")) { + String[] parts = connecId.split("_"); + if (parts.length >= 2) { + robotRoomMapping.remove(parts[1]); + } + } + } + + // 断开 TCP 连接 + TaurusClient client = robotUser != null ? robotUser.getClient() : null; + if (client != null && client.isConnected()) { + client.killConnection(); + log.info("已清理失败机器人{"+robotId+"}的 TCP 连接"); + } + + // 通知连接管理器清理资源 + robotConnectionManager.disconnectFromGameServer(connecId); + + log.info("完成失败机器人资源的清理:connecId:{"+connecId+"}, roomId:{"+roomId+"}"); + } catch (Exception e) { + log.error("清理失败机器人资源时发生异常", e); + } + } + } \ No newline at end of file diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXMainServer.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXMainServer.java index f8f1d65..4ce2fcc 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXMainServer.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/EXMainServer.java @@ -7,8 +7,7 @@ import com.robot.MainServer; import com.robot.data.Player; import com.robot.data.Room; import com.taurus.core.plugin.redis.Redis; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.taurus.core.util.Logger; import redis.clients.jedis.Jedis; import robot.zp.info.RobotUser; import robot.zp.thread.ResourceCleanupUtil; @@ -22,7 +21,7 @@ import static robot.zp.EXGameController.robotRoomMapping; * TCP服务端接收robot_mgr的协议 同时作为客户端连接game_zp_fls处理AI逻辑 */ public class EXMainServer extends MainServer{ - private static final Logger log = LoggerFactory.getLogger(EXMainServer.class); + private static final Logger log = Logger.getLogger(EXMainServer.class); private static final RobotConnectionManager robotConnectionManager = new RobotConnectionManager(); @@ -64,7 +63,7 @@ public class EXMainServer extends MainServer{ String robotskey = "g{"+Config.DEFAULT_GROUP_ID+"}:play:"+Config.DEFAULT_PID; Map maprobot = jedis2.hgetAll(robotskey); for(Map.Entry entry : maprobot.entrySet()) { - log.info("{}:{}", entry.getKey(), entry.getValue()); + log.info(entry.getKey() + ":" + entry.getValue()); //是否创建 RobotUser robotUser = new RobotUser(); robotUser.setRobotId(entry.getKey()); @@ -87,8 +86,8 @@ public class EXMainServer extends MainServer{ } log.info("福禄寿机器人服务器已启动"); - log.info("服务器将监听端口 {} 用于接收robot_mgr管理协议", gameSetting.port); - log.info("当前线程池配置: {}", ThreadPoolConfig.getThreadPoolStatus()); + log.info("服务器将监听端口 " + gameSetting.port + " 用于接收 robot_mgr 管理协议"); + log.info("当前线程池配置:" + ThreadPoolConfig.getThreadPoolStatus()); jedis2.close(); } @@ -123,16 +122,16 @@ public class EXMainServer extends MainServer{ //每30秒执行一次资源清理 Thread.sleep(30000); ResourceCleanupUtil.performCleanup(); - log.info("线程池状态: {}", ThreadPoolConfig.getThreadPoolStatus()); + log.info("线程池状态:" + ThreadPoolConfig.getThreadPoolStatus()); } catch (InterruptedException e) { break; } catch (Exception e) { - log.error("资源清理任务异常: {}", e.getMessage(), e); + log.error("资源清理任务异常:" + e.getMessage(), e); // 发生异常时尝试清理 try { ResourceCleanupUtil.performCleanup(); } catch (Exception cleanupEx) { - log.error("异常清理也失败: {}", cleanupEx.getMessage(), cleanupEx); + log.error("异常清理也失败:" + cleanupEx.getMessage(), cleanupEx); } } } diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/RobotConnectionManager.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/RobotConnectionManager.java index d75dbe2..fba5270 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/RobotConnectionManager.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/RobotConnectionManager.java @@ -33,7 +33,7 @@ import static robot.zp.EXGameController.robotRoomMapping; */ public class RobotConnectionManager { - private final Logger log = Logger.getLogger(RobotConnectionManager.class); + private static final Logger log = Logger.getLogger(RobotConnectionManager.class); private static final Map fuLuShouHandlerInstances = new ConcurrentHashMap<>(); //记录活跃连接 用于资源清理判断 @@ -44,39 +44,14 @@ public class RobotConnectionManager { //连接最大生存时间(5 分钟) private static final long MAX_CONNECTION_LIFETIME = 5 * 60 * 1000; + private final EXGameController exGameController; private final String host= Config.GAME_SERVER_HOST; private final int port= Integer.parseInt(Config.GAME_SERVER_PORT); - /*福禄寿游戏算法相关 start*/ - private final Map>> playerOutcardsMapByConn = new ConcurrentHashMap<>(); - private final Map>> playerchisMapByConn = new ConcurrentHashMap<>(); - private final Map>> playerpengsMapByConn = new ConcurrentHashMap<>(); - private final Map>> playermingsMapByConn = new ConcurrentHashMap<>(); - private final Map>> playerzisMapByConn = new ConcurrentHashMap<>(); - - private Map> getPlayerOutcardsMap(String connecId) { - return playerOutcardsMapByConn.computeIfAbsent(connecId, k -> new ConcurrentHashMap<>()); - } - private Map> getPlayerchisMap(String connecId) { - return playerchisMapByConn.computeIfAbsent(connecId, k -> new ConcurrentHashMap<>()); - } - private Map> getPlayerpengsMap(String connecId) { - return playerpengsMapByConn.computeIfAbsent(connecId, k -> new ConcurrentHashMap<>()); - } - private Map> getPlayermingsMap(String connecId) { - return playermingsMapByConn.computeIfAbsent(connecId, k -> new ConcurrentHashMap<>()); - } - private Map> getPlayerzisMap(String connecId) { - return playerzisMapByConn.computeIfAbsent(connecId, k -> new ConcurrentHashMap<>()); - } private int pid = 0; private Map count = new HashMap(); - /*福禄寿游戏算法相关 end*/ - - - public RobotConnectionManager() { exGameController = new EXGameController(); @@ -99,10 +74,17 @@ public class RobotConnectionManager { } FuLuShouHandler newInstance = new FuLuShouHandler(); - log.info("创建新的 FuLuShouHandler 实例:{}", connecId); + + //从 Redis 恢复状态 + boolean restored = newInstance.restoreFromRedis(connecId); + if (restored) { + log.info("从 Redis 恢复 FuLuShouHandler 实例:" + connecId); + } else { + log.info("创建新的 FuLuShouHandler 实例:" + connecId); + } fuLuShouHandlerInstances.put(connecId, newInstance); - log.info("当前 FuLuShouHandler 实例总数:{}", fuLuShouHandlerInstances.size()); + log.info("当前 FuLuShouHandler 实例总数:" + fuLuShouHandlerInstances.size()); return newInstance; } @@ -139,7 +121,7 @@ public class RobotConnectionManager { * 断开与游戏服务器的连接(主动断开) */ public void disconnectFromGameServer(String connecId) { - log.info("开始主动断开连接:{}", connecId); + log.info("开始主动断开连接:" + connecId); RobotUser robotUser = robotRoomMapping.remove(connecId); //标记连接为非活跃 @@ -148,22 +130,20 @@ public class RobotConnectionManager { //清理连接数据 if (connecId != null) { + //从 Redis 删除状态 + FuLuShouHandler.removeFromRedis(connecId); + FuLuShouHandler handler = fuLuShouHandlerInstances.get(connecId); if (handler != null) { //清理所有集合数据以释放内存 - handler.clearAllData(); - log.info("清空 FuLuShouHandler 集合数据:{}", connecId); + handler.clearAllData(); + log.info("清空 FuLuShouHandler 集合数据:" + connecId); } //移除实例和相关数据 - fuLuShouHandlerInstances.remove(connecId); - playerOutcardsMapByConn.remove(connecId); - playerchisMapByConn.remove(connecId); - playerpengsMapByConn.remove(connecId); - playermingsMapByConn.remove(connecId); - playerzisMapByConn.remove(connecId); - - log.info("清理完成,当前活跃连接数:{}, 实例数:{}", activeConnections.size(), fuLuShouHandlerInstances.size()); + fuLuShouHandlerInstances.remove(connecId); + + log.info("清理完成,当前活跃连接数:" + activeConnections.size() + ", 实例数:" + fuLuShouHandlerInstances.size()); } if (robotUser != null) { @@ -173,12 +153,12 @@ public class RobotConnectionManager { if (client.isConnected()) { client.killConnection(); } - log.info("客户端主动断开连接完成:{}", connecId); + log.info("客户端主动断开连接完成:" + connecId); } catch (Exception e) { - log.error("断开客户端连接时发生异常:{}, 错误:{}", connecId, e.getMessage(), e); + log.error("断开客户端连接时发生异常:" + connecId + ", 错误:" + e.getMessage(), e); } } else { - log.warn("客户端连接不存在:{}", connecId); + log.warn("客户端连接不存在:" + connecId); } //同时清理机器人房间映射 @@ -202,12 +182,12 @@ public class RobotConnectionManager { //清理过期连接 for (String connecId : expiredConnections) { - log.info("清理过期连接实例:{}", connecId); + log.info("清理过期连接实例:" + connecId); disconnectFromGameServer(connecId); } if (!expiredConnections.isEmpty()) { - log.info("本次清理了 {} 个过期连接实例", expiredConnections.size()); + log.info("本次清理了 " + expiredConnections.size() + " 个过期连接实例"); } } @@ -220,20 +200,21 @@ public class RobotConnectionManager { @Override public void handleEvent(Event event) { //获取 msg - Message message = (Message) event.getParameter("msg"); + Message message = (Message) event.getParameter("msg"); - ITObject param = message.param; - //回调协议号 - String command = message.command; - log.debug("fls OnEvent msg: {}", command); + ITObject param = message.param; + //回调协议号 + String command = message.command; + log.info("【福禄寿】收到游戏协议:command=" + command + ", connecId=" + connecId); - //根据玩法ID处理不同的回调 - if (StringUtil.isNotEmpty(command)) { - //直接处理协议 - handleProtocol(command, message, client, connecId); - } - } - }; + //根据玩法 ID 处理不同的回调 + if (StringUtil.isNotEmpty(command)) { + log.info("开始处理协议:" + command + ", connecId=" + connecId); + //直接处理协议 + handleProtocol(command, message, client, connecId); + } + } + }; //添加连接状态监听器 IEventListener connectListener = new IEventListener() { @@ -258,6 +239,11 @@ public class RobotConnectionManager { String connecId = robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId(); if(client.isConnected()){ try { + log.info(String.valueOf(response.messageData.param)); + if (response.messageData.param==null) { + log.info("警告:reconnectToGameServer 重连时未获取到参数"); + return; + } ITObject obj = response.messageData.param.getTObject("tableInfo"); ITObject reloadInfo = response.messageData.param.getTObject("reloadInfo"); if (obj != null) { @@ -272,10 +258,10 @@ public class RobotConnectionManager { robotUser.setSeat(seat); } } - log.info("playerData: {}", playerData); + log.info("playerData: " + playerData); - log.info("obj: {}", obj); - log.info("reloadInfo: {}", reloadInfo); + log.info("obj: " + obj); + log.info("reloadInfo: " + reloadInfo); if (reloadInfo != null) { //重连回来的 int curren_outcard_seat = reloadInfo.getInt("curren_outcard_seat"); @@ -301,7 +287,7 @@ public class RobotConnectionManager { } } - log.info("hcard>0{}", hcard); + log.info("hcard>0" + hcard); if (hcard.size() > 0) { //同步手牌 FuLuShouHandler currentInstance = getFuLuShouHandlerInstance(connecId); @@ -311,9 +297,9 @@ public class RobotConnectionManager { if (currentHand.isEmpty() || hcard.size() > currentHand.size()) { //手牌集合为空 或者 玩家出牌了 currentInstance.updateHandCard(hcard); - log.info("断线重连:同步手牌数据,服务器手牌:{}", hcard); + log.info("断线重连:同步手牌数据,服务器手牌:" + hcard); } else { - log.info("断线重连:使用Redis恢复的手牌数据,数量:{}", currentHand.size()); + log.info("断线重连:使用 Redis 恢复的手牌数据,数量:" + currentHand.size()); } if (outcard_list.size() > 0) { @@ -326,28 +312,23 @@ public class RobotConnectionManager { List currentOutCards = currentInstance.getChuGuoCardInhand(); if (currentOutCards.isEmpty() || outcards.size() > currentOutCards.size()) { currentInstance.updateOutCard(outcards); - log.info("断线重连:同步出牌数据,服务器出牌:{}", outcards); + log.info("断线重连:同步出牌数据,服务器出牌:" + outcards); } else { - log.info("断线重连:使用Redis恢复的出牌数据,数量:{}", currentOutCards.size()); + log.info("断线重连:使用 Redis 恢复的出牌数据,数量:" + currentOutCards.size()); } } - //非阻塞的延迟执行,增加更完善的异常处理 + //非阻塞的延迟执行 scheduleDelay(() -> { try { - //重新获取当前实例,确保数据一致性 + //重新获取当前实例 确保数据一致性 FuLuShouHandler reconnectedInstance = getFuLuShouHandlerInstance(connecId); - Map> currentPlayerOutcardsMap = getPlayerOutcardsMap(connecId); - Map> currentPlayerchisMap = getPlayerchisMap(connecId); - Map> currentPlayerpengsMap = getPlayerpengsMap(connecId); - Map> currentPlayermingsMap = getPlayermingsMap(connecId); - Map> currentPlayerzisMap = getPlayerzisMap(connecId); - reconnectedInstance.outCard(client, currentPlayerOutcardsMap, currentPlayerchisMap, currentPlayerpengsMap, currentPlayermingsMap, currentPlayerzisMap); + reconnectedInstance.outCard(client); log.info("断线重连后成功执行出牌操作"); } catch (Exception e) { log.error("断线重连后执行出牌操作时发生异常", e); - //即使出牌失败,也要确保连接状态正确 + //即使出牌失败 也要确保连接状态正确 try { if (robotUser != null) { robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_READY); @@ -373,10 +354,8 @@ public class RobotConnectionManager { /** * 处理接收到的游戏协议 - * 福禄寿支持的协议: - * 核心流程:811(发牌), 819(摸牌), 812(出牌广播), 813(出牌提示), 814(放招提示), 612(动作), 611(出牌), 815(动作通知), 816(胡牌), 817(结算), 820(换玩家) - * 飘鸟系统:1015(飘操作), 833(飘鸟提示), 2031(飘鸟提示 reload), 2032(飘鸟事件) - * 房间相关:2001, 2002, 2005, 2008, 2009 + * + * 福禄寿协议 */ private void handleProtocol(String command, Message message, TaurusClient client, String connecId) { RobotUser robotUser = robotRoomMapping.get(connecId); @@ -385,7 +364,7 @@ public class RobotConnectionManager { EXGameController.updateLastAccessTime(connecId); if (robotUser == null) { - log.error("未找到机器人用户信息,连接ID: {}", connecId); + log.error("未找到机器人用户信息,连接 ID: " + connecId); return; } @@ -398,145 +377,64 @@ public class RobotConnectionManager { //福禄寿 机器人处理事件 //初始化手牌 if ("811".equalsIgnoreCase(command)) { + log.info("【811】初始化手牌,connecId=" + connecId); robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_WORKING); - //初始化手牌 - String key = robotId+""; - if (jedis2.hget("{robortInfo}:" + key, "circleId") != null && jedis2.hget("{robortInfo}:" + key, "pid") != null) { - String circleId = jedis2.hget("{robortInfo}:" + key, "circleId"); - String pid = jedis2.hget("{robortInfo}:" + key, "pid"); - String getStart = "g{" + circleId + "}:play:" + pid; - if (!pid.equals("0")){ - jedis2.hset(getStart, key, "2"); - } - } - handler.initHandCards(message); + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.initHandCards(message); + currentInstance.saveToRedis(connecId); } //出牌广播 else if ("812".equalsIgnoreCase(command)) { - ITArray outcard_map = param.getTArray("outcard_map"); - ITArray opchicards = param.getTArray("opchicards"); - ITArray oppengcards = param.getTArray("oppengcards"); - ITArray opmingcards = param.getTArray("opmingcards"); - ITArray opzicards = param.getTArray("opzicards"); - - //获取当前连接专用的Maps - Map> currentPlayerOutcardsMap = getPlayerOutcardsMap(connecId); - Map> currentPlayerchisMap = getPlayerchisMap(connecId); - Map> currentPlayerpengsMap = getPlayerpengsMap(connecId); - Map> currentPlayermingsMap = getPlayermingsMap(connecId); - Map> currentPlayerzisMap = getPlayerzisMap(connecId); - - //清空旧数据 用新数据完全覆盖 - currentPlayerOutcardsMap.clear(); - currentPlayerchisMap.clear(); - currentPlayerpengsMap.clear(); - currentPlayermingsMap.clear(); - currentPlayerzisMap.clear(); - //出过的牌 - if (outcard_map != null) { - for (int i = 0; i < outcard_map.size(); i++) { - ITObject playerData = outcard_map.getTObject(i); - int playerId = playerData.getInt("playerId"); - ITArray outcardsArray = playerData.getTArray("outcards"); - - List outcardsList = new ArrayList<>(); - for (int j = 0; j < outcardsArray.size(); j++) { - outcardsList.add(outcardsArray.getInt(j)); - } - - //存储到当前连接的Map中(覆盖旧数据) - currentPlayerOutcardsMap.put(playerId, outcardsList); - } - } - - //吃的牌 - if (opchicards != null) { - for (int i = 0; i < opchicards.size(); i++) { - ITObject playerData = opchicards.getTObject(i); - int playerId = playerData.getInt("playerId"); - ITArray outchiArray = playerData.getTArray("opchicards"); - - List outchiList = new ArrayList<>(); - for (int j = 0; j < outchiArray.size(); j++) { - outchiList.add(outchiArray.getInt(j)); - } - currentPlayerchisMap.put(playerId, outchiList); - } - } - - //碰的牌 - if (oppengcards != null) { - for (int i = 0; i < oppengcards.size(); i++) { - ITObject playerData = oppengcards.getTObject(i); - int playerId = playerData.getInt("playerId"); - ITArray outpengArray = playerData.getTArray("oppengcards"); - - List outpengList = new ArrayList<>(); - for (int j = 0; j < outpengArray.size(); j++) { - outpengList.add(outpengArray.getInt(j)); - } - currentPlayerpengsMap.put(playerId, outpengList); - } - } - - //明杠的牌 - if (opmingcards != null) { - for (int i = 0; i < opmingcards.size(); i++) { - ITObject playerData = opmingcards.getTObject(i); - int playerId = playerData.getInt("playerId"); - ITArray outmingArray = playerData.getTArray("opmingcards"); - - List outmingList = new ArrayList<>(); - for (int j = 0; j < outmingArray.size(); j++) { - outmingList.add(outmingArray.getInt(j)); - } - currentPlayermingsMap.put(playerId, outmingList); - } - } - - //暗杠的牌 - if (opzicards != null) { - for (int i = 0; i < opzicards.size(); i++) { - ITObject playerData = opzicards.getTObject(i); - int playerId = playerData.getInt("playerId"); - ITArray outziArray = playerData.getTArray("opzicards"); - - List outziList = new ArrayList<>(); - for (int j = 0; j < outziArray.size(); j++) { - outziList.add(outziArray.getInt(j)); - } - currentPlayerzisMap.put(playerId, outziList); - } - } - - handler.onDiscardBroadcast(message); + log.info("【812】出牌广播,connecId=" + connecId); + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.onDiscardBroadcast(message); + currentInstance.saveToRedis(connecId); } - //摸牌 + //未知协议 3005 - 可能是游戏结束或其他通知 + else if ("3005".equalsIgnoreCase(command)) { + log.warn("【3005】收到未知协议 3005,connecId=" + connecId + ", param=" + param); + log.warn("这可能是游戏结束、流局或其他系统通知,需要检查服务器文档"); + // TODO: 根据 3005 的实际含义添加处理逻辑 + // 如果是游戏结束,可能需要清空状态或等待下一局 + } + //摸牌事件 else if ("819".equalsIgnoreCase(command)) { - handler.drawCard(message); + log.info("【819】摸牌事件,connecId=" + connecId + ", param=" + param); + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.drawCard(message); + currentInstance.saveToRedis(connecId); } - //出牌提示 + //出牌提示(轮到机器人出牌) else if ("813".equalsIgnoreCase(command)) { - //获取当前连接的 Maps - Map> currentPlayerOutcardsMap = getPlayerOutcardsMap(connecId); - Map> currentPlayerchisMap = getPlayerchisMap(connecId); - Map> currentPlayerpengsMap = getPlayerpengsMap(connecId); - Map> currentPlayermingsMap = getPlayermingsMap(connecId); - Map> currentPlayerzisMap = getPlayerzisMap(connecId); - - handler.makeDiscardDecision(client, currentPlayerOutcardsMap, currentPlayerchisMap, currentPlayerpengsMap, currentPlayermingsMap, currentPlayerzisMap); + log.info("【813】出牌提示,connecId=" + connecId); + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.makeDiscardDecision(client); + currentInstance.saveToRedis(connecId); } - //放招提示 + //放招提示/动作提示 else if ("814".equalsIgnoreCase(command)) { - handler.actionTip(param, client); + log.info("【814】动作提示,connecId=" + connecId); + log.info("【814】参数详情:" + param.toString()); + ITArray tipList = param.getTArray("tip_list"); + if (tipList != null) { + log.info("【814】tip_list 数量:" + tipList.size()); + for (int i = 0; i < tipList.size(); i++) { + TObject tip = (TObject) tipList.get(i).getObject(); + log.info("【814】tip[" + i + "]: type=" + tip.getInt("type") + ", id=" + tip.getInt("id")); + } + } + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.actionTip(param, client); + currentInstance.saveToRedis(connecId); + log.info("【814】处理完成,已保存到 Redis"); } //飘操作 else if ("1015".equalsIgnoreCase(command)) { - log.info("收到飘操作协议:{}", param); + log.info("收到飘操作协议:" + param); } - //2026.02.03修改 玩家加入房间 + //玩家加入房间 else if ("2001".equalsIgnoreCase(command)) { - //直接使用定时任务替代Thread.sleep,避免嵌套异步调用 + //直接使用定时任务替代Thread.sleep scheduleDelay(() -> { Jedis jedis = Redis.use().getJedis(); try { @@ -559,7 +457,7 @@ public class RobotConnectionManager { //更新机器人剩余数量 updateLeftoverRobot(robotId); disconnectFromGameServer(connecId); - log.info("2001发送退出房间协议1005,robotId: {}", robotId); + log.info("2001 发送退出房间协议 1005,robotId: " + robotId); }); } } @@ -573,11 +471,11 @@ public class RobotConnectionManager { } } }, 6, TimeUnit.SECONDS); - log.info("玩家{}加入房间:{}", robotUser.getCurrentRoomId(), param); + log.info("玩家" + robotUser.getCurrentRoomId() + "加入房间:" + param); } - //2026.02.03修改 玩家退出房间也要检查 + //玩家退出房间 else if ("2002".equalsIgnoreCase(command)) { - //直接使用定时任务替代Thread.sleep,避免嵌套异步调用 + //直接使用定时任务替代Thread.sleep scheduleDelay(() -> { Jedis jedis = Redis.use().getJedis(); try { @@ -600,7 +498,7 @@ public class RobotConnectionManager { //更新机器人剩余数量 updateLeftoverRobot(robotId); disconnectFromGameServer(connecId); - log.info("2002发送退出房间协议1005,robotId: {}", robotId); + log.info("2002 发送退出房间协议 1005,robotId: " + robotId); }); } } @@ -614,22 +512,22 @@ public class RobotConnectionManager { } }, 6, TimeUnit.SECONDS); } - //2026.02.05修改 玩家解散房间 + //解散房间 else if ("2005".equalsIgnoreCase(command)) { EXGameController.removeRobotRoomInfo(String.valueOf(robotId)); //更新机器人剩余数量 updateLeftoverRobot(robotId); disconnectFromGameServer(connecId); - log.info("2005玩家发送解散房间协议,robotId: {}", robotId); + log.info("2005 玩家发送解散房间协议,robotId: " + robotId); } - //2026.02.03修改 解散房间时候恢复机器人账号可以使用 + //恢复机器人账号 else if ("2008".equalsIgnoreCase(command)) { updateLeftoverRobot(Integer.parseInt(robotUser.getRobotId())); disconnectFromGameServer(connecId); } - //2026.02.03修改 通过机器人房间映射直接获取房间信息 + //获取房间信息 else if ("2009".equalsIgnoreCase(command)) { - //直接使用定时任务替代Thread.sleep,避免嵌套异步调用 + //直接使用定时任务替代Thread.sleep scheduleDelay(() -> { Jedis jedis = null; try { @@ -666,18 +564,18 @@ public class RobotConnectionManager { disconnectFromGameServer(connecId); //更新机器人剩余数量 updateLeftoverRobot(paramRobotId); - log.info("2009发送退出房间协议1005,robotId: {}", paramRobotId); + log.info("2009 发送退出房间协议 1005,robotId: " + paramRobotId); }); } } } } } catch (NumberFormatException e) { - log.error("2009协议数字格式异常,robotId: {}, connecId: {}", param.get("aid"), connecId); + log.error("2009 协议数字格式异常,robotId: " + param.get("aid") + ", connecId: " + connecId); } catch (NullPointerException e) { - log.error("2009协议空指针异常,connecId: {}", connecId); + log.error("2009 协议空指针异常,connecId: " + connecId); } catch (Exception e) { - log.error("2009协议处理异常: {}, connecId: {}", e.getMessage(), connecId, e); + log.error("2009 协议处理异常:" + e.getMessage() + ", connecId: " + connecId, e); } finally { if (jedis != null) { jedis.close(); @@ -685,11 +583,11 @@ public class RobotConnectionManager { } }, 6, TimeUnit.SECONDS); } - //结算 + //结算事件 else if ("817".equalsIgnoreCase(command)) { //清空所有 FuLuShouHandler 相关的集合数据 handler.clearAllData(); - + Integer type = param.getInt("type"); if (type == 1 || type == 2) { //为 1 为大结算 为 2 为解散 if (count != null && count.containsKey(pid)) { @@ -700,7 +598,7 @@ public class RobotConnectionManager { } //更新机器人剩余数量 updateLeftoverRobot(Integer.parseInt(robotUser.getRobotId())); - + //游戏结束后主动断开连接 disconnectFromGameServer(connecId); } @@ -709,29 +607,37 @@ public class RobotConnectionManager { client.send("1003", params, new ICallback() { @Override public void action(MessageResponse messageResponse) { - + } }); } - //服务器通知客户端有玩家执行了操作 + //动作通知(有玩家执行了操作) else if ("815".equalsIgnoreCase(command)) { + log.info("【815】玩家动作通知,connecId=" + connecId + ", param=" + param); handler.onPlayerAction(param); + log.info("【815】处理完成,准备保存到 Redis"); + //处理完协议后保存到 Redis + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.saveToRedis(connecId); + log.info("【815】已保存到 Redis"); } //飘鸟提示 else if ("833".equalsIgnoreCase(command)) { handler.piaoNiaoTip(); + //处理完协议后保存到 Redis + FuLuShouHandler currentInstance = fuLuShouHandlerInstances.get(connecId); + currentInstance.saveToRedis(connecId); } - //飘鸟提示 reload + //飘鸟提示 else if ("2031".equalsIgnoreCase(command)) { - log.info("收到飘鸟提示 reload: {}", param); + log.info("收到飘鸟提示 reload: " + param); } //飘鸟事件 else if ("2032".equalsIgnoreCase(command)) { - log.info("收到飘鸟事件:{}", param); + log.info("收到飘鸟事件:" + param); } - //2001-2009 房间相关协议保持原有逻辑 } catch (Exception e) { - log.error("处理接收到的游戏协议异常:{}, command: {}", e.getMessage(), command); + log.error("处理接收到的游戏协议异常:" + e.getMessage() + ", command: " + command); } finally { if (jedis0 != null) { jedis0.close(); @@ -753,7 +659,7 @@ public class RobotConnectionManager { jedis2.hset("{grobot}:" + robotId, "start", "0"); - log.info("机器人 {} 退出房间,修改gallrobot为0", robotId); + log.info("机器人 " + robotId + " 退出房间,修改 gallrobot 为 0"); } finally { jedis2.close(); } @@ -763,14 +669,14 @@ public class RobotConnectionManager { * 机器人登录 */ public void login(RobotUser robotUser){ - log.info("login:{}", robotUser.getRobotId()); + log.info("login:" + robotUser.getRobotId()); ITObject object = null; AccountBusiness accountBusiness = null; accountBusiness = new AccountBusiness(); try { //先快速登录 object = accountBusiness.fastLogin(Integer.parseInt(robotUser.getRobotId())); - log.info("object:{}", object); + log.info("object:" + object); if(object==null){ object = accountBusiness.idPasswordLogin(Integer.parseInt(robotUser.getRobotId()), robotUser.getPassword()); } @@ -792,7 +698,7 @@ public class RobotConnectionManager { connectGame(robotUser); robotUser.setConnecId(robotUser.getCurrentRoomId()+"_"+robotUser.getRobotId()); - log.info("重启获取的机器人还有当前房间,准备加入: {}", robotUser.getConnecId()); + log.info("重启获取的机器人还有当前房间,准备加入:" + robotUser.getConnecId()); exGameController.webGroupJoinRoom(robotUser); } } @@ -824,7 +730,7 @@ public class RobotConnectionManager { if(robotUser.getClient().isConnected()){ robotUser.setIsconnect(true); }else{ - log.info("reconnect{}", robotUser.getClient().getGameID()); + log.info("reconnect" + robotUser.getClient().getGameID()); TaurusClient client = new TaurusClient(robotUser.getGameHost()+":"+robotUser.getGamePort(), "cm"+robotUser.getRobotId(), TaurusClient.ConnectionProtocol.Tcp); client.setSession(robotUser.getLoginsession()); client.connect(); diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/handler/FuLuShouHandler.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/handler/FuLuShouHandler.java index 73d9aaf..7e3f48d 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/handler/FuLuShouHandler.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/handler/FuLuShouHandler.java @@ -4,23 +4,31 @@ 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.taurus.core.plugin.redis.Redis; +import com.taurus.core.util.Logger; +import redis.clients.jedis.Jedis; +import robot.zp.thread.ThreadPoolConfig; import taurus.client.Message; import taurus.client.TaurusClient; import taurus.util.CardUtil; +import taurus.util.FuLuShouAIStrategy; +import taurus.util.FuLuShouSuanFa; +import taurus.util.FuLuShouSuanFa.ActionRecord; +import taurus.util.FuLuShouSuanFa.ActionType; +import taurus.util.FuLuShouSuanFa.HuResult; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Random; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; + +import java.util.*; /** * 福禄寿游戏算法处理器 * 专门处理福禄寿麻将的游戏逻辑和算法 */ public class FuLuShouHandler { - private static final Logger log = LoggerFactory.getLogger(FuLuShouHandler.class); + private static final Logger log = Logger.getLogger(FuLuShouHandler.class); //手牌 private final List handCards = new ArrayList<>(); @@ -37,12 +45,33 @@ public class FuLuShouHandler { //杠的牌 private final List gangGroup = new ArrayList<>(); + //算法实例 + private final FuLuShouSuanFa suanFa = new FuLuShouSuanFa(); + + //AI实例 + private final FuLuShouAIStrategy aiStrategy = new FuLuShouAIStrategy(); + + //计算息数 + private final List actionRecords = new ArrayList<>(); + + //吃的牌组 + private final List> chowGroupsComplete = new ArrayList<>(); + + //碰的牌组 + private final List> pongGroupsComplete = new ArrayList<>(); + + //杠的牌组 + private final List> gangGroupsComplete = new ArrayList<>(); + + //听牌列表(由服务器 815 协议返回) + private List> tingList = new ArrayList<>(); + //会话标识 public String session = ""; - // 访问令牌 + //令牌 public String token = ""; - // 当前操作牌 + //当前操作牌 private int currentCard = 0; /** @@ -59,30 +88,10 @@ public class FuLuShouHandler { return outCards; } - /** - * 获取碰的牌 - */ - public List getPongGroup() { - return pongGroup; - } - - /** - * 获取吃的牌 - */ - public List getChowGroup() { - return chowGroup; - } - - /** - * 获取杠的牌 - */ - public List getGangGroup() { - return gangGroup; - } - /** * 获取手牌 */ + @Deprecated public List getChangShaCardInhand() { return handCards; } @@ -90,12 +99,42 @@ public class FuLuShouHandler { /** * 获取出过的牌 */ + @Deprecated public List getChuGuoCardInhand() { return outCards; } /** - * 初始化手牌 (协议 811) + * 获取吃的牌组 + */ + public List> getChowGroupsComplete() { + return chowGroupsComplete; + } + + /** + * 获取碰的牌组 + */ + public List> getPongGroupsComplete() { + return pongGroupsComplete; + } + + /** + * 获取杠的牌组 + */ + public List> getGangGroupsComplete() { + return gangGroupsComplete; + } + + /** + * 获取听牌列表 + */ + public List> getTingList() { + return tingList; + } + + /** + * 初始化手牌 (协议 811 - GAME_EVT_PLAYER_DEAL) + * @param message 包含手牌数据的消息 */ public void initHandCards(Message message) { ITObject param = message.param; @@ -110,12 +149,13 @@ public class FuLuShouHandler { handCards.add(cardList.getInt(i)); } - log.info("福禄寿初始化手牌:{} 张", handCards.size()); - log.debug("手牌详情:{}", handCards); + log.info("福禄寿初始化手牌:" + handCards.size() + " 张"); + log.debug("手牌详情:" + handCards); } /** - * 摸牌处理 (协议 819) + * 摸牌处理 (协议 819 - GAME_EVT_DRAW) + * @param message 包含摸牌数据的消息 */ public void drawCard(Message message) { ITObject param = message.param; @@ -127,13 +167,15 @@ public class FuLuShouHandler { if (drawnCard > 0) { handCards.add(drawnCard); currentCard = drawnCard; - log.info("福禄寿摸牌:{}", drawnCard); - log.debug("当前手牌数量:{}", handCards.size()); + log.info("福禄寿摸牌:" + drawnCard); + log.debug("当前手牌数量:" + handCards.size()); } } /** - * 出牌广播处理 (协议 812) + * 出牌广播处理 (协议 812 - GAME_EVT_DISCARD) + * 其他玩家出牌时触发的广播 + * @param message 包含出牌信息的消息 */ public void onDiscardBroadcast(Message message) { ITObject param = message.param; @@ -142,96 +184,189 @@ public class FuLuShouHandler { } currentCard = param.getInt("card"); - log.debug("出牌广播:card={}", currentCard); + log.debug("出牌广播:card=" + currentCard); } /** - * 动作提示处理 (协议 814) + * 动作提示处理 (协议 814 - GAME_EVT_FZTIPS) * 处理吃、碰、杠、胡等决策 */ public void actionTip(ITObject param, TaurusClient client) { + log.info("========== 开始处理 814 动作提示 =========="); ITArray tipList = param.getTArray("tip_list"); if (tipList == null || tipList.size() == 0) { + log.info("没有可用的动作提示 (tip_list 为空)"); return; } - log.info("收到动作提示,tip_list 数量:{}", tipList.size()); + log.info("收到动作提示,tip_list 数量:" + tipList.size()); - // 优先处理胡牌 for (int i = 0; i < tipList.size(); i++) { TObject tip = (TObject) tipList.get(i).getObject(); int type = tip.getInt("type"); int id = tip.getInt("id"); - - if (type == 6) { // 胡牌 - ITObject params = TObject.newInstance(); - params.putString("session", session + "," + token); - params.putInt("qi", 0); - params.putInt("id", id); - - delayedAction(client, params, "胡牌"); - return; + String actionName = ""; + switch (type) { + case 1: actionName = "吃"; break; + case 2: actionName = "碰"; break; + case 3: case 4: case 5: actionName = "杠"; break; + case 6: actionName = "胡(点炮)"; break; + case 7: actionName = "胡(自摸)"; break; + } + log.info("可选动作 [" + i + "]: " + actionName + " (type=" + type + ", id=" + id + ")"); + } + + //处理胡牌 + for (int i = 0; i < tipList.size(); i++) { + TObject tip = (TObject) tipList.get(i).getObject(); + int type = tip.getInt("type"); + int id = tip.getInt("id"); + + //胡牌(包括点炮和自摸) + if (type == 6 || type == 7) { + //检查是否 11 息 + int totalXi = getTotalXi(); + log.info("检测到胡牌动作,当前息数:" + totalXi + ", type=" + type); + if (totalXi >= 11) { + log.info("胡牌检测通过,总息数:" + totalXi + ",准备胡牌,type=" + type); + ITObject params = TObject.newInstance(); + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", id); + + delayedAction(client, params, "胡牌"); + return; + } else { + log.info("胡牌但息数不足,当前息数:" + totalXi + ",需要继续做牌"); + } } } - // TODO: 实现福禄寿专用的吃碰杠决策算法 - // 这里需要根据福禄寿的规则来实现评分系统 + //处理碰、杠等操作时检查是否是复字牌(复字牌不能吃碰杠) + List actionableTips = new ArrayList<>(); + log.info("开始遍历 tipList,tipList.size()=" + tipList.size()); + for (int i = 0; i < tipList.size(); i++) { + TObject tip = (TObject) tipList.get(i).getObject(); + int type = tip.getInt("type"); + int id = tip.getInt("id"); + int card = currentCard; + + log.info("index=" + i + ", type=" + type + ", id=" + id + ", currentCard=" + card); + log.info("card=" + card + ", canOperate=" + FuLuShouSuanFa.canOperateCompoundCard(card, false)); + + //是复字牌 跳过(复字牌只能自摸) + if (card > 0 && !FuLuShouSuanFa.canOperateCompoundCard(card, false)) { + log.info("复字牌不能吃碰杠,跳过:card=" + card); + continue; + } + + actionableTips.add(id); + log.info("成功添加 id=" + id + ", 当前 actionableTips=" + actionableTips); + } - // 默认过 + log.info("可执行的动作数量:" + actionableTips.size() + ", tips=" + actionableTips); + + //AI 决策:选择最佳操作 + if (!actionableTips.isEmpty()) { + log.info("开始调用 aiStrategy.selectBestAction, actionableTips.size()=" + actionableTips.size()); + int bestId = aiStrategy.selectBestAction(this, actionableTips, tipList); + log.info("bestId=" + bestId + ", actionableTips=" + actionableTips); + if (bestId > 0) { + ITObject params = TObject.newInstance(); + params.putString("session", session + "," + token); + params.putInt("qi", 0); + params.putInt("id", bestId); + + String actionName = getActionName(bestId, tipList); + log.info("执行动作:" + actionName); + delayedAction(client, params, actionName); + return; + } else { + log.warn("AI 选择不操作,bestId=" + bestId); + } + } else { + log.info("没有可执行的动作,将选择过"); + } + + //默认过 ITObject params = TObject.newInstance(); params.putString("session", session + "," + token); params.putInt("qi", 0); params.putInt("id", 0); + log.info("默认过操作"); delayedAction(client, params, "默认过"); + log.info("========== 814 动作提示处理完成 =========="); } /** - * 同步手牌 + * 获取动作名称 + */ + private String getActionName(int id, ITArray tipList) { + for (int i = 0; i < tipList.size(); i++) { + TObject tip = (TObject) tipList.get(i).getObject(); + if (tip.getInt("id") == id) { + int type = tip.getInt("type"); + switch (type) { + case 1: return "吃"; + case 2: return "碰"; + case 3: case 4: case 5: return "杠"; + case 6: return "胡(点炮)"; + case 7: return "胡(自摸)"; + } + } + } + return "未知动作"; + } + + /** + * 同步手牌数据 + * @param handCard 新的手牌列表 */ public void updateHandCard(List handCard) { - log.info("updateHandCard 同步手牌:{}", handCard); + log.info("updateHandCard 同步手牌:" + handCard); handCards.clear(); handCards.addAll(handCard); - log.info("updateHandCard 同步手牌完成,数量:{}", handCards.size()); + log.info("updateHandCard 同步手牌完成,数量:" + handCards.size()); } /** - * 同步出牌 + * 同步出牌记录 + * @param outCard 新的出牌列表 */ public void updateOutCard(List outCard) { outCards.clear(); outCards.addAll(outCard); - log.info("updateOutCard 同步出牌完成,数量:{}", outCards.size()); + log.info("updateOutCard 同步出牌完成,数量:" + outCards.size()); } - + /** - * 出牌决策 (协议 813) - 兼容旧方法名 + * 出牌决策 (协议 813 - GAME_EVT_DISCARD_TIP) + * 当轮到机器人出牌时调用 */ - public void outCard(TaurusClient client, - Map> playerOutcardsMap, - Map> playerchisMap, - Map> playerpengsMap, - Map> playermingsMap, - Map> playerzisMap) { - makeDiscardDecision(client, playerOutcardsMap, playerchisMap, playerpengsMap, playermingsMap, playerzisMap); - } - public void makeDiscardDecision(TaurusClient client, - Map> playerOutcardsMap, - Map> playerchisMap, - Map> playerpengsMap, - Map> playermingsMap, - Map> playerzisMap) { - if (handCards.isEmpty()) { - log.warn("手牌为空,无法出牌"); - return; + public void makeDiscardDecision(TaurusClient client) { + log.info("============ 813 出牌提示收到 ============"); + log.info("当前手牌数量:" + handCards.size()); + log.info("当前手牌:" + handCards); + + if (handCards.isEmpty()) { + log.error("ERROR: 手牌为空!是否没有收到 811 初始化手牌协议?"); + log.warn("尝试从 Redis 恢复手牌..."); + // TODO: 是否需要在这里调用 restoreFromRedis()? + } + + if (handCards.isEmpty()) { + log.warn("手牌为空,无法出牌"); + return; + } + + //AI智能选择最佳出牌 + int cardToOut = aiStrategy.selectBestDiscardCard(this); + + if (cardToOut < 0) { + log.error("AI 出牌选择失败,使用默认出牌"); + cardToOut = handCards.get(0); } - //TODO: 实现福禄寿专用的出牌算法 - //需要分析其他玩家的出牌、吃碰杠情况 - - //临时:选择第一张牌 - int cardToOut = handCards.get(0); - ITObject params = TObject.newInstance(); params.putInt("card", cardToOut); @@ -241,55 +376,311 @@ public class FuLuShouHandler { params.putTArray("outcard_list", CardUtil.maJiangToTArray(cardsToSend)); } - params.putTArray("card_list", CardUtil.maJiangToTArray(handCards)); - params.putString("session", session + "," + token); - - // 记录出牌 + //关键修复:先移除要出的牌,再发送剩余手牌 outCards.add(cardToOut); handCards.remove(Integer.valueOf(cardToOut)); - log.info("福禄寿出牌:{}", cardToOut); - log.debug("剩余手牌:{}", handCards); + params.putTArray("card_list", CardUtil.maJiangToTArray(handCards)); + params.putString("session", session + "," + token); - // 延迟发送,模拟思考时间 + log.info("福禄寿出牌:" + cardToOut + "(AI 智能选择)"); + log.debug("剩余手牌:" + handCards); + + //延迟发送 delayedDiscard(client, params); } /** - * 玩家动作通知处理 (协议 815) + * 出牌决策(兼容旧方法名) */ - public void onPlayerAction(ITObject param) { - Integer card = param.getInt("card"); - Integer type = param.getInt("type"); - Integer playerId = param.getInt("playerid"); - - log.debug("玩家动作通知:playerId={}, type={}, card={}", playerId, type, card); - - // 根据类型更新牌型记录 - if (type == 2) { // 碰 - pongGroup.add(card); - pongGroup.add(card); - pongGroup.add(card); - } else if (type == 1) { // 吃 - chowGroup.add(card); - } else if (type == 3 || type == 4 || type == 5) { // 杠 - gangGroup.add(card); - gangGroup.add(card); - gangGroup.add(card); - gangGroup.add(card); - } + @Deprecated + public void outCard(TaurusClient client) { + makeDiscardDecision(client); } /** - * 飘鸟提示处理 (协议 833) + * 玩家动作通知处理 (协议 815 - GAME_EVT_ACTION) + * 当有其他玩家执行操作时更新本地状态 + */ + public void onPlayerAction(ITObject param) { + log.info("========== 开始处理 815 玩家动作通知 =========="); + log.info("收到参数:" + param.toString()); + + Integer card = param.getInt("card"); + Integer type = param.getInt("type"); + Integer playerId = param.getInt("playerid"); + Integer fromSeat = param.getInt("from_seat"); + + log.info("解析参数:playerId=" + playerId + ", type=" + type + ", card=" + card + ", fromSeat=" + fromSeat); + + if (card == null || type == null) { + log.error("ERROR: card 或 type 为 null!"); + return; + } + + log.debug("当前手牌:" + handCards); + log.debug("当前 chowGroupsComplete: " + chowGroupsComplete); + log.debug("当前 pongGroupsComplete: " + pongGroupsComplete); + + //从 Redis 获取机器人 ID 列表 + List robotIdsList = getRobotIdsFromRedis(); + boolean isRobotAction = robotIdsList.contains(playerId); + + //碰 + if (type == 2) { + log.info("检测到碰牌操作:card=" + card + ", playerId=" + playerId); + + if (isRobotAction) { + log.info("碰牌前手牌数量:" + handCards.size() + ", 手牌:" + handCards); + + pongGroup.add(card); + pongGroup.add(card); + pongGroup.add(card); + actionRecords.add(new ActionRecord(ActionType.PONG, card)); + + List pongGroupComplete = new ArrayList<>(); + pongGroupComplete.add(card); + pongGroupComplete.add(card); + pongGroupComplete.add(card); + pongGroupsComplete.add(pongGroupComplete); + + int removeCount = 0; + boolean removed1 = handCards.remove(Integer.valueOf(card)); + if (removed1) removeCount++; + boolean removed2 = handCards.remove(Integer.valueOf(card)); + if (removed2) removeCount++; + boolean removed3 = handCards.remove(Integer.valueOf(card)); + if (removed3) removeCount++; + + if (removeCount == 3) { + log.info("从手牌中移除 3 张:" + card); + } else { + log.error("这会导致手牌数量错误,服务器会认为机器人手牌异常!"); + } + + log.info("机器人碰牌完成:playerId=" + playerId + ", 碰的牌=" + card + ", 剩余手牌:" + handCards.size()); + + //校验手牌数量 + int totalMeldCards = chowGroupsComplete.size() * 3 + pongGroupsComplete.size() * 3 + gangGroupsComplete.size() * 4; + int expectedHandCount = 19 - totalMeldCards; + validateHandCards(expectedHandCount); + } else { + log.info("其他玩家碰牌,不修改机器人手牌:playerId=" + playerId + ", card=" + card); + } + + log.info("当前 pongGroupsComplete: " + pongGroupsComplete); + log.info("当前 actionRecords: " + actionRecords.size() + " 条记录"); + } + //吃 + else if (type == 1) { + log.info("检测到吃牌操作:card=" + card + ", playerId=" + playerId); + + if (isRobotAction) { + //从手牌中移除组成句子的另外两张牌 + List chowCards = findChowCards(card); + + if (!chowCards.isEmpty()) { + log.info("吃牌前手牌数量:" + handCards.size() + ", 手牌:" + handCards); + + //从手牌中移除吃的牌(移除手牌中的那两张) + int removeCount = 0; + for (int i = 1; i < chowCards.size(); i++) { + Integer cardToRemove = chowCards.get(i); + boolean removed = handCards.remove(cardToRemove); + if (removed) { + removeCount++; + log.debug("从手牌中移除:" + cardToRemove); + } else { + log.error("吃牌时从手牌中移除失败!cardToRemove=" + cardToRemove); + } + } + + if (removeCount != 2) { + } + + for (int c : chowCards) { + chowGroup.add(c); + } + + actionRecords.add(new ActionRecord(ActionType.CHOW, chowCards)); + chowGroupsComplete.add(chowCards); + + log.info("机器人吃牌完成:playerId=" + playerId + ", 吃的牌=" + card + ", 组合=" + chowCards); + log.info("吃牌后剩余手牌:" + handCards + ", 数量:" + handCards.size()); + + //校验手牌数量 + int totalMeldCards = chowGroupsComplete.size() * 3 + pongGroupsComplete.size() * 3 + gangGroupsComplete.size() * 4; + int expectedHandCount = 19 - totalMeldCards; + validateHandCards(expectedHandCount); + } else { + log.error("机器人吃牌但未找到组合!card=" + card); + log.error("这会导致手牌数量错误,服务器会认为机器人手牌异常!"); + } + } else { + log.info("其他玩家吃牌,不修改机器人手牌:playerId=" + playerId + ", card=" + card); + } + + log.info("当前 chowGroupsComplete: " + chowGroupsComplete); + log.info("当前 actionRecords: " + actionRecords.size() + " 条记录"); + } + //杠 + else if (type == 3 || type == 4 || type == 5) { + log.info("检测到杠牌操作:card=" + card + ", playerId=" + playerId + ", 杠类型=" + type); + + if (isRobotAction) { + log.info("杠牌前手牌数量:" + handCards.size() + ", 手牌:" + handCards); + + gangGroup.add(card); + gangGroup.add(card); + gangGroup.add(card); + gangGroup.add(card); + actionRecords.add(new ActionRecord(ActionType.GANG, card)); + + List gangGroupComplete = new ArrayList<>(); + gangGroupComplete.add(card); + gangGroupComplete.add(card); + gangGroupComplete.add(card); + gangGroupComplete.add(card); + gangGroupsComplete.add(gangGroupComplete); + + int removeCount = 0; + for (int i = 0; i < 4; i++) { + boolean removed = handCards.remove(Integer.valueOf(card)); + if (removed) { + removeCount++; + log.debug("第" + (i + 1) + "次移除:" + card); + } else { + log.error("杠牌第" + (i + 1) + "次移除失败!card=" + card + ", 剩余手牌=" + handCards); + } + } + + if (removeCount == 4) { + log.info("从手牌中移除 4 张:" + card); + } else { + log.error("杠牌应该移除 4 张,实际移除:" + removeCount + ", card=" + card + ", 剩余手牌=" + handCards); + } + + log.info("机器人杠牌完成:playerId=" + playerId + ", 杠的牌=" + card + ", 剩余手牌:" + handCards.size()); + + //校验手牌数量 + int totalMeldCards = chowGroupsComplete.size() * 3 + pongGroupsComplete.size() * 3 + gangGroupsComplete.size() * 4; + int expectedHandCount = 19 - totalMeldCards; + validateHandCards(expectedHandCount); + } else { + log.info("其他玩家杠牌,不修改机器人手牌:playerId=" + playerId + ", card=" + card); + } + + log.info("当前 gangGroupsComplete: " + gangGroupsComplete); + log.info("当前 actionRecords: " + actionRecords.size() + " 条记录"); + } + //胡 + else if (type == 7) { + log.info("检测到胡牌操作:card=" + card + ", playerId=" + playerId + ", hu_xi=" + param.getInt("hu_xi")); + + if (isRobotAction) { + log.info("机器人自己胡牌,清空所有状态准备下一局"); + clearAllData(); + log.info("机器人胡牌处理完成,等待下一局开始"); + } else { + log.info("其他玩家胡牌,保留机器人当前状态"); + log.info("当前手牌数量:" + handCards.size() + ", 已吃碰杠组数:chow=" + chowGroupsComplete.size() + ", pong=" + pongGroupsComplete.size() + ", gang=" + gangGroupsComplete.size()); + } + } + //昭牌 (type=5 或 type=6) + else if (type == 5 || type == 6) { + log.info("检测到昭牌操作:card=" + card + ", playerId=" + playerId + ", type=" + type); + if (!isRobotAction) { + log.info("其他玩家昭牌,保留机器人当前状态"); + } + } + else { + log.warn("未知的操作类型:type=" + type); + } + + if (type == 2) { + List pongCards = Arrays.asList(card, card, card); + if (!suanFa.isKan(pongCards)) { + log.warn("检测到不合法的碰牌:" + card); + } + } + //杠 + else if (type == 3 || type == 4 || type == 5) { + List gangCards = Arrays.asList(card, card, card, card); + if (!suanFa.isGang(gangCards)) { + log.warn("检测到不合法的杠牌:" + card); + } + } + + log.debug("更新后的 chowGroupsComplete: " + chowGroupsComplete); + log.debug("更新后的 pongGroupsComplete: " + pongGroupsComplete); + log.debug("更新后的 gangGroupsComplete: " + gangGroupsComplete); + log.debug("更新后的 actionRecords: " + actionRecords.size() + " 条"); + + ITArray tingListArray = param.getTArray("ting_list"); + if (tingListArray != null && tingListArray.size() > 0) { + log.info("服务器返回听牌列表,数量:" + tingListArray.size()); + this.tingList.clear(); + for (int i = 0; i < tingListArray.size(); i++) { + try { + Object tipObj = tingListArray.get(i).getObject(); + if (tipObj instanceof TObject) { + TObject tip = (TObject) tipObj; + Map tingInfo = new HashMap<>(); + tingInfo.put("card", tip.getInt("card")); + //保存value数组 + ITArray valueArray = tip.getTArray("value"); + if (valueArray != null) { + List values = new ArrayList<>(); + for (int j = 0; j < valueArray.size(); j++) { + values.add(valueArray.getInt(j)); + } + tingInfo.put("value", values); + } + this.tingList.add(tingInfo); + log.info("听牌=" + tip.getInt("card") + ", 组合="); + } + } catch (Exception e) { + log.warn("解析 ting_list 第" + i + "个元素失败:" + e.getMessage()); + } + } + log.info("机器人已听牌!听牌数量:" + this.tingList.size()); + } else { + //没有听牌时清空列表 + this.tingList.clear(); + log.debug("未听牌"); + } + + log.info("========== 815 玩家动作通知处理完成 =========="); + } + + /** + * 飘鸟提示处理 (协议 833 - GAME_EVT_PIAONIAO_TIP) */ public void piaoNiaoTip() { log.info("收到飘鸟提示"); - //TODO: 实现飘鸟决策逻辑 + } + + /** + * 从 Redis 获取机器人 ID 列表 + * @return 机器人 ID 列表 + */ + private List getRobotIdsFromRedis() { + List robotIdsList = new ArrayList<>(); + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + Set allKeys = jedis.keys("{robot}:*"); + for (String key : allKeys) { + robotIdsList.add(Integer.valueOf(key.replace("{robot}:",""))); + } + } finally { + jedis.close(); + } + return robotIdsList; } /** * 清理所有数据 + * 在每局游戏结束时调用 */ public void clearAllData() { handCards.clear(); @@ -297,35 +688,316 @@ public class FuLuShouHandler { pongGroup.clear(); chowGroup.clear(); gangGroup.clear(); + actionRecords.clear(); + chowGroupsComplete.clear(); + pongGroupsComplete.clear(); + gangGroupsComplete.clear(); + tingList.clear(); currentCard = 0; log.info("福禄寿处理器数据已清空"); } + /** + * 胡牌检测 + * @return 胡牌结果 + */ + public HuResult checkWin() { + List allCards = new ArrayList<>(handCards); + + //添加已吃的牌 + for (List group : chowGroupsComplete) { + allCards.addAll(group); + } + //添加已碰的牌 + for (List group : pongGroupsComplete) { + allCards.addAll(group); + } + //添加已杠的牌 + for (List group : gangGroupsComplete) { + allCards.addAll(group); + } + + HuResult result = suanFa.checkWin(allCards, chowGroupsComplete, pongGroupsComplete, gangGroupsComplete); + + if (result.isWin()) { + log.info("胡牌检测成功!总息数:" + result.getTotalXi()); + } else { + log.debug("胡牌检测失败:" + result.getMessage()); + } + + return result; + } + + /** + * 计算当前总息数 + * 总息数 = 打牌过程中累计的息(碰、吃、招) + 胡牌时半搭子(或单字)的息 + * @return 总息数 + */ + public int getTotalXi() { + //计算打牌过程中吃碰杠累计的息数 + int actionXi = suanFa.calculateXiFromActions(actionRecords); + + //计算胡牌时手牌中的半搭子(或单字)息数 + HuResult result = checkWin(); + int handXi = 0; + + if (result.isWin()) { + //胡牌检测成功 需要从手牌中提取半搭子或单字信息 + //第一种胡牌类型:五句 + 两个半搭子 + if (chowGroupsComplete.size() + pongGroupsComplete.size() + gangGroupsComplete.size() == 5) { + //提取两个半搭子 + List> halfCombos = extractHalfCombosFromHand(); + if (halfCombos.size() == 2) { + handXi = FuLuShouSuanFa.calculateHalfComboXi(halfCombos); + log.debug("胡牌检测通过,两个半搭子贡献息数:" + handXi); + } + } + //第二种胡牌类型:六句 + 任意一字 + else if (chowGroupsComplete.size() + pongGroupsComplete.size() + gangGroupsComplete.size() == 6) { + //提取单张牌 + int singleCard = extractSingleCardFromHand(); + if (singleCard != 0) { + handXi = FuLuShouSuanFa.calculateSingleCardXi(singleCard); + log.debug("胡牌检测通过,单张牌贡献息数:" + handXi + ", card=" + singleCard); + } + } + } + + int totalXi = actionXi + handXi; + log.debug("计算总息数 - 动作息数:" + actionXi + ", 手牌息数:" + handXi); + log.info("计算总息数结果:" + totalXi); + + return totalXi; + } + + /** + * 从手牌中查找吃牌的三张组合 + * @param card 吃的那张牌(别人打出的牌) + * @return 三张牌的组合 (包括别人的那张牌 + 手牌中的两张) + */ + private List findChowCards(int card) { + List result = new ArrayList<>(); + + //复字牌不能吃别人的牌 + if (FuLuShouSuanFa.isCompoundCard(card)) { + log.warn("复字牌不能吃别人的牌:card=" + card); + return result; + } + + //从8个句子组合中找到包含这张牌的组合 + List> compoundGroups = getCompoundGroups(); + for (List group : compoundGroups) { + if (group.contains(card)) { + //检查手牌中是否有这个组合中的另外两张牌 + List neededCards = new ArrayList<>(group); + neededCards.remove(Integer.valueOf(card)); + + //手牌中必须有另外两张才能吃 + boolean hasAllNeeded = true; + List cardsToRemoveFromHand = new ArrayList<>(); + + for (Integer needed : neededCards) { + boolean found = false; + for (Integer handCard : handCards) { + if (handCard.equals(needed) && !cardsToRemoveFromHand.contains(handCard)) { + found = true; + cardsToRemoveFromHand.add(handCard); + break; + } + } + if (!found) { + hasAllNeeded = false; + break; + } + } + + if (hasAllNeeded) { + result.add(card); + result.addAll(cardsToRemoveFromHand); + log.info("找到吃牌组合:card=" + card + ", 手牌中的牌=" + cardsToRemoveFromHand + ", 完整组合=" + result); + return result; + } + } + } + + log.debug("无法找到吃牌组合:card=" + card + ", 手牌=" + handCards); + return result; + } + + /** + * 校验手牌数量是否正确 + * @param expectedCount 期望的手牌数量 + * @return 是否校验通过 + */ + private boolean validateHandCards(int expectedCount) { + int actualCount = handCards.size(); + if (actualCount != expectedCount) { + log.error("已吃的牌组:" + chowGroupsComplete); + log.error("已碰的牌组:" + pongGroupsComplete); + log.error("已杠的牌组:" + gangGroupsComplete); + return false; + } + log.debug("手牌数量校验通过:" + actualCount + "张"); + return true; + } + + /** + * 获取所有复字牌组合 + * 福禄寿麻将规则:每个数字的 1-4 可以组成句子 + */ + private List> getCompoundGroups() { + List> groups = new ArrayList<>(); + + //100 系列:101-104 + groups.add(Arrays.asList(101, 102, 103)); + groups.add(Arrays.asList(101, 102, 104)); + groups.add(Arrays.asList(101, 103, 104)); + groups.add(Arrays.asList(102, 103, 104)); + + //200 系列:201-204 + groups.add(Arrays.asList(201, 202, 203)); + groups.add(Arrays.asList(201, 202, 204)); + groups.add(Arrays.asList(201, 203, 204)); + groups.add(Arrays.asList(202, 203, 204)); + + //300 系列:301-304 + groups.add(Arrays.asList(301, 302, 303)); + groups.add(Arrays.asList(301, 302, 304)); + groups.add(Arrays.asList(301, 303, 304)); + groups.add(Arrays.asList(302, 303, 304)); + + //400 系列:401-404 + groups.add(Arrays.asList(401, 402, 403)); + groups.add(Arrays.asList(401, 402, 404)); + groups.add(Arrays.asList(401, 403, 404)); + groups.add(Arrays.asList(402, 403, 404)); + + //500 系列:501-504 + groups.add(Arrays.asList(501, 502, 503)); + groups.add(Arrays.asList(501, 502, 504)); + groups.add(Arrays.asList(501, 503, 504)); + groups.add(Arrays.asList(502, 503, 504)); + + //600 系列:601-604 + groups.add(Arrays.asList(601, 602, 603)); + groups.add(Arrays.asList(601, 602, 604)); + groups.add(Arrays.asList(601, 603, 604)); + groups.add(Arrays.asList(602, 603, 604)); + + //700 系列:701-704 + groups.add(Arrays.asList(701, 702, 703)); + groups.add(Arrays.asList(701, 702, 704)); + groups.add(Arrays.asList(701, 703, 704)); + groups.add(Arrays.asList(702, 703, 704)); + + //800 系列:801-804 + groups.add(Arrays.asList(801, 802, 803)); + groups.add(Arrays.asList(801, 802, 804)); + groups.add(Arrays.asList(801, 803, 804)); + groups.add(Arrays.asList(802, 803, 804)); + + return groups; + } + private List> extractHalfCombosFromHand() { + List> halfCombos = new ArrayList<>(); + + //统计每种牌的数量 + Map cardCount = new HashMap<>(); + for (int card : handCards) { + cardCount.put(card, cardCount.getOrDefault(card, 0) + 1); + } + + //优先查找对子 + for (Map.Entry entry : cardCount.entrySet()) { + if (entry.getValue() >= 2) { + List pair = new ArrayList<>(); + pair.add(entry.getKey()); + pair.add(entry.getKey()); + halfCombos.add(pair); + + entry.setValue(entry.getValue() - 2); + + //如果已经有两个半搭子 返回 + if (halfCombos.size() >= 2) { + return halfCombos; + } + } + } + + //福、上 + List specialChars = Arrays.asList(101, 201); + //大、人、禄、寿 + List subSpecialChars = Arrays.asList(301, 501, 701, 1013); + + for (int special : specialChars) { + if (cardCount.containsKey(special) && cardCount.get(special) >= 1) { + for (int subSpecial : subSpecialChars) { + if (cardCount.containsKey(subSpecial) && cardCount.get(subSpecial) >= 1) { + List combo = new ArrayList<>(); + combo.add(special); + combo.add(subSpecial); + halfCombos.add(combo); + + cardCount.put(special, cardCount.get(special) - 1); + cardCount.put(subSpecial, cardCount.get(subSpecial) - 1); + + if (halfCombos.size() >= 2) { + return halfCombos; + } + } + } + } + } + + //查找大人组合 + if (cardCount.containsKey(301) && cardCount.get(301) >= 1 && + cardCount.containsKey(501) && cardCount.get(501) >= 1) { + List dareCombo = new ArrayList<>(); + dareCombo.add(301);//大 + dareCombo.add(501);//人 + halfCombos.add(dareCombo); + } + + return halfCombos; + } + + /** + * 从手牌中提取单张牌(用于六句加一字胡牌类型) + * @return 单张牌的 ID,如果无法提取则返回 0 + */ + private int extractSingleCardFromHand() { + if (handCards.size() != 1) { + log.warn("手牌数量不是 1 张,无法提取单张牌,当前手牌数:" + handCards.size()); + return 0; + } + + return handCards.get(0); + } + /** * 延迟执行动作 */ private void delayedAction(TaurusClient client, ITObject params, String actionName) { - Thread thread = new Thread(() -> { + ThreadPoolConfig.getBusinessThreadPool().execute(() -> { try { int delaySeconds = 1 + new Random().nextInt(2); - log.info("执行{}动作,延迟{}秒", actionName, delaySeconds); + log.info("执行" + actionName + "动作,延迟" + delaySeconds + "秒"); Thread.sleep(delaySeconds * 1000); client.send("612", params, response -> { - log.info("{}动作发送完成", actionName); + log.info(actionName + "动作发送完成"); }); } catch (Exception e) { - log.error("执行{}动作时发生异常:{}", actionName, e.getMessage(), e); + log.error("执行" + actionName + "动作时发生异常:" + e.getMessage(), e); } }); - thread.start(); } /** * 延迟出牌 */ private void delayedDiscard(TaurusClient client, ITObject params) { - Thread thread = new Thread(() -> { + ThreadPoolConfig.getBusinessThreadPool().execute(() -> { try { int delay = new Random().nextInt(4); Thread.sleep(delay * 1000); @@ -334,9 +1006,173 @@ public class FuLuShouHandler { log.debug("出牌发送完成"); }); } catch (Exception e) { - log.error("出牌时发生异常:{}", e.getMessage(), e); + log.error("出牌时发生异常:" + e.getMessage(), e); } }); - thread.start(); + } + + /** + * 保存状态到 Redis + */ + public void saveToRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + Map stateMap = new HashMap<>(); + + //序列化集合 + Gson gson = new Gson(); + stateMap.put("handCards", gson.toJson(handCards)); + stateMap.put("outCards", gson.toJson(outCards)); + stateMap.put("pongGroup", gson.toJson(pongGroup)); + stateMap.put("chowGroup", gson.toJson(chowGroup)); + stateMap.put("gangGroup", gson.toJson(gangGroup)); + stateMap.put("chowGroupsComplete", gson.toJson(chowGroupsComplete)); + stateMap.put("pongGroupsComplete", gson.toJson(pongGroupsComplete)); + stateMap.put("gangGroupsComplete", gson.toJson(gangGroupsComplete)); + stateMap.put("actionRecords", gson.toJson(actionRecords)); + stateMap.put("tingList", gson.toJson(tingList)); + + stateMap.put("currentCard", String.valueOf(currentCard)); + stateMap.put("session", session); + stateMap.put("token", token); + + String redisKey = "{fls}:" + connecId; + jedis.hmset(redisKey, stateMap); + //1小时过期时间 + jedis.expire(redisKey, 3600); + + log.info("保存 FuLuShouHandler 状态到 Redis: " + connecId); + } catch (Exception e) { + log.error("保存 FuLuShouHandler 状态到 Redis 失败:" + e.getMessage()); + } finally { + jedis.close(); + } + } + + /** + * 从 Redis 恢复状态 + */ + public boolean restoreFromRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + String redisKey = "{fls}:" + connecId; + Map stateMap = jedis.hgetAll(redisKey); + + if (stateMap == null || stateMap.isEmpty()) { + log.info("Redis 中没有找到 FuLuShouHandler 状态数据" + connecId); + return false; + } + + //反序列化集合 + Gson gson = new Gson(); + Type listIntegerType = new TypeToken>(){}.getType(); + Type listListIntegerType = new TypeToken>>(){}.getType(); + Type actionRecordListType = new TypeToken>(){}.getType(); + Type tingListType = new TypeToken>>(){}.getType(); + + if (stateMap.containsKey("handCards")) { + handCards.clear(); + List restoredHandCards = gson.fromJson(stateMap.get("handCards"), listIntegerType); + if (restoredHandCards != null) handCards.addAll(restoredHandCards); + } + + if (stateMap.containsKey("outCards")) { + outCards.clear(); + List restoredOutCards = gson.fromJson(stateMap.get("outCards"), listIntegerType); + if (restoredOutCards != null) outCards.addAll(restoredOutCards); + } + + if (stateMap.containsKey("pongGroup")) { + pongGroup.clear(); + List restoredPongGroup = gson.fromJson(stateMap.get("pongGroup"), listIntegerType); + if (restoredPongGroup != null) pongGroup.addAll(restoredPongGroup); + } + + if (stateMap.containsKey("chowGroup")) { + chowGroup.clear(); + List restoredChowGroup = gson.fromJson(stateMap.get("chowGroup"), listIntegerType); + if (restoredChowGroup != null) chowGroup.addAll(restoredChowGroup); + } + + if (stateMap.containsKey("gangGroup")) { + gangGroup.clear(); + List restoredGangGroup = gson.fromJson(stateMap.get("gangGroup"), listIntegerType); + if (restoredGangGroup != null) gangGroup.addAll(restoredGangGroup); + } + + if (stateMap.containsKey("chowGroupsComplete")) { + chowGroupsComplete.clear(); + List> restoredChowGroups = gson.fromJson(stateMap.get("chowGroupsComplete"), listListIntegerType); + if (restoredChowGroups != null) chowGroupsComplete.addAll(restoredChowGroups); + } + + if (stateMap.containsKey("pongGroupsComplete")) { + pongGroupsComplete.clear(); + List> restoredPongGroups = gson.fromJson(stateMap.get("pongGroupsComplete"), listListIntegerType); + if (restoredPongGroups != null) pongGroupsComplete.addAll(restoredPongGroups); + } + + if (stateMap.containsKey("gangGroupsComplete")) { + gangGroupsComplete.clear(); + List> restoredGangGroups = gson.fromJson(stateMap.get("gangGroupsComplete"), listListIntegerType); + if (restoredGangGroups != null) gangGroupsComplete.addAll(restoredGangGroups); + } + + if (stateMap.containsKey("actionRecords")) { + actionRecords.clear(); + List restoredRecords = gson.fromJson(stateMap.get("actionRecords"), actionRecordListType); + if (restoredRecords != null) actionRecords.addAll(restoredRecords); + } + + if (stateMap.containsKey("tingList")) { + tingList.clear(); + List> restoredTingList = gson.fromJson(stateMap.get("tingList"), tingListType); + if (restoredTingList != null) tingList.addAll(restoredTingList); + } + + //恢复基本属性 + if (stateMap.containsKey("currentCard")) { + currentCard = Integer.parseInt(stateMap.get("currentCard")); + } + + session = stateMap.getOrDefault("session", ""); + token = stateMap.getOrDefault("token", ""); + log.info("从 Redis 恢复 FuLuShouHandler 状态成功:" + connecId + ", 手牌数量:" + handCards.size()); + return true; + } catch (Exception e) { + log.error("从 Redis 恢复 FuLuShouHandler 状态失败:" + e.getMessage()); + return false; + } finally { + jedis.close(); + } + } + + /** + * 检查指定的手牌是否可以胡牌(用于 AI 模拟) + * @param handCards 手牌列表 + * @param chowGroups 吃的牌组 + * @param pongGroups 碰的牌组 + * @param gangGroups 杠的牌组 + * @return 胡牌结果 + */ + public FuLuShouSuanFa.HuResult checkWinWithHand(List handCards, List> chowGroups, List> pongGroups, List> gangGroups) { + //使用算法实例检查胡牌 + return suanFa.checkWin(handCards, chowGroups, pongGroups, gangGroups); + } + + /** + * 从 Redis 删除状态 + */ + public static void removeFromRedis(String connecId) { + Jedis jedis = Redis.use("group1_db2").getJedis(); + try { + String redisKey = "{fls}:" + connecId; + jedis.del(redisKey); + log.debug("从 Redis 删除 FuLuShouHandler 状态:" + connecId); + } catch (Exception e) { + log.error("从 Redis 删除 FuLuShouHandler 状态失败:" + e.getMessage()); + } finally { + jedis.close(); + } } } diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ResourceCleanupUtil.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ResourceCleanupUtil.java index 106891e..c45b221 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ResourceCleanupUtil.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ResourceCleanupUtil.java @@ -28,7 +28,7 @@ public class ResourceCleanupUtil { return; } - log.info("开始执行资源清理,待清理资源数: {}", pendingCleanupResources.size()); + log.info("开始执行资源清理,待清理资源数:" + pendingCleanupResources.size()); int cleanedCount = 0; Set resourcesToClean = ConcurrentHashMap.newKeySet(); @@ -40,13 +40,13 @@ public class ResourceCleanupUtil { pendingCleanupResources.remove(resourceId); cleanedCount++; - log.info("已清理资源: {}", resourceId); + log.info("已清理资源:" + resourceId); } catch (Exception e) { - log.error("清理资源时发生异常: {}, 错误: {}", resourceId, e.getMessage(), e); + log.error("清理资源时发生异常:" + resourceId + ", 错误:" + e.getMessage(), e); } } - log.info("资源清理完成,共清理: {} 个资源", cleanedCount); + log.info("资源清理完成,共清理:" + cleanedCount + " 个资源"); //执行常规清理 performRegularCleanup(); @@ -62,10 +62,10 @@ public class ResourceCleanupUtil { //输出当前系统状态 log.info("=== 系统资源状态 ==="); - log.info("{}", ThreadPoolConfig.getThreadPoolStatus()); + log.info(ThreadPoolConfig.getThreadPoolStatus()); } catch (Exception e) { - log.error("执行常规清理时发生异常: {}", e.getMessage(), e); + log.error("执行常规清理时发生异常:" + e.getMessage(), e); } } diff --git a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ThreadPoolConfig.java b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ThreadPoolConfig.java index 2511401..280b701 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ThreadPoolConfig.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/robot/zp/thread/ThreadPoolConfig.java @@ -58,14 +58,14 @@ public class ThreadPoolConfig { * 执行延迟任务,替代Thread.sleep */ public static void scheduleDelay(Runnable task, long delay, TimeUnit unit) { - log.debug("提交延迟任务: 延迟{} {}, 当前时间: {}", delay, unit, System.currentTimeMillis()); + log.debug("提交延迟任务:延迟" + delay + " " + unit + ", 当前时间:" + System.currentTimeMillis()); SCHEDULED_EXECUTOR_SERVICE.schedule(() -> { try { - log.debug("执行延迟任务开始: 当前时间: {}", System.currentTimeMillis()); + log.debug("执行延迟任务开始:当前时间:" + System.currentTimeMillis()); task.run(); - log.debug("执行延迟任务完成: 当前时间: {}", System.currentTimeMillis()); + log.debug("执行延迟任务完成:当前时间:" + System.currentTimeMillis()); } catch (Exception e) { - log.error("延迟任务执行异常: {}", e.getMessage(), e); + log.error("延迟任务执行异常:" + e.getMessage(), e); } }, delay, unit); } diff --git a/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouAIStrategy.java b/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouAIStrategy.java new file mode 100644 index 0000000..51070a8 --- /dev/null +++ b/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouAIStrategy.java @@ -0,0 +1,2384 @@ +package taurus.util; + +import com.taurus.core.util.Logger; +import robot.zp.handler.FuLuShouHandler; + +import java.util.*; + +/** + * 福禄寿 AI 决策算法 + * 参考长沙麻将的 AI 设计,独立封装福禄寿的智能决策逻辑 + */ +public class FuLuShouAIStrategy { + + private static final Logger log = Logger.getLogger(FuLuShouAIStrategy.class); + + private static final double SPECIAL_CHAR_WEIGHT = 10.0; //福、上 + private static final double SUB_SPECIAL_CHAR_WEIGHT = 5.0; //大、人、禄、寿 + private static final double PAIR_WEIGHT = 8.0; //对子 + private static final double TRIPLE_WEIGHT = 15.0; //坎(3 张) + private static final double QUAD_WEIGHT = 20.0; //招(4 张) + private static final double HALF_COMBO_WEIGHT = 3.0; //半搭子潜力 + private static final double TING_WEIGHT = 15.0; //听牌加分 + private static final double WIN_WEIGHT = 30.0; //胡牌加分 + + /** + * AI 选择最佳出牌 + * @param handler 游戏处理器(包含当前手牌等信息) + * @return 最佳要出的牌 + */ + public int selectBestDiscardCard(FuLuShouHandler handler) { + List handCards = handler.getHandCards(); + + if (handCards.isEmpty()) { + log.warn("手牌为空,无法出牌"); + return -1; + } + + //1. 检查是否已胡牌 + FuLuShouSuanFa.HuResult winResult = handler.checkWin(); + int totalXi = handler.getTotalXi(); + int completeGroups = handler.getChowGroupsComplete().size() + handler.getPongGroupsComplete().size() + handler.getGangGroupsComplete().size(); + log.info("出牌决策 - 当前手牌数:" + handCards.size() + ", 已组成句子数:" + + completeGroups + ", 总息数:" + totalXi + ", 是否胡牌:" + winResult.isWin()); + + // 【优化】胡牌判断:只要牌型符合且息数≥11 就胡牌 + if (winResult.isWin() && totalXi >= 11) { + log.info("【胡牌检测】已胡牌且息数充足(" + totalXi + "息),不需要出牌"); + return handCards.get(0); + } + + //2. 关键修复:检查是否已听牌(服务器返回的 ting_list) + List> serverTingList = handler.getTingList(); + boolean isServerTing = (serverTingList != null && !serverTingList.isEmpty()); + + //2.1 先检查能否直接胡牌(自摸) + if (!isServerTing) { + // 即使服务器没说听牌,也要检查是否已经可以胡牌 + FuLuShouSuanFa.HuResult selfWinCheck = handler.checkWin(); + if (selfWinCheck.isWin() && totalXi >= 11) { + log.info("【胡牌检测】虽未显示听牌,但实际已满足胡牌条件!息数=" + totalXi); + log.info("【胡牌策略】直接宣布胡牌(自摸)"); + return handCards.get(0); // 随便打一张触发胡牌逻辑 + } + } + + //2.2 检查是否真的听牌(AI 自主分析) + TingAnalysis aiTingAnalysis = analyzeTingAdvanced(handler); + boolean isAITing = aiTingAnalysis.isTing && !aiTingAnalysis.tingCards.isEmpty(); + + //2.3 综合判断:以 AI 分析为准,结合服务器提示 + boolean finalIsTing = isServerTing || isAITing; + + if (finalIsTing) { + log.info("【听牌确认】机器人已听牌!服务器听牌=" + isServerTing + ", AI 分析听牌=" + isAITing); + log.info("【听牌张数】AI 分析听牌数量:" + aiTingAnalysis.tingCards.size()); + + //2.4 听牌时优先检查能否胡牌 + if (canWinWithCurrentHand(handler)) { + log.info("【胡牌机会】当前可以胡牌!应该选择胡牌而不是继续出牌"); + return handCards.get(0); // 触发胡牌 + } + + //2.5 不能胡牌时,智能选择出牌(平衡安全与质量) + log.info("【听牌策略】暂时不能胡牌,选择最优出牌方案..."); + return selectOptimalDiscardForTing(handler, aiTingAnalysis); + } + + // 【新增】3. 灵活做牌策略:统筹全局,智能选择最快路径 + // 核心思路:不局限于"先凑组后凑息",而是根据牌局动态选择最优解 + int neededGroups = 5; // 目标:至少 5 组 + int remainingGroups = neededGroups - completeGroups; + int currentHandSize = handCards.size(); + + log.info("【做牌进度】已完成:" + completeGroups + "组,还需要:" + remainingGroups + + "组,当前手牌:" + currentHandSize + "张"); + + // 智能判断:根据剩余组数和手牌数,选择最佳策略 + boolean isNearTing = completeGroups >= 4; + boolean shouldAggressiveChow = remainingGroups <= 2 && currentHandSize >= 7; // 剩 2 组且手牌多,主动吃牌 + + if (isNearTing) { + log.info("【激进模式】已接近听牌,全力冲刺最后 " + (neededGroups - completeGroups) + "组!"); + } + if (shouldAggressiveChow) { + log.info("【主动吃牌】手牌充足,可以主动吃牌加速听牌!"); + } + + //3. 重要保护:不要拆已经完成的坎/招/吃 + List protectedCards = new ArrayList<>(); + + // 保护已碰的坎 + for (List group : handler.getPongGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + + // 保护已杠的招 + for (List group : handler.getGangGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + + // 保护已吃的句子 + for (List group : handler.getChowGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + + log.info("【保护列表】已完成的坎/招/吃:" + protectedCards); + + //4. 智能打牌策略(灵活统筹版):根据牌局动态调整 + // 核心原则: + // - 【动态权衡】根据剩余组数、手牌数、牌河信息,选择最快路径 + // - 【主动吃牌】接近听牌且手牌充足时,拆搭子吃牌加速 + // - 【保留对子】对子仍是碰牌基础,但不是唯一选择 + // - 【计算概率】评估哪种方式更快 (碰 vs 吃 vs 自摸) + // - 【灵活拆牌】必要时拆特殊字、拆坎子,追求最快速度 + // - 【牌河分析】观察已出牌,判断哪些牌容易来 + + int bestDiscard = -1; + + // 【新增】牌河分析:统计已出牌,判断每张牌的剩余数量 + Map remainingCards = analyzeRemainingCards(handler); + log.info("【牌河分析】已完成,共追踪 " + remainingCards.size() + "种牌的剩余数量"); + + // 4.1 首先分析每张手牌的价值(包括模拟打牌效果) + Map cardValues = new HashMap<>(); + for (int card : handCards) { + if (protectedCards.contains(card)) { + cardValues.put(card, 999.0); // 保护牌价值最高 + continue; + } + + // 【灵活统筹】根据局势动态调整牌价值 + double baseValue = evaluateCardValueFlexible(handler, card, isNearTing, shouldAggressiveChow); + double simulationBonus = simulateDiscardEffectSafe(handler, card); // 负值表示损失 + + // 【新增】牌河加成:如果这张牌的剩余数量多,价值提升 + int remaining = remainingCards.getOrDefault(card, 4); + if (remaining >= 3) { + baseValue += 2.0; // 还有很多,值得保留 + log.debug(" - [牌河] 剩余" + remaining + "张,加分 +2.0"); + } else if (remaining <= 1) { + baseValue -= 3.0; // 快没了,降低价值 + log.debug(" - [牌河] 剩余" + remaining + "张,减分 -3.0 (可能绝张)"); + } + + double value = baseValue + simulationBonus; + cardValues.put(card, value); + log.debug("【牌价值评估】card=" + card + ", 基础价值=" + String.format("%.2f", baseValue) + + ", 模拟效果=" + String.format("%.2f", simulationBonus) + ", 总价值=" + String.format("%.2f", value)); + } + + // 4.2 找出价值最低的牌打出 + double minValue = Double.MAX_VALUE; + for (int card : handCards) { + if (protectedCards.contains(card)) { + continue; + } + + double value = cardValues.get(card); + if (value < minValue) { + minValue = value; + bestDiscard = card; + } else if (value == minValue && bestDiscard != -1) { + // 价值相同时,优先打普通数字牌 + if (card >= 401 && card <= 804) { + bestDiscard = card; + } + } + } + + // 4.3 根据价值分类给出提示 + if (bestDiscard != -1) { + double value = cardValues.get(bestDiscard); + if (value < 3.0) { + log.info("【出牌策略 1】打孤立普通数字牌(价值最低):" + bestDiscard); + } else if (value < 6.0) { + log.info("【出牌策略 2】打无潜力单张:" + bestDiscard); + } else if (value < 10.0) { + log.info("【出牌策略 3】打低价值特殊字:" + bestDiscard); + } else { + log.info("【出牌策略 4】无奈拆牌:" + bestDiscard); + } + } + + // 5. 兜底策略 + if (bestDiscard == -1) { + for (int card : handCards) { + if (!protectedCards.contains(card)) { + bestDiscard = card; + log.warn("【兜底】打第一张非保护牌:" + card); + break; + } + } + } + + if (bestDiscard == -1) { + bestDiscard = handCards.get(0); // 最终兜底 + } + + log.info("【最终出牌】" + bestDiscard); + return bestDiscard; + } + + /** + * 【新增】灵活统筹模式下的牌价值评估 + * @param handler 游戏处理器 + * @param card 要评估的牌 + * @param isNearTing 是否接近听牌 + * @param shouldAggressiveChow 是否应该主动吃牌 + * @return 牌的价值分数(越高越重要) + */ + private double evaluateCardValueFlexible(FuLuShouHandler handler, int card, boolean isNearTing, boolean shouldAggressiveChow) { + double value = 0.0; + List handCards = handler.getHandCards(); + + // 1. 【基础】对子价值 - 碰牌仍是重要方式 + int count = countCards(handCards, card); + if (count >= 2) { + if (isNearTing && !shouldAggressiveChow) { + value += 25.0; // 接近听牌且不吃牌时,对子最珍贵 + log.debug(" - [听牌] 对子加分:+25.0 (count=" + count + ")"); + } else if (shouldAggressiveChow) { + value += 18.0; // 主动吃牌时,对子稍降权 + log.debug(" - [吃牌] 对子加分:+18.0 (count=" + count + ", 可拆搭子)"); + } else { + value += 15.0; // 普通对子分 + log.debug(" - [普通] 对子加分:+15.0 (count=" + count + ")"); + } + } + if (count >= 3) { + if (shouldAggressiveChow && isNearTing) { + value += 10.0; // 极端情况下可以拆坎 + log.debug(" - [激进] 坎加分:+10.0 (count=" + count + ", 可拆)"); + } else { + value += 20.0; // 坎的基础分 + log.debug(" - [保守] 坎加分:+20.0 (count=" + count + ")"); + } + } + + // 2. 【动态】特殊字价值 - 根据息数情况调整 + int currentXi = handler.getTotalXi(); + if (isSpecialChar(card)) { + if (currentXi >= 15) { + value += 4.0; // 息数充足,特殊字不重要 + log.debug(" - [息足] 特殊字加分:+4.0 (currentXi=" + currentXi + ")"); + } else if (currentXi <= 6) { + value += 12.0; // 息数不足,保留高息牌 + log.debug(" - [息缺] 特殊字加分:+12.0 (currentXi=" + currentXi + ", 需凑息)"); + } else { + value += 7.0; // 正常特殊字分 + log.debug(" - [普通] 特殊字加分:+7.0"); + } + } else if (isSubSpecialChar(card)) { + value += 3.0; // 次特殊字 + log.debug(" - 次特殊字加分:+3.0"); + } + + // 3. 【关键】句子潜力价值 - 主动吃牌时大幅提升 + if (canFormSentencePotential(handCards, card)) { + if (shouldAggressiveChow) { + value += 12.0; // 主动吃牌时,搭子很珍贵 + log.debug(" - [吃牌] 句子潜力加分:+12.0 (可主动吃)"); + } else if (isNearTing) { + value += 3.0; // 接近听牌但不吃牌时,搭子一般 + log.debug(" - [听牌] 句子潜力加分:+3.0"); + } else { + value += 6.0; // 普通句子潜力分 + log.debug(" - [普通] 句子潜力加分:+6.0"); + } + } + + // 4. 【调整】连牌潜力价值 - 多面搭子价值高 + if (hasRelatedCards(handCards, card)) { + int relatedCount = countRelatedCards(handCards, card); // 计算相关牌数量 + + // 【新增】计算搭子质量:根据剩余牌数判断 + double chowProbability = calculateChowProbability(handler, card); + + if (relatedCount >= 2 && shouldAggressiveChow) { + if (chowProbability > 0.6) { + value += 15.0; // 多面搭子 + 高概率,极有价值 + log.debug(" - [吃牌] 多面搭子加分:+15.0 (related=" + relatedCount + ", prob=" + String.format("%.2f", chowProbability) + ")"); + } else { + value += 10.0; // 多面搭子正常价值 + log.debug(" - [吃牌] 多面搭子加分:+10.0 (related=" + relatedCount + ", prob=" + String.format("%.2f", chowProbability) + ")"); + } + } else if (relatedCount >= 2) { + value += 6.0 + (chowProbability * 3.0); // 多面搭子 + 概率加成 + log.debug(" - [普通] 多面搭子加分:+" + String.format("%.1f", 6.0 + chowProbability * 3.0) + " (related=" + relatedCount + ")"); + } else if (shouldAggressiveChow) { + value += 4.0 + (chowProbability * 2.0); // 单面搭子 + 概率加成 + log.debug(" - [吃牌] 单面搭子加分:+" + String.format("%.1f", 4.0 + chowProbability * 2.0)); + } else { + value += 2.0; // 普通连牌分 + log.debug(" - [普通] 连牌潜力加分:+2.0"); + } + } + + // 5. 普通数字牌基础分 + if (card >= 401 && card <= 804) { + value += 1.0; // 基础分 + log.debug(" - 普通数字牌基础分:+1.0"); + } + + return value; + } + + /** + * 计算某张牌的相关牌数量 (用于判断搭子质量) + */ + private int countRelatedCards(List handCards, int card) { + if (card < 101 || card > 804) { + return 0; + } + + int base = (card / 100) * 100; + int num = card % 100; + int relatedCount = 0; + + // 检查相邻的数字牌 + for (int i = Math.max(1, num - 2); i <= Math.min(4, num + 2); i++) { + if (i == num) continue; + int relatedCard = base + i; + if (handCards.contains(relatedCard)) { + relatedCount++; + } + } + + return relatedCount; + } + + /** + * 【新增】计算吃牌概率:根据剩余牌数判断吃到某张牌的概率 + * @param handler 游戏处理器 + * @param card 当前手牌 + * @return 吃到相关牌的概率 (0.0-1.0) + */ + private double calculateChowProbability(FuLuShouHandler handler, int card) { + if (card < 101 || card > 804) { + return 0.0; + } + + Map remainingCards = analyzeRemainingCards(handler); + int base = (card / 100) * 100; + int num = card % 100; + + // 统计能吃到的牌的总剩余数 + int totalRemaining = 0; + int maxPossible = 0; + + // 检查所有能组成句子的相邻牌 + for (int i = Math.max(1, num - 2); i <= Math.min(4, num + 2); i++) { + if (i == num) continue; + int relatedCard = base + i; + int remaining = remainingCards.getOrDefault(relatedCard, 0); + totalRemaining += remaining; + maxPossible += 4; // 每种牌最多 4 张 + } + + // 计算概率 + if (maxPossible == 0) { + return 0.0; + } + + double probability = (double) totalRemaining / maxPossible; + log.debug(" - [概率] 牌 " + card + " 的吃牌概率:" + String.format("%.2f", probability) + + " (剩余" + totalRemaining + "/" + maxPossible + ")"); + + return probability; + } + + /** + * 评估单张手牌的价值 + * @param handler 游戏处理器 + * @param card 要评估的牌 + * @return 牌的价值分数(越高越重要) + */ + private double evaluateCardValue(FuLuShouHandler handler, int card) { + // 默认调用普通模式 + return evaluateCardValueFlexible(handler, card, false, false); + } + + /** + * 检查是否有形成句子的潜力 + */ + private boolean canFormSentencePotential(List handCards, int card) { + // 检查是否有相同牌的另外一张(形成对子后能碰) + int count = countCards(handCards, card); + if (count >= 2) { + return true; + } + + // 检查是否有能组成句子的相邻牌 + if (card >= 101 && card <= 804) { + int base = (card / 100) * 100; + int num = card % 100; + + // 检查是否有相邻的数字牌 + for (int i = Math.max(1, num - 2); i <= Math.min(4, num + 2); i++) { + if (i == num) continue; + int relatedCard = base + i; + if (handCards.contains(relatedCard)) { + return true; + } + } + } + + return false; + } + + /** + * 检查是否有相关的连牌 + */ + private boolean hasRelatedCards(List handCards, int card) { + if (card < 101 || card > 804) { + return false; + } + + int base = (card / 100) * 100; + int num = card % 100; + + // 检查同一组的牌 + for (int c : handCards) { + if (c >= base && c < base + 100 && c != card) { + return true; + } + } + + return false; + } + + /** + * 检查是否听牌 + * @param handler 游戏处理器 + * @return 听的牌列表 + */ + public List checkTing(FuLuShouHandler handler) { + List tingList = new ArrayList<>(); + List handCards = handler.getHandCards(); + + //遍历所有可能的牌(101-804) + for (int card = 101; card <= 804; card++) { + //跳过已经用完的牌 + if (isCardExhausted(handler, card)) { + continue; + } + + //假设摸到这张牌 + handCards.add(card); + + //检查是否胡牌 + if (handler.checkWin().isWin() && handler.getTotalXi() >= 11) { + tingList.add(card); + } + + //移除假设的牌 + handCards.remove(Integer.valueOf(card)); + } + + return tingList; + } + + /** + * 高级听牌分析:不仅检查是否听牌,还分析听牌质量和胡牌概率 + * @param handler 游戏处理器 + * @return 听的牌列表及其质量评分 + */ + public Map checkTingAdvanced(FuLuShouHandler handler) { + Map tingMap = new HashMap<>(); + List handCards = handler.getHandCards(); + int currentXi = handler.getTotalXi(); + + //遍历所有可能的牌(101-804) + for (int card = 101; card <= 804; card++) { + //跳过已经用完的牌 + if (isCardExhausted(handler, card)) { + continue; + } + + //假设摸到这张牌 + handCards.add(card); + + //检查是否胡牌 + FuLuShouSuanFa.HuResult result = handler.checkWin(); + if (result.isWin()) { + //计算胡牌质量 + double quality = calculateWinQuality(handler, card); + + //息数达标才加入 + if (currentXi >= 11) { + tingMap.put(card, quality); + log.debug("【高级听牌】听 " + card + ", 质量分=" + String.format("%.2f", quality)); + } + } + + //移除假设的牌 + handCards.remove(Integer.valueOf(card)); + } + + return tingMap; + } + + /** + * 计算胡牌质量 + * @param handler 游戏处理器 + * @param winCard 胡的牌 + * @return 质量分数(越高越好) + */ + private double calculateWinQuality(FuLuShouHandler handler, int winCard) { + double quality = 100.0; // 基础分 + int totalXi = handler.getTotalXi(); + + // 1. 息数奖励:超过 11 息越多,质量越高 + if (totalXi > 11) { + quality += (totalXi - 11) * 5.0; + } + + // 2. 特殊字奖励:胡特殊字质量更高 + if (isSpecialChar(winCard)) { + quality += 50.0; + } else if (isSubSpecialChar(winCard)) { + quality += 20.0; + } + + // 3. 自摸奖励:假设能自摸 + quality += 30.0; + + return quality; + } + + /** + * 【新增】牌河分析:统计已出牌,计算每张牌的剩余数量 + * @param handler 游戏处理器 + * @return 每种牌的剩余数量映射 (key=牌,value=剩余张数) + */ + private Map analyzeRemainingCards(FuLuShouHandler handler) { + Map remainingMap = new HashMap<>(); + + // 初始化所有牌为 4 张 + for (int card = 101; card <= 804; card++) { + remainingMap.put(card, 4); + } + + // 减去手牌中的牌 + for (int c : handler.getHandCards()) { + remainingMap.put(c, remainingMap.get(c) - 1); + } + + // 减去已吃碰杠的牌 + for (List group : handler.getChowGroupsComplete()) { + for (int c : group) { + remainingMap.put(c, Math.max(0, remainingMap.get(c) - 1)); + } + } + for (List group : handler.getPongGroupsComplete()) { + for (int c : group) { + remainingMap.put(c, Math.max(0, remainingMap.get(c) - 1)); + } + } + for (List group : handler.getGangGroupsComplete()) { + for (int c : group) { + remainingMap.put(c, Math.max(0, remainingMap.get(c) - 1)); + } + } + + // TODO: 如果能看到其他玩家的手牌或弃牌堆,也应该减去 + // 目前只能看到自己可见的牌 + + return remainingMap; + } + + /** + * 判断某张牌是否已经用完 + */ + private boolean isCardExhausted(FuLuShouHandler handler, int card) { + int count = 0; + + //统计手牌中的数量 + for (int c : handler.getHandCards()) { + if (c == card) count++; + } + + //统计已使用的牌(吃碰杠) + for (List group : handler.getChowGroupsComplete()) { + for (int c : group) { + if (c == card) count++; + } + } + for (List group : handler.getPongGroupsComplete()) { + for (int c : group) { + if (c == card) count++; + } + } + for (List group : handler.getGangGroupsComplete()) { + for (int c : group) { + if (c == card) count++; + } + } + + //福禄寿每种牌最多 4 张 + return count >= 4; + } + + /** + * 评估所有牌的价值 + * @param handler 游戏处理器 + * @return 每张牌的价值映射 + */ + public Map evaluateAllCards(FuLuShouHandler handler) { + Map values = new HashMap<>(); + List handCards = handler.getHandCards(); + + //统计每种牌的数量 + Map cardCount = new HashMap<>(); + for (int card : handCards) { + cardCount.put(card, cardCount.getOrDefault(card, 0) + 1); + } + + //评估每张牌 + for (int card : handCards) { + double value = evaluateSingleCard(handler, card, cardCount.get(card)); + values.put(card, value); + } + + return values; + } + + /** + * 评估单张牌的价值 - 修复版:综合考虑手牌 + 已碰杠的牌 + * @param handler 游戏处理器 + * @param card 牌的 ID + * @param handCount 该牌在手牌中的数量 + * @return 牌的价值分数 + */ + public double evaluateSingleCard(FuLuShouHandler handler, int card, int handCount) { + double value = 0.0; + + // 统计这张牌在所有地方的总数量(手牌 + 吃碰杠) + int totalCount = handCount; + + // 统计已使用的牌(吃碰杠) + for (List group : handler.getChowGroupsComplete()) { + for (int c : group) { + if (c == card) totalCount++; + } + } + for (List group : handler.getPongGroupsComplete()) { + for (int c : group) { + if (c == card) totalCount++; + } + } + for (List group : handler.getGangGroupsComplete()) { + for (int c : group) { + if (c == card) totalCount++; + } + } + + //========== 1. 特殊字价值(最重要) ========== + if (isSpecialChar(card)) { + value += SPECIAL_CHAR_WEIGHT * 2.0; // 福、上:双倍权重 + log.debug("【特殊字评估】" + card + " 是福/上,基础分 +" + (SPECIAL_CHAR_WEIGHT * 2.0)); + } else if (isSubSpecialChar(card)) { + value += SUB_SPECIAL_CHAR_WEIGHT * 1.5; // 大、人、禄、寿:1.5 倍权重 + log.debug("【特殊字评估】" + card + " 是大/人/禄/寿,基础分 +" + (SUB_SPECIAL_CHAR_WEIGHT * 1.5)); + } + + //========== 2. 对子、坎、招价值(基于总数) ========== + // 关键修复:使用 totalCount 而不是 handCount + if (totalCount == 2) { + value += PAIR_WEIGHT; + log.debug("【对子评估】" + card + " 有" + totalCount + "张,形成对子,+" + PAIR_WEIGHT); + } else if (totalCount == 3) { + value += TRIPLE_WEIGHT; + log.debug("【坎评估】" + card + " 有" + totalCount + "张,形成坎,+" + TRIPLE_WEIGHT); + } else if (totalCount == 4) { + value += QUAD_WEIGHT; + log.debug("【招评估】" + card + " 有" + totalCount + "张,形成招,+" + QUAD_WEIGHT); + } + + //========== 3. 连搭潜力 ========== + if (handCount > 0) { // 只有手中还有这张牌才计算连搭 + value += evaluateCardPotential(handler, card); + } + + //========== 4. 安全性 ========== + value -= getCardSafety(handler, card); + + //========== 5. 息数价值(核心修复) ========== + // 如果这张牌能带来息数,大幅加分 + int xiValue = calculateXiValue(card, totalCount); + if (xiValue > 0) { + value += xiValue * 5.0; // 每 1 息价值 5 分 + log.debug("【息数评估】" + card + " 能提供" + xiValue + "息,+" + (xiValue * 5.0)); + } + + //========== 6. 重要修改:给所有普通牌一个基础价值,防止按顺序打牌 ========== + // 关键修复:让 AI 根据实际牌型选择打哪张,而不是按顺序 + if (!isSpecialChar(card) && !isSubSpecialChar(card)) { + // 普通牌的基础价值:根据牌的序号差异化(避免固定顺序) + value += (card % 100) * 0.01; // 很小的权重,用于打破平局 + log.debug("【普通牌基础值】" + card + " 基础价值 +" + (card % 100) * 0.01); + } + + return value; + } + + /** + * 计算某张牌的息数价值 + */ + private int calculateXiValue(int card, int totalCount) { + // 福禄寿麻将规则:不同牌有不同的息数 + if (card == 101 || card == 201) { // 福、上 + return totalCount * 3; // 每张 3 息 + } else if (card >= 301 && card <= 704) { // 大、人、禄、寿等 + return totalCount * 1; // 每张 1 息 + } + return 0; // 普通数字牌无息 + } + + /** + * 评估牌的连搭潜力 + */ + private double evaluateCardPotential(FuLuShouHandler handler, int card) { + double potential = 0.0; + List handCards = handler.getHandCards(); + + //检查是否能与其他牌形成半搭子 + for (int otherCard : handCards) { + if (otherCard == card) continue; + + List combo = Arrays.asList(card, otherCard); + if (isValidHalfCombo(combo)) { + potential += HALF_COMBO_WEIGHT;//可以形成半搭子 + } + } + + return potential; + } + + /** + * 简化的半搭子验证 + */ + private boolean isValidHalfCombo(List combo) { + if (combo.size() != 2) return false; + int card1 = combo.get(0); + int card2 = combo.get(1); + + //对子 + if (card1 == card2) return true; + + //特殊组合 + if ((card1 == 201 && (card2 == 301 || card2 == 501)) || + (card2 == 201 && (card1 == 301 || card1 == 501))) { + return true; + } + if ((card1 == 101 && (card2 == 701 || card2 == 1013)) || + (card2 == 101 && (card1 == 701 || card1 == 1013))) { + return true; + } + if ((card1 == 301 && card2 == 501) || (card2 == 301 && card1 == 501)) { + return true; + } + + return false; + } + + /** + * 获取牌的安全性(基于已出现的牌) + * @return 安全性分数(越高越安全) + */ + private double getCardSafety(FuLuShouHandler handler, int card) { + int visibleCount = 0; + + //统计自己打出的牌 + for (int c : handler.getOutCards()) { + if (c == card) visibleCount++; + } + + //其他玩家吃碰杠的牌 + for (List group : handler.getChowGroupsComplete()) { + for (int c : group) { + if (c == card) visibleCount++; + } + } + for (List group : handler.getPongGroupsComplete()) { + for (int c : group) { + if (c == card) visibleCount++; + } + } + for (List group : handler.getGangGroupsComplete()) { + for (int c : group) { + if (c == card) visibleCount++; + } + } + + //出现的越多越安全 + return visibleCount * 2.0; + } + + /** + * 选择最安全的牌 + */ + public int selectSafeDiscard(FuLuShouHandler handler) { + List handCards = handler.getHandCards(); + int safestCard = handCards.get(0); + double maxSafety = -1; + + for (int card : handCards) { + double safety = getCardSafety(handler, card); + if (safety > maxSafety) { + maxSafety = safety; + safestCard = card; + } + } + + return safestCard; + } + + /** + * 听牌时选择最安全的牌 - 优化版 + * 核心原则: + * 1. 不打听牌中的牌 + * 2. 优先打已出现的牌(安全牌) + * 3. 优先打普通数字牌(非特殊字) + * 4. 避免拆对子、搭子 + * 5. 在保证听牌的前提下,选择质量最高的打法 + */ + public int selectSafeDiscardForTing(FuLuShouHandler handler) { + List handCards = handler.getHandCards(); + List> tingList = handler.getTingList(); + + if (handCards.isEmpty()) { + log.warn("【听牌出牌】手牌为空!"); + return -1; + } + + //1. 提取所有听的牌 + List tingCards = new ArrayList<>(); + if (tingList != null) { + for (Map tingInfo : tingList) { + Integer tingCard = (Integer) tingInfo.get("card"); + if (tingCard != null && !tingCards.contains(tingCard)) { + tingCards.add(tingCard); + } + } + } + log.info("【听牌保护】听的牌:" + tingCards); + + //2. 找出绝对不能打的牌(听的牌) + List protectedCards = new ArrayList<>(tingCards); + + //3. 也不要拆已经完成的坎/招/吃 + for (List group : handler.getPongGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + for (List group : handler.getGangGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + for (List group : handler.getChowGroupsComplete()) { + if (!group.isEmpty()) { + protectedCards.add(group.get(0)); + } + } + + log.info("【听牌保护】保护列表(听的牌 + 已完成组合):" + protectedCards); + + //4. 从手牌中排除保护牌,找到可以打的牌 + int bestDiscard = -1; + double minScore = Double.MAX_VALUE; + + for (int card : handCards) { + if (protectedCards.contains(card)) { + log.debug("【听牌出牌】保护牌,不能打:" + card); + continue; + } + + //计算这张牌的"危险分数"(越低越安全) + double score = calculateTingDiscardScore(handler, card); + log.debug("【听牌出牌】card=" + card + ", 危险分数=" + String.format("%.2f", score)); + + if (score < minScore) { + minScore = score; + bestDiscard = card; + } + } + + //5. 如果所有牌都是保护牌(极端情况),随便打一张 + if (bestDiscard == -1) { + bestDiscard = handCards.get(0); + log.warn("【听牌出牌】所有牌都是保护牌,强制打第一张:" + bestDiscard); + } + + log.info("【听牌出牌】最终选择:" + bestDiscard); + return bestDiscard; + } + + /** + * 计算听牌时打某张牌的危险分数 + * @param handler 游戏处理器 + * @param card 要评估的牌 + * @return 危险分数(越低越安全) + */ + private double calculateTingDiscardScore(FuLuShouHandler handler, int card) { + double score = 0.0; + + //1. 已出现的牌越多,越安全(分数越低) + double safety = getCardSafety(handler, card); + score -= safety; // 安全分越高,总分越低 + + //2. 特殊字更危险(因为可能别人需要) + if (isSpecialChar(card)) { + score += 20.0; // 福、上:高危险 + } else if (isSubSpecialChar(card)) { + score += 10.0; // 大、人、禄、寿:中等危险 + } + + //3. 检查是否是手牌中的对子(拆对子很危险) + int count = countCards(handler.getHandCards(), card); + if (count >= 2) { + score += 15.0; // 对子:高危险 + } + + //4. 检查是否能与手牌中其他牌形成搭子 + if (hasRelatedCards(handler.getHandCards(), card)) { + score += 8.0; // 有连搭潜力:中高危险 + } + + return score; + } + + /** + * AI 选择最佳动作 (吃碰杠胡) + * @param handler 游戏处理器 + * @param tips 可执行的动作 ID 列表 + * @param tipList 完整的提示列表 + * @return 最佳动作 ID(0 表示放弃) + */ + public int selectBestAction(FuLuShouHandler handler, List tips, Object tipList) { + //优先级:胡牌 > 杠 > 碰 > 吃 + //基于胡牌规则的智能决策:分析还需要多少组才能胡牌 + + log.info("========== 开始 AI 决策 =========="); + log.info("可选动作 ID 列表:" + tips); + + // ========== 1. 优先检查胡牌 - 这是最高优先级 ========== + for (int id : tips) { + Object tip = findTipById(tipList, id); + if (tip == null) continue; + + int type = getTipType(tip); + int card = getTipCard(tip); + + // 胡牌且息数足够,直接返回 + if (type == 6 || type == 7) { // type=6: 点炮胡,type=7: 自摸胡 + int totalXi = handler.getTotalXi(); + if (totalXi >= 11) { + log.info("【胡牌】检测通过,总息数:" + totalXi + ",直接选择胡牌,id=" + id + ", type=" + type); + return id; + } else { + log.info("【胡牌】但息数不足:" + totalXi + ",不能胡牌,需要继续做牌"); + // 关键修复:息数不足时,胡牌动作的评分应该为 0,让 AI 选择放弃 + continue; // 跳过这个胡牌动作,不参与后续评分 + } + } + } + + // ========== 1.5 听牌时的特殊处理 ========== + // 如果已经听牌,但有多个胡牌选项(如多面听),选择最优的胡牌方式 + List tingCards = checkTing(handler); + boolean isTing = !tingCards.isEmpty(); + + if (isTing) { + log.info("【听牌状态】当前已听牌,听的牌:" + tingCards); + + // 检查是否有听更多的可能 + TingAnalysis currentTingAnalysis = analyzeTingAdvanced(handler); + if (currentTingAnalysis.isTing && currentTingAnalysis.tingCards.size() > tingCards.size()) { + log.info("【听牌优化】AI 分析可以听更多张(" + currentTingAnalysis.tingCards.size() + " > " + tingCards.size() + ")"); + // 如果有更好的听牌方式,可以考虑调整手牌(这里暂时简化处理) + } + } + + // ========== 综合智能分析 ========== + List handCards = handler.getHandCards(); + int completeGroups = handler.getChowGroupsComplete().size() + + handler.getPongGroupsComplete().size() + + handler.getGangGroupsComplete().size(); + int currentXi = handler.getTotalXi(); + // tingCards 已经在前面定义过了 + + log.info("【手牌分析】当前手牌数:" + handCards.size() + ", 已组成句子:" + completeGroups); + log.info("【息数分析】当前息数:" + currentXi + "/11 息"); + log.info("【听牌分析】是否听牌:" + (!isTing ? "否" : "是,听:" + tingCards)); + log.info("【胡牌规则】目标:5 组 +2 半搭 (19 张) 或 6 组 +1 字 (19 张),需 11 息"); + + // 计算还需要多少组才能胡牌 + int neededFor5Group = 5 - completeGroups; + int neededFor6Group = 6 - completeGroups; + + log.info("【需求分析】达到 5 组还需:" + neededFor5Group + "组,达到 6 组还需:" + neededFor6Group + "组"); + + // 根据息数和听牌状态,制定当前策略 + String strategy = determineStrategy(currentXi, !tingCards.isEmpty(), neededFor5Group); + log.info("【当前策略】" + strategy); + + // 分析每个动作的价值 + int bestId = 0; + double maxScore = 0; + + for (int id : tips) { + Object tip = findTipById(tipList, id); + if (tip == null) continue; + + int type = getTipType(tip); + int card = getTipCard(tip); + + // 智能评估:结合策略、句子、息数、听牌 + double score = evaluateActionWithStrategy(handler, type, card, neededFor5Group, neededFor6Group, strategy); + + log.info("【评估】动作:type=" + type + " (1=吃,2=碰,3/4/5=杠), card=" + card + + ", 评分=" + String.format("%.2f", score)); + + if (score > maxScore) { + maxScore = score; + bestId = id; + } + } + + log.info("最佳动作评分:maxScore=" + String.format("%.2f", maxScore) + ", bestId=" + bestId); + + // 核心策略修改:强制积累息数 + // 关键逻辑:即使评分低,也要选择有息数的动作 + log.info("【进入强制检查】maxScore=" + maxScore + ", bestId=" + bestId + ", tips=" + tips); + if (maxScore < 1.0 && bestId == 0) { + log.info("【开始检查息数动作】tips.size()=" + tips.size()); + // 检查是否有任何能获得息数的动作 + int xiActionId = findXiGainAction(handler, tips, tipList); + log.info("【息数动作检查结果】xiActionId=" + xiActionId); + if (xiActionId > 0) { + log.info("【强制攒息】虽然没有完美动作,但有能获得息数的动作,强制执行 id=" + xiActionId); + return xiActionId; + } else { + log.info("【没有找到息数动作,开始检查句子动作】"); + // 如果没有任何动作能获得息数,选择一个能组成句子的动作(即使只有 0 息) + for (int id : tips) { + Object tip = findTipById(tipList, id); + if (tip == null) { + log.info("【跳过】找不到 tip, id=" + id); + continue; + } + + int type = getTipType(tip); + int card = getTipCard(tip); + + log.info("【检查句子】id=" + id + ", type=" + type + ", card=" + card); + // 检查这个动作能否组成完整句子 + boolean canFormSentence = false; + if (type == 1) { // 吃 + canFormSentence = canEatToFormSentence(handler.getHandCards(), card); + } else if (type == 2) { // 碰 + int count = countCards(handler.getHandCards(), card); + canFormSentence = (count >= 2); + log.info("【碰牌检查】card=" + card + ", count=" + count + ", canFormSentence=" + canFormSentence); + } else if (type == 3 || type == 4 || type == 5) { // 杠 + int count = countCards(handler.getHandCards(), card); + canFormSentence = (count >= 3); + log.info("【杠牌检查】card=" + card + ", count=" + count + ", canFormSentence=" + canFormSentence); + } + + if (canFormSentence) { + log.info("【强制整牌】虽无息数但能组成句子,执行 id=" + id); + return id; + } + } + log.info("【句子动作也没有找到】"); + } + } else { + log.info("【跳过强制检查】因为 maxScore>=1.0 或 bestId!=0"); + } + + // 只有当所有动作都无价值时才放弃 + if (maxScore < 1.0) { + log.info("【放弃】所有动作都无法帮助组成句子或获得息数,选择放弃"); + return 0; + } + + log.info("【选择】AI 决策结果:选择动作 id=" + bestId + ", 分数=" + String.format("%.2f", maxScore)); + log.info("========== AI 决策完成 =========="); + return bestId; + } + + /** + * 智能评估动作价值 - 结合策略、句子、息数、听牌的综合判断 + * @param handler 游戏处理器 + * @param type 动作类型(1=吃,2=碰,3/4/5=杠,6=胡) + * @param card 涉及的牌 + * @param neededFor5Group 达到 5 组还需要的组数 + * @param neededFor6Group 达到 6 组还需要的组数 + * @param strategy 当前策略 + * @return 动作评分 + */ + private double evaluateActionWithStrategy(FuLuShouHandler handler, int type, int card, + int neededFor5Group, int neededFor6Group, String strategy) { + double score = 0.0; + int currentXi = handler.getTotalXi(); + List handCards = handler.getHandCards(); + List tingCards = checkTing(handler); + boolean isTing = !tingCards.isEmpty(); + + //========== 1. 基础分 ========== + switch (type) { + case 1://吃 + score = 8.0; + break; + case 2://碰 + score = 9.0; + break; + case 3: case 4: case 5://杠 + score = 12.0; + break; + case 6://胡(点炮) + // 关键修复:息数不足时,胡牌评分为 0 + if (currentXi >= 11) { + score = 50.0; + } else { + score = 0.0; // 息数不够,不能胡 + log.info("【胡牌评分】息数不足 (" + currentXi + "<11), 评分强制为 0"); + } + break; + case 7://胡(自摸) + // 关键修复:息数不足时,自摸评分也为 0 + if (currentXi >= 11) { + score = 100.0; // 自摸胡牌优先级最高 + } else { + score = 0.0; // 息数不够,不能胡 + log.info("【自摸评分】息数不足 (" + currentXi + "<11), 评分强制为 0"); + } + break; + } + + //========== 2. 核心逻辑:这个动作能否组成完整的句子? ========== + boolean canFormCompleteSentence = false; + + if (type == 1) { // 吃牌:检查是否能形成完整句子 + canFormCompleteSentence = canEatToFormSentence(handCards, card); + } else if (type == 2) { // 碰牌:手中有对子就能形成坎 + int count = countCards(handCards, card); + canFormCompleteSentence = (count >= 2); + } else if (type == 3 || type == 4 || type == 5) { // 杠牌 + int count = countCards(handCards, card); + canFormCompleteSentence = (count >= 3); + } + + // 先计算息数(用于后续综合评估) + int xiGain = 0; + switch (type) { + case 1: + xiGain = calculateChowXi(handler, card); + break; + case 2: + xiGain = getPongXi(card); + break; + case 3: case 4: case 5: + //关键修复:杠牌前必须检查手牌中是否有足够的牌 + if (!canPerformGang(handler, card, type)) { + log.warn("【杠牌校验】手牌中没有 4 张" + card + ", 无法杠牌,强制降低评分为 0"); + return 0.0; // 直接返回 0 分,不让 AI 选择这个动作 + } + xiGain = getGangXi(card); + break; + } + + // 关键提示:判断是否是高息牌 + boolean isHighXiCard = (xiGain >= 6); + if (isHighXiCard) { + log.info("【高息牌】此牌有" + xiGain + "息,是高价值目标!"); + } + + //========== 3. 核心策略:优先攒息数(最重要修改) ========== + // 关键逻辑:息数是胡牌的必要条件,必须优先保证 + int newXi = currentXi + xiGain; + + // 3.1 息数价值评估(最高优先级) + if (xiGain > 0) { + // 基础息数分:每 1 息值 10 分(大幅提高权重) + score += xiGain * 10.0; + log.info("【息数基础分】获得" + xiGain + "息,+" + (xiGain * 10.0) + "分"); + + // 高息牌额外奖励(≥6 息) + if (xiGain >= 6) { + score += 50.0; + log.info("【高息奖励】获得高息" + xiGain + ",额外 +50 分"); + } + + // 达到胡牌线额外奖励 + if (newXi >= 11) { + score += 100.0; + log.info("【胡牌达标】总息数达到" + newXi + "(≥11),+100 分"); + } else if (newXi >= 8) { + score += 30.0; + log.info("【接近胡牌】总息数达到" + newXi + ",+30 分"); + } + } + + // 3.2 整牌价值(次要优先级) + if (canFormCompleteSentence) { + score += 20.0; + log.info("【句子价值】此动作能组成完整句子,+20 分"); + + // 如果正好是需要的组数,额外加分 + if (neededFor5Group > 0 || neededFor6Group > 0) { + score += 15.0; + log.info("【需求匹配】当前需要句子,此动作正好满足,+15 分"); + } + } + + //========== 4. 特殊字奖励 ========== + if (isSpecialChar(card)) { + score += 5.0; + } else if (isSubSpecialChar(card)) { + score += 2.0; + } + + log.debug("【动作评分详情】type=" + type + ", card=" + card + ", 当前息数=" + currentXi + + ", 获得息数=" + xiGain + ", 策略=" + strategy + ", 最终评分=" + String.format("%.2f", score)); + return score; + } + + /** + * 判断执行动作后是否还能保持听牌状态 + */ + private boolean willKeepTingAfterAction(FuLuShouHandler handler, int type, int card, List currentTingCards) { + // 简化判断:如果是吃碰杠,且不改变手牌结构太多,通常能保持听牌 + if (type == 1 || type == 2 || type == 3 || type == 4 || type == 5) { + // 假设执行这个动作 + List tempHand = new ArrayList<>(handler.getHandCards()); + + if (type == 1) { + // 吃牌会移除 2 张手牌 + // TODO: 这里可以详细模拟,暂时简化处理 + return true; // 假设能保持 + } else if (type == 2) { + // 碰牌会移除 2 张手牌 + return true; + } else if (type == 3 || type == 4 || type == 5) { + // 杠牌会移除 3 张手牌 + return true; + } + } + return false; + } + + /** + * 【新增】检查是否可以执行杠牌操作 + * @param handler 游戏处理器 + * @param card 要杠的牌 + * @param gangType 杠牌类型 (3=明杠,4=暗杠,5=昭杠) + * @return 是否可以杠牌 + */ + private boolean canPerformGang(FuLuShouHandler handler, int card, int gangType) { + List handCards = handler.getHandCards(); + + // 统计手牌中这张牌的数量 + int count = 0; + for (int c : handCards) { + if (c == card) { + count++; + } + } + + // 根据杠牌类型判断需要的牌数 + switch (gangType) { + case 3: // 明杠:手牌有 3 张 + 别人打出的 1 张 + return count >= 3; + case 4: // 暗杠:手牌有 4 张 + return count >= 4; + case 5: // 昭杠:手牌有 3 张 + 已经碰的 1 张 + // 需要检查是否有碰过这张牌 + for (List pongGroup : handler.getPongGroupsComplete()) { + if (!pongGroup.isEmpty() && pongGroup.get(0) == card) { + return count >= 3; // 有碰过,手牌再有 3 张就可以 + } + } + return count >= 4; // 没有碰过,需要手牌有 4 张 + default: + return false; + } + } + + /** + * 查找能获得息数的动作 - 强制攒息策略 + * @param handler 游戏处理器 + * @param tips 可选动作 ID 列表 + * @param tipList 动作详情 + * @return 能获得息数的动作 ID,如果没有则返回 0 + */ + private int findXiGainAction(FuLuShouHandler handler, List tips, Object tipList) { + int bestXiId = 0; + int maxXi = 0; + + for (int id : tips) { + Object tip = findTipById(tipList, id); + if (tip == null) continue; + + int type = getTipType(tip); + int card = getTipCard(tip); + + // 计算这个动作能获得多少息数 + int xiGain = 0; + switch (type) { + case 1: // 吃 + xiGain = calculateChowXi(handler, card); + break; + case 2: // 碰 + xiGain = getPongXi(card); + break; + case 3: case 4: case 5: // 杠 + xiGain = getGangXi(card); + break; + } + + log.info("【检查息数】动作 id=" + id + ", type=" + type + ", card=" + card + ", 可获得息数=" + xiGain); + + // 选择息数最多的动作 + if (xiGain > maxXi) { + maxXi = xiGain; + bestXiId = id; + } + } + + if (maxXi > 0) { + log.info("【找到高息动作】最佳选择:id=" + bestXiId + ", 可获得" + maxXi + "息"); + return bestXiId; + } else { + log.info("【无高息动作】所有动作都无法获得息数(都是普通数字牌)"); + return 0; + } + } + + /** + * 判断吃牌后是否能形成完整的句子 + * @param handCards 当前手牌 + * @param chowCard 要吃的那张牌 + * @return 是否能形成完整句子 + */ + private boolean canEatToFormSentence(List handCards, int chowCard) { + // 从 8 个固定句子组合中找到包含这张牌的组合 + List> compoundGroups = getCompoundGroups(); + + for (List group : compoundGroups) { + if (group.contains(chowCard)) { + // 检查手牌中是否有这个组合中的另外两张牌 + List neededCards = new ArrayList<>(group); + neededCards.remove(Integer.valueOf(chowCard)); + + boolean hasAllNeeded = true; + for (Integer needed : neededCards) { + boolean found = false; + for (Integer handCard : handCards) { + if (handCard.equals(needed)) { + found = true; + break; + } + } + if (!found) { + hasAllNeeded = false; + break; + } + } + + if (hasAllNeeded) { + log.info("【吃牌判断】吃" + chowCard + "可以形成完整句子:" + group); + return true; + } + } + } + + return false; + } + + /** + * 获取所有复字牌组合(与 FuLuShouHandler 保持一致) + * 福禄寿麻将规则:每个数字的 1-4 可以组成句子 + */ + private List> getCompoundGroups() { + List> groups = new ArrayList<>(); + + //100 系列:101-104 + groups.add(Arrays.asList(101, 102, 103)); + groups.add(Arrays.asList(101, 102, 104)); + groups.add(Arrays.asList(101, 103, 104)); + groups.add(Arrays.asList(102, 103, 104)); + + //200 系列:201-204 + groups.add(Arrays.asList(201, 202, 203)); + groups.add(Arrays.asList(201, 202, 204)); + groups.add(Arrays.asList(201, 203, 204)); + groups.add(Arrays.asList(202, 203, 204)); + + //300 系列:301-304 + groups.add(Arrays.asList(301, 302, 303)); + groups.add(Arrays.asList(301, 302, 304)); + groups.add(Arrays.asList(301, 303, 304)); + groups.add(Arrays.asList(302, 303, 304)); + + //400 系列:401-404 + groups.add(Arrays.asList(401, 402, 403)); + groups.add(Arrays.asList(401, 402, 404)); + groups.add(Arrays.asList(401, 403, 404)); + groups.add(Arrays.asList(402, 403, 404)); + + //500 系列:501-504 + groups.add(Arrays.asList(501, 502, 503)); + groups.add(Arrays.asList(501, 502, 504)); + groups.add(Arrays.asList(501, 503, 504)); + groups.add(Arrays.asList(502, 503, 504)); + + //600 系列:601-604 + groups.add(Arrays.asList(601, 602, 603)); + groups.add(Arrays.asList(601, 602, 604)); + groups.add(Arrays.asList(601, 603, 604)); + groups.add(Arrays.asList(602, 603, 604)); + + //700 系列:701-704 + groups.add(Arrays.asList(701, 702, 703)); + groups.add(Arrays.asList(701, 702, 704)); + groups.add(Arrays.asList(701, 703, 704)); + groups.add(Arrays.asList(702, 703, 704)); + + //800 系列:801-804 + groups.add(Arrays.asList(801, 802, 803)); + groups.add(Arrays.asList(801, 802, 804)); + groups.add(Arrays.asList(801, 803, 804)); + groups.add(Arrays.asList(802, 803, 804)); + + return groups; + } + + /** + * 评估牌型改进(智能做牌核心) + * 分析执行动作后对手牌的改善程度 + */ + private double evaluateHandImprovement(FuLuShouHandler handler, int type, int card) { + double improvementScore = 0.0; + List handCards = handler.getHandCards(); + + // 1. 检查是否能形成完整的句子(吃牌时) + if (type == 1) { + // 检查这个吃牌是否能形成高价值的句子组合 + if (canFormHighValueCombo(handCards, card)) { + improvementScore += 8.0; + log.debug("【牌型优化】吃牌能形成高价值组合,card=" + card); + } + } + + // 2. 检查碰牌后是否形成坎/招 + if (type == 2) { + int count = countCards(handCards, card); + if (count == 2) { + // 手中有 2 张,碰后成坎 + improvementScore += 6.0; + log.debug("【牌型优化】碰牌形成坎,card=" + card); + } else if (count == 3) { + // 手中有 3 张,碰后成招(实际应该是杠) + improvementScore += 4.0; + } + } + + // 3. 检查是否保留了好牌(特殊字、易成型牌) + if (isSpecialChar(card) || isSubSpecialChar(card)) { + // 特殊字本身价值高,保留它们 + improvementScore += 3.0; + } + + // 4. 检查手牌中对子数量(对子多更容易胡牌) + if (type == 2) { // 碰牌会增加明刻,减少手牌对子 + int pairCount = countPairs(handCards); + if (pairCount >= 3) { + improvementScore += 5.0; // 对子多,可以适度碰牌 + log.debug("【牌型优化】手牌对子数量充足:" + pairCount); + } + } + + // 5. 检查是否有利于听牌(简化版) + if (willImproveWaitingShape(handler, type, card)) { + improvementScore += 7.0; + log.debug("【牌型优化】有利于改善听牌形状"); + } + + return improvementScore; + } + + /** + * 判断是否能形成高价值的句子组合 + */ + private boolean canFormHighValueCombo(List handCards, int card) { + // 检查是否有与之配对的牌 + for (int other : handCards) { + if (canFormSentence(card, other)) { + // 如果能形成特殊组合(福禄寿、上大人等),价值更高 + if (isSpecialCombination(card, other)) { + return true; + } + return true; + } + } + return false; + } + + /** + * 判断是否是特殊组合(高价值) + */ + private boolean isSpecialCombination(int card1, int card2) { + // 福禄寿组合 + if ((card1 == 101 && card2 == 701) || (card1 == 701 && card2 == 101)) { + return true; + } + // 上大人组合 + if ((card1 == 201 && card2 == 301) || (card1 == 301 && card2 == 201)) { + return true; + } + return false; + } + + /** + * 统计某张牌的数量 + */ + private int countCards(List handCards, int card) { + int count = 0; + for (int c : handCards) { + if (c == card) count++; + } + return count; + } + + /** + * 统计手牌中对子数量 + */ + private int countPairs(List handCards) { + Map countMap = new HashMap<>(); + for (int card : handCards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + int pairs = 0; + for (int count : countMap.values()) { + if (count >= 2) pairs++; + } + return pairs; + } + + /** + * 判断是否能改善听牌形状 + */ + private boolean willImproveWaitingShape(FuLuShouHandler handler, int type, int card) { + // 简化实现:如果执行动作后手牌更规整,则认为有利 + List handCards = handler.getHandCards(); + + // 统计搭子数量 + int existingCombos = countExistingCombos(handCards); + + // 如果这个动作能增加搭子,则认为有利 + if (type == 1 || type == 2) { + return true; // 吃碰都会增加完成的组合 + } + + return existingCombos < 4; // 搭子少,需要积极做牌 + } + + /** + * 统计手牌中已有的搭子数量 + */ + private int countExistingCombos(List handCards) { + int combos = 0; + Map countMap = new HashMap<>(); + + for (int card : handCards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + // 统计对子、坎、招 + for (int count : countMap.values()) { + if (count >= 2) combos++; + } + + return combos; + } + + /** + * 精确计算吃牌的息数 + * @param handler 游戏处理器 + * @param card 要吃的那张牌 + * @return 这个吃牌动作能获得的息数 + */ + /** + * 计算吃牌的息数 - 根据规则修复 + * 规则核心:吃任何牌都有息数,不管是否组成固定句子 + * - 吃包含"上"或"福"的句子:12 息 + * - 吃其他任何句子:3 息 + */ + private int calculateChowXi(FuLuShouHandler handler, int card) { + List handCards = handler.getHandCards(); + + // 尝试找到能组成的句子 + List> compoundGroups = getCompoundGroups(); + + for (List group : compoundGroups) { + if (group.contains(card)) { + // 检查手牌中是否有这个组合中的另外两张牌 + List neededCards = new ArrayList<>(group); + neededCards.remove(Integer.valueOf(card)); + + boolean hasAllNeeded = true; + for (Integer needed : neededCards) { + boolean found = false; + for (Integer handCard : handCards) { + if (handCard.equals(needed)) { + found = true; + break; + } + } + if (!found) { + hasAllNeeded = false; + break; + } + } + + // 如果手牌中有这两张,说明可以吃这个组合 + if (hasAllNeeded) { + // 根据组合中的牌计算息数 + return calculateXiForCompoundGroup(group); + } + } + } + + // 关键修复:即使不能组成固定句子,吃普通数字牌也有 3 息! + // 规则:吃任何普通数字牌的句子 = 3 息 + log.info("【吃牌息数】虽不能组成固定句子,但吃普通牌也有 3 息"); + return 3; + } + + /** + * 计算某个复字牌组合的息数 + * @param group 三字组合 + * @return 该组合的息数 + */ + /** + * 计算吃牌组合的息数 - 根据规则修复 + * 规则第 52-55 行: + * - 吃包含"上"或"福"的句子(上大人、福禄寿):12 息 + * - 吃其他句子:3 息 + */ + private int calculateXiForCompoundGroup(List group) { + // 检查是否包含福 (101) 或上 (201) + boolean hasFuOrShang = group.contains(101) || group.contains(201); + + if (hasFuOrShang) { + return 12; // 高息:福禄寿、上大人 + } else { + // 吃其他任何句子:3 息(化三千、七十士、尔小生、八九子、邱乙己、佳作七) + return 3; + } + } + + /** + * 判断执行动作后是否听牌 + * @param handler 游戏处理器 + * @param type 动作类型(1=吃,2=碰,3/4/5=杠) + * @param card 涉及的牌 + * @return 是否听牌 + */ + private boolean willTingAfterAction(FuLuShouHandler handler, int type, int card) { + //模拟执行动作后的手牌状态 + List simulatedHandCards = new ArrayList<>(handler.getHandCards()); + + //根据动作类型移除对应的牌 + switch (type) { + case 1://吃 - 需要 3 张牌组成一句话 + //吃牌会消耗手牌中的 2 张 + 当前的 1 张 + if (simulatedHandCards.contains(card)) { + simulatedHandCards.remove(Integer.valueOf(card)); + //还需要再移除一张能与之组成句子的牌 + for (int other : simulatedHandCards) { + if (canFormSentence(card, other)) { + simulatedHandCards.remove(Integer.valueOf(other)); + break; + } + } + } + break; + case 2://碰 - 需要 2 张相同的牌 + int count = 0; + for (int c : simulatedHandCards) { + if (c == card) count++; + } + if (count >= 2) { + simulatedHandCards.remove(Integer.valueOf(card)); + simulatedHandCards.remove(Integer.valueOf(card)); + } + break; + case 3: case 4: case 5://杠 - 需要 3 张相同的牌 + count = 0; + for (int c : simulatedHandCards) { + if (c == card) count++; + } + if (count >= 3) { + simulatedHandCards.remove(Integer.valueOf(card)); + simulatedHandCards.remove(Integer.valueOf(card)); + simulatedHandCards.remove(Integer.valueOf(card)); + } + break; + } + + //检查模拟后的手牌是否听牌 + return simulateTingCheck(handler, simulatedHandCards); + } + + /** + * 判断两张牌是否能组成句子 + */ + private boolean canFormSentence(int card1, int card2) { + //对子 + if (card1 == card2) return true; + + //福禄寿组合:福 (101)、禄 (701)、寿 (1013) + List fuLuShou = Arrays.asList(101, 701, 1013); + if (fuLuShou.contains(card1) && fuLuShou.contains(card2)) { + return true; + } + + //上大人组合:上 (201)、大 (301)、人 (501) + List shangDaRen = Arrays.asList(201, 301, 501); + if (shangDaRen.contains(card1) && shangDaRen.contains(card2)) { + return true; + } + + //化三千组合:化 (301)、三 (1003)、千 (1015) + List huaSanQian = Arrays.asList(301, 1003, 1015); + if (huaSanQian.contains(card1) && huaSanQian.contains(card2)) { + return true; + } + + //七十士组合:七 (401)、十 (701)、士 (1017) + List qiShiShi = Arrays.asList(401, 701, 1017); + if (qiShiShi.contains(card1) && qiShiShi.contains(card2)) { + return true; + } + + //尔小生组合:尔 (501)、小 (1009)、生 (1019) + List erXiaoSheng = Arrays.asList(501, 1009, 1019); + if (erXiaoSheng.contains(card1) && erXiaoSheng.contains(card2)) { + return true; + } + + //八九子组合:八 (601)、九 (801)、子 (1020) + List baJiuZi = Arrays.asList(601, 801, 1020); + if (baJiuZi.contains(card1) && baJiuZi.contains(card2)) { + return true; + } + + //邱乙己组合:邱 (1021)、乙 (1022)、己 (1023) + List qiuYiJi = Arrays.asList(1021, 1022, 1023); + if (qiuYiJi.contains(card1) && qiuYiJi.contains(card2)) { + return true; + } + + //佳作七组合:佳 (1024)、作 (1007)、七 (401) + List jiaZuoQi = Arrays.asList(1024, 1007, 401); + if (jiaZuoQi.contains(card1) && jiaZuoQi.contains(card2)) { + return true; + } + + return false; + } + + /** + * 模拟检查是否听牌 + * @param handler 游戏处理器 + * @param simulatedHandCards 模拟的手牌 + * @return 是否听牌 + */ + private boolean simulateTingCheck(FuLuShouHandler handler, List simulatedHandCards) { + //遍历所有可能的牌 + for (int card = 101; card <= 804; card++) { + //跳过已经用完的牌 + if (isCardExhausted(handler, card)) { + continue; + } + + //假设摸到这张牌 + simulatedHandCards.add(card); + + //使用 handler 的胡牌检测(但不实际修改状态) + //这里需要临时创建一个检测逻辑 + FuLuShouSuanFa tempSuanFa = new FuLuShouSuanFa(); + FuLuShouSuanFa.HuResult result = tempSuanFa.checkWin(simulatedHandCards, + handler.getChowGroupsComplete(), + handler.getPongGroupsComplete(), + handler.getGangGroupsComplete()); + + if (result.isWin() && result.getTotalXi() >= 11) { + simulatedHandCards.remove(Integer.valueOf(card)); + return true;//听这张牌 + } + + simulatedHandCards.remove(Integer.valueOf(card)); + } + + return false; + } + + /** + * 判断执行动作后是否可以胡牌 + * @param handler 游戏处理器 + * @param type 动作类型(6=胡) + * @param card 涉及的牌 + * @return 是否可以胡牌 + */ + private boolean canWinAfterAction(FuLuShouHandler handler, int type, int card) { + if (type != 6) {//只有胡牌才需要检查 + return false; + } + + //直接检查当前状态是否胡牌 + FuLuShouSuanFa.HuResult result = handler.checkWin(); + return result.isWin() && result.getTotalXi() >= 11; + } + + /** + * 获取碰的息数 - 根据规则修复 + * 规则第 45-49 行: + * - 碰"上"或"福":12 息 + * - 碰其他字:2 息 + */ + private int getPongXi(int card) { + if (card == 101 || card == 201) { // 福、上:高息 + return 12; + } else { + // 碰其他任何字:2 息(包括大、人、禄、寿等所有字) + return 2; + } + } + + /** + * 获取杠的息数 - 根据规则修复 + * 规则第 57-61 行: + * - 招"上"或"福":16 息 + * - 招其他字:6 息 + */ + private int getGangXi(int card) { + if (card == 101 || card == 201) { // 福、上:高息 + return 16; + } else { + // 招其他任何字:6 息(包括大、人、禄、寿等所有字) + return 6; + } + } + + /** + * 判断是否是特殊字(福、上) + */ + public boolean isSpecialChar(int card) { + return card == 101 || card == 201; + } + + /** + * 判断是否是次特殊字(大、人、禄、寿) + */ + public boolean isSubSpecialChar(int card) { + return card == 301 || card == 501 || card == 701 || card == 1013; + } + + /** + * 根据 ID 查找对应的 tip(使用反射) + */ + private Object findTipById(Object tipList, int id) { + log.info("【findTipById】开始查找,tipList=" + tipList + ", tipList.getClass()=" + (tipList != null ? tipList.getClass().getName() : "null") + ", id=" + id); + try { + // 先尝试作为 List 处理 + if (tipList instanceof List) { + List list = (List) tipList; + log.info("【findTipById】List 大小=" + list.size()); + for (Object obj : list) { + log.info("【findTipById】遍历对象,obj=" + obj + ", obj.getClass()=" + (obj != null ? obj.getClass().getName() : "null")); + try { + // 参考 FuLuShouHandler 的方式:先 getObject() 再 getInt() + Object realObj = obj; + if (obj.getClass().getName().contains("TDataWrapper")) { + // TDataWrapper 需要先调用 getObject() 获取真实的 TObject + realObj = obj.getClass().getMethod("getObject").invoke(obj); + log.info("【findTipById】TDataWrapper.getObject()=" + realObj); + } + Object idValue = realObj.getClass().getMethod("getInt", String.class).invoke(realObj, "id"); + log.info("【findTipById】获取到 id 值=" + idValue); + if (idValue != null && ((Integer) idValue).intValue() == id) { + log.info("【findTipById】找到匹配的对象!"); + return realObj; + } + } catch (Exception e) { + log.warn("【findTipById】获取单个对象的 id 失败:" + e.getMessage()); + // 忽略单个对象的获取错误,继续下一个 + continue; + } + } + } else { + // 如果不是 List,尝试直接使用反射获取 size 和 get 方法(兼容 TArray) + log.warn("【findTipById】tipList 不是 List 类型,尝试作为 TArray 处理"); + try { + Object sizeObj = tipList.getClass().getMethod("size").invoke(tipList); + if (sizeObj instanceof Integer) { + int size = (Integer) sizeObj; + log.info("【findTipById】TArray 大小=" + size); + for (int i = 0; i < size; i++) { + try { + Object wrapperObj = tipList.getClass().getMethod("get", int.class).invoke(tipList, i); + log.info("【findTipById】遍历 TArray 对象,index=" + i + ", wrapperObj=" + wrapperObj + ", wrapperObj.getClass()=" + (wrapperObj != null ? wrapperObj.getClass().getName() : "null")); + try { + // TDataWrapper 需要先调用 getObject() 获取真实的 TObject + Object realObj = wrapperObj.getClass().getMethod("getObject").invoke(wrapperObj); + log.info("【findTipById】TDataWrapper.getObject()=" + realObj); + Object idValue = realObj.getClass().getMethod("getInt", String.class).invoke(realObj, "id"); + log.info("【findTipById】获取到 id 值=" + idValue); + if (idValue != null && ((Integer) idValue).intValue() == id) { + log.info("【findTipById】找到匹配的对象!"); + return realObj; + } + } catch (Exception e) { + log.warn("【findTipById】获取单个对象的 id 失败:" + e.getMessage()); + continue; + } + } catch (Exception e) { + log.warn("【findTipById】获取 TArray 元素失败:" + e.getMessage()); + continue; + } + } + } + } catch (Exception e) { + log.warn("【findTipById】无法获取 size 方法,可能不是 TArray 类型:" + e.getMessage()); + } + } + } catch (Exception e) { + log.error("【findTipById】查找 tip 时发生异常", e); + } + log.info("【findTipById】未找到,返回 null"); + return null; + } + + /** + * 获取 tip 的 type 字段(使用反射) + */ + private int getTipType(Object tip) { + try { + return (Integer) tip.getClass().getMethod("getInt", String.class).invoke(tip, "type"); + } catch (Exception e) { + log.error("获取 tip type 失败", e); + return 0; + } + } + + /** + * 获取 tip 的 card 字段(使用反射) + */ + private int getTipCard(Object tip) { + try { + return (Integer) tip.getClass().getMethod("getInt", String.class).invoke(tip, "card"); + } catch (Exception e) { + log.error("获取 tip card 失败", e); + return 0; + } + } + + /** + * 根据息数和听牌状态确定当前策略 - 修复版:整牌和攒息数同步进行 + * @param currentXi 当前息数 + * @param isTing 是否已听牌 + * @param neededFor5Group 达到 5 组还需要的组数 + * @return 策略描述 + */ + private String determineStrategy(int currentXi, boolean isTing, int neededFor5Group) { + // 核心思想:整牌和攒息数必须同步进行,不能偏废 + + if (currentXi >= 11 && isTing) { + return "保听等胡(已听牌且息数足够,等待胡牌)"; + } else if (currentXi >= 8 && !isTing) { + return "听牌优先冲胡牌(息数接近达标,优先凑句子听牌同时补息)"; + } else if (currentXi >= 5 && currentXi < 8 && neededFor5Group <= 2) { + return "高息整牌双管齐下(句子接近完成,选择既能整牌又有息的动作)"; + } else if (currentXi >= 5 && currentXi < 8 && neededFor5Group > 2) { + return "平衡发展优先高息(需要句子也需要息,优先选有息的句子)"; + } else if (currentXi < 5 && neededFor5Group <= 2) { + return "急追息数不忘整牌(息数严重不足,优先高息但也要能组成句子)"; + } else if (currentXi < 5 && neededFor5Group > 2) { + return "高息整牌双重优先(句子和息数都缺,必须选既有息又能整牌的动作)"; + } else { + return "稳健做牌(按部就班,每个动作都要考虑息数和整牌)"; + } + } + /** + * 高级听牌分析 - 修复版:主动寻找最优听牌方案 + * @param handler 游戏处理器 + * @return 听牌分析结果(包括听的牌、听牌数量、预估息数等) + */ + public TingAnalysis analyzeTingAdvanced(FuLuShouHandler handler) { + TingAnalysis analysis = new TingAnalysis(); + List handCards = handler.getHandCards(); + int currentXi = handler.getTotalXi(); + + //1. 遍历所有可能的打牌选择 + for (int discardCard : handCards) { + //2. 模拟打出这张牌后的手牌 + List simulatedHand = new ArrayList<>(handCards); + simulatedHand.remove(Integer.valueOf(discardCard)); + + //3. 检查打这张牌后能听哪些牌 + List tingAfterDiscard = findTingCardsAfterDiscard(handler, simulatedHand); + + if (!tingAfterDiscard.isEmpty()) { + //4. 计算这个打牌选择的综合评分 + TingOption option = new TingOption(); + option.discardCard = discardCard; + option.tingCards = tingAfterDiscard; + option.tingCount = tingAfterDiscard.size(); + option.avgWinQuality = calculateAverageWinQuality(handler, tingAfterDiscard); + option.safetyScore = getCardSafety(handler, discardCard); + + //5. 综合评分 = 听牌张数 * 质量 + 息数加成 + option.totalScore = (option.tingCount * 10.0) + (option.avgWinQuality / 10.0) + option.safetyScore; + + analysis.options.add(option); + + log.debug("【听牌选项】打" + discardCard + " -> 听" + option.tingCount + "张,质量=" + + String.format("%.2f", option.avgWinQuality) + ", 安全=" + String.format("%.2f", option.safetyScore)); + } + } + + //6. 找出最优的听牌选项 + if (!analysis.options.isEmpty()) { + TingOption bestOption = analysis.options.get(0); + for (TingOption option : analysis.options) { + if (option.totalScore > bestOption.totalScore) { + bestOption = option; + } + } + + analysis.isTing = true; + analysis.bestOption = bestOption; + analysis.tingCards = bestOption.tingCards; + + log.info("【听牌分析】最优听牌方案:打" + bestOption.discardCard + ", 听" + bestOption.tingCount + + "张,综合评分=" + String.format("%.2f", bestOption.totalScore)); + } else { + analysis.isTing = false; + log.debug("【听牌分析】未找到听牌机会"); + } + + return analysis; + } + + /** + * 检查打某张牌后能听哪些牌 + */ + private List findTingCardsAfterDiscard(FuLuShouHandler handler, List simulatedHand) { + List tingCards = new ArrayList<>(); + + // 遍历所有可能的牌(101-804) + for (int card = 101; card <= 804; card++) { + // 跳过已经用完的牌 + if (isCardExhausted(handler, card)) { + continue; + } + + // 假设摸到这张牌 + simulatedHand.add(card); + + // 检查是否胡牌 + FuLuShouSuanFa.HuResult result = handler.checkWinWithHand(simulatedHand, + handler.getChowGroupsComplete(), + handler.getPongGroupsComplete(), + handler.getGangGroupsComplete()); + + if (result.isWin() && result.getTotalXi() >= 11) { + tingCards.add(card); + } + + // 移除假设的牌 + simulatedHand.remove(Integer.valueOf(card)); + } + + return tingCards; + } + + /** + * 计算听多张牌的平均胡牌质量 + */ + private double calculateAverageWinQuality(FuLuShouHandler handler, List tingCards) { + if (tingCards.isEmpty()) return 0.0; + + double totalQuality = 0.0; + for (int card : tingCards) { + totalQuality += calculateWinQualityForCard(handler, card); + } + + return totalQuality / tingCards.size(); + } + + /** + * 计算听某张牌的胡牌质量 + */ + private double calculateWinQualityForCard(FuLuShouHandler handler, int winCard) { + double quality = 50.0; // 基础分 + + // 1. 特殊字奖励 + if (isSpecialChar(winCard)) { + quality += 30.0; + } else if (isSubSpecialChar(winCard)) { + quality += 15.0; + } + + // 2. 自摸奖励 + quality += 20.0; + + // 3. 剩余牌数奖励(越多越好胡) + int remainingCount = getRemainingCardCount(handler, winCard); + quality += remainingCount * 2.0; + + return quality; + } + + /** + * 获取某张牌的剩余数量 + */ + private int getRemainingCardCount(FuLuShouHandler handler, int card) { + int usedCount = 0; + + // 统计手牌中的数量 + for (int c : handler.getHandCards()) { + if (c == card) usedCount++; + } + + // 统计已使用的牌(吃碰杠) + for (List group : handler.getChowGroupsComplete()) { + for (int c : group) { + if (c == card) usedCount++; + } + } + for (List group : handler.getPongGroupsComplete()) { + for (int c : group) { + if (c == card) usedCount++; + } + } + for (List group : handler.getGangGroupsComplete()) { + for (int c : group) { + if (c == card) usedCount++; + } + } + + // 福禄寿每种牌最多 4 张 + return 4 - usedCount; + } + + /** + * 听牌时选择最优出牌 - 平衡安全与质量 + * @param handler 游戏处理器 + * @param analysis 听牌分析结果 + * @return 最优的出牌 + */ + public int selectOptimalDiscardForTing(FuLuShouHandler handler, TingAnalysis analysis) { + List handCards = handler.getHandCards(); + + if (handCards.isEmpty()) { + log.warn("【听牌出牌】手牌为空!"); + return -1; + } + + //1. 如果有 AI 分析的最优选项,优先使用 + if (analysis.bestOption != null) { + log.info("【听牌出牌】采用 AI 分析的最优方案:打" + analysis.bestOption.discardCard); + return analysis.bestOption.discardCard; + } + + //2. 否则回退到安全牌策略 + log.info("【听牌出牌】无 AI 分析结果,采用保守安全牌策略"); + return selectSafeDiscardForTing(handler); + } + + /** + * 检查当前手牌是否可以胡牌 + * @param handler 游戏处理器 + * @return 是否可以胡牌 + */ + public boolean canWinWithCurrentHand(FuLuShouHandler handler) { + FuLuShouSuanFa.HuResult result = handler.checkWin(); + int totalXi = handler.getTotalXi(); + + boolean canWin = result.isWin() && totalXi >= 11; + if (canWin) { + log.info("【胡牌检测】当前手牌满足胡牌条件!息数=" + totalXi); + } + return canWin; + } + + /** + * 听牌分析结果类 + */ + public static class TingAnalysis { + public boolean isTing = false; // 是否听牌 + public List tingCards = new ArrayList<>(); // 听的牌列表 + public List options = new ArrayList<>(); // 所有听牌选项 + public TingOption bestOption; // 最优选项 + } + + /** + * 听牌选项类 + */ + public static class TingOption { + public int discardCard; // 打出的牌 + public List tingCards; // 听的牌 + public int tingCount; // 听牌张数 + public double avgWinQuality; // 平均胡牌质量 + public double safetyScore; // 安全分数 + public double totalScore; // 综合评分 + } + + /** + * 【新增核心方法】模拟打某张牌后的效果(安全版本,不修改手牌) + * @param handler 游戏处理器 + * @param cardToDiscard 要打出的牌 + * @return 模拟评分(负值表示损失,正值表示收益) + */ + private double simulateDiscardEffectSafe(FuLuShouHandler handler, int cardToDiscard) { + double score = 0.0; + List handCards = handler.getHandCards(); + + // 1. 创建模拟手牌(不修改原始数据) + List simulatedHand = new ArrayList<>(handCards); + simulatedHand.remove(Integer.valueOf(cardToDiscard)); + + // 2. 基于模拟手牌检查听牌(使用临时构造的算法实例) + FuLuShouSuanFa tempSuanFa = new FuLuShouSuanFa(); + boolean canTingAfterDiscard = checkTingWithSimulatedHand(tempSuanFa, simulatedHand, handler); + boolean canWinAfterDiscard = checkWinWithSimulatedHand(tempSuanFa, simulatedHand, handler); + + if (canWinAfterDiscard) { + score -= 100.0; // 打掉后还能胡牌,大幅减分(好牌) + log.debug(" - 【模拟】打掉后能胡牌,评分 -100"); + } else if (canTingAfterDiscard) { + score -= 50.0; // 打掉后还能听牌,减分(好牌) + log.debug(" - 【模拟】打掉后仍能听牌,评分 -50"); + } + + // 3. 考虑息数影响 + int xiLoss = calculateXiLoss(cardToDiscard); + if (xiLoss > 0) { + score += xiLoss * 8.0; // 损失息数,加分(坏牌) + log.debug(" - 【模拟】损失息数:" + xiLoss + ", 评分 +" + (xiLoss * 8.0)); + } + + // 4. 考虑安全性(已出现的牌越多越安全) + double safety = getCardSafety(handler, cardToDiscard); + score -= safety * 2.0; // 越安全减分越多(好牌) + log.debug(" - 【模拟】安全性:" + safety); + + // 5. 考虑是否破坏潜在组合 + if (willBreakPotentialCombo(handCards, cardToDiscard)) { + score += 15.0; // 破坏潜在组合,加分(坏牌) + log.debug(" - 【模拟】破坏潜在组合,评分 +15"); + } + + return score; + } + + /** + * 使用模拟手牌检查是否听牌(不修改原始数据) + */ + private boolean checkTingWithSimulatedHand(FuLuShouSuanFa suanFa, List simulatedHand, FuLuShouHandler handler) { + // 遍历所有可能的牌(101-804) + for (int card = 101; card <= 804; card++) { + // 跳过已经用完的牌 + if (isCardExhausted(handler, card)) { + continue; + } + + // 假设摸到这张牌 + simulatedHand.add(card); + + // 检查是否胡牌 + FuLuShouSuanFa.HuResult result = suanFa.checkWin(simulatedHand, + handler.getChowGroupsComplete(), + handler.getPongGroupsComplete(), + handler.getGangGroupsComplete()); + + if (result.isWin() && handler.getTotalXi() >= 11) { + simulatedHand.remove(Integer.valueOf(card)); // 恢复 + return true; + } + + // 移除假设的牌 + simulatedHand.remove(Integer.valueOf(card)); + } + return false; + } + + /** + * 使用模拟手牌检查是否胡牌(不修改原始数据) + */ + private boolean checkWinWithSimulatedHand(FuLuShouSuanFa suanFa, List simulatedHand, FuLuShouHandler handler) { + FuLuShouSuanFa.HuResult result = suanFa.checkWin(simulatedHand, + handler.getChowGroupsComplete(), + handler.getPongGroupsComplete(), + handler.getGangGroupsComplete()); + return result.isWin() && handler.getTotalXi() >= 11; + } + + /** + * 计算打掉某张牌损失的潜在息数 + */ + private int calculateXiLoss(int card) { + // 如果这张牌是特殊字,损失潜在息数 + if (card == 101 || card == 201) { // 福、上 + return 3; // 每张 3 息 + } else if (card >= 301 && card <= 704) { // 大、人、禄、寿等 + return 1; // 每张 1 息 + } + return 0; + } + + /** + * 检查打掉某张牌是否会破坏潜在组合 + */ + private boolean willBreakPotentialCombo(List handCards, int card) { + // 检查是否有对子 + int count = countCards(handCards, card); + if (count == 2) { + return true; // 拆对子 + } + + // 检查是否是顺子的一部分 + if (card >= 101 && card <= 804) { + int base = (card / 100) * 100; + int num = card % 100; + + // 检查是否有相邻的牌 + for (int i = Math.max(1, num - 2); i <= Math.min(4, num + 2); i++) { + if (i == num) continue; + int relatedCard = base + i; + if (handCards.contains(relatedCard)) { + return true; // 破坏顺子潜力 + } + } + } + + return false; + } + +} diff --git a/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouSuanFa.java b/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouSuanFa.java index 7f63d4a..78a64b3 100644 --- a/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouSuanFa.java +++ b/robots/zhipai/robot_zp_fls/src/main/java/taurus/util/FuLuShouSuanFa.java @@ -1,4 +1,621 @@ package taurus.util; +import java.util.*; + +/** + * 福禄寿字牌游戏算法 + */ public class FuLuShouSuanFa { + + //特殊牌标记 + //福 + private static final int SPECIAL_FU = 101; + //禄 + private static final int SUB_SPECIAL_LU = 701; + //寿 + private static final int SUB_SPECIAL_SHOU = 1013; + //上 + private static final int SPECIAL_SHANG = 201; + //大 + private static final int SUB_SPECIAL_DA = 301; + //人 + private static final int SUB_SPECIAL_REN = 501; + + /** + * 判断是否是特殊字(福、上) + */ + public static boolean isSpecialChar(int card) { + return card == SPECIAL_FU || card == SPECIAL_SHANG; + } + + /** + * 判断是否是次特殊字(大、人、禄、寿) + */ + public static boolean isSubSpecialChar(int card) { + return card == SUB_SPECIAL_DA || card == SUB_SPECIAL_REN || card == SUB_SPECIAL_LU || card == SUB_SPECIAL_SHOU; + } + + /** + * 检查两个半搭子的息数计算 + * @param halfCombos 两个半搭子(每个半搭子是 2 张牌的列表) + * @return 总息数 + */ + public static int calculateHalfComboXi(List> halfCombos) { + if (halfCombos.size() != 2) { + return 0; + } + + List combo1 = halfCombos.get(0); + List combo2 = halfCombos.get(1); + + int xi1 = getHalfComboBaseXi(combo1); + int xi2 = getHalfComboBaseXi(combo2); + + //有一个属于特殊组合 + if (isSpecialHalfCombo(combo1) || isSpecialHalfCombo(combo2)) { + return xi1 + xi2; + } + + //都不属于特殊组合 取最大值 + return Math.max(xi1, xi2); + } + + /** + * 获取单个半搭子的基础息值 + */ + public static int getHalfComboBaseXi(List combo) { + if (combo.size() != 2) { + return 0; + } + + int card1 = combo.get(0); + int card2 = combo.get(1); + + //特殊二字组合:上大、上人、福禄、福寿 + if ((card1 == SPECIAL_SHANG && (card2 == SUB_SPECIAL_DA || card2 == SUB_SPECIAL_REN)) || + (card2 == SPECIAL_SHANG && (card1 == SUB_SPECIAL_DA || card1 == SUB_SPECIAL_REN))) { + return 4; + } + + if ((card1 == SPECIAL_FU && isFuLuShouRelated(card2)) || + (card2 == SPECIAL_FU && isFuLuShouRelated(card1))) { + return 4; + } + + //大人组合 + if ((card1 == SUB_SPECIAL_DA && card2 == SUB_SPECIAL_REN) || + (card2 == SUB_SPECIAL_DA && card1 == SUB_SPECIAL_REN)) { + return 4; + } + + //对子 + if (card1 == card2) { + if (card1 == SPECIAL_SHANG || card1 == SPECIAL_FU) { + return 12; + } + return 3;//其他对子 + } + + //检查是否包含特殊字 + if (isSpecialChar(card1) || isSpecialChar(card2)) { + return 4; + } + + return 0; + } + + /** + * 判断是否是特殊半搭子 + */ + public static boolean isSpecialHalfCombo(List combo) { + if (combo.size() != 2) { + return false; + } + + int card1 = combo.get(0); + int card2 = combo.get(1); + + //上大、上人、福禄、福寿 + if (card1 == SPECIAL_SHANG && (card2 == SUB_SPECIAL_DA || card2 == SUB_SPECIAL_REN)) { + return true; + } + if (card2 == SPECIAL_SHANG && (card1 == SUB_SPECIAL_DA || card1 == SUB_SPECIAL_REN)) { + return true; + } + if (card1 == SPECIAL_FU && isFuLuShouRelated(card2)) { + return true; + } + if (card2 == SPECIAL_FU && isFuLuShouRelated(card1)) { + return true; + } + + return false; + } + + /** + * 判断是否与福禄寿相关 + */ + private static boolean isFuLuShouRelated(int card) { + //禄或寿 + return card == SUB_SPECIAL_LU || card == SUB_SPECIAL_SHOU; + } + + /** + * 计算六句加一字时单字的息 + */ + public static int calculateSingleCardXi(int card) { + if (isSpecialChar(card)) { + return 8; + } + if (isSubSpecialChar(card)) { + return 4; + } + return 0; + } + + /** + * 计算碰的息数 + */ + public static int getPongXi(int card) { + if (isSpecialChar(card)) { + return 12; + } + return 2; + } + + /** + * 计算吃(放)的息数 + * @param cards 吃的三张牌 + */ + public static int getChowXi(List cards) { + if (cards.size() != 3) { + return 0; + } + + //检查是否包含福或上(福禄寿、上大人句子) + for (int card : cards) { + if (isSpecialChar(card)) { + return 12; + } + } + + return 3; + } + + /** + * 计算招(杠)的息数 + */ + public static int getGangXi(int card) { + if (isSpecialChar(card)) { + return 16; + } + return 6; + } + + /** + * 检查复字牌是否可以操作(吃、碰、杠、胡) + * @param card 要操作的牌 + * @param isSelfDraw 是否是自摸 + * @return 是否可以操作 + */ + public static boolean canOperateCompoundCard(int card, boolean isSelfDraw) { + //如果是自摸 任何牌都可以操作 + if (isSelfDraw) { + return true; + } + + if (isCompoundCard(card)) { + return false; + } + + //其他单字牌可以正常操作 + return true; + } + + /** + * 检测是否是复字牌 + */ + /** + * 判断是否是复字牌 + * @param card 牌的 ID + * @return 是否是复字牌 + */ + public static boolean isCompoundCard(int card) { + return (card >= 901 && card <= 908) || (card >= 701 && card <= 703); + } + + /** + * 检测是否是坎(三张相同的单字牌) + */ + public static boolean isKan(List cards) { + if (cards.size() != 3) { + return false; + } + + //不能有复字牌 + for (int card : cards) { + if (isCompoundCard(card)) { + return false; + } + } + + //三张必须相同 + return cards.get(0).intValue() == cards.get(1).intValue() && cards.get(1).intValue() == cards.get(2).intValue(); + } + + /** + * 检测是否是招(杠) + */ + public static boolean isGang(List cards) { + if (cards.size() != 4) { + return false; + } + + //统计每种牌的数量 + Map countMap = new HashMap<>(); + for (int card : cards) { + countMap.put(card, countMap.getOrDefault(card, 0) + 1); + } + + //四张相同的单字牌 + if (countMap.size() == 1) { + int card = cards.get(0); + return !isCompoundCard(card); + } + + //三张单字牌 + 一张复字牌 + if (countMap.size() == 2) { + boolean hasCompound = false; + boolean hasThreeSame = false; + + for (Map.Entry entry : countMap.entrySet()) { + if (isCompoundCard(entry.getKey())) { + hasCompound = true; + } + if (entry.getValue() == 3) { + hasThreeSame = true; + } + } + + return hasCompound && hasThreeSame; + } + + return false; + } + + /** + * 检测胡牌是否符合规则 + * @param allCards 所有牌(19 张) + * @param chowGroups 吃的牌组 + * @param pongGroups 碰的牌组 + * @param gangGroups 杠的牌组 + * @return 胡牌结果 + */ + public HuResult checkWin(List allCards, List> chowGroups, List> pongGroups, List> gangGroups) { + + if (allCards.size() != 19) { + return new HuResult(false, "牌数不是 19 张"); + } + + //五句 + 两个半搭子 + HuResult result1 = checkWinType1(allCards, chowGroups, pongGroups, gangGroups); + if (result1.isWin() && result1.getTotalXi() >= 11) { + return result1; + } + + //六句 + 任意一字 + HuResult result2 = checkWinType2(allCards, chowGroups, pongGroups, gangGroups); + if (result2.isWin() && result2.getTotalXi() >= 11) { + return result2; + } + + //两种形式都不符合 返回失败原因 + String msg = "不符合胡牌条件"; + if (result1.isWin() && result1.getTotalXi() < 11) { + msg = "息数不足 11 息(当前:" + result1.getTotalXi() + "息)"; + } else if (result2.isWin() && result2.getTotalXi() < 11) { + msg = "息数不足 11 息(当前:" + result2.getTotalXi() + "息)"; + } + + return new HuResult(false, msg); + } + + /** + * 检查第一种胡牌类型:五句 + 两个半搭子 + */ + private HuResult checkWinType1(List allCards, List> chowGroups, List> pongGroups, List> gangGroups) { + //已组成的句子数量 + int sentenceCount = chowGroups.size() + pongGroups.size() + gangGroups.size(); + + if (sentenceCount != 5) { + return new HuResult(false, "句子数量不是 5 个"); + } + + //计算剩余牌是否构成两个半搭子 + int usedCards = sentenceCount * 3; + int remaining = allCards.size() - usedCards; + + if (remaining != 4) { + return new HuResult(false, "剩余牌数不是 4 张"); + } + + //提取剩余的 4 张牌 + List remainingCards = new ArrayList<>(); + List usedCardList = new ArrayList<>(); + + //添加所有已使用的牌 + for (List group : chowGroups) { + usedCardList.addAll(group); + } + for (List group : pongGroups) { + usedCardList.addAll(group); + } + for (List group : gangGroups) { + usedCardList.addAll(group); + } + + //找出未使用的牌 + Map cardCountMap = new HashMap<>(); + for (int card : allCards) { + cardCountMap.put(card, cardCountMap.getOrDefault(card, 0) + 1); + } + for (int card : usedCardList) { + cardCountMap.put(card, cardCountMap.get(card) - 1); + } + + for (Map.Entry entry : cardCountMap.entrySet()) { + for (int i = 0; i < entry.getValue(); i++) { + remainingCards.add(entry.getKey()); + } + } + + //检查4张牌是否能组成两个半搭子(每半搭 2 张) + if (remainingCards.size() != 4) { + return new HuResult(false, "无法提取 4 张剩余牌"); + } + + //尝试所有可能的两两分组 + boolean canFormTwoHalfCombos = false; + + List combo1 = Arrays.asList(remainingCards.get(0), remainingCards.get(1)); + List combo2 = Arrays.asList(remainingCards.get(2), remainingCards.get(3)); + if (isValidHalfCombo(combo1) && isValidHalfCombo(combo2)) { + canFormTwoHalfCombos = true; + } + + if (!canFormTwoHalfCombos) { + combo1 = Arrays.asList(remainingCards.get(0), remainingCards.get(2)); + combo2 = Arrays.asList(remainingCards.get(1), remainingCards.get(3)); + if (isValidHalfCombo(combo1) && isValidHalfCombo(combo2)) { + canFormTwoHalfCombos = true; + } + } + + if (!canFormTwoHalfCombos) { + combo1 = Arrays.asList(remainingCards.get(0), remainingCards.get(3)); + combo2 = Arrays.asList(remainingCards.get(1), remainingCards.get(2)); + if (isValidHalfCombo(combo1) && isValidHalfCombo(combo2)) { + canFormTwoHalfCombos = true; + } + } + + if (!canFormTwoHalfCombos) { + return new HuResult(false, "剩余 4 张牌无法组成两个有效半搭子"); + } + + //计算总息数 + int totalXi = calculateTotalXiFromGroups(chowGroups, pongGroups, gangGroups, Arrays.asList(combo1, combo2), 0); + + HuResult result = new HuResult(true, "五句 + 两半搭胡牌", totalXi); + return result; + } + + /** + * 检查第二种胡牌类型:六句 + 任意一字 + */ + private HuResult checkWinType2(List allCards, List> chowGroups, List> pongGroups, List> gangGroups) { + int sentenceCount = chowGroups.size() + pongGroups.size() + gangGroups.size(); + + if (sentenceCount != 6) { + return new HuResult(false, "句子数量不是 6 个"); + } + + int usedCards = sentenceCount * 3; + int remaining = allCards.size() - usedCards; + + if (remaining != 1) { + return new HuResult(false, "剩余牌数不是 1 张"); + } + + //提取剩余的 1 张牌 + List usedCardList = new ArrayList<>(); + for (List group : chowGroups) { + usedCardList.addAll(group); + } + for (List group : pongGroups) { + usedCardList.addAll(group); + } + for (List group : gangGroups) { + usedCardList.addAll(group); + } + + Map cardCountMap = new HashMap<>(); + for (int card : allCards) { + cardCountMap.put(card, cardCountMap.getOrDefault(card, 0) + 1); + } + for (int card : usedCardList) { + cardCountMap.put(card, cardCountMap.get(card) - 1); + } + + int singleCard = 0; + for (Map.Entry entry : cardCountMap.entrySet()) { + if (entry.getValue() > 0) { + singleCard = entry.getKey(); + break; + } + } + + if (singleCard == 0) { + return new HuResult(false, "无法提取单张牌"); + } + + //计算总息数 + int totalXi = calculateTotalXiFromGroups(chowGroups, pongGroups, gangGroups, new ArrayList<>(), singleCard); + + return new HuResult(true, "六句 + 一字胡牌", totalXi); + } + + /** + * 判断是否是有效的半搭子(2 张牌) + */ + private boolean isValidHalfCombo(List combo) { + if (combo.size() != 2) { + return false; + } + + int card1 = combo.get(0); + int card2 = combo.get(1); + + //对子是有效的半搭子 + if (card1 == card2) { + return true; + } + + //特殊组合:上大、上人、福禄、福寿 + if ((card1 == SPECIAL_SHANG && (card2 == SUB_SPECIAL_DA || card2 == SUB_SPECIAL_REN)) || + (card2 == SPECIAL_SHANG && (card1 == SUB_SPECIAL_DA || card1 == SUB_SPECIAL_REN))) { + return true; + } + + if ((card1 == SPECIAL_FU && isFuLuShouRelated(card2)) || + (card2 == SPECIAL_FU && isFuLuShouRelated(card1))) { + return true; + } + + //大人组合 + if ((card1 == SUB_SPECIAL_DA && card2 == SUB_SPECIAL_REN) || + (card2 == SUB_SPECIAL_DA && card1 == SUB_SPECIAL_REN)) { + return true; + } + + return false; + } + + /** + * 从各组动作中计算总息数 + */ + private int calculateTotalXiFromGroups(List> chowGroups, List> pongGroups, List> gangGroups, List> halfCombos, int singleCard) { + int totalXi = 0; + + //吃 + for (List cards : chowGroups) { + totalXi += getChowXi(cards); + } + + //碰 + for (List group : pongGroups) { + if (!group.isEmpty()) { + totalXi += getPongXi(group.get(0)); + } + } + + //杠 + for (List group : gangGroups) { + if (!group.isEmpty()) { + totalXi += getGangXi(group.get(0)); + } + } + + //半搭子 + if (halfCombos.size() == 2) { + totalXi += calculateHalfComboXi(halfCombos); + } + + //单张牌 + if (singleCard != 0) { + totalXi += calculateSingleCardXi(singleCard); + } + + return totalXi; + } + + /** + * 只从吃碰杠动作中计算息数(不包含手牌中的潜在息数) + * 根据福禄寿规则:只有吃碰杠对方的牌才能获得息数,手牌中的对子/组合没有息数 + * @param actionRecords 动作记录列表 + * @return 总息数 + */ + public int calculateXiFromActions(List actionRecords) { + int totalXi = 0; + + for (ActionRecord record : actionRecords) { + switch (record.type) { + case PONG: + totalXi += getPongXi(record.card); + break; + case CHOW: + totalXi += getChowXi(record.cards); + break; + case GANG: + totalXi += getGangXi(record.card); + break; + } + } + + return totalXi; + } + + /** + * 动作记录类 + */ + public static class ActionRecord { + public ActionType type; + public int card; + public List cards; + + public ActionRecord(ActionType type, int card) { + this.type = type; + this.card = card; + } + + public ActionRecord(ActionType type, List cards) { + this.type = type; + this.cards = cards; + } + } + + /** + * 动作类型枚举 + */ + public enum ActionType { + PONG, //碰 + CHOW, //吃 + GANG //杠 + } + + /** + * 胡牌结果类 + */ + public static class HuResult { + private boolean win; + private String message; + private int totalXi; + + public HuResult(boolean win, String message) { + this.win = win; + this.message = message; + this.totalXi = 0; + } + + public HuResult(boolean win, String message, int totalXi) { + this.win = win; + this.message = message; + this.totalXi = totalXi; + } + + public boolean isWin() { return win; } + public String getMessage() { return message; } + public int getTotalXi() { return totalXi; } + public void setTotalXi(int xi) { this.totalXi = xi; } + } }