够级五回合大乱斗:从跟牌改成对决,以及机器人怎么出牌

在这里插入图片描述

传统够级是顺序跟牌。这次把 Game_gj 整条局流程拆掉重写,做成 6 人红蓝对决、5 回合比牌。房间状态机、发牌、牌型、结算、机器人出牌都是新的,堆在 melee/ 下面。

局流程变成:

Init → (FaPai → Playing → DuelReveal → RoundSettle) × 5 → End

定庄、翻牌换座、cgjopcard 跟牌都不再驱动状态机。客户端按新协议走:发牌确认 gj.cgjrounddealconfirm,出牌 gj.cgjplaycard

时序先抽出来

对决时刻动画很多:明牌、大类公告、胜家、收分、金钟罩。一开始这些间隔散在 GJRoomSet 里,改一次节奏要翻好几处。后来统一收到 GJMeleeStateMachineConfig,房间只读配置执行步进。

public static final int DEAL_TIMEOUT_MS = 1800;
public static final int PLAY_TIMEOUT_MS = 20000;
public static final int REVEAL_FIRST_STEP_WAIT_MS = 3600;
public static final int REVEAL_NEXT_STEP_WAIT_MS = 1000;
public static final int SETTLE_TO_BOMB_PLAYERS_WAIT_MS = 5000;
public static final int ROBOT_PLAY_DELAY_MIN_MS = 1000;
public static final int ROBOT_PLAY_DELAY_MAX_MS = 6000;

DuelReveal / RoundSettle 不走固定超时,由内部步骤往前推。getSettleStepWaitMs 看上一步是什么:炸弹收分完等 5 秒再发金钟罩,无助力时胜家到收分会短 500ms。改动画只动这个类。
在这里插入图片描述

发牌:108 张共用库,按回合凑牌

手牌张数写死在 GJMeleeSimpleDeal

public static final int[] HAND_COUNTS = {6, 8, 10, 12, 14};
public static final int TOTAL_ROUND = 5;
public static final int BRIGHT_PER_ROUND = 2;

每回合从共用牌库补到目标张数,末尾最多两张明牌。上回合没打出的明牌继续占名额,下一回合只按打出的明牌张数补发。打出的牌和认输剩余手牌回收入库尾部,不洗。

补什么牌不走顺序 pop。GJMeleeDealEngine 按回合切 DealMode:R1 偏散、R2 看命中率、R3 够级牌锚点、R4 大型结构、R5 反推出牌升级。权重在 GJMeleeRoundDealProfile

R1 炸弹空发只有 2%。R2~R5 先看留牌种子:假顺窗口里不同点 ≥3,80% 补成真顺;有连对苗头就补连对。没种子不空发长顺/长连对。炸弹空发也压过一轮,避免每把满天炸。

第 5 回合发牌直接问出牌器「补这张之后最优出牌变强没有」:

private static Integer findBestUpgradeCard(DeckPool pool, List<Integer> workingHand) {
    GJMeleeCardTypeResult baseResult = GJMeleeCardTypeAnalyzer.analyze(
            GJMeleeRobotPlayPicker.pickBestStructureOnly(workingHand));
    long baseKey = baseResult == null ? 0 : baseResult.compareKey;
    // 遍历牌库,trial 后 compareKey 更大的留下
}

发牌和出牌共用同一套牌力,不然补牌目标和机器人实际打出去的对不上。
在这里插入图片描述

出牌不校验牌型,结算时再识别

playCard 只要求是手牌子集、本回合还没出过。主牌型在结算侧由 GJMeleeCardTypeAnalyzer.analyze 取。能组成炸弹就走炸弹瀑布,不跟下面比倍数;没有炸弹才按现网倍数取最大,平倍走连刻 → 连对 → 顺子 → … → 单牌。

