كيف علمت منظمة العفو الدولية لعب Tetris لـ NES. الجزء الأول: تحليل كود اللعبة

في هذه المقالة ، سأستكشف الآليات البسيطة الخادعة لـ Nintendo Tetris ، وفي الجزء الثاني سأشرح كيف أنشأت ذكاء اصطناعي يستغل هذه الميكانيكا.


جربها بنفسك


عن المشروع


بالنسبة لأولئك الذين يفتقرون إلى المثابرة والصبر والوقت اللازم لإتقان Nintendo Tetris ، قمت بإنشاء منظمة العفو الدولية التي يمكن أن تلعب من تلقاء نفسها. يمكنك أخيرًا الوصول إلى المستوى 30 وأكثر من ذلك. سترى كيفية الحصول على أقصى عدد من النقاط ومراقبة التغيير الذي لا نهاية له في عدادات الصف والمستويات والإحصائيات. سوف تتعلم الألوان التي تظهر في المستويات التي لا يستطيع الشخص تسلقها. انظر إلى أي مدى يمكن أن تذهب.

المتطلبات


لتشغيل AI ، تحتاج إلى محاكي NES / Famicom FCEUX عالمي . تم تطوير الذكاء الاصطناعي لـ FCEUX 2.2.2 ، وهو أحدث إصدار من المحاكي في وقت الكتابة.

ستحتاج أيضًا إلى ملف Nintendo Tetris ROM (الإصدار الأمريكي). حاول البحث عنه على جوجل .

تنزيل


قم بفك ضغط lua/NintendoTetrisAI.lua من ملف zip المصدر هذا.

إطلاق


إطلاق FCEUX. من القائمة ، حدد ملف | فتح ROM ... في مربع الحوار فتح ملف ، حدد ملف Nintendo Tetris ROM وانقر فوق فتح. ستبدأ اللعبة.

من القائمة ، حدد ملف | لوا | نافذة Lua Script جديدة ... في نافذة Lua Script ، أدخل المسار إلى NintendoTetrisAI.lua أو انقر فوق الزر "استعراض" للعثور عليه. بعد ذلك ، انقر فوق تشغيل.

سيقوم البرنامج النصي الموجود في Lua بإعادة توجيهك إلى الشاشة الأولى من القائمة. اترك نوع اللعبة A-Type ، ويمكنك اختيار أي موسيقى. على أجهزة الكمبيوتر البطيئة ، يمكن تشغيل الموسيقى بفظاظة شديدة ، ثم يجب إيقاف تشغيلها. اضغط على Start (Enter) للانتقال إلى شاشة القائمة التالية. في القائمة الثانية ، يمكنك استخدام مفاتيح الأسهم لتغيير مستوى البدء. انقر فوق ابدأ لبدء اللعبة. وهنا سيتحكم الذكاء الاصطناعي.

إذا قمت بعد تحديد مستوى في شاشة القائمة الثانية ، فاضغط مع الاستمرار على زر لوحة الألعاب A (يمكنك تغيير تخطيط لوحة المفاتيح في قائمة Config | Input ...) واضغط على Start ، عندئذٍ سيكون المستوى الأولي 10 أكثر من القيمة المحددة. مستوى الدخول الأقصى هو التاسع عشر.

التكوين


لجعل اللعبة تعمل بشكل أسرع ، افتح برنامج Lua النصي في محرر نصوص. في بداية الملف ، ابحث عن السطر التالي.

PLAY_FAST = false

استبدل false true كما هو موضح أدناه.

PLAY_FAST = true

احفظ الملف. ثم انقر فوق زر إعادة التشغيل في نافذة Lua Script.

ميكانيكا نينتندو تتريس


وصف Tetrimino


يتطابق كل شكل من أشكال tetrimino مع اسم من حرف واحد يشبه شكله.


يقوم مصممو Nintendo Tetris بتعيين أمر tetrimino الموضح أعلاه بشكل تعسفي. تظهر الأرقام في الاتجاه الذي تظهر فيه على الشاشة ، وتخلق الدائرة صورة متماثلة تقريبًا (ربما هذا هو سبب اختيار هذا الترتيب). يعطي مؤشر التسلسل لكل tetrimino معرف رقمي فريد. تعتبر معرفات التسلسل والنوع مهمة على مستوى البرمجة ؛ بالإضافة إلى ذلك ، فإنها تظهر في ترتيب الأرقام المعروضة في مجال الإحصائيات (انظر أدناه).


تم ترميز الاتجاهات الـ 19 المستخدمة في Nintendo Tetris tetrimino في جدول يقع على $8A9C من ذاكرة وحدة التحكم في NES. يتم تمثيل كل شكل كتسلسل من 12 بايت يمكن تقسيمه إلى ثلاثيات (Y, tile, X) تصف كل مربع في الشكل. تشير القيم الست عشرية للإحداثيات أعلاه $7F إلى أعداد صحيحة سالبة ( $FF= −1 و $FE = −2 ).

; Y0 T0 X0 Y1 T1 X1 Y2 T2 X2 Y3 T3 X3

8A9C: 00 7B FF 00 7B 00 00 7B 01 FF 7B 00 ; 00: T up
8AA8: FF 7B 00 00 7B 00 00 7B 01 01 7B 00 ; 01: T right
8AB4: 00 7B FF 00 7B 00 00 7B 01 01 7B 00 ; 02: T down (spawn)
8AC0: FF 7B 00 00 7B FF 00 7B 00 01 7B 00 ; 03: T left

8ACC: FF 7D 00 00 7D 00 01 7D FF 01 7D 00 ; 04: J left
8AD8: FF 7D FF 00 7D FF 00 7D 00 00 7D 01 ; 05: J up
8AE4: FF 7D 00 FF 7D 01 00 7D 00 01 7D 00 ; 06: J right
8AF0: 00 7D FF 00 7D 00 00 7D 01 01 7D 01 ; 07: J down (spawn)

8AFC: 00 7C FF 00 7C 00 01 7C 00 01 7C 01 ; 08: Z horizontal (spawn)
8B08: FF 7C 01 00 7C 00 00 7C 01 01 7C 00 ; 09: Z vertical

8B14: 00 7B FF 00 7B 00 01 7B FF 01 7B 00 ; 0A: O (spawn)

8B20: 00 7D 00 00 7D 01 01 7D FF 01 7D 00 ; 0B: S horizontal (spawn)
8B2C: FF 7D 00 00 7D 00 00 7D 01 01 7D 01 ; 0C: S vertical

8B38: FF 7C 00 00 7C 00 01 7C 00 01 7C 01 ; 0D: L right
8B44: 00 7C FF 00 7C 00 00 7C 01 01 7C FF ; 0E: L down (spawn)
8B50: FF 7C FF FF 7C 00 00 7C 00 01 7C 00 ; 0F: L left
8B5C: FF 7C 01 00 7C FF 00 7C 00 00 7C 01 ; 10: L up

8B68: FE 7B 00 FF 7B 00 00 7B 00 01 7B 00 ; 11: I vertical
8B74: 00 7B FE 00 7B FF 00 7B 00 00 7B 01 ; 12: I horizontal (spawn)

8B80: 00 FF 00 00 FF 00 00 FF 00 00 FF 00 ; 13: Unused


يوجد في أسفل الجدول سجل واحد غير مستخدم ، ومن المحتمل أن يمنح الفرصة لإضافة اتجاه آخر. ومع ذلك ، في أجزاء مختلفة من الرمز ، يشير $13 إلى أن معرف الاتجاه لـ tetrimino النشط لم يتم تعيين قيمة له.

لسهولة القراءة ، يتم عرض إحداثيات المربعات في العلامة العشرية أدناه.

-- { { X0, Y0 }, { X1, Y1 }, { X2, Y2 }, { X3, Y3 }, },

{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, -1 }, }, -- 00: T up
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 01: T right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 02: T down (spawn)
{ { 0, -1 }, { -1, 0 }, { 0, 0 }, { 0, 1 }, }, -- 03: T left

