ホーム » Julia言語で入門するプログラミング(その2)

Julia言語で入門するプログラミング(その2)


「Julia言語で入門するプログラミング」第2回である。まだ第1回を読まれていない方はぜひそちらから読んでいただきたい。

前の記事

一覧はこちら

if文

前回の記事の終わりの方で、ダメージ計算関数の自動テストを作ると言ったが、ちょっと後に回す。先にif文を説明する必要がある。

if文とは、条件分岐のことである。プログラムで、ある条件が成り立つときにこういった処理をして欲しい、別の条件のときにはこうして欲しい、といった処理を書きたいことはよくある。そのために使われる構文がif文である。

例えば、ダメージを受けてHPを減らす処理を考えよう。残りHPが10のときに、20のダメージを受けたとして、HPがマイナスになって欲しくはないはずだ。HPは0になるのが普通のゲームだ。そのようなときは、次のような処理を書くことになる。

function 残HP計算(残HP, ダメージ)
    ダメージ計算後残HP = 0
    if 残HP - ダメージ < 0
        ダメージ計算後残HP = 0
    else
        ダメージ計算後残HP = 残HP - ダメージ
    end
    return ダメージ計算後残HP
end

中身の説明は後でするとして、まずはこのコードをJuliaのREPLに貼り付け、その後、残HP計算処理を呼び出してみよう。

julia> 残HP計算(10, 3)
7

julia> 残HP計算(10, 10)
0

julia> 残HP計算(10, 20)
0

確かに、10の残HPに対して20のダメージの時には、マイナスではなく0になっていることがわかる。

では、if文の説明に入る。まず、if文の骨格を示すと次のようになる。

if (条件式1)
  (条件式1が満たされた時に実行される処理)
elseif (条件式2)
  (条件式2が満たされた時に実行される処理)
elseif (条件式3)
  (条件式3が満たされた時に実行される処理)
else
  (条件式が全て満たされなかった時に実行される処理)
end

これらのうち、elseif とelseに関しては、なくても構わない。elseifはいくつあってもいいが、elseは一つだけだ。if文は上から順に評価される。そのため、仮に条件式1と条件式2が全く同じものであっても、「条件式1が満たされた時に実行される処理」のみが実行される。条件式2も満たされているはずだが、「条件式1が満たされた時に実行される処理」は評価されない。

条件式

条件式とはどんなものだろうか?条件式には、真偽値と呼ばれるものが入る。これは真か偽か決定できる値で、英語ではBooleanと呼ばれる。Bool値と言う呼ばれ方をすることもある。例えば、「数値xは1と等しい」は、xの値が1かどうかで結果は変わるにしろ、真か偽か決定できる。「配列aの長さは10より小さい」も、真か偽か決定できる。こう書くと、真か偽か決定できないものなどあるのか?という気がしてくるが、実際には真でも偽でもないものは多くある。例えば、10という値は真でも偽でもない。

なぜこんなことを言うかと言うと、C言語やRubyなど一部の言語では、if文の条件式に真偽値以外の値を入れることができるためである。C言語に至っては、そもそも真偽値という概念がなく、「真と評価される値」「偽と評価される値」があるだけだ。例えば、数値は0であれば偽と評価され、それ以外の値は真と評価される。そのため、C言語では、if (10) {...}という記述は完全に正当だが、Juliaでは文法エラーとなる。

Juliaには真偽値を作るための手段が下記のようになっている。しばらくこまかい文法の話が続くが、我慢して欲しい。

  • 真偽値そのもの(true、false)
  • 比較演算子 (==、!=、 ===、 !==、<、>、<=、>=、≤、≥)
  • 論理演算子 (!、&&、||)
真偽値そのもの

true/falseはそれぞれ真と偽を意味する。

julia> true
true

julia> false
false

実は真偽値は数値との比較や演算が可能である。trueは1として、falseは0として評価される。

julia> true == 1
true

julia> true == 0
false

julia> true == 2
false

julia> false == 0
true

julia> false == 1
false

julia> true + 0
1

julia> true + 1
2

julia> true - false
1

julia> true + false
1

だからといって、trueを使うべき場面で1と使えると言うわけでもない。

julia> if true
         println("trueの処理")
       else
         println("falseの処理")
       end
trueの処理

julia> if 1
         println("trueの処理")
       else
         println("falseの処理")
       end
ERROR: TypeError: non-boolean (Int64) used in boolean context

trueを数値の1として扱えて嬉しい場面は、私にはあまり思い浮かばないが、とにかくそのように動くようにはなっている。

比較演算子

比較演算子は両辺の値を比較して、結果の真偽を決める。

==は等しいと言う意味である。=記号は代入として使われているので、==が等しいと言う意味で使われたのだろう。

julia> 1 == 1
true

julia> 1 == 1.0
true

