トラック
/
x86-64 Assembly
x86-64 Assembly
/
演習
/
ブラックジャック
ブラックジャック

ブラックジャック

学習演習

はじめに

条件分岐

rflags

rflagsという特別なレジスターがあります。 そのビットは、さまざまな条件を表すフラグのように働きます。

その一部を以下に示します。

名前 記号 ビット
キャリー CF 0
ゼロ ZF 6
符号 SF 7
オーバーフロー OF 11

比較命令

rflagsのフラグは、直接変更されることは_ありません_。 代わりに、さまざまな命令によって設定されます。

たとえば、ZFは、算術演算やビット演算の結果がゼロのときに設定されます。

条件を判定するためによく使われる命令の1つがcmpです。 cmpは2つのオペランドを受け取り、フラグを更新しますが、オペランド自体は変更しません。

cmp命令

cmp命令は、第1オペランドから第2オペランドを引き、その結果に応じてフラグを設定します。

Aを第1オペランド、Bを第2オペランドとすると、次のようになります。

フラグ 設定される条件
CF A < B(符号なし)
ZF A == B
SF A < B(符号付き、オーバーフローなし)
OF 符号付きの減算でオーバーフロー

分岐

既定では、x86-64のコードは上から下へ順番に実行されます。

しかし、この動作を変える必要がある場面は数多くあります。 たとえば、条件に応じて別の命令を実行したい場合です。

高水準言語では、通常、if...elseのような抽象的な構文でこれを行います。 しかし、x86-64にはそのようなものはありません。

代わりに、x86-64には、コードの別の位置へ実行を移す命令が用意されています。 これをbranchingと呼びます。

Note

このような命令はすでに2つ見てきました。callとretです。

関数が呼び出されると、実行は呼び出し元から呼び出された関数へ移ります。 そして、戻るときには、実行は呼び出し元へ戻ります。

retが見つからなければ、実行はある関数から次の関数へフォールスルーします。 これは、コードの流れを最適化するために使われることもあります。

無条件ジャンプ

jmp命令は、プログラムの実行をコードの別の位置へ無条件に移します。 その唯一のオペランドはラベルで、実行が続く位置のアドレスを持ちます。

たとえば、次の関数を見てみましょう。

fn:
    mov rax, 5
    jmp end

    add rax, 10
end:
    ret

fnが呼び出されると、実行はmov rax, 5から始まります。 これにより、その時点でraxの値は5になります。

次の命令はjmp endで、実行をラベルendへ移します。

endの次にある命令はretで、実行を呼び出し元の関数へ戻します。

add rax, 10はjmp endの後、endの前に位置しているため、決して実行されないことに注目してください。 fnが戻るときのraxの値は5です。

条件付きジャンプ

jcc命令群は、特定の条件が満たされたときにだけ、プログラムの実行を別の位置へ移します。 そうでなければ、実行はそのまま順番に続きます。

それぞれの条件は、rflags内の1つまたは複数のフラグに対応します。 jccのバリアントには、フラグがセットされていることを調べるものと、クリアされていることを調べるものがあります。

jccのccはそのままの文字ではなく、調べるフラグに対応する特定の接尾辞を指します。

接尾辞は数多くあり、その多くは別の接尾辞と同じ条件を調べます。 そのうちのいくつかはフラグを直接参照し、特定のフラグがセットされていればラベルへジャンプするようになっています。

接尾辞 ジャンプする条件
z ZF == 1
c CF == 1
s SF == 1
o OF == 1

他の多くは、cmp命令における意味を表すために選ばれています。 たとえば、次のとおりです。

命令 接尾辞 ジャンプする条件
cmp A, B e A == B
cmp A, B l A < B(符号付き)
cmp A, B b A < B(符号なし)
cmp A, B g A > B(符号付き)
cmp A, B a A > B(符号なし)

l、b、g、aの後にeを付けると、条件に等しい場合を含めることができます。

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

