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

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


「Julia言語で入門するプログラミング」第11回である。未読の方は第1回〜第10回を読んで欲しい。

一覧はこちら

「刃に毒を塗る」の残りの実装

前回の続きで、「刃に毒を塗る」の実装である。25%の確率で毒にするところからである。ダメージが0でない時に状態異常を付与する判定をするようにしよう。

まずはキャラクターが状態異常を保持できるようにしよう。

#Tキャラクター.jl
mutable struct Tキャラクター共通データ
    ...
    防御力
    状態異常s #追加
    物理攻撃時状態異常付与確率
    ...
    Tキャラクター共通データ(名前, HP, MP, 攻撃力, 防御力, スキルs) = begin
        ...
        new(..., 防御力, Set(), Dict()...)
    end
end

状態異常s」という名前で保持することにした。ここで「Set」というものが出てきた。これは数学でいうところの「集合」を表現するデータ構造だ。配列のように複数の要素を保持できるが、重複したデータは存在しない。あるキャラクターが毒状態のときに重ねて毒攻撃を受けても、状態異常sは毒を一つ持つだけだ。

ダメージを与えた時に毒状態にするためのテストを先に書いておこう。

#game_test.jl
@testset "刃に毒を塗ってから通常攻撃実行" begin
    p = createプレイヤー()
    刃に毒を塗る = T行動(createスキル(:刃に毒を塗る), p, p)
    行動実行!(刃に毒を塗る, get乱数生成器())

    乱数生成器 = get乱数生成器stub()
    for i in 1:3 #25%の確率で状態異常付与
        m = createモンスター()
        プレイヤーからモンスターへ攻撃 = T行動(T通常攻撃(), p, m)
        行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
        @test :毒 in m.状態異常s
    end
    for i in 1:7 #75%の確率で外れる
        m = createモンスター()
        プレイヤーからモンスターへ攻撃 = T行動(T通常攻撃(), p, m)
        行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
        @test isempty(m.状態異常s)
    end    
end

このテストを通すべく、次のように実装しよう。

#戦闘.jl
function 攻撃実行!(攻撃者, 防御者, コマンド::T通常攻撃, 乱数生成器)
    ...
    HP減少!(防御者, 防御者ダメージ)
    if 防御者ダメージ > 0
        状態異常付与判定!(攻撃者, 防御者, 乱数生成器)
    end
end

function 状態異常付与判定!(攻撃者, 防御者, 乱数生成器)
    状態異常付与確率 = 攻撃者.物理攻撃時状態異常付与確率
    for (状態異常, 付与確率) in 状態異常付与確率
        乱数 = 乱数生成器()
        if 乱数 < 付与確率
            状態異常付与!(防御者, 状態異常)
        end
    end
end

function 状態異常付与!(対象者, 状態異常)
    push!(対象者.状態異常s, 状態異常)
end

これでテストが通るはずだ。特に難しいところはないはずだ。

ダメージ0の時に状態異常を付与できないテストケースも追加しておこう。

@testset "刃に毒を塗ってから通常攻撃実行するもダメージ0" begin
    p = createプレイヤー(攻撃力=0)
    刃に毒を塗る = T行動(createスキル(:刃に毒を塗る), p, p)
    行動実行!(刃に毒を塗る, get乱数生成器())

    乱数生成器 = get乱数生成器stub()
    for i in 1:10 #ダメージ0なので毒にできない
        m = createモンスター()
        プレイヤーからモンスターへ攻撃 = T行動(T通常攻撃(), p, m)
        行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
        @test isempty(m.状態異常s)
    end
end

このテストは通るはずだ。

通常攻撃だけでなく、スキル攻撃にも対応しておこう。

@testset "刃に毒を塗ってからスキル攻撃実行" begin
    p = createプレイヤー()
    刃に毒を塗る = T行動(createスキル(:刃に毒を塗る), p, p)
    行動実行!(刃に毒を塗る, get乱数生成器())

    乱数生成器 = get乱数生成器stub()
    m = createモンスター()
    プレイヤーからモンスターへ攻撃 = T行動(createスキル(:連続攻撃), p, m)
    行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
    @test :毒 in m.状態異常s
end

