プログラミング はじめの一歩 JavaScript + p5.js編
5:暗号をといて箱を開ける

この記事の詳しい内容には毎日新聞の「暗号をといて箱を開ける」ページから有料記事に進むことで読めます。

概要

鍵のかかった箱があります。この箱は、指定された数だけ、文字の書かれたダイヤルを回すことで鍵の暗号を解いて開けることができます。箱には暗号の文字分の空欄があり、1文字解くたびにその文字が入ります。文字だけでは分かりづらいので、記事の図をお借りして例を見てみます。

上図では空欄が3つあるので、暗号は3文字です。数として2が指定されているので、ダイヤルの文字は右回りに2つ飛ばしで読みます。

  • 1回め
  • ダイヤルの矢印は「と」を指しているので、暗号の最初の文字は「と」になります。左の空欄に「と」が入ります。ダイヤルを回します。

  • 2回め
  • ダイヤルの文字は右回りに2つ飛ばしで読むので、暗号の2つめの文字は「ま」になります。真ん中の空欄に「ま」が入ります。ダイヤルを回します。

  • 3回め
  • 右回りに2つ飛ばしで読むので、暗号の3つめの文字は「と」になり、右の空欄に「と」が入ります。結果、暗号は「とまと」となります。

問題はもう1つ、下図の箱です。飛ばす数は3で、空欄は4つあり、ダイヤルの文字は同じです。

上図の文字盤をなぞるなり、自分で絵を描くなりすると、答えは簡単に分かります。「とじまり」です。ではこれをプログラミングで表現するには、どのようにすればよいでしょう? というのが本稿の課題です

簡単そうに思えるものから始める

いきなり箱の画像の読み込みや、空欄の文字の表示方法に取りかかるのではなく、考えるべきはこのお題を解決する論理、つまりどうすれば暗号が分かるのか? です。そのときには暗号が4文字の難しそうな方ではなく、やさしそうに思える3文字の方から取りかかります。

地道に1つずつ進む

プログラミングに着手するとっかかりが分からないので、分かっている情報を整理してみましょう。

  1. ダイヤルの矢印は最初、’と’を指しています。
  2. 次にダイヤルを回します。指定された数は2なので、右に2つに回ります。
  3. すると、矢印は’と’から2つ進んだ’ま’を指します。

見た目は下図のようなイメージです。

これをプログラミングで表すわけです。

1の「ダイヤルの矢印は最初、’と’を指している」を考えてみましょう。これは、ダイヤルと矢印を画像を作り、矢印を-45度傾けることではありません。そうではなく、もっと根本的な仕組みを考えます。’と’、’り’、’ま’、’じ’とある4つの文字のうち、’と’を表すにはどうするか? 最初に思い付くのは、変数に’と’を割り当てる方法です。

let arrow = 'と';
print(arrow); // 'と'

変数arrowを出力すると、’と’が表示されるので、プログラムの開始時に変数arrowを’と’で初期化しておくと、「ダイヤルの矢印は最初、’と’を指している」ことが表せそうです。同様にarrowに’り’を代入するとarrowで’り’を、’ま’を代入すると’ま’を表すことができます。

しかし’と’、’り’、’ま’、’じ’はダイヤルの文字なので、’と’の次には’り’が、’り’の次には’ま’が来ます。これは順番があるということです。単純に文字を変数に代入するこの方法では、順番がうまく表せそうにありません。

順番をとっかかりに配列が思いつけたらラッキーです。’と’, ‘り’, ‘ま’, ‘じ’を入れた配列を作成すると、インデックス番号で要素が参照できます。配列名[0]なら’と’を、配列[2]なら’ま’を指すことができます。そして、’と’のインデックス番号に2を足すと’ま’が参照できます。つまりダイヤルを右に2つ回すことが表現できるのです。

// ダイヤルの文字を、'と'から右回り入れた配列。
let codeArray = ['と', 'り', 'ま', 'じ'];
print(codeArray[0]); // 'と'
print(codeArray[0 + 2]);  // 'ま'

ここまで出て来た決め打ちの数値を変数化します。配列のインデックス番号は矢印の役割を果たすのでarrowNumberとします。指定された2はspecifiedNumにします。

// ダイヤルの文字を、'と'から右回り入れた配列。
let codeArray = ['と', 'り', 'ま', 'じ'];
// 配列codeArrayでの、矢印が指している文字のインデックス番号
let arrowNumber = 0;
// 問題で指定された数
let specifiedNum = 2;

