بلک‌جک

بلک‌جک

تمرین یادگیری

مقدمه

شرط‌ها

RFLAGs

یک ثبات ویژه به نام rflags وجود دارد. بیت‌های آن مانند «پرچم» برای شرط‌های مختلف عمل می‌کنند.

برخی از آن‌ها در ادامه فهرست شده‌اند:

اسم نماد بیت
رقم نقلی CF ۰
صفر ZF ۶
علامت SF ۷
سرریز OF ۱۱

دستورهای مقایسه

پرچم‌های 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 را در آن نقطه ۵ قرار می‌دهد.

دستور بعدی jmp end است که اجرا را به برچسب end منتقل می‌کند.

بعد از end، دستور بعدی ret است که اجرا را به تابع فراخوان برمی‌گرداند.

توجه کنید که چون add rax, 10 بعد از jmp end و قبل از end قرار دارد، هرگز اجرا نمی‌شود. مقدار rax هنگام بازگشت fn برابر ۵ است.

پرش شرطی

خانواده‌ی دستورهای jcc اجرای برنامه را فقط در صورت برآورده شدن شرطی مشخص، به نقطه‌ی دیگری منتقل می‌کنند. در غیر این صورت، اجرا به‌طور ترتیبی ادامه می‌یابد.

هر شرط به یک یا چند پرچم در rflags نگاشت می‌شود. برخی از گونه‌های jcc بررسی می‌کنند که پرچمی تنظیم شده است و برخی دیگر بررسی می‌کنند که پاک شده است.

cc در jcc تحت‌اللفظی نیست، بلکه به پسوند مشخص مرتبط با پرچم آزموده‌شده اشاره دارد.

پسوندهای زیادی وجود دارد و بسیاری از آن‌ها همان شرطی را بررسی می‌کنند که پسوند دیگری بررسی می‌کند. برخی از آن‌ها مستقیماً به یک پرچم اشاره دارند، به‌طوری که دستور اگر آن پرچم مشخص تنظیم شده باشد، به یک برچسب می‌پرد:

پسوند می‌پرد اگر
z ZF == ۱
c CF == ۱
s SF == ۱
o OF == ۱

بسیاری دیگر طوری انتخاب شده‌اند که به معنایشان در دستور 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 تنظیم شده باشد می‌پرند. این به این دلیل است که با cmp، ZF وقتی تنظیم می‌شود که تفریق صفر شود، که متناظر با برابر بودن دو عملوند است.

با این حال، پسوندهای دیگر ترکیبی از پرچم‌ها را بررسی می‌کنند و نمی‌توان آن‌ها را مستقیماً با یک پسوند تک‌پرچمی جایگزین کرد.

پسوندی را ترجیح دهید که معناشناسی مقایسه‌تان را بهتر توصیف می‌کند.

برچسب‌های محلی

برچسب‌ها در کل فایل مبدأ قابل مشاهده‌اند و محلیِ یک تابع نیستند. بنابراین استفاده‌ی دوباره از یک اسم برچسب ممکن نیست.

برای تقلید رفتار یک برچسب محلی، 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

در این تمرین، کارت‌ها با عدد نمایش داده می‌شوند؛ هر کارت عددی با ارزش عددی خودش نمایش داده می‌شود و جک‌ها، ملکه‌ها، شاه‌ها و آس‌ها به‌ترتیب با ۱۱، ۱۲، ۱۳ و ۱۴ مشخص می‌شوند (در این بازی از جوکر استفاده نمی‌شود).

برای اینکه کار با این نمایش راحت‌تر باشد، چند ثابت در ابتدای فایل تعریف شده‌اند؛ C2 تا C10 برای کارت‌های عددی‌اند، CJ برای جک، CQ برای ملکه، CK برای شاه و CA برای آس.

فرض بر این است که یک دست استاندارد ۵۲ تایی فرانسوی به کار می‌رود، اما در بیشتر نسخه‌ها چند دست با هم بُر زده می‌شود و برای بازی مخلوط می‌شوند.

Note

این‌ها دستورالعمل‌هایی هستند که در این مفهوم به آن‌ها اشاره شده است:

دستورالعمل توضیح
cmp a, b پرچم‌ها را بر اساس a - b تنظیم می‌کند
jmp a اجرای کد اینجا متوقف می‌شود و در برچسب a ادامه می‌یابد
jcc a اگر شرط موجود در cc برقرار باشد، کد در برچسب a ادامه می‌یابد

این‌ها شرط‌هایی هستند که در یک 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 بسته به دست، ۱ یا ۱۱ است (بعداً بیشتر درباره‌ی این صحبت می‌کنیم). کارت‌های تصویری (CJ، CQ و CK) ۱۰ امتیاز حساب می‌شوند و هر کارت دیگری ارزش عددی خودش را دارد.

