Black Jack

Black Jack

Exercício de aprendizagem

Introdução

Condicionais

Flags de registrador (RFLAGs)

Existe um registrador especial chamado rflags. Seus bits funcionam como flags para diversas condições.

Algumas delas estão listadas abaixo:

nome símbolo bit
carry CF 0
zero ZF 6
sinal SF 7
overflow OF 11

Instruções de comparação

As flags em rflags não são modificadas diretamente. Em vez disso, elas são definidas por muitas instruções diferentes.

Por exemplo, ZF é definida por muitas operações aritméticas ou bit a bit quando o resultado é zero.

Uma das instruções mais comuns usadas para testar condições é cmp. Ela recebe dois operandos e atualiza as flags, mas não modifica os seus operandos.

A instrução cmp

A instrução cmp subtrai o segundo operando do primeiro e define as flags de acordo com o resultado.

Se A é o primeiro operando e B, o segundo:

flag definida quando
CF A < B (sem sinal)
ZF A == B
SF A < B (com sinal, sem overflow)
OF há overflow na subtração com sinal

Ramificação

Por padrão, o código em x86-64 é executado sequencialmente de cima para baixo.

Mas há muitas situações em que é necessário modificar esse comportamento. Por exemplo, para executar um conjunto diferente de instruções em resposta a uma condição.

Em linguagens de nível mais alto, isso costuma ser feito com abstrações como as condicionais if...else. No entanto, elas não existem em x86-64.

Em vez disso, x86-64 fornece instruções que transferem efetivamente a execução para outro ponto do código. Isso é chamado de branching.

Note

Já vimos duas dessas instruções: call e ret.

Quando uma função é chamada, a execução é transferida do chamador para a função chamada. E, no retorno, a execução é transferida de volta para o chamador.

Se nenhum ret for encontrado, a execução passa direto de uma função para a seguinte. Isso às vezes pode ser usado para otimizar o fluxo do código.

Salto incondicional

A instrução jmp transfere incondicionalmente a execução do programa para outro ponto do código. Seu único operando é um rótulo que contém o endereço do ponto onde a execução continuará.

Considere, por exemplo, a seguinte função:

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

Quando fn é chamada, a execução começa em mov rax, 5. Isso define o valor em rax como 5 nesse ponto.

A próxima instrução é jmp end, que transfere a execução para o rótulo end.

Depois de end, a próxima instrução é ret, que transfere a execução de volta para a função chamadora.

Repare que, como add rax, 10 está localizada depois de jmp end e antes de end, ela nunca é executada. O valor de rax quando fn retorna é 5.

Salto condicional

A família de instruções jcc transfere a execução do programa para outro ponto apenas se uma condição específica for satisfeita. Caso contrário, a execução continua sequencialmente.

Cada condição corresponde a uma ou mais flags em rflags. Algumas variantes de jcc testam se uma flag está definida, outras testam se ela está limpa.

O cc em jcc não é literal, mas se refere ao sufixo específico associado à flag testada.

Existem muitos sufixos, e muitos deles testam a mesma condição que outro. Alguns se referem diretamente a uma flag, de modo que a instrução salta para um rótulo se a flag específica estiver definida:

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

Muitos outros são escolhidos para se referir ao seu significado em uma instrução cmp. Por exemplo:

instrução sufixo salta se
cmp A, B e A == B
cmp A, B l A < B (com sinal)
cmp A, B b A < B (sem sinal)
cmp A, B g A > B (com sinal)
cmp A, B a A > B (sem sinal)

É possível adicionar e depois de l, b, g ou a para incluir a igualdade na condição:

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 os sufixos, existem variantes que verificam o comportamento oposto. Elas têm a mesma sintaxe, mas com um n antes do sufixo.

Por exemplo, jnz salta quando ZF não está definida. Da mesma forma, jnae salta quando A não é >= B (A e B interpretados como inteiros sem sinal).

Note

Alguns sufixos são aliases para as mesmas condições. Por exemplo, jz (sufixo z, para ZF) e je (sufixo e, para igual) saltam ambos quando ZF está definida. Isso acontece porque, com cmp, ZF é definida quando a subtração resulta em zero, o que corresponde a dois operandos iguais.

Outros sufixos, porém, testam uma combinação de flags e não podem ser substituídos diretamente por um único sufixo de flag.

Prefira o sufixo que descreve melhor a semântica da sua comparação.

Rótulos locais

Os rótulos são visíveis em todo o arquivo-fonte, eles não são locais a uma função. Então é impossível reutilizar o nome de um rótulo.

Para imitar o comportamento de um rótulo local, o NASM tem uma notação especial para um rótulo declarado com um ponto (.) antes dele. Essa notação define um rótulo que inclui implicitamente o nome do rótulo anterior sem ponto:

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

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

Ainda é possível saltar para esse rótulo de qualquer lugar do código usando o nome completo do rótulo, por exemplo, jmp fn1.example.

No entanto, um salto que usa a parte do rótulo a partir do ponto será feito para o rótulo dentro da função superior. Por exemplo, .example se comporta como se fosse local à função:

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

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

Instruções

Neste exercício, você vai implementar algumas regras do Blackjack, como a forma como o jogo é jogado e pontuado.

Note

