Black Jack

Black Jack

Esercizio di apprendimento

Introduzione

I condizionali

RFLAGs

Esiste un registro speciale chiamato rflags. I suoi bit agiscono come flag per varie condizioni.

Alcuni di essi sono elencati di seguito:

nome simbolo bit
riporto CF 0
zero ZF 6
segno SF 7
overflow OF 11

Le istruzioni di confronto

I flag in rflags non vengono modificati direttamente. Vengono invece impostati da molte istruzioni diverse.

Per esempio, ZF viene impostato da molte operazioni aritmetiche o bit a bit quando il risultato è zero.

Una delle istruzioni più comuni usate per verificare le condizioni è cmp. Prende due operandi e aggiorna i flag, ma non modifica i suoi operandi.

L'istruzione CMP

L'istruzione cmp sottrae il secondo operando dal primo e imposta i flag in base al risultato.

Se A è il primo operando e B il secondo:

flag viene impostato quando
CF A < B (senza segno)
ZF A == B
SF A < B (con segno, senza overflow)
OF overflow nella sottrazione con segno

Le diramazioni

Per impostazione predefinita, il codice in x86-64 viene eseguito in sequenza dall'alto verso il basso.

Ma ci sono molte situazioni in cui è necessario modificare questo comportamento. Per esempio, per eseguire un insieme diverso di istruzioni in risposta a una condizione.

Nei linguaggi di livello superiore, questo si fa di solito con astrazioni come i condizionali if...else. In x86-64, però, non esistono.

Al loro posto, x86-64 fornisce istruzioni che di fatto trasferiscono l'esecuzione in un altro punto del codice. Questa operazione si chiama branching.

Note

Abbiamo già visto due istruzioni di questo tipo: call e ret.

Quando viene chiamata una funzione, l'esecuzione viene trasferita dal chiamante alla funzione chiamata. Ed al ritorno, l'esecuzione viene trasferita di nuovo al chiamante.

Se non si trova alcun ret, l'esecuzione prosegue da una funzione alla successiva. A volte questo può servire a ottimizzare il flusso del codice.

Il salto incondizionato

L'istruzione jmp trasferisce incondizionatamente l'esecuzione del programma in un altro punto del codice. Il suo unico operando è un'etichetta che contiene l'indirizzo del punto in cui l'esecuzione proseguirà.

Considera, per esempio, la seguente funzione:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

Quando viene chiamata fn, l'esecuzione parte da mov rax, 5. A quel punto, questo imposta il valore di rax a 5.

L'istruzione successiva è jmp end, che trasferisce l'esecuzione all'etichetta end.

Dopo end, l'istruzione successiva è ret, che trasferisce l'esecuzione di nuovo alla funzione chiamante.

Nota che, dato che add rax, 10 si trova dopo jmp end e prima di end, non viene mai eseguita. Quando fn restituisce, il valore di rax è 5.

Il salto condizionato

La famiglia di istruzioni jcc trasferisce l'esecuzione del programma in un altro punto solo se è soddisfatta una condizione specifica. Altrimenti, l'esecuzione prosegue in sequenza.

Ogni condizione corrisponde a uno o più flag in rflags. Alcune varianti di jcc verificano che un flag sia impostato, altre che sia azzerato.

Il cc in jcc non è letterale, ma si riferisce al suffisso specifico associato al flag verificato.

Ci sono molti suffissi e molti di essi verificano la stessa condizione di un altro. Alcuni si riferiscono direttamente a un flag, così che l'istruzione salta a un'etichetta se quel flag specifico è impostato:

suffisso salta se
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

Molti altri sono scelti per riferirsi al loro significato in un'istruzione cmp. Per esempio:

istruzione suffisso salta se
cmp A, B e A == B
cmp A, B l A < B (con segno)
cmp A, B b A < B (senza segno)
cmp A, B g A > B (con segno)
cmp A, B a A > B (senza segno)

È possibile aggiungere e dopo l, b, g o a per includere l'uguaglianza nella condizione:

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

Per tutti i suffissi esistono varianti che verificano il comportamento opposto. Hanno la stessa sintassi, ma con una n prima del suffisso.

Per esempio, jnz salta quando ZF non è impostato. Analogamente, jnae salta quando A non è >= B (A e B interpretati come interi senza segno).

Note

Alcuni suffissi sono alias delle stesse condizioni. Per esempio, jz (suffisso z, per ZF) e je (suffisso e, per uguale) saltano entrambi quando ZF è impostato. Questo perché, con cmp, ZF viene impostato quando la sottrazione dà zero, il che corrisponde all'uguaglianza dei due operandi.

Altri suffissi, invece, verificano una combinazione di flag e non possono essere sostituiti direttamente da un suffisso relativo a un singolo flag.

Preferisci il suffisso che descrive meglio la semantica del tuo confronto.

Le etichette locali

Le etichette sono visibili nell'intero file sorgente, non sono locali a una funzione. Quindi è impossibile riutilizzare il nome di un'etichetta.

Per imitare il comportamento di un'etichetta locale, NASM ha una notazione speciale per un'etichetta dichiarata con un punto (.) davanti. Questa notazione definisce un'etichetta che include implicitamente il nome dell'etichetta precedente non puntata:

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

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

È comunque possibile saltare a questa etichetta da qualsiasi punto del codice usando il nome completo dell'etichetta, per esempio jmp fn1.example.

Tuttavia, un salto che usa la parte dell'etichetta a partire dal punto verrà fatto all'etichetta interna alla funzione superiore. Per esempio, .example si comporta come se fosse locale alla funzione:

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

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