通常攻撃の時と違い、テストはシンプルにしてある。毒の確率が25%だから、とループを回したりはしてない。これは、スキル攻撃の中でも乱数を使っているためである。通常攻撃の時には、毒の判定だけで乱数を使っていたため、攻撃の度に乱数スタブが0, 0.1, 0.2, …と消費されていくのが明確なのだが、スキル攻撃の時にはもっとややこしくなってしまう。本質的には、テストコード側では乱数そのものというよりは、判定結果をコントロールしたいだけなのに、現状乱数の生成の仕方を細かくコントロールしすぎているという問題があるので、ここは別途改良しよう。

スキル攻撃の時にも、通常攻撃と同様に状態異常判定を組み込むようにする。

function 攻撃実行!(攻撃者, 防御者, スキル::Tスキル, 乱数生成器)
            ...
            HP減少!(防御者, 防御者ダメージ)
            if 防御者ダメージ > 0
                状態異常付与判定!(攻撃者, 防御者, 乱数生成器)
            end
            ...
end

あとは、相手を毒状態にしたというメッセージを表示したい。これはお決まりの方法だ。特には解説不要だろう。

#戦闘.jl
function 状態異常付与!(対象者, 状態異常)
    ...
    状態異常付与イベント通知!(対象者, 状態異常) #追加
end

#Tキャラクター.jl
mutable struct Tキャラクター共通データ
    ...
    状態異常付与イベントリスナーs #追加
    ...
    Tキャラクター共通データ(名前, HP, MP, 攻撃力, 防御力, スキルs) = begin
        ...
        new(..., [状態異常付与ui処理!], ...)
    end
end

#キャラクター.jl
function 状態異常付与イベント通知!(対象者, 状態異常)
    for リスナー in 対象者.状態異常付与イベントリスナーs
        リスナー(対象者, 状態異常)
    end
end

#ui.jl
function 状態異常付与ui処理!(対象者, 状態異常)
    println("$(対象者.名前)は$(状態異常)状態になった!")
end

#ui_stub.jl
function 状態異常付与ui処理!(対象者, 状態異常) end

さて、動かしてみるとわかるが、

  • 刃に毒を塗ったあと、本当に毒を付与できる状態になっているのか?
  • 毒を付与されたモンスターが毒状態になっているのか?

というあたりがわかりづらいので、これらの情報も画面に表示されるようにしよう。戦況表示関数を修正する。これも特段難しいところはない。

#ui.jl
function 戦況表示(プレイヤーs, モンスターs)
    function 表示(c::Tキャラクター)
        s = "$(c.名前) HP:$(c.HP) MP:$(c.MP)"

        状態異常s = c.状態異常s
        if length(状態異常s) > 0
            s *= " " * join(["$(j)" for j in 状態異常s], " ")
        end

        付与s = keys(c.物理攻撃時状態異常付与確率)
        if length(付与s) > 0
            s *= " " * join(["$(f)付与" for f in 付与s], " ")
        end
        return s
    end

    結果 = ["*****プレイヤー*****", 
            join([表示(p) for p in プレイヤーs], "\n"), 
            "*****モンスター*****", 
            join([表示(m) for m in モンスターs], "\n"), 
            "********************"]

    return join(結果, "\n")
end

次のようになる。

*****プレイヤー*****
太郎 HP:100 MP:5 毒付与
花子 HP:40 MP:0
遠藤君 HP:30 MP:20
高橋先生 HP:100 MP:0
*****モンスター*****
ドラゴン HP:280 MP:70 毒
********************

「一度相手を毒状態にすると毒を与える効果が切れる」の実装

これも特に難しくはない。状態異常の付与に成功したら、物理攻撃時状態異常付与確率の辞書から消してしまおう。辞書から指定されたキーの値を消すにはdelete!を使えばいい。

#戦闘.jl
function 状態異常付与判定!(攻撃者, 防御者, 乱数生成器)
        ...
        if 乱数 < 付与確率
            状態異常付与!(防御者, 状態異常)
            物理攻撃時状態異常付与解除!(攻撃者, 状態異常) #追加
        end
        ...
end

