تمام مضامین پر واپس

ایجنٹ برج: وہ گیم کھیلنا جو آپ کو دکھائی نہیں دیتی

شائع کردہ:
agent-runcandyrushwebgltoolingevidence

کسی AI ایجنٹ سے گیم کا playtest کرانے کا سیدھا سا طریقہ یہ ہے کہ اسے گیم دکھا دی جائے۔ کینوس کا اسکرین شاٹ لو، تصویر ویژن ماڈل کو دو، پوچھو کہ اسے کیا نظر آ رہا ہے، ایک چال طے کرو، کوئی کی دباؤ، پھر اسکرین شاٹ لو۔ یہ کام کرتا ہے — اسی معنی میں جس معنی میں ایک demo کام کرتا ہے۔

اور ایک تیز ردِعمل والے survivors گیم کے لیے یہ تقریباً بدترین ممکنہ ڈیزائن بھی ہے۔ ہر فیصلے کی قیمت ایک تصویر ہے۔ context جمع ہوتا رہتا ہے یہاں تک کہ سیشن کا معیار گر جاتا ہے۔ ماڈل ایک رینڈر شدہ فریم پڑھ کر وہ حالت دوبارہ اخذ کر رہا ہوتا ہے جو انجن پہلے ہی بالکل درست جانتا ہے — دشمنوں کی جگہ، ہٹ پوائنٹس، کول ڈاؤن — اور اسے بدتر طریقے سے اخذ کرتا ہے۔ اور جس لمحے کوئی دلچسپ بات ہوتی ہے، وہ ہو چکی ہوتی ہے، کیونکہ اسکرین شاٹ اور کی پریس کے درمیان سِم آگے چلتا رہا۔

CandyRush ایک deterministic، fixed-timestep سِمولیشن ہے۔ یہی حقیقت ایک مختلف ڈیزائن ممکن بناتی ہے: سِم روکو، حالت JSON کے طور پر پڑھو، ان پٹ اسی بفر سے داخل کرو جو replay سسٹم استعمال کرتا ہے، اور بالکل مقررہ تعداد میں ٹِکس آگے بڑھاؤ۔ ساختہ متن اندر، deterministic ٹِکس باہر۔ پورے لوپ میں کہیں کوئی پکسل نہیں۔

یہی سطح ایجنٹ برج ہے، اور یہ اس وقت production پر زندہ ہے۔

JS انٹری پوائنٹس
20
window.__candy*
ٹھنڈے مینو → چلتی رَن
2.0s
صفر کلک
600 ٹِکس آگے
150–310ms
پہلے 21,500ms
EditMode ٹیسٹ
178
10 برج ٹیسٹ فائلیں
براؤزر جانچ کے دور
3
25 فکس، F1–F16 + R3F1–R3F9
منسوب کیے گئے ایررز
277
فی سین لوڈ، GH #610

اس کی ساخت

لوپ چار قدموں کا ہے، اور ان میں سے کسی میں بھی کچھ "دیکھنا" شامل نہیں:

// 1. پڑھو — تازہ ترین snapshot، پہلے سے parse شدہ
const s = window.__candyState;

// 2. فیصلہ کرو — ساختہ ڈیٹا پر سادہ حساب
const threat = s.enemies.filter(e => e.damageReadyInTicks < 10);
const dir = fleeFrom(threat, s.player);

// 3. داخل کرو — اُس ٹِک کے لیے deterministic ان پٹ جو سِم اگلا استعمال کرے گا
window.__candyInput({ moveX: dir.x, moveY: dir.y });

// 4. آگے بڑھاؤ — بالکل N ٹِکس، پھر فوراً تازہ snapshot
window.__candyStep(6);
await window.__candyWait();

پوری سطح production بلڈز سے #if UNITY_EDITOR || DEVELOPMENT_BUILD کے پیچھے مکمل طور پر compile out ہو جاتی ہے۔ ڈیو WebGL بلڈ میں یہ مزید صفحے کے URL پر gated ہے — اور یہی وہ چیز ہے جو ایک ہی deploy کو دو مختلف سامعین کی خدمت کرنے دیتی ہے۔

