この記事の詳しい内容には毎日新聞の「メモのかくれたメッセージ」ページから有料記事に進むことで読めます。
概要
メッセージが隠されたメモがあります。ある決まりにしたがってメモを読むと、隠されたメモを解読することができます。
下図は例のメモです。
決まりは下図のフローチャートで表されます。
例のメモを読んでみると、
1回め:
- 今いるマスは空白?
- スタートのマスは空白でないので、文字「り」を読む
- 右のマスへ移動する
2回め:
- 今いるマスは空白?
- 右のマスは空白なので、下のマスへ移動する
- 右のマスへ移動する
3回め:
- 今いるマスは空白?
- 右のマスは空白でないので、文字「す」を読む
- 右のマスへ移動する
となり、隠されたメッセージは「りす」であることが分かります。
ここで問題です。下図のメモに隠されたメッセージは何でしょうか?
論理を考える
マス目を手で描き、フローチャートの決まりにしたがってマスを移動すると、問題の答えは「あき」だと分かります。
移動は次のように行います。
1回め:
- 今いるマスは空白?
- スタートのマスは空白でないので、文字「あ」を読む
- 右のマスへ移動する
2回め:
- 今いるマスは空白?
- 右のマスは空白でないので、文字「き」を読む
- 右のマスへ移動する
3回め:
- 今いるマスは空白?
- 右のマスは空白なので、下のマスへ移動する
- 右のマスへ移動する
では、これをプログラミングで行うにはどうすればよいのでしょう?
2次元配列
プログラミングでは、マス目(格子模様、グリッド)の表現に2次元配列がよく用いられます。2次元配列とは、全体を囲む大きな配列の中に、要素として小さな配列を含む配列を言います。次の配列arry2dは簡単な2次元配列の例です。この配列は0番めの要素として配列[1,2,3]を、1番めの要素として配列[4,5,6]を持っています。
let array2d = [[1,2,3],[4,5,6]];
2次元配列はまた、次のように記述されることがよくあります。
let array2d = [
[1, 2, 3],
[4, 5, 6]
];
2次元配列の要素(含まれる配列)を出力してみると、それに含まれる配列であることが分かります。具体的に言うと、array2d[0]は[1, 2, 3]で、array2d[1]は[4, 5, 6]です。これは、2次元配列の要素は、グリッドの横向きの行が表せるということです。
array2d[0]は[1, 2, 3]なので、[1,2,3][0]で1が参照できるように、array2d[0][0]で1が参照できます。これは、2次元配列に[][]をつづけることで、2次元配列の要素に含まれる要素にアクセスできるということです。
array2dに含まれる1にアクセスするには、まずarray2dに[0]をつづけて、1を含む0番めの要素を参照し、そして1のインデックス番号である0を2つめの[]に使用します(array2d[0][0])。
すると、グリッドの各マスは2次元配列につづける[][]で、ちょうど住所の地番のようにアクセスできるようになります。
コードでは次のように記述できます。
let arr2d = [
[1, 2, 3],
[4, 5, 6]
];
// arr2dの行を出力
for (let r = 0; r < 2; r++) {
print(arr2d[r]);
// 0番めの要素は配列[1, 2, 3]
// 1番めの要素は配列[4, 5, 6]
}
// arr2dの行を下に1つ進むたびに
for (let r = 0; r < 2; r++) {
// arr2dの列を横に進む
for (let c = 0; c < 3; c++) {
print(arr2d[r][c]); // 1,2,3,4,5,6
}
}
// 1はarr2dの0番めの配列の0番めの要素
print(arr2d[0][0]); // 1
// 6はarr2dの1番めの配列の2番めの要素
print(arr2d[1][2]); // 6
今いるマスの右や下のマス
このarr2d[][]と、要素のインデックス番号を参照する変数を使用すると、今いるマスの1つ右のマスや1つ下のマスに相当する要素にアクセスできます。
たとえば、上記arr2dで2はarr2d[0][1]で参照できるので、変数cを0、rを1とすると、arr2d[c][r]で参照できます。すると、rに1を加えることで同じ行の1つ右の要素(3)にアクセスできます。また、cに1を加えることで、次の行の同じ列の要素(5)にアクセスできます。
// 2はarr2d[0][1]
let c = 0, r = 1;
print(arr2d[c][r]); // 2
// 2の1つ右 rに1を足す
print(arr2d[c][r + 1]); // 3
// 2の1つ下 cに1を足す
print(arr2d[c + 1][r]); // 5
論理を実装した基本的なプログラム
ここまでの話にもとづくと、プログラムの基本的な論理は、次のように表せます。
・メモは2次元配列で表す
・プログラムは、フローチャートの下図の部分を3回繰り返す
例のメモは、次の2次元配列で表すことができます。空白は空の文字列(”)で表せます。
// メモを2次元配列で表す
let memosA = [
['り', '', 'み'],
['', 'く', 'す'],
['', 'あ', '']
];
変数には、2次元配列のインデックス番号を追跡するもの2つが必要です。次のコードでは、rowとcolumnという名前にしています。また読み取った文字は覚えておく必要があるので、専用の配列(messages)も用意します。
// 2次元配列に使用する行と列のインデックス番号
let row = 0,
column = 0;
// 解読した文字を追加する配列
let messages = [];
上記のフローチャートの一部を関数で表すことを考えます。例のメモに加え、問題のメモも扱えるように(できるだけ汎用的に)したいので、関数にはメモを表す2次元配列を渡すようにします。そして、フローチャートの図に沿ってコードを考えます。
すると、たとえば次のような関数が記述できます。
// メモの配列を受け取り、変数rowとcolumnを操作してメッセージを探り、
// messages配列に追加する
function finedMessage(arr) {
// 今対象としている文字
const target = arr[row][column];
// 空白なら
if (target === '') {
// 行を下に1つ移動
row++;
}
else {
// 空白でないなら、その文字を配列に追加
messages.push(target);
}
// 空白であるかないかにかかわらず、列を右に1つ移動
column++;
}
このfinedMessage()関数は、今いるマスを特定する変数rowとcolumnに適切な数値が代入されていることを前提としています。rowとcolumnは最初0で初期化しているので、memosA配列が渡された場合には、変数targetに、arr[0][0]にある’り’が代入されます。
つづくif文では、このtargetが空白かどうかを調べ、空白ならrowに1を加えます。これによりrowは1になるので、次の呼び出しで arr[row][column]を調べるときには、memosAの2行めの配列を参照することになります。
空白でないなら、targetをmessages配列に追加します。そして、targetが空であるかないかに関係なく、columnに1を加えることで、次の呼び出しでarr[row][column]を調べるときに、1つ右の要素を参照するようにします。
このfinedMessage()関数を3回呼び出すと、上図で示したフローチャートの一部を3回繰り返すことになります。
以下は全コードです。setup()関数内でfinedMessage()関数にmemosAを渡すと、例のメモが解読でき、memosBを渡すとお題のメモが解読できます。
// メモを2次元配列で表す
let memosA = [
['り', '', 'み'],
['', 'く', 'す'],
['', 'あ', '']
];
let memosB = [
['あ', 'き', '', ''],
['', '', 'さ', 'た'],
['', 'こ', '', 'め']
];
// 2次元配列に使用する行と列のインデックス番号
let row = 0,
column = 0;
// 解読した文字を追加する配列
let messages = [];
function setup() {
noCanvas();
// 3回繰り返す
for (let i = 0; i < 3; i++) {
// finedMessage()関数にメモの配列を渡して隠されたメッセージを探る
finedMessage(memosA);
}
// 結果を出力
print(messages);
}
// メモの配列を受け取り、変数rowとcolumnを操作してメッセージを探り、
// messages配列に追加する
function finedMessage(arr) {
// 今対象としている文字
const target = arr[row][column];
// 空白なら
if (target === '') {
// 行を下に1つ移動
row++;
}
else {
// 空白でないなら、その文字を配列に追加
messages.push(target);
}
// 空白であるかないかにかかわらず、列を右に1つ移動
column++;
}
下図はそのそれぞれの出力結果を示しています。
視覚化
次は、上記の基本的な論理を目で見えるようにしていきましょう。方法はいくつも考えられますが、以下では、Mapオブジェクトに文字データを持たせ、それを2次元配列に入れて使用することにします。
Mapオブジェクトを要素に持つ2次元配列は、文字を要素に持つ2次元配列(前出のmemosAやmemosB)と同じ構造にする必要があります。次のsetMap()関数は2次元配列を引数にとり、行数と列数に相当する要素数を調べて、同じ構造を作成しています。
// 与えられた2次元配列と同じ構造を持つ、Mapオブジェクトの2次元配列を作成して返す
function setMap(arr2d) {
let arr = []; // 呼び出し元に返す配列
// 渡された2次元配列の外側の配列の要素数(行数)分だけ繰り返す
for (let i = 0; i < arr2d.length; i++) {
// 内側の配列を作成
arr[i] = [];
// 渡された配列の内側の最初の配列の要素数(列数)分だけ繰り返す
for (let j = 0; j < arr2d[0].length; j++) {
// Mapオブジェクトを作成
const map = new Map();
map.set('letter', arr2d[i][j]); // 文字を割り当てる
map.set('isMessage', false); // メッセージであるかどうか
// 返す配列にMapオブジェクトを入れていく
arr[i][j] = map;
}
}
return arr;
}
この関数に文字の2次元配列を渡すと、Mapオブジェクトの2次元配列が返されます。
let memosA = [
['り', '', 'み'],
['', 'く', 'す'],
['', 'あ', '']
];
const mapArray = setMap(memosA);
下図はMapオブジェクトの2次元配列をprint()関数で出力し、要素の配列をクリックして展開したところです。Mapオブジェクトのletter属性には2次元配列からの文字を割り当て、isMessage属性には全部falseを設定しています。
finedMessage()関数は、文字の2次元配列でなくMapオブジェクトの2次元配列に変わるので、それに応じた修正を行います。またMapオブジェクトから取り出した文字がメッセージであった場合には、その文字を配列に追加する代わりに、そのMapオブジェクトのisMessage属性をtrueにします(基本的な論理で使用した配列messagesは使用しません)。
function finedMessage(arr) {
// ターゲットはMapオブジェクト
const targetMap = arr[row][column];
// 文字を得る
const letter = targetMap.get('letter');
// 空白なら
if (letter === '') {
row++;
}
else {
// 空白でないなら、isMessageをtrueにする(messages配列に追加する代わり)
targetMap.set('isMessage', true);
}
column++;
}
グリッドや文字の描画には、「格子ブロックのひな型コード」が利用できます。このひな型コードに、Mapオブジェクトを要素に持つ2次元配列の行数(arr2d.length)と列数(arr2d[0].length)を与え、矩形の空きやオフセット量など変数を設定すると、グリッドが描けます。
// 矩形を並べてグリッドを描き、かくれたメッセージの文字には赤丸を描いて、メモの文字を描く
function drawMapGrid(arr2d) {
// 格子ブロックのひな型コード
const rows = arr2d.length;
const columns = arr2d[0].length;
const gutter = 0;
const w = 80;
const h = w;
const offsetX = 10;
const offsetY = 10;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < columns; c++) {
// 描画する(x,y)位置
const x = offsetX + c * (gutter + w);
const y = offsetY + r * (gutter + h);
rect(x, y, w, h);
// Mapオブジェクトの情報を利用
// isMessageがtrueなら(空白でなく文字を持っているなら)
if (arr2d[r][c].get('isMessage')) {
// 赤丸用の色
stroke('red');
// 円を描く
ellipse(x + w / 2, y + h / 2, 70);
// 線の色を元に戻す
stroke(0);
}
// Mapオブジェクトが持つ文字を描画する(''は描画されない)
const txt = arr2d[r][c].get('letter');
text(txt, x + 10, y + 60);
}
}
}
この関数コードでは、MapオブジェクトのisMessage属性を調べ、それがtrueの場合にはそのMapオブジェクトのletter属性の文字が隠されたメッセージであるということなので、赤い丸を描いています。これによりメッセージでないほかの文字と区別できます。
そして最後、矩形と赤丸を描いた後、Mapオブジェクトのletter属性の値を調べてそれを適切に見える位置に描いています。
ここまで述べてきた関数を順に呼び出すと、p5.jsのキャンバスにメモが描画でき、隠されたメッセージに当たる文字が赤丸で囲まれます。
const mapArray = setMap(memosA);
// 3回繰り返す
for (let i = 0; i < 3; i++) {
finedMessage(mapArray);
}
// 描画する
drawMapGrid(mapArray);
全コード
以下は、setMap()関数にお題のメモ(memosB)を渡したときの全コードです。
// メモを2次元配列で表す
let memosA = [
['り', '', 'み'],
['', 'く', 'す'],
['', 'あ', '']
];
let memosB = [
['あ', 'き', '', ''],
['', '', 'さ', 'た'],
['', 'こ', '', 'め']
];
let row = 0, column = 0;
function setup() {
createCanvas(400, 300);
background(220);
textSize(60);
const mapArray = setMap(memosB);
// 3回繰り返す
for (let i = 0; i < 3; i++) {
finedMessage(mapArray);
}
// 描画する
drawMapGrid(mapArray);
}
// 与えられた2次元配列と同じ構造を持つ、Mapオブジェクトの2次元配列を作成して返す
function setMap(arr2d) {
let arr = []; // 呼び出し元に返す配列
// 渡された2次元配列の外側の配列の要素数(行数)分だけ繰り返す
for (let i = 0; i < arr2d.length; i++) {
// 内側の配列を作成
arr[i] = [];
// 渡された配列の内側の最初の配列の要素数(列数)分だけ繰り返す
for (let j = 0; j < arr2d[0].length; j++) {
// Mapオブジェクトを作成
const map = new Map();
map.set('letter', arr2d[i][j]); // 文字を割り当てる
map.set('isMessage', false); // メッセージであるかどうか
// 返す配列にMapオブジェクトを入れていく
arr[i][j] = map;
}
}
return arr;
}
// 矩形を並べてグリッドを描き、かくれたメッセージの文字には赤丸を描いて、メモの文字を描く
function drawMapGrid(arr2d) {
// 格子ブロックのひな型コード
const rows = arr2d.length;
const columns = arr2d[0].length;
const gutter = 0;
const w = 80;
const h = w;
const offsetX = 10;
const offsetY = 10;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < columns; c++) {
// 描画する(x,y)位置
const x = offsetX + c * (gutter + w);
const y = offsetY + r * (gutter + h);
rect(x, y, w, h);
// Mapオブジェクトの情報を利用
// isMessageがtrueなら(空白でなく文字を持っているなら)
if (arr2d[r][c].get('isMessage')) {
// 赤丸用の色
stroke('red');
// 円を描く
ellipse(x + w / 2, y + h / 2, 70);
// 線の色を元に戻す
stroke(0);
}
// Mapオブジェクトが持つ文字を描画する(''は描画されない)
const txt = arr2d[r][c].get('letter');
text(txt, x + 10, y + 60);
}
}
}
function finedMessage(arr) {
// ターゲットはMapオブジェクト
const targetMap = arr[row][column];
// 文字を得る
const letter = targetMap.get('letter');
// 空白なら
if (letter === '') {
row++;
} else {
// 空白でないなら、isMessageをtrueにする(messages配列に追加する代わり)
targetMap.set('isMessage', true);
}
column++;
}