{ { 0, -1 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 04: J left
{ { -1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 05: J up
{ { 0, -1 }, { 1, -1 }, { 0, 0 }, { 0, 1 }, }, -- 06: J right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 07: J down (spawn)

{ { -1, 0 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 08: Z horizontal (spawn)
{ { 1, -1 }, { 0, 0 }, { 1, 0 }, { 0, 1 }, }, -- 09: Z vertical

{ { -1, 0 }, { 0, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0A: O (spawn)

{ { 0, 0 }, { 1, 0 }, { -1, 1 }, { 0, 1 }, }, -- 0B: S horizontal (spawn)
{ { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 }, }, -- 0C: S vertical

{ { 0, -1 }, { 0, 0 }, { 0, 1 }, { 1, 1 }, }, -- 0D: L right
{ { -1, 0 }, { 0, 0 }, { 1, 0 }, { -1, 1 }, }, -- 0E: L down (spawn)
{ { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 0F: L left
{ { 1, -1 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 10: L up

{ { 0, -2 }, { 0, -1 }, { 0, 0 }, { 0, 1 }, }, -- 11: I vertical
{ { -2, 0 }, { -1, 0 }, { 0, 0 }, { 1, 0 }, }, -- 12: I horizontal (spawn)


يتم وضع جميع الاتجاهات في مصفوفة 5 × 5.


في الشكل أعلاه ، يشير المربع الأبيض إلى مركز المصفوفة ، وهي النقطة المرجعية لتدوير الشكل.

يتم عرض جدول التوجيه بيانياً أدناه.


يتم عرض معرف الاتجاه (فهرس الجدول) بالنظام الست عشري في الزاوية اليمنى العليا من كل مصفوفة. وتظهر فن الإستذكار الذي تم اختراعه لهذا المشروع في الزاوية اليسرى العليا. u و r و d و l و h و v هي اختصارات "للأعلى واليمين والأسفل واليسار والأفقي والرأسي". على سبيل المثال ، من الأسهل الإشارة إلى اتجاه Jd بدلاً من $07 .

يتم تمييز المصفوفات التي تحتوي على اتجاهات الأشكال أثناء الإنشاء بإطار أبيض.

يمكن إعطاء Tetrimino I و S و Z 4 اتجاهات منفصلة ، لكن مبدعي Nintendo Tetris قرروا أن يقتصروا على اثنين. بالإضافة إلى ذلك ، لا تعتبر Zv و Sv صورًا متطابقة مثالية لبعضهما البعض. يتم إنشاء كلاهما عن طريق عكس اتجاه عقارب الساعة ، مما يؤدي إلى اختلال التوازن.

يحتوي جدول التوجيه أيضًا على قيم تجانب لكل مربع في كل شكل موجه. ومع ذلك ، مع دراسة متأنية ، يصبح من الواضح أن قيم نوع واحد من tetrimino هي نفسها دائمًا.

تيضياقلامأنا
7B7D7C7B7D7C7B

قيم التجانب هي مؤشرات الجدول (اللون الزائف) للنموذج الموضح أدناه.


يتم وضع مربعات $7B و $7C و $7D أسفل "ATIS" مباشرة من كلمة "STATISTICS". هذه هي الأنواع الثلاثة من المربعات التي تصنع منها tetrimino.

للفضول ، سأقول أنه يتم استخدام النعام وطيور البطريق في نهاية وضع B-Type. تمت مناقشة هذا الموضوع بالتفصيل في قسم "إنهاء".

فيما يلي نتيجة تعديل ROM بعد استبدال $7B بـ $29 . القلب هو البلاط تحت الرمز P في جدول النمط لجميع الاتجاهات T.


يبقى بلاط القلب في الملعب حتى بعد تثبيت Ts المعدلة في مكانها. كما هو موضح أدناه في قسم "إنشاء Tetrimino" ، هذا يعني أن ملعب اللعب يخزن القيم الحقيقية لمؤشرات البلاط لـ Tetrimino التي تم لعبها.

جعل مبرمجو اللعبة من الممكن استخدام 4 مربعات منفصلة لكل شكل ، وليس نوعًا واحدًا ثابتًا من المربعات. هذه ميزة مفيدة يمكن استخدامها لتعديل مظهر اللعبة. يحتوي جدول النمط على الكثير من المساحة الفارغة للبلاط الجديد الذي يمكن أن يمنح كل tetrimino مظهرًا فريدًا.

من السهل جدًا معالجة إحداثيات المربعات. على سبيل المثال ، يتم عرض نسخة معدلة من الثلاثيات الأربع الأولى في جدول التوجيه أدناه.

8A9C: FE 7B FE FE 7B 02 02 7B FE 02 7B 02 ; 00: T up

هذا التغيير مشابه لما يلي:

{ { -2, -2 }, { 2, -2 }, { -2, 2 }, { 2, 2 }, }, -- 00: T up

والنتيجة هي tetrimino الانقسام.


عند تحريك tetrimino المقسوم ، لا يمكن أن تتجاوز مربعاته حدود ساحة اللعب ولا يمكن أن تمر من خلال الأرقام التي تم قفلها مسبقًا. بالإضافة إلى ذلك ، تمنع اللعبة الدوران في هذا الاتجاه إذا أدت إلى سقوط مربع خارج حدود الملعب أو إلى حقيقة أن المربع يتداخل مع مربع مستلقٍ بالفعل.

يتم قفل tetrimino المقسم في مكانه عندما يكون هناك دعم لأي من مربعاته. إذا تم حظر الشكل ، فستستمر المربعات المعلقة في الهواء في التعليق.

تعالج اللعبة تقسيم tetriminos مثل أي شخصية عادية. هذا يجعلنا نفهم أنه لا يوجد جدول إضافي لتخزين البيانات الوصفية للأشكال. على سبيل المثال ، قد يكون هناك جدول يخزن حجم المربع المحيط لكل اتجاه للتحقق من التصادمات مع محيط الملعب. ولكن لا يتم استخدام هذا الجدول. بدلاً من ذلك ، تقوم اللعبة ببساطة بفحص المربعات الأربعة مباشرةً قبل التلاعب بالشكل.

بالإضافة إلى ذلك ، يمكن أن تكون إحداثيات المربعات أي قيم ؛ لا تقتصر على الفاصل الزمني [−2, 2] . وبالطبع ، فإن القيم التي تتجاوز هذا الفاصل الزمني ستعطينا أرقامًا غير قابلة للتطبيق لا يمكن وضعها في الملعب. الأهم من ذلك ، كما هو مذكور في قسم "حالات اللعبة وأوضاع العرض" ، عندما يتم تثبيت الشكل في مكانه ، فإن آلية تنظيف الخطوط المملوءة تفحص فقط إزاحة الصفوف من −2 إلى 1 من المربع المركزي للشكل ؛ لن يتم التعرف على مربع y خارج هذا الفاصل الزمني.

تناوب تتريمينو


في رسم توضيحي لجدول التوجيه ، يتكون الدوران من الانتقال من مصفوفة إلى أحد المصفوفات على اليسار أو على اليمين مع نقل السلسلة إذا لزم الأمر. تم ترميز هذا المفهوم في جدول بسعر $88EE .

; CCW CW
88EE: 03 01 ; Tl Tr
88F0: 00 02 ; Tu Td
88F2: 01 03 ; Tr Tl
88F4: 02 00 ; Td Tu
88F6: 07 05 ; Jd Ju
88F8: 04 06 ; Jl Jr
88FA: 05 07 ; Ju Jd
88FC: 06 04 ; Jr Jl
88FE: 09 09 ; Zv Zv
8900: 08 08 ; Zh Zh
8902: 0A 0A ; OO
8904: 0C 0C ; Sv Sv
8906: 0B 0B ; Sh Sh
8908: 10 0E ; Lu Ld
890A: 0D 0F ; Lr Ll
890C: 0E 10 ; Ld Lu
890E: 0F 0D ; Ll Lr
8910: 12 12 ; Ih Ih
8912: 11 11 ; Iv Iv


لجعل الأمر أكثر وضوحًا ، سننقل كل عمود من هذا الجدول إلى صف الجدول أدناه.
TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
بعكس اتجاه عقارب الساعةTlTuTrTdJdJlJuJrZvZhOSvShLuLrLdLlIhIv
في اتجاه عقارب الساعةTrTdTlTuJuJrJdJlZvZhOSvShLdLlLuLrIhIv

يمكن تفسير الاستفهام في العناوين أعلاه كمؤشر تسلسل أو مفتاح توزيع. على سبيل المثال ، تحويل Tu عكس اتجاه عقارب الساعة يعطينا Tl ، وتحويل Tu باتجاه عقارب الساعة يعطي Tr .

يشفر جدول الدوران تسلسلات معرفات الاتجاه المرتبطة بالسلسلة ؛ لذلك ، يمكننا تعديل التسجيلات بحيث يحول الدوران نوعًا واحدًا من tetrimino إلى نوع آخر. يمكن استخدام هذه التقنية للاستفادة من صف غير مستخدم في جدول التوجيه.

أمام جدول الاستدارة هو رمز للوصول إليه.

88AB: LDA $0042
88AD: STA $00AE ; originalOrientationID = orientationID;

88AF: CLC
88B0: LDA $0042
88B2: ASL
88B3: TAX ; index = 2 * orientationID;

88B4: LDA $00B5
88B6: AND #$80 ; if (not just pressed button A) {
88B8: CMP #$80 ; goto aNotPressed;
88BA: BNE $88CF ; }

88BC: INX
88BD: LDA $88EE,X
88C0: STA $0042 ; orientationID = rotationTable[index + 1];

88C2: JSR $948B ; if (new orientation not valid) {
88C5: BNE $88E9 ; goto restoreOrientationID;
; }

88C7: LDA #$05
88C9: STA $06F1 ; play rotation sound effect;
88CC: JMP $88ED ; return;

aNotPressed:

88CF: LDA $00B5
88D1: AND #$40 ; if (not just pressed button B) {
88D3: CMP #$40 ; return;
88D5: BNE $88ED ; }

88D7: LDA $88EE,X
88DA: STA $0042 ; orientationID = rotationTable[index];

88DC: JSR $948B ; if (new orientation not valid) {
88DF: BNE $88E9 ; goto restoreOrientationID;
; }

88E1: LDA #$05
88E3: STA $06F1 ; play rotation sound effect;
88E6: JMP $88ED ; return;

restoreOrientationID:

88E9: LDA $00AE
88EB: STA $0042 ; orientationID = originalOrientationID;

88ED: RTS ; return;


للدوران بعكس اتجاه عقارب الساعة ، يتم طرح فهرس جدول التدوير بمضاعفة معرف الاتجاه. بإضافة 1 إليه نحصل على مؤشر دوران باتجاه عقارب الساعة.

يتم تخزين إحداثيات x و y ومعرف الاتجاه لـ tetrimino الحالي في العناوين $0040 و $0041 و $0042 على التوالي.

يستخدم الكود متغيرًا مؤقتًا لعمل نسخة احتياطية من معرف الاتجاه. في وقت لاحق ، بعد تغيير الاتجاه ، يتحقق الرمز من أن جميع المربعات الأربعة تقع داخل حدود مجال اللعب ولا يتداخل أي منها مع المربعات القائمة بالفعل (يقع رمز التحقق عند $948B ، أسفل جزء الرمز الموضح أعلاه). إذا كان الاتجاه الجديد غير صحيح ، فسيتم استعادة الاتجاه الأصلي ، ولا يسمح للاعب بتدوير الشكل.

مع العد بصليب ، تحتوي وحدة تحكم NES على ثمانية أزرار ، يتم تمثيل $00B6 بت العنوان $00B6 .

76543210
أبحددابدألأعلىللأسفلإلى اليسارإلى اليمين

على سبيل المثال ، $00B6 على القيمة $81 بينما يحمل اللاعب A و Left.

من ناحية أخرى ، $00B5 عن الضغط على الأزرار ؛ البتات $00B5 صحيحة فقط خلال تكرار واحد من حلقة اللعبة (إطار واحد). يستخدم الرمز $00B5 للاستجابة للضغط A و B. يجب تحرير كل واحد منهم قبل استخدامه مرة أخرى.

$00B5 و $00B6 عبارة عن مرايا $00F5 و $00F6 . يستخدم الكود الموجود في الأقسام التالية هذه العناوين بالتبادل.

إنشاء Tetrimino


يتكون ملعب Nintendo Tetris من مصفوفة بها 22 صفًا و 10 أعمدة بحيث يتم إخفاء الصفين العلويين من المشغل.


كما هو موضح في الكود أدناه ، عند إنشاء شخصية Tetrimino ، يتم وضعه دائمًا في إحداثيات (5, 0) الملعب.

98BA: LDA #$00
98BC: STA $00A4
98BE: STA $0045
98C0: STA $0041 ; Tetrimino Y = 0
98C2: LDA #$01
98C4: STA $0048
98C6: LDA #$05
98C8: STA $0040 ; Tetrimino X = 5


يوجد أدناه مصفوفة 5 × 5 متراكبة على هذه النقطة.


لا تحتوي أي من مصفوفات الإنشاء على مربعات فوق نقطة البداية. أي عند إنشاء tetrimino ، تصبح جميع مربعاتها الأربعة مرئية للاعب على الفور. ومع ذلك ، إذا قام اللاعب بتدوير القطعة بسرعة قبل أن يتاح لها الوقت لإسقاطها ، فسيتم إخفاء جزء من القطعة مؤقتًا في أول سطرين من الملعب.

عادة نعتقد أن اللعبة تنتهي عندما يصل كومة الذاكرة المؤقتة إلى القمة. لكن في الواقع ، هذا ليس صحيحًا تمامًا. تنتهي اللعبة عندما يتعذر إنشاء القطعة التالية. أي ، قبل ظهور الشكل ، يجب أن تكون جميع الخلايا الأربعة في الملعب المقابلة لمواقع مربعات tetrimino التي تم إنشاؤها خالية. قد يتم قفل الشكل في مكانه بحيث يظهر جزء من مربعاته في خطوط مرقمة بشكل سلبي ، ولا تنتهي اللعبة ؛ ومع ذلك ، في Nintendo Tetris ، الخطوط السالبة هي تجريد متعلق فقط بـ tetrimino النشط. بعد حجب الشكل (يصبح مستلقيا) ، تتم كتابة المربعات في سطور من الصفر وأكثر فقط إلى الحقل. من الناحية النظرية ، اتضح أن الخطوط ذات الأرقام السالبة يتم مسحها تلقائيًا بعد الحظر. ولكن في الواقع ، فإن اللعبة ببساطة لا تخزن هذه البيانات ، وتقطع الأجزاء العلوية من الأرقام.

يتم تخزين المساحة المرئية في مجال اللعب 20 × 10 بسعر $0400 خطًا بخط ، ويحتوي كل بايت على قيمة تجانب الخلفية. يُشار إلى الخلايا الفارغة بالبلاط $EF ، وهو مربع أسود خالص.

عند إنشاء شكل ، يتم استخدام ثلاثة جداول بحث. إذا كان هناك معرف اتجاه تعسفي ، فإن الجدول عند $9956 يعطينا معرف الاتجاه عند إنشاء النوع المقابل من tetrimino.

9956: 02 02 02 02 ; Td
995A: 07 07 07 07 ; Jd
995E: 08 08 ; Zh
9960: 0A ; O
9961: 0B 0B ; Sh
9963: 0E 0E 0E 0E ; Ld
9967: 12 12 ; Ih


من الأسهل إظهار ذلك في الجدول.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TdTdTdTdJdJdJdJdZhZhOShShLdLdLdLdIhIh

على سبيل المثال ، يتم ربط جميع اتجاهات J بـ Jd .

يحتوي الجدول في $993B على نوع Tetrimino لمعرف الاتجاه المحدد.

993B: 00 00 00 00 ; T
993F: 01 01 01 01 ; J
9943: 02 02 ; Z
9945: 03 ; O
9946: 04 04 ; S
9948: 05 05 05 05 ; L
994C: 06 06 ; I


للتوضيح ، سأعرض كل شيء في شكل جدولي.

TuTrTdTlJlJuJrJdZhZvOShSvLrLdLlLuIvIh
TTTTJJJJZZOSSLLLLII

سنلقي نظرة على جدول البحث الثالث في القسم التالي.

اختيار Tetrimino


تستخدم Nintendo Tetris سجل إزاحة الملاحظات الخطية 16 بت (LFSR) كمُنشئ للأرقام العشوائية الزائفة (PRNG) في تكوين فيبوناتشي. يتم تخزين قيمة 16 بت على أنها نهاية كبيرة في العناوين $0017 - $0018 . يتم استخدام رقم تعسفي $8988 .

80BC: LDX #$89
80BE: STX $0017
80C0: DEX
80C1: STX $0018


يتم إنشاء كل رقم عشوائي زائف لاحق على النحو التالي: يُنظر إلى القيمة على أنها رقم 17 بت ، ويتم الحصول على البت الأكثر أهمية من خلال إجراء XOR للبتين 1 و 9. ثم يتم تحويل القيمة إلى اليمين ، مع تجاهل البت الأقل أهمية.


تحدث هذه العملية عند $AB47 .

AB47: LDA $00,X
AB49: AND #$02
AB4B: STA $0000 ; extract bit 1

AB4D: LDA $01,X
AB4F: AND #$02 ; extract bit 9

AB51: EOR $0000
AB53: CLC
AB54: BEQ $AB57
AB56: SEC ; XOR bits 1 and 9 together

AB57: ROR $00,X
AB59: INX
AB5A: DEY ; right shift
AB5B: BNE $AB57 ; shifting in the XORed value

AB5D: RTS ; return


من المثير للاهتمام ، يمكن تعيين معلمات الروتين أعلاه بحيث يمكن لوظيفة الاستدعاء تحديد عرض سجل التحول والعنوان الذي يمكن العثور عليه في الذاكرة. ومع ذلك ، يتم استخدام نفس المعلمات في كل مكان ، لذلك يمكننا أن نفترض أن المطورين استعاروا هذا الرمز في مكان ما.

بالنسبة لأولئك الذين يرغبون في تعديل الخوارزمية ، فقد كتبتها في Java.

 int generateNextPseudorandomNumber(int value) { int bit1 = (value >> 1) & 1; int bit9 = (value >> 9) & 1; int leftmostBit = bit1 ^ bit9; return (leftmostBit << 15) | (value >> 1); } 

ويمكن ضغط كل هذا الرمز إلى سطر واحد.

 int generateNextPseudorandomNumber(int value) { return ((((value >> 9) & 1) ^ ((value >> 1) & 1)) << 15) | (value >> 1); } 

يولد هذا PRNG باستمرار وحتمية 32،767 قيمة فريدة ، بدءًا من كل دورة من البذرة الأصلية. هذا واحد أقل من نصف الأرقام المحتملة التي يمكن احتواؤها في السجل ، ويمكن استخدام أي قيمة في هذه المجموعة كبذور. تنشئ العديد من القيم خارج المجموعة سلسلة تؤدي في النهاية إلى رقم من المجموعة. ومع ذلك ، تؤدي بعض الأرقام الأولية إلى تسلسل لا نهائي من الأصفار.

لتقييم أداء PRNG هذا تقريبًا ، قمت بإنشاء تمثيل رسومي للقيم التي ينشئها استنادًا إلى جملة باستخدام RANDOM.ORG .


عند إنشاء الصورة ، تم استخدام PRNG كمولد أرقام عشوائي عشوائي ، بدلاً من الأعداد الصحيحة 16 بت. يتم تلوين كل بكسل استنادًا إلى قيمة البت 0. حجم الصورة 128 × 256 ، أي أنها تغطي التسلسل بأكمله.

وبصرف النظر عن الخطوط الملحوظة بالكاد على الجانبين العلوي واليسار ، فإنه يبدو عشوائيًا. لا تظهر أنماط واضحة.

بعد البدء ، يقوم PRNG بتحويل السجل باستمرار ، ويعمل مرة واحدة على الأقل في الإطار. لا يحدث هذا ليس فقط على شاشة البداية وشاشات القائمة ، ولكن أيضًا عندما يقع tetrimino بين عمليات إنشاء الأشكال. أي أن الشكل الذي يظهر بعد ذلك يعتمد على عدد الإطارات التي يأخذها اللاعب لوضع الشكل. في الواقع ، تعتمد اللعبة على عشوائية تصرفات الشخص الذي يتفاعل معها.

أثناء إنشاء الشكل ، يتم تنفيذ الرمز على العنوان $9907 ، والذي يحدد نوع الشكل الجديد.

9907: INC $001A ; spawnCount++;

9909: LDA $0017 ; index = high byte of randomValue;

990B: CLC
990C: ADC $001A ; index += spawnCount;

990E: AND #$07 ; index &= 7;

9910: CMP #$07 ; if (index == 7) {
9912: BEQ $991C ; goto invalidIndex;
; }

9914: TAX
9915: LDA $994E,X ; newSpawnID = spawnTable[index];

9918: CMP $0019 ; if (newSpawnID != spawnID) {
991A: BNE $9938 ; goto useNewSpawnID;
; }

invalidIndex:

991C: LDX #$17
991E: LDY #$02
9920: JSR $AB47 ; randomValue = generateNextPseudorandomNumber(randomValue);

9923: LDA $0017 ; index = high byte of randomValue;

9925: AND #$07 ; index &= 7;

9927: CLC
9928: ADC $0019 ; index += spawnID;

992A: CMP #$07
992C: BCC $9934
992E: SEC
992F: SBC #$07
9931: JMP $992A ; index %= 7;

9934: TAX
9935: LDA $994E,X ; newSpawnID = spawnTable[index];

useNewSpawnID:

9938: STA $0019 ; spawnID = newSpawnID;

993A: RTS ; return;


في العنوان $001A يخزن $001A لعدد الأشكال التي تم إنشاؤها باستخدام الطاقة. يتم تنفيذ زيادة العداد بواسطة السطر الأول من الروتين الفرعي ، وبما أنه عداد أحادي البايت ، بعد كل 256 قطعة ، فإنه يعود إلى الصفر مرة أخرى. نظرًا لعدم إعادة تعيين العداد بين الألعاب ، فإن تاريخ الألعاب السابقة يؤثر على عملية اختيار الشكل. هذه طريقة أخرى تستخدم فيها اللعبة اللاعب كمصدر للعشوائية.

يقوم الروتين بتحويل البايت الأكثر أهمية للرقم الزائف العشوائي ( $0017 ) إلى نوع tetrimino ويستخدمه كمؤشر للجدول الموجود في $994E لتحويل النوع إلى معرف اتجاه إنشاء الشكل.

994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih


في المرحلة الأولى من التحويل ، يضاف عداد الأشكال التي تم إنشاؤها إلى البايت العلوي. ثم يتم تطبيق قناع لحفظ 3 بتات أقل فقط. إذا لم تكن النتيجة 7 ، فهذا هو النوع الصحيح من tetrimino ، وإذا لم يكن نفس الشكل المحدد السابق ، فسيتم استخدام الرقم كمؤشر في الجدول لإنشاء الأشكال. خلاف ذلك ، يتم إنشاء الرقم العشوائي الزائف التالي ويتم تطبيق القناع للحصول على 3 بتات أقل من البايت العلوي ، ثم يضاف معرف اتجاه إنشاء الشكل السابق. أخيرًا ، يتم تنفيذ عملية modulo للحصول على النوع الصحيح من tetrimino ، والذي يستخدم كمؤشر في جدول إنشاء الشكل.

نظرًا لأن المعالج لا يدعم القسمة مع الباقي ، تتم محاكاة هذا العامل من خلال طرح 7 بشكل متكرر حتى تصبح النتيجة أقل من 7. يتم تطبيق القسمة مع الباقي على مجموع البايت العلوي مع تطبيق القناع وعلى معرف إنشاء الاتجاه السابق. الحد الأقصى لقيمة هذا المبلغ هو 25. أي ، لتخفيضه إلى ما تبقى من 4 ، مطلوب فقط 3 تكرارات.

في بداية كل لعبة ، تتم تهيئة معرف اتجاه إنشاء الشكل ( $0019 ) بقيمة Tu ( $00 ). يمكن استخدام هذه القيمة بسعر $9928 أثناء إنشاء الشكل الأول.

عند استخدام معرف الاتجاه السابق لإنشاء شكل ، بدلاً من النوع السابق ، يضيف Tetrimino تشويهًا ، لأن قيم معرف الاتجاه غير موزعة بالتساوي. يظهر هذا في الجدول:

00 دولار$02$07$08$0A$0B$0E$12
02013404
13124515
24235626
35346030
46450141
50561252
61602363
72013404

تحتوي كل خلية على نوع tetrimino ، يتم حسابه عن طريق إضافة معرف الاتجاه للشكل الذي تم إنشاؤه (العمود) إلى قيمة 3 بت (صف) ، ثم تطبيق باقي القسمة على 7 على المجموع. يحتوي كل صف على نسخ مكررة ، لأن $07 و $0E بالتساوي بمقدار 7 ، في حين أن $0B و $12 لديهم رصيد مشترك. الخطان 0 و 7 متماثلان لأنهما على مسافة 7.

هناك 56 مجموعة إدخال ممكنة ، وإذا تم توزيع أنواع tetrimino الناتجة بالتساوي ، فيمكننا أن نتوقع أنه في الجدول أعلاه ، يجب أن يظهر كل نوع 8 مرات بالضبط. ولكن كما هو موضح أدناه ، ليس هذا هو الحال.

اكتبالتردد
ت9
ي8
ض8
يا8
ق9
لام7
أنا7

تظهر T و S في كثير من الأحيان ، و L و I - في كثير من الأحيان. ولكن لا يتم تنفيذ التعليمات البرمجية المنحرفة باستخدام معرف الاتجاه في كل مرة يتم استدعاء روتين.

افترض أن PRNG يخلق سلسلة من القيم الإحصائية الإحصائية الموزعة بشكل موحد. هذا في الواقع افتراض عادل ، بالنظر إلى كيفية محاولة اللعبة الحصول على العشوائية الصحيحة من تصرفات اللاعب. لن تؤثر إضافة عدد الأرقام التي تم إنشاؤها إلى العنوان $990C على التوزيع ، لأن الرقم يزداد بالتساوي بين المكالمات. يشبه استخدام قناع البت بسعر $990E تطبيق القسمة على 8 مع الباقي ، وهو ما لا يؤثر أيضًا على التوزيع. لذلك ، فإن التحقق من $9910 يذهب إلى invalidIndex في 1/8 من جميع الحالات. واحتمال الضرب عند التحقق من العنوان $9918 ، حيث تتم مقارنة الرقم المختار حديثًا مع الشكل السابق ، هو 7/8 ، مع احتمال صدفة 1/7.وهذا يعني أن هناك المزيد من احتمال 7/8 × 1/7 = 1/8أن يكون في invalidIndex. بشكل عام ، هناك احتمال بنسبة 25 ٪ لاستخدام رمز منحرف و 75 ٪ احتمال استخدام رمز يحدد Tetrimino بالتساوي.

في مجموعة مكونة من 224 tetriminos ، يكون التوقع الرياضي 32 حالة لكل نوع. لكن في الواقع ، ينشئ الكود التوزيع التالي:

اكتبالتردد
ت33
ي32
ض32
يا32
ق33
لام31
أنا31

وهذا يعني ، مسح 90 خطًا والوصول إلى المستوى 9 ، سيحصل اللاعب على T و S إضافيين وأخرى أقل L و I مما هو متوقع إحصائيًا.

يتم اختيار Tetrimino مع الاحتمالات التالية:

اكتبالاحتمال
ت14.73٪
ي14.29٪
ض14.29٪
يا14.29٪
ق14.73٪
لام13.84٪
أنا13.84٪

يبدو أنه في البيان الذي يشير إلى أن "العصا الطويلة" لا تظهر أبدًا عند الحاجة ، هناك جزء من الحقيقة (على الأقل لنينتندو تتريس).

Tetrimino Shift


تستخدم نينتندو تتريس Delift Auto Shift (DAS). يؤدي النقر على "يسار" أو "يمين" إلى تحريك خلية tetrimino على الفور أفقيًا. أثناء الضغط على أحد أزرار الاتجاه هذه ، تتسبب اللعبة في تغيير الشكل تلقائيًا كل 6 إطارات مع تأخير أولي يبلغ 16 إطارًا.

يتم التحكم في هذا النوع من الحركة الأفقية بواسطة الرمز الموجود في العنوان $89AE. كما هو الحال في رمز التدوير ، يتم استخدام متغير مؤقت هنا لعمل نسخة احتياطية من الإحداثيات في حالة ما إذا كان الموضع الجديد غير صحيح. لاحظ أن الشيك يمنعك من تحريك القطعة أثناء ضغط اللاعب لأسفل.

89AE: LDA $0040
89B0: STA $00AE ; originalX = tetriminoX;

89B2: LDA $00B6 ; if (pressing down) {
89B4: AND #$04 ; return;
89B6: BNE $8A09 ; }

89B8: LDA $00B5 ; if (just pressed left/right) {
89BA: AND #$03 ; goto resetAutorepeatX;
89BC: BNE $89D3 ; }

89BE: LDA $00B6 ; if (not pressing left/right) {
89C0: AND #$03 ; return;
89C2: BEQ $8A09 ; }

89C4: INC $0046 ; autorepeatX++;
89C6: LDA $0046 ; if (autorepeatX < 16) {
89C8: CMP #$10 ; return;
89CA: BMI $8A09 ; }

89CC: LDA #$0A
89CE: STA $0046 ; autorepeatX = 10;
89D0: JMP $89D7 ; goto buttonHeldDown;

resetAutorepeatX:

89D3: LDA #$00
89D5: STA $0046 ; autorepeatX = 0;

buttonHeldDown:

89D7: LDA $00B6 ; if (not pressing right) {
89D9: AND #$01 ; goto notPressingRight;
89DB: BEQ $89EC ; }

89DD: INC $0040 ; tetriminoX++;
89DF: JSR $948B ; if (new position not valid) {
89E2: BNE $8A01 ; goto restoreX;
; }

89E4: LDA #$03
89E6: STA $06F1 ; play shift sound effect;
89E9: JMP $8A09 ; return;

notPressingRight:

89EC: LDA $00B6 ; if (not pressing left) {
89EE: AND #$02 ; return;
89F0: BEQ $8A09 ; }

89F2: DEC $0040 ; tetriminoX--;
89F4: JSR $948B ; if (new position not valid) {
89F7: BNE $8A01 ; goto restoreX;
; }

89F9: LDA #$03
89FB: STA $06F1 ; play shift sound effect;
89FE: JMP $8A09 ; return;

restoreX:

8A01: LDA $00AE
8A03: STA $0040 ; tetriminoX = originalX;

8A05: LDA #$10
8A07: STA $0046 ; autorepeatX = 16;

8A09: RTS ; return;


x



رمي تتريمينو


سرعة الهبوط التلقائي لـ Tetrimino هي دالة لرقم المستوى. يتم ترميز السرعات على أنها عدد الإطارات المقدمة للنزول في الجدول الموجود في $898E. نظرًا لأن NES تعمل بسرعة 60.0988 إطارًا / ثانية ، يمكنك حساب الفترة بين السلالات والسرعة.

المستوىإطارات النسبالفترة (النسب)السرعة (الخلايا / الخلايا)
0480.7991.25
1437151.40
238.6321.58
333.5491.82
4284662.15
5233832.61
618.3003.34
713.2164.62
88.1337.51
9610010.02
10-125.08312.02
13-154.06715.05
16-183.05003/20
19-282.03330.05
29+10.01760.10

يحتوي الجدول على إجمالي 30 إدخالًا. بعد المستوى 29 ، تكون قيمة إطارات الهبوط دائمًا 1.

عدد صحيح من إطارات الهبوط ليس طريقة مفصلة للغاية لوصف السرعة. كما هو موضح في الرسم البياني أدناه ، تزداد السرعة أضعافًا مضاعفة مع كل مستوى. في الواقع ، المستوى 29 أسرع مرتين من المستوى 28.


مع 1 إطار / نزول ، ليس لدى اللاعب أكثر من 1/3 من الثانية لوضع الرقم ، وبعد ذلك سيبدأ في التحرك. وبسرعة الهبوط هذه ، لا تسمح DAS للرقم بالوصول إلى حواف الملعب حتى يتم تثبيته في مكانه ، وهو ما يعني لمعظم الناس نهاية سريعة للعبة. ومع ذلك ، تمكن بعض اللاعبين ، ولا سيما Thor Akerlund ، من هزيمة DAS مع الاهتزاز السريع للأزرار المتقاطعة ( D-pad). في رمز النقل الموضح أعلاه ، يمكن ملاحظة أنه أثناء تحرير زر الاتجاه الأفقي من خلال الإطار ، فمن الممكن تحويل tetrimino عند المستويات 29 وما فوق بنصف التردد. هذا هو الحد الأقصى النظري ، ولكن أي اهتزاز للإبهام فوق 3.75 الصنابير / ثانية يمكن أن يهزم التأخير الأصلي لـ 16 إطارًا.

إذا تزامن النسب الأوتوماتيكي الذي يتحكم فيه اللاعب (بالضغط على "لأسفل") وحدث في إطار واحد ، فإن التأثير لا يضيف. يؤدي أي من هذين الحدثين أو كليهما إلى انخفاض الشكل بمقدار خلية واحدة بالضبط في هذا الإطار.

يقع منطق التحكم في الزناد في $8914. جدول إطار الهبوط تحت التسمية . كما ذكر أعلاه ، في المستوى 29 وما فوق ، فإن السرعة تساوي باستمرار مصراع / إطار. (العنوان ) يبدأ الهبوط عندما يصل إلى ( ). يتم تنفيذ الزيادة في عنوان خارج جزء التعليمات البرمجية هذا. أثناء الهبوط التلقائي أو المتحكم فيه ، تتم إعادة تعيينه إلى 0. تتم تهيئة المتغير ( ) بالقيمة (على العنوان

8914: LDA $004E ; if (autorepeatY > 0) {
8916: BPL $8922 ; goto autorepeating;
; } else if (autorepeatY == 0) {
; goto playing;
; }

; game just started
; initial Tetrimino hanging at spawn point

8918: LDA $00B5 ; if (not just pressed down) {
891A: AND #$04 ; goto incrementAutorepeatY;
891C: BEQ $8989 ; }

; player just pressed down ending startup delay

891E: LDA #$00
8920: STA $004E ; autorepeatY = 0;
8922: BNE $8939

playing:

8924: LDA $00B6 ; if (left or right pressed) {
8926: AND #$03 ; goto lookupDropSpeed;
8928: BNE $8973 ; }

; left/right not pressed

892A: LDA $00B5
892C: AND #$0F ; if (not just pressed only down) {
892E: CMP #$04 ; goto lookupDropSpeed;
8930: BNE $8973 ; }

; player exclusively just presssed down

8932: LDA #$01
8934: STA $004E ; autorepeatY = 1;

8936: JMP $8973 ; goto lookupDropSpeed;

autorepeating:

8939: LDA $00B6
893B: AND #$0F ; if (down pressed and not left/right) {
893D: CMP #$04 ; goto downPressed;
893F: BEQ $894A ; }

; down released

8941: LDA #$00
8943: STA $004E ; autorepeatY = 0
8945: STA $004F ; holdDownPoints = 0
8947: JMP $8973 ; goto lookupDropSpeed;

downPressed:

894A: INC $004E ; autorepeatY++;
894C: LDA $004E
894E: CMP #$03 ; if (autorepeatY < 3) {
8950: BCC $8973 ; goto lookupDropSpeed;
; }

8952: LDA #$01
8954: STA $004E ; autorepeatY = 1;

8956: INC $004F ; holdDownPoints++;

drop:

8958: LDA #$00
895A: STA $0045 ; fallTimer = 0;

895C: LDA $0041
895E: STA $00AE ; originalY = tetriminoY;

8960: INC $0041 ; tetriminoY++;
8962: JSR $948B ; if (new position valid) {
8965: BEQ $8972 ; return;
; }

; the piece is locked

8967: LDA $00AE
8969: STA $0041 ; tetriminoY = originalY;

896B: LDA #$02
896D: STA $0048 ; playState = UPDATE_PLAYFIELD;
896F: JSR $9CAF ; updatePlayfield();

8972: RTS ; return;

lookupDropSpeed:

8973: LDA #$01 ; tempSpeed = 1;

8975: LDX $0044 ; if (level >= 29) {
8977: CPX #$1D ; goto noTableLookup;
8979: BCS $897E ; }

897B: LDA $898E,X ; tempSpeed = framesPerDropTable[level];

noTableLookup:

897E: STA $00AF ; dropSpeed = tempSpeed;

8980: LDA $0045 ; if (fallTimer >= dropSpeed) {
8982: CMP $00AF ; goto drop;
8984: BPL $8958 ; }

8986: JMP $8972 ; return;

incrementAutorepeatY:

8989: INC $004E ; autorepeatY++;
898B: JMP $8972 ; return;


lookupDropSpeed

fallTimer$0045dropSpeed$00AFfallTimer$8892

autorepeatY$004E$0A$8739) ، التي تُفسر على أنها −96. تسبب الحالة في البداية تأخيرًا أوليًا. يبقى أول Tetrimino معلقًا في الهواء عند نقطة الإنشاء حتى autorepeatYيرتفع إلى 0 ، الأمر الذي يستغرق 1.6 ثانية. ومع ذلك ، عندما تضغط لأسفل في هذه المرحلة ، يتم autorepeatYتعيينها على الفور إلى 0. ومن المثير للاهتمام أنه يمكنك تحريك الشكل وتدويره في هذه المرحلة من التأخير الأولي دون إلغائه. يتم تنفيذ

الزيادة autorepeatYأثناء الضغط باستمرار. عندما يصل إلى 3 ، يحدث نزول يتحكم فيه الإنسان (نزول "ناعم") ويتم autorepeatYتعيينه 1. لذلك ، يتطلب النزول الأولي الأولي 3 إطارات ، ولكن يتم تكراره بعد ذلك في كل إطار.

بالإضافة إلى ذلك ، autorepeatYتزداد من 0 إلى 1 فقط عندما تدرك اللعبة أن اللاعب قد قام للتو بالنقر لأسفل (في$00B5) ، لكنه لا يعترف باستمرار. هذا مهم لأنه يتم autorepeatYإعادة تعيينه إلى 0 عند إنشاء Tetrimino (على العنوان $98E8) ، مما يخلق ميزة مهمة: إذا قام اللاعب نفسه بتخفيض الرقم وتم حظره ، واستمر في الضغط على "أسفل" عند إنشاء الشكل التالي ، والذي يحدث غالبًا بمستويات عالية ، ثم هذا لن يؤدي إلى نزول ناعم للرقم الجديد. ولكي يحدث ذلك ، يجب على اللاعب تحرير "لأسفل" ، ثم الضغط على الزر مرة أخرى.

يمكن أن يزيد النسب اللطيف المحتمل من النقاط. holdDownPoints( $004F) يزداد مع كل هبوط ، ولكن عند تحريره ، تتم إعادة تعيين "Down" إلى 0. لذلك ، لتسجيل النقاط ، من الضروري خفض tetrimino إلى القفل باستخدام نزول ناعم. الهبوط الناعم قصير المدى ، والذي قد يحدث في طريق الشكل ، لا يؤثر على النقاط. يتم تحديث الحساب في$9BFE، ولكن يتم holdDownPointsإعادة تعيينها إلى 0 بعد ذلك بقليل ، على العنوان $9C2F.

الشيك ، الذي يمنع اللاعب من القيام بنزول ناعم مع التحول الأفقي للشكل ، يعقد مجموعة النقاط. هذا يعني أن الخطوة الأخيرة قبل تثبيت القطعة في مكانها يجب أن تكون "لأسفل".

عند الذهاب إلى أسفل، tetriminoY( $0041) يتم نسخها إلى originalY( $00AE). إذا تبين أن الموضع الجديد الذي تم إنشاؤه بواسطة الزيادة tetriminoYغير صحيح (أي أن الرقم إما يدفع من خلال أرضية الملعب ، أو يتم فرضه على المربعات القائمة بالفعل) ، فإن tetrimino يبقى في الموضع السابق. في هذه الحالة ، يتم استعادتهtetriminoYويعتبر الرقم مسدود. وهذا يعني أن التأخير قبل القفل (الحد الأقصى لعدد الإطارات التي تتوقعها tetrimino ، وهي تمسك في الهواء قبل القفل) يساوي التأخير في الهبوط.

النسب الجامد (السقوط الفوري) غير مدعوم في Nintendo Tetris.

انزلاق والتمرير


يحتوي كتيب Nintendo Tetris اليدوي على مثال زلق مصور:


يتكون الانزلاق من التحول على طول سطح الأشكال الأخرى أو على طول أرضية الملعب. وعادة ما يستخدم لدفع الرقم تحت مربع متدلي. يمكن تنفيذ الانزلاق حتى يصل مؤقت السقوط إلى سرعة الهبوط ، وبعد ذلك سيتم قفل الشكل في مكانه. يظهر مثال متحرك أدناه.


من ناحية أخرى ، يتيح لك التمرير دفع الأشكال إلى أماكن لا يمكن الوصول إليها بأي طريقة أخرى (انظر أدناه).


مثل الانزلاق ، لا يمكن التمرير بدون تأخير القفل. ولكن أبعد من ذلك ، فإن التمرير يستغل طريقة تعامل اللعبة مع الأشكال. قبل تحريك الشكل أو تدويره ، تتحقق اللعبة من أنه بعد تغيير الموضع ، ستكون جميع مربعات tetrimino في خلايا فارغة داخل حدود الملعب. مثل هذا الفحص ، كما هو موضح أدناه ، لا يمنع الدوران من خلال الكتل المملوءة القريبة. كما هو مذكور في قسم وصف Tetrimino ، يحتوي كل صف من جداول التوجيه على 12 بايت ؛ لذلك ، يتم حساب الفهرس في هذا الجدول عن طريق ضرب معرف الاتجاه لـ tetrimino النشط في 12. كما هو موضح أدناه ، يتم تنفيذ جميع المضاعفات في الروتين باستخدام التحولات والإضافات.

948B: LDA $0041
948D: ASL
948E: STA $00A8
9490: ASL
9491: ASL
9492: CLC
9493: ADC $00A8
9495: ADC $0040
9497: STA $00A8

9499: LDA $0042
949B: ASL
949C: ASL
949D: STA $00A9
949F: ASL
94A0: CLC
94A1: ADC $00A9
94A3: TAX ; index = 12 * orientationID;
94A4: LDY #$00

94A6: LDA #$04
94A8: STA $00AA ; for(i = 0; i < 4; i++) {

94AA: LDA $8A9C,X ; squareY = orientationTable[index];
94AD: CLC
94AE: ADC $0041 ; cellY = squareY + tetriminoY;
94B0: ADC #$02 ; if (cellY < -2 || cellY >= 20) {
94B2: CMP #$16 ; return false;
94B4: BCS $94E9 ; }

94B6: LDA $8A9C,X
94B9: ASL
94BA: STA $00AB
94BC: ASL
94BD: ASL
94BE: CLC
94BF: ADC $00AB
94C1: CLC
94C2: ADC $00A8
94C4: STA $00AD

94C6: INX
94C7: INX ; index += 2;

94C8: LDA $8A9C,X ; squareX = orientationTable[index];
94CB: CLC
94CC: ADC $00AD
94CE: TAY ; cellX = squareX + tetriminoX;
94CF: LDA ($B8),Y ; if (playfield[10 * cellY + cellX] != EMPTY_TILE) {
94D1: CMP #$EF ; return false;
94D3: BCC $94E9 ; }

94D5: LDA $8A9C,X
94D8: CLC
94D9: ADC $0040 ; if (cellX < 0 || cellX >= 10) {
94DB: CMP #$0A ; return false;
94DD: BCS $94E9 ; }

94DF: INX ; index++;
94E0: DEC $00AA
94E2: BNE $94AA ; }

94E4: LDA #$00
94E6: STA $00A8
94E8: RTS ; return true;

94E9: LDA #$FF
94EB: STA $00A8
94ED: RTS




index = (orientationID << 3) + (orientationID << 2); // index = 8 * orientationID + 4 * orientationID;

(cellY << 3) + (cellY << 1) // 8 * cellY + 2 * cellY


يؤدي كل تكرار للدورة إلى تغيير موضع tetrimino من خلال الإحداثيات النسبية لأحد المربعات من طاولة التوجيه للحصول على موقع الخلية المقابل في الملعب. ثم تتحقق من أن إحداثيات الخلية داخل حدود الملعب ، وأن الخلية نفسها فارغة.

تصف التعليقات بشكل أوضح كيفية التحقق من تباعد الأسطر. بالإضافة إلى الخلايا في الخطوط المرئية ، يعتبر الرمز الخطين المخفيين فوق الملعب كمواقع قانونية للمربعات دون استخدام شرط مركب. يعمل هذا لأنه في الكود الإضافي ، تكون الأرقام السالبة التي تمثلها المتغيرات أحادية البايت معادلة للقيم الأكبر من 127. في هذه الحالة ، القيمة الدنيا هي −2 ، والتي يتم تخزينها

94AA: LDA $8A9C,X ; squareY = orientationTable[index];
94AD: CLC
94AE: ADC $0041 ; cellY = squareY + tetriminoY;
94B0: ADC #$02 ; if (cellY + 2 >= 22) {
94B2: CMP #$16 ; return false;
94B4: BCS $94E9 ; }


cellY$FE(254 بالتدوين العشري).

مؤشر ميدان اللعب هو مجموع cellYضرب 10 و cellX. ومع ذلك ، عندما تكون cellY−1 ( $FF= 255) أو −2 ( $FE= 254) ، ينتج المنتج −10 ( $F6= 246) و −20 ( $EC= 236). في الفاصل الزمني ، لا cellXيمكن أن يكون أكثر من 9 ، مما يعطي مؤشرًا أقصى قدره 246 + 9 = 255 ، وهذا أبعد بكثير من نهاية الملعب. ومع ذلك ، يتم تهيئة اللعبة $0400- $04FFبقيمة $EF(من مربع فارغ) ، وإنشاء 56 بايت إضافية من المساحة الفارغة.

غريب أن الاختيار الفاصلcellXيتم إجراؤها بعد فحص خلية الملعب. لكنها تعمل بشكل صحيح بأي ترتيب. بالإضافة إلى ذلك ، يؤدي التحقق من الفاصل الزمني إلى تجنب الحالة المركبة ، كما هو موضح في التعليق أدناه. أمثلة التمرير الموضحة أدناه ممكنة بسبب الطريقة التي يتحقق بها هذا الرمز من المواضع.

94D5: LDA $8A9C,X
94D8: CLC
94D9: ADC $0040 ; if (cellX >= 10) {
94DB: CMP #$0A ; return false;
94DD: BCS $94E9 ; }






كما هو موضح أدناه ، يمكنك حتى إجراء انزلاق مع التمرير.


تستفيد منظمة العفو الدولية بشكل كامل من قدرات الحركة في Nintendo Tetris ، بما في ذلك الانزلاق والتمرير.

المستوى 30 وما فوق


بعد الوصول إلى المستوى 30 ، يبدو أنه تم إعادة تعيين المستوى إلى صفر.


لكن المستوى 31 يظهر أن شيئًا آخر يحدث:


توجد قيم المستوى المعروضة في الجدول في العنوان $96B8.

96B8: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

كما هو مبين أدناه، أن أمر هذا الجدول نمط بحيث البلاط مع $00يوم $0Fهي رموز لرموزا 0على F. هذا يعني أنه عند عرض رقم عشري أو سداسي عشري ، يتم استخدام قيمة الرقم نفسه كمؤشر لجدول النمط. في حالتنا ، يتم تخزين قيم المستوى على شكل عشري مرمز ثنائي (BCD) ؛ كل عاب من كل بايت في التسلسل هو قيمة تجانب.


لسوء الحظ ، يبدو أن مصممي اللعبة افترضوا أن لا أحد سيمر المستوى 29 ، وبالتالي قرروا إدراج 30 إدخالًا فقط في الجدول. القيم المعروضة الغريبة هي وحدات بايت مختلفة بعد الجدول. $0044يتم استخدام بايت واحد فقط (في العنوان ) للإشارة إلى رقم المستوى ، وهذا هو السبب في أن اللعبة تدور ببطء حول القيم 256 الموضحة أدناه.

000123456789ABCDEF
000010203040506070809101112131415
11617181920212223242526272829000A
2141E28323C46505A646E78828C96A0AA
3B4BEC620E62006212621462166218621
4A621C621E62106222622462266228622
5A622C622E6220623262385A829F04A4A
64A4A8D0720A5A8290F8D072060A649E0
7151053BDD696A88A0AAAE8BDEA968D06
820CAA5BEC901F01EA5B9C905F00CBDEA
99638E9028D06204C6797BDEA9618690C
A8D06204C6797BDEA961869068D0620A2
B0AB1B88D0720C8CAD0F7E649A549C914
C3004A920854960A5B12903D078A90085
DAAA6AAB54AF05C0AA8B9EA9685A8A5BE
EC901D00AA5A818690685A84CBD97A5B9
FC904D00AA5A838E90285A84CBD97A5A8

أول 20 قيمة ترتيبية هي في الواقع جدول آخر يخزن الإزاحات في الملعب لكل من الصفوف العشرين. نظرًا لأن مجال اللعب يبدأ وكل سطر يحتوي على 10 خلايا ، فإن عنوان الخلية العشوائية هو: نظرًا لأن المعالج لا يدعم الضرب مباشرة ، فإن جدول البحث هذا يوفر طريقة سريعة للغاية للحصول على المنتج. يشغل الجدول المقابل 40 بايت التالية. يحتوي على 20 عنوانًا بتنسيق endian قليلًا للاسم 0 (منطقة ذاكرة VRAM تحتوي على قيم مربعات الخلفية). هم يشيرون إلى خطوط تعويض الملعب على . البايت المتبقية التي تتكون منها قيم المستوى المعروضة هي تعليمات.

96D6: 00 ; 0
96D7: 0A ; 10
96D8: 14 ; 20
96D9: 1E ; 30
96DA: 28 ; 40
96DB: 32 ; 50
96DC: 3C ; 60
96DD: 46 ; 70
96DE: 50 ; 80
96DF: 5A ; 90
96E0: 64 ; 100
96E1: 6E ; 110
96E2: 78 ; 120
96E3: 82 ; 130
96E4: 8C ; 140
96E5: 96 ; 150
96E6: A0 ; 160
96E7: AA ; 170
96E8: B4 ; 180
96E9: BE ; 190


$0400

$0400 + 10 * y + x



$0400 + [$96D6 + y] + x

$06



الصفوف والإحصائيات


يشغل عدد الصفوف المكتملة وإحصاءات tetrimino 2 بايت لكل منها في العناوين التالية.

العناوينالكمية
0050 - 0051الرتب
03F0 - 03F1ت
03F2 - 03F3ي
03F4 - 03F5ض
03F6 - 03F7يا
03F8 - 03F9ق
03FA - 03FBلام
03FC - 03FDأنا

في الواقع ، يتم تخزين هذه القيم على أنها BCDs صغيرة معبأة ذات 16 بت. على سبيل المثال ، يظهر عدد الصفوف أدناه ، وهو 123. يتم حساب وحدات البايت من اليمين إلى اليسار بحيث يتم ترتيب الأرقام العشرية بالترتيب.


ومع ذلك ، افترض مصممو اللعبة أنه لن تكون أي من القيم أكبر من 999. لذلك ، يعالج منطق العرض البايت الأول بشكل صحيح على أنه BCD معبأ ، حيث يتم استخدام كل قضمة كقيمة تجانب. لكن البايت الثاني بأكمله يستخدم في الواقع كأعلى رقم عشري. عندما تنتقل الأرقام السفلية من 99إلى 00، تحدث الزيادة العادية للبايت الثاني. ونتيجة لذلك ، يمر البايت الثاني عبر 256 قطعة. يظهر مثال على ذلك أدناه.


بعد مسح الخط ، يتم تنفيذ التعليمات البرمجية التالية لزيادة عدد الصفوف. يتم إجراء الفحوصات للأرقام الوسطى والسفلى بحيث تظل بين 0 و 9. ولكن يمكن زيادة الرقم العلوي بشكل لا نهائي. إذا كان الرقم السفلي بعد زيادة عدد الصفوف هو 0 ، فهذا يعني أن اللاعب قد أكمل للتو مجموعة من 10 أسطر وتحتاج إلى زيادة رقم المستوى. كما ترى من الكود أدناه ، يتم إجراء فحص إضافي قبل زيادة المستوى. يرتبط الفحص الثاني بمستوى الدخول المحدد. للانتقال إلى مستوى معين ، بغض النظر عن المستوى الأولي ، يجب على اللاعب مسح

9BA8: INC $0050 ; increment middle-lowest digit pair
9BAA: LDA $0050
9BAC: AND #$0F
9BAE: CMP #$0A ; if (lowest digit > 9) {
9BB0: BMI $9BC7
9BB2: LDA $0050
9BB4: CLC
9BB5: ADC #$06 ; set lowest digit to 0, increment middle digit
9BB7: STA $0050
9BB9: AND #$F0
9BBB: CMP #$A0 ; if (middle digit > 9) {
9BBD: BCC $9BC7
9BBF: LDA $0050
9BC1: AND #$0F
9BC3: STA $0050 ; set middle digit to 0
9BC5: INC $0051 ; increment highest digit
; }
; }






9BC7: LDA $0050
9BC9: AND #$0F
9BCB: BNE $9BFB ; if (lowest digit == 0) {
9BCD: JMP $9BD0

9BD0: LDA $0051
9BD2: STA $00A9
9BD4: LDA $0050
9BD6: STA $00A8 ; copy digits from $0050-$0051 to $00A8-$00A9

9BD8: LSR $00A9
9BDA: ROR $00A8
9BDC: LSR $00A9
9BDE: ROR $00A8
9BE0: LSR $00A9
9BE2: ROR $00A8 ; treat $00A8-$00A9 as a 16-bit packed BCD value
9BE4: LSR $00A9 ; and right-shift it 4 times
9BE6: ROR $00A8 ; this leaves the highest and middle digits in $00A8

9BE8: LDA $0044
9BEA: CMP $00A8 ; if (level < [$00A8]) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }
; }


X10Xخطوط. على سبيل المثال ، إذا بدأ اللاعب في المستوى 5 ، فسيبقى عليه حتى يمسح 60 خطًا ، وبعد ذلك سيذهب إلى المستوى 6. بعد ذلك ، سيؤدي كل 10 خطوط إضافية إلى زيادة في رقم المستوى.

لإجراء هذا الفحص ، يتم نسخ قيمة الصفوف المعبأة من $0050- $0051إلى $00A8- $00A9. ثم يتم تحويل النسخة إلى اليمين 4 مرات ، والتي بالنسبة لـ BCD معبأة تشبه القسمة على 10. يتم تجاهل أصغر رقم عشري ، ويتم تحويل الأرقام الأعلى والأوسط بموضع واحد ، مما يؤدي إلى الحلمات $00A8.


ومع ذلك ، في العنوان ، $9BEAتتم مقارنة رقم المستوى مباشرة مع القيمة المعبأة لـ BCD $00A8. لا يوجد بحث في الجدول لتحويل قيمة BCD إلى عشري ، وهذا خطأ واضح. على سبيل المثال ، في الصورة أعلاه ، يجب مقارنة رقم المستوى بـ $12(18 بالأرقام العشرية) ، وليس 12. لذلك ، إذا قرر اللاعب البدء من المستوى 17 ، فسيذهب المستوى بالفعل إلى 120 صفًا ، لأن 18 أكثر من 17.

يوضح الجدول العدد المتوقع للصفوف المطلوبة للانتقال في كل مستوى أولي. تتم مقارنته بما يحدث بالفعل بسبب خطأ.

012345678910111213141516171819
102030405060708090100110120130140150160170180190200
102030405060708090100100100100100100100110120130140

المبلغ المتوقع هو نفسه صحيح بالنسبة للمستويات الأولية 0-9. في الواقع ، الصدفة لمستوى الدخول 9 عشوائي. 10-15 يذهب أيضًا إلى المستوى التالي بـ 100 صف ، لأن $10- هذا هو 16 في شكل عشري. أكبر فرق بين المتوقع والفعلي هو 60 صفًا.

أظن أن الخطأ يرجع إلى تغييرات التصميم في المراحل اللاحقة من التطوير. انظر إلى شاشة القائمة ، مما يسمح للاعب باختيار مستوى الدخول.


لا يوجد تفسير لكيفية البدء من المستويات فوق 9. ولكن في كتيب نينتندو تتريس ، تم الكشف عن هذا السر:


يبدو أن هذه الميزة المخفية تم اختراعها في اللحظة الأخيرة. ربما تمت إضافته قريبًا جدًا من تاريخ الإصدار ، والذي لم يسمح باختباره بالكامل.

في الواقع ، يحتوي التحقق من السلسلة الأولية على خطأ ثانٍ يتعلق بإخراج قيم الفاصل الزمني. فيما يلي التعليقات في الكود والتي توضح بشكل أفضل ما يحدث على مستوى منخفض. تتم المقارنة بطرح علامة النتيجة والتحقق منها. لكن الرقم الموقّع أحادي البايت يقتصر على -128 إلى 127. إذا كان الفرق أقل من -128 ، يتم ترحيل الرقم وتصبح النتيجة رقمًا موجبًا. يتم شرح هذا المبدأ في التعليقات على التعليمات البرمجية. عند التحقق من وجود الفرق في هذه الفترة ، يجب أن يؤخذ في الاعتبار أن رقم المستوى ، عند زيادة القيم التي تزيد عن 255 ، ينقل التحويل إلى 0 ، و

9BE8: LDA $0044
9BEA: CMP $00A8 ; if (level - [$00A8] < 0) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }




9BE8: LDA $0044 ; difference = level - [$00A8];
9BEA: CMP $00A8 ; if (difference < 0 && difference >= -128) {
9BEC: BPL $9BFB

9BEE: INC $0044 ; increment level
; }


$00A8من المحتمل أن يحتوي على أي قيمة ، لأن عابته العلوية مأخوذة من $0051، والتي يمكن أن تحدث الزيادة بلا حدود.

تتداخل هذه التأثيرات ، مما يخلق فترات يبقى فيها رقم المستوى عن طريق الخطأ دون تغيير. تحدث الفترات على فترات منتظمة تبلغ 2900 صف ، بدءًا من 2190 صفًا ، وتستمر لـ 800 صف. على سبيل المثال ، من 2190 ( L90) إلى 2990 ( T90) ، يظل المستوى يساوي $DB( 96) ، كما هو موضح أدناه.


الفترة التالية تحدث من 5090 إلى 5890 ، المستوى يساوي باستمرار $AD( 06). بالإضافة إلى ذلك ، خلال هذه الفترات ، لا تتغير لوحة الألوان أيضًا.

صفحة التلوين Tetrimino


في كل مستوى ، يتم تعيين بلاط tetrimino 4 ألوان فريدة. يتم أخذ الألوان من الجدول الموجود في $984C. يتم إعادة استخدام سجلاتها كل 10 مستويات. من اليسار إلى اليمين: أعمدة الجدول المناظرة للمناطق الأسود والأبيض والأزرق والأحمر في الصورة أدناه.

