Black Jack

Black Jack

Ejercicio de aprendizaje

Introducción

Condicionales

RFLAGs

Hay un registro especial llamado rflags. Sus bits funcionan como banderas para distintas condiciones.

Algunas de ellas se enumeran a continuación:

nombre símbolo bit
acarreo CF 0
cero ZF 6
signo SF 7
desbordamiento OF 11

Instrucciones de comparación

Las banderas de rflags no se modifican directamente. En cambio, las activan muchas instrucciones distintas.

Por ejemplo, ZF la activan muchas operaciones aritméticas o de bits cuando el resultado es cero.

Una de las instrucciones más comunes que se usan para comprobar condiciones es cmp. Toma dos operandos y actualiza las banderas, pero no modifica sus operandos.

La instrucción CMP

La instrucción cmp resta el segundo operando del primero y activa las banderas según el resultado.

Si A es el primer operando y B, el segundo:

bandera se activa cuando
CF A < B (sin signo)
ZF A == B
SF A < B (con signo, sin desbordamiento)
OF hay desbordamiento en la resta con signo

Bifurcación

De forma predeterminada, el código en x86-64 se ejecuta secuencialmente de arriba a abajo.

Pero hay muchas situaciones en las que es necesario modificar este comportamiento. Por ejemplo, para ejecutar un conjunto distinto de instrucciones en respuesta a una condición.

En los lenguajes de alto nivel, esto suele hacerse con abstracciones como los condicionales if...else. Sin embargo, esos no existen en x86-64.

En su lugar, x86-64 ofrece instrucciones que, en la práctica, transfieren la ejecución a otro punto del código. Esto se conoce como branching.

Note

Ya vimos dos de esas instrucciones: call y ret.

Cuando se llama a una función, la ejecución se transfiere del llamador a la función llamada. Y, al regresar, la ejecución se transfiere de vuelta al llamador.

Si no se encuentra un ret, la ejecución continúa hacia abajo, de una función a la siguiente. A veces esto puede usarse para optimizar el flujo del código.

Salto incondicional

La instrucción jmp transfiere de forma incondicional la ejecución del programa a otro punto del código. Su único operando es una etiqueta que contiene la dirección del punto donde continuará la ejecución.

Considera, por ejemplo, la siguiente función:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

Cuando se llama a fn, la ejecución empieza en mov rax, 5. Esto establece el valor de rax en 5 en ese punto.

La siguiente instrucción es jmp end, que transfiere la ejecución a la etiqueta end.

Después de end, la siguiente instrucción es ret, que transfiere la ejecución de vuelta a la función que hizo la llamada.

Fíjate que, como add rax, 10 está ubicada después de jmp end y antes de end, nunca se ejecuta. El valor de rax cuando fn regresa es 5.

Salto condicional

La familia de instrucciones jcc transfiere la ejecución del programa a otro punto solo si se cumple una condición específica. De lo contrario, la ejecución continúa de forma secuencial.

Cada condición corresponde a una o más banderas de rflags. Algunas variantes de jcc comprueban que una bandera esté activada; otras, que esté limpia.

El cc de jcc no es literal, sino que hace referencia al sufijo específico asociado con la bandera que se comprueba.

Hay muchos sufijos, y muchos de ellos comprueban la misma condición que otro. Algunos se refieren directamente a una bandera, de modo que la instrucción salta a una etiqueta si esa bandera específica está activada:

sufijo salta si
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

Muchos otros se eligen para referirse a su significado en una instrucción cmp. Por ejemplo:

instrucción sufijo salta si
cmp A, B e A == B
cmp A, B l A < B (con signo)
cmp A, B b A < B (sin signo)
cmp A, B g A > B (con signo)
cmp A, B a A > B (sin signo)

Es posible agregar e después de l, b, g o a para incluir la igualdad en la condición:

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

Para todos los sufijos existen variantes que comprueban el comportamiento opuesto. Tienen la misma sintaxis, pero con una n antes del sufijo.

Por ejemplo, jnz salta cuando ZF no está activada. De manera similar, jnae salta cuando A no es >= B (A y B interpretados como enteros sin signo).

Note

Algunos sufijos son alias de las mismas condiciones. Por ejemplo, jz (sufijo z, para ZF) y je (sufijo e, para igual) saltan ambos cuando ZF está activada. Esto se debe a que, con cmp, ZF se activa cuando la resta da cero, lo que corresponde a que los dos operandos sean iguales.

Otros sufijos, sin embargo, comprueban una combinación de banderas y no pueden sustituirse directamente por un único sufijo de bandera.

Prefiere el sufijo que describa mejor la semántica de tu comparación.

Etiquetas locales

Las etiquetas son visibles en todo el archivo fuente; no son locales a una función. Por eso es imposible reutilizar el nombre de una etiqueta.

Para imitar el comportamiento de una etiqueta local, NASM tiene una notación especial para las etiquetas declaradas con un punto (.) delante. Esta notación define una etiqueta que incluye de forma implícita el nombre de la etiqueta anterior que no lleva punto:

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

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

Todavía es posible saltar a esta etiqueta desde cualquier parte del código usando el nombre completo de la etiqueta, por ejemplo, jmp fn1.example.

Sin embargo, un salto que use la parte de la etiqueta a partir del punto se hará a la etiqueta que está dentro de la función superior. Por ejemplo, .example se comporta como si fuera local a la función:

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

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

Instrucciones

En este ejercicio vas a implementar algunas reglas del Blackjack, como la forma en que se juega y se puntúa.

Note

