前提・実現したいこと
JSの練習として簡単なゲームを作成しています。
1~13の数字の中で、自身が選択した数字とCOMが選択した数字を比べて
大きいほうが勝ち。
一度出した数字は再度選択することはできず、13セット繰り返して
勝利したセット数が多かったほうがゲームに勝つというシンプルなルールです
自身はセレクトボックスから数字を選択。
COMはrandom関数で数字を取得・選択させる方法をとっています。
発生している問題
表題のように、COMがranndom関数で一度出した数字を
再度出せないようにしたいのですがうまくいきません。
下記のコードでは同じ数字を選択してしまいます。
該当のソースコード
let history_com_num = []; let selected_com_num = getRandomNum(13) + 1; for(let i = 0; i < history_com_num.length; i++){ if(history_com_num[i] !== selected_com_num){ break; } else { selected_com_num = getRandomNum(13) + 1; } } if(selected_p_num > selected_com_num){ setMessage(`第${current_set_num + 1}回目はあなたの勝ち!`); } else if(selected_p_num < selected_com_num){ setMessage(`第${current_set_num + 1}回目はCOMの勝ち!`); } else{ setMessage(`第${current_set_num + 1}回目はドロー!`); } history_com_num.push(selected_com_num);
Javascript
試したこと
history_com_numの配列に選択したカードをpushで入れていき、
for文で回してrandom関数で選択した数字と配列内の値が一致しない
場合は抜ける、同じ場合は再取得というコードのつもりです。
補足情報(FW/ツールのバージョンなど)
ここにより詳細な情報を記載してください。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答3件
0
質問にあるコードはダメとは言いませんが、そのやり方だとちょっと面倒な気がします。
とりあえず、プログラミングってのは『こう書けばいい』というものではなく、現実世界のシミュレーションです。
一旦、現在のコードを読んでみましょう。コードを読むコツは『一行レベルで、その行が何をしているかを考えながら読む』です。
JavaScript
1// history_com_numを配列とする 2let history_com_num = []; 3 4// 1~13までの乱数をselected_com_num入れる 5let selected_com_num = getRandomNum(13) + 1; 6 7 // history_com_numの個数分、ループ 8 for(let i = 0; i < history_com_num.length; i++){ 9 // 現在のhistory_com_numの値が乱数と同じでないなら 10 if(history_com_num[i] !== selected_com_num){ 11 // ループから抜ける 12 break; 13 // それ以外なら(= 同じなら) 14 } else { 15 // 乱数を再発行! 16 selected_com_num = getRandomNum(13) + 1; 17 } 18 } 19 20// selected_p_num が selected_com_numより大きいなら 21if(selected_p_num > selected_com_num){ 22 // 第***回目はあなたの勝ち...のように出力 23 setMessage(`第${current_set_num + 1}回目はあなたの勝ち!`); 24 25// 以下略 26
これを疑似コードとして抜き出してみる。
1. history_com_numを配列とする 2. 1~13までの乱数をselected_com_num入れる 3. history_com_numの個数分、ループ 3.1. 現在のhistory_com_numの値が乱数と同じでないなら 3.1.1. ループから抜ける 3.2. それ以外なら(= 同じなら) 3.2.1. 乱数を再発行 4. selected_p_num が selected_com_numより大きいなら 4.1. 第***回目はあなたの勝ち...のように出力 ...
この疑似コードをもとに実際に手作業でシミュレーションしてみてください。つまり、数学の手順なり、料理のレシピとしてなりでやってみる。実際の値を入れてみたりとか。
まず、(1)で配列を用意。(2)で乱数を発行。で、selected_com_num = 3 だとする。
(3)でループしようとしますが、まだhistory_com_numには何も入っていません。空の状態です。なのでループせずに(4)へ。
(4)では、selected_p_numが何者かわかりませんが、ユーザが入力した値だと仮定して、比較する。
selected_p_num = 5 なら 5 > 3 を満たすので(4.1)を処理する。つまり『第**回目はあなたの勝ち』とか。
で、最後のやつで配列に追加。
で、仮にこのコードはlet history_com_num = [];
以外は関数になっていて、その関数を何度も呼んでいると仮定すると、
history_com_num = { 3 }
となっている。
で、(2)に行き、selected_com_num = 3 とする。history_com_num には1個データが入っているので、(3)でループします。
(3.1) で一個目のデータ 3 と selected_com_num を比較する。 3≠3 は満たさないので処理せず。
その次の(3.2)で強制的に処理。つまり(3.2.1)で乱数を再発行。そこで selecteced_com_num = 3 だとする。
でもそのまま(4)へ。……となる。
そうすると history_com_num = { 3, 3, 3, 4, 1, 2, 2, 3,... }
のように同じ数字が出てくる可能性がある。
乱数の再発行で別の数字が出ればなんとかなるかもしれませんが。
もし、そもそも関数にもせずに、完全にそのコードだけなら、複数回選択しないので…
で、私なら、『現実世界でならどうするか』を考えてみます。たとえば、『手作業でやるならどうするか』とか。
そう考えると、私なら、
0. 過去履歴として配列を持つ 1. 乱数発行し、selectedNum に入れる 2. 無限ループ 2.1. 過去履歴にselectedNumと同じものが無いなら 2.1.1. ループから抜ける 2.2. 乱数を再発行 3. selecedNumの値を過去履歴として保存 4. ユーザ入力と比較して…
と言う風にやりますね。
乱数を再発行するのはいつですか? そう、数字を乱数として取り出すときと、「過去履歴にあるものと同じであったとき」ですね。
ということは、言い換えると『常に乱数を発行し続け、今までに無いものなら乱数発行を止める』ということが可能ですね。
よって、(2)では無限ループで、(2.1)で「同じものが無いなら無限ループから抜ける」と言う風にする。
そうすれば、違う値が出るまで何度もループすることになりますし。
さらに、(2.1)では、実際にはさらにhistory_com_num分ループしています。ですがこれは関数にまとめて、
if( hasSameValue( history_com_num, selectedNum ) )...
のようにすればコンパクトにできます。
あえて関数にせずともできるとは思いますが、面倒なので私なら関数にする。
後はこれを実装するだけですね。
それと、他の方も仰っているように「配列として用意して、それをシャッフルする」という手も取れますね。
Fisher Yates Shuffleだったかな。
これのように、いったん、selectedNums = { 1, 2, 3, 4, 5, ..., 12, 13 } と入れていき、
それをシャッフルして selectedNums = { 2, 4, 12, 6, 1, ... } のようにランダムな並びにするっていうことも可能ですね。
ただし、これは今回のように「13回行う」とかみたいに回数が分かっている前提ですね。
起動ごとに乱数を発行して…とかだと厳しいです。(過去履歴はどこに保存しておくのかとか)
投稿2021/12/04 08:13
総合スコア4962
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
以下の順に回答します。
- 配列のシャッフルにより、COMの出す数字の配列を作る案
- ご質問にあるコードと同様のものを実装してみる案
- ジェネレータを使う案
3.1 ジェネレータを使ったコード例 (???? サンプル )
1. 配列のシャッフルでCOMの出す数字の配列を作る案
COMが1回目から13回目までの各セットに選択する数字の配列を、対戦開始時にあらかじめ作っておく、という方法が考えられます。この配列は1から13までの整数を1個ずつ含み、かつ並び順をランダムにしたもので、たとえば以下のようなものです。
[6,13,10,2,4,11,7,8,3,9,12,5,1]
このとき、COMは最初に6を出し、次に13を出し、3回目に10を出し・・・というように数字を出してきます。
このような配列を作るには、まず1から13までの整数を1個ずつ含み、かつ、並び順が昇順のもの
[1,2,3,4,5,6,7,8,9,10,11,12,13]
を作り、これをランダムに並び替えます。配列の順序をランダムに並び替えるのはシャッフル(shuffle)と呼ばれます。
シャッフルのアルゴリズム
でググると上位のほうに
がヒットすると思います。これが有名です。これのお手本となるjavaScriptのコードも探せば色々と出てきますが、たとえばStackoverflowで高評価のベストアンサーとなっている、これあたりを拝借すればよいかと思います。
javascript
1// ???? https://stackoverflow.com/a/2450976 2function shuffle(array) { 3 let currentIndex = array.length, randomIndex; 4 5 // While there remain elements to shuffle... 6 while (currentIndex != 0) { 7 8 // Pick a remaining element... 9 randomIndex = Math.floor(Math.random() * currentIndex); 10 currentIndex--; 11 12 // And swap it with the current element. 13 [array[currentIndex], array[randomIndex]] = [ 14 array[randomIndex], array[currentIndex]]; 15 } 16 17 return array; 18}
以下は、上記のshuffle
を使ってCOMの出す数字の配列 comNumbers
を作る例です。
javascript
1const comNumbers = shuffle([...Array(13)].map((_, i) => i+1)); 2console.log(comNumbers); // => .e.g. [6,13,10,2,4,11,7,8,3,9,12,5,1]
2. ご質問にあるコードと同様のものを実装してみる案
質問にある、
history_com_numの配列に選択したカードをpushで入れていき、
for文で回してrandom関数で選択した数字と配列内の値が一致しない
場合は抜ける、同じ場合は再取得というコード
と同じ趣旨のコード例を挙げると以下です。(selected_com_num
が history_com_num
に含まれていない値になるまで、selected_com_num = getRandomNum(13) + 1
を繰り返す部分のみです。history_com_num
にpushで追加する部分は割愛しています。)
javascript
1let selected_com_num = getRandomNum(13) + 1; 2 3while (true) { 4 let index = -1; 5 for (let i=0; i < history_com_num.length; i ++) { 6 if (history_com_num[i] === selected_com_num) { 7 index = i; 8 break; 9 } 10 } 11 if (index === -1) { 12 break; 13 } 14 selected_com_num = getRandomNum(13) + 1; 15} 16 17console.log(selected_com_num);
全体の構造は、while(true)
という無限ループです。ここから抜けるのは
javascript
1 if (index === -1) { 2 break; 3 }
という break
です。ここでbreak
されるのは、index
が-1のときですが、index
が-1のときというのは、selected_com_num が、配列history_com_numに含まれていないときです。含まれているときは、forループによって、一致したhistory_com_numの要素のインデクスi
が index
に代入されますが、それは0以上の整数です。
index が 0以上だった場合は、whileループ本体の最後で、再び
javascript
1selected_com_num = getRandomNum(13) + 1;
を実行して、selected_com_num を取り直します。
ですが、これはあまりいいやり方ではありません。たとえば、history_com_numが[2, 8, 4]
だったとして、万が一、 selected_com_num
が 2 か 8 か 4 のどれかであり続けると、while からbreakしません。それでも実用上問題ないのは、
そう多くない試行回数で、selected_com_num は、history_com_num に含まれない数になるはず
という楽観的な見通しがほぼ間違いなく成り立つからです。しかし、実用上問題ないと言える根拠としては
selected_com_numの取り得る値の場合の数が高々13だから
というやや曖昧というかざっくりな理由によるものなので、私見としては正直あまりスッキリしないモヤモヤが残るコードという感じがします。
3. ジェネレータを使う案
最後に、ジェネレータを使うコードを紹介します。
ユーザーの対戦相手であるCOM が備えるべき仕組みとして、以下のようなものを考えます。対戦の開始時点で、COM は以下のような配列を内部に持っています。
- COM の内部にある配列の初期状態:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
第1セットに臨むにあたって、COMは上記の配列からランダムにひとつを選びます。例えば、6 が選ばれたとしましょう。これは、COMが内部にもつ配列から 6 が取り出されたことを意味します。
- 第1セットで選ばれた数: 6
このとき、COMの内部にある配列から、選ばれた6が削除されるような仕組みをCOMは持っているとします。そうすると、第1セット終了後のCOMの内部にある配列は以下になります。
- 第1セット終了後のCOM内部の配列:
[1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13]
第2セットでCOMは上記の第1セット終了時の配列からランダムにひとつを選びます。例えば、11 が選ばれたとしましょう。以下のようになります。
- 第2セットで選ばれた数: 11
- 第2セット終了後のCOM内部の配列:
[1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13]
以降のセットも、COMは内部に持っている配列からひとつの要素をランダムに取り出して、それをユーザーとの対戦に使用します。取り出された要素は配列から削除されるようにすれば、13セット目の終了時には、以下のように空の配列になるはずです。
- 第13セット終了後のCOM内部の配列:
[]
このような仕組みを持つCOMが実装されたオブジェクトが、com
という変数に入るとしましょう。com
が備えるべき要件は以下です。
-
com
は初期状態で、[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
という配列を内部に持っている。 -
com
には、次のセットで出す数字(またはその数字を持つオブジェクト)を得るためのメソッドがある。このメソッド名を仮にcom.next()
とする。 -
com.next()
が返す数(あるいは、返すオブジェクトがあるプロパティとして持つ数)はCOMが次に出す数とする。これはそれまでに出した数のどれとも重複してはならない。 -
上記の重複排除を実現するために
com.next()
するたびに内部の配列から、次に出す数に選ばれたものを削除する。 -
初期状態では
com
は1から13までの13個の数を持っているので、com.next()
で、COMの出す数として有効な数値が返されるのは13回目までで、14回目以降は(対戦に使える有効な)数を返さないように構成されている必要がある。
このようなnext
メソッドを持つcom
を実装する方法として、何らかのクラスを自作してそれのインスタンスとしてcom
を得ることもできるでしょう。けれども、もっと簡単な方法があります。それはジェネレータ関数を使って、これの返す値としてcom
を得ることです。
com
を得るためのジェネレータ関数は以下のようなものになります。
javascript
1// 1以上n以下の整数をランダムな順序で各1回ずつ生成するジェネレータを返す。 2function* randomSequenceFromOneTo(n) { 3 const numbers = [...Array(n)].map((_, i) => i+1); 4 5 while (numbers.length > 0) { 6 const index = Math.floor(Math.random() * numbers.length); 7 const value = numbers[index]; 8 numbers.splice(index, 1); 9 yield value; 10 } 11}
上記のジェネレータ関数randomSequenceFromOneTo(n)
を使って、一対戦が13セットマッチに対応するcom
を得るには以下のようにします。
javascript
1const com = randomSequenceFromOneTo(13);
こうして得られた com
を使って、COMが次に出す数を得るには以下のようにします。
javascript
1const { value, done } = com.next();
こうすると、変数value
には次にCOMが出す数が、変数done
にはtrueかfalseのブール値が入ってきます。done
が true になっているときは、すでにcom
が内部にもつ配列numbers
が空になっており、次に出せる数が無いことを意味しています。そのときのvalue
はundefined になります。
randomSequenceFromOneTo(13)
で作った com
が内部に持つ数字の配列は、初期状態で長さ13ですが、それが com.next()
が呼ばれるたびに1ずつ減っていき、第13セットが終わったときには配列は空になります。その後にさらにcom.next()
を呼んでも、返されるオブジェクトとしてはdone
がtrueで、value
の無いものが返ってきます。
3.1 ジェネレータを使ったコード例
以下のリンク先は、先のジェネレータ関数randomSequenceFromOneTo(n)
を使ってゲームを実装してみた例です。
このサンプルでは、
- 初期表示では、全5セット対戦の第1セットを開始するところから始まります。
- 5セット対戦なので、ユーザーもCOMも出せる数は1以上5以下の整数です。
- 5セット目が終わると、ボタンが「総合成績を表示」になります。これをクリックすると全5セットの戦績が表示されます。
- 総合成績が表示されると、画面最上部は再度の対戦を受けつける状態になります。
- 再度の対戦では、対戦セット数を 5, 8, 13 の三通りから選べます。
このサンプルの主目的はジェネレータ関数の使用例を示すことですので、ゲームを構成するデータ構造だったりロジックだったりは、リファクタリングの余地が多々あると思われます。そのような観点で参考にしていただければと思います。
投稿2021/12/04 04:22
編集2021/12/05 16:02退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
いちばん手っ取り早いのは、「1から13まで入った配列を用意しておいて、最初にシャッフルしたあとで先頭から順に使っていく」ような方法かと思います。
投稿2021/12/04 04:10
総合スコア146018
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。