実験9 迷路


ここで行うこと。

 移動ロボットは色々なことができますが、一番基本的なことは障害物を避けて二点間を移動することです。J-Botで用いているプログラムは比較的シンプルです。物体を避けるための様々のセンサーを使いますが、迷路のような複雑な障害物などを通過できないでしょう。

 この実験では、基本的な迷路探索アルゴリズムを調べます。J-Botは壁に当ること無しに迷路を探索するのに助けになる以前の実験で構成し、テストした赤外線検出器を用います。壁はJ-Botが交差できない境界と考えましょう。J-Botでは前に垂直の壁か、あるいはラインフォロワーで用いられる黒いテープで作られた仮想の壁を用います。後者の場合、J-Botの赤外線センサーは床に照準を合わせます。床は赤外線を反射するような色であるべきです。典型的には白い床が適切です。J-Botは白い床と壁とされている黒いテープの間の違いを検出します。

2つのセンサークラスは2つのタイプの迷路の壁を扱うためにすでに定義されています。IrRangeSensorは壁を検出し、IrRangeDropOffSensorは白い床上の黒いテープでできた仮想壁を検出するために用いられます。どちらかのセンサーはこの実験で提示されるアプリケーションで用いることができます。J-Botのために作られた迷路に依存します。

迷路探索は非常に精密になります。開始と停止点は可能ですが、J-Botには付加的な認識が必要になります。J-Botは迷路を出た時に検出する付加的なセンサーをたやすく扱うことができます。例えば、IRセンサーはJ-Botがアーチを潜り抜けたことを検出するようにすることができます。この実験では、停止点検出を先に行い、シンプルにJ-Botを永久に走らせます。迷路の中でコンテストでは走らせ、J-Botが迷路を脱出したかあるいは、停止点に達したら単に持ち上げるようにします。

迷路を操縦する多くの方法があります。この実験では次の3つを提示します。

Random Walk
Right Hand Rule
Basic Backtracking


Random Walkは以前に行った実験のプログラムを少し手直ししたものを用いた普通の障害物回避プログラムに似ていて、J-Botはもし障害物が前に見つかったらランダムな方向に回転します。理論的には、J-Botは迷路の出口を最終的には見つけるでしょうが、同じ場所を何回も通るので非常に時間がかかります。

Right Hand Rule法は迷路の出口を見つける単純なメカニズムを用いています。人間が右手を壁に付けて歩くことによって迷路の出口を見つけることができるので、right hand ruleと呼ばれます。この方法は迷路が島を持たないならばいつも使えます。迷路の外壁を形作る壁に接触していない壁ができた時に島ができます。島の無い迷路は単純迷路と呼ばれます。1つあるいはそれ以上の島を持つ迷路は複雑迷路と呼ばれます。Right Hand RuleはJ-Botを単純迷路から出口に出します。もし外壁に近いところから開始したならば複雑迷路でJ-Botは出口にいけます。もし、外壁に近くないところから開始したならば壁を永久に回ります。Right Hand Ruleは実装するのに簡単で、単純迷路ではrandom walkよりはやく脱出できます。

Random WalkとRight Hand Rule法はJ-Botが辿った跡を保持していません。J-Botはアルゴリズム的に記憶を持たないので、一回以上同じ点を通る羽目になるかどうか知りません。J-Botは単純に直接のフィードバックで応答しています。

Basic Backtracking法はメモリーの中に行動を入れます。J-Botが通った場所と特定の道筋が迷路から出られないならば次に何処を見るべきかを記憶します。Random WalkとRight Hand Rule法は精密な動作を必要としませんが、Basic Backtracking法は特定の道筋が袋小路になっているかどうか探索するために続けることのできる場所を保存する必要があります。

実験9-1 Random Walk

Random Walk法は動作を制御するタスクと物体を検出するセンサーオブジェクトを用います。先ほど述べたように、IrRangeSensorIrRangeDropSensorクラスが用いられます。次のプログラムはIrRangeSensorを用いています。

Random Walk迷路探索法に背後にある理論は物体に遭遇した時にランダムな動きをすることです。

下のRandomWalkTaskクラスは以前に用いたAvoidObstacleTaskクラスに非常に良く似ています。

単純な障害物回避タスク
package JBot;

import java.util.* ;
import stamp.util.os.* ;

/**
 * 単純な障害物回避タスク
 * 
 * センサーオブジェクトを用いて物体から離れるようにする
 * J-Botは固定増加分移動する
 * 前に障害物に遭遇した場合、J-Botはランダムな方向に移動する。
 */

public class RandomWalkTask extends Task {
  JBotInterface jbot = new RampingJBot ( new MultitaskingJBot ()) ;
  BaseSensor sensor ;
  Random random = new Random () ;

  private boolean flipCoin () {
    return random.next() < ( Random.MAX_RAND / 2 ) ;
  }


  public RandomWalkTask ( BaseSensor sensor ) {
    this.sensor = sensor ;
  }

  protected void execute () {
    final int turnAround = 1 ;

    switch ( state ) {
    case initialState:
      // 多数のステップが要求された時にのみ状態変化が起こる。

      if ( sensor.obstacleDetected ()) {
        int direction = sensor.obstacleDirection () ;

        if ( direction < 75 ) {
          // 左に何かがある
          jbot.pivot ( -2 ) ;
        } else
        if ( direction > 105 ) {
          // 右に何かがある
          jbot.pivot ( 2 ) ;
        } else {
          // 前に障害物がある

          if ( flipCoin ()) {
            // 後退し、他の事をする
            jbot.move ( -2 ) ;
            jbot.wait ( turnAround ) ;
          } else {
            // 左か右に回転することを試みる
            if ( flipCoin ()) {
              jbot.pivot ( -2 ) ;
            } else {
              jbot.pivot ( 2 ) ;
            }
          }
        }
      } else {
        // 何も検出されないので前進する。

        jbot.move (jbot.continuousForward) ;
      }
      break;

    case turnAround:
      // J-Botは後退し、障害物から離れる

      if ( flipCoin ()) {
        // 180゜回転する
        jbot.pivot ( 4 ) ;
      } else if ( flipCoin ()) {
        // 右に90゜回転する
        jbot.pivot ( -2 ) ;
      } else {
        // 左に90゜回転する
        jbot.pivot ( 2 ) ;
      }

      // 初期ステータスチェックのため後ろに戻る
      jbot.wait ( initialState ) ;
      break;

    default:                    // おかしなステータスを捕獲する
      stop () ;
      break;
    }
  }
}


RandomWalkTaskクラスとAvoidObstacleTaskクラス定義の間には3つの大きな違いがあります。第一にRandomWalkTaskは障害物までの距離をチェックしません。これは前述したどちらかのタイプのセンサーが用いられることによってなされます。センサーが見ている床は正しい距離結果を出さないので、障害物までの距離はいつもゼロと報告されます。

二番目の違いは大部分の動作は障害物に出会った時にランダムに選ばれます。例外は障害物が右か左のみに検出された時です。これがランダム性が入り込むところです。flipCoinメソッドは結果を発生するためにRandomオブジェクトを用います。random.nextメソッドは0~MAX_RANDの間の数字を返します。もしそのオブジェクトが本当にランダムな順序の数字を発生するならば、半分はMAX_RAND/2より大きく、半分はこの値より小さいでしょう。これは基本的にコインを投げて裏が出るか表が出るかと同じです。



RandomWalkTest1テストプログラムは全ての仕事がsensorオブジェクトとRandomWalkTaskによってなされるため、比較的簡単です。

Random Walkを用いた迷路テストプログラム

import JBot.*;
import java.util.* ;
import stamp.core.*;
import stamp.util.os.* ;

/**
 * Random Walkを用いた迷路テストプログラム
 * 
 * ランダムな動作を用いて迷路を脱出しようと試みる
 */

