プログラミング はじめの一歩 JavaScript + p5.js編
23:今は何を覚えているかな

この記事の詳しい内容には毎日新聞の「今は何を覚えているかな」ページから有料記事に進むことで読めます。

概要

オウムのロボット1号、2号、3号がいます。オウムロボットは下記の手順の通りに言葉を覚えていきますが、覚えられるのは1つだけで、新しく覚えると、前に覚えた言葉は忘れてしまいます。また、ロボットがロボットに言葉を教えることもできます。

手順:

  1. オウム1号 <- 'つばき'
  2. オウム2号 <- 'ひいらぎ'
  3. オウム3号 <- オウム1号
  4. オウム1号 <- オウム2号
  5. オウム2号 <- オウム3号

* 「オウム1号 <- 'つばき'」は、オウム1号に"つばき"という言葉を教えることを意味します。「オウム3号 <- オウム1号」はオウム1号が覚えている言葉をオウム2号に教えることを意味します。

ここで問題です。上記手順でオウム1号、2号、3号に言葉を覚えさせました。オウム1号、2号、3号が今覚えている言葉はそれぞれ何でしょう?

論理を考える

この問題は、前の「22:オウムが話す言葉」で見たOumuRobotクラスで解決できます。

OumuRobotクラスを使った解決策

「22:オウムが話す言葉」ではオウムロボットをOOPの手法で作成しているので、そのインスタンスはいくつでも作成できます。次のようにオウムロボットを3つ作成し、手順の命令を実行します。

// OumuRobotクラスのインスタンスを2つ作成
const o1 = new OumuRobot('オウム1号');
const o2 = new OumuRobot('オウム2号');
const o3 = new OumuRobot('オウム3号');

// オウム1号 <- 'つばき'
o1.listen('つばき');
o1.speech();
// オウム2号 <- 'ひいらぎ'
o2.listen('ひいらぎ');
o2.speech();
// オウム3号 <- オウム1号
o3.listen(o1.getWord());
o3.speech();
// オウム1号 <- オウム2号
o1.listen(o2.getWord());
o1.speech();
// オウム2号 <- オウム3号
o2.listen(o3.getWord());
o2.speech();

下図はこの結果です。

この結果から、オウム1号が今覚えているのは”ひいらぎ”で、オウム2号は”つばき”、オウム3号も”つばき”であることが分かります。

このように、クラスのコンストラクタに渡す引数を変えるだけで、特性の異なるインスタンスが容易にいくつでも作成できるのがオブジェクト指向プログラミングの大きな特徴の1つです。

もちろん解決策はほかにもあります。以降では、親玉(ボス)が子飼いのオウムロボットを管理する、という構造のOOPを用いた解決策を見ていきます。ただし分かりやすいのはOumuRobotクラスを使った方法です。

オウムを管理するボスクラス

オウムロボットを管理Bossクラスを考えます。オウムロボットに必要なのは名前と今覚えている言葉だけなので、OumuRobotクラスを使う必要はなく、ObjectやMapオブジェクトで簡易的に表すことができます。

オウム(を表すオブジェクト)はBossクラスの配列に入れて管理します。これはまさに鳥かごです。Bossクラスに、オウムを名前で区別できるメソッドを持たせ、特定したオウムに言葉を覚えさせます。

// 半角数字を全角数字に変換して返す関数
// https://www.yoheim.net/blog.php?q=20191101
function zenkaku2Hankaku(str) {
    return str.replace(/[0-9]/g, function(s) {
        return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
    });
}

// オウムを管理するボスクラス
class Boss {
    constructor() {
        // オウムを入れる配列
        this.oumus = [];
        // オウムを表すMapオブジェクトを3個作成して、oumus配列に追加
        for (let i = 0; i < 3; i++) {
            const map = new Map();
            // iの半角数値を全角数字に変換して名前に使う
            const zenkaku = zenkaku2Hankaku(String(i + 1))
            map.set('name', 'オウム' + zenkaku + '号'); // オウムの名前
            map.set('word', '');
            this.oumus.push(map);
        }
    }

