लेख

लेख · · 15 मिनट का पाठ

मैंने AI agent को Amiga की floppy disks दीं। एक शाम बाद Pirates! Unity में चल रहा था

मैंने एक coding agent को 1990 के Amiga वाले Sid Meier's Pirates! की चार floppy disk images दीं और कहा कि गेम को Unity में ले आए। काम Claude Code में Claude Opus 5.5 ने किया। एक शाम के बाद गेम को title screen से लेकर duel, शहर और नक्शे पर समुद्री सफ़र से होते हुए समुद्री लड़ाई तक खेला जा सकता था, वह भी मूल graphics और sound के साथ। रफ़्तार से ज़्यादा अहम यह है कि नतीजे की जाँच कैसे हुई: फ़ैसलों की एक ही script को C# port पर भी चलाया गया और processor emulator में चल रहे मूल machine code पर भी। 802 runs में, यानी तुलना की गई करीब 60 लाख lines में, दोनों versions ने खिलाड़ी को एक ही चीज़ दिखाई।

मैं यहाँ इस काम के चरण इसलिए लिख रहा हूँ कि ये मिलकर एक ऐसा तरीका बनाते हैं जिसे किसी दूसरे गेम और किसी दूसरे model के साथ दोहराया जा सकता है। आख़िर में मैं इसी ढाँचे पर बना दूसरा project दिखाता हूँ: Centurion: Defender of Rome का DOS से JavaScript में port। दोनों सीखने के लिए बनाए गए निजी projects हैं: मूल गेम, उनके graphics और उनका sound copyright के तहत ही हैं, और मैं इन्हें बाँटता नहीं हूँ।

Unity port में Pirates! की title screen: सूर्यास्त के समय समुद्री डाकुओं का झंडा फहराता galleon, 1990 के मूल graphics में बना हुआ।

Unity में title screen। तस्वीर गेम की मूल files से run time पर decode होती है।

Disk image से decompiled code तक

Agent ने शुरुआत files को पहचानने से की। Amiga की disk image (ADF) sectors की कच्ची नक़ल होती है, इसलिए उसके अंदर की चीज़ें निकालने के लिए Python की कुछ दर्जन lines काफ़ी थीं। गेम का program एक overlay executable निकला: उसका मुख्य हिस्सा memory में बना रहता है, और code के तीन और segments ज़रूरत पड़ने पर disk से पढ़े जाते हैं। Ghidra, जो एक मुफ़्त decompiler है, इस format को अपने आप नहीं समझता। Agent ने एक script लिखी जो सारे segments को एक ही image में तय addresses पर रखती है और overlays में जाने वाले 45 indirect jumps को सामान्य calls में बदल देती है। इसके बाद decompiler को करीब 630 functions एक ही program की तरह दिखे।

गेम के data में एक हैरानी छिपी थी। .rez extension वाली files की बनावट Macintosh resources जैसी है, क्योंकि Amiga वाला version ख़ुद Mac से किया गया port था। Format पहचान में आते ही agent ने 210 texts, strings की 82 lists, छह युगों के शहरों का data, तस्वीरें, animations, sprites और sounds निकाल लिए। Code का सबसे अच्छा नक्शा ये texts ही साबित हुए: "text दिखाओ" वाली routine को जो string number दिया जाता है, उसी से पता चल जाता है कि कोई function गेम की कौन-सी screen सँभालता है।

तेरह agents code पढ़ते हैं, नौ उसे दोबारा लिखते हैं

Decompiled code करीब 29 हज़ार lines का था, और उसमें functions या variables के कोई नाम नहीं थे। मुख्य agent ने उसे करीब 2 हज़ार lines के टुकड़ों में काटा और हर टुकड़ा एक अलग agent को एक ही लिखित brief के साथ दिया। हर agent को एक file लिखनी थी जिसमें उसके टुकड़े के हर function का ब्योरा हो: सटीक formulas, constants, random ranges, दिखाए जाने वाले texts के numbers, और पढ़े व लिखे जाने वाले global variables। Brief में एक वाक्य था जो आगे चलकर अहम साबित हुआ: ग़लत formula उस कमी से बदतर है जिस पर निशान लगा दिया गया हो। इसलिए agents को वह सब चिह्नित करना था जो वे तय नहीं कर पाए, और अस्पष्ट जगहों को assembly में जाँचना था। यहाँ decompiler ने functions के बहुत-से arguments खो दिए थे, क्योंकि 35 साल पुराना compiler 16-bit वाला int type इस्तेमाल करता था।

