プログラミング はじめの一歩 JavaScript + p5.js編
16:3段重ねのアイスを注文したら?

この記事の詳しい内容には毎日新聞の「3段重ねのアイスを注文したら?」ページから有料記事に進むことで読めます。

概要

3段重ねのアイスを売る店があります。たとえば「チョコとバニラ、ストロベリーのアイスをください」と言うと、注文した順に、下からチョコ、バニラ、ストロベリーが3段重ねになったアイスが渡されます。

ここで問題です。「バニラ、ストロベリー、チョコミントのアイスをください」と注文すると、どんなアイスが出てくるでしょうか?

論理を考える

このお題で注目すべきは、受け取る3段重ねアイスは注文したアイスの順に重ねられるということです。食べる分には同じかもしれませんが、プログラミングの立場から言うと、これは重要です。「チョコ、バニラ、ストロベリー」で頼んで「チョコ、ストロベリー、バニラ」が出てきたら、それはミスということです。

まずは3段重ねにするアイスを文字列で表すところから始めましょう。これは、「1:おかしに当たり券を入れる」の「物事をどう表すか?」で述べている、プログラミングのとっかかりを見つけるときの最もシンプルな方法です。

‘チョコ’, ‘バニラ’, ‘ストロベリー’

では、注文された順番はどうやってメモしましょう。変数でしょうか? 注文は3つなので変数も3つ済むでしょう。3つくらいなら順番は間違えないかもしれません。

const order1 = ‘チョコ’;
const order2 = ‘バニラ’;
const order3 = ‘ストロベリー’;

といった具合です。しかしこの順番を守ることをずっと覚えておかなければなりません。order2をorder1と間違えてしまうと、その時点でプログラミングミスになります。

これよりももっと簡単で、順番を気にしなくてよい方法があります。それは配列です。あらかじめ配列を作っておいて、注文があるたびにそこに追加していけば、後は配列が順番をキープしてくれます。

こんな感じです。店員はこの配列を使って3段重ねのアイスを作ればよいわけです。

let orders = []; // 注文を入れる配列

// 注文があるたびに配列に追加する
function orderIce(ice) {
    orders.push(ice);
}

orderIce('チョコ'); // ['チョコ']
orderIce('バニラ'); // ['チョコ', 'バニラ']
チョコ、バニラ、ストロベリーのアイスをください

お客がアイスを1つずつ注文して、それを店員が確認する論理は次のようなコードで表すことができます。

// 注文を入れる配列
let orders = [];

function setup() {
    noCanvas();
    // 注文と確認
    orderIce('チョコ'); // 'チョコを注文'
    print('チョコと');
    orderIce('バニラ'); // 'バニラを注文'
    print('バニラと');
    orderIce('ストロベリー'); // 'ストロベリーを注文'
    print('ストロベリーのアイスをください');
    print('---------------------');
    // 店員の応答へ
    receiveOrder();
}

// アイスの文字列を受け取り、orders配列に追加する
// => アイスの重なりの作成
function orderIce(ice) {
    orders.push(ice);
}

// 注文を受け取る店員の応答
function receiveOrder() {
    print('分かりました');
    // 注文の配列を走査して注文を出力
    for (let i = 0; i < orders.length; i++) {
        print(orders[i] + ',');
    }
    print('ですね');
    // 注文内容はorders配列に入っている!
    print('どうぞ、' + orders + 'のアイスです');
}

このコードを実行すると、[コンソール]に下図が表示されます。お分かりのように、赤の下線を引いた部分がorders配列です。これがこのお題のポイントです。

このプログラムは、チョコ、バニラ、ストロベリーの順でしかアイスを作れないわけではありません。3回呼び出しているorderIce()関数に渡すアイスの文字列を変えると、変えた順番に重ねたアイスが作れます。

可視化

店員まで注文が届き店員が確認したので、次は店員がアイスを作る作業です。作るといってもチョコをスプーンでかきとるのではなく、店員の作業はイメージの描画です。

アイスのイメージはまだ読み込んでいないので、ここから始めます。

let iceImages; // アイスのイメージを入れる配列
let cupImage; // カップのイメージ

function preload() {
    // アイスのイメージを作成
    const choco = loadImage('images/choco.png');
    const vanilla = loadImage('images/vanilla.png');
    const strawberry = loadImage('images/strawberry.png');
    // アイスのイメージを配列にまとめる
    iceImages = [choco, vanilla, strawberry];
    // カップのイメージ
    cupImage = loadImage('images/cup.png');
}