En este ejercicio, las cartas se representan con números; cada carta numérica se representa con su valor numérico, mientras que las jotas, las reinas, los reyes y los ases se identifican con 11, 12, 13 y 14, respectivamente (en el juego no se usan comodines).

Para que sea más fácil trabajar con esta representación, al inicio del archivo se definen algunas constantes: C2 a C10 son para las cartas numéricas, CJ para una jota, CQ para una reina, CK para un rey y CA para un as.

Se asume una baraja estándar de 52 cartas de palo francés, pero en la mayoría de las versiones se mezclan varias barajas para jugar.

Note

Estas son las instrucciones mencionadas en este concepto:

Instrucción Descripción
cmp a, b establece los flags según a - b
jmp a el código deja de ejecutarse aquí y continúa en la etiqueta a
jcc a el código continúa en la etiqueta a si se cumple la condición en cc

Estas son las condiciones que se comprueban en un jcc después de un cmp a, b:

Instrucción Salta cuando
je a == b
jl a < b (con signo)
jg a > b (con signo)
jb a < b (sin signo)
ja a > b (sin signo)
jle a <= b (con signo)
jge a >= b (sin signo)
jbe a <= b (sin signo)
jae a >= b (sin signo)
jne a != b
jnl !(a < b) (con signo)
jng !(a > b) (con signo)
jnb !(a < b) (sin signo)
jna !(a > b) (sin signo)
jnle !(a <= b) (con signo)
jnge !(a >= b) (con signo)
jnbe !(a <= b) (sin signo)
jnae !(a >= b) (sin signo)

1. Calcula el valor de una carta

En el Blackjack, el valor de un CA es 1 u 11, según la mano (hablaremos más de esto después). Las figuras (CJ, CQ y CK) valen 10 puntos y cualquier otra carta vale su valor numérico.

Define la función value_of_card con el parámetro card, un número que representa una carta. La función debe devolver el valor numérico de la carta recibida. Como un CA puede tomar varios valores (1 o 11), por ahora fijamos el valor de un CA en 1. Más adelante implementarás una función que determine el valor de un CA dada una mano existente.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Determina qué carta tiene un valor más alto

Define la función higher_card con los parámetros card_one y card_two, dos números que representan cada uno una carta. Para el puntaje, los valores de un CJ, un CQ y un CK son todos 10. La función debe devolver cuál carta tiene el valor más alto para el puntaje. Si ambas cartas tienen el mismo valor, devuelve las dos.

Un CA puede tomar varios valores, así que para esta tarea fijaremos su valor en 1.

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

higher_card(4, 6)
// => 6

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

Para devolver dos enteros desde una función, debes usar los dos registros rax y rdx:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Si solo se devuelve una carta, rdx debe establecerse en 0.

3. Calcula el valor de un as

Como se mencionó antes, un CA vale ya sea 1 o 11 puntos, según la mano. Las reglas del Blackjack exigen que los valores de los CA se elijan para maximizar el puntaje de la mano, pero sin superar los 21 (lo que se conoce como «pasarse»).

Define la función value_of_ace con los parámetros card_one y card_two, que son dos números que representan un par de cartas que ya están en la mano antes de recibir el último CA. Tu función debe devolver qué valor, 1 u 11, se le asignará al CA que está por llegar. Recuerda: el valor de la nueva mano (con el CA) debe ser lo más alto posible sin pasarse de 21.

Pista: si ya tenemos un CA en la mano, el valor del CA que está por llegar sería 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Determina si la mano es un blackjack

Si a un jugador le reparten un CA y una carta de diez (C10, CJ, CQ o CK) como sus dos primeras cartas, el jugador tiene un puntaje de 21. Esta mano se conoce como blackjack.

Define la función is_blackjack con los parámetros card_one y card_two, que son dos números que representan un par de cartas. La función debe devolver 1 si la mano de dos cartas es un blackjack, y 0 en caso contrario. Para que sea más fácil trabajar con los valores, las constantes TRUE y FALSE, equivalentes a 1 y 0 respectivamente, están definidas al inicio del archivo.

Nota: el cálculo del puntaje se puede hacer de muchas maneras. Pero, si es posible, nos gustaría que compruebes si hay un CA y una carta de diez en la mano, en lugar de sumar los valores de las cartas.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. Dividir pares

Si las dos primeras cartas de un jugador tienen el mismo valor (por ejemplo, una mano con dos C6, o una mano con un CQ y un CK), el jugador puede optar por tratarlas como dos manos separadas. Esto se conoce como «dividir pares».

Define la función can_split_pairs con los parámetros card_one y card_two, que son dos números que representan un par de cartas. La función debe devolver 1 si la mano de dos cartas se puede dividir en dos manos, y 0 en caso contrario. Para que sea más fácil trabajar con los valores, las constantes TRUE y FALSE, equivalentes a 1 y 0 respectivamente, están definidas al inicio del archivo.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Doblar la apuesta

Cuando las dos cartas repartidas al inicio suman 9, 10 u 11 puntos, el jugador puede hacer una apuesta adicional igual a su apuesta original. Esto se conoce como «doblar la apuesta».

Define la función can_double_down con los parámetros card_one y card_two, que son dos números que representan un par de cartas. La función debe devolver 1 si la mano de dos cartas le permite al jugador «doblar la apuesta», y 0 en caso contrario. Para que sea más fácil trabajar con los valores, las constantes TRUE y FALSE, equivalentes a 1 y 0 respectivamente, están definidas al inicio del archivo.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Black Jack?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.