見出し画像

「りんご」と言うと「ゴリラ」と言うのはどうやってプログラミングする? ー 第2章❸ 配列の中からループで言葉を見つける ー 『JavaScriptでやる1分間プログラミング』

前回はボタンをクリックすると音声認識がスタートし、そこでユーザーが「りんご」のように何か言葉を言うとそれを取得するところまでプログラミングをしていきました。まさにしりとりの”第一歩”をコーディングしたわけです。

ここでコーディングを簡単に振り返ってみます。

HTMLのbuttonタグの中で、ボタンクリック(onclick)に関数(SayToShiri)をリンクさせ、ボタンがクリックされるとその関数が呼び出されます(つまり実行されるということ)。

そして、SayToShiri関数の中で音声認識をするMagicRecognizeを呼び出しました。ここでマイクがオンになり、皆さんが何か言うことを待つ状態になります。何か言葉を言うと、それが音声認識機能でテキストに変換され、その処理が完了すると、そこからコールバック関数としてShiritori関数を呼び出す仕組みです。今のところこの関数では認識された言葉をAlertのポップアップで表示するだけになっています。

画像1

プログラミングではこのように「Aが起きたらBが実行される」というような動作の連続として(sequence = スィークエンス)書いていくのがほとんどです。onclick -> SayToShiri, MagicRecognize > Shiritori のようにプログラミングの流れ(flow)を考えていくのです。今はどうしてこんな複雑なことをやるのだろうと思うかもしれませんが、理解していくとどうして関数を作って動作をまとめていくのがよいのかはっきりとわかってきます

Shiriが言える言葉を「配列」に入れる

では「りんご」という言葉が取得できたので、次は「ご」で始まる言葉を探します。その前に、Shiriが選べる言葉のリストを作りましょう。今回のしりとりゲームでは、コンピューターのShiriにはこうした制限をかけます(なぜ制限するかは前回詳しく説明しました)。

Shiriは果物野菜の名前だけを言う

そこで果物と野菜の名前をどこかから持ってきて、それをもとにリストを作ります。今回参考にしたのはこのサイトです。

これを参考にすると野菜と果物で200種類の名前がゲットできます。しりとりとりに使う言葉としては十分でしょう。

ではこの200種類の名前を入れておく配列の指定はどうやるのでしょうか。

まず配列の入れ物の名前はShiriWordsとします。そしてそれを括弧を付けて宣言すると完了です。

const ShiriWords = [];

イメージとしてはこれでこんな入れ物ができたと考えてください。

画像2

データの入れ物には「変数」と「定数」がある

ここでletではなくconstとしているのは、Shiriがしゃべる言葉のリストはプログラミングの中で変更をしないからです。constというのはconstant(固定の、不変の)という意味で、これを付けて作った入れ物は中身を途中で変えることができません。つまりそれは「変数」ではなく「定数」ということです。

もちろんletでもプログラムは動きますが、単語リストを「定数」として指定するのには意味があります。今回のような単純なプログラムだと分かりづらいのですが、例えばShiriは200個の単語しかしらないはずなので、別のプログラムでその個数が変わったらゲームがおかしくなりますよね。データの入れ物は中身をどんどんと入れ替えて使うものと、一度入れたらずっとそのままにしておくものと2種類あると考えておいてください。

そこで今回は上記で紹介したサイトから野菜と果物の名前をこのShiriWords配列に入れました。