julia> 1 + 2 == 3
true

julia> x = 10
julia> x == 10
true

julia> 1 == "a"
false

julia> "a" == "a"
true

!=は等しくないと言う意味である。

julia> 1 != 2
true

julia> 1 != 1
false

julia> 1 != "a"
true

===!==の説明は難しいのでいったん省く。パッと使う限りでは、==!=との違いがわからないだろう。いずれ話すべきタイミングで話そう。

<><=>=はおなじみの不等号だ。複数つなげることもできる。

そのうえ、<=の代わりに>=を使うことできる。JuliaのREPLで\leと押してtabキーを押すととなり、\geと押してtabキーを押すととなる。

julia> 1 < 2
true

julia> 1 <= 2
true

julia> 1 ≤ 2
true

julia> 2 <= 2
true

julia> 2 ≤ 2
true

julia> 1 > 2
false

julia> 1 >= 2
false

julia> 1 ≥ 2
false

julia> 2 >= 2
true

julia> 2 ≥ 2
true

julia> 1 < 2 < 3
true

julia> 1 < 2 > 3
false

julia> 2 < 3 > 1 
true

数値と文字列の大小比較のようなことはできない。

julia> 1 ≥ "a"
ERROR: MethodError: no method matching isless(::String, ::Int64)

一方、文字列同士の大小比較は可能だ。辞書順の評価になる。

julia> "abc" < "def"
true

julia> "ab" < "abc"
true

文字列の大小比較は明示的に行うことはあまりないと思うが、文字列を集めてソートするときなどにはどちらが大きいかを決める必要があるので、意識しておいたほうがいいことがたまにある。

論理演算子

真偽値に対する演算である。

!は否定演算子と呼ばれるもので、真と偽をひっくり返す。

julia> !true
false

julia> !false
true

julia> !(4 != 5)
false

julia> !(1 == 1)
false

&&は論理積演算子と呼ばれるもので、2つの真偽値の両方が真の時に真となり、片方でも偽のときには偽となるものである。

julia> true && true
true

julia> true && false
false

&&は短絡評価という面白い動きを行う。複数の真偽値を&&でつないでいく時、1つでも偽があると、全体が偽となる。

julia> true && false && true && true
false

そのため、先頭から確認して偽があると、そこで論理積全体の評価が確定するため、後の方まで見に行かないのである。これを短絡評価という。

例えば、1 < "a"というのは、不正な比較としてエラーになる。しかし、これが含まれる式であっても短絡評価のためにエラーとならないことがある。

julia> true && (1 < "a")
ERROR: MethodError: no method matching isless(::Int64, ::String)

julia> false && (1 < "a")
false

上の方の式では、&&の1つ目がtrueのため、論理積全体の値の確定のためには、後半の1 < "a"の評価が必要であり、評価しようとしてエラーとなった。一方、下の式では、&&の1つ目がfalseのため、論理積全体の値はその時点でfalseと確定するため、1 < "a"の評価は行われないのだ。

細かい話に思われるかもしれないが、意外と重要だ。このトリックはよく使われる。

ゲームっぽい例を出そう。仮に、連続攻撃にボーナスがつく仕様になったとしよう。1撃目のダメージの下2桁がゾロ目だった時に、残りの攻撃のダメージが2倍になるのだ。この処理はどうなるだろうか。連続攻撃のダメージが連続ダメージという配列で渡されたとする。こんな感じの関数になるだろう。

function 連続ダメージ合計(連続ダメージ)
    合計ダメージ = 0
    if 下二桁がゾロ目(連続ダメージ[1])
        (連続攻撃の2撃目以降の攻撃を2倍する処理)
    else
        (普通の連続攻撃の処理)
    end
    return 合計ダメージ
end

ところが、連続攻撃が全て回避された時に、このコードは破綻する。なぜなら、連続ダメージ配列には、空振りした攻撃のダメージが入らないからだ(ということにしよう)。そのため、連続攻撃が全て外れると、連続ダメージ配列は、要素数0の配列になり、連続ダメージ[1]はエラーとなる。

これを回避するためには、連続ダメージ配列の長さを調べてやればいい。length関数は、配列の長さ(要素数)を調べる関数だ。

function 連続ダメージ合計(連続ダメージ)
    合計ダメージ = 0
    if length(連続ダメージ) ≥ 1
        if 下二桁がゾロ目(連続ダメージ[1])
            (連続攻撃の2撃目以降の攻撃を2枚する処理)
        else
            (普通の連続攻撃の処理)
        end
    end 
    return 合計ダメージ
end

ただ、こうではなく、次のように書くこともできる。

