بلاك جاك

بلاك جاك

تمرين تعلّمي

مقدمة

الجمل الشرطية

RFLAGs

يوجد سجل خاص يُسمى rflags. تعمل بتاته كأعلام لحالات مختلفة.

بعضها مذكور أدناه:

الاسم الرمز البت
الحمل CF 0
الصفر ZF 6
الإشارة SF 7
الفيضان OF 11

تعليمات المقارنة

لا تُعدَّل الأعلام في rflags مباشرةً. بل تضبطها تعليمات مختلفة كثيرة.

على سبيل المثال، يُضبط ZF بواسطة عمليات حسابية أو عمليات على البتات كثيرة عندما تكون النتيجة صفرًا.

من أكثر التعليمات شيوعًا في اختبار الشروط التعليمة cmp. إنها تأخذ معاملَين وتحدّث الأعلام، لكنها لا تعدّل معامليها.

تعليمة cmp

تطرح التعليمة cmp المعامل الثاني من الأول وتضبط الأعلام وفقًا للنتيجة.

إذا كان A المعامل الأول و B المعامل الثاني:

العلم يُضبط عندما
CF A < B (بدون إشارة)
ZF A == B
SF A < B (بإشارة، بدون فيضان)
OF فيضان في الطرح بإشارة

التفرّع

افتراضيًا، يُنفَّذ الكود في x86-64 بالتتابع من الأعلى إلى الأسفل.

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

في اللغات عالية المستوى، يُفعل هذا عادةً باستخدام تجريدات مثل الجمل الشرطية if...else. لكن هذه لا توجد في x86-64.

بدلًا من ذلك، توفر x86-64 تعليمات تنقل التنفيذ فعليًا إلى موضع آخر في الكود. وهذا ما يُسمى branching.

Note

لقد رأينا بالفعل تعليمتين من هذا النوع: call و ret.

عندما تُستدعى دالة، يُنقل التنفيذ من المستدعي إلى الدالة المستدعاة. وعند الإرجاع، يُنقل التنفيذ مجددًا إلى المستدعي.

إذا لم يوجد ret، ينتقل التنفيذ مباشرةً من دالة إلى التي تليها. ويمكن أحيانًا استخدام هذا لتحسين مسار الكود.

القفز غير الشرطي

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

تأمّل، على سبيل المثال، الدالة التالية:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

عندما تُستدعى fn، يبدأ التنفيذ من mov rax, 5. وهذا يضبط قيمة rax على 5 عند تلك النقطة.

التعليمة التالية هي jmp end، وهي تنقل التنفيذ إلى التسمية end.

بعد end، التعليمة التالية هي ret، وهي تنقل التنفيذ مجددًا إلى الدالة المستدعية.

لاحظ أنه بما أن add rax, 10 تقع بعد jmp end وقبل end، فإنها لا تُنفَّذ أبدًا. قيمة rax عندما تُرجع fn هي 5.

القفز الشرطي

تنقل مجموعة التعليمات jcc تنفيذ البرنامج إلى نقطة أخرى فقط إذا تحقق شرط معيّن. وإلا، يستمر التنفيذ بالتتابع.

يقابل كل شرط عَلَمًا واحدًا أو أكثر في rflags. تختبر بعض صيغ jcc أن عَلَمًا ما مضبوط، بينما تختبر أخرى أنه مُصفَّر.

الرمز cc في jcc ليس حرفيًا، بل يشير إلى اللاحقة المحددة المرتبطة بالعلم المختبَر.

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

اللاحقة تقفز إذا
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

وهناك لاحقات أخرى كثيرة اختيرت لتشير إلى معناها في تعليمة cmp. على سبيل المثال:

التعليمة اللاحقة تقفز إذا
cmp A, B e A == B
cmp A, B l A < B (بإشارة)
cmp A, B b A < B (بدون إشارة)
cmp A, B g A > B (بإشارة)
cmp A, B a A > B (بدون إشارة)

من الممكن إضافة e بعد l أو b أو g أو a لتضمين التساوي في الشرط:

cmp rcx, r8
jge two      ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two      ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison

لكل اللاحقات، توجد صيغ تفحص السلوك المعاكس. لها الصياغة نفسها، لكن مع n قبل اللاحقة.

على سبيل المثال، تقفز jnz عندما لا يكون ZF مضبوطًا. وبالمثل، تقفز jnae عندما لا يكون A >= B (حيث يُفسَّر A و B كعددين صحيحين بدون إشارة).

Note

