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