    // オウムに言葉を覚えさせる nameはオウムの名前、wordは覚えさせる言葉
    // => nameからMapオブジェクトを特定し、set()を使ってwordを上書きする
    learnWord(name, word) {
        const oumuMap = this.getOumuMapFromName(name);
        oumuMap.set('word', word);
        print(this.oumus);
    }

    // 指定されたオウムに指定されたオウムから言葉を覚えさせる
    learnFromOumu(toOumu, fromOumu) {
        // 教える方のMapオブジェクトを特定
        const teacherMap = this.getOumuMapFromName(fromOumu);
        // 教えられる方のMapオブジェク特定
        const studentMap = this.getOumuMapFromName(toOumu);
        // 教える方のMapオブジェクトのget('word')で言葉を取得し、
        // それを教えられる方のMapオブジェクトのset()で設定
        studentMap.set('word', teacherMap.get('word'));
        print(this.oumus);
    }

    // 名前からそれに相当するオウムのMapオブジェクトを返す
    // => 内部で使用するメソッド
    getOumuMapFromName(name) {
        // このカウンタ変数はforの外で使うので、ここで初期化
        let idx = 0;
        // Mapオブジェクトの配列を走査して
        for (idx = 0; idx < this.oumus.length; idx++) {
            // Mapオブジェクトのnameを調べる
            const oumuName = this.oumus[idx].get('name');
            // それがパラメータのnameと同じなら、
            if (oumuName === name) {
                // 以降のループは不要なので、breakでループを抜ける
                break;
            }
        }
        // 特定したMapオブジェクトを返す
        const targetMap = this.oumus[idx];
        return targetMap;
    }
}

Bossクラスには配列のoumusプロパティを定義します。そしてMapオブジェクトのオウムを3つ作成し、oumus配列に追加します。

zenkaku2Hankaku()関数は半角数字を全角数字に変換して返す関数です。Mapオブジェクトを作成するとき、カウンタ変数のiを名前に使用したいのですが、iは半角なので、この関数を使って大文字に変えて名前に使用しています。つまりMapオブジェクトを作成のnameキーは、’オウム1号’ではなく1を全角にした’オウム1号’になります。

getOumuMapFromName()は、オウムの名前からそれに相当するMapオブジェクトを調べて返すメソッドで、Bossクラスの内部で使用します。

実行と結果

setup()関数内では、次のように使用します。Bossクラスのメソッドには、手順と同じ順番で引数が指定できます(数字は全角で指定します)。

const boss = new Boss();
boss.learnWord('オウム1号', 'つばき');
boss.learnWord('オウム2号', 'ひいらぎ');
boss.learnFromOumu('オウム3号', 'オウム1号');
boss.learnFromOumu('オウム1号', 'オウム2号');
boss.learnFromOumu('オウム2号', 'オウム3号');

これを実行すると、各メソッドに記述したprint()関数が下図のoumus配列を出力します。これを見ると、オウム1号が”ひいらぎ”、オウム2号が”つばき”、オウム3号も”つばき”を今覚えていることが分かります。

視覚化

視覚化にあたっては、下図の画像ファイルを使用します。ボスは仮想的なものでイメージや位置は必要ありません。オウム3羽のイメージはまったくの飾りで、吹き出しは言葉を描画するときの背景として使用します。

全コード

以下は視覚化したプログラムの全コードです。

let boss;
let oumuImage, hukidashiImage;
let o1Text = '',
    o2Text = '',
    o3Text = '';

function preload() {
    oumuImage = loadImage('images/oumus.png');
    hukidashiImage = loadImage('images/hukidashi.png');
}

