Blackjack

Blackjack

Exercício de aprendizagem

Introdução

Condicionais

RFLAGs

Existe um registo especial chamado rflags. Os seus bits funcionam como flags para várias condições.

Algumas delas estão listadas abaixo:

nome símbolo bit
transporte CF 0
zero ZF 6
sinal SF 7
transbordo OF 11

Instruções de comparação

As flags em rflags não são modificadas diretamente. Em vez disso, 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. 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 ao 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 transbordo)
OF transbordo na subtração com sinal

Ramificação

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

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

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

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

Note

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

Quando uma função é chamada, a execução é transferida de quem a chama para a função chamada. E, ao regressar, a execução é transferida de volta para quem a chamou.

Se não for encontrado nenhum ret, a execução passa diretamente de uma função para a seguinte. Isto pode, por vezes, 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. O seu único operando é um rótulo que tem o endereço do ponto onde a execução vai continuar.

Considera, 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. Isto define o valor em rax como 5 nesse ponto.

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

Depois de end, a instrução seguinte é ret, que transfere a execução de volta para a função que a chamou.

Repara que, como add rax, 10 está depois de jmp end e antes de end, nunca é executada. O valor de rax quando fn regressa é 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 se verificar. 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 está limpa.

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

Há muitos sufixos e muitos deles testam a mesma condição que outro. Alguns referem-se 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 referirem ao seu significado numa 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 acrescentar 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. Têm a mesma sintaxe, mas com um n antes do sufixo.

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

Note

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

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

Prefere o sufixo que descreve melhor a semântica da tua comparação.

Rótulos locais

Os rótulos são visíveis em todo o ficheiro fonte; não são locais a uma função. Por isso, é 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 do nome. Esta 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 este rótulo a partir de qualquer ponto do código, usando o nome completo do rótulo, por exemplo, jmp fn1.example.

No entanto, um salto que use a parte do rótulo a partir do ponto será feito para o rótulo dentro da função superior. Por exemplo, .example comporta-se 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 vais implementar algumas regras do Blackjack, como a forma como o jogo se joga e se pontua.

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, respetivamente (os jokers não são usados no jogo).

Para facilitar o trabalho com esta representação, algumas constantes são definidas no topo do ficheiro; 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 baralhados em conjunto para jogar.

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 deixa de ser executado aqui e continua na etiqueta a
jcc a o código continua na etiqueta a se a condição em cc se verificar

Estas são as condições verificadas num 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. Calcula o valor de uma carta

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

Define a função value_of_card com o parâmetro card, um número que representa uma carta. A função deve devolver 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 para já. Mais tarde, vais implementar uma função para determinar o valor de um CA, dada uma mão existente.

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. Determina qual carta tem o valor mais alto

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

Um CA pode assumir vários valores, por isso vamos fixar o seu valor em 1 para esta tarefa.

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

higher_card(4, 6)
// => 6

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

Para devolver dois inteiros de uma função, deves usar os dois registos rax e rdx:

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

Se apenas uma carta for devolvida, rdx tem de ser definido como 0.

3. Calcula o valor de um ás

Como mencionado anteriormente, 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 (rebentar).

Define 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. A tua função deve devolver que valor, 1 ou 11, será atribuído ao próximo CA. Lembra-te: o valor da nova mão (com o CA) tem de ser o mais alto possível sem ultrapassar 21.

Dica: se já tivermos um CA na mão, então o valor para o próximo CA seria 1.

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. Determina se a mão é um blackjack

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

Define 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 devolver 1 se a mão de duas cartas for um blackjack, 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.

Nota: O cálculo da pontuação pode ser feito de várias formas. Mas, se possível, gostaríamos que verificasses se há um CA e uma carta de valor 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 uma CQ e um CK), o jogador pode optar por tratá-las como duas mãos separadas. Isto é conhecido como «dividir pares».

Define 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 devolver 1 se a mão de duas cartas puder ser dividida em duas mãos, 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. Dobrar a aposta

Quando as duas cartas iniciais distribuídas totalizam 9, 10 ou 11 pontos, um jogador pode fazer uma aposta adicional igual à sua aposta original. Isto é conhecido como «dobrar a aposta».

Define 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 devolver 1 se a mão de duas cartas permitir ao jogador «dobrar a aposta», 0 caso contrário. Para facilitar o trabalho com os valores, as constantes TRUE e FALSE, equivalentes a 1 e 0, respetivamente, são definidas no topo do ficheiro.

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
Editar via GitHub A ligação abre numa nova janela ou separador
x86-64 Assembly Exercism

Estás pronto para começar Blackjack?

Inscreve-te no Exercism para aprenderes e dominares x86-64 Assembly com 22 conceitos130 exercícios, e mentoria humana real, tudo grátis.