بعض اللاحقات أسماء بديلة لنفس الشروط. على سبيل المثال، jz (اللاحقة z، الخاصة بـ ZF) و je (اللاحقة e، وتعني التساوي) تقفزان كلتاهما عندما يكون ZF مضبوطًا. ذلك لأن ZF مع cmp يُضبط عندما يكون ناتج الطرح صفرًا، وهو ما يقابل تساوي المعاملين.

غير أن لاحقات أخرى تختبر توليفة من الأعلام ولا يمكن استبدالها مباشرةً بلاحقة عَلَم واحدة.

فضّل اللاحقة التي تصف دلالات مقارنتك على نحو أفضل.

التسميات المحلية

تكون التسميات مرئية في ملف المصدر بالكامل، وليست محلية داخل دالة. لذلك يستحيل إعادة استخدام اسم تسمية.

لمحاكاة سلوك التسمية المحلية، يوفّر NASM صياغة خاصة لتسمية تُعلَن بنقطة (.) قبلها. تُعرّف هذه الصياغة تسمية تتضمن ضمنيًا اسم التسمية السابقة غير المنقوطة:

section .text
fn1:
    ...
.example: ; this is fn1.example
    ...
    ret

fn2:
    ...
.example: ; this is fn2.example
    ...
    ret

لا يزال من الممكن القفز إلى هذه التسمية من أي مكان في الكود باستخدام اسم التسمية الكامل، مثل jmp fn1.example.

غير أن القفز الذي يستخدم الجزء من التسمية بدءًا من النقطة سيكون إلى التسمية داخل الدالة الأعلى. على سبيل المثال، يتصرف .example كما لو كان محليًا داخل الدالة:

section .text
fn1:
    ...
.example:
    ...
    jmp .example ; this jumps to fn1.example

fn2:
    ...
.example:
    ...
    jmp .example ; this jumps to fn2.example

التعليمات

في هذا التمرين ستُنفّذ بعض قواعد بلاك جاك، مثل طريقة لعب اللعبة واحتساب نقاطها.

Note

في هذا التمرين، تُمثَّل البطاقات بأعداد: تُمثَّل كل بطاقة رقمية بقيمتها العددية، بينما يُرمز إلى الولد والملكة والملك والآس بالأعداد 11 و12 و13 و14 على الترتيب (ولا تُستخدم أوراق الجوكر في اللعبة).

ولتسهيل التعامل مع هذا التمثيل، عُرِّفت بعض الثوابت في أعلى الملف؛ فالثوابت C2 إلى C10 للبطاقات الرقمية، وCJ للولد، وCQ للملكة، وCK للملك، وCA للآس.

يُفترض استخدام مجموعة قياسية من 52 بطاقة بالبذلات الفرنسية، لكن في معظم النسخ تُخلط عدة مجموعات معًا عند اللعب.

Note

هذه هي التعليمات المذكورة في هذا المفهوم:

التعليمة الوصف
cmp a, b يضبط الأعلام وفقًا لناتج a - b
jmp a يتوقف تنفيذ الكود هنا ويستمر عند التسمية a
jcc a يستمر الكود عند التسمية a إذا تحقق الشرط في cc

وهذه هي الشروط التي تُفحص في jcc بعد cmp a, b:

التعليمة يقفز عندما
je a == b
jl a < b (مُوقَّع)
jg a > b (مُوقَّع)
jb a < b (غير مُوقَّع)
ja a > b (غير مُوقَّع)
jle a <= b (مُوقَّع)
jge a >= b (غير مُوقَّع)
jbe a <= b (غير مُوقَّع)
jae a >= b (غير مُوقَّع)
jne a != b
jnl !(a < b) (مُوقَّع)
jng !(a > b) (مُوقَّع)
jnb !(a < b) (غير مُوقَّع)
jna !(a > b) (غير مُوقَّع)
jnle !(a <= b) (مُوقَّع)
jnge !(a >= b) (مُوقَّع)
jnbe !(a <= b) (غير مُوقَّع)
jnae !(a >= b) (غير مُوقَّع)

1. احسب قيمة البطاقة

في بلاك جاك، قيمة CA إما 1 أو 11، حسب اليد (وسنتناول هذا لاحقًا). وتُحتسب البطاقات المصوّرة (CJ وCQ وCK) بـ10 نقاط، في حين تساوي أي بطاقة أخرى قيمتها العددية.

عرّف الدالة value_of_card ذات المعامل card، وهو عدد يمثل بطاقة. ويجب أن تُرجع الدالة القيمة العددية للبطاقة المُمرَّرة. ولأن CA يمكن أن تأخذ أكثر من قيمة (1 أو 11)، سنثبّت قيمة CA عند 1 في الوقت الحالي. لاحقًا، ستُنفّذ دالة لتحديد قيمة CA، بالنظر إلى يد قائمة.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. حدّد أي بطاقة لها قيمة أعلى