これらの変数を使うと、codeArray[arrowNumber]で’と’が参照できます。そしてarrowNumberにspecifiedNumを足すことで、’ま’が参照できます。

// 矢印が指す文字を読む => ダイヤルの矢印は最初’と’を指している
const char1 = codeArray[arrowNumber];
print(char1); // 'と'

// specifiedNum分だけ矢印を右に回す => ダイヤルを右に2つ回す
arrowNumber = arrowNumber + specifiedNum;

// 矢印が指す文字を読む => 矢印は'ま’を指す
const char2 = codeArray[arrowNumber];
print(char2); // 'ま'

ここまでうまくいっているように思えます。ダイヤルはあと1回回すので、同じことを繰り返します。しかし次のコードを実行すると、[コンソール]にundefinedが表示されます。

// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
// 矢印が指す文字を読む
const char3 = codeArray[arrowNumber];
print(char3); // undefined

う、失敗だ、ほかの方法を探そう、とあきらめるのはまだ早いです。この時点でarrowNumberは4です。codeArray[4]には要素がないので、char3がundefinedになるのは当然なのです。

ではどうしましょうか? ’と’, ‘り’, ‘ま’, ‘じ’はダイヤルの文字なので、配列に入れました。配列に入れることで、’と’の次に ‘り’が来て、’り’の次に’ま’が来ることが表現できるようになりました。では、’じ’の次は? そうです、ここまでの論理では、’じ’の次に’と’が来ること、’と’, ‘り’, ‘ま’, ‘じの順番はずっと繰り返されること、つまりダイヤルであることがまだ実現できていないのです。

‘じ’の次を’と’にしたい、ということは、arrowNumberが4以上になったらarrowNumberを0に戻すことで解決できます。

if (arrowNumber >= codeArray.length) {
    arrowNumber = 0;
}
const char3 = codeArray[arrowNumber];
print(char3); // 'と'

ここまでのコードを実行すると、[コンソール]に’と”ま”と’が表示されます。問題もなさそうなので、簡単そうに思えるものから始めた課題はクリアできたように思えます。次はこの論理を、指定された数が3で空欄が4つという、難しそうに思える課題に応用してみましょう。

難しい課題に取り組む

簡単そうな課題で構築できた論理でも、よくできた論理であれば、そのままで難しそうに見える課題も解決できます。これは実際、よくあることです。しかし簡単な課題では問題とならなかったことが難しい課題で露見すると、論理を再考する必要に迫られます。これもよくあることです。

問題のあぶり出し

では、簡単な課題で構築できた論理を、そのまま難しい課題に当てはめてみましょう。今度はspecifiedNumは3で、空欄は4つあるので、4回繰り返します。

// 指定された数は3
let specifiedNum = 3;
let arrowNumber = 0;

// 空欄は4つなので4回繰り返す
// 1回め
// 矢印が指す文字を読む
const char4 = codeArray[arrowNumber];
print(char4); // 'と'
// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
print(arrowNumber); // 3

// 2回め
// 矢印が指す文字を読む
const char5 = codeArray[arrowNumber];
print(char5); // 'じ'
// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
print(arrowNumber); // 6
// 'じ'の次は'と'にする
if (arrowNumber >= codeArray.length) {
    arrowNumber = 0;
}
print(arrowNumber); // 0
// 3回め
const char6 = codeArray[arrowNumber];
print(char6); // 'と'
// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
print(arrowNumber); // 3
if (arrowNumber >= codeArray.length) {
    arrowNumber = 0;
}
print(arrowNumber); // 3
// 4回め
const char7 = codeArray[arrowNumber];
print(char7); // 'じ'
}

表示されるべき結果は’と”じ”ま”り’なのですが、残念ながら’と”じ”と”じ’です。これは、簡単な課題で構築した論理が難しい課題では通用しなかったことを示しています。

ではどこに問題があるのでしょう? こういう場合は、変数の値を片っ端から出力し、期待する値になっていない場所を探します。この課題では、変化するのはarrowNumberしかないので、これを上記コードのように出力します。下図はその結果です。上から順に検討します。

(1)の3は、arrowNumberの初期値0にspecifiedNumの3を足したarrowNumber + specifiedNumの結果です。codeArray[3]となるので、[‘と’, ‘り’, ‘ま’, ‘じ’]の’じ’が参照されます。これは期待する結果です。

