Black Jack

Black Jack

Ejercicio de aprendizaje

Introducción

Condicionales

RFLAGs

Hay un registro especial llamado rflags. Sus bits actúan como banderas para diversas 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 su lugar, las establecen muchas instrucciones diferentes.

Por ejemplo, ZF se activa con muchas operaciones aritméticas o bit a bit cuando el resultado es cero.

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

Instrucción CMP

La instrucción cmp resta el segundo operando al primero y establece 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 desbordamiento en una resta con signo

Bifurcación

Por defecto, 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 diferente de instrucciones en respuesta a una condición.

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

En su lugar, x86-64 proporciona instrucciones que transfieren efectivamente la ejecución a otra ubicación del código. Esto se llama branching.

Note

Ya hemos visto dos de esas instrucciones: call y ret.

Cuando se llama a una función, la ejecución se transfiere desde la función que hace la llamada hasta la función llamada. Y, al volver, la ejecución se transfiere de nuevo a la función que hizo la llamada.

Si no se encuentra ningún ret, la ejecución pasa directamente a la siguiente función. Esto a veces puede usarse para optimizar el flujo del código.

Salto incondicional

La instrucción jmp transfiere incondicionalmente 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 comienza 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 en que, como add rax, 10 está situada después de jmp end y antes de end, nunca se ejecuta. El valor de rax cuando fn termina 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 secuencialmente.

Cada condición se corresponde con una o más banderas de rflags. Algunas variantes de jcc comprueban que una bandera está activada; otras comprueban que está desactivada.

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

Hay muchos sufijos y muchos de ellos comprueban la misma condición que otro. Algunos de ellos se refieren directamente a una bandera, de modo que la instrucción salta a una etiqueta si la 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 hacer referencia 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 añadir 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, hay 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 igualdad) ambos saltan 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 se pueden sustituir 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 lo tanto, es imposible reutilizar el nombre de una etiqueta.

Para imitar el comportamiento de una etiqueta local, NASM tiene una notación especial para una etiqueta declarada con un punto (.) delante. Esta notación define una etiqueta que incluye implícitamente el nombre de la etiqueta anterior sin 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 lugar 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 que comienza en el punto se hará a la etiqueta 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, se definen algunas constantes al principio del archivo; de 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 supone una baraja francesa estándar de 52 cartas, pero en la mayoría de las versiones se barajan varias barajas juntas para jugar.

Note

Estas son las instrucciones mencionadas en este concepto:

Instrucción Descripción
cmp a, b establece las banderas 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. Calcular el valor de una carta

En el Blackjack, el valor de un CA es 1 u 11, según la mano (hablaremos de esto más adelante). Las figuras (CJ, CQ y CK) puntúan 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 que se le pasa. Como un CA puede tomar varios valores (1 o 11), de momento fijamos el valor de un CA en 1. Más adelante implementarás una función para determinar 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. Determinar 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. A la hora de puntuar, los valores de un CJ, un CQ y un CK son todos 10. La función debe devolver qué carta tiene el valor más alto para puntuar. Si ambas cartas tienen el mismo valor, devuelve las dos.

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

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

higher_card(4, 6)
// => 6

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

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

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Si solo se devuelve una carta, hay que establecer rdx en 0.

3. Calcular el valor de un as

Como se ha mencionado antes, un CA vale o bien 1 o bien 11 puntos, según la mano. Las reglas del Blackjack exigen que los valores de los CA se elijan para maximizar la puntuación de la mano sin pasarse de 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 asignará al próximo CA. 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 para el próximo CA sería 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Determinar 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, entonces el jugador tiene una puntuación 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, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.

Nota: el cálculo de la puntuación se puede hacer de muchas maneras. Pero, si es posible, nos gustaría que comprobaras 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 parejas

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

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, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Doblar

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

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 permite al jugador «doblar», y 0 en caso contrario. Para que sea más fácil trabajar con los valores, al principio del archivo se definen las constantes TRUE y FALSE, equivalentes a 1 y 0, respectivamente.

can_double_down(14, 9)
// => 1

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

¿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.