function 物理攻撃時状態異常付与解除!(攻撃者, 状態異常)
    delete!(攻撃者.物理攻撃時状態異常付与確率, 状態異常)
end

簡単だった。

しかし、この後にテストを実行すると失敗することに気づくだろう。これは、毒の付与確率が25%であることのテストをするために、繰り返しで実行していた部分が壊れてしまうからである。あまり筋のいいテストの書き方ではなかった。

乱数生成スタブの種類追加

今の乱数生成スタブは、0, 0.1, 0.2, …と繰り返すというものだが、もっと柔軟なものを用意するようにしよう。get乱数生成stubに配列を引数として渡せるようにして、その配列が乱数値として順番に表示されるようなものにしたい。次のようなものになる。

#乱数.jl
function get乱数生成器stub(指定数列)
    if length(指定数列) == 0
        throw(DomainError("指定数列として1つ以上の要素を含むようにしてください"))
    end
    function 数列生成(c::Channel)
        i = 1
        while (true)
            put!(c, 指定数列[i])
            i += 1
            if length(指定数列) < i
                i = 1
            end
        end
    end
    chnl = Channel(数列生成);
    
    return function exec()
        return take!(chnl)
    end
end

次のように使う。

julia> 乱数生成器 = get乱数生成器stub([0.0, 0.1, 0.2, 0.3])
julia> 乱数生成器()
0.0
julia> 乱数生成器()
0.1
julia> 乱数生成器()
0.2
julia> 乱数生成器()
0.3
julia> 乱数生成器()
0.0

これを使ってテストコードを書き直す。25%の確率で成功することをテストしたい。0.2を返すスタブであれば成功し、0.3を返すスタブであれば失敗するはずだ。次のようなテストに変更した。重複が気になるかもしれないが、私はこれでいいと思っている。何度か書いている気もするが、テストコードは明快さが重要だからだ。多少の保守性の悪さは目を瞑っても良い。

@testset "刃に毒を塗ってから通常攻撃実行" begin
    @testset "成功" begin
        p = createプレイヤー()
        刃に毒を塗る = T行動(createスキル(:刃に毒を塗る), p, p)
        行動実行!(刃に毒を塗る, get乱数生成器())

        m = createモンスター()
        プレイヤーからモンスターへ攻撃 = T行動(T通常攻撃(), p, m)
        乱数生成器 = get乱数生成器stub([0.2]) #25%の確率で状態異常付与なので成功する想定
        行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
        @test :毒 in m.状態異常s
    end

    @testset "失敗" begin
        p = createプレイヤー()
        刃に毒を塗る = T行動(createスキル(:刃に毒を塗る), p, p)
        行動実行!(刃に毒を塗る, get乱数生成器())

        m = createモンスター()
        プレイヤーからモンスターへ攻撃 = T行動(T通常攻撃(), p, m)
        乱数生成器 = get乱数生成器stub([0.3]) #25%の確率で状態異常付与なので失敗する想定
        行動実行!(プレイヤーからモンスターへ攻撃, 乱数生成器)
        @test isempty(m.状態異常s)
    end
end

これでテストは通るはずだ。さらに、もともとのget乱数生成stubの引数なしバージョンは、今回作った引数ありバージョンの引数に[0.0, 0.1, ..., 0.9]を指定したものと同じである。今はそれぞれで似たような実装を抱えているが、引数なしバージョンは引数ありバージョンを呼び出すように変更しよう。

function get乱数生成器stub()
    return get乱数生成器stub([0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9])
end

問題なく動くはずだ。

julia> 乱数生成器 = get乱数生成器stub()
julia> 乱数生成器()
0.0
julia> 乱数生成器()
0.1
julia> 乱数生成器()
0.2

「毒状態のキャラクターは1ターンごとにダメージを受ける」の実装

毒状態の真髄、毎ターンのダメージ計算処理だ。もちろん次の仕様も入れたい。

  • 雑魚敵は1ターンにつき最大HPの50%のダメージ
  • ボス敵は1ターンにつき最大HPの20%のダメージ

順序としては、まずはダメージ計算処理を実装し、次にボス敵と雑魚敵のところを実装しよう。

まずはテストから作ろう。モンスターを毒状態にして、行動後処理!が呼ばれると20%のダメージを受けるというシナリオだ。

