プログラミング はじめの一歩 JavaScript + p5.js編
34:メモのかくれたメッセージ

この記事の詳しい内容には毎日新聞の「メモのかくれたメッセージ」ページから有料記事に進むことで読めます。

概要

メッセージが隠されたメモがあります。ある決まりにしたがってメモを読むと、隠されたメモを解読することができます。

下図は例のメモです。

決まりは下図のフローチャートで表されます。

例のメモを読んでみると、
1回め:

  1. 今いるマスは空白?
  2. スタートのマスは空白でないので、文字「り」を読む
  3. 右のマスへ移動する

2回め:

  1. 今いるマスは空白?
  2. 右のマスは空白なので、下のマスへ移動する
  3. 右のマスへ移動する

3回め:

  1. 今いるマスは空白?
  2. 右のマスは空白でないので、文字「す」を読む
  3. 右のマスへ移動する

となり、隠されたメッセージは「りす」であることが分かります。

ここで問題です。下図のメモに隠されたメッセージは何でしょうか?

論理を考える

マス目を手で描き、フローチャートの決まりにしたがってマスを移動すると、問題の答えは「あき」だと分かります。

移動は次のように行います。

1回め:

  1. 今いるマスは空白?
  2. スタートのマスは空白でないので、文字「あ」を読む
  3. 右のマスへ移動する

2回め:

  1. 今いるマスは空白?
  2. 右のマスは空白でないので、文字「き」を読む
  3. 右のマスへ移動する

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++;
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA