Day25 紙牌接龍-結算畫面採用Modal和修正移牌優先權

前言 今天會調整結算畫面的顯示、修正連點移牌的優先權錯誤(應該最優先移入結算牌堆而非七牌堆)。 結算畫面調整 安裝套件 bootstrap-vue-next 昨日完成的結算畫面是跳出來的瀏覽器訊息,畫面完全看各家的瀏覽器制式化只能點確認,即使擋住原本的遊戲畫面是我想要的效果,但更想要的是可客製化頁面的互動視窗Modal。 雖然可以自己土炮撰寫Modal但看帳號名字就知道我很懶,我打算撿現成的套件看能不能快速客製化介面…然後就找到bootstrap-vue-next這個套件,聽名字就知道是針對Vue3特別拉出來的實現。 先照著官方文件安裝依賴: npm i bootstrap bootstrap-vue-next npm i unplugin-vue-components -D 這個 unplugin-vue-components 主要是方便自動載入有副作用(side effect)的功能到你的元件中,詳細可參考官方說明,畢竟Bootstrap有副作用才方便?! 接著調整vite.config.js的內容,主要是在plugins屬性對應的陣列中添加Components 包裹 BootstrapVueNextResolver的依賴: // vite.config.js import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import {BootstrapVueNextResolver} from 'unplugin-vue-components/resolvers' // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), Components({ resolvers: [BootstrapVueNextResolver()], }), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) 最後在main.js載入相關的CSS,記得把bootstrap移到上方避免之前撰寫的css被蓋掉: // main.js/ts import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue-next/dist/bootstrap-vue-next.css' // 略 前面都完成後,發現接龍畫面的卡片的英文、符號都變得比較Q版,上方導覽列 NavgationBarCSS有點跑版。 ...

October 4, 2023 · 1 min · 宗嘉

Day24 紙牌接龍-結算畫面

前言 目前實作的紙牌接龍還沒有結算畫面,所以今天就來做! 初步思考 製作結算畫面本身不是問題,畢竟畫面沒有要做得超級漂亮的情況下都是沒問題的! 問題是何時跳出結算畫面? 我想到的情形有兩種: 結算牌堆四堆都集完13張的情況 畫面中7牌堆的牌全部已經打開的情況 我認為第一種判斷結算牌堆的方式實作起來比較簡單,接下來實作也會朝這個方向前進。 實作邏輯 製作檢查完成牌組的函數 宣告一個函數checkSolitaireGameDone負責檢查紙牌接龍是否完成。 依序檢查各結算牌堆,若數量不為13就直接回傳 否false 最後就回傳 是true 程式碼如下: /** * 檢查紙牌接龍是否完成 * @param {CardStacks} cardStacks */ function checkSolitaireGameDone(cardStacks) { for (let i = 0; i < FOUR_SUITS.length; i++) { if (cardStacks[FOUR_SUITS[i]].length !== 13) return false; } return true; } 監控觸發檢查 不意外的又使用到watch這個關鍵字做監控,這部分就是跟Day4的連連看一樣, 當 牌堆cardStacks 發生變化就去觸發檢查,判定完成就使用alert跳出結算訊息。 使用者點擊alert()之後,才會執行重設遊戲的函數resetGame() // DragDemo.vue watch(cardStacks, (newCardStacks) => { const isDone = checkSolitaireGameDone(newCardStacks); if (isDone) { alert(`遊戲結束,花費時間: ${gameTime.value} 秒 總分數: ${gameScore.value}!!!`); resetGame(); } }) 小結 今天在實作結算畫面的過程雖然遇到一些Bug,但不影響遊戲可以正常跳出結算訊息並重置。 只是目前用alert跳出那個結算訊息不太像一個正常遊戲的畫面,所以明天會繼續實作真正的結算畫面和修正目前發現的Bug: 昨日開發的連點移動的優先度不正常 預期優先移到結算牌堆的牌,自動移牌時竟然在7牌堆區中左右來回移動。 發牌區的牌應該要自動移到結算牌堆,竟然先移動到7牌堆 程式碼: https://github.com/kabuto412rock/ithelp-pokergame/tree/day24

October 3, 2023 · 1 min · 宗嘉

Day23 連點2下自動移牌

