Hoe Block Blast zijn spelers "oogst" voor advertentie-inkomsten

Ik heb een video over dit spel gepost die meer dan 15 miljoen weergaven kreeg, dus ik wilde een volledige technische analyse maken: de code en hoe je dit zelf kunt reproduceren.

Wat betreft de context: tussen januari en mei 2026 verdiende Block Blast naar schatting 127 miljoen dollar aan advertenties. Candy Crush Saga verdiende in dezelfde periode 81,1 miljoen dollar (schattingen van Sensor Tower).

Het grootste deel van Block Blast zit niet in de app die je installeert

We zijn begonnen met de Android-app, versie 10.8.2. We hebben de Java-zijde gedecompileerd met jadx en de native Cocos-bibliotheek met Ghidra, en we vonden bijna niets interessants in het pakket dat je uit de store downloadt. Block Blast is opgebouwd uit kleine modules die de code "traits" noemt, en elke trait heeft varianten die "arms" worden genoemd.

Door de app te draaien op een schone Android 14-emulator zonder account en de cache en logcat in de gaten te houden, zagen we hoe het werkt: wanneer de app wordt gestart, stuurt de server een configuratie die bepaalt welke "arm" van welke "trait" je krijgt, en de app downloadt deze als versleutelde JavaScript. De configuratie van mijn installatie noemde ongeveer 700 traits. Mijn installatie selecteerde er 254 van, waarvan 101 na de installatie werden gedownload.

(Lijst van 254 traits, waaronder onder andere: RewardHighValueAdHarvestTrait, AdLevelTrait, AdPlusMinusStrategyTrait, en vele anderen die betrekking hebben op animaties, geluiden en spelmechanieken).

Twee mensen die naast elkaar spelen, kunnen dus verschillende versies van het spel draaien, en de studio kan jouw versie wijzigen zonder dat er een app-update nodig is. De meeste van die 254 traits zijn gewoon: tutorials, animaties, geluiden. Een handvol gaat over wat er gebeurt als je verliest. Eén daarvan gaat over advertenties.

De regel die wacht op een dure advertentie

Deze regel heet RewardHighValueAdHarvestTrait. Door te zoeken naar waar het spel deze bestanden ontgrendelt in de native bibliotheek, hebben we het gedecodeerd naar 15.616 bytes aan platte, geminificeerde JavaScript. De constructor stelt de parameters in:

// RewardHighValueAdHarvestTrait.js · regels 63–67
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";

De regel wacht tot drie zaken gelijktijdig gebeuren:

  1. Je hebt je laatste drie beloonde advertenties (rewarded ads) volledig bekeken.
  2. De advertentie die op dit moment is geladen, is minstens 3,5 keer zoveel waard als de vorige.
  3. Je score is 1.000 punten hoger dan je huidige topscore.

Al deze drie punten worden gecontroleerd in één functie (hier herschreven met optional chaining voor leesbaarheid, maar de logica blijft gelijk):

// shouldTriggerDeath · regels 262–267 (geformatteerd)
shouldTriggerDeath() {
  if (!this.canTrigger()) return false;                  // laatste 3 rewarded ads voltooid
  const d = this.storageData;
  return d.currentGameNum === hs.classGameInfo?.gameNum   // hetzelfde spel
  && !d.currentGameHasTriggeredDeath                 // is nog niet afgevuurd
  && !(hs.classScoreInfo?.score < this.currentScoreThreshold)  // voorbij topscore + 1.000
  && !!this.isTargetAdReady();                       // geladen ad ≥ 3.5× de vorige
}

Hoe de controles werken

1. Voltooien van de laatste drie rewarded ads Elke keer dat een rewarded ad start, voegt de regel false toe aan een lijst en bewaart deze de laatste drie (regels 174–183). Als de ad SDK rapporteert dat de beloning is verdiend, verandert deze waarde in true (191–199). De check vereist dat alle drie exact true zijn:

// canTrigger · regels 268–275
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;
  });
};

Deze vlag komt uit de onUserRewarded callback van AppLovin MAX in de Java-laag; een advertentie die je voortijdig sluit, blijft dus op false staan.