آج ہی براؤزر سے اسے چلانا

ڈیو چینل deploy شدہ اور عوامی ہے۔ سادہ URL پر گیم معمول کے مطابق چلتی ہے۔ ایک query parameter لگاتے ہی برج فعال ہو جاتا ہے:

URLبرج
unity.irsik.software/games/candyrushdev/بند — عام طور پر کھیلیں
unity.irsik.software/games/candyrushdev/?agent=1کھلا
ایڈیٹر، ڈیسک ٹاپ پلیئرزہمیشہ کھلا

?agent=1، ?agent=true، خالی ?agent، یا مکمل path segment کے طور پر agent-mode — یہ سب اسے فعال کرتے ہیں؛ ?agent=0 واضح opt-out ہے اور path والی شکل پر بھاری پڑتا ہے۔

یہ گیٹ ایک بہت سادہ وجہ سے موجود ہے۔ اس سے پہلے، ڈیو چینل کھیلنے کے لیے لوڈ کرنے والا ہر انسان ایک مکمل state snapshot کی قیمت ادا کر رہا تھا — serialize ہو کر، پھر CANDY_STATE لائن کے طور پر console.log — ہر چھ ٹِکس بعد، تقریباً دس بار فی سیکنڈ، ایسے output کے لیے جو کوئی شخص کبھی نہیں پڑھے گا۔

یہ صاف کہہ دینا ضروری ہے: یہ کوئی سیکیورٹی باؤنڈری نہیں ہے۔ برج production بلڈز سے پہلے ہی غائب ہے، اور جو شخص ڈیو چینل لوڈ کر سکتا ہے وہ خود بھی query لگا سکتا ہے۔ یہ صرف اتنا روکتا ہے کہ کسی کھلاڑی کو اتفاقاً ایجنٹ رویہ نہ مل جائے۔ بس اتنا ہی اس کا مقصد ہے۔

ایک سیشن، ٹھنڈے page load سے

یہ مضمون لکھتے ہوئے میں نے یہ سب لائیو deploy کے خلاف چلایا۔ نیچے ہر قدر وہی ہے جو واپس آئی۔

// دنیا کے وجود میں آنے سے پہلے ہی اسے pin کر دو۔ page reload سے بچ جاتا ہے۔
window.__candyPinSeed(777001);
await window.__candyWait();
// → {"ok":true,"cmd":"pinSeed","seed":777001,"seq":1}

// ٹھنڈے مین مینو سے رَن شروع کرو۔ نہ لابی، نہ کلک، نہ Play بٹن۔
window.__candyStartRun({ characterId: 0 });
await window.__candyWait();
// → {"ok":true,"cmd":"startRun","characterId":0,"arcId":"","status":"loading","seq":2}

// سین اترنے تک poll کرو۔
while ((window.__candyState || {}).runState === "Menu") {
  await new Promise(r => setTimeout(r, 250));
}

دو سیکنڈ بعد، ایک ٹھنڈے مینو سے:

{ "runState": "Paused", "tick": 0, "seed": 777001, "pauseOwners": ["agent"] }

آخری سطر غور سے پڑھیے، کیونکہ پورا ڈیزائن اسی ایک object میں سمویا ہوا ہے۔ رَن ٹِک صفر پر منجمد ہے، ایک ایسے pause token کے قبضے میں جو صرف ایجنٹ کا ہے، کسی اور کا نہیں۔ صفر بے قابو real-time ٹِکس چلے ہیں۔ seed وہی ہے جو مانگا گیا تھا۔ ابھی تک ایسا کچھ نہیں ہوا جو ایجنٹ نے خود نہ کرایا ہو۔

یہ خوبی مفت نہیں ملی — یہ فکس R3F1 ہے، اور نیچے براؤزر جانچ والا حصہ بتاتا ہے کہ اسے حاصل کرنے میں کیا قیمت لگی۔

API سطح

بیس انٹری پوائنٹس، سب window پر نصب۔ ہر ایک قطار میں لگنے والی کمانڈ ہے، synchronous کال نہیں۔

حالت پڑھنا

ٹیلی میٹری
کالاثر
__candyStateیہ property ہے، function نہیں۔ تازہ ترین snapshot، پہلے سے parse شدہ۔
__candyDump()تازہ snapshot کی درخواست کرتا ہے اور synchronously پچھلا واپس کرتا ہے۔
__candySetMaxEntities(n)فی اینٹیٹی فہرستوں پر nearest-N حد بڑھائیں یا گھٹائیں۔ ڈیفالٹ 15۔
__candyVfxAudit()لوڈ شدہ سین کے ہر VisualEffect کا نام بتاتا ہے۔ نیچے بگ کی تلاش دیکھیں۔

عمل کرنا

کنٹرول
کالاثر
__candyInput(...)اُس ٹِک کے لیے حرکت داخل کرتا ہے جو سِم اگلا استعمال کرے گا۔ حرکت ہی واحد فی ٹِک ان پٹ ہے — CandyRush خودکار حملے والا survivor گیم ہے، اس لیے fire، dash یا ability بٹن ہے ہی نہیں۔
__candyLevelUp(action, index)level-up picker حل کرتا ہے: Pick، Skip، Reroll، Banish۔ بتاتا ہے کہ عمل واقعی لاگو ہوا یا نہیں، تاکہ charge سے رکا ہوا no-op کبھی کامیابی نہ سمجھا جائے۔
__candyStep(n)SimulateOneTick() براہِ راست چلا کر بالکل n ٹِکس آگے بڑھاتا ہے۔
__candyStepUntil(...)ایک بجٹ تک آگے بڑھتا ہے، شرط پوری ہوتے ہی رک جاتا ہے، اور بتاتا ہے کون سی۔
__candyPause() / __candyResume()مخصوص owner token سے pause، تاکہ یہ گیم کے اندرونی pause کو مٹانے کے بجائے اس کے ساتھ نبھا کرے۔
__candyStartRun(...)کسی بھی مرحلے سے، ٹھنڈے مین مینو سمیت، بغیر UI کے رَن بوٹ کرتا ہے۔
__candyCheat(name, ...)نو sim-safe cheats، جو اگلے simulate ہونے والے ٹِک کے شروع میں لاگو ہوتے ہیں۔
__candyWait(timeoutMs)ایک promise۔ قطار میں لگی کمانڈ کا انتظار کرنے کا معتمد طریقہ۔

وقت کا سفر

تعمیرِ نو
کالاثر
__candyPinSeed(seed)اگلی رَن کے لیے content seed pin کرتا ہے۔ page reload سے بچ جاتا ہے۔
__candyExportReplay()رَن برآمد کرتا ہے: seed، کردار، arc، اور ان پٹ، level-up اور cheat کی streams۔
__candyRewind(k)سِم کو deterministically currentTick - k پر بحال کرتا ہے۔
__candyLoadReplay(json, ...)ایک export لوڈ کرتا ہے اور یا تو اسے اصل رفتار پر دکھاتا ہے یا تیزی سے چلا کر تصدیق کرتا ہے۔

ہر emit کے ساتھ ایک CANDY_STATE <json> کنسول لائن بھی لکھی جاتی ہے، تو ایسا ایجنٹ جو صرف کنسول پڑھ سکتا ہے — window تک بالکل رسائی کے بغیر — پھر بھی پوری طرح معلومات پاتا ہے۔

snapshot دراصل کیا لے کر آتا ہے

یہی وہ حصہ ہے جو طے کرتا ہے کہ پورا خیال کام کرے گا یا نہیں، اور یہاں لالچ یہ ہوتا ہے کہ انجن کے اندرونی حصے اُنڈیل دو اور ایجنٹ خود سمجھ لے۔ برج جان بوجھ کر اس کے الٹ کرتا ہے۔

