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

ایک ری پلے ہارنس جو CI میں ڈیٹرمنزم ثابت کرتا ہے

شائع کردہ:
candyrushdeterminismci-cdunity

CandyRush ایک ڈیٹرمنسٹک سمولیشن پر بنی ہے: ایک ہی seed اور ایک ہی ان پٹس کو ہر tick پر بالکل ایک جیسا گیم بنانا چاہیے۔ یہی خاصیت بعد میں سرور سے تصدیق شدہ ہفتہ وار اسکورز کی بنیاد بنی۔ مئی 2026 کے شروع میں یہ زیادہ تر ایک یقین ہی تھا، جس کی پشت پر الگ الگ کلاسز کے بہت سے چھوٹے یونٹ ٹیسٹ تھے۔

8 مئی کو میں نے یہ خلا ایک ری پلے ہارنس سے پُر کیا جو عام ٹیسٹ سویٹس میں چلتا ہے۔ یہ کیا کرتا ہے، ایک جدول میں:

تہہمنظرنامہطوالتڈائجسٹچلنے کا وقت
EditMode بنیادی تہہseed والا RNG، fixed-point حساب، لوپ شیڈولنگ، مصنوعی ان پٹ اسٹریم3,600 ticks (60 سیکنڈ)61 ہیش، ہر سیکنڈ ایکایک سیکنڈ سے کم
PlayMode اینٹیٹی اسٹیٹاصل گیم سین، مکمل dependency injection، اسٹیج 1، ایک ایبلٹی1,800 ticks (30 سیکنڈ)30 ہیش، ہر سیکنڈ ایکایک بار سین لوڈ، پھر سمولیشن 10x رفتار پر

ہر تہہ ہر سمولیٹڈ سیکنڈ پر سمولیشن کی حالت کا ایک ہیش بناتی ہے اور اس سلسلے کا موازنہ ریپوزٹری میں کمٹ شدہ fixture سے کرتی ہے۔ کسی بھی سیکنڈ پر فرق ہو تو ٹیسٹ ناکام ہو جاتا ہے اور وہ پہلے ticks دکھاتا ہے جہاں راستے الگ ہوئے۔

ہارنس ایک ہی کمٹ میں آیا: 21 فائلیں، 1,054 سطریں شامل۔ مرج ہونے سے پہلے ہی اس نے ڈیٹرمنزم کی ایک حقیقی خرابی بھی ڈھونڈ لی۔

یہ کہاں سے آیا

ہارنس ایک فالو اپ کام تھا۔ ایک دن پہلے PR #254 نے ہر projectile کلاس کو ڈیٹرمنزم کی خلاف ورزیوں کے لیے جانچا تھا۔ اس نے پانچ مردہ projectile کلاسز حذف کیں اور باقی نو زندہ کلاسز کو Simulate کے اندر tick پر مبنی وقت پر منتقل کیا: اب کوئی easing لائبریری کا callback یہ طے نہیں کرتا کہ تلوار کا hitbox کب بند ہو، اور گزرے وقت کے لیے float جمع کرنے والے متغیرات بھی ختم ہوئے۔ وہ PR 21 فائلوں پر تھا، +879/-740۔

اس کی قبولیت کی شرائط میں ایک end-to-end ری پلے ٹیسٹ شامل تھا جو اس PR میں سما نہیں سکا۔ تو میں نے واضح قبولیت کی شرائط کے ساتھ ایک فالو اپ issue لکھا: ریکارڈ شدہ ان پٹ اسٹریم اور seed، بغیر لائیو ان پٹ کے playback، مقررہ tick وقفوں پر بائٹ بہ بائٹ موازنے کے قابل اسٹیٹ ڈائجسٹ، Windows پر ایک حوالہ رن جس کی تصدیق کم از کم ایک اور OS پر ہو، اور ایک CI گیٹ۔

ایک agent نے اس issue کے مطابق اسے بنایا۔ میں نے ڈیزائن اور diff کا جائزہ لیا، اور issue اسی دن بند ہو گیا۔ ڈائجسٹ، Windows حوالہ اور CI گیٹ فراہم ہوئے۔ دو چیزیں مختلف طریقے سے آئیں یا آئیں ہی نہیں، اور دونوں پر میں آگے بات کرتا ہوں: ان پٹ فائل میں ریکارڈ ہونے کے بجائے کوڈ میں متعین ہے، اور کسی دوسرے OS پر تصدیق نہیں ہوئی۔