public class RandomWalkTest1 {

  static public void main () {
    new RandomWalkTask
          ( new IrRangeSensor // IrRangeDropOffSensor
                        ( CPU.pin7    // left DAC pin
                        , CPU.pin1    // left PWM pin
                        , CPU.pin0    // left detector pin
                        , CPU.pin8    // right DAC pin
                        , CPU.pin3    // right PWM pin
                        , CPU.pin2    // right detector pin
                        , 3           // deadband
                        )) ;

    try {
      Task.TaskManager () ;
    } catch ( Exception e ) {
      System.out.println ( "Error detected" ) ;
    } finally {
      System.out.println ( "All done" ) ;
    }
  }
}


TaskManagerメソッドはRandomWalkTaskタスクが決して終了しないのでこれも終了すべきではありません。J-Botが迷路を脱出した時に、もしRandomWalkTaskクラスがチェックができるように改められたならば、TaskManagerメソッドが戻ります。

プログラムは電源が入るとすぐに走り始めます。J-Botが迷路の中を動くのを見ましょう。壁に接触するはずはありません。もしそういうことがあったとしたら、IR LEDを回して各側のセンサーを外側に向け、横にある障害物をすぐに検出できるようにします。

J-Botは後ろにセンサーがないのでバックする時に壁にぶつかることを頭に入れておいてください。迷路が通路の幅が十分ならばJ-Botは問題はないでしょう。通路の幅が僅かに広い場合、J-Botは通路を操縦できません。一般的に、障害物の間をと売り抜けるとすると、障害物は少なくとも約30cm離れていなければなりません。

課題9-1

  1. 距離センサーは最適化されていません。全ての16の距離をいつもチェックします。これは必要でしょうか? 最初に最大距離をチェックするのは可能だろうか? もし、長い距離が検出された場合、近接距離をチェックする必要はあるだろうか?
  2. 距離センサーからの距離の結果は用いられていません。もし距離の結果がJ-Botが壁に近くなるようにチェックされるならばJ-Botの動きの違いはどうだろうか? もし下を見ているIR距離センサーを代わりに用いたとしたら、距離はいかに判断されるだろうか?
  3. RandomWalkTaskは障害物までの距離が使えるかどうかを知るためにセンサーをポーリングしています。ポーリングは走らせるのに時間がかからないセンサーのタスクやサーボモータのタスクのような他のタスクにオーバーヘッドを付け加えます。RandomWalkTaskがセンサータスクをしている間に動作が止まってしまうBasicSensotクラス内のsetEventを用いなさい。この修正によって障害物を検出したときにJ-Botの応答時間が改善されただろうか? そうでなければいけない。
  4. IRsensorクラスは1から16までの障害物距離を戻します。デフォールトの実装では距離を検出した距離に関わらず16回センサーをチェックします。動作の前に障害物までの最大距離が1よりも大きい場合、センサークラスは1と最大距離-1の間の値をチェックする必要はありません。センサークラスを範囲の値の間のみをチェックするようにセンサークラスを修正しなさい。この修正によって障害物を検出したときにJ-Botの応答時間が改善されただろうか? そうでなければいけない。
  5. J-Botが壁を検出するのは簡単ですが、J-Botが正確に回転するのには問題があります。この困難を脱出するには問題を簡単化し、迷路の代わりに動作の正確さに着目します。障害物は端のある線だと考えます。テープの線を用いて迷路を作りなさい。このとき、交差は30cm毎に起こることを注意してください。J-Botは各々の線を端まで追従するようにプログラムしなさい。この線追従プログラムは線を追従するように少しの修正をすればいいだけですから、J-Botは正確な回転について心配する必要はありません。

実験9-2 Right Hand Rule、右手の法則

単純な迷路は実際には様々な点で曲がる一本の線でできています。このタイプの迷路からJ-Botを脱出させるには、単純にJ-Botが迷路を脱出するまで線を辿っていくことです。

RightHandRuleTaskクラスは次の操作をセンサーからのフィードバックに基づいた動きとして定義します。



right hand ruleを用いた迷路探索タスク
package JBot;

import java.util.* ;
import stamp.util.os.* ;

/**
 * right hand ruleを用いた迷路探索タスク 
 * 
 * 右側の壁を伝わって迷路を出ようとする。
 * 一番初めにJ-Botは壁を検出するまで直進する。
 * それがJ-Botの右側になるように回転する。
 * 前方に障害物が無いと仮定して短距離直進する。
 * 壁がまだそこにあることを確かめるために右に回転する。
 * これはセンサーがJ-Botのすぐ右に壁があることを検出できないため
 * 必要である。前進して右を見るときだけ壁を検出できる。もし壁がまだ
 * 側にあれば、J-Botは左に回転し前進を続ける。
 * この過程は右回転で壁が検出されないか、壁が前にありJ-Botが左に回転
 * するまで繰り返される。
 * このプログラムが走っている迷路は厳密な方法でセンサーを用いている。
 * すなわち、J-Botが新しい障害物チェックに反応する自由動作に対する
 * 動きの前に、センサーが正しい結果を出すまで待ちます。
 */

public class RightHandRuleTask extends Task {
  JBotInterface jbot = new WheelEncoderJBot ( new MultitaskingJBot ()) ;
  BaseSensor sensor ;

  /**
   * 新しいタスクを作る
   *
   * 入力: BaseSensor sensor: 用いるセンサー
   */
  public RightHandRuleTask ( BaseSensor sensor ) {
    this.sensor = sensor ;
    sensor.setEvent ( this ) ;
    jbot.setNextEvent ( this ) ;
  }


  /**
   * 次の状態を設定する。タスクを停止する。
   * 通知された時に提示された状態でタスクがリスタートする。
   */
  protected void waitNextState ( int nextState ) {
    nextState(nextState) ;
    suspend() ;
  }


  /**
   * センサーを開始する。次の状態を設定する。タスクを停止する。
   * 通知された時に提示された状態でタスクがリスタートする。
   *
   * @param nextState next task state to be execute when sensor is done
   */
  protected void waitSensorNextState ( int nextState ) {
    waitNextState(nextState) ;
    sensor.obstacleDetected();
  }


  static final int checkForObstacle = 1 ;
  static final int doneWallToRight = 2 ;
  static final int checkAlongWall = 3 ;
  static final int turnToWallOnRight = 4 ;
  static final int checkWallOnRight = 5 ;
  static final int checkForWall = 6 ;