const ShiriWords = ["アーティチョーク", "アイスプラント", "あけび", "アサイー", 
"あしたば", "アスパラガス", "アセロラ", "アピオス", "アボカド", "アメリカンチェリー", 
"アロエ", "あんず", "いちご", "いちじく", "いよかん", "ウコン", "うど", "エシャレット", 
"エシャロット", "おかひじき", "おかわかめ", "オクラ", "オリーブ", "オレンジ", "かいわれ大根", 
"かぶ", "かぼす", "かぼちゃ", "カムカム", "からし菜", "ガラナ", "カリフラワー", "かりん", 
"かんぴょう", "キウイ", "キムチ", "キャベツ", "きゅうり", "ぎょうじゃにんにく", "グァバ", 
"クコの実", "クランベリー", "グリーンピース", "グレープフルーツ", "クレソン", "くわい", 
"ケール", "ゴーヤ", "ゴールデンベリー", "コールラビ", "ココナッツ", "こごみ", "ごぼう", 
"ザーサイ", "さくらんぼ", "ざくろ", "さやいんげん", "さやえんどう", "サンチュ", 
"シークワーサー", "ししとう", "しそ", "ジャックフルーツ", "じゅんさい", "スイカ", "ずいき", 
"スウィーティー", "スターフルーツ", "すだち", "ズッキーニ", "スナップエンドウ", "スプラウト", 
"すもも", "せとか", "せり", "セロリ", "ぜんまい", "そうめんかぼちゃ", "そら豆", "ソルダム", 
"タアサイ", "タイガーナッツ", "たけのこ", "タマリンド", "たらの芽", "チコリ", "チンゲン菜", 
"つくし", "つるむらさき", "デコポン", "とうもろこし", "トマト", "ドライフルーツ", 
"ドラゴンフルーツ", "ドリアン", "とんぶり", "ナス", "なつみかん", "なつめ", "ニラ", 
"にんにく", "にんにくの芽", "ぬか漬け", "ねぎ", "ネクタリン", "ノニ", "ノビル", 
"パイナップル", "パクチー", "バジル", "ハスカップ", "パセリ", "はっさく", "パッションフルーツ", 
"バナナ", "パパイヤ", "パプリカ", "ハヤトウリ", "ビーツ", "ピーマン", "ピクルス", "びわ", 
"フェイジョア", "フェンネル", "ふき", "ふきのとう", "フダンソウ", "ぶどう", "ブラックベリー", 
"ブルーベリー", "プルーン", "ブロッコリー", "ブロッコリースーパースプラウト", "ブンタン", 
"へちま", "ほうれん草", "ポポー", "ポンカン", "マカ", "マキベリー", "マコモダケ", 
"マスカット", "マルベリー", "マンゴー", "マンゴスチン", "みかん", "ミニトマト", "みょうが", 
"ミント", "メキャベツ", "メロゴールド", "メロン", "メンマ", "もやし", "モロヘイヤ", "ヤマモモ", 
"ヤングコーン", "ゆず", "ユリ根", "よもぎ", "ライチ", "ライム", "ラズベリー", "らっきょう", 
"ラディッシュ", "りんご", "ルッコラ", "ルバーブ", "レーズン", "レタス", "レモン", "レンコン", 
"わらび", "うめ", "うめぼし", "えだまめ", "かき", "きりぼしだいこん", "きんかん", "こうらいにんじん", 
"こまつな", "しかくまめ", "しゅんぎく", "しょうが", "だいこん", "たかな", "たまねぎ", "とうがん", 
"とうみょう", "なし", "なのはな", "にんじん", "のざわな", "はくさい", "みずな", "みつば", "むらさきキャベツ", 
"むらさき玉ねぎ", "もも", "ようなし"];

こんなにたくさんのリストでも、単なるコピペでできないこともないのですが、フォーマットしたり先頭の漢字をひらがなにしたりと多少のデータ処理は必要となります。ウェブサイトのデータをもとにリストを作る方法についてはここでは詳しく触れませんが、皆さんはこの定数をそのまま使ってください。これをShiritori.jsの中に入れるとこうなります(注:コードではリスト項目に改行を入れていないため一行になっています)。

//====================================================
// MagicWand (JS Version)
//====================================================
var SpeechRecognition = SpeechRecognition || webkitSpeechRecognition;
//音声合成
function MagicSpeak(text) {
   var voice = new SpeechSynthesisUtterance();
   voice.text = text;
   voice.volume = 1;
   voice.rate = 1;
   voice.pitch = 1;
   voice.lang = "ja-JP";
   window.speechSynthesis.speak(voice);
}

