#1 एक डिटरमिनिस्टिक कोर, जिसे कंपाइलर लागू करता है
· Nick
अभी देखने को कुछ नहीं है, इसलिए यह पहली पोस्ट उस हिस्से के बारे में है जिसे कोई नहीं देखेगा: सिमुलेशन कोर। गेम को 20 टिक प्रति सेकंड पर 1,000 तक क़ैदी और स्टाफ़ चलाने हैं और यह समझाना है कि किसी ने कुछ भी क्यों किया। यह तभी हो सकता है जब एक जैसे इनपुट हमेशा एक जैसी दुनिया दें। मैंने तय किया कि पहले दिन से ही इसे ऐसी ख़ूबी बनाऊँगा जिसकी जाँच मशीन करे।
अभी कहाँ हैं
मैं पहले माइलस्टोन में हूँ, कोर का ढाँचा। सिमुलेशन एक सादी .NET लाइब्रेरी है जिसमें Godot का कोई कोड नहीं है, और Godot 4 क्लाइंट अभी सिर्फ़ एक ख़ाली 3D सीन खोलता है। न दुनिया का मॉडल है, न टाइलें, न किरदार, न सेव। 1,000 किरदारों का बजट एक लक्ष्य है; उस पैमाने पर मैंने अभी कुछ नहीं मापा है।
टिक
कोर में समय टिकों की एक पूर्णांक गिनती है, 20 प्रति सेकंड। फ़्लोटिंग-पॉइंट समय नहीं है। एक टिक तीन चरणों में चलता है: बाहर से आए कमांड लागू करना, सिस्टम को एक ही जगह लिखे क्रम में चलाना, फिर टाले गए बदलाव पक्के करना और काउंटर आगे बढ़ाना। अगर कोई एरर टिक से बाहर निकल जाए, तो दुनिया को ख़राब चिह्नित कर रोक दिया जाता है। आज सिस्टम की सूची ख़ाली है; लूप इसलिए है कि पहले सिस्टम के पास जाने की जगह हो।
कमांड और एक लॉग
दुनिया सिर्फ़ कमांड से बदलती है। कमांड एक छोटा रिकॉर्ड है, जिसकी एक स्थिर टाइप id और हाथ से लिखी बाइनरी एन्कोडिंग होती है। हर कमांड के लिए दुनिया उसे एन्कोड करती है, उस पर टिक और क्रम संख्या की मुहर लगाती है, उसे जाँचती है (सिर्फ़ पढ़कर), लॉग में लिखती है, और तभी लागू करती है। अस्वीकार किए गए कमांड भी लॉग होते हैं, जिससे बग रिपोर्ट में मदद मिलनी चाहिए। सीड और लॉग मिलकर किसी रन को हूबहू दोहराने के लिए हैं। रीप्ले फ़ाइल का फ़ॉर्मेट अभी नहीं लिखा गया है।
रैंडम नंबर
रैंडम नंबर जनरेटर मैंने ख़ुद लिखे (SplitMix64 और xoshiro256**) और कोर में स्टैंडर्ड लाइब्रेरी के Random पर रोक लगा दी। दुनिया में चार नाम वाली स्ट्रीम हैं (कमांड, ज़रूरतें, AI, इवेंट), जो सीड से बनती हैं और दुनिया की स्थिति में रखी जाती हैं, इसलिए वे सेव और हैश दोनों में शामिल होती हैं। जिस काम में क्रम मायने नहीं रखना चाहिए, उसके लिए डोमेन, एंटिटी और टिक पर आधारित एक बिना-स्थिति वाला की-आधारित जनरेटर है। सीड 42 के आउटपुट पक्के कर दिए गए हैं, क्योंकि असल में वही रीप्ले फ़ॉर्मेट है: जनरेटर बदलो और हर पुराना रीप्ले टूट जाएगा।
दुनिया का एक हैश
क्रम के प्रति संवेदनशील एक छोटा हैशर सीड, टिक, रैंडम स्ट्रीम और कमांड लॉग के सार को 64 बिट में समेट देता है। जो दो रन एक जैसे होने चाहिए, उनकी तुलना एक संख्या से हो सकती है। अभी यह सिर्फ़ इन्हीं हिस्सों को कवर करता है, क्योंकि दुनिया में और कुछ है ही नहीं। अगला क़दम है रन के दौरान हर N टिक पर हैश की तुलना करना।
नॉन-डिटरमिनिज़्म को बिल्ड एरर बनाना
मैं नियम याद रखने के भरोसे नहीं रहना चाहता था। कोर प्रतिबंधित API पकड़ने वाले एनालाइज़र के साथ बिल्ड होता है: दीवार-घड़ी का समय, बिना सीड के रैंडम नंबर, रैंडम GUID, रैंडमाइज़्ड स्ट्रिंग हैश, थ्रेड और पैरेलल हेल्पर, SIMD, और कल्चर पर निर्भर स्ट्रिंग API, ये सब बिल्ड को फ़ेल कर देते हैं। आगे की जाँचें पक्का करती हैं कि कोई बदलने वाली स्टैटिक स्थिति न हो, और कंपाइल हुए कोड में फिसलकर आई किसी भी चीज़ को खोजती हैं। मेरी मशीन रूसी लोकेल पर चलती है जिसमें दशमलव के लिए कॉमा है, जो कल्चर पर निर्भर API पर रोक लगाने की अच्छी वजह है।
फिर मैं और आगे गया और कोर से स्टैंडर्ड हैश कलेक्शन (Dictionary, HashSet और उनके साथी) हटा दिए, क्योंकि उनका इटरेशन क्रम भटकाव का पुराना स्रोत है। कोर को ऐरे, लिस्ट और दस्तावेज़ में लिखे क्रम वाले अपने दो छोटे कलेक्शन मिलते हैं। यह पूरी तरह अभेद्य नहीं है: जो API किसी जेनेरिक इंटरफ़ेस के ज़रिए हैश कलेक्शन लौटाए, वह अब भी निकल सकता है, और सुरक्षा का एक हिस्सा सिर्फ़ टेस्ट में काम करता है।
इसे कैसे बनाया जाता है
कोड के आसपास के टूल भी ऑटोमेटेड हैं, और वही स्क्रिप्ट Windows और macOS पर चलती हैं। सामान्य तरीक़ा कैसे बन रहा है पर है।
आगे
इस माइलस्टोन में अभी बाक़ी है: मंज़िलों वाला दुनिया का मॉडल, सेव और रीप्ले फ़ाइलें, डिटरमिनिज़्म टेस्ट, और बेंचमार्क बेसलाइन वाला बिना इंटरफ़ेस का सिनेरियो रनर। उसके बाद Godot का बिल्डिंग व्यू आएगा। देखें रोडमैप।