  /**
   * メインタスクルーチン
   * センサーあるいはJ-Botの動きのタスクを開始し、停止する。
   * 対応するセンサーあるいはタスクが完了した時にタスクが開始され、
   * このタスクをリスタートする。
   */
  protected void execute () {
    switch ( state ) {
    case initialState:
      // 障害物チェックセンサーを開始する
      waitSensorNextState(checkForObstacle);
      break;

    case checkForObstacle:
      // 障害物が検出されたかどうかチェックする
      if ( sensor.obstacleDetected()) {
        int direction = sensor.obstacleDirection();
        if ( direction < 75 ) {
          // 左に障害物あり、180゜左に旋回する。
          jbot.pivot(4) ;
        } else if ( direction > 135 ) {
          // 右に障害物あり、90゜左に旋回する。
          jbot.pivot(2) ;
        } else {
          // 前方に障害物あり。90゜左に旋回する。
          jbot.pivot(2) ;
        }

        // 動作が完了するまで待つ。
        waitNextState(doneWallToRight);
      } else {
        // 何も検出されない。1ステップ前進する。再度繰り返す。
        jbot.move(1) ;
        waitNextState(initialState);
      }
      break;

    case doneWallToRight:
      // J-Botは障害物から離れた。
      waitNextState( checkAlongWall ) ;
      sensor.obstacleDetected();
      break;

    case checkAlongWall:
      // 壁は右側にあるべき。
      // 前に何があるかチェックする
      if ( sensor.obstacleDetected ()) {
        int direction = sensor.obstacleDirection();
        if ( direction < 75 ) {
          // 左に障害物あり、多分角にいる。
          jbot.pivot(2) ;
        } else if ( direction > 135 ) {
          // 右に障害物あり、多分、端に近い。
          jbot.pivot(1) ;
        } else {
          // 前方に障害物がある。
          jbot.pivot(2) ;
        }
        waitNextState(doneWallToRight);
      } else {
        // 何も検出されない。1ステップ前進し壁をチェックする。

        jbot.move(1) ;
        waitNextState(turnToWallOnRight);
      }
      break;

    case turnToWallOnRight:
      // 前方に移動する。右側に壁があることをチェックする。
      jbot.pivot(-2);
      waitNextState(checkWallOnRight);
      break;

    case checkWallOnRight:
      // J-Botは前方に移動した。
      // 右側にまだ壁があるかどうか知るためにセンサーを用いる。
      waitSensorNextState(checkForWall) ;
      break;

    case checkForWall:
      // 右側に壁がある。
      // J-Botは壁に今直面する。
      // 障害物の状況により移動する。
      if ( sensor.obstacleDetected ()) {
        // 壁はまだそこにある。左に後ろに回る
        jbot.pivot(2);
        waitNextState(doneWallToRight);
      } else {
        // 前方になにもない。壁は右にあるだろう。
        jbot.move(1);
        waitNextState(initialState);
      }

      break;

    default:                    // defaultはおかしな状態を捕捉する。
      stop () ;
      break;
    }
  }
}


状態変数を変えるためのこれらの短い連続したメソッド呼び出しとタスクを停止させることがexecuteメソッド内で度々用いられるのでwaitNextStatewaitSensorNextStateメソッドは含まれます。基本的にタスクが走り、センサーを開始し、データが準備でき、メソッドを実行し、センサーの結果に基づいて何をするか決定した時にセンサーによってサスペンドが再開されます。同様の処理がセンサーの結果によって初期化される動作に対して用いられます。このアプローチでは、ある時点ではたった一つだけのタスクしか走りません。繰り返し行われる順序的な実行は、一般的にこのタスクで、センサータスクで、J-Bot動作のタスクでもあります。

タスクresumeメソッドは現在の状態を変えません。 stopメソッドは停止の状態に変化します。 これはresumeメソッドはwaitNextStateメソッドの定義の中で用いられなければならないからです。

 センサーは障害物の範囲を得るのにかなり多くの時間をかけるので、タスクサイクルはJ-Botのstart-stop動作を導きます。それはたった1秒の数分の1でしょうが、J-Botの動作は滑らかではありません。実験1で行ったアプローチと比較した兼ね合いがあります。実験1では、J-Botは連続的に動きますが、J-Botが動いているのでセンサーの読みは正確ではなく、従ってその動きの制御は正確ではありません。この実験では、動きは非常に正確ですが、その動きは連続的ではありません。

executeメソッドはタスク制御がなされるところです。そのタスクはwaitSensorNextStateを用いてセンサーを開始させるinitialStateから始まります。executeメソッドはセンサーが結果を得た時に再び呼ばれ、switch文のcheckForObstacleに行くように通知します。センサーからの結果はテストされJ-Botの動作制御はjbotオブジェクトを通して初期化されます。次の状態はセンサーの結果に依存してinitialStatedoneWallToRightになります。もし次の状態がinitialStateだとしたらJ-Botは1cm前進します。この距離はセンサーから得られた障害物までの距離によって大きくすることができますが、このプログラムではこれをチェックしていません。もしそのようにしたら、J-Botは障害物にぶつかる事無しに長い距離を移動できます。

このタスクには2つの論理的高レベルの場合があります。一つ目はJ-Botは壁が何処にあるか知らないこと。二つ目はJ-Botは壁は右側にあると思っていることです。始めの場合で、J-Botは壁が検出されるまで前進します。そして、二番目の場合の右側に壁が来た時に回転します。二番目の場合では、J-Botは短い距離前進し、壁があるかどうかチェックするために右回転します。もしあったらば、J-Botは左回転し壁に沿って移動しようと前進します。もし壁が無かった場合、回転しJ-Botは壁が右側に来ることを願って前進します。始めの移動の後でこの仮定が正しいとしたら壁がくるでしょう。

ほとんどの仕事はRightHandRuleTaskでなされるので、テストプログラムは比較的シンプルです。

右手の法則での迷路テストプログラム
package JBot;

import java.util.* ;
import stamp.core.*;
import stamp.util.os.* ;

/**
 * 右手の法則での迷路テストプログラム
 */

public class RightHandRuleTest1 {

  static public void main () {
    new RightHandRuleTask
          ( new IrRangeSensor // IrRangeDropOffSensor
                        ( CPU.pin7    // left DAC pin
                        , CPU.pin1    // left PWM pin
                        , CPU.pin0    // left detector pin
                        , CPU.pin8    // right DAC pin
                        , CPU.pin3    // right PWM pin
                        , CPU.pin2    // right detector pin
                        , 3           // deadband
                        )) ;

    try {
      Task.TaskManager () ;
    } catch ( Exception e ) {
      System.out.println ( "Error detected" ) ;
    } finally {
      System.out.println ( "All done" ) ;
    }
  }
}


タスクの中で修正すると色々とおかしなエラーが出るため、このテストプログラムにはtry/catch/finallyを付け加えました。これは、礼儀正しくシステムを終了させるためです。mainメソッドは実験9-1 とさほど異なってはいません。距離センサーが作られ、全てを開始させる新しく作ったタスクオブジェクトに距離センサーが渡されます。Task.TaskManagerは迷路出口検出メカニズムを装備していないため決して終了しません。

J-Botは実験9-1と比べて比較的遠くに壁があるようにします。少なくとも30cmぐらいの通路を通るようにします。たいていの場合、広いほうが良い結果になります。

RightHandRuleTaskはいくつかの欠点があります。J-Botが壁に接近すること。一時に1cmしか前進しないこと。45度の回転で十分な時でも90度回ってしまう。これらの欠点を正すことはプログラムをさらに複雑にしますが、新しいプログラムはJ-Botが行う方法を修正します。特に、J-Botが壁に接近することは壁がお互いに接近しているところを横切ることを意味します。

距離センサーは実験9-1で注意したように最適化してありません。センサーに対する応答時間を改善することは、このアプリケーションにも見られる発作的な動作を低減することができます。

課題9-2

右手の法則を左手の法則に変えなさい。

実験9-3 基本的なバックトラック

以前の実験では迷路をあちこち動いていましたが、ロボットが訪れたところを記録することによって得られる情報を利用していません。この実験では異なったアプローチで行います。JBotは訪れた地帯の地図を保持し、何処を探索すべきか決定する情報として用います。マッピングとバックトラッキングの方法はより知的な迷路探索プログラムを実装するためのひとつの方法です。

JBotはPCから外され、電池駆動を必要とします。(付加的なハードウェアを付けた良い方法の)ワイアレス接続を用いた簡潔さはJ-Bot地図を格納でき、がPCに再装着した時にプリントできます。これはメッセージウィンドウからコピーするのと似ています。
Stored map
Y range -1 2
X range -1 2
 2: . O . .
 1: O + O .
 0: O[+]+ O
-1: . O O .
例では、4×4の要素の配列があります。探索されていない領域はピリオド(.)で表されており、障害物はOで表されています。J-Botが移動する領域は正符号(+)で示され、開始位置はカギ括弧([])で示されます。