function 連続ダメージ合計(連続ダメージ)
    合計ダメージ = 0
    if length(連続ダメージ) ≥ 1 && 下二桁がゾロ目(連続ダメージ[1])
        (連続攻撃の2撃目以降の攻撃を2倍する処理)
    else
        (普通の連続攻撃の処理)
    end
    return 合計ダメージ
end

長さが1以上の時には、1つめの要素の値を元に判定するという、これはこれで自然な書き方だ。長さが0の時には、前半の条件がfalseになるので、後半の処理のことは気にしなくていい。

||は論理和演算子と呼ばれるもので、2つの真偽値の片方が真の時に真となり、両方偽の時に偽となるものである。

julia> true || false
true

julia> false || false
false

||にも短絡評価がある。trueとなった瞬間に評価が確定するので、それ以降の部分は評価されない。

julia> true || (1 < "a")
true

julia> false || (1 < "a")
ERROR: MethodError: no method matching isless(::Int64, ::String)

練習問題

解答例はページの最後に記載している。ただ、プログラムの書き方に唯一の正解はない。それよりは、掲示した自動テストが正解するかどうかが重要だ。関数を作ってみたら、テストコードを実行してみて欲しい。

  • 問題1
    • 引数が負の数かどうかを判定する負の数関数を作ろう。引数が負の数ならtrue、0以上の正の数ならfalseを返す。引数は1つだけ、数値と仮定していい。
using Test
@testset "if文問題1" begin
    @test 負の数(1) == false
    @test 負の数(0) == false
    @test 負の数(-1) == true
end
  • 問題2
    • 引数の絶対値を返す関数を作ってみよう。if文の条件式の中で、問題1で作った関数を呼び出すようにしよう。引数は1つだけ、数値と仮定していい。
using Test
@testset "if文問題2" begin
    @test 絶対値(1) == 1
    @test 絶対値(0) == 0
    @test 絶対値(-1) == 1
end
  • 問題3
    • 引数が3の倍数だと”Fizz”、5の倍数だと”Buzz”、15の倍数だと”FizzBuzz”、という文字列を返すfizzbuzz関数を作ろう。それ以外は空文字を返す。これは「Fizz-Buzz問題」と呼ばれる有名な問題だ。引数は1つだけ、正の整数と仮定していい。%という演算子を使おう。n%mでnをmで割ったあまりとなる。10%3 == 1だ。
using Test
@testset "if文問題3" begin
    @test fizzbuzz(1) == ""
    @test fizzbuzz(2) == ""
    @test fizzbuzz(3) == "Fizz"
    @test fizzbuzz(4) == ""
    @test fizzbuzz(5) == "Buzz"
    @test fizzbuzz(6) == "Fizz"
    @test fizzbuzz(7) == ""
    @test fizzbuzz(8) == ""
    @test fizzbuzz(9) == "Fizz"
    @test fizzbuzz(10) == "Buzz"
    @test fizzbuzz(15) == "FizzBuzz"
    @test fizzbuzz(20) == "Buzz"
    @test fizzbuzz(30) == "FizzBuzz"
end

if文まとめ

if文というか、条件式や真偽値の説明がかなり長くなってしまった。だが、if文もfor文と同じくあらゆる言語に登場する超重要関数だ。正直なところ、for文とif文と関数が使えたら、大体のプログラムは作れる。消化不良の部分があったら、しっかりとプログラムを書いて身につけよう。練習問題3のFizz-Buzz問題が自力で書けたら文句なしに合格だ。

自動テスト

いよいよダメージ計算関数の自動テストだ。

ダメージ計算の関数は、現状、攻撃力、防御力を引数にとり、ダメージを返している。いったん、現在の実装の詳細は忘れて、ダメージ計算関数に期待する振る舞いを考えよう。この振る舞いを自動テストとして残しておきたい。

まず、関数のインターフェースはこうなっている。実装の詳細は忘れよう。

function ダメージ計算(攻撃力, 防御力)
    ...
end

ひどまず、ダメージ計算関数に期待するところを書き並べたい。

  • 攻撃力10、防御力10の時に、ダメージは10になって欲しい。
  • 攻撃力15、防御力100の時に、ダメージは2になって欲しい。
  • 攻撃力14、防御力100の時に、ダメージは1になって欲しい。
  • 攻撃力0、防御力10の時に、ダメージは0になって欲しい。

ここまでは正常系といったところだ。攻撃力を防御力で割って10倍した値を四捨五入したい。四捨五入になっているかどうかを調べるためのケースが、攻撃力15と14のケースだ。

次は、異常系について考える。「例外」という言葉が出てきているのだが、まだ説明していない。いずれ説明するが、簡単にいうと、「不正な動きと判断して処理を中断する」という意味だ。

  • 攻撃力-1、防御力10の時に、例外を発生させて欲しい。
  • 攻撃力10、防御力-1の時に、例外を発生させて欲しい。
  • 攻撃力-1、防御力-1の時に、例外を発生させて欲しい。
  • 攻撃力10、防御力0の時に、例外を発生させて欲しい。