(2)の6は、3になったarrowNumberにspecifiedNumを足した結果です。このarrowNumberはその下のif文(課題で構築した論理の重要な部分)によって、0になります((3)の0)。

if (arrowNumber >= codeArray.length) {
    arrowNumber = 0;
}

0であるarrowNumberでcodeArray[arrowNumber]を実行すると、当然のことながら[‘と’, ‘り’, ‘ま’, ‘じ’]の’と’が返されます。これは期待する結果ではありません。arrowNumberはここでは、’ま’のインデックス番号である2であってほしいわけです。問題はここにあります。

(4)の3は、0であるarrowNumberにspecifiedNumを足した結果です。arrowNumberは3なので、つづくif条件のarrowNumber >= codeArray.length はfalseとなり、arrowNumber = 0;は実行されません。これはつまり、このif文はここに書いても意味がないということです。

解決方法をひねり出す

ではどうすればよいのでしょう? 頭の使いどころです。arrowNumberが6のとき、arrowNumberは要素が4つのcodeArrayの範囲から、言わば3つ飛び出た値です。これを正当な0から3の範囲に収めるには、arrowNumberの6からcodeArrayの要素数である4を引きます。変数で表すと、arrowNumber – codeArray.length です。これを新しいarrowNumberの値にします。

上記コードのif文の{}内を次のように書き換えると、’と”じ”ま”り’が出力されます。

if (arrowNumber >= codeArray.length) {
    // arrowNumber = 0;
    arrowNumber = arrowNumber - codeArray.length;
}

実は簡単な課題で用いたarrowNumber = 0 は、arrowNumber – codeArray.lengthの簡単な課題でだけ使用できる限定版で、arrowNumber – codeArray.lengthは簡単な課題でも使用できます。

これで難しい課題を解決する基本的な論理が構築できました。ただしコードを見ると、同じような記述が何回も繰り返されています。同様のコードが3回つづくと関数化を検討するタイミングだと言われます。関数化については以降で見ていきます。

数学的な解決方法

剰余演算を利用すると、たとえば0,1,2,0,1,2…といった一連の値の繰り返しが得られます(参考「4_1:経時的変化 Creative Coding p5.js」)。

%演算子の左辺が0,1,2,3,4と1づつ大きくなる値で、右辺が4のとき、計算の結果は0,1,2,3,…となります。このとき、左辺を1つ飛ばしにすると、0,2,0,…となります。これを利用する方法です。

0 % 4 => 0 (0を4で割ると0、余りは0)---'と'
1 % 4 => 1 (1を4で割ると0、余りは1)
2 % 4 => 2 (2を4で割ると0、余りは2)---'ま'
3 % 4 => 3 (3を4で割ると0、余りは3)
4 % 4 => 0 (4を4で割ると1、余りは0)---'と'
5 % 4 => 1 (5を4で割ると1、余りは1)
6 % 4 => 2 (6を4で割ると1、余りは2)

左辺の値が配列の長さに収まっている間(商が0)は左辺の値がそのまま使用できます。左辺の値が配列の長さを超えると(商が1以上)、その超過分(余り)が使用できます。

%演算子の左辺をarrowNumber、右辺を配列の文字数(characterNum)にし、その結果の値をインデックス番号としてcodeArray配列に使用します。specifiedNumが2のときには1つ飛ばしの結果が得られます。

// ダイヤルの文字を、'と'から右回り入れた配列。
let codeArray = ['と', 'り', 'ま', 'じ'];
// 配列の文字数
const characterNum = codeArray.length;
// 問題で指定された数
let specifiedNum = 2;
// 配列codeArrayでの、矢印が指している文字のインデックス番号
let arrowNumber = 0;

// 空欄のボックス数は3なので、3回繰り返す
const char1 = codeArray[arrowNumber % characterNum];
print(char1);  // 'と'
// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
const char2 = codeArray[arrowNumber % characterNum];
print(char2);  // 'ま'
// specifiedNum分だけ矢印を右に回す
arrowNumber = arrowNumber + specifiedNum;
const char3 = codeArray[arrowNumber % characterNum];
print(char3);  // 'と'

このコードは同じことを繰り返しているだけなので、短くできます。