これをする事には幾つかの必要なことがでてきます。一つ目は探索をするメインプログラム、MazeMapTaskTest1です。これはJ-Botを迷路で動かし、地図を作ります。その地図は探索が完了した時にJ-BotのEEPROMに格納されます。二つ目のプログラムDumpMazeMapTaskTest1は、J-Botが迷路を走った後でロードされます。EEPROMを読み、例が示しているようにメッセージウィンドウに情報を表示します。EEPROMの領域はプログラムとこのデータによって共有されますが、両方に対する空間は十分あります。新しいプログラムのダウンロードはデータ領域を侵しませんのでこのようなアプローチ法が可能です。

MazeMapTaskTest1プログラムは次のような方法でテストします。このプログラムはPCからJ-Botにダウンロードされます。電源とPCケーブルはJ-Botから取り外します。この時点でプログラムはEEPROMに格納されています。J-Botは電池を装着しなければなりません。J-Botを迷路に置き、電池の電源を入れます。J-Botは行ったところの地図を作りながら迷路を動き回ります。全ての迷路を探索し終わったと思ったらブザーを鳴らします。この時点で地図はEEPROMに格納されます。

J-BotはPCに再装着され、DumpMazeMapTaskTest1プログラムがダウンロードされ走ります。地図の結果が表示され、実際の迷路と比較されます。この時点ではメモリー中にDumpMazeMapTaskTest1があるため、この作業を繰り返しすためにMazeMapTaskTest1が再びダウンロードされなければなりません。

いかに動作するか

この作業を行うのに必要なクラスが2つあります。これらはこの実験の後でもう少し詳細に説明します。主な作業は次に説明する「地図を作る」で説明します。他のクラスは「補足クラス」で説明します。

2つのメインプログラムMazeMapTaskTest1DumpMazeMapTaskTest1はこのプログラムで説明します。これらは存在しているクラスを用いたり、この実験で説明しているものを用いるので比較的短くなっています。MazeMapTaskTest1は以前の例のようなモジュールで、大体互換性のあるようなオブジェクトでサンプルオブジェクトを取り替えることが可能です。例えば、MazeMapTaskTest1はIR検出センサークラスIrRangeSensorを用いています。これは以前に用いた接触センサークラスのような互換のあるセンサーによって取り替えられます。同様に、J-Botの動作も取り替えられます。MazeMapTaskTest1はJ-Botの動きをより正確に保持するために用いた車輪エンコーダーを用いています。

MazeMapTaskTest1には2つの仮定があります。一つ目は、センサーの範囲は少なくとも10cmであること。もしこうでなかったら、接触センサーと同じになってしまい、地図作り作業はJ-Botが障害物にぶつかる事を防ぐような補正が必要になります。2番目は、J-Botは1つの動作が固定的な距離で動きます。これは制御と地図作りを簡単にしています。もしJ-Botの動作が連続的に走ることができても非常に難しい問題になります。最後に、プログラムはJ-Botが探索できる領域は制限以内であることが仮定されます。例えば、迷路の通路は約30cmの広さでなければなりません。J-Botは30cmの通路内で簡単に入る15×15cmの領域を占有すると仮定します。障害物までの開いている部分は非常に狭いと思われます。勿論これは用いているセンサーに依存しますが、もし迷路がJ-Botが簡単に操作できるように設定されているならばこれが最良でしょう。J-Botのセンサーがミスをするかセンサーの結果を不適格に解釈するため、狭い壁の端をJ-Botに提示しないことを意味します。一般的に、少なくとも30cmの壁で出来ている迷路を作ることが最適です。

IR検出器に対する迷路を作る最適な方法は、少なくとも10cmの高さ、15cmの幅、30cmの長さの白い箱を用いることです。これは次々と置くことによって迷路を再構築できます。もしJ-Botが事故で箱にぶつかっても移動も出来ます。

下記のリストはMazeMapTaskTest1です。

MapMazeTaskクラスをテストする
import stamp.core.*;
import stamp.util.os.* ;
import JBot.* ;

/**
 * MapMazeTaskクラスをテストする
 * 
 * J-Botは地図を作っている間迷路を探索する
 */

public class MazeMapTaskTest1 {
  public static void main () {
    int buzzerPin = CPU.pin4 ;
    FREQOUT buzzer ;

    printableMazeMap map = new printableMazeMap ( 20, 20 ) ;
    new MapMazeTask
      ( new IrRangeSensor ( CPU.pin7    // left DAC pin
                          , CPU.pin1    // left PWM pin
                          , CPU.pin0    // left detector pin
                          , CPU.pin8    // right DAC pin
                          , CPU.pin3    // right PWM pin
                          , CPU.pin2    // right detector pin
                          , 1           // deadband
                          )
      , 5           // 3-5cmの検出に対する範囲値
      , new RampingJBot ( new MultitaskingJBot ())
      , map ) ;

    // タスクを走らせる
    Task.TaskManager () ;

    // EEPROMに地図を書き込む
    map.save();

    // ブザーを鳴らす
    buzzer = new FREQOUT ( buzzerPin ) ;
    buzzer.freqout ( 100, 100 ) ;
    CPU.delay ( 10000 ) ;
    buzzer.stop();

    System.out.println ( "All done" ) ;
  }
}


mainメソッドはprintbleMazeMapMapMazeTaskを作っています。後者はRampingJBotであるJBotInterfaceと一緒に地図を用います。そのコンストラクターの二番目のパラメータは障害物がJ-Botの前3-5cm以内にあるという範囲値を示しています。

printbleMazeMapのコンストラクタは2つのパラメータを持っていて、yとxの地図の寸法です。この場合、400の要素を持った地図が作られます。各々の要素はJ-Botが占有する15cm四方に対応してします。これは地図は一辺が3mの領域を表現することを意味しています。

戸外に行き、巨大な迷路を作る前に二つのことを心に留めて置いてください。一つ目は、作業が多いこと。二つ目はJ-Botは自分で思っているよりは正確に動作することができないことです。

J-Botが移動または回転する時に動作は望んだように正確ではないことを思い出してください。例えば、J-Botが実際右に回転したとしたら、正確に90度ではなく85とか95度ぐらいになるでしょう。これは細かな誤差に見えて注目すべきことではないように思えますが、この誤差は問題になります。少量の誤差の複数の動作の組み合わせでの累積誤差のためです。J-Botを5,6回回転させれば45度以上狂っています。

このような理由で、テストする迷路は大きくすべきではありません。1m四方ぐらいのものは実験には十分でしょう。

Taks.TaskManagerメソッドが呼ばれた時にMapMazeTaskはsensorとJBotタスクで動作します。これらのタスクはJ-Botを迷路中を巡らせます。迷路がその能力の最善で探索されたとプログラムが決定したときに全てのタスクが停止します。Task.TaskManagerメソッドは戻り、map.saveメソッドが呼ばれます。これはEEPROMに現在の地図を格納します。FREQOUTオブジェクトは探索が完了し地図が保存されたことを示すために一秒の音を鳴らします。電源を切り、J-BotはPCに再接続されます。

次はDumpMazeMapTaskTest1プログラムをロードします。

MazeMapTaskTest1の地図をダンプする
import stamp.core.*;
import JBot.*;

/**
 * MazeMapTaskTest1の地図をダンプする
 */

public class DumpMazeMapTaskTest1  {
  public static void main() {
    printableMazeMap map = printableMazeMap.load() ;

    map.print("Stored map");
  }
}


クラスメソッドloadはEEPROMメモリーを読むのに用いられ、MazeMapTaskTest1プログラムで用いたのと同じprintableMazeMapを作ります。そのprintメソッドはメッセージウィンドウの地図の内容を表示します。

