この記事の詳しい内容には毎日新聞の「ロボットはどう動く」ページから有料記事に進むことで読めます。
目次
概要
このロボットは、下図に示すルール通りに動いて、ゴールを目指します。ルールを見てロボットの動きを考え、進む道をたどってみましょう。
ルールは「はじまり」を表す黒丸から「前進する」の円に矢印が引かれているので、まずは前進します。そして、もし壁が近づいたら「右に曲がる」状態になります。右に曲がると目の前に壁がなくなるのでまた前進します。ゴールに着いたら止まります。
ここで問題です。下図の道を進んだ場合、ロボットは(1)から(4)のどのゴールに着くでしょう?
手で書いて考える
上記ルールがしっかり頭に入ってさえいれば、答えは簡単に分かります。ロボットは、
- スタート地点から前進を開始し
- 少し行くと壁に近づくので、右に曲がり
- また前進し
- 少し行くとまた壁に近づくので、右に曲がり
- また前進すると
- ゴールに着く
という動きをするので、答えは(3)です。
では、これをプログラミングで表現するにはどうすればよいのでしょう?
論理を考える
このお題で特徴的なのは、ロボットは白い道の上を移動するということです。これは逆に言うと、道でないところは(壁があるので)通れないということです。
またロボットは、自分の前に何もないときはそのまま前進し、壁が近づくと右に回る、という動きをします。これは、ロボットは自分の直前だけが見えるカメラを持っているようなものです。ロボットは、自分の直前だけを見て、そこに何もないと前進し、壁があると右に回るという動作をする、と考えることができます。
ロボットの直前という場所はどう表せるか? プログラミングではよくグリッドが利用されます。
道の地図をグリッドで表す
お題の地図は、下に示す11 x 8のグリッドで表すことができます。グレーのマスは行けない場所、白いマスは移動できる場所、淡いピンクのマスはゴールの場所です。
ロボットは自分の直前だけを見て次の動作を決めるので、今ロボットがいるマスの1つ上を調べれば、前進するか右に回るかが決められます。
11 x 8 のグリッドはプログラミングで作成することができます。そのときには、下図に示すように、各マスに地番(行番号,列番号)をつけることができます。ロボットのスタート位置は(6,5)なので、次の動作を決めるにはその1つ上の(5,5)のマスを調べればよいわけです。
グリッドはJavaScriptの2次元配列で表すことができます。そのとき配列要素に、’壁’や’道’、’ゴール’などの情報を持たせると、その位置に特性を持たせることができます。次のmap2dは11 x 8 の2次元配列です、0で壁(行けない場所)を、1で道、2でゴールを表すことで、お題の道の地図を表現しています。
const map2d = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
マスの特性へのアクセス
map2dを次のように使うと、地図の地番に当たるマスの特性を調べることができます。
print(map2d[6][5]); // 1 => 道
print(map2d[1][9]); // 2 => ゴール
print(map2d[2][5]); // 0 => 壁
ロボットが今いるマスがmap2d[r][c]だとすると、1つ上のマスの特性にはmap2d[r-1][c]でアクセスできます。実際にそこに行かなくても、2次元配列につづける[][]内の値を変えることで、そのマスが壁か道かゴールかを調べることができます。これはプログラミングでこしらえたカメラのようなものです。調べたマスの特性が1ならそこに前進し、0なら右に回るようにします。2ならそこがゴールだと判断できます。
print(map2d[6][5]); // 1 => スタートの地番
print(map2d[5][5]); // 1 => 1つ上の地番は1なので、そこに前進できる
前進するとは?
前進するとは、今向かっている方向のまま前に進むことです。これは至極当たり前のように聞こえますが、プログラミングしていると、混乱するときがあります。まず、このロボットには”前”と”後ろ”があります。たとえばボールを動かす場合、ボールには前も後もないので、右に曲がるときには、右に移動させれば済みますが、このロボットには前後があるので、前進するときには、ロボットの前が向いている方向に進める必要があります。これは、ロボットには”今どの方向を向いているのか”という情報が要るということです。
今向いている方向が上なら、次に進むべき”前のマス”は今いるマスの1つ上のマスとなり、今向いている方向が上なら、次に進むべき”前のマス”は今いるマスの1つ右のマスとなります。
右に曲がるとは?
右に曲がるとは、今向いている方向から右に90度向きを変えることです。右に曲がるのだから右に進むのだろうと思いますが、これはロボットの主観で、たとえば客観的に右に進んでいるロボットが壁の近くで右に曲がると、向いているのは下になります。この場合にも、ロボットが”今自分はどの方向を向いているのか”を知っていることが役立ちます。
OOPが役立つ
今向いている方向に進むとか、右に曲がったのに下に進むとか、考えすぎるとわけが分からなくなるかもしれませんが、これらは全部、ロボットの身になって考えると解決できます。ロボットの身になるということはつまり、ロボットをOOP(オブジェクト指向プログラミング)の方法で作成するということです。
ロボットが今向いている方向が上なら、右に曲がるには、上方向の右に90度回った向き(客観的に見ると右)に進めばよく、ロボットが今向いている方向が右なら、右に曲がるには、右方向の右に90度回った向き(客観的に見ると下)に進めばよいわけです。
とは言え、ロボットの主観的な右と、客観的に見た右を同じ言葉で表すと混乱するので、ロボットが今向いている方向は絶対的な東西南北で表す方法もあります。
全コード
以下は、お題の問題を解決するプログラムの全コードです。
// ロボットが移動するグリッドを作成する
// 0はグレー、そこには移動できない
// 1は白、そこに移動できる
// 2は淡いピンク、ゴール(停止する)
const map2d = [
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
[0, 2, 1, 1, 1, 1, 1, 1, 1, 1, 0],
[0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
let robot; // Robotクラスのインスタンス
let robotImages;
function preload() {
const robotNImage = loadImage('images/robotN.png');
const robotEImage = loadImage('images/robotE.png');
const robotSImage = loadImage('images/robotS.png');
const robotWImage = loadImage('images/robotW.png');
robotImages = [robotNImage, robotEImage, robotSImage, robotWImage];
}
function setup() {
createCanvas(500, 400);
imageMode(CENTER);
noStroke();
// ロボットを作成
robot = new Robot(6, 5, map2d, robotImages);
// [GO!]ボタン
const goButton = setButton('GO', {
x: 340,
y: 370
});
// マウスプレスでスタート
goButton.mousePressed(() => {
// 2秒に1回、ロボットは自分の前のセルを調べ、それに応じた行動を取る
const timerID = window.setInterval(() => {
robot.checkNextCellAndMove();
robot.update();
}, 2000);
// ロボットにタイマーIDを教える
robot.setTimer(timerID);
});
}
function draw() {
background(223, 233, 181);
drawMapGrid();
robot.display();
}
function drawMapGrid() {
// 格子ブロックのひな型コードの短縮版
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 11; c++) {
// 描画する(x,y)位置、
const x = 25 + c * 40;
const y = 25 + r * 40;
const num = map2d[r][c]
if (num === 0) {
fill(223, 233, 181);
}
else if (num === 1) {
fill(255);
}
else if (num === 2) {
fill(212, 152, 150);
}
rect(x, y, 50, 50);
}
}
}
// Robotクラス
class Robot {
constructor(r, c, map, imgs) {
this.step = 40; // ステップ値はdrawMapGrid()関数のw値と同じにする
this.r = r; // 今いる行数
this.c = c; // 今いる列数
this.map = map; // 二次元配列
this.timer = null;
this.direction = 'north';
this.offsetX = 25; // オフセット値はdrawMapGrid()関数のオフセット値と同じにする
this.offsetY = 30;
this.x = this.c * this.step + this.step / 2 + this.offsetX;
this.y = this.r * this.step + this.step / 2 + this.offsetY;
this.imgs = imgs;
this.currentImage = 0
}
// 描画
display() {
image(this.imgs[this.currentImage], this.x, this.y);
}
// 更新 => ロボットはthis.rかthis.cに変化があると移動する
update() {
this.x = this.c * this.step + this.step / 2 + this.offsetX;
this.y = this.r * this.step + this.step / 2 + this.offsetY;
}
// ゴール時にタイマーをクリアするためのタイマーIDを設定する
setTimer(timer) {
this.timer = timer;
}
// 前に進む: 今向かっている方向のまま進むこと
goForward() {
switch (this.direction) {
case 'north':
this.r--; // 方向がnorthなら上に1つ進む
break;
case 'east': // 方向がeastなら右に1つ進む
this.c++;
break;
case 'south': // 方向がsouthなら下に1つ進む
this.r++;
break;
case 'west':
this.c--; // 方向がwestなら左に1つ進む
break;
}
}
// 右に曲がる:今向かっている方向から90度右に方向を変えること
turnToRight() {
switch (this.direction) {
case 'north':
this.direction = 'east'; // 方向がnorthならeastに変える
this.currentImage = 1; // 右が前のイメージ
break;
case 'east':
this.direction = 'south'; // 方向がeastならsouthに変える
this.currentImage = 2; // 下が前のイメージ
break;
case 'south':
this.direction = 'west'; // 方向がsouthならwestに変える
this.currentImage = 3; // 左が前のイメージ
break;
case 'west':
this.direction = 'north'; // 方向がwestならnorthに変える
this.currentImage = 0; // 上が前のイメージ
break;
}
}
// 次のセルを調べ、今の進行方向に合わせて進むか曲がるかする
checkNextCellAndMove() {
let nextCell; // 次の進もうとするセル
switch (this.direction) {
// 今の方向がnorthなら次のセルは1つ上のセル
case 'north':
nextCell = this.map[this.r - 1][this.c];
break;
// 今の方向がeastなら次のセルは1つ右のセル
case 'east':
nextCell = this.map[this.r][this.c + 1];
break;
// 今の方向がsouthなら次のセルは1つ下のセル
case 'south':
nextCell = this.map[this.r + 1][this.c];
break;
// 今の方向がwestなら次のセルは1つ左のセル
case 'west':
nextCell = this.map[this.r][this.c - 1];
break;
}
// 次のセルの数値によって取るべきアクションを決める
if (nextCell === 0) {
print('一歩先は壁なので右に曲がる');
this.turnToRight();
}
else if (nextCell === 1) {
print('一歩先は道なのでそのまま前進');
this.goForward();
}
else if (nextCell === 2) {
print('一歩先はゴールなので前進して止まる');
this.goForward();
this.stop();
}
}
// 止まる => this.rやthis.cを変更しないので移動しない
stop() {
print('ゴールに着いた');
// タイマーをクリア
window.clearInterval(this.timer);
}
}
function setButton(label, pos) {
const button = createButton(label);
button.size(100, 30);
button.position(pos.x, pos.y);
return button;
}
ボタンのクリックでロボットが行動を開始し、[コンソール]にロボットが次に進もうとするマスの情報が表示されます。下図をクリックすると、実行画面が開きます。