labag 3.0.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
+ # LaBaG (Slot Machine Engine)
1
2
 
2
- # LaBaG
3
-
4
- LaBaG 是一款用 TypeScript 撰寫的高彈性拉霸機遊戲引擎,支援多種模式、事件監聽、組合得分,適合用於遊戲開發、教學或自訂玩法。
3
+ LaBaG 是一款用 TypeScript 撰寫的高彈性拉霸機 (Slot Machine) 遊戲引擎,支援多種模式、自訂圖案權重與中獎組合,適合用於遊戲開發、教學或自訂玩法。
5
4
 
6
5
  [![npm version](https://img.shields.io/npm/v/labag.svg)](https://www.npmjs.com/package/labag)
7
6
 
@@ -9,112 +8,120 @@ LaBaG 是一款用 TypeScript 撰寫的高彈性拉霸機遊戲引擎,支援
9
8
 
10
9
  ## 特色
11
10
 
12
- - 支援多種拉霸模式(可自訂/擴充)
13
- - 圖案與分數組合彈性設定
14
- - 事件監聽機制,方便擴充互動
15
- - TypeScript 強型別,易於二次開發
11
+ - 🎰 **高度客製化**:支援自訂圖案 (Pattern) 與中獎規則 (Payout)。
12
+ - ⚖️ **權重控制**:可設定每個圖案出現的權重 (Weight),控制機率。
13
+ - 💰 **彈性獎勵計算**:自動計算中獎組合與獎勵金額。
14
+ - 🔧 **TypeScript 強型別**:提供完整的型別定義,開發更安心。
16
15
 
17
16
  ---
18
17
 
19
18
  ## 安裝
20
19
 
20
+ 使用 npm 安裝:
21
+
21
22
  ```bash
22
23
  npm install labag
23
24
  ```
24
25
 
25
26
  ---
26
27
 
27
- ## 基本用法
28
+ ## 快速開始
29
+
30
+ ### 1. 引入模組
28
31
 
29
32
  ```typescript
30
- import { labag, modeList, LaBaG } from "labag";
31
-
32
- labag.init();
33
- while (labag.isRunning()) {
34
- labag.play();
35
- // 可在此取得 labag.patterns, labag.score 等資訊
36
- }
37
- console.log(labag.score);
33
+ import { LaBaG, Pattern, Payout } from "labag";
38
34
  ```
39
35
 
40
- ### 事件監聽
36
+ ### 2. 定義圖案 (Patterns)
37
+
38
+ 設定每個圖案的 ID、權重 (出現機率) 與圖片路徑。
41
39
 
42
40
  ```typescript
43
- labag.addEventListener("roundEnd", (game) => {
44
- console.log(game.patterns, game.marginScore, game.score);
45
- });
41
+ const patterns: Pattern[] = [
42
+ { id: 1, weight: 10, image: "cherry.png" }, // 稀有
43
+ { id: 2, weight: 20, image: "bell.png" },
44
+ { id: 3, weight: 50, image: "bar.png" }, // 常見
45
+ ];
46
46
  ```
47
47
 
48
- ---
49
-
50
- ## API 介紹
48
+ ### 3. 定義中獎規則 (Payouts)
51
49
 
52
- ### 主要類別
50
+ 設定當特定圖案出現特定次數時的獎勵。
53
51
 
54
- - `LaBaG`:拉霸機主體,管理分數、模式、事件等
55
- - `Mode`:遊戲模式,決定圖案機率與特殊行為
56
- - `Pattern`:圖案及分數設定
52
+ ```typescript
53
+ const payouts: Payout[] = [
54
+ { id: 1, pattern_id: 1, match_count: 3, reward: 1000 }, // 3個櫻桃 -> 1000分
55
+ { id: 2, pattern_id: 1, match_count: 2, reward: 50 }, // 2個櫻桃 -> 50分
56
+ { id: 3, pattern_id: 2, match_count: 3, reward: 500 }, // 3個鈴鐺 -> 500分
57
+ ];
58
+ ```
57
59
 
58
- ### 主要方法
60
+ ### 4. 建立實例並開始遊玩
59
61
 
60
- - `labag.init()`:初始化遊戲
61
- - `labag.play()`:執行一次拉霸
62
- - `labag.isRunning()`:判斷是否還可繼續遊玩
63
- - `labag.addMode(mode)`:新增模式
64
- - `labag.addEventListener(event, fn)`:註冊事件
62
+ ```typescript
63
+ const game = new LaBaG(patterns, payouts);
65
64
 
66
- ### 主要屬性
65
+ // 進行一次拉霸
66
+ const result = game.spin();
67
67
 
68
- - `labag.score`:目前總分
69
- - `labag.patterns`:本輪轉出的圖案陣列
70
- - `labag.modes`:目前所有模式
68
+ console.log("轉出圖案:", result.reels);
69
+ console.log("獲得獎勵:", result.reward);
70
+ ```
71
71
 
72
72
  ---
73
73
 
74
- ## 內建模式
74
+ ## API 參考
75
75
 
76
- | 模式名稱 | 說明 |
77
- |------------|--------------------------|
78
- | normal | 標準機率分布 |
79
- | greenwei | 特殊加倍與觸發條件 |
80
- | pikachu | 特殊復活與替換 |
81
- | superhhh | 高風險高報酬 |
76
+ ### `LaBaG` 類別
82
77
 
83
- 可自訂/擴充模式,詳見 `src/modes/` 範例。
78
+ #### `constructor(patterns: Pattern[], payouts: Payout[])`
84
79
 
85
- ---
80
+ 初始化拉霸機引擎。
86
81
 
87
- ## 圖案與分數
82
+ - `patterns`: 圖案列表,包含權重設定。
83
+ - `payouts`: 中獎規則列表。
88
84
 
89
- | 圖案名稱 | 三連分數 | 雙連分數 | 單出分數 |
90
- |------------|----------|----------|----------|
91
- | gss | 800 | 400 | 180 |
92
- | hhh | 1500 | 800 | 300 |
93
- | hentai | 2500 | 1200 | 500 |
94
- | handson | 2900 | 1450 | 690 |
95
- | kachu | 12000 | 8000 | 1250 |
96
- | rrr | 20000 | 12000 | 2500 |
85
+ #### `spin()`
97
86
 
98
- ---
87
+ 進行一次拉霸,隨機產生結果並計算獎勵。
88
+
89
+ - **回傳值**: `{ reels: Pattern[], reward: number }`
90
+ - `reels`: 轉出的圖案陣列 (預設為 3 個)。
91
+ - `reward`: 計算出的總獎勵金額。
92
+
93
+ #### `randomPattern(): Pattern`
99
94
 
100
- ## 事件列表
95
+ 根據權重隨機抽取一個圖案。
101
96
 
102
- | 事件名稱 | 說明 |
103
- |----------------|------------------|
104
- | gameStart | 遊戲開始 |
105
- | roundStart | 每輪開始 |
106
- | rollSlots | 轉動產生圖案 |
107
- | calculateScore | 計算分數 |
108
- | roundEnd | 每輪結束 |
109
- | gameOver | 遊戲結束 |
97
+ #### `caculateReward(reels: Pattern[]): number`
98
+
99
+ 計算給定圖案組合的總獎勵。
110
100
 
111
101
  ---
112
102
 
113
- ## 進階用法
103
+ ## 型別定義
104
+
105
+ ### `Pattern`
114
106
 
115
- - 可自訂模式(繼承 `Mode` 並設定 eventListener)
116
- - 可自訂圖案與分數
117
- - 可串接前端 UI 或 CLI
107
+ ```typescript
108
+ type Pattern = {
109
+ id: string | number;
110
+ weight: number; // 出現權重,數字越大機率越高
111
+ image: string; // 圖片路徑或代碼
112
+ };
113
+ ```
114
+
115
+ ### `Payout`
116
+
117
+ ```typescript
118
+ type Payout = {
119
+ id: string | number;
120
+ pattern_id: Pattern["id"]; // 對應的圖案 ID
121
+ match_count: number; // 需要出現的次數 (例如 3 代表出現 3 次)
122
+ reward: number; // 獎勵金額
123
+ };
124
+ ```
118
125
 
119
126
  ---
120
127
 
package/dist/labag.d.ts CHANGED
@@ -4,10 +4,11 @@ export declare class LaBaG {
4
4
  reels: Pattern[];
5
5
  payouts: Payout[];
6
6
  constructor(patterns: Pattern[], payouts: Payout[]);
7
- spin(): {
7
+ spin(bet: number): {
8
8
  reels: Pattern[];
9
9
  reward: number;
10
+ multiplier: number;
10
11
  };
11
12
  randomPattern(): Pattern;
12
- caculateReward(reels: Pattern[]): number;
13
+ calculateMultiplier(reels: Pattern[]): number;
13
14
  }
package/dist/labag.js CHANGED
@@ -11,16 +11,18 @@ class LaBaG {
11
11
  this.reels = [patterns[0], patterns[1], patterns[2]];
12
12
  this.payouts = payouts;
13
13
  }
14
- spin() {
14
+ spin(bet) {
15
15
  this.reels = [
16
16
  this.randomPattern(),
17
17
  this.randomPattern(),
18
18
  this.randomPattern(),
19
19
  ];
20
- const reward = this.caculateReward(this.reels);
20
+ const multiplier = this.calculateMultiplier(this.reels);
21
+ const reward = bet * multiplier;
21
22
  return {
22
23
  reels: this.reels,
23
24
  reward,
25
+ multiplier,
24
26
  };
25
27
  }
26
28
  randomPattern() {
@@ -35,18 +37,18 @@ class LaBaG {
35
37
  }
36
38
  throw new Error("No pattern found");
37
39
  }
38
- caculateReward(reels) {
40
+ calculateMultiplier(reels) {
39
41
  const patternCounts = {};
40
42
  for (const pattern of reels) {
41
43
  patternCounts[pattern.id] = (patternCounts[pattern.id] || 0) + 1;
42
44
  }
43
- let totalReward = 0;
45
+ let totalMultiplier = 0;
44
46
  for (const payout of this.payouts) {
45
47
  if (patternCounts[payout.pattern_id] === payout.match_count) {
46
- totalReward += payout.reward;
48
+ totalMultiplier += payout.multiplier;
47
49
  }
48
50
  }
49
- return totalReward;
51
+ return totalMultiplier;
50
52
  }
51
53
  }
52
54
  exports.LaBaG = LaBaG;
package/dist/test.js CHANGED
@@ -1,24 +1,172 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const labag_1 = require("./labag");
4
+ // --- 設定 (Configuration) ---
5
+ const BET_AMOUNT = 200;
6
+ const SIMULATION_COUNT = 1_000_000;
7
+ // --- 資料定義 (Data Definitions) ---
4
8
  const patterns = [
5
- { id: "A", weight: 50, image: "A.png" },
6
- { id: "B", weight: 30, image: "B.png" },
7
- { id: "C", weight: 20, image: "C.png" },
9
+ {
10
+ id: 1,
11
+ weight: 36,
12
+ image: "https://fanyu.vercel.app/api/album/item/1mDK1ewfLiV3fAB1HbjvwdSaDTdJdBGG3",
13
+ },
14
+ {
15
+ id: 2,
16
+ weight: 24,
17
+ image: "https://fanyu.vercel.app/api/album/item/1oB-uZhPPfjfTtG4CITnb3_E-Ops9JTA0",
18
+ },
19
+ {
20
+ id: 3,
21
+ weight: 17,
22
+ image: "https://fanyu.vercel.app/api/album/item/1bMJdRB8uerQZfGYINzBI9Vaw32bZljl2",
23
+ },
24
+ {
25
+ id: 4,
26
+ weight: 12,
27
+ image: "https://fanyu.vercel.app/api/album/item/1In8LF1wVfLXpPkp57a20zX84QgsAeLQx",
28
+ },
29
+ {
30
+ id: 5,
31
+ weight: 8,
32
+ image: "https://fanyu.vercel.app/api/album/item/1Zo_PjrXm-4TBrL2cLAeFkEl1el9kTR56",
33
+ },
34
+ {
35
+ id: 6,
36
+ weight: 3,
37
+ image: "https://fanyu.vercel.app/api/album/item/19NMnVgcb-9IsknNcfe9TpCyPBIcGnhQU",
38
+ },
8
39
  ];
9
40
  const payouts = [
10
- { id: "payout1", pattern_id: "A", match_count: 3, reward: 100 },
11
- { id: "payout2", pattern_id: "B", match_count: 3, reward: 50 },
12
- { id: "payout3", pattern_id: "C", match_count: 3, reward: 20 },
13
- { id: "payout4", pattern_id: "A", match_count: 2, reward: 10 },
14
- { id: "payout5", pattern_id: "B", match_count: 2, reward: 5 },
15
- { id: "payout6", pattern_id: "C", match_count: 2, reward: 2 },
41
+ { id: 1, match_count: 2, pattern_id: 1, reward: 56 },
42
+ { id: 2, match_count: 3, pattern_id: 1, reward: 242 },
43
+ { id: 3, match_count: 2, pattern_id: 2, reward: 119 },
44
+ { id: 4, match_count: 3, pattern_id: 2, reward: 578 },
45
+ { id: 5, match_count: 2, pattern_id: 3, reward: 266 },
46
+ { id: 6, match_count: 3, pattern_id: 3, reward: 1345 },
47
+ { id: 7, match_count: 2, pattern_id: 4, reward: 571 },
48
+ { id: 8, match_count: 3, pattern_id: 4, reward: 3503 },
49
+ { id: 9, match_count: 2, pattern_id: 5, reward: 2136 },
50
+ { id: 10, match_count: 3, pattern_id: 5, reward: 11727 },
51
+ { id: 11, match_count: 2, pattern_id: 6, reward: 18708 },
52
+ { id: 12, match_count: 3, pattern_id: 6, reward: 182200 },
16
53
  ];
17
- const labag = new labag_1.LaBaG(patterns, payouts);
18
- let totalReward = 0;
19
- for (let i = 0; i < 10; i++) {
20
- const result = labag.spin();
21
- console.log(`Spin ${i + 1}:`, result);
22
- totalReward += result.reward;
54
+ // --- 輔助函式 (Helpers) ---
55
+ /**
56
+ 格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
57
+ */
58
+ const formatCurrency = (val) => new Intl.NumberFormat("en-US", {
59
+ style: "decimal",
60
+ minimumFractionDigits: 0,
61
+ }).format(val);
62
+ /**
63
+ * 格式化百分比
64
+ */
65
+ const formatPct = (val) => `${val.toFixed(4)}%`;
66
+ /**
67
+ * 根據圖案和賠率計算理論統計數據。
68
+ * 假設滾輪獨立且賠率條件互斥(例如每次旋轉只有一種賠彩)。
69
+ */
70
+ function calculateTheoreticalStats(patterns, payouts, bet) {
71
+ const totalWeight = patterns.reduce((sum, p) => sum + p.weight, 0);
72
+ let ev = 0;
73
+ let varianceSum = 0; // E[X^2]
74
+ const hitProbabilities = {};
75
+ for (const payout of payouts) {
76
+ const pattern = patterns.find((p) => p.id === payout.pattern_id);
77
+ if (!pattern)
78
+ continue;
79
+ const p = pattern.weight / totalWeight;
80
+ // 二項分布概率計算特定圖案數量 (假設 3 個滾輪)
81
+ // Binomial probability for specific pattern count (assuming 3 reels)
82
+ // k=3: p^3
83
+ // k=2: C(3,2) * p^2 * (1-p) = 3 * p^2 * (1-p)
84
+ const prob = payout.match_count === 3 ? Math.pow(p, 3) : 3 * Math.pow(p, 2) * (1 - p);
85
+ ev += prob * payout.reward;
86
+ // 假設賠率互斥: E[X^2] = sum(prob * reward^2)
87
+ // Assuming disjoint payouts: E[X^2] = sum(prob * reward^2)
88
+ varianceSum += prob * Math.pow(payout.reward, 2);
89
+ hitProbabilities[payout.reward] =
90
+ (hitProbabilities[payout.reward] || 0) + prob;
91
+ }
92
+ const variance = varianceSum - Math.pow(ev, 2);
93
+ const stdDev = Math.sqrt(variance);
94
+ const rtp = (ev / bet) * 100;
95
+ return { ev, rtp, stdDev, hitProbabilities };
23
96
  }
24
- console.log("Total Reward:", totalReward);
97
+ // --- 模擬邏輯 (Simulation Logic) ---
98
+ function runSimulation(game, count) {
99
+ let totalReward = 0;
100
+ let winCount = 0;
101
+ let maxWin = 0;
102
+ const hitFrequency = {};
103
+ for (let i = 0; i < count; i++) {
104
+ const result = game.spin();
105
+ totalReward += result.reward;
106
+ if (result.reward > 0) {
107
+ winCount++;
108
+ if (result.reward > maxWin)
109
+ maxWin = result.reward;
110
+ hitFrequency[result.reward] = (hitFrequency[result.reward] || 0) + 1;
111
+ }
112
+ }
113
+ return {
114
+ totalReward,
115
+ winCount,
116
+ maxWin,
117
+ hitFrequency,
118
+ };
119
+ }
120
+ // --- 主要執行區塊 (Main Execution) ---
121
+ // 1. 理論分析 (Theoretical Analysis)
122
+ console.log("正在計算理論數值...");
123
+ const theo = calculateTheoreticalStats(patterns, payouts, BET_AMOUNT);
124
+ console.log("==========================================");
125
+ console.log(" 理論分析 (Theoretical) ");
126
+ console.log("==========================================");
127
+ console.log(`理論期望值 (EV) : ${theo.ev.toFixed(2)}`);
128
+ console.log(`理論 RTP : ${theo.rtp.toFixed(2)}%`);
129
+ console.log(`標準差 (SD) : ${theo.stdDev.toFixed(2)}`);
130
+ console.log("------------------------------------------");
131
+ // 2. 模擬 (Simulation)
132
+ console.log(`\n正在執行模擬 (n=${formatCurrency(SIMULATION_COUNT)})...`);
133
+ const game = new labag_1.LaBaG(patterns, payouts);
134
+ const sim = runSimulation(game, SIMULATION_COUNT);
135
+ const simEV = sim.totalReward / SIMULATION_COUNT;
136
+ const simRTP = (simEV / BET_AMOUNT) * 100;
137
+ const simHitRate = (sim.winCount / SIMULATION_COUNT) * 100;
138
+ // RTP 的信賴區間 (95% CI): 平均值 +/- 1.96 * (標準差 / sqrt(N))
139
+ // 標準誤差 (Standard Error) = 標準差 / sqrt(模擬次數)
140
+ const standardError = theo.stdDev / Math.sqrt(SIMULATION_COUNT);
141
+ // 信賴區間的邊際誤差 (Margin of Error) = 1.96 * 標準誤差
142
+ const marginOfError = 1.96 * standardError;
143
+ // RTP 的誤差百分比 = (邊際誤差 / 投注金額) * 100
144
+ const rtpMargin = (marginOfError / BET_AMOUNT) * 100;
145
+ console.log("==========================================");
146
+ console.log(" 模擬結果 (Simulation) ");
147
+ console.log("==========================================");
148
+ console.log(`總投注 : ${formatCurrency(BET_AMOUNT * SIMULATION_COUNT)}`);
149
+ console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
150
+ console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
151
+ console.log(`模擬 RTP : ${simRTP.toFixed(2)}% (95% CI: ±${rtpMargin.toFixed(2)}%)`);
152
+ console.log(`命中率 : ${simHitRate.toFixed(2)}%`);
153
+ console.log(`最大單次獎金 : ${sim.maxWin}`);
154
+ console.log(`誤差 (Sim - Theo) : ${(simRTP - theo.rtp).toFixed(2)}%`);
155
+ console.log("\n==========================================");
156
+ console.log(" 獎金分布比較 (Distribution) ");
157
+ console.log("==========================================");
158
+ console.log(`| 獎金 | 理論機率 | 模擬頻率 | 模擬次數 |`);
159
+ console.log(`|--------|------------|------------|----------|`);
160
+ // 取得理論和模擬中的所有唯一獎金,以確保表格完整
161
+ // Get all unique rewards from both theoretical and simulation to ensure complete table
162
+ const allRewards = Array.from(new Set([
163
+ ...Object.keys(theo.hitProbabilities).map(Number),
164
+ ...Object.keys(sim.hitFrequency).map(Number),
165
+ ])).sort((a, b) => a - b);
166
+ for (const reward of allRewards) {
167
+ const theoProb = (theo.hitProbabilities[reward] || 0) * 100;
168
+ const simCount = sim.hitFrequency[reward] || 0;
169
+ const simProb = (simCount / SIMULATION_COUNT) * 100;
170
+ console.log(`| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`);
171
+ }
172
+ console.log("==========================================");
@@ -7,5 +7,5 @@ export type Payout = {
7
7
  id: string;
8
8
  pattern_id: Pattern["id"];
9
9
  match_count: number;
10
- reward: number;
10
+ multiplier: number;
11
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "labag",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/labag.ts CHANGED
@@ -11,16 +11,18 @@ export class LaBaG {
11
11
  this.reels = [patterns[0], patterns[1], patterns[2]];
12
12
  this.payouts = payouts;
13
13
  }
14
- spin() {
14
+ spin(bet: number): { reels: Pattern[]; reward: number; multiplier: number } {
15
15
  this.reels = [
16
16
  this.randomPattern(),
17
17
  this.randomPattern(),
18
18
  this.randomPattern(),
19
19
  ];
20
- const reward = this.caculateReward(this.reels);
20
+ const multiplier = this.calculateMultiplier(this.reels);
21
+ const reward = bet * multiplier;
21
22
  return {
22
23
  reels: this.reels,
23
24
  reward,
25
+ multiplier,
24
26
  };
25
27
  }
26
28
 
@@ -40,17 +42,18 @@ export class LaBaG {
40
42
  throw new Error("No pattern found");
41
43
  }
42
44
 
43
- caculateReward(reels: Pattern[]): number {
45
+ calculateMultiplier(reels: Pattern[]): number {
44
46
  const patternCounts: { [key: string]: number } = {};
45
47
  for (const pattern of reels) {
46
48
  patternCounts[pattern.id] = (patternCounts[pattern.id] || 0) + 1;
47
49
  }
48
- let totalReward = 0;
50
+
51
+ let totalMultiplier = 0;
49
52
  for (const payout of this.payouts) {
50
53
  if (patternCounts[payout.pattern_id] === payout.match_count) {
51
- totalReward += payout.reward;
54
+ totalMultiplier += payout.multiplier;
52
55
  }
53
56
  }
54
- return totalReward;
57
+ return totalMultiplier;
55
58
  }
56
59
  }
package/src/test.ts CHANGED
@@ -1,26 +1,217 @@
1
1
  import { LaBaG } from "./labag";
2
+ import { Pattern, Payout } from "./types";
2
3
 
3
- const patterns = [
4
- { id: "A", weight: 50 , image: "A.png"},
5
- { id: "B", weight: 30 , image: "B.png"},
6
- { id: "C", weight: 20 , image: "C.png"},
4
+ // --- 設定 (Configuration) ---
5
+ const BET_AMOUNT = 200;
6
+ const SIMULATION_COUNT = 1_000_000;
7
+
8
+ // --- 資料定義 (Data Definitions) ---
9
+ const patterns: Pattern[] = [
10
+ {
11
+ id: 1,
12
+ weight: 36,
13
+ image:
14
+ "https://fanyu.vercel.app/api/album/item/1mDK1ewfLiV3fAB1HbjvwdSaDTdJdBGG3",
15
+ },
16
+ {
17
+ id: 2,
18
+ weight: 24,
19
+ image:
20
+ "https://fanyu.vercel.app/api/album/item/1oB-uZhPPfjfTtG4CITnb3_E-Ops9JTA0",
21
+ },
22
+ {
23
+ id: 3,
24
+ weight: 17,
25
+ image:
26
+ "https://fanyu.vercel.app/api/album/item/1bMJdRB8uerQZfGYINzBI9Vaw32bZljl2",
27
+ },
28
+ {
29
+ id: 4,
30
+ weight: 12,
31
+ image:
32
+ "https://fanyu.vercel.app/api/album/item/1In8LF1wVfLXpPkp57a20zX84QgsAeLQx",
33
+ },
34
+ {
35
+ id: 5,
36
+ weight: 8,
37
+ image:
38
+ "https://fanyu.vercel.app/api/album/item/1Zo_PjrXm-4TBrL2cLAeFkEl1el9kTR56",
39
+ },
40
+ {
41
+ id: 6,
42
+ weight: 3,
43
+ image:
44
+ "https://fanyu.vercel.app/api/album/item/19NMnVgcb-9IsknNcfe9TpCyPBIcGnhQU",
45
+ },
7
46
  ];
8
47
 
9
- const payouts = [
10
- { id: "payout1", pattern_id: "A", match_count: 3, reward: 100 },
11
- { id: "payout2", pattern_id: "B", match_count: 3, reward: 50 },
12
- { id: "payout3", pattern_id: "C", match_count: 3, reward: 20 },
13
- { id: "payout4", pattern_id: "A", match_count: 2, reward: 10 },
14
- { id: "payout5", pattern_id: "B", match_count: 2, reward: 5 },
15
- { id: "payout6", pattern_id: "C", match_count: 2, reward: 2 },
48
+ const payouts: Payout[] = [
49
+ { id: 1, match_count: 2, pattern_id: 1, reward: 56 },
50
+ { id: 2, match_count: 3, pattern_id: 1, reward: 242 },
51
+ { id: 3, match_count: 2, pattern_id: 2, reward: 119 },
52
+ { id: 4, match_count: 3, pattern_id: 2, reward: 578 },
53
+ { id: 5, match_count: 2, pattern_id: 3, reward: 266 },
54
+ { id: 6, match_count: 3, pattern_id: 3, reward: 1345 },
55
+ { id: 7, match_count: 2, pattern_id: 4, reward: 571 },
56
+ { id: 8, match_count: 3, pattern_id: 4, reward: 3503 },
57
+ { id: 9, match_count: 2, pattern_id: 5, reward: 2136 },
58
+ { id: 10, match_count: 3, pattern_id: 5, reward: 11727 },
59
+ { id: 11, match_count: 2, pattern_id: 6, reward: 18708 },
60
+ { id: 12, match_count: 3, pattern_id: 6, reward: 182200 },
16
61
  ];
17
62
 
18
- const labag = new LaBaG(patterns, payouts);
63
+ // --- 輔助函式 (Helpers) ---
64
+
65
+ /**
66
+ 格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
67
+ */
68
+ const formatCurrency = (val: number) =>
69
+ new Intl.NumberFormat("en-US", {
70
+ style: "decimal",
71
+ minimumFractionDigits: 0,
72
+ }).format(val);
73
+
74
+ /**
75
+ * 格式化百分比
76
+ */
77
+ const formatPct = (val: number) => `${val.toFixed(4)}%`;
78
+
79
+ /**
80
+ * 根據圖案和賠率計算理論統計數據。
81
+ * 假設滾輪獨立且賠率條件互斥(例如每次旋轉只有一種賠彩)。
82
+ */
83
+ function calculateTheoreticalStats(
84
+ patterns: Pattern[],
85
+ payouts: Payout[],
86
+ bet: number,
87
+ ) {
88
+ const totalWeight = patterns.reduce((sum, p) => sum + p.weight, 0);
89
+ let ev = 0;
90
+ let varianceSum = 0; // E[X^2]
91
+ const hitProbabilities: Record<number, number> = {};
92
+
93
+ for (const payout of payouts) {
94
+ const pattern = patterns.find((p) => p.id === payout.pattern_id);
95
+ if (!pattern) continue;
96
+
97
+ const p = pattern.weight / totalWeight;
98
+ // 二項分布概率計算特定圖案數量 (假設 3 個滾輪)
99
+ // Binomial probability for specific pattern count (assuming 3 reels)
100
+ // k=3: p^3
101
+ // k=2: C(3,2) * p^2 * (1-p) = 3 * p^2 * (1-p)
102
+ const prob =
103
+ payout.match_count === 3 ? Math.pow(p, 3) : 3 * Math.pow(p, 2) * (1 - p);
104
+
105
+ ev += prob * payout.reward;
106
+ // 假設賠率互斥: E[X^2] = sum(prob * reward^2)
107
+ // Assuming disjoint payouts: E[X^2] = sum(prob * reward^2)
108
+ varianceSum += prob * Math.pow(payout.reward, 2);
109
+
110
+ hitProbabilities[payout.reward] =
111
+ (hitProbabilities[payout.reward] || 0) + prob;
112
+ }
113
+
114
+ const variance = varianceSum - Math.pow(ev, 2);
115
+ const stdDev = Math.sqrt(variance);
116
+ const rtp = (ev / bet) * 100;
117
+
118
+ return { ev, rtp, stdDev, hitProbabilities };
119
+ }
120
+
121
+ // --- 模擬邏輯 (Simulation Logic) ---
122
+ function runSimulation(game: LaBaG, count: number) {
123
+ let totalReward = 0;
124
+ let winCount = 0;
125
+ let maxWin = 0;
126
+ const hitFrequency: Record<number, number> = {};
127
+
128
+ for (let i = 0; i < count; i++) {
129
+ const result = game.spin();
130
+ totalReward += result.reward;
131
+
132
+ if (result.reward > 0) {
133
+ winCount++;
134
+ if (result.reward > maxWin) maxWin = result.reward;
135
+ hitFrequency[result.reward] = (hitFrequency[result.reward] || 0) + 1;
136
+ }
137
+ }
138
+
139
+ return {
140
+ totalReward,
141
+ winCount,
142
+ maxWin,
143
+ hitFrequency,
144
+ };
145
+ }
146
+
147
+ // --- 主要執行區塊 (Main Execution) ---
148
+
149
+ // 1. 理論分析 (Theoretical Analysis)
150
+ console.log("正在計算理論數值...");
151
+ const theo = calculateTheoreticalStats(patterns, payouts, BET_AMOUNT);
152
+
153
+ console.log("==========================================");
154
+ console.log(" 理論分析 (Theoretical) ");
155
+ console.log("==========================================");
156
+ console.log(`理論期望值 (EV) : ${theo.ev.toFixed(2)}`);
157
+ console.log(`理論 RTP : ${theo.rtp.toFixed(2)}%`);
158
+ console.log(`標準差 (SD) : ${theo.stdDev.toFixed(2)}`);
159
+ console.log("------------------------------------------");
160
+
161
+ // 2. 模擬 (Simulation)
162
+ console.log(`\n正在執行模擬 (n=${formatCurrency(SIMULATION_COUNT)})...`);
163
+ const game = new LaBaG(patterns, payouts);
164
+ const sim = runSimulation(game, SIMULATION_COUNT);
165
+
166
+ const simEV = sim.totalReward / SIMULATION_COUNT;
167
+ const simRTP = (simEV / BET_AMOUNT) * 100;
168
+ const simHitRate = (sim.winCount / SIMULATION_COUNT) * 100;
169
+
170
+ // RTP 的信賴區間 (95% CI): 平均值 +/- 1.96 * (標準差 / sqrt(N))
171
+ // 標準誤差 (Standard Error) = 標準差 / sqrt(模擬次數)
172
+ const standardError = theo.stdDev / Math.sqrt(SIMULATION_COUNT);
173
+ // 信賴區間的邊際誤差 (Margin of Error) = 1.96 * 標準誤差
174
+ const marginOfError = 1.96 * standardError;
175
+ // RTP 的誤差百分比 = (邊際誤差 / 投注金額) * 100
176
+ const rtpMargin = (marginOfError / BET_AMOUNT) * 100;
177
+
178
+ console.log("==========================================");
179
+ console.log(" 模擬結果 (Simulation) ");
180
+ console.log("==========================================");
181
+ console.log(
182
+ `總投注 : ${formatCurrency(BET_AMOUNT * SIMULATION_COUNT)}`,
183
+ );
184
+ console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
185
+ console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
186
+ console.log(
187
+ `模擬 RTP : ${simRTP.toFixed(2)}% (95% CI: ±${rtpMargin.toFixed(2)}%)`,
188
+ );
189
+ console.log(`命中率 : ${simHitRate.toFixed(2)}%`);
190
+ console.log(`最大單次獎金 : ${sim.maxWin}`);
191
+ console.log(`誤差 (Sim - Theo) : ${(simRTP - theo.rtp).toFixed(2)}%`);
192
+
193
+ console.log("\n==========================================");
194
+ console.log(" 獎金分布比較 (Distribution) ");
195
+ console.log("==========================================");
196
+ console.log(`| 獎金 | 理論機率 | 模擬頻率 | 模擬次數 |`);
197
+ console.log(`|--------|------------|------------|----------|`);
198
+
199
+ // 取得理論和模擬中的所有唯一獎金,以確保表格完整
200
+ // Get all unique rewards from both theoretical and simulation to ensure complete table
201
+ const allRewards = Array.from(
202
+ new Set([
203
+ ...Object.keys(theo.hitProbabilities).map(Number),
204
+ ...Object.keys(sim.hitFrequency).map(Number),
205
+ ]),
206
+ ).sort((a, b) => a - b);
207
+
208
+ for (const reward of allRewards) {
209
+ const theoProb = (theo.hitProbabilities[reward] || 0) * 100;
210
+ const simCount = sim.hitFrequency[reward] || 0;
211
+ const simProb = (simCount / SIMULATION_COUNT) * 100;
19
212
 
20
- let totalReward = 0;
21
- for (let i = 0; i < 10; i++) {
22
- const result = labag.spin();
23
- console.log(`Spin ${i + 1}:`, result);
24
- totalReward += result.reward;
213
+ console.log(
214
+ `| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`,
215
+ );
25
216
  }
26
- console.log("Total Reward:", totalReward);
217
+ console.log("==========================================");
@@ -8,5 +8,5 @@ export type Payout = {
8
8
  id: string;
9
9
  pattern_id: Pattern["id"];
10
10
  match_count: number;
11
- reward: number;
11
+ multiplier: number;
12
12
  };