哈囉,冒險者們 🧙‍♂️🔥

歡迎來到宗嘉的部落格~希望能找到適合你的程式魔法

使用Gin Request Context可能遇到的問題(附解法)

撰文原因 問題程式碼(範例) 解法 1. 暫時先傳 context.TODO() 2. 使用 context.WithoutCancel 3. 不要用go關鍵字 結語 參考資料 撰文原因 最近工作遇到服務使用的新方法需要傳遞context到函數中執行, 結果導致本來要跑在背景任務沒完成(ex: 通知訊息/發送郵件)。 問題程式碼(範例) 事情是因為傳入的是 c.Request.Context(),然而這個gin所實作的Context在回應HTTP Response結束時會自動取消, 導致引用ctx的do函數在Context連帶著被取消後內部的函數也跟著取消。 程式碼範例如下: package main import ( "context" "fmt" "net/http" "time" "github.com/gin-gonic/gin" ) func main() { // Create a Gin router with default middleware (logger and recovery) r := gin.Default() // Define a simple GET endpoint r.GET("/ping", func(c *gin.Context) { go doWork(c.Request.Context()) time.Sleep(2 * time.Second) // Return JSON response c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) r.Run("127.0.0.1:8080") } func doWork(ctx context.Context) { defer ctx.Done() elapsed := 0 * time.Millisecond for { select { case <-ctx.Done(): // Check if the context is done (cancelled or timed out) fmt.Printf("worker stopped err: %v\n", ctx.Err()) return default: secs := 500 * time.Millisecond fmt.Printf("working... %v passed\n", elapsed) time.Sleep(secs) elapsed += secs } if elapsed >= 5*time.Second { fmt.Println("worker completed") break } } } 實際跑起來會發現doWork只是每0.5秒印出一段訊息,原本預期最多跑5秒就會結束, 然而實際上2秒就會因為gin response被標記為完成自動結束, 這邊只是粗略模擬尊重context機制的函式庫的行為(qmgo…), 當select捕捉到<-ctx.Done()會停止或取消後續的行為。 ...

October 5, 2025 · 1 min · 宗嘉

Day30 使用Cloudflare部屬Vue靜態網站

目前撲克牌遊戲網站都是在自己的電腦用npm run dev啟動,今天會介紹如何部屬Vue專案到CloudFlare提供對外連線的網站,操作有誤的地方再請多多指教。 使用Github Aciton建立 以下的操作需要事先註冊Cloudflare的帳號 和GitHub的帳號 第0步 建立CloudFlare Page的Project 我是參考這個YouTube影片學習如何建立Cloudflare Page專案, 但跟影片不同的部分我在1:56我是選only-select-repositories選擇單一Repo的權限並命名專案名稱ithelp-game-test。 影片看到5分鐘的時候,Cloudflare網站本身停止回應,後面的步驟就沒跟著影片教學做,後面第1~3步驟是我一步步看文件試出來的並非完全照官方建議走,因為需按專案本身調整。 第1步 建立 Cloudflare API Token 參考Cloudflare Pages GitHub Action說明以下只是我再額外自己截圖實作的步驟。 登入帳號來到Cloudflare的儀表板,先點選左下角的Workers & Pages 接著點擊右手邊連結Manage API tokens進入管理API Token的頁面 點擊藍色按鈕Create Token前往建立API Token的頁面 來到API Tokens頁面後點選Create Custom Token旁的藍色按鈕Get Started 填寫Token name這邊幫Token取名為Deploy with github 在Permisions區塊點選Add新增一個權限Account/Cloudflare/Edit ,接著畫面拉到最下方點擊藍色按鈕Continue to summary。 此時Cloudflare會讓你再次確認權限,只需要注意畫面上有出現All accounts - Cloudflare Pages:Edit這個,沒問題就繼續點擊藍色按鈕Create Token。 至此Cloudflare API Token建立成功,點擊按鈕Copy先複製起來 第2步 將Cloudflare API Token設置於Github Repo 此處Github儲存庫是kabuto412rock/ithelp-pokergame ...

October 9, 2023 · 2 min · 宗嘉

Day29 簡單評估是否還有活路

目前所製作的經典紙牌接龍其實是源自於Klondike Solitaire的規則,只是發牌區是一次抽一張的循環制,因為網路上有看到每次都是抽3張但只能移動最上面那一張的規則,那種非常難玩挑戰性也很大。 判斷紙牌接龍無解這個問題這兩天困擾我很久,在查過無數資料略讀幾篇論文後,尤其實際有用的資料大多是英文論文😭,發現這絕對不是一兩天的空餘時間就可以理解的目標,所以決定降低目標不去判斷是否已經死局,改為【簡單評估是否還有活路】。 簡單評估是否還有活路 尋找活路的最容易想到的3種可能移動方式: 七牌堆最後一張後面要接的牌存在於發牌區中 任一七牌堆壓在隱藏牌上放的那張可以接在其他七牌堆的後面 發牌區任一張牌或七牌堆的最後一張 可移動結算牌堆 開始實作 按照簡單評估的三項規則依序檢查,若有檢查可移動的方式為true就不會繼續檢查後續的規則,以下為程式碼: // poker-helper.js /** 檢查是否還有效的移動卡牌 * @param {CardStacks} cardStacks * @returns {boolean} 有有效移動為true 可能沒有為false */ function checkValidMove(cardStacks) { const dealerStacksValues = cardStacks['dealerStacks'].map((card) => card.value); const seventLastValues = {}; // 1. `七牌堆`最後一張後面要接的牌存在於`發牌區`中 let haveMove1 = SEVEN_STACKS.some((name) => { const stack = cardStacks[name]; if (stack.length === 0) { return false; } const lastCard = stack[stack.length - 1]; const lastCardNumber = lastCard.value % 13; seventLastValues[name] = lastCard.value; // A後面沒有要接的,但可移動到結算牌堆(回應成功) if (lastCardNumber === 0) return true; const targetPokerValues = getDifferentColorPokerValues(lastCard.value - 1); return dealerStacksValues.some((value) => { return targetPokerValues.includes(value); }); }); if (haveMove1) { console.log("第1活局: `七牌堆`最後一張後面要接的牌存在於`發牌區`"); return true; } // 2. 任一`七牌堆`壓在隱藏牌上放的那張可以接在其他`七牌堆`的後面(不包含本身牌堆) let haveMove2 = SEVEN_STACKS.some((name) => { const stack = cardStacks[name]; if (stack.length === 0) { return false; } let firstOpenCard = null; for (let i = 1; i < stack.length; i++) { if (stack[i].isOpen && (!stack[i - 1].isOpen)) { firstOpenCard = stack[i]; break; } } // 沒有壓在隱藏牌上的牌或 放的那張是K無法接在其他牌堆後面(回應失敗) if (firstOpenCard === null || firstOpenCard.value % 13 === 12) { return false; } // 檢查是否有可以接的牌 const targetPokerValues = getDifferentColorPokerValues(firstOpenCard.value + 1) return SEVEN_STACKS.some((name2) => { if (name === name2) return false; if (seventLastValues[name2]) { return targetPokerValues.includes(seventLastValues[name2]); } return false; }); }); if (haveMove2) { console.log("第2活局: 任一`七牌堆`壓在隱藏牌上放的那張可以接在其他`七牌堆`的後面(不包含本身牌堆)"); return true; } // 3. `發牌區`任一張牌或`七牌堆`的最後一張 可移動至 `結算牌堆` let haveMove3 = FOUR_SUITS.some((suit, index) => { const stackLen = cardStacks[suit].length; if (stackLen === 13) return false; let targetValue = index * 13 + stackLen; return dealerStacksValues.includes(targetValue) || Object.values(seventLastValues).includes(targetValue); }); if (haveMove3) { console.log("第3活局: `發牌區`任一張牌或`七牌堆`的最後一張 可移動至 `結算牌堆`"); } return haveMove3; } /** 取得相同數字不同顏色的撲克牌編碼 * @param {Number} pokerValue 對應撲克牌的編號 * @returns {Array} [number1, number2] */ function getDifferentColorPokerValues(pokerValue) { const red = (Math.floor(pokerValue / 13) % 3) == 0 const number = pokerValue % 13; return [number + (red ? 13 : 0), number + (red ? 26 : 39)]; } 然後在DragDemo.vue中使用函數checkValidMove(cardStacks)將結果【是否有可移動的牌(推測)】儲存在計算ref變數。 ...

October 8, 2023 · 2 min · 宗嘉

Day28 評估是否無牌可走,建議棄權的提示(嘗試失敗)

前言 因為在玩紙牌接龍的過程中可能會被玩到進入無解的情況,也就是再怎麼移動撲克牌都無法完成,所以如何在遊玩中告知玩家這件事情蠻重要的,避免不必要的嘗試。 思考如何實現 老實說這篇文章可能是對我來說最難寫的,雖然身為開發者可以知道7牌堆全部的蓋牌底下是什麼花色數字, 但如何知道場上已經無解又是一回事。 基本上不考慮暴力的窮舉法,其實查到的結果大致可以推導無解的情況,不論是先天或是後天操作導致的無解都是因為疊在蓋牌上方的牌無法移動導致蓋牌無法打開,所以昨天自動結算才可以依照已經沒有蓋牌的情況下當作全部完成。 所以理論上7牌堆如果出現壓住隱藏牌的那一張未來完全無法移動,那就代表死局。 決定判斷死局的演算法(有誤) 雖然看了很多篇文章也理解死局的構成就是壓在蓋牌上的牌無法移動, 但沒有相對簡單的判斷的方法… 在邊玩接龍邊思考死局判斷的過程中,想到一個判斷方法在此命名為異色大一若隱即死局。 名稱聽起來很中二,檢查方式是7牌堆共7行,每一直行最上面壓住隱牌的第一張牌視為檢查點(Ex: 梅花6), 檢查隱藏牌中是否有兩個花色與檢查點不同且大一號的數字(Ex: 紅心7、方塊7),若有則視為此局已無解(死局),若無則繼續檢查其他的檢查點。 範例圖: 黑色框為隱藏牌、紫色為檢查點、橘色為相對檢查點大一號的異色牌 小結 實作後發現此演算法有錯誤會出現判斷活局/死局相反的情況, 但還是把程式碼推上去,明天繼續研究是否有其他方法解🫡。 程式碼: https://github.com/kabuto412rock/ithelp-pokergame/tree/day28 參考文件 知乎-深度剖析微软《纸牌》玩法 Quora-Does the solitaire game always have a solution?

October 7, 2023 · 1 min · 宗嘉

Day27 當牌全開時自動結算

前言 如標題所言,今天要做的就是補上『當牌全開時視為已完成遊戲』自動結算遊戲時間、分數, 有實際在現實世界中用撲克牌玩過接龍的應該知道其實最麻煩的就是收牌結尾,因為最後只是慢慢把所有牌都移到結算牌堆。 事先說明: 此篇不會實作自動移牌的動畫效果。 開發前思考 判斷牌全開不難! 不需要判斷全部牌是否已打開, 只需要判斷7牌堆每堆最上面第一張牌是否已打開,若有蓋著那就不能結算, 若每堆第一張都沒蓋著就可以開始結算。 結算分數時只需要考慮補上發牌區、移牌區有幾張牌,再用加分的方式計算最後的總分。 實作過程 修改遊戲結算時機 原本函數checkSolitaireGameDone(cardStacks)是判斷結算牌堆集滿13張牌則當作遊戲結束,但思考過後結算牌堆都集滿13張等同於七牌區的牌全空一樣會回應true,因此放心調整為檢查七牌堆的最上方開牌的狀態當作遊戲結束的依據。 // poker-helper.js /** * 檢查紙牌接龍是否完成 * @param {CardStacks} cardStacks */ function checkSolitaireGameDone(cardStacks) { // 檢查每組牌堆第一張牌覆蓋著,就代表遊戲還沒結束 const isDone = SEVEN_STACKS.reduce((prev, stackName) => { const stack = cardStacks[stackName]; if (stack.length > 0 && (!stack[0].isOpen)) { return false; } return prev; }, true); return isDone; } 計算剩餘牌數&結算分數 新增函數getRemainCardCount(cardStacks)計算並返回發牌區和七牌堆的剩餘牌數 // poker-helper.js /** * 計算剩餘在發牌區和7牌堆的張數 * @param {CardStacks} cardStacks * @returns {Object} {dealer: number, seven: number} */ function getRemainCardCount(cardStacks) { let dealerStacksCount = cardStacks['dealerStacks'].length; let sevenCount = 0; SEVEN_STACKS.forEach((name) => { sevenCount += cardStacks[name].length; }); return { dealer: dealerStacksCount, seven: sevenCount, }; } 在接龍頁面使用computed宣告一個變數remainCardCounts,負責返回剩餘的牌數。 ...

October 6, 2023 · 1 min · 宗嘉