注文内容(orders配列、[“チョコ”, “バニラ”, “ストロベリー”])は店員に届いているので、次は店員がアイスを作る作業です。これは具体的にいうと、preload()で読み込んだイメージの描画です。

文字列からイメージにたどり着く方法1

では、注文内容からどうやって該当するイメージを特定すればよいでしょう? これはたとえば’チョコ’ならchocoイメージを特定するということです。

orders配列にはアイスの文字列が入っているので、これをforループで走査すると、個々の文字列にはたどり着きます。繰り返し回数はorders配列の長さなので3です。最初の繰り返しのとき、orders[0]が’チョコ’ならchocoイメージ(iceImages[0])をメモし、’バニラ’ならvanillaイメージ(iceImages[1])をメモします。これは2回め、3回めも同様です。メモはどうやってしましょう? 繰り返しの1回めと2回め、3回めを区別する必要があります。

ここでも配列が利用できます。空の配列を用意し、繰り返しごとにメモしたいイメージを配列に追加します。

function receiveOrder() {
    print('分かりました');
    ...
    // 注文内容はorders配列に入っている!
    // 注文内容を1つずつ調べ、注文に合ったアイスをcupice配列に入れる
    let cupice = []; // 3段重ねのアイス
    for (let i = 0; i < orders.length; i++) {
        // nameはアイスの名前
        // アイスの名前を手掛かりに、対応するイメージを配列に追加
        const name = orders[i];
        if (name === 'チョコ') {
            cupice.push(iceImages[0]);
        }
        else if (name === 'バニラ') {
            cupice.push(iceImages[1]);
        }
        else if (name === 'ストロベリー') {
            cupice.push(iceImages[2]);
        }
    }
    print(cupice); // 確認
    // まだつづく
}

orders配列とcupice配列をprint()関数で出力すると、3つの文字列から3つのイメージにたどり着けていることが分かります。

後はcupice配列の3つのイメージを適切な位置に描画するだけです。

// 基準となるカップの位置を変数で決めておく
// 後で変えたいときはこの変数の値を変えるだけで済む
const cupX = width / 2 - cupImage.width / 2;
const cupY = height / 2;
// cupice配列を走査して、アイスのイメージを描画
for (let i = 0; i < cupice.length; i++) {
    image(cupice[i], cupX + 3, cupY - 25 - i * cupice[i].width / 2);
}
// カップのイメージを描画
image(cupImage, cupX, cupY);

描画位置を決めるには、最初x,y位置を当てずっぽうで決めて描画し、結果を見て調整します。ある程度決まったら、上記コードのように位置を変数に割り当て、位置は変数で決めるようにします。変数に割り当てておくと、後から直したいときに1カ所の修正で済むので楽です。上記コードでは、まずカップの位置を決め、それを基準にアイスの各イメージの位置を決めています。

カップ(コーン)のイメージは、コーンがアイスより手前にあるように見せたいので、アイスより後で描画します。

以下はここまでの全コードです。

// 可視化

let orders = [];
let iceImages; // アイスのイメージを入れる配列
let cupImage; // カップのイメージ

function preload() {
    // アイスのイメージを作成
    const choco = loadImage('images/choco.png');
    const vanilla = loadImage('images/vanilla.png');
    const strawberry = loadImage('images/strawberry.png');
    // アイスのイメージを配列にまとめる
    iceImages = [choco, vanilla, strawberry];
    // カップのイメージ
    cupImage = loadImage('images/cup.png');
}

function setup() {
    createCanvas(400, 300);
    background(220);
    // 注文と確認
    orderIce('チョコ');
    print('チョコと');
    orderIce('バニラ');
    print('バニラと');
    orderIce('ストロベリー');
    print('ストロベリーのアイスをください');
    print('---------------------');
    // 店員の応答へ
    receiveOrder();
}

// アイスの文字列を受け取り、orders配列に追加する
// => アイスの重なりの作成
function orderIce(ice) {
    orders.push(ice);
}