{
  "tick": 12456,
  "runState": "InRun",
  "pauseOwners": [],
  "seed": 3921184017,
  "player": { "x": 12.3, "y": -4.1, "hp": 84, "maxHp": 100, "level": 7,
              "killsThisRun": 41, "damageDealtLastTick": 12.5,
              "abilities": [ { "name": "WoodenWand", "cooldownRemainingTicks": 34, "ready": false } ] },
  "enemiesTotal": 137,
  "enemiesInContact": 2,
  "incomingContactDps": 7.5,
  "enemies": [ { "id": 91, "x": 15.0, "y": -2.0, "type": "gumdrop", "hp": 12,
                 "vx": -0.05, "vy": 0.02, "contactDamage": 5,
                 "damageReadyInTicks": 34, "marked": false, "behavior": "chaser" } ],
  "run": { "segmentIndex": 3, "archetype": "Hunt", "quotaProgress": 2, "quotaTarget": 5,
           "gates": [ { "id": 0, "x": 12.5, "y": -30.0, "lockState": "Locked", "leadsTo": "Boss" } ] }
}

یہ ایک معاہدہ ہے، ڈھیر نہیں۔ ہر قدر انجن کے فیلڈ کی نقل کے بجائے اخذ شدہ اور ایجنٹ کے لیے تیار ہے، کیونکہ خام فیلڈز ایجنٹ کو ایسے constants سے دوبارہ حساب کرنے پر مجبور کرتے ہیں جو اسے نظر نہیں آتے — ایک پوشیدہ coupling جو گیم کے rebalance ہوتے ہی خاموشی سے ٹوٹ جاتی ہے۔

چند انتخاب الگ سے بیان کرنے کے قابل ہیں، کیونکہ ہر ایک کے پیچھے ایک بگ چھپا بیٹھا ہے:

  • enemies[].vx/vy وہ حرکت ہے جو پچھلے ٹِک پر واقعی ہوئی — flow-field steering، separation اور رکاوٹ سے دھکیلے جانے کے بعد — دشمن کا "ارادہ" نہیں۔ ایک chaser ہر ٹِک پر دوبارہ کھلاڑی کا رخ کرتا ہے، اس لیے خام ارادے سے extrapolate کرنا غلط نتیجہ دیتا ہے۔
  • enemiesTotal اور incomingContactDps پورے swarm کے ہیں، جبکہ فی اینٹیٹی فہرست صرف قریب ترین 15 کا نمونہ ہے۔ جب میدان میں سینکڑوں ہوں تو نمونے سے آنے والا نقصان نکالنا اُس وقت تک محفوظ لگتا ہے جب تک آپ مر نہ جائیں۔
  • run، boss اور result لاگو نہ ہونے پر حذف ہوتے ہیں، کبھی صفر نہیں کیے جاتے۔ صفر شدہ run "چلتی رَن کا سیگمنٹ 0" پڑھا جائے گا، اور صفر شدہ result "0 ستارے"۔
  • pauseOwners بتاتا ہے کہ سِم کس کے قبضے میں ہے["agent"] بمقابلہ ["agent","levelup"]۔ یہی "resume کال کرو" اور "levelUp کال کرو" کا فرق ہے، اور اکیلا runState:"Paused" یہ فرق بیان نہیں کر سکتا۔
  • تمام شرحیں فی ٹِک ہیں، تاکہ وہ __candyStep(n) کے ساتھ جُڑ سکیں۔
  • NaN اور ±Infinity 0 کے طور پر شائع ہوتے ہیں، ساتھ ایک nonFinite key جو خراب راستوں کی فہرست دیتی ہے۔ خام NaN جائز JSON نہیں اور پہلے ایجنٹ کو پورا snapshot گنوا دیتا تھا۔ null شائع کرنے کو ایک تیز تر وجہ سے رد کیا گیا: hp < 20 جیسا موازنہ null کے مقابلے میں سچ پڑھا جاتا ہے، یعنی ایک ٹوٹا ہوا سِم ایک پُراعتماد غلط فیصلے میں بدل جاتا ہے۔

اصل مصنوع determinism ہے