#game_test.jl
@testset "毒ダメージ" begin
    @testset "20%ダメージ" begin
        m = createモンスター(HP=100)
        状態異常付与!(m, :毒)
        行動後処理!(m, nothing, nothing)
        @test m.HP == 100 - 20
    end
end 

まず、行動後処理!という処理が、何らかの行動を行った後に呼ばれるようにしよう。引数や内容は行動前処理!に似せている。

#戦闘.jl
function ゲームループ(プレイヤーs, モンスターs, 乱数生成器)
                ...
                行動前処理!(行動者, プレイヤーs, モンスターs)
                行動 = 行動決定(行動者, プレイヤーs, モンスターs)
                行動実行!(行動, 乱数生成器)
                行動後処理!(行動者, プレイヤーs, モンスターs) #追加
                if is戦闘終了(プレイヤーs, モンスターs)
                    return
                end
                ...
end

function 行動後処理!(行動者::Tキャラクター, プレイヤーs, モンスターs)
    行動後処理イベント通知!(行動者)
end

function 行動後処理イベント通知!(行動者::Tキャラクター)
    for リスナー in 行動者.行動後処理イベントリスナーs
        リスナー(行動者)
    end
end

行動後処理!で毒のダメージが計算されるように、毒ダメージの計算処理を作り、それをイベントリスナーとして登録する。さらに、毒ダメージが発生したことも画面上に表示したいので、毒ダメージ発生のイベントも定義している。

#スキル.jl
function 毒ダメージ発生!(対象者)
    if :毒 in 対象者.状態異常s
        毒ダメージ発生イベント通知!(対象者)
        毒ダメージ = round(Int, 対象者.最大HP * 0.2, RoundDown) 
        HP減少!(対象者, 毒ダメージ)
    end
end

#Tキャラクター.jl
mutable struct Tキャラクター共通データ
    ...
    行動前処理イベントリスナーs
    行動後処理イベントリスナーs #追加
    戦闘不能イベントリスナーs
    ...
    毒ダメージ発生イベントリスナーs #追加
    Tキャラクター共通データ(名前, HP, MP, 攻撃力, 防御力, スキルs) = begin
        new(...,  
            [getかばう解除!(:行動前処理)], 
            [毒ダメージ発生!], #追加
            [getかばう解除!(:戦闘不能)], 
            ...,
            [毒ダメージ発生ui処理!]) #追加
    end
end

#キャラクター.jl
function 毒ダメージ発生イベント通知!(対象者)
    for リスナー in 対象者.毒ダメージ発生イベントリスナーs
        リスナー(対象者)
    end
end

#ui.jl
function 毒ダメージ発生ui処理!(対象者)
    println("$(対象者.名前)は毒に苦しんでいる!")
end

#ui_stub.jl
function 毒ダメージ発生ui処理!(対象者) end

ごちゃごちゃ書いたが、特に目新しいことはない。これまでと同じような流れだ。テストが通ることを確認し、画面からも動作確認をしておこう。

ドラゴンの攻撃!
高橋先生は40のダメージを受けた!
高橋先生の残りHP:20
ドラゴンは毒に苦しんでいる!
ドラゴンは80のダメージを受けた!
ドラゴンの残りHP:45

雑魚敵とボス敵の区別

いよいよ仕上げだ。ボス敵は1ターンごとに20%のダメージ、雑魚敵は1ターンごとに50%のダメージとなるようにしよう。モンスターを作る際に、ボスかどうかを指定できるようにする。テストコードは次のようになるだろう。2ケースあるうちの一つは先程のテストケースを少しいじったもの、もう一つは新しいテストケースだ。このテストが通るように実装していこう。

#game_test.jl
@testset "毒ダメージ" begin
    @testset "ボス敵:20%ダメージ" begin
        m = createモンスター(HP=100, isボス=true)
        状態異常付与!(m, :毒)
        行動後処理!(m, nothing, nothing)
        @test m.HP == 100 - 20
    end
    @testset "ザコ敵:50%ダメージ" begin
        m = createモンスター(HP=100, isボス=false)
        状態異常付与!(m, :毒)
        行動後処理!(m, nothing, nothing)
        @test m.HP == 100 - 50
    end