ڈائجسٹ کیسے بنتی ہے

ڈائجسٹ (tick, hash) جوڑوں کی ایک فہرست ہے۔ PlayMode تہہ ہر ہیش لائیو سین سے بناتی ہے:

ulong hash = 17;
hash = hash * 31 + tick;

// Player position: FixedVector2 raw values are bit-identical across platforms.
var pos = player.FixedPosition;
hash = hash * 31 + (ulong)pos.X.RawValue;
hash = hash * 31 + (ulong)pos.Y.RawValue;

// Then: player projectiles, enemy projectiles, enemies (id, position, HP),
// each list sorted into a stable order and prefixed with its count.

یہاں تین فیصلے اہم ہیں۔

float نہیں، fixed-point کی خام قدریں ہیش کریں۔ سمولیشن پوزیشنز کو Fixed64 میں رکھتی ہے، جو 64 بٹ کی fixed-point قسم ہے۔ اس کا خام integer ہی اصل حالت ہے۔ دو مشینیں یا تو اس پر بٹ بہ بٹ متفق ہوتی ہیں یا نہیں؛ کوئی epsilon ایڈجسٹ کرنے کی ضرورت نہیں۔ واحد استثنا دشمن کا HP ہے، جو اس وقت تک float تھا، اور ہیش سے پہلے اسے ہزارویں حصے تک گول کیا جاتا ہے۔

مواد کے ساتھ تعداد بھی ہیش کریں۔ ایک اضافی projectile، جس کا کوئی اور اثر نہ ہو، تب بھی ہیش بدل دیتا ہے۔

ہیش سے پہلے ترتیب دیں۔ Unity کا FindObjectsByType اشیاء کو غیر متعین ترتیب میں لوٹاتا ہے۔ ہر فہرست پہلے ایک ڈیٹرمنسٹک ترتیب میں لائی جاتی ہے، اور اسی اصول نے بگ کو بے نقاب کیا۔

منظرنامہ کسی انسانی سیشن سے ریکارڈ ہونے کے بجائے کوڈ میں مقرر ہے: seed 12345، اسٹیج 1، ThunderRing ایبلٹی، اور 1,800 ticks کی خاموش (idle) ان پٹ ترتیب۔ اس کے برعکس EditMode تہہ seed والی ایک مصنوعی ان پٹ اسٹریم بناتی ہے (3,600 ticks پر 30% کثافت)، تاکہ سب سے نچلی تہہ پر کھلاڑی کا ان پٹ بھی جانچا جائے۔ کمٹ شدہ fixture ہی ریکارڈنگ ہے: ایک چھوٹی ٹیکسٹ فائل جس کے ہیڈر میں منظرنامے کی تفصیل ہے، پھر ہر سیکنڈ کا ایک ہیش۔

# Candy Rush replay determinism baseline
schema=1
scenario=playmode_entity_state_v1
os=windows
snapshot_count=30
seed=12345
ticks=1800
snapshot_interval=60
ability=ThunderRing
---
60:C6202871EAEDBD4B
120:C8229854DEC63708
...

بنتے ہوئے جو خرابی ملی

پہلا ورژن projectiles کو EntityId سے ترتیب دیتا تھا، وہی اصول جو سمولیشن میں ہر جگہ استعمال ہوتا ہے۔ projectiles کے لیے یہ اصول دراصل خالی نکلا۔

وجہ: دونوں projectile کلاسز SetEntityId فراہم کرتی تھیں، مگر اسے کبھی کسی نے کال نہیں کیا۔ گیم کے ہر projectile کا EntityId = 0 تھا۔ ایسی فہرست کو ترتیب دینا جس کی ہر کلید صفر ہو، کچھ بھی نہیں کرتا، اس لیے ہیش خاموشی سے FindObjectsByType کی غیر متعین ترتیب پر واپس چلا گیا۔

