dsh-blackjack 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +5 -0
- package/dist/client/Table.d.ts +16 -1
- package/dist/client/Table.js +82 -11
- package/dist/client/index.js +30 -16
- package/dist/client/state.d.ts +39 -0
- package/dist/client/state.js +12 -0
- package/dist/client.js +13 -3
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +37 -0
- package/dist/render.js +3 -1
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export interface MeView {
|
|
|
40
40
|
exchangeThresholdChips: number;
|
|
41
41
|
baseBetChips: number;
|
|
42
42
|
githubLogin: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* 加注上限。**可选**:插件发到 npm 后可能连到任何一版服务端,比这个字段更早的
|
|
45
|
+
* 服务端不会返回它。缺失时图形牌桌只隐藏自定义注额输入,预设按钮照常可用。
|
|
46
|
+
*/
|
|
47
|
+
maxRaiseBetChips?: number;
|
|
43
48
|
}
|
|
44
49
|
/** Every failure this client surfaces: HTTP status plus the server's error code. */
|
|
45
50
|
export declare class ApiError extends Error {
|
package/dist/client/Table.d.ts
CHANGED
|
@@ -24,7 +24,22 @@ export interface BlackjackTableProps {
|
|
|
24
24
|
state: BlackjackApiState;
|
|
25
25
|
onAction: (action: string) => void;
|
|
26
26
|
onDeal: (betChips?: number) => void;
|
|
27
|
+
/** 玩家收起已结算的牌局,回到空闲态。 */
|
|
28
|
+
onDismiss: () => void;
|
|
27
29
|
/** Brief inline notice for a failed action/deal call (container-owned; cleared on the next successful one). */
|
|
28
30
|
error?: string;
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 动作要额外掏多少 CHIP。镜像服务端 `game.ts` 的 INCREMENT:双倍/分牌各再出
|
|
34
|
+
* 一份注额,保险出一半。抽成导出的纯函数,好让"余额不足时必须置灰"能被断言。
|
|
35
|
+
*/
|
|
36
|
+
export declare function actionCostChips(action: string, betChips: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* 注额校验。抽成导出的纯函数,好让边界(0、小数、超余额、超服务端上限)
|
|
39
|
+
* 能被直接断言,而不是只能靠驱动输入框间接覆盖。
|
|
40
|
+
*/
|
|
41
|
+
export declare function betError(chips: number, opts: {
|
|
42
|
+
balanceChips: number;
|
|
43
|
+
maxChips: number;
|
|
44
|
+
}): string | null;
|
|
45
|
+
export declare function BlackjackTable({ state, onAction, onDeal, onDismiss, error }: BlackjackTableProps): JSX.Element;
|
package/dist/client/Table.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { stakeLine, outcomeLine } from '../copy.js';
|
|
2
4
|
const SUITS = { S: '♠', H: '♥', D: '♦', C: '♣' };
|
|
3
5
|
const RED_SUITS = new Set(['H', 'D']);
|
|
4
6
|
const n = (v) => v.toLocaleString('en-US');
|
|
@@ -24,7 +26,17 @@ const CSS = `
|
|
|
24
26
|
font: inherit; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--bj-line);
|
|
25
27
|
background: rgba(255,255,255,.08); color: inherit; cursor: pointer;
|
|
26
28
|
}
|
|
27
|
-
.bj-actions button:hover { background: rgba(255,255,255,.16); }
|
|
29
|
+
.bj-actions button:hover:not(:disabled) { background: rgba(255,255,255,.16); }
|
|
30
|
+
.bj-actions button:disabled { opacity: .4; cursor: not-allowed; }
|
|
31
|
+
.bj-custom { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
|
|
32
|
+
.bj-custom input {
|
|
33
|
+
font: inherit; width: 9em; padding: 5px 8px; border-radius: 6px;
|
|
34
|
+
border: 1px solid var(--bj-line); background: rgba(255,255,255,.06); color: inherit;
|
|
35
|
+
}
|
|
36
|
+
.bj-hint { opacity: .7; font-size: 12px; }
|
|
37
|
+
.bj-group { margin-top: 12px; }
|
|
38
|
+
.bj-group-title { font-weight: 600; margin-bottom: 2px; }
|
|
39
|
+
.bj-note { opacity: .75; font-size: 12px; margin-bottom: 6px; }
|
|
28
40
|
.bj-meta { opacity: .85; margin-bottom: 8px; }
|
|
29
41
|
.bj-error {
|
|
30
42
|
color: #c0392b; border: 1px solid rgba(192,57,43,.4); border-radius: 6px;
|
|
@@ -53,22 +65,81 @@ function MeHeader({ me }) {
|
|
|
53
65
|
return (_jsxs("div", { className: "bj-meta", children: ["\u4F59\u989D\uFF1A", n(me.chips), " CHIP\u3000\u4ECA\u65E5\u5269\u4F59\u514D\u8D39\u624B\uFF1A", me.freeHandsRemaining, me.points > 0 ? ` 积分:${n(me.points)}` : '', me.poolEmpty ? '(本期奖池已发完,现在赢牌将发放积分)' : '', me.sponsorText ? _jsx("div", { children: me.sponsorText }) : null] }));
|
|
54
66
|
}
|
|
55
67
|
function RoundFooter({ round }) {
|
|
56
|
-
|
|
57
|
-
return null;
|
|
58
|
-
const payout = round.payoutChips ?? 0;
|
|
59
|
-
return (_jsxs("div", { className: "bj-meta", children: [payout > 0 ? `本手赢得 ${n(payout)} CHIP。` : '本手没有收获,明天再来。', round.pointsGranted ? _jsxs("div", { children: ["\u672C\u671F\u5956\u6C60\u5DF2\u53D1\u5B8C\uFF0C\u6539\u4E3A\u53D1\u653E ", n(round.pointsGranted), " \u79EF\u5206\u3002"] }) : null] }));
|
|
68
|
+
return (_jsxs("div", { className: "bj-meta", children: [round.phase === 'settled' ? outcomeLine(round) : stakeLine(round), round.pointsGranted ? _jsxs("div", { children: ["\u672C\u671F\u5956\u6C60\u5DF2\u53D1\u5B8C\uFF0C\u6539\u4E3A\u53D1\u653E ", n(round.pointsGranted), " \u79EF\u5206\u3002"] }) : null] }));
|
|
60
69
|
}
|
|
61
|
-
|
|
70
|
+
/**
|
|
71
|
+
* 动作要额外掏多少 CHIP。镜像服务端 `game.ts` 的 INCREMENT:双倍/分牌各再出
|
|
72
|
+
* 一份注额,保险出一半。抽成导出的纯函数,好让"余额不足时必须置灰"能被断言。
|
|
73
|
+
*/
|
|
74
|
+
export function actionCostChips(action, betChips) {
|
|
75
|
+
if (action === 'double' || action === 'split')
|
|
76
|
+
return betChips;
|
|
77
|
+
if (action === 'insure')
|
|
78
|
+
return Math.ceil(betChips / 2);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
function ActionBar({ round, me, onAction }) {
|
|
62
82
|
if (round.actions.length === 0)
|
|
63
83
|
return null;
|
|
64
|
-
|
|
84
|
+
const chips = me?.chips ?? 0;
|
|
85
|
+
return (_jsx("div", { className: "bj-actions", children: round.actions.map((action) => {
|
|
86
|
+
// 服务端只按牌面给可选动作,不看余额;余额不够时点下去必然 402,
|
|
87
|
+
// 只换来一句笼统的失败提示。这里替玩家先拦住并说明原因。
|
|
88
|
+
const cost = actionCostChips(action, round.betChips);
|
|
89
|
+
const short = cost > chips;
|
|
90
|
+
return (_jsxs("button", { type: "button", disabled: short, title: short ? `还要再出 ${n(cost)} CHIP,余额不足` : undefined, onClick: () => onAction(action), children: [ACTION_LABELS[action] ?? action, cost > 0 ? `(${n(cost)})` : ''] }, action));
|
|
91
|
+
}) }));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 注额校验。抽成导出的纯函数,好让边界(0、小数、超余额、超服务端上限)
|
|
95
|
+
* 能被直接断言,而不是只能靠驱动输入框间接覆盖。
|
|
96
|
+
*/
|
|
97
|
+
export function betError(chips, opts) {
|
|
98
|
+
if (!Number.isInteger(chips) || chips < 1)
|
|
99
|
+
return '注额要填正整数。';
|
|
100
|
+
if (chips > opts.maxChips)
|
|
101
|
+
return `单局最多 ${n(opts.maxChips)} CHIP。`;
|
|
102
|
+
if (chips > opts.balanceChips)
|
|
103
|
+
return '余额不足以下这个注。';
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function DealButtons({ me, onDeal, freeLabel }) {
|
|
107
|
+
const freeLeft = me?.freeHandsRemaining ?? 0;
|
|
108
|
+
const presets = me ? [me.baseBetChips, me.baseBetChips * 2] : [];
|
|
109
|
+
return (_jsxs("div", { className: "bj-actions", children: [_jsxs("button", { type: "button", disabled: freeLeft <= 0, onClick: () => onDeal(), children: [freeLabel, me ? `·剩 ${freeLeft} 手` : ''] }), presets.map((amount) => (_jsxs("button", { type: "button", disabled: (me?.chips ?? 0) < amount, onClick: () => onDeal(amount), children: [n(amount), " CHIP"] }, amount)))] }));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 自定义注额。**只在服务端给出了 `maxRaiseBetChips` 时出现**——插件可能连到
|
|
113
|
+
* 比这个字段更早的服务端,那时没有可信上界,宁可退化成只有预设按钮,也不要
|
|
114
|
+
* 让玩家填一个必定被服务端 400 掉的数字。
|
|
115
|
+
*/
|
|
116
|
+
function CustomBet({ me, onDeal }) {
|
|
117
|
+
const max = me.maxRaiseBetChips;
|
|
118
|
+
const [open, setOpen] = useState(false);
|
|
119
|
+
const [text, setText] = useState('');
|
|
120
|
+
if (max === undefined)
|
|
121
|
+
return null;
|
|
122
|
+
const chips = Number(text);
|
|
123
|
+
const err = text.trim() === '' ? '请输入注额。' : betError(chips, { balanceChips: me.chips, maxChips: max });
|
|
124
|
+
return (_jsxs("div", { children: [_jsx("div", { className: "bj-actions", children: _jsx("button", { type: "button", onClick: () => setOpen(!open), children: "\u81EA\u5B9A\u4E49\u6CE8\u989D" }) }), open ? (_jsxs("div", { className: "bj-custom", children: [_jsx("input", { type: "number", min: 1, max: max, value: text, onChange: (e) => setText(e.target.value), "aria-label": "\u81EA\u5B9A\u4E49\u6CE8\u989D" }), _jsx("button", { type: "button", disabled: err !== null, onClick: () => onDeal(chips), children: "\u5F00\u59CB" }), _jsx("span", { className: "bj-hint", children: err ?? `1 ~ ${n(max)} CHIP` })] })) : null] }));
|
|
125
|
+
}
|
|
126
|
+
function IdleTable({ me, onDeal }) {
|
|
127
|
+
const freeLeft = me?.freeHandsRemaining ?? 0;
|
|
128
|
+
const base = me?.baseBetChips ?? 0;
|
|
129
|
+
const presets = me ? [base, base * 2] : [];
|
|
130
|
+
const affordable = me ? presets.some((a) => me.chips >= a) : false;
|
|
131
|
+
return (_jsxs("div", { children: [_jsx("div", { className: "bj-meta", children: "\u5F53\u524D\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u724C\u5C40\u3002" }), _jsxs("div", { className: "bj-group", children: [_jsxs("div", { className: "bj-group-title", children: ["\u514D\u8D39\u624B \u00B7 ", freeLeft > 0 ? `今日剩 ${freeLeft} 手` : '今日已用完,明天再来'] }), me ? (_jsxs("div", { className: "bj-note", children: ["\u4E0D\u7528\u51FA CHIP\uFF1B\u8D62\u4E86\u62FF ", n(base), " CHIP\uFF08\u5929\u751F 21 \u70B9 ", n(Math.floor(base * 1.5)), "\uFF09\uFF0C\u8F93\u4E86\u4E0D\u635F\u5931\u4EFB\u4F55\u4E1C\u897F\u3002"] })) : null, _jsx("div", { className: "bj-actions", children: _jsx("button", { type: "button", disabled: freeLeft <= 0, onClick: () => onDeal(), children: "\u5F00\u59CB\u514D\u8D39\u7684\u4E00\u5C40" }) })] }), me ? (_jsxs("div", { className: "bj-group", children: [_jsxs("div", { className: "bj-group-title", children: ["\u7528\u4F59\u989D\u52A0\u6CE8", affordable ? '' : ' · 余额不足'] }), _jsx("div", { className: "bj-note", children: "\u52A0\u6CE8\u7684 CHIP \u4ECE\u4F60\u7684\u4F59\u989D\u91CC\u51FA\uFF0C\u8D62\u4E86\u7FFB\u500D\uFF0C\u8F93\u4E86\u6536\u4E0D\u56DE\u3002" }), _jsx("div", { className: "bj-actions", children: presets.map((amount) => (_jsxs("button", { type: "button", disabled: me.chips < amount, onClick: () => onDeal(amount), children: [n(amount), " CHIP"] }, amount))) }), _jsx(CustomBet, { me: me, onDeal: onDeal })] })) : null] }));
|
|
65
132
|
}
|
|
66
|
-
|
|
67
|
-
|
|
133
|
+
/**
|
|
134
|
+
* 结算后的出口。牌局结算不再让画面直接跳回空闲态——完整牌面与结果留在原地,
|
|
135
|
+
* 由玩家决定再来一局还是收起(0.1.2 之前这里什么都没有,爆牌瞬间就被冲掉了)。
|
|
136
|
+
*/
|
|
137
|
+
function SettledBar({ me, onDeal, onDismiss }) {
|
|
138
|
+
return (_jsxs("div", { children: [_jsx(DealButtons, { me: me, onDeal: onDeal, freeLabel: "\u518D\u6765\u4E00\u5C40\uFF08\u514D\u8D39" }), _jsx("div", { className: "bj-actions", children: _jsx("button", { type: "button", onClick: onDismiss, children: "\u6536\u8D77" }) })] }));
|
|
68
139
|
}
|
|
69
|
-
export function BlackjackTable({ state, onAction, onDeal, error }) {
|
|
140
|
+
export function BlackjackTable({ state, onAction, onDeal, onDismiss, error }) {
|
|
70
141
|
if (!state.consented)
|
|
71
142
|
return _jsx(ConsentPrompt, {});
|
|
72
143
|
const { me, round } = state;
|
|
73
|
-
return (_jsxs("div", { className: "bj-table", children: [_jsx("style", { children: CSS }), error ? _jsx("div", { className: "bj-error", children: error }) : null, me ? _jsx(MeHeader, { me: me }) : null, round ? (_jsxs("div", { children: [round.hands.map((hand, i) => (_jsx(HandRow, { hand: hand, label: round.hands.length > 1 ? `你的第 ${i + 1} 手` : '你的牌' }, i))), _jsx(DealerRow, { dealer: round.dealer }), _jsx(RoundFooter, { round: round }), _jsx(ActionBar, { round: round, onAction: onAction })] })) : (_jsx(
|
|
144
|
+
return (_jsxs("div", { className: "bj-table", children: [_jsx("style", { children: CSS }), error ? _jsx("div", { className: "bj-error", children: error }) : null, me ? _jsx(MeHeader, { me: me }) : null, round ? (_jsxs("div", { children: [round.hands.map((hand, i) => (_jsx(HandRow, { hand: hand, label: round.hands.length > 1 ? `你的第 ${i + 1} 手` : '你的牌' }, i))), _jsx(DealerRow, { dealer: round.dealer }), _jsx(RoundFooter, { round: round }), _jsx(ActionBar, { round: round, me: me, onAction: onAction }), round.phase === 'settled' ? _jsx(SettledBar, { me: me, onDeal: onDeal, onDismiss: onDismiss }) : null] })) : (_jsx(IdleTable, { me: me, onDeal: onDeal }))] }));
|
|
74
145
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useState } from 'react';
|
|
3
3
|
import { BlackjackTable } from './Table.js';
|
|
4
|
+
import { reduce } from './state.js';
|
|
4
5
|
/**
|
|
5
6
|
* Client module entry: registers the graphical table into the KEYED
|
|
6
7
|
* `conversation.chat.commandview` slot under `key: 'blackjack'`, so every
|
|
@@ -23,41 +24,54 @@ async function getJson(path, init) {
|
|
|
23
24
|
}
|
|
24
25
|
/** Brief, vocabulary-safe notice shown when an action/deal call fails; never echoes the raw server error text. */
|
|
25
26
|
const ACTION_FAILED_NOTICE = '上一次操作没有成功,已为你刷新到最新状态。';
|
|
26
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Container: owns data fetching. 牌局以发牌/动作的**返回值**为准,只有玩家点
|
|
29
|
+
* "收起"才清空;余额刷新单独走,绝不碰牌局。0.1.2 之前这里把动作返回值整个
|
|
30
|
+
* 丢掉、再拉一次 /api/state,而那个端点只报进行中的牌局——于是玩家一爆牌,
|
|
31
|
+
* 画面就跳回"当前没有进行中的牌局",从头到尾看不见自己怎么输的。
|
|
32
|
+
*/
|
|
27
33
|
function BlackjackTableContainer() {
|
|
28
34
|
const [state, setState] = useState(null);
|
|
29
35
|
const [actionError, setActionError] = useState(null);
|
|
30
|
-
|
|
36
|
+
/** 整份同步:挂载时,以及动作失败后状态存疑时。 */
|
|
37
|
+
const sync = useCallback(() => {
|
|
31
38
|
getJson('/blackjack/api/state')
|
|
32
|
-
.then(setState)
|
|
39
|
+
.then((payload) => setState((s) => reduce(s ?? { consented: false }, { type: 'synced', payload })))
|
|
33
40
|
.catch(() => setState({ consented: false }));
|
|
34
41
|
}, []);
|
|
42
|
+
/** 只取 me:动作之后余额与免费手会变,但牌局必须留着。 */
|
|
43
|
+
const refreshMe = useCallback(() => {
|
|
44
|
+
getJson('/blackjack/api/state')
|
|
45
|
+
.then((payload) => setState((s) => (s ? reduce(s, { type: 'meRefreshed', me: payload.me }) : s)))
|
|
46
|
+
.catch(() => { });
|
|
47
|
+
}, []);
|
|
35
48
|
useEffect(() => {
|
|
36
|
-
|
|
37
|
-
}, [
|
|
38
|
-
const
|
|
49
|
+
sync();
|
|
50
|
+
}, [sync]);
|
|
51
|
+
const post = useCallback((path, body) => {
|
|
39
52
|
getJson(path, {
|
|
40
53
|
method: 'POST',
|
|
41
54
|
headers: { 'content-type': 'application/json' },
|
|
42
55
|
body: JSON.stringify(body),
|
|
43
56
|
})
|
|
44
|
-
.then(() => {
|
|
57
|
+
.then((round) => {
|
|
45
58
|
setActionError(null);
|
|
46
|
-
|
|
59
|
+
setState((s) => (s ? reduce(s, { type: 'round', round }) : s));
|
|
60
|
+
refreshMe();
|
|
47
61
|
})
|
|
48
62
|
.catch(() => {
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
// notice stays up until the next successful action clears it.
|
|
63
|
+
// 失败(含 404 "no active round")后牌局状态存疑,整份重同步,
|
|
64
|
+
// 免得动作栏停在一个已经不存在的牌局上。
|
|
52
65
|
setActionError(ACTION_FAILED_NOTICE);
|
|
53
|
-
|
|
66
|
+
sync();
|
|
54
67
|
});
|
|
55
|
-
}, [
|
|
56
|
-
const onDeal = useCallback((betChips) =>
|
|
57
|
-
const onAction = useCallback((action) =>
|
|
68
|
+
}, [refreshMe, sync]);
|
|
69
|
+
const onDeal = useCallback((betChips) => post('/blackjack/api/deal', betChips === undefined ? {} : { betChips }), [post]);
|
|
70
|
+
const onAction = useCallback((action) => post('/blackjack/api/action', { action }), [post]);
|
|
71
|
+
const onDismiss = useCallback(() => setState((s) => (s ? reduce(s, { type: 'dismissed' }) : s)), []);
|
|
58
72
|
if (state === null)
|
|
59
73
|
return null;
|
|
60
|
-
return _jsx(BlackjackTable, { state: state, onAction: onAction, onDeal: onDeal, error: actionError ?? undefined });
|
|
74
|
+
return (_jsx(BlackjackTable, { state: state, onAction: onAction, onDeal: onDeal, onDismiss: onDismiss, error: actionError ?? undefined }));
|
|
61
75
|
}
|
|
62
76
|
export function apply(ctx) {
|
|
63
77
|
ctx.slots.inject('conversation.chat.commandview', () => ctx.slots.register({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { MeView, RoundView } from '../api.js';
|
|
2
|
+
/**
|
|
3
|
+
* 图形牌桌的状态迁移。抽成纯函数是为了让"结算后的牌局必须留在屏幕上"这条
|
|
4
|
+
* 能被直接断言——它原先埋在 React 容器里,没有 DOM 环境就测不到,于是
|
|
5
|
+
* 0.1.2 带着这个缺陷发了出去。
|
|
6
|
+
*
|
|
7
|
+
* 关键不变式:**只有 `dismissed` 能清掉 `round`。** 余额刷新绝不碰它。
|
|
8
|
+
* @module dsh-blackjack/client/state
|
|
9
|
+
*/
|
|
10
|
+
export interface TableState {
|
|
11
|
+
consented: boolean;
|
|
12
|
+
me?: MeView;
|
|
13
|
+
round?: RoundView;
|
|
14
|
+
}
|
|
15
|
+
export type TableEvent =
|
|
16
|
+
/** 挂载时的一次性同步:整份状态以服务端为准(含"当前没有进行中的牌局")。 */
|
|
17
|
+
{
|
|
18
|
+
type: 'synced';
|
|
19
|
+
payload: {
|
|
20
|
+
consented: boolean;
|
|
21
|
+
me?: MeView;
|
|
22
|
+
round?: RoundView;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** 发牌/动作的返回值。牌局以它为准——包括结算态,那正是要留在屏幕上的东西。 */
|
|
26
|
+
| {
|
|
27
|
+
type: 'round';
|
|
28
|
+
round: RoundView;
|
|
29
|
+
}
|
|
30
|
+
/** 动作之后余额与免费手会变,只更新 me。 */
|
|
31
|
+
| {
|
|
32
|
+
type: 'meRefreshed';
|
|
33
|
+
me?: MeView;
|
|
34
|
+
}
|
|
35
|
+
/** 玩家点了"收起",回到空闲态。 */
|
|
36
|
+
| {
|
|
37
|
+
type: 'dismissed';
|
|
38
|
+
};
|
|
39
|
+
export declare function reduce(state: TableState, event: TableEvent): TableState;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function reduce(state, event) {
|
|
2
|
+
switch (event.type) {
|
|
3
|
+
case 'synced':
|
|
4
|
+
return { consented: event.payload.consented, me: event.payload.me, round: event.payload.round };
|
|
5
|
+
case 'round':
|
|
6
|
+
return { ...state, round: event.round };
|
|
7
|
+
case 'meRefreshed':
|
|
8
|
+
return { ...state, me: event.me };
|
|
9
|
+
case 'dismissed':
|
|
10
|
+
return { ...state, round: undefined };
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
|
-
"use strict";var
|
|
6
|
+
"use strict";var h=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var H=(e,n)=>{for(var i in n)h(e,i,{get:n[i],enumerable:!0})},E=(e,n,i,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of T(n))!V.call(e,o)&&o!==i&&h(e,o,{get:()=>n[o],enumerable:!(a=R(n,o))||a.enumerable});return e};var I=e=>E(h({},"__esModule",{value:!0}),e);var Z={};H(Z,{apply:()=>Y,inject:()=>K});module.exports=I(Z);var c=require("react");var j=require("react");var b=e=>e.toLocaleString("en-US");function y(e){return e.mode==="free"?`\u8FD9\u662F\u514D\u8D39\u7684\u4E00\u5C40\uFF1A\u8D62\u4E86\u62FF ${b(e.betChips)} CHIP\uFF0C\u8F93\u4E86\u4E0D\u635F\u5931\u4EFB\u4F55\u4E1C\u897F\u3002`:`\u672C\u5C40\u5DF2\u52A0\u6CE8 ${b(e.betChips)} CHIP\uFF0C\u4ECE\u4F60\u7684\u4F59\u989D\u91CC\u51FA\u3002`}function C(e){let n=e.payoutChips??0,i=e.betChips;return e.hands.length>0&&e.hands.every(a=>a.outcome==="push")?e.mode==="free"?"\u5E73\u5C40\u3002\u8FD9\u624B\u4E0D\u635F\u5931\u4E5F\u4E0D\u83B7\u5F97\u3002":`\u5E73\u5C40\u3002\u52A0\u6CE8\u7684 ${b(i)} CHIP \u539F\u6837\u9000\u56DE\u3002`:n>0?e.mode==="free"?`\u8D62\u4E86\uFF0C\u62FF\u5230 ${b(n)} CHIP\u3002`:`\u8D62\u4E86\uFF0C\u62FF\u56DE ${b(n)} CHIP\uFF08\u52A0\u6CE8 ${b(i)}\uFF09\u3002`:e.mode==="free"?"\u8FD9\u624B\u8F93\u4E86\u3002\u514D\u8D39\u624B\u4E0D\u635F\u5931\u4EFB\u4F55\u4E1C\u897F\u3002":`\u8FD9\u624B\u8F93\u4E86\uFF0C\u52A0\u6CE8\u7684 ${b(i)} CHIP \u6536\u4E0D\u56DE\u3002`}var t=require("react/jsx-runtime"),P={S:"\u2660",H:"\u2665",D:"\u2666",C:"\u2663"},$=new Set(["H","D"]),l=e=>e.toLocaleString("en-US"),D={hit:"\u8981\u724C",stand:"\u505C\u724C",double:"\u53CC\u500D",split:"\u5206\u724C",insure:"\u4E70\u4FDD\u9669",decline:"\u4E0D\u4E70\u4FDD\u9669"},M=`
|
|
7
7
|
.bj-table { --bj-felt: #0f3d2e; --bj-line: rgba(255,255,255,.18); font-size: 14px; }
|
|
8
8
|
.bj-hand { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 4px 0; }
|
|
9
9
|
.bj-card {
|
|
@@ -22,13 +22,23 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
font: inherit; padding: 6px 14px; border-radius: 6px; border: 1px solid var(--bj-line);
|
|
23
23
|
background: rgba(255,255,255,.08); color: inherit; cursor: pointer;
|
|
24
24
|
}
|
|
25
|
-
.bj-actions button:hover { background: rgba(255,255,255,.16); }
|
|
25
|
+
.bj-actions button:hover:not(:disabled) { background: rgba(255,255,255,.16); }
|
|
26
|
+
.bj-actions button:disabled { opacity: .4; cursor: not-allowed; }
|
|
27
|
+
.bj-custom { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
|
|
28
|
+
.bj-custom input {
|
|
29
|
+
font: inherit; width: 9em; padding: 5px 8px; border-radius: 6px;
|
|
30
|
+
border: 1px solid var(--bj-line); background: rgba(255,255,255,.06); color: inherit;
|
|
31
|
+
}
|
|
32
|
+
.bj-hint { opacity: .7; font-size: 12px; }
|
|
33
|
+
.bj-group { margin-top: 12px; }
|
|
34
|
+
.bj-group-title { font-weight: 600; margin-bottom: 2px; }
|
|
35
|
+
.bj-note { opacity: .75; font-size: 12px; margin-bottom: 6px; }
|
|
26
36
|
.bj-meta { opacity: .85; margin-bottom: 8px; }
|
|
27
37
|
.bj-error {
|
|
28
38
|
color: #c0392b; border: 1px solid rgba(192,57,43,.4); border-radius: 6px;
|
|
29
39
|
padding: 4px 8px; margin-bottom: 8px;
|
|
30
40
|
}
|
|
31
|
-
`;function
|
|
41
|
+
`;function k({card:e}){if("hidden"in e)return(0,t.jsx)("div",{className:"bj-card bj-card-back","aria-label":"\u6697\u724C"});let n=P[e.suit]??e.suit,i=$.has(e.suit)?"bj-card-red":"bj-card-black";return(0,t.jsx)("div",{className:`bj-card ${i}`,children:`${e.rank}${n}`})}function B({hand:e,label:n}){let i=e.soft?"\u8F6F":"";return(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"bj-meta",children:[n,"\uFF08",i,e.total,"\uFF09",e.doubled?" [\u53CC\u500D]":""]}),(0,t.jsx)("div",{className:"bj-hand",children:e.cards.map((a,o)=>(0,t.jsx)(k,{card:a},o))})]})}function J({dealer:e}){return(0,t.jsxs)("div",{children:[(0,t.jsxs)("div",{className:"bj-meta",children:["\u5E84\u5BB6",e.total!==void 0?`\uFF08${e.total}\uFF09`:""]}),(0,t.jsx)("div",{className:"bj-hand",children:e.cards.map((n,i)=>(0,t.jsx)(k,{card:n},i))})]})}function L(){return(0,t.jsx)("div",{className:"bj-table",children:(0,t.jsxs)("p",{children:["\u8BF7\u5148\u5728\u547D\u4EE4\u884C\u8F93\u5165 ",(0,t.jsx)("code",{children:"/blackjack agree"}),"\uFF0C\u9605\u8BFB\u5E76\u540C\u610F\u89C4\u5219\u540E\u624D\u80FD\u5F00\u59CB\u3002"]})})}function A({me:e}){return(0,t.jsxs)("div",{className:"bj-meta",children:["\u4F59\u989D\uFF1A",l(e.chips)," CHIP\u3000\u4ECA\u65E5\u5269\u4F59\u514D\u8D39\u624B\uFF1A",e.freeHandsRemaining,e.points>0?`\u3000\u79EF\u5206\uFF1A${l(e.points)}`:"",e.poolEmpty?"\uFF08\u672C\u671F\u5956\u6C60\u5DF2\u53D1\u5B8C\uFF0C\u73B0\u5728\u8D62\u724C\u5C06\u53D1\u653E\u79EF\u5206\uFF09":"",e.sponsorText?(0,t.jsx)("div",{children:e.sponsorText}):null]})}function X({round:e}){return(0,t.jsxs)("div",{className:"bj-meta",children:[e.phase==="settled"?C(e):y(e),e.pointsGranted?(0,t.jsxs)("div",{children:["\u672C\u671F\u5956\u6C60\u5DF2\u53D1\u5B8C\uFF0C\u6539\u4E3A\u53D1\u653E ",l(e.pointsGranted)," \u79EF\u5206\u3002"]}):null]})}function O(e,n){return e==="double"||e==="split"?n:e==="insure"?Math.ceil(n/2):0}function z({round:e,me:n,onAction:i}){if(e.actions.length===0)return null;let a=n?.chips??0;return(0,t.jsx)("div",{className:"bj-actions",children:e.actions.map(o=>{let r=O(o,e.betChips),s=r>a;return(0,t.jsxs)("button",{type:"button",disabled:s,title:s?`\u8FD8\u8981\u518D\u51FA ${l(r)} CHIP\uFF0C\u4F59\u989D\u4E0D\u8DB3`:void 0,onClick:()=>i(o),children:[D[o]??o,r>0?`\uFF08${l(r)}\uFF09`:""]},o)})})}function U(e,n){return!Number.isInteger(e)||e<1?"\u6CE8\u989D\u8981\u586B\u6B63\u6574\u6570\u3002":e>n.maxChips?`\u5355\u5C40\u6700\u591A ${l(n.maxChips)} CHIP\u3002`:e>n.balanceChips?"\u4F59\u989D\u4E0D\u8DB3\u4EE5\u4E0B\u8FD9\u4E2A\u6CE8\u3002":null}function _({me:e,onDeal:n,freeLabel:i}){let a=e?.freeHandsRemaining??0,o=e?[e.baseBetChips,e.baseBetChips*2]:[];return(0,t.jsxs)("div",{className:"bj-actions",children:[(0,t.jsxs)("button",{type:"button",disabled:a<=0,onClick:()=>n(),children:[i,e?`\xB7\u5269 ${a} \u624B`:""]}),o.map(r=>(0,t.jsxs)("button",{type:"button",disabled:(e?.chips??0)<r,onClick:()=>n(r),children:[l(r)," CHIP"]},r))]})}function F({me:e,onDeal:n}){let i=e.maxRaiseBetChips,[a,o]=(0,j.useState)(!1),[r,s]=(0,j.useState)("");if(i===void 0)return null;let m=Number(r),p=r.trim()===""?"\u8BF7\u8F93\u5165\u6CE8\u989D\u3002":U(m,{balanceChips:e.chips,maxChips:i});return(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bj-actions",children:(0,t.jsx)("button",{type:"button",onClick:()=>o(!a),children:"\u81EA\u5B9A\u4E49\u6CE8\u989D"})}),a?(0,t.jsxs)("div",{className:"bj-custom",children:[(0,t.jsx)("input",{type:"number",min:1,max:i,value:r,onChange:v=>s(v.target.value),"aria-label":"\u81EA\u5B9A\u4E49\u6CE8\u989D"}),(0,t.jsx)("button",{type:"button",disabled:p!==null,onClick:()=>n(m),children:"\u5F00\u59CB"}),(0,t.jsx)("span",{className:"bj-hint",children:p??`1 \uFF5E ${l(i)} CHIP`})]}):null]})}function G({me:e,onDeal:n}){let i=e?.freeHandsRemaining??0,a=e?.baseBetChips??0,o=e?[a,a*2]:[],r=e?o.some(s=>e.chips>=s):!1;return(0,t.jsxs)("div",{children:[(0,t.jsx)("div",{className:"bj-meta",children:"\u5F53\u524D\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684\u724C\u5C40\u3002"}),(0,t.jsxs)("div",{className:"bj-group",children:[(0,t.jsxs)("div",{className:"bj-group-title",children:["\u514D\u8D39\u624B \xB7 ",i>0?`\u4ECA\u65E5\u5269 ${i} \u624B`:"\u4ECA\u65E5\u5DF2\u7528\u5B8C\uFF0C\u660E\u5929\u518D\u6765"]}),e?(0,t.jsxs)("div",{className:"bj-note",children:["\u4E0D\u7528\u51FA CHIP\uFF1B\u8D62\u4E86\u62FF ",l(a)," CHIP\uFF08\u5929\u751F 21 \u70B9 ",l(Math.floor(a*1.5)),"\uFF09\uFF0C\u8F93\u4E86\u4E0D\u635F\u5931\u4EFB\u4F55\u4E1C\u897F\u3002"]}):null,(0,t.jsx)("div",{className:"bj-actions",children:(0,t.jsx)("button",{type:"button",disabled:i<=0,onClick:()=>n(),children:"\u5F00\u59CB\u514D\u8D39\u7684\u4E00\u5C40"})})]}),e?(0,t.jsxs)("div",{className:"bj-group",children:[(0,t.jsxs)("div",{className:"bj-group-title",children:["\u7528\u4F59\u989D\u52A0\u6CE8",r?"":" \xB7 \u4F59\u989D\u4E0D\u8DB3"]}),(0,t.jsx)("div",{className:"bj-note",children:"\u52A0\u6CE8\u7684 CHIP \u4ECE\u4F60\u7684\u4F59\u989D\u91CC\u51FA\uFF0C\u8D62\u4E86\u7FFB\u500D\uFF0C\u8F93\u4E86\u6536\u4E0D\u56DE\u3002"}),(0,t.jsx)("div",{className:"bj-actions",children:o.map(s=>(0,t.jsxs)("button",{type:"button",disabled:e.chips<s,onClick:()=>n(s),children:[l(s)," CHIP"]},s))}),(0,t.jsx)(F,{me:e,onDeal:n})]}):null]})}function q({me:e,onDeal:n,onDismiss:i}){return(0,t.jsxs)("div",{children:[(0,t.jsx)(_,{me:e,onDeal:n,freeLabel:"\u518D\u6765\u4E00\u5C40\uFF08\u514D\u8D39"}),(0,t.jsx)("div",{className:"bj-actions",children:(0,t.jsx)("button",{type:"button",onClick:i,children:"\u6536\u8D77"})})]})}function w({state:e,onAction:n,onDeal:i,onDismiss:a,error:o}){if(!e.consented)return(0,t.jsx)(L,{});let{me:r,round:s}=e;return(0,t.jsxs)("div",{className:"bj-table",children:[(0,t.jsx)("style",{children:M}),o?(0,t.jsx)("div",{className:"bj-error",children:o}):null,r?(0,t.jsx)(A,{me:r}):null,s?(0,t.jsxs)("div",{children:[s.hands.map((m,p)=>(0,t.jsx)(B,{hand:m,label:s.hands.length>1?`\u4F60\u7684\u7B2C ${p+1} \u624B`:"\u4F60\u7684\u724C"},p)),(0,t.jsx)(J,{dealer:s.dealer}),(0,t.jsx)(X,{round:s}),(0,t.jsx)(z,{round:s,me:r,onAction:n}),s.phase==="settled"?(0,t.jsx)(q,{me:r,onDeal:i,onDismiss:a}):null]}):(0,t.jsx)(G,{me:r,onDeal:i})]})}function f(e,n){switch(n.type){case"synced":return{consented:n.payload.consented,me:n.payload.me,round:n.payload.round};case"round":return{...e,round:n.round};case"meRefreshed":return{...e,me:n.me};case"dismissed":return{...e,round:void 0}}}var S=require("react/jsx-runtime"),K=["slots"];async function x(e,n){let i=await fetch(e,n),a=await i.text(),o=a.length>0?JSON.parse(a):{};if(!i.ok){let r=typeof o?.error=="string"?o.error:`${i.status}`;throw new Error(r)}return o}var Q="\u4E0A\u4E00\u6B21\u64CD\u4F5C\u6CA1\u6709\u6210\u529F\uFF0C\u5DF2\u4E3A\u4F60\u5237\u65B0\u5230\u6700\u65B0\u72B6\u6001\u3002";function W(){let[e,n]=(0,c.useState)(null),[i,a]=(0,c.useState)(null),o=(0,c.useCallback)(()=>{x("/blackjack/api/state").then(d=>n(u=>f(u??{consented:!1},{type:"synced",payload:d}))).catch(()=>n({consented:!1}))},[]),r=(0,c.useCallback)(()=>{x("/blackjack/api/state").then(d=>n(u=>u&&f(u,{type:"meRefreshed",me:d.me}))).catch(()=>{})},[]);(0,c.useEffect)(()=>{o()},[o]);let s=(0,c.useCallback)((d,u)=>{x(d,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(u)}).then(N=>{a(null),n(g=>g&&f(g,{type:"round",round:N})),r()}).catch(()=>{a(Q),o()})},[r,o]),m=(0,c.useCallback)(d=>s("/blackjack/api/deal",d===void 0?{}:{betChips:d}),[s]),p=(0,c.useCallback)(d=>s("/blackjack/api/action",{action:d}),[s]),v=(0,c.useCallback)(()=>n(d=>d&&f(d,{type:"dismissed"})),[]);return e===null?null:(0,S.jsx)(w,{state:e,onAction:p,onDeal:m,onDismiss:v,error:i??void 0})}function Y(e){e.slots.inject("conversation.chat.commandview",()=>e.slots.register({name:"conversation.chat.commandview",key:"blackjack",inject:()=>({})},W))}
|
|
32
42
|
return module.exports;
|
|
33
43
|
}
|
|
34
44
|
});
|
package/dist/copy.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RoundView } from './api.js';
|
|
2
|
+
/** 这一局的性质与代价。进行中时显示,让玩家随时知道自己在打哪种局。 */
|
|
3
|
+
export declare function stakeLine(round: RoundView): string;
|
|
4
|
+
/**
|
|
5
|
+
* 结算结果。分模式措辞——原先两种局共用一句「本手没有收获,明天再来」,
|
|
6
|
+
* 而加注局只要有余额马上就能再来,那句话是错的。
|
|
7
|
+
*/
|
|
8
|
+
export declare function outcomeLine(round: RoundView): string;
|
package/dist/copy.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 牌局文案的单一来源。命令面(render.ts)与图形牌桌(client/Table.tsx)共用
|
|
3
|
+
* 这里的字符串,否则两边会各说各话——而运营者第一次自己上手时,正是因为
|
|
4
|
+
* 界面没讲清楚"免费手"和"加注"的区别而看不懂在玩什么。
|
|
5
|
+
*
|
|
6
|
+
* 红线(见 render.ts 的 BANNED):单位一律 CHIP,不用「筹码」;不出现赌博词汇。
|
|
7
|
+
* @module dsh-blackjack/copy
|
|
8
|
+
*/
|
|
9
|
+
const n = (v) => v.toLocaleString('en-US');
|
|
10
|
+
/** 这一局的性质与代价。进行中时显示,让玩家随时知道自己在打哪种局。 */
|
|
11
|
+
export function stakeLine(round) {
|
|
12
|
+
return round.mode === 'free'
|
|
13
|
+
? `这是免费的一局:赢了拿 ${n(round.betChips)} CHIP,输了不损失任何东西。`
|
|
14
|
+
: `本局已加注 ${n(round.betChips)} CHIP,从你的余额里出。`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 结算结果。分模式措辞——原先两种局共用一句「本手没有收获,明天再来」,
|
|
18
|
+
* 而加注局只要有余额马上就能再来,那句话是错的。
|
|
19
|
+
*/
|
|
20
|
+
export function outcomeLine(round) {
|
|
21
|
+
const payout = round.payoutChips ?? 0;
|
|
22
|
+
const bet = round.betChips;
|
|
23
|
+
// 加注局平局会原样退回本金,payout > 0 —— 先判平局,否则会被说成赢了。
|
|
24
|
+
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)})。`;
|
|
33
|
+
}
|
|
34
|
+
return round.mode === 'free'
|
|
35
|
+
? '这手输了。免费手不损失任何东西。'
|
|
36
|
+
: `这手输了,加注的 ${n(bet)} CHIP 收不回。`;
|
|
37
|
+
}
|
package/dist/render.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stakeLine, outcomeLine } from './copy.js';
|
|
1
2
|
const SUITS = { S: '♠', H: '♥', D: '♦', C: '♣' };
|
|
2
3
|
/**
|
|
3
4
|
* Regex of vocabulary this project's copy must never contain (parent spec §2
|
|
@@ -37,13 +38,14 @@ export function renderRound(view) {
|
|
|
37
38
|
lines.push(`庄家:${dealer}${view.dealer.total !== undefined ? `(${view.dealer.total})` : ''}`);
|
|
38
39
|
if (view.phase === 'settled') {
|
|
39
40
|
const won = view.payoutChips ?? 0;
|
|
40
|
-
lines.push(
|
|
41
|
+
lines.push(outcomeLine(view));
|
|
41
42
|
if (view.pointsGranted)
|
|
42
43
|
lines.push(`本期奖池已发完,改为发放 ${n(view.pointsGranted)} 积分。`);
|
|
43
44
|
if (won > 0)
|
|
44
45
|
lines.push('(已获得的 CHIP 有效期至本赛季末)');
|
|
45
46
|
}
|
|
46
47
|
else {
|
|
48
|
+
lines.push(stakeLine(view));
|
|
47
49
|
const map = {
|
|
48
50
|
hit: '/bj-hit 要牌', stand: '/bj-stand 停牌', double: '/bj-double 双倍',
|
|
49
51
|
split: '/bj-split 分牌', insure: '/bj-insure 买保险', decline: '/bj-decline 不买保险',
|