984C: 0F 30 21 12 ; level 0
9850: 0F 30 29 1A ; level 1
9854: 0F 30 24 14 ; level 2
9858: 0F 30 2A 12 ; level 3
985C: 0F 30 2B 15 ; level 4
9860: 0F 30 22 2B ; level 5
9864: 0F 30 00 16 ; level 6
9868: 0F 30 05 13 ; level 7
986C: 0F 30 16 12 ; level 8
9870: 0F 30 27 16 ; level 9





تتوافق القيم مع لوحة ألوان NES.


دائمًا ما يكون أول لونين لكل إدخال باللونين الأسود والأبيض. ومع ذلك ، يتم تجاهل اللون الأول بالفعل ؛ بغض النظر عن القيمة ، يعتبر لونًا شفافًا يتم من خلاله رؤية خلفية سوداء صلبة.

يتم الوصول إلى جدول الألوان في الروتين في $9808. يعتمد فهرس جدول الألوان على رقم المستوى مقسومًا على العدد المتبقي من 10. تنسخ الدورة الإدخال إلى جداول اللوحة في VRAM. يتم تقسيم القسمة مع الباقي بطرح ثابت من 10 حتى تكون النتيجة أقل من 10. وتبين بداية الروتين الفرعي مع التعليقات أدناه.

9808: LDA $0064
980A: CMP #$0A
980C: BMI $9814
980E: SEC
980F: SBC #$0A
9811: JMP $980A ; index = levelNumber % 10;

9814: ASL
9815: ASL
9816: TAX ; index *= 4;

9817: LDA #$00
9819: STA $00A8 ; for(i = 0; i < 32; i += 16) {

981B: LDA #$3F
981D: STA $2006
9820: LDA #$08
9822: CLC
9823: ADC $00A8
9825: STA $2006 ; palette = $3F00 + i + 8;

9828: LDA $984C,X
982B: STA $2007 ; palette[0] = colorTable[index + 0];

982E: LDA $984D,X
9831: STA $2007 ; palette[1] = colorTable[index + 1];

9834: LDA $984E,X
9837: STA $2007 ; palette[2] = colorTable[index + 2];

983A: LDA $984F,X
983D: STA $2007 ; palette[3] = colorTable[index + 3];

9840: LDA $00A8
9842: CLC
9843: ADC #$10
9845: STA $00A8
9847: CMP #$20
9849: BNE $981B ; }

984B: RTS ; return;






9808: LDA $0064 ; index = levelNumber;
980A: CMP #$0A ; while(index >= 10) {
980C: BMI $9814
980E: SEC
980F: SBC #$0A ; index -= 10;
9811: JMP $980A ; }


ومع ذلك ، كما هو موضح في القسم السابق ، يتم استخدام الطرح والتفرع على أساس علامة الاختلاف في المقارنة. يقتصر عدد البايتات الموقعة أحادية البايت على 128- 127- وتعكس التعليقات المحدثة أدناه هذا المبدأ. التعليقات أدناه مبسطة. تكشف هذه الصياغة عن خطأ في الكود. يتم تخطي عملية التقسيم المتبقية تمامًا للمستويات من 138 وما فوق. بدلاً من ذلك ، يتم تعيين الفهرس مباشرةً إلى رقم المستوى ، الذي يوفر الوصول إلى وحدات البايت بعد نهاية جدول الألوان. كما هو موضح أدناه ، يمكن أن يؤدي هذا إلى وجود tetrimino غير مرئي تقريبًا.

9808: LDA $0064 ; index = levelNumber;
; difference = index - 10;
980A: CMP #$0A ; while(difference >= 0 && difference <= 127) {
980C: BMI $9814
980E: SEC ; index -= 10;
980F: SBC #$0A ; difference = index - 10;
9811: JMP $980A ; }




9808: LDA $0064 ; index = levelNumber;
980A: CMP #$0A ; while(index >= 10 && index <= 137) {
980C: BMI $9814
980E: SEC
980F: SBC #$0A ; index -= 10;
9811: JMP $980A ; }





فيما يلي ألوان جميع المستويات 256. يتم ترتيب المربعات في 10 أعمدة للتأكيد على الاستخدام الدوري لجدول الألوان ، الذي تم اختراقه عند المستوى 138. يتم الإشارة إلى الصفوف والأعمدة في الرؤوس العشرية.


بعد 255 ، يعود رقم المستوى إلى 0.

بالإضافة إلى ذلك ، كما هو مذكور في القسم السابق ، لا تتغير بعض المستويات حتى تتم إزالة 800 صف. خلال هذه المستويات الطويلة ، تبقى الألوان دون تغيير.

وضع اللعبة