2. De waarde van de huidige advertentie De regel leest de waarde van de beste geladen rewarded ad via NativeAd.callNativeGetMaxEcpmNumber("2") (regel 304). In de Java-laag gaat deze aanroep door elk klaarstaand rewarded ad-slot, neemt de omzet die AppLovin MAX rapporteert, vermenigvuldigt deze met 1.000 en geeft de hoogste waarde terug. De waarde van je laatste rewarded ad krijgt dezelfde vermenigvuldiging (regel 310).

// classifyLoadedRewardLevel · regels 290–293
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. De topscore-drempel Aan het begin van elk spel slaat de regel je topscore op (regel 145). De grens ligt bij die topscore plus scoreDeltaStep (regels 107–110). Elke speler krijgt één variant: mijn installatie gebruikt 1.000, terwijl de gebundelde configuratie van de app ook 500 en 750 hanteert. Als je een "revive" gebruikt nadat de regel is afgevuurd, gaat de drempel een stap omhoog, zodat de regel in hetzelfde spel opnieuw kan worden geactiveerd.

De dodelijke puzzel

Wanneer aan de voorwaarden is voldaan, vraagt het spel om een "lethale puzzel". Voordat de volgende set stukken wordt gedeeld, draait het spel een lijst met "bottom offer"-regels. Als deze regel kwalificeert, vervangt hij de lijst door één specifieke generator:

// onBottomOfferAlgo · regels 245–251
t.prototype.onBottomOfferAlgo = function() {
  if (this.shouldTriggerDeath()) {
    hs.algorithmBottomSequenceInfo.setAlgorithmBottomList([ this.deathAlgorithm ]);
    this.storageData.currentGameHasTriggeredDeath = !0;
    this.saveStorageData();
  }
};

deathAlgorithm geeft hs.OFFERTYPE.ZHISI_TI terug (regel 102). Het spel stuurt dit verzoek naar de puzzel-engine, die drie stuk-ID's teruggeeft.

Om dit te verifiëren, hebben we het gedownloade bestand zelf gedraaid met testwaarden voor advertenties en scores. De dodelijke puzzel wordt exact opgevraagd wanneer aan alle drie de voorwaarden is voldaan.

We hebben dit ook live getest in een kopie van de iPhone-app in de simulator van Apple. De regel was actief en las echte advertentieprijzen. Er was een rewarded ad van Google AdMob geladen met een prijs van 72,39 tegenover 37,41 voor de vorige (een factor 1,94). Omdat dit onder de vereiste 3,5 lag, vuurde de regel niet af.

Wat is de "lethale puzzel"?

De naam ZHISITI wordt in het hoofdscript gekoppeld aan algomustdeath. Algoritme 24001 bestaat zowel in JavaScript als in de native C++ bibliotheek (waar de functie funcdeathblcok heet).

De JavaScript-kern werkt als volgt:

