書けるようにならないC言語入門
crawlのソースがなんとなく読めるようになる程度を目標にしたC言語入門です。
よく話題になっている攻撃力の計算法を通してC言語を少しだけ学び 自力でスポイラー並みの情報を手に入れられるようにすることを目標にします。
ここでは、STR,DEXが40,10でスキルが全部27の丘ドワーフ斧使い、処刑人の斧(+9,+9)のダメージを計算します。
ソースの入手
関数
とりあえず、fight.ccの129行目を見ましょう。
void you_attack(int monster_attacked, bool unarmed_attacks)
{
とあります。これは「関数」の開始を表しています。関数とは手続きの集まりのことです。関数は「呼ばれる」とその集まっている手続きが順番に実行されます。「手続き」とは何かというのは難しいですが、関数を呼ぶことやHPを減らしたり殴る強さを計算したりそういったこと全部です。この関数は名前からもわかるように「あなたが攻撃した」ときに呼ばれます。
つぎに、後ろの(int monster_attacked, bool unarmed_attacks)の意味はなんでしょう。これは「引数」と呼ばれます。引数は二つ monster_attacked と unarmed_attacks です。何のためにあるかというと、十二番目の敵を殴ったか八番目の敵を殴ったか、追加攻撃を出すか否かを指定するためにあります。0番目の敵を殴る関数、1番目の敵を殴る関数…と作っていたらやってられませんね。そのために you_attack(12, true); とすれば、12番目の敵を追加攻撃可能で殴る手続きが行われるのです。
int と前に書いてありますが、これはなんでしょうか。これは monster_attacked の引数の型です。0.5772番目の敵を殴ってくれとかいわれても困るので、整数じゃないと駄目だよと言っています。bool は true か false だけだよということです。
さて、残った void とはなんでしょう。これは「帰り値の型」です。you_attack を呼んだ別の関数は結果がどうだったかを知りたいかもしれませんね。そのときに、例えば、当たったかどうかを知りたいのに、2.718だったよとかいわれても困ります。ここでは呼んだ人に何も教える気はないので何も教える気がないことを示す「void 型」にしています。
もう一度みてみましょう。
void you_attack(int monster_attacked, bool unarmed_attacks)
{
これはyou_attackという名前の関数がここからはじまることを示しています。この関数は引数を二つ取り、それは monster_attacked と unarmed_attacks です。それぞれ int(整数)型と bool(真偽)型です。そして、you_attackを呼んだ関数には何も返しません。
変数の宣言
では、次の行。
struct monsters *defender = &menv[monster_attacked];
int your_to_hit;
int damage_done = 0;
bool hit = false;
unsigned char stab_bonus = 0; // this is never negative {dlb}
int temp_rand; // for probability determination {dlb}
一つ目の行はおいておきましょう。133行目"int your_to_hit;"は「変数の宣言」です。your_to_hit という名前の「int型の変数」を作りました。int型の物を入れる容器だと思って置いてください。その下の行はほとんど同じですが、"= 0"がついてます。この = は「代入」しています。右側にあるものを左側に入れています。つまり、まとめると、damage_done という名前の「int型の変数」を作りそこに0を入れました。
次は型が違いますが、一緒です。bool型の変数 hit に false を入れて宣言。unsigned char型(0~511の整数型)を宣言しています。 一つ目の行にもどると、"struct monsters *"と*がついています。これを「struct monsters 型」への「ポインタ」変数と呼びます。この変数は数字のようなものではなくて別の変数への矢印です。特に気にしなくてOK。&menv[monster_attacked]; を代入していますが、ようするに攻撃を受けるモンスターの情報の入っている場所です。C上の意味は多分しばらくしたら分かります。
さて、我々は受けた攻撃力が気になるので damage_done 変数を追いましょう。検索すると694行目で
damage_done = random2(damage);
とでてきます。「=」は damage_done に右辺の値を働きをしました。ですから、これは random2(damage) を左辺に代入しています。random2 は stuff.cc に定義されていますが、引数の damage を受け取って、0~(damage-1) を返します。 stuff.cc を見ると
int random2(int max)
とあり、関数の宣言の話の通りですね。 とにかく、damageを制するものは、damage_doneを制することが分かりました。412行目を見ると
int damage = 1;
とあります。どうやらここが damage の始まりのようです。
if~条件分岐
その次の命令を見ましょう。
if (!ur_armed) // empty-handed
{
damage = 3;
とあります。これはif文です。if文は後ろの括弧の中身が0でないときにその続きの { から対応する } までを実行します。!は否定です。 ここで、ほかに条件判定ででてくる演算子。
A == B :右辺と左辺が等しい時に、1。それ以外は0。
A || B :A と B が片方0でない時に1。または。
A && B :A と B が両方0でない時に1。かつ。
つまり、ur_armedが0であるとき、武器を装備していない時に {~} が実行されます。というわけで、斧を装備しているので関係ありませんね。というわけで対応する } まで飛ぶと480行目から、
}
else
{
if (you.inv[ weapon ].base_type == OBJ_WEAPONS
|| item_is_staff( you.inv[ weapon ] ))
{
damage = property( you.inv[ weapon ], PWPN_DAMAGE );
}
}
とあります。この else というのは前の {} が実行されなかったときだけ、次のブロックを実行しろということです。今回、前の415行目から480行目は実行されなかったので代わりに、482から488までを実行します。
構造体
さて、中を見ると.というものがでてきました。これは「構造体」の「メンバー」へのアクセスを意味します。「構造体」というのはいくつかの変数をまとめて一つにしたものです。そのまとめられた変数の一つ一つがメンバーです。you という構造体変数のなかの inv は所持品の「配列」を意味しています。
配列というのは、変数が行列しているようなものです。inv[7]とやるとinvという変数の行列の7番目の変数のことになります。ここでは weapon に装備しているものが何番目にいるかが入っているので you.inv[ weapon ] で装備しているもののことが分かります。さらに、こいつも構造体なのでそのなかの base_type へアクセスしてます。you 構造体に他にどういうメンバーがいるかを調べるには externs.h の struct player を見れば分かります。
結局、あなたの装備しているものの基本種類が武器である、または、あなたが装備しているものが杖である、ならば485~487を実行しろとなります。 いま、処刑人の斧(+9,+9)を握っているので当然実行されて、damageには property( you.inv[ weapon ], PWPN_DAMAGE ) つまり、処刑人の斧のダメージ20が入ります。
代入
次に、damageがでてくるのは660行目。少しもどると、
if ((your_to_hit >= defender->evasion || one_chance_in(30))
|| ((defender->speed_increment <= 60
|| defender->behaviour == BEH_SLEEP)
&& !one_chance_in(10 + you.skills[SK_STABBING])))
{
hit = true;
int dammod = 78;
const int dam_stat_val = calc_stat_to_dam_base();
if (dam_stat_val > 11)
dammod += (random2(dam_stat_val - 11) * 2);
else if (dam_stat_val < 9)
dammod -= (random2(9 - dam_stat_val) * 3);
damage *= dammod; //random2(you.strength);
damage /= 78;
とあります。ifの中身は攻撃が当たったかですね。 calc_stat_to_dam_base()は3963行目を見れば分かるのでいいでしょう。検索機能を使えばそのなかの関数も分かります。weapon_skill, hands_reqd_for_weapon は wpn-misc.cc にあります。
ここでは新しく出てきたのは *=,/=,-=,+= です。これらはほとんど一緒です。damage /= 78; というのは、damage = damage / 78;という意味です。つまり、左辺の値を78で割った値をdamageに代入せよ、ということになります。*はかけるです。縦棒が一本多いですが気にせずに。 const static inline といったものがでてきましたが、これらは、値が変化しない・他のファイルから呼ばれない・高速化のために展開する、という意味ですが、読む分にはいらないでしょう。
if (water_attack)
damage += random2avg(10,2);
敵が水にいる場合なので気にしないことにしましょう。
プリプロセッサ
#if DEBUG_DIAGNOSTICS
const int str_damage = damage;
#endif
というよく分からないのがでてきました。これはソースからプログラムを作るときに、DEBUG_DIAGNOSTICS というスイッチが押されていると、この#ifから#endifまでが「ある」ことになります。押されていないと削除されたのと同じ状態になります。DEBUG_DIAGNOSTICS の意味を辞書で調べると、虫取り診断学となり、デバッグのときにメッセージを出力するためにあることが分かります。
damage += slaying_bonus(PWPN_DAMAGE);
player.cc を見ると slaying_bonus(PWPN_DAMAGE) は指輪等の殺戮補正の和を返してくることが分かります。
これでようやく、
damage_done = random2(damage);
にもどってきました。damage の半分が damage_done ですね。ここからは damage は忘れて、重要な damage_done の方を追いましょう。
if (ur_armed && (you.inv[ weapon ].base_type == OBJ_WEAPONS
|| item_is_staff( you.inv[ weapon ] )))
{
damage_done *= 25 + (random2( you.skills[ wpn_skill ] + 1 ));
damage_done /= 25;
}
これはいままでの説明でいきますね。 武器を装備しているならば、you.skills[ wpn_skill ] (これは斧スキルの値) 以下のランダムな自然数(当然0を含む。笑。)をかけて25を足す。damage_done に先の値をかけて25で割る。 期待値としては大体1.6倍になることが分かります。
damage_done *= 30 + (random2(you.skills[SK_FIGHTING] + 1));
damage_done /= 30;
似たような感じ。
if (you.might > 1)
damage_done += 1 + random2(10);
腕力強化中の追加ダメージ。
if (you.hunger_state == HS_STARVING)
damage_done -= random2(5);
飢えてると弱くなる。
以下、読めると思うのでしばらく省略します。891行目で
if (hurt_monster(defender, damage_done))
ダメージを与えました。
グローバル変数と標準関数
当たっているときの処理がまた始まります。
if (hit && damage_done > 0
|| (hit && damage_done < 1 && mons_has_ench(defender,ENCH_INVIS)))
{
strcpy(info, "You ");
strcat(info, damage_noise);
strcat(info, " ");
strcat(info, ptr_monam(defender, DESC_NOCAP_THE));
strcat(info, damage_noise2);
strcpy はソース探してもなかった!これはコンパイラの作成者があらかじめ用意してくれている標準関数です。info に "You " という文字列を書き込みます。strcat は文字列を追加します。だから結局、info に damage_noise が追加されます。
info が宣言されてないのに使われてる? これはグローバル変数です。acr.cc の中で宣言されています。
damage_done は damage_done が宣言されている中括弧の中でしか使えません。これを「ローカル」変数と呼びます。しかし、 info は「グローバル」変数で何処からでも使えます。気にしない方向で。
mpr(info);
この関数はメッセージを表示します。
1030行目から
if (hit)
{
武器追加ダメージ等の処理を始めます。
1106行目
// magic staves have their own special damage
if (ur_armed && item_is_staff( you.inv[weapon] ))
specdam = 0;
if (you.magic_points >= STAFF_COST
&& random2(20) <= you.skills[SK_EVOCATIONS])
{
杖による追加攻撃の話ですね。発動スキルが20ならば必ず追加ダメージが入ることが分かります。しかし、弱すぎます。2マナ払ってるのだから威力を一桁増やして欲しいですね。
switch~どれにしようかな
1252行目から
switch (melee_brand)
{
case SPWPN_NORMAL:
break;
case SPWPN_FLAMING:
<中略>
case SPWPN_VORPAL:
specdam = 1 + random2(damage_done) / 2;
break;
<中略>
} /* end switch */
switch という斬新なものがでてきましたね。これはなんでしょう。
これは switch-case文です。後ろの()の中身で分岐します。melee_brand の値は武器の魔法属性から決まります。いま、切断なので、これは SPWN_VORPAL になっています。そうすると、case SPWPN_VORPAL: の行に飛びます。その後ろに break; とありますね。これは switch 文を抜けるという意味です。break文が実行されるとswitchの後ろである1505行目まで移動します。
では、この break; がないとどうなるのでしょうか。そのまま次へ行きます。1255行目の break; がないと無属性の武器でも火炎属性武器の命令が実行されてしまいますので、うれしくなってしまいます。 これで、平均25%の追加ダメージが入ることが分かりました。
それに対して、822行目の
不意打ちの switch を見ると break がありません。
switch (wpn_skill)
{
case SK_SHORT_BLADES:
{
int bonus = (you.dex * (you.skills[SK_STABBING] + 1)) / 5;
if (you.inv[ weapon ].sub_type != WPN_DAGGER)
bonus /= 2;
bonus = stepdown_value( bonus, 10, 10, 30, 30 );
damage_done += bonus;
}
// fall through
case SK_LONG_SWORDS:
damage_done *= 10 + you.skills[SK_STABBING] /
(stab_bonus + (wpn_skill == SK_SHORT_BLADES ? 0 : 1));
damage_done /= 10;
// fall through
default:
damage_done *= 12 + you.skills[SK_STABBING] / stab_bonus;
damage_done /= 12;
}
これは、長剣の時には、他の武器に加えてダメージが大きくなり、短剣だとさらにおまけしちゃうということになっているわけですね。 おっと忘れてました。この default: というのは、case XXX: のどれにも当てはまらない時に飛びます。省略すると、どれにもあてはまらなかった時に switch文の最後に飛びます。
これで、今回のダメージ計算は完了しましたが、ソースの世界を探求するついでに知っておいたほうがいい命令をいくつか。
goto~行く
1091行目
goto mons_dies;
mons_diesと書かれた行に行きます。 1518行目
mons_dies:
ここに来ます。
for~何回も
167行目
for (int i = 0; i < RA_PROPERTIES; i++)
art_proprt[i] = 0;
これはなんでしょう。for(A;B;C){D} というのは、
0. Aを実行
1. Bでなかったら for文を終わる。
2. Dする。
3. Cをして、1番に行く。
ということです。 だから、この場合、iを0にする。i が RA_PROPERTIES より小さいなら、art_proprt という配列の i番目を0にする。iを一つ増やす。i が RA_PROPERTIES より小さいなら、art_proprt という配列の i番目を0にする。iを一つ増やす。 …i が RA_PROPERTIES より小さくなくなったからやめる。となるので、RA_PROPERTIES より小さい i に関して art_proprtの i番目がすべて0になります。 また、Dが一文だけの時は、{}を(本当は省略ではないのだが)省略することができます。
continue
上のDの中にたまに continue; という命令が混ざっていることがあります。これは2.をやめて、3.に行くことを意味しています。また、for の中にある break は for文を終わることを意味しています。
終わりに
これだけ分かればソースを大体読めるでしょう。あとは読みながら命令を調べていけば大丈夫です。 更なる rougelike界の発展を祈って。
最終更新:2023年03月11日 18:26