ロボットを動かそう


移動命令 NaviCmd()

前節で、 ステップが一回進むと (STEP ボタンが一回押されると) Loop() の中身が一回実行されると話し、コード 5.5 でターン毎に「1 ターン進みました」と表示してみました。 ここにロボットを移動させる命令を書けばいいのです。 それを確認するのが次のコードです。

コード 6.1
void Loop(void){
  NaviCmd(1, 0);
}

1 ステップ毎に一歩前進するはずです。 マップはどれでもいいのですが、 悩んだのなら見晴らしのいい a25x70_level01.txt にしてください。

NaviCmd() は二つのパラメータを持ちます。 その組み合わせによりロボットを動かすことができます。 パラメータとロボットの対応は以下の通りです。

一つ目のパラメータが一つ前のコマに進むかどうか、 二つ目のパラメータは旋回するかどうか、するのならどっちの方角かを決めます。 例えば

コード 6.2
void Loop(void){
  NaviCmd(1, 1);
}

としてみましょう。 ロボットは次のような軌跡を描きます。

この動きを理解するためにはロボットの立場に立ってみる必要があります。 マップ a25x70_level01.txt ではロボットは最初真上を向いています。

NaviCmd(1, 1) を一回実行すると、前に進みつつ右回転するので次のようになります。 スタート地点は明るい黄色です。

ただこれは第三者の目線で俯瞰した状態です。 ロボットにとって前は前です。 上を「前」で表現すると、ロボットにとって スタート地点は次のように右後ろに見えるはずです。

もういっかい Navi(1,1) を実行すると横に見えるようになります。

これを繰り返すので六角形の軌跡を描くのです。

ステップ毎に異なる移動

さて次のような軌跡を描かせる方法を考えてみましょう。

  1. 前進 (1, 0)
  2. 右に旋回しつつ前進 (1, 1)
  3. 右に旋回しつつ前進 (1, 1)
  4. 前進 (1, 0)
  5. 右に旋回しつつ前進 (1, 1)
  6. 右に旋回しつつ前進 (1, 1)
  7. 前進 (1, 0)
  8. 右に旋回しつつ前進 (1, 1)

と動かせばよさそうです。 しかし

コード 6.3
void Loop(void){
  NaviCmd(1, 0);
  NaviCmd(1, 1);
  NaviCmd(1, 1);
  NaviCmd(1, 0);
  NaviCmd(1, 1);
  NaviCmd(1, 1);
  NaviCmd(1, 0);
  NaviCmd(1, 1);
}

と書いても動きません。 Loop() の中身はターン毎に実行されます。 NaviCmd() は最後の指令が生きるので、 これはコード 6.2 と同じ意味になります。

今何ターン目か数えて 1 ターン目なら前進、2 ターン目なら右旋回しつつ前進、 3 ターン目も右旋回しつつ前進、4 ターン目なら前進… とターンに応じて命令を変える 必要があります。 実現方法は幾つかありますが、一番簡単なのは if 文を使う方法でしょう。 C 言語の if 文は

if (条件式) 命令;

と書きます。「条件式」が成り立つ時だけ命令を実行するのです。 C 言語のコードは、改行とか空白とか意味を持ちません。 そのため次のように改行と字下げを入れて書くのが一般的です。

if (条件式)
  命令;

波括弧を使って次のように書く流儀もあります。

if (条件式) {
  命令;
}

実は波括弧を入れなくてはならない時と、入れなくてもいい (入れてもいい) 時とがあるのですが、とりあえず今はアヤフヤで話を進めます。

---

さて、今のターン数は gTurn という変数に入っています。 それを使って先のルールを書くと次のようなコードになるでしょう。

コード 6.4
void Loop(void){
  if (gTurn == 1)
    NaviCmd(1, 0);
  if (gTurn == 2)
    NaviCmd(1, 1);
  if (gTurn == 3)
    NaviCmd(1, 1);
  if (gTurn == 4)
    NaviCmd(1, 0);
  if (gTurn == 5)
    NaviCmd(1, 1);
  if (gTurn == 6)
    NaviCmd(1, 1);
  if (gTurn == 7)
    NaviCmd(1, 0);
  if (gTurn == 8)
    NaviCmd(1, 1);
}

