
目次
はじめに
前回の記事ではポーカーアプリを作る準備にあたって、Vue3+CompositionAPIの環境構築
を行いました。
今回は実際に作成したソースコードについて説明していきます。
前回の記事はこちら
作成したポーカーゲームはこちら
作成したファイルについて
プロジェクトディレクトリの"/src/"配下のパスがアプリのソースコードになっており、
今回は手を入れた以下のファイルに絞って説明していきます。
App.vue store - index.ts assets - json/cardList.json components - PorkerGame.vue - ResultGame.vue - StartGame.vue - PorkerGame - HandCard.vue - HandResult.vue
ソースコードの説明
App.vue
ルートパスアクセスで読み込まれるファイル。
Vuexで定義したstateのscreenTypeの値でスタート画面、ゲーム画面、結果画面のいずれかの画面を表示している。
<template> <div class="main"> <div class="main-screen"> <start-game v-if="store.getters.getScreenType === 0" /> <porker-game v-if="store.getters.getScreenType === 1" /> <result-game v-if="store.getters.getScreenType === 2" /> </div> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent } from "vue"; import StartGame from "@/components/StartGame.vue"; import PorkerGame from "@/components/PorkerGame.vue"; import ResultGame from "@/components/ResultGame.vue";
export default defineComponent({ components: { StartGame, PorkerGame, ResultGame, }, setup() { const store = useStore(key);
return { store, }; }, }); </script>
<style lang="scss" scoped> .main { &-screen { width: 800px; height: 400px; padding: 40px; margin: 40px auto; border: 3px solid lightgray; border-radius: 6px; } } </style>
/store/index.ts
Vuexのstoreファイル。
異なるcomponentで値を共有するために使用しており、
本アプリでは以下の4つのstateを定義しています。
1.screenType
表示画面を管理
2.selectCardIndexList
交換対象のカードを管理
3.handChangeDone
ハンドの交換判定を管理
4.resultArray
ゲーム結果を格納したリストを管理
import { InjectionKey } from "vue"; import { createStore, Store } from "vuex";
export interface State { screenType: number; // 表示画面 0:スタート画面 1:ゲーム画面 2:結果画面 selectCardIndexList: number[]; // 交換対象のカードリスト handChangeDone: boolean; // 交換済み判定フラグ resultArray: string[]; // ゲーム結果を格納したリスト }
// define injection key export const key: InjectionKey<Store<State>> = Symbol();
export const store = createStore<State>({ state: { screenType: 0, selectCardIndexList: [], handChangeDone: false, resultArray: [], }, getters: { getScreenType(state) { return state.screenType; }, getSelectCardIndexList(state) { return state.selectCardIndexList; }, getHandChangeDone(state) { return state.handChangeDone; }, getResultArray(state) { return state.resultArray; }, }, mutations: { setScreenType(state, val) { state.screenType = val; }, setSelectCardIndexList(state, val) { state.selectCardIndexList = val; }, setHandChangeDone(state, val) { state.handChangeDone = val; }, setResultArray(state, val) { state.resultArray = val; }, }, actions: { setScreenType({ commit }, val) { commit("setScreenType", val); }, setSelectCardIndexList({ commit }, val) { commit("setSelectCardIndexList", val); }, setHandChangeDone({ commit }, val) { commit("setHandChangeDone", val); }, setResultArray({ commit }, val) { commit("setResultArray", val); }, }, });
/assets/json/cardList.json
全55枚のトランプを定義したJSONファイル。
マーク(ダイヤ:s,スペード:s,ハート:h,クラブ:c,ジョーカー:joker)と数値をキーとした構造となっています。
ジョーカーの数値は100に設定おりますが、13よりも大きければ問題ありません。
[ { "mark": "d", "num": 1 }, { "mark": "d", "num": 2 }, { "mark": "d", "num": 3 }, { "mark": "d", "num": 4 }, { "mark": "d", "num": 5 }, { "mark": "d", "num": 6 }, { "mark": "d", "num": 7 }, { "mark": "d", "num": 8 }, { "mark": "d", "num": 9 }, { "mark": "d", "num": 10 }, { "mark": "d", "num": 11 }, { "mark": "d", "num": 12 }, { "mark": "d", "num": 13 }, { "mark": "s", "num": 1 }, ... { "mark": "joker", "num": 100 }, { "mark": "joker", "num": 100 } ]
/components/StartGame.vue
ゲームタイトル、ルール説明、ゲームスタートボタンを表示しているスタート画面。
ゲームスタートボタン押下でmenuIndexのstateを更新し、ゲーム画面を表示している。
<template> <div class="start-game"> <h1>ポーカーゲーム</h1> <div class="context"> <h3>ルール説明</h3> <p>・全5回ゲームを行い、それぞれのゲームの役をポイントとして表示</p> <p>・手札の交換は一回のみ可能</p> <p>・ジョーカーは2枚</p> </div> <button type="button" class="btn btn-orange" @click="startPorkerGame()"> ゲームスタート </button> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent } from "vue";
export default defineComponent({ components: {}, setup() { const store = useStore(key);
// method const startPorkerGame = (): void => { store.dispatch("setScreenType", 1); };
return { store, startPorkerGame, }; }, }); </script>
<style lang="scss" scoped> .start-game { display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; .btn { margin-top: 32px; } } </style>
/components/PorkerGame.vue
ゲーム画面。
ハンドの表示や役の結果はそれぞれ別componentに定義している。
シャッフルした山札から5枚カード引き、昇順でソートしたものをハンドとして設定し、
componentに渡している。
手札の交換は選択したカードを捨て、捨てた枚数分新たに山札からカードを引いている。
次のゲームへ進むボタン押下で、再度ゲームを始められ、全5ゲームを終えると結果発表ボタンが表示され、
結果発表画面に遷移することができる。
<template> <div class="porker-game"> <h2 class="game-count"> {{ state.currentGameCount }}/{{ maxGameCount }}回目 <span v-if="!store.getters.getHandChangeDone">交換前</span> <span v-else>交換後</span> </h2> <hand-card :hand-card-list="state.handCardList" /> <hand-result :hand-card-list="state.handCardList" /> <div class="change-hand-button"> <button v-if="!store.getters.getHandChangeDone" type="button" class="btn btn-orange" @click="exchangeHand()" > <span v-if="store.getters.getSelectCardIndexList.length > 0" >選択したカードを交換</span ><span v-else>交換しない</span> </button> <button v-else-if="state.currentGameCount < maxGameCount" type="button" class="btn btn-blue" @click="nextGame()" > 次のゲームへ </button> <button v-else type="button" class="btn btn-blue" @click="showResultGame()" > 結果発表 </button> </div> </div> </template>
<script lang="ts"> import { defineComponent, reactive } from "vue"; import HandCard from "@/components/PorkerGame/HandCard.vue"; import HandResult from "@/components/PorkerGame/HandResult.vue"; import { useStore } from "vuex"; import { key } from "@/store/"; import cardListJson from "@/assets/json/cardList.json";
interface Card { mark: string; num: number; }
/** * 全カードをシャッフル * * @param {Object[]} cardList 全カード * @return {Object[]} シャッフルした全カード */ function getShuffleCardList(cardList: Card[]) { if (typeof window !== "undefined") { for (let i = cardList.length - 1; i >= 0; i--) { const j = Math.floor(Math.random() * (i + 1));
[cardList[i], cardList[j]] = [cardList[j], cardList[i]]; } }
return cardList; }
/** * 引数の数だけカードを絞り込む * * @param {Object[]} cardList 全カード * @param {number} num 引くカードの枚数 * @return { [s: string]: any } 絞り込んだカードリスト */ function getFilterCardList(cardList: Card[], num: number) { const filterCardList = Object.create(cardList).splice(0, num);
return filterCardList; }
/** * 数値の昇順でソート(ジョーカーは最後) * * @param {Object[]} handCardList ソート前の手札 * @return {Object[]} ソート後の手札 */ function getSortHandCardList(handCardList: Card[]) { let sortHandCardList = handCardList.slice();
sortHandCardList = sortHandCardList.sort((a: Card, b: Card) => { if (a.num < b.num) return -1; if (a.num > b.num) return 1;
return 0; });
return sortHandCardList; }
export default defineComponent({ components: { HandCard, HandResult }, setup() { const store = useStore(key); const state = reactive({ handCardList: [] as Card[], currentGameCount: 1, // 現在のゲーム数 }); const maxGameCount = 5; // 最大ゲーム数 const isHandChange = false; const handNum = 5; let shuffleCardList: Card[] = []; let deckCardList: Card[] = [];
/** * ゲーム開始処理 * シャッフルした山札から5枚カードを引き、昇順にソートを行い手札として設定する。 */ function initGame() { shuffleCardList = getShuffleCardList(cardListJson); // 全カードをシャッフル state.handCardList = getFilterCardList(shuffleCardList, handNum); // 引数の数だけカードを絞り込む state.handCardList = getSortHandCardList(state.handCardList); // 数値の昇順でソート(ジョーカーは最後) deckCardList = shuffleCardList.filter((card: Card) => { let result = false; const targetData = state.handCardList.find( (handCard: Card) => handCard.num === card.num );
if (!targetData) { result = true; }
return result; }); // 山札の更新 }
initGame(); // ゲーム開始処理
/** * ゲーム画面を表示 */ const startPorkerGame = (): void => { store.dispatch("setScreenType", 1); };
/** * 手札の交換処理 */ const exchangeHand = (): void => { const selectCardIndexList = store.getters.getSelectCardIndexList; let newHand = state.handCardList.filter((data: Card, index: number) => { return !selectCardIndexList.includes(index); });
newHand = newHand.concat( getFilterCardList(deckCardList, selectCardIndexList.length) ); newHand = getSortHandCardList(newHand); state.handCardList = newHand; store.dispatch("setSelectCardIndexList", []); store.dispatch("setHandChangeDone", true); };
/** * 次のゲームへ進む */ const nextGame = (): void => { state.currentGameCount++; initGame(); // ゲーム開始処理 store.dispatch("setHandChangeDone", false); };
/** * 結果画面を表示 */ const showResultGame = (): void => { store.dispatch("setScreenType", 2); store.dispatch("setHandChangeDone", false); };
return { store, state, maxGameCount, isHandChange, handNum, deckCardList, startPorkerGame, exchangeHand, nextGame, showResultGame, }; }, }); </script>
<style lang="scss" scoped> .porker-game { .game-count { text-align: center; } .change-hand-button { display: flex; align-items: center; justify-content: center; } } </style>
/components/PorkerGame/HandCard.vue
ハンドを画面に表示するコンポーネント。
トランプの画像をクリックで選択処理(selectCard)が実行され、
storeで管理しているselectCardIndexListを更新する。
selectCardIndexListはPorkerGame.vueで定義している手札交換処理(exchangeHand)で使用している。
<template> <div class="hand-card"> <ul class="card-list" :class="{ exchanged: store.getters.getHandChangeDone }" > <li v-for="(card, index) in handCardList" :key="index" @click="selectCard(index)" > <img :class="{ 'is-selected': store.getters.getSelectCardIndexList.includes(index), }" :src=" require(`@/assets/images/cardImages/${cardImageFileName( card.mark, card.num )}.png`) " /> </li> </ul> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent, computed } from "vue";
export default defineComponent({ components: {}, props: { handCardList: { type: Array, default: () => [], }, }, setup() { const store = useStore(key);
// computed const cardImageFileName = computed(() => { return (mark: string, num: number) => { let fileName = "";
if (mark === "joker") { fileName = "joker"; } else { fileName = `${mark}-${String(num)}`; }
return fileName; }; });
// method const selectCard = (index: number): void => { if (store.getters.getHandChangeDone) { return; }
let newSelectCardIndexList = store.getters.getSelectCardIndexList; let targetIndex = newSelectCardIndexList.indexOf(index);
if (targetIndex >= 0) { newSelectCardIndexList.splice(targetIndex, 1); } else { newSelectCardIndexList.push(index); }
store.dispatch("setSelectCardIndexList", newSelectCardIndexList); };
return { store, cardImageFileName, selectCard, }; }, }); </script>
<style lang="scss"> .hand-card { .card-list { display: flex; align-items: center; justify-content: center; &.exchanged { li { img { cursor: default; } } } li { width: 150px; &:not(:first-child) { margin-left: 12px; } img { width: 100%; cursor: pointer; &.is-selected { outline: 3px solid blue; } } } } } </style>
/components/PorkerGame/HandResult.vue
ポーカーの役を表示するコンポーネント。
ハンドからジョーカーの数とジョーカーを抜いたハンドを定義してから、役の判定を行っている。
ジョーカーを抜いたハンドから「key:数値,value:マークの配列」のオブジェクト配列定義(※1)し、
以下の役が成立しているかを事前に判定。
■フラッシュ
ジョーカーを抜いたハンドに1種類のみのマークしか存在しないかどうか
■ロイヤルストレート(10,11,12,13,A)
ジョーカーを抜いたハンドが全て10からA内の数値かどうか
■ストレート
ジョーカーを抜いたハンドが階段状に1ずつインクリメントされているかどうか。
差が2以上あった場合でもジョーカーがある場合は、代用できるようにしている。
■フルハウス
※1のオブジェクト配列を使用し、スリーカードとワンペアの両方が成立しているかを判定。
ジョーカーが1枚ある場合はワンペアが2つ成立していればフルハウスとして判定する。
(ジョーカー2枚ある場合はフルハウスではなくフォーカードが成立するため、2枚あるときのパターンは考慮していない)
■ツーペア
※1のオブジェクト配列を使用し、ワンペアが2つ成立しているかどうかを判定。
上記の役成立結果を用いて最終的な役の結果を取得している。
<template> <div class="hand-result"> <p> <span v-if="store.getters.getHandChangeDone">交換後の役</span ><span v-else>現在の役</span>:{{ checkHandResult }} </p> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent, computed, PropType } from "vue";
interface Card { mark: string; num: number; }
/** * フラッシュの役判定 * * @param {Object[]} noJokerHandList ジョーカーを除いたハンド * @return {boolean} true:フラッシュ役 false:フラッシュ役でない */ function checkIsFlash(noJokerHandList: Card[]) { let result = false; const set = new Set( noJokerHandList.map((data: Card) => { return data.mark; }) );
if (Array.from(set).length === 1) { result = true; }
return result; }
/** * ストレートの役判定(ただし、ロイヤルストレートの形は除く) * * @param {Object[]} noJokerHandList ジョーカーを除いたハンド * @param {number} jokerNum ジョーカーの数 * @return {boolean} true:ストレート役 false:ストレート役でない */ function checkIsStraight(noJokerHandList: Card[], jokerNum: number) { let result = true; let stockJoker: number = jokerNum; // ジョーカーのカウント
// ストレートかを判定 for (let i = 0; i < noJokerHandList.length - 1; i++) { const diffNum = noJokerHandList[i + 1].num - noJokerHandList[i].num;
if (diffNum !== 1) { if (stockJoker > 0 && diffNum > 1 && diffNum - 1 <= stockJoker) { stockJoker -= diffNum - 1; } else { result = false; } } }
return result; }
/** * ロイヤルストレートの役判定(10, 11, 12, 13, Aの形) * * @param {Object[]} noJokerHandList ジョーカーを除いたハンド * @param {number} jokerNum ジョーカーの数 * @return {boolean} true:ロイヤルストレート役 false:ロイヤルストレート役でない */ function checkIsRoyalStraight(noJokerHandList: Card[], jokerNum: number) { let result = false; const handCardNum: number[] = noJokerHandList.map((data: Card) => { return data.num; }); const royalStraightHandNum: number[] = [1, 10, 11, 12, 13]; const diffHand: number[] = royalStraightHandNum.filter((data: number) => { return handCardNum.indexOf(data) >= 0; });
if (diffHand.length + jokerNum >= 5) { result = true; }
return result; }
/** * 手札内で一番重複しているカードの枚数の取得 * * @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト * @return {number} 手札内で一番重複しているカードの枚数 */ function getMaxSameCardNum(numKeyHand: { [n: number]: Card[] }) { let maxSameCard = 0;
for (const key in numKeyHand) { if (key === "j") { continue; }
if (numKeyHand[key].length > maxSameCard) { maxSameCard = numKeyHand[key].length; } }
return maxSameCard; }
/** * フルハウスの役判定 * * @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト * @param {number} jokerNum ジョーカーの数 * @return {boolean} true:フルハウス役 false:フルハウス役でない */ function checkIsFullHouse( numKeyHand: { [n: number]: Card[] }, jokerNum: number ) { let result = false;
if (jokerNum === 0) { let threeCard = false; let twoCard = false;
for (const key in numKeyHand) { if (numKeyHand[key].length === 3) { threeCard = true; } else if (numKeyHand[key].length === 2) { twoCard = true; } }
if (threeCard && twoCard) { result = true; } } else if (jokerNum === 1) { let twoCardPair = 0;
for (const key in numKeyHand) { if (numKeyHand[key].length === 2) { twoCardPair++; } }
if (twoCardPair === 2) { result = true; } }
return result; }
/** * ツーペアの役判定 * * @param {Object} numKeyHand ハンドの数値をkeyとしてオブジェクト * @param {number} jokerNum ジョーカーの数 * @return {boolean} true:フルハウス役 false:フルハウス役でない */ function checkIsTwoPair(numKeyHand: { [n: number]: Card[] }, jokerNum: number) { let result = false;
if (jokerNum === 0) { let twoCardPair = 0;
for (const key in numKeyHand) { if (numKeyHand[key].length === 2) { twoCardPair++; } }
if (twoCardPair === 2) { result = true; } }
return result; }
/** * 数字をkeyとしたオブジェクトを取得 * * @param {Object[]} noJokerHandList ジョーカーを除いたハンド * @return {Object} 数字をkeyとしたオブジェクト */ function getNumKeyHand(noJokerHandList: Card[]) { const numKeyHand: { [n: number]: Card[] } = {};
for (let i = 0; i < noJokerHandList.length; i++) { if (!numKeyHand[noJokerHandList[i].num]) { numKeyHand[noJokerHandList[i].num] = []; }
numKeyHand[noJokerHandList[i].num].push(noJokerHandList[i]); }
return numKeyHand; }
/** * ポーカーの役の取得 * * @param {Object[]} handCardList ハンド * @return {string} ポーカーの役 */ function getHandResult(handCardList: Card[]) { let handResult = "役なし"; const noJokerHandList: Card[] = handCardList.filter( (data: Card) => data.mark !== "joker" ); const jokerNum: number = handCardList.filter( (data: Card) => data.mark === "joker" ).length; const numKeyHand: { [n: number]: Card[] } = getNumKeyHand(noJokerHandList); const maxPairNum: number = getMaxSameCardNum(numKeyHand); const isFlash = checkIsFlash(noJokerHandList); const isRoyalStraight = checkIsRoyalStraight(noJokerHandList, jokerNum); const isStraight = checkIsStraight(noJokerHandList, jokerNum); const isFullHouse = checkIsFullHouse(numKeyHand, jokerNum); const isTwoPair = checkIsTwoPair(numKeyHand, jokerNum);
// フラッシュかどうかの判定 if (isFlash) { if (isRoyalStraight) { // ロイヤルストレートフラッシュかどうかの判定 handResult = "ロイヤルストレートフラッシュ"; } else if (isStraight) { // ストレートフラッシュかどうかの判定 handResult = "ストレートフラッシュ"; } else { // フラッシュかどうかの判定 handResult = "フラッシュ"; } } else if (isRoyalStraight || isStraight) { // ストレートかどうかの判定 handResult = "ストレート"; } else if (maxPairNum + jokerNum === 5) { handResult = "ファイブカード"; } else if (maxPairNum + jokerNum === 4) { handResult = "フォーカード"; } else if (isFullHouse) { handResult = "フルハウス"; } else if (maxPairNum + jokerNum === 3) { handResult = "スリーカード"; } else if (isTwoPair) { handResult = "ツーペア"; } else if (maxPairNum + jokerNum === 2) { handResult = "ワンペア"; }
return handResult; }
export default defineComponent({ components: {}, props: { handCardList: { type: Array as PropType<Array<Card>>, default: () => [], }, }, setup(props) { const store = useStore(key); const checkHandResult = computed(() => { let handResult = getHandResult(props.handCardList); // ポーカーの役の取得
if (store.getters.getHandChangeDone) { const newResultArray = store.state.resultArray;
newResultArray.push(handResult);
store.dispatch("setResultArray", newResultArray); }
return handResult; });
return { store, checkHandResult, }; }, }); </script>
<style lang="scss"> .hand-result { p { font-size: 28px; font-weight: bold; color: red; text-align: center; } } </style>
/components/resultGame.vue
ゲームの結果を表示する画面。
役毎にポイントを設定し、5ゲームの集計結果を算出する。
最初に戻るボタンを押下で、スタート画面に遷移する。
<template> <div class="result-game"> <h2>結果発表</h2> <div class="result-list"> <ul> <li v-for="(result, index) in store.getters.getResultArray" :key="index" > <p>{{ index + 1 }}回目: {{ result }} ({{ pointList[result] }}点)</p> </li> </ul> </div> <div class="point"> <p>点数: {{ resultPoint }}点</p> </div> <div class="reset"> <button type="button" class="btn btn-blue" @click="resetGame()"> 最初に戻る </button> </div> </div> </template>
<script lang="ts"> import { useStore } from "vuex"; import { key } from "@/store/"; import { defineComponent } from "vue";
export default defineComponent({ components: {}, setup() { const store = useStore(key); let resultPoint = 0; const pointList: { [s: string]: number } = { ワンペア: 10, ツーペア: 20, スリーカード: 30, ストレート: 40, フラッシュ: 50, フルハウス: 60, フォーカード: 70, ストレートフラッシュ: 80, ロイヤルストレートフラッシュ: 90, ファイブカード: 100, 役なし: 0, };
/** * 結果ポイントの計算 */ function calcResultPoint() { const resultArray = store.getters.getResultArray;
for (let i = 0; i < resultArray.length; i++) { resultPoint += pointList[resultArray[i]]; } }
calcResultPoint(); // 結果ポイントの計算
/** * ゲームのリセット */ const resetGame = (): void => { store.dispatch("setScreenType", 0); store.dispatch("setResultArray", []); };
return { store, resultPoint, pointList, resetGame, }; }, }); </script>
<style lang="scss" scoped> .result-game { display: flex; height: 100%; flex-direction: column; justify-content: center; align-items: center; .point { font-size: 32px; color: red; font-weight: bold; } } </style>
まとめ
工夫した点としては最大2枚のジョーカーを考慮したロジックを考えたことです。
ジョーカーを抜いたハンドとジョーカーの数を使用することで、複雑にならずにプログラムを書くことができました。
Vue3のCompositionAPIは初めて触ってみた感想としては、data,computed,methodオプションを全てsetupオプション内に記述するため、可読性の考慮が求められるなと感じました。
リアクティブな変数を定義する際にreactiveやrefを使用するなどいった変更はありますが、基本的にはVue2までの書き方と似ているため、
Vue2をやられていた方であればCompositionAPIの学習コストはそこまで高くないと感じました。
この記事に関するご相談やご質問など、お気軽にお問い合わせください。
関連する記事
-
マーケティング/オピニオン
-
プロダクション/オピニオン
-
プロダクション/オピニオン