یہ ایک چھپا ہوا مسئلہ تھا، صرف ٹیسٹ کا مسئلہ نہیں۔ سمولیشن کا کوئی بھی کوڈ جو projectiles کے لیے "EntityId سے ترتیب" کو "ڈیٹرمنسٹک ترتیب" سمجھتا تھا، غلط تھا۔ اب ہارنس projectiles کو ان کی خام fixed-point پوزیشن سے ترتیب دیتا ہے، جو اصل ڈیٹرمنسٹک حالت ہے۔ غائب IDs کو ایک الگ issue کے طور پر ٹریک کیا گیا، جو اسی کمٹ سے بند ہوا۔

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

ہر تہہ کے تین ٹیسٹ

ہر تہہ یہی تین جانچیں چلاتی ہے:

  • ایک ہی OS، رن بہ رن۔ ایک ہی پروسیس میں ڈائجسٹ دو بار بنائیں اور ہیشز کا بالکل ایک ہونا لازم کریں۔ یہ کسی fixture کے بغیر wall-clock وقت، بے ترتیب dictionaries اور Unity callbacks کی ترتیب جیسے پوشیدہ راستے پکڑ لیتا ہے۔
  • اپنی کمٹ شدہ بیس لائن سے مطابقت۔ تازہ ڈائجسٹ کا موازنہ اسی OS کے کمٹ شدہ fixture سے کریں۔ یہ regression گیٹ ہے: گیم کا رویہ بدلنے والی کوئی بھی تبدیلی یہاں ناکام ہوتی ہے، چاہے وہ اب بھی ڈیٹرمنسٹک ہو۔
  • دوسرے OS کی کمٹ شدہ بیس لائنز سے مطابقت۔ تازہ ڈائجسٹ کا موازنہ ہر دوسرے OS کے fixture سے کریں۔ یہ کراس پلیٹ فارم گیٹ ہے۔

ان کے گرد دو طریقۂ کار ہیں۔ fixture موجود نہ ہو تو bootstrap ہوتا ہے: ٹیسٹ اسے لکھتا ہے اور آپ سے کمٹ کرنے کو کہتا ہے۔ جان بوجھ کر رویہ بدلنے کے لیے ایک واضح regenerate ٹیسٹ استعمال ہوتا ہے جو عام رن میں کبھی نہیں چلتا، اس لیے fixture کی دوبارہ منظوری ہمیشہ ایک نام والا، قابلِ جائزہ عمل ہوتا ہے، کوئی ضمنی اثر نہیں۔ fixtures کے ساتھ ایک schema ورژن بھی ہوتا ہے؛ مختلف schema والا peer ناکام ہونے کے بجائے چھوڑ دیا جاتا ہے، اس لیے فارمیٹ کی تبدیلی کے لیے تمام آپریٹنگ سسٹمز سے بیک وقت کمٹس درکار نہیں ہوتے۔

CI میں ہارنس self-hosted Windows runner پر عام EditMode اور PlayMode مراحل میں چلتا ہے، اور ناکام ٹیسٹ بلڈ کو ناکام کر دیتا ہے۔

آج یہ کیا ثابت کرتا ہے، صاف الفاظ میں

کراس پلیٹ فارم ڈیزائن موجود ہے۔ کراس پلیٹ فارم ثبوت موجود نہیں۔

میں نے ہسٹری دیکھی: آج تک کمٹ ہونے والے ہر ری پلے fixture پر os=windows کا ٹیگ ہے۔ نومبر 2026 تک ان کی تعداد پانچ ہے (اصل دو، اور بعد میں شامل کیے گئے props، boomerang اور stage-segment منظرنامے)، اور کبھی کوئی macOS، Linux یا موبائل fixture کمٹ نہیں ہوا۔ چنانچہ peer ٹیسٹ نے کبھی دو آپریٹنگ سسٹمز کا موازنہ نہیں کیا۔ یہی وہ قبولیت کی شرط تھی جو پوری نہیں ہوئی۔

ہارنس ہر رن پر جو چیز واقعی ثابت کرتا ہے:

  • Windows پر سمولیشن اینٹیٹی کی سطح پر رن بہ رن ڈیٹرمنسٹک ہے؛
  • fixture کی آخری منظوری کے بعد سے گیم کا رویہ نہیں بدلا۔

