解析如何透過結果集架構實現高效、可擴展的老虎機遊戲開發流程
本文介紹結果集架構的設計理念與實作方式,說明如何透過模組化的 Component 架構將老虎機遊戲拆解為可重用的功能模組,包含遊戲狀態機、Spin 流程管理、Feature 觸發與結算機制,實現快速開發與高品質交付。
在老虎機遊戲開發中,結果集 是一種預生成種子記錄池(Pre-generated Seed Record Pool)架構。 它的核心理念是將遊戲的隨機結果預先計算並打包成可重播的記錄,用於特定場景下的結果分發, 同時保持與自然機率完全一致的數學期望值。
結果集 並非取代即時 RNG,而是在需要精確控制 RTP 分佈的場景下, 提供一個經過數學驗證的結果集合,確保每組結果的長期回報率嚴格收斂於目標值。
即時計算的做法是:每次 Spin 抽一次亂數,依權重表決定停輪位置,再現場跑一次完整的連線判定。 這個流程本身沒有問題,卻有三個難以迴避的限制。
換個角度說,預生成把老虎機的隨機性拆成兩層: 結果本身的隨機性在離線階段就已經由自然機率模型決定, 線上階段只剩「抽哪一筆」的隨機性。 數學模型的正確性因此可以在離線階段被完整驗證與封存,不再受線上程式碼變更影響。
結果集 採用多層級的樹狀結構設計,從上到下逐級篩選:
最頂層是一個以識別碼為索引的容器,管理多個結果集實例。 每個結果集代表一組特定的結果記錄,對應一個目標 RTP 區間。
分層的意義在於把「選哪一組」與「選哪一筆」拆開。 最上層決定的是數學特性(要多高的期望值、要什麼樣的波動形狀), 最下層決定的則是具體畫面。這種切分讓 RTP 調整不需要碰到任何單局資料, 只要換一組索引即可,也讓同一批底層記錄能被多個不同 RTP 版本共用。
每個 結果集 包含以下核心屬性:
分組設計的目的是將高賠率與低賠率的記錄分開管理,透過組間的權重分配來微調整體 RTP。
這個設計解決了一個很實際的問題:如果整池記錄混在一起均勻抽取, 那麼這一池的 RTP 就是固定的,想改就得重新產生整池資料。 但只要按賠率把記錄分成兩組,調整組間權重就能在一定範圍內連續調節期望值: 高倍組權重上調,整體 RTP 隨之上升,而底層資料完全不必重跑。 這等於用一個一維參數,換來了一整段 RTP 的覆蓋範圍。
分組數量本身也是取捨。組數越多,可調節的粒度越細、對波動率的控制越精準, 但每一組都必須維持足夠的樣本量,否則組內抽樣本身就會失去代表性。 實務上兩到三組通常已經足以覆蓋常見需求。
最底層是一筆完整的 Spin 結果記錄, 包含轉輪停止位置、中獎組合、倍數等所有重播所需的資訊。 每筆記錄使用自定義的 TLV(Type-Length-Value) 二進位編碼格式壓縮儲存。
為了高效儲存大量的 Spin 結果記錄,結果集 採用緊湊的 TLV 二進位編碼格式:
[numerator u32][denominator u32][segment count u8][types u8×N][lengths u32×N][data...]
這個格式的設計重點在於:
為什麼不直接用通用序列化格式?主要是因為結果記錄的數量級。 當一款遊戲需要數百萬筆記錄、而同時上線的遊戲又有數十款時, 單筆多出幾十位元組的差異,累積起來就是可觀的體積差距, 並直接影響能否把整份資料常駐在記憶體中。
用賠率當作前置欄位,是這個格式最關鍵的一個決定。 篩選階段只需要知道倍數,若必須先把整筆記錄解出來才能判斷倍數, 等於為了一個數字付出整筆解碼的成本。把它放在固定偏移量的開頭, 篩選就退化成一次指標運算。用分子分母而非浮點數, 則是為了確保賠率比較與加總完全沒有精度誤差。 RTP 是要拿去做稽核的數字,不能容忍浮點累積誤差。
欄寬對照表則體現「按需分配位元組」的思路。 一款 5 軸、每軸權重序列長度不超過 255 的遊戲,停輪位置一個位元組就夠; 換成序列長度上千的遊戲則需要兩個位元組。若統一採用最大寬度, 大多數遊戲都要為用不到的空間付費。讓每款遊戲宣告自己的寬度表, 等於在不改動編碼框架的前提下取得接近最佳的壓縮率。
當然,這種設計也有代價:格式是自訂的,不能直接用通用工具檢視, 因此必須額外提供除錯用的可讀化輸出,並為格式本身建立版本欄位, 以免日後擴充段落型別時無法向後相容既有資料。
當系統需要從 結果集 提取一筆結果時,會經過以下多階段選取流程:
根據目標 RTP 值,定位到最接近的 RTP 錨點(Anchor)。 系統支援最多 3 個錨點,當目標 RTP 落在兩個錨點之間時, 使用線性插值(Linear Interpolation)進行機率性選擇:
P(選擇 AnchorA) = (AnchorB.RTP - targetRTP) / (AnchorB.RTP - AnchorA.RTP)
這確保了在兩個離散 RTP 檔位之間,能夠產生任意精度的目標 RTP。
這裡值得說明的是為什麼用「機率性選擇」而不是「混合結果」。 若試圖把兩個錨點的記錄依比例混在一起抽,得到的分佈形狀會是兩者的疊加, 波動特性可能落在兩者之外。改成每一局整體性地決定要用哪一組錨點, 期望值一樣是線性插值的結果,但每一局的結果都完整來自某個經過驗證的分佈, 數學性質因此更乾淨、也更容易對外說明。
在選定的錨點下,透過加權隨機從多個候選結果組中選取一個。 每一組帶有一個權重值,決定其被選中的機率。
在選定的結果組內,再透過權重在 2 個子分組之間選取。 分組的拆分通常依賠率範圍劃分,例如「低倍數結果組」與「高倍數結果組」。
在選定的分組內,以均勻分佈隨機抽取一筆結果記錄, 解碼後還原為遊戲的種子結構,供遊戲引擎重播。
最底層維持均勻抽取是有意的設計。所有的數學調節都已經在上面三層完成, 最後一層若再引入權重,會讓整體期望值的推導變得難以驗證。 保持均勻的另一個好處是:只要記錄池本身的統計特性被驗證過, 抽樣結果的分佈就必然等同於該池的分佈,不需要額外證明。
要讓四層選取都維持常數時間,索引設計是關鍵。 常見做法是為每一層預先建好累積權重陣列,抽樣時以二分搜尋定位, 使單層成本為對數級;而最底層由於是均勻抽取,直接以偏移量定址即可。 由於記錄採定長或段落長度前置的編碼,第 k 筆記錄的位置可以直接算出來, 不必逐筆掃描,這也是編碼格式與索引策略互相配合的具體例子。
從 結果集 取出的種子可以透過確定性重播(Deterministic Replay)還原完整的遊戲結果。 以轉輪式老虎機為例,一個整數種子透過混合基數分解(Mixed-Radix Decomposition) 拆解為每條 Reel 的停止位置:
reel[i].stop = seed % len(reel[i]); seed = seed / len(reel[i])
這個機制確保同一個種子值永遠產生相同的遊戲結果,方便進行結果驗證與審計。
混合基數分解的精妙之處,在於它建立了「整數」與「盤面」之間的雙射關係: 每一個合法的種子值恰好對應一個盤面,每一個盤面也恰好對應一個種子值,沒有重複也沒有遺漏。 這代表儲存一個整數,就等同於儲存了完整盤面, 同時也保證了「均勻抽取種子」等價於「均勻抽取盤面」,機率推導因此完全成立。
對稽核而言,確定性帶來三個具體價值:
需要注意的是,確定性只有在整條計算路徑都沒有隱含狀態時才成立。 任何依賴當前時間、未初始化記憶體、或雜湊表走訪順序的邏輯, 都會讓同一個種子產生不同結果。維持確定性因此是一項持續性的紀律, 而不是一次性的架構決定。
同一款遊戲往往需要提供數個 RTP 版本以符合不同市場的規範要求。 傳統做法是為每個版本各做一套權重表,代價是每一套都要重新跑完整的數學驗證, 而且版本之間的差異散落在多份設定裡,維護成本隨版本數線性上升。
結果集架構提供了另一條路徑:底層記錄只產生一次, 不同 RTP 版本只是同一批記錄上的不同索引與權重配置。 由於每一筆記錄的賠率已經在編碼時前置存放, 要組出一個目標 RTP 的版本,本質上是一個帶約束的權重配置問題: 在滿足期望值等於目標值的前提下,選擇一組讓波動特性最接近設計意圖的權重。
這樣做的直接好處是,各版本之間的遊戲手感保持一致。 因為所有版本抽的是同一批盤面,玩家看到的畫面組合、動畫節奏、特殊功能觸發樣態都相同, 差別只在各類結果出現的相對頻率。相較之下,重做權重表的做法很容易讓不同版本在體感上產生難以解釋的差異。
結果集 中的每一筆記錄都來自自然機率模型的真實 Spin 結果,經過萃取(Extract)→ 編碼(Encode)→ 儲存。 它不改變遊戲的數學模型,只是將「即時計算」轉換為「預計算查表」。 這意味著 結果集 的 RTP、波動率、Hit Rate 等指標都嚴格等同於原始自然機率模型。
這一點必須說得更精確一些:單一結果池的統計特性等同於原模型, 這是由生成過程保證的:記錄是從自然機率模型中依其原生分佈抽樣而來, 池夠大時,池內的賠率分佈就是原模型分佈的一個高保真樣本。
而當多個池以權重組合起來時,整體期望值是各池期望值的加權平均, 這是設計上刻意保留的調節維度。換句話說,結果集架構不是在「改變機率」, 而是在已驗證的分佈之間做選擇。每一個被抽出的盤面, 都是自然機率模型原本就會產生、且機率完全符合模型的結果。
要維持這個等價性,生成階段有兩個必須守住的紀律:
這兩點守住了,結果集才真正只是自然機率的一種儲存與分發形式, 而不是另一套獨立的數學模型。