Istruzioni

In questo esercizio implementerai alcune regole del blackjack, come il modo in cui si gioca e si assegnano i punti.

Note

In questo esercizio, le carte sono rappresentate da numeri; ogni carta numerica è rappresentata dal suo valore numerico, mentre i fanti, le regine, i re e gli assi sono identificati rispettivamente da 11, 12, 13 e 14 (i jolly non si usano nel gioco).

Per rendere più facile lavorare con questa rappresentazione, all'inizio del file sono definite alcune costanti: da C2 a C10 per le carte numeriche, CJ per un fante, CQ per una regina, CK per un re e CA per un asso.

Si presume l'uso di un mazzo standard di 52 carte con semi francesi, ma nella maggior parte delle versioni si mescolano insieme più mazzi per giocare.

Note

Queste sono le istruzioni menzionate in questo concetto:

Istruzione Descrizione
cmp a, b imposta i flag in base al risultato di a - b
jmp a il codice smette di eseguire qui e continua dall'etichetta a
jcc a il codice continua dall'etichetta a se la condizione in cc è soddisfatta

Queste sono le condizioni controllate in un jcc dopo un cmp a, b:

Istruzione Salta quando
je a == b
jl a < b (signed)
jg a > b (signed)
jb a < b (unsigned)
ja a > b (unsigned)
jle a <= b (signed)
jge a >= b (unsigned)
jbe a <= b (unsigned)
jae a >= b (unsigned)
jne a != b
jnl !(a < b) (signed)
jng !(a > b) (signed)
jnb !(a < b) (unsigned)
jna !(a > b) (unsigned)
jnle !(a <= b) (signed)
jnge !(a >= b) (signed)
jnbe !(a <= b) (unsigned)
jnae !(a >= b) (unsigned)

1. Calcola il valore di una carta

Nel blackjack, il valore di un CA è 1 oppure 11, a seconda della mano (ne parleremo più avanti). Le figure (CJ, CQ e CK) valgono 10 punti e qualsiasi altra carta vale il suo valore numerico.

Definisci la funzione value_of_card con il parametro card, un numero che rappresenta una carta. La funzione deve restituire il valore numerico della carta passata. Dato che un CA può assumere più valori (1 o 11), per il momento fissiamo il valore di un CA a 1. Più avanti implementerai una funzione per determinare il valore di un CA data una mano esistente.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Determina quale carta ha il valore più alto

Definisci la funzione higher_card con i parametri card_one e card_two, due numeri che rappresentano ciascuno una carta. Ai fini del punteggio, i valori di un CJ, di una CQ e di un CK sono tutti 10. La funzione deve restituire quale carta ha il valore più alto ai fini del punteggio. Se entrambe le carte hanno lo stesso valore, restituiscile entrambe.

Un CA può assumere più valori, quindi per questo compito ne fisseremo il valore a 1.

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

higher_card(4, 6)
// => 6

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

Per restituire due numeri interi da una funzione, devi usare entrambi i registri rax e rdx:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Se viene restituita una sola carta, rdx deve essere impostato a 0.

3. Calcola il valore di un asso

Come accennato prima, un CA vale o 1 o 11 punti, a seconda della mano. Le regole del blackjack richiedono che i valori dei CA siano scelti per massimizzare il punteggio della mano senza però superare 21 (cioè senza «sballare»).

Definisci la funzione value_of_ace con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte già presenti nella mano prima di ricevere l'ultimo CA. La funzione deve restituire quale valore, 1 o 11, sarà assegnato al CA in arrivo. Ricorda: il valore della nuova mano (con il CA) deve essere il più alto possibile senza superare 21.

Suggerimento: se abbiamo già un CA in mano, il valore del CA in arrivo sarà 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Determina se la mano è un blackjack

Se a un giocatore vengono servite un CA e una carta da dieci (C10, CJ, CQ o CK) come prime due carte, il giocatore ha un punteggio di 21. Questa mano è nota come blackjack.

Definisci la funzione is_blackjack con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte. La funzione deve restituire 1 se la mano di due carte è un blackjack, 0 altrimenti. Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.

Nota: il calcolo del punteggio si può fare in molti modi. Ma se possibile, vorremmo che controllassi se nella mano c'è un CA e una carta da dieci, invece di sommare i valori delle carte.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. Dividere le coppie

Se le prime due carte di un giocatore hanno lo stesso valore (ad esempio una mano con due C6, oppure una mano con un CQ e un CK), il giocatore può scegliere di trattarle come due mani separate. Questa operazione è nota come «dividere le coppie».

Definisci la funzione can_split_pairs con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte. La funzione deve restituire 1 se la mano di due carte può essere divisa in due mani, 0 altrimenti. Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Raddoppiare

Quando le due carte iniziali servite totalizzano 9, 10 o 11 punti, un giocatore può piazzare una puntata aggiuntiva pari alla puntata originale. Questa operazione è nota come «raddoppiare».

Definisci la funzione can_double_down con i parametri card_one e card_two, due numeri che rappresentano una coppia di carte. La funzione deve restituire 1 se la mano di due carte permette al giocatore di «raddoppiare», 0 altrimenti. Per rendere più facile lavorare con i valori, all'inizio del file sono definite le costanti TRUE e FALSE, equivalenti rispettivamente a 1 e 0.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
x86-64 Assembly Exercism

Vuoi iniziare Black Jack?

Iscriviti a Exercism per imparare e padroneggiare x86-64 Assembly con 22 concetti130 esercizi e il mentoring di persone reali, tutto gratis.