想定していない値が入ってきたときにどうなって欲しいかというところも考えているのがここだ。

攻撃力や防御力が負の数になったときには、不正な呼び出しだと判断するという意味だ。一応考え方としては、攻撃力が負の数のとき、ダメージも負の数となりHPが回復するという考え方もあってもいい。それはプログラム開発と言うよりも、ゲームのデザインに関する範疇なので、プログラマが不用意に決定してはいけないのだが、今回は我々がゲームデザイナとプログラマを兼任しているので、決めてしまおう。私はおかしな仕様だと思うので、エラーにするようにする。

防御力0については、迷うところだ。敵の防御力を下げる魔法みたいなものがあるとして、結果として防御力0という状態があり得るのかどうか?あり得るとするのであれば、その際に正しいダメージ量が計算されるようにしなければならないし、あり得ないのであれば例外とする必要がある。これも本来プログラマが勝手には決められないことだが、私はありえないとすることにして、エラーにするようにする。防御力を下げる魔法をいくらかけても防御力は1未満にはならないということだ。

ダメージ計算に対する期待結果も揃ったので、これをチェックする自動テストを作ろう。

まずは上4つの正常系のテストだ。

@testset "ダメージ計算" begin
    @test ダメージ計算(10, 10) == 10
    @test ダメージ計算(15, 100) == 2
    @test ダメージ計算(14, 100) == 1
    @test ダメージ計算(0, 100) == 0
end

これは問題なく通るはずだ。一安心だ。

@testset "ダメージ計算" begin
    @test ダメージ計算(10, 10) == 10
    @test ダメージ計算(15, 100) == 2
    @test ダメージ計算(14, 100) == 1
    @test ダメージ計算(0, 100) == 0
    @test_throws DomainError ダメージ計算(-1, 10)
    @test_throws DomainError ダメージ計算(10, -1)
    @test_throws DomainError ダメージ計算(-1, -1)
    @test_throws DomainError ダメージ計算(10, 0)
end

次に、4つテストケースを追加した。攻撃力や防御力があり合えない値の時に、不正な処理とするテストケースだ。これを実装するには「例外」について学ぶ必要があるが、これは割と上級のトピックなので、後々解説しよう。ひとまずこれらのテストケースはコメントアウトしておこう。

@testset "ダメージ計算" begin
    @test ダメージ計算(10, 10) == 10
    @test ダメージ計算(15, 100) == 2
    @test ダメージ計算(14, 100) == 1
    @test ダメージ計算(0, 100) == 0
    #=
    @test_throws DomainError ダメージ計算(-1, 10)
    @test_throws DomainError ダメージ計算(10, -1)
    @test_throws DomainError ダメージ計算(-1, -1)
    @test_throws DomainError ダメージ計算(10, 0)
    =#
end

コメント

さらっとコメントアウトという言葉を使ったが、そもそもコメントというものについて全く触れていなかったので少しだけ解説しよう。

プログラムの中に、プログラムに関する補足説明を書きたくなることがある。そのようなコメントはプログラムの実行からは全く無視されて欲しい。でなければエラーになる。

プログラム中にコメントを書くには、#記号を使う。#記号はその部分から行の最後の部分までをコメントとして扱う。さらに、複数行に渡る長いコメントを書きたい時には、#==#で囲む。

println(1 + 2) #行コメント

#=
複数行にわたる
コメント
=#

「コメントアウト」という言葉は、プログラム行をコメント記号の中に入れることで、一時的に実行の対象外とすることである。実験的に消したいときなどによく使われる。

本来、プログラムの補足説明を書くという機能と、プログラムを一時的に無効化するという機能は、全く別の意図の機能である。そのため、それぞれの文法が開発されてもよかった。杓子定規にいけば、そちらの方があるべきなような気もする。しかし、実際にはあらゆる言語が、コメントという機能だけを提供している。「これでいいじゃん」という感じである。私はこの合理性が好きだ。

モンスターにもチャンスを

前回のプログラムは、勇者がモンスターを一方的に攻撃するという不公平極まりないものだった。モンスターにもチャンスをあげよう。

前回の最後の形がこうだった。

using Test