کوئی بھی harness چلتی گیم کو کرید سکتا ہے۔ اسے جانچ کا آلہ بنانے والی چیز یہ ہے کہ سیشن دوبارہ پیدا کیا جا سکتا ہے، اور اس کے لیے کچھ مخصوص فیصلے کرنے پڑے:

  • ان پٹ اُس ٹِک سے منسلک ہے جو سِم اگلا پڑھے گا، بالکل ویسے جیسے ہارڈویئر sampling خود کرتی ہے۔ ایجنٹ کا داخل کردہ فریم بالکل اسی طرح استعمال ہوتا ہے جیسے sampled یا replayed فریم۔
  • داخل کرنا ہارڈویئر sampling کو دبا دیتا ہے، تاکہ صفحے پر کوئی آوارہ کی پریس مقابلہ نہ کر سکے۔ ایجنٹ کا ان پٹ مستند اور ریکارڈ شدہ ہے، اس لیے سیشن ہو بہو دوبارہ چلتا ہے۔
  • آگے بڑھنا wall-clock وقت کو کبھی نہیں چھوتا۔ یہ پہلے ایجنٹ کا pause token لیتا ہے، تاکہ frame والا لوپ دو قدموں کے بیچ خود سے ٹِک نہ کر سکے۔
  • Cheats اگلے simulate ہونے والے ٹِک کے شروع میں لاگو ہوتے ہیں، کمانڈ callback سے کبھی نہیں۔ spawn اور kill deterministic RNG خرچ کرتے ہیں؛ frame کے وقت لاگو ہونے والا cheat RNG کو ٹِک سے ہٹ کر آگے بڑھا دیتا اور خاموشی سے ہر replay کو بگاڑ دیتا۔ ہر اطلاق اپنے ٹِک کے ساتھ ریکارڈ ہو کر export میں جاتا ہے۔

اس کا ثبوت بے تکلف ہے اور بالکل درست شکل کا: دو رَنز، دونوں seed 777001 پر pin، بغیر کسی ان پٹ کے اسی ٹِک 1800 تک آگے بڑھائی گئیں، اور byte-for-byte موازنہ۔ پچیس کے پچیس دشمنوں کی جگہیں بالکل ملیں۔ صفر فرق۔

دلچسپ مقام پر رک جانا

سادہ لوپ — چھ ٹِکس آگے، پڑھو، فیصلہ کرو، دہراؤ — ہر فیصلے پر ایک پورا round trip لیتا ہے۔ __candyStepUntil اسے ایک کال میں سمیٹ دیتا ہے:

__candyStepUntil({ maxTicks: 600, stopOn: ["hpBelow:40", "levelReached:5"] });
const { result } = await __candyWait();
// { ok:true, cmd:"stepUntil", requested:600, stepped:214, tick:8931, stopped:"hpBelow" }

stopOn ایک بند مجموعہ ہے، predicate زبان نہیں: hpBelow:N، levelReached:N، segmentChanged، gateUnlocked، bossSpawned، quotaMet، اور ساتھ gameover و levelup جو ہمیشہ روکتے ہیں۔ ایک مقررہ ترتیب کے مطابق صرف ایک وجہ جیتتی ہے — مرنے سے HP بھی ہر حد سے نیچے چلا جاتا ہے، اور یہی ترتیب اسے بے کار hpBelow رپورٹ کرنے سے روکتی ہے۔

مقام سے متعلق predicates جان بوجھ کر چھوڑ دیے گئے۔ ایجنٹ انہیں دو کالوں کے بیچ خود گن سکتا ہے، اور بند مجموعے میں شامل ہر predicate وہ چیز ہے جسے سِم کو اس کے بعد ہر ٹِک پر ہمیشہ جانچنا پڑے گا۔

وہ بگ جو اس سب کو درست ثابت کرتا ہے

ایک ڈیو WebGL بلڈ میں، Unity کا اپنا native VFX رن ٹائم ہر Game سین لوڈ پر یہ چھاپتا تھا:

Invalid VFX Particle System. It is skipped.

277 بار، ایک ہی جھونکے میں، اور کسی چیز کا نام لیے بغیر۔ نہ GameObject، نہ path، نہ asset، نہ GUID۔ یہ string Unity کے اپنے C++ ماڈیول سے آتی ہے — یہ بھیجی گئی .wasm کے اندر رہتی ہے اور پروجیکٹ کے کوڈ سے بدلی نہیں جا سکتی۔ اس نے اسکرین پر موجود Development Console کو اس حد تک بھر دیا کہ اصل ایررز نظر آنا بند ہو گئے، اور یہی اس کی پوری قیمت تھی: گیم میں کچھ بھی غلط نظر نہیں آتا تھا۔