दूसरे चरण में भी बँटवारा वैसा ही था। नौ agents ने गेम के नियम C# में दोबारा लिखे, हर एक ने अपनी address range के लिए। इससे करीब 16 हज़ार lines का code बना, और गेम की state एक byte array में गई जिसे ठीक उसी तरह address किया जाता है जैसे मूल गेम की memory को। यह देखने में सुंदर नहीं लगता, लेकिन गलतियों की एक पूरी श्रेणी ख़त्म कर देता है। पुराना code structures को बेझिझक एक-दूसरे के ऊपर रख देता है और tables को उनकी शुरुआत से एक byte पहले से पढ़ लेता है। Memory image पर बने port को line दर line translate करके assembly से मिलाया जा सकता है, और constants की tables वहीं रहती हैं जहाँ code उन्हें ढूँढता है। Drawing, sound और controls की जगह एक interface के calls आ गए, जिसे engine अलग से implement करता है।

Oracle: processor emulator में मूल code

Decompiled code पढ़ लेने से यह साबित नहीं होता कि port मूल गेम जैसा ही बर्ताव करता है। इसलिए agent ने तुलना करने वाला एक tool बनाया। Motorola 68000 processor का मूल machine code Unicorn emulator में चलता है। System routines, यानी menus, text boxes, input पढ़ना और drawing, की जगह Python functions लगाए गए। ये functions गेम के हर सवाल का जवाब एक सीधे-सादे decision generator के हिसाब से देते हैं और गेम ने जो कुछ दिखाया उसे दर्ज करते जाते हैं। C# port को फ़ैसलों का ठीक वही क्रम एक "scripted player" के ज़रिए मिलता है, जो Unity के बाहर चलता है। आख़िर में दोनों records की line दर line तुलना होती है। Texts में सोना, crew की संख्या, तारीखें और शहरों के नाम होते हैं, इसलिए records का मेल खाना नियमों की एक मज़बूत परीक्षा है।

पहले पूरे माप में 802 में से 800 runs मेल खाए। दोनों गड़बड़ियों की वजह एक ही थी: floating-point arithmetic। गेम एक software library इस्तेमाल करता है जो आज के processors की तरह round नहीं करती। उसमें 81 का वर्गमूल 8.999999999999998 आता है, और गेम नतीजों को काटकर पूर्णांक बना देता है, इसलिए सोने और crew के कुछ अनुपातों पर port में crew का morale एक अंक ज़्यादा निकल रहा था। इलाज यह था कि library की routines को instruction दर instruction दोबारा लिखा जाए: 1,572 instructions, 43 routines, और मूल code से निकाले गए 21,701 test vectors, जिनमें कोई अंतर नहीं मिला। इस बदलाव के बाद सभी 802 runs मेल खाते हैं, और 120 अतिरिक्त runs में duel, समुद्री सफ़र और समुद्र व ज़मीन की लड़ाइयों में गेम की state को frame दर frame मिलाया गया।

तुलना से 1990 के मूल गेम के दो bugs भी सामने आए: किसी शहर पर तिरछी दिशा से हमला करते समय null pointer के ज़रिए नक्शे को पढ़ना, और crew के शून्य हो जाने पर शून्य से भाग। ऐसी जगहों पर port को कोई एक बर्ताव चुनना पड़ता है, और तुलना वाले tool में एक switch है जो मूल code को भी उसी रास्ते पर चला देता है। नतीजे की सीमाएँ भी हैं। Tool न drawing की तुलना करता है, न sound की, और शादी, पूरा किया गया ख़ज़ाने का नक्शा या सबसे ऊँचे ranks जैसे दुर्लभ रास्ते बहुत कम परखे गए।

Presentation और वे चीज़ें जो 1990 में नहीं थीं

जो layer खिलाड़ी को दिखती है, वह Unity में code से बनाई गई, बने-बनाए scenes के बिना। तस्वीरें और animations मूल resources से decode होते हैं, sprites अपने रंग मौजूदा screen की palette से लेते हैं, और music को गेम के चार-channel वाले sequencer को emulate करके audio files में render किया गया। Agent चलते हुए गेम को command line से चलाता था, screenshots लेता था और उन्हें देखता था। इससे कई ऐसी खामियाँ पकड़ में आईं जिन्हें नियमों का कोई test नहीं पकड़ता, जैसे browser version में sound का न होना, क्योंकि audio conversion के लिए ffmpeg program चाहिए था और वह install नहीं था।

Unity port में Caribbean का नक्शा: शहरों वाले द्वीप, दिशा के pointer के साथ खिलाड़ी का जहाज़, और तूफ़ानी बादल।

नक्शे पर समुद्री सफ़र। Background 160 गुणा 200 cells की terrain grid और मूल tiles से बना है।