public static GJMeleeCardTypeResult analyze(List<Integer> cardList) {
    GJMeleeCardTypeResult bomb = tryBombCategory(parse, cards, pointSum);
    if (bomb != null) {
        return bomb;
    }
    GJMeleeCardTypeResult best = null;
    best = keepMaxMultiplier(best, tryConsecTriplet(parse, cards, pointSum));
    best = keepMaxMultiplier(best, tryConsecPair(parse, cards, pointSum));
    best = keepMaxMultiplier(best, tryStraight(parse, cards, pointSum));
    // …
}

21 种牌型在 GJ_MELEE_CARD_TYPE,value 对协议,category() 切四大类(炸弹 4 / 刻子 3 / 对子 2 / 单牌 1)。炸弹内部 compareRank() 和 value 不完全一致,三王炸比六星炸大,对照表写在对决规则里。

选牌预览走 gj.cgjquerycardtype,返回 previewScore = pointSum × typeMultiplier,不落库。点数:王 5,10~A/2 为 3,3~9 为 1。

结算按大类,不是 9 场 1v1

每大类取全局牌力最强的人当胜者,向敌方非跳过座位收分。先 detectBombMode

  • 没炸弹:跳过炸弹大类
  • 红蓝都有炸弹且会暴击:本回合只结炸弹
  • 有炸弹但不暴击:BOMB_THEN_OTHERS,打出炸弹的人后续大类不竞争、不助力、不被收分(金钟罩)
Set<Integer> skipAfterBomb = bombMode == GJMeleeRoundBombMode.BOMB_THEN_OTHERS
        ? new HashSet<>(bombPlayerPos) : Collections.emptySet();

for (int category : categoryOrder) {
    Set<Integer> skipPos = category == CATEGORY_BOMB ? Collections.emptySet() : skipAfterBomb;
    GJMeleeCategorySettlePlan plan = buildCategoryPlan(category, typeResultMap, posTeamMap,
            skipPos, baseScore);
}

暴击 10 倍,同大类分出胜负就算,不要求 cardType 枚举相同。助力按大类,弱者也算,倍数 5 × 人数,多赢部分的 10% 分给助力者。广播拆成 Category → Winner → DuelSettle,间隔走上面的 config。

同阵营三人携带归零认输,截断剩余结算步进,直接 SGJ_SetEnd。认输座本回合后面也不再进 chargeList。

机器人出牌:桌上的和外面的是两套

陪玩有两条路径。房内机器人/托管走服务端 GJMeleeRobotPlayPicker;外部进程 wangmaoqh_robotGjAi 听协议自己出。
在这里插入图片描述

房内:分回合策略,不是每把打最强

GJRoomSet 出牌阶段扫座位。机器人第一次进本回合会随机挂 1~6 秒,到点再 autoSubmit。人类超时打最小单张,机器人和托管打策略选牌。

if (setPos.getRoomPos().isRobot()) {
    Long dueMillis = this.roundRobotPlayDueMillis.get(i);
    if (dueMillis == null) {
        this.roundRobotPlayDueMillis.put(i, now + CommMath.randomInt(
                GJMeleeStateMachineConfig.ROBOT_PLAY_DELAY_MIN_MS,
                GJMeleeStateMachineConfig.ROBOT_PLAY_DELAY_MAX_MS));
        continue;
    }
    if (now < dueMillis) {
        continue;
    }
}
this.autoSubmitRoundPlayBestForPos(i);

真正选牌在 pickRoundPlay。入口按回合切开,带上明牌、阵营、R1 是否持王:

public static List<Integer> pickRoundPlay(List<Integer> handCards, int curRound,
                                          Map<Integer, List<Integer>> brightCardsByPos,
                                          int selfPos, Map<Integer, Integer> posTeamMap,
                                          Map<Integer, Boolean> hadKingInRound1ByPos) {
    PlayContext ctx = new PlayContext(curRound, brightCardsByPos, selfPos, posTeamMap,
            hadKingInRound1ByPos);
    if (curRound == 1) {
        return pickRound1Play(handCards);
    }
    if (curRound == 4) {
        return pickRound4Play(handCards, ctx);
    }
    if (curRound == 5) {
        return pickRound5Play(handCards, ctx);
    }
    if (curRound >= 3) {
        return pickRound3Play(handCards, ctx);
    }
    return pickBestPlay(handCards, ctx);
}