يحدد وضع اللعبة المخزن في العنوان $00C0أي من الشاشات والقوائم المختلفة المعروضة حاليًا للمستخدم.

القيمةالوصف
00شاشة المعلومات القانونية
01شاشة البداية
02قائمة نوع اللعبة
03قائمة المستويات والارتفاعات
04اللعبة / أعلى الدرجات / النهاية / الإيقاف المؤقت
05تجريبي

كما هو موضح أعلاه ، تحتوي اللعبة على روتين مكتوب بذكاء يعمل بمثابة بيان تبديل باستخدام جدول التنقل الصغير الموجود مباشرة بعد المكالمة. توضح القائمة أعلاه عناوين جميع أوضاع اللعبة. لاحظ أن وضعي "Game" و "Demo" يستخدمان نفس الرمز. هذا الروتين لا يعود أبداً. بدلاً من ذلك ، يستخدم الرمز عنوان المرسل ؛ عادة ما يشير إلى التعليمات مباشرة بعد استدعاء القفزة إلى روتين (ناقص 1 بايت) ، ولكن في هذه الحالة يشير إلى جدول القفز. يظهر عنوان الإرجاع من المكدس ويتم تخزينه في - . بعد حفظ عنوان جدول القفز ، يستخدم الرمز القيمة في السجل A كمؤشر ويقوم بالانتقال المقابل.

8161: LDA $00C0
8163: JSR $AC82 ; switch(gameMode) {
8166: 00 82 ; case 0: goto 8200; //
8168: 4F 82 ; case 1: goto 824F; //
816A: D1 82 ; case 2: goto 82D1; //
816C: D7 83 ; case 3: goto 83D7; //
816E: 5D 81 ; case 4: goto 815D; // / / /
8170: 5D 81 ; case 5: goto 815D; //
; }




$0000$0001

AC82: ASL
AC83: TAY
AC84: INY

AC85: PLA
AC86: STA $0000
AC88: PLA ; pop return address off of stack
AC89: STA $0001 ; and store it at $0000-$0001

AC8B: LDA ($00),Y
AC8D: TAX
AC8E: INY
AC8F: LDA ($00),Y
AC91: STA $0001
AC93: STX $0000
AC95: JMP ($0000) ; goto Ath 16-bit address
; in table at [$0000-$0001]


يمكن للكود أن يستخدم روتين التبديل هذا طالما أن المؤشرات قريبة من 0 ولا توجد مسافات أو قليلة بين الحالات المحتملة.

شاشة المعلومات القانونية


تبدأ اللعبة بشاشة تعرض إشعارًا قانونيًا.


في الجزء السفلي من الشاشة ، تم ذكر أليكسي Pazhitnov كمخترع ومصمم ومبرمج أول Tetris. في عام 1984 ، عمل كمطور كمبيوتر في مركز دورودنيتسين للحوسبة (معهد أبحاث رائد في الأكاديمية الروسية للعلوم في موسكو) ، طور نموذجًا أوليًا للعبة على إلكترونيات 60 (استنساخ سوفيتي DEC LSI-11 ). تم تطوير نموذج أولي لوضع نص أحادي اللون أخضر يتم فيه تحديد المربعات بواسطة أزواج من الأقواس المربعة []. بمساعدة تلميذ يبلغ من العمر 16 عامًا Vadim Gerasimov ومهندس كمبيوتر Dmitry Pavlovsky بعد أيام قليلة من اختراع اللعبة ، تم نقل النموذج الأولي إلى جهاز كمبيوتر IBM مع MS DOS و Turbo Pascal. على مدار عامين ، قاموا بإتقان اللعبة معًا ، بإضافة ميزات مثل ألوان tetrimino والإحصاءات ، والأهم من ذلك ، رمز التوقيت والرسومات الذي سمح للعبة بالعمل على مجموعة متنوعة من طرازات وأجهزة الكمبيوتر المستنسخة.

لسوء الحظ ، نظرًا لخصائص الاتحاد السوفييتي في ذلك الوقت ، كانت محاولاتهم لتحقيق الدخل من اللعبة فاشلة ، وفي النهاية قرروا مشاركة إصدار الكمبيوتر الشخصي مع أصدقائهم مجانًا. منذ تلك اللحظة ، بدأت "تتريس" في الانتشار الفيروسي في جميع أنحاء البلاد وخارجها ، منسوخة من قرص إلى قرص. ولكن منذ أن تم تطوير اللعبة من قبل موظفي وكالة حكومية ، امتلكتها الدولة ، وفي عام 1987 تولت المنظمة المسؤولة عن التجارة الدولية في التقنيات الإلكترونية ترخيص اللعبة ( Electronorgtekhnika (ELORG)) قد يكون الاختصار V / O على شاشة المعلومات القانونية قصيرًا للإصدار Originale. حاولت

شركة البرمجيات البريطانية أندروميدا الحصول على حقوق لـ Tetris ، وقبل إتمام الصفقة ، قامت بترخيص اللعبة من الباطن لموردين آخرين ، على سبيل المثال ، الناشر البريطاني لألعاب الكمبيوتر Mirrorsoft . قامت شركة Mirrorsoft بدورها بترخيصها من الباطن لشركة Tengen ، وهي شركة تابعة لـ Atari Games. منح Tengen برنامج Bullet-Proof حقوق تطوير لعبة لأجهزة الكمبيوتر ووحدات التحكم في اليابان ، مما أدى إلى Tetris لـ Nintendo Famicom . فيما يلي شاشة المعلومات القانونية الخاصة به.


من المثير للاهتمام ، في هذا الإصدار ، يُطلق على تلميذ المدرسة Vadim Gerasimov المصمم والمبرمج الأصلي.

في محاولة لتأمين النسخة المحمولة من وحدة تحكم Game Boy القادمة ، استخدمت Nintendo برنامج Bullet-Proof لإبرام صفقة ناجحة مباشرة مع ELORG. في عملية إبرام الصفقة ، قامت شركة ELORG بمراجعة عقدها مع أندروميدا ، مضيفة أن أندروميدا حصلت فقط على حقوق ألعاب الكمبيوتر وأجهزة الآركيد. ولهذا السبب ، كان على برنامج Bullet-Proof دفع رسوم ELORG لجميع الخراطيش التي تم بيعها لـ Famicom ، لأن الحقوق التي تلقتها من Tengen تبين أنها مزيفة. ولكن من خلال المصالحة مع ELORG ، تمكنت Bullet-Proof Software أخيرًا من الحصول على حقوق ألعاب وحدة التحكم العالمية لـ Nintendo.

قامت Bullet-Proof Software بترخيص حقوق ألعاب Nintendo المحمولة من الباطن ، وقاموا معًا بتطوير Game Boy Tetris ، والذي ينعكس في شاشة المعلومات القانونية الموضحة أدناه.


مع حقوق ألعاب وحدة التحكم العالمية ، طورت Nintendo إصدار Tetris لـ NES الذي نستكشفه في هذه المقالة. ثم قامت Bullet-Proof Software بترخيص حقوق Nintendo من الباطن ، مما سمح لها بمواصلة بيع خراطيش Famicom في اليابان.

تبع ذلك معركة قانونية معقدة. طالب كل من نينتندو وتينجن أن يتوقف الطرف المعارض عن إنتاج وبيع نسختهم من اللعبة. ونتيجة لذلك ، فازت Nintendo ، ودُمرت مئات الآلاف من خراطيش Tengen Tetris . كما منع حكم المحكمة العديد من الشركات الأخرى مثل Mirrorsoft من إنشاء إصدارات وحدة التحكم.

لم يتلق باجيتنوف أي خصومات من ELORG أو الدولة السوفيتية. ومع ذلك ، انتقل في عام 1991 إلى الولايات المتحدة وفي عام 1996 بدعم من مالك برنامج Bullet-Proofشارك Henka Rogers في تأسيس شركة Tetris ، مما سمح له بالاستفادة من إصدارات الأجهزة المحمولة ووحدات التحكم الحديثة.

من المثير للاهتمام أن ننظر إلى شاشة المعلومات القانونية كنافذة تعطي فكرة عن الأصل المتواضع للعبة والمعارك التي تلت ذلك من أجل حقوق الملكية الفكرية ، لأن هذه الشاشة بالنسبة لمعظم اللاعبين مجرد عقبة مزعجة ، يبدو أن اختفائها يجب أن ينتظر إلى الأبد. يتم تعيين التأخير بواسطة عدادات ، يتم العد بالتتابع من 255 إلى 0. لا يمكن تخطي المرحلة الأولى ، ويتم تخطي المرحلة الثانية بالضغط على زر البدء. لذلك ، تظهر شاشة المعلومات القانونية 4.25 ثانية على الأقل ولا تزيد عن 8.5 ثانية. ومع ذلك ، أعتقد أن معظم اللاعبين يستسلمون ، ويتوقفون عن الضغط على Start خلال الفاصل الزمني الأول ، وبسبب هذا ينتظرون الانتهاء الكامل.

يخضع توقيت المراحل ، بالإضافة إلى بقية اللعبة ، إلى معالج مقاطعة غير مقنع يسمى في بداية كل فترة طمس رأسية ، وهي فترة زمنية قصيرة بين عرض إطارات التلفزيون. أي ، كل 16.6393 مللي ثانية ، تتم مقاطعة تنفيذ البرنامج العادي بواسطة التعليمات البرمجية التالية. يبدأ المعالج بتمرير قيم السجلات الرئيسية إلى المكدس واستردادها بعد الانتهاء حتى لا تتداخل مع المهمة المتقطعة. تقوم المكالمة بتحديث VRAM ، وتحويل وصف نموذج الذاكرة إلى ما يتم عرضه على الشاشة. علاوة على ذلك ، يقلل المعالج من قيمة عداد شاشة المعلومات القانونية إذا كانت أكبر من الصفر. التحدي

8005: PHA
8006: TXA
8007: PHA
8008: TYA
8009: PHA ; save A, X, Y

800A: LDA #$00
800C: STA $00B3
800E: JSR $804B ; render();

8011: DEC $00C3 ; legalScreenCounter1--;

8013: LDA $00C3
8015: CMP #$FF ; if (legalScreenCounter1 < 0) {
8017: BNE $801B ; legalScreenCounter1 = 0;
8019: INC $00C3 ; }

801B: JSR $AB5E ; initializeOAM();

801E: LDA $00B1
8020: CLC
8021: ADC #$01
8023: STA $00B1
8025: LDA #$00
8027: ADC $00B2
8029: STA $00B2 ; frameCounter++;

802B: LDX #$17
802D: LDY #$02
802F: JSR $AB47 ; randomValue = generateNextPseudorandomNumber(randomValue);

8032: LDA #$00
8034: STA $00FD
8036: STA $2005 ; scrollX = 0;
8039: STA $00FC
803B: STA $2005 ; scrollY = 0;

803E: LDA #$01
8040: STA $0033 ; verticalBlankingInterval = true;

8042: JSR $9D51 ; pollControllerButtons();

8045: PLA
8046: TAY
8047: PLA
8048: TAX
8049: PLA ; restore A, X, Y

804A: RTI ; resume interrupted task


render()initializeOAM()ينفذ الخطوة المطلوبة من قبل معدات توليد الإطار. يستمر المعالج في العمل عن طريق زيادة عداد الإطار - القيمة النهائية الصغيرة 16 بت المخزنة في العنوان $00B1- $00B2والتي يستخدمها في أماكن مختلفة للتوقيت المتحكم فيه. بعد ذلك ، يتم إنشاء الرقم العشوائي الزائف التالي ؛ كما ذكر أعلاه ، يحدث هذا بغض النظر عن الوضع مرة واحدة على الأقل لكل إطار. $8040يتم تعيين علامة الفاصل الزمني للمسح الرأسي على العنوان ، مما يعني أن المعالج قد تم تنفيذه للتو. أخيرًا ، يتم فحص أزرار جهاز التحكم ؛ يتم وصف سلوك هذا الروتين أدناه في قسم العرض التوضيحي.

يتم verticalBlankingIntervalاستخدام العلم من قبل الروتين الذي نوقش أعلاه. يستمر حتى يبدأ تنفيذ معالج المقاطعة.

AA2F: JSR $E000 ; updateAudio();

AA32: LDA #$00
AA34: STA $0033 ; verticalBlankingInterval = false;

AA36: NOP

AA37: LDA $0033
AA39: BEQ $AA37 ; while(!verticalBlankingInterval) { }

AA3B: LDA #$FF
AA3D: LDX #$02
AA3F: LDY #$02
AA41: JSR $AC6A ; fill memory page 2 with all $FF's

AA44: RTS ; return;


يتم استخدام روتين الحظر هذا على مرحلتين من توقيت شاشة المعلومات القانونية ، والتي يتم تنفيذها واحدة تلو الأخرى. يتجاوز نص Lua AI هذا التأخير عن طريق تعيين كلا العدادات على 0.

8236: LDA #$FF
8238: JSR $A459

...

A459: STA $00C3 ; legalScreenCounter1 = 255;

A45B: JSR $AA2F ; do {
A45E: LDA $00C3 ; waitForVerticalBlankingInterval();
A460: BNE $A45B ; } while(legalScreenCounter1 > 0);

A462: RTS ; return;


823B: LDA #$FF
823D: STA $00A8 ; legalScreenCounter2 = 255;

; do {

823F: LDA $00F5 ; if (just pressed Start) {
8241: CMP #$10 ; break;
8243: BEQ $824C ; }

8245: JSR $AA2F ; waitForVerticalBlankingInterval();

8248: DEC $00A8 ; legalScreenCounter2--;
824A: BNE $823F ; } while(legalScreenCounter2 > 0);

824C: INC $00C0 ; gameMode = TITLE_SCREEN;




تجريبي


يعرض العرض حوالي 80 ثانية من اللعب المسجل مسبقًا. لا يقوم فقط بعرض ملف الفيديو ، ولكنه يستخدم نفس المحرك الموجود في اللعبة. أثناء التشغيل ، يتم استخدام جدولين. يحتوي الأول ، الموجود في العنوان $DF00، على التسلسل التالي لإنشاء tetrimino:

TJTSZJTSZJSZLZJTTSITO JSZLZLIOLZLIOJTSITOJ

عند إنشاء شكل ، يتم تحديده عشوائيًا أو قراءته من الجدول ، اعتمادًا على الوضع. يحدث التبديل في العنوان $98EB. يتم استخراج نوع Tetrimino من البتات 6 و 5 و 4 من كل بايت. من وقت لآخر ، تعطينا هذه العملية قيمة - النوع الخطأ. ومع ذلك، فإن الجدول خلق الأشكال ( يقع) المستخدمة لتحويل نوع في tetrimino التوجه ID الواقع بين اثنين من الجداول المرتبطة: معنى

98EB: LDA $00C0
98ED: CMP #$05
98EF: BNE $9903 ; if (gameMode == DEMO) {

98F1: LDX $00D3
98F3: INC $00D3
98F5: LDA $DF00,X ; value = demoTetriminoTypeTable[++demoIndex];

98F8: LSR
98F9: LSR
98FA: LSR
98FB: LSR
98FC: AND #$07
98FE: TAX ; tetriminoType = bits 6,5,4 of value;

98FF: LDA $994E,X
9902: RTS ; return spawnTable[tetriminoType];
; } else {
; pickRandomTetrimino();
; }


$07$994E

993B: 00 00 00 00 ; T
993F: 01 01 01 01 ; J
9943: 02 02 ; Z
9945: 03 ; O
9946: 04 04 ; S
9948: 05 05 05 05 ; L
994C: 06 06 ; I


994E: 02 ; Td
994F: 07 ; Jd
9950: 08 ; Zh
9951: 0A ; O
9952: 0B ; Sh
9953: 0E ; Ld
9954: 12 ; Ih


9956: 02 02 02 02 ; Td
995A: 07 07 07 07 ; Jd
995E: 08 08 ; Zh
9960: 0A ; O
9961: 0B 0B ; Sh
9963: 0E 0E 0E 0E ; Ld
9967: 12 12 ; Ih


$07يجبرها على القراءة بعد نهاية الجدول ، في اليوم التالي ، مما يعطي Td( $02).

بسبب هذا التأثير ، يمكن أن يمنحنا هذا المخطط تسلسلًا غير محدود ، ولكن قابل للتكرار من المعرفات العشوائية الزائفة لتوجيه الأشكال التي تم إنشاؤها. ستعمل الشفرة لأن أي عنوان عشوائي في تسلسل متغير من وحدات البايت لا يسمح لنا بتحديد مكان انتهاء الجدول. في الواقع ، $DF00يمكن أن يكون التسلسل في العنوان جزءًا من شيء غير ذي صلة تمامًا بهذا ، خاصة بالنظر إلى أن تعيين 5 بتات غير صفرية المتبقية غير واضح ، ويظهر التسلسل الذي تم إنشاؤه إمكانية التكرار.

أثناء تهيئة الوضع التجريبي ، $00D3تتم إعادة تعيين فهرس الجدول ( ) على العنوان $872B.

يحتوي الجدول الثاني من العرض التوضيحي على سجل لأزرار لوحة الألعاب المشفرة في أزواج من وحدات البايت. بتات البايت الأولى تتوافق مع الأزرار.

76543210
أبحددابدألأعلىللأسفلإلى اليسارإلى اليمين

يخزن البايت الثاني عدد الإطارات التي يتم خلالها الضغط على مجموعة من الأزرار.

يأخذ الجدول العناوين $DD00- $DEFFويتكون من 256 زوجًا. يتم الوصول إليه بواسطة روتين فرعي في العنوان $9D5B. نظرًا لأن جدول زر العرض التوضيحي يبلغ طوله 512 بايت ، يلزم وجود فهرس ثنائي البايت للوصول إليه. يتم تخزين الفهرس في نهاية قليلا في - . تتم تهيئته بقيمة عنوان الجدول ، ويتم تنفيذ الزيادة من خلال التعليمات البرمجية التالية. غادر المبرمجون معالجة إدخال اللاعب في الشفرة ، مما يتيح لنا إلقاء نظرة على عملية التطوير واستبدال العرض التوضيحي بسجل آخر. يتم تنشيط وضع التسجيل التجريبي عند تعيين قيمة.

