この記事の詳しい内容には毎日新聞の「ロボットが止まるマスは?」ページから有料記事に進むことで読めます。
目次
概要
下図のルートを移動するロボットがいます。ロボットは今自分がいるマスを調べ、それがチェック模様のマスなら2マス進み、それ以外なら1マス進むことを、ゴールまでつづけます。さて、ロボットは、ゴールに着くまでに何回チェックのマスに止まるでしょうか?
論理を考える
このお題は、マスを1つ進むとか2つ進むとか、結構ややこしいです。お題が何を言っているのか分かっていないと話にならないので、まずは頭でしっかり理解しておきましょう。
頭で理解する
上図をマウスでなぞりながら頭で考えてみると、
- スタートのマスはチェック模様でないので1マス進む
- 進んだ先のマスはチェック模様でないので1マス進む
- 進んだ先のマスはチェック模様なので2マス進む
- 進んだ先のマスはチェック模様なので2マス進む
- 進んだ先のマスはチェック模様でないので1マス進む
- 進んだ先のマスはチェック模様でないので1マス進む
- 進んだ先のマスはチェック模様なので2マス進む
- 進んだ先のマスはゴールなので終了
となります。3,4,7でチェック模様のマスに止まっているので、答えは3回です。
プログラミングのとっかかりを探る
自分の頭で考える分には、面倒くさがりさえしなければ、簡単に答えは得られます。問題はこれをどうやってプログラミングで表現するか、です。
上で考えた頭の働きを疑似コードにすると、
今いるマスを調べる
もしそれがチェック模様のマスならば
2マス進む
そうでないなら
1マス進む
これをゴールに着くまで繰り返す
となるでしょう。繰り返すということなので、「10:大小の皿を片づける」で見たループやタイマーが利用できるかもしれません。
また、スタートとゴールがあり、その間にチェックと白のマスが並んでいるので、マスの位置は別にして、スタートからゴールまでのマスは下図の順番で並んでいると考えることができます。
順番があり構成要素の数が分かっていると、並びは配列で表すことができます。上の絵の代わりに文字列で表すと次のようになります。
let cells = ['start', 'white', 'check', 'white', 'check', 'check', 'white', 'white', 'check', 'white', 'goal'];
このcells配列を使うと、たとえばcells[0]で’start’が参照でき、cells[1]で1つめの’white’が参照できます。変数currentIndexが0だとすると、’start’はcells[currentIndex]で参照でき、その右隣りの’white’はcells[currentIndex + 1]で参照できます。
currentIndex = 0;
print(cells[currentIndex]); // start 0番めの要素
print(cells[currentIndex + 1]); // white 1番めの要素
そして、currentIndexに1足した値を再度currentIndexに代入すると、cells[currentIndex]で、前のcurrentIndex位置の1つ右隣りの要素が参照できます。これは今の場合、1マス進むことと考えることができます。
currentIndex = 0;
print(cells[currentIndex]); // start
currentIndex = currentIndex + 1;
print(cells[currentIndex]); // white startから1マス進んだ
同様に、currentIndexに2を足して再度currentIndexに代入することは、2マス進むことと考えることができます。
currentIndex = 0;
print(cells[currentIndex]); // start 9番めの要素
currentIndex = currentIndex + 2;
print(cells[currentIndex]); // check 2マス進んだ
ここまでをコードにまとめると、次のように記述できます。
let cells = ['start', 'white', 'check', 'white', 'check', 'check', 'white', 'white', 'check', 'white', 'goal'];
let currentIndex = 0;
const currentCell = cells[currentIndex];
if (currentCell === 'check') {
print(currentCell, '2マス進む');
currentIndex = currentIndex + 2;
}
else {
print(currentCell, '1マス進む'); // start 1マス進む
currentIndex = currentIndex + 1;
}
変数currentIndexは0なので、変数currentCellには、cells[0]により’start’が割り当てられます。 currentCellは’check’でないのでif文の条件を満たさず、elseの{と}の間のコードが実行されます。その結果[コンソール]に”start 1マス進む”が表示され、 currentIndex = currentIndex + 1によってcurrentIndexは1になります。
繰り返しはどうする?
前のコードは”start 1マス進む”が表示されるだけで、次に進みません。次に進むには、新しくなった(1加算された)currentIndexをコードの最初に戻して、cells[currentIndex]から次のcurrentCell、つまり’white’を参照する必要があります。
最初に戻ることは繰り返すことです。繰り返しには、前述したようにループやタイマーが利用できます。
whileループ
まずは、whileループで考えてみましょう。while文では、()内の条件がtrueである間、{と}の間に記述したコードが繰り返し実行されます。このとき気を付けなければいけないのが、()内の条件がいつか必ずfalseになるようにすることです。trueのまま変わらなければ無限ループにおちいります。
いつか必ずfalseになる条件とは、今の場合は、ゴールに到着していない間、です。’goal’はcells[11]にあり、currentIndexに1か2を足して配列要素を右向きに参照していくので、条件には currentIndex <= 10 が記述できます。cellsの現在のインデックス位置が10以下である間はwhileの{と}の間のコードを実行し、cellsの現在のインデックス位置が10より大きくなったら(つまり11)、それは'goal'なのでループを抜けます。
while (currentIndex <= 10) {
const currentCell = cells[currentIndex];
if (currentCell === 'check') {
print(currentCell, '2マス進む'); // check 2マス進む
currentIndex = currentIndex + 2;
}
else {
print(currentCell, '1マス進む'); // start 1マス進む,white 1マス進む,goal 1マス進む
currentIndex = currentIndex + 1;
}
}
下図はこの実行結果です。
タイマーを使う
上記コードは結果を見てもうまくいっており、3回というお題の答えもカウントできます。しかし残念ながら一瞬で終わってしまいます。マスを進む”移動”には時間、つまり相当の間(ま)が必要なので、ロボットの移動を表現したい場合には、一瞬で終わるwhileループの方法は適当ではないと言えます。
そこで思い浮かぶのは、「10:大小の皿を片づける」でも採った、タイマーを使う方法です。一定時間おきにcurrentIndexを調べ、1か2か足してマスを進めて、currentIndexが10以上になったらタイマーをクリアします。
次のコードはそのための関数です。この関数はcells[currentIndex]、つまりcellsの要素を受け取ります。またこの関数では、お題の「今いるマスがチェック模様かそうでないか」より細かく、白いマスかスタートかゴールかも調べています。その方が自然に思え、またスタートやゴールに別の特徴を持たせたいときにもそのまま使えるからです。
function goNext(currentCell) {
if (currentCell === 'white') {
print(currentCell, '1マス進む');
currentIndex = currentIndex + 1;
}
else if (currentCell === 'check') {
print(currentCell, '2マス進む');
currentIndex = currentIndex + 2;
}
else if (currentCell === 'goal') {
print(currentCell, 'ゴール到着');
}
else if (currentCell === 'start') {
print(currentCell, '1マス進む');
currentIndex = currentIndex + 1;
}
}
この関数は次のように使用します。
const timerID = window.setInterval(() => {
// タイマーにも終了する条件が必要
if (currentIndex >= 10) {
print('終了');
window.clearInterval(timerID);
}
goNext(cells[currentIndex]);
}, 2000);
[コンソール]にはwhileループのときと同様の結果が表示されますが、今度は2秒ずつ間をおいて表示されます。
論理は以上です。
視覚化
本稿の視覚化にあたっては、ロボットやマスを表すイメージの描画に加え、ロボットの移動も行います。ロボットはお題の通り、チェック模様のマスに止まったら2マス移動し、ほかのマスでは1マス移動します。ゴールのマスに着いたら止まります。ただし移動はアニメーションではなく、”瞬間移動”です。
位置のデータが必要
本稿最初の図を見て分かるのは、白やチェック模様のマスを置く位置を決める必要があるということです。これは自分で決めるよりしかたありません。p5.jsのキャンバスはその左上隅を原点とするので、分かりやすいのは、描画ツールの描画面の左上隅とキャンバスの左上隅が揃っていると見なし、マスを配置してその位置をメモしていく方法です。
サンプルプログラム
次に示すサンプルコードでは、下図の画像ファイルを使用しています。マスとマスをつなぐ線はp5.jsのline()関数で描きます。
以下はそのコードです。
let cells; // cell,pos,imageプロパティを持つオブジェクトの配列
let currentIndex = 0;
let robotImage;
let robotPos;
function preload() {
const startImage = loadImage('images/start.png');
const whiteImage = loadImage('images/white.png');
const checkImage = loadImage('images/check.png');
const goalImage = loadImage('images/goal.png');
cells = [{ // 0番めのセル = スタートのマスを表す
cell: 'start',
pos: [105, 95],
image: startImage
}, { // 1番めのセル = 白いマスを表す
cell: 'white',
pos: [195, 80],
image: whiteImage
}, { // 2番めのセル = チェックのマスを表す
cell: 'check',
pos: [255, 120],
image: checkImage
}, {
cell: 'white',
pos: [195, 165],
image: whiteImage
}, {
cell: 'check',
pos: [115, 160],
image: checkImage
}, {
cell: 'check',
pos: [125, 220],
image: checkImage
}, {
cell: 'white',
pos: [205, 235],
image: whiteImage
}, {
cell: 'white',
pos: [255, 280],
image: whiteImage
}, {
cell: 'check',
pos: [185, 300],
image: checkImage
}, {
cell: 'white',
pos: [115, 335],
image: whiteImage
}, {
cell: 'goal',
pos: [185, 375],
image: goalImage
}];
robotImage = loadImage('images/robot.png');
}
function setup() {
createCanvas(400, 500);
// ロボットの最初の位置
robotPos = cells[currentIndex].pos;
// [GO!]ボタン
const goButton = setButton('GO!', {
x: 150,
y: 450
});
// マウスプレスでスタート
goButton.mousePressed(() => {
// 2秒おきにgoNext()関数を呼び出す
const timerID = window.setInterval(() => {
// cellsの最後まで行ったら終わり
if (currentIndex >= 10) {
print('終了');
window.clearInterval(timerID);
}
// 今のセルから次のセルに進む
goNext(cells[currentIndex]);
}, 2000);
});
}
function draw() {
background(220);
// バックの白い矩形
noStroke();
rect(50, 10, 300, 420);
// マスをつなぐ太い線
strokeWeight(10);
stroke(198, 163, 99);
// ゴールの次の線は引かないので、1減らす
for (let i = 0; i < cells.length - 1; i++) {
// cells配列を使ってマスをつなぐ太い線を描く
line(cells[i].pos[0] + 27, cells[i].pos[1] + 13, cells[i + 1].pos[0] + 27, cells[i + 1].pos[1] + 13);
// cells配列を使ってマスを描く
image(cells[i].image, cells[i].pos[0], cells[i].pos[1]);
}
// ゴールのセルを描く
image(cells[10].image, cells[10].pos[0] + 13, cells[10].pos[1] - 30);
// ロボットを描く
image(robotImage, robotPos[0] + 5, robotPos[1] - 60);
}
// currentオブジェクトから現在のセル(whiteやcheckなど)を調べ、
// それに応じてcurrentIndexに1や2を加算して、
// その新しいcurrentIndexを使ってロボットを1マスか2マス進める
function goNext(current) {
// 現在のセルを調べる
currentCell = current.cell;
if (currentCell === 'white') {
print(currentCell, '1マス進む');
currentIndex = currentIndex + 1;
}
else if (currentCell === 'check') {
print(currentCell, '2マス進む');
currentIndex = currentIndex + 2;
}
else if (currentCell === 'goal') {
print(currentCell, 'ゴール到着');
}
else if (currentCell === 'start') {
print(currentCell, '1マス進む');
currentIndex = currentIndex + 1;
}
// cellsの新しいcurrentIndex番めの位置にロボットを進める
robotPos = cells[currentIndex].pos;
}
function setButton(label, pos) {
const button = createButton(label);
button.size(120, 40);
button.position(pos.x, pos.y);
return button;
}
cells配列の要素の作成でコードが長くなっていますが、要素自体は難しくはありません。要素はObjectオブジェクトで、cellとpos、imageプロパティを持ちます。cellはそのマスのタイプで、’start’や’white’、’check’などを指定します。オブジェクトは全部で11個あり、マスの並び(‘start’, ‘white’, ‘check’, ‘white’, ‘check’,…’goal’)で並べます。posは前に決めたマスのxy位置です。imageプロパティにはpreload()関数で読み込んだマスのイメージを割り当てます。
cells = [
{ // 0番めのセル = スタートのマスを表す
cell: 'start',
pos: [105, 95],
image: startImage
},
{ // 1番めのセル = 白いマスを表す
cell: 'white',
pos: [195, 80],
image: whiteImage
},
...
ロボットの位置を進めるのは、goNext()関数の最後の行です。この1行により、新しいインデックス位置にあるマスのposプロパティをrobotPos変数が参照するようになります。
robotPos = cells[currentIndex].pos;
描画はdraw()関数で行います。ここではマスのオブジェクトのposプロパティを使ってマスを描くほか、その値を微調整して太い線の描画に利用しています。
// ゴールの次の線は引かないので、1減らす
for (let i = 0; i < cells.length - 1; i++) {
// cells配列を使ってマスをつなぐ太い線を描く
line(cells[i].pos[0] + 27, cells[i].pos[1] + 13, cells[i + 1].pos[0] + 27, cells[i + 1].pos[1] + 13);
// cells配列を使ってマスを描く
image(cells[i].image, cells[i].pos[0], cells[i].pos[1]);
}
// ゴールのセルを描く
image(cells[10].image, cells[10].pos[0] + 13, cells[10].pos[1] - 30);
// ロボットを描く
image(robotImage, robotPos[0] + 5, robotPos[1] - 60);
下図をクリックすると、ウィンドウに実行画面が表示されます。