पहली बार खेलने के बाद मैंने तीन बदलाव करवाए। ख़ज़ाने वाले बेड़े के पहुँचने की तारीख वाला सवाल मूल गेम का copy protection था और उसके लिए छपा हुआ manual चाहिए था; अब quiz बनी हुई है, लेकिन सही जवाब "कप्तान के notes" में दिखता है। शहरों की list सिर्फ़ शहर का data दिखाती थी, क्योंकि गेम मानकर चलता था कि खिलाड़ी के पास छपा हुआ नक्शा है; अब चुना हुआ शहर Caribbean के chart पर चिह्नित होता है। Duel screen को दोनों पक्षों की force, morale और weapon वाली एक table मिली, जिसका layout मूल गेम जैसा है। इन additions को बंद किया जा सकता है, और तुलना वाले tests इनके बिना चलते हैं, इसलिए records अब भी 1990 के code से मेल खाते हैं।

मूल Amiga गेम का screenshot: एक दीवार के सामने दो तलवारबाज़, और नीचे duel का शीर्षक तथा Force, Morale और Weapon की table।

मूल गेम में duel। Screenshot का स्रोत: OpenRetro Game Database।

Unity port में duel: जहाज़ के deck पर तलवारबाज़, और नीचे शीर्षक तथा दोनों पक्षों की force, morale और weapon की table।

वही screen port में। Table के आँकड़े duel की state से live पढ़े जाते हैं।

Caribbean का chart जिस पर Port-de-Paix शहर चिह्नित है, खिलाड़ी की स्थिति, और शहर के data व फ़्रांसीसी झंडे वाला एक box।

मूल गेम में जोड़ी गई चीज़: Caribbean के chart पर चुना हुआ शहर।

Centurion: वही ढाँचा JavaScript में, बढ़ाए गए gameplay के साथ

दूसरा project है Centurion: Defender of Rome, 1990 का एक DOS गेम, जिसे बिना किसी emulator के JavaScript और Canvas element पर लाया गया। चरण वही थे, बस नाम अलग थे। Executable को पहले unpack करना पड़ा, क्योंकि वह EXEPACK tool से compress किया गया था। गेम के पाँच modules (ज़मीनी लड़ाई, gladiator arena, रथ दौड़, नौसैनिक लड़ाई और intro) एक overlay file में थे और एक ही data segment साझा करते थे। Campaign की state उस segment के 64 kilobytes की एक जीवित copy पर चलती है, क्योंकि गेम का engine 16-bit overflow, integer division और signed bytes पर टिका है, जिन्हें JavaScript के floating-point numbers अपने आप नहीं दोहराते। Save किया हुआ गेम मूल format से byte दर byte मेल खाता है, और tests इसकी जाँच करते हैं।

Browser port में Centurion की title screen: भूमध्य सागर के नक्शे के ऊपर CENTURION शब्द, credits का panel, और Start Game, Difficulty तथा Game Options के buttons।

Browser में Centurion की title screen, मूल graphics के साथ।

यहाँ मैं हूबहू port से आगे गया। मैंने बढ़ाए गए नियमों के लिए एक switch जोड़ा, जो default रूप से बंद रहता है: legions को मिलाना और एक province में कई legions, हल्के velites, युद्ध के हाथी और समुद्र से landing। इस port का code Pirates वाले models से अलग models ने लिखा: ज़्यादातर Gemini 3.8 Flash ने, और कुछ हिस्सा Codex के ज़रिए चलाए गए OpenAI के gpt-6.1-sol ने। यह चुनाव उन tools से तय हुआ जो मेरे पास मौजूद थे, models की किसी तुलना से नहीं।

Centurion port में game options का menu: Extra Features: OFF वाली entry, difficulty level, save और load, और बगल में rank व ख़ज़ाने के साथ Senate का mandate।

Options menu। पहली entry उन बढ़ाए गए नियमों को चालू करती है जो मूल गेम में नहीं थे।

Centurion port में Italia province की screen: शहर का दृश्य, बनाए जाने वाले amphitheatre के साथ games का menu, और province की स्थिति: मिज़ाज, tribute, सैनिक।

Games के menu के साथ province की screen।

Centurion में Gemini को किन दिक्कतों का सामना करना पड़ा

