Blackjack

Blackjack

Tanulófeladat

Bevezetés

Feltételes elágazások

Az rflags regiszter

Van egy speciális regiszter, amelynek a neve rflags. A bitjei különféle feltételek jelzőbitjeiként viselkednek.

Ezek közül néhányat az alábbi táblázat sorol fel:

név szimbólum bit
átvitel CF 0
nulla ZF 6
előjel SF 7
túlcsordulás OF 11

Összehasonlító utasítások

A rflags jelzőbitjeit nem lehet közvetlenül módosítani. Ehelyett sok különböző utasítás állítja be őket.

Például a ZF-t sok aritmetikai vagy bitenkénti művelet beállítja, amikor az eredmény nulla.

Az egyik leggyakoribb, feltételek vizsgálatára használt utasítás a cmp. Két operandust vesz, és frissíti a jelzőbiteket, de nem módosítja az operandusait.

A cmp utasítás

A cmp utasítás az első operandusból vonja ki a másodikat, és az eredmény alapján állítja be a jelzőbiteket.

Ha A az első operandus, B pedig a második:

jelzőbit beáll, ha
CF A < B (előjel nélküli)
ZF A == B
SF A < B (előjeles, túlcsordulás nélkül)
OF túlcsordulás előjeles kivonásnál

Elágazás

Alapértelmezés szerint az x86-64 kódja felülről lefelé, sorban hajtódik végre.

De sok helyzetben szükség van ennek a viselkedésnek a megváltoztatására. Például hogy egy feltétel hatására más utasításokat hajtsunk végre.

A magasabb szintű nyelvekben ezt általában olyan absztrakciókkal oldják meg, mint az if...else elágazás. Az x86-64-ben azonban ilyenek nincsenek.

Helyette az x86-64 olyan utasításokat kínál, amelyek lényegében a kód egy másik pontjára adják át a végrehajtást. Ezt hívják branching-nek.

Note

Két ilyen utasítást már láttunk: a call-t és a ret-et.

Amikor egy függvényt meghívnak, a végrehajtás a hívótól a meghívott függvényhez kerül. Visszatéréskor pedig a végrehajtás visszakerül a hívóhoz.

Ha nincs ret, a végrehajtás átcsúszik az egyik függvényből a következőbe. Ezt néha a kódfolyam optimalizálására lehet használni.

Feltétel nélküli ugrás

A jmp utasítás feltétel nélkül a kód egy másik pontjára irányítja a program végrehajtását. Egyetlen operandusa egy címke, amely annak a pontnak a címét tartalmazza, ahol a végrehajtás folytatódik.

Vegyük például a következő függvényt:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

Amikor meghívják az fn-t, a végrehajtás a mov rax, 5-nél kezdődik. Ez abban a pontban 5-re állítja a rax értékét.

A következő utasítás a jmp end, amely átadja a végrehajtást az end címkének.

Az end után a következő utasítás a ret, amely visszaadja a végrehajtást a hívó függvénynek.

Vegyük észre, hogy mivel az add rax, 10 a jmp end után és az end előtt található, soha nem hajtódik végre. Az rax értéke 5 lesz, amikor az fn visszatér.

Feltételes ugrás

A jcc utasításcsalád csak akkor adja át a program végrehajtását egy másik pontnak, ha egy adott feltétel teljesül. Ellenkező esetben a végrehajtás sorban folytatódik.

Minden feltétel a rflags egy vagy több jelzőbitjének felel meg. A jcc egyes változatai azt vizsgálják, hogy egy jelzőbit be van állítva, mások azt, hogy nincs.

A jcc-ben a cc nem szó szerinti, hanem a vizsgált jelzőbithoz tartozó konkrét utótagra utal.

Sok utótag létezik, és közülük sok ugyanazt a feltételt vizsgálja, mint egy másik. Némelyik közvetlenül egy jelzőbitre utal, így az utasítás egy címkére ugrik, ha az adott jelzőbit be van állítva:

utótag ugrik, ha
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

Sok másikat azért választottak, hogy a cmp utasításban betöltött jelentésükre utaljanak. Például:

utasítás utótag ugrik, ha
cmp A, B e A == B
cmp A, B l A < B (előjeles)
cmp A, B b A < B (előjel nélküli)
cmp A, B g A > B (előjeles)
cmp A, B a A > B (előjel nélküli)