Neste exercício, as cartas são representadas por números; cada carta numérica é representada pelo seu valor numérico, enquanto valetes, damas, reis e ases são identificados por 11, 12, 13 e 14, respectivamente (os coringas não são usados no jogo).

Para facilitar o trabalho com essa representação, algumas constantes são definidas no topo do arquivo; C2 a C10 são para cartas numéricas, CJ para um valete, CQ para uma dama, CK para um rei e CA para um ás.

Assume-se um baralho padrão de 52 cartas com naipes franceses, mas na maioria das versões, vários baralhos são embaralhados juntos para o jogo.

Note

Estas são as instruções mencionadas neste conceito:

Instrução Descrição
cmp a, b define as flags de acordo com a - b
jmp a o código para de executar aqui e continua no rótulo a
jcc a o código continua no rótulo a se a condição em cc for atendida

Estas são as condições verificadas em um jcc depois de um cmp a, b:

Instrução Salta quando
je a == b
jl a < b (com sinal)
jg a > b (com sinal)
jb a < b (sem sinal)
ja a > b (sem sinal)
jle a <= b (com sinal)
jge a >= b (sem sinal)
jbe a <= b (sem sinal)
jae a >= b (sem sinal)
jne a != b
jnl !(a < b) (com sinal)
jng !(a > b) (com sinal)
jnb !(a < b) (sem sinal)
jna !(a > b) (sem sinal)
jnle !(a <= b) (com sinal)
jnge !(a >= b) (com sinal)
jnbe !(a <= b) (sem sinal)
jnae !(a >= b) (sem sinal)

1. Calcule o valor de uma carta

No Blackjack, o valor de um CA é ou 1 ou 11, dependendo da mão (mais sobre isso depois). As cartas de figura (CJ, CQ e CK) valem 10 pontos, e qualquer outra carta vale o seu valor numérico.

Defina a função value_of_card com o parâmetro card, um número que representa uma carta. A função deve retornar o valor numérico da carta recebida. Como um CA pode assumir vários valores (1 ou 11), fixamos o valor de um CA em 1 por enquanto. Mais adiante, você vai implementar uma função para determinar o valor de um CA a partir de uma mão existente.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Determine qual carta tem o valor mais alto

Defina a função higher_card com os parâmetros card_one e card_two, dois números, cada um representando uma carta. Para fins de pontuação, os valores de um CJ, de uma CQ e de um CK são todos 10. A função deve retornar qual carta tem o valor mais alto para a pontuação. Se as duas cartas tiverem o mesmo valor, retorne as duas.

Um CA pode assumir vários valores, então vamos fixar o valor dele em 1 para esta tarefa.

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

higher_card(4, 6)
// => 6

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

Para retornar dois inteiros de uma função, você deve usar os dois registradores rax e rdx:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Se apenas uma carta for retornada, rdx deve ser definido como 0.

3. Calcule o valor de um ás

Como mencionado antes, um CA vale ou 1 ou 11 pontos, dependendo da mão. As regras do Blackjack exigem que os valores dos CAs sejam escolhidos para maximizar a pontuação da mão, mas sem ultrapassar 21 (estourar).

Defina a função value_of_ace com os parâmetros card_one e card_two, que são dois números que representam um par de cartas já na mão antes de receber o último CA. Sua função deve retornar qual valor, 1 ou 11, será atribuído ao próximo CA. Lembre-se: o valor da nova mão (com o CA) precisa ser o mais alto possível sem ultrapassar 21.

Dica: se já temos um CA na mão, o valor do próximo CA seria 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Determine se a mão é um blackjack

Se um jogador recebe um CA e uma carta de dez (C10, CJ, CQ ou CK) como as duas primeiras cartas, então o jogador tem uma pontuação de 21. Essa mão é conhecida como blackjack.

Defina a função is_blackjack com os parâmetros card_one e card_two, que são dois números que representam um par de cartas. A função deve retornar 1 se a mão de duas cartas for um blackjack, e 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.

Nota: O cálculo da pontuação pode ser feito de várias maneiras. Mas, se possível, gostaríamos que você verificasse se há um CA e uma carta de dez na mão, em vez de somar os valores das cartas.

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. Dividir pares

Se as duas primeiras cartas de um jogador tiverem o mesmo valor (por exemplo, a mão de dois C6s, ou a mão de um CQ e um CK), o jogador pode optar por tratá-las como duas mãos separadas. Isso é conhecido como "dividir pares".

Defina a função can_split_pairs com os parâmetros card_one e card_two, que são dois números que representam um par de cartas. A função deve retornar 1 se a mão de duas cartas puder ser dividida em duas mãos, e 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Dobrar a aposta

Quando as duas cartas originais distribuídas somam 9, 10 ou 11 pontos, o jogador pode fazer uma aposta adicional igual à aposta original. Isso é conhecido como "dobrar a aposta".

Defina a função can_double_down com os parâmetros card_one e card_two, que são dois números que representam um par de cartas. A função deve retornar 1 se a mão de duas cartas permitir que o jogador "dobre a aposta", e 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respectivamente, são definidas no topo do arquivo.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Editar via GitHub O link abre em uma nova janela ou aba
x86-64 Assembly Exercism

Tudo pronto para começar Black Jack?

Crie sua conta no Exercism para aprender e dominar x86-64 Assembly com 22 conceitos130 exercícios e mentoria humana de verdade, tudo de graça.