// rC · main/index.45fec.js regels 318602–318678 (uittreksel)
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: past nu, i: past nu niet
}
...
iC(r, o[p], o[f], i[_], h);               // probeer elke volgorde van twee stukken die passen
iC(r, o[p], i[_], o[f], h);               // en één die niet past, na het wissen van lijnen
...
if (0 == h.length) {                     // geen enkele volgorde werkte: neem deze set
  u = [ o[p], o[f], i[_] ];
...
a.names = [ "致死题", "致死题", "致死题" ];   // "lethale puzzel" x3

Het algoritme zoekt naar twee stukken die nu passen en één die niet past, en behoudt de set alleen als geen enkele volgorde of positie (inclusief het wissen van lijnen) ervoor zorgt dat alle drie de stukken geplaatst kunnen worden. Omdat twee van de drie stukken bij het delen wel passen, lijkt de hand niet onmogelijk totdat je het daadwerkelijk probeert.

Andere regels die de dodelijke puzzel aanvragen

De advertentie-regel is niet de enige die dit doet. Tijdens het testen op de iPhone vuurde een tweede regel: PersonalMarginBreakTrait. Zodra je 10% boven je topscore zit, wordt er bij elke deal een kans berekend; 20% van de tijd vraagt deze regel om dezelfde dodelijke puzzel:

PersonalMarginBreakTrait · onAlgorithmStrategyPriority, regels 153–159 · live config: prob 20, recordPercent 10, replaceAlgoId 9

Het model dat bepaalt waar je verliest

Naast handgeschreven regels downloadt het spel getrainde modellen. Eén daarvan bepaalt waar in een level de dodelijke puzzel verschijnt. Dit is een XGBoost-classifier met 500 bomen voor de level-modus (genaamd "Chapter"). Het bestand heet predicttraveldifficulty_dead.

De 52 inputs gaan vooral over je prestaties: je huidige level, hoe vaak je het hebt geprobeerd, hoeveel spellen je deze week hebt voltooid en hoe lang je de app al hebt. Het model kiest uit 12 opties de hoogste score. Een andere regel vertaalt deze keuze naar een specifiek deel van het level: de eerste 30%, 30 tot 80%, of de laatste 20%. Wanneer je dat deel bereikt, biedt het spel je een extra revive aan als je die gewoonlijk accepteert (meer dan 75% van de tijd). Vanaf de vierde deal daarna wordt de dodelijke puzzel aangevraagd.

Het model dat jouw "prijs" bepaalt

Een tweede model bepaalt hoeveel jij als speler waard bent. Het spel downloadt xgboostregressionmodel111.json. Dit is een XGBoost-regressiemodel met 59 beslissingsbomen en 27 inputs.

Veel van de inputs gaan over geld: 18 inputs gaan over de advertenties die het spel voor jou heeft geladen, vandaag en gisteren. De overige inputs gaan over je locatie, je telefoon, de installatiedatum en je eerste spelbeurt. De belangrijkste factoren zijn je eerste score, de prijs van je laatste advertentie, je RAM, je telefoonmodel en je land.

De output wordt finalEcpm genoemd. eCPM (effective Cost Per Mille) is de industriestandaard voor de waarde van duizend advertenties. Het model schat dus letterlijk wat jij als gebruiker waard bent in advertentie-inkomsten.

Impact van locatie en score op waarde

Door het model na te bootsen, zagen we hoe de geschatte waarde verandert voor een nieuwe speler in de VS met een eerste score van 1.000:

VariabeleiPhone 15 (Ad value)Android (Ad value)
Basis (VS, score 1.000)25.4848.96
In Brazilië8.0817.90
In India5.5914.83
Eerste score 50025.7953.57
Eerste score 2.00021.1252.64
Eerste score 10.00020.3458.26

Het spel gebruikt deze prijzen om te bepalen welk type advertenties je krijgt en hoeveel. Als je rewarded ads minstens twee keer zoveel waard zijn als full-screen ads, krijg je bijvoorbeeld een extra kans om te reviven (waarbij een rewarded ad wordt afgespeeld). Als full-screen ads meer opbrengen, krijg je die kans niet.

Een model dat voorspelt of je morgen minder speelt

Er is ook een LightGBM-model met 1.000 bomen en 93 inputs dat voorspelt of je morgen minder actief zult zijn. Als de score 0,69 of hoger is, vervangt een bijbehorende regel de revive-knop door een roze knop, waarbij de revive gratis is (zonder advertentie). Dit gebeurt maximaal één keer per dag om de speler vast te houden.

Server-modellen

Sommige regels sturen je data naar de server en handelen op basis van scores die terugkomen van modellen met namen als churn, LTV, revive-click en xDeepFM. De gewichten van deze modellen blijven op de server. Eén van deze verzoeken (/v1/ecpm_prediction) verstuurt 40 velden, waaronder je provider, opslagcapaciteit, het tijdstip en statistieken over je recente advertentieprijzen.

Overige regels die het spel beïnvloeden

Er zijn meer regels die je stukken of score beïnvloeden:

RegelEffect
MaturityDevelopmentV2TraitBerekent een "volwassenheidsscore" (gewoonte, vaardigheid, ad-historie). Bij een lage score worden moeilijke puzzels vervangen door makkelijke vullingen.
FailStreakStrategyTraitVraagt om makkelijkere stukken na twee spellen onder je recordscore.
MultiClearBoostThenKillTraitStuurt user ID naar de server. Bij bepaalde categorieën ("niet gevoelig voor geforceerd verlies") wacht het op een grote clear en vraagt dan kort daarna de dodelijke puzzel aan.
MABXiBaiTraitEen experiment om te bepalen hoe snel de dodelijke puzzel kan worden opgevraagd (bijv. na 4, 6, 10, 17 of 30 minuten).
FastHandPunishTraitVraagt om moeilijke stukken als je laatste zes sets gemiddeld in minder dan 4,2 seconden zijn geplaatst en je boven de 93% van je topscore zit.
AdPlusMinusStrategyTraitVoegt na een bepaald soort revive-aanbod een willekeurige bonus toe tot 10% van je topscore.

Veelgestelde vragen

Is dit hetzelfde op iPhone? Ja. De iOS-versie downloadt dezelfde regels van een eigen server. Er is zelfs een V2-versie van de ad-regel waarbij de check op advertentie-historie kan worden uitgeschakeld, waardoor een enkele (zelfs voortijdig gesloten) advertentie genoeg kan zijn om de regel te activeren.

Wat gebeurt er als ik zonder wifi speel? De regels zijn lokaal opgeslagen, dus ze blijven draaien. Echter, de specifieke ad-regel vuurt alleen af als er een rewarded ad is geladen. Zonder internet worden er geen ads geladen, dus deze specifieke regel kan niet afvuren. Andere regels (zoals die voor snel spelen) blijven wel werken.

Helpt een adblocker? Browser-blockers werken niet in de app. Een systeembrede blocker (zoals Private DNS op Android of een VPN-blocker) kan voorkomen dat rewarded ads laden. Hierdoor kan de ad-regel niet afvuren, maar de andere regels die geen ads gebruiken, blijven actief.

Is dit waarom ik altijd verlies vlak nadat ik mijn topscore heb verbeterd? Exact. De PersonalMarginBreakTrait zorgt ervoor dat er 20% kans is op een dodelijke puzzel zodra je 10% boven je record zit. De ad-regel wacht op 1.000 punten boven je record.

Kan ik dit vermijden? Niet echt. Op Android kan het sluiten van een advertentie de ad-regel tijdelijk uitschakelen, maar op iPhone kunnen V2-versies dit negeren. Andere regels (timing-experimenten, big clears) kijken helemaal niet naar advertenties.

Is dit niet gewoon "adaptive difficulty"? Nee. Adaptive difficulty past de stukken aan op basis van hoe je speelt. Deze regels kijken naar advertentieprijzen, ad-resultaten en je score ten opzichte van je record. Bovendien is de "lethale puzzel" niet simpelweg een "moeilijkere" set, maar een set die specifiek is gekozen omdat deze onmogelijk te plaatsen is.

Is dit toegestaan? Controleert Apple de apps niet? Apple controleert de app in de store, maar deze regels worden ná installatie gedownload van de server. Apple staat het downloaden van JavaScript-updates toe, zolang de kernfunctie van de app niet verandert. Of het aanpassen van puzzelstukken op basis van advertentieprijzen hieronder valt, is aan Apple.

Waarom draait dit op de telefoon en niet op de server? De server bepaalt wie welke regel krijgt, maar de beslissing wanneer hij vuurt, vereist realtime data die alleen de telefoon heeft: je huidige bord, de volgende set stukken en de advertentie die op dat moment in het geheugen staat.

Verificatie van de bevindingen

  • Apps: Android 10.8.2 (via APKPure) en iOS 7.4.9 (via publieke mirror).
  • Bronnen: De regels zijn gedownload van bband1.afafb.com (Android) en bbios1.afafb.com (iOS).
  • Decoding: De traits zijn versleuteld met XXTEA en gecomprimeerd met Brotli.
  • Testen: De logica is geverifieerd met Node.js en Python scripts op zowel emulators als live apparaten.

Technische bewijzen (Hashes)

  • Android base APK: a520fcc1d1ba1ff0c643e30b059c87a01fde650ec46be5c85fde5d815ec82447
  • Android rule (decoded): f94a5d54d7f6ab0af097f408bda09b091e7767b17749f8ba828b41b638b330ad
  • iOS IPA: 7fc9b47e10dc8303f662e1c0ad88b93e7119e2846972eddb1a18bd205b54056a
  • iOS rule (decoded): 179bf50ae3c0c4a9bd5457281e4876faae728778f6181dbb0d3799b42ec70d61