9D5B: LDA $00D0 ; if (recording mode) {
9D5D: CMP #$FF ; goto recording;
9D5F: BEQ $9DB0 ; }

9D61: JSR $AB9D ; pollController();
9D64: LDA $00F5 ; if (start button pressed) {
9D66: CMP #$10 ; goto startButtonPressed;
9D68: BEQ $9DA3 ; }

9D6A: LDA $00CF ; if (repeats == 0) {
9D6C: BEQ $9D73 ; goto finishedMove;
; } else {
9D6E: DEC $00CF ; repeats--;
9D70: JMP $9D9A ; goto moveInProgress;
; }

finishedMove:

9D73: LDX #$00
9D75: LDA ($D1,X)
9D77: STA $00A8 ; buttons = demoButtonsTable[index];

9D79: JSR $9DE8 ; index++;

9D7C: LDA $00CE
9D7E: EOR $00A8
9D80: AND $00A8
9D82: STA $00F5 ; setNewlyPressedButtons(difference between heldButtons and buttons);

9D84: LDA $00A8
9D86: STA $00CE ; heldButtons = buttons;

9D88: LDX #$00
9D8A: LDA ($D1,X)
9D8C: STA $00CF ; repeats = demoButtonsTable[index];

9D8E: JSR $9DE8 ; index++;

9D91: LDA $00D2 ; if (reached end of demo table) {
9D93: CMP #$DF ; return;
9D95: BEQ $9DA2 ; }

9D97: JMP $9D9E ; goto holdButtons;

moveInProgress:

9D9A: LDA #$00
9D9C: STA $00F5 ; clearNewlyPressedButtons();

holdButtons:

9D9E: LDA $00CE
9DA0: STA $00F7 ; setHeldButtons(heldButtons);

9DA2: RTS ; return;

startButtonPressed:

9DA3: LDA #$DD
9DA5: STA $00D2 ; reset index;

9DA7: LDA #$00
9DA9: STA $00B2 ; counter = 0;

9DAB: LDA #$01
9DAD: STA $00C0 ; gameMode = TITLE_SCREEN;

9DAF: RTS ; return;


$00D1$00D2$872D

9DE8: LDA $00D1
9DEA: CLC ; increment [$00D1]
9DEB: ADC #$01 ; possibly causing wrap around to 0
9DED: STA $00D1 ; which produces a carry

9DEF: LDA #$00
9DF1: ADC $00D2
9DF3: STA $00D2 ; add carry to [$00D2]

9DF5: RTS ; return


$00D0$FF. في هذه الحالة ، يتم تشغيل الكود التالي ، مخصص للكتابة على جدول الأزرار للعرض التوضيحي. ومع ذلك ، يتم تخزين الجدول في PRG-ROM. لن تؤثر محاولة الكتابة عليه على البيانات المحفوظة. بدلاً من ذلك ، تؤدي كل عملية كتابة إلى تشغيل تحويل مصرفي ، مما يؤدي إلى حدوث خلل موضح أدناه.

recording:

9DB0: JSR $AB9D ; pollController();

9DB3: LDA $00C0 ; if (gameMode != DEMO) {
9DB5: CMP #$05 ; return;
9DB7: BNE $9DE7 ; }

9DB9: LDA $00D0 ; if (not recording mode) {
9DBB: CMP #$FF ; return;
9DBD: BNE $9DE7 ; }

9DBF: LDA $00F7 ; if (getHeldButtons() == heldButtons) {
9DC1: CMP $00CE ; goto buttonsNotChanged;
9DC3: BEQ $9DE4 ; }

9DC5: LDX #$00
9DC7: LDA $00CE
9DC9: STA ($D1,X) ; demoButtonsTable[index] = heldButtons;

9DCB: JSR $9DE8 ; index++;

9DCE: LDA $00CF
9DD0: STA ($D1,X) ; demoButtonsTable[index] = repeats;

9DD2: JSR $9DE8 ; index++;

9DD5: LDA $00D2 ; if (reached end of demo table) {
9DD7: CMP #$DF ; return;
9DD9: BEQ $9DE7 ; }

9DDB: LDA $00F7
9DDD: STA $00CE ; heldButtons = getHeldButtons();

9DDF: LDA #$00
9DE1: STA $00CF ; repeats = 0;

9DE3: RTS ; return;

buttonsNotChanged:

9DE4: INC $00CF ; repeats++;

9DE6: RTS
9DE7: RTS ; return;





هذا يشير إلى أن المطورين يمكنهم تشغيل البرنامج جزئيًا أو كليًا في ذاكرة الوصول العشوائي.

للتغلب على هذه العقبة ، قمت بإنشاء lua/RecordDemo.luaواحدة موجودة في ملف مضغوط مع شفرة المصدر . بعد التبديل إلى وضع التسجيل التجريبي ، يعيد توجيه عمليات الكتابة إلى الجدول في وحدة تحكم Lua. يمكن من خلاله نسخ البايت ولصقه في ROM.

لتسجيل العرض التوضيحي الخاص بك ، قم بتشغيل FCEUX وقم بتنزيل ملف Nintendo Tetris ROM (File | Open ROM ...). ثم افتح نافذة Lua Script (File | Lua | New Lua Script Window ...) ، استعرض للوصول إلى الملف أو أدخل المسار. اضغط على زر التشغيل لبدء وضع التسجيل التجريبي ، ثم انقر فوق نافذة FCEUX لتحويل التركيز إليه. يمكنك التحكم في الأشكال حتى امتلاء جدول الأزرار. بعد ذلك ، ستعود اللعبة تلقائيًا إلى شاشة التوقف. انقر فوق Stop في نافذة Lua Script لإيقاف البرنامج النصي. ستظهر البيانات المسجلة في Output Console ، كما هو موضح في الشكل أدناه.


حدد جميع المحتويات وانسخ إلى الحافظة (Ctrl + C). ثم قم بتشغيل محرر Hex (Debug | Hex Editor ...). من قائمة Hex Editor ، حدد View | ملف ROM ثم ملف | انتقل إلى العنوان. في مربع حوار Goto ، أدخل 5D10 (عنوان جدول الأزرار التجريبية في ملف ROM) وانقر فوق Ok. ثم الصق محتويات الحافظة (Ctrl + V).


أخيرًا ، في قائمة FCEUX ، حدد NES | إعادة إذا تمكنت من تكرار كل هذه الخطوات ، فيجب استبدال النسخة التجريبية بإصدارك الخاص.

إذا كنت تريد حفظ التغييرات ، فحدد File | احفظ Rom As ... وأدخل اسم ملف ROM المعدل ، ثم انقر فوق Save.

بطريقة مماثلة ، يمكنك ضبط تسلسل tetriminos الذي تم إنشاؤه.

شاشة الموت


كما ذكر أعلاه ، لا يمكن لمعظم اللاعبين التعامل مع سرعة نزول الأرقام عند المستوى 29 ، مما يؤدي بسرعة إلى إكمال اللعبة. لذلك ، أصبح اللاعبون مرتبطين باسم "شاشة الموت". ولكن من وجهة نظر فنية ، لا تسمح شاشة الموت للاعب بالذهاب إلى أبعد من ذلك بسبب خطأ لا يكون فيه الانحدار السريع خطأً في الواقع ، ولكنه ميزة. كان المصممون لطيفين للغاية لدرجة أنهم سمحوا للعبة بالاستمرار بينما كان اللاعب قادرًا على تحمل السرعة الخارقة.

تظهر شاشة الموت الحقيقية على ما يقرب من 1550 صفًا تم سحبها. يتجلى بطرق مختلفة. في بعض الأحيان يتم إعادة تشغيل اللعبة. في حالات أخرى ، تصبح الشاشة سوداء. عادة ما تتجمد اللعبة ("تجمد") فورًا بعد حذف صف ، كما هو موضح أدناه. غالبًا ما يسبق هذه التأثيرات التحف الرسومية العشوائية.


شاشة الموت هي نتيجة خطأ في الرمز يضيف نقاطًا عند حذف الصفوف. يتم تخزين الحساب المكون من ستة أحرف باعتباره BCD معبأ قليلاً 24 بت ويقع في $0053- $0055. لإجراء تحويلات بين عدد الصفوف والنقاط التي تم الحصول عليها ، يتم استخدام جدول ؛ كل إدخال فيه قيمة BCD صغيرة 16 بت معبأة. بعد زيادة العدد الإجمالي للصفوف وربما المستوى ، يتم ضرب القيمة في هذه القائمة في المستوى رقم زائد واحد ، وتتم إضافة النتيجة إلى النقاط. هذا واضح في الجدول من كتيب نينتندو تتريس اليدوي:

9CA5: 00 00 ; 0: 0
9CA7: 40 00 ; 1: 40
9CA9: 00 01 ; 2: 100
9CAB: 00 03 ; 3: 300
9CAD: 00 12 ; 4: 1200





كما هو موضح أدناه ، تتم محاكاة عملية الضرب بدورة تضيف نقاطًا إلى النتيجة. يتم تنفيذه بعد قفل الشكل ، حتى لو لم يتم مسح الصفوف. لسوء الحظ ، لا يحتوي Ricoh 2A03 على وضع عشري ثنائي 6502 ؛ يمكنه تبسيط جسم الدورة بشكل كبير. بدلاً من ذلك ، يتم إجراء الإضافة في خطوات باستخدام الوضع الثنائي. يتم الحصول على أي رقم يتجاوز 9 بعد الجمع بشكل أساسي بطرح 10 وزيادة الأرقام الموجودة على اليسار. على سبيل المثال ، يتم تحويلها إلى . لكن هذا المخطط غير محمي بالكامل. خذ : الشيك غير قادر على تحويل النتيجة إلى

9C31: LDA $0044
9C33: STA $00A8
9C35: INC $00A8 ; for(i = 0; i <= level; i++) {

9C37: LDA $0056
9C39: ASL
9C3A: TAX
9C3B: LDA $9CA5,X ; points[0] = pointsTable[2 * completedLines];

9C3E: CLC
9C3F: ADC $0053
9C41: STA $0053 ; score[0] += points[0];

9C43: CMP #$A0
9C45: BCC $9C4E ; if (upper digit of score[0] > 9) {

9C47: CLC
9C48: ADC #$60
9C4A: STA $0053 ; upper digit of score[0] -= 10;
9C4C: INC $0054 ; score[1]++;
; }

9C4E: INX
9C4F: LDA $9CA5,X ; points[1] = pointsTable[2 * completedLines + 1];

9C52: CLC
9C53: ADC $0054
9C55: STA $0054 ; score[1] += points[1];

9C57: AND #$0F
9C59: CMP #$0A
9C5B: BCC $9C64 ; if (lower digit of score[1] > 9) {

9C5D: LDA $0054
9C5F: CLC ; lower digit of score[1] -= 10;
9C60: ADC #$06 ; increment upper digit of score[1];
9C62: STA $0054 ; }

9C64: LDA $0054
9C66: AND #$F0
9C68: CMP #$A0
9C6A: BCC $9C75 ; if (upper digit of score[1] > 9) {

9C6C: LDA $0054
9C6E: CLC
9C6F: ADC #$60
9C71: STA $0054 ; upper digit of score[1] -= 10;
9C73: INC $0055 ; score[2]++;
; }

9C75: LDA $0055
9C77: AND #$0F
9C79: CMP #$0A
9C7B: BCC $9C84 ; if (lower digit of score[2] > 9) {

9C7D: LDA $0055
9C7F: CLC ; lower digit of score[2] -= 10;
9C80: ADC #$06 ; increment upper digit of score[2];
9C82: STA $0055 ; }

9C84: LDA $0055
9C86: AND #$F0
9C88: CMP #$A0
9C8A: BCC $9C94 ; if (upper digit of score[2] > 9) {

9C8C: LDA #$99
9C8E: STA $0053
9C90: STA $0054
9C92: STA $0055 ; max out score to 999999;
; }

9C94: DEC $00A8
9C96: BNE $9C37 ; }


$07 + $07 = $0E$14$09 + $09 = $12$18. للتعويض عن ذلك ، لا يتجاوز أحد الأرقام العشرية في الإدخالات في بطاقة النقاط 6. بالإضافة إلى ذلك ، حتى تتمكن من استخدامها ، يكون الرقم الأخير من جميع الإدخالات دائمًا 0.

يستغرق الأمر بعض الوقت لإكمال هذه الدورة الطويلة والمعقدة. على المستويات العالية ، يؤثر عدد كبير من التكرارات على توقيت اللعبة ، لأنه يستغرق أكثر من 1/60 ثانية لإنشاء كل إطار. كل هذا نتيجة تؤدي إلى مظاهر مختلفة من "شاشة الموت".

يحد برنامج Lua AI النصي من عدد التكرارات في الحلقة إلى 30 - القيمة القصوى التي يمكن أن يحققها المصممون كما صممها المصممون ، مما يلغي شاشة الموت.

النهايات


في كتيب Nintendo Tetris ، تم وصف لعبة A-Type كما يلي:


تكافئ اللعبة اللاعبين الذين سجلوا عددًا كبيرًا بما فيه الكفاية من النقاط في إحدى الرسوم المتحركة الخمس للنهايات. يعتمد اختيار النهاية تمامًا على الرقمين في أقصى اليسار من النتيجة المكونة من ستة أرقام. كما هو موضح أدناه ، للحصول على أحد النهايات ، يجب على اللاعب تسجيل ما لا يقل عن 30،000 نقطة. ومن الجدير بالذكر أن - مرآة العناوين - . يتم تكرار الحساب في العناوين - . بعد اجتياز الاختبار الأول ، يتم تحديد الرسوم المتحركة النهائية ببيان التبديل التالي.

9A4D: LDA $0075
9A4F: CMP #$03
9A51: BCC $9A5E ; if (score[2] >= $03) {

9A53: LDA #$80
9A55: JSR $A459
9A58: JSR $9E3A
9A5B: JMP $9A64 ; select ending;
; }


$0060$007F$0040$005F$0073$0075



A96E: LDA #$00
A970: STA $00C4
A972: LDA $0075 ; if (score[2] < $05) {
A974: CMP #$05 ; ending = 0;
A976: BCC $A9A5 ; }

A978: LDA #$01
A97A: STA $00C4
A97C: LDA $0075 ; else if (score[2] < $07) {
A97E: CMP #$07 ; ending = 1;
A980: BCC $A9A5 ; }

A982: LDA #$02
A984: STA $00C4
A986: LDA $0075 ; else if (score[2] < $10) {
A988: CMP #$10 ; ending = 2;
A98A: BCC $A9A5 ; }

A98C: LDA #$03
A98E: STA $00C4
A990: LDA $0075 ; else if (score[2] < $12) {
A992: CMP #$12 ; ending = 3;
A994: BCC $A9A5 ; }

A996: LDA #$04 ; else {
A998: STA $00C4 ; ending = 4;
; }


في النهاية ، يتم إطلاق صواريخ ذات حجم متزايد من منصة الإطلاق بجوار كاتدرائية سانت باسيل. في النهاية الرابعة ، يتم عرض المركبة الفضائية بوران - النسخة السوفيتية من مكوك الفضاء الأمريكي. في أفضل النهاية ، ترتفع الكاتدرائية نفسها في الهواء ، وتتدلى UFO فوق لوحة الإطلاق. فيما يلي صورة لكل نهاية والنتيجة المرتبطة بها.
30000–49999
50000–69999
70000–99999
100000–119999
120000+

في وضع لعبة B-Type ، يتم تنفيذ اختبار آخر ، موصوف في كتيب Nintendo Tetris على النحو التالي:


إذا نجح اللاعب في مسح 25 صفًا ، فستعرض اللعبة النهاية ، اعتمادًا على المستوى الأولي. تتكون نهايات المستويات من 0 إلى 8 من حيوانات وأشياء تحلق أو تجري في الإطار ، تمر بشكل غامض خلف كاتدرائية القديس باسيل. يظهر UFO من أفضل نهاية لوضع A-Type في النهاية 3. في النهاية 4 ، تظهر التيروصورات الطائرة المنقرضة ، وفي النهاية 7 تظهر تنانين الطيران الأسطورية. في النهايتين 2 و 6 ، يتم عرض طيور بلا أجنحة: تشغيل طيور البطريق والنعام. في نهاية 5 ، تمتلئ السماء بمناطيد GOOD (يجب عدم الخلط بينها وبين المناطيد Goodyear). وفي نهاية 8 ، اكتسح الكثير من "بوراناس" الشاشة ، على الرغم من وجود واحدة فقط في الواقع.

يتم استخدام ارتفاع البداية (زائد 1) كمضاعف ، يكافئ اللاعب بعدد كبير من الحيوانات / الأشياء لزيادة التعقيد.

في أفضل نهاية لـ B-Type ، تظهر قلعة مليئة بشخصيات من عالم Nintendo: الأميرة Peach تصفق بيديها ، وتلعب Kid Icarus الكمان ، وتدق Donkey Kong على الطبل الكبير ، وترقص Mario و Luigi ، و Bowser يلعب الأكورديون ، و Samus يلعب التشيلو ، Link - على الناي ، بينما ترتفع قباب كاتدرائية القديس باسيل في الهواء. يعتمد مقدار هذه العناصر الموضحة في النهاية على الارتفاع الأولي. فيما يلي صور لجميع النهايات العشر.


يمكن للذكاء الاصطناعي مسح جميع الصفوف الخمسة والعشرين المطلوبة في وضع B-Type بسرعة عند أي مستوى أو ارتفاع أولي ، مما يسمح لك بمشاهدة أي من النهايات. من الجدير أيضًا تقييم مدى تعامله مع أكوام كبيرة من الكتل العشوائية.

في النهايات من 0 إلى 8 ، يمكن أن يتحرك ما يصل إلى 6 كائنات في الإطار. يتم تخزين إحداثيات y للكائنات في جدول موجود عند $A7B7. يتم تخزين المسافات الأفقية بين الكائنات في جدول في العنوان . يحدد تسلسل القيم بعلامة في العنوان سرعة واتجاه الكائنات. يتم تخزين مؤشرات العفريت في . في الواقع ، يتكون كل كائن من اثنين من العفاريت مع مؤشرات متجاورة. للحصول على الفهرس الثاني تحتاج إلى إضافة 1. على سبيل المثال ، يتكون التنين من