R1 故意不强出。10 以上的顺骨架、对子、炸弹和王留下,其余扔掉。绝对不出王。一手全是该留的,才打一张伤害最小的非王。这是给后面凑级炸留种子,和发牌侧「R1 有王 → 后面 80% 走级炸路线」对上。

R3 顺子不到 8 张不出。最强若是单牌,改优先连刻 / 连对 / 8 顺。

R4、R5 会看敌营级炸威胁。明牌有王 +40,R1 持过王 +30,明牌同点够级牌 ≥2 张 +20。威胁 ≥60 就避炸弹,改出非炸最优:

if (ctx.enemyBombThreat >= ENEMY_BOMB_THREAT_THRESHOLD) {
    List<Integer> nonBomb = pickBestNonBomb(handCards, ctx);
    if (CollectionUtils.isNotEmpty(nonBomb)) {
        return nonBomb;
    }
}

威胁不高时,R4 有普通炸弹 85% 打出去,否则 fallback 四连对 / 三连刻 / 7 顺。R5 能成型级炸就打级炸,否则最强结构。

候选来自 listFormableCardTypes。比的不是纯牌型大小,是预览分 点数和 × 倍数,同分再 GJMeleeCardTypeCompare。结构牌确定后还会附带安全散牌抬 pointSum,附带前试一次 analyze,主牌型变了就丢掉。王在凑级炸路上会留着,不往王炸里提前扔。手牌 ≥3 张同点也不会拆散去凑别的。

外部 GjAi:对齐状态机,别在 FaPai 就出牌

外部机器人以前用 contains 匹配事件,GoldCarryChoiceNotify 会被误当成出牌相关;发牌一到就出牌,Playing 还没到。这次改成精确匹配,RoundDeal 只确认,等 SGJ_ChangeStatus(Playing) 再出。别人 PlayCardDone 到了自己还没出,当作 ChangeStatus 丢了,补一刀。

if ("Playing".equalsIgnoreCase(state)) {
    inPlayingPhase = true;
    schedulePlayCardIfNeeded(RandomUtils.nextInt(1, 4));
    return;
}

出牌决策在 GjOutLogic.decideRoundPlay。优先用手牌里还能对上的 cardTypeList 最强项(和 picker 目标一致);列表空了才本地回退:王组、同点最大组、最小单张。GjCardType 对齐 GJ_MELEE_CARD_TYPE 的 1~21。明牌按阵营过滤,阵营未知时按座位奇偶,和 assignTeamsBySeat 一致。

携带归零不抢着发 C2S。服务端对机器人已经 50% 补充 / 认输,GjAi 只同步停手,避免 pending 清了再报错。金币场准备 carryGold=0,桌内分由服务端按 minCarry/maxCarry 随机写,不扣账户。

melee/ 把发牌、识别、结算、出牌策略、时序拆开了。房间负责步进和协议,算法尽量无状态。发牌第 5 回合会调用出牌器,出牌器又调用识别器,三条线共用 compareKey / 倍数,不然机器人打出来的和结算对不上。

外部 AI 没有嵌服务端 picker(不在一个进程),所以用 cardTypeList 对齐牌力,本地只做兜底。cardTypeList 后来从发牌包里拿掉、改成查询预览之后,外部这条会更多走回退,这块后面还得把 cgjquerycardtype 接上,否则陪玩和房内机器人不是同一套脑。

单测主要铺在 DealEngine、RobotPlayPicker、CardTypeAnalyzer、状态机间隔上。对决节奏、留牌补顺、威胁避炸这类东西,改权重比改流程便宜,所以常量和步进都尽量集中。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