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

Day20 實作結算牌堆元件(四) 實作移入7牌堆的拖曳&遊戲重置

前言 今天會先實作結算牌堆的牌要可以拖曳至7牌堆, 另外因為每次要重新開始都要切換頁面或按F5重新整理也會實作一個重置遊戲的按鈕 拖曳結算牌堆的牌 至 7牌堆 原本以為會花點時間想怎麼做,但實際上思考、實作都沒花多少時間就完成。 因為結算牌堆拖曳的牌一次只會拖曳一張,且拖曳到7牌堆的過程判斷基本上抄之前7牌堆自己的拖曳判斷方法就差不多完成,只多了一個先判斷拖曳的目標牌堆是否為七牌堆,甚至不用像其他牌堆拖曳:move還要額外去寫『拖曳成功後要觸發修改陣列』的函數changeOption.vlaue。 以下是對應結算牌堆<FinishedArea >內元件的屬性:move實作程式碼: // DragDemo.vue /** 結算牌堆移動 */ function finishedCardMove(evt) { const to = getDomName(evt.to); const { futureIndex, element } = evt.draggedContext; let result = validNames.includes(to); // 只能移動至目標牌堆的最後一張牌 result = result && futureIndex == cardStacks[to].length; // 檢查疊牌順序、花色是否正確 result = result && checkNextOk(cardStacks[to], element); return result; } 調整樣板<FinishedArea >的屬性:moveCard="finishedCardMove"即可套用上方的函數。 <!-- DragDemo.vue --> <div class="text">結算牌堆</div> <FinishedArea :fourCards="cardStacks" :moveCard="finishedCardMove" @doms="setFourCardDoms" :change="cardChange" /> 實作重置遊戲的按鈕 因為遊戲初始化就是寫在onMounted但重設的部分不夠完整,至少並沒有考慮到結算牌堆的部分,舊版初始化程式碼如下: onMounted(() => { const data = geneateShuffleDeck(52); const everyIndex = [0, 1, 3, 6, 10, 15, 21, 28]; validNames.forEach((name, idx) => { cardStacks[name] = data.slice(everyIndex[idx], everyIndex[idx + 1]); }); cardStacks.dealerStacks = data.slice(28).map(card => ({ ...card, isOpen: true })); }); 所以乾脆將初始化遊戲寫成函數resetGame(),這樣點擊重置按鈕、渲染元件onMounted時都可以呼叫同個函數。 // DragDemo.vue onMounted(() => { resetGame(); }); 意外發現發牌區<DealerArea>開牌到第幾張的狀態是包在元件內不利於初始化,決定先將狀態提升父元件的程式碼中DragDemo.vue,將開牌到第幾張的索引改用props方式傳入通知元件<DealerArea>要更新索引。 // DragDemo.vue <script setup> let dealer = reactive({ index: 0 }); </script> <template> <div class="text">發牌區</div> <DealerArea :dealer="dealer" :deck="cardStacks.dealerStacks" :moveCard="dealerMove" /> </template> 為了讓<DealerArea>取得發牌索引的變化,在definProps新增屬性dealer對應一個reactive物件,當物件dealer參考變動時則發牌區的索引index會被設值 【變動後dealer的index】,參考下方程式碼: ...

September 29, 2023 · 2 min · 宗嘉

Day 19 實作結算牌堆元件(三)整合至接龍頁面