A7B7: 98 A8 C0 A8 90 B0 ; 0
A7BD: B0 B8 A0 B8 A8 A0 ; 1
A7C3: C8 C8 C8 C8 C8 C8 ; 2
A7C9: 30 20 40 28 A0 80 ; 3
A7CF: A8 88 68 A8 48 78 ; 4
A7D5: 58 68 18 48 78 38 ; 5
A7DB: C8 C8 C8 C8 C8 C8 ; 6
A7E1: 90 58 70 A8 40 38 ; 7
A7E7: 68 88 78 18 48 A8 ; 8


$A77B

A77B: 3A 24 0A 4A 3A FF ; 0
A781: 22 44 12 32 4A FF ; 1
A787: AE 6E 8E 6E 1E 02 ; 2
A78D: 42 42 42 42 42 02 ; 3
A793: 22 0A 1A 04 0A FF ; 4
A799: EE DE FC FC F6 02 ; 5
A79F: 80 80 80 80 80 FF ; 6
A7A5: E8 E8 E8 E8 48 FF ; 7
A7AB: 80 AE 9E 90 80 02 ; 8


$A771

A771: 01 ; 0: 1
A772: 01 ; 1: 1
A773: FF ; 2: -1
A774: FC ; 3: -4
A775: 01 ; 4: 1
A776: FF ; 5: -1
A777: 02 ; 6: 2
A778: 02 ; 7: 2
A779: FE ; 8: -1


$A7F3

A7F3: 2C ; 0: dragonfly
A7F4: 2E ; 1: dove
A7F5: 54 ; 2: penguin
A7F6: 32 ; 3: UFO
A7F7: 34 ; 4: pterosaur
A7F8: 36 ; 5: blimp
A7F9: 4B ; 6: ostrich
A7FA: 38 ; 7: dragon
A7FB: 3A ; 8: Buran


$38و $39. يتم تضمين البلاط لهذه العفاريت في جداول الأنماط أدناه.


قمنا بفحص الطاولة المركزية للنمط أعلاه ، يتم استخدامه لعرض tetrimino وملعب اللعب. ومن المثير للاهتمام أنه يحتوي على الأبجدية بأكملها ، بينما يحتوي الآخرون على جزء منه فقط لتوفير المساحة. لكن الأكثر إثارة للاهتمام هو الطائرات و طائرات الهليكوبتر في جدول النمط على اليسار. لا تظهر في النهايات أو في أجزاء أخرى من اللعبة. واتضح أن الطائرة والمروحية لديها مؤشرات العفاريت $30و $16ويمكنك تغيير الجدول المبين أعلاه، أن نراهم في العمل.



لسوء الحظ ، لا يتم عرض حوامل طائرات الهليكوبتر ، ولكن الدوارات الرئيسية والذيل متحركة بشكل جميل.

2 لاعب مقابل


يحتوي Nintendo Tetris على وضع غير مكتمل للاعبين يمكنك تمكينه عن طريق تغيير عدد اللاعبين ( $00BE) إلى 2. كما هو موضح أدناه ، يظهر حقلين للألعاب على خلفية وضع لاعب واحد.


لا توجد حدود بين الحقول لأن المنطقة المركزية للخلفية سوداء صلبة. تشير القيم 003الموضحة أعلى ميادين اللعب إلى عدد الصفوف التي تم محوها من قبل كل لاعب. يظهر الرقم المشترك الوحيد للاعبين في نفس المكان كما في وضع لاعب واحد. لسوء الحظ ، يقع في الملعب الصحيح. يتم تلوين المربعات والبلاط الآخر بشكل غير صحيح. وعندما يخسر اللاعب اللعبة مرة أخرى.

ولكن إذا تجاهلت هذه المشاكل ، فإن الوضع قابل للتشغيل تمامًا. يمكن لكل لاعب التحكم بشكل مستقل في القطع في الملعب المقابل. وعندما يكتب اللاعب مزدوجًا أو ثلاثيًا أو Tetris (أي يمسح صفين أو ثلاثة أو أربعة صفوف) ، تظهر صفوف القمامة التي تحتوي على مربع مفقود في الجزء السفلي من ساحة لعب الخصم.

يوجد حقل إضافي في $0500. أ $0060- $007Fعادة ما تكون مرآة $0040- $005Fتستخدم للاعب الثاني.

على الأرجح ، تم التخلي عن هذا الوضع المثير للاهتمام بسبب جدول التطوير المزدحم. أو ربما ترك عمدا غير مكتمل. أحد أسباب اختيار Tetris للعبة المرفقة مع Nintendo Game Boy هو تشجيعها على شراء Game Link Cable- ملحق يربط بين Game Boys معًا لتشغيل 2 لاعب مقابل الوضع. أضاف هذا الكبل عنصر "اجتماعية" إلى النظام - شجع الأصدقاء على شراء Game Boy للانضمام إلى المرح. ربما كانت نينتندو خائفة من أنه إذا كان إصدار وحدة التحكم في اللعبة يحتوي على 2 لاعب مقابل الوضع ، فإن القوة "الإعلانية" لـ Tetris ، التي حفزت شراء Game Boy ، قد تضعف.

الموسيقى والمؤثرات الصوتية


يتم تشغيل موسيقى الخلفية عند تعيين $06F5إحدى القيم المدرجة في الجدول.

القيمةالوصف
01موسيقى شاشة البداية غير المستخدمة
02تم تحقيق هدف وضع B-Type
03موسيقى -1
04موسيقى -2
05موسيقى -3
06موسيقى 1 اليجرو
07موسيقى - 2 أليغرو
08موسيقى 3 ​​اليجرو
09شاشة التهاني
0Aالنهايات
0Bتم تحقيق هدف وضع B-Type
يمكنك الاستماع إلى الموسيقى غير المستخدمة من شاشة التوقف هنا . في اللعبة نفسها ، لا شيء يبدو أثناء شاشة التوقف.

Music-1 هي نسخة من " Dance of the Dragee Fairy " ، موسيقى راقصة الباليه من الفصل الثالث من pas de deux waltz "The Nutcracker" من تأليف تشايكوفسكي. الموسيقى النهائية هي شكل مختلف من " آيات بولفايتر " ، وهي أغنية من أوبرا كارمن جورج بيزيت. يتم ترتيب هذه المؤلفات من قبل مؤلف الموسيقى المتبقية من موسيقى هيروكازو تاناكا .

Music-2 مستوحاة من الأغاني الفولكلورية الروسية التقليدية. الموسيقى -3 غامضة ومستقبلية وعطاء. لفترة من الوقت ، كانت نغمة رنين هاتف Nintendo of America لدعم العملاء.

لمساعدة اللاعب على الوقوع في حالة من الذعر عندما يقترب ارتفاع الكومة من سقف الملعب ، تبدأ نسخة من موسيقى الخلفية في اللعب بوتيرة سريعة ( $06- $08).

ومن المثير للاهتمام أنه من بين المؤلفات الموسيقية لا يوجد " تشابمان " ، وهو موضوع شهير يبدو في Game Boy Tetris.

وبدأت المؤثرات الصوتية عن طريق الكتابة إلى $06F0و $06F1وفقا للجدول التالي.

العنوانالقيمةالوصف
06F002ستارة نهاية اللعبة
06F003صاروخ في النهاية
06F101اختيار خيار القائمة
06F102اختيار شاشة القائمة
06F103Tetrimino Shift
06F104تلقى تتريس
06F105تناوب تتريمينو
06F106مستوى جديد
06F107قفل Tetrimino
06F108النقيق
06F109تنظيف الصف
06F10Aتمتلئ الصف

حالات اللعبة وأوضاع العرض


أثناء اللعب ، يتم تمثيل الحالة الحالية للعبة بعدد صحيح في العنوان $0048. في معظم الأحيان يكون له معنى $01يشير إلى أن اللاعب يتحكم في tetrimino النشط. ومع ذلك ، عندما يتم تثبيت القطعة في مكانها ، تنتقل اللعبة تدريجيًا من دولة $02إلى أخرى $08، كما هو موضح في الجدول.

الشرطالوصف
00معرف الاتجاه غير المعين
01يتحكم اللاعب في tetrimino النشط
02قفل Tetrimino في الملعب
03فحص الصفوف المملوءة
04عرض الرسوم المتحركة لتنظيف الصف
05تحديث الصفوف والإحصائيات
06التحقق من هدف وضع B-Type
07غير مستخدم
08إنشاء Tetrimino التالي
09غير مستخدم
0Aتحديث لعبة الستارة
0Bزيادة حالة الألعاب

يحدث تفرع الرمز ، وفقًا لحالة اللعبة ، على العنوان التالي $81B2: في حالة التبديل ، ينتقل إلى الرمز الذي يعين قيمة تشير إلى أن الاتجاه غير محدد. المعالج لا يسمى أبداً؛ ومع ذلك ، حالة اللعبة بمثابة إشارة إلى أجزاء أخرى من التعليمات البرمجية. تسمح الحالة للاعب بتحويل وتدوير وخفض التتريمينو النشط: كما هو موضح في الأقسام السابقة ، فإن إجراءات التحول والتناوب والخفض للشكل قبل تنفيذ الرمز تحقق من المواضع الجديدة لـ tetrimino. الطريقة الوحيدة لمنع شكل في الوضع الخاطئ هي إنشائه أعلى شكل موجود. في هذه الحالة تنتهي اللعبة. كما هو موضح أدناه ، يقوم رمز الحالة بإجراء هذا الفحص.

81B2: LDA $0048
81B4: JSR $AC82 ; switch(playState) {
81B7: 2F 9E ; case 00: goto 9E2F; // Unassign orientationID
81B9: CF 81 ; case 01: goto 81CF; // Player controls active Tetrimino
81BB: A2 99 ; case 02: goto 99A2; // Lock Tetrimino into playfield
81BD: 6B 9A ; case 03: goto 9A6B; // Check for completed rows
81BF: 39 9E ; case 04: goto 9E39; // Display line clearing animation
81C1: 58 9B ; case 05: goto 9B58; // Update lines and statistics
81C3: F2 A3 ; case 06: goto A3F2; // B-Type goal check; Unused frame for A-Type
81C5: 03 9B ; case 07: goto 9B03; // Unused frame; Execute unfinished 2 player mode logic
81C7: 8E 98 ; case 08: goto 988E; // Spawn next Tetrimino
81C9: 39 9E ; case 09: goto 9E39; // Unused
81CB: 11 9A ; case 0A: goto 9A11; // Update game over curtain
81CD: 37 9E ; case 0B: goto 9E37; // Increment play state
; }


$00orientationID$13

9E2F: LDA #$13
9E31: STA $0042 ; orientationID = UNASSIGNED;

9E33: RTS ; return;


$00

$01

81CF: JSR $89AE ; shift Tetrimino;
81D2: JSR $88AB ; rotate Tetrimino;
81D5: JSR $8914 ; drop Tetrimino;

81D8: RTS ; return;


$02. إذا كان الوضع المقفل صحيحًا ، فإنه يضع علامة على الخلايا الأربع المرتبطة بمجال اللعب على أنها مشغولة. وبخلاف ذلك ، تنتقل إلى حالة - الستار المشؤوم لنهاية اللعبة.

99A2: JSR $948B ; if (new position valid) {
99A5: BEQ $99B8 ; goto updatePlayfield;
; }

99A7: LDA #$02
99A9: STA $06F0 ; play curtain sound effect;

99AC: LDA #$0A
99AE: STA $0048 ; playState = UPDATE_GAME_OVER_CURTAIN;

99B0: LDA #$F0
99B2: STA $0058 ; curtainRow = -16;

99B4: JSR $E003 ; updateAudio();

99B7: RTS ; return;


$0A


يتم رسم الستارة من أعلى الملعب إلى أسفل ، وينزل خط واحد كل 4 إطارات. curtainRow( $0058) تتم التهيئة بقيمة with16 ، مما يؤدي إلى حدوث تأخير إضافي قدره 0.27 ثانية بين القفل النهائي وبداية الرسم المتحرك. في العنوان $9A21في حالة $0Aالكود الموضح أدناه ، يتم الوصول إلى جدول الضرب ، والذي يتم عرضه بشكل خاطئ كأرقام مستوى. يتم ذلك لتدرج curtainRowبمقدار 10. بالإضافة إلى ذلك ، كما هو موضح أعلاه ، يبدأ الرمز في العنوان $9A51الرسوم المتحركة النهائية إذا كانت نقاط اللاعب لا تقل عن 30،000 نقطة ؛ وإلا ، فإنه يتوقع النقر فوق ابدأ. يكتمل الرمز عن طريق تعيين قيمة لحالة اللعبة ، ولكن لا يتم استدعاء المعالج المقابل بسبب اكتمال اللعبة.

9A11: LDA $0058 ; if (curtainRow == 20) {
9A13: CMP #$14 ; goto endGame;
9A15: BEQ $9A47 ; }

9A17: LDA $00B1 ; if (frameCounter not divisible by 4) {
9A19: AND #$03 ; return;
9A1B: BNE $9A46 ; }

9A1D: LDX $0058 ; if (curtainRow < 0) {
9A1F: BMI $9A3E ; goto incrementCurtainRow;
; }

9A21: LDA $96D6,X
9A24: TAY ; rowIndex = 10 * curtainRow;

9A25: LDA #$00
9A27: STA $00AA ; i = 0;

9A29: LDA #$13
9A2B: STA $0042 ; orientationID = NONE;

drawCurtainRow:

9A2D: LDA #$4F
9A2F: STA ($B8),Y ; playfield[rowIndex + i] = CURTAIN_TILE;
9A31: INY
9A32: INC $00AA ; i++;
9A34: LDA $00AA
9A36: CMP #$0A ; if (i != 10) {
9A38: BNE $9A2D ; goto drawCurtainRow;
; }

9A3A: LDA $0058
9A3C: STA $0049 ; vramRow = curtainRow;

incrementCurtainRow:

9A3E: INC $0058 ; curtainRow++;

9A40: LDA $0058 ; if (curtainRow != 20) {
9A42: CMP #$14 ; return;
9A44: BNE $9A46 ; }

9A46: RTS ; return;

endGame:

9A47: LDA $00BE
9A49: CMP #$02
9A4B: BEQ $9A64 ; if (numberOfPlayers == 1) {

9A4D: LDA $0075
9A4F: CMP #$03
9A51: BCC $9A5E ; if (score[2] >= $03) {

9A53: LDA #$80
9A55: JSR $A459
9A58: JSR $9E3A
9A5B: JMP $9A64 ; select ending;
; }

9A5E: LDA $00F5 ; if (not just pressed Start) {
9A60: CMP #$10 ; return;
9A62: BNE $9A6A ; }
; }

9A64: LDA #$00
9A66: STA $0048 ; playState = INITIALIZE_ORIENTATION_ID;
9A68: STA $00F5 ; clear newly pressed buttons;

9A6A: RTS ; return;


$00

يتم نسخ خطوط الملعب بشكل تدريجي إلى VRAM لعرضها. فهرس الصف الحالي المراد نسخه موجود في vramRow( $0049). $9A3C vramRowيتم تعيين قيمة على العنوان curtainRow، مما يجعل هذا الخط مرئيًا في النهاية عند العرض.

تحدث عمليات المعالجة باستخدام VRAM أثناء فاصل طمس رأسي ، يتم التعرف عليه بواسطة معالج المقاطعة الموضح في قسم "شاشة المعلومات القانونية". يطلق عليه روتين فرعي الموضح أدناه (تم وضع علامة عليه في تعليقات معالج المقاطعة باسم render()). يشبه وضع التقديم وضع اللعبة. يتم تخزينه في العنوان ويمكن أن يحتوي على إحدى القيم التالية:

804B: LDA $00BD
804D: JSR $AC82 ; switch(renderMode) {
8050: B1 82 ; case 0: goto 82B1; // Legal and title screens
8052: DA 85 ; case 1: goto 85DA; // Menu screens
8054: 44 A3 ; case 2: goto A344; // Congratulations screen
8056: EE 94 ; case 3: goto 94EE; // Play and demo
8058: 95 9F ; case 4: goto 9F95; // Ending animation
; }


$00BD

القيمةالوصف
00شاشة مع القانونية المعلومات وشاشة التوقف
01شاشات القائمة
02شاشة التهاني
03لعبة وعرض
04إنهاء الرسوم المتحركة

يظهر جزء من وضع العرض $03أدناه. كما ترى أدناه ، فإنه يمر في VRAM خطًا من ساحة اللعب مع فهرس . إذا كان أكبر من 20 ، فإن الروتين لا يفعل شيئًا. يحتوي الجدول ( ) على عناوين VRAM بتنسيق endian الصغير المطابق للخطوط المعروضة لملعب اللعب الذي تم تحويله بمقدار 6 في الوضع العادي وبواسطة and2 و 12 لمجال اللعب في الوضع غير المكتمل 2 Player Versus. وحدات البايت في هذا الجدول هي جزء من قائمة القيم التي يتم عرضها بشكل خاطئ كأرقام مستوى بعد المستوى 29. يتم الحصول على البايتات السفلية والعلوية المجاورة لكل عنوان بشكل منفصل ويتم دمجها بشكل أساسي في عنوان 16 بت ، والذي يتم استخدامه في دورة النسخ. يتم تنفيذ زيادة في نهاية الروتين.

952A: JSR $9725 ; copyPlayfieldRowToVRAM();
952D: JSR $9725 ; copyPlayfieldRowToVRAM();
9530: JSR $9725 ; copyPlayfieldRowToVRAM();
9533: JSR $9725 ; copyPlayfieldRowToVRAM();


copyPlayfieldRowToVRAM()vramRowvramRow

9725: LDX $0049 ; if (vramRow > 20) {
9727: CPX #$15 ; return;
9729: BPL $977E ; }

972B: LDA $96D6,X
972E: TAY ; playfieldAddress = 10 * vramRow;