ज़्यादातर समय उन जगहों पर गया जहाँ decompiled code या files के नाम कुछ और इशारा करते थे और गेम असल में कुछ और करता था। Project के notes से पाँच उदाहरण:

  • बिना data के overlays। गेम के पाँचों modules एक ही file, OVL0.OVL, में रहते हैं और एक ही address पर load होते हैं, लेकिन सभी एक साझा data segment इस्तेमाल करते हैं। इसलिए किसी overlay का code अकेले Ghidra में खोलने पर न strings दिखती थीं, न tables। एक script ने हर overlay को data segment के साथ जोड़कर पाँच synthetic executables बनाए, और तभी जाकर decompiler ने minigames के code को campaign के structures से जोड़ा।
  • जीते हुए provinces की सीमाएँ। Mouse से click होने वाले regions के नक्शे से province की रूपरेखा खींचने पर दाँतेदार रेखाएँ बनती थीं, जो हाथ से बने नक्शे पर ठीक नहीं बैठती थीं। Code में एक अलग ही तरीका निकला: नक्शे की तस्वीर में हर province का गहरे रंग का, एक pixel चौड़ा छिपा हुआ path है, और data की एक table 27 provinces में से हर एक के लिए एक शुरुआती बिंदु देती है। गेम उस path को शुरुआती बिंदु से शुरू करके साम्राज्य के रंग में दोबारा रंग देता है, और port भी यही करता है।
  • पीछे की ओर खिसकते frames। मूल game loop 9–12 Hz पर चलता था, जबकि browser एक second में 60 बार draw करता है। तीन जगहों पर sprite एक पल के लिए एक frame पीछे चला जाता था। Intro वाला globe 10 steps में palette के 14 रंग खिसकाकर घूमता है, लेकिन table में पहले step का मान शून्य है; हर step पर palette खिसकाने से महाद्वीप हर चक्कर में एक बार झटका खाते थे। Marching करती cohorts और gladiators के वारों में क्रम को हूबहू दोहराना पड़ा: पहले बीच वाला frame, फिर स्थिति का बदलाव।
  • भ्रामक file names। BB1.CBM और BB2.CBM bitmaps लड़ाई के banners जैसे लगते थे, और screen के ऊपरी हिस्से में draw करने पर पूरे युद्धक्षेत्र को ढक लेते थे। Code इन्हें तभी load करता है जब computer में VGA card न हो: ये पुराने EGA और CGA cards के लिए पूरी screen वाले backgrounds हैं। इसी routine से यह भी पता चला कि लड़ाई से पहले की तैनाती के दो चरण होते हैं। Formation चुनने पर उपलब्ध tactics की list बदल जाती है, और Cannae वाला दाँव wedge चुनने के बाद ही दिखता है।
  • मुश्किल से सुनाई देने वाला music। AdLib sound card के driver के 17 tracks और 24 instruments decode करने के बाद धुनें सही थीं, लेकिन करीब −55 dB पर बज रही थीं। Driver को disassemble करने पर पता चला कि वह हर note पर, instrument में दर्ज attenuation में से note की velocity का एक चौथाई हिस्सा घटाता है। Code की एक line ने करीब 32 dB वापस ला दिए। दूसरी खामी browser की तरफ़ थी: बहुत छोटे notes पर Web Audio API में पहले से schedule किए गए volume changes को रद्द करने से वह attack भी मिट जाता था जो अभी चल ही रहा था, और note चुप हो जाता था।

Centurion port में ज़मीनी लड़ाई: wedge formation के लिए पाँच tactics की list, जिसमें Cannae Tactic भी है, और युद्धक्षेत्र पर तैनात टुकड़ियाँ।

लड़ाई से पहले तैनाती का दूसरा चरण: wedge formation के लिए उपलब्ध tactics।

Circus Maximus की रथ दौड़ अपने आप में एक अलग काम थी। वहाँ रथ कोई एक तस्वीर नहीं है। रथ की तीन classes, सारथी के चार रंग, चार actions, मोड़ में झुकाव के सोलह कोण और सरपट दौड़ के तीन frames, इन सबके हर मेल के लिए अलग bitmap रखने का मतलब होता 2,300 से ज़्यादा frames, इसलिए गेम बनाने वालों ने memory में 16-byte वाले nodes का एक tree बनाया। हर node उसी level के अगले element की ओर और अपने पहले child की ओर इशारा करता है, और उसमें एक frame index, animation के एक के बाद एक आने वाले frames के offsets की table, और visibility का एक flag होता है। Root चार घोड़े हैं। रथ के hull में एक अतिरिक्त horizontal offset है, जिसमें मोड़ काटने की physics 0 से 15 pixels का झुकाव लिखती है, और सारथी उसका child है, इसलिए उसे वही स्थिति विरासत में मिलती है। यही वजह है कि मोड़ पर सारथी hull के साथ झुकता है, जबकि घोड़े track की धुरी पर ही दौड़ते रहते हैं। टूटते axle और लुढ़कते मलबे वाली शाखाएँ हर समय मौजूद रहती हैं; बस उनका visibility flag बंद रहता है।

