Day26 實作功能【返回上一步】

前言 因為玩接龍有時會有按錯步,這時候沒有返回上一步的機制就只能硬著頭皮玩下去或按重置,今天想解決這個問題。 開發前的思考 在ReactJS官方學習文件中OOXX遊戲Tic-Tac-Toe也有提到時光旅行的實作,基本上就是每一步更動後的結果狀態都推入(Push)陣列變數history裡面,時光回朔就是將結果狀態直接設為history[index]。 目前開發下來的程式碼大概要回到上一步的只有分數、牌組、發牌的索引, 經過的時間應該就不用上一步。 實作過程 儲存遊戲變化的歷史 首先宣告一個負責儲存歷史的ref變數history const history = ref([]); 宣告函數pushStateToHistory()負責撰寫把最新的狀態推入history 累積超過30個後會將最舊的狀態移除再推入最新的狀態,避免暫存太多的上一步驟。 因為reactive的關係所以不得不手動深度複製每張卡牌,這也是為什麼要做elemFunc的原因 /** 儲存當前狀態到歷史紀錄 */ function pushStateToHistory() { if (history.value.length > 30) { const startIndex = history.value.length - 30; history.value = history.value.slice(startIndex, history.value.length); } const elemFunc = (card) => ({ "value": card.value, "isOpen": card.isOpen, "isDone": card.isDone, }); history.value = [ ...history.value, { "cardStacks": { first: cardStacks.first.slice().map(elemFunc), second: cardStacks.second.slice().map(elemFunc), third: cardStacks.third.slice().map(elemFunc), fourth: cardStacks.fourth.slice().map(elemFunc), fifth: cardStacks.fifth.slice().map(elemFunc), sixth: cardStacks.sixth.slice().map(elemFunc), seventh: cardStacks.seventh.slice().map(elemFunc), dealerStacks: cardStacks.dealerStacks.slice().map(elemFunc), club: cardStacks.club.slice().map(elemFunc), diamond: cardStacks.diamond.slice().map(elemFunc), heart: cardStacks.heart.slice().map(elemFunc), spade: cardStacks.spade.slice().map(elemFunc), }, "gameScore": JSON.parse(JSON.stringify(gameScore.value)), "dealer": { index: dealer.index }, } ]; } 接著在程式碼中【分數、卡牌有變動】的情況都補上執行pushStateToHistory();去儲存當下的狀態: 發牌區移動/結算牌堆移動/7牌堆移動成功移動後 遊戲初始化resetGame 連點自動拖曳clickAutoMove只要成功移動,最後也會執行 發牌區<DealerArea />點擊開牌也會執行pushStateToHistory();,因為發牌索引dealer.index產生變化 ...

October 5, 2023 · 1 min · 宗嘉

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 · 宗嘉