master
zhouwei 2026-07-11 16:43:09 +08:00
parent aa005007d6
commit a54afc33eb
6 changed files with 4648 additions and 370 deletions

View File

@ -412,7 +412,7 @@ public class RobotConnectionManager {
robotUser.setStatus(ROBOTEventType.ROBOT_INTOROOM_WORKING);
//初始化收手牌
String key = robotId+"";
log.info("key+++++++++++++++++++++++++++++++++++" + key);
log.info("新的一局key+++++++++++++++++++++++++++++++++++" + key);
if (jedis2.hget("{robortInfo}:" + key, "circleId") != null && jedis2.hget("{robortInfo}:" + key, "pid") != null) {
String circleId = jedis2.hget("{robortInfo}:" + key, "circleId");
log.info("circleId +++++++++++++++++++++++++"+circleId);
@ -431,6 +431,7 @@ public class RobotConnectionManager {
}
//出牌广播
else if ("812".equalsIgnoreCase(command)) {
log.info("[协议812] 出牌广播 param:{}", param);
ITArray outcard_map = param.getTArray("outcard_map");
ITArray opchicards = param.getTArray("opchicards");
ITArray oppengcards = param.getTArray("oppengcards");
@ -549,22 +550,40 @@ public class RobotConnectionManager {
Map<Integer, List<Integer>> currentPlayerzisMap = getPlayerzisMap(connecId);
huNanChangSha.outCard(client, currentPlayerOutcardsMap, currentPlayerchisMap, currentPlayerpengsMap, currentPlayermingsMap, currentPlayerzisMap);
//处理完协议后保存到Redis
HeChi currentInstance = huNanChangShaInstances.get(connecId);
currentInstance.saveToRedis(connecId);
//处理完协议后保存到Redis加try-catch避免Redis异常导致机器人卡死
try {
HeChi currentInstance = huNanChangShaInstances.get(connecId);
if (currentInstance != null) {
currentInstance.saveToRedis(connecId);
}
} catch (Exception redisEx) {
log.warn("813处理完成后保存Redis失败(不影响出牌): {}", redisEx.getMessage());
}
//收到补杠协议
}else if("838".equalsIgnoreCase(command)) {
huNanChangSha.bugang(param,robotUser,client);
//处理完协议后保存到Redis
HeChi currentInstance = huNanChangShaInstances.get(connecId);
currentInstance.saveToRedis(connecId);
//处理完协议后保存到Redis加try-catch避免Redis异常导致机器人卡死
try {
HeChi currentInstance = huNanChangShaInstances.get(connecId);
if (currentInstance != null) {
currentInstance.saveToRedis(connecId);
}
} catch (Exception redisEx) {
log.warn("838处理完成后保存Redis失败(不影响出牌): {}", redisEx.getMessage());
}
}
//放招提示
else if ("814".equalsIgnoreCase(command)) {
huNanChangSha.actionCard(param, client);
//处理完协议后保存到Redis
HeChi currentInstance = huNanChangShaInstances.get(connecId);
currentInstance.saveToRedis(connecId);
//处理完协议后保存到Redis加try-catch避免Redis异常导致机器人卡死
try {
HeChi currentInstance = huNanChangShaInstances.get(connecId);
if (currentInstance != null) {
currentInstance.saveToRedis(connecId);
}
} catch (Exception redisEx) {
log.warn("814处理完成后保存Redis失败(不影响出牌): {}", redisEx.getMessage());
}
}
//2026.02.03修改 玩家加入房间
else if ("2001".equalsIgnoreCase(command)) {
@ -748,6 +767,7 @@ public class RobotConnectionManager {
//服务器通知客户端有玩家执行了操作
else if ("815".equalsIgnoreCase(command)) {
huNanChangSha.shanchuchuguopai(param);
log.info("[协议815] 玩家操作通知 - param:{}", param);
//处理完协议后保存到Redis
HeChi currentInstance = huNanChangShaInstances.get(connecId);
currentInstance.saveToRedis(connecId);
@ -783,8 +803,15 @@ public class RobotConnectionManager {
});
}
//服务器返回的错误码如3005
else if ("3005".equalsIgnoreCase(command)) {
System.out.println("[3005错误] 服务器返回错误码3005! param:" + param);
log.error("[协议3005] 服务器返回错误! param:{}", param);
}
} catch (Exception e) {
log.error("处理接收到的游戏协议");
System.out.println("【ERROR】处理游戏协议异常! 类型=" + e.getClass().getName()
+ ", 信息=" + e.getMessage() + ", 协议号=" + command);
e.printStackTrace(System.out);
} finally {
jedis0.close();
jedis2.close();

View File

@ -28,6 +28,30 @@ public class HeChi {
public static boolean isTinPeng = false;
private final List<Integer> changShaCardInhandgang = new ArrayList<>();
/**
*
* 101-109=~, 201-209=~, 301-309=~
* 401=,402=,403=西,404=, 501=,502=,503=
*/
private static String cardToString(int card) {
int suit = card / 100;
int value = card % 100;
String[] suitNames = {"", "万", "筒", "条", "风", "箭"};
String[] valueNames = {"", "一", "二", "三", "四", "五", "六", "七", "八", "九"};
switch (suit) {
case 1: case 2: case 3:
return valueNames[value] + suitNames[suit] + "(" + card + ")";
case 4:
String[] windNames = {"", "东风", "南风", "西风", "北风"};
return windNames[value] + "(" + card + ")";
case 5:
String[] arrowNames = {"", "红中", "发财", "白板"};
return arrowNames[value] + "(" + card + ")";
default:
return "未知(" + card + ")";
}
}
//河池麻将出过的牌
private final List<Integer> changShachuguopai = new ArrayList<>();
@ -55,6 +79,25 @@ public class HeChi {
private Integer lastChowCard2 = null;
private Integer lastChowCard3 = null;
/**
* 612()
* outCard6116116123005
*/
private volatile long last612SendTime = 0;
/**
*
* 147/258/369()
* 147: value%3==1 (1,4,7)
* 258: value%3==2 (2,5,8)
* 369: value%3==0 (3,6,9)
*
*/
private boolean isChowMenziRestricted(int discardCard) {
// 【已禁用】河池麻将无147/258/369吃牌门子出牌限制规则
return false;
}
//会话标识
public String session = "";
//访问令牌
@ -264,6 +307,9 @@ public class HeChi {
if (param == null) {
return null;
}
int card = param.getInt("card");
int seat = param.getInt("seat");
log.info("[812出牌广播] 座位{}打出牌:{}", seat, cardToString(card));
changShaCard = param.getInt("card");
}
return null;
@ -281,22 +327,37 @@ public class HeChi {
if (command.equalsIgnoreCase("819")) {
ITObject param = message.param;
if (param == null) {
System.out.println("[819摸牌-诊断] WARNING: param为空无法获取摸牌数据changSaCardInhand当前:" + changShaCardInhand);
return null;
}
Jedis jedis222 = Redis.use("group1_db2").getJedis();
Integer seat1 = param.getInt("seat");
Integer seat2 = robotUser.getSeat();
System.out.println("819机器人座位号 " + seat2);
System.out.println("819真人座位号 " + seat1);
int drawnCard = param.getInt("card");
// ====== 诊断日志使用System.out确保可见 ======
System.out.println("[819摸牌-诊断] 协议原始数据 - seat(协议):" + seat1 + ", seat(机器人):" + seat2 +
", card(摸到的牌):" + drawnCard + "(" + cardToString(drawnCard) + ")");
System.out.println("[819摸牌-诊断] 摸牌前手牌(" + changShaCardInhand.size() + "张):" + changShaCardInhand);
// ====== 结束诊断 ======
if (Objects.equals(seat1, seat2)) {
System.out.println("819能不能进来");
int drawnCard = param.getInt("card");
// 是机器人自己摸牌
log.info("[819摸牌] 机器人座位{}摸到牌:{}", seat2, cardToString(drawnCard));
changShaSuanFaTest.drawnCards = drawnCard;//存储摸到的牌
changShaCardInhand.add(drawnCard);
log.info("param.getInt(player): {}", param.getInt("player"));
log.info("摸到的牌 +++++++++ : {}", drawnCard);
// ====== 诊断日志 ======
System.out.println("[819摸牌-诊断] ✓ 确认是机器人自己摸牌!添加后手牌(" + changShaCardInhand.size() + "张):" + changShaCardInhand);
// ====== 结束诊断 ======
log.info("[819摸牌] 摸牌后手牌:{} (含红中{}张)", changShaCardInhand,
changShaCardInhand.stream().filter(c -> c == 501).count());
} else {
// 是其他玩家摸牌
log.info("[819摸牌] 座位{}玩家摸牌(非机器人,机器人座位{})", seat1, seat2);
System.out.println("[819摸牌-诊断] ✗ 不是机器人摸牌跳过。seat1=" + seat1 + ", seat2=" + seat2);
}
jedis222.close();
@ -357,9 +418,11 @@ public class HeChi {
/**
*
* 3005
*
* @param client
* @param params
* @param params _pendingType
* @param actionName
*/
private void delayedActionCard(TaurusClient client, ITObject params, String actionName) {
//使用线程池执行延迟动作
@ -370,7 +433,10 @@ public class HeChi {
Thread.sleep(delaySeconds * 1000);
client.send("612", params, response -> {
log.info("{}动作发送完成", actionName);
// 记录612实际发送时间供outCard发送611时参考
last612SendTime = System.currentTimeMillis();
log.info("{}动作发送完成, 时间戳:{}", actionName, last612SendTime);
// 注意状态修改已在actionCard()中立即执行,此处不再重复修改
});
} catch (Exception e) {
log.error("执行{}动作时发生异常: {}", actionName, e.getMessage(), e);
@ -432,27 +498,71 @@ public class HeChi {
* {"tip_list":[{"type":3,"id":1,"opcard":[206],"weight":3,"card":206}]}
*/
public String actionCard(ITObject param, TaurusClient client) {
System.out.println("814所有参数" + param);
// System.out.println("[814吃碰杠胡] 收到操作选项:" + param); //已精简
try {
ITArray tipList = param.getTArray("tip_list");
// 诊断tipList是否为null
if (tipList == null) {
System.out.println("[814] ⚠️ tipList为null! param=" + param);
tipList = TArray.newInstance();//防止后续NPE
}
System.out.println("[814] tipList.size=" + tipList.size());
ITObject params = TObject.newInstance();
int card = 0;
//循环
List<Integer> yupanhandcard = new ArrayList<>();
yupanhandcard.addAll(changShaCardInhand);
//进行操作之前能否下听
// 统计手牌中红中数量
int actionHongZhongCount = 0;
List<Integer> handWithoutHZ = new ArrayList<>();
for (Integer c : changShaCardInhand) {
if (c == 501) {
actionHongZhongCount++;
} else {
handWithoutHZ.add(c);
}
}
// 修复日志SLF4J无binding导致{}不被替换改用System.out.println确保可看到值
// System.out.println("[814操作前] 手牌:" + changShaCardInhand + ", 红中数量:" + actionHongZhongCount); //已精简
List<Integer> shifoutingpai = changShaSuanFaTest.handscardshifoutingpai(changShaCardInhand, chowGroup, pongGroup, gangdepai);
boolean beforelisten = false;//记录操作之前的下听状态
log.info("shifoutingpai{}", shifoutingpai);
// System.out.println("[814操作前] 听牌结果:" + shifoutingpai + ", 是否已听牌:" + (shifoutingpai.size() > 0)); //已精简
if (shifoutingpai.size() > 0) {
beforelisten = true; //操作之前是听牌的
}
log.info("beforelisten{}", beforelisten);
// 兜底检测如果handscardshifoutingpai没检测到听牌再用WinCard直接检测手牌能否胡牌
// 只要手牌加任何一张牌就能胡,说明已经在听牌状态
if (!beforelisten && changShaCardInhand.size() == 13) {
boolean canWin = false;
List<Integer> candidateCards = new ArrayList<>();
for (int j = 101; j <= 109; j++) candidateCards.add(j);
for (int j = 201; j <= 209; j++) candidateCards.add(j);
for (int j = 301; j <= 309; j++) candidateCards.add(j);
for (int j = 401; j <= 404; j++) candidateCards.add(j);
for (int j = 502; j <= 503; j++) candidateCards.add(j);
for (Integer candidate : candidateCards) {
WinCard win = new WinCard(handWithoutHZ, candidate, actionHongZhongCount, 501);
if (win.tryWin()) {
canWin = true;
break;
}
}
if (canWin) {
beforelisten = true;
// System.out.println("[814兜底检测] handscardshifoutingpai未检测到听牌但WinCard检测到手牌可胡 → 已听牌"); //已精简
}
}
System.out.println("beforelisten:" + beforelisten); //保留:关键状态标记
//如果杠了之后还能继续听牌则杠
//优先处理胡牌
@ -501,16 +611,22 @@ public class HeChi {
params.putInt("qi", 0);
params.putInt("id", 0);
delayedActionCard(client, params, "不开杠");
System.out.println("不开杠111");
return "不开杠";
}
// 明杠:立即修改本地状态确保后续出牌使用正确手牌
Util.removeCard(changShaCardInhand, card, 3);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
log.info("[状态同步] 明杠{}立即更新本地手牌:{}", cardToString(card), changShaCardInhand);
params.putString("session", session + "," + token);
params.putInt("qi", 0);
params.putInt("id", id);
delayedActionCard(client, params, "开杠");
System.out.println("开杠222");
return "开杠";
} else {
params.putString("session", session + "," + token);
@ -542,7 +658,15 @@ public class HeChi {
return "不开杠";
}
// 暗杠:立即修改本地状态确保后续出牌使用正确手牌
Util.removeCard(changShaCardInhand, card, 4);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
log.info("[状态同步] 暗杠{}立即更新本地手牌:{}", cardToString(card), changShaCardInhand);
params.putString("session", session + "," + token);
params.putInt("qi", 0);
params.putInt("id", id);
@ -577,7 +701,15 @@ public class HeChi {
}
// 补杠:立即修改本地状态确保后续出牌使用正确手牌
Util.removeCard(changShaCardInhand, card, 1);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
gangdepai.add(card);
log.info("[状态同步] 补杠{}立即更新本地手牌:{}", cardToString(card), changShaCardInhand);
params.putString("session", session + "," + token);
params.putInt("qi", 0);
params.putInt("id", id);
@ -652,6 +784,7 @@ public class HeChi {
sj.putInt("weight", weight);
sj.putInt("type", type);
sj.putTArray("opcard", opcard);
sj.putInt("card", card); // 必须存入card字段否则后续getInt("card")会NPE
idObject.put(id, sj);
}
//计算分数
@ -673,37 +806,84 @@ public class HeChi {
if (entry.getKey() == changeid) {
tmp = entry.getValue();
log.debug("tmp ++++++++++= {}", tmp);
if (tmp.getInt("type") == 2) {
//碰
ITArray outcards = tmp.getTArray("opcard");
for (int i = 0; i < outcards.size(); i++) {
Util.removeCard(changShaCardInhand, outcards.getInt(0), 2);
}
pongGroup.add(outcards.getInt(0));
pongGroup.add(outcards.getInt(0));
pongGroup.add(outcards.getInt(0));
} else if (tmp.getInt("type") == 1) {
//吃
ITArray outcards = tmp.getTArray("opcard");
for (int i = 0; i < outcards.size(); i++) {
Util.removeCard(changShaCardInhand, outcards.getInt(i), 1);
}
log.debug("判断吃 +++++++++{}", card);
log.debug("判断吃 ========== {}", outcards);
chowGroup.add(card);
chowGroup.add(outcards.getInt(0));
chowGroup.add(outcards.getInt(1));
// 记录最后吃的三张牌(用于门子规则判断)
lastChowCard1 = card;
lastChowCard2 = outcards.getInt(0);
lastChowCard3 = outcards.getInt(1);
System.out.println("记录最后吃的三张牌(用于门子规则判断" + lastChowCard1 + lastChowCard2 + lastChowCard3);
}
break; // 找到就跳出,避免不必要的遍历
}
}
// 防御性检查如果changeid在idObject中找不到对应条目
if (tmp == null) {
System.out.println("[814] ⚠️ changeid=" + changeid + " 在idObject中未找到! keys=" + idObject.keySet() + ", 兜底执行'过'");
params.putString("session", session + "," + token);
params.putInt("qi", 0);
params.putInt("id", 0);
delayedActionCard(client, params, "changeid未找到-过");
return null;
}
// ===== 🚀 碰杠破坏性评估 =====
int opType = tmp.getInt("type");
int opCard = tmp.getInt("card");
List<Integer> beforeOpHand = new ArrayList<>(yupanhandcard);
if (opType == 2 || opType == 3 || opType == 5) {
try {
double damage = ChangshaWinSplitCard.assessOperationDamage(
opType,
opCard,
beforeOpHand,
new ArrayList<>(), // 操作后的手牌稍后计算
beforelisten,
false // 是否听牌稍后检测
);
System.out.println("【碰杠破坏性评估】操作类型=" + opType +
", 牌=" + cardToString(opCard) +
", 破坏性评分=" + String.format("%.1f", damage) +
(damage > 10 ? " ⚠️ 破坏性较高!" : ""));
if (damage > 20) {
// System.out.println("⚠️ 警告:此操作破坏性极高(" + damage + "),建议重新考虑!");
}
} catch (Exception e) {
log.warn("破坏性评估异常: {}", e.getMessage());
}
}
// ===== 结束破坏性评估 =====
if (tmp.getInt("type") == 2) {
//碰 - 立即修改本地状态确保后续出牌(813/611)使用正确的手牌
ITArray outcards = tmp.getTArray("opcard");
// 立即执行状态修改
int pengCard = outcards.getInt(0);
Util.removeCard(changShaCardInhand, pengCard, 3);
pongGroup.add(pengCard);
pongGroup.add(pengCard);
pongGroup.add(pengCard);
log.info("[状态同步] 碰牌{}立即更新本地手牌:{}", cardToString(pengCard), changShaCardInhand);
} else if (tmp.getInt("type") == 1) {
//吃 - 立即修改本地状态确保后续出牌(813/611)使用正确的手牌
ITArray outcards = tmp.getTArray("opcard");
// 立即执行状态修改
Util.removeCard(changShaCardInhand, outcards.getInt(0), 1);
Util.removeCard(changShaCardInhand, outcards.getInt(1), 1);
chowGroup.add(card);
chowGroup.add(outcards.getInt(0));
chowGroup.add(outcards.getInt(1));
log.info("[状态同步] 吃牌{}{}{}立即更新本地手牌:{}",
cardToString(card), cardToString(outcards.getInt(0)), cardToString(outcards.getInt(1)),
changShaCardInhand);
// 记录最后吃的三张牌(用于门子规则判断)
lastChowCard1 = card;
lastChowCard2 = outcards.getInt(0);
lastChowCard3 = outcards.getInt(1);
}
params.putString("session", session + "," + token);
params.putInt("qi", 0);
params.putInt("id", changeid);
@ -800,6 +980,7 @@ public class HeChi {
params.putInt("qi", 0);
params.putInt("id", id);
// 补杠/开杠:立即修改本地状态确保后续出牌使用正确手牌
if (type == 3) {
Util.removeCard(changShaCardInhand, card, 3);
gangdepai.add(card);
@ -821,6 +1002,7 @@ public class HeChi {
gangdepai.add(card);
gangdepai.add(card);
}
delayedActionCard(client, params, "");
return null;
}
@ -830,6 +1012,7 @@ public class HeChi {
params.putInt("qi", 0);
params.putInt("id", id);
// 补杠/开杠:立即修改本地状态确保后续出牌使用正确手牌
if (type == 3) {
Util.removeCard(changShaCardInhand, card, 3);
gangdepai.add(card);
@ -863,6 +1046,22 @@ public class HeChi {
delayedActionCard(client, params, "默认动作");
return null;
} catch (Exception e) {
// SLF4J NOP logger无法输出参数用System.out.println确保异常可见
System.out.println("[814] ⚠️ actionCard内部异常! 类型=" + e.getClass().getName() + ", 信息=" + e.getMessage());
e.printStackTrace(System.out);
// 兜底:发送"过"/空操作避免机器人卡死
try {
ITObject fallbackParams = TObject.newInstance();
fallbackParams.putString("session", session + "," + token);
fallbackParams.putInt("qi", 0);
fallbackParams.putInt("id", 0);
delayedActionCard(client, fallbackParams, "异常兜底-过");
} catch (Exception ex) {
System.out.println("[814] ⚠️ 兜底操作也失败: " + ex.getMessage());
}
return null;
}
}
@ -917,81 +1116,71 @@ public class HeChi {
}
// 河池麻将出牌 - 先过滤掉红中(501),不让算法处理
// 河池麻将出牌 - 先过滤掉红中(501),不让算法处理红中通过hongZhongCount传入算法
List<Integer> handCardsWithoutHongZhong = new ArrayList<>();
for (Integer card : changShaCardInhand) {
if (card == 501) {
System.out.println("红中先过滤掉" + changShaCardInhand);
// 红中不参与算法处理通过hongZhongCount传入
} else {
handCardsWithoutHongZhong.add(card);
}
}
System.out.println("过滤掉红中的手牌" + handCardsWithoutHongZhong);
log.info("[出牌分析] 手牌(含红中):{}, 红中数量:{}, 过滤后手牌:{}", changShaCardInhand, hongZhongCount, handCardsWithoutHongZhong);
log.info("河池麻将出牌{}", changShaCardInhand);
// 河池麻将出牌
String changShaOutCard = changShaSuanFaTest.outCardSuanFa(handCardsWithoutHongZhong, pongGroup, chowGroup, gangdepai, resultList,hongZhongCount);
//如果最后吃的牌是147,258,369同个门子的牌不能打出 需要换张牌打
//如果最后吃了牌147/258/369同个门子(同花色)同组的牌不能打出,需要换张牌打
if (lastChowCard1 != null && lastChowCard2 != null && lastChowCard3 != null) {
boolean canOut = false;
int outcard = Integer.parseInt(changShaOutCard);
if (lastChowCard1 - outcard == 3 && lastChowCard2 - outcard < 3
&& lastChowCard3 - outcard < 3) {
canOut = true;
} else if (outcard - lastChowCard1 == 3 && outcard - lastChowCard2 < 3
&& outcard - lastChowCard3 < 3) {
canOut = true;
}
if (canOut) {
System.out.println("如果最后吃的牌是147,258,369同个门子的牌不能打出 需要换张牌打" + outcard);
System.out.println("lastChowCard1 " + lastChowCard1);
System.out.println("lastChowCard2 " + lastChowCard2);
System.out.println("lastChowCard3 " + lastChowCard3);
System.out.println("[门子规则检查] 原选牌:" + outcard + "(" + cardToString(outcard) + "), 吃的牌:[" +
lastChowCard1 + "," + lastChowCard2 + "," + lastChowCard3 + "]");
if (isChowMenziRestricted(outcard)) {
System.out.println("门子规则限制:吃的牌[" + lastChowCard1 + "," + lastChowCard2 + "," + lastChowCard3 + "]同组同门子牌不能打出,原选牌:" + outcard);
List<Integer> tempHandCards = new ArrayList<>();
tempHandCards.addAll(handCardsWithoutHongZhong);
tempHandCards.remove(Integer.valueOf(outcard));
System.out.println("完整手牌 changShaCardInhand" + changShaCardInhand);
System.out.println("从手牌中删除禁止打的牌后的手牌" + tempHandCards);
if (!tempHandCards.isEmpty()) {
System.out.println("进入重新计算");
// 逐步移除被门子规则禁止的牌,重新选牌(增加最大重试次数防止死循环)
int maxRetries = tempHandCards.size(); // 最多尝试手牌张数次
int retryCount = 0;
while (tempHandCards.size() > 1 && retryCount < maxRetries) {
retryCount++;
tempHandCards.remove(Integer.valueOf(outcard));
System.out.println("移除被门子限制的牌:" + outcard + "(" + cardToString(outcard) + "),第" + retryCount + "次重试,剩余手牌:" + tempHandCards);
changShaOutCard = changShaSuanFaTest.outCardSuanFa(tempHandCards, pongGroup, chowGroup, gangdepai, resultList,hongZhongCount);
// 安全检查outCardSuanFa返回空或无效
if (changShaOutCard == null || changShaOutCard.isEmpty()) {
System.out.println("[门子规则] ⚠️ outCardSuanFa返回空! 强制使用剩余手牌第一张");
if (!tempHandCards.isEmpty()) {
changShaOutCard = String.valueOf(tempHandCards.get(0));
}
break;
}
int newOutcard = Integer.parseInt(changShaOutCard);
boolean newcanOut = false;
while (tempHandCards.size() > 1) {
if (lastChowCard1 - newOutcard == 3 && lastChowCard2 - newOutcard < 3
&& lastChowCard3 - newOutcard < 3) {
newcanOut = true;
} else if (newOutcard - lastChowCard1 == 3 && newOutcard - lastChowCard2 < 3
&& newOutcard - lastChowCard3 < 3) {
newcanOut = true;
}
if (newcanOut){
System.out.println("新选的牌也被门子规则禁止,继续尝试" + newOutcard);
tempHandCards.remove(Integer.valueOf(newOutcard));
changShaOutCard = changShaSuanFaTest.outCardSuanFa(tempHandCards, pongGroup, chowGroup, gangdepai, resultList,hongZhongCount);
newOutcard = Integer.parseInt(changShaOutCard);
} else {
changShaOutCard = String.valueOf(newOutcard);
break;
}
if (!isChowMenziRestricted(newOutcard)) {
System.out.println("找到不受门子限制的出牌:" + newOutcard + "(" + cardToString(newOutcard) + "),共重试" + retryCount + "次");
break;
} else {
System.out.println("新选的牌" + newOutcard + "(" + cardToString(newOutcard) + ")也被门子规则禁止,继续尝试");
outcard = newOutcard;
}
}
if (retryCount >= maxRetries) {
System.out.println("[门子规则] ⚠️ 达到最大重试次数" + maxRetries + "次! 强制使用当前候选牌:" + changShaOutCard);
}
} else {
System.out.println("[门子规则检查] 选牌" + outcard + "(" + cardToString(outcard) + ")不受门子限制,正常出牌");
}
// if (canOut && owner.cardInhand.size() == 2) {
// owner.isFenghu = true;
// } else if (canOut) {
// ITObject reconParam = new TObject();
// owner.sendEvent(Router.GAME_EVT__UPDATE_RECONECT, reconParam);
// return;
//
// }
// 清理门子数据(无论是否触发限制都要清理)
lastChowCard1 = null;
lastChowCard2 = null;
lastChowCard3 = null;
}
log.info("河池麻将出牌{}", changShaCardInhand);
ITObject params = TObject.newInstance();
@ -1020,19 +1209,39 @@ public class HeChi {
changShaCardInhand.remove(Integer.valueOf(cardToOut));
log.info("打过后的手牌 +++ {}", changShaCardInhand);
params.putString("session", session + "," + token);
System.out.println("河池麻将出牌参数 +" + params);
log.info("[出牌决定] 机器人打出:{} (手牌:{}, 已出牌:{})",
cardToString(cardToOut), changShaCardInhand, changShachuguopai);
//清理最后一次吃的数据
lastChowCard1 = null;
lastChowCard2 = null;
lastChowCard3 = null;
//使用线程池替代CompletableFuture.runAsync + Thread.sleep
//使用线程池发送出牌协议611
getBusinessThreadPool().execute(() -> {
try {
int ot = new Random().nextInt(4);
Thread.sleep(ot * 1000);
client.send("611", params, response -> {
int ot = new Random().nextInt(4); // 基础随机延迟0~3秒
// ===== 关键修复检测是否有待确认的612(吃碰杠)协议 =====
// 问题根因actionCard()立即修改本地状态后outCard基于新状态构建card_list
// 但如果612还没到达服务器服务器用旧状态校验 → 3005错误
long now = System.currentTimeMillis();
long elapsedSince612 = now - last612SendTime;
// 如果5秒内有612发送记录需要额外等待确保612先到达服务器
int extraDelayMs = 0;
if (last612SendTime > 0 && elapsedSince612 < 5000) {
// 612可能在路上额外等待1~2秒让网络传输完成
extraDelayMs = 1000 + new Random().nextInt(1000);
log.info("[611顺序保护] 检测到{}ms前有612发送额外延迟{}ms确保到达顺序",
elapsedSince612, extraDelayMs);
}
// ===== 结束修复 =====
Thread.sleep(ot * 1000 + extraDelayMs);
client.send("611", params, response -> {
log.info("[611出牌] 发送完成");
});
log.info("[611出牌] 总延迟={}ms(基础{}ms+保护{}ms)",
ot * 1000 + extraDelayMs, ot * 1000, extraDelayMs);
} catch (Exception e) {
log.error("线程执行错误", e);
}
@ -1055,17 +1264,24 @@ public class HeChi {
* @return
*/
public String shanchuchuguopai(ITObject param) {
log.info("对面吃碰删除出过的牌组");
if (param == null) {
return null;
}
Integer card = param.getInt("card"); // 操作牌值
Integer type = param.getInt("type"); // 操作类型
Integer playerid = param.getInt("playerid");
String typeName = "";
switch (type) {
case 1: typeName = "吃"; break;
case 2: typeName = "碰"; break;
case 3: typeName = "明杠"; break;
case 5: typeName = "暗杠/补杠"; break;
default: typeName = "未知(type=" + type + ")"; break;
}
log.info("[815玩家操作] 玩家{}对牌{}执行{}操作", playerid, cardToString(card), typeName);
//Redis中获取机器人ID列表
List<Integer> robotIdsList = new ArrayList<>();

View File

@ -7,130 +7,80 @@ import java.util.*;
public class TinHuChi {
/**
*
* - Bug
*/
public static void main(String[] args) {
// System.out.println("=== 测试开始 ===\n");
//
// // 测试1你的例子
List<Integer> hand1 = new ArrayList<>();
System.out.println("=== 核心Bug修复验证测试 ===\n");
hand1.add(209);
hand1.add(209);
hand1.add(206);
hand1.add(206);
hand1.add(104);
hand1.add(104);
hand1.add(105);
hand1.add(105);
hand1.add(101);
hand1.add(202);
hand1.add(201);
hand1.add(109);
hand1.add(203);
ChangShaSuanFaTest changShaSuanFaTest = new ChangShaSuanFaTest();
String s = changShaSuanFaTest.outCardSuanFa(hand1, new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>(), 1);
// int addcard =203;
// Map<String, Object> map = new HashMap<>();
// //碰之后的map
// Map<String, Object> map2 = new HashMap<>();
//
// //先判断碰之前还需要几手牌,以及孤章
//// int jiangnum = checkduijiang(changShaCardInhand);
// List<Integer> tmpChangSch = new ArrayList<>();
// tmpChangSch.addAll(hand1);
//// tmpChangSch.add(addcard);
// int i = ChangshaWinSplitCard.checkNormalHu(tmpChangSch, map);
// System.out.println("checkNormalHu 孤章数量 " + ((List<Integer>) map.get("cardResiue")).size());
// System.out.println("checkNormalHu 手数 " + map.get("remainingMelds"));
//
//
// //碰之后
// List<Integer> pengtemphand = new ArrayList<>();
// pengtemphand.addAll(tmpChangSch);
// Util.removeCard(pengtemphand, addcard, 3);
//
// ChangshaWinSplitCard.checkNormalHu(pengtemphand, map2);
// //判断两个map是否找到更优
// System.out.println("碰之前 map1: 手数" + Integer.parseInt(map.get("remainingMelds").toString()));
// System.out.println("碰之后 map2 手数:" + Integer.parseInt(map2.get("remainingMelds").toString()));
// //碰之后的手数小于碰之前的手数,可以碰
// if (Integer.parseInt(map2.get("remainingMelds").toString()) < Integer.parseInt(map.get("remainingMelds").toString())) {
// System.out.println("===============碰之后的手数小于碰之前的手数,可以碰 决定碰牌================== ++++ ");
// } else if (Integer.parseInt(map2.get("remainingMelds").toString()) == Integer.parseInt(map.get("remainingMelds").toString())) { //碰完后和碰之前手数相等,需要判断孤章数量
// //碰之后的数量
// int size2 = ((List<Integer>) map2.get("cardResiue")).size();
// System.out.println("碰之后的孤章数量 " + size2);
//
// int size1 = ((List<Integer>) map.get("cardResiue")).size();
// System.out.println("碰之前的孤章数量 " + size1);
// if (size2 < size1) {
// System.out.println("===============碰之后的孤章数量 小于 碰之前的孤章数量可以碰 ================== ++++ ");
// }
// //碰之后的手数大于碰之前的手数,或者碰之后的孤章数量大于碰之前的孤章数量不能碰
// } else if (Integer.parseInt(map2.get("remainingMelds").toString()) > Integer.parseInt(map.get("remainingMelds").toString()) || ((List<Integer>) map2.get("cardResiue")).size() > ((List<Integer>) map.get("cardResiue")).size()) {
// System.out.println("碰之后的手数大于碰之前的手数,或者碰之后的孤章数量大于碰之前的孤章数量不能碰");
// }
}
public static List<Integer> checktingpai(List<Integer> cardhand) {
List<Integer> tpcards = new ArrayList<>();
List<Integer> tmphc = cardhand;
for (int i = 0; i < cardhand.size(); i++) {
int tmpcard = tmphc.get(0);
tmphc.remove(0);
for (int j = 101; j <= 109; j++) {
WinCard win = new WinCard(tmphc, j);
if (win.tryWin()) {
if (!tpcards.contains(tmpcard)) {
tpcards.add(tmpcard);
}
}
}
for (int j = 201; j <= 209; j++) {
WinCard win = new WinCard(tmphc, j);
if (win.tryWin()) {
if (!tpcards.contains(tmpcard)) {
tpcards.add(tmpcard);
}
}
}
for (int j = 301; j <= 309; j++) {
WinCard win = new WinCard(tmphc, j);
if (win.tryWin()) {
if (!tpcards.contains(tmpcard)) {
tpcards.add(tmpcard);
}
}
}
for (int j = 401; j <= 404; j++) {
WinCard win = new WinCard(tmphc, j);
if (win.tryWin()) {
if (!tpcards.contains(tmpcard)) {
tpcards.add(tmpcard);
}
}
}
for (int j = 502; j <= 503; j++) {
WinCard win = new WinCard(tmphc, j);
if (win.tryWin()) {
if (!tpcards.contains(tmpcard)) {
tpcards.add(tmpcard);
}
}
}
tmphc.add(tmpcard);
// ===== 测试场景杠后11张手牌应该检测到打八万(108)能听牌 =====
// 用户原始问题:机器人杠六万后抓到七万,手牌为:
// [107, 108×2, 204, 205, 206, 302×2, 305, 306, 307]
// 预期结果打108拆八万对子能听106六万或109九万
System.out.println("【测试场景1】杠后11张 - 多对子听牌检测");
List<Integer> testHand1 = new ArrayList<>();
testHand1.add(107); // 七万
testHand1.add(108); // 八万
testHand1.add(108); // 八万(对子)
testHand1.add(204); // 四饼
testHand1.add(205); // 五饼
testHand1.add(206); // 六饼
testHand1.add(302); // 二条
testHand1.add(302); // 二条(对子)
testHand1.add(305); // 五条
testHand1.add(306); // 六条
testHand1.add(307); // 七条
System.out.println("手牌:" + testHand1);
System.out.println("牌数:" + testHand1.size() + "张");
// 测试1checktingpaiWithWildcard 能否识别出打108后能听牌
List<Integer> tingResult = ChangshaWinSplitCard.checktingpaiWithWildcard(testHand1, 0, 0);
System.out.println("\n checktingpaiWithWildcard结果打出这些牌可听: " + tingResult);
if (tingResult.contains(108)) {
System.out.println(" 【成功】正确识别出打108八万可以听牌");
} else if (!tingResult.isEmpty()) {
System.out.println(" 部分成功:检测到可听牌" + tingResult + "但未包含108");
} else {
System.out.println(" 失败未检测到任何听牌选项原bug状态");
}
return tpcards;
// 测试2analyzeBestDiscardWithWildcard 能否生成包含108的候选列表
List<Integer> discardCandidates = ChangshaWinSplitCard.analyzeBestDiscardWithWildcard(testHand1, 0, 0);
System.out.println("\n analyzeBestDiscardWithWildcard候选列表: " + discardCandidates);
if (discardCandidates.contains(108)) {
System.out.println(" 【成功】候选列表包含108允许拆对子换取听牌");
} else {
System.out.println(" 候选列表不包含108: " + discardCandidates);
}
// 测试3完整出牌决策流程
System.out.println("\n===== 完整出牌决策测试 =====");
ChangShaSuanFaTest changShaSuanFaTest = new ChangShaSuanFaTest();
String finalDecision = changShaSuanFaTest.outCardSuanFa(
testHand1,
new ArrayList<>(), // chowGroup
new ArrayList<>(), // pengCard
new ArrayList<>(), // gangdepai
new ArrayList<>(), // 已出牌列表
1 // 游戏阶段
);
System.out.println("最终决策 → 打出: " + finalDecision);
if (finalDecision.equals("108")) {
System.out.println(" 决策正确!选择打八万(108)进入听牌状态!");
} else if (finalDecision.equals("206")) {
System.out.println(" 决策错误!打了六饼(206)而不是八万(108)");
} else if (finalDecision.equals("302")) {
System.out.println(" 决策保守:打二条(302),未追求最优听牌");
} else {
System.out.println(" 决策:打出" + finalDecision);
}
System.out.println("\n=== 测试完成 ===\n");
}
/**
@ -150,4 +100,4 @@ public class TinHuChi {
}
}
}

View File

@ -151,44 +151,131 @@ public class WinCard {
return true;
}
public boolean tryWin() {
if (this.cardList.size() == 0 && this.pair_count == 1) {
/**
* 2+1
* 1 + 2 =
*/
private boolean tryKezi2Zhong(int card) {
if (this.zhong_count >= 2 && Util.checkCardAndRomve(card, this.cardList, 1)) {
List<Integer> cardGroup = Arrays.asList(card, this.zhongid, this.zhongid);
this.zhong_count -= 2;
this.push(cardGroup);
return true;
}
return false;
}
/**
* 2+1
* 3//
* 1 + 2 =
*/
private boolean tryShunzi2Zhong(int card) {
int suit = card / 100;
if (suit < 1 || suit > 3) {
return false;
}
int cardValue = card % 100;
if (this.zhong_count < 2) {
return false;
}
// 配置1牌在首位(V, W, W)V需<=7
if (cardValue <= 7 && Util.checkCardAndRomve(card, this.cardList, 1)) {
this.zhong_count -= 2;
List<Integer> cardGroup = Arrays.asList(card, this.zhongid, this.zhongid);
this.push(cardGroup);
return true;
}
if (this.cardList.size() == 0) {
// 配置2牌在末位(W, W, V)V需>=3
if (cardValue >= 3 && this.zhong_count >= 2 && Util.checkCardAndRomve(card, this.cardList, 1)) {
this.zhong_count -= 2;
List<Integer> cardGroup = Arrays.asList(this.zhongid, this.zhongid, card);
this.push(cardGroup);
return true;
}
// 配置3牌在中间(W, V, W)V需>=2且<=8
if (cardValue >= 2 && cardValue <= 8 && this.zhong_count >= 2 && Util.checkCardAndRomve(card, this.cardList, 1)) {
this.zhong_count -= 2;
List<Integer> cardGroup = Arrays.asList(this.zhongid, card, this.zhongid);
this.push(cardGroup);
return true;
}
return false;
}
/**
* 3=
* 3
*/
private boolean tryZhongKezi() {
if (this.zhong_count >= 3) {
this.zhong_count -= 3;
List<Integer> cardGroup = Arrays.asList(this.zhongid, this.zhongid, this.zhongid);
this.push(cardGroup);
return true;
}
return false;
}
/**
* /2=
*/
private boolean tryZhongPair() {
if (this.pair_count > 0) {
return false;
}
if (this.zhong_count >= 2) {
this.zhong_count -= 2;
List<Integer> cardGroup = Arrays.asList(this.zhongid, this.zhongid);
this.push(cardGroup);
this.pair_count = 1;
return true;
}
return false;
}
public boolean tryWin() {
// 终止条件:所有牌(普通牌+红中)都分配完毕,且有将牌 → 胡牌
if (this.cardList.size() == 0 && this.zhong_count == 0 && this.pair_count == 1) {
return true;
}
// cardList为空但还有红中剩余 → 红中自身组牌(兜底:只有普通牌全拆完了才让红中自己组)
if (this.cardList.size() == 0) {
// 3个红中=红中刻子
if (tryZhongKezi()) {
if (tryWin()) {
return true;
}
this.rollBack();
}
// 2个红中=对子(将牌)
if (tryZhongPair()) {
if (tryWin()) {
return true;
}
this.pair_count = 0;
this.rollBack();
}
// 红中剩余1个无法组任何牌 → 不可能胡牌
return false;
}
int activeCard = this.cardList.get(0);
if (tryPair(activeCard)) {
if (tryWin()) {
return true;
}
this.pair_count = 0;
this.rollBack();
}
if (tryKezi(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
if (tryShunzi(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
// 红中作为万能牌替代其他牌(优先级高于红中自身组牌)
// 1个万能牌组合1红中替代1张缺失牌
if (tryKezi1Zhong(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
if (tryShunzi1Zhong(activeCard)) {
if (tryWin()) {
@ -204,6 +291,41 @@ public class WinCard {
this.rollBack();
}
// 2个万能牌组合2红中替代2张缺失牌
if (tryKezi2Zhong(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
if (tryShunzi2Zhong(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
// 普通牌组合(不含红中,红中不够替代时走这条路)
if (tryPair(activeCard)) {
if (tryWin()) {
return true;
}
this.pair_count = 0;
this.rollBack();
}
if (tryKezi(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
if (tryShunzi(activeCard)) {
if (tryWin()) {
return true;
}
this.rollBack();
}
return false;
}
@ -221,6 +343,27 @@ public class WinCard {
Collections.sort(this.cardList);
}
/**
* (501)
* cardInhandwildCardCount
* @param cardInhand
* @param addCard 0
* @param wildCardCount ()
* @param wildCardId ID=501
*/
public WinCard(List<Integer> cardInhand, int addCard, int wildCardCount, int wildCardId) {
this.stack = new Stack<List<Integer>>();
this.cardList = new ArrayList<Integer>();
this.cardList.addAll(cardInhand);
this.zhongid = wildCardId;
this.zhong_count = wildCardCount;
if (addCard != 0) {
this.cardList.add(addCard);
}
Collections.sort(this.cardList);
}
public static void main(String[] args) {
long time = System.currentTimeMillis();