経緯や理由はわかりませんが、日本のビデオゲームとポーカーミニゲームは切っても切り離せない関係にあります。RPGのカジノコーナーに行けば、多くの場合はポーカーと出会うことができるでしょう。
さてそんなポーカーですが、自分で作ってみたくなったことはありませんか?今回は、JavaScriptを使って、簡単なポーカーを作ってみましょう。
ビデオゲームポーカー
日本におけるポーカーは、一般的に古典的で素朴なポーカーが採用されることがほとんどです。テキサス・ホールデムも有名ですが、特にビデオゲーム内のミニゲームにおいては、多くの場合、最もシンプルなものが採用され、対戦相手はおらず、1人で役を作って遊ぶものに仕上がっています。
ルールとしては、プレイヤーにカードが5枚配られ、プレイヤーは保持したいカードを複数選びます。1度きりの交換後、手札の役を見て、強い役ほど報酬が多くもらえる、という作りになっています。
例えばスマートフォンゲームのグランブルーファンタジーにおいても同様のミニゲームが実装されており、気軽にポーカーが楽しめるようになっています。
私がグラブルを始めた当時、最強クラスの召喚石「アナト」をとるために、必死でカジノ通いをした記憶があります。
ゲームのポーカーは、現実のポーカーとはまた違った特徴を持っていますが、それでもなかなか楽しいものです。今回はこれを作ってみましょう。
ポーカーを作る
それではポーカーを作っていきましょう。今回作るのはポーカー部分だけで、ベットや報酬部分、ダブルアップについては作成しません。
完成済みのソースコードは以下に置いてあります:
https://github.com/subterraneanflowerblog/gbf-poker-modoki
カードの準備
まずはHTMLファイルを用意します。以下の内容を適当な名前で保存します。内容としては、CSSファイル「style.css」とJavaScriptファイル「poker.js」「main.js」を読み込んでいるだけです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ポーカー</title>
<link rel="stylesheet" href="style.css">
<script src="poker.js" defer></script>
<script src="main.js" defer></script>
</head>
<body>
</body>
</html>
HTMLファイルが用意できたら、次はHTMLファイルと同じフォルダに「poker.js」を作り、編集していきます。ポーカーに必要なカードデータを入力していきます。
const Suit = {
SPADE: '♠️',
CLUB: '♣️',
DIAMOND: '♦️',
HEART: '♥️'
};
const suitList = [
Suit.SPADE,
Suit.CLUB,
Suit.DIAMOND,
Suit.HEART
];
const cardList = [
{rank: 2, label: '2' },
{rank: 3, label: '3' },
{rank: 4, label: '4' },
{rank: 5, label: '5' },
{rank: 6, label: '6' },
{rank: 7, label: '7' },
{rank: 8, label: '8' },
{rank: 9, label: '9' },
{rank: 10, label: '10' },
{rank: 11, label: 'J' },
{rank: 12, label: 'Q' },
{rank: 13, label: 'K' },
{rank: 14, label: 'A' }
];
// 各スートごとに2からAまで作成する
const deckBase =
suitList
.map((suit) => cardList.map((card) => ({suit, ...card})))
.flat()
.map(Object.freeze);
// ジョーカー
const joker = Object.freeze({ isWildcard: true, label: 'Joker' });
// 山札クラス
class Deck {
constructor(options = {}) {
this._deck = [...deckBase]; // deckBaseをコピー
if(options.includesJoker) { this._deck.push(joker); }
// シャッフル
this._deck.sort((a, b) => Math.random() - 0.5);
}
// 山札からカードを取り出すメソッド
deal(num) {
return this._deck.splice(0, num);
}
}
これで、カードデータと山札クラスが出来上がりました。カードデータは、単純にスートと数字の組み合わせを生成しているだけです。
山札クラスは、シャッフルされた山札を生成します。加えて、山札からカードを指定枚数引く、dealメソッドを実装しています。
試しに表示してみる
次に「main.js」をHTMLファイルと同じフォルダに作成し、編集します。main.jsでは、poker.jsで定義した変数やクラスを用いて、実際のゲーム進行処理を行います。
ゲームを作る前に、まずはデータの表示ができるかどうかだけを確認します。main.jsに、以下の内容を書き込んでください。
// カードを表す要素を作成する関数
const createCardElement = (card) => {
const elem = document.createElement('div');
elem.classList.add('card');
// 「♣️K」のような表示を作る
const cardLabel = document.createElement('div');
cardLabel.innerText = `${card.suit || ''}${card.label}`;
elem.appendChild(cardLabel);
return elem;
};
//
// メイン処理
//
(function startGame() {
// カード情報作成
const deck = new Deck({includesJoker: true});
const cards = deck.deal(5);
// カードを描画する
// renderTargetは描画対象(ここではdocument.bodyにしておきます)
// stateは現在の状態(手札のリスト)です
(function render(renderTarget, state) {
renderTarget.innerText = ''; // 描画内容をクリア
// カードの組を表示するコンテナを作成
const container = document.createElement('div');
container.classList.add('card-group');
renderTarget.appendChild(container);
// 各カードの内容をコンテナに詰め込む
for(const card of state.cardList) {
const cardElem = createCardElement(card);
container.appendChild(cardElem);
}
})(document.body, {cardList: cards});
})();
startGame関数は、ゲームをスタートします。山札からカードを5枚引き、そのデータを表示しています。
startGame関数の中にあるrender関数は、いままでの表示内容を消去し、データを元にまた新たに表示しなおします。render関数は、初めの1回と、データに変更があるたびに呼び出すようにします。引数としては、表示対象の要素と、現在の状態(state)を受け取ります。
また、各要素にクラス名(card、card-groupなど)を振ってあります。これは後ほどCSSでのスタイリングをするためです。
これで表示の準備はできましたので、HTMLファイルをダブルクリックで開いてみましょう。ダブルクリックでブラウザが開かない場合、ブラウザのウィンドウ上にHTMLファイルをドロップすることでも、開くことができます。
以下の画像のように表示されたでしょうか?(絵文字を使用しているので、環境によってスートの表示は違います)
表示されたら、何度かリロードして、毎回ランダムに表示関わることを確認してください。
これでポーカーの最初の部分は実装できました。
見た目を整える
これでも表示はできるのですが、見た目が貧相です。少し見た目を整えてみましょう。
次は「style.css」ファイルを作成します。クラスはすでにJavaScriptファイル内で振ってあるので、そこにスタイルを適用していくだけです。
body {
font-family: sans-serif;
}
.card-group {
display: flex;
font-family: sans-serif;
}
.card {
margin: 0 1em;
border-radius: 3px;
box-shadow: 0 0 3px 1px rgba(0, 0, 0, 0.2);
padding: 1em;
width: 5em;
}
.card > div {
text-align: center;
}
これで多少は見た目が良くなるはずです。確認してみましょう。
少しカードっぽい見た目になりました!よりセンスのある方は、自分でスタイルをいじってみてもいいでしょう。
スタイルが適用されない場合、HTMLファイル内でのCSSファイルの指定を間違えてないか、style.cssファイルの名前を間違えていないか、置き場所を間違えていないか等を確認してみてください。
カード保持/交換の切り替え
次にカードを保持するか交換するかのデータを追加し、切り替えができるようにします。
まずはカードを引く部分で、各カードに保持フラグを追加します。main.jsのstartGame内部の冒頭付近を編集します。
// カード情報作成
const deck = new Deck({includesJoker: true});
const cards = deck.deal(5).map((c) => ({isHold: false, ...c}));
各カードにisHoldフラグを追加しています。
そして、そのデータに合わせ表示を変更します。main.jsの冒頭にあるcreateCardElement関数を編集します。
// カードを表す要素を作成する関数
const createCardElement = (card) => {
const elem = document.createElement('div');
elem.classList.add('card');
// 「♣️K」のような表示を作る
const cardLabel = document.createElement('div');
cardLabel.innerText = `${card.suit || ''}${card.label}`;
elem.appendChild(cardLabel);
// isHoldフラグがあれば、「HOLD」表示を追加し、
// 要素にholdクラスを追加する
if(card.isHold) {
const holdIndicator = document.createElement('div');
holdIndicator.innerText = 'HOLD';
elem.appendChild(holdIndicator);
elem.classList.add('hold');
}
return elem;
};
これで、保持状態が表示に反映されるようになったはずです。しかし、まだ保持状態の切り替え機能を実装していないため、その確認はできません。
なので、次は切り替え機能を作りましょう。カードをクリックすると切り替えられるようにします。render関数の処理を変更しましょう。
// カードを描画する
// renderTargetは描画対象(ここではdocument.bodyにしておきます)
// stateは現在の状態(手札のリスト)です
(function render(renderTarget, state) {
renderTarget.innerText = ''; // 描画内容をクリア
// カードの組を表示するコンテナを作成
const container = document.createElement('div');
container.classList.add('card-group');
renderTarget.appendChild(container);
// 各カードの内容をコンテナに詰め込む
for(const card of state.cardList) {
const cardElem = createCardElement(card);
// カードをクリックすると保持状態を切り替え
// 全体を再描画する
cardElem.addEventListener('click', () => {
card.isHold = !card.isHold;
render(renderTarget, state);
});
container.appendChild(cardElem);
}
})(document.body, {cardList: cards});
カードをクリックしたときの処理を追加しました。カードをクリックすると保持状態を切り替え、再描画します。
また、状態が分かりやすいように、style.cssに保持状態時のスタイルを追加しておきましょう。追記場所はファイルの一番下で大丈夫です。
.card.hold {
background-color: rgb(177, 206, 177);
}
これで動作を確認してみましょう。表示されたカードをクリックすると、状態が切り替わるはずです。
カードの交換
カードの交換の実装は簡単で、isHoldフラグのついていないカードを入れ替えるだけです。render関数に、カード交換のボタンを追加してみましょう。
// カードを描画する
// renderTargetは描画対象(ここではdocument.bodyにしておきます)
// stateは現在の状態(手札のリストとゲームフェーズ)です
(function render(renderTarget, state) {
renderTarget.innerText = ''; // 描画内容をクリア
// カードの組を表示するコンテナを作成
const container = document.createElement('div');
container.classList.add('card-group');
renderTarget.appendChild(container);
// 各カードの内容をコンテナに詰め込む
for(const card of state.cardList) {
const cardElem = createCardElement(card);
// カードをクリックすると保持状態を切り替え
// 全体を再描画する
cardElem.addEventListener('click', () => {
card.isHold = !card.isHold;
render(renderTarget, state);
});
container.appendChild(cardElem);
}
// カード交換ボタン
// クリックすると保持フラグのついていないカードを交換し
// 再描画する
const changeButton = document.createElement('button');
changeButton.innerText = '交換する';
changeButton.addEventListener('click', () => {
const newCardList = state.cardList
.map((c) => c.isHold ? c : deck.deal(1)[0]);
render(renderTarget, {
cardList: newCardList,
phase: 'done'
});
});
renderTarget.appendChild(changeButton);
})(document.body, {cardList: cards});
ここで追加したカード交換ボタンは、クリック時に各カードを、isHoldフラグがtrueならばそのままに、falsyならば山札から新しく1枚カードを引きます。
そして画面を再描画するのですが、このときstateに「phase」というキーを追加しています。これはゲームのフェーズ(交換フェーズ、完了フェーズ、など)を表します。phaseは後ほど使います。
さて、この状態で動作させてみると、正常に動作することがわかると思います。つまりHOLD状態のカードは保持され、その他のカードは交換されます。
また、それと同時にいくつかの不具合も見つかります。例えば何度も交換ボタンを押すと山札が尽きてエラーになる不具合です。
先ほど追加したphaseの値を見て、処理を変えてみましょう。phaseが’done’のとき、カードのクリックをできなくする処理と、交換ボタンを「次のゲームへ」ボタンに変更する処理を追加します。
render関数は以下のようになります:
(function render(renderTarget, state) {
renderTarget.innerText = ''; // 描画内容をクリア
// カードの組を表示するコンテナを作成
const container = document.createElement('div');
container.classList.add('card-group');
renderTarget.appendChild(container);
// 各カードの内容をコンテナに詰め込む
for(const card of state.cardList) {
const cardElem = createCardElement(card);
// カードをクリックすると保持状態を切り替え
// 全体を再描画する
// ゲームフェーズがdoneのときは押せない
if(state.phase !== 'done') {
cardElem.addEventListener('click', () => {
card.isHold = !card.isHold;
render(renderTarget, state);
});
}
container.appendChild(cardElem);
}
// 現在のゲームフェーズを見て処理を変える
if(state.phase === 'done') {
// 次のゲームを開始するボタン
const nextGameButton = document.createElement('button');
nextGameButton.innerText = '次のゲームへ';
nextGameButton.addEventListener('click', () => {
startGame();
});
renderTarget.appendChild(nextGameButton);
} else {
// カード交換ボタン
// クリックすると保持フラグのついていないカードを交換し
// 再描画する
const changeButton = document.createElement('button');
changeButton.innerText = '交換する';
changeButton.addEventListener('click', () => {
const newCardList = state.cardList
.map((c) => c.isHold ? c : deck.deal(1)[0]);
render(renderTarget, {
cardList: newCardList,
phase: 'done'
});
});
renderTarget.appendChild(changeButton);
}
})(document.body, {cardList: cards});
まずはphaseがdoneのとき、カードにイベントリスナを登録しないようにします。これはphase !== ‘done’のときだけイベントリスナを登録することで実現できます。該当部分だけを抜き出すと、以下のようになります:
// カードをクリックすると保持状態を切り替え
// 全体を再描画する
// ゲームフェーズがdoneのときは押せない
if(state.phase !== 'done') {
cardElem.addEventListener('click', () => {
card.isHold = !card.isHold;
render(renderTarget, state);
});
}
次に、表示するボタンも変更します。phaseがdoneのときは「次のゲームへ」ボタンを、そうでないときは「交換する」ボタンを表示します。該当部分だけを抜き出すと、以下のようになります:
// 現在のゲームフェーズを見て処理を変える
if(state.phase === 'done') {
// 次のゲームを開始するボタン
const nextGameButton = document.createElement('button');
nextGameButton.innerText = '次のゲームへ';
nextGameButton.addEventListener('click', () => {
startGame();
});
renderTarget.appendChild(nextGameButton);
} else {
// カード交換ボタン
// クリックすると保持フラグのついていないカードを交換し
// 再描画する
const changeButton = document.createElement('button');
changeButton.innerText = '交換する';
changeButton.addEventListener('click', () => {
const newCardList = state.cardList
.map((c) => c.isHold ? c : deck.deal(1)[0]);
render(renderTarget, {
cardList: newCardList,
phase: 'done'
});
});
renderTarget.appendChild(changeButton);
}
「次のゲームへ」ボタンは、ゲームを再スタートします。実際の処理としては、ゲーム全体を囲っているstartGame関数を呼び出しているだけです。
「交換する」ボタンには変更はありません。
ここまでのコード
poker.js
const Suit = {
SPADE: '♠️',
CLUB: '♣️',
DIAMOND: '♦️',
HEART: '♥️'
};
const suitList = [
Suit.SPADE,
Suit.CLUB,
Suit.DIAMOND,
Suit.HEART
];
const cardList = [
{rank: 2, label: '2' },
{rank: 3, label: '3' },
{rank: 4, label: '4' },
{rank: 5, label: '5' },
{rank: 6, label: '6' },
{rank: 7, label: '7' },
{rank: 8, label: '8' },
{rank: 9, label: '9' },
{rank: 10, label: '10' },
{rank: 11, label: 'J' },
{rank: 12, label: 'Q' },
{rank: 13, label: 'K' },
{rank: 14, label: 'A' }
];
// 各スートごとに2からAまで作成する
const deckBase =
suitList
.map((suit) => cardList.map((card) => ({suit, ...card})))
.flat()
.map(Object.freeze);
// ジョーカー
const joker = Object.freeze({ isWildcard: true, label: 'Joker' });
// 山札クラス
class Deck {
constructor(options = {}) {
this._deck = [...deckBase]; // deckBaseをコピー
if(options.includesJoker) { this._deck.push(joker); }
// シャッフル
this._deck.sort((a, b) => Math.random() - 0.5);
}
// 山札からカードを取り出すメソッド
deal(num) {
return this._deck.splice(0, num);
}
}
main.js
// カードを表す要素を作成する関数
const createCardElement = (card) => {
const elem = document.createElement('div');
elem.classList.add('card');
// 「♣️K」のような表示を作る
const cardLabel = document.createElement('div');
cardLabel.innerText = `${card.suit || ''}${card.label}`;
elem.appendChild(cardLabel);
// isHoldフラグがあれば、「HOLD」表示を追加し、
// 要素にholdクラスを追加する
if(card.isHold) {
const holdIndicator = document.createElement('div');
holdIndicator.innerText = 'HOLD';
elem.appendChild(holdIndicator);
elem.classList.add('hold');
}
return elem;
};
//
// メイン処理
//
(function startGame() {
// カード情報作成
const deck = new Deck({includesJoker: true});
const cards = deck.deal(5).map((c) => ({isHold: false, ...c}));
// カードを描画する
// renderTargetは描画対象(ここではdocument.bodyにしておきます)
// stateは現在の状態(手札のリストとゲームフェーズ)です
(function render(renderTarget, state) {
renderTarget.innerText = ''; // 描画内容をクリア
// カードの組を表示するコンテナを作成
const container = document.createElement('div');
container.classList.add('card-group');
renderTarget.appendChild(container);
// 各カードの内容をコンテナに詰め込む
for(const card of state.cardList) {
const cardElem = createCardElement(card);
// カードをクリックすると保持状態を切り替え
// 全体を再描画する
// ゲームフェーズがdoneのときは押せない
if(state.phase !== 'done') {
cardElem.addEventListener('click', () => {
card.isHold = !card.isHold;
render(renderTarget, state);
});
}
container.appendChild(cardElem);
}
// 現在のゲームフェーズを見て処理を変える
if(state.phase === 'done') {
// 次のゲームを開始するボタン
const nextGameButton = document.createElement('button');
nextGameButton.innerText = '次のゲームへ';
nextGameButton.addEventListener('click', () => {
startGame();
});
renderTarget.appendChild(nextGameButton);
} else {
// カード交換ボタン
// クリックすると保持フラグのついていないカードを交換し
// 再描画する
const changeButton = document.createElement('button');
changeButton.innerText = '交換する';
changeButton.addEventListener('click', () => {
const newCardList = state.cardList
.map((c) => c.isHold ? c : deck.deal(1)[0]);
render(renderTarget, {
cardList: newCardList,
phase: 'done'
});
});
renderTarget.appendChild(changeButton);
}
})(document.body, {cardList: cards});
})();
役を判別する
ここまでで、ゲームの流れはできたように思います。ですが、まだ役についての機能が存在しません。次は役の判別機能を作りましょう。
今度は「poker.js」のほうを編集します。poker.jsの下の方にどんどん追加していきましょう。
役の判定については様々な方法があるのですが、今回はわかりやすさを重視して、素直に条件を調べていく方法を取ります。速度は出ませんが、実装が簡単です。
まず、poker.jsに便利関数を追加していきましょう。役を判定するにあたり、ストの枚数や、各ランクの枚数をカウントする関数が欲しくなってきます。以下のような関数を、poker.jsの一番下に追加します:
// スートごとの枚数をカウントする関数
const countSuit = (cardList) => {
let wildcard = 0;
const count = {
[Suit.SPADE]: 0,
[Suit.CLUB]: 0,
[Suit.DIAMOND]: 0,
[Suit.HEART] : 0
};
for(const card of cardList) {
if(card.isWildcard) { wildcard++; }
else { count[card.suit]++; }
}
return {
wildcard,
...count
}
};
// ランクごとの枚数をカウントする関数
const countRank = (cardList) => {
// インデックスが0から14までの15要素の配列
// 0,1は未使用で2から14はそれぞれのカード
// 全て0で初期化しておく
let wildcardCount = 0;
const rankCount = new Array(15).fill(0);
// カウントする
for(const card of cardList) {
if(card.isWildcard) {
wildcardCount++;
} else {
rankCount[card.rank]++;
}
}
return {
wildcard: wildcardCount,
rank: rankCount
};
};
countSuit関数は、スートごとの枚数を算出します。countRank関数は、ランクごとの枚数を算出します。これらは後ほど使います。
次に、ジョーカーを取り除く関数と、カードリストをランクでソートする関数を作ります:
// ジョーカーを取り除く関数
const removeJoker = (cardList) => {
return cardList.filter((c) => !c.isWildcard);
};
// カードのランクでソートする関数
const sortByRank = (cardList) => {
return [...cardList].sort((a, b) => a.rank - b.rank);
};
ジョーカーを取り除く関数は、役の判定の際に、ジョーカーが邪魔な場合に使います。ランクでソートする関数は、ストレートなどの役の判定に使います。
フラッシュ/ストレート/ストレートフラッシュ
判定処理の都合上、強い役から順番に処理していきたいところです。ただ、ロイヤルストレートフラッシュを判定するにはストレートフラッシュの判定が必要で、ストレートフラッシュの判定にはフラッシュの判定と、ストレートの判定が必要です。
なので、まずはフラッシュとストレートの判定を作ります。poker.jsに追加していきます。まずはフラッシュから:
// フラッシュ(5枚全てが同じスート)
// ジョーカーも考慮する
const isFlush = (cardList) => {
const count = countSuit(cardList);
return suitList.some(
(s) => count[s] + count.wildcard === 5
);
};
フラッシュの判定は簡単です。スートの枚数をカウントして、ジョーカーの枚数と足して5になるスートがあれば、フラッシュです。
次にストレートの判定を作ります。これはなかなかに難しく、特にジョーカーが混ざるとかなり複雑になります。以下のようにします:
// ストレート(5枚のランクが連続している)
// ジョーカーも考慮する
const isStraight = (cardList) => {
const count = countSuit(cardList);
// ランクでソートしておく
const canonical = sortByRank(removeJoker(cardList));
let straight = true;
let remainWildcard = count.wildcard;
for(let index = 0; index < canonical.length - 1; index++) {
const current = canonical[index];
const next = canonical[index+1];
// 隣り合うカード間のランク差
// ランクでソート済みなので、差を調べるだけでわかる
// 隣同士の差が1ならストレート
let gap = next.rank - current.rank;
// A(14), 2, 3, 4, 5はストレート
// (余談)K, A, 2, 3, 4などAを挟むのはストレートではないらしい
const isLast = index === canonical.length - 2;
const aceStart = isLast && current.label == '5' && next.label === 'A';
if(aceStart) { gap = 1; }
if(isLast && current.label == '4' && next.label === 'A') {
gap = 2;
}
// gapが2でジョーカーが残っていれば埋める
// 5, Joker, 7, 8, 9、みたいなやつ
const useJoker = gap == 2 && remainWildcard > 0;
if(useJoker) {
gap = 1;
remainWildcard--;
}
// 差が1でなければストレートではない
if(gap !== 1) {
straight = false;
break;
}
}
return straight;
};
カードをランクでソートし、隣り合うカード同士のランク差がすべて1ならば、ストレートです。隣り合うカードの差が2のときはジョーカーで埋めます。
なお、「A, 2, 3, 4, 5」はストレートとして認められています。
ここまできたら、ストレートフラッシュの判定は簡単です。ストレートかつフラッシュであればいいのですから。
// ストレートフラッシュ
const isStraightFlush = (cardList) => {
return isStraight(cardList) && isFlush(cardList);
};
これで、フラッシュ/ストレート/ストレートフラッシュの判定関数ができました。
ロイヤルストレートフラッシュ
ロイヤルストレートフラッシュは、ストレートフラッシュの特殊な場合です。10からAまでの連番のストレートフラッシュが成立したとき、ロイヤルストレートフラッシュとなります。
これも簡単に判定でき、以下のようになります:
// ロイヤルストレートフラッシュ
const isRoyalStraightFlush = (cardList) => {
// ストレートフラッシュでないならfalse
if(!isStraightFlush(cardList)) { return false; }
// 10〜Aが全てあるかチェック
// ストレートフラッシュは満たしているので、
// 重複チェックは不要
const royalCards = ['10', 'J', 'Q', 'K', 'A'];
let score = 0;
for(const card of cardList) {
if(card.isWildcard || royalCards.includes(card.label)) {
score++;
}
}
return score === 5;
};
ロイヤルストレートフラッシュについては、これだけです。
フォーカード等
次いで、ファイブカード、フォーカード、スリーカードについて作っていきましょう。これらはカードの枚数をチェックするだけで実現できるので、簡単です。
// ファイブカード
const isFiveOfAKind = (cardList) => {
const count = countRank(cardList);
return count.rank.some((c) => c + count.wildcard === 5);
};
// フォーカード
const isFourOfAKind = (cardList) => {
const count = countRank(cardList);
return count.rank.some((c) => c + count.wildcard === 4);
}
// スリーカード
const isThreeOfAKind = (cardList) => {
const count = countRank(cardList);
return count.rank.some((c) => c + count.wildcard === 3);
};
ワンペア/ツーペア/フルハウス
最後にワンペア/ツーペア/フルハウスです。ツーペア/ワンペアについては難しいところはありません:
// ツーペア
const isTwoPair = (cardList) => {
const count = countRank(cardList);
// ジョーカーがあるとツーペアになりえない
if(count.wildcard) { return false; }
// 2枚が2つあればツーペア
return count.rank.filter((c) => c === 2).length === 2;
};
// ワンペア
const isOnePair = (cardList) => {
const count = countRank(cardList);
return count.rank.some((c) => c + count.wildcard === 2);
};
気をつけるのは、ジョーカーがあるとツーペアが成立しない点です。ジョーカーがあると、スリーカードが優先されるからです。
また、これらの関数は完全にワンペア/ツーペアであることを保証しません。例えばフルハウスの手札に対してもisOnePairはtrueを返します。よって関数の呼び出し順序は重要になってきます。
フルハウスについては、2枚3枚パターンか、ツーペア+ジョーカーのパターンです:
// フルハウス
const isFullHouse = (cardList) => {
const count = countRank(cardList);
// ジョーカーがあれば、
// ジョーカーを除いてツーペアを調べる
if(count.wildcard) {
return isTwoPair(removeJoker(cardList));
}
// ジョーカーがなければ、
// 2枚と3枚のランクを探す
const two = count.rank.find((c) => c === 2);
const three = count.rank.find((c) => c === 3);
return !!(two && three);
};
役名の取得
ここまでできたら、役名の取得関数を作りましょう。強い役から順番に判定していきます。
// 役名を取得する関数
const getHandName = (cardList) => {
if(isRoyalStraightFlush(cardList)) {
return 'ロイヤルストレートフラッシュ';
} else if(isFiveOfAKind(cardList)) {
return 'ファイブカード';
} else if(isStraightFlush(cardList)) {
return 'ストレートフラッシュ';
} else if(isFourOfAKind(cardList)) {
return 'フォーカード';
} else if(isFullHouse(cardList)) {
return 'フルハウス';
} else if(isFlush(cardList)) {
return 'フラッシュ';
} else if(isStraight(cardList)) {
return 'ストレート';
} else if(isThreeOfAKind(cardList)){
return 'スリーカード';
} else if(isTwoPair(cardList)) {
return 'ツーペア';
} else if(isOnePair(cardList)) {
return 'ワンペア';
} else {
return 'ノーペア';
}
};
これで役名を判定することができるようになりました!
ゲームを仕上げる
役の判別までできたので、ゲームを仕上げましょう!「交換する」ボタンを押した後に、役名を取得して、表示する機能をつけます。
main.jsの60行目付近、phaseがdoneのときの処理に、役名表示処理を追加します:
// 現在のゲームフェーズを見て処理を変える
if(state.phase === 'done') {
// 役の表示
const hand = getHandName(state.cardList);
const handLabel = document.createElement('div');
handLabel.innerText = hand;
renderTarget.appendChild(handLabel);
// 次のゲームを開始するボタン
const nextGameButton = document.createElement('button');
nextGameButton.innerText = '次のゲームへ';
nextGameButton.addEventListener('click', () => {
startGame();
});
renderTarget.appendChild(nextGameButton);
} else {
これで役名の表示ができるはずです。実際に動かしてみましょう。
できました!長い道のりでしたが、ようやく完成しました!
さいごに
今回はシンプルなポーカーゲームを作りましたが、より発展的な、ベットや報酬、ダブルアップ等が存在するポーカーゲームも作れるでしょう。
また、デザインやアニメーションに凝ってみても良いかもしれません。今回作ったのは、なかなか殺風景ですからね。
また、役の判定について最適化しても良いかもしれません。今回は愚直な方法をとっているので、処理効率は良くありません。
いろいろと工夫のしがいはあると思うので、何か思いついたらぜひ挑戦してみてください。