等しいかどうかの判定はこのようにイコールを二個連続で書いて表現します。 一個ではありません。

さてこのコードですが、gTurn が 1 ならば最初の if 文の条件式が成り立って 3 行目の NaviCmd(1, 0) が実行されます。 gTurn が 1 ならそれは 2 でも 3 でも 4 でもありません。 しかしこのコードでは 1 だと分かったのに次の if 文で 2 か、その次の if 文で 3 かどうかを判定しています。 あまり美しくありません。 この表現は少し主観的と思うかもしれません。 正しくは効率が悪くコンピュータの能力を無駄遣いしているという表現が正しいかもしれませんが、人はエレガントな方法を美しいと思う性分ですのでこの表現を続けます。

if 文は必要ならば次のように else を追加拡張することができます。

if (条件式)
  命令A;
else
  命令B;

このフレーズでは条件式が成り立てば命令Aを実行するのは今までと同じです。 違うのは命令Bで、こちらは条件式が成り立たなければ実行します。 先のコード 6.4 も gTurn が 1 で無ければ 2 かどうか、 2 でも無ければ 3 かどうか、と成り立たない時だけ別の数かどうか調べた方が なんとなく美しいと思うのではないでしょうか。 この else フレーズを追加するとコード 6.4 は 6.5 のように表せます。 今の命令B部分に新しい if 文を追加しているのです。

コード 6.5
void Loop(void){
  if (gTurn == 1)
    NaviCmd(1, 0);
  else if (gTurn == 2)
    NaviCmd(1, 1);
  else if (gTurn == 3)
    NaviCmd(1, 1);
  else if (gTurn == 4)
    NaviCmd(1, 0);
  else if (gTurn == 5)
    NaviCmd(1, 1);
  else if (gTurn == 6)
    NaviCmd(1, 1);
  else if (gTurn == 7)
    NaviCmd(1, 0);
  else if (gTurn == 8)
    NaviCmd(1, 1);
}

さてここで 9 ターン目以降はどうなるでしょうか? 9 ターン目以降は何をするか書かれていません。 Loop() の中で NaviCmd() を通らないのです。 NaviCmd() が無い場合、前回の NaviCmd() の指令が踏襲されます。 8 ターン目の NaviCmd(1,1) です。 このため 8 ターン以降は右に旋回しつつ前進し続けるので軌跡は次のようになります。

これはなんか動きとして美しくありません。 そこでまずは 9 ターン目以降ロボットを停止させてみましょう。 次のように最後に else 節を追加してロボットを止めれば実現できます。

コード 6.6
void Loop(void){
  if (gTurn == 1)
    NaviCmd(1, 0);
  else if (gTurn == 2)
    NaviCmd(1, 1);
  else if (gTurn == 3)
    NaviCmd(1, 1);
  else if (gTurn == 4)
    NaviCmd(1, 0);
  else if (gTurn == 5)
    NaviCmd(1, 1);
  else if (gTurn == 6)
    NaviCmd(1, 1);
  else if (gTurn == 7)
    NaviCmd(1, 0);
  else if (gTurn == 8)
    NaviCmd(1, 1);
  else
    NaviCmd(0, 0);

}

では次に、9 ターン目以降も 1 ターン目と同じ軌跡を描かせたいと思います。

コード 6.7
void Loop(void){
  if (gTurn == 1)
    NaviCmd(1, 0);
  else if (gTurn == 2)
    NaviCmd(1, 1);
  else if (gTurn == 3)
    NaviCmd(1, 1);
  else if (gTurn == 4)
    NaviCmd(1, 0);
  else if (gTurn == 5)
    NaviCmd(1, 1);
  else if (gTurn == 6)
    NaviCmd(1, 1);
  else if (gTurn == 7)
    NaviCmd(1, 0);
  else if (gTurn == 8)
    NaviCmd(1, 1);
  else if (gTurn == 9)
    NaviCmd(1, 1);
  else if (gTurn == 10)
    NaviCmd(1, 0);
  else if (gTurn == 11)
    NaviCmd(1, 1);
  else if (gTurn == 12)
    NaviCmd(1, 1);
  :
  〜以下略〜

}

と書き加える案は美しくないし、そもそもキリがありません。 次の節でどうすれば実現できるか考えてみましょう。

