labag 2.6.6 → 3.0.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/src/labag.ts CHANGED
@@ -1,308 +1,57 @@
1
- import { Mode } from "./mode";
2
- import { patterns } from "./pattern";
3
- import { Pattern, LaBaGEvent, PatternName, ModeName } from "./types";
1
+ import { Pattern, Payout } from "./types";
4
2
  import { randInt } from "./utils/randInt";
5
3
 
6
- /**
7
- * 拉霸遊戲的主要類別。
8
- */
9
- export class LaBaG {
10
- /** 總遊玩次數限制 */
11
- times: number;
12
- /** 已遊玩次數 */
13
- played: number;
14
- /** 當前進行的輪次 */
15
- rounds: number;
16
- /** 當前分數 */
17
- score: number;
18
- /** 邊際分數 */
19
- marginScore: number;
20
- /** 產生的隨機數字 */
21
- randNums: number[];
22
- /** 當前轉出的圖案組合 */
23
- patterns: [Pattern | null, Pattern | null, Pattern | null];
24
- /** 遊戲模式列表 */
25
- modes: Mode[];
26
- /** 事件監聽器列表 */
27
- eventListeners: Record<LaBaGEvent, ((game: LaBaG) => void)[]>;
28
-
29
- __defaultMode__: Mode;
30
4
 
31
- constructor(times: number = 30) {
32
- this.times = times;
33
- this.played = 0;
34
- this.rounds = 0;
35
- this.score = 0;
36
- this.marginScore = 0;
37
- this.randNums = [];
38
- this.patterns = [null, null, null];
39
- this.modes = [];
40
- this.eventListeners = {
41
- gameOver: [],
42
- gameStart: [],
43
- roundStart: [],
44
- roundEnd: [],
45
- rollSlots: [],
46
- calculateScore: [],
5
+ export class LaBaG {
6
+ patterns: Pattern[];
7
+ reel: Pattern[];
8
+ payouts: Payout[];
9
+
10
+ constructor(patterns: Pattern[], payouts: Payout[]) {
11
+ this.patterns = patterns;
12
+ this.reel = [patterns[0], patterns[1], patterns[2]];
13
+ this.payouts = payouts;
14
+ }
15
+ spin() {
16
+ this.reel = [
17
+ this.randomPattern(),
18
+ this.randomPattern(),
19
+ this.randomPattern(),
20
+ ];
21
+ const reward = this.caculateReward(this.reel);
22
+ return {
23
+ reel: this.reel,
24
+ reward,
47
25
  };
48
-
49
- this.__defaultMode__ = new Mode({
50
- active: true,
51
- name: "normal",
52
- rates: {
53
- gss: 36,
54
- hhh: 24,
55
- hentai: 17,
56
- handson: 12,
57
- kachu: 8,
58
- rrr: 3,
59
- },
60
- eventListener: {
61
- gameStart: (game) => {
62
- game.played = 0;
63
- game.rounds = 0;
64
- game.score = 0;
65
- game.marginScore = 0;
66
- game.randNums = [];
67
- game.patterns = [null, null, null];
68
- },
69
- roundStart: (game) => {
70
- game.played += 1;
71
- game.rounds += 1;
72
- game.marginScore = 0;
73
- },
74
-
75
- rollSlots: (game) => {
76
- const { ranges } = game.getCurrentConfig();
77
- const rangesAcc =
78
- ranges.length > 0 ? ranges[ranges.length - 1].threshold : 0;
79
-
80
- // 產生 3 個隨機數字並直接尋找對應圖案
81
- for (let i = 0; i < 3; i++) {
82
- const num = randInt(1, rangesAcc);
83
- game.randNums[i] = num;
84
-
85
- let matchedPattern: Pattern | null = null;
86
- for (let j = 0; j < ranges.length; j++) {
87
- if (num <= ranges[j].threshold) {
88
- matchedPattern = ranges[j].pattern;
89
- break;
90
- }
91
- }
92
- game.patterns[i] = matchedPattern;
93
- }
94
- },
95
- calculateScore: (game, mode) => {
96
- const [p1, p2, p3] = game.patterns;
97
- if (!p1 || !p2 || !p3) {
98
- game.marginScore = 0;
99
- return;
100
- }
101
- if (p1.name === p2.name && p2.name === p3.name) {
102
- // 三個圖案相同
103
- this.marginScore += p1.scores[0];
104
- } else if (
105
- p1.name === p2.name ||
106
- p2.name === p3.name ||
107
- p1.name === p3.name
108
- ) {
109
- // 兩個圖案相同
110
- if (p1.name === p2.name) {
111
- this.marginScore += p1.scores[1];
112
- this.marginScore += p3.scores[2];
113
- } else if (p2.name === p3.name) {
114
- this.marginScore += p2.scores[1];
115
- this.marginScore += p1.scores[2];
116
- } else {
117
- this.marginScore += p1.scores[1];
118
- this.marginScore += p2.scores[2];
119
- }
120
- this.marginScore = Math.round(
121
- this.marginScore / mode.variable.twoMatchDivisor,
122
- );
123
- } else {
124
- // 三個圖案皆不同
125
- this.marginScore += p1.scores[2];
126
- this.marginScore += p2.scores[2];
127
- this.marginScore += p3.scores[2];
128
- this.marginScore = Math.round(
129
- this.marginScore / mode.variable.allDifferentDivisor,
130
- );
131
- }
132
- },
133
- roundEnd: (game) => {
134
- game.score += game.marginScore;
135
- },
136
- },
137
- variable: {
138
- twoMatchDivisor: 1.4,
139
- allDifferentDivisor: 3,
140
- },
141
- });
142
- this.addMode(this.__defaultMode__);
143
26
  }
144
27
 
145
- /**
146
- * 觸發指定事件。
147
- * @param event - 要觸發的事件名稱。
148
- */
149
- private emit(event: LaBaGEvent) {
150
- const listeners = this.eventListeners[event];
151
- for (let i = 0; i < listeners.length; i++) {
152
- listeners[i](this);
153
- }
154
- }
155
-
156
- /**
157
- * 新增事件監聽器。
158
- * @param event - 要監聽的事件名稱。
159
- * @param listener - 事件觸發時要執行的函式。
160
- * @remarks 可以為同一事件添加多個監聽器,這些監聽器將按照添加的順序依次執行。
161
- * @example
162
- * game.addEventListener("gameStart", (game) => {
163
- * console.log("遊戲開始了!");
164
- * });
165
- * game.addEventListener("roundEnd", (game) => {
166
- * console.log("一輪結束了!");
167
- * });
168
- */
169
- addEventListener(event: LaBaGEvent, callbackFn: (game: LaBaG) => void) {
170
- this.eventListeners[event].push(callbackFn);
171
- }
172
-
173
- /**
174
- * 移除事件監聽器。
175
- * @param event - 要移除監聽器的事件名稱。
176
- * @param listener - 要移除的監聽器函式。
177
- * @remarks 這將從指定事件的監聽器列表中移除第一個匹配的函式。
178
- * @example
179
- * const onGameStart = (game) => {
180
- * console.log("遊戲開始了!");
181
- * };
182
- * game.addEventListener("gameStart", onGameStart);
183
- * // 之後如果想要移除這個監聽器:
184
- * game.removeEventListener("gameStart", onGameStart);
185
- */
186
- removeEventListener(event: LaBaGEvent, callbackFn: (game: LaBaG) => void) {
187
- const listeners = this.eventListeners[event];
188
- const index = listeners.indexOf(callbackFn);
189
- if (index !== -1) {
190
- listeners.splice(index, 1);
191
- }
192
- }
193
-
194
- /**
195
- * 新增遊戲模式。
196
- * @param mode - 要新增的遊戲模式實例。
197
- * @remarks 這將把指定的模式添加到遊戲的模式列表中,並自動註冊該模式定義的事件監聽器。
198
- * @example
199
- * const myMode = new Mode({
200
- * active: false,
201
- * name: "myMode",
202
- * rates: {
203
- * gss: 10,
204
- * hhh: 20,
205
- * hentai: 30,
206
- * handson: 20,
207
- * kachu: 10,
208
- * rrr: 10,
209
- * },
210
- * });
211
- * game.addMode(myMode);
212
- */
213
- addMode(mode: Mode<any>) {
214
- this.modes.push(mode);
215
- // 註冊特定模式的監聽器
216
- Object.entries(mode.eventListener).forEach(([event, listener]) => {
217
- if (listener) {
218
- this.addEventListener(event as LaBaGEvent, (game) =>
219
- listener(game, mode),
220
- );
221
- }
222
- });
223
- }
224
-
225
- /**
226
- * 取得目前遊戲的相關設定
227
- */
228
- getCurrentConfig() {
229
- const activeModes = this.modes.filter((m) => m.active);
230
- if (activeModes.length === 0) {
231
- throw new Error("目前沒有啟用中的模式,無法轉動拉霸機。");
232
- }
233
- // 合併所有啟用中模式的機率設定
234
- const combinedRates: Record<PatternName, number> = {
235
- gss: 0,
236
- hhh: 0,
237
- hentai: 0,
238
- handson: 0,
239
- kachu: 0,
240
- rrr: 0,
241
- };
242
- for (let i = 0; i < activeModes.length; i++) {
243
- const mode = activeModes[i];
244
- for (const patternName in mode.rates) {
245
- combinedRates[patternName as PatternName] +=
246
- mode.rates[patternName as PatternName];
28
+ randomPattern(): Pattern {
29
+ const totalWeight = this.patterns.reduce(
30
+ (sum, pattern) => sum + pattern.weight,
31
+ 0,
32
+ );
33
+ const randNum = randInt(1, totalWeight);
34
+ let cumulativeWeight = 0;
35
+ for (const pattern of this.patterns) {
36
+ cumulativeWeight += pattern.weight;
37
+ if (randNum <= cumulativeWeight) {
38
+ return pattern;
247
39
  }
248
40
  }
249
-
250
- // 預先計算合併後的區間
251
- const ranges: { threshold: number; pattern: Pattern }[] = [];
252
- let acc = 0;
253
- for (const pattern of patterns) {
254
- const rate = combinedRates[pattern.name];
255
- if (rate !== undefined) {
256
- acc += rate;
257
- ranges.push({ threshold: acc, pattern });
258
- }
259
- }
260
-
261
- return { modes: activeModes, ranges };
262
- }
263
-
264
- init() {
265
- this.emit("gameStart");
266
- }
267
-
268
- roundStart() {
269
- this.emit("roundStart");
270
- }
271
-
272
- rollSlots() {
273
- this.emit("rollSlots");
274
- }
275
-
276
- calculateScore() {
277
- this.emit("calculateScore");
278
- }
279
-
280
- roundEnd() {
281
- this.emit("roundEnd");
282
- }
283
-
284
- gameOver() {
285
- this.emit("gameOver");
41
+ throw new Error("No pattern found");
286
42
  }
287
43
 
288
- play() {
289
- if (!this.isRunning) {
290
- throw new Error("遊戲已結束,無法繼續遊玩。");
44
+ caculateReward(reels: Pattern[]): number {
45
+ const patternCounts: { [key: string]: number } = {};
46
+ for (const pattern of reels) {
47
+ patternCounts[pattern.id] = (patternCounts[pattern.id] || 0) + 1;
291
48
  }
292
- this.roundStart();
293
- this.rollSlots();
294
- this.calculateScore();
295
- this.roundEnd();
296
- if (!this.isRunning) {
297
- this.gameOver();
49
+ let totalReward = 0;
50
+ for (const payout of this.payouts) {
51
+ if (patternCounts[payout.pattern_id] === payout.match_count) {
52
+ totalReward += payout.reward;
53
+ }
298
54
  }
299
- }
300
-
301
- getMode(name: ModeName) {
302
- return this.modes.find((m) => m.name === name);
303
- }
304
-
305
- get isRunning() {
306
- return this.played < this.times;
55
+ return totalReward;
307
56
  }
308
57
  }
package/src/test.ts CHANGED
@@ -1,45 +0,0 @@
1
- import { checker, labag } from "./index";
2
- import { recorder } from "./index";
3
- labag.addEventListener("gameStart", (game) => {
4
- console.log("Game Started!");
5
- console.log(`Total Rounds: ${game.times}\n`);
6
- });
7
- labag.addEventListener("roundStart", (game) => {
8
- console.log(`--- Round ${game.rounds} Start ---`);
9
- });
10
- labag.addEventListener("rollSlots", (game) => {
11
- const { modes, ranges } = game.getCurrentConfig();
12
- console.log(`Active Modes: ${modes.map((m) => m.name).join(", ")}`);
13
- console.log(
14
- `Probability Ranges: ${ranges
15
- .map((r) => `${r.pattern.name}<=${r.threshold}`)
16
- .join(", ")}`,
17
- );
18
- });
19
- labag.addEventListener("roundEnd", (game) => {
20
- console.log(game.patterns.map((p) => (p ? p.name : "null")).join(" | "));
21
- console.log(`Margin Score: ${game.marginScore}`);
22
- console.log(`Score: ${game.score}\n`);
23
- });
24
-
25
- labag.init();
26
- recorder.init();
27
-
28
- while (labag.isRunning) {
29
- labag.play();
30
- }
31
-
32
- console.log("Game Over");
33
- console.log(`Final Score: ${labag.score}`);
34
- console.log(
35
- `Active Modes at end: ${labag
36
- .getCurrentConfig()
37
- .modes.map((m) => m.name)
38
- .join(", ")}`,
39
- );
40
- console.log(`record: ${JSON.stringify(recorder.getRecord(), null, 2)}`);
41
- console.log(
42
- checker.check(recorder.getRecord())
43
- ? "Record is valid!"
44
- : "Record is invalid!",
45
- );
@@ -1,31 +1,12 @@
1
- import { LaBaG } from "src/labag";
2
- import { modes } from "src/modes";
3
- import { patterns } from "src/pattern";
4
-
5
- /**
6
- * 拉霸遊戲的事件類型。
7
- */
8
- export type LaBaGEvent =
9
- | "gameOver"
10
- | "gameStart"
11
- | "roundStart"
12
- | "roundEnd"
13
- | "rollSlots"
14
- | "calculateScore";
15
-
16
- /**
17
- * 代表一個圖案及其對應的分數。
18
- */
19
1
  export type Pattern = {
20
- /** 圖案名稱 */
21
- name: string;
22
- /** 對應的分數陣列 */
23
- scores: number[];
2
+ id: string;
3
+ weight: number;
4
+ image: string;
24
5
  };
25
6
 
26
- /**
27
- * 圖案名稱的型別。
28
- */
29
- export type PatternName = (typeof patterns)[number]["name"];
30
-
31
- export type ModeName = (typeof modes)[number]["name"] | "normal";
7
+ export type Payout = {
8
+ id: string;
9
+ pattern_id: Pattern["id"];
10
+ match_count: number;
11
+ reward: number;
12
+ };
package/src/mode.ts DELETED
@@ -1,90 +0,0 @@
1
- import { LaBaG } from "./labag";
2
- import { patterns } from "./pattern";
3
- import { LaBaGEvent, Pattern, PatternName } from "./types";
4
- interface ModeConfig<
5
- VariableType extends Record<string, any>,
6
- N extends string = string,
7
- > {
8
- active: boolean;
9
- name: N;
10
- rates: Record<PatternName, number>;
11
- eventListener?: Partial<
12
- Record<LaBaGEvent, (game: LaBaG, mode: Mode<VariableType>) => void>
13
- >;
14
- variable?: VariableType;
15
- }
16
-
17
- /**
18
- * 代表遊戲的一種模式,包含機率設定和事件監聽器。
19
- */
20
- export class Mode<
21
- VariableType extends Record<string, any> = Record<string, any>,
22
- N extends string = string,
23
- > implements ModeConfig<VariableType, N> {
24
- /** 模式是否啟用 */
25
- active: boolean;
26
- /** 模式名稱 */
27
- name: N;
28
- /** 各圖案出現的機率 */
29
- rates: Record<PatternName, number>;
30
- // 預先計算的區間,用於高效查找
31
- ranges: { threshold: number; pattern: Pattern }[];
32
- /** 事件監聽器 */
33
- eventListener: Partial<
34
- Record<LaBaGEvent, (game: LaBaG, mode: Mode<VariableType>) => void>
35
- >;
36
-
37
- /** 模式專屬的變數儲存空間 */
38
- variable: VariableType;
39
- /** 機率總和 */
40
-
41
- /**
42
- * 建立一個新的遊戲模式。
43
- * @param config 模式的設定,包括啟用狀態、名稱、機率、事件監聽器和專屬變數。
44
- * @remarks 會根據提供的機率設定預先計算出對應的區間,以便在遊戲中快速查找圖案。
45
- * @example
46
- * const myMode = new Mode({
47
- * active: false,
48
- * name: "myMode",
49
- * rates: {
50
- * gss: 10,
51
- * hhh: 20,
52
- * hentai: 30,
53
- * handson: 20,
54
- * kachu: 10,
55
- * rrr: 10,
56
- * },
57
- * eventListener: {
58
- * gameStart: (game, mode) => {
59
- * console.log("遊戲開始了!", mode.name);
60
- * },
61
- * roundEnd: (game, mode) => {
62
- * console.log("一輪結束了!", mode.name);
63
- * },
64
- * },
65
- * variable: {
66
- * myCustomValue: 123,
67
- * },
68
- * });
69
- */
70
- constructor(config: ModeConfig<VariableType, N>) {
71
- const { active, name, rates, eventListener, variable } = config;
72
- this.active = active;
73
- this.name = name;
74
- this.rates = rates;
75
- this.eventListener = eventListener ?? {};
76
- this.variable = variable ?? ({} as VariableType);
77
-
78
- // 預先計算機率區間
79
- this.ranges = [];
80
- let acc = 0;
81
- // 遍歷定義的圖案以確保順序一致
82
- for (const pattern of patterns) {
83
- const rate = rates[pattern.name];
84
- if (rate !== undefined) {
85
- acc += rate;
86
- this.ranges.push({ threshold: acc, pattern });
87
- }
88
- }
89
- }
90
- }
@@ -1,97 +0,0 @@
1
- import { patterns } from "../pattern";
2
- import { Mode } from "../mode";
3
- import { randInt } from "../utils/randInt";
4
-
5
- export default new Mode({
6
- active: false,
7
- name: "greenwei",
8
- rates: {
9
- gss: 0,
10
- hhh: 0,
11
- hentai: 0,
12
- handson: 0,
13
- kachu: 0,
14
- rrr: 0,
15
- },
16
- eventListener: {
17
- gameStart: (_, mode) => {
18
- mode.active = false;
19
- mode.variable.times = 0;
20
- mode.variable.count = 0;
21
- },
22
- roundStart: (_, mode) => {
23
- if (!mode.active) return;
24
- mode.variable.times -= 1;
25
- },
26
- rollSlots: (_, mode) => {
27
- mode.variable.randNum = randInt(1, 100);
28
- },
29
- calculateScore: (game, mode) => {
30
- if (mode.active) {
31
- game.marginScore = Math.round(
32
- game.marginScore * mode.variable.mutiplier,
33
- );
34
- }
35
- },
36
- roundEnd: (game, mode) => {
37
- const { patterns } = game;
38
- const { variable } = mode;
39
-
40
- let gssCount = 0;
41
- let allGSS = true;
42
- for (const p of patterns) {
43
- if (p?.name === mode.variable.bindPattern.name) {
44
- gssCount++;
45
- } else {
46
- allGSS = false;
47
- }
48
- }
49
-
50
- variable.count += gssCount;
51
-
52
- if (mode.active) {
53
- if (allGSS) {
54
- variable.times += mode.variable.extendTimes;
55
- }
56
- if (variable.times <= 0) {
57
- mode.active = false;
58
- }
59
- }
60
- if (!mode.active) {
61
- let activated = false;
62
- if (variable.randNum <= variable.rate && allGSS) {
63
- activated = true;
64
- variable.times += mode.variable.extendTimes;
65
- } else if (variable.count >= mode.variable.requiredBindPatternCount) {
66
- activated = true;
67
- variable.times += mode.variable.bonusTimes;
68
- variable.count -= mode.variable.requiredBindPatternCount;
69
- }
70
-
71
- if (activated) {
72
- mode.active = true;
73
- for (let i = 0; i < patterns.length; i++) {
74
- if (patterns[i]?.name === mode.variable.bindPattern.name) {
75
- patterns[i] = variable.pattern;
76
- }
77
- }
78
- }
79
- }
80
- },
81
- },
82
- variable: {
83
- times: 0,
84
- rate: 35,
85
- randNum: 0,
86
- count: 0,
87
- pattern: {
88
- name: "greenwei",
89
- scores: [800, 400, 180],
90
- },
91
- extendTimes: 2,
92
- bindPattern: patterns[0],
93
- bonusTimes: 2,
94
- requiredBindPatternCount: 20,
95
- mutiplier: 3,
96
- },
97
- });
@@ -1,5 +0,0 @@
1
- import greenwei from "./greenwei";
2
- import pikachu from "./pikachu";
3
- import superhhh from "./superhhh";
4
-
5
- export const modes = [pikachu, greenwei, superhhh] as const;
@@ -1,52 +0,0 @@
1
- import { patterns } from "../pattern";
2
- import { Mode } from "../mode";
3
-
4
- export default new Mode({
5
- active: false,
6
- name: "pikachu",
7
- rates: {
8
- gss: 0,
9
- hhh: 0,
10
- hentai: 0,
11
- handson: 0,
12
- kachu: 0,
13
- rrr: 0,
14
- },
15
- eventListener: {
16
- gameStart: (_, mode) => {
17
- mode.active = false;
18
- mode.variable.times = 0;
19
- },
20
- roundEnd: (game, mode) => {
21
- const { patterns } = game;
22
- const hasBindPattern = patterns.some(
23
- (p) => p && p.name === mode.variable.bindPattern.name,
24
- );
25
-
26
- if (!game.isRunning && hasBindPattern) {
27
- mode.active = true;
28
- game.played -= mode.variable.bonusRounds;
29
- mode.variable.times += 1;
30
- patterns.forEach((p, i) => {
31
- if (p?.name === mode.variable.bindPattern.name) {
32
- patterns[i] = mode.variable.pattern;
33
- }
34
- });
35
- return;
36
- }
37
-
38
- if (mode.active && hasBindPattern) {
39
- game.played -= Math.min(mode.variable.times, mode.variable.bonusRounds);
40
- }
41
- },
42
- },
43
- variable: {
44
- times: 0,
45
- pattern: {
46
- name: "pikachu",
47
- scores: [12000, 8000, 1250],
48
- },
49
- bindPattern: patterns[4],
50
- bonusRounds: 3,
51
- },
52
- });