تابع value_of_card را با پارامتر card تعریف کنید، عددی که نماینده‌ی یک کارت است. این تابع باید ارزش عددی کارت ورودی را برگرداند. از آنجا که یک CA می‌تواند چند مقدار مختلف بگیرد (۱ یا ۱۱)، فعلاً ارزش CA را روی ۱ ثابت می‌کنیم. بعداً تابعی پیاده‌سازی خواهید کرد که با داشتن یک دست موجود، ارزش یک 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 همگی ۱۰ است. این تابع باید کارتی را برگرداند که برای امتیازدهی ارزش بیشتری دارد. اگر ارزش هر دو کارت برابر باشد، هر دو را برگردانید.

یک CA می‌تواند چند مقدار داشته باشد، پس در این تمرین ارزش آن را ۱ در نظر می‌گیریم.

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 را روی ۰ تنظیم کنید.

3. محاسبه‌ی ارزش یک آس

همان‌طور که پیش‌تر اشاره شد، یک CA بسته به دست یا ۱ امتیاز دارد یا ۱۱ امتیاز. قواعد بلک‌جک ایجاب می‌کند که ارزش CAها طوری انتخاب شود که امتیاز دست را به بیشترین مقدار برساند، اما از ۲۱ فراتر نرود (عبور از ۲۱ در این بازی «سوختن» نامیده می‌شود).

تابع value_of_ace را با پارامترهای card_one و card_two تعریف کنید؛ این‌ها دو عدد هستند و نماینده‌ی جفتی از کارت‌ها که پیش از گرفتن CA آخر در دست قرار دارند. تابع شما باید برگرداند که به CA پیش رو کدام مقدار، ۱ یا ۱۱، اختصاص می‌یابد. یادتان باشد: ارزش دست جدید (همراه با CA) باید تا حد ممکن بالا باشد، بی‌آنکه از ۲۱ فراتر رود.

راهنما: اگر از قبل یک CA در دست داشته باشیم، آنگاه ارزش CA پیش رو ۱ خواهد بود.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. تعیین اینکه دست یک بلک‌جک است یا نه

اگر در اولین دو کارتی که به بازیکن داده می‌شود، یک CA و یک کارت ده (C10، CJ، CQ یا CK) باشد، آنگاه بازیکن امتیاز ۲۱ دارد. به این دست بلک‌جک می‌گویند.

تابع is_blackjack را با پارامترهای card_one و card_two تعریف کنید؛ این‌ها دو عدد هستند و نماینده‌ی جفتی از کارت‌ها. اگر دست دو کارتی یک بلک‌جک باشد، تابع باید ۱ برگرداند و در غیر این صورت ۰. برای اینکه کار با مقدارها راحت‌تر شود، ثابت‌های TRUE و FALSE، به‌ترتیب معادل ۱ و ۰، در ابتدای فایل تعریف شده‌اند.

نکته: محاسبه‌ی امتیاز به روش‌های زیادی می‌تواند انجام شود. اما اگر ممکن است، می‌خواهیم بررسی کنید که آیا یک CA و یک کارت ده در دست وجود دارند، نه اینکه ارزش کارت‌ها را جمع کنید.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. تقسیم جفت‌ها

اگر دو کارت اول بازیکن ارزش یکسانی داشته باشند (مثلاً دستی با دو C6، یا دستی با یک CQ و یک CK)، بازیکن می‌تواند آن‌ها را به‌عنوان دو دست جداگانه در نظر بگیرد. به این کار «تقسیم جفت‌ها» می‌گویند.

تابع can_split_pairs را با پارامترهای card_one و card_two تعریف کنید؛ این‌ها دو عدد هستند و نماینده‌ی جفتی از کارت‌ها. اگر دست دو کارتی را بتوان به دو دست تقسیم کرد، تابع باید ۱ برگرداند و در غیر این صورت ۰. برای اینکه کار با مقدارها راحت‌تر شود، ثابت‌های TRUE و FALSE، به‌ترتیب معادل ۱ و ۰، در ابتدای فایل تعریف شده‌اند.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. دو برابر کردن شرط

وقتی مجموع دو کارت اولی که داده می‌شود ۹، ۱۰ یا ۱۱ امتیاز باشد، بازیکن می‌تواند شرطی اضافی برابر با شرط اولیه‌اش بگذارد. به این کار «دو برابر کردن شرط» می‌گویند.

تابع can_double_down را با پارامترهای card_one و card_two تعریف کنید؛ این‌ها دو عدد هستند و نماینده‌ی جفتی از کارت‌ها. اگر دست دو کارتی به بازیکن اجازه‌ی «دو برابر کردن شرط» را بدهد، تابع باید ۱ برگرداند و در غیر این صورت ۰. برای اینکه کار با مقدارها راحت‌تر شود، ثابت‌های TRUE و FALSE، به‌ترتیب معادل ۱ و ۰، در ابتدای فایل تعریف شده‌اند.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
x86-64 Assembly Exercism

آماده‌اید بلک‌جک را شروع کنید؟

در Exercism ثبت‌نام کنید تا x86-64 Assembly را همراه با 22 مفهوم130 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.