غالب نظریہ یہ تھا کہ کوئی portal display mode WebGL پر ناکام ہو رہا ہے۔ برج چلانے والے ایک ایجنٹ نے اس کے بجائے یہ جدول پیش کیا:

مشاہدہپیمائش
مین مینو پر، کسی میدان کے لوڈ ہونے سے پہلے0 ایررز
Game سین لوڈ ہونے پر277 ایررز، ایک ~33 ms کا جھونکا، سب ٹِک 0 پر
دوسرا سین لوڈ، مختلف arcکل 277 → 554 — یعنی ہر بوٹ پر بالکل 277
نئے streamed chunks میں 600+ ٹِکس چلنا0 اضافی
ٹِک ~1400 تک کھیلنا، دشمن 8 → 710 اضافی

یہ ایک بار کا، initialization کے وقت کا جھونکا ہے، جو ہر سین لوڈ پر deterministic طور پر دہراتا ہے — نہ فی spawn، نہ فی frame۔ اس نے غالب نظریے کو سراسر غلط بھی ثابت کر دیا — mode check کبھی ٹوٹا ہی نہیں تھا؛ وہ درست طور پر WebGL پر VFX portal بنانے سے انکار کرتا ہے۔ اسے بس کبھی بولنے کا موقع ہی نہ ملا، کیونکہ دس VisualEffect کمپوننٹس ایک ایسے authored prefab میں پکا دیے گئے تھے جہاں تک mode check پہنچ ہی نہیں سکتا، اور وہ ہر pooled gate کے ساتھ سین میں آ جاتے تھے۔ تین gate pools، prewarm تین: کل تیس۔

سب سے ٹیڑھی تفصیل وہ ہے جو صرف زندہ پیمائش ہی پکڑ سکتی ہے۔ وہ تیسوں پہلے ہی inactive تھے، اور Unity پھر بھی ایرر دیتا تھا — یعنی انہیں غیر فعال کرنے سے کبھی فائدہ ہونا ہی نہیں تھا۔ انہیں حذف کرنا پڑا۔ فکس ایک prefab تھا، بالکل کوئی C# نہیں، اور 850 سطریں مردہ YAML کی حذف۔

اس میں سے کوئی بات کسی اسکرین شاٹ سے قابلِ رسائی نہیں، اور کوئی بات یونٹ ٹیسٹس سے قابلِ رسائی نہیں۔ وہاں تک پہنچنے کے لیے مین مینو پر کنسول کا جھونکا گننا پڑا، پھر سین لوڈ کے بعد دوبارہ، پھر دوسرے سین لوڈ کے بعد ایک بار اور، اور پھر چھ سو ٹِکس چل کر یہ ثابت کرنا پڑا کہ گنتی بڑھ نہیں رہی۔ یہ ایک پیمائشی سیشن ہے، اور برج دراصل اسی کے لیے ہے۔

تین دور، براؤزر میں غلط ثابت ہونے کے

اس کہانی کا ایماندار حصہ یہ ہے کہ برج جب پہلی بار آیا تو کام نہیں کرتا تھا۔

C# والا رخ یونٹ ٹیسٹ شدہ ہے — دس ٹیسٹ فائلوں میں 178 EditMode ٹیسٹ میتھڈز، جو dispatch، stepping، cheat کی ترتیب، replay کی کٹائی، اور ہر acknowledgement کا عین JSON byte-for-byte طے کرتے ہیں۔ لیکن .jslib پرت، یعنی اصل JavaScript-سے-C# حد، صرف ایک حقیقی WebGL development بلڈ میں چلتی ہے۔ وہ ساختی طور پر پروجیکٹ کے ہر خودکار ٹیسٹ کی پہنچ سے باہر ہے۔

چنانچہ اسے ہاتھ سے، براؤزر میں، تین بار جانچا گیا۔

#403 دائربرج آیا (#425) + دور 1 و 2تیسرا دوررَن ٹیلی میٹری + replay viewer (#467)Replay پلیٹ فارم (#574)Typed DTOs (#608)
10 جولائی → 1 اگست 2026۔ پہلے کے بعد ہر نشان CandyRush dev میں مرج ہونے والا ایک pull request ہے۔