剰余と変数

要は 9 ターンは 1 ターンと、10 ターンは 2 ターンと同じ動きをすればいいんです。 少し整理してみましょう。

ターン数 1234567891011121314151617181920...
動き ...
同じ動きのターン数 12345678912345678912...

1〜9 を繰り返しています。 なんとかして 1〜9 を繰り返させてれば、いつまでも同じ動きをするロボットを作ることができます。 しかしここで、もう一歩踏み込んでみましょう。 コード 6.7 をよく見ると、 1 ステップ目と 4 ステップ目や 7 ステップ目、 2 ステップ目と 5 ステップ目や 8 ステップめ、そして 3 ステップ目と 6 ステップ目や 9 ステップ目も同じ動きです。 すなわち 1〜9 を繰り返すのでなく、1〜3 を繰り返してもいいのです。

ターン数 1234567891011121314151617181920...
動き ...
同じ動きのターン数 12312312312312312312...

なんとかして 1〜3 を繰り返すことができれば、 すごくシンプルなコードが作れそうです。 そのやり方ですが自ら数え上げる方法もありますし、 gTurn から求める方法もあります。 まずは後者から説明します。


gTurn から求める方法

小学校で割り算を習ったばかりの頃、「余り」というものを教わったと思います。 それを使うのです。 3 回毎にくりかえすので 3 で割った余りを考えてみましょう。

元の数 1234567891011121314151617181920...
3 で割った余り 12012012012012012012...

1, 2, 3 でなく 1, 2, 0 ですが見事繰り返しが実現できました。 C 言語では割った余りを求めるのに専用の演算子があります。 % です。 % を使ってコードを書くと次のようになります。 長くてかつ未完成だったコード 6.7 に比べてとてもシンプルになりました。

コード 6.8
void Loop(void){
  if (gTurn%3 == 1)
    NaviCmd(1, 0);
  else if (gTurn%3 == 2)
    NaviCmd(1, 1);
  else if (gTurn%3 == 0)
    NaviCmd(1, 1);
}

さてこのコードは gTurn%3 という計算を三回も行っています。 同じ計算なので三回とも同じ結果が出るはずです。 同じ結果が出る計算を三回もするのは無駄です。 一回やってそれをメモしておき、以降そのメモを参照するようにした方が 「美しい」です。

一旦計算した結果をメモするにはプログラムの「変数」という機能を使います。 C 言語では事前に「こういうタイプのこういう名前の変数を作ってください」と お願いする必要があります。 これを変数宣言といいいます。

コード 6.9
void Loop(void){
  int a;
  a = gTurn%3;

  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 0)
    NaviCmd(1, 1);
}

コード6.9 では Loop() の最初に int a; と書いています。 これが変数宣言です。 int というタイプの a という名前の変数を作っているのです。 そして 2 行目でその a に gTurn%3 の計算結果を入れて、以降の if 文ではその値を参照しています。 ここで「int というタイプ」とサラッと流しましたが、int というタイプは何でしょうか。 簡単に言うと整数を記憶することができるタイプです。 整数しか記憶できないので 3.5 とか 9.7 とかの値は入れられません。 その点不便と思うかもしれませんがコンピュータが一番得意とするタイプなので、 特に深い理由が無い限り int 型を使うのが無難だと思います。


自力で数え上げる方法

変数というのは情報を記録しておくことが出来る機能です。 そこに自分が何ターン目に呼ばれたかメモしておけば、 わざわざ gTurn を使わなくても済みます。

これを実現する方針は a を毎回 1 足していって、足した数が 4 になったら 1 に戻せばいいんです。 とりあえずダメモトで次のように書いて見ましょう。 最後の if 文の条件式は a==4 でもいいのですが、 うっかりどこかで a に変な値が入ってもリカバリできるよう 4 以上かにしています。

コード 6.10
void Loop(void){
  int a;
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 3)
    NaviCmd(1, 1);
  a = a + 1;
  if (a >= 4)
    a = 1;

}

残念、エラーが発生しました。 以前作ったプログラムを動かすかと聞いています。 我々が実行したいのは改造したコード (6.10) ですので「いいえ」を選びます。