end 

まずは、モンスターがボスかどうかを指定できる必要がある。

#game_test.jl
function createモンスター(;名前="ドラゴン", HP=400, MP=80, 攻撃力=20, 防御力=10, スキルs=[], isボス=false)
    return Tモンスター(名前, HP, MP, 攻撃力, 防御力, スキルs, isボス)
end

もちろん、プロダクトコード側に修正が必要だ。構造体にisボスというフィールドを作る必要がある。ここで初めて、モンスター専用のフィールドが出てきた。これまでプレイヤーとモンスターは全く同じフィールドを使っていたが、ボスかどうか、というフィールドはモンスター専用なわけだ。ボスプレイヤーというものを作っても面白いかもしれないが、今はやらない。俺たちに上下関係なんてないんだ。

#Tキャラクター.jl
mutable struct Tモンスター <: Tキャラクター
    _キャラクター共通データ::Tキャラクター共通データ
    isボス #追加
end

#キャラクター.jl
function Tモンスター(名前, HP, MP, 攻撃力, 防御力, スキルs, isボス)
    return Tモンスター(Tキャラクター共通データ(名前, HP, MP, 攻撃力, 防御力, スキルs), isボス)    
end

新しく追加したフィールドを使って、毒ダメージの計算処理を変更しよう。最大HPに対して、ボス敵は20%、ザコ敵は50%のダメージだ。プレイヤー側はどうするべきか?決めていなかったが20%ということにしよう。プレイヤー側はザコ敵相手にはまず負けないし20連戦くらいこなせちゃうが、ボス敵とは死闘を繰り広げる、というくらいのバランス調整が普通だと思うので、どちらかといえばボス敵側だという判断だ。

#スキル.jl
include("Tキャラクター.jl")
...
function 毒ダメージ発生!(対象者)
    if :毒 in 対象者.状態異常s
        毒ダメージ発生イベント通知!(対象者)
        毒ダメージ = 毒ダメージ計算(対象者) #関数化した
        HP減少!(対象者, 毒ダメージ)
    end
end

function 毒ダメージ計算(対象者)
    function 係数(対象者::Tプレイヤー)
        return 0.2
    end

    function 係数(対象者::Tモンスター)
        if 対象者.isボス
            return 0.2
        else
            return 0.5
        end
    end

    round(Int, 対象者.最大HP * 係数(対象者), RoundDown) 
end

ドラゴンはボス敵と言う設定にしておこう。

#game.jl
function main(乱数生成器)
    モンスター = Tモンスター("ドラゴン", 400, 80, 40, 10, [createスキル(:連続攻撃)], true) #引数追加
    ...
end

if文が気になるかもしれない。ここはTボスモンスターという構造体を定義して、型によるディスパッチを行った方がいいのでは?次のようなイメージだ。

  • Tキャラクター
    • Tプレイヤー
    • Tモンスター
      • Tボスモンスター
      • Tザコモンスター

しかし、この方法は採らない。なぜかというと、モンスターには次のような別の階層構造を考えているからだ。

  • Tモンスター
    • Tドラゴン族
    • T猛獣族
    • Tゴーレム族

Juliaでは型の階層構造はピラミッド型に制限されており、複数の親を持つことはできない。ボスドラゴンはボスモンスター型であり、ドラゴン族型である、というふうにはできないのだ。

この場合どうすればいいかというと、第7回で説明したHoly Traitsパターンを使う。階層構造とは別に型の情報をつくり、それでディスパッチするのだ。[1] … Continue reading

ボスかザコか、という区別は「モンスターヒエラルキー」という名前にすることにしよう。新しくモンスターヒエラルキー.jlというファイルを作る。

#モンスターヒエラルキー.jl
struct Tボスモンスター end
struct Tザコモンスター end

function モンスターヒエラルキー(モンスター::Tモンスター) 
    if モンスター.isボス
        return Tボスモンスター()
    else
        return Tザコモンスター()
    end
end

モンスターヒエラルキーをつかうことで、係数を取得する関数はシンプルにできる。

