dsh-blackjack 0.1.4 → 0.2.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 +43 -26
- package/blackjack.cordis.yml +10 -0
- package/dist/api.d.ts +27 -3
- package/dist/api.js +43 -3
- package/dist/client/Table.d.ts +10 -2
- package/dist/client/Table.js +39 -36
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.js +91 -9
- package/dist/client.js +12 -2
- package/dist/commands.d.ts +4 -1
- package/dist/commands.js +165 -80
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -0
- package/dist/copy.d.ts +16 -2
- package/dist/copy.js +14 -19
- package/dist/i18n/en.d.ts +110 -0
- package/dist/i18n/en.js +170 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/messages.d.ts +412 -0
- package/dist/i18n/messages.js +33 -0
- package/dist/i18n/zh.d.ts +110 -0
- package/dist/i18n/zh.js +168 -0
- package/dist/index.js +9 -2
- package/dist/render.d.ts +3 -3
- package/dist/render.js +19 -31
- package/dist/router.d.ts +16 -3
- package/dist/router.js +12 -11
- package/package.json +2 -1
package/dist/commands.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import { ApiError } from './api.js';
|
|
2
|
-
import {
|
|
3
|
-
const AGREE_HINT = '请先阅读须知并执行 /blackjack agree 后再使用。';
|
|
4
|
-
const NO_ROUND_HINT = '当前没有进行中的牌局,先用 /blackjack deal 开一局。';
|
|
5
|
-
const USAGE = 'agree | deal [注额] | exchange [数量] | reset';
|
|
6
|
-
const n = (v) => v.toLocaleString('en-US');
|
|
2
|
+
import { renderMe, renderRound } from './render.js';
|
|
7
3
|
const ACTIONS = [
|
|
8
|
-
{ name: 'bj-hit', action: 'hit'
|
|
9
|
-
{ name: 'bj-stand', action: 'stand'
|
|
10
|
-
{ name: 'bj-double', action: 'double'
|
|
11
|
-
{ name: 'bj-split', action: 'split'
|
|
12
|
-
{ name: 'bj-insure', action: 'insure'
|
|
13
|
-
{ name: 'bj-decline', action: 'decline'
|
|
4
|
+
{ name: 'bj-hit', action: 'hit' },
|
|
5
|
+
{ name: 'bj-stand', action: 'stand' },
|
|
6
|
+
{ name: 'bj-double', action: 'double' },
|
|
7
|
+
{ name: 'bj-split', action: 'split' },
|
|
8
|
+
{ name: 'bj-insure', action: 'insure' },
|
|
9
|
+
{ name: 'bj-decline', action: 'decline' },
|
|
14
10
|
];
|
|
15
11
|
function ok(text) {
|
|
16
12
|
return { kind: 'success', text };
|
|
@@ -26,10 +22,11 @@ function fail(text) {
|
|
|
26
22
|
*
|
|
27
23
|
* Handlers must never throw: an uncaught throw settles as `command/done`
|
|
28
24
|
* with `kind:'error'` and writes internal detail into the session log, so
|
|
29
|
-
* every branch below is wrapped to translate failures into
|
|
25
|
+
* every branch below is wrapped to translate failures into `m`'s
|
|
26
|
+
* plain-language copy (never internal status codes, URLs, or stack traces).
|
|
30
27
|
*/
|
|
31
28
|
export function registerCommands(ctx, deps) {
|
|
32
|
-
const { api, identity } = deps;
|
|
29
|
+
const { api, identity, m } = deps;
|
|
33
30
|
// Cheap in-memory hint for "which round is active" so most actions skip a
|
|
34
31
|
// round-trip; always falls back to the server (api.activeRound) when this
|
|
35
32
|
// is empty, e.g. right after a process restart.
|
|
@@ -37,33 +34,41 @@ export function registerCommands(ctx, deps) {
|
|
|
37
34
|
function noteRound(round) {
|
|
38
35
|
activeRoundId = round.phase === 'settled' ? undefined : round.id;
|
|
39
36
|
}
|
|
40
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Pull a numeric structured-detail field out of an `ApiError`, or
|
|
39
|
+
* `undefined` if it's missing or the wrong type.
|
|
40
|
+
*
|
|
41
|
+
* Missing/malformed is not the normal case — every code that ships with a
|
|
42
|
+
* detail field always sends it (see the site table in
|
|
43
|
+
* `packages/server/src/http.ts`'s `playerError` call sites) — but a
|
|
44
|
+
* cross-version mismatch (an older server that has the code but not yet the
|
|
45
|
+
* field) must be handled without inventing a number. **Do not default this
|
|
46
|
+
* to 0**: review caught an earlier version of this file doing exactly that,
|
|
47
|
+
* which renders a false statement ("needs 0 CHIP" / "the most you can raise
|
|
48
|
+
* is 0 CHIP") — a refusal quoting a threshold of zero is worse than a
|
|
49
|
+
* generic refusal, because it's actively wrong, not just vague. Every call
|
|
50
|
+
* site below falls back to `m.genericFailure()` when this returns
|
|
51
|
+
* `undefined`, rather than passing a fabricated number to a `Messages`
|
|
52
|
+
* method that promises a real one.
|
|
53
|
+
*/
|
|
54
|
+
function numberDetail(e, key) {
|
|
55
|
+
const v = e.details[key];
|
|
56
|
+
return typeof v === 'number' ? v : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Translate a caught failure into player-safe copy in the resolved locale. Never leaks status codes, URLs, stack traces, or the server's raw code/message. */
|
|
41
59
|
async function mapError(e, token) {
|
|
42
60
|
if (!(e instanceof ApiError))
|
|
43
|
-
return fail(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (e.status === 403 && e.code === 'below threshold')
|
|
49
|
-
return fail('余额还没到兑换门槛。');
|
|
50
|
-
if (e.status === 402)
|
|
51
|
-
return fail('余额不足以完成这个动作。');
|
|
52
|
-
// 404 = 这局在服务端已经不存在了(最常见的来路:玩家刚在图形牌桌上把它打
|
|
53
|
-
// 完,而这里的 activeRoundId 还停在旧 id 上)。缓存必须清掉,否则下一条动作
|
|
54
|
-
// 命令会拿着同一个死 id 再撞一次;提示也要说清楚该怎么办,而不是给一句通用
|
|
55
|
-
// 的"操作没有成功"。
|
|
56
|
-
if (e.status === 404) {
|
|
57
|
-
activeRoundId = undefined;
|
|
58
|
-
return fail(NO_ROUND_HINT);
|
|
59
|
-
}
|
|
60
|
-
if (e.status === 409) {
|
|
61
|
+
return fail(m.genericFailure());
|
|
62
|
+
// 409:已经有一局在进行中——把那一局连同牌面一起找回来展示,而不是只丢
|
|
63
|
+
// 一句"已经有一局在进行中"。找不到(网络失败,或局已经在这之间结束)就
|
|
64
|
+
// 退化成那句纯文本,绝不让这里的失败盖住原始的 409。
|
|
65
|
+
const activeRoundExists = async () => {
|
|
61
66
|
if (token) {
|
|
62
67
|
try {
|
|
63
68
|
const round = await api.activeRound(token);
|
|
64
69
|
if (round) {
|
|
65
70
|
noteRound(round);
|
|
66
|
-
return fail([
|
|
71
|
+
return fail([m.roundAlreadyInProgress(), renderRound(round, m)].join('\n'));
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
catch {
|
|
@@ -71,15 +76,93 @@ export function registerCommands(ctx, deps) {
|
|
|
71
76
|
// failure here mask the original 409.
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
return fail(
|
|
79
|
+
return fail(m.roundAlreadyInProgress());
|
|
80
|
+
};
|
|
81
|
+
// 这局在服务端已经不存在了(最常见的来路:玩家刚在图形牌桌上把它打完,
|
|
82
|
+
// 而这里的 activeRoundId 还停在旧 id 上)。缓存必须清掉,否则下一条动作
|
|
83
|
+
// 命令会拿着同一个死 id 再撞一次;提示也要说清楚该怎么办,而不是给一句
|
|
84
|
+
// 通用的"操作没有成功"。
|
|
85
|
+
const roundNotFound = () => {
|
|
86
|
+
activeRoundId = undefined;
|
|
87
|
+
return fail(m.noRoundHint());
|
|
88
|
+
};
|
|
89
|
+
// 按 code 匹配是 Task 7 的核心:改之前这里比对的其实是服务端的英文散文
|
|
90
|
+
// (`ApiError.code` 曾经被塞进了 message),一旦服务端改一个字,这些分支
|
|
91
|
+
// 就悄悄失效却不报错。现在 code 是服务端 `playerError()` 发的稳定
|
|
92
|
+
// kebab-case 标识,是两边的 API 契约。
|
|
93
|
+
//
|
|
94
|
+
// **这个 switch 必须先于 status>=500 短路运行**(review 抓到的一个真实
|
|
95
|
+
// bug):`exchange-unavailable` 就是走 503 的,之前的版本在 switch 前面
|
|
96
|
+
// 挡了一句"status===0 || status>=500 就返回 serviceUnreachable()",
|
|
97
|
+
// 那句"暂时连不上牌桌服务"的通用兜底会先接管,`exchange-unavailable`
|
|
98
|
+
// 这个 case 因此永远执行不到——牌局本身明明是好的,玩家却被告知"整个
|
|
99
|
+
// 服务连不上",误导他去重启插件。5xx 的兜底现在挪到了 `default` 分支里,
|
|
100
|
+
// 只在 code 不在下面这张已知表里时才生效。
|
|
101
|
+
switch (e.code) {
|
|
102
|
+
case 'no-free-hands-left': return fail(m.dailyFreeHandsExhausted());
|
|
103
|
+
case 'below-exchange-threshold': {
|
|
104
|
+
const thresholdChips = numberDetail(e, 'thresholdChips');
|
|
105
|
+
return thresholdChips === undefined ? fail(m.genericFailure()) : fail(m.belowExchangeThreshold(thresholdChips));
|
|
106
|
+
}
|
|
107
|
+
case 'would-exceed-season-cap': {
|
|
108
|
+
const capChips = numberDetail(e, 'capChips');
|
|
109
|
+
const exchangedThisSeasonChips = numberDetail(e, 'exchangedThisSeasonChips');
|
|
110
|
+
return capChips === undefined || exchangedThisSeasonChips === undefined
|
|
111
|
+
? fail(m.genericFailure())
|
|
112
|
+
: fail(m.seasonCapReached(capChips, exchangedThisSeasonChips));
|
|
113
|
+
}
|
|
114
|
+
case 'github-binding-required': return fail(m.githubBindingRequired());
|
|
115
|
+
case 'exchange-unavailable': return fail(m.exchangeUnavailable());
|
|
116
|
+
case 'insufficient-balance': return fail(m.insufficientBalance());
|
|
117
|
+
case 'bet-amount-too-high': {
|
|
118
|
+
const maxChips = numberDetail(e, 'maxChips');
|
|
119
|
+
return maxChips === undefined ? fail(m.genericFailure()) : fail(m.betAmountAboveLimit(maxChips));
|
|
120
|
+
}
|
|
121
|
+
case 'illegal-action': return fail(m.illegalAction());
|
|
122
|
+
case 'register-rate-limited': return fail(m.registerRateLimited());
|
|
123
|
+
// 401:服务端不认识本机这枚令牌。这条以前是裸 `{ error: 'unauthorized' }`,
|
|
124
|
+
// 没有 code,于是落进 default → `genericFailure()`,玩家看到"请稍后再试"
|
|
125
|
+
// ——而这个场景里稍后再试永远不会好(令牌不会自己变回被认识的样子)。
|
|
126
|
+
// 现在服务端发 `unknown-player-token`,这里给出真的能走出去的两步。
|
|
127
|
+
case 'unknown-player-token': return fail(m.unknownPlayerToken());
|
|
128
|
+
case 'round-not-found': return roundNotFound();
|
|
129
|
+
case 'active-round-exists': return activeRoundExists();
|
|
130
|
+
// 这四个 code 存在是为了 API 契约的完整性(服务端所有玩家可见错误都
|
|
131
|
+
// 带稳定 code),但插件自己从不会产出触发它们的请求:`mode` 是硬编码
|
|
132
|
+
// 常量,注额/兑换数量在发出前已经做过整数校验(`invalid-bet-amount`
|
|
133
|
+
// 现在专指"不是正整数"这个格式问题,`doDeal` 早在发出请求前就挡掉了;
|
|
134
|
+
// 真正现实可达的"超过上限"已经拆成了上面的 `bet-amount-too-high`),
|
|
135
|
+
// `bet-not-insurable` 只在运营者把汇率配成奇数时出现。真撞上说明协议
|
|
136
|
+
// 之外出了什么问题,通用兜底文案已经足够,不值得为一句玩家永远看不到
|
|
137
|
+
// 的话专门写文案。
|
|
138
|
+
case 'invalid-mode':
|
|
139
|
+
case 'invalid-chip-amount':
|
|
140
|
+
case 'invalid-bet-amount':
|
|
141
|
+
case 'bet-not-insurable':
|
|
142
|
+
return fail(m.genericFailure());
|
|
143
|
+
default:
|
|
144
|
+
// 未知 code:可能来自比这版插件更新的服务端(加了新 code 没同步),
|
|
145
|
+
// 也可能是插件这边漏映射了。**这正是"兜底必须存在"这条要求所在**:
|
|
146
|
+
// 按 HTTP 状态语义能猜的就猜一个更具体的措辞,猜不动就落到通用兜底
|
|
147
|
+
// ——永远不把服务端的英文诊断散文或裸 code 甩给玩家,也不能让玩家
|
|
148
|
+
// 看到一个空字符串。网络层失败(status 0)和任何未识别的 5xx 都落在
|
|
149
|
+
// 这里,不在已知 code 表里的每一条也是。
|
|
150
|
+
if (e.status === 0 || e.status >= 500)
|
|
151
|
+
return fail(m.serviceUnreachable());
|
|
152
|
+
if (e.status === 402)
|
|
153
|
+
return fail(m.insufficientBalance());
|
|
154
|
+
if (e.status === 404)
|
|
155
|
+
return roundNotFound();
|
|
156
|
+
if (e.status === 409)
|
|
157
|
+
return activeRoundExists();
|
|
158
|
+
return fail(m.genericFailure());
|
|
75
159
|
}
|
|
76
|
-
return fail('操作没有成功,请稍后再试。');
|
|
77
160
|
}
|
|
78
161
|
/** Resolve the bearer token, guiding to consent when absent, then run `body` catching every failure. */
|
|
79
162
|
async function withToken(body) {
|
|
80
163
|
const token = await identity.current();
|
|
81
164
|
if (!token)
|
|
82
|
-
return fail(
|
|
165
|
+
return fail(m.agreeHint());
|
|
83
166
|
try {
|
|
84
167
|
return await body(token);
|
|
85
168
|
}
|
|
@@ -100,28 +183,36 @@ export function registerCommands(ctx, deps) {
|
|
|
100
183
|
async function showStatus() {
|
|
101
184
|
const token = await identity.current();
|
|
102
185
|
if (!token)
|
|
103
|
-
return ok(
|
|
186
|
+
return ok(m.consentText());
|
|
104
187
|
try {
|
|
105
188
|
const round = await api.activeRound(token);
|
|
106
189
|
if (round) {
|
|
107
190
|
noteRound(round);
|
|
108
|
-
return ok(renderRound(round));
|
|
191
|
+
return ok(renderRound(round, m));
|
|
109
192
|
}
|
|
110
193
|
activeRoundId = undefined;
|
|
111
194
|
const me = await api.me(token);
|
|
112
|
-
return ok(renderMe(me));
|
|
195
|
+
return ok(renderMe(me, m));
|
|
113
196
|
}
|
|
114
197
|
catch (e) {
|
|
115
198
|
return mapError(e, token);
|
|
116
199
|
}
|
|
117
200
|
}
|
|
118
|
-
|
|
201
|
+
/**
|
|
202
|
+
* 同意须知。**必须先把须知显示出来,再由玩家显式确认。**
|
|
203
|
+
*
|
|
204
|
+
* 原先一步就同意,须知全文只在不带参数的 /blackjack 里显示,没有任何东西
|
|
205
|
+
* 强制这个顺序——直接打 `/blackjack agree` 就在没见过条款的情况下同意了。
|
|
206
|
+
* 这份须知(第三方声明、不可提现、不可转让、赛季有效期)是整个合规定位的
|
|
207
|
+
* 载体,未读即同意等于让它失去意义。改成两步,与 `reset confirm` 同一个模式。
|
|
208
|
+
*/
|
|
209
|
+
async function doAgree(confirmArg) {
|
|
210
|
+
if (confirmArg !== 'confirm') {
|
|
211
|
+
return ok(m.consentText());
|
|
212
|
+
}
|
|
119
213
|
try {
|
|
120
214
|
await identity.accept();
|
|
121
|
-
return ok(
|
|
122
|
-
'已确认须知,欢迎上桌。',
|
|
123
|
-
'/blackjack deal 开始今天的免费手,或 /blackjack deal <注额> 用余额加注。',
|
|
124
|
-
].join('\n'));
|
|
215
|
+
return ok(m.agreeWelcome());
|
|
125
216
|
}
|
|
126
217
|
catch (e) {
|
|
127
218
|
return mapError(e);
|
|
@@ -137,10 +228,10 @@ export function registerCommands(ctx, deps) {
|
|
|
137
228
|
return { mode: 'raise', betChips: bet };
|
|
138
229
|
})();
|
|
139
230
|
if (!body)
|
|
140
|
-
return fail(
|
|
231
|
+
return fail(m.invalidBetAmount());
|
|
141
232
|
const round = await api.startRound(token, body);
|
|
142
233
|
noteRound(round);
|
|
143
|
-
return ok(renderRound(round));
|
|
234
|
+
return ok(renderRound(round, m));
|
|
144
235
|
}
|
|
145
236
|
/**
|
|
146
237
|
* Exchange has TWO paths, and which one applies is a property of the player,
|
|
@@ -165,45 +256,39 @@ export function registerCommands(ctx, deps) {
|
|
|
165
256
|
const minutes = Math.max(1, Math.round(pairing.expiresInSeconds / 60));
|
|
166
257
|
// 只给链接,不单独列配对码:链接里已经含它了,而单独列出会让人以为要
|
|
167
258
|
// 手动填到某处(运营者第一次用就这么理解了,连开了三次都没敢往下走)。
|
|
168
|
-
return ok(
|
|
169
|
-
'首次兑换要先绑定 GitHub,只需做这一次。',
|
|
170
|
-
'',
|
|
171
|
-
'请在浏览器里打开这个链接:',
|
|
172
|
-
pairing.url,
|
|
173
|
-
'',
|
|
174
|
-
'打开后点「用 GitHub 继续」授权,然后在同一个页面填写要兑换的数量并提交。',
|
|
175
|
-
`链接约 ${minutes} 分钟内有效;过期了就再执行一次 /blackjack exchange 拿一个新的。`,
|
|
176
|
-
'绑定完成后,以后直接用 /blackjack exchange <数量> 兑换,不用再开浏览器。',
|
|
177
|
-
].join('\n'));
|
|
259
|
+
return ok(m.pairingInstructions(pairing.url, minutes));
|
|
178
260
|
}
|
|
179
261
|
if (amountArg === undefined) {
|
|
180
|
-
return fail(
|
|
181
|
-
`已绑定 GitHub 账号:${me.githubLogin}`,
|
|
182
|
-
`当前余额:${n(me.chips)} CHIP 兑换门槛:${n(me.exchangeThresholdChips)} CHIP`,
|
|
183
|
-
'请用 /blackjack exchange <数量> 指定要兑换的 CHIP 数量。',
|
|
184
|
-
].join('\n'));
|
|
262
|
+
return fail(m.exchangeAmountPrompt(me.githubLogin, me.chips, me.exchangeThresholdChips));
|
|
185
263
|
}
|
|
186
264
|
const chips = Number(amountArg);
|
|
187
265
|
if (!Number.isInteger(chips) || chips <= 0)
|
|
188
|
-
return fail(
|
|
266
|
+
return fail(m.invalidExchangeAmount());
|
|
189
267
|
const result = await api.exchange(token, chips);
|
|
190
268
|
// 必须讲清楚这份额度什么时候会被用上:运营者兑换完就问"我该怎么用"——
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
269
|
+
// 因为兑换成功的提示从没说过触发条件,玩家以为兑换完就能直接花。这句话现在
|
|
270
|
+
// 由插件自己按 result.quota 里的结构化事实拼词(m.exchangedQuotaNote),
|
|
271
|
+
// 不再是服务端返回的现成散文(Task 6)。result.quota 是可选的:插件发到
|
|
272
|
+
// npm 后可能连到一个比这个字段更早的服务端(还在吐老的 note 字段),这种
|
|
273
|
+
// 情况下跳过这一行,而不是在 result.quota.inactiveRecycleDays 上崩溃。
|
|
274
|
+
const lines = [m.exchangeSuccess(result.chips, result.exchangedChips)];
|
|
275
|
+
// quota 的两个字段都要真的用上。`expiresAtSeasonEnd` 以前是"发了但没人读"
|
|
276
|
+
// ——服务端一路把它传到这里,插件却无条件说「它不会因赛季结束而清零」。
|
|
277
|
+
// 服务端哪天真改成赛季末回收、老实发 true,这句话就成了又一次假承诺
|
|
278
|
+
// (和当年的「有效期至本赛季末」同一个形状,方向反过来)。交给 Messages
|
|
279
|
+
// 按事实挑措辞,插件这边不做措辞判断。
|
|
280
|
+
if (result.quota)
|
|
281
|
+
lines.push(m.exchangedQuotaNote(result.quota.inactiveRecycleDays, result.quota.expiresAtSeasonEnd));
|
|
282
|
+
return ok(lines.join('\n'));
|
|
198
283
|
}
|
|
199
284
|
async function doReset(confirmArg) {
|
|
200
285
|
if (confirmArg !== 'confirm') {
|
|
201
|
-
return ok(
|
|
286
|
+
return ok(m.resetConfirmPrompt());
|
|
202
287
|
}
|
|
203
288
|
try {
|
|
204
289
|
await identity.clear();
|
|
205
290
|
activeRoundId = undefined;
|
|
206
|
-
return ok(
|
|
291
|
+
return ok(m.resetDone());
|
|
207
292
|
}
|
|
208
293
|
catch (e) {
|
|
209
294
|
return mapError(e);
|
|
@@ -222,13 +307,13 @@ export function registerCommands(ctx, deps) {
|
|
|
222
307
|
try {
|
|
223
308
|
token = await identity.current();
|
|
224
309
|
if (!token)
|
|
225
|
-
return fail(
|
|
310
|
+
return fail(m.agreeHint());
|
|
226
311
|
const id = await currentRoundId(token);
|
|
227
312
|
if (!id)
|
|
228
|
-
return fail(
|
|
313
|
+
return fail(m.noRoundHint());
|
|
229
314
|
const result = await api.act(token, id, action);
|
|
230
315
|
noteRound(result);
|
|
231
|
-
return ok(renderRound(result));
|
|
316
|
+
return ok(renderRound(result, m));
|
|
232
317
|
}
|
|
233
318
|
catch (e) {
|
|
234
319
|
return mapError(e, token);
|
|
@@ -240,11 +325,11 @@ export function registerCommands(ctx, deps) {
|
|
|
240
325
|
const sub = parts[0] ?? '';
|
|
241
326
|
switch (sub) {
|
|
242
327
|
case '': return await showStatus();
|
|
243
|
-
case 'agree': return await doAgree();
|
|
328
|
+
case 'agree': return await doAgree(parts[1]);
|
|
244
329
|
case 'deal': return await withToken((token) => doDeal(token, parts[1]));
|
|
245
330
|
case 'exchange': return await withToken((token) => doExchange(token, parts[1]));
|
|
246
331
|
case 'reset': return await doReset(parts[1]);
|
|
247
|
-
default: return fail(
|
|
332
|
+
default: return fail(m.unknownSubcommand(sub, m.commandUsage()));
|
|
248
333
|
}
|
|
249
334
|
}
|
|
250
335
|
catch (e) {
|
|
@@ -256,14 +341,14 @@ export function registerCommands(ctx, deps) {
|
|
|
256
341
|
}
|
|
257
342
|
ctx.commands.register({
|
|
258
343
|
name: 'blackjack',
|
|
259
|
-
description:
|
|
260
|
-
input: { hint:
|
|
344
|
+
description: m.blackjackCommandDescription(),
|
|
345
|
+
input: { hint: m.commandUsage() },
|
|
261
346
|
handler: blackjackHandler,
|
|
262
347
|
});
|
|
263
|
-
for (const { name, action
|
|
348
|
+
for (const { name, action } of ACTIONS) {
|
|
264
349
|
ctx.commands.register({
|
|
265
350
|
name,
|
|
266
|
-
description,
|
|
351
|
+
description: m.actionDescriptions[action],
|
|
267
352
|
handler: () => doAction(action),
|
|
268
353
|
});
|
|
269
354
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { LocaleId } from './i18n/index.js';
|
|
2
3
|
/** Cordis plugin name, also used in loader diagnostics and log lines. */
|
|
3
4
|
export declare const PLUGIN_NAME = "dsh-blackjack";
|
|
4
5
|
/**
|
|
@@ -9,5 +10,6 @@ export declare const TOKEN_REF = "DSH_BLACKJACK_TOKEN";
|
|
|
9
10
|
export interface ConfigType {
|
|
10
11
|
serverUrl: string;
|
|
11
12
|
enabled: boolean;
|
|
13
|
+
locale: LocaleId;
|
|
12
14
|
}
|
|
13
15
|
export declare const Config: Schema<Partial<ConfigType>, ConfigType>;
|
package/dist/config.js
CHANGED
|
@@ -13,4 +13,9 @@ export const Config = Schema.object({
|
|
|
13
13
|
.default('https://server-production-493c.up.railway.app')
|
|
14
14
|
.description('牌局服务端地址;自建奖池时改成你自己的部署地址'),
|
|
15
15
|
enabled: Schema.boolean().default(true).description('停用后不注册命令也不挂路由'),
|
|
16
|
+
// 命令回复的语言。**图形牌桌不看这一项**——它自动跟随 dsh 的语言设置。
|
|
17
|
+
// Node 侧读不到宿主的 locale(实测确认,见 spec),所以纯终端 profile 需要
|
|
18
|
+
// 在这里设一次。
|
|
19
|
+
locale: Schema.union(['en', 'zh']).default('en')
|
|
20
|
+
.description('命令回复的语言;图形牌桌自动跟随 dsh 的设置'),
|
|
16
21
|
});
|
package/dist/copy.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { RoundView } from './api.js';
|
|
2
|
+
import type { Messages } from './i18n/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* 牌局文案的单一来源。命令面(render.ts)与图形牌桌(client/Table.tsx)共用
|
|
5
|
+
* 这里的逻辑,否则两边会各说各话——而运营者第一次自己上手时,正是因为
|
|
6
|
+
* 界面没讲清楚"免费手"和"加注"的区别而看不懂在玩什么。
|
|
7
|
+
*
|
|
8
|
+
* 措辞本身来自 `m: Messages`(见 src/i18n),这里只负责"该说哪句"的判断逻辑:
|
|
9
|
+
* 免费/加注两种模式分支、平局要先于赢判断(加注局平局也会退回本金,
|
|
10
|
+
* payoutChips > 0)。
|
|
11
|
+
*
|
|
12
|
+
* 红线(见 render.ts 的 BANNED,及 i18n/messages.ts 的 BANNED_EN):单位一律
|
|
13
|
+
* CHIP,不用「筹码」;不出现赌博词汇。
|
|
14
|
+
* @module dsh-blackjack/copy
|
|
15
|
+
*/
|
|
2
16
|
/** 这一局的性质与代价。进行中时显示,让玩家随时知道自己在打哪种局。 */
|
|
3
|
-
export declare function stakeLine(round: RoundView): string;
|
|
17
|
+
export declare function stakeLine(round: RoundView, m: Messages): string;
|
|
4
18
|
/**
|
|
5
19
|
* 结算结果。分模式措辞——原先两种局共用一句「本手没有收获,明天再来」,
|
|
6
20
|
* 而加注局只要有余额马上就能再来,那句话是错的。
|
|
7
21
|
*/
|
|
8
|
-
export declare function outcomeLine(round: RoundView): string;
|
|
22
|
+
export declare function outcomeLine(round: RoundView, m: Messages): string;
|
package/dist/copy.js
CHANGED
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 牌局文案的单一来源。命令面(render.ts)与图形牌桌(client/Table.tsx)共用
|
|
3
|
-
*
|
|
3
|
+
* 这里的逻辑,否则两边会各说各话——而运营者第一次自己上手时,正是因为
|
|
4
4
|
* 界面没讲清楚"免费手"和"加注"的区别而看不懂在玩什么。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 措辞本身来自 `m: Messages`(见 src/i18n),这里只负责"该说哪句"的判断逻辑:
|
|
7
|
+
* 免费/加注两种模式分支、平局要先于赢判断(加注局平局也会退回本金,
|
|
8
|
+
* payoutChips > 0)。
|
|
9
|
+
*
|
|
10
|
+
* 红线(见 render.ts 的 BANNED,及 i18n/messages.ts 的 BANNED_EN):单位一律
|
|
11
|
+
* CHIP,不用「筹码」;不出现赌博词汇。
|
|
7
12
|
* @module dsh-blackjack/copy
|
|
8
13
|
*/
|
|
9
|
-
const n = (v) => v.toLocaleString('en-US');
|
|
10
14
|
/** 这一局的性质与代价。进行中时显示,让玩家随时知道自己在打哪种局。 */
|
|
11
|
-
export function stakeLine(round) {
|
|
12
|
-
return round.mode === 'free'
|
|
13
|
-
? `这是免费的一局:赢了拿 ${n(round.betChips)} CHIP,输了不损失任何东西。`
|
|
14
|
-
: `本局已加注 ${n(round.betChips)} CHIP,从你的余额里出。`;
|
|
15
|
+
export function stakeLine(round, m) {
|
|
16
|
+
return round.mode === 'free' ? m.freeStakeLine(round.betChips) : m.raiseStakeLine(round.betChips);
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* 结算结果。分模式措辞——原先两种局共用一句「本手没有收获,明天再来」,
|
|
18
20
|
* 而加注局只要有余额马上就能再来,那句话是错的。
|
|
19
21
|
*/
|
|
20
|
-
export function outcomeLine(round) {
|
|
22
|
+
export function outcomeLine(round, m) {
|
|
21
23
|
const payout = round.payoutChips ?? 0;
|
|
22
24
|
const bet = round.betChips;
|
|
23
25
|
// 加注局平局会原样退回本金,payout > 0 —— 先判平局,否则会被说成赢了。
|
|
24
26
|
if (round.hands.length > 0 && round.hands.every((h) => h.outcome === 'push')) {
|
|
25
|
-
return round.mode === 'free'
|
|
26
|
-
? '平局。这手不损失也不获得。'
|
|
27
|
-
: `平局。加注的 ${n(bet)} CHIP 原样退回。`;
|
|
28
|
-
}
|
|
29
|
-
if (payout > 0) {
|
|
30
|
-
return round.mode === 'free'
|
|
31
|
-
? `赢了,拿到 ${n(payout)} CHIP。`
|
|
32
|
-
: `赢了,拿回 ${n(payout)} CHIP(加注 ${n(bet)})。`;
|
|
27
|
+
return round.mode === 'free' ? m.freePush() : m.raisePush(bet);
|
|
33
28
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
if (payout > 0)
|
|
30
|
+
return round.mode === 'free' ? m.freeWin(payout) : m.raiseWin(payout, bet);
|
|
31
|
+
return round.mode === 'free' ? m.freeLose() : m.raiseLose(bet);
|
|
37
32
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export declare const en: {
|
|
2
|
+
freeStakeLine: (bet: number) => string;
|
|
3
|
+
raiseStakeLine: (bet: number) => string;
|
|
4
|
+
freeWin: (payout: number) => string;
|
|
5
|
+
freeLose: () => string;
|
|
6
|
+
freePush: () => string;
|
|
7
|
+
raiseWin: (payout: number, bet: number) => string;
|
|
8
|
+
raiseLose: (bet: number) => string;
|
|
9
|
+
raisePush: (bet: number) => string;
|
|
10
|
+
handLabel: () => string;
|
|
11
|
+
splitHandLabel: (handIndex: number) => string;
|
|
12
|
+
dealerLabel: () => string;
|
|
13
|
+
totalInParens: (total: number, isSoft: boolean) => string;
|
|
14
|
+
doubled: () => string;
|
|
15
|
+
actionsPrefix: () => string;
|
|
16
|
+
actionLabels: {
|
|
17
|
+
hit: string;
|
|
18
|
+
stand: string;
|
|
19
|
+
double: string;
|
|
20
|
+
split: string;
|
|
21
|
+
insure: string;
|
|
22
|
+
decline: string;
|
|
23
|
+
};
|
|
24
|
+
actionsSeparator: () => string;
|
|
25
|
+
balanceLine: (chips: number, exchangedChips: number) => string;
|
|
26
|
+
freeHandsLine: (freeHandsRemaining: number, seasonId: string) => string;
|
|
27
|
+
thresholdLine: (exchangeThresholdChips: number) => string;
|
|
28
|
+
pointsLine: (points: number) => string;
|
|
29
|
+
poolEmptyNotice: () => string;
|
|
30
|
+
pointsGrantedNotice: (points: number) => string;
|
|
31
|
+
chipValidityNotice: () => string;
|
|
32
|
+
consentText: () => string;
|
|
33
|
+
agreeHint: () => string;
|
|
34
|
+
noRoundHint: () => string;
|
|
35
|
+
commandUsage: () => string;
|
|
36
|
+
actionDescriptions: {
|
|
37
|
+
hit: string;
|
|
38
|
+
stand: string;
|
|
39
|
+
double: string;
|
|
40
|
+
split: string;
|
|
41
|
+
insure: string;
|
|
42
|
+
decline: string;
|
|
43
|
+
};
|
|
44
|
+
blackjackCommandDescription: () => string;
|
|
45
|
+
genericFailure: () => string;
|
|
46
|
+
serviceUnreachable: () => string;
|
|
47
|
+
dailyFreeHandsExhausted: () => string;
|
|
48
|
+
belowExchangeThreshold: (thresholdChips: number) => string;
|
|
49
|
+
insufficientBalance: () => string;
|
|
50
|
+
roundAlreadyInProgress: () => string;
|
|
51
|
+
githubBindingRequired: () => string;
|
|
52
|
+
seasonCapReached: (capChips: number, exchangedThisSeasonChips: number) => string;
|
|
53
|
+
exchangeUnavailable: () => string;
|
|
54
|
+
betAmountAboveLimit: (maxChips: number) => string;
|
|
55
|
+
illegalAction: () => string;
|
|
56
|
+
registerRateLimited: () => string;
|
|
57
|
+
unknownPlayerToken: () => string;
|
|
58
|
+
agreeWelcome: () => string;
|
|
59
|
+
invalidBetAmount: () => string;
|
|
60
|
+
pairingInstructions: (url: string, minutes: number) => string;
|
|
61
|
+
exchangeAmountPrompt: (githubLogin: string, chips: number, exchangeThresholdChips: number) => string;
|
|
62
|
+
invalidExchangeAmount: () => string;
|
|
63
|
+
exchangeSuccess: (chips: number, exchangedChips: number) => string;
|
|
64
|
+
exchangedQuotaNote: (inactiveRecycleDays: number, expiresAtSeasonEnd: boolean) => string;
|
|
65
|
+
resetConfirmPrompt: () => string;
|
|
66
|
+
resetDone: () => string;
|
|
67
|
+
unknownSubcommand: (sub: string, usage: string) => string;
|
|
68
|
+
poolSwitchNotice: () => string;
|
|
69
|
+
noCredentialHint: () => string;
|
|
70
|
+
hiddenCardLabel: () => string;
|
|
71
|
+
tableHandLabel: () => string;
|
|
72
|
+
tableSplitHandLabel: (handIndex: number) => string;
|
|
73
|
+
tableDealerLabel: () => string;
|
|
74
|
+
tableActionLabels: {
|
|
75
|
+
hit: string;
|
|
76
|
+
stand: string;
|
|
77
|
+
double: string;
|
|
78
|
+
split: string;
|
|
79
|
+
insure: string;
|
|
80
|
+
decline: string;
|
|
81
|
+
};
|
|
82
|
+
tableBalanceHeader: (chips: number, freeHandsRemaining: number) => string;
|
|
83
|
+
tablePointsSuffix: (points: number) => string;
|
|
84
|
+
tableExchangedSuffix: (exchangedChips: number) => string;
|
|
85
|
+
tableExchangeThresholdReached: (exchangeThresholdChips: number) => string;
|
|
86
|
+
tableExchangeThresholdShort: (shortChips: number, exchangeThresholdChips: number) => string;
|
|
87
|
+
tableExchangedQuotaNote: (exchangedChips: number) => string;
|
|
88
|
+
tablePoolEmptyNotice: () => string;
|
|
89
|
+
actionShortfallHint: (costChips: number) => string;
|
|
90
|
+
tableCostSuffix: (costChips: number) => string;
|
|
91
|
+
betAmountMissing: () => string;
|
|
92
|
+
betAmountExceedsCap: (maxChips: number) => string;
|
|
93
|
+
betAmountExceedsBalance: () => string;
|
|
94
|
+
customBetToggle: () => string;
|
|
95
|
+
customBetSubmit: () => string;
|
|
96
|
+
customBetRange: (maxChips: number) => string;
|
|
97
|
+
noActiveRoundNotice: () => string;
|
|
98
|
+
freeHandsGroupTitle: () => string;
|
|
99
|
+
freeHandsRemainingToday: (freeHandsRemaining: number) => string;
|
|
100
|
+
freeHandsExhaustedToday: () => string;
|
|
101
|
+
freeHandExplainer: (baseChips: number, naturalBlackjackPayoutChips: number) => string;
|
|
102
|
+
startFreeHandButton: () => string;
|
|
103
|
+
raiseGroupTitle: () => string;
|
|
104
|
+
insufficientBalanceSuffix: () => string;
|
|
105
|
+
raiseExplainer: () => string;
|
|
106
|
+
rematchFreeLabel: () => string;
|
|
107
|
+
dealButtonFreeHandsSuffix: (freeHandsRemaining: number) => string;
|
|
108
|
+
dismissButton: () => string;
|
|
109
|
+
actionFailedNotice: () => string;
|
|
110
|
+
};
|