// 注文を受け取る店員の応答
function receiveOrder() {
    print('分かりました');
    print(orders);
    for (let i = 0; i < orders.length; i++) {
        print(orders[i] + ',');
    }
    print('ですね');
    // 注文内容はorders配列に入っている!
    // 注文内容を1つずつ調べ、注文に合ったアイスをcupice配列に入れる
    let cupice = []; // 3段重ねのアイス
    for (let i = 0; i < orders.length; i++) {
        // nameはアイスの名前
        // アイスの名前を手掛かりに、対応するイメージを配列に追加
        const name = orders[i];
        if (name === 'チョコ') {
            cupice.push(iceImages[0]);
        }
        else if (name === 'バニラ') {
            cupice.push(iceImages[1]);
        }
        else if (name === 'ストロベリー') {
            cupice.push(iceImages[2]);
        }
    }
    print(cupice); // 確認
    // 基準となるカップの位置を変数で決めておく
    // 後で変えたいときはこの変数の値を変えるだけで済む
    const cupX = width / 2 - cupImage.width / 2;
    const cupY = height / 2;
    // cupice配列を走査して、アイスのイメージを描画
    for (let i = 0; i < cupice.length; i++) {
        image(cupice[i], cupX + 3, cupY - 25 - i * cupice[i].width / 2);
    }
    // カップのイメージを描画
    image(cupImage, cupX, cupY);
}

下図は実行結果です。アイスは下からチョコ、バニラ、ストロベリーの順に重なっています。アイスはチョコ、バニラ、ストロベリーの順で描画されるので、上のものが下のものを少し隠します。また手前にコーンが来てチョコを少し隠しています。

文字列からイメージにたどり着く方法2

注文内容から該当するイメージを特定する方法はほかにもあります。これまでほかのお題の解決でもよく使用してきたObjcetオブジェクトの使用もその1つです。同一のオブジェクトにアイスの名前とイメージのプロパティを持たせることで、文字列とイメージを関連付けています。nameプロパティでオブジェクトを特定すると、そのオブジェクトのimageプロパティがそのアイスのイメージになります。

// nameプロパティとimageプロパティを持つアイスのオブジェクトをices配列に入れる
// オブジェクトはアイスを表す
ices = [
  { name: 'チョコ', image: choco },
  { name: 'バニラ', image: vanilla },
  { name: 'ストロベリー', image: strawberry }
];

これに対応するreceiveOrder()関数は以下のコードです。方法1のif…else ifの連続と比べるとコードが単純です。

// 注文を受け取る店員の応答
function receiveOrder() {
  ...
  // 注文内容を1つずつ調べ、注文に合ったアイスをcupice配列に入れる
  let cupice = [];
  for (let i = 0; i < orders.length; i++) {
    // nameはアイスの名前
    const name = orders[i];
    for (let j = 0; j < ices.length; j++) {
      // 注文のアイスの名前と、アイスのオブジェクトの名前が同じなら
      if (name === ices[j].name) {
        // それは同じ名前を持つアイスのオブジェクトなのでcupice配列に追加する
        cupice.push(ices[j]);
      }
    }
  }
  print(cupice);  // 確認
  const cupX = width / 2 - cupImage.width / 2;
  const cupY = height / 2;
  // cupice配列を走査して、アイスのイメージを描画
  for (let i = 0; i < cupice.length; i++) {
    image(cupice[i].image, cupX + 3, cupY - 25 - i * cupice[i].image.width / 2);
  }
  // カップのイメージを描画
  image(cupImage, cupX, cupY);
}
文字列からイメージにたどり着く方法3

方法の3つめは、Objectオブジェクトの代わりにMapオブジェクトを使う方法です。Mapオブジェクトは比較的最近使えるようになった新しいJavaScriptのオブジェクトです。Objectオブジェクトと似ていますが、Objectオブジェクトのプロパティに当たるキーとその値を扱う独自のメソッドを持っています。

// preload()内
// アイスをMapオブジェクトで表しices配列に追加
ices = [
    new Map([
        ['name', 'チョコ'],
        ['image', choco]
    ]),
    new Map([
        ['name', 'バニラ'],
        ['image', vanilla]
    ]),
    new Map([
        ['name', 'ストロベリー'],
        ['image', strawberry]
    ])
];

// receiveOrder()内
...

