【アプリ開発初心者が行う】あみだくじアプリを作ろう②(機能の構築)

webアプリ

前回でアプリの構想とWebページの大まかな雰囲気の作成までが終わりました。

ここからはWebページ上に配置したボタンなどに機能を加えていきます。

少し難しい部分もあるので頑張っていきます。

ここまでの流れは下の記事で確認してください!

機能実装の流れ

あみだくじアプリの機能を作っていこうと思います。

実装する機能ですが、大きく分けると

・Input情報の読み取り

・ランダムであみだくじを作成

・ランダムであたりを作る

・あみだくじをたどるボタンを作る

の4つです。

それぞれ細かい必要事項などはあるのでそれぞれの項目で詳しいことは書きます。

Input情報の読み取り

まずは前回作ったwebページにユーザーが書いた内容を読み取る機能です。

読み取るデータは、

・記載された選択肢(改行区切りで読み取り)

・あたりの数

です。

追加で選択肢が2個未満だった場合とあたりの数が選択肢の数以上だった場合にエラーメッセージを表示する用にします。

以下がプログラムと現在のwebページの状態です。

・HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>あみだくじ</title>
  <link rel="stylesheet" href="CSS/style_2.css">
</head>

<body>
  <h1>あみだくじ</h1>
  <div>
    <label for="choices">選択肢を入力してください (改行区切り):</label><br>
    <textarea id="choices" rows="8" cols="40"placeholder="例: 選択肢1
    選択肢2 
    選択肢3"></textarea><br>
    <input type="number" id="winner-count" min="1" placeholder="あたりの数" />
    <button onclick="generateAmida()">あみだくじを引く</button>
  </div><br>
  <canvas id="canvas" style="background: #ffffff;">
	</canvas>
  <script src="Javascript/app.js"></script>

</body>
</html>

・JavaScript

let choices = [];
let numberOfWinners;

function generateAmida() {
    // ユーザー入力から選択肢を取得
    const inputText = document.getElementById('choices').value;
    choices = inputText.split(/\n/).map(choice => choice.trim());

    // 選択肢が空でないかチェック
    if (choices.length < 2) {
        alert('選択肢を2つ以上入力してください');
        return;
    }

    // あたりの数を取得
    const winnerInput = document.getElementById('winner-count').value;
    numberOfWinners = parseInt(winnerInput, 10) || 1;  // 1 以上の数字を設定

    // あたりの数が選択肢より多い場合はエラー
    if (numberOfWinners > choices.length) {
        alert('あたりの数は選択肢の数より少なくなければなりません');
        return;
    }    
}

・webページ

それでは、プログラムの説明をしていきます。

HTMLに関しては前回のものと変化はありません。

JavaScriptですが、1,2行目はいいと思うので4行目から進めていきます。

まず、“generateAmida()”という関数を定義しています。

generateAmida()はHTMLの17行目で書いているように、“あみだくじを引く”ボタンを押した際に実行される関数です。

この関数の中にInput情報の読み取り機能を作っています。

選択肢の読み取り

選択肢の読み取りは6~13行目で行っています。

6行目で”choices”内に書いてある情報を読み取ります。

この”choices”はHTMLの13行目にあるように選択肢を記入するボックスのことです。

読み取った情報は改行のある文章なので改行区切りで1つ1つの選択肢として読み取ります。

それを7行目で行っています。

“.split(/\n/)”の部分で改行(\n)ごとに分割しています。

“.map(choice => choice.trim())”では配列の各要素に対して trim() を実行しています。

trim() は、文字列の前後の空白やタブ、改行などを取り除く関数です。

以上をまとめると、

  1. “choices”から読み取った文章をInputTextに入れる。
  2. InputTextの値を“.split(/\n/)”で改行区切りにして、“.map(choice => choice.trim())”で空白などを取り除く
  3. 読み取った選択肢をリストとしてchoicesに格納する。

といったことを行っています。

10~13行目のif文は選択肢の数が2個未満になっていないか確認しています。

choices.lengthでchoicesに保存したリストの個数を読み取って2未満の場合はエラーメッセージが出る仕組みです。

あたりの数の読み取り

あたりの数の読み取りも先ほどの選択肢の数の読み取りと似たような流れになっています。

HTMLで定義したようにあたりの数は“winner-count”に記入されているので、16行目でここに書かれた数値を読み取っています。

17行目では、parseInt(winnerInput, 10)によってwinnerInput を 10進数(基数10)として整数に変換しています。

abcなど文字が入力されていた場合はNaNとして認識されるようになっています。

その後の“|| 1”は左側(parseInt の結果)が「falsy(偽と評価される値)」だった場合、右側の 1 を使うという意味です。

NaN や 0、null、undefined、空文字列などは falsy とみなされます。

つまり、このコードは、

・winnerInput が有効な数値 → そのまま numberOfWinners に設定

・winnerInput が無効(数値でない、空文字など)→ numberOfWinners に 1 を設定

といった処理を行っています。