عرّف الدالة higher_card ذات المعاملين card_one وcard_two، وهما عددان يمثل كل منهما بطاقة. لأغراض احتساب النقاط، تساوي قيمة CJ وCQ وCK جميعًا 10. ويجب أن تُرجع الدالة البطاقة ذات القيمة الأعلى لأغراض الاحتساب. وإذا تساوت قيمتا البطاقتين، فأرجِع كلتيهما.

يمكن أن تأخذ CA أكثر من قيمة، لذا سنثبّت قيمتها عند 1 في هذه المهمة.

higher_card(13, 11)
// => {13, 11}

higher_card(4, 6)
// => 6

>>> higher_card(13, 14)
// => 13
Note

لإرجاع عددين صحيحين من دالة، ينبغي استخدام السجلّين rax وrdx معًا:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

إذا أُرجعت بطاقة واحدة فقط، فيجب ضبط rdx على 0.

3. احسب قيمة الآس

كما ذُكر سابقًا، تساوي CA إما 1 أو 11 نقطة، حسب اليد. وتقتضي قواعد بلاك جاك أن تُختار قيم بطاقات CA بما يعظّم نقاط اليد دون تجاوز 21 (وهو ما يُعرف بالانفجار).

عرّف الدالة value_of_ace ذات المعاملين card_one وcard_two، وهما عددان يمثلان بطاقتين موجودتين في اليد قبل استلام بطاقة CA الأخيرة. ويجب أن تُرجع الدالة القيمة التي ستُسنَد إلى بطاقة CA القادمة، سواء كانت 1 أو 11. وتذكّر: يجب أن تكون قيمة اليد الجديدة (مع CA) أعلى ما يمكن دون تجاوز 21.

تلميح: إذا كانت لدينا بالفعل بطاقة CA في اليد، فستكون قيمة بطاقة CA القادمة 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. حدّد ما إذا كانت اليد بلاك جاك

إذا وُزّعت على اللاعب بطاقة CA وبطاقة عشرية (C10 أو CJ أو CQ أو CK) كأول بطاقتين، فإن نقاط اللاعب تبلغ 21. وتُعرف هذه اليد باسم بلاك جاك.

عرّف الدالة is_blackjack ذات المعاملين card_one وcard_two، وهما عددان يمثلان بطاقتين. ويجب أن تُرجع الدالة 1 إذا كانت اليد المكوّنة من بطاقتين بلاك جاك، و0 خلاف ذلك. ولتسهيل التعامل مع القيم، عُرِّف الثابتان TRUE وFALSE، وهما على الترتيب مكافئان لـ1 و0، في أعلى الملف.

ملاحظة: يمكن إجراء حساب النقاط بطرق عديدة. لكن إن أمكن، نودّ أن تتحقق من وجود بطاقة CA وبطاقة عشرية داخل اليد، بدلًا من جمع قيم البطاقات.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. تقسيم الأزواج

إذا كانت أول بطاقتين للاعب متساويتين في القيمة (مثلًا، يد فيها بطاقتا C6، أو يد فيها CQ وCK)، فيمكن للاعب أن يختار التعامل معهما كيدين منفصلتين. ويُعرف هذا بـ«تقسيم الأزواج».

عرّف الدالة can_split_pairs ذات المعاملين card_one وcard_two، وهما عددان يمثلان بطاقتين. ويجب أن تُرجع الدالة 1 إذا كان يمكن تقسيم اليد المكوّنة من بطاقتين إلى يدين، و0 خلاف ذلك. ولتسهيل التعامل مع القيم، عُرِّف الثابتان TRUE وFALSE، وهما على الترتيب مكافئان لـ1 و0، في أعلى الملف.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. مضاعفة الرهان

عندما يبلغ مجموع البطاقتين الأصليتين الموزَّعتين 9 أو 10 أو 11 نقطة، يمكن للاعب أن يضع رهانًا إضافيًا يساوي رهانه الأصلي. ويُعرف هذا بـ«مضاعفة الرهان».

عرّف الدالة can_double_down ذات المعاملين card_one وcard_two، وهما عددان يمثلان بطاقتين. ويجب أن تُرجع الدالة 1 إذا كانت اليد المكوّنة من بطاقتين تتيح للاعب «مضاعفة الرهان»، و0 خلاف ذلك. ولتسهيل التعامل مع القيم، عُرِّف الثابتان TRUE وFALSE، وهما على الترتيب مكافئان لـ1 و0، في أعلى الملف.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
x86-64 Assembly Exercism

مستعد لبدء بلاك جاك؟

سجّل في Exercism لتتعلّم وتتقن x86-64 Assembly عبر 22 مفهومًا130 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.