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.
@@ -0,0 +1,170 @@
1
+ const n = (v) => v.toLocaleString('en-US');
2
+ export const en = {
3
+ freeStakeLine: (bet) => `This hand is free: win and you take ${n(bet)} CHIP; lose and you are out nothing.`,
4
+ raiseStakeLine: (bet) => `${n(bet)} CHIP is committed from your balance for this hand.`,
5
+ freeWin: (payout) => `You won ${n(payout)} CHIP.`,
6
+ freeLose: () => 'This hand did not win. A free hand costs you nothing.',
7
+ freePush: () => 'A tie. Nothing gained, nothing lost.',
8
+ raiseWin: (payout, bet) => `You won — ${n(payout)} CHIP back (${n(bet)} committed).`,
9
+ raiseLose: (bet) => `This hand lost; the ${n(bet)} CHIP you committed is gone.`,
10
+ raisePush: (bet) => `A tie. Your ${n(bet)} CHIP comes back.`,
11
+ handLabel: () => 'Your hand: ',
12
+ splitHandLabel: (handIndex) => `Hand ${handIndex}: `,
13
+ dealerLabel: () => 'Dealer: ',
14
+ totalInParens: (total, isSoft) => ` (${isSoft ? 'soft ' : ''}${total})`,
15
+ doubled: () => ' [doubled]',
16
+ actionsPrefix: () => 'Available: ',
17
+ actionLabels: {
18
+ hit: '/bj-hit hit',
19
+ stand: '/bj-stand stand',
20
+ double: '/bj-double double down',
21
+ split: '/bj-split split',
22
+ insure: '/bj-insure buy insurance',
23
+ decline: '/bj-decline decline insurance',
24
+ },
25
+ actionsSeparator: () => ' ',
26
+ balanceLine: (chips, exchangedChips) => `Balance: ${n(chips)} CHIP Exchanged so far: ${n(exchangedChips)} CHIP`,
27
+ freeHandsLine: (freeHandsRemaining, seasonId) => `Free hands left today: ${freeHandsRemaining} Season: ${seasonId}`,
28
+ thresholdLine: (exchangeThresholdChips) => `Exchange threshold: ${n(exchangeThresholdChips)} CHIP (once reached, use /blackjack exchange)`,
29
+ pointsLine: (points) => `Points: ${n(points)}`,
30
+ poolEmptyNotice: () => "Note: this season's pool is spent; wins now grant points instead.",
31
+ pointsGrantedNotice: (points) => `This season's pool is spent, so you were granted ${n(points)} points instead.`,
32
+ chipValidityNotice: () => '(At season end, CHIP below the exchange threshold converts to points and is reclaimed; CHIP past the threshold carries over.)',
33
+ // 第四条跟着中文一起改(运营者单独批准,见 messages.ts 里 consentText 的
34
+ // 文档):披露的不再只有赛季末回收,还有赛季中途的长期不活跃回收——服务端
35
+ // 的 recycleInactive 每天跑,把游戏余额和已兑换额度一起清零。
36
+ // 一个字都不许比中文多:不报天数(`inactive_recycle_days` 是可调配置,
37
+ // render.test.ts 里那条 `\b\d+\s*(day|week|month)\b` 反向断言就是为这个
38
+ // 存在的),也不把「有效期」说成一句"在赛季内有效"的保证——早先一版写作
39
+ // `Rewards are valid for the season`,把中文的**限制**说成了**承诺**,被打回过。
40
+ consentText: () => [
41
+ '[Please read before first use]',
42
+ "· This is a third-party mini-game published by a community developer; it is not affiliated with any model vendor and does not represent any vendor's position.",
43
+ '· Taking part is free: you get a limited number of free hands each day, losing costs you nothing, and your own API key and balance are unaffected.',
44
+ '· CHIP you win can only be exchanged, within this plugin, for model API quota to spend: it cannot be withdrawn, cannot be transferred to anyone else, and cannot be moved into any vendor account.',
45
+ '· Rewards carry a season-limited validity period; whatever is left unused is reclaimed under the rules after the season ends, or after a long stretch of inactivity.',
46
+ '',
47
+ 'Once you have read and agree to the above, run /blackjack agree confirm to start.',
48
+ ].join('\n'),
49
+ agreeHint: () => 'Please read the notice and run /blackjack agree before using this.',
50
+ noRoundHint: () => 'No hand is in progress right now — run /blackjack deal to start one.',
51
+ commandUsage: () => 'agree | deal [amount] | exchange [amount] | reset',
52
+ actionDescriptions: {
53
+ hit: 'Blackjack: draw one card from the shoe',
54
+ stand: 'Blackjack: stand, ending your turn and moving to settlement',
55
+ double: 'Blackjack: double down — double your committed CHIP and draw exactly one more card',
56
+ split: 'Blackjack: split a pair of equal-rank cards into two hands',
57
+ insure: 'Blackjack: buy insurance when the dealer shows an Ace',
58
+ decline: 'Blackjack: decline insurance',
59
+ },
60
+ blackjackCommandDescription: () => 'Blackjack table (needs an open conversation): status, deal, exchange quota, reset',
61
+ genericFailure: () => "That didn't go through — please try again in a moment.",
62
+ serviceUnreachable: () => "Can't reach the table service right now — please try again shortly.",
63
+ dailyFreeHandsExhausted: () => "You're out of free hands for today — come back tomorrow.",
64
+ belowExchangeThreshold: (thresholdChips) => `Your balance hasn't reached the exchange threshold yet (needs ${n(thresholdChips)} CHIP).`,
65
+ insufficientBalance: () => "Your balance isn't enough to complete this action.",
66
+ roundAlreadyInProgress: () => 'A hand is already in progress.',
67
+ // Not "run /blackjack exchange again" — that's the very command that
68
+ // produced this code, and running it again just lands on the "give me an
69
+ // amount" prompt, not an actual way out. See this method's doc in
70
+ // messages.ts: no server code path can reach this state today.
71
+ githubBindingRequired: () => "This wallet's GitHub link looks inconsistent, so a direct exchange isn't possible right now. Please try again later; if it keeps happening, contact the operator.",
72
+ seasonCapReached: (capChips, exchangedThisSeasonChips) => `This would go over this season's exchange cap (cap ${n(capChips)} CHIP, already exchanged ${n(exchangedThisSeasonChips)} CHIP this season).`,
73
+ exchangeUnavailable: () => "Exchanging isn't available right now (an operator deployment setting is missing) — the table itself is unaffected, please try again later.",
74
+ betAmountAboveLimit: (maxChips) => `That amount is over the limit — the most you can raise is ${n(maxChips)} CHIP.`,
75
+ illegalAction: () => "That move isn't available anymore — the hand's state may have changed. Run /blackjack to see the current state.",
76
+ registerRateLimited: () => 'Too many registrations from this network right now — please try again later.',
77
+ // Both commands, not one: `reset` alone never gets a new token, and `agree`
78
+ // alone is short-circuited by `identity.accept()`'s `if (existing) return
79
+ // existing`, handing back the very credential the server rejected. See this
80
+ // method's doc in messages.ts — the path was checked against the code.
81
+ unknownPlayerToken: () => "The table service no longer recognizes the credential stored on this machine (the server was reset, or serverUrl now points at a different deployment). Run /blackjack reset confirm to clear it, then /blackjack agree to start over.",
82
+ agreeWelcome: () => [
83
+ 'You have confirmed the notice — welcome to the table.',
84
+ "Run /blackjack deal to start today's free hand, or /blackjack deal <amount> to raise from your balance.",
85
+ ].join('\n'),
86
+ invalidBetAmount: () => 'The amount must be a positive integer.',
87
+ pairingInstructions: (url, minutes) => [
88
+ 'Linking a GitHub account is required for your first exchange — this is a one-time step.',
89
+ '',
90
+ 'Open this link in your browser:',
91
+ url,
92
+ '',
93
+ 'After opening it, click "Continue with GitHub" to authorize, then enter the amount to exchange on the same page and submit.',
94
+ `The link stays valid for about ${minutes} ${minutes === 1 ? 'minute' : 'minutes'}; if it expires, just run /blackjack exchange again for a new one.`,
95
+ 'Once linked, use /blackjack exchange <amount> directly from now on — no more browser trips needed.',
96
+ ].join('\n'),
97
+ exchangeAmountPrompt: (githubLogin, chips, exchangeThresholdChips) => [
98
+ `GitHub account linked: ${githubLogin}`,
99
+ `Current balance: ${n(chips)} CHIP Exchange threshold: ${n(exchangeThresholdChips)} CHIP`,
100
+ 'Use /blackjack exchange <amount> to specify how much CHIP to exchange.',
101
+ ].join('\n'),
102
+ invalidExchangeAmount: () => 'The exchange amount must be a positive integer.',
103
+ exchangeSuccess: (chips, exchangedChips) => [
104
+ `You exchanged ${n(chips)} CHIP.`,
105
+ `Total exchanged so far: ${n(exchangedChips)} CHIP`,
106
+ "This quota automatically takes over the one request that fails once your own model quota runs out; it isn't touched otherwise, and there's nothing you need to do.",
107
+ ].join('\n'),
108
+ // 新写的英文,不是中文的直译:三件事一件不能多、一件不能少——只能在插件内
109
+ // 消费/不可提现/不可转让、不因赛季结束清零、N 天无游戏无消费触发回收进奖池。
110
+ // 参见 messages.ts 里这条方法的文档:Task 3 曾经因为英文比中文说得更肯定
111
+ // 被打回,这里对着中文原句逐条核对过一遍。
112
+ // The `true` branch drops the middle clause and nothing else — it does NOT
113
+ // claim the opposite. The server only says "this expires at season end"; it
114
+ // says nothing about when, or at what rate, and inventing those details
115
+ // would be the same false promise 「有效期至本赛季末」 was, in reverse.
116
+ exchangedQuotaNote: (inactiveRecycleDays, expiresAtSeasonEnd) => expiresAtSeasonEnd
117
+ ? `Exchanged quota is spendable only inside this plugin — it can't be withdrawn or transferred. An account with ${inactiveRecycleDays} consecutive days of no play and no spending has its balance recycled into the pool.`
118
+ : `Exchanged quota is spendable only inside this plugin — it can't be withdrawn or transferred. It doesn't expire at the end of a season; but an account with ${inactiveRecycleDays} consecutive days of no play and no spending has its balance recycled into the pool.`,
119
+ resetConfirmPrompt: () => 'This clears the table credential saved on this machine (server-side records are unaffected; pairing again will recover it). Confirm with /blackjack reset confirm',
120
+ resetDone: () => 'Local credential cleared. Run /blackjack agree to start over.',
121
+ unknownSubcommand: (sub, usage) => `Unknown subcommand "${sub}". Available: ${usage}`,
122
+ poolSwitchNotice: () => 'Switched to the pool balance to continue this request.',
123
+ noCredentialHint: () => "The fallback route only takes over once your own model quota is exhausted; right now no API key is configured, so your exchanged quota can't be used yet.",
124
+ hiddenCardLabel: () => 'Hidden card',
125
+ tableHandLabel: () => 'Your hand',
126
+ tableSplitHandLabel: (handIndex) => `Hand ${handIndex}`,
127
+ tableDealerLabel: () => 'Dealer',
128
+ tableActionLabels: {
129
+ hit: 'Hit',
130
+ stand: 'Stand',
131
+ double: 'Double down',
132
+ split: 'Split',
133
+ insure: 'Buy insurance',
134
+ decline: 'Decline insurance',
135
+ },
136
+ // 分隔符用 ' · ',不是双 ASCII 空格:中文那边用的 U+3000(全角空格)在 CSS
137
+ // 里天生不可折叠,视觉上就是宽间隔;英文这边如果用两个普通空格,落进
138
+ // Table.tsx 那个没有 white-space 覆盖的 <div>(默认 `normal`)时,连续空白
139
+ // 会被 HTML 渲染折叠成一个,四段字段就会挤成一整行看不出分段。已经在这个
140
+ // 文件里的 `insufficientBalanceSuffix`/`freeHandsRemainingToday` 等地方用过
141
+ // 同一个可见分隔符,这里跟着统一,而不是再造一个会被折叠的写法。
142
+ tableBalanceHeader: (chips, freeHandsRemaining) => `Balance: ${n(chips)} CHIP · Free hands left today: ${freeHandsRemaining}`,
143
+ tablePointsSuffix: (points) => ` · Points: ${n(points)}`,
144
+ tableExchangedSuffix: (exchangedChips) => ` · Exchanged so far: ${n(exchangedChips)} CHIP`,
145
+ tableExchangeThresholdReached: (exchangeThresholdChips) => `You've reached the exchange threshold (${n(exchangeThresholdChips)} CHIP) — use /blackjack exchange to turn CHIP into model quota`,
146
+ tableExchangeThresholdShort: (shortChips, exchangeThresholdChips) => `${n(shortChips)} CHIP short of the exchange threshold (${n(exchangeThresholdChips)}) — once you cross it, use /blackjack exchange to turn CHIP into model quota`,
147
+ tableExchangedQuotaNote: (exchangedChips) => `The ${n(exchangedChips)} CHIP you've exchanged automatically takes over the one request that fails once your own model quota runs out; it isn't touched otherwise.`,
148
+ tablePoolEmptyNotice: () => "(This season's pool is spent — wins now grant points instead)",
149
+ actionShortfallHint: (costChips) => `${n(costChips)} more CHIP needed — balance too low`,
150
+ tableCostSuffix: (costChips) => ` (${n(costChips)})`,
151
+ betAmountMissing: () => 'Enter an amount.',
152
+ betAmountExceedsCap: (maxChips) => `The cap for a single hand is ${n(maxChips)} CHIP.`,
153
+ betAmountExceedsBalance: () => "Your balance isn't enough for this amount.",
154
+ customBetToggle: () => 'Custom amount',
155
+ customBetSubmit: () => 'Start',
156
+ customBetRange: (maxChips) => `1 to ${n(maxChips)} CHIP`,
157
+ noActiveRoundNotice: () => 'No hand is in progress right now.',
158
+ freeHandsGroupTitle: () => 'Free hands',
159
+ freeHandsRemainingToday: (freeHandsRemaining) => ` · ${freeHandsRemaining} left today`,
160
+ freeHandsExhaustedToday: () => ' · used up for today — come back tomorrow',
161
+ freeHandExplainer: (baseChips, naturalBlackjackPayoutChips) => `No CHIP required. Win and you take ${n(baseChips)} CHIP; a natural blackjack in the first two cards (an Ace with a 10/J/Q/K) pays ${n(naturalBlackjackPayoutChips)}. Lose or tie and you are out nothing.`,
162
+ startFreeHandButton: () => 'Start a free hand',
163
+ raiseGroupTitle: () => 'Raise from your balance',
164
+ insufficientBalanceSuffix: () => ' · balance too low',
165
+ raiseExplainer: () => 'The CHIP you raise comes from your balance: win and it doubles, lose and it is gone.',
166
+ rematchFreeLabel: () => 'Play again (free)',
167
+ dealButtonFreeHandsSuffix: (freeHandsRemaining) => ` — ${freeHandsRemaining} left`,
168
+ dismissButton: () => 'Dismiss',
169
+ actionFailedNotice: () => "That didn't go through — you've been refreshed to the latest state.",
170
+ };
@@ -0,0 +1,4 @@
1
+ import type { LocaleId, Messages } from './messages.js';
2
+ export { LOCALES } from './messages.js';
3
+ export type { LocaleId, Messages } from './messages.js';
4
+ export declare function resolveMessages(locale: LocaleId): Messages;
@@ -0,0 +1,6 @@
1
+ import { zh } from './zh.js';
2
+ import { en } from './en.js';
3
+ export { LOCALES } from './messages.js';
4
+ export function resolveMessages(locale) {
5
+ return locale === 'zh' ? zh : en;
6
+ }
@@ -0,0 +1,412 @@
1
+ /**
2
+ * 面向玩家的全部文案,按 locale 分成两份实现。
3
+ *
4
+ * 为什么是"接口 + 两个实现"而不是 t(key) 查表:`satisfies Messages` 让漏翻一条
5
+ * 直接编译失败,根本发不出去;查表方案漏翻通常只是静默回退或输出 key。这个项目
6
+ * 已经因为"同一句话在两处不同步"吃过两次亏,多语言会把这个风险乘以二。
7
+ *
8
+ * 为什么每条是函数而不是模板串:中文没有复数、英文有(1 hand / 2 hands),语序
9
+ * 也不同。函数让两种语言各自把话说顺。
10
+ */
11
+ export declare const LOCALES: readonly ["zh", "en"];
12
+ export type LocaleId = (typeof LOCALES)[number];
13
+ /**
14
+ * 英文文案的红线,对标 render.ts 的 `BANNED`。
15
+ *
16
+ * 曾经写作 `/gambl|casino|\bwager\b|\bbet\b|real money/i`,只挡五个字面 token,
17
+ * 比中文那条松得多,而且松在两个地方:
18
+ *
19
+ * 一是词形。`\bbet\b` 的词边界在 bet 之后就断了,bets / betting / bettor 全部
20
+ * 漏网——而这三个正是英文里真正会写出来的形态,光秃秃的 "bet" 反而少见。
21
+ *
22
+ * 二是概念。中文的 `赢钱` 禁的是"把赢来的东西说成钱"这件事本身,英文这边却
23
+ * 只禁 real money,cash out / winnings / payout / jackpot 一律放行。这个项目的
24
+ * 全部合规站位就建立在"赢来的 CHIP 不是钱、不可提现"上,用 payout 或 winnings
25
+ * 描述结算,等于在文案层面把这条站位拆掉。
26
+ *
27
+ * 收紧的时机是现在而不是以后:后面还有五个任务要写新的英文文案,每一条都按
28
+ * 这条正则验收。先把线划到位,比等文案写完再回头重审所有文案便宜得多。
29
+ *
30
+ * 词边界要当心:`bett?` 单独用会误伤 better/betterment,所以后缀写成显式的
31
+ * `(?:s|ing|or|ors)?` 再收一个 `\b`。i18n.test.ts 里有一张"必须拦下"和一张
32
+ * "不许误伤"的清单钉住这两侧。
33
+ */
34
+ export declare const BANNED_EN: RegExp;
35
+ export interface Messages {
36
+ /** 免费局进行中:不出 CHIP,赢了拿多少。 */
37
+ freeStakeLine(betChips: number): string;
38
+ /** 加注局进行中:已从余额里出多少。 */
39
+ raiseStakeLine(betChips: number): string;
40
+ freeWin(payoutChips: number): string;
41
+ freeLose(): string;
42
+ freePush(): string;
43
+ raiseWin(payoutChips: number, betChips: number): string;
44
+ raiseLose(betChips: number): string;
45
+ raisePush(betChips: number): string;
46
+ /**
47
+ * 手牌标签及其后缀标点全部在这里——不是"你的牌"四个字单独一条,是因为
48
+ * 冒号、括号这类标点在两种语言里长得不一样(全角冒号 vs ASCII 冒号),把它们
49
+ * 留在 render.ts 拼接会让 render.ts 里又出现语言相关的字符。函数返回的是可以
50
+ * 直接和牌面字符串拼接的完整前缀。
51
+ */
52
+ handLabel(): string;
53
+ /** 分牌后第 N 手的标签,同样带上后缀标点。 */
54
+ splitHandLabel(handIndex: number): string;
55
+ dealerLabel(): string;
56
+ /**
57
+ * 点数展示:`(软18)`/`(18)` 这个带括号的整体,`isSoft` 决定是否插入"软"。
58
+ *
59
+ * 曾经叫 `soft()`,两次不同任务的 review 各自独立提过一次:那个名字读起来像
60
+ * "返回软点数",可它真正的产物是"括号里的点数",`isSoft` 只是其中一个开关。
61
+ * 三个生产调用点里有两个(`render.ts` 与 `Table.tsx` 的庄家那一行)传的是
62
+ * `false`,也就是说这个方法最常见的用法恰恰和它的旧名字相反。
63
+ */
64
+ totalInParens(total: number, isSoft: boolean): string;
65
+ /** 双倍标记,含前导空格;未双倍时调用方直接用空字符串,不调用这个方法。 */
66
+ doubled(): string;
67
+ /** "可选:" 前缀,后面接 actionLabels 拼出来的动作列表。 */
68
+ actionsPrefix(): string;
69
+ /** 六个动作按钮的完整文案(含斜杠指令),键与 view.actions 里的动作名一致。 */
70
+ actionLabels: Record<'hit' | 'stand' | 'double' | 'split' | 'insure' | 'decline', string>;
71
+ /**
72
+ * 拼接 actionLabels 之间的分隔符。中文原本用全角空格(U+3000)分隔——这也是
73
+ * 玩家看到的输出的一部分,不是纯排版细节,所以必须由 locale 决定,不能在
74
+ * render.ts 里写死一个两边共用的字符(写死 ASCII 空格会悄悄改掉中文原有输出;
75
+ * 写死全角空格则会让英文动作列表看起来断裂)。
76
+ */
77
+ actionsSeparator(): string;
78
+ balanceLine(chips: number, exchangedChips: number): string;
79
+ freeHandsLine(freeHandsRemaining: number, seasonId: string): string;
80
+ thresholdLine(exchangeThresholdChips: number): string;
81
+ pointsLine(points: number): string;
82
+ poolEmptyNotice(): string;
83
+ /** 奖池已发完,改发积分时的提示,需要报出实际发放的积分数。 */
84
+ pointsGrantedNotice(points: number): string;
85
+ /** 赛季结束时未达兑换门槛的 CHIP 会被折算回收的提醒。 */
86
+ chipValidityNotice(): string;
87
+ /**
88
+ * 首次使用须知的全文(含标题行、四条正文、空行、末尾的下一步指引),玩家要
89
+ * 对着它点同意。
90
+ *
91
+ * 为什么整段是一个方法而不是拆成七条:这七行是一份声明,不是七句可以各自
92
+ * 增删的界面文案。拆开之后,任何一次"只改其中一行"的改动都不会强迫改动者
93
+ * 重读上下文,而这段话的分寸恰恰在于几条之间的相互限定(比如"只能在插件内
94
+ * 兑换"和后面三条禁止是一体的)。整段搬运也让中英两版能一行对一行地比对。
95
+ *
96
+ * 措辞上的红线:英文版不许比中文版更具体,也不许更含糊。「按规则回收」是
97
+ * 刻意留的模糊——回收细则还会变,写死天数就是又一次假承诺(这个项目已经
98
+ * 因为把话说得比代码更精确而翻过两次车)。三条禁止(不可提现/不可转让/
99
+ * 不可转入厂商账户)和"无关联 + 不代表其立场"这两组,任何一版都必须齐全。
100
+ *
101
+ * **第四条为什么在 i18n 分支里被改写过一次**(这是本分支唯一一处经运营者
102
+ * 单独批准、跳出"中文只搬运不改写"那条规则的合规文案改动):原句只说
103
+ * 「赛季结束后未使用的部分按规则回收」,只披露了赛季末这一个触发条件。可
104
+ * 服务端 `jobs.ts` 的 `recycleInactive` 每天 00:15 由 cron 跑一次,把
105
+ * `inactive_recycle_days`(当前配置 7 天)内既无游戏、也无消费的账户交给
106
+ * `ledger.ts` 的 `recycleAll`——**游戏余额和已兑换额度一起清零**,而且完全
107
+ * 不等赛季结束,赛季中途就会发生。也就是说"赢了 CHIP、停手一周、回来余额
108
+ * 归零"这条最常见的真实路径,在玩家点"同意"的那一刻一个字都没被告知,而
109
+ * 这段话正是这个项目全部合规站位的承重墙。现在两个触发条件都写进去了。
110
+ *
111
+ * 改写之后仍然不写天数:`inactive_recycle_days` 是运营者随时可调的配置,
112
+ * 把 7 写进须知,运营者下次调参就把这句话变成假的——这正是上一段那两次
113
+ * 翻车的同一个形状。`render.test.ts` 里英文那侧有一条反向断言
114
+ * (`\b\d+\s*(day|week|month)\b` 不许出现)专门守着这一点,中文那侧则
115
+ * 正面钉住「按规则」这个刻意的模糊还在。两侧同时钉住了新加的这半句本身,
116
+ * 免得它哪天被"顺一顺措辞"顺没了。
117
+ */
118
+ consentText(): string;
119
+ /** 还没同意须知时,任何需要凭证的命令都用这句拒绝并指路。 */
120
+ agreeHint(): string;
121
+ /** 没有进行中的牌局时,动作命令(bj-hit 等)用这句指路去 deal。 */
122
+ noRoundHint(): string;
123
+ /** `/blackjack` 命令的用法提示,同时用在 command 注册的 `input.hint` 和"未知子命令"的错误里。 */
124
+ commandUsage(): string;
125
+ /** 六个 bj-* 命令注册时各自的 description,键与 ACTIONS 里的 action 名一致。 */
126
+ actionDescriptions: Record<'hit' | 'stand' | 'double' | 'split' | 'insure' | 'decline', string>;
127
+ /**
128
+ * `/blackjack` 命令本身的 description。
129
+ *
130
+ * **「需要先有会话」必须放在最前面,别挪到后面去。** 宿主在启动页
131
+ * (Into the Unknown)会渲染命令补全,补全里显示的就是这句——但在那一屏
132
+ * 执行命令只会静默失败:命令真的跑了,一行都不渲染,用户看到的是"插件坏了"。
133
+ * 补全是启动页上唯一由我们提供内容的地方(其余挂点要么是替换式的、要么在
134
+ * 那一屏根本不渲染,2026-08-22 实测),所以这句话是我们能在失败现场说的
135
+ * 唯一一句。而补全的描述列**会截断**,放后面等于没写。
136
+ */
137
+ blackjackCommandDescription(): string;
138
+ /**
139
+ * 兜底失败文案:`ApiError` 的 `code` 不在已知映射表里时用这句——可能来自
140
+ * 一个比这版插件更新的服务端(加了新 code 没同步),也可能是插件这边漏映射
141
+ * 了。不管哪种,绝不能把服务端的英文诊断散文(`ApiError.message`)直接
142
+ * 甩给玩家:那正是 Task 7 要修的缺陷本身(把 message 错当 code 匹配)。
143
+ */
144
+ genericFailure(): string;
145
+ /** 网络层失败(status 0 或 5xx):牌桌服务本身连不上。 */
146
+ serviceUnreachable(): string;
147
+ /** code `no-free-hands-left`:今日免费手已用完。 */
148
+ dailyFreeHandsExhausted(): string;
149
+ /**
150
+ * code `below-exchange-threshold`(原名 `below-threshold`——review 指出
151
+ * "below WHICH threshold"是个契约名不该留给读者猜的问题,改了名):余额
152
+ * 没到兑换门槛。`thresholdChips` 来自服务端 `error.thresholdChips`——一个
153
+ * code 配一个数字,能让玩家知道具体还差多少,比一句不带数字的通用拒绝更
154
+ * 有用。**`thresholdChips` 缺失(跨版本的服务端没带这个字段)时不要编一个
155
+ * 数字出来**:`mapError` 在这种情况下改叫 `genericFailure()`,不会用 0 之
156
+ * 类的假数字调用这个方法——见 `commands.ts` 里 `numberDetail` 的文档。
157
+ */
158
+ belowExchangeThreshold(thresholdChips: number): string;
159
+ /** code `insufficient-balance`:余额不足以完成这个动作(开局加注、双倍/分牌/保险、或兑换超额)。 */
160
+ insufficientBalance(): string;
161
+ /**
162
+ * code `active-round-exists`:已经有一局在进行中。单独一行文本——`mapError`
163
+ * 的恢复分支里会把它和 `renderRound(round, m)` 拼在一起展示,所以这句话
164
+ * 本身不能带牌局细节。
165
+ */
166
+ roundAlreadyInProgress(): string;
167
+ /**
168
+ * code `github-binding-required`:直接兑换(`POST /api/exchange`)要求已绑定
169
+ * GitHub 身份(服务端读的是 `players.github_id`)。正常操作下这条在
170
+ * `doExchange` 里走不到——未绑定的玩家会先被 `me.githubLogin === null`
171
+ * (`players.github_login`)分流去配对流程,根本不会调用这个端点。
172
+ *
173
+ * **不要建议玩家重新执行 `/blackjack exchange`。** review 抓到过一版文案
174
+ * 这么写,但唯一能拿到这个 code 的路径就是先跑了
175
+ * `/blackjack exchange <数量>`——重新执行只会命中 `exchangeAmountPrompt`
176
+ * (因为 `me.githubLogin` 非空,插件不会再走配对分支),是一个循环建议,
177
+ * 不是真的恢复路径。核实过 `github_id`/`github_login` 在服务端只有一处
178
+ * 写入(`pairing.ts` 的 OAuth 回调,两个字段总是同一条 UPDATE 一起写),
179
+ * 也没有任何清除 `github_id` 单独留下 `github_login` 的代码路径——也就是说
180
+ * 触发这个 code 需要的"`github_login` 非空但 `github_id` 为空"这种状态,
181
+ * 在当前代码库里根本走不到,这是纯粹面向未来(服务端换了实现、数据被手动
182
+ * 改过)的防御性分支,不该假装有一个真实存在的恢复步骤。措辞因此只说"绑定
183
+ * 状态不一致、稍后再试、持续出现请联系运营者",不指一条不存在的路。
184
+ */
185
+ githubBindingRequired(): string;
186
+ /**
187
+ * code `would-exceed-season-cap`(原名 `season-cap-reached`——review 指出
188
+ * 触发条件是 `exchangedThisSeasonChips + chips > capChips`,也就是"这一笔
189
+ * 会让累计超过上限",不是"已经到达上限";旧名字暗示的是后者,跟这里的
190
+ * 中英文案一直在说的「会超过」/"would go over"对不上,改了名让 code 配得上
191
+ * 自己的文案)。`capChips`/`exchangedThisSeasonChips` 来自服务端,报出
192
+ * "上限是多少、已经用掉多少",比一句"兑换失败"有用得多。
193
+ */
194
+ seasonCapReached(capChips: number, exchangedThisSeasonChips: number): string;
195
+ /**
196
+ * code `exchange-unavailable`:运营者部署缺配置(`PUBLIC_URL`/
197
+ * `GITHUB_CLIENT_ID`/`GITHUB_CLIENT_SECRET` 之一),兑换整个功能暂时用不了。
198
+ * 故意不说"连不上牌桌服务"(`serviceUnreachable`)——牌局本身其实是好的,
199
+ * 只是兑换这一项不可用,说成"整个服务连不上"会让玩家误以为要重开插件。
200
+ *
201
+ * 这个方法曾经是死代码:`mapError` 里 `status>=500` 的短路判断写在 code
202
+ * 匹配之前,而这个 code 走的是 503,于是这句文案永远渲染不到,玩家看到的
203
+ * 一直是 `serviceUnreachable()` 那句——恰恰是上一段说"故意不要"的那句话
204
+ * (review 抓到)。`mapError` 现在把 code 匹配挪到了 5xx 短路之前,见
205
+ * `commands.ts` 的注释。
206
+ */
207
+ exchangeUnavailable(): string;
208
+ /**
209
+ * code `bet-amount-too-high`:加注数额超过服务端配置的上限。`maxChips` 来自
210
+ * 服务端 `error.maxChips`——这个数字之前只活在英文诊断散文里
211
+ * ("betChips must be an integer in [1, N]"),Task 7 把它拆成了字段。
212
+ *
213
+ * 这个 code 曾经叫 `invalid-bet-amount`,同一个 code 还盖着"不是正整数"
214
+ * 这另一种情况——但这句文案只讲得通"超过上限"。review 指出这只在插件自己
215
+ * 从不发送非法格式的前提下才是真话(`doDeal` 确实做了整数校验),而那个
216
+ * 前提活在另一个包里;服务端因此把两种情况拆成了两个 code:
217
+ * `invalid-bet-amount` 现在专指"不是正整数"(映射到 `genericFailure`——
218
+ * 插件自己触发不了这种请求),`bet-amount-too-high` 专指"超过上限"(这个
219
+ * 才是玩家真的会撞到的:加注数额没错格式,只是太大)。
220
+ */
221
+ betAmountAboveLimit(maxChips: number): string;
222
+ /**
223
+ * code `illegal-action`:这个动作在当前这手牌上不可用了。真实可达:玩家在
224
+ * 图形牌桌和命令行之间切换时,命令行的 `doAction` 只按 `ACTIONS` 里的固定
225
+ * 六个动作直接发送,不会先问一遍服务端"现在能做什么"——如果这手牌的状态
226
+ * 已经在别处推进过(比如刚在牌桌上点了停牌),这条命令仍会把已经不可用的
227
+ * 动作发过去。
228
+ */
229
+ illegalAction(): string;
230
+ /**
231
+ * code `register-rate-limited`:`/blackjack agree confirm` 触发的注册被服务端
232
+ * 的注册限流拒绝(`register_max_per_hour`/`register_max_per_ip_per_hour`,
233
+ * 防的是脚本批量注册领免费手掏空奖池)。真实玩家撞上的多半是共享出口 IP
234
+ * (公司网络、校园网),不是自己做错了什么,所以措辞不能听起来像指责。
235
+ */
236
+ registerRateLimited(): string;
237
+ /**
238
+ * code `unknown-player-token`:服务端不认识本机保存的那枚玩家令牌
239
+ * (`packages/server/src/auth.ts` 的 401)。
240
+ *
241
+ * 这条曾经是整个服务端唯一一处站在错误码体系之外的玩家可见错误:服务端发的是
242
+ * 裸 `{ error: 'unauthorized' }`,`mapError` 认不出,落进 `default` → 5xx/其它
243
+ * 状态都不匹配 → `genericFailure()`,玩家看到"操作没有成功,请稍后再试"。
244
+ * 而这句话在这个场景里是**一条死路**——令牌不会自己变回被认识的样子,玩家
245
+ * 每重试一次就再撞一次同一堵墙。
246
+ *
247
+ * 真实来路有两条,都不是玩家做错了什么:服务端换过库(Railway 换卷、重新
248
+ * 部署到空数据库),或者玩家把 `serverUrl` 改指到了另一个部署(README 明确
249
+ * 鼓励自建奖池)。
250
+ *
251
+ * 恢复路径是在代码里核对过的,不是照文档抄的:本机令牌就是同意记录
252
+ * (`identity.ts` 的注释),而 `identity.accept()` 一上来就 `if (existing)
253
+ * return existing`——不先清掉那枚坏令牌,`agree confirm` 只会把同一枚令牌
254
+ * 原样还回来。所以必须两步:`/blackjack reset confirm` 走 `identity.clear()`
255
+ * 抹掉凭证,再 `/blackjack agree` 重新走一遍须知(它自己会指向
256
+ * `agree confirm`,那一步才 `api.register()` 换一枚新令牌)。措辞里两条命令
257
+ * 都要点名,只说 `reset` 或只说 `agree` 都走不通。
258
+ */
259
+ unknownPlayerToken(): string;
260
+ /** agree confirm 成功后的欢迎语,含下一步指引(deal / deal <注额>)。 */
261
+ agreeWelcome(): string;
262
+ /**
263
+ * 注额不是正整数时的拒绝文案,命令行(`commands.ts` 的 `doDeal`)与图形牌桌
264
+ * (`client/Table.tsx` 的 `betError`)共用这一条。
265
+ *
266
+ * 牌桌那边原本另有一条 `betAmountRequired()`——两条的英文渲染出来一模一样
267
+ * ("The amount must be a positive integer."),中文却是两种说法。收口 review
268
+ * 指出这有两个问题:玩家从输出上根本分不清哪句归哪个界面,而改了其中一条的
269
+ * 人不会知道还有另一条要跟着改——正是这个项目吃过两次亏的那个形状。这里选择
270
+ * 合并成一条而不是把两版中文对齐:对齐中文只是让两份字面量恰好相等,下一次
271
+ * 编辑照样会把它们分开;合并才真正让"一句话只有一处"。
272
+ *
273
+ * 合并的代价是牌桌上的中文由「注额要填正整数。」变成了这一句(同一件事的
274
+ * 另一种说法,措辞变更已在收口报告里单独声明)。这不属于牌桌文案必须单开一
275
+ * 条的那类情形——见下面 `table*` 命名规则那段:牌桌单开方法是为了标点和拼接
276
+ * 位置不同,而这两条都是独立成句、标点相同,没有非分开不可的理由。
277
+ */
278
+ invalidBetAmount(): string;
279
+ /**
280
+ * 首次兑换(未绑定 GitHub)时的配对指引全文:怎么打开链接、点什么按钮、
281
+ * 链接多久过期。含 `url` 是因为链接本身嵌在指引的中段,不是拼接在末尾。
282
+ */
283
+ pairingInstructions(url: string, minutes: number): string;
284
+ /** 已绑定但没给兑换数量时:回显已绑定账号、当前余额与门槛,并要求补上数量。 */
285
+ exchangeAmountPrompt(githubLogin: string, chips: number, exchangeThresholdChips: number): string;
286
+ /** `exchange` 的数量不是正整数时的拒绝文案。 */
287
+ invalidExchangeAmount(): string;
288
+ /**
289
+ * 兑换成功后的说明(不含已兑换额度的有效性说明——那一句由 `exchangedQuotaNote`
290
+ * 单独负责,commands.ts 按 `result.quota` 是否存在决定要不要拼接在后面)。
291
+ */
292
+ exchangeSuccess(chips: number, exchangedChips: number): string;
293
+ /**
294
+ * 已兑换额度的有效性说明,接住 `POST /api/exchange` 返回的
295
+ * `quota.inactiveRecycleDays`。Task 6 之前这句话由服务端的
296
+ * `exchangedQuotaNote()`(`packages/server/src/routes/account.ts`)直接吐出
297
+ * 现成中文;服务端不知道调用者用什么语言,让它也维护一份译文,等于把"同一句
298
+ * 话在两处不同步"的老问题复制到语言维度——这个项目已经因为这个吃过两次亏。
299
+ * 现在服务端只吐事实(`quota`),措辞统一搬到这里。
300
+ *
301
+ * **按事实描述,一个字都不能比代码兑现的多**:这份额度不会因赛季结束而
302
+ * 清零——服务端的 `seasonSettle` 从头到尾不触碰 `exchanged_micro`。真正会
303
+ * 收走它的只有不活跃回收(`recycleAll` 同时清空游戏余额与已兑换额度)。早期
304
+ * 这里(当时还在服务端)写的是"有效期至本赛季末",那是一句代码不兑现的
305
+ * 承诺,而玩家恰恰在兑换成功那一刻看到它。中文版这次是原句搬运,不重写。
306
+ *
307
+ * `inactiveRecycleDays` 必须原样使用服务端传来的数字,不能在插件这边写死——
308
+ * 写死的话运营者调了 `inactive_recycle_days` 之后文案就又和行为对不上了。
309
+ *
310
+ * `expiresAtSeasonEnd` 同理,而且更要紧:它一路从服务端
311
+ * (`routes/account.ts` 的 `quota.expiresAtSeasonEnd`)经 `api.ts` 传到这里,
312
+ * 之前**没有任何人读它**,而上面那句「它不会因赛季结束而清零」是无条件说
313
+ * 出口的。也就是说服务端哪天真的改成"赛季末回收已兑换额度"、老老实实把
314
+ * 这个字段发成 `true`,插件照样把那句话说给玩家听——这正是
315
+ * 「有效期至本赛季末」当年翻车的同一个形状(文案比代码更肯定),只是方向
316
+ * 反了过来。收口 review 要求这个事实必须真的驱动措辞。
317
+ *
318
+ * 为什么是给这条方法加一个参数,而不是新开一条方法:措辞归措辞、判断归
319
+ * 判断,"这一句要不要说"是两种语言各自的语序问题(中文靠「但」承接,英文
320
+ * 靠分号),留在 `commands.ts` 里拼接就等于又把语言相关的判断挪出 i18n。
321
+ * 加参数还有一个编译期好处:签名一改,`zh.ts`/`en.ts` 两边都必须重新处理
322
+ * 这个分支,`satisfies Messages` 不给静默漏掉的机会。
323
+ *
324
+ * `true` 分支**只是把那半句拿掉,不反过来断言"会在赛季末清零"**:服务端
325
+ * 只告诉我们"会过期",没告诉我们什么时候、按什么比例、折不折算积分。把
326
+ * 一个没有细节的过期承诺写进兑换成功页,就是再造一次假承诺。
327
+ */
328
+ exchangedQuotaNote(inactiveRecycleDays: number, expiresAtSeasonEnd: boolean): string;
329
+ /** `reset` 不带 confirm 时的二次确认提示。 */
330
+ resetConfirmPrompt(): string;
331
+ /** `reset confirm` 完成后的提示。 */
332
+ resetDone(): string;
333
+ /** `/blackjack` 收到无法识别的子命令时的错误文案,`usage` 由调用方传入 `commandUsage()` 的结果。 */
334
+ unknownSubcommand(sub: string, usage: string): string;
335
+ /**
336
+ * 续命成功切换到奖池路由时的提示。当前版本里它只写进 `ctx.logger.info`,
337
+ * 玩家看不到——见 router.ts 里 `registerRouter` 的已知限制说明——但文案仍然
338
+ * 要走 locale,为将来真的有通知位那天做准备。
339
+ */
340
+ poolSwitchNotice(): string;
341
+ /** 没配 key 时的说明:续命只在自己额度耗尽时接管,配置缺失时用不上已兑换额度。 */
342
+ noCredentialHint(): string;
343
+ /** 暗牌(庄家扣着的那张)的 `aria-label`。 */
344
+ hiddenCardLabel(): string;
345
+ /** 牌桌上"你的牌"标签,不带冒号——点数是紧跟在后面的括号,不是直接接牌面。 */
346
+ tableHandLabel(): string;
347
+ /** 牌桌上分牌后第 N 手的标签,同样不带冒号。 */
348
+ tableSplitHandLabel(handIndex: number): string;
349
+ /** 牌桌上"庄家"标签,不带冒号。 */
350
+ tableDealerLabel(): string;
351
+ /** 六个动作按钮在牌桌上的纯文案(不含 render.ts 那份的 `/bj-xxx` 斜杠指令前缀)。 */
352
+ tableActionLabels: Record<'hit' | 'stand' | 'double' | 'split' | 'insure' | 'decline', string>;
353
+ /** 余额条第一行:余额与今日剩余免费手,牌桌专用的组合(不含赛季号)。 */
354
+ tableBalanceHeader(chips: number, freeHandsRemaining: number): string;
355
+ /** 余额条里追加的积分片段,仅当积分 > 0 时拼接,含前导分隔符。 */
356
+ tablePointsSuffix(points: number): string;
357
+ /** 余额条里追加的已兑换额度片段,仅当已兑换 > 0 时拼接,含前导分隔符。 */
358
+ tableExchangedSuffix(exchangedChips: number): string;
359
+ /** 已达兑换门槛时的提示,指路 /blackjack exchange。 */
360
+ tableExchangeThresholdReached(exchangeThresholdChips: number): string;
361
+ /** 未达兑换门槛时的提示,报出还差多少。 */
362
+ tableExchangeThresholdShort(shortChips: number, exchangeThresholdChips: number): string;
363
+ /** 已有兑换额度时,说明这份额度什么时候会被用到。 */
364
+ tableExchangedQuotaNote(exchangedChips: number): string;
365
+ /** 奖池已发完的简短提示(牌桌上的版本,比 poolEmptyNotice 短,套在括号里)。 */
366
+ tablePoolEmptyNotice(): string;
367
+ /** 余额不够触发某个动作(双倍/分牌/保险)时,按钮 title 里说明还差多少。 */
368
+ actionShortfallHint(costChips: number): string;
369
+ /** 动作按钮标签后面追加的费用后缀,如"(10,000)"。 */
370
+ tableCostSuffix(costChips: number): string;
371
+ /** 自定义注额:留空提交时的提示。 */
372
+ betAmountMissing(): string;
373
+ /** 自定义注额:超过服务端上限时的提示(`betError` 用)。 */
374
+ betAmountExceedsCap(maxChips: number): string;
375
+ /** 自定义注额:超过余额时的提示(`betError` 用)。 */
376
+ betAmountExceedsBalance(): string;
377
+ /** "自定义注额"这个词本身:展开按钮的文案,也是输入框的 aria-label。 */
378
+ customBetToggle(): string;
379
+ /** 自定义注额输入框旁的提交按钮。 */
380
+ customBetSubmit(): string;
381
+ /** 自定义注额输入框旁的可填范围提示。 */
382
+ customBetRange(maxChips: number): string;
383
+ /** 空闲态:"当前没有进行中的牌局。" */
384
+ noActiveRoundNotice(): string;
385
+ /** 免费手分组标题(不含今日状态后缀)。 */
386
+ freeHandsGroupTitle(): string;
387
+ /** 免费手分组标题的后缀:今日还剩几手,含前导分隔符。 */
388
+ freeHandsRemainingToday(freeHandsRemaining: number): string;
389
+ /** 免费手分组标题的后缀:今日已用完,含前导分隔符。 */
390
+ freeHandsExhaustedToday(): string;
391
+ /**
392
+ * 免费手玩法说明:不出 CHIP、赢了拿多少、天生 21 点拿多少、输了或平局不损失。
393
+ * 天生 21 点的赔付额由调用方算好传入(base * 1.5 取整),这里只负责措辞。
394
+ */
395
+ freeHandExplainer(baseChips: number, naturalBlackjackPayoutChips: number): string;
396
+ /** 开始免费一局的按钮文案。 */
397
+ startFreeHandButton(): string;
398
+ /** 加注分组标题(不含余额不足后缀)。 */
399
+ raiseGroupTitle(): string;
400
+ /** 余额不足时追加在分组标题后的后缀,含前导分隔符。 */
401
+ insufficientBalanceSuffix(): string;
402
+ /** 加注玩法说明:钱从余额出、赢了翻倍、输了收不回。 */
403
+ raiseExplainer(): string;
404
+ /** 结算后"再来一局"按钮的免费局标签(后面会紧跟 `dealButtonFreeHandsSuffix`)。 */
405
+ rematchFreeLabel(): string;
406
+ /** "再来一局"按钮上追加的剩余免费手数量。 */
407
+ dealButtonFreeHandsSuffix(freeHandsRemaining: number): string;
408
+ /** 结算后"收起"按钮的文案。 */
409
+ dismissButton(): string;
410
+ /** 动作/发牌请求失败后的兜底提示(container 组件用,绝不回显服务端原始错误)。 */
411
+ actionFailedNotice(): string;
412
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * 面向玩家的全部文案,按 locale 分成两份实现。
3
+ *
4
+ * 为什么是"接口 + 两个实现"而不是 t(key) 查表:`satisfies Messages` 让漏翻一条
5
+ * 直接编译失败,根本发不出去;查表方案漏翻通常只是静默回退或输出 key。这个项目
6
+ * 已经因为"同一句话在两处不同步"吃过两次亏,多语言会把这个风险乘以二。
7
+ *
8
+ * 为什么每条是函数而不是模板串:中文没有复数、英文有(1 hand / 2 hands),语序
9
+ * 也不同。函数让两种语言各自把话说顺。
10
+ */
11
+ export const LOCALES = ['zh', 'en'];
12
+ /**
13
+ * 英文文案的红线,对标 render.ts 的 `BANNED`。
14
+ *
15
+ * 曾经写作 `/gambl|casino|\bwager\b|\bbet\b|real money/i`,只挡五个字面 token,
16
+ * 比中文那条松得多,而且松在两个地方:
17
+ *
18
+ * 一是词形。`\bbet\b` 的词边界在 bet 之后就断了,bets / betting / bettor 全部
19
+ * 漏网——而这三个正是英文里真正会写出来的形态,光秃秃的 "bet" 反而少见。
20
+ *
21
+ * 二是概念。中文的 `赢钱` 禁的是"把赢来的东西说成钱"这件事本身,英文这边却
22
+ * 只禁 real money,cash out / winnings / payout / jackpot 一律放行。这个项目的
23
+ * 全部合规站位就建立在"赢来的 CHIP 不是钱、不可提现"上,用 payout 或 winnings
24
+ * 描述结算,等于在文案层面把这条站位拆掉。
25
+ *
26
+ * 收紧的时机是现在而不是以后:后面还有五个任务要写新的英文文案,每一条都按
27
+ * 这条正则验收。先把线划到位,比等文案写完再回头重审所有文案便宜得多。
28
+ *
29
+ * 词边界要当心:`bett?` 单独用会误伤 better/betterment,所以后缀写成显式的
30
+ * `(?:s|ing|or|ors)?` 再收一个 `\b`。i18n.test.ts 里有一张"必须拦下"和一张
31
+ * "不许误伤"的清单钉住这两侧。
32
+ */
33
+ export const BANNED_EN = /gambl|casino|\bwager(?:s|ing|ed)?\b|\bbett?(?:s|ing|or|ors)?\b|real money|\bcash(?:ing|es|ed)?[\s-]?out|winnings|payout|jackpot/i;