すべての接尾辞には、反対の動作を調べるバリアントがあります。 構文は同じですが、接尾辞の前にnが付きます。

たとえば、jnzはZFがセットされていないときにジャンプします。 同様に、jnaeはAがB以上ではないとき(AとBを符号なし整数として解釈した場合)にジャンプします。

Note

一部の接尾辞は、同じ条件の別名です。 たとえば、jz(接尾辞z、ZFに対応)とje(接尾辞e、等しいことに対応)は、どちらもZFがセットされているときにジャンプします。 これは、cmpでは減算の結果がゼロのとき、つまり2つのオペランドが等しいときにZFがセットされるためです。

一方、他の接尾辞は複数のフラグの組み合わせを調べるもので、単一のフラグの接尾辞で直接置き換えることはできません。

比較の意味をよりよく表す接尾辞を選びましょう。

ローカルラベル

ラベルはソースファイル全体から見え、関数内に限定されません。 そのため、同じラベル名を再利用することはできません。

ローカルラベルの動作を模倣するために、NASMには、先頭にピリオド(.)を付けて宣言するラベルの特別な記法があります。 この記法で定義されるラベルには、直前のドットの付いていないラベルの名前が暗黙的に含まれます。

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

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

完全なラベル名を使えば、コードのどこからでもこのラベルへジャンプできます。たとえばjmp fn1.exampleです。

ただし、ドットから始まる部分を使ってジャンプした場合は、その上にある関数の_内側_のラベルへジャンプします。 たとえば、.exampleはその関数にローカルであるかのように振る舞います。

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

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

説明

この演習では、ブラックジャックのルールの一部を実装します。たとえば、ゲームの進め方や得点の数え方などです。

Note

この演習では、カードは数値で表します。数札はその数値で表し、ジャック、クイーン、キング、エースはそれぞれ11、12、13、14で識別します(ジョーカーはゲームでは使いません)。

この表現を扱いやすくするために、ファイルの先頭にいくつかの定数が定義されています。C2からC10は数札、CJはジャック、CQはクイーン、CKはキング、CAはエースに対応します。

標準的なフランス式スートの52枚のデッキを前提としていますが、ほとんどの遊び方では、複数のデッキを混ぜて使います。

Note

このコンセプトで登場する命令は次のとおりです。

命令 説明
cmp a, b a - bに応じてフラグを設定する
jmp a コードの実行をここで止め、ラベルaから続ける
jcc a ccの条件が満たされていれば、ラベルaからコードを続ける

これらは、cmp a, bのあとにjccで判定される条件です。

命令 ジャンプする条件
je a == b
jl a < b(符号あり)
jg a > b(符号あり)
jb a < b(符号なし)
ja a > b(符号なし)
jle a <= b(符号あり)
jge a >= b(符号なし)
jbe a <= b(符号なし)
jae a >= b(符号なし)
jne a != b
jnl !(a < b)(符号あり)
jng !(a > b)(符号あり)
jnb !(a < b)(符号なし)
jna !(a > b)(符号なし)
jnle !(a <= b)(符号あり)
jnge !(a >= b)(符号あり)
jnbe !(a <= b)(符号なし)
jnae !(a >= b)(符号なし)

1. カードの値を計算する

ブラックジャックでは、CAの値は手札に応じて1か11のどちらかになります(詳しくは後ほど説明します)。 絵札(CJ、CQ、CK)は10点として数えられ、それ以外のカードはその_数値_がそのまま値になります。

カードを表す数値cardを仮引数にとるvalue_of_card関数を定義します。 この関数は、渡されたカードの_数値_を返さなければなりません。 CAは複数の値(1または11)をとりうるので、ここではCAの値を1に固定します。 後ほど、すでにある手札からCAの値を決める関数を実装します。

value_of_card(13)
// => 10

value_of_card(4)
// => 4

value_of_card(14)
// => 1

2. どちらのカードの値が大きいかを判定する