// 開始時に戻す
arrowNumber = 0;
// 空欄のボックス数は3
let boxesNum = 3;
for (let i = 0; i < boxesNum; i++) {
    const char = codeArray[arrowNumber % characterNum];
    arrowNumber = arrowNumber + specifiedNum;
    print(char);
}

難しい課題の解決にも、変数の値を変えるだけで対応できます。

arrowNumber = 0;
specifiedNum = 3;
boxesNum = 4;
for (let i = 0; i < boxesNum; i++) {
    const chara = codeArray[arrowNumber % characterNum];
    arrowNumber = arrowNumber + specifiedNum;
    print(chara);
}

「地道に1つずつ進む」で見た方法のコードは非常に長いものでしたが、%演算子を使うと、かなり短くて済みます。

関数化

「地道に1つずつ進む」で見たコードは非常に長く、しかも似たような記述が並んでいました。一般的に、似たコードが3回つづくと、それはコードを関数にまとめるタイミングだと言われます。以下では、「地道に1つずつ進む」で見た長いコードを関数にまとめて短くする方法を探っていきます。

似たコードをまとめる

関数化に取り組むのは次のコードです。

function setup() {
    createCanvas(400, 300);
    background(200);

    // ダイヤルの文字を、'と'から右回り入れた配列。
    let codeArray = ['と', 'り', 'ま', 'じ'];
    // 配列codeArrayでの矢印が指している文字のインデックス番号
    let arrowNumber = 0;
    // 問題で指定された数
    let specifiedNum = 2;

    // 空欄のボックス数は3なので3回繰り返す
    // 1回め
    // 矢印が指す文字を読む
    const char1 = codeArray[arrowNumber];
    print(char1); // 'と'
    // specifiedNum分だけ矢印を右に回す
    arrowNumber = arrowNumber + specifiedNum;

    // 2回め
    const char2 = codeArray[arrowNumber];
    print(char2);
    arrowNumber = arrowNumber + specifiedNum;

    // 3回め
    if (arrowNumber >= codeArray.length) {
        // 配列の長さからの超過分を配列に収める
        arrowNumber = arrowNumber - codeArray.length
    }
    const char3 = codeArray[arrowNumber];
    print(char3);
}

上記コードをながめると、矢印が指す文字を得るコードとspecifiedNum分だけ矢印を右に回すコードが重複しているように見えます。そして3回めだけ、if文があります。

では、矢印が指す文字を得るコードとspecifiedNum分だけ矢印を右に回すコードを関数にまとめればよいのでしょうか? しかしそうすると、3回めだけ、if文を実行してから関数を呼び出す必要が生じます。言い方を変えると、3回めの呼び出しを行う前にif文を実行することを覚えておかなければならない、ということです。

一方、if文の条件は「arrowNumberがcodeArrayの要素数より大きくなったら」という意味です。これを記述したのは3回めで問題が生じたからでした。1回めと2回めでarrowNumberがcodeArrayの要素数より大きくなることはないので、必要なかったのです。

ということは、このif文を関数に入れても、1回めと2回めでは条件が満たされないので、支障はないことになります。何より、if文を関数に入れておくと、3回めの呼び出しの前にif文を実行することを覚えておく必要がなくなり、何回めでも同じように呼び出せるので、物事がシンプルになります。

そこで次のような関数を考えました。暗号の文字を返すので、名前はgetCodeCharにしています。

// codeArrayとarrowNumber、specifiedNumを使って、矢印が指す文字を返す関数
function getCodeChar() {
    // 似たコードをまとめる

    // 記述する順番を論理的に考える必要がある
    // if文は1回めと2回めは必要ないが、3回めで最初に必要なので、ここに要る
    if (arrowNumber >= codeArray.length) {
        // 配列の長さからの超過分を配列に収める
        arrowNumber = arrowNumber - codeArray.length
    }
    // 文字を得る
    const char = codeArray[arrowNumber];
    // specifiedNum分だけ矢印を右に回す
    arrowNumber = arrowNumber + specifiedNum;
    // 得られた文字を呼び出し元に返す
    // returnは関数の最後に書く。returnより後のコードは実行されない
    return char;
}

似たコードをまとめるときには、そのコードが何をやっているのかをよく考えて、順番を論理的に書く必要があります。たとえばarrowNumber = arrowNumber + specifiedNum; が関数の最初に来ると、結果はまるで異なります。

またこの関数では、変数のcodeArrayとarrowNumber、specifiedNumを使っているので、これらの変数はsetup()関数の外に出して、グローバル変数に変える必要があります。