function setup() {
    createCanvas(300, 200);
    textAlign(CENTER);
    boss = new Boss();

    // HTMLのリストを作成
    // <ul>要素
    const ul = createElement('ul');

    // <li>要素
    const li1 = createElement('ul', 'オウム1号 <- つばき');
    const li2 = createElement('ul', 'オウム2号 <- ひいらぎ');
    const li3 = createElement('ul', 'オウム3号 <- オウム1号');
    const li4 = createElement('ul', 'オウム1号 <- オウム2号');
    const li5 = createElement('ul', 'オウム2号 <- オウム3号');
    // <li>要素を配列にまとめ、
    let list = [li1, li2, li3, li4, li5];
    // 一気に設定
    for (let i = 0; i < list.length; i++) {
        // <li>要素のCSSスタイルを設定
        list[i].style('border', 'solid black thin'); // 枠線
        list[i].style('width', '200px'); // 幅
        list[i].style('cursor', 'pointer'); // カーソル
        // <li>要素の親は<ul>要素
        ul.child(list[i]);
    }
    ul.position(0, 200);
    // <ul>要素がクリックされたら
    ul.elt.onclick = (e) => {
        // 子要素を全部取得
        const childs = ul.elt.childNodes;
        // このインデックス番号が知りたい
        let index;
        // 子要素を走査して
        for (index = 0; index < childs.length; index++) {
            // クリックされた要素(<li>要素)と、走査中の子要素が一致したらそこで終了
            if (e.target === childs[index]) {
                break;
            }
        }
        // インデックス番号が0なら0番めの<li>要素がクリックされたということ
        if (index === 0) {
            boss.learnWord('オウム1号', 'つばき');
        }
        // インデックス番号が1なら1番めの<li>要素がクリックされたということ
        else if (index === 1) {
            boss.learnWord('オウム2号', 'ひいらぎ');
        }
        // インデックス番号が2なら1番めの<li>要素がクリックされたということ
        else if (index === 2) {
            boss.learnFromOumu('オウム3号', 'オウム1号');
        }
        // インデックス番号が3なら3番めの<li>要素がクリックされたということ
        else if (index === 3) {
            boss.learnFromOumu('オウム1号', 'オウム2号');
        }
        // インデックス番号が3なら3番めの<li>要素がクリックされたということ
        else if (index === 4) {
            boss.learnFromOumu('オウム2号', 'オウム3号');
        }
    }
}

function draw() {
    background(220);
    // オウムロボットのイメージ
    image(oumuImage, 50, 50);
    // 吹き出しのイメージ3つ
    image(hukidashiImage, 40, 70);
    image(hukidashiImage, 105, 70);
    image(hukidashiImage, 170, 70);
    // ボスからオウムが覚えている言葉を取得
    getWords()
    // オウムが覚えている言葉を描画
    text(o1Text, 70, 90);
    text(o2Text, 135, 90);
    text(o3Text, 200, 90);
}

function getWords() {
    // ボスから、オウムが覚えている言葉を得る
    const words = boss.getAllWords();
    // 変数にそれぞれ代入
    o1Text = words[0];
    o2Text = words[1];
    o3Text = words[2];
}

function zenkaku2Hankaku(str) {
    return str.replace(/[0-9]/g, function(s) {
        return String.fromCharCode(s.charCodeAt(0) + 0xFEE0);
    });
}

// オウムを管理するボスクラス
class Boss {
    constructor() {
        this.oumus = [];
        for (let i = 0; i < 3; i++) {
            const map = new Map();
            const zenkaku = zenkaku2Hankaku(String(i + 1))
            map.set('name', 'オウム' + zenkaku + '号');
            map.set('word', '');
            this.oumus.push(map);
        }
    }

    learnWord(name, word) {
        const oumuMap = this.getOumuMapFromName(name);
        oumuMap.set('word', word);
        print(this.oumus);
    }

    learnFromOumu(toOumu, fromOumu) {
        const teacherMap = this.getOumuMapFromName(fromOumu);
        const studentMap = this.getOumuMapFromName(toOumu);
        studentMap.set('word', teacherMap.get('word'));
        print(this.oumus);
    }

    getOumuMapFromName(name) {
        let idx = 0;
        for (idx = 0; idx < this.oumus.length; idx++) {
            const oumuName = this.oumus[idx].get('name');
            if (oumuName === name) {
                break;
            }
        }
        const targetMap = this.oumus[idx];
        return targetMap;
    }

    // oumus配列に含まれる3つのMapオブジェクトのword値を調べ、配列で返す
    getAllWords() {
        const w0 = this.oumus[0].get('word');
        const w1 = this.oumus[1].get('word');
        const w2 = this.oumus[2].get('word');
        return [w0, w1, w2];
    }
}

リスト項目を上から順にクリックしていくことで、お題の手順が実行できます。

コメントを残す

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

CAPTCHA