前言 今天要實做的是點擊自動移牌的功能也算是昨天提示的延伸,差別只在會實際移動卡牌。 我打算連點移牌功能只做在七牌堆和發牌區,結算牌堆就不提供此功能。 實作過程 處理連點事件 調整發牌區/七牌堆的卡片元件<Card>添加對應的屬性@dblclick="emit('card-click', element)", 這會讓卡牌元件<Card>被連點兩下(Double Click)時,向父元件發送事件card-click然後攜帶的參數element則是Card物件 // Card { value: 0, // 卡牌對應數值,Ex: 梅花A isOpen: false, // 是否已開牌 } 然後修改上層樣板(DragDemo.vue)的部分: 為了接收card-click事件進行處理,在發牌區的樣板修改成有添加@card-click="(card) => clickAutoMove('dealerStacks', card) 在七牌堆的樣板,這七行依序添加屬性@dblcick 第一牌堆 @dblclick="clickAutoMove('first', element)" 第二牌堆 @dblclick="clickAutoMove('second', element)" 中間略… 第七牌堆的屬性 @dblclick="clickAutoMove('seventh', element)" 處理自動移動的邏輯 這邊出現的新函數clickAutoMove(fromName, card),主要是用來處理自動移動的邏輯,流程如下: 先利用findFollowDeckName找出card可以拖曳到的牌堆名稱,然後依照優先順序排序(結算牌堆排第一)。 如果沒有找到對應的牌堆,則不執行後續。 取出第一個牌堆名稱當作目標牌堆 接著就判斷來源牌堆是發牌堆還是七牌堆,來做不同的處理(修改對應牌組陣列 還有 加分等等) 可以參考下方的程式碼片段: // DragDemo.vue /** * 自動移動 * @param {String} fromName 來自的牌堆名稱 * @param {Card} card 想移動的牌 */ function clickAutoMove(fromName, card) { const toNames = findFollowDeckName(cardStacks, card).sort((a, b) => { const aOrder = a.length + FOUR_SUITS.includes(a) ? -100 : 0; const bOrder = b.length + FOUR_SUITS.includes(b) ? -100 : 0; return aOrder - bOrder; }) // 如果沒找到對應牌堆,則不執行 if (toNames.length == 0) { console.log(`卡牌${PokerValuesMap[card.value].content}沒有符合移動的規則`); return; } const toName = toNames[0]; const isToFinishedArea = FOUR_SUITS.includes(toName); if (fromName == 'dealerStacks') { // 來自`發牌堆` const fromIndex = cardStacks[fromName].findIndex(c => c.value == card.value); const newFromCards = [ ...cardStacks[fromName].slice(0, fromIndex), ...cardStacks[fromName].slice(fromIndex + 1) ]; const newToCards = [ ...cardStacks[toName], cardStacks[fromName][fromIndex] ]; cardStacks[fromName] = newFromCards; cardStacks[toName] = newToCards; gameScore.value += isToFinishedArea ? 25 : 10; } else if (SEVEN_STACKS.includes(fromName)) { // 來自7牌堆 const fromLength = cardStacks[fromName].length; const fromIndex = cardStacks[fromName].findIndex(c => c.value == card.value); if (isToFinishedArea) { if (fromIndex != fromLength - 1) { console.log(`卡牌${PokerValuesMap[card.value].content}不是${fromName}的最後一張牌,不可移入結算牌堆`); return; } const newFromCards = cardStacks[fromName].slice(0, fromIndex); const newToCards = [ ...cardStacks[toName], card ]; cardStacks[fromName] = newFromCards; cardStacks[toName] = newToCards; gameScore.value += 15; } else { const newFromCards = cardStacks[fromName].slice(0, fromIndex); const newToCards = [ ...cardStacks[toName], ...cardStacks[fromName].slice(fromIndex) ]; cardStacks[fromName] = newFromCards; cardStacks[toName] = newToCards; } } } 另外在poker-helper.js新增函數findFollowDeckName(cardstacks, targetCard),主要是用來找出targetCard可以拖曳到的牌堆名稱,因為可能可以拖曳到複數個牌堆,所以回傳字串組成的一維陣列: ...

October 2, 2023 · 3 min · 宗嘉

Day22 接龍移牌提示