J-BotをPCに接続したままにしてこれらのプログラムをテストする簡単な方法は、車輪が接触しないようにJ-Botを小さな箱や物の上に載せます。サーボモータのケーブルを取り外すことは車輪エンコーダが車輪が実際に動いた時のみに動作するので正しくないことに注意してください。障害物をJ-Botの前に置きます。MazeMapTaskTest1プログラムを走らせることは通過した部分、J-Botが開始したところ、回りの障害物の地図を作ります。プログラムを動かしている間に車輪が動くのを監視でき、System.out.printlnメソッドを追加して状況情報をプリントすることが出来ます。勿論、J-Botを邪魔しないで走らせた時はさらに興味深いことを得るでしょう。

次は処理の大部分を行っている地図作りをみてみましょう。

地図を作る

MapMazeTaskはJ-Botを制御しprintableMazeMapでその動きを記録するメインタスクです。MapMazeTaskTaskクラスを拡張しています。そのexecuteメソッドは前に説明したメインプログラムMazeMapTaskTest1の中のTask.TaskManagerによって繰り返し呼ばれます。次のソースはMapMazeTaskクラスです。

障害物と壁を避けながら迷路を探索する
package JBot;

import stamp.util.os.* ;
import stamp.core.*;

/**
 * 障害物と壁を避けながら迷路を探索する
 * 
 * printableMazeMapで迷路の状況を保存する
 * タスクは探索する場所が無くなった時に停止する
 * センサーの範囲は約3cmと仮定する
 * J-Botは15cm四方内に入っているとする
 * 各々の地図のマス目は15×15cm四方である
 * 任意の障害物はJ-Botの前方にあると仮定する
 * これは地図作りと意思決定を簡単にする
 * このプログラムはバックトラックをしたときに障害物をチェックしない
 */

public class MapMazeTask extends Task {
  protected JBotInterface    jbot ;
  protected BaseSensor       sensor ;
  protected int              obstacleRange ;
  protected printableMazeMap map ;

  protected char facing ;
  protected char backtrackPath[];
  protected int  pathIndex ;
  protected int  pathSize ;

  /**
   * 迷路の地図を作るタスクを作る
   *
   * 入力:BaseSensor sensor: 障害物をチェックするセンサー
   * 入力:int obstacleRange: 3-5cmに対する検出範囲
   * 入力:JBotInterface jbot: JBotを動かすため
   * 入力:printableMazeMap: map: 迷路を地図化する文字地図
   */
  public MapMazeTask ( BaseSensor sensor
                     , int        obstacleRange
                     , JBotInterface jbot
                     , printableMazeMap map ) {
    this.sensor = sensor ;
    this.obstacleRange = obstacleRange ;
    this.jbot = jbot ;
    this.map = map ;
    backtrackPath = new char [ 128 ] ;

    // デフォールトの方向を設定する
    facing = mazeMap.north ;
  }

  static protected final int stepDistance = 5 ; // cm 

  static protected final int computePath = 1 ;
  static protected final int followPath = 2 ;

  protected int checkingOffset = 0 ;

  /**
   * タスク状態ハンドラー
   */
  protected void execute () {
    switch ( state ) {
    case initialState:
      if ( checkingOffset == 6 ) {
        // 通過したマスをマークする
        map.move(facing);
        map.markTraveled();

        checkingOffset = 0 ;
      }

      // 前方に何があるか見る
      if ( sensor.ready()) {
        // センサーが読まれ、チックする
        if (    sensor.obstacleDetected ()
             && (    sensor.obstacleDistance(sensor.obstacleDirection())
                  <= obstacleRange)) {
          // 前方に障害物
          if ( checkingOffset == 0 ) {
            // 障害物が直前にある
            nextState(computePath);
          } else {
            // 障害物が前方5cmにある
            // 論理的地図のマスの中心をバックアップする
            jbot.move(-checkingOffset);
            jbot.wait(computePath);
            checkingOffset = 0 ;
          }
        } else {
          // センサーの範囲内で前方に移動する
          checkingOffset += stepDistance ;
          jbot.move(stepDistance);
          jbot.wait(initialState);
        }
      }
      break ;

    case computePath:
      // J-Botは現在のマス位置の中心にいる
      // 遭遇した障害物をマークする
      map.markObstacle(facing);

      //map.print("map");
      //map.printPath();

      // 新しい領域の探索のための道のりを計算する
      pathSize = map.computeBacktrackPath(backtrackPath) ;
      if ( 0 == pathSize ) {
// 探索が全て完了した。ブザーを鳴らし、地図をプリントするのを待つ。
        jbot.stop();
        stop();
        break;
      }

      // backtrackPathとpathSizeで指定された道のりに従う
      pathIndex = 0 ;
      nextState(followPath);

    case followPath:
      // その道のりで次のステップに従う
      if ( facing == backtrackPath[pathIndex]) {
        // その道のりに沿って移動する
        // 注意: 障害物に対するチェックはされない(要修正)
        // バックトラックパスのインデックスを修正する
        ++pathIndex;
        --pathSize;

        if ( pathSize == 0 ) {
          // 最後の動作の方向に向く
          nextState(initialState);
        } else {
          // 地図の位置を補正する
          map.move(facing);
          map.markTraveled();

          // pathに沿ってJ-Botを移動する
          jbot.move(6);
          jbot.wait(followPath) ;
        }
      } else {
        // pathに従って右の方向に向く
        int dir = facing-backtrackPath[pathIndex] ;
        if ( dir == 3 ) {
          dir = -1 ;
        } else if ( dir == -3 ) {
          dir = 1 ;
        }
        jbot.pivot (dir*2) ;
        jbot.wait ( followPath ) ;

        // 論理的方向を補正する
        facing = backtrackPath[pathIndex] ;
      }
      break;

    default:                    // 異常な状態を捕獲する
      stop () ;
      break;
    }
  }
}


MapMazeTaskはコンストラクタとexecuteメソッドの2つのメソッドで出来ています。コンストラクタはパラメータを保存します。バックトラックパスのための文字配列を作り、J-Botの方向を北(north)に設定します。この方向は地図化の目的に対して厳密で、方位の北に対応しています。

MapMazeTaskはJ-Botを直線に沿って移動させ、J-Botを90度旋回させるだけです。これは地図化とバックトラック処理を非常に簡単化します。初期のJ-Botの位置に関して0とか90度の角度ではない通路に沿っての動きは段階的な方法で探索されるでしょう。

executeメソッドは3つの状態に分けられます。

initialState
computePath
followPath
initialStateはJ-Botの前方に障害物があるかどうかを知るセンサーをチェックします。それが直前かあるいは片方にあるかどうかは問題になりません。システムの粒子性のため、J-Botを部分的に妨げる障害物は地図のマスのサイズすなわち15cmと同じ幅と仮定します。

J-Botは短いステップ、すなわちセンサーが検出できる最大の距離として仮定される5cmで前進します。あるセンサーは長い距離で使え、動きはjbot.moveメソッド呼び出しの中で用いられているstepDistanceの値を変えることによって補正することが出来ます。checkingOffset変数はJ-Botがどのくらい前進したかを記録しておくために用いられます。もしJ-Botが次の15cm四方の途中で障害物を検出した時はJ-BotはcheckingOffset値を用いて論理的な地図マスで後退し、障害物としてマークします。ひとつの地図のマス幅は地図が示すよりも実際には余裕があるけれどもJ-Botが30cmの通路の中心を移動することは15cmの通路としてマークすることを意味します。

J-Botが通過した領域はmap.markTraveledを呼ぶことによって地図に保存されます。これはcheckingOffsetが15cmになった時に起こります。地図上での論理的な位置はmap.moveメソッドを用いて補正されます。方向変数facingはJ-Botが面している論理的な方向を保持するのに用いられます。

地図の不明瞭さは実際J-Botの有利です。J-Botは障害物にいかに接近しているか心配せずに以前に通過したことのある領域を通り抜けます。それは少なくとも3cmかも知れません。