前言 今天會實作發牌區、7牌堆的牌可以拖曳到結算牌堆,且拖曳的過程需遵守結算牌堆的同色疊牌由A至K的規則。 整理重複的函數 先將昨天在DealerAreaView.vue撰寫的程式碼移動到拖曳練習的頁面DragDemo.vue。 樣板的部分是沒什麼問題,只是發現有太多函數宣告出現在DragDemo.vue,所以將常數FOUR_SUITS和判斷結算牌堆規則的checkNextOk2函數先移入工具目錄utils/內的程式碼,DragDemo.vue改用import的方式載入通用的常數、函數。 // DragDemo.vue import { FOUR_SUITS } from '../utils/constants'; import { geneateShuffleDeck, checkNextOk, checkNextOk2 } from "../utils/poker-helper"; 設定結算牌堆用的資料 在原本的cardStack中添加針對結算牌堆四花色的撲克牌陣列 // DragDemo.vue const cardStacks = reactive({ // 略... /** @type {Card[]} */ club: [], /** @type {Card[]} */ diamond: [], /** @type {Card[]} */ heart: [], /** @type {Card[]} */ spade: [] }); 因為<FinishedArea />的屬性:fourCard只有要求傳入的物件需要有對應花色名稱的KEY就可以,沒有硬性要求不能有其他屬性, 所以樣板的傳值我就簡單設定cardStacks傳入,如下程式碼: <!-- DragDemo.vue --> <div> <div class="text">發牌區</div> <DealerArea :deck="cardStacks.dealerStacks" :moveCard="dealerMove" /> <div class="text">結算牌堆</div> <FinishedArea :fourCards="cardStacks" @doms="setFourCardDoms" :change="cardChange" /> </div> 在DragDemo.vue中對變數cardStacks新增四個key調整,而原本的函數getDomName(dom)也要跟著調整, 主要除了上層對應7個牌堆、也需要對應到結算牌堆的HTML元素。 底下程式碼中使用花色列表FOUR_SUITS一個個檢查, 若有相同的元素則回傳結算牌堆的花色名稱 (club/diamond/heart/spade),否則依然回傳none。 // DragDemo.vue function getDomName(dom) { if (dom == first.value.targetDomElement) { return 'first'; } else if (dom == second.value.targetDomElement) { return 'second'; } else if (dom == third.value.targetDomElement) { return 'third'; } else if (dom == fourth.value.targetDomElement) { return 'fourth'; } else if (dom == fifth.value.targetDomElement) { return 'fifth'; } else if (dom == sixth.value.targetDomElement) { return 'sixth'; } else if (dom == seventh.value.targetDomElement) { return 'seventh'; } else { for (let i = 0; i < FOUR_SUITS.length; i++) { const name = FOUR_SUITS[i]; if (dom == fourCardsDom[name]) { return name; } } return 'none'; } } 上方程式碼寫到的變數fourCardsDom是一個字典,KEY會是花色(字串),VALUE對應畫色的結算牌堆(HTML元素)。 ...

September 28, 2023 · 2 min · 宗嘉

Day 18 實作結算牌堆元件(二) 整合拖曳相容不同規則

前言 昨天完成結算牌堆樣版的部分,今天接著處理整合拖曳到接龍之前的步驟, 先調整結算牌堆的內部結構、方法。 調整結算牌堆 FinishedArea 先調整結算牌堆<FinishedArea />內部需要的props結構,就底下這三種: props.fourCards對應四個牌堆的陣列 props.moveCard對應<draggable>元件的屬性:move判斷是否可以拖曳成功的函數 props.change對應<draggable>內的列表更新時觸發的函數(這部分等等再說明) 程式碼如下: // FinishedArea.vue const props = defineProps({ fourCards: { type: Object, required: true, validator: (value) => { return ( value.hasOwnProperty('club') && value.hasOwnProperty('diamond') && value.hasOwnProperty('heart') && value.hasOwnProperty('spade') ); }, }, moveCard: { type: Function, default: () => { return false; } }, change: { type: Function, default: () => { return false; } } }) 然後結算牌堆就像發牌區一樣,卡牌陣列都是由外部props傳入且props的陣列內容都會變動, 所以也需要watch監控props.fourCards變化調整元件內的fourCards內的值。 // FinishedArea.vue const fourCards = ref({ club: [], diamond: [], heart: [], spade: [], }); watch(props.fourCards, (newVal) => { fourCards.value = newVal; }); 另外設計一個用來將對應HTML元素(HTML Element)傳回父元件的emit事件,此事件只會在元件渲染成功時觸發一次,發送doms主要是讓父元件可以判斷是被拖曳至哪一個牌堆。 ...

September 27, 2023 · 2 min · 宗嘉