するとsetup()関数からは、次のようにして呼び出し、結果を得ることができます。

// ダイヤルの文字を、'と'から右回り入れた配列。
let codeArray = ['と', 'り', 'ま', 'じ'];
// 配列codeArrayでの矢印が指している文字のインデックス番号
let arrowNumber = 0;

// 問題で指定された数
let specifiedNum = 2;

function setup() {
    createCanvas(400, 300);
    background(200);
    // 空欄のボックス数は3なので3回繰り返す
    // getCodeChar()関数を呼び出して1文字めを得る
    const char1 = getCodeChar();
    print(char1); // 'と'
    // getCodeChar()関数を呼び出して2文字めを得る
    const char2 = getCodeChar();
    print(char2); // 'ま'
    // getCodeChar()関数を呼び出して3文字めを得る
    const char3 = getCodeChar();
    print(char3); // 'と'
}

同じ関数の呼び出しを繰り返しているので、forループが使用でき、コードはさらに短くなります(上記コードにつづけて書く場合には、arrowNumberを最初の0に戻しておきます)。

// 3回の繰り返しはforループで実行できる
let boxesNum = 3;
// arrowNumberを初期値に戻す
// arrowNumber = 0;
for (let i = 0; i < boxesNum; i++) {
    const char = getCodeChar();
    print(char);
}
関数のパラメータ

関数にパラメータを設定すると、関数が返す結果を動的に変えることができます。たとえばspecifiedNumを受け取るように変更すると、getCodeChar()関数はspecifiedNumが2の場合と3の場合を返すようになります。

// specifiedNumをパラメータに取る
function getCodeChar(specified) {
    if (arrowNumber >= codeArray.length) {
        // 配列の長さからの超過分を配列に収める
        arrowNumber = arrowNumber - codeArray.length
    }
    // 文字を得る
    const char = codeArray[arrowNumber];
    // specifiedNum分だけ矢印を右に回す
    arrowNumber = arrowNumber + specified;
    // 得られた文字を呼び出し元に返す
    // returnは関数の最後に書く。returnより後のコードは実行されない
    return char;
}

getCodeChar()関数を呼び出すときには、2か3、または2か3を代入した変数(specifiedNum)を引数として渡します。そして指定された数が2のときには、空欄の箱の数を3、3のときには4にします。

// ダイヤルの文字を、'と'から右回り入れた配列。
let codeArray = ['と', 'り', 'ま', 'じ'];
// 配列codeArrayでの矢印が指している文字のインデックス番号
let arrowNumber = 0;

// 問題で指定された数。3に変える
let specifiedNum = 2;

// 空欄の箱の数。4に変える
let boxesNum = 3;

function setup() {
    createCanvas(400, 300);
    background(200);
    // 結果の文字を入れる配列
    let resultArray = [];

    for (let i = 0; i < boxesNum; i++) {
        const char = getCodeChar(specifiedNum);
        resultArray.push(char);
    }
    // 結果を出力
    print('指定された数: ' + specifiedNum);
    print('空欄の箱の数: ' + boxesNum);
    print(resultArray);
}

下図はそれぞれの場合での実行結果です。関数に引数の値を変えて渡すことで、結果が変化していることが分かります。

視覚化

論理がかたまったら、残るは視覚化です。視覚化するには、箱やダイヤルなどのイメージが必要ですが、その前に「ダイヤルを右に回す」ことをどうやって表現するかを考えておく必要があります。

ダイヤルを回すのですから、ダイヤルのイメージを右回りにドラッグすると右に回る、というのが自然に思えます。しかしこれにはまた別の論理が必要になり、それは本稿の論点ではありません。そこで、最も簡単そうに思えるボタンのクリックで表すことにします。

とは言え、文字を指す矢印の変化は表す必要があります。矢印が’と’を指しているなら、空欄の箱には’と’を表示し、ボタンがクリックされたら、specifiedNum分だけ矢印が回転し、’ま’を指すように表す必要があります。

これには、実際に矢印の画像を作成し、そのイメージを回転させる方法もあるでしょう。現実世界に即したやり方なので合点のいく方法です。しかし実際にやろうとすると、イメージの回転角度を制御し、その角度と’と’, ‘り’, ‘ま’, ‘じ’との関係性をつねに把握しておく必要があります。