computePath状態はmap.markObstacleを用いて障害物と同様に探索された領域をマークします。その道筋(path)はbacktrackPath配列の中に置かれます。配列の各々の要素はJ-Botが移動が必要な方向で、現在の位置から開始し、未探索の領域を得るために用います。このマスの配列の中には多くの方向の値が入っています。もし数が0のとき、J-Botによってアクセスできる全ての領域が探索されています。そのタスクとサーボモータは停止し先ほど説明したTask.TaskManagerメソッドは戻ります。

followPath状態はバックトラックの道筋に沿ってJ-Botが移動するのに用いられます。その道筋の最後のステップはJ-Botの位置が用いられますが、J-Botは領域内には移動しません。その代わりに、この時点でinitialStateに入り、J-Botは未探索の領域が開いていて通過できるかどうかあるいは値が入っていて障害物があるかどうかを見るためにセンサーを用います。

followPath状態はJ-Botの現在の方向と希望の方向を比較します。もし必要ならばJ-Botを望みの方向に回転させます。dir変数の使用は最適化です。始めの回転計算が270度のとなった場合、これは90度回転と同じです。後者はより効率的で正確です。J-Botは左右に90度あるいは180度の回転をするという意味になります。

道筋のステップは最後ではないという仮定で、J-Botは望みの方向に15cm移動します。map.moveメソッド呼び出しは迷路地図の中の論理的位置を変えます。map.markTraveledメソッド呼び出しはその道筋がいつも通過した領域上にあるので明らかに冗長です。

論理は比較的シンプルですが、これを設計するにはうまい考えが必要になります。地図を用いる簡単な方法のひとつがこれです。さらに複雑にすることは容易い仕事ではありませんが、よい挑戦になります。

補足クラス

printableMazeMapMapMazeTaskによって用いられます。これは次のクラス階層構造に基づいています。
charArray
 charMap
  mazeMap
   printableMazeMap
これらのクラスは、2つの目的を持っています。charArrayクラスはJavelinは1次元配列のみ実装されており、この実験で用いる地図は二次元配列ですから必要になります。charMapは配列は通常N+1の要素では0からNまでアクセスされるから、よりダイナミックな配列の使用法をするときに用います。charMapはマイナスの添え字を使うことが出来ます。地図は作られた時に用いられる最大のサイズにアクセスして大きく出来ます。J-Botは論理的に0,0から開始することができ、探索のために任意の方向に移動できるので、地図作りに非常に手ごろです。従来の地図では同じ事をするには4倍必要になります。

mazeMapは地図の中で用いられ、格納される定数定義の値と同様にバックトラックの能力を付け加えています。printableMazeMapはEEPROM格納メソッドとSystem.out.printlnの状態報告が使えるようにこのクラスを拡張しています。これには付加的な地図作りやバックトラック能力はつけていません。

次のソースはcharArrayクラスの定義です。これは二次元配列を一次元配列に写像します。同様の方法を用いて任意の型の配列で用いることができます。xLowのようなメソッドは配列のサイズを決定するために一貫とした方法を用いており、サブクラスの動的な性質を考慮に入れることができます。このクラスはもし配列が不適切にアドレスされた時にIndexOutOfBoundsExceptionを投げます。

二次元文字配列
package JBot;

import stamp.core.*;

/**
 * 二次元文字配列
 */

public class charArray {
  protected char s[];
  protected int xSize ;
  protected int ySize ;

  /**
   * 2次元の文字配列を作る
   *
   * 入力:int ySize: Yのサイズ
   * 入力:int xSize: Xのサイズ
   */
  public charArray ( int ySize, int xSize ) {
    s = new char [xSize * ySize];
    this.xSize = xSize ;
    this.ySize = ySize ;
    clear();
  }

  /**
   * xのサイズを得る
   *
   * 戻り: xのサイズ
   */
  public int xSize(){
    return xSize;
  }

  /**
   * yのサイズを得る
   *
   * 戻り: yのサイズ
   */
  public int ySize(){
    return ySize;
  }

  /**
   * xサイズの最小境界を得る
   *
   * 戻り: 0
   */
  public int xLow(){
    return 0;
  }

  /**
   * yサイズの最小境界を得る
   *
   * 戻り: 0
   */
  public int yLow(){
    return 0;
  }

  /**
   * x添え字の最大を得る
   *
   * 戻り:x添え字の最大
   */
  public int xHigh(){
    return xSize-1;
  }

  /**
   * y添え字の最大を得る
   *
   * 戻り:y添え字の最大
   */
  public int yHigh(){
    return ySize-1;
  }


  /**
   * 配列をクリアする
   */
  public void clear(){
    setAll (0);
  }

  /**
   * 全ての配列を設定する
   *
   * 入力:int c: 全てに文字を入れる
   */
  public void setAll(int c){
    int size = xSize * ySize ;

    for ( int i = 0; i < size ; ++ i ) {
      s[i]=(char)c;
    }
  }

  /**
   * 配列の添え字を得る
   *
   * 入力:int y: 配列の添え字
   * 入力:int x: 配列の添え字
   *
   * 戻り: 配列の添え字
   */
  protected int getIndex ( int y, int x ) throws IndexOutOfBoundsException {
    if (( x >= xSize ) || ( y >= ySize )) {
      IndexOutOfBoundsException.throwIt() ;
    }

    return (y * xSize) + x ;
  }

  /**
   * 配列から文字を得る
   *
   * 入力:int y: 配列の添え字
   * 入力:int x: 配列の添え字
   *
   * @returns character from array
   */
  public int get ( int y, int x ) throws IndexOutOfBoundsException {
    return s[getIndex(y,x)] & 0xff ;
  }

  /**
   * 配列に文字をセットする
   *
   * 入力:int y: 配列の添え字
   * 入力:int x: 配列の添え字
   * 入力:int c: セットする文字
   */
  public void set ( int y, int x, int c ) throws IndexOutOfBoundsException {
    s[getIndex(y,x)] = (char) c ;
  }
}


charMapクラスは正の数字のみの添え字しか使えない従来の配列上で負の境界を持った論理的な地図を表現します。これはcharArrayで用いられているgetIndexを再定義することによって操作されます。そのクラスは地図の最大と最小地図範囲を保持し、それらにアクセスするパブリックなメソッドを持っています。



2次元の文字地図
package JBot;

import stamp.core.*;

/**
 * 2次元の文字地図
 * 配列の端に負の軸をラップする
 */

public class charMap extends charArray {
  protected int xLow ;
  protected int yLow ;
  protected int xHigh ;
  protected int yHigh ;

  /**
   * 2次元の文字地図を作る
   *
   * 入力:int ySize: Yのサイズ
   * 入力:int xSize: Xのサイズ
   */
  public charMap ( int ySize, int xSize ) {
    super(ySize,xSize);
    resetBounds();
  }

  /**
   * 下限と上限をリセットする
   */
  public void resetBounds(){
    xLow  = 0 ;
    yLow  = 0 ;
    xHigh = 0 ;
    yHigh = 0 ;
  }

  /**
   * xサイズの最小境界を得る
   *
   * 戻り: xLow
   */
  public int xLow(){
    return xLow;
  }

  /**
   * yサイズの最小境界を得る
   *
   * 戻り: yLow
   */
  public int yLow(){
    return yLow;
  }


  /**
   * x添え字の最大を得る
   *
   * 戻り:x添え字の最大
   */
  public int xHigh(){
    return xHigh;
  }

  /**
   * y添え字の最大を得る
   *
   * 戻り:y添え字の最大
   */
  public int yHigh(){
    return yHigh;
  }