پہلے دور نے دو blockers نکالے۔ کئی درجے ایک ساتھ چڑھنے پر برج مکمل طور پر جام ہو جاتا تھا: قطار میں لگا دوسرا picker اسکرین پر صاف کھلا ہوتا، مگر snapshot میں کوئی انتخاب ہوتا ہی نہیں، اور صرف انسانی ماؤس کلک ہی اسے ہٹا سکتی تھی۔ ابتدائی weapon-select gate نہ نظر آتا تھا نہ چلایا جا سکتا تھا، یعنی ایجنٹ رَن شروع تک نہیں کر سکتا تھا۔ اسی دور نے یہ بھی پکڑا کہ دستاویز میں دیا گیا انتظار کا نسخہ deadlock کرتا ہے، اور وجہ دہرانے کے قابل ہے: Chrome پوشیدہ ٹیب پر requestAnimationFrame مکمل معطل کر دیتا ہے، اور خودکار ایجنٹ تقریباً ہمیشہ ٹیب چھپا کر ہی چلاتا ہے۔ Unity کا اپنا لوپ ایک fallback ٹائمر پر لنگڑاتا ہوا چلتا رہتا ہے، اس لیے سِم تو ٹِک کرتا رہتا ہے — مگر page JS میں rAF پر مبنی کوئی بھی انتظار چلتا ہی نہیں۔

دوسرے دور نے determinism کی کہانی کو اس کی بنیاد پر ٹوٹا ہوا پایا: pin کیا گیا seed page reload سے بچتا ہی نہیں تھا، کیونکہ وہ C# میموری میں رہتا تھا جسے WASM reload تباہ کر دیتا ہے۔ 424242 pin کریں، reload کریں، اور رَن 2328243829 پر بوٹ ہوتی۔ پہلے دور کی رپورٹ نے اسے "غیر تصدیق شدہ" کہا تھا۔ دوسرے دور نے اسے ٹوٹا ہوا تصدیق کر دیا۔

تیسرے دور نے اُس چیز کے فکس کی پیمائش کی جو یہ سب تکلیف دہ بنائے ہوئے تھی: step(600) 21,500 ms سے گر کر لگاتار کالوں میں 152 / 210 / 311 ms ہو گیا۔ دس سِم سیکنڈ، پانچویں حصہ سیکنڈ میں، وہ بھی ٹیب چھپی ہوئی حالت میں۔ اسی دور نے ایک مکمل خودمختار رَن بھی ثابت کی — weapon select، تین level-ups، اور موت، صفر resume کالز اور Play کے بعد صفر کلکس کے ساتھ۔

پھر دوسرے نصف کی دوبارہ جانچ نے پایا کہ weapon-select کی رپورٹنگ واپس بگڑ چکی ہے، اور یہ کہ دوبارہ داخل ہونے والا rewind رَن کو ایک ایسے ابتدائی gate پر پھنسا سکتا ہے جو تین سو ٹِکس پہلے حل ہو چکا تھا۔

ان دوروں سے پچیس نمبر شدہ فکسیں نکلیں — F1 سے F16، پھر R3F1 سے R3F9۔ ان میں نظر آنے والا نمونہ مستقل اور ذرا عاجز کر دینے والا ہے: تقریباً ہر ایک ایسا معاملہ ہے جہاں C# درست تھا، ٹیسٹ سبز تھے، اور چیز پھر بھی کام نہیں کرتی تھی — کیونکہ خرابی ترتیب میں، وقت بندی میں، یا کسی ایسے براؤزر رویے میں رہتی تھی جسے کوئی ٹیسٹ harness ماڈل نہیں کرتا۔

رسیدیں

