labag 3.1.1 → 3.1.2

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/dist/labag.js CHANGED
@@ -18,7 +18,7 @@ class LaBaG {
18
18
  this.randomPattern(),
19
19
  ];
20
20
  const multiplier = this.calculateMultiplier(this.reels);
21
- const reward = Math.floor(bet * multiplier * 1000) / 1000;
21
+ const reward = Math.floor(bet * multiplier);
22
22
  return {
23
23
  reels: this.reels,
24
24
  reward,
package/dist/test.d.ts CHANGED
@@ -1 +1,3 @@
1
- export {};
1
+ import { Pattern, Payout } from "./types";
2
+ export declare const patterns: Pattern[];
3
+ export declare const payouts: Payout[];
package/dist/test.js CHANGED
@@ -1,191 +1,193 @@
1
1
  "use strict";
2
- // import { LaBaG } from "./labag";
3
- // import { Pattern, Payout } from "./types";
4
2
  Object.defineProperty(exports, "__esModule", { value: true });
5
- // // --- 設定 (Configuration) ---
6
- // const BET_AMOUNT = 200;
7
- // const SIMULATION_COUNT = 1_000_000;
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
- // },
46
- // ];
47
- // const payouts: Payout[] = [
48
- // { id: 1, match_count: 2, pattern_id: 1, reward: 56 },
49
- // { id: 2, match_count: 3, pattern_id: 1, reward: 242 },
50
- // { id: 3, match_count: 2, pattern_id: 2, reward: 119 },
51
- // { id: 4, match_count: 3, pattern_id: 2, reward: 578 },
52
- // { id: 5, match_count: 2, pattern_id: 3, reward: 266 },
53
- // { id: 6, match_count: 3, pattern_id: 3, reward: 1345 },
54
- // { id: 7, match_count: 2, pattern_id: 4, reward: 571 },
55
- // { id: 8, match_count: 3, pattern_id: 4, reward: 3503 },
56
- // { id: 9, match_count: 2, pattern_id: 5, reward: 2136 },
57
- // { id: 10, match_count: 3, pattern_id: 5, reward: 11727 },
58
- // { id: 11, match_count: 2, pattern_id: 6, reward: 18708 },
59
- // { id: 12, match_count: 3, pattern_id: 6, reward: 182200 },
60
- // ];
61
- // // --- 輔助函式 (Helpers) ---
62
- // /**
63
- // 格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
64
- // */
65
- // const formatCurrency = (val: number) =>
66
- // new Intl.NumberFormat("en-US", {
67
- // style: "decimal",
68
- // minimumFractionDigits: 0,
69
- // }).format(val);
70
- // /**
71
- // * 格式化百分比
72
- // */
73
- // const formatPct = (val: number) => `${val.toFixed(4)}%`;
74
- // /**
75
- // * 根據圖案和賠率計算理論統計數據。
76
- // * 假設滾輪獨立且賠率條件互斥(例如每次旋轉只有一種賠彩)。
77
- // */
78
- // function calculateTheoreticalStats(
79
- // patterns: Pattern[],
80
- // payouts: Payout[],
81
- // bet: number,
82
- // ) {
83
- // const totalWeight = patterns.reduce((sum, p) => sum + p.weight, 0);
84
- // let ev = 0;
85
- // let varianceSum = 0; // E[X^2]
86
- // const hitProbabilities: Record<number, number> = {};
87
- // for (const payout of payouts) {
88
- // const pattern = patterns.find((p) => p.id === payout.pattern_id);
89
- // if (!pattern) continue;
90
- // const p = pattern.weight / totalWeight;
91
- // // 二項分布概率計算特定圖案數量 (假設 3 個滾輪)
92
- // // Binomial probability for specific pattern count (assuming 3 reels)
93
- // // k=3: p^3
94
- // // k=2: C(3,2) * p^2 * (1-p) = 3 * p^2 * (1-p)
95
- // const prob =
96
- // payout.match_count === 3 ? Math.pow(p, 3) : 3 * Math.pow(p, 2) * (1 - p);
97
- // ev += prob * payout.reward;
98
- // // 假設賠率互斥: E[X^2] = sum(prob * reward^2)
99
- // // Assuming disjoint payouts: E[X^2] = sum(prob * reward^2)
100
- // varianceSum += prob * Math.pow(payout.reward, 2);
101
- // hitProbabilities[payout.reward] =
102
- // (hitProbabilities[payout.reward] || 0) + prob;
103
- // }
104
- // const variance = varianceSum - Math.pow(ev, 2);
105
- // const stdDev = Math.sqrt(variance);
106
- // const rtp = (ev / bet) * 100;
107
- // return { ev, rtp, stdDev, hitProbabilities };
108
- // }
109
- // // --- 模擬邏輯 (Simulation Logic) ---
110
- // function runSimulation(game: LaBaG, count: number) {
111
- // let totalReward = 0;
112
- // let winCount = 0;
113
- // let maxWin = 0;
114
- // const hitFrequency: Record<number, number> = {};
115
- // for (let i = 0; i < count; i++) {
116
- // const result = game.spin();
117
- // totalReward += result.reward;
118
- // if (result.reward > 0) {
119
- // winCount++;
120
- // if (result.reward > maxWin) maxWin = result.reward;
121
- // hitFrequency[result.reward] = (hitFrequency[result.reward] || 0) + 1;
122
- // }
123
- // }
124
- // return {
125
- // totalReward,
126
- // winCount,
127
- // maxWin,
128
- // hitFrequency,
129
- // };
130
- // }
131
- // // --- 主要執行區塊 (Main Execution) ---
132
- // // 1. 理論分析 (Theoretical Analysis)
133
- // console.log("正在計算理論數值...");
134
- // const theo = calculateTheoreticalStats(patterns, payouts, BET_AMOUNT);
135
- // console.log("==========================================");
136
- // console.log(" 理論分析 (Theoretical) ");
137
- // console.log("==========================================");
138
- // console.log(`理論期望值 (EV) : ${theo.ev.toFixed(2)}`);
139
- // console.log(`理論 RTP : ${theo.rtp.toFixed(2)}%`);
140
- // console.log(`標準差 (SD) : ${theo.stdDev.toFixed(2)}`);
141
- // console.log("------------------------------------------");
142
- // // 2. 模擬 (Simulation)
143
- // console.log(`\n正在執行模擬 (n=${formatCurrency(SIMULATION_COUNT)})...`);
144
- // const game = new LaBaG(patterns, payouts);
145
- // const sim = runSimulation(game, SIMULATION_COUNT);
146
- // const simEV = sim.totalReward / SIMULATION_COUNT;
147
- // const simRTP = (simEV / BET_AMOUNT) * 100;
148
- // const simHitRate = (sim.winCount / SIMULATION_COUNT) * 100;
149
- // // RTP 的信賴區間 (95% CI): 平均值 +/- 1.96 * (標準差 / sqrt(N))
150
- // // 標準誤差 (Standard Error) = 標準差 / sqrt(模擬次數)
151
- // const standardError = theo.stdDev / Math.sqrt(SIMULATION_COUNT);
152
- // // 信賴區間的邊際誤差 (Margin of Error) = 1.96 * 標準誤差
153
- // const marginOfError = 1.96 * standardError;
154
- // // RTP 的誤差百分比 = (邊際誤差 / 投注金額) * 100
155
- // const rtpMargin = (marginOfError / BET_AMOUNT) * 100;
156
- // console.log("==========================================");
157
- // console.log(" 模擬結果 (Simulation) ");
158
- // console.log("==========================================");
159
- // console.log(
160
- // `總投注 : ${formatCurrency(BET_AMOUNT * SIMULATION_COUNT)}`,
161
- // );
162
- // console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
163
- // console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
164
- // console.log(
165
- // `模擬 RTP : ${simRTP.toFixed(2)}% (95% CI: ±${rtpMargin.toFixed(2)}%)`,
166
- // );
167
- // console.log(`命中率 : ${simHitRate.toFixed(2)}%`);
168
- // console.log(`最大單次獎金 : ${sim.maxWin}`);
169
- // console.log(`誤差 (Sim - Theo) : ${(simRTP - theo.rtp).toFixed(2)}%`);
170
- // console.log("\n==========================================");
171
- // console.log(" 獎金分布比較 (Distribution) ");
172
- // console.log("==========================================");
173
- // console.log(`| 獎金 | 理論機率 | 模擬頻率 | 模擬次數 |`);
174
- // console.log(`|--------|------------|------------|----------|`);
175
- // // 取得理論和模擬中的所有唯一獎金,以確保表格完整
176
- // // Get all unique rewards from both theoretical and simulation to ensure complete table
177
- // const allRewards = Array.from(
178
- // new Set([
179
- // ...Object.keys(theo.hitProbabilities).map(Number),
180
- // ...Object.keys(sim.hitFrequency).map(Number),
181
- // ]),
182
- // ).sort((a, b) => a - b);
183
- // for (const reward of allRewards) {
184
- // const theoProb = (theo.hitProbabilities[reward] || 0) * 100;
185
- // const simCount = sim.hitFrequency[reward] || 0;
186
- // const simProb = (simCount / SIMULATION_COUNT) * 100;
187
- // console.log(
188
- // `| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`,
189
- // );
190
- // }
191
- // console.log("==========================================");
3
+ exports.payouts = exports.patterns = void 0;
4
+ const labag_1 = require("./labag");
5
+ // --- 設定 (Configuration) ---
6
+ const BET_AMOUNT = 100;
7
+ const SIMULATION_COUNT = 1000000;
8
+ // --- 資料定義 (Data Definitions) ---
9
+ exports.patterns = [
10
+ {
11
+ id: "1",
12
+ weight: 36,
13
+ image: "https://fanyu.vercel.app/api/album/item/1mDK1ewfLiV3fAB1HbjvwdSaDTdJdBGG3",
14
+ },
15
+ {
16
+ id: "2",
17
+ weight: 24,
18
+ image: "https://fanyu.vercel.app/api/album/item/1oB-uZhPPfjfTtG4CITnb3_E-Ops9JTA0",
19
+ },
20
+ {
21
+ id: "3",
22
+ weight: 17,
23
+ image: "https://fanyu.vercel.app/api/album/item/1bMJdRB8uerQZfGYINzBI9Vaw32bZljl2",
24
+ },
25
+ {
26
+ id: "4",
27
+ weight: 12,
28
+ image: "https://fanyu.vercel.app/api/album/item/1In8LF1wVfLXpPkp57a20zX84QgsAeLQx",
29
+ },
30
+ {
31
+ id: "5",
32
+ weight: 8,
33
+ image: "https://fanyu.vercel.app/api/album/item/1Zo_PjrXm-4TBrL2cLAeFkEl1el9kTR56",
34
+ },
35
+ {
36
+ id: "6",
37
+ weight: 3,
38
+ image: "https://fanyu.vercel.app/api/album/item/19NMnVgcb-9IsknNcfe9TpCyPBIcGnhQU",
39
+ },
40
+ ];
41
+ exports.payouts = [
42
+ { id: "1", match_count: 2, pattern_id: "1", multiplier: 0.56 },
43
+ { id: "2", match_count: 3, pattern_id: "1", multiplier: 2.5 },
44
+ { id: "3", match_count: 2, pattern_id: "2", multiplier: 1.05 },
45
+ { id: "4", match_count: 3, pattern_id: "2", multiplier: 10.0 },
46
+ { id: "5", match_count: 2, pattern_id: "3", multiplier: 1.3 },
47
+ { id: "6", match_count: 3, pattern_id: "3", multiplier: 20.0 },
48
+ { id: "7", match_count: 2, pattern_id: "4", multiplier: 2.1 },
49
+ { id: "8", match_count: 3, pattern_id: "4", multiplier: 45.0 },
50
+ { id: "9", match_count: 2, pattern_id: "5", multiplier: 2.6 },
51
+ { id: "10", match_count: 3, pattern_id: "5", multiplier: 75.0 },
52
+ { id: "11", match_count: 2, pattern_id: "6", multiplier: 7.0 },
53
+ { id: "12", match_count: 3, pattern_id: "6", multiplier: 600.0 },
54
+ ];
55
+ // --- 輔助函式 (Helpers) ---
56
+ /**
57
+ 格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
58
+ */
59
+ const formatCurrency = (val) => new Intl.NumberFormat("en-US", {
60
+ style: "decimal",
61
+ minimumFractionDigits: 0,
62
+ }).format(val);
63
+ /**
64
+ * 格式化百分比
65
+ */
66
+ const formatPct = (val) => `${val.toFixed(4)}%`;
67
+ /**
68
+ * 根據圖案和賠率計算理論統計數據。
69
+ * 假設滾輪獨立且賠率條件互斥(例如每次旋轉只有一種賠彩)。
70
+ */
71
+ function calculateTheoreticalStats(patterns, payouts, bet) {
72
+ const totalWeight = patterns.reduce((sum, p) => sum + p.weight, 0);
73
+ let ev = 0;
74
+ let varianceSum = 0; // E[X^2]
75
+ const hitProbabilities = {};
76
+ const payoutStats = [];
77
+ for (const payout of payouts) {
78
+ const pattern = patterns.find((p) => p.id === payout.pattern_id);
79
+ if (!pattern)
80
+ continue;
81
+ const p = pattern.weight / totalWeight;
82
+ // 二項分布概率計算特定圖案數量 (假設 3 個滾輪)
83
+ // Binomial probability for specific pattern count (assuming 3 reels)
84
+ // k=3: p^3
85
+ // k=2: C(3,2) * p^2 * (1-p) = 3 * p^2 * (1-p)
86
+ const prob = payout.match_count === 3 ? Math.pow(p, 3) : 3 * Math.pow(p, 2) * (1 - p);
87
+ const reward = Math.floor(payout.multiplier * bet);
88
+ const rtpContribution = ((prob * reward) / bet) * 100; // RTP 貢獻 = (概率 * 獎金 / 投注) * 100
89
+ ev += prob * reward;
90
+ // 假設賠率互斥: E[X^2] = sum(prob * reward^2)
91
+ // Assuming disjoint payouts: E[X^2] = sum(prob * reward^2)
92
+ varianceSum += prob * Math.pow(reward, 2);
93
+ hitProbabilities[reward] = (hitProbabilities[reward] || 0) + prob;
94
+ payoutStats.push({
95
+ id: payout.id,
96
+ patternId: payout.pattern_id,
97
+ matchCount: payout.match_count,
98
+ prob,
99
+ reward,
100
+ rtpContribution,
101
+ });
102
+ }
103
+ const variance = varianceSum - Math.pow(ev, 2);
104
+ const stdDev = Math.sqrt(variance);
105
+ const rtp = (ev / bet) * 100;
106
+ return { ev, rtp, stdDev, hitProbabilities, payoutStats };
107
+ }
108
+ // --- 模擬邏輯 (Simulation Logic) ---
109
+ function runSimulation(game, count) {
110
+ let totalReward = 0;
111
+ let winCount = 0;
112
+ let maxWin = 0;
113
+ const hitFrequency = {};
114
+ for (let i = 0; i < count; i++) {
115
+ const result = game.spin(BET_AMOUNT);
116
+ totalReward += result.reward;
117
+ if (result.reward > 0) {
118
+ winCount++;
119
+ if (result.reward > maxWin)
120
+ maxWin = result.reward;
121
+ hitFrequency[result.reward] = (hitFrequency[result.reward] || 0) + 1;
122
+ }
123
+ }
124
+ return {
125
+ totalReward,
126
+ winCount,
127
+ maxWin,
128
+ hitFrequency,
129
+ };
130
+ }
131
+ // --- 主要執行區塊 (Main Execution) ---
132
+ // 1. 理論分析 (Theoretical Analysis)
133
+ console.log("正在計算理論數值...");
134
+ const theo = calculateTheoreticalStats(exports.patterns, exports.payouts, BET_AMOUNT);
135
+ console.log("==========================================");
136
+ console.log(" 理論分析 (Theoretical) ");
137
+ console.log("==========================================");
138
+ console.log(`理論期望值 (EV) : ${theo.ev.toFixed(2)}`);
139
+ console.log(`理論 RTP : ${theo.rtp.toFixed(2)}%`);
140
+ console.log(`標準差 (SD) : ${theo.stdDev.toFixed(2)}`);
141
+ console.log("------------------------------------------");
142
+ console.log("\n==========================================");
143
+ console.log(" RTP 貢獻分析 (RTP Contribution) ");
144
+ console.log("==========================================");
145
+ console.log(`| ID | Pattern | Count | Reward | Prob (%) | RTP Contrib (%) |`);
146
+ console.log(`|----|---------|-------|--------|----------|-----------------|`);
147
+ theo.payoutStats.forEach((stat) => {
148
+ console.log(`| ${stat.id.padEnd(2)} | ${stat.patternId.padEnd(7)} | ${stat.matchCount} | ${stat.reward.toString().padEnd(6)} | ${(stat.prob * 100).toFixed(4).padEnd(8)} | ${stat.rtpContribution.toFixed(4).padEnd(15)} |`);
149
+ });
150
+ console.log("------------------------------------------");
151
+ console.log(`總 RTP: ${theo.rtp.toFixed(2)}%`);
152
+ // 2. 模擬 (Simulation)
153
+ console.log(`\n正在執行模擬 (n=${formatCurrency(SIMULATION_COUNT)})...`);
154
+ const game = new labag_1.LaBaG(exports.patterns, exports.payouts);
155
+ const sim = runSimulation(game, SIMULATION_COUNT);
156
+ const simEV = sim.totalReward / SIMULATION_COUNT;
157
+ const simRTP = (simEV / BET_AMOUNT) * 100;
158
+ const simHitRate = (sim.winCount / SIMULATION_COUNT) * 100;
159
+ // RTP 的信賴區間 (95% CI): 平均值 +/- 1.96 * (標準差 / sqrt(N))
160
+ // 標準誤差 (Standard Error) = 標準差 / sqrt(模擬次數)
161
+ const standardError = theo.stdDev / Math.sqrt(SIMULATION_COUNT);
162
+ // 信賴區間的邊際誤差 (Margin of Error) = 1.96 * 標準誤差
163
+ const marginOfError = 1.96 * standardError;
164
+ // RTP 的誤差百分比 = (邊際誤差 / 投注金額) * 100
165
+ const rtpMargin = (marginOfError / BET_AMOUNT) * 100;
166
+ console.log("==========================================");
167
+ console.log(" 模擬結果 (Simulation) ");
168
+ console.log("==========================================");
169
+ console.log(`總投注 : ${formatCurrency(BET_AMOUNT * SIMULATION_COUNT)}`);
170
+ console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
171
+ console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
172
+ console.log(`模擬 RTP : ${simRTP.toFixed(2)}% (95% CI: ±${rtpMargin.toFixed(2)}%)`);
173
+ console.log(`命中率 : ${simHitRate.toFixed(2)}%`);
174
+ console.log(`最大單次獎金 : ${sim.maxWin}`);
175
+ console.log(`誤差 (Sim - Theo) : ${(simRTP - theo.rtp).toFixed(2)}%`);
176
+ console.log("\n==========================================");
177
+ console.log(" 獎金分布比較 (Distribution) ");
178
+ console.log("==========================================");
179
+ console.log(`| 獎金 | 理論機率 | 模擬頻率 | 模擬次數 |`);
180
+ console.log(`|--------|------------|------------|----------|`);
181
+ // 取得理論和模擬中的所有唯一獎金,以確保表格完整
182
+ // Get all unique rewards from both theoretical and simulation to ensure complete table
183
+ const allRewards = Array.from(new Set([
184
+ ...Object.keys(theo.hitProbabilities).map(Number),
185
+ ...Object.keys(sim.hitFrequency).map(Number),
186
+ ])).sort((a, b) => a - b);
187
+ for (const reward of allRewards) {
188
+ const theoProb = (theo.hitProbabilities[reward] || 0) * 100;
189
+ const simCount = sim.hitFrequency[reward] || 0;
190
+ const simProb = (simCount / SIMULATION_COUNT) * 100;
191
+ console.log(`| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`);
192
+ }
193
+ console.log("==========================================");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "labag",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/labag.ts CHANGED
@@ -18,7 +18,7 @@ export class LaBaG {
18
18
  this.randomPattern(),
19
19
  ];
20
20
  const multiplier = this.calculateMultiplier(this.reels);
21
- const reward = Math.floor(bet * multiplier * 1000) / 1000;
21
+ const reward = Math.floor(bet * multiplier);
22
22
  return {
23
23
  reels: this.reels,
24
24
  reward,
package/src/test.ts CHANGED
@@ -1,217 +1,254 @@
1
- // import { LaBaG } from "./labag";
2
- // import { Pattern, Payout } from "./types";
3
-
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
- // },
46
- // ];
47
-
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 },
61
- // ];
62
-
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;
212
-
213
- // console.log(
214
- // `| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`,
215
- // );
216
- // }
217
- // console.log("==========================================");
1
+ import { LaBaG } from "./labag";
2
+ import { Pattern, Payout } from "./types";
3
+
4
+ // --- 設定 (Configuration) ---
5
+ const BET_AMOUNT = 100;
6
+ const SIMULATION_COUNT = 1000000;
7
+
8
+ // --- 資料定義 (Data Definitions) ---
9
+
10
+ export const patterns: Pattern[] = [
11
+ {
12
+ id: "1",
13
+ weight: 36,
14
+ image:
15
+ "https://fanyu.vercel.app/api/album/item/1mDK1ewfLiV3fAB1HbjvwdSaDTdJdBGG3",
16
+ },
17
+ {
18
+ id: "2",
19
+ weight: 24,
20
+ image:
21
+ "https://fanyu.vercel.app/api/album/item/1oB-uZhPPfjfTtG4CITnb3_E-Ops9JTA0",
22
+ },
23
+
24
+ {
25
+ id: "3",
26
+ weight: 17,
27
+ image:
28
+ "https://fanyu.vercel.app/api/album/item/1bMJdRB8uerQZfGYINzBI9Vaw32bZljl2",
29
+ },
30
+ {
31
+ id: "4",
32
+ weight: 12,
33
+ image:
34
+ "https://fanyu.vercel.app/api/album/item/1In8LF1wVfLXpPkp57a20zX84QgsAeLQx",
35
+ },
36
+
37
+ {
38
+ id: "5",
39
+ weight: 8,
40
+ image:
41
+ "https://fanyu.vercel.app/api/album/item/1Zo_PjrXm-4TBrL2cLAeFkEl1el9kTR56",
42
+ },
43
+ {
44
+ id: "6",
45
+ weight: 3,
46
+ image:
47
+ "https://fanyu.vercel.app/api/album/item/19NMnVgcb-9IsknNcfe9TpCyPBIcGnhQU",
48
+ },
49
+ ];
50
+
51
+ export const payouts: Payout[] = [
52
+ { id: "1", match_count: 2, pattern_id: "1", multiplier: 0.56 },
53
+ { id: "2", match_count: 3, pattern_id: "1", multiplier: 2.5 },
54
+ { id: "3", match_count: 2, pattern_id: "2", multiplier: 1.05 },
55
+ { id: "4", match_count: 3, pattern_id: "2", multiplier: 10.0 },
56
+ { id: "5", match_count: 2, pattern_id: "3", multiplier: 1.3 },
57
+ { id: "6", match_count: 3, pattern_id: "3", multiplier: 20.0 },
58
+ { id: "7", match_count: 2, pattern_id: "4", multiplier: 2.1 },
59
+ { id: "8", match_count: 3, pattern_id: "4", multiplier: 45.0 },
60
+ { id: "9", match_count: 2, pattern_id: "5", multiplier: 2.6 },
61
+ { id: "10", match_count: 3, pattern_id: "5", multiplier: 75.0 },
62
+ { id: "11", match_count: 2, pattern_id: "6", multiplier: 7.0 },
63
+ { id: "12", match_count: 3, pattern_id: "6", multiplier: 600.0 },
64
+ ];
65
+
66
+ // --- 輔助函式 (Helpers) ---
67
+
68
+ /**
69
+ 格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
70
+ */
71
+ const formatCurrency = (val: number) =>
72
+ new Intl.NumberFormat("en-US", {
73
+ style: "decimal",
74
+ minimumFractionDigits: 0,
75
+ }).format(val);
76
+
77
+ /**
78
+ * 格式化百分比
79
+ */
80
+ const formatPct = (val: number) => `${val.toFixed(4)}%`;
81
+
82
+ /**
83
+ * 根據圖案和賠率計算理論統計數據。
84
+ * 假設滾輪獨立且賠率條件互斥(例如每次旋轉只有一種賠彩)。
85
+ */
86
+ function calculateTheoreticalStats(
87
+ patterns: Pattern[],
88
+ payouts: Payout[],
89
+ bet: number,
90
+ ) {
91
+ const totalWeight = patterns.reduce((sum, p) => sum + p.weight, 0);
92
+ let ev = 0;
93
+ let varianceSum = 0; // E[X^2]
94
+ const hitProbabilities: Record<number, number> = {};
95
+
96
+ const payoutStats: {
97
+ id: string;
98
+ patternId: string;
99
+ matchCount: number;
100
+ prob: number;
101
+ reward: number;
102
+ rtpContribution: number;
103
+ }[] = [];
104
+
105
+ for (const payout of payouts) {
106
+ const pattern = patterns.find((p) => p.id === payout.pattern_id);
107
+ if (!pattern) continue;
108
+
109
+ const p = pattern.weight / totalWeight;
110
+ // 二項分布概率計算特定圖案數量 (假設 3 個滾輪)
111
+ // Binomial probability for specific pattern count (assuming 3 reels)
112
+ // k=3: p^3
113
+ // k=2: C(3,2) * p^2 * (1-p) = 3 * p^2 * (1-p)
114
+ const prob =
115
+ payout.match_count === 3 ? Math.pow(p, 3) : 3 * Math.pow(p, 2) * (1 - p);
116
+
117
+ const reward = Math.floor(payout.multiplier * bet);
118
+ const rtpContribution = ((prob * reward) / bet) * 100; // RTP 貢獻 = (概率 * 獎金 / 投注) * 100
119
+
120
+ ev += prob * reward;
121
+ // 假設賠率互斥: E[X^2] = sum(prob * reward^2)
122
+ // Assuming disjoint payouts: E[X^2] = sum(prob * reward^2)
123
+ varianceSum += prob * Math.pow(reward, 2);
124
+
125
+ hitProbabilities[reward] = (hitProbabilities[reward] || 0) + prob;
126
+
127
+ payoutStats.push({
128
+ id: payout.id,
129
+ patternId: payout.pattern_id,
130
+ matchCount: payout.match_count,
131
+ prob,
132
+ reward,
133
+ rtpContribution,
134
+ });
135
+ }
136
+
137
+ const variance = varianceSum - Math.pow(ev, 2);
138
+ const stdDev = Math.sqrt(variance);
139
+ const rtp = (ev / bet) * 100;
140
+
141
+ return { ev, rtp, stdDev, hitProbabilities, payoutStats };
142
+ }
143
+
144
+ // --- 模擬邏輯 (Simulation Logic) ---
145
+ function runSimulation(game: LaBaG, count: number) {
146
+ let totalReward = 0;
147
+ let winCount = 0;
148
+ let maxWin = 0;
149
+ const hitFrequency: Record<number, number> = {};
150
+
151
+ for (let i = 0; i < count; i++) {
152
+ const result = game.spin(BET_AMOUNT);
153
+ totalReward += result.reward;
154
+
155
+ if (result.reward > 0) {
156
+ winCount++;
157
+ if (result.reward > maxWin) maxWin = result.reward;
158
+ hitFrequency[result.reward] = (hitFrequency[result.reward] || 0) + 1;
159
+ }
160
+ }
161
+
162
+ return {
163
+ totalReward,
164
+ winCount,
165
+ maxWin,
166
+ hitFrequency,
167
+ };
168
+ }
169
+
170
+ // --- 主要執行區塊 (Main Execution) ---
171
+
172
+ // 1. 理論分析 (Theoretical Analysis)
173
+ console.log("正在計算理論數值...");
174
+ const theo = calculateTheoreticalStats(patterns, payouts, BET_AMOUNT);
175
+
176
+ console.log("==========================================");
177
+ console.log(" 理論分析 (Theoretical) ");
178
+ console.log("==========================================");
179
+ console.log(`理論期望值 (EV) : ${theo.ev.toFixed(2)}`);
180
+ console.log(`理論 RTP : ${theo.rtp.toFixed(2)}%`);
181
+ console.log(`標準差 (SD) : ${theo.stdDev.toFixed(2)}`);
182
+ console.log("------------------------------------------");
183
+
184
+ console.log("\n==========================================");
185
+ console.log(" RTP 貢獻分析 (RTP Contribution) ");
186
+ console.log("==========================================");
187
+ console.log(`| ID | Pattern | Count | Reward | Prob (%) | RTP Contrib (%) |`);
188
+ console.log(`|----|---------|-------|--------|----------|-----------------|`);
189
+
190
+ theo.payoutStats.forEach((stat) => {
191
+ console.log(
192
+ `| ${stat.id.padEnd(2)} | ${stat.patternId.padEnd(7)} | ${stat.matchCount} | ${stat.reward.toString().padEnd(6)} | ${(stat.prob * 100).toFixed(4).padEnd(8)} | ${stat.rtpContribution.toFixed(4).padEnd(15)} |`,
193
+ );
194
+ });
195
+ console.log("------------------------------------------");
196
+ console.log(`總 RTP: ${theo.rtp.toFixed(2)}%`);
197
+
198
+ // 2. 模擬 (Simulation)
199
+ console.log(`\n正在執行模擬 (n=${formatCurrency(SIMULATION_COUNT)})...`);
200
+ const game = new LaBaG(patterns, payouts);
201
+ const sim = runSimulation(game, SIMULATION_COUNT);
202
+
203
+ const simEV = sim.totalReward / SIMULATION_COUNT;
204
+ const simRTP = (simEV / BET_AMOUNT) * 100;
205
+ const simHitRate = (sim.winCount / SIMULATION_COUNT) * 100;
206
+
207
+ // RTP 的信賴區間 (95% CI): 平均值 +/- 1.96 * (標準差 / sqrt(N))
208
+ // 標準誤差 (Standard Error) = 標準差 / sqrt(模擬次數)
209
+ const standardError = theo.stdDev / Math.sqrt(SIMULATION_COUNT);
210
+ // 信賴區間的邊際誤差 (Margin of Error) = 1.96 * 標準誤差
211
+ const marginOfError = 1.96 * standardError;
212
+ // RTP 的誤差百分比 = (邊際誤差 / 投注金額) * 100
213
+ const rtpMargin = (marginOfError / BET_AMOUNT) * 100;
214
+
215
+ console.log("==========================================");
216
+ console.log(" 模擬結果 (Simulation) ");
217
+ console.log("==========================================");
218
+ console.log(
219
+ `總投注 : ${formatCurrency(BET_AMOUNT * SIMULATION_COUNT)}`,
220
+ );
221
+ console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
222
+ console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
223
+ console.log(
224
+ `模擬 RTP : ${simRTP.toFixed(2)}% (95% CI: ±${rtpMargin.toFixed(2)}%)`,
225
+ );
226
+ console.log(`命中率 : ${simHitRate.toFixed(2)}%`);
227
+ console.log(`最大單次獎金 : ${sim.maxWin}`);
228
+ console.log(`誤差 (Sim - Theo) : ${(simRTP - theo.rtp).toFixed(2)}%`);
229
+
230
+ console.log("\n==========================================");
231
+ console.log(" 獎金分布比較 (Distribution) ");
232
+ console.log("==========================================");
233
+ console.log(`| 獎金 | 理論機率 | 模擬頻率 | 模擬次數 |`);
234
+ console.log(`|--------|------------|------------|----------|`);
235
+
236
+ // 取得理論和模擬中的所有唯一獎金,以確保表格完整
237
+ // Get all unique rewards from both theoretical and simulation to ensure complete table
238
+ const allRewards = Array.from(
239
+ new Set([
240
+ ...Object.keys(theo.hitProbabilities).map(Number),
241
+ ...Object.keys(sim.hitFrequency).map(Number),
242
+ ]),
243
+ ).sort((a, b) => a - b);
244
+
245
+ for (const reward of allRewards) {
246
+ const theoProb = (theo.hitProbabilities[reward] || 0) * 100;
247
+ const simCount = sim.hitFrequency[reward] || 0;
248
+ const simProb = (simCount / SIMULATION_COUNT) * 100;
249
+
250
+ console.log(
251
+ `| ${reward.toString().padEnd(6)} | ${formatPct(theoProb).padEnd(10)} | ${formatPct(simProb).padEnd(10)} | ${simCount.toString().padStart(8)} |`,
252
+ );
253
+ }
254
+ console.log("==========================================");