また別の、もっともっと簡単な方法として、ダイヤルと矢印を合体させたイメージを使う方法があります。これはズルをしているような方法なのですが、プログラミングではよく用いられます。

具体的に言うと、下図のような画像を’と’, ‘り’, ‘ま’, ‘じ’ごとに用意し、矢印が’と’を指すときには0.pngのイメージを、’ま’を指すときには2.pngのイメージを描画するのです。「矢印が文字を指す」ことはないのでイカサマのようではありますが、記述するコードの複雑性が相当排除できます。

コードは次のようなものが記述できます。ただし、論理は対応しているものの、イメージが空欄4つの場合に対応していないので、specifiedNumは2、空欄は3つのままで実行します。

let codeArray = ['と', 'り', 'ま', 'じ'];
let arrowNumber = 0;
let specifiedNum = 2;
// 使っていない
//let boxesNum = 3;

// 箱のイメージ
let boxImage;
// 矢印のイメージ
let arrowImage;
let arrowImagesArray;
// 答えを入れる配列
let answerArray = [];

function preload() {
    // 空欄3つの箱のイメージ
    boxImage = loadImage('images/box.png');
    // 4つの矢印のイメージを読み込んで配列に入れる
    const image0 = loadImage('images/0.png'); // 'と'
    const image1 = loadImage('images/1.png'); // 'り'
    const image2 = loadImage('images/2.png'); // 'ま'
    const image3 = loadImage('images/3.png'); // 'じ'
    // codeArrayの要素と同じ順番で配列に入れる
    arrowImagesArray = [image0, image1, image2, image3];
}

function setup() {
    createCanvas(400, 300);
    // 最初は、矢印が'と'を指すイメージ
    arrowImage = arrowImagesArray[0];

    // [回す]ボタン
    const dialButton = setButton(specifiedNum + 'だけ右に回す', {
        x: 130,
        y: 250
    });
    // マウスプレスで、ダイヤルを右にspecifiedNum分回す
    dialButton.mousePressed(() => {
        // 「地道に1つずつ進む」で構築した論理
        const len = codeArray.length;
        if (arrowNumber >= len) {
            arrowNumber = arrowNumber - len;
        }
        const chara = codeArray[arrowNumber];
        // 矢印のイメージも、文字と同じに特定できる
        arrowImage = arrowImagesArray[arrowNumber];
        // 文字とイメージが特定できたら、ダイヤルを2つ回す
        arrowNumber = arrowNumber + specifiedNum;
        // 確認用
        print(chara);
        // 文字を答えの配列に追加
        answerArray.push(chara);
    });
}

function draw() {
    background(220);
    // boxImageを描画してからarrowImageを描画する
    image(boxImage, 80, boxImage.height / 2);
    // ダイヤルがうまく重なるように位置を調整
    image(arrowImage, 87, 86);
    textSize(18);
    text('数は' + specifiedNum, 170, 94);
    textSize(30);
    // 答えの文字を描く
    text(answerArray[0], 170, 136);
    text(answerArray[1], 208, 136);
    text(answerArray[2], 245, 136);
}

// specifiedNumをパラメータに取る
function getCodeChar(specified) {
    if (arrowNumber >= codeArray.length) {
        // 配列の長さからの超過分を配列に収める
        arrowNumber = arrowNumber - codeArray.length
    }
    // 文字を得る
    const char = codeArray[arrowNumber];
    // specifiedNum分だけ矢印を右に回す
    arrowNumber = arrowNumber + specified;
    // 得られた文字を呼び出し元に返す
    // returnは関数の最後に書く。returnより後のコードは実行されない
    return char;
}

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

下図はボタンを2回クリックした時点での実行結果です。空欄の文字は白い箱に描いているのではなく、箱のイメージの上の、空欄の枠にうまく収まるような位置に描いています。これもイカサマと言えばイカサマです。

では最後に、specifiedNumと空欄の数の変化に対応したコードを紹介します。[箱を開ける]ボタンを作成して、解いた暗号が合っている場合には、[コンソール]に’OPEN’が表示されます。このコードでは、[‘と’, ‘り’, ‘ま’, ‘じ’]の配列をやめ、代わりに対応する文字と画像イメージを持つオブジェクトの配列を使用しています。

let boxImageArray;
let boxImage;

let arrowImage;

let specifiedNum = 2;

let arrowNumber = 0;
let answerArray = [];

let codeObjectArray;