  /**
   * 配列の添え字を得る
   *
   * 入力:int y: 配列の添え字
   * 入力:int x: 配列の添え字
   *
   * 戻り: 配列の添え字
   */
  protected int getIndex ( int y, int x ) throws IndexOutOfBoundsException {
    // 境界が拡張される必要があるかどうかチェックする
    if ( x < xLow ) {
      if (( xHigh - x ) >= xSize ) {
        IndexOutOfBoundsException.throwIt() ;
      }
      xLow = x ;
    } else if ( x > xHigh ) {
      if (( x - xLow ) >= xSize ) {
        IndexOutOfBoundsException.throwIt() ;
      }
      xHigh = x ;
    }

    if ( y < yLow ) {
      if (( yHigh - y ) >= ySize ) {
        IndexOutOfBoundsException.throwIt() ;
      }
      yLow = y ;
    } else if ( y > yHigh ) {
      if (( y - xLow ) >= ySize ) {
        IndexOutOfBoundsException.throwIt() ;
      }
      yHigh = y ;
    }

    // 実際の添え字を返す
    return super.getIndex (( y < 0 ) ? ( y + ySize ) : y
                          ,( x < 0 ) ? ( x + xSize ) : x ) ;
  }
}


下で示すMazeMapクラスはcharMap上に作ります。障害物や通過点のような値を与え、またバックトラックパスを探す時にパスの値を用います。yとx変数に論理的な位置を保持しています。これらはmoveメソッドを用いることによって変化します。J-Botが実行できる動作に合致した東西南北に基づいた論理的な方位が仮定されています。markTraveledmarkObstacleメソッドはJ-Botがその周りに関して手に入れられることのできる情報に基づいた地図に値付けするのに用います。

迷路のための地図サポート
package JBot;

import stamp.util.os.* ;

/**
 * 迷路のための地図サポート
 * 
 * 基本的なバックトラックパス作成を作る
 */

public class mazeMap extends charMap {
  protected int x, y ;

  static final public char unknown = 0 ;
  static final public char obstacle = 1 ;
  static final public char traveled = 2 ;
  static final public char path = 3 ;

  static final public char north = 0 ;
  static final public char east = 1 ;
  static final public char south = 2 ;
  static final public char west = 3 ;

  /**
   * 迷路地図を作る
   */
  public mazeMap ( int ySize, int xSize ) {
    // 地図を設定する
    super(ySize,xSize);

    // 地図をクリアする
    setAll (unknown) ;

    // 現在の位置を設定し地図に印を付ける
    x = 0 ;
    y = 0 ;
    markTraveled() ;
  }

  /**
   * カーソル位置を動かす
   *
   * 入力:int direction: 動かす方向
   */
  public void move ( int direction ) {
    // 1マス動かす
    switch (direction) {
    case north: y++; break ;
    case east:  x++; break ;
    case south: y--; break ;
    case west:  x--; break ;
    }
  }

  /**
   * 移動したときに現在の位置(y,x)を印す
   */
  public void markTraveled () {
    set ( y, x, traveled ) ;
  }

  /**
   * 現在の位置(y,x)の前の領域に印す
   */
  public void markObstacle (int direction) {
    int x1, y1 ;

    switch (direction) {
    default:
    case north:
      x1 = x ;
      y1 = y + 1 ;
      break ;

    case east:
      x1 = x + 1 ;
      y1 = y ;
      break ;

    case south:
      x1 = x ;
      y1 = y - 1 ;
      break ;

    case west:
      x1 = x - 1 ;
      y1 = y ;
      break ;
    }

    set ( y1, x1, obstacle ) ;
  }

  /**
   * unknownと印されたマスに対して単純な最初の調査
   * バックトラックパスを作る間に地図を変える
   * 戻る前にオリジナルな状態に地図を戻す
   *
   * 入力;char[] backtrackPath: 場所のパスの配列
   *
   * 戻り:int: パスの要素 (0=no path)
   */
  public int computeBacktrackPath( char backtrackPath[]) {
    int x, y, step, next ;
    int pathSize = 0 ;
    int pathIndex = 1 ;

    pathIndex = 0 ;
    pathSize = 1 ;
    x = this.x ;
    y = this.y ;
    backtrackPath[0]=north;
    set(y,x,path);

    searchLoop: while(true) {
      switch (backtrackPath[pathIndex]) {
        default:
        case north: next = get(y+1,x) ; break;
        case east:  next = get(y,x+1) ; break;
        case south: next = get(y-1,x) ; break;
        case west:  next = get(y,x-1) ; break;
      }

      switch (next) {
      default:
      case path:
      case obstacle:
        // この方向には行けない。他を試す
        while ( backtrackPath[pathIndex] == west ) {
          // 探索が完了したかどうかをチェックする
          if ( pathSize == 1 ) {
            // 全てが終わった。全てが探索した。
            pathSize = 0 ;
            break searchLoop ;
          } else {
            // cut back path
            -- pathSize ;
            -- pathIndex ;

            // 現在の調査座標を戻す
            switch (backtrackPath[pathIndex]) {
              default:
              case north: --y ; break;
              case east:  --x ; break;
              case south: ++y ; break;
              case west:  ++x ; break;
            }
          }
        }

        // 次の方向を試す
        backtrackPath[pathIndex] ++ ;
        break;

      case traveled:
        // パス位置の終わりを変更する
        switch (backtrackPath[pathIndex]) {
        default:
        case north: ++y ; break;
        case east:  ++x ; break;
        case south: --y ; break;
        case west:  --x ; break;
        }

        // 循環パスを防ぐためにパスの部分としてマスに印す
        set(y,x,path);

        // 次の調査を設定する
        ++ pathSize ;
        ++ pathIndex ;
        backtrackPath[pathIndex]=north;
        break;

      case unknown:
        // パスを見つけた。地図のパスをリセットする
        x = this.x ;
        y = this.y ;

        for ( int i = 0 ; i < pathSize ; ++ i ) {
          // 移動したパスのマスを戻す
          set(y,x,traveled);

          // 次のマスが何処かを決定する
          switch (backtrackPath[i]) {
            default:
            case north: ++y; break;
            case east:  ++x; break;
            case south: --y; break;
            case west:  --x; break;
          }
        }

        break searchLoop ;
      }
    }

    // 全てが終わった。地図を戻す。
    for ( y = yLow ; y <= yHigh ; ++ y ) {
      for ( x = xLow ; x <= xHigh ; ++ x ) {
        if ( get(y,x) == path ) {
          set(y,x,traveled);
        }
      }
    }

    return pathSize ;
  }
}



computeBacktrackPathは現在の地図の内容を用いながら、修正しながら未探索の領域にたいするパスを調査します。地図はunknown(探索されていない)に初期化され、そのメソッドは現在の論理的位置(y,x)からunknowと印されたマスを探します。調査はパスが作られた時にパスのマスを移動したマスに変えながら続けます。パスの一部として印されたマスはそのメソッドが、オリジナルの位置を保持したまま、これらの点が移動したマスとして元に変えられる点として戻るまでその状態のままでいます。そのメソッドがパスに対してマスがすでに見つけられているかどうかを決定出来るようになされます。もしこれが為されなければ、循環調査が無限ループに陥ります。

そのメソッドは一番近い未探索の場所を探すことはありません。単純なアルゴリズムを用いてそれを簡単に見つけるよう試みています。そのアルゴリズムは、北の方向から探し始めます。その方向にパスが見つけられなかった場合、その右のマスに移動し探索し、最後には西方向まで行います。これは西にすぐに未探索のマスがあったとしても、始めに北に10マス個先に未探索の場所を見つけた場合に探しにいくことを意味します。

地図を検索する多くの方法があります。これは実装するのに簡単な方法で、付加的なスペースを必要としません。Javelinのようなメモリーの上限が厳しいときに使います。