その後の20~23行目のif文ではあたりの数と選択肢の数を比較し、あたりの数が選択肢の数以上だった時にエラーメッセージを表示するようにしています。

あみだくじの生成

選択肢の数とあたりの数が決まったので次はあみだくじを作っていきます。

あみだくじは毎回ランダムで横線を引くようにします。

ランダムで横線を引くのはランダムの数値を返すMath.random()を利用して実装することにしました。

HTMLは上のプログラムと一緒なのでJavaScriptのみ記載します。

let choices = [];
let numberOfWinners;  // デフォルトのあたりの数
let lines = [];
let tree,treeHeight;

function generateAmida() {
    /* 
    上のプログラムの5行目以降 
    */
    tree = choices.length;
    treeHeight = 15;

    //canvasの設定
    const canvas = document.getElementById('canvas');
    if (!canvas.getContext || !canvas){
        return;
    }
    const ctx = canvas.getContext('2d');
    canvas.width = (tree + 1) * 50;     // 横:線と余白
    canvas.height = (treeHeight + 3) * 30; // 縦:線と余白
    ctx.clearRect(0,0,canvas.width,	canvas.height);

    ctx.strokeStyle = 'black';
    ctx.font = '14px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';

    for (let i = 0; i < choices.length; i++) {
        const x = (i + 1) * 50;
        ctx.fillText(choices[i], x, 25); // 30pxくらい上に表示
    }

    function random(){
        return Math.floor(Math.random() * 2);
    }
    
    let redLine = [];//あみだを辿るときに使う座標を格納するコンテナ
    let redPoint;

    // 隣に横線があるか確認する関数
    function hasAdjacentHorizontalLine(yIndex, x) {
        const linesAtY = redLine[yIndex];
        return linesAtY.some(pair => {
            if (!Array.isArray(pair)) return false;
            const [start, end] = pair;
            return (
                (start === x - 1 && end === x) || // 左隣に横線がある
                (start === x && end === x + 1)    // 今まさに引こうとしている場所
            );
        });
    }
    for (let y = 1; y <= treeHeight; y++){
        redPoint = 0;
        redLine[y - 1] = [];//ここで2重配列を作る
        for (let x = 1; x <= tree; x++){
            if (x === tree || random() === 0 || y === treeHeight|| hasAdjacentHorizontalLine(y - 1, x)){
                redLine[y - 1][redPoint] = [0, 0];
                redPoint++;
    
                ctx.beginPath();
                ctx.moveTo( 50 * x ,y * 30 );
                ctx.lineTo( 50 * x ,((y + 1) * 30));
                ctx.stroke();
            } else {
                for (let t = 0; t < 2; t++){  //同じ座標を同じ列に2つ置く(右回り用と左回り用)
                    redLine[y - 1][redPoint]= [x, x + 1];
                    redPoint++;
                }
                ctx.beginPath();
                ctx.moveTo( 50 * x, y * 30 );
                ctx.lineTo( 50 * x,((y + 1) * 30));
                ctx.lineTo( 50 * (x + 1),((y + 1) * 30));
                ctx.stroke();
                
            }
        }
    }
    const winnerIndices = [];
    while (winnerIndices.length < numberOfWinners) {
        const rand = Math.floor(Math.random() * choices.length);
        if (!winnerIndices.includes(rand)) {
            winnerIndices.push(rand);
        }
    }

    ctx.font = '24px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'top';
    ctx.fillStyle = 'red';

    for (let i = 0; i < winnerIndices.length; i++) {
        const x = (winnerIndices[i] + 1) * 50;
        const y = (treeHeight + 1) * 30 ; // 下に少し余白をつける
        ctx.fillText('○', x, y);
    }
    const undo = ctx.getImageData(0, 0, canvas.width, canvas.height); //赤い線のない画像を保存しておく
}

このプログラムを使用している際の流れは下のようになります。

今回は4の部分を主に作っています。

描画スペースの設定と選択肢の表示

14~26行目はあみだくじを描画する場所の設定を行っています。

今回はcanvaというものを使用するので、それを使用するという宣言が14~17行目です。

また、18行目で2次元で描くように設定しています。

19~21行目であみだくじを描く場所のサイズを設定しています。

横幅は選択肢の数に応じて変更するようにしました。

23~26行目ではcanva内での文字の設定を行っています。

色やフォントのサイズなどを設定しました。

あみだくじの上に選択肢の名前を配置しているのが28~31行目です。

選択肢の名前はchoicesの中に格納されているのでそれらを配置しました。

choices.lengthで選択肢の個数を取得してforループでそれぞれを配置しています。

ここまでであみだくじを描画するcanvaの設定と選択肢の表示が終わりました。

ランダムな数値の生成

ランダムな数値を生成するのは33~35行目で行っています。

ここではMath.random()を利用しました。

Math.random()を利用すると0~1までの小数が生成されます。

今回はそれに2を書けることで0~2の値がランダムで生成されるようにしました。

その後、Math.floor()を利用すると小数点以下を切り捨てることができます。

