把柏青哥寫成一台狀態機——以及一個關於「真實」的意外

2026年6月19日 · wemee

遊戲開發 狀態機 機率 遊戲設計 柏青哥

最近寫了一個小東西:用程式模擬封入式柏青哥。所謂封入式,是現在日本的主流機型,機台內的鋼珠在內部循環、不噴到外面,珠數改用電子計數。既然如此,我乾脆連內部那些珠也省略,只保留它的「機率骨架」,用純文字加一點 SVG 把畫面示意出來。沒有華麗演出,重點是把這台機器「怎麼運作」說清楚。成品就放在站上的 /game/pachinko,可以邊讀邊開著玩。

寫的過程裡遇到一件值得記下來的事,後面會講。先從它的結構說起。

柏青哥其實是一台狀態機

把柏青哥剝掉聲光,剩下的是一組很乾淨的狀態流轉:

  • 通常時:平常狀態。發射的珠大多數落空、慢慢消耗,只有少數進入「啟動口」。每進一次啟動口,就觸發一次大當り抽選。
  • 大當り:抽中後,盤面下方的「大入賞口」開啟,珠容易進去、給較多賞球。它由數個「回合」組成,跑完就結束。
  • 連莊區(RUSH/ST):大當り之後常會進入一段高機率狀態,在限定轉數內若再中就繼續連莊。這裡才是出玉的來源。

整台機器就是在「通常時 → 大當り → 連莊區 → (連莊或掉回通常時)」之間繞。寫成狀態機之後,三種主流機台的差別其實只落在一個點上——大當り是怎麼被判定的

  • デジパチ(1種):啟動口觸發數字盤抽選,三個數字對齊就中。
  • 羽根物(2種):沒有數字盤。珠進始動口讓「羽根」張開,趁機把珠送進中央役物,珠物理上滾進 V 入賞口才算中。
  • 1種2種混合:通常時是 1 種的數字抽選;進連莊區後改用 2 種的機制——電チュー抽出「小當り」,再讓珠通過 V,才確定連莊。

這個觀察直接決定了程式架構:共用一個核心(狀態、持珠帳本、發射迴圈),各機台只實作自己的判定邏輯。新增一台機器不需要動到核心,也不需要動到畫面。

一個關於「真實」的意外

我想讓機率盡量貼近真機,於是對標了一台現行販售的甘デジ(シンフォギア3 Light),把它公開的規格抄進去:通常時大當り 1/99.9、RUSH 突入率約 45%、繼續率約 92%、賞球 1/5/7、初當り幾乎都是最小的 3R。

每個數字都來自真機。然後我跑了模擬,結果是——玩家長期穩賺,回收率破 100%。

這顯然不對。真實的柏青哥,長期回收率落在 85~90%,玩家是慢慢虧的。個別機率明明都「真實」,組合起來卻完全失真。問題出在它們是相乘的關係:

整體回收率 ≈ 突入率 × 繼續率 × 每次出玉

三個槓桿各自看都合理,乘在一起卻把期望值頂到正的。其中最致命的是繼續率:92% 的繼續率代表平均連莊約十二、三次,一條連莊鏈動輒上萬顆珠,遠遠蓋過打進初當り的成本。

這裡還牽到一個業界常識:公称繼續率是偏樂觀的數字。把它直接當成模型參數,威力會過強。要同時滿足「公称 92%」「真實回收率」「合理的回轉率」,在數學上會打架,因為最小回合(3R)的出玉本身有下限,壓不下去。

最後的取捨是:保留招牌的「突入率 45%」,但把繼續率改用實質值(約 80%、平均連莊四次左右),讓整體回收率落回真機水準。這比硬湊公称數字更貼近實際坐在機台前的體感。

換句話說,玩的時候那種「一直連、一直爽」的感覺,並不是錯覺,而是被設計出來的——爽在當下,數學在長期把它收回去。

怎麼確認自己沒有算錯

EV 用手算容易出錯,尤其牽涉到連莊鏈這種會自我延續的結構。我的做法是不算,直接讓程式跑幾百萬發、統計實際回收率。核心引擎是純邏輯、不碰畫面,所以可以直接在 Node 裡空跑:

import { machines } from './src/lib/games/pachinko';
import { createCore } from './src/lib/games/pachinko/core';

for (const m of machines) {
  const core = createCore(m);
  for (let i = 0; i < 6_000_000; i++) {
    if (core.state.balls <= 0) core.insert();
    core.shoot();
  }
  const s = core.state;
  const rtp = 100 * (1 + (s.balls - s.invested) / s.totalLaunched);
  console.log(m.spec.name.padEnd(26), '回收率', rtp.toFixed(1) + '%');
}

回收率就定義成「每發珠的淨增量」:1 + (持珠 − 投資) / 累計發射。改一個機率、重跑一次,馬上看到新的回收率,調參就變成一件踏實的事,而不是憑感覺。三台最後校準的結果:

機台回收率
デジパチ(1種 ST)約 89%
羽根物(2種)約 91%
1種2種混合約 88%

各台調的槓桿不同:デジパチ動回轉率與實質繼續率,羽根物動 V 入賞率與每回合續行率,混合動 ST 轉數、回合分布與 V 通過率。但驗證的方法都一樣——跑、看數字、再調。

架構上的一點筆記

值得一提的是「核心 vs 機台」的分界,因為它正好對應到前面那個觀察:機台之間唯一真正不同的,是發射一顆珠時的判定邏輯。所以核心只管共通的事(持珠加減、發射迴圈、事件廣播),每台機器是一個小模組,提供自己的 shoot() 邏輯,以及要怎麼在盤面上呈現、要顯示哪些數據、導覽文字寫什麼。畫面層完全不認識任何一台機器的細節,它只是把機台描述出來的東西畫出來。

結果就是:加一台新機器,等於加一個檔案、註冊一下,核心和畫面一行都不用改。這種「差異被收斂到一個點」的感覺,是寫完之後回頭看最滿意的地方。

還沒做的事

它離真機還有距離,誠實列一下省略掉的部分:遊タイム(天井)、確変與通常大當り的內部振り分け、ST 結束後的時短引き戻し、以及非等価交換造成的房屋邊際。回轉率目前固定成一個「中等偏緊」的台,但真機的回收率還會被釘(回轉率)和交換率左右——這部分是房屋端的設定,不在機台機率裡。

這些都還能往下做。不過光是把骨架立起來、把那個「個別真實、整體失真」的坑填平,就已經是一段挺有意思的過程了。

留言 0

留言載入中…