//音声認識
function MagicRecognize(callback) {
   var recognition = new SpeechRecognition();
   recognition.continuous = false;
   recognition.lang = "ja-JP";
   recognition.interimResults = false;
   recognition.maxAlternatives = 1;
   recognition.start();
   recognition.onresult = function (event) {
       var reco = event.results[0][0].transcript;
       //コールバック関数を呼び出す
       callback(reco);
   }
}

//Shiriの単語リスト:野菜と果物合わせて200語
const ShiriWords = ["アーティチョーク", "アイスプラント", "あけび", "アサイー", "あしたば", "アスパラガス", "アセロラ", "アピオス", "アボカド", "アメリカンチェリー", "アロエ", "あんず", "いちご", "いちじく", "いよかん", "ウコン", "うど", "エシャレット", "エシャロット", "おかひじき", "おかわかめ", "オクラ", "オリーブ", "オレンジ", "かいわれ大根", "かぶ", "かぼす", "かぼちゃ", "カムカム", "からし菜", "ガラナ", "カリフラワー", "かりん", "かんぴょう", "キウイ", "キムチ", "キャベツ", "きゅうり", "ぎょうじゃにんにく", "グァバ", "クコの実", "クランベリー", "グリーンピース", "グレープフルーツ", "クレソン", "くわい", "ケール", "ゴーヤ", "ゴールデンベリー", "コールラビ", "ココナッツ", "こごみ", "ごぼう", "ザーサイ", "さくらんぼ", "ざくろ", "さやいんげん", "さやえんどう", "サンチュ", "シークワーサー", "ししとう", "しそ", "ジャックフルーツ", "じゅんさい", "スイカ", "ずいき", "スウィーティー", "スターフルーツ", "すだち", "ズッキーニ", "スナップエンドウ", "スプラウト", "すもも", "せとか", "せり", "セロリ", "ぜんまい", "そうめんかぼちゃ", "そら豆", "ソルダム", "タアサイ", "タイガーナッツ", "たけのこ", "タマリンド", "たらの芽", "チコリ", "チンゲン菜", "つくし", "つるむらさき", "デコポン", "とうもろこし", "トマト", "ドライフルーツ", "ドラゴンフルーツ", "ドリアン", "とんぶり", "ナス", "なつみかん", "なつめ", "ニラ", "にんにく", "にんにくの芽", "ぬか漬け", "ねぎ", "ネクタリン", "ノニ", "ノビル", "パイナップル", "パクチー", "バジル", "ハスカップ", "パセリ", "はっさく", "パッションフルーツ", "バナナ", "パパイヤ", "パプリカ", "ハヤトウリ", "ビーツ", "ピーマン", "ピクルス", "びわ", "フェイジョア", "フェンネル", "ふき", "ふきのとう", "フダンソウ", "ぶどう", "ブラックベリー", "ブルーベリー", "プルーン", "ブロッコリー", "ブロッコリースーパースプラウト", "ブンタン", "へちま", "ほうれん草", "ポポー", "ポンカン", "マカ", "マキベリー", "マコモダケ", "マスカット", "マルベリー", "マンゴー", "マンゴスチン", "みかん", "ミニトマト", "みょうが", "ミント", "メキャベツ", "メロゴールド", "メロン", "メンマ", "もやし", "モロヘイヤ", "ヤマモモ", "ヤングコーン", "ゆず", "ユリ根", "よもぎ", "ライチ", "ライム", "ラズベリー", "らっきょう", "ラディッシュ", "りんご", "ルッコラ", "ルバーブ", "レーズン", "レタス", "レモン", "レンコン", "わらび", "うめ", "うめぼし", "えだまめ", "かき", "きりぼしだいこん", "きんかん", "こうらいにんじん", "こまつな", "しかくまめ", "しゅんぎく", "しょうが", "だいこん", "たかな", "たまねぎ", "とうがん", "とうみょう", "なし", "なのはな", "にんじん", "のざわな", "はくさい", "みずな", "みつば", "むらさきキャベツ", "むらさき玉ねぎ", "もも", "ようなし"];

画像3

りんごの「ご」はどうやってプログラミングする?

さて、「リンゴ」という言葉が入ってきたら、Shiritori関数の中でいよいよ「ゴリラ」を探します。ただ、ゴリラは野菜・果物ではないので、Shiriとしりとりをする場合は恐らく「ごぼう」あたりなのでしょう。いずれにしてもShiritori関数の中にそのロジックを書いていきます。今はShiritori関数はAlertの行は削除して、完全に空にしています。

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Shiriとりゲーム</title>
   <script src="shiritori.js"></script>
</head>
<body>
   <h1>Shiriとりゲーム</h1>
   <button type="button" onclick="SayToShiri();">Shiriに言葉を言う</button>
   <script>
       function SayToShiri() {
           MagicRecognize(Shiritori);
       }

       function Shiritori(text) {
         //ここに「リンゴ」⇒「ゴリラ」のコードを書く
       }
   </script>
</body>
</html>

そこで、Shiritori関数にすでにtextという変数が渡されているのに気づきましたか。実はこれが音声認識された言葉、つまり「りんご」なのです。これはMagicRecognizeが音声認識で取得した言葉がここに入ってくるわけです。

最後の文字をプログラミングで取得する

まず最初にやらないといけないのは、textに入っている言葉の最後の文字が何であるかをチェックすることです。そのコードがこれです。

text.slice (-1);

文字列にはsliceという関数があり、これを使って文字列の中の特定の文字をゲットできます(注:以下で”番目”を見るときにインデックスが0から始まっていることに注意。0,1、2と数えていくため”3番目の文字”のインデクスは2になります)。

text.slice (何番目の文字から, 何番目の文字まで)
✅ 文字列の一番最初の文字 ⇒ text.slice (0, 1)
✅ 先頭から3文字分 ⇒ text.slice(0, 2)
✅ 2文字目から5文字目まで ⇒ text.slice(1, 4)
数字一つだと何番目から後すべてということになります。
✅ 3文字目以降全部 ⇒ text.slice(2)

そしてここでマイナスの数字を使うと「後ろから○番目」という意味になります。

✅ 先頭3番目の文字から、後ろ2番目の文字まで ⇒ text.slice(2, -2)
数字一つだと後ろから何番目以降すべてということになります。
✅ 最後から3文字分 ⇒ text.slice(-3)
✅ 最後の1文字 ⇒ text.slice (-1)

これを使うと、text.slice (-1)で「りんご」の「ご」が取得できます。

//これでポップアップに「ご」が表示される
let text = "りんご";
alert (text.slice(-1));

「ご」で始まる野菜・果物はどうやって探す?

ユーザーが言った言葉の最後が「ご」だと分かったら、あとはShiriWordsの中から「ご」で始まる言葉を探します。ShiriWordsには200の言葉が入っていました。それを一つひとつ見て、「ご」から始まるかどうかを調べればよさそうです。その「一つひとつ見る」というのはどうやるのでしょうか。

果物・野菜名を「ループ」する方法

ではここでJavaScriptで配列を一つひとつチェックする「ループ」という方法についてみていきます。

ループにはいくつか方法がありますが、一番複雑そうで実は一番簡単なのがforループです。以下に書いたShiritori関数の中の4行をよく見てください。

function Shiritori(text) {
   //Shiriが選ぶ言葉を探す
   for (let i = 0; i < ShiriWords.length; i++) {
       //Shiriの先頭の文字がmyWordの最後の文字と同じだったら
       if (ShiriWords[i].slice(0, 1) == text.slice(-1)) {
           //音声で選んだ言葉を言う
           MagicSpeak (ShiriWords[i]);
           //ループを脱出する
           break;
       }
   }
}

forループはこんな仕組みです。セミコロンで区切られた3つの箇所で、3つのループ条件を指定します。

for (1.最初のインデクス;2.どこまでループするか;3.何個ずつみるか) {
   //一個一個に実行する処理を書く
}

そこで上のコードではこうループを指定していました。

   for (let i = 0; i < ShiriWords.length; i++)

これはまず、① ループは0番目からスタートします。ShiriWords.lengthといのは配列の長さ、つまり全部の個数です(つまり200個)。ですから0番目からスタートして② 最後200個目(順番で言うと199番目)までループします。そしてi++というのは飛ばさないで ③ 一個一個インデックスのiを増やしていくということなのです。

これでiという数字が0から一つずつ増えていきます。つまり、

ShiriWords[0], ShiriWords[1], ShiriWords[2], ShiriWOrds[3] .... ShiroWords[199]

iの数字が上がることでShiriWordsの要素に一つひとつアクセスできるということです。ちなみに実際のShiriWordsはこんな感じでしたよね。

const ShiriWords = ["アーティチョーク", "アイスプラント", "あけび", ・・・

なので、それぞれの値はこうなります。

ShiriWords [0] は アーディチョーク
ShiriWords [1] は アイスプラント
ShiriWords [2] は あけび ・・・


なんだかループは複雑で今一つピンときませんよね。でも実はこれが本当に一番わかりやすいループ方法なんです。他のループについては後でまた解説する機会を設けます。今はこの書き方を真似てやるだけで十分です。そのうち慣れてくるとその意味がよく分かるようになるはずです。

リン「ご」⇒ 「ゴ」リラのプログラムは?

このループで200個の野菜・果物の名前をチェックできるようになります。そして一つひとつ見ていく中で、ユーザーが言った言葉の最後の文字と、Shiriが選ぶ言葉の先頭文字が同じかどうかをチェックします。それがこのIF文です。

if (ShiriWords[i].slice(0, 1) == text.slice(-1)) 

これで、ShiriWordsの名前の先頭の文字と、textの最後の文字が同じであるかどうかをチェックしています。

もし二つの文字が同じならその言葉がShiriが選ぶ言葉です。言葉がピックアップできたらMagicSpeakでその野菜・果物の名前をしゃべるようにします。言葉が選ばれたらもうループする必要はありません。ですからbreakを実行してループをストップさせます(実際にはループを抜け出します)。

もし文字が同じでなかったら、例のi++で次の野菜・果物の名前に移ります。言葉が見つかるまでこのループは止まらないのです。

「りんご」と言ったら「ごぼう」と言った!

ではindex.htmlはこうなります。変更したらファイルを保存し、ブラウザで試してみてください。

<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Shiriとりゲーム</title>
   <script src="shiritori.js"></script>
</head>
<body>
   <h1>Shiriとりゲーム</h1>
   <button type="button" onclick="SayToShiri();">Shiriに言葉を言う</button>
   <script>
       function SayToShiri() {
           MagicRecognize(Shiritori);
       }

       function Shiritori(text) {
           //Shiriが選ぶ言葉を探す
           for (let i = 0; i < ShiriWords.length; i++) {
               if (ShiriWords[i].slice(0, 1) == text.slice(-1)) {
                   MagicSpeak (ShiriWords[i]);
                   break;
               }
           }
       }
   </script>
</body>
</html>

ブラウザ内の「Shiriに言葉を言う」ボタンをクリックして「りんご」と言ってみてください。すると「ごぼう」と言うはずです!

画像4

では続けてみましょう。「ごぼう」の「う」が来たので、今度は「うさぎ」というと、

ぎょうじゃにんにく

「行者にんにく」ときました! では「く」なので「クッパ」と言ってみます。すると、

パイナップル

はい、とてもいい感じでしりとりになってますよね!

画像5

でも何かがおかしい・・・

先に示したしりとりパターンは確かにうまくいくのですが、皆さんが好きな言葉でしりとりをやるとおかしなところがでてきます。例えば、

✅ 言葉をいっても何も返さない
✅ 音引きで終わる言葉(例:クッキー)は絶対に何も言わない
✅ 拗音の小さい文字で終わる言葉(例:キャリーぱみゅぱみゅ)も絶対に何も言わない
✅ 平気で「ん」で終わる言葉(例:レモン)を言う
✅ 自分が「ん」で終わる言葉を言うと何も言わない

なんだか問題続出ですよ!

画像6

さて、これからこの問題をなんとか解決していきます。その中でプログラミングの基礎をじっくり理解する例が続々と出てきますので是非続けていってください!

ということで次回はこの「バグ」のいくつかを修正してみます。

乞うご期待!


この記事が気に入ったらサポートをしてみませんか?