これらを組み合わせMath.floor(Math.random()*2)とすることでランダムで0か1の値を生成することができるようになりました。

横線があるかどうかの確認

ここから37行目以降の話になります。

37,38行目では値を格納するためredLineとredPointを定義しています。

これらは後々あみだくじをたどるために必要になります。

あみだくじをたどる際にどこに横線があるかなどを記憶しておくためのものになります。

41~51行目であみだくじを生成する際に横線の横に横線を作らないための関数です。

あみだくじなので横線が続くとどうしていいかわからなくなりますしね。

まず、関数のyIndexとxですがこれはあみだくじの高さと何本目かです。

あみだくじの縦線を横に並べたときに座標を決めると落ちていく方向をy、横方向をxとしています。

そして、const linesAtY = redLine[yIndex]でredLineに保存しておいた各高さにおける横線の座標のデータを取り出します。

例えば、redLine[2] = [[0, 1], [2, 3]] なら、2段目に2本の横線(0番〜1番、2番〜3番)があるということになります。

その後のコードですが、44行目のif文でデータにおかしなものが入っていた際にfalseを返して無視するようにしています。

また、そのようなことがなかった場合は47,48行目で再度、線が引けるか確認しています。

左隣に横線があるか、今引こうとしている位置にすでに横線があるのどちらかがtrueなら true を返す=「その場所には線を引けない」という意味です。

実際に横線を追加する前にこの関数でチェックし、「OKなら追加」という流れになります。

あみだくじの生成

52行目のfor文では1段目から最下段までループして、各段に線を描画するようにしています。

次の2行では

redPoint: 現在の段に何本線を登録したかカウント

redLine[y – 1]: この段(y-1行目)の線リスト(2重配列)

という処理をしました。

さらに55行目のforループで各縦線ごとに処理を始めます。

ここまでの2つのforループで格段において各縦線の処理を行うという流れになりました。

次のif文で下記の条件のときは、横線を引かず、ただの縦線だけを描画するようにしています。

・x === tree:右端なので横線引けない

・random() === 0:ランダムで「引かない」場合

・y === treeHeight:一番下の段は横線を引かない

・hasAdjacentHorizontalLine(…):隣に線があると引けない(ルール)

縦線のみを引く処理が57~63行目です。

redLine に [0, 0] を登録して、「ここには横線なし」と記録し、redPointの値を1つ大きくします。

その位置に「縦線だけ」を描画するためにctxで下向きの線を描くようにしました。

横線を引く場合が64~73行目です。

横線を引くときは2回登録を行っています。これはあみだを「右回り」「左回り」でたどるためです。

双方向に対応した構造にしておきました。

線を描画する際は縦線+横線のL字を描画して、あみだっぽい形になります。

このようにしてfor文を回すことであみだくじができるようになりました。

あまりうまく説明できていない気がしますが、プログラムを読んでいると徐々にわかると思います。

何度も読んでみてください。

あたりの作成

長くなりましたがこれで最後です。

最後はあみだくじの下にあたりの丸を付けます。

あたりとなる部分もランダムになるようにしました。

まず、78~84行目ですが、ここではあたりの丸を付ける場所をランダムで決定しています。

まず、空の配列 winnerIndices を作成します。

この配列には、ランダムに選ばれたインデックス(rand)が格納されます。

while ループでは、winnerIndices の長さが numberOfWinners に達するまで、ランダムなインデックスを選びます。

Math.random() と Math.floor() を使用して、0 以上 choices.length 未満のランダムな整数 rand を生成します。

この方法は34行目で行ったのと同様の方法です。

winnerIndices.includes(rand) で、すでに選ばれたインデックスが winnerIndices 配列に含まれていないか確認し、含まれていなければそのインデックスを配列に追加します。

これをあたりの数を超えないようにwhileループで繰り返します。

以上でランダムであたりとなる場所が決定されました。

次の86~89行目であたりとなった場所に表示する“〇”の書式を定義しています。

決めているのはフォント、場所、サイズです。

あまり難しい部分ではないので飛ばします。

最後に91~94行目で実際にあたりの“〇”を書いていきます。

winnerIndices 配列の各インデックスについてループを行います。

x は、インデックス(winnerIndices[i])に基づいて計算された位置です。+1 を加えて、50px ごとに配置されるようになっています。

y は、treeHeight + 1 に 30 を掛けた値で、少し下に余白を加えて位置を調整しています。

ctx.fillText(‘○’, x, y) で、指定した位置に赤い「○」を描画します。

以上でランダムであたりとなる部分を選び、あたりとして“〇”を表示できるようになりました。

まとめ

今回はあみだくじアプリにあみだくじを生成する機能を加えていきました。

あまりうまく説明できていない部分も多いと思うのでよくわからない部分があったらコメントしてください。随時、修正していきます。

かなり完成してきたのであともう少しです。

次回はあみだくじをたどるためのボタンを配置していきます。

それではまた次回!

コメント

タイトルとURLをコピーしました