Centurion में रथ के sprite tree का diagram: चार घोड़ों वाला node, तीन classes में hull का node, crash की states वाले nodes, सारथी के चार animations, और बिना झुकाव तथा 12 pixels के झुकाव के साथ जोड़ा गया पूरा रथ।

गेम के data से दोबारा तैयार किया गया रथ का sprite tree। Diagram इसी project में मूल, decode किए गए resources से बनाया गया।

Track भी tiles से नहीं बना है। गेम coordinates के 72 stored जोड़ों को फैलाकर 368 control points बनाता है और track की चौड़ाई में स्थिति को 16 lanes में बाँटता है। Lane से मोड़ की सुरक्षित speed तय होती है, जिसका हिसाब (lane number + 15) × 4 से लगता है: अंदरूनी दीवार के पास यह 60 है, बाहरी barrier के पास 116–120। अंदर वाली lane सबसे छोटा रास्ता है, लेकिन इस सीमा से ऊपर की speed पर hairpin में घुसने से hull झुक जाता है, रथ बाहर की ओर धकेला जाता है और crash के लिए पासा फेंका जाता है; सीमा जितनी ज़्यादा लाँघी जाए, crash की संभावना उतनी बढ़ती जाती है। Speed bar पर लाल marker खिलाड़ी को यही सीमा दिखाता है।

Circus Maximus के track की geometry: stadium की तस्वीर पर रंगों से चिह्नित सीधे हिस्से, braking zones और hairpins, और सुरक्षित speed का chart जो अंदर वाली lane में 60 से बढ़कर बाहर वाली में 120 तक जाता है।

Track के 368 points और lane के हिसाब से मोड़ की सुरक्षित speed।

Games के अलावा इस काम से क्या बचा

मैंने इस तरीके को एक skill के रूप में लिख लिया है, यानी निर्देशों का ऐसा set जिसे agent मिलता-जुलता काम मिलने पर load कर लेता है। इसमें दोनों projects से निकले तीन सिद्धांत हैं। गेम के नियम हूबहू port किए जाते हैं, और presentation की जगह एक interface आता है। Port मूल गेम का memory layout बनाए रखता है। नतीजे पर तब तक भरोसा नहीं किया जाता जब तक वह मूल code के साथ तुलना में पास न हो जाए। Skill में analysis और porting करने वाले agents के briefs और Pirates project की scripts भी शामिल हैं।

सबसे दिलचस्प नतीजा agents के बारे में नहीं, fans के बारे में है। जिसके पास कोई पसंदीदा गेम है और जिसे उसमें बरसों से किसी चीज़ की कमी खलती रही है, वह आज source code के बिना, जल्दी और कम ख़र्च में उसे अपने हिसाब से बदल सकता है। Pirates में जिस quiz के लिए छपा हुआ manual चाहिए था, वह अब ख़ुद जवाब बता देता है, और एक नक्शा जुड़ गया जो गेम में कभी था ही नहीं। Centurion में मैंने वे नियम जोड़े जो original में नहीं थे: legions को मिलाना, जंगी हाथी और समुद्र से उतरना। जब गेम के नियम C# या JavaScript का आम code बन जाते हैं, तो अगला बदलाव code का एक आम बदलाव होता है, binary को patch करना नहीं। इसी तरह गेम को किसी तेज़ engine या दूसरे platform पर भी ले जाया जा सकता है: Centurion browser में चलता है, और Pirates Linux और browser के लिए build होता है। यह तरीक़ा सिर्फ़ पुराने गेम तक सीमित भी नहीं है। Decompiler, processor emulator और records की तुलना किसी नए program पर भी वैसे ही काम करते हैं; ज़्यादा code का मतलब ज़्यादा काम है, कोई अलग तरीक़ा नहीं।

स्रोत

  • दोनों projects की मेरी अपनी सामग्री: code analysis के notes, agents के लिए briefs, तुलना वाले tool की report (802 runs, मिले हुए अंतरों की list), और specifications के साथ Centurion port का विवरण। ये प्रकाशित नहीं हैं।
  • National Security Agency, Ghidra, reverse engineering का वह suite जिससे दोनों गेम decompile किए गए।
  • Unicorn Engine, वह processor emulator जिसमें मूल 68000 code चलाया गया।
  • OpenRetro Game Database, Amiga पर Pirates! का page, मूल गेम के screenshot का स्रोत।

मैं AI से तैयार की गई सामग्री को अपनी रोज़ की सीखने की प्रक्रिया के हिस्से के तौर पर इस्तेमाल करता हूँ।