下に示すようにprintableMazeMapは保存する方法のEEPROMメソッドとSystem.out.println指向のメソッドを付け加え、mazeMapの内容をリストアし表示します。loadメソッドは新しいprintableMazeMapのオブジェクトを返すstatic classメソッドです。地図の内容はEEPROM領域の最初に格納されています。さらに賢い版ではEEPROM領域のどこかに位置を指定して入れる事が出来るでしょう。saveメソッドはEEPROMの中にある地図オブジェクトに格納します。

printprintPathメソッドは地図と現在の探索パスを各々表示します。より出力が容易くするために地図とパス配列で用いられる値をバイナリー値を変換します。

Printable mazeMap class
package JBot ;

import stamp.core.*;
import stamp.util.os.* ;

/**
 * Printable mazeMap class
 * 
 * mazeMapとパスの詳細をプリントする
 */

public class printableMazeMap extends mazeMap{
  public char path[] ;

  /**
   * プリントできるmazeMapオブジェクトを作る
   */
  public printableMazeMap ( int y, int x ) {
    super( y, x ) ;
    path = new char [ 128 ] ;
  }

  /**
   * EEPROMに地図を保存する
   */
  public void save() {
    EEPROM.write(0,(byte)ySize);
    EEPROM.write(1,(byte)xSize);
    EEPROM.write(2,(byte)(xSize+ySize));
    EEPROM.write(3,(byte)yLow);
    EEPROM.write(4,(byte)yHigh);
    EEPROM.write(5,(byte)xLow);
    EEPROM.write(6,(byte)xHigh);

    int i = 7 ;
    for ( int y = yLow ; y <= yHigh ; ++ y ) {
      for ( int x = xLow ; x <= xHigh ; ++ x, ++i ) {
        EEPROM.write(i,(byte)get(y,x));
      }
    }
  }

  /**
   * EEPROMから地図をロードする
   */
  static public printableMazeMap load() {
    printableMazeMap map ;
    int x, y ;
    y = EEPROM.read(0);
    x = EEPROM.read(1);
    if ((x+y) == EEPROM.read(2)) {
      map = new printableMazeMap(y,x);
      map.realLoad();
    } else {
      map = new printableMazeMap(1,1);
    }
    return map;
  }

  /**
   * EEPROMから符号付byteを読む
   *
   * 入力:int i: EEPROMの添え字
   *
   * 戻り:int: 符号付byte
   */
  static protected int EEPROMread(int i) {
    int result = EEPROM.read(i);

    return ( result < 128 ) ? result : ( result - 256 ) ;
  }

  /**
   * EEPROMから地図をロードする
   */
  protected void realLoad() {
    resetBounds();
    yLow  = EEPROMread(3);
    yHigh = EEPROMread(4);
    xLow  = EEPROMread(5);
    xHigh = EEPROMread(6);

    int i = 7 ;
    for ( int y = yLow ; y <= yHigh ; ++ y ) {
      for ( int x = xLow ; x <= xHigh ; ++ x, ++i ) {
        set(y,x,EEPROM.read(i));
      }
    }
  }

  /**
   * 現在のバックトラックパスを探し、それをプリントする
   */
  public void printPath () {
    int pathSize;
    char c ;

    pathSize = computeBacktrackPath ( path ) ;

    if ( pathSize == 0 ) {
      System.out.println ( "No path found" ) ;
    } else {
      System.out.print ( "Path = " ) ;
      for ( int i = 0 ; i < pathSize ; ++ i ) {
        switch(path[i]){
        default:
        case north: c='N'; break;
        case east:  c='E'; break;
        case south: c='S'; break;
        case west:  c='W'; break;
        }
        System.out.print ( c ) ;
      }
      System.out.println ( " " ) ;
    }
  }


  protected void print ( String prefix, int y, int x ) {
    System.out.print ( prefix ) ;
    System.out.print ( " " ) ;
    System.out.print ( y ) ;
    System.out.print ( " " ) ;
    System.out.print ( x ) ;
    System.out.println ( " " ) ;
  }

  /**
   * 地図をプリントする
   *
   * 入力:Sting title: 地図の表題
   */
  public void print ( String title ) {
    char next, c ;

    System.out.println ( " " ) ;
    System.out.println ( title ) ;
    print ( "Y range", yLow(), yHigh()) ;
    print ( "X range", xLow(), xHigh()) ;

    for ( int y = yHigh ; y >= yLow ; -- y ) {
      if ( y >= 0 ) {
        System.out.print ( " " ) ; // handle leading sign
      }
      System.out.print ( y ) ;
      System.out.print ( ":" ) ;
      next    = ' ' ;

      for ( int x = xLow ; x <= xHigh ; ++ x ) {
        if (( this.y == y ) && ( this.x == x )) {
          // put [] around current position
          System.out.print ( '[' ) ;
          next = ']' ;
        } else {
          System.out.print ( next ) ;
          next = ' ' ;
        }

        switch (get(y,x)) {
        default:       c = '?' ; break;
        case unknown:  c = '.' ; break;
        case obstacle: c = 'O' ; break;
        case traveled: c = '+' ; break;
        }

        System.out.print (c) ;
      }
      System.out.println ( next ) ;
    }
  }
}


printableMazeMapクラスは地図作りのアプリケーションに用いられますが、下に示されるMazeMapTest1を用いて開発されます。これはJ-Botを迷路の隅々まで移動させずにテストするためにプログラム中で移動させパスを作ります。このクラスは探索と結果のアプリケーションには必要ありませんが、新しいパス探索アルゴリズムを開発しようとするときに計りしれないほど貴重なものです。

MapMazeTaskクラスをテストする
import stamp.core.*;
import stamp.util.os.* ;
import JBot.* ;

/**
 * MapMazeTaskクラスをテストする
 * 
 * J-Botは地図を作る間迷路を探索する
 */

public class MazeMapTest1 {
  public static void main () {
    printableMazeMap test = new printableMazeMap ( 20, 20 ) ;

    test.print("Initial");
    test.printPath();

    // 障害物を置いて地図を作る
/*
    for ( int i = 0 ; i < 2 ; ++ i ) {
      test.move(mazeMap.north);
      test.markTraveled();
      test.markObstacle(mazeMap.east);
      test.markObstacle(mazeMap.west);
    }

    test.move(mazeMap.north);
    test.markTraveled();
    test.markObstacle(mazeMap.north);
    test.markObstacle(mazeMap.west);
    test.move(mazeMap.east);
    test.markTraveled();
    test.markObstacle(mazeMap.north);
    test.markObstacle(mazeMap.east);
*/

    test.move(mazeMap.north);
    test.markTraveled();
    test.markObstacle(mazeMap.north);
    test.markObstacle(mazeMap.east);
    test.markObstacle(mazeMap.west);
    test.move(mazeMap.south);
    test.markObstacle(mazeMap.east);
    test.markObstacle(mazeMap.south);

    test.print("Basic movement");
    test.printPath();
  }
}


課題9-3

  1. computeBacktrackPathメソッドは初期探索で奥行きを得ます。他の探索アルゴリズムを実装しなさい。例えば、広い初期探索は非常に複雑ですが、異なった結果をもたらすでしょう。頑張れば直近の未探索場所を探すことが出来るでしょう。

  2. J-Botが移動するパスをEEPROMに記録するように迷路探索プログラムを修正しなさい。この情報をメッセージウィンドウ上に表示するプログラムを作りなさい。J-Botは後にPCに接続する必要があることを思い出してください。
問題9

  1. 迷路中での一巡はなんだろうか? 循環パスと異なるのだろうか?

  2. 単純と複雑な迷路の違いはなんだろうか?

  3. 右手の法則を用いたときにJ-Botは何故迷路を脱出するのに失敗するのだろうか?

  4. センサー検出の遅れはアルゴリズムと動作にいかに影響を与えるのだろうか?

実験9終わり