labag 3.0.0 → 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 +76 -69
- package/dist/labag.d.ts +5 -4
- package/dist/labag.js +12 -10
- package/dist/test.js +170 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/labag.ts +13 -11
- package/src/test.ts +217 -0
- package/src/types/index.ts +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
# LaBaG (Slot Machine Engine)
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
LaBaG 是一款用 TypeScript 撰寫的高彈性拉霸機遊戲引擎,支援多種模式、事件監聽、組合得分,適合用於遊戲開發、教學或自訂玩法。
|
|
3
|
+
LaBaG 是一款用 TypeScript 撰寫的高彈性拉霸機 (Slot Machine) 遊戲引擎,支援多種模式、自訂圖案權重與中獎組合,適合用於遊戲開發、教學或自訂玩法。
|
|
5
4
|
|
|
6
5
|
[](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 {
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Pattern, Payout } from "./types";
|
|
2
2
|
export declare class LaBaG {
|
|
3
3
|
patterns: Pattern[];
|
|
4
|
-
|
|
4
|
+
reels: Pattern[];
|
|
5
5
|
payouts: Payout[];
|
|
6
6
|
constructor(patterns: Pattern[], payouts: Payout[]);
|
|
7
|
-
spin(): {
|
|
8
|
-
|
|
7
|
+
spin(bet: number): {
|
|
8
|
+
reels: Pattern[];
|
|
9
9
|
reward: number;
|
|
10
|
+
multiplier: number;
|
|
10
11
|
};
|
|
11
12
|
randomPattern(): Pattern;
|
|
12
|
-
|
|
13
|
+
calculateMultiplier(reels: Pattern[]): number;
|
|
13
14
|
}
|
package/dist/labag.js
CHANGED
|
@@ -4,23 +4,25 @@ exports.LaBaG = void 0;
|
|
|
4
4
|
const randInt_1 = require("./utils/randInt");
|
|
5
5
|
class LaBaG {
|
|
6
6
|
patterns;
|
|
7
|
-
|
|
7
|
+
reels;
|
|
8
8
|
payouts;
|
|
9
9
|
constructor(patterns, payouts) {
|
|
10
10
|
this.patterns = patterns;
|
|
11
|
-
this.
|
|
11
|
+
this.reels = [patterns[0], patterns[1], patterns[2]];
|
|
12
12
|
this.payouts = payouts;
|
|
13
13
|
}
|
|
14
|
-
spin() {
|
|
15
|
-
this.
|
|
14
|
+
spin(bet) {
|
|
15
|
+
this.reels = [
|
|
16
16
|
this.randomPattern(),
|
|
17
17
|
this.randomPattern(),
|
|
18
18
|
this.randomPattern(),
|
|
19
19
|
];
|
|
20
|
-
const
|
|
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
|
-
|
|
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
|
|
45
|
+
let totalMultiplier = 0;
|
|
44
46
|
for (const payout of this.payouts) {
|
|
45
47
|
if (patternCounts[payout.pattern_id] === payout.match_count) {
|
|
46
|
-
|
|
48
|
+
totalMultiplier += payout.multiplier;
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
|
-
return
|
|
51
|
+
return totalMultiplier;
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
exports.LaBaG = LaBaG;
|
package/dist/test.js
CHANGED
|
@@ -1,2 +1,172 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const labag_1 = require("./labag");
|
|
4
|
+
// --- 設定 (Configuration) ---
|
|
5
|
+
const BET_AMOUNT = 200;
|
|
6
|
+
const SIMULATION_COUNT = 1_000_000;
|
|
7
|
+
// --- 資料定義 (Data Definitions) ---
|
|
8
|
+
const patterns = [
|
|
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
|
+
},
|
|
39
|
+
];
|
|
40
|
+
const payouts = [
|
|
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 },
|
|
53
|
+
];
|
|
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 };
|
|
96
|
+
}
|
|
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("==========================================");
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/labag.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { Pattern, Payout } from "./types";
|
|
2
2
|
import { randInt } from "./utils/randInt";
|
|
3
3
|
|
|
4
|
-
|
|
5
4
|
export class LaBaG {
|
|
6
5
|
patterns: Pattern[];
|
|
7
|
-
|
|
6
|
+
reels: Pattern[];
|
|
8
7
|
payouts: Payout[];
|
|
9
8
|
|
|
10
9
|
constructor(patterns: Pattern[], payouts: Payout[]) {
|
|
11
10
|
this.patterns = patterns;
|
|
12
|
-
this.
|
|
11
|
+
this.reels = [patterns[0], patterns[1], patterns[2]];
|
|
13
12
|
this.payouts = payouts;
|
|
14
13
|
}
|
|
15
|
-
spin() {
|
|
16
|
-
this.
|
|
14
|
+
spin(bet: number): { reels: Pattern[]; reward: number; multiplier: number } {
|
|
15
|
+
this.reels = [
|
|
17
16
|
this.randomPattern(),
|
|
18
17
|
this.randomPattern(),
|
|
19
18
|
this.randomPattern(),
|
|
20
19
|
];
|
|
21
|
-
const
|
|
20
|
+
const multiplier = this.calculateMultiplier(this.reels);
|
|
21
|
+
const reward = bet * multiplier;
|
|
22
22
|
return {
|
|
23
|
-
|
|
23
|
+
reels: this.reels,
|
|
24
24
|
reward,
|
|
25
|
+
multiplier,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -41,17 +42,18 @@ export class LaBaG {
|
|
|
41
42
|
throw new Error("No pattern found");
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
calculateMultiplier(reels: Pattern[]): number {
|
|
45
46
|
const patternCounts: { [key: string]: number } = {};
|
|
46
47
|
for (const pattern of reels) {
|
|
47
48
|
patternCounts[pattern.id] = (patternCounts[pattern.id] || 0) + 1;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
|
|
51
|
+
let totalMultiplier = 0;
|
|
50
52
|
for (const payout of this.payouts) {
|
|
51
53
|
if (patternCounts[payout.pattern_id] === payout.match_count) {
|
|
52
|
-
|
|
54
|
+
totalMultiplier += payout.multiplier;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
|
-
return
|
|
57
|
+
return totalMultiplier;
|
|
56
58
|
}
|
|
57
59
|
}
|
package/src/test.ts
CHANGED
|
@@ -0,0 +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
|
+
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("==========================================");
|
package/src/types/index.ts
CHANGED