それぞれがカードを表す2つの数値card_oneとcard_twoを仮引数にもつhigher_card関数を定義します。 得点の計算では、CJ、CQ、CKの値はどれも10です。 この関数は、得点計算でより値の大きい方のカードを返さなければなりません。 2枚のカードの値が等しい場合は、両方を返します。

CAは複数の値をとりうるので、このタスクでは値を1に固定します。

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

higher_card(4, 6)
// => 6

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

関数から2つの整数を返すには、raxとrdxの両方のレジスターを使う必要があります。

returning_two_values:
    mov rax, rdi
    mov rdx, rsi
    ret

カードを1枚だけ返す場合は、rdxを0に設定しなければなりません。

3. エースの値を計算する

先ほど述べたように、CAは手札に応じて1点または11点の_either_どちらかになります。 ブラックジャックのルールでは、21を超えない範囲で手札の得点ができるだけ高くなるようにCAの値を選びます(21を超えることを「バースト」といいます)。

最後のCAを受け取る_前に_すでに手札にある2枚のカードを表す2つの数値card_oneとcard_twoを仮引数にとるvalue_of_ace関数を定義します。 この関数は、これから加わるCAに1と11のどちらの値を割り当てるかを返さなければなりません。 思い出してください。新しい手札(CAを含む)の値は、21を超え_ない_範囲でできるだけ高くする必要があります。

ヒント:すでに手札にCAがある場合、これから加わるCAの値は1になります。

value_of_ace(6, 13)
// => 1

value_of_ace(7, 3)
// => 11

4. 手札がブラックジャックかどうかを判定する

プレイヤーが最初の2枚としてCAと10のカード(C10、CJ、CQ、CK)を配られた場合、そのプレイヤーの得点は21になります。 この手札はブラックジャックと呼ばれます。

2枚のカードを表す2つの数値card_oneとcard_twoを仮引数にとるis_blackjack関数を定義します。 この関数は、2枚の手札がブラックジャックであれば1を、そうでなければ0を返さなければなりません。 値を扱いやすくするために、それぞれ1と0に相当する定数TRUEとFALSEがファイルの先頭に定義されています。

注意:得点の_計算_にはさまざまな方法があります。 ただし、可能であれば、カードの値を_合計する_のではなく、手札の中にCAと10のカードがあるかどうかを調べるようにしてください。

is_blackjack(14, 13)
// => 1

is_blackjack(10, 9)
// => 0

5. ペアを分割する

プレイヤーの最初の2枚のカードが同じ値である場合(たとえばC6が2枚の手札、またはCQとCKの手札)、プレイヤーはそれらを2つの別々の手札として扱うことを選べます。 これは「ペアの分割」と呼ばれます。

2枚のカードを表す2つの数値card_oneとcard_twoを仮引数にとるcan_split_pairs関数を定義します。 この関数は、2枚の手札を2つの手札に分割できる場合は1を、そうでなければ0を返さなければなりません。 値を扱いやすくするために、それぞれ1と0に相当する定数TRUEとFALSEがファイルの先頭に定義されています。

can_split_pair(12, 13)
// => 1

can_split_pair(10, 14)
// => 0

6. ダブルダウンする

配られた最初の2枚のカードの合計が9、10、11点のとき、プレイヤーは元の賭け金と同じ額の追加の賭け金を置くことができます。 これは「ダブルダウン」と呼ばれます。

2枚のカードを表す2つの数値card_oneとcard_twoを仮引数にとるcan_double_down関数を定義します。 この関数は、2枚の手札でプレイヤーが「ダブルダウン」できる場合は1を、そうでなければ0を返さなければなりません。 値を扱いやすくするために、それぞれ1と0に相当する定数TRUEとFALSEがファイルの先頭に定義されています。

can_double_down(14, 9)
// => 1

can_double_down(10, 2)
// => 0
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
x86-64 Assembly Exercism

ブラックジャックを始める準備はできましたか?

Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。