Block Blast 如何用服务器下发规则按广告价格发牌
How Block Blast harvests its players for ad money
作者逆向拆解 Block Blast 安卓 10.8.2 与 iOS 7.4.9 版本,发现游戏启动后从服务器下载加密 JavaScript 规则,其中 RewardHighValueAdHarvestTrait 在玩家连续看完三次激励广告、当前加载广告价值达到上一次的 3.5 倍、且分数超过历史最高分 1000 分时,请求生成无解的致死题。
作者逆向拆解 Block Blast 的远程下发规则,展示了广告价格如何与发牌逻辑直接挂钩。
从他们服务器下载的规则决定了你的方块、你的分数和你的广告。它会等待一个昂贵的广告,然后向你索要一个你无法放置的方块。
Ayo Omolewa
2026年10月15日
我发布了一段关于这款游戏的视频,获得了超过1500万次观看,所以我想做一份完整的技术说明:代码,以及你如何自己复现它。
关于它为何重要的背景:2026年1月至5月期间,Block Blast 从广告中赚取了约1.27亿美元。同期 Candy Crush Saga 赚取了8110万美元(Sensor Tower 估算)。
Block Blast 的大部分内容并不在你安装的应用里
我们从 Android 应用版本 10.8.2 开始。我们用 jadx 反编译了 Java 部分,用 Ghidra 反编译了原生 Cocos 库,结果在你从商店下载的安装包里几乎没发现什么有趣的东西。Block Blast 由代码中称为 trait 的小模块构建而成,每个 trait 都有称为 arm 的变体。在一个没有账号的干净 Android 14 模拟器上运行它,并观察其缓存和 logcat,揭示了它的工作方式:应用启动时,服务器会发送一份配置,说明你获得哪个 trait 的哪个 arm,然后应用将它们作为加密的 JavaScript 下载下来。我这次安装获得的配置列出了约700个 trait。我的安装选择了其中254个,有101个是在安装后下载的。
CTAgloReviveTraitCTReviveTimeOutSkipSplashTraitClassNewDeathOptimizeTraitDeadlyToHardTraitDecreaseReviveUIShowByLimitHighScoreTraitDifferentStrategyDieTraitEndlessDeathBoardTraitFastHandPunishTraitGLBFDieReplayTraitIsCacheReviveNumTraitIsClassADReviveNoInerestingTraitIsOpenInAdvanceDeathByLowEfficiencyGiveReviveTraitIsOpenRandomToRandomNoDieTraitLevelReviveCondiTraitLevelReviveCountTraitLevelReviveTraitMABXiBaiTraitMultiClearBoostThenKillTraitRepateCPlusPlusRandomNoDeathTraitReviveBuffTraitReviveCleanBoardTraitReviveCountTraitReviveLimitTraitRewardHighValueAdHarvestTraitAdLevelTraitAdPlusMinusStrategyTraitAddClearTipsExTraitAddMoreGameTraitAdjustPercentChTraitAlgoFillSortEdgeTraitAlgoSDK_modifyBlockPosTraitAlgoSDK_useAlgoSoAloneTraitAlgoSameBlockShangZengTraitAlgoSymmetryBlockTraitBComboScoreChangeTraitBCommentDisplayTraitBPuzzleTimeTraitBReplaceBinaryTraitBackSpaceClickLimitTraitBaseplateConditionDiffTraitBaseplateLaneTraitBaseplateTraitBlockLimitClassicsTraitBlockLimitLevelTraitBlockPoolRoundAdjustV2TraitBlockPoolStrategyExperimentTraitButtonClickAddShakeTraitCTAdvertisementMultipleCallTraitCTAdvertisementNoHideTraitCTAgloNameSpecialTraitCTAgloNameTraitCTAgloStrategyTraitCTBAlgorithmGaohuibaoARGTraitCTChapterScoreComboTraitCTChapterSkinBonesTraitCTChapterWinOverTraitCTClassSkinBonesTraitCTComboDotTraitCTCommentskinWithFeedBackTraitCTDotClickGameEndInfoTraitCTDotClickUsrDataGameEndTraitCTDotDeadReportToUsrDataGameEndTraitCTDotFirstClickDataGameEndTraitCTEffectAccTraitTraitCTGBMClearTraitCTGBMEmptySurfaceGameStartDotTraitCTGameComboScoreTraitCTGameEndStatusDotTraitCTLowDeviceEffectTraitCTMergeBlocksAgloNameTraitCTMergeBlocksSkinBonesTraitCTSkinBonesTraitChapterAlgoStrategyTraitChapterCollectScoreFixedTraitChapterWinShowRainbowTraitClassGuideEndNoAwaitNextFrameTraitClassicsRandomReplaceTraitCleanSceneRandomSkinTraitCleanSceneUseSequenceSkinTraitClearBoardHeartRippleAniTraitClearBoardPlusBlockPosTraitClearBoardPlusTraitComboAddRemoveNumTraitComboModeCtrlTraitComboRainbowTraitComboScoreSplitTraitComboTimeTraitCombo_tips_lv_blockTraitComboflyScoreTraitConsecutiveLossRecoveryTraitContinueSameMoreRoundLimitTraitControlHighestScoreRhythmTraitCrossClearHardTraitCtrlAlgoLimitTimeTraitDailyPerformanceReviewTraitDailyWinStreakStateTraitDifficultChangeColorTraitEightHoursTravelTraitExceedAimScoreTraitExpendBoardScoreTraitFastNearTkxcClassTraitFastNearTkxcTraitFeatclearscreenbombTraitFeatclearscreenbombfixTraitFillCleanAlgoTraitFillExcludeTraitFirstGameScoreTraitFirstHardMoreClearAlgoTraitFiveRoundStatisticsTraitGLBFMiddleTipsViewTraitGLBFProgressTraitGLGameAdStrategyOptimizeTraitGLResultView6TraitGLShowAdsByGameTimeTraitGameInfoDisplayTraitGameLobbyNewEntryCtrlTraitGameLobbyTraitGameLobbyUnlockByBreakHighScoreTraitGameOverShowMiniGameIconTraitGateWinStreakStateTraitGpEmptyBoardRatioGuardTraitHighProgressShowBannerTraitHighScoreBreakthroughBoostTraitHighScoreLimitByUserAbilityTraitHighValueInterstitialReliefTraitHighWeightSessionQuestionReplacerTraitHoleFillPriorityTraitHotAreaAdaptScreenBottomStateTraitHotAreaClickUtilTraitInitBoardDelayTraitInitBoardSnakeProStyleProTraitInitialBoardDisableTraitIsOpenBottomCoverChangeTraitIsOpenChangeSkinTraitIsOpenClassEncourageEffectByScoreTraitIsOpenComboContinuousTraitIsOpenComboInterruptionStrategyReplaceBlockTraitIsOpenExtraReportTraitIsOpenGLHallMoreGamesPopupTraitIsOpenGlLoadAllGameBackTraitIsOpenLevelOverTipsChangeTraitIsOpenMoreCleanChangeTraitIsOpenMoreCleanTraitIsOpenNoVideoNoRevivieTraitIsOpenOperaPosTraitIsOpenRainbowPutBlocksEffectTraitIsOpenRightPutTraitIsOpenShang1ToShang3TraitIsOpenVibrateTraitIsOpenZhiJueToKunNanTraitIsOpenZiMuShangZengTraitIsPuzzleTimeTraitIsShowAchievementTraitIsStatisticalDataCollectTraitIsTravelMoreDiamondAwardTraitIsTurnAroundSurfaceTraitIsWarmhandsTraitJewelNewOutBlockTraitLDEnableNoSyncHighScoreRollingTraitLWMDualConditionHardPuzzleTraitLaneSchemeTraitLevelConfigTraitLevelExpAlgorithmTraitLevelLoopTraitLevelNoThemeTraitLevelRandomReplaceTraitLevelRemoteThemeTraitLevelWayHelpConfigTraitLevelWayHelpTraitLimitHighComboTraitLimitNoviceBlockSizeTraitLineDynamicSendTraitLoadingTieringTraitLocalThemeSaveTraitLoginTimeFirstDifficultyRatioTraitLongThinkClearAssistTraitLowFeedbackPleasurableOfferTraitLowHardSetFps30TraitLowWeightFillBlockSortAdjustAlgoTraitLowWeightFillBlockSortTraitMaturityDevelopmentV2TraitMergeBlocksIsPuzzleTimeTraitMergeBlocksLaneSchemeTraitMoreClearStopComboTraitMoreTimeToOpreateStateTraitMultiElementCollectTraitNewAbilityModelTraitNewBottomOfferTraitNewPlayerContinuePlayIncentiveTraitNewSeasonNotifyTraitNewStructTraitNewUserLevelTraitNew_player_direct_guide_to_thirdTraitNewbieScoreRegulationTraitNoBannerAdNewUserByDaysTraitOpenChangeContinueHardTraitOpenComboModelTraitOpenLevelModelTraitOriginSkinShowGreyDefultSetBtnTraitOverHighScoreClearBoardTraitParallelClearHardTraitParam230012TraitPixelBitControllerTraitPlaceBackAdaptTraitPlaceHabitAdaptPathTraitPlaytimeNoInterstitial1TraitPostGameToServerTraitPreClearScreenAnimOptimizeTraitProblemIntervalTimeRandomBetaTraitPromotionMoreGamesInBlockCtrlTraitPutBlockOptimizeTraitPuzzle100TraitPuzzle100WeightConditionTraitPuzzleAfterTimingTraitPuzzleBeforeTimingTraitPuzzleConditionDiffTraitPuzzleFail100TraitPuzzleSuccess100TraitPuzzleTiming100TraitPuzzleTimingMaxPriorityTraitRatioAdjustTraitRatioDownTraitReduceTravelSettlementTraitReplaceBoardTraitReplaceClearnScreenAniTraitRightPutHintCtrTraitRobotModelEventDataTraitSequencePredictDynamicDifficultyTraitServerControl_board_init_v2TraitSetAddDefultSkinBtnTraitSettleFireworkTraitSetupNewTraitShangZengAddRandomTraitShangzeng3ReplaceZimushangzengTraitShowSSSWhenSkinChangeTraitShowTravellClearAnimaTraitSimpleMultiSolutionTuning2TraitSkinChangeAblePushTraitSkinPreEliminateGoodPerformanceTraitSkinRandomTraitSkinThree24NewColorsTraitSkinThreeOptimizationsTraitSkinThreeStageTraitSlowPlayerAnimTraitSmallgameLocalResTraitSolidColorTraitTerminateRefreshScoreTraitTheMoreEliminateTheMoreGoodEffectTraitTravelEntryClockShadowFixTraitTravelEntryClockShadowTraitTravelHappyOverTraitTravelNoSkinTraitTravelResultOptimizingTraitTravelScoreCutoffByProgressTrait
因此,两个并排游玩的玩家可能运行着不同的游戏,而工作室无需更新应用就能改变你的游戏。这 254 个特性中大多数是普通的:教程、动画、音效。少数与失败时机有关。其中一个与广告有关。
等待昂贵广告的规则
它被称为 RewardHighValueAdHarvestTrait。通过找到游戏在其原生库中解锁这些文件的位置,我们将其解码为 15,616 字节的纯压缩 JavaScript。其构造函数设置了参数:
t.HIGH_PRICE_MULTIPLIER = 3.5;
t.MEDIUM_PRICE_MULTIPLIER = 1.5;
t.MIN_HISTORY_AD_WATCH_COUNT = 3;
t.RECENT_AD_NO_ESCAPE_COUNT = 3;
t.TARGET_AD_LEVEL = "high";
它等待三个条件同时满足:你完成了最近三个激励广告,当前加载的广告价值至少是你上一个广告的 3.5 倍,并且你的分数超过最高分 1,000 分。这三个条件都在一个函数中检查。以下是该函数,其中压缩的空值检查已改写为可选链;逻辑相同:
shouldTriggerDeath() {
if (!this.canTrigger()) return false; // finished your last 3 rewarded ads
const d = this.storageData;
return d.currentGameNum === hs.classGameInfo?.gameNum // same game
&& !d.currentGameHasTriggeredDeath // hasn't fired yet
&& !(hs.classScoreInfo?.score < this.currentScoreThreshold) // past high score + 1,000
&& !!this.isTargetAdReady(); // loaded ad ≥ 3.5× the last one
}
以下是它如何检查每个条件。
1. 你完成了最近三个激励广告。 每次激励广告开始时,规则会将 false 推入一个列表并保留最新的三个(第 174–183 行)。如果广告 SDK 报告奖励已获得,它会将该条目翻转为 true(191–199)。检查要求所有三个都恰好为 true:
t.prototype.canTrigger = function() {
var e = this.storageData;
if (e.totalRewardShowCount < this.MIN_HISTORY_AD_WATCH_COUNT) return !1;
var t = e.recentRewardResults.slice(0, this.RECENT_AD_NO_ESCAPE_COUNT);
return !(t.length < this.RECENT_AD_NO_ESCAPE_COUNT) && t.every(function(e) {
return !0 === e;
});
};
该标志来自 Java 层中 AppLovin MAX 的 onUserRewarded 回调,因此你提前关闭的广告会保持为 false。
2. 当前加载的广告价值至少是你上一个广告的 3.5 倍。 规则使用 NativeAd.callNativeGetMaxEcpmNumber("2") 读取当前加载的最佳激励广告的价值(第 304 行)。在 Java 层,该调用会遍历每个就绪的激励广告位,取 AppLovin MAX 报告的收入,乘以 1,000,并返回最高值。你上一个激励广告的价值也乘以 1,000(第 310 行),因此比较是同类比较:
t.prototype.classifyLoadedRewardLevel = function(e) {
var t = this.storageData.lastRewardShowEcpm;
return !Number.isFinite(t) || t <= 0 ? "none" : e >= t * this.HIGH_PRICE_MULTIPLIER ? "high" : e >= t * this.MEDIUM_PRICE_MULTIPLIER ? "medium" : "none";
};
3. 你超过了你之前的最高分。 每局游戏开始时,规则会保存你的最高分(第 145 行)。门槛是该最高分加上 scoreDeltaStep(第 107–110 行)。每个玩家有一个分支:我的分支 332243014 使用 1,000,而应用内置配置中其他分支使用 500 和 750。如果你在规则触发后复活,门槛会再提高一步,因此它可以在同一局游戏中再次触发。
然后它请求致命谜题。 在发给你下一组棋子之前,游戏会按顺序运行一系列“底部报价”规则。当此规则符合条件时,它会将列表替换为单个生成器:
t.prototype.onBottomOfferAlgo = function() {
if (this.shouldTriggerDeath()) {
hs.algorithmBottomSequenceInfo.setAlgorithmBottomList([ this.deathAlgorithm ]);
this.storageData.currentGameHasTriggeredDeath = !0;
this.saveStorageData();
}
};
deathAlgorithm 返回 hs.OFFER_TYPE.ZHI_SI_TI(第 102 行)。游戏将该请求发送到其谜题引擎,并发出它返回的三个棋子 ID(主脚本第 45062–45121 行和第 15074–15123 行)。如果引擎未能找到一组,规则会重置并在下一次发牌时重试。
为了观察它的决策过程,我们运行了下载的文件本身,未经修改,并使用测试广告和分数值。它恰好在所有三个条件都成立时请求致命谜题:28 个案例,包括两个广告而非三个、一个广告提前关闭、分数差 1 分,以及广告价值略低于 3.5 倍。
随后我们在 Apple 模拟器中运行的 iPhone 应用测试副本里对它进行了实机测试,并在游玩时读取该规则的输入。记录中有三个不错的激励广告。分数已经越过界线。一条来自 Google AdMob 的激励广告已加载就绪。该规则将其定价为 72.39,而上一条为 37.41,是它的 1.94 倍。它需要 3.5 倍。因此在 iPhone 上,该规则已生效,正在读取真实广告价格,并等待一条价值为你上一条 3.5 倍的广告。
致命谜题
该名称在主脚本中经过三张表映射:
23878 e[e.ZHI_SI_TI = 9] = "ZHI_SI_TI";
16092 … r[n.OFFER_TYPE.ZHI_SI_TI] = i.AlgoClassTypeConst.algo_must_death, …
258441 e[e.algo_must_death = 24001] = "algo_must_death";
算法 24001 存在两份:一份在 JavaScript 中,另一份在游戏的原生 C++ 库中,那里的函数名为 func_death_blcok(原文如此)。它们执行相同的搜索。以下是 JavaScript 核心部分:
for (var d = 0; d < n.length; d++) if (41 != d) {
var m = n[d];
r.canPut(m) ? o.push(m) : i.push(m); // o: fits now, i: doesn't fit now
}
…
iC(r, o[p], o[f], i[_], h); // try every order of two pieces that fit
iC(r, o[p], i[_], o[f], h); // and one that doesn't, clearing lines
iC(r, o[f], o[p], i[_], h); // after each placement
iC(r, o[f], i[_], o[p], h);
if (0 == h.length) { // no order worked: take this set
u = [ o[p], o[f], i[_] ];
…
a.names = [ "致死题", "致死题", "致死题" ]; // "lethal puzzle" ×3
它寻找两块当下能放下的方块和一块放不下的方块,并且只有在没有任何顺序、没有任何位置组合能放下全部三块时才保留该组合,同时计入沿途会消除的每一行。三块中有两块在发牌时就能放下,所以这手牌在你亲自尝试之前看起来并不像不可能完成。
下面是在你的浏览器中运行游戏自身代码的效果。逐步查看:
第 1 步,共 5 步
一手普通牌
按“New random board”生成一个全新的中局棋盘,游戏代码会为它挑选一手新牌。在证据包中的测试棋盘上,游戏的生成器将一块竖直多米诺、一块水平多米诺和一个 3×3 方块接受为一手致命牌:我们尝试了全部 171 种摆放方式,没有一种能完成它。在空棋盘上,同样的三块方块在每种顺序下都有 67,680 种摆放方式。
要求它的规则不止广告规则
致命谜题是一个生成器,任何规则都可以请求它。广告规则只是其中一个调用方。当我们在 iPhone 上实机测试它时,第二个调用方在镜头前触发了:PersonalMarginBreakTrait。一旦你超过最高分 10%,它就会在每次发牌时掷骰,并有 20% 的概率请求同一个致命谜题:
t.prototype.onAlgorithmStrategyPriority = function() {
this.hasReplacedThisGame = !1;
if (this.state.isFirstGame) ; else if (this.isReviveRound()) ; else if (this.state.isBreakHighScore && this.isCurrentScoreOverRecordPercent()) if (100 * Math.random() >= this.prob) ; else {
var e = this.replaceAlgoId; // 9, the lethal puzzle
…
if (this.callNewAlgorithm(e)) ; else {
我们的纪录是 1,304,所以它的界线是 1,434。在 3,356 分时它掷骰了,游戏发出了一个五格长的横条、一个五格高的竖条和一个锯齿形。游戏自身对该次发牌的记录指明此规则为调用方,显示算法 9 既被请求也被使用,并将三块方块全部标记为致死题,lethal puzzle。它们无法被放置。两次独立的穷举搜索结论一致:零。该规则、线上设置和这次发牌都在 iPhone 实机包 中。
决定你在哪里输掉的模型
到目前为止的规则都是手写的。游戏还会下载训练好的模型并在你的手机上运行,其中一个模型决定致命谜题在关卡中的出现位置。
这是一个用于游戏关卡模式(代码中称为 Chapter)的 500 棵树 XGBoost 分类器。其文件名为 predict_travel_difficulty_dead,它的 52 个输入大多关于你一直以来的表现:你当前的关卡、你尝试过它的次数、你本周完成的游戏数,以及你拥有该应用的时间。游戏向它提供 12 个选项,即一个名为 sw_process 的设置的四个值乘以三个难度等级,并保留模型评分最高的那个:
if (0 == e) for (var i = 1; i <= 4; i++) for (var l = 1; l <= 3; l++) {
var c = this.getInputFeature(this._jsonModel.learner.feature_names, i, l);
if ((u = this.xgb_clf_predict(c, …)) > n) { // keep the highest-scoring option
n = u;
a = i;
o = l;
}
}
…
storage.setItem("DieModel_last_predict_info", { swProcess: a, travelLvDifficulty: o, pre: n });
另一条规则会把这次选择映射到关卡中的某一段:前 30%、30% 到 80%,或最后 20%。当你到达那一段时,如果你通常接受复活(超过 75% 的情况下),它会给你一次额外复活。然后,从那次之后的第四次发牌开始,它会要求你解出致命谜题:
var t = hs.chapterConfigInfo.getChapterProgress(), i = e.getLastPredictInfo(this.traitName), r = i.swProcess, …;
…
1 == r ? s = [ 0, .3 ] : 2 == r ? s = [ .3, .8 ] : 3 == r ? s = [ .8, 1 ] : 4 == r && (s = [ 1, -1 ]);
return t < s[0] ? a.First : t <= s[1] ? a.Second : a.Third; // before, inside or after that stretch
…
if ((n = this.saveData).needZs && n.zsRoundNum > 3) {
hs.algorithmStrategyInfo.setAlgorithmPriorityList([ hs.OFFER_TYPE.ZHI_SI_TI ]); // the lethal puzzle
hs.algorithmStrategyInfo.setAlgorithmFailList([ hs.OFFER_TYPE.SHANG_ZENG_3 ]);
}
其他规则对同一次选择作相反解读,当它落在第四个选项上时会让关卡变简单。
给你定价的模型
第二个模型给你定价。游戏从服务器下载 xgboost_regression_model111.json,并在你的手机上运行它。
这是一个 XGBoost 回归模型:59 棵决策树,最深 8 层,27 个输入。其中大部分与钱有关:18 个输入涉及游戏今天和昨天为你加载的广告。其余输入是你的位置、你的手机、你安装的时间,以及你的第一局游戏表现如何。这些树最依赖你的首次得分、你最近一条广告的价格、你的 RAM、你的手机型号和你的国家。
代码把它的答案称为 finalEcpm。eCPM 是广告行业对一千次广告价值的称呼,也是游戏存储广告价格所用的同一单位。所以这个模型是在估算你在广告上值多少钱。
GPT-6 Astra 移植了游戏的输入代码和树遍历逻辑,移植版复现了游戏自身的输出,精确到最后一位小数。我们用一个虚构的美国新玩家运行它,首次得分为 1,000,一次是在从 App Store 安装的 iPhone 15 上,一次是在 Android 上,然后一次只改变一个输入:
| 只改变这一项 | 广告价值,iPhone 15 | 广告价值,Android |
|---|---|---|
| 什么都不改(美国,首次得分 1,000) | 25.48 | 48.96 |
| 改为住在巴西 | 8.08 | 17.90 |
| 改为住在印度 | 5.59 | 14.83 |
| 首次得分 500 | 25.79 | 53.57 |
| 首次得分 2,000 | 21.12 | 52.64 |
| 首次得分 10,000 | 20.34 | 58.26 |
把同一个玩家从美国移到巴西,其价值会降到约三分之一,移到印度则降得更多。你的第一局游戏也有影响,但不是线性的:在 iPhone 上,首次得分 500 时你的价值高于 2,000 或 10,000。
游戏用这样的价格来决定你获得哪种广告以及多少广告。最清楚的例子是向服务器请求两个针对你的价格,一个用于全屏广告,一个用于激励广告,然后进行比较:
t.prototype.checkEcpm = function() {
var e, t, o, r = this.saveData.interEcpm, i = this.saveData.rewardEcpm; // your full-screen and rewarded prices
…
return (…isReverse) ? r <= i * (…multiple) : i >= r * (…multiple);
};
在应用发布的版本中,如果你的激励广告价值至少是全屏广告的两倍(其他版本中是 1.5 倍或 2.5 倍),你就会获得一次额外复活机会,而复活会播放一条激励广告。如果全屏广告对你更值钱,你就不会获得。
其他规则也以同样方式运作。FullScreenAdAddByEcpmTrait 在你的价格高时增加一条额外全屏广告。IsOpenEcpmTooLowNoInterTrait 在你几乎不值钱时跳过全屏广告。还有几条规则会在你的激励广告足够值钱时开放一次提前复活或第三次复活。
你可以自己试试手机上的模型:
25.48
你的广告价值
每 1,000 次广告(eCPM)
finalEcpm 25.47779944720001 0/59 棵树移动
这是游戏中遍历树的那部分。它是一个普通循环:从根开始,在每个输入上向左或向右,累加叶子权重。
for (var m = parseFloat(a + ""), u = 0; u < t.length; u++) { // m starts at the base score; t is the 59 trees
for (var p = t[u], f = 0, _ = 0; _ < o; ) {
_ += 1;
var h = p.left_children[f], g = p.right_children[f], y = p.split_conditions[f], C = p.default_left[f], v = s[e[p.split_indices[f]]], T = -1;
(null === v && 1 === C || v < y) && (T = h);
(null === v && 1 !== C || v >= y) && (T = g);
if (-1 === T) {
m += parseFloat(p.base_weights[f].toString()); // reached a leaf: add its weight
break;
}
f = T;
}
if (_ >= o) throw new Error("[EcpmFeature] 树的遍历超过最大深度,可能存在死循环。");
// "Tree traversal exceeded max depth, possible infinite loop."
}
一个猜测你明天会玩得更少的模型
还有一个包含 93 个输入的 1,000 棵树 LightGBM 模型,用于预测你明天是否会更不活跃。它最依赖你拥有该应用的时间、你的最高分、你的 RAM 和你的平均分。如果它给你的评分达到 0.69 或以上,一条配套规则会换上一个粉色复活按钮,并且复活会跳过广告:
return this.state.predictionScore >= this._lowActivityThreshold; // 0.69
…
if (hs.tp.isClassAdvertisement_RewardProxyShieldPlayAdvertisement(e) || …) {
var r = cc.isValid(this._pinkReviveNode);
if (!this.isTrigger || !r) return;
e.returnValue = !0;
e.args[0] = !0; // shield the ad: the revive goes through without one
this.saveNextDayLowActivityReviveTime(Date.now());
}
它每天生效一次。
他们服务器上的模型
有些规则会把你的数据发出去,并根据返回的分数采取行动,这些分数来自服务器模型,名字诸如 churn、LTV、revive-click 和 xDeepFM。它们的权重留在服务器上,所以我们只能展示手机发送了什么。其中发往 /v1/ecpm_prediction 的一个请求会发送 40 个字段,包括你的运营商、存储空间、当前小时,以及你近期广告价格的统计数据。
其他影响你游戏的规则
广告规则并不是唯一会改变你的方块或分数的规则。我们在 iPhone 上抓到一个正在实时运行的规则。MaturityDevelopmentV2Trait 会给每个会话打一个由三部分组成的“成熟度”分数:习惯(10.98)、技巧(14.66)和你的广告历史(19.27)。我们的加起来是 44.91,低于它的阈值 65,于是它把本该发给你的部分困难谜题换成了简单填充。另一个规则 FailStreakStrategyTrait 在我们连续两局低于我们的纪录后介入,要求更简单的方块。
这些是在 Android 上追踪广告规则时发现的,每一条在我的研究文件里都有同类证据。
| 规则 | 代码做了什么 |
|---|---|
MultiClearBoostThenKillTrait | 把你的用户 ID 发送到两个服务器端点,并取回两个标签。代码把这两个层级都命名为“怼死不敏感”。对于这些层级,它会等待一次大消除,然后在游戏持续到大约你平常一局的时间后,过几轮再请求那个致命谜题。 |
MABXiBaiTrait | 一个六臂实验,测试致命谜题可以多快被请求:从不,或在 4、6、10、约 17 或 30 分钟后。对每个实验臂,它都会报告你是否在 15 分钟内开始了新一局,以及你接受了多大比例的复活提议。 |
FastHandPunishTrait | 当你最近六组方块平均每组用时低于 4.2 秒,且你的分数已超过最高分的 93% 时,请求困难方块。 |
AdPlusMinusStrategyTrait | 在一种额外复活提议之后,给你的当前分数加上一个随机奖励,最高为你最高分的 10%。 |
人们问的问题
iPhone 上也是一样吗?
是的。我们也拆解了 iPhone 版 Block Blast,版本 7.4.9,并由第二次 GPT-6 Astra 运行从头核对了这些发现。它从自己的更新服务器下载同一条规则,除了有一行不同,文件与 Android 版一致。其他一切都相同:三次广告、3.5×、分数条,以及同一个致命谜题生成器。
iPhone 应用还列出了这条规则的第二个版本 RewardHighValueAdHarvestV2Trait,在那个版本里,广告历史检查可以被关闭:
t.prototype.canTrigger = function() {
var e = this.storageData, t = this.viewCountThreshold;
if (e.totalRewardShowCount < t) return !1;
if (!this.requireRecentNoEscape) return !0;
var r = e.recentRewardResults.slice(0, t);
return !(r.length < t) && r.every(function(e) {
return !0 === e;
});
};
该应用随包发布了它的 12 个版本。其中一半关闭了那项检查,并且它们需要 1 到 3 次广告,所以在其中一些版本里,你提前关闭的一条激励广告就足够了。价格门槛取决于你拥有该应用的时间:第一周内,该会话加载的第一条广告按 3.2× 计算;到第 30 天按 2.7× 计算;之后按你上一条广告的 3× 计算。
如果我没有 wifi 玩会怎样?
这条规则下载后会保存在你的手机上,所以离线时仍会运行。但它只在加载激励广告时触发,而广告是通过互联网加载的,所以一旦没有加载任何广告,它就无法触发。其他规则会继续运行。在我的 Android 安装中,那条在你接近纪录且玩得很快时请求困难方块的规则完全在手机上运行。那条向服务器询问哪些玩家“不介意被强制死亡”的规则在离线时得不到答案,所以该会话中它保持关闭。
广告拦截器有用吗?
这取决于拦截器的类型。浏览器广告拦截器(无论 iPhone 还是 Android)只在浏览器内生效,因此不会影响游戏。而覆盖整部手机的拦截器,比如 Android 上的 Private DNS,或任一平台上基于 DNS 或 VPN 的拦截器,会阻止激励广告加载,于是这条规则就无法触发,原因与离线游玩相同。不使用广告的规则仍会继续运行:快速游玩规则、对致命谜题可被请求时机进行计时的实验,以及——由于你仍在线——向服务器询问你情况的规则。
这就是为什么我总是在打破最高分后立刻输掉吗?
这正是这些规则的目标所在。我们抓到 PersonalMarginBreakTrait 在实机运行:一旦你超过自己的纪录 10%,每次发牌都有 20% 的概率是致命谜题。广告规则则等到你超过纪录 1,000 分(其他版本为 500 或 750 分)时触发,并且在复活后它会提高门槛,还能再次触发。
我能避开它吗?
基本不能。在 Android 上,原始规则要求你最近三次激励广告都成功发放奖励,所以提前关闭一个广告确实会暂时关闭该规则,直到你再次连续三次成功看完广告。但在 iPhone 上,V2 版本可以完全跳过这项检查,在单个广告后就会触发,哪怕是你关闭的广告。而其他请求致命谜题的规则,比如计时实验和等待大消除的那条,根本不看你的广告。唯一能在所有平台上阻止这条规则的办法,就是没有加载好的激励广告,也就是离线或使用覆盖整部手机的广告拦截器时的情况。
这不就是自适应难度吗?
自适应难度会根据你的游玩方式改变方块。这条规则并不看你放置方块的水平。它的输入是你近期的广告结果、内存中广告的价格,以及你相对于纪录的分数。而它请求的生成器并不是更难的方块组合,而是一组无论怎么放置都无解的方块。
这允许吗?Apple 不审核应用吗?
Apple 审核的是你从商店下载的应用,而这条规则并不在其中。应用在你安装后从游戏服务器下载该规则,由服务器决定哪些玩家获得哪个版本。Apple 确实允许应用下载这样的 JavaScript 更新,Expo Updates 等工具就是这样工作的,只要更新不改变应用的本质。至于根据广告价格改变你的方块是否符合这一条件,由 Apple 判断。
为什么它在手机上运行而不是在他们的服务器上?
服务器确实决定谁获得该规则以及哪个版本。但决策本身需要只有你手机在那一刻才知道的信息:你的棋盘、它即将发出的下一组方块,以及当前已加载的广告。所以这部分在手机上运行,这也是它可能被拆解分析的原因。
我如何知道这是真的
- 应用。Android:来自 APKPure 的 10.8.2 XAPK,下载于 2026 年 10 月 11 日。其所有拆分 APK 均通过签名验证,版本与 APKMirror 上的公开列表一致。iOS:来自公共镜像的 7.4.9 IPA。其 4,670 个游戏资源(包括主脚本和目录)均通过 Apple 代码签名验证。
- 这些规则来自它们的服务器。Android 应用的缓存显示,它从
bband1.afafb.com下载了该规则,而这也是它获取所有 traits 的同一主机。我们单独抓取了该 URL,文件与应用的缓存副本逐字节一致。iOS 应用的签名目录列出了两条 iOS 规则,应用从bbios1.afafb.com获取它们。截至 10 月 15 日,三个 URL 仍提供相同的文件,因此你可以自行核查(命令见下文)。 - 它被分配并加载了。在一个没有账号的全新 Android 14 模拟器上,UTC 时间 10 月 11 日 23:01:15,应用已准备好配置,为此规则选中了 arm 332243014,模块已加载且其存储已初始化,它所接入的 bottom-offer 流水线也已开启。
- 解码是精确的。这些 traits 使用 XXTEA 加密,并用 Brotli 压缩。解码后规则的哈希如下。对于原生库,我的离线解码与运行中应用的内存转储在 25,317,680 字节上完全匹配,零差异。
- Android 游戏本身没有任何改动。唯一的改动是一个渲染变通方案,以便游戏能在模拟器上绘制。没有编辑任何分数、广告、标志或配置值。
自行核查
共有三个证据包:Android(113 KB,SHA-256 bd55151e6a1527f628459c2fffa115d5b14a765a3fb5a3f729387fbac6f707c8)和 iOS(2.7 MB,SHA-256 02bdb0e0e9b02d77844efbcf4b82f48e17f2b688fc48cbf84f83e75ae289df7d),外加第三个用于实机 iPhone 抓取的证据包(270 KB,SHA-256 5f36863daabd2b500c8062d005f41e6c5e7dcea5a238c89e558e19a928dcc57f)。它们包含解码后的规则、加密的原始文件、每个应用其余部分的摘录及其原始行号、测试脚本和来源记录。你需要 Node.js 和 Python 3。
# the rule, run unmodified with fake inputs: 28 checks, prints "passed": true
node proof/verify_reward_harvest.js
# brute-force search of the test board: 0 solutions in every order
python3 proof/verify_unsolvable.py
# the game's own generator on that board: picks pieces [2, 3, 13]
node proof/verify_packaged_algorithm.js
# both iOS rules, the generator, two solvers and the code-signature check
python3 proof/run_all.py
curl -s https://bband1.afafb.com/update/block_android_online/remoteRes/0/remote/RewardHighValueAdHarvestTrait/index.700f5.js | shasum -a 256
# dec91aa4001f651a74bf101d512587b9a364cb71ea2a81a9cc9dbbcd2444b477
curl -s https://bbios1.afafb.com/update/block_ios_online/remoteRes/0/remote/RewardHighValueAdHarvestTrait/index.ff35e.js | shasum -a 256
# 61ac70045995f44b3c6bb22b2d062a386593ce6a632fbcec3863a51110850cc2
curl -s https://bbios1.afafb.com/update/block_ios_online/remoteRes/0/remote/RewardHighValueAdHarvestV2Trait/index.677f6.js | shasum -a 256
# e38953430980f4b6a10b29337ec9a41634bb732ee84dd5997bfe78507b26ee1e
| Android 基础 APK | a520fcc1d1ba1ff0c643e30b059c87a01fde650ec46be5c85fde5d815ec82447 |
|---|---|
| Android XAPK | 91988cf6e92cd0d346b869df0a8c6740f3a5fd8b722e21779e045426c7553015 |
| Android 规则,已解码 | f94a5d54d7f6ab0af097f408bda09b091e7767b17749f8ba828b41b638b330ad |
| Android 主脚本 | 8dce6532cafc25d982f2af3659647e0e0017c3902cbc31f872d375804dc3678c |
| iOS IPA | 7fc9b47e10dc8303f662e1c0ad88b93e7119e2846972eddb1a18bd205b54056a |
| iOS 规则,已解码 | 179bf50ae3c0c4a9bd5457281e4876faae728778f6181dbb0d3799b42ec70d61 |
绕过应用保护所需的密钥和步骤不在证据包中。如果你是记者或研究人员,想验证解码步骤,请给我发邮件。
来源:Hacker News · ayoships.com