function preload() {
    // 空欄3つの箱のイメージ
    const box3Image = loadImage('images/box3.png');
    // 空欄4つの箱のイメージ
    const box4Image = loadImage('images/box4.png');
    // 配列に入れる
    // ラジオボタンのクリックで箱のイメージを変えるときに便利
    boxImageArray = [box3Image, box4Image];

    // 'と'、'り'、'ま'、'じ'と、矢印の4つのイメージは対応付けできるので、オブジェクトにまとめる
    // charaプロパティとして'と'、imgプロパティとして矢印が'と'を指すイメージを持つオブジェクト
    const image0 = loadImage('images/0.png');
    const obj0 = {
        chara: 'と',
        img: image0
    };
    // charaプロパティとして'り'、imgプロパティとして矢印が'り'を指すイメージを持つオブジェクト
    const image1 = loadImage('images/1.png');
    const obj1 = {
        chara: 'り',
        img: image1
    };
    // charaプロパティとして'ま'、imgプロパティとして矢印が'ま'を指すイメージを持つオブジェクト
    const image2 = loadImage('images/2.png');
    const obj2 = {
        chara: 'ま',
        img: image2
    };
    // charaプロパティとして'じ'、imgプロパティとして矢印が'じ'を指すイメージを持つオブジェクト
    const image3 = loadImage('images/3.png');
    const obj3 = {
        chara: 'じ',
        img: image3
    };
    // 3つのオブジェクトを配列に入れる
    codeObjectArray = [obj0, obj1, obj2, obj3];
}

function setup() {
    createCanvas(400, 300);
    // 最初の箱のイメージは空欄3つのbox3Imageにする
    boxImage = boxImageArray[0];
    // 最初の矢印イメージは'と'を指したimage0
    arrowImage = codeObjectArray[0].img;

    const p = createP('ダイヤルを回す数');
    p.position(170, 5);
    const radioButton = createRadio();
    radioButton.option(2);
    radioButton.option(3);
    radioButton.style('width', '120px');
    radioButton.position(300, 20);
    // print(radioButton)
    radioButton.selected('2');
    radioButton.elt.onclick = () => {
        specifiedNum = radioButton.value();
        //print(radioButton.value());
        boxImage = boxImageArray[specifiedNum - 2];
        arrowNumber = 0;
        arrowImage = codeObjectArray[arrowNumber].img;
        dialButton.elt.innerHTML = specifiedNum + 'だけ右に回す';
        answerArray = [];
    };

    // [回す]ボタン
    const dialButton = setButton(specifiedNum + 'だけ右に回す', {
        x: 100,
        y: 250
    });
    // マウスプレスで、ダイヤルを右にspecifiedNum分回す
    dialButton.mousePressed(() => {
        const len = codeObjectArray.length;
        if (arrowNumber >= len) {
            arrowNumber = arrowNumber - len;
        }
        const chara = codeObjectArray[arrowNumber].chara;
        arrowImage = codeObjectArray[arrowNumber].img;
        answerArray.push(chara);
        arrowNumber = arrowNumber + int(specifiedNum);
    });
    // [開ける]ボタン
    const openButton = setButton('箱を開ける', {
        x: 250,
        y: 250
    });
    // マウスプレスで、ダイヤルを右にspecifiedNum分回す
    openButton.mousePressed(() => {
        if (int(specifiedNum) === 2) {
            if (answerArray.length === 3 && answerArray[0] === 'と' && answerArray[1] === 'ま' && answerArray[2] === 'と') {
                print('OPEN');
            }
        }
        else if (int(specifiedNum) === 3) {
            if (answerArray.length === 4 && answerArray[0] === 'と' && answerArray[1] === 'じ' && answerArray[2] === 'ま' && answerArray[3] === 'り') {
                print('OPEN');
            }
        }
    });
}

function draw() {
    background(220);
    image(boxImage, 80, boxImage.height / 2);
    image(arrowImage, 87, 86);
    textSize(18);
    text('数は' + specifiedNum, 170, 94);
    textSize(30);
    text(answerArray[0], 170, 136);
    text(answerArray[1], 208, 136);
    text(answerArray[2], 245, 136);
    if (int(specifiedNum) === 3) {
        text(answerArray[3], 280, 136);
    }
}

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

specifiedNumは[ダイヤルを回す数]チェックボックスで指定できます。

コメントを残す

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

CAPTCHA