#スキル.jl
function 毒ダメージ計算(対象者)
    係数(::Tプレイヤー) = 0.2
    係数(対象者::Tモンスター) = 係数(モンスターヒエラルキー(対象者))
    係数(::Tボスモンスター) = 0.2
    係数(::Tザコモンスター) = 0.5

    round(Int, 対象者.最大HP * 係数(対象者), RoundDown) 
end

if文がなくなり、1行の代入形式で表現できるようになったため、いくぶん宣言的になっている。

プレイヤー側のテストも作り、通ることを確認しておこう。

#game_test.jl
@testset "プレイヤー:20%ダメージ" begin
    p = createプレイヤー(HP=100)
    状態異常付与!(p, :毒)
    行動後処理!(p, nothing, nothing)
    @test p.HP == 100 - 20
end

よろしい、それでは最後に敵キャラクターを複数にして、ボスドラゴンとミニドラゴンという構成にしよう。

#game.jl
function main(乱数生成器)
    プレイヤー1 = Tプレイヤー("太郎", 100, 20, 10, 10, [createスキル(:連続攻撃), createスキル(:かばう), createスキル(:ヒール), createスキル(:刃に毒を塗る)])
    プレイヤー2 = Tプレイヤー("花子", 100, 20, 10, 10, [createスキル(:大振り), createスキル(:連続攻撃)])
    プレイヤー3 = Tプレイヤー("遠藤君", 100, 20, 10, 10, [createスキル(:大振り), createスキル(:かばう)])
    プレイヤー4 = Tプレイヤー("高橋先生", 100, 20, 10, 10, [createスキル(:ヒール), createスキル(:連続攻撃)])

    モンスター1 = Tモンスター("ボスドラゴン", 400, 80, 40, 10, [createスキル(:連続攻撃)], true)
    モンスター2 = Tモンスター("ミニドラゴン", 50, 10, 5, 10, [createスキル(:連続攻撃)], false)
    モンスター3 = Tモンスター("ミニドラゴン", 50, 10, 5, 10, [createスキル(:連続攻撃)], false)

    モンスター遭遇イベント通知!([モンスター遭遇イベントui処理!])
    
    プレイヤーs = [プレイヤー1, プレイヤー2, プレイヤー3, プレイヤー4]
    モンスターs = [モンスター1, モンスター2, モンスター3]

    ゲームループ(プレイヤーs, モンスターs, 乱数生成器)

    if is全滅(モンスターs)
        戦闘勝利イベント通知!([戦闘勝利イベントui処理!])
    else
        戦闘敗北イベント通知!([戦闘敗北イベントui処理!])
    end
end

無事戦闘が行えたと思う。

お疲れ!本当にお疲れ!コングラッチレーション!(まだまだ最終回じゃないよ!)

ふうー、なんとか「刃に毒を塗る」というスキルも実装し終えることができた。随分長いことかかってしまったが、太郎のスキルが一通り実装できたのは感無量だ。一区切りついたので、改めて当初計画していたスキルのリストを確認しておこう。

  • 太郎
    • 連続攻撃
      • 2〜5回の連続攻撃を行う。1回当たりのダメージは半減する。
    • かばう
      • 一定期間、指定した相手が受ける攻撃を代わりに受ける。
    • ヒール
      • HPを回復する。
    • 刃に毒を塗る
      • 攻撃がヒットしたら一定確率で毒を与えることができるようになる。
  • 花子
    • ファイア
      • 敵に炎属性の攻撃。
    • アイス
      • 敵に氷属性の攻撃。
    • ドレイン
      • 敵のHPを吸収する。
    • 集中
      • 魔法攻撃の威力を上昇させる。
  • 遠藤君
    • 大振り
      • 命中率は低いが通常の2倍の威力の攻撃を行う。
    • かばう
      • 一定期間、指定した相手が受ける攻撃を代わりに受ける。
    • 捨て身
      • 自分のHPと引き換えに相手に大ダメージを与える
  • 高橋先生
    • ヒール
      • HPを回復する。
    • バリア
      • 指定した相手が受けるダメージを軽減させる。
    • 濃霧
      • 一定期間敵味方全ての攻撃の命中率を下げる。
    • 金縛り
      • 指定した相手を一定期間行動不能にする。