972F: TXA
9730: ASL
9731: TAX
9732: INX ; high = vramPlayfieldRows[vramRow * 2 + 1];
9733: LDA $96EA,X
9736: STA $2006
9739: DEX

973A: LDA $00BE
973C: CMP #$01
973E: BEQ $975E ; if (numberOfPlayers == 2) {

9740: LDA $00B9
9742: CMP #$05
9744: BEQ $9752 ; if (leftPlayfield) {

9746: LDA $96EA,X
9749: SEC
974A: SBC #$02
974C: STA $2006 ; low = vramPlayfieldRows[vramRow * 2] - 2;

974F: JMP $9767 ; } else {

9752: LDA $96EA,X
9755: CLC
9756: ADC #$0C
9758: STA $2006 ; low = vramPlayfieldRows[vramRow * 2] + 12;

975B: JMP $9767 ; } else {

975E: LDA $96EA,X
9761: CLC
9762: ADC #$06 ; low = vramPlayfieldRows[vramRow * 2] + 6;
9764: STA $2006 ; }

; vramAddress = (high << 8) | low;

9767: LDX #$0A
9769: LDA ($B8),Y
976B: STA $2007
976E: INY ; for(i = 0; i < 10; i++) {
976F: DEX ; vram[vramAddress + i] = playfield[playfieldAddress + i];
9770: BNE $9769 ; }

9772: INC $0049 ; vramRow++;
9774: LDA $0049 ; if (vramRow < 20) {
9776: CMP #$14 ; return;
9778: BMI $977E ; }

977A: LDA #$20
977C: STA $0049 ; vramRow = 32;

977E: RTS ; return;


vramPlayfieldRows$96EA

vramRow. إذا وصلت القيمة إلى 20 ، فيتم تعيينها بقيمة 32 ، مما يعني أن النسخة مكتملة بالكامل. كما هو موضح أعلاه ، يتم نسخ 4 خطوط فقط لكل إطار.

معالج الدولة $03مسؤول عن التعرف على الخطوط المكتملة وإزالتها من الملعب. خلال 4 مكالمات منفصلة ، يقوم بمسح إزاحة الخط [−2, 1]بالقرب من مركز tetrimino (كل من إحداثيات جميع مربعات tetrimino في هذا الفاصل الزمني). يتم تخزين فهارس الصفوف المكتملة في $004A- $004D؛ يُستخدم المؤشر المسجل 0 للإشارة إلى عدم وجود صفوف مكتملة في هذا التمرير. المعالج مبين أدناه. لا يسمح الفحص في البداية للمعالج بالتنفيذ عند نقل خطوط الملعب إلى VRAM (معالج الحالة)

9A6B: LDA $0049
9A6D: CMP #$20 ; if (vramRow < 32) {
9A6F: BPL $9A74 ; return;
9A71: JMP $9B02 ; }

9A74: LDA $0041 ; rowY = tetriminoY - 2;
9A76: SEC
9A77: SBC #$02 ; if (rowY < 0) {
9A79: BPL $9A7D ; rowY = 0;
9A7B: LDA #$00 ; }

9A7D: CLC
9A7E: ADC $0057
9A80: STA $00A9 ; rowY += lineIndex;

9A82: ASL
9A83: STA $00A8
9A85: ASL
9A86: ASL
9A87: CLC
9A88: ADC $00A8
9A8A: STA $00A8 ; rowIndex = 10 * rowY;

9A8C: TAY
9A8D: LDX #$0A
9A8F: LDA ($B8),Y
9A91: CMP #$EF ; for(i = 0; i < 10; i++) {
9A93: BEQ $9ACC ; if (playfield[rowIndex + i] == EMPTY_TILE) {
9A95: INY ; goto rowNotComplete;
9A96: DEX ; }
9A97: BNE $9A8F ; }

9A99: LDA #$0A
9A9B: STA $06F1 ; play row completed sound effect;

9A9E: INC $0056 ; completedLines++;

9AA0: LDX $0057
9AA2: LDA $00A9
9AA4: STA $4A,X ; lines[lineIndex] = rowY;

9AA6: LDY $00A8
9AA8: DEY
9AA9: LDA ($B8),Y
9AAB: LDX #$0A
9AAD: STX $00B8
9AAF: STA ($B8),Y
9AB1: LDA #$00
9AB3: STA $00B8
9AB5: DEY ; for(i = rowIndex - 1; i >= 0; i--) {
9AB6: CPY #$FF ; playfield[i + 10] = playfield[i];
9AB8: BNE $9AA9 ; }

9ABA: LDA #$EF
9ABC: LDY #$00
9ABE: STA ($B8),Y
9AC0: INY ; for(i = 0; i < 10; i++) {
9AC1: CPY #$0A ; playfield[i] = EMPTY_TILE;
9AC3: BNE $9ABE ; }

9AC5: LDA #$13
9AC7: STA $0042 ; orientationID = UNASSIGNED;

9AC9: JMP $9AD2 ; goto incrementLineIndex;

rowNotComplete:

9ACC: LDX $0057
9ACE: LDA #$00
9AD0: STA $4A,X ; lines[lineIndex] = 0;

incrementLineIndex:

9AD2: INC $0057 ; lineIndex++;

9AD4: LDA $0057 ; if (lineIndex < 4) {
9AD6: CMP #$04 ; return;
9AD8: BMI $9B02 ; }

9ADA: LDY $0056
9ADC: LDA $9B53,Y
9ADF: CLC
9AE0: ADC $00BC
9AE2: STA $00BC ; totalGarbage += garbageLines[completedLines];

9AE4: LDA #$00
9AE6: STA $0049 ; vramRow = 0;
9AE8: STA $0052 ; clearColumnIndex = 0;

9AEA: LDA $0056
9AEC: CMP #$04
9AEE: BNE $9AF5 ; if (completedLines == 4) {
9AF0: LDA #$04 ; play Tetris sound effect;
9AF2: STA $06F1 ; }

9AF5: INC $0048 ; if (completedLines > 0) {
9AF7: LDA $0056 ; playState = DISPLAY_LINE_CLEARING_ANIMATION;
9AF9: BNE $9B02 ; return;
; }

9AFB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;

9AFD: LDA #$07
9AFF: STA $06F1 ; play piece locked sound effect;

9B02: RTS ; return;


vramRow$03دعا في كل إطار). إذا تم الكشف عن الصفوف المعبأة ، يتم vramRowإعادة تعيينها إلى 0 ، مما يفرض النقل الكامل.

lineIndex( $00A9) تتم تهيئته بقيمة 0 ويتم تنفيذ زيادته في كل تمريرة.

على عكس حالة $0Aاللعبة وروتين نسخ حقل اللعبة ، الذي يستخدم جدول ضرب العنوان $96D6، كتلة تبدأ $9A82بضربات rowY10 باستخدام التحولات والإضافات:

rowIndex = (rowY << 1) + (rowY << 3); // rowIndex = 2 * rowY + 8 * rowY;

يتم ذلك فقط لأنه rowYمحدود بالفاصل الزمني [0, 20]، ويغطي جدول الضرب فقط [0, 19]. قد يمتد مسح الصف إلى ما بعد نهاية الملعب. ومع ذلك ، كما ذكرنا سابقًا ، فإن اللعبة تبدأ $0400- $04FFبقيمة$EF(بلاطة فارغة) ، إنشاء أكثر من 5 خطوط مخفية فارغة إضافية تحت أرضية الملعب.

الكتلة التي تبدأ بـ $9ADAهي جزء من الوضع غير المكتمل لـ 2 Player Versus. كما ذكر أعلاه ، يضيف مسح الصفوف حطامًا إلى ساحة لعب الخصم. يتم تحديد عدد صفوف القمامة من خلال الجدول الموجود في العنوان $9B53: تقوم الدورة الموجودة في العنوان بتحويل المادة فوق الصف المعبأ سطر واحد لأسفل. يستغل حقيقة أن كل سطر في تسلسل مستمر يتم فصله عن الآخر بمقدار 10 بايت. تقوم الحلقة التالية بمسح الخط العلوي. يتم تنفيذ الرسوم المتحركة الواضحة للصف أثناء حالة اللعبة ، ولكن كما هو موضح أدناه ، لا يحدث في معالج حالة اللعبة ، وهو فارغ تمامًا.

9B53: 00 ; no cleared lines
9B54: 00 ; Single
9B55: 01 ; Double
9B56: 02 ; Triple
9B57: 04 ; Tetris


$9AA6

$04

9E39: RTS ; return;

بدلاً من ذلك ، أثناء حالة اللعبة $04، يتم تنفيذ التفرع التالي لوضع العرض $03. والقيم المتطابقة مطلوبة لوضع 2 Player Versus غير المكتمل. يظهر روتين أدناه . يتم استدعاؤها في كل إطار ، ولكن الشرط في البداية يسمح بتنفيذها فقط في كل إطار رابع. في كل مسار ، يتنقل خلال قائمة مؤشرات الصفوف المكتملة ويزيل عمودين في هذه الصفوف ، وينتقل من العمود المركزي إلى الخارج. يتم إنشاء عنوان VRAM 16 بت بنفس الطريقة الموضحة في روتين حقل النسخ. ومع ذلك ، في هذه الحالة ، تقوم بإجراء إزاحة بواسطة فهرس العمود الذي تم الحصول عليه من الجدول أدناه.

94EE: LDA $0068
94F0: CMP #$04
94F2: BNE $9522 ; if (playState == DISPLAY_LINE_CLEARING_ANIMATION) {

94F4: LDA #$04
94F6: STA $00B9 ; leftPlayfield = true;

94F8: LDA $0072
94FA: STA $0052
94FC: LDA $006A
94FE: STA $004A
9500: LDA $006B
9502: STA $004B
9504: LDA $006C
9506: STA $004C
9508: LDA $006D
950A: STA $004D
950C: LDA $0068
950E: STA $0048 ; mirror values;

9510: JSR $977F ; updateLineClearingAnimation();

; ...
; }


leftPlayfield

updateLineClearingAnimation()

977F: LDA $00B1 ; if (frameCounter not divisible by 4) {
9781: AND #$03 ; return;
9783: BNE $97FD ; }

9785: LDA #$00 ; for(i = 0; i < 4; i++) {
9787: STA $00AA ; rowY = lines[i];
9789: LDX $00AA ; if (rowY == 0) {
978B: LDA $4A,X ; continue;
978D: BEQ $97EB ; }

978F: ASL
9790: TAY
9791: LDA $96EA,Y
9794: STA $00A8 ; low = vramPlayfieldRows[2 * rowY];

9796: LDA $00BE ; if (numberOfPlayers == 2) {
9798: CMP #$01 ; goto twoPlayers;
979A: BNE $97A6 ; }

979C: LDA $00A8
979E: CLC
979F: ADC #$06
97A1: STA $00A8 ; low += 6;

97A3: JMP $97BD ; goto updateVRAM;

twoPlayers:

97A6: LDA $00B9
97A8: CMP #$04
97AA: BNE $97B6 ; if (leftPlayfield) {

97AC: LDA $00A8
97AE: SEC
97AF: SBC #$02
97B1: STA $00A8 ; low -= 2;

97B3: JMP $97BD ; } else {

97B6: LDA $00A8
97B8: CLC
97B9: ADC #$0C ; low += 12;
97BB: STA $00A8 ; }

updateVRAM:

97BD: INY
97BE: LDA $96EA,Y
97C1: STA $00A9
97C3: STA $2006
97C6: LDX $0052 ; high = vramPlayfieldRows[2 * rowY + 1];
97C8: LDA $97FE,X
97CB: CLC ; rowAddress = (high << 8) | low;
97CC: ADC $00A8
97CE: STA $2006 ; vramAddress = rowAddress + leftColumns[clearColumnIndex];
97D1: LDA #$FF
97D3: STA $2007 ; vram[vramAddress] = 255;

97D6: LDA $00A9
97D8: STA $2006
97DB: LDX $0052 ; high = vramPlayfieldRows[2 * rowY + 1];
97DD: LDA $9803,X
97E0: CLC ; rowAddress = (high << 8) | low;
97E1: ADC $00A8
97E3: STA $2006 ; vramAddress = rowAddress + rightColumns[clearColumnIndex];
97E6: LDA #$FF
97E8: STA $2007 ; vram[vramAddress] = 255;

97EB: INC $00AA
97ED: LDA $00AA
97EF: CMP #$04
97F1: BNE $9789 ; }

97F3: INC $0052 ; clearColumnIndex++;
97F5: LDA $0052 ; if (clearColumnIndex < 5) {
97F7: CMP #$05 ; return;
97F9: BMI $97FD ; }

97FB: INC $0048 ; playState = UPDATE_LINES_AND_STATISTICS;

97FD: RTS ; return;




97FE: 04 03 02 01 00 ; left columns
9803: 05 06 07 08 09 ; right columns


لتنظيف الرسوم المتحركة ، مطلوب 5 تمريرات. ثم ينتقل الرمز إلى حالة اللعبة التالية. يحتوي

معالج حالة اللعبة $05على التعليمات البرمجية الموضحة في قسم "الصفوف والإحصائيات". ينتهي المعالج بهذا الرمز: لا يتم إعادة تعيين المتغير حتى نهاية حالة اللعبة ، وبعد ذلك يتم استخدامه لتحديث العدد الإجمالي للصفوف والنتيجة. يسمح هذا التسلسل بتنفيذ خطأ مثير للاهتمام. في الوضع التجريبي ، تحتاج إلى الانتظار حتى تجمع اللعبة الصف الكامل ، ثم اضغط سريعًا على بدء حتى انتهاء الرسوم المتحركة لمسح السلسلة. ستعود اللعبة إلى شاشة التوقف ، ولكن إذا اخترت الوقت المناسب ، فسيتم حفظ القيمة . يمكنك الآن بدء اللعبة في وضع A-Type. عند قفل مكان الشكل الأول ، معالج حالة اللعبة

9C9E: LDA #$00
9CA0: STA $0056 ; completedLines = 0;

9CA2: INC $0048 ; playState = B_TYPE_GOAL_CHECK;

9CA4: RTS ; return;


completedLines$05completedLines$03يبدأ مسح الصفوف المكتملة. لن يجدها ، ولكن تركها completedLinesدون تغيير. أخيرًا ، عندما يتم تحقيق حالة اللعبة ، $05سيزداد إجمالي عدد الصفوف والنتيجة ، كما لو كنت قد سجلتها.

أسهل طريقة للقيام بذلك هي الحصول على أكبر قدر ، بانتظار العرض التجريبي لجمع Tetris (سيكون هناك اثنان منهم في العرض). بمجرد أن ترى وميض الشاشة ، انقر فوق ابدأ.


بعد بدء لعبة جديدة ، ستستمر الشاشة في الوميض. كل هذا بفضل التعليمات البرمجية التالية التي يطلق عليها معالج المقاطعة. في الواقع ، إذا تركت الرقم الأول ينزل تلقائيًا إلى أرضية الملعب ، فستزداد النتيجة بقيمة أكبر ، لأن ( ) سيحفظ أيضًا قيمته من العرض التوضيحي. هذا صحيح حتى في الحالات التي لم يملأ فيها العرض صفًا واحدًا. لا يتم إعادة تعيينه حتى يتم الضغط على زر "أسفل". علاوة على ذلك ، إذا قمت بالنقر فوق ابدأ أثناء الرسوم المتحركة لمسح صفوف تركيبة Tetris في الوضع التجريبي ، ثم انتظر حتى يبدأ العرض التوضيحي مرة أخرى ، فلن يتم احتساب نقاط Tetris فقط في العرض التوضيحي ، ولكن سيتم خلط التوقيت بالكامل. نتيجة لذلك ، سيخسر العرض التجريبي اللعبة. بعد تقليص نهاية اللعبة ، يمكنك العودة إلى شاشة التوقف بالنقر فوق ابدأ.

9673: LDA #$3F
9675: STA $2006
9678: LDA #$0E
967A: STA $2006 ; prepare to modify background tile color;

967D: LDX #$00 ; color = DARK_GRAY;

967F: LDA $0056
9681: CMP #$04
9683: BNE $9698 ; if (completedLines == 4) {

9685: LDA $00B1
9687: AND #$03
9689: BNE $9698 ; if (frameCounter divisible by 4) {

968B: LDX #$30 ; color = WHITE;

968D: LDA $00B1
968F: AND #$07
9691: BNE $9698 ; if (frameCounter divisible by 8) {

9693: LDA #$09
9695: STA $06F1 ; play clear sound effect;

; }
; }
; }

9698: STX $2007 ; update background tile color;


holdDownPoints$004FholdDownPoints



تقوم حالة اللعبة $06بإجراء فحص مستهدف لألعاب B-Type. في الوضع A-Type ، فهو في الأساس إطار غير مستخدم. تحتوي

حالة اللعبة $07حصريًا على 2 لاعب غير كامل مقابل المنطق. في وضع لاعب واحد ، يتصرف مثل إطار غير مستخدم.

تتم $08مناقشة حالة اللعبة في القسمين "إنشاء Tetrimino" و "اختيار Tetrimino". لا

يتم $09استخدام حالة اللعبة . $0Bيزيد من حالة اللعبة ، ولكن يبدو أيضًا غير مستخدم.

وأخيرًا ، الدورة الرئيسية للعبة:

; while(true) {

8138: JSR $8161 ; branchOnGameMode();

813B: CMP $00A7 ; if (vertical blanking interval wait requested) {
813D: BNE $8142 ; waitForVerticalBlankingInterval();
813F: JSR $AA2F ; }

8142: LDA $00C0
8144: CMP #$05
8146: BNE $815A ; if (gameMode == DEMO) {

8148: LDA $00D2
814A: CMP #$DF
814C: BNE $815A ; if (reached end of demo table) {

814E: LDA #$DD
8150: STA $00D2 ; reset demo table index;

8152: LDA #$00
8154: STA $00B2 ; clear upper byte of frame counter;

8156: LDA #$01
8158: STA $00C0 ; gameMode = TITLE_SCREEN;
; }
; }
815A: JMP $8138 ; }

Source: https://habr.com/ru/post/ar420725/


All Articles