labag 3.1.0 → 3.1.1
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 +1 -1
- package/dist/test.js +189 -170
- package/package.json +1 -1
- package/src/labag.ts +1 -1
- package/src/test.ts +217 -217
package/dist/labag.js
CHANGED
package/dist/test.js
CHANGED
|
@@ -1,172 +1,191 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
// import { LaBaG } from "./labag";
|
|
3
|
+
// import { Pattern, Payout } from "./types";
|
|
2
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
const
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log(
|
|
155
|
-
console.log("
|
|
156
|
-
console.log("
|
|
157
|
-
console.log(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.log(
|
|
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("==========================================");
|
package/package.json
CHANGED
package/src/labag.ts
CHANGED
package/src/test.ts
CHANGED
|
@@ -1,217 +1,217 @@
|
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
const payouts: Payout[] = [
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
// --- 輔助函式 (Helpers) ---
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
格式化數字為貨幣或百分比 (Format numbers as currency or percentage)
|
|
67
|
-
*/
|
|
68
|
-
const formatCurrency = (val: number) =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const formatPct = (val: number) => `${val.toFixed(4)}%`;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
function calculateTheoreticalStats(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// --- 模擬邏輯 (Simulation Logic) ---
|
|
122
|
-
function runSimulation(game: LaBaG, count: number) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
183
|
-
);
|
|
184
|
-
console.log(`總獎金 : ${formatCurrency(sim.totalReward)}`);
|
|
185
|
-
console.log(`模擬期望值 (EV) : ${simEV.toFixed(2)}`);
|
|
186
|
-
console.log(
|
|
187
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
).sort((a, b) => a - b);
|
|
207
|
-
|
|
208
|
-
for (const reward of allRewards) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
console.log("==========================================");
|
|
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("==========================================");
|