PRمرجکیا آیاdiffفائلیں
#42511 جولائیبرج — ٹیلی میٹری، کنٹرول، seed pinning، export+7,845/−189101
#42911 جولائیدور 2+3: F1–F16، R3F1–R3F9، براؤزر سے تصدیق شدہ+5,414/−22450
#46713 جولائیرَن/gate/boss ٹیلی میٹری، spatial layer، replay viewer+8,422/−126120
#57421 جولائیReplay پلیٹ فارم: production capture، ریکارڈنگ سے بوٹ+4,899/−61778
#6081 اگستTyped DTOs، ایک serializer config، golden سے طے شدہ wire+1,916/−54130
#611 + #6131 اگستVFX audit کمانڈ، پھر 277 ایررز حذف+95/−8515

یہ اب بھی کیا نہیں کر سکتا

انہیں صاف کہہ دینا اس سے سستا ہے کہ کوئی خود ٹھوکر کھا کر دریافت کرے:

  • fire، dash یا ability کے ان پٹ سرے سے نہیں ہیں۔ CandyRush خودکار حملے والا survivor گیم ہے؛ فی ٹِک ان پٹ صرف حرکت ہے۔ ہمیشہ بے اثر رہنے والے ان پٹ فیلڈز سجاوٹ کے طور پر رکھنے کے بجائے حذف کر دیے گئے۔
  • spawnEnemies صرف وہ اقسام بناتا ہے جن کا موجودہ سیگمنٹ میں زندہ pool ہو۔ acknowledgement دونوں صورتوں میں queued:true کہتا ہے — enemiesTotal دیکھیں۔
  • setHp کم از کم 1 پر رک جاتا ہے۔ healthbar صفر کرنا موت کے عمل کو چھوڑ دیتا اور سِم کو ادھ مرا چھوڑ دیتا۔ مرنے کے لیے اصل نقصان اٹھانا پڑے گا۔
  • rewind page reload سے نہیں بچتا — زیرِ التوا تعمیرِ نو process state ہے۔ محفوظ شدہ pin اور دستی export مل کر یہ صورت سنبھال لیتے ہیں۔
  • .jslib حد اب بھی ٹیسٹوں سے غیر تصدیق شدہ ہے، مستقل طور پر اور ساختی طور پر۔ براؤزر جانچ کے وجود کی پوری وجہ یہی ہے۔
  • agent-mode/ path stub اس وقت deploy شدہ ڈیو چینل پر 404 دیتا ہے۔ یہ WebGL template میں موجود ہے مگر لکھنے کے وقت لائیو بلڈ میں نہیں۔ ?agent=1 استعمال کریں، وہ کام کرتا ہے۔

اور ایک عملی تنبیہ جس کا برج سے کوئی تعلق نہیں: ڈیو چینل ایک development WebGL بلڈ ہے، اور بھاری ہے۔ ٹھنڈا لوڈ کرنے پر 113.2 MB asset ڈیٹا اور ایک 21.9 MB wasm ماڈیول آیا۔ Content-Type درست طور پر application/wasm بھیجا جاتا ہے، اس لیے streaming instantiation ٹھیک چلتا ہے — یہ بس ایک بڑا payload ہے۔ سیکنڈوں کے بجائے منٹوں کی توقع رکھیں، اور یہ بھی کہ برج ظاہر ہونے سے پہلے ٹیب کافی دیر "still waiting on run dependencies" پر بیٹھی رہے گی۔ جب یہ آ جائے، __candyStartRun آپ کو دو سیکنڈ میں ایک چلتی رَن میں پہنچا دیتا ہے۔


ویژن ماڈل جوڑنے کے بجائے یہ سب بنانے کی اصل وجہ لاگت نہیں ہے، اگرچہ فی فیصلہ یہ تقریباً ایک درجہ سستا ہے۔ اصل وجہ یہ ہے کہ اسکرین شاٹ اُس حالت کا ایک نقصان دہ render ہے جو انجن پہلے ہی بالکل درست جانتا ہے — اور اوپر گنائے گئے ہر بگ میں قطار میں لگا picker، reload کے ہاتھوں کھایا گیا seed، پھنسا ہوا weapon gate، وہ 277 بے نام ایررز — سب اسکرین شاٹ میں نظر نہیں آتے اور snapshot میں صاف ظاہر ہیں۔

گیم پہلے سے ہی deterministic تھی۔ برج نے بس اسے ضائع کرنا بند کر دیا۔