エラー表示を見ると「error C4700: 初期化されていないローカル変数 'a' が使用されます」と書かれてます。 C 言語の変数宣言は、メモできる場所を用意するだけでそこに何が書かれているか決まっていません。 そのため必ず何か情報を格納してから使わないといけないのです。 ここでは int a; と変数宣言した後、すぐ次の if 文で a が 1 かと確認しています。 それがいけなかったのです。 ということで a を初期化する、すなわちなにか値を入れて見ましょう。

コード 6.11
void Loop(void){
  int a;
  a = 1;
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 3)
    NaviCmd(1, 1);
  a = a + 1;
  if (a >= 4)
    a = 1;
}

と 1 を代入してみましょう。 エラーは無くコンパイルできるようになります。 話を進める前に、宣言と代入は次のように 1 行にまとめて書くこともできます。 よく使うフレーズなのでここで紹介しておきましょう。 また a=a+1 も a+=1 とか a++ と書くのが一般的なのでそう表現します。

コード 6.12
void Loop(void){
  int a = 1;
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 3)
    NaviCmd(1, 1);
  a++;
  if (a >= 4)
    a = 1;
}

さてこのコード、コンパイルは通るようになったのですが上手く動きません。 毎回 Loop() 冒頭で a に 1 が代入するため前進しかしないからです。

Loop() が呼ばれる度に初期化されるのが原因ならば Loop() の中でなく、外 (トップレベル) で宣言すればいいのです。 話が Loop() の外まで及ぶのでコード全体を示します。

コード 6.13
const char DLLNAME[NAMESIZE] = "サンプルロボット 2 号";
int a = 1;

void Init(int n){
  printf("作戦 %d でリセットされました\n", n);
}

void Loop(void){
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 3)
    NaviCmd(1, 1);
  a++;
  if (a >= 4)
    a = 1;
}

無事こちらの望む通りの動きをするようになりました。

と思うのもつかの間、何度か RESET ボタンを押してリスタートすると妙なことに気づくでしょう。 軌跡が他に二パターンあるのです。

 

STEP もしくは START が押されると a は 1,2,3 を繰り返します。 RESET が押された時 a は 1 なのかもしれませんが、2 や 3 の時もありうるからです。 毎回同じパターンの軌跡を描くためには RESET が押された時に a を 1 に初期化する 必要があります。 RESET が押されると Init() の中身が実行されるので、そこで a に 1 を入れるようにすればいいのです。

コード 6.14
const char DLLNAME[NAMESIZE] = "サンプルロボット 2 号";
int a;

void Init(int n){
  printf("作戦 %d でリセットされました\n", n);
  a = 1;
}

void Loop(void){
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 1);
  else if (a == 3)
    NaviCmd(1, 1);
  a++;
  if (a >= 4)
    a = 1;
}

ミッションを達成するには

さて本コンテストのミッションは相手のスタート位置に行って戻ることです。 まずはマップ a25x70_level01.txt でやってみましょう。

21 回前進した後、右に旋回しつつ前進、右回転を二回して 22 回前進すればミッションは達成しそうです。 つまり以下のようなコードで実現できそうです。

コード 6.15
void Loop(void){
  if (a == 1)
    NaviCmd(1, 0);
  else if (a == 2)
    NaviCmd(1, 0);
  else if (a == 3)
  :
  〜中略〜
  else if (a == 21)
    NaviCmd(1, 1);
  else if (a == 22)
    NaviCmd(0, 1);
  else if (a == 23)
    NaviCmd(0, 1);
  else if (a == 24)
    NaviCmd(1, 0);
  else if (a == 25)
    NaviCmd(1, 0);
  :
  〜中略〜
  a++;
}

できるかもしれませんが、冗長で美しくありません。 1 ターン毎に動きを指定するのでなく、何ターン以内なら、 何ターン以降ならといった条件式を上手く使うと次のように単純なコードにできます。

コード 6.16
void Loop(void){
  if (a < 21)
    NaviCmd(1, 0);
  else if (a == 21)
    NaviCmd(1, 1);
  else if (a < 24)
    NaviCmd(0, 1);
  else
    NaviCmd(1, 0);
  a++;
}

マップ a25x70_level01.txt に限れば無事にゴールできるようになりました。


[戻る]