دوسری ضمانت وہی نکلی جو میں نے سب سے زیادہ استعمال کی۔ ستمبر اور نومبر کے درمیان Windows fixtures کو بہت سے الگ الگ کمٹس میں دوبارہ منظور کیا گیا، اور ہر کمٹ کا نام اس جان بوجھ کر کی گئی سمولیشن تبدیلی پر تھا جس کی وجہ سے ایسا ہوا: seed ملانے کی ایک اصلاح، flow-field کی دوبارہ تحریر، waves میں تنوع، دشمنوں کی کثافت پر کارکردگی کا ایک دور۔ ان میں سے کوئی بھی تبدیلی خاموشی سے رویے کی کوئی تبدیلی اندر نہیں لا سکتی تھی۔

گیم جو کراس پلیٹ فارم دعوے کرتی ہے وہ دوسرے ثبوتوں پر قائم ہیں: سمولیشن میں fixed-point حساب، اور بعد میں جمع کرائے گئے رنز کی سرور پر دوبارہ سمولیشن۔ وہ الگ کہانیاں ہیں۔

بعد میں جو دو چیزیں غلط ہوئیں، اور ان کی اصلاح

Inconclusive کا مطلب پاس نہیں۔ جب موازنے کے لیے کوئی دوسرا OS نہ ہو تو peer ٹیسٹ Inconclusive رپورٹ کرتا تھا۔ یہ بے ضرر لگتا تھا، مگر Unity کا batch ٹیسٹ رنر Inconclusive کو ناکامی والے exit code سے جوڑتا ہے۔ صرف Windows والے سیٹ اپ پر صفر ناکامیوں والا سویٹ بھی غیر صفر exit کرتا تھا۔ جون میں peer ٹیسٹ کو بدلا گیا تاکہ peers نہ ہونے کی صورت میں وہ ایک وضاحتی پیغام کے ساتھ پاس ہو۔

fixture کمٹ ہوتے وقت ہی پرانا ہو سکتا ہے۔ جون میں PlayMode کے دونوں self-baseline ٹیسٹ سرخ ہو گئے۔ کچھ بھی دوبارہ بنانے سے پہلے اسے ٹھیک کرنے والے agent نے تصدیق کی کہ الگ الگ Unity پروسیسز میں تازہ ہیشز بالکل یکساں ہیں اور fixtures کے کمٹ کے بعد سمولیشن کا کوئی کوڈ نہیں بدلا: وہ پہلے کے کام کے دوران پرانی حالت میں کمٹ ہو گئے تھے۔ اس کے بعد ہی انہیں واضح regenerate ٹیسٹ سے دوبارہ بنایا گیا۔ پہلے دوبارہ بنانے سے یہ ثبوت مٹ جاتا کہ سمولیشن واقعی بہکی تھی یا نہیں۔

جو بات عام طور پر لاگو ہوتی ہے

وہ حالت ہیش کریں جو آپ واقعی محفوظ کرتے ہیں۔ fixed-point کی خام قدروں نے موازنے کو بالکل درست بنایا۔ float کو رواداری (tolerance) چاہیے (HP کی گولائی ایسی ہی ایک ہے)، اور رواداری وہ جگہ ہے جہاں بگ چھپتے ہیں۔

ہر ہیش کی جانے والی فہرست کو مکمل ترتیب چاہیے۔ "ID سے ترتیب" تبھی ترتیب ہے جب IDs منفرد ہوں۔ کسی sort پر بھروسہ کرنے سے پہلے مختلف کلیدوں کو گن لیں۔

بیس لائن کی دوبارہ ترتیب کو ایک واضح عمل بنائیں۔ golden-file ٹیسٹ جو ہر رن پر اپنی ہی فائل دوبارہ لکھ دے، کچھ ثابت نہیں کرتا۔ ایک الگ، اختیاری regenerate مرحلہ ہر بیس لائن تبدیلی کو جائزے میں نمایاں رکھتا ہے۔

جو ابھی ثابت نہیں ہوا اسے نام دیں۔ ہارنس ڈیزائن میں کراس پلیٹ فارم ہے اور ثبوت میں ایک ہی پلیٹ فارم کا۔ یہ کہنا ہی "ڈیٹرمنزم کی تصدیق شدہ" کے بیج کو دیانت دار رکھتا ہے۔