前言 今天要實作接龍移牌提示,以下是會需要處理的題目: 怎麼取得場上牌的拖曳路線? 找到拖曳路線後,如何顯示要拖曳至哪個地方的提示(文字or動畫)? 取得拖曳路線 目前可知拖曳區塊有7牌堆、發牌區、結算牌堆,其中卡牌可拖曳的方向有: 7牌堆可以內部自拖曳或結算牌堆 發牌區只能拖曳至7牌堆、結算牌堆 結算牌堆只能拖曳至7牌堆 初步分析: 可以先計算可以移入7牌堆、結算牌堆牌尾的撲克牌 預計執行步驟: 計算出7牌堆、結算牌堆各自牌尾後能放什麼牌,儲存在Map 從發牌區/7牌堆/結算牌堆依序判斷可拖曳卡牌的數字是否存在Map中? 是,回傳比對成功的結果: { "可拖曳卡牌所在的牌堆", "拖曳卡牌在牌堆的位置", "預計移入的牌堆"} 否,繼續比對下一張直到無牌可比 實際程式碼 參數帶入要計算的全部牌堆,計算回傳每張牌可被移入的牌堆。 因為有可能出現梅花A可以移入結算牌堆或7牌堆的情況,所以實作設計成一張牌只會對應一個牌堆,此例梅花A會優先被移入結算牌堆。 // utils/poker-helper.js /** * 找出7牌堆、結算牌堆各牌尾後要接的牌 * @param {CardStacks} cardstacks * @returns {Map<Number, String>} Map<撲克牌編號, 目標牌堆名稱> */ function findTailCards(cardstacks) { const result = new Map(); // 找出可拖曳至7牌堆尾巴的牌 SEVEN_STACKS.forEach((name) => { const stack = cardstacks[name]; if (stack.length === 0) { [12, 25, 38, 51].forEach((value) => { result.set(value, name); }); return; } const lastCard = stack[stack.length - 1]; const lastCardNumber = lastCard.value % 13; const lastCardSymbol = Math.floor(lastCard.value / 13); // 檢查是否為A,則跳過 if (lastCardNumber === 0) { return; } const matchNumber = lastCardNumber - 1; const isBlack = lastCardSymbol % 3 == 0; [matchNumber + (isBlack ? 13 : 0), matchNumber + (isBlack ? 26 : 39)].forEach((value) => { result.set(value, name); }); }); // 找出可拖曳至結算牌堆尾巴的牌 FOUR_SUITS.forEach((name, index) => { const stack = cardstacks[name]; if (stack.length === 0) { result.set(0 + index * 13, name); return; } const lastCard = stack[stack.length - 1]; const lastCardNumber = lastCard.value % 13; // 檢查是否為K,則跳過 if (lastCardNumber === 12) { return; } const matchNumber = lastCardNumber + 1; result.set(matchNumber + index * 13, name); }); return result; } 參數帶入要計算的全部牌堆、發牌區發到的位置,一旦檢查到有一個卡牌符合則返回拖曳路線的資訊, 若無則返回null值。 // utils/poker-helper.js /** 取得一個移動提示 * @param {CardStacks} cardStacks * @param {number} dealerIndex * @returns {MoveHint | null} 移動提示 */ function getMoveHint(cardStacks, dealerIndex) { const tailValuesMap = findTailCards(cardStacks); let hintAnswer = null; // 發牌區 let startIndex = dealerIndex < 3 ? 0 : dealerIndex - 3; const dealerCards = cardStacks['dealerStacks'].slice(startIndex, dealerIndex); dealerCards.forEach((card) => { if (tailValuesMap.has(card.value)) { hintAnswer = { fromName: 'dealerStacks', card: card, fromIndex: cardStacks['dealerStacks'].findIndex((c) => c.value === card.value), toName: tailValuesMap.get(card.value), }; } }); if (hintAnswer != null) return hintAnswer; // 7個牌堆 SEVEN_STACKS.forEach((name) => { let len = cardStacks[name].length; for (let i = 0; i < len; i++) { let card = cardStacks[name][i]; // 由上往下找,遇到未開牌就跳過 if (!card.isOpen) continue; if (tailValuesMap.has(card.value)) { const toName = tailValuesMap.get(card.value); // 只能拿最後一張牌放 結算牌堆 if (FOUR_SUITS.includes(toName) && i !== len - 1) continue; hintAnswer = { fromName: name, card: card, fromIndex: i, toName: toName, }; break; } } }); if (hintAnswer != null) return hintAnswer; // 結算牌堆 FOUR_SUITS.forEach((name) => { let len = cardStacks[name].length; if (len == 0) return; let card = cardStacks[name][len - 1]; if (tailValuesMap.has(card.value)) { hintAnswer = { fromName: name, card: card, fromIndex: len - 1, toName: tailValuesMap.get(card.value), }; } }); return hintAnswer; } 執行拖曳提示動畫 目前已經可以呼叫函數getMoveHint取得拖曳路線的資訊 ...

October 1, 2023 · 3 min · 宗嘉

Day21 顯示接龍分數、遊戲時間

前言 今天預計實作的項目顯示分數、顯示遊玩時間, 但實作顯示分數必須配合實作累計分數的功能,不然分數都不會變動也是尷尬😂。 儲存分數、遊玩時間的變數宣告: const gameScore = ref(0); const gameTime = ref(0); const gameTimer = ref(null); 實作遊戲分數 實作分數累計規則 先整理出接龍的分數在什麼情況會增加? 從發牌區移出則加10分,因為發牌區的牌不會被重新移入所以不用擔心重複加分。 // DragDemo.vue /** 發牌區移動 */ function dealerMove(evt) { // 略 if(result){ changeOption.value = () => { // 略 gameScore.value += 10; } } } 7牌堆的牌被打開則加5分,因為被打開的牌不會被蓋回去。 原本程式就會將7牌堆最一張牌設為打開,改判斷最後一張原本是蓋牌才開牌、加5分避免分數重複累加。 // DragDemo.vue watch(cardStacks, (stacks) => { // 檢查每組牌堆最後一張 validNames.forEach(cardName => { if (stacks[cardName].length > 0) { const lastCard = stacks[cardName][stacks[cardName].length - 1]; if (!lastCard.isOpen) { lastCard.isOpen = true; gameScore.value += 5; } } }); }); 卡牌移入結算牌堆加15分,移出則扣15分。 移出的部分直接調整結算牌堆移動進行扣分 /** 結算牌堆移動 */ function finishedCardMove(evt) { // 略 if (result) { changeOption.value = () => { gameScore.value -= 15; changeOption.value = null; }; } return result; } 移入結算牌堆則會需要調整發牌區和7牌堆的移動,一樣是調整changeOption.value的程式碼要加15分。 因為第1點的規則(10分),如果直接從發牌區拖曳至結算牌堆會加25分(15+10) ...

September 29, 2023 · 2 min · 宗嘉