function ダメージ計算(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

function main()
    モンスターHP = 30
    モンスター防御力 = 10
    プレイヤー攻撃力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    for _ in 1:3
        println("----------")
        println("勇者の攻撃!")
        モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
        モンスターHP = モンスターHP - モンスターダメージ
        println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
        println("モンスターの残りHP:" * string(モンスターHP))
    end

    println("戦闘に勝利した!")
end

main()

ここから、モンスターも攻撃できるように変更しよう。

function main()
    モンスターHP = 30
    モンスター攻撃力 = 10 #追加行
    モンスター防御力 = 10
    プレイヤーHP = 30 #追加行
    プレイヤー攻撃力 = 10
    プレイヤー防御力 = 10 #追加行

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    for _ in 1:3
        println("----------")
        println("勇者の攻撃!")
        モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
        モンスターHP = モンスターHP - モンスターダメージ
        println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
        println("モンスターの残りHP:" * string(モンスターHP))

        #追加部分
        println("----------")
        println("モンスターの攻撃!")
        プレイヤーダメージ = ダメージ計算(モンスター攻撃力, プレイヤー防御力)
        プレイヤーHP = プレイヤーHP - プレイヤーダメージ
        println("勇者は" * string(プレイヤーダメージ) * "のダメージを受けた!")
        println("勇者の残りHP:" * string(プレイヤーHP))
    end

    println("戦闘に勝利した!")
end

とりあえず、モンスターが攻撃できるようにしただけである。実行してみるとわかるが、勇者が先にモンスターのHPを0にするのだが、モンスターは倒れず(あるいは倒れながら)勇者に一撃を喰らわし、両者ともにHP0で終了する。河原で夕日を背景に殴り合う高校生のような死闘を繰り広げるのだ。青春だ。最後に、戦闘に勝利したという的外れなメッセージとともにゲームは終了する。

さて、これからいくつか改良を加えたい。

  1. HPが0になっても攻撃できるのはおかしいので、一方のHPが0になったら戦闘が終了するように変更する。
  2. 常に勇者が先制攻撃をするのは不公平なので、勇者とモンスターの攻撃順はランダムになるようにする。

これで、毎回同じ流れの固定された戦闘ではなくなり、どちらが勝つかわからない手に汗握る展開になるのだ。ただ、まだ見ているだけだ。勇者に指示を出すことはできない。それは次回のお楽しみだ。

まずは、1つ目から実装しよう。早速if文の出番だ。攻撃が終了したら、ダメージを受けた側の残HPをチェックして、0だったらプログラムを終了するようにしよう。

次のようにした。勇者の攻撃後、モンスターの攻撃後、それぞれで攻撃が受けた側のHPをチェックし、0だったらbreakしている。breakというのは、ループからの脱出である。forの中でbreakに到達すると、forの終わりのendまで一気にジャンプする。

for _ in 1:3
    println("----------")
    println("勇者の攻撃!")
    (略)
    if モンスターHP == 0
        break
    end

    println("----------")
    println("モンスターの攻撃!")
    (略)
    if プレイヤーHP == 0
        break
    end
end

これを実行すると、次のような結果になる。モンスターがHP=0になってもなお攻撃してくることはなく、戦闘が終了している。

モンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:20
----------
モンスターの攻撃!
勇者は10のダメージを受けた!
勇者の残りHP:20
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:10
----------
モンスターの攻撃!
勇者は10のダメージを受けた!
勇者の残りHP:10
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:0
戦闘に勝利した!

何度実行しても同じだ。勇者は必ずモンスターよりも先に攻撃するので、モンスターは必ず負けることになる。

次は2を実装しよう。常に勇者が先制攻撃をするのは不公平なので、勇者とモンスターの攻撃順はランダムになるようにするのだ。これにもif文と、乱数というものを使う。

乱数とは、ランダムな数のことである。Juliaはランダムな数を生成できる関数を標準で使うことができる。rand()という関数は、0から1の間の小数をランダムに生成する。

julia> rand()
0.03587016158053946

julia> rand()
0.6696669182272514

julia> rand()
0.3851182443503387

rand() < 0.5となる確率は50%、rand() < 0.3となる確率は30%である。乱数を使うことで、プログラムの動きは予想のつかないものになる。

勇者とモンスターは対等に扱いたいので、50%で勇者が先制攻撃し、50%でモンスターが先制するようにしよう。

とりあえず、rand() < 0.5のとき、勇者が先攻、そうでない時にモンスターが先攻になるようにしよう。

もともとのプログラムは勇者が先攻のケースなので、if rand() < 0.5で、くるむ。

for _ in 1:3
    if rand() < 0.5
        #勇者が先攻
        println("----------")
        println("勇者の攻撃!")
        (略)
        if モンスターHP == 0
            break
        end


        #モンスターが後攻
        println("----------")
        println("モンスターの攻撃!")
        (略)
        if プレイヤーHP == 0
            break
        end
    end
end

そして、else がモンスター先攻のケースとなる。とりあえず、カジュアルにifの中身をコピーして上下をひっくり返そう。

for _ in 1:3
    if rand() < 0.5
        #勇者が先攻
        println("----------")
        println("勇者の攻撃!")
        (略)
        if モンスターHP == 0
            break
        end

        #モンスターが後攻
        println("----------")
        println("モンスターの攻撃!")
        (略)
        if プレイヤーHP == 0
            break
        end
    else
        #モンスターが先攻
        println("----------")
        println("モンスターの攻撃!")
        (略)
        if プレイヤーHP == 0
            break
        end

        #勇者が後攻
        println("----------")
        println("勇者の攻撃!")
        (略)
        if モンスターHP == 0
            break
        end        
    end
end

通常、ソースコード のコピぺは悪だと言われる。それはその通りだ。だが、私は最初の第一歩としてはコピペは全く問題ないと考えている。終わった時に綺麗にできていれば良い。

ついでに、戦闘が終了した時のメッセージも、次のように変えておこう。

if モンスターHP == 0
    println("戦闘に勝利した!")        
else
    println("戦闘に敗北した・・・")
end

ここまでで、次のような形になっているはずだ。

using Test

function ダメージ計算(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

function main()
    モンスターHP = 30
    モンスター攻撃力 = 10
    モンスター防御力 = 10
    プレイヤーHP = 30
    プレイヤー攻撃力 = 10
    プレイヤー防御力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    for _ in 1:3
        if rand() < 0.5
            #勇者が先攻
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
            if モンスターHP == 0
                break
            end

            #モンスターが後攻
            println("----------")
            println("モンスターの攻撃!")
            プレイヤーダメージ = ダメージ計算(モンスター攻撃力, プレイヤー防御力)
            プレイヤーHP = プレイヤーHP - プレイヤーダメージ
            println("勇者は" * string(プレイヤーダメージ) * "のダメージを受けた!")
            println("勇者の残りHP:" * string(プレイヤーHP))
            if プレイヤーHP == 0
                break
            end
        else
            #モンスターが先攻
            println("----------")
            println("モンスターの攻撃!")
            プレイヤーダメージ = ダメージ計算(モンスター攻撃力, プレイヤー防御力)
            プレイヤーHP = プレイヤーHP - プレイヤーダメージ
            println("勇者は" * string(プレイヤーダメージ) * "のダメージを受けた!")
            println("勇者の残りHP:" * string(プレイヤーHP))
            if プレイヤーHP == 0
                break
            end

            #勇者が後攻
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
            if モンスターHP == 0
                break
            end
        end
    end

    if モンスターHP == 0
        println("戦闘に勝利した!")        
    else
        println("戦闘に敗北した・・・")
    end
end

main()

いろいろ粗のあるコードだが、とりあえずやりたいことはできているはずだ。これを動かしてみよう。確率次第だが、何回か実行していれば、勇者が勝ったり負けたりするところを見ることができるだろう。

モンスターに遭遇した!
戦闘開始!
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:20
----------
モンスターの攻撃!
勇者は10のダメージを受けた!
勇者の残りHP:20
----------
モンスターの攻撃!
勇者は10のダメージを受けた!
勇者の残りHP:10
----------
勇者の攻撃!
モンスターは10のダメージを受けた!
モンスターの残りHP:10
----------
モンスターの攻撃!
勇者は10のダメージを受けた!
勇者の残りHP:0
戦闘に敗北した・・・

まずは動くものが作れた。これが大事だ。動くものを作って、それから改良しよう。

リファクタリング

プログラムの動作を変えずに、内部の構造を改良することをリファクタリングという。今からやるのはリファクタリングだ。コードには何の機能追加もしない。ただ、読みやすく改良するだけだ。

リファクタリングで大事なことは、動作を変えないことだ。動作を変えないことを保証するために、自動テストを作ろう。

自動テストを作るにあたって、ちょっと問題がある。プログラム中の乱数だ。乱数は毎度値を変えるので、自動テストとは相性が悪い。いろいろやり方はあるが、ひとまず今は安直に乱数を排除しよう。そのためにちょっとだけ手を加える。main関数の引数に、配列を渡せるようにして、それを内部で生成する乱数の代わりに使う。リファクタリングが終わったら元に戻す。

次のような感じだ。引数に偽乱数列をとり、プログラム中で乱数の代わりに使っている。また、for文のループの何回目かで、使う配列の要素も変わるのでfor i in 1:3に変えている。

function main(偽乱数列)
    (略)
    for i in 1:3
        if 偽乱数列[i] < 0.5
        #if rand() < 0.5

呼び出し側はこのようになる。main処理の分岐のパターンを網羅するように偽の乱数列を作って呼び出している。さらに、main中にこれから書かれるテストケースをジャッジするために、@testsetで括る。

@testset "main処理リファクタリング" begin
    main([0.1, 0.1, 0.1])
    main([0.1, 0.1, 0.9])
    main([0.1, 0.9, 0.1])
    main([0.1, 0.9, 0.9])
    main([0.9, 0.1, 0.1])
    main([0.9, 0.1, 0.9])
    main([0.9, 0.9, 0.1])
    main([0.9, 0.9, 0.9])
end

これを呼び出すと、偽乱数のパターンを変えながら8回main処理が呼ばれ、それぞれのパターンで実行される。

これで自動テストの準備は完了だ。あとはリファクタリングする箇所に合わせて、テストケースを作成して、テストが通ることを確かめながらリファクタリングを進めていく。

while文

まず最初に手をつけたいのは、for文だ。for文は3回しか実行していない。本当は3ターンの攻撃で終了することが決まっているのはおかしい。それがなんとか成り立っているのは、勇者もモンスターもHPを30に設定しており、攻撃は必ず10ダメージ固定だからだ。本来は、戦闘はターンは事前に決まらない。どちらかが倒れたら終わりで、それまでは永久に続くのが望ましい。実際、どちらかが倒れたらループを抜けるように、breakまで作っているのだ。ここらで、3回限りのforとは手を切ることにしよう。

ここでfor文のループの回数を100億回にしてもいいのだが、別の手段がある。それがwhile文だ。for文はループの回数が決まっている時に使うが、while文はループの終了条件が決まっている時に使う。

while文はこんな形をしている。if文よりも簡単だ。

while(終了条件)
  (処理)
end

例えば、次のようにREPLに打ち込んでみよう。

julia> s = 0
0

julia> while s < 100
         s = s + 30
         println(s)
       end

結果はこうなる。

30
60
90
120

while 内部の処理を、条件式が満たされる間は続けている。次の処理に入る時に、条件式をチェックして、満たされるなら内部の処理を行っている。

これを使ってみよう。まず、書き換え前はこのような処理になっている。

for i in 1:3
    if 偽乱数[i] < 0.5
        (略)
        if モンスターHP == 0
            break
        end

        (略)
        if プレイヤーHP == 0
            break
        end
    else
        (略)
    end
end

まず、書き換え前後で動作が変わっていないことを確かめる準備として、次のようなテストケースを追加しよう。ループの条件を変えても、3回攻撃したらゲームが終了することを期待している。

for i in 1:3
    if 偽乱数[i] < 0.5
        (略)
        if モンスターHP == 0
            @test i == 3
            break
        end

        (略)
        if プレイヤーHP == 0
            @test i == 3
            break
        end
    else
        (略)
    end
end

else句の中にも同様の@testが入っていると思って欲しい。これを実行すると、テストがOKになるはずだ。

次にwhile文に書き換える。今回はwhile文の条件式はtrueだ。条件式が常に真なので、中身は無限に実行される。だが、途中で戦闘終了条件を満たすと、breakしてwhileから脱出する。

while true
    if 偽乱数[i] < 0.5
        (略)
        if モンスターHP == 0
            @test i == 3
            break
        end

        (略)
        if プレイヤーHP == 0
            @test i == 3
            break
        end
    else
        (略)
    end
end

これを実行するとエラーになる。

main処理リファクタリング: Error During Test at /Users/kenji/Documents/SourceCode/rpg/rpg2.jl:199
  Got exception outside of a @test
  UndefVarError: i not defined

iというものが定義されていないと言われた。for i in 1:3iを消してしまったからだ。これを補うために、iを作ってあげよう。

    i = 0
    while true
        i = i + 1
        if 偽乱数列[i] < 0.5

これでテストが通るはずだ。テストが通るので、自信を持ってwhileに置き換えられる。

これで次のような形になっている。長くなるがほとんど答え合わせのためだけに全文載せているだけなので、飛ばしてもらって構わない。

using Test

function ダメージ計算(攻撃力, 防御力)
    return round(Int, 10 * 攻撃力/防御力)
end

function main(偽乱数列)
    モンスターHP = 30
    モンスター攻撃力 = 10
    モンスター防御力 = 10
    プレイヤーHP = 30
    プレイヤー攻撃力 = 10
    プレイヤー防御力 = 10

    println("モンスターに遭遇した!")
    println("戦闘開始!")

    i = 0
    while true
        i = i + 1
        if 偽乱数列[i] < 0.5
        #if rand() < 0.5
            #勇者が先攻
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
            if モンスターHP == 0
                @test i == 3
                break
            end

            #モンスターが後攻
            println("----------")
            println("モンスターの攻撃!")
            プレイヤーダメージ = ダメージ計算(モンスター攻撃力, プレイヤー防御力)
            プレイヤーHP = プレイヤーHP - プレイヤーダメージ
            println("勇者は" * string(プレイヤーダメージ) * "のダメージを受けた!")
            println("勇者の残りHP:" * string(プレイヤーHP))
            if プレイヤーHP == 0
                @test i == 3
                break
            end
        else
            #モンスターが先攻
            println("----------")
            println("モンスターの攻撃!")
            プレイヤーダメージ = ダメージ計算(モンスター攻撃力, プレイヤー防御力)
            プレイヤーHP = プレイヤーHP - プレイヤーダメージ
            println("勇者は" * string(プレイヤーダメージ) * "のダメージを受けた!")
            println("勇者の残りHP:" * string(プレイヤーHP))
            if プレイヤーHP == 0
                @test i == 3
                break
            end

            #勇者が後攻
            println("----------")
            println("勇者の攻撃!")
            モンスターダメージ = ダメージ計算(プレイヤー攻撃力, モンスター防御力)
            モンスターHP = モンスターHP - モンスターダメージ
            println("モンスターは" * string(モンスターダメージ) * "のダメージを受けた!")
            println("モンスターの残りHP:" * string(モンスターHP))
            if モンスターHP == 0
                @test i == 3
                break
            end
        end
    end

    if モンスターHP == 0
        println("戦闘に勝利した!")        
    else
        println("戦闘に敗北した・・・")
    end
end

@testset "main処理リファクタリング" begin
    main([0.1, 0.1, 0.1])
    main([0.1, 0.1, 0.9])
    main([0.1, 0.9, 0.1])
    main([0.1, 0.9, 0.9])
    main([0.9, 0.1, 0.1])
    main([0.9, 0.1, 0.9])
    main([0.9, 0.9, 0.1])
    main([0.9, 0.9, 0.9])
end

次に気になるのは、このような部分だ。

モンスターHP = 30
モンスター攻撃力 = 10
モンスター防御力 = 10
プレイヤーHP = 30
プレイヤー攻撃力 = 10
プレイヤー防御力 = 10

本来はモンスター/プレイヤーの2パターンと、それぞれが持つHP/攻撃力/防御力の3パターンだ。その2 * 3 = 6パターンが、並列に記述されている。もっとうまいやり方はないだろうか?

これを実現するには構造体というものを使うのだが、・・・長くなってきたし、キリもいいので次にしよう。

その2の終わりに

さて、ここまでで、for文、関数、if文、while文の説明をしてきた。これらは、プログラミングの基本構文にあたる。ここまでの知識は、ほとんどのプログラミング言語で応用できる。なんと、ここまでの知識の内容があれば、あらゆるプログラムを作ることができるという定理まであるのだ。(実際には、for文はwhile文で書き換えられるので、for文すら「原理的には」不要と言える)

https://ja.wikipedia.org/wiki/構造化定理

次回からは、だんだんと応用的な内容になっていく。ここまでの知識でも、十分高度なプログラムを作ることはできる。ここから先の内容は、ある意味、不要なものかもしれない。しかし、逆に考えれば、不要であるにもかかわらず、あえて作られたことには意味があるはずだ。

今のプログラムはまだ大したことは何もしていない。にもかかわらず、既に少しごちゃごちゃし、見づらくなってきている。プログラミングは、自分が作り上げた複雑さをいかに制御するかが大きな問題となる。複雑さに飲み込まれると、機能を追加する時にどこを変更すればいいかわからず、そして変更のたびに不具合を生んでしまう、モンスタープログラムが出来上がってしまう。それはとても悲しいことだ。

Juliaには(そして他の言語にも)、そのような複雑さを制御するための素敵な機能が提供されている。少しずつ学んでいき、より良いプログラミングライフを送っていこう。

練習問題の解答例

最後に、練習問題の解答例を載せておこう。あくまで一例だ。

if文

  • 問題1
    • 引数が負の数かどうかを判定する関数を作ろう。引数が負の数ならtrue、0以上の正の数ならfalseを返す。引数は1つだけ、数値と仮定していい。
  • 回答
function 負の数(x)
    if x < 0
        return true
    else
        return false
    end
end
  • 問題2
    • 引数の絶対値を返す関数を作ってみよう。if文の条件式の中で、問題1で作った関数を呼び出すようにしよう。引数は1つだけ、数値と仮定していい。
  • 回答
function 絶対値(x)
    if 負の数(x)
        return -x
    else
        return x
    end
end
  • 問題3
    • 引数が3の倍数だと”Fizz”、5の倍数だと”Buzz”、15の倍数だと”FizzBuzz”、という文字列を返すfizzbuzz関数を作ろう。それ以外は空文字を返す。これは「FizzBuzz問題」と呼ばれる有名な問題だ。引数は1つだけ、正の整数と仮定していい。%という演算子を使おう。n%mでnをmで割ったあまりとなる。10%3 == 1だ。
  • 回答
function fizzbuzz(x)
    if x%15 == 0
      return "FizzBuzz"
    elseif x%3 == 0
      return "Fizz"
    elseif x%5 == 0
      return "Buzz"
    else
      return ""
    end
end

続きの記事

次の記事

Julia言語で入門するプログラミング