次は花子のスキルの実装だ。ここでの目玉は「魔法」である。魔法攻撃力や魔法防御力といったパラメータを作ったり、属性攻撃という概念を作ったりする必要がある。それなりに骨が折れそうだが、この辺りまで実装すれば、基本的なRPGゲームの戦闘システムとしては最低ラインに達したといえるのではないだろうか?あとは威力なんかを調整して種類を増やせばいい。

その後の遠藤君は実装済みのスキルが多いこともあり、あっさり終わりそうだ。高橋先生のスキルは特殊系のものが多いのでややこしいかもしれない。

それが終わったら、敵キャラクターのバリエーションを増やしたい。こんな敵キャラクターを想定していたのだ。

  • ドラゴン
    • 口から炎を吐き、鋭い爪と固い鱗を持つ強力なモンスターだ。爬虫類なので体温が下がると活動が鈍くなるぞ。
  • ケルベロス
    • 頭が3つある魔犬。普段は地獄の番犬をしている。弱ると遠吠えで仲間を呼ぶので、ある程度ダメージを与えたら一気に倒してしまうべし。
  • 青銅魔人
    • 魔法の力で動く金属の巨大人形。とにかく頑丈なのでまともにダメージは与えられない。しばらくするとエネルギー切れで動かなくなるので、攻撃を受けないように時間稼ぎしよう。

それぞれ個性があるが、うまく型を分類できればあまり困難なく実装できるに違いない。

その後の展望は、これは完全に見通しなく語っているので実現できるか不明だが、以下のようなことを考えている。

  • レベルの概念の導入
    • 今は戦闘に勝ってもご褒美がないのが寂しい。経験値やレベルみたいなものがあるといいなと思う。ゴールドみたいなゲーム内通貨をご褒美にしないのは、アイテムや装備品みたいなところにまで手を出す必要があるので大変だからだ。
  • ファイルのセーブ・ロードの仕組み
    • レベルの概念まで入れ始めると、当然セーブをしたいという話になってくる。ここでファイルI/Oの機能について触れることになる。セーブデータの改造防止のための仕組みを入れたりするのも面白いかもしれない。
  • メタプログラミング
    • Juliaをやるからにはメタプログラミングを導入したいが、なかなかいい題材が出てきていないんだよなあ。下手に導入するとややこしくなるだけだし・・・。とはいえ、少しアイデアはある。それはイベント関連の処理だ。画面に何かを表示するたびに、構造体にフィールドを追加して、リスナーに登録して、イベントを発行し、リスナーに通知して、というのを繰り返している。こういうコーディングにおける繰り返し処理を解決するのがメタプログラミングなのだ。何かイベントを定義するだけで、このような決まり切った定型コードが自動で生成されたら嬉しいよね?というのを、メタプログラミング導入のとっかかりにできないかと考えている。
  • ユーザーによる拡張機構
    • いわゆるMODみたいな感じで、ユーザーが自作のモンスターやスキルを定義できて、それを取り込めるような仕組みを作りたい。そのためには、専用の「ドメイン特化言語」を作る必要があるかもしれない。そうなるともう私の能力を超えている気もするが、それはそれでやりがいのある話である。

次回の予定

とかなり夢みがちな将来の展望を語ったが、実は次回は他にもやりたいことがあるのだ。今の戦闘画面は、なんというかやはり見づらい。コンソールにダーっと流れていくだけなので、どこからどこまでが区切りかわかりづらいのだ。とはいえ、UIフレームワークなどを導入すると、フレームワークの使い方の勉強のようになってしまい本意ではないので、そこまでやるつもりはないのだが、固定された画面でHPの値やメッセージやだけが切り替わるくらいにはしたいと考えている。これでテストプレイも快適になるだろう。ついでの文字コードなんかについても話せたらいいなと思っている。

コード

今回のコードは以下のURLで確認できる。

https://github.com/muuumin-soft/julia-intro-prog/tree/main/rpg11

続き

第12回

References

References
1 なお、型ではなく値によってディスパッチするテクニックもあるようだが、いろいろ注意事項のあるやり方のようなので、公式ドキュメントの該当箇所を紹介するに留めておく。Value Type