// 注文内容を1つずつ調べ、注文に合ったアイスをcupice配列に入れる
let cupice = [];
for (let i = 0; i < orders.length; i++) {
    // nameはアイスの名前
    const name = orders[i];
    for (let j = 0; j < ices.length; j++) {
        // ices配列の要素はMapオブジェクトなので、get()メソッドが使用できる
        // 対象のMapオブジェクトのname値がnameと同じなら、それは同じ名前のアイスのMapオブジェクトなので、
        // 配列に追加する
        if (name === ices[j].get('name')) {
            cupice.push(ices[j]);
        }
    }
}
print(cupice); // 確認
// 基準となるカップの位置を変数で決めておく
// 後で変えたいときはこの変数の値を変えるだけで済む
const cupX = width / 2 - cupImage.width / 2;
const cupY = height / 2;
// cupice配列を走査して、アイスのイメージを描画
for (let i = 0; i < cupice.length; i++) {
    // Mapオブジェクトのget()を使ってイメージを参照する
    image(cupice[i].get('image'), cupX + 3, cupY - 25 - i * cupice[i].get('image').width / 2);
}
// カップのイメージを描画
image(cupImage, cupX, cupY);
アイスが何段にも重なる夢のアプリ

最後に、3段に限らず、理論上いくつでもアイスを重ねられる夢のようなアプリ(しかし食べられない)のコードを示しておきます。お客は<select>要素で好きなアイスを選びます。このとき選択したアイスが画面にで表示されます。選択が終わったら[注文終わり]ボタンをクリックします。店員は「わかりました。」と応じ、少し待つと、注文したアイスが表示されます。

以下は全コードです。

let orders = [];
let ices;
let cupImage;

let customerImage, clerkImage, bg;

function preload() {
    cupImage = loadImage('images/cup.png');
    const choco = loadImage('images/choco.png');
    const vanilla = loadImage('images/vanilla.png');
    const strawberry = loadImage('images/strawberry.png');
    const chocomint = loadImage('images/chocomint.png');
    ices = [{
        name: 'チョコ',
        image: choco
    }, {
        name: 'バニラ',
        image: vanilla
    }, {
        name: 'ストロベリー',
        image: strawberry
    }, {
        name: 'チョコミント',
        image: chocomint
    }];
    // お客と店員と背景のイメージ
    customerImage = loadImage('images/customer.png');
    clerkImage = loadImage('images/clerk.png');
    bg = loadImage('images/bg.png');
}

function setup() {
    createCanvas(400, 300);
    textSize(20);
    background(bg);

    // <select>要素の作成
    const select = createSelect();
    select.position(10, 10);
    // 選択肢を設定
    select.option('アイスを選択');
    for (let i = 0; i < ices.length; i++) {
        select.option(ices[i].name);
    }
    select.selected(ices[0]);
    // 選択肢に変化があったら
    select.changed(() => {
        // 選択した値(0から4)
        const iceStr = select.value();
        //print(val)
        // 'パンを選ぶ'が選ばれたときはこの関数を抜ける
        if (iceStr === 'アイスを選択') return;
        orderIce(iceStr);
    });

    // [注文終わり]ボタン
    const orderButton = setButton('注文終わり', {
        x: 140,
        y: 10
    });
    // ボタンのクリックで店員の応答
    orderButton.mousePressed(() => {
        receiveOrder();
    });

    // お客と店員のイメージを描画
    image(customerImage, 50, 180);
    image(clerkImage, 250, 180);
}

function orderIce(ice) {
    orders.push(ice);
    fill(207, 98, 137);
    let i = 0;
    for (i = 0; i < orders.length; i++) {
        text(orders[i], 60, 70 + 20 * i);
    }
}

function receiveOrder() {
    fill(116, 101, 78);
    text('わかりました。', 200, 70);
    let cupice = [];
    for (let i = 0; i < orders.length; i++) {
        const name = orders[i];
        for (let j = 0; j < ices.length; j++) {
            if (name === ices[j].name) {
                cupice.push(ices[j]);
            }
        }
    }

    const cupX = width / 2 - cupImage.width / 2;
    const cupY = height / 2 + 50;

    window.setTimeout(() => {
        for (let i = 0; i < cupice.length; i++) {
            image(cupice[i].image, cupX + 3, cupY - 25 - i * cupice[i].image.width / 2);
        }
        image(cupImage, cupX, cupY);
    }, 2000);
}

function setButton(label, pos) {
    const button = createButton(label);
    button.size(90, 30);
    button.position(pos.x, pos.y);
    return button;
}

クリックすると実行画面が開きます

コメントを残す

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

CAPTCHA