前回のプレイヤーが出したカードの判定 素数大富豪をJavaScriptでつくる(2)の続きです。今回はコンピュータにカードを出させる処理を考えます。
動作確認はこちらから ⇒ Web版素数大富豪、別名 素数の出会い系サイト
Contents
CandidateクラスとPrimeNumberFromCardsクラス
最初にこの処理で使うクラスを示します。Candidateクラスはコンピュータが出すカードの候補になるものです。カードの番号でつくられる素数または合成数と素数または合成数をつくるカードの番号の配列だけでなく、素因数をつかった計算式も取得できるようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Candidate { constructor(){ this.Number = 0; // カードの番号でつくられる素数または合成数 this.CardNumbers = []; // 素数または合成数をつくるカードの番号の配列 this.PrimeNumberCards = []; // 素因数をつくるカード(PrimeNumberFromCards)の配列 } GetFormula(){ let factor = []; this.PrimeNumberCards.forEach(x => { if(x.Count > 1) factor.push(x.PrimeNumber + ' ^ ' + x.Count); else factor.push(x.PrimeNumber); }); return factor.join(' * '); } GetCardNumbersToFactorField(){ let ret = []; for(let i=0; i<this.PrimeNumberCards.length; i++){ let primeNumberFromCards = this.PrimeNumberCards[i]; for(let j=0; j<primeNumberFromCards.CardNumbers.length; j++){ ret.push(primeNumberFromCards.CardNumbers[j]); } if(primeNumberFromCards.Count > 1) ret.push(primeNumberFromCards.Count); } return ret; } GetCardNumbersToPlayField(){ return this.CardNumbers; } } |
カードによって作られる素因数の情報を管理するクラスです。
1 2 3 4 5 6 7 |
class PrimeNumberFromCards { constructor(){ this.PrimeNumber = 0; // 素因数 this.CardNumbers = []; // 素因数をつくるカードの番号の配列 this.Count = 1; // 素数の累乗指数 } } |
全体の流れ
全体の流れはコンピュータが持っているカードを整数の配列に変換して、そこから出さなければならない枚数を使って順列をつくります。そこから整数をつくってそれが素数であれば候補のひとつとみなします。
合成数であった場合は合成数出しが可能かもしれないので、コンピュータが持っているカードで作ることができる素因数で割り切ることができるかどうかを調べます。素因数分解が可能である場合はほんとうにコンピュータがもっているカードだけで合成数と素因数と指数を出すことができるかを調べて、候補に加えます。
プレイヤーがカードを出して、しばらくしてコンピュータにカードを出させたいので0.5秒間の時間をおいています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function CompPutCards(){ isPlayerDraw = true; let text = document.getElementById('comp-text'); text.innerHTML = 'コンピュータが考えています'; setTimeout(function(){ let nextPlayerTurn = true; if(playerCards.length > 0) nextPlayerTurn = CompPutCards2(); if(nextPlayerTurn && playerCards.length > 0 && compCards.length > 0){ EnableBottons(true); } if(playerCards.length == 0) ShowPlayerWinMessage('あなたの勝ちです') if(compCards.length == 0) ShowPlayerWinMessage('コンピュータの勝ちです') }, 500); } |
GetCandidates関数で候補になりそうな手を取得してそのなかから最善に近い手を探します。カードは偶数のものは使いにくいので偶数のカードを多く使う手を探します。そのあとできるだけたくさんの枚数を消費できる手を採用します。
また残りが偶数ばかりになり、奇数のカードを引くとそれを1枚だけ出してなかなか上がれない問題が起きることがわかったので、1枚出しの結果、偶数のカードばかりになる場合はパスすることにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
function CompPutCards2(){ let candidates = GetCandidates(); // 偶数のカードを多く使う手を探す candidates = GetCandidatesUseEvenNumbers(candidates); // できるだけ多くの枚数を消費できる手を探す let candidate = GetCandidatesUseManyCards(candidates); // 最適のものを探そうとして候補がなくなったら適当に選ぶ if(candidate == null && candidates.length > 0){ let index = Math.floor(Math.random() * candidates.length); candidate = candidates[index]; } // 候補が見つからない場合はパス if(candidate == null){ CompPass(); return false; } // 1枚出しの結果、偶数のカードばかりになる場合はパスする let len = candidate.GetCardNumbersToPlayField().length; if(len == 1){ let arr = []; compCards.forEach(card => { arr.push(card.Number); }); let pass = true; for(let i=0;i<arr.length; i++){ if(arr[i] % 2 != 0) pass = false; } if(pass == true){ CompPass(); return false; } } // カードを出すことができた場合はnumberMinとcardCountの値を更新する numberMin = candidate.Number; cardCount = candidate.CardNumbers.length; // 戻り値はジョーカーの1枚出しの場合はtrue、それ以外はfalse let joker = CompMoveCard(candidate); // コンピュータが出した手を表示する let text = document.getElementById('comp-text'); if(text != null && candidate.PrimeNumberCards.length != 0) text.innerHTML = 'コンピュータの手は ' + candidate.Number + ' = ' + candidate.GetFormula(); else if(text != null) text.innerHTML = 'コンピュータの手は ' + candidate.Number; ShowCards(); // コンピュータのカードがなくなったら終了 if(compCards.length == 0){ InitBottons(); return false; } // ジョーカーの1枚出しの場合はプレイヤーは強制的にパスとなる if(joker){ ShowPassMessage('ジョーカーを出したのでコンピュータが親です'); return false; } // グロタンディーク素数切りの場合はプレイヤーは強制的にパスとなる if(candidate.Number == 57 && candidate.GetCardNumbersToFactorField()) { ShowPassMessage('グロタンディーク素数 57を出したのでコンピュータが親です'); return false; } // ラマヌジャン革命ならその旨を表示する(強制パスにはならない) if(candidate.Number == 1729 && candidate.GetCardNumbersToFactorField()){ isRevolution = isRevolution ? false : true; if(isRevolution) ShowCompRevolutionMassage('1729を出したのでラマヌジャン革命です'); else ShowCompRevolutionMassage('1729を出したのでラマヌジャン革命返しです'); } return true; } |
コンピュータがパスをするときの処理
コンピュータがパスをするときの処理です。カードを1枚取ります。そしてその旨を示すメッセージを表示させます。
1 2 3 4 5 6 7 |
function CompPass(){ compCards.push(shuffledCards[0]); shuffledCards.splice(0, 1); Pass(); ShowCards(); ShowNotificationMessage('コンピュータはパスをしました'); } |
ラマヌジャン革命のときの処理
ラマヌジャン革命のメッセージを表示させる関数を示します。革命なので背景色を赤にしています。
1 2 3 4 5 6 7 8 9 |
function ShowCompRevolutionMassage(notifyText){ let element = CreateMessageElement(300, '#ff0000', notifyText); let buttonOK = AppendOkButton(element); buttonOK.onclick = function(e){ element.remove(); } } |
カードの番号の順列をつくる
コンピュータのカードから順列を取得する処理を示します。GetPermutationCardNumber関数にコンピュータが持っているカードと枚数を渡して順列を取得します。それらをまとめて重複を取り除きます。
そのあと自作のDivideNumberArray関数で素数が生成される順列と合成数が生成されるものにわけます。素数が生成される順列はそのまま候補に加え、合成数が生成される順列は素因数分解可能かを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
function GetCandidates(){ // コンピュータのカードから順列を取得する let permutationOfNumbers = []; permutationOfNumbers = permutationOfNumbers.concat(GetPermutationCardNumber(compCards, 1)); permutationOfNumbers = permutationOfNumbers.concat(GetPermutationCardNumber(compCards, 2)); permutationOfNumbers = permutationOfNumbers.concat(GetPermutationCardNumber(compCards, 3)); permutationOfNumbers = permutationOfNumbers.concat(GetPermutationCardNumber(compCards, 4)); // 重複を取り除く permutationOfNumbers = RemoveArrayDuplication(permutationOfNumbers); // 取得した順列を素数をつくるものと合成数をつくるものにわける let primeNumberArrays = []; let notPrimeNumberArrays = []; DivideNumberArray(permutationOfNumbers, primeNumberArrays, notPrimeNumberArrays); // 素数を候補として取得 let candidates1 = GetPrimeCandidates(primeNumberArrays); // 素因数分解のために使う素因数も取得する let primeNumberFromCardsList = GetPrimeFactors(primeNumberArrays); let candidates2 = GetCompositeCandidates(notPrimeNumberArrays, primeNumberFromCardsList); return candidates1.concat(candidates2); } |
GetPermutationCardNumber関数は以下の様になっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
function GetPermutationCardNumber(compNumbers, count){ let ret = []; // コンピュータのカードをNumberに変換する let compNumbers2 = []; compNumbers.forEach(card => { if(card.Suit != Suit.Joker) compNumbers2.push(card.Number); else compNumbers2.push(-1); }); let numbers = Permutation(compNumbers2, count); numbers.forEach(number => { if(number.indexOf(-1) == -1) ret.push(number); else { let index1 = number.indexOf(-1); for(let i=0; i<=13; i++){ let arr1 = number.concat(); arr1.splice(index1, 1, i); let index2 = arr1.indexOf(-1); if(index2 == -1) ret.push(arr1); else { for(let j=0; j<=13; j++){ let arr2 = arr1.concat(); arr2.splice(index2, 1, j); ret.push(arr2); } } } } }); return ret; } |
これは単純に渡された配列から順列を生成する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function Permutation(arr, count){ let arrs = []; let arrLength = arr.length; if(arrLength < count){ return arrs; } else if(count == 1){ for(let i = 0; i < arrLength; i ++){ arrs[i] = [arr[i]]; } } else { for(let i = 0; i < arrLength; i ++){ let parts = arr.slice(0); parts.splice(i, 1)[0]; let results = Permutation(parts, count - 1); for(let j = 0; j < results.length; j ++){ arrs.push([arr[i]].concat(results[j])); } } } return arrs; } |
順列の重複を取り除く
順列が格納された配列のなかから重複しているものを取り除く関数です。重複を取り除くためにSetを使いたいのですがそのままではうまくいかないので一旦文字列に変換しています。そのあと重複を取り除き、残ったものを配列に戻しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function RemoveArrayDuplication(permutationOfNumbers) { let temp = []; permutationOfNumbers.forEach(x => { temp.push(x.join(',')); }); temp = Array.from(new Set(temp)); ret = []; temp.forEach(x => { let arr1 = x.split(','); let arr2 = []; arr1.forEach(str => { arr2.push(Number(str)); }); ret.push(arr2); }); return ret; } |
順列が生成するのは素数か合成数か?
コンピュータのカードから取得した順列を素数をつくるものと合成数をつくるものにわける関数です。
1 2 3 4 5 6 7 8 9 10 11 12 |
function DivideNumberArray(numberArrays, primeNumberArrays, notPrimeNumberArrays){ numberArrays.forEach(array => { let str = ''; let num = GetNumberFromArray(array); if(IsPrimeNumber(num)){ primeNumberArrays.push(array); } else{ notPrimeNumberArrays.push(array); } }); } |
DivideNumberArray関数によって素数をつくる順列であると分類されたものを候補のリストに追加する関数です。このときcardCountとnumberMinで指定された条件を満たさないものは弾いています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function GetPrimeCandidates(primeNumberArrays){ let ret = []; primeNumberArrays.forEach(array => { if(cardCount != -1 && array.length != cardCount) return; let primeNumber = GetNumberFromArray(array); let candidate = new Candidate(); candidate.Number = primeNumber; candidate.CardNumbers = array.concat(); candidate.PrimeNumberCards = []; // 合成数出しではないので空 if(numberMin == -1 || (!isRevolution && numberMin < candidate.Number) || (isRevolution && numberMin > candidate.Number)){ ret.push(candidate); } }); return ret; } |
素因数のリストをつくる
これはDivideNumberArray関数によって素数をつくる順列であると分類されたものから素因数のリストをつくる関数です。最後に素因数が小さい順にソートしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function GetPrimeFactors(primeNumberArrays){ let ret = []; primeNumberArrays.forEach(array => { let primeNumber = GetNumberFromArray(array); let primeNumberFromCards = new PrimeNumberFromCards(); primeNumberFromCards.PrimeNumber = primeNumber; primeNumberFromCards.CardNumbers = array.concat(); primeNumberFromCards.Count = 1; ret.push(primeNumberFromCards); }); ret.sort((a, b) => (a.PrimeNumber - b.PrimeNumber)) return ret; } |
素数の候補を取得する
DivideNumberArray関数によって合成数をつくる順列であると分類されたものから素因数分解可能なものを取得する関数です。ここでもcardCountとnumberMinで指定された条件を満たさないものは弾いています。
そのあとGetCandidatesPrimeFactorization関数で素因数分解可能なものを集めています。さらにそのなかからコンピュータのカードだけで合成数と素因数と指数をすべてそろえることができるものだけを返しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function GetCompositeCandidates(notPrimeNumberArrays, primeNumberFromCardsList){ let ret = []; let notPrimeNumberArrays1 = []; notPrimeNumberArrays.forEach(arr =>{ if(cardCount != -1 && arr.length != cardCount) return; let notPrimeNumber = GetNumberFromArray(arr); if(numberMin == -1 || (!isRevolution && numberMin < notPrimeNumber) || (isRevolution && numberMin > notPrimeNumber)){ notPrimeNumberArrays1.push(arr); } }); // 素因数分解できるか確認 let candidates1 = GetCandidatesPrimeFactorization(notPrimeNumberArrays1, primeNumberFromCardsList); let compCardNums = []; compCards.forEach(card => { compCardNums.push(card.Number); }); let len = candidates1.length; for(let i=0; i<len; i++){ let candidate = candidates1[i]; if(CanRemoveCard(candidate, compCardNums)) ret.push(candidate); } return ret; } |
合成数は素因数分解可能か?
GetCandidatesPrimeFactorization関数は素因数分解可能なものを集めて返します。第一引数は合成数出しの候補の配列、第二引数は素因数の配列です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
function GetCandidatesPrimeFactorization(notPrimeNumberArrays, primeNumberFromCardsList) { let ret = []; let len = primeNumberFromCardsList.length; notPrimeNumberArrays.forEach(array => { let num = GetNumberFromArray(array); let tempNum = num; if(num < 2) return; let candidate = new Candidate(); candidate.Number = num; candidate.CardNumbers = array.concat(); candidate.PrimeNumberCards = []; // 合成数出し candidate.Formula = ''; for(let i=0; i<len; i++){ let primeNum = primeNumberFromCardsList[i].PrimeNumber; let count = 0; while(tempNum % primeNum == 0){ count++; tempNum /= primeNum; } if(count > 0){ let cardNums = primeNumberFromCardsList[i].CardNumbers; let primeNumberFromCards = new PrimeNumberFromCards(); primeNumberFromCards.PrimeNumber = primeNum; primeNumberFromCards.CardNumbers = cardNums.concat(); primeNumberFromCards.Count = count; candidate.PrimeNumberCards.push(primeNumberFromCards); } if(tempNum == 1) break; } if(tempNum == 1) ret.push(candidate); }); return ret; } |
数字の配列から素数または合成数を取得する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function GetNumberFromArray(array) { let ret = 0; array.forEach(num => { if(num == '') return; let number = Number(num); if(number < 10) ret *= 10; else ret *= 100; ret += number; }); return ret; } |
合成数出しの候補を取得する
素因数分解できそうな候補のなかから本当に出すことができる候補なのかを調べる関数です。コンピュータが持っているカードの番号のコピーを作成して、そこから合成数と素因数、指数があるか調べて見つかったら取り除きます。全部の数が見つかって取り除くことができたら素因数分解可能と判断できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
function CanRemoveCard(candidate, compCardNums){ let copyCompCardNums = compCardNums.concat(); for(let i=0; i<candidate.CardNumbers.length; i++){ let index = copyCompCardNums.indexOf(candidate.CardNumbers[i]); if(index != -1) copyCompCardNums.splice(index, 1); else return false; } let primeNumberCards = candidate.PrimeNumberCards; let newPrimeNumberFromCardsList = []; for(let i=0; i<primeNumberCards.length; i++){ // primeNumberCards (素数を構成するカード群) let primeNumberFromCards = primeNumberCards[i]; let cardNums = primeNumberFromCards.CardNumbers; let len = cardNums.length; for(let j=0; j<len; j++){ let cardNum = cardNums[j]; let index = copyCompCardNums.indexOf(cardNum); if(index != -1) copyCompCardNums.splice(index, 1); else return false; } // 指数が2以上の場合は素因数だけでなく指数も含まれているかを調べる let count = primeNumberFromCards.Count; if(count != 1){ let index = copyCompCardNums.indexOf(count); if(index != -1) copyCompCardNums.splice(index, 1); else { // 指数が見つからない場合は素因数が(count-1)個あるか調べる // あれば a ^ 3 を a * a * aのように変換する // これもできない場合は素因数をつくるカードが足りないと判断する for(let j=0; j<count-1; j++){ for(let k=0; k<cardNums.length; k++){ let index = copyCompCardNums.indexOf(cardNums[k]); if(index != -1) copyCompCardNums.splice(index, 1); else return false; } } // a ^ 3 を a * a * aのように変換する処理 // ループのなかでnew PrimeNumberFromCards()を追加することはできないので // ループを抜けてから追加する primeNumberFromCards.Count = 1; for(let m=0; m<count-1; m++){ let newPrimeNumberFromCards = new PrimeNumberFromCards(); newPrimeNumberFromCards.Count = 1; newPrimeNumberFromCards.PrimeNumber = primeNumberFromCards.PrimeNumber; newPrimeNumberFromCards.CardNumbers = primeNumberFromCards.CardNumbers.concat(); newPrimeNumberFromCardsList.push(newPrimeNumberFromCards); } } } } // ループの中で作成した素因数を追加する if(newPrimeNumberFromCardsList.length > 0){ newPrimeNumberFromCardsList.forEach(primeNumberFromCards =>{ candidate.PrimeNumberCards.push(primeNumberFromCards); }); } return true; } |
最適そうな手を探す
候補が取得できたらそのなかから最適に近いものを探します。偶数のカードは使いにくいのでできるだけたくさん消費できる手を探します。
これは偶数のカードをできるだけ多く消費する候補を取得する関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function GetCandidatesUseEvenNumbers(candidates){ let ret = []; let max = 0; for(let i=0; i<candidates.length; i++){ // 出すカードの番号をarrに格納する let max0 = 0; let arr = []; arr = arr.concat(candidates[i].GetCardNumbersToPlayField()); arr = arr.concat(candidates[i].GetCardNumbersToFactorField()); // arrのなかから偶数のものを数える arr.forEach(num => { if(num % 2 == 0){ max0++; } }); if(max == max0){ ret.push(candidates[i]); } // 暫定的最大値よりも多い候補がみつかったらこれまでストックしていたものは破棄して // 新しい候補を配列に加える if(max < max0){ ret = []; ret.push(candidates[i]); max = max0; } } return ret; } |
できるだけ多くのカードを消費する候補を探す関数です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function GetCandidatesUseManyCards(candidates){ let ret = null; if(candidates.length > 0){ let maxCount = 0; for(let i=0; i<candidates.length; i++){ let arr = []; arr = arr.concat(candidates[i].GetCardNumbersToPlayField()); arr = arr.concat(candidates[i].GetCardNumbersToFactorField()); if(maxCount < arr.length){ maxCount = arr.length; ret = candidates[i]; } } return ret; } return null; } |
カードを場に出す処理
コンピュータが出すべきカードが決まったら場もしくは素因数場に移動させます。もし番号のカードが見つからない場合はジョーカーが使われます。ジョーカーの1枚出しであることを検出するためにその場合はtrueを返すようにしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
function CompMoveCard(candidate){ let useJoker = false; // カードを場に移動 let numbers1 = candidate.GetCardNumbersToPlayField(); for(let j=0; j<numbers1.length; j++){ let done = false; for(let i =0; i<compCards.length; i++){ if(compCards[i].Number == numbers1[j]){ compPutCards.push(compCards[i]); compCards.splice(i, 1); done = true; break; } } if(!done){ for(let i =0; i<compCards.length; i++){ if(compCards[i].Suit == Suit.Joker){ compPutCards.push(compCards[i]); compCards.splice(i, 1); useJoker = true; break; } } } } // カードを素因数場に移動 let numbers2 = candidate.GetCardNumbersToFactorField(); for(let j=0; j<numbers2.length; j++){ let done = false; for(let i =0; i<compCards.length; i++){ if(compCards[i].Number == numbers2[j]){ compPrimeFactor.push(compCards[i]); compCards.splice(i, 1); done = true; break; } } if(!done){ for(let i =0; i<compCards.length; i++){ if(compCards[i].Suit == Suit.Joker){ compPrimeFactor.push(compCards[i]); compCards.splice(i, 1); break; } } } } if(numbers1.length == 1 && numbers2.length == 0 && useJoker) return true; else return false; } |
パスとゲームの終了
コンピュータがカードを出す前にプレイヤーのカードがなくなっていたらプレイヤーの勝利です。コンピュータがカードを出したあとコンピュータのカードがなくなっていればコンピュータの勝利です。この場合はメッセージを表示させます。
1 2 3 4 5 6 7 8 9 10 11 |
function ShowPlayerWinMessage(notifyText){ let element = CreateMessageElement(300, '#ff0000', notifyText); let buttonOK = AppendOkButton(element); buttonOK.onclick = function(e){ element.remove(); let text = document.getElementById('comp-text'); text.innerHTML = ''; } } |
コンピュータがパスをする場合はメッセージを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function ShowPassMessage(notifyText){ let element = CreateMessageElement(300, '#800000', notifyText); let buttonOK = AppendOkButton(element); buttonOK.onclick = function(e){ element.remove(); let text = document.getElementById('comp-text'); text.innerHTML = ''; Pass(); ShowCards(); CompPutCards(); } } |