Az e-t az l, b, g vagy a után lehet tenni, hogy a feltétel az egyenlőséget is tartalmazza:

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

Minden utótaghoz léteznek olyan változatok, amelyek az ellenkező viselkedést vizsgálják. A szintaxisuk ugyanaz, csak az utótag előtt egy n áll.

Például a jnz akkor ugrik, ha a ZF nincs beállítva. Hasonlóképpen a jnae akkor ugrik, ha A nem nagyobb vagy egyenlő, mint B (A és B előjel nélküli egészként értelmezve).

Note

Néhány utótag ugyanazoknak a feltételeknek a másik neve. Például a jz (a z utótag, a ZF-re) és a je (az e utótag, az egyenlőségre) is akkor ugrik, amikor a ZF be van állítva. Ez azért van, mert a cmp-nél a ZF akkor áll be, amikor a kivonás eredménye nulla, ami azt jelenti, hogy a két operandus egyenlő.

Más utótagok azonban jelzőbitek kombinációját vizsgálják, és nem helyettesíthetők közvetlenül egyetlen jelzőbites utótaggal.

Azt az utótagot válaszd, amelyik jobban leírja az összehasonlításod szemantikáját.

Helyi címkék

A címkék az egész forrásfájlban láthatók, nem egy függvényre korlátozódnak. Ezért egy címkenevet nem lehet újra felhasználni.

Ahhoz, hogy utánozza egy helyi címke viselkedését, a NASM-ben külön jelölés létezik az olyan címkékre, amelyek neve előtt pont (.) áll. Ez a jelölés olyan címkét definiál, amely implicit módon magában foglalja az előző, pont nélküli címke nevét:

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

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

A kód bármely pontjáról lehet erre a címkére ugrani a teljes címkenév használatával, például jmp fn1.example.

Egy olyan ugrás azonban, amely a címkének a ponttól kezdődő részét használja, a felső függvényen belüli címkére történik. Például a .example úgy viselkedik, mintha a függvényen belül lenne helyi:

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

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

Utasítások

Ebben a feladatban a Blackjack néhány szabályát fogod megvalósítani, például azt, hogyan zajlik a játék, és hogyan számolják a pontokat.

Note

Ebben a feladatban a lapokat számok képviselik; a számkártyákat a számértékük jelöli, a bubi, a dáma, a király és az ász pedig rendre 11, 12, 13 és 14. (A jokereket nem használjuk a játékban.)

Hogy könnyebb legyen ezzel a reprezentációval dolgozni, néhány konstans a fájl elején van definiálva: a C2-től a C10-ig a számkártyáké, a CJ a bubié, a CQ a dámáé, a CK a királyé, a CA pedig az ászé.

Szabványos, francia színekből álló 52 lapos paklit feltételezünk, a legtöbb változatban azonban több paklit kevernek össze a játékhoz.

Note

Ezek a fogalomban említett utasítások:

Utasítás Leírás
cmp a, b a jelzőket a - b alapján állítja be
jmp a a kód itt leáll, és az a címkén folytatódik
jcc a a kód az a címkén folytatódik, ha a cc feltétel teljesül

Ezeket a feltételeket ellenőrzi a jcc egy cmp a, b után:

Utasítás Mikor ugrik
je a == b
jl a < b (előjeles)
jg a > b (előjeles)
jb a < b (előjel nélküli)
ja a > b (előjel nélküli)
jle a <= b (előjeles)
jge a >= b (előjel nélküli)
jbe a <= b (előjel nélküli)
jae a >= b (előjel nélküli)
jne a != b
jnl !(a < b) (előjeles)
jng !(a > b) (előjeles)
jnb !(a < b) (előjel nélküli)
jna !(a > b) (előjel nélküli)
jnle !(a <= b) (előjeles)
jnge !(a >= b) (előjeles)
jnbe !(a <= b) (előjel nélküli)
jnae !(a >= b) (előjel nélküli)

1. Számítsd ki egy lap értékét

A Blackjackben a CA értéke 1 vagy 11, a kéztől függően (erről később még lesz szó). A figurás lapok (CJ, CQ és CK) 10 pontot érnek, minden más lap a számértékét éri.

Definiáld a value_of_card függvényt a card paraméterrel, amely egy lapot képviselő szám. A függvénynek a kapott lap számértékét kell visszaadnia. Mivel a CA több értéket is felvehet (1 vagy 11), egyelőre rögzítsük a CA értékét 1-ben. Később majd írsz egy függvényt, amely egy meglévő kéz alapján meghatározza a CA értékét.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Határozd meg, melyik lapnak nagyobb az értéke

Definiáld a higher_card függvényt a card_one és card_two paraméterekkel, amelyek két lapot képviselő számok. A pontozás szempontjából a CJ, a CQ és a CK értéke egyaránt 10. A függvénynek azt kell visszaadnia, hogy melyik lapnak nagyobb a pontozási értéke. Ha a két lap értéke egyenlő, mindkettőt add vissza.

A CA több értéket is felvehet, ezért ebben a részfeladatban 1-ben rögzítjük az értékét.

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

higher_card(4, 6)
// => 6

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

Ahhoz, hogy egy függvényből két egész számot adj vissza, a rax és az rdx regisztert is használnod kell:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Ha csak egy lapot adsz vissza, a rdx-et 0-ra kell állítani.

3. Számítsd ki egy ász értékét

Ahogy korábban említettük, a CA vagy 1, vagy 11 pontot ér, a kéztől függően. A Blackjack szabályai szerint a CA-k értékét úgy kell megválasztani, hogy maximalizálja a kéz pontszámát, de ne lépje túl a 21-et (azaz ne „égjen be").

Definiáld a value_of_ace függvényt a card_one és card_two paraméterekkel; ez a két szám a kézben már meglévő két lapot képviseli, mielőtt megkapod az utolsó CA-t. A függvényednek azt kell visszaadnia, hogy melyik értéket, 1-et vagy 11-et kapja majd a következő CA. Ne feledd: az új kéz értékének (a CA-val együtt) a lehető legnagyobbnak kell lennie, anélkül hogy túllépné a 21-et.

Tipp: ha már van a kezünkben CA, akkor a következő CA értéke 1 lesz.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Állapítsd meg, hogy blackjack-e a kéz

Ha a játékos első két lapként egy CA-t és egy tízes lapot (C10, CJ, CQ vagy CK) kap, akkor 21 pontja lesz. Ezt a kezet nevezik blackjacknek.

Definiáld az is_blackjack függvényt a card_one és card_two paraméterekkel, amelyek két lapot képviselő számok. A függvénynek 1-et kell visszaadnia, ha a kétlapos kéz blackjack, egyébként 0-t. Hogy könnyebb legyen az értékekkel dolgozni, a fájl elején definiálva vannak a TRUE és FALSE konstansok, amelyek rendre 1-et és 0-t jelentenek.

Megjegyzés: A pontszám kiszámítását többféleképpen is meg lehet oldani. De ha lehet, szeretnénk, ha azt ellenőriznéd, hogy van-e a kézben egy CA és egy tízes lap, ahelyett hogy összeadnád a lapok értékét.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. Párok szétválasztása

Ha a játékos első két lapja azonos értékű (például két C6, vagy egy CQ és egy CK), a játékos dönthet úgy, hogy két külön kézként kezeli őket. Ezt nevezik „párok szétválasztásának".

Definiáld a can_split_pairs függvényt a card_one és card_two paraméterekkel, amelyek két lapot képviselő számok. A függvénynek 1-et kell visszaadnia, ha a kétlapos kéz két kézre választható szét, egyébként 0-t. Hogy könnyebb legyen az értékekkel dolgozni, a fájl elején definiálva vannak a TRUE és FALSE konstansok, amelyek rendre 1-et és 0-t jelentenek.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Duplázás

Ha az eredeti két lap összesen 9, 10 vagy 11 pontot ér, a játékos az eredeti tétével megegyező további tétet tehet. Ezt nevezik „duplázásnak".

Definiáld a can_double_down függvényt a card_one és card_two paraméterekkel, amelyek két lapot képviselő számok. A függvénynek 1-et kell visszaadnia, ha a kétlapos kéz lehetővé teszi a játékos számára a „duplázást", egyébként 0-t. Hogy könnyebb legyen az értékekkel dolgozni, a fájl elején definiálva vannak a TRUE és FALSE konstansok, amelyek rendre 1-et és 0-t jelentenek.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Szerkesztés GitHubon A hivatkozás új ablakban vagy lapon nyílik meg
x86-64 Assembly Exercism

Készen állsz elkezdeni a(z) Blackjack feladatot?

Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) x86-64 Assembly nyelvet 22 fogalom130 feladat segítségével, valódi emberi mentorálással, mindez ingyen.