dsh-poker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -0
  4. package/cordis.patch.yml +3 -0
  5. package/lib/client.js +2419 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/index.js +3051 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/invariant-C3ZiFskw.js +13 -0
  10. package/lib/invariant-C3ZiFskw.js.map +1 -0
  11. package/lib/invariant.js +3 -0
  12. package/lib/join/card-back-landlord.png +0 -0
  13. package/lib/join/card-back.png +0 -0
  14. package/lib/join/face-a.png +0 -0
  15. package/lib/join/face-j.png +0 -0
  16. package/lib/join/face-k.png +0 -0
  17. package/lib/join/face-q.png +0 -0
  18. package/lib/join/index.html +13 -0
  19. package/lib/join/index.js +8910 -0
  20. package/lib/join/index.js.map +1 -0
  21. package/lib/join/join.css +14 -0
  22. package/lib/join/joker-black.png +0 -0
  23. package/lib/join/joker-red.png +0 -0
  24. package/lib/join/role-farmer-b.png +0 -0
  25. package/lib/join/role-farmer-c.png +0 -0
  26. package/lib/join/role-farmer.png +0 -0
  27. package/lib/join/role-landlord-b.png +0 -0
  28. package/lib/join/role-landlord.png +0 -0
  29. package/lib/join/role-spectator.png +0 -0
  30. package/lib/types/client/CardBack.d.ts +7 -0
  31. package/lib/types/client/CardBack.d.ts.map +1 -0
  32. package/lib/types/client/CardFace.d.ts +8 -0
  33. package/lib/types/client/CardFace.d.ts.map +1 -0
  34. package/lib/types/client/HostApp.d.ts +4 -0
  35. package/lib/types/client/HostApp.d.ts.map +1 -0
  36. package/lib/types/client/InviteDialog.d.ts +15 -0
  37. package/lib/types/client/InviteDialog.d.ts.map +1 -0
  38. package/lib/types/client/JoinForm.d.ts +13 -0
  39. package/lib/types/client/JoinForm.d.ts.map +1 -0
  40. package/lib/types/client/LobbyView.d.ts +34 -0
  41. package/lib/types/client/LobbyView.d.ts.map +1 -0
  42. package/lib/types/client/SeatAvatar.d.ts +18 -0
  43. package/lib/types/client/SeatAvatar.d.ts.map +1 -0
  44. package/lib/types/client/SeatRow.d.ts +8 -0
  45. package/lib/types/client/SeatRow.d.ts.map +1 -0
  46. package/lib/types/client/SettingsCard.d.ts +6 -0
  47. package/lib/types/client/SettingsCard.d.ts.map +1 -0
  48. package/lib/types/client/SettlementView.d.ts +6 -0
  49. package/lib/types/client/SettlementView.d.ts.map +1 -0
  50. package/lib/types/client/TableView.d.ts +16 -0
  51. package/lib/types/client/TableView.d.ts.map +1 -0
  52. package/lib/types/client/card-stack.d.ts +3 -0
  53. package/lib/types/client/card-stack.d.ts.map +1 -0
  54. package/lib/types/client/host-api.d.ts +50 -0
  55. package/lib/types/client/host-api.d.ts.map +1 -0
  56. package/lib/types/client/index.d.ts +5 -0
  57. package/lib/types/client/index.d.ts.map +1 -0
  58. package/lib/types/client/slots.d.ts +3 -0
  59. package/lib/types/client/slots.d.ts.map +1 -0
  60. package/lib/types/client/store.d.ts +15 -0
  61. package/lib/types/client/store.d.ts.map +1 -0
  62. package/lib/types/config.d.ts +76 -0
  63. package/lib/types/config.d.ts.map +1 -0
  64. package/lib/types/crypto.d.ts +9 -0
  65. package/lib/types/crypto.d.ts.map +1 -0
  66. package/lib/types/engine/cards.d.ts +22 -0
  67. package/lib/types/engine/cards.d.ts.map +1 -0
  68. package/lib/types/engine/deal.d.ts +11 -0
  69. package/lib/types/engine/deal.d.ts.map +1 -0
  70. package/lib/types/engine/hands.d.ts +16 -0
  71. package/lib/types/engine/hands.d.ts.map +1 -0
  72. package/lib/types/engine/index.d.ts +7 -0
  73. package/lib/types/engine/index.d.ts.map +1 -0
  74. package/lib/types/engine/play.d.ts +73 -0
  75. package/lib/types/engine/play.d.ts.map +1 -0
  76. package/lib/types/engine/random.d.ts +2 -0
  77. package/lib/types/engine/random.d.ts.map +1 -0
  78. package/lib/types/engine/score.d.ts +11 -0
  79. package/lib/types/engine/score.d.ts.map +1 -0
  80. package/lib/types/identity/player.d.ts +6 -0
  81. package/lib/types/identity/player.d.ts.map +1 -0
  82. package/lib/types/index.d.ts +8 -0
  83. package/lib/types/index.d.ts.map +1 -0
  84. package/lib/types/invariant.d.ts +9 -0
  85. package/lib/types/invariant.d.ts.map +1 -0
  86. package/lib/types/join/JoinApp.d.ts +2 -0
  87. package/lib/types/join/JoinApp.d.ts.map +1 -0
  88. package/lib/types/join/main.d.ts +2 -0
  89. package/lib/types/join/main.d.ts.map +1 -0
  90. package/lib/types/net/auth.d.ts +16 -0
  91. package/lib/types/net/auth.d.ts.map +1 -0
  92. package/lib/types/net/join-page.d.ts +6 -0
  93. package/lib/types/net/join-page.d.ts.map +1 -0
  94. package/lib/types/net/json.d.ts +3 -0
  95. package/lib/types/net/json.d.ts.map +1 -0
  96. package/lib/types/net/protocol.d.ts +2 -0
  97. package/lib/types/net/protocol.d.ts.map +1 -0
  98. package/lib/types/net/routes.d.ts +4 -0
  99. package/lib/types/net/routes.d.ts.map +1 -0
  100. package/lib/types/net/views.d.ts +3 -0
  101. package/lib/types/net/views.d.ts.map +1 -0
  102. package/lib/types/net/ws.d.ts +8 -0
  103. package/lib/types/net/ws.d.ts.map +1 -0
  104. package/lib/types/persist/domain.d.ts +104 -0
  105. package/lib/types/persist/domain.d.ts.map +1 -0
  106. package/lib/types/persist/schemas.d.ts +79 -0
  107. package/lib/types/persist/schemas.d.ts.map +1 -0
  108. package/lib/types/room/RoomManager.d.ts +142 -0
  109. package/lib/types/room/RoomManager.d.ts.map +1 -0
  110. package/lib/types/settle/audit.d.ts +15 -0
  111. package/lib/types/settle/audit.d.ts.map +1 -0
  112. package/lib/types/settle/escrow.d.ts +4 -0
  113. package/lib/types/settle/escrow.d.ts.map +1 -0
  114. package/lib/types/settle/grant.d.ts +7 -0
  115. package/lib/types/settle/grant.d.ts.map +1 -0
  116. package/lib/types/settle/ledger.d.ts +39 -0
  117. package/lib/types/settle/ledger.d.ts.map +1 -0
  118. package/lib/types/settle/math.d.ts +12 -0
  119. package/lib/types/settle/math.d.ts.map +1 -0
  120. package/lib/types/types.d.ts +253 -0
  121. package/lib/types/types.d.ts.map +1 -0
  122. package/package.json +116 -0
  123. package/prepare.mjs +16 -0
package/lib/client.js ADDED
@@ -0,0 +1,2419 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-poker", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
+ value: mod,
22
+ enumerable: true
23
+ }) : target, mod));
24
+
25
+ //#endregion
26
+ let react = require("react");
27
+ react = __toESM(react);
28
+ let react_dom = require("react-dom");
29
+ react_dom = __toESM(react_dom);
30
+ let react_jsx_runtime = require("react/jsx-runtime");
31
+ react_jsx_runtime = __toESM(react_jsx_runtime);
32
+
33
+ //#region src/settle/math.ts
34
+ const STAKE_LADDER_M = [
35
+ 1,
36
+ 2,
37
+ 5,
38
+ 10,
39
+ 20,
40
+ 50,
41
+ 100
42
+ ];
43
+ const DEFAULT_STAKE_M = 1;
44
+ const DEFAULT_MAX_MULTIPLIER = 8;
45
+ const DEFAULT_WELCOME_ATOMS = 200000000n;
46
+ const ATOMS_PER_M = 1000000n;
47
+ const ATOMS_PER_B = 1000000000n;
48
+ function stakeAtomsFromM(stakeM) {
49
+ return BigInt(stakeM) * ATOMS_PER_M;
50
+ }
51
+ function perFarmerCapAtoms(stakeAtoms, maxMultiplier) {
52
+ return stakeAtoms * 3n * BigInt(maxMultiplier) * 2n;
53
+ }
54
+ function seatCapAtoms(stakeAtoms, maxMultiplier, seatCount = 3) {
55
+ return BigInt(seatCount - 1) * perFarmerCapAtoms(stakeAtoms, maxMultiplier);
56
+ }
57
+ /** Display token atoms as AI-token units: M (million) or B (billion). Never dump raw zeros. */
58
+ function formatM(atoms) {
59
+ const sign = atoms < 0n ? "-" : "";
60
+ const abs = atoms < 0n ? -atoms : atoms;
61
+ if (abs >= ATOMS_PER_B) return `${sign}${formatScaled(abs, ATOMS_PER_B)}B`;
62
+ return `${sign}${formatScaled(abs, ATOMS_PER_M)}M`;
63
+ }
64
+ function formatScaled(abs, unit) {
65
+ const whole = abs / unit;
66
+ const rem = abs % unit;
67
+ if (rem === 0n) return whole.toString();
68
+ const decimals = unit.toString().length - 1;
69
+ const frac = rem.toString().padStart(decimals, "0").replace(/0+$/, "");
70
+ return `${whole.toString()}.${frac}`;
71
+ }
72
+
73
+ //#endregion
74
+ //#region src/client/host-api.ts
75
+ const PREFIX = "/doudizhu";
76
+ async function request(path, init) {
77
+ const response = await fetch(`${PREFIX}${path}`, {
78
+ credentials: "same-origin",
79
+ ...init,
80
+ headers: {
81
+ "content-type": "application/json",
82
+ "x-requested-with": "doudizhu",
83
+ ...init?.headers ?? {}
84
+ }
85
+ });
86
+ const text = await response.text();
87
+ const body = text ? JSON.parse(text) : {};
88
+ if (!response.ok) throw new Error(body.message ?? body.error ?? response.statusText);
89
+ return body;
90
+ }
91
+ function createRoom(input) {
92
+ return request("/api/rooms", {
93
+ method: "POST",
94
+ body: JSON.stringify(input)
95
+ });
96
+ }
97
+ function joinRoom(input) {
98
+ return request("/api/join", {
99
+ method: "POST",
100
+ body: JSON.stringify(input)
101
+ });
102
+ }
103
+ function sendCommand(roomId, command) {
104
+ return request(`/api/rooms/${roomId}/command`, {
105
+ method: "POST",
106
+ body: JSON.stringify(command)
107
+ });
108
+ }
109
+ function fetchSince(roomId, seq) {
110
+ return request(`/api/rooms/${roomId}/since?seq=${seq}`);
111
+ }
112
+ function connectChannel(ticket, onEvent, fallback) {
113
+ const proto = location.protocol === "https:" ? "wss" : "ws";
114
+ let closed = false;
115
+ let ws = null;
116
+ let poll;
117
+ try {
118
+ ws = new WebSocket(`${proto}://${location.host}${PREFIX}/ws?ticket=${encodeURIComponent(ticket)}`, "doudizhu.v1");
119
+ ws.onmessage = (event) => {
120
+ try {
121
+ onEvent(JSON.parse(String(event.data)));
122
+ } catch {}
123
+ };
124
+ ws.onerror = () => {
125
+ startPoll();
126
+ };
127
+ ws.onclose = () => {
128
+ if (!closed) startPoll();
129
+ };
130
+ } catch {
131
+ startPoll();
132
+ }
133
+ function startPoll() {
134
+ if (poll || closed) return;
135
+ poll = setInterval(() => {
136
+ fetchSince(fallback.roomId, fallback.seq()).then((body) => {
137
+ for (const event of body.events) onEvent(event);
138
+ }).catch(() => {});
139
+ }, 800);
140
+ }
141
+ return () => {
142
+ closed = true;
143
+ ws?.close();
144
+ if (poll) clearInterval(poll);
145
+ };
146
+ }
147
+
148
+ //#endregion
149
+ //#region \0dsh-css:/Users/gehonglu/remote-code/dsh-doudizhu/src/client/styles.module.css.mjs
150
+ const css = "._5O8MPa_root{z-index:20;background:var(--dsw-alias-bg-base,#fff);min-width:0;color:var(--dsw-alias-label-primary,#1a1a1a);pointer-events:auto;flex-direction:column;font-family:inherit;display:flex;position:fixed;inset:0;overflow:hidden}._5O8MPa_topbar{border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);justify-content:space-between;align-items:center;padding:12px 20px;display:flex}._5O8MPa_brand{letter-spacing:.01em;color:var(--dsw-alias-label-primary,#1a1a1a);font-size:15px;font-weight:600}._5O8MPa_muted{color:var(--dsw-alias-label-tertiary,#8a8a8a);font-size:13px}._5O8MPa_close{appearance:none;border:1px solid var(--dsw-alias-border-l2,#e8e8e8);color:var(--dsw-alias-label-secondary,#5c5c5c);cursor:pointer;font:inherit;background:0 0;border-radius:8px;padding:5px 14px;font-size:13px}._5O8MPa_close:hover{color:var(--dsw-alias-label-primary,#1a1a1a);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_lobby,._5O8MPa_settlement{flex:1;padding:24px 20px;overflow:auto}._5O8MPa_card{background:var(--dsw-alias-bg-layer-3,#fff);border:1px solid var(--dsw-alias-border-l2,#e8e8e8);border-radius:14px;max-width:760px;margin:0 auto;padding:28px 28px 24px}._5O8MPa_card h2,._5O8MPa_card h3{margin:0 0 4px;font-size:17px;font-weight:600;line-height:1.4}._5O8MPa_card>._5O8MPa_primary{align-self:center;width:100%;margin-top:4px}._5O8MPa_tabs{border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);gap:0;margin:-4px 0 16px;display:flex}._5O8MPa_tab{appearance:none;font:inherit;color:var(--dsw-alias-label-tertiary,#8a8a8a);cursor:pointer;background:0 0;border:0;border-bottom:2px solid #0000;flex:1;margin-bottom:-1px;padding:8px 12px;font-size:14px;font-weight:500}._5O8MPa_tab:hover{color:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_tabActive{color:var(--dsw-alias-label-primary,#1a1a1a);border-bottom-color:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_row{flex-wrap:wrap;gap:12px;margin:4px 0 8px;display:flex}._5O8MPa_createFields{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:4px 16px;display:grid}._5O8MPa_createFields>._5O8MPa_field{min-width:0}._5O8MPa_createFields>._5O8MPa_field+._5O8MPa_field{border-top:0}._5O8MPa_fieldWide{grid-column:1/-1}._5O8MPa_createFields ._5O8MPa_select,._5O8MPa_createFields ._5O8MPa_input{box-sizing:border-box;width:100%}._5O8MPa_field{min-width:160px;color:var(--dsw-alias-label-primary,#1a1a1a);flex-direction:column;gap:6px;padding:12px 0;font-size:13px;font-weight:500;display:flex}._5O8MPa_field+._5O8MPa_field{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8)}._5O8MPa_fieldRow{flex-direction:row;align-items:center;gap:8px}._5O8MPa_joinForm{flex-direction:column;display:flex}._5O8MPa_joinFields{flex-direction:column;width:100%;display:flex}._5O8MPa_joinFields ._5O8MPa_primary{width:100%;height:44px;font-weight:600}._5O8MPa_joinInline{gap:32px;padding:14px 0;display:flex}._5O8MPa_joinField{flex:1 1 0;align-items:center;gap:16px;min-width:0;display:flex}._5O8MPa_joinField ._5O8MPa_settingsLabel{white-space:nowrap;color:var(--dsw-alias-label-primary,#1a1a1a);flex:none;font-size:13px;font-weight:500;line-height:1.5}._5O8MPa_joinField ._5O8MPa_input{box-sizing:border-box;flex:auto;width:100%;min-width:0}@media (width<=480px){._5O8MPa_joinInline{flex-direction:column;gap:12px}._5O8MPa_joinFields ._5O8MPa_input{height:44px}}._5O8MPa_select,._5O8MPa_input{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);height:40px;color:var(--dsw-alias-label-primary,#1a1a1a);font:inherit;border-radius:10px;padding:0 14px;font-size:14px}._5O8MPa_select:focus-visible,._5O8MPa_input:focus-visible{border-color:var(--dsw-alias-brand-primary,#4d6bfe);outline:none}._5O8MPa_primary,._5O8MPa_ghost,._5O8MPa_danger{appearance:none;cursor:pointer;font:inherit;border-radius:10px;padding:7px 16px;font-size:14px;line-height:1.5}._5O8MPa_primary{background:var(--dsw-alias-label-primary,#1a1a1a);color:var(--dsw-alias-bg-layer-3,#fff);border:1px solid #0000}._5O8MPa_ghost{color:var(--dsw-alias-label-secondary,#5c5c5c);border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:0 0}._5O8MPa_ghost:hover:not(:disabled){color:var(--dsw-alias-label-primary,#1a1a1a);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_danger{color:var(--dsw-alias-label-error,#d64545);border:1px solid var(--dsw-alias-label-error,#d64545);background:0 0}._5O8MPa_primary:disabled,._5O8MPa_ghost:disabled{opacity:.4;cursor:default}._5O8MPa_hint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:0 0 8px;font-size:13px;font-weight:400;line-height:1.5}._5O8MPa_warn{color:var(--dsw-alias-label-secondary,#8a6a1a)}._5O8MPa_error{color:var(--dsw-alias-label-error,#d64545);margin:8px 0 0;font-size:13px}._5O8MPa_table{flex-direction:column;flex:1;gap:8px;min-width:0;min-height:0;padding:12px 20px 8px;display:flex;overflow:hidden auto}._5O8MPa_opponents{flex:none;justify-content:space-between;align-items:flex-start;gap:12px;width:100%;min-width:0;display:flex}._5O8MPa_opponentsThree{justify-content:space-between}._5O8MPa_seat{flex-direction:column;flex:1 1 0;align-items:flex-start;gap:4px;min-width:0;display:flex}._5O8MPa_seatRight{align-items:flex-end}._5O8MPa_seatCenter{align-items:center}._5O8MPa_seatHead{align-items:center;gap:8px;min-width:0;max-width:100%;display:flex}._5O8MPa_seatRight ._5O8MPa_seatHead{flex-direction:row-reverse}._5O8MPa_seatMeta{min-width:0;overflow:hidden}._5O8MPa_seatName{flex-wrap:wrap;align-items:center;gap:4px;font-size:15px;font-weight:600;display:flex}._5O8MPa_selfRow{flex-wrap:nowrap;flex:none;justify-content:center;align-items:center;gap:16px;width:100%;min-width:0;display:flex}._5O8MPa_selfSeat{flex-direction:column;flex:none;justify-content:center;align-items:center;gap:4px;display:flex}._5O8MPa_selfRow ._5O8MPa_hand{flex:auto;justify-content:flex-start;width:auto;min-width:0;padding-left:0}._5O8MPa_roleHero{flex-wrap:wrap;justify-content:center;gap:12px 16px;margin:12px 0 4px;display:flex}._5O8MPa_roleHeroItem{color:var(--dsw-alias-label-secondary,#5c5c5c);flex-direction:column;align-items:center;gap:6px;margin:0;font-size:12px;font-weight:500;display:flex}._5O8MPa_roleHeroImg{object-fit:contain;background:0 0;width:128px;height:128px}._5O8MPa_badge{background:var(--dsw-alias-bg-module-platform,#f3f3f3);color:var(--dsw-alias-label-secondary,#5c5c5c);border-radius:999px;flex:none;padding:3px 10px;font-size:13px;font-weight:600;display:inline-block}._5O8MPa_ready{color:var(--dsw-alias-label-success,#2f8f62);font-size:12px}._5O8MPa_avatar{object-fit:contain;background:0 0;width:96px;height:96px}._5O8MPa_remain{align-items:flex-end;gap:8px;max-width:100%;display:flex}._5O8MPa_remainCompact{flex-direction:column;align-items:flex-start;gap:2px;min-height:0}._5O8MPa_seatRight ._5O8MPa_remainCompact{align-items:flex-end}._5O8MPa_backs{isolation:isolate;align-items:flex-end;min-width:0;display:flex}._5O8MPa_backs>*{margin-left:-40px}._5O8MPa_backs>:first-child{margin-left:0}._5O8MPa_backsCompact{width:108px;min-width:0;max-width:100%;padding:0}._5O8MPa_backsCompact>*{margin-left:-28px}._5O8MPa_backsCompact>:first-child{margin-left:0}._5O8MPa_backsCompact ._5O8MPa_cardBack{width:44px;min-width:44px;height:62px;box-shadow:1px 1px #00000014}._5O8MPa_cardsLeft{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary,#1a1a1a);min-width:1.25em;font-size:22px;font-weight:700;line-height:1}._5O8MPa_centerPlay{flex:auto;justify-content:center;align-items:center;gap:8px;min-width:0;min-height:0;display:flex;overflow:hidden}._5O8MPa_bottom{flex-wrap:wrap;flex:none;justify-content:center;align-items:flex-end;gap:6px;display:flex}._5O8MPa_bottom ._5O8MPa_cardFace,._5O8MPa_bottom ._5O8MPa_cardBack{width:52px;height:73px}._5O8MPa_bottom ._5O8MPa_remain{gap:4px}._5O8MPa_hand{flex:none;justify-content:center;width:100%;min-width:0;padding:22px 8px 6px;display:flex;overflow-x:auto}._5O8MPa_handInner{align-items:flex-end;width:max-content;margin:0 auto;display:flex}._5O8MPa_handInner button{cursor:pointer;z-index:0;background:0 0;border:0;flex:none;margin-left:-36px;padding:0;position:relative}._5O8MPa_handInner button:first-child{margin-left:0}._5O8MPa_handInner button:hover{z-index:2}._5O8MPa_handInner button:has(._5O8MPa_selected){z-index:3}._5O8MPa_selected{transform:translateY(-18px)}._5O8MPa_actions{flex-wrap:wrap;flex:none;justify-content:center;gap:8px;display:flex}._5O8MPa_cardFace,._5O8MPa_cardBack{border:1px solid var(--dsw-alias-border-l2,#e0ddd6);color:#1a1a1a;background:#fff;border-radius:10px;flex-direction:column;flex:none;width:88px;height:124px;padding:0;display:flex;position:relative;overflow:hidden;box-shadow:0 1px 2px #0000000f}._5O8MPa_cardBack{color:#1a1a1a;background:#fff;justify-content:center;align-items:center;padding:0}._5O8MPa_cardBackLandlord{box-shadow:0 0 0 2px #c62828}._5O8MPa_backsCompact ._5O8MPa_cardBackLandlord{box-shadow:0 0 0 2px #c62828,1px 1px #00000014}._5O8MPa_red{color:#c62828}._5O8MPa_wild{box-shadow:0 0 0 2px var(--dsw-alias-brand-primary,#4d6bfe)}._5O8MPa_rank{z-index:1;text-shadow:0 0 3px #fff,0 0 6px #fff;font-size:15px;font-weight:700;line-height:1;position:absolute;top:4px;left:6px}._5O8MPa_suit{z-index:1;text-shadow:0 0 3px #fff,0 0 6px #fff;font-size:16px;line-height:1;position:absolute;bottom:4px;right:6px}._5O8MPa_plainFace{justify-content:center;align-items:center}._5O8MPa_suitCenter{font-size:36px;line-height:1}._5O8MPa_jokerFace{justify-content:center;align-items:center;padding:0}._5O8MPa_jokerArt,._5O8MPa_faceArt,._5O8MPa_cardBackArt{pointer-events:none;object-fit:contain;background:0 0;width:100%;height:100%}@media (width<=720px){._5O8MPa_table{padding:8px 10px 6px}._5O8MPa_avatar{width:56px;height:56px}._5O8MPa_roleHeroImg{width:96px;height:96px}._5O8MPa_selfRow{gap:8px}._5O8MPa_selfSeat{flex-direction:column;gap:4px}._5O8MPa_seatHead,._5O8MPa_seatRight ._5O8MPa_seatHead{flex-direction:column;align-items:flex-start;gap:2px}._5O8MPa_seatRight ._5O8MPa_seatHead{align-items:flex-end}._5O8MPa_cardFace,._5O8MPa_cardBack{width:72px;height:102px}._5O8MPa_hand{padding-top:20px}._5O8MPa_handInner button,._5O8MPa_backs>*{margin-left:-32px}._5O8MPa_backsCompact{width:78px;padding:0}._5O8MPa_backsCompact ._5O8MPa_cardBack{width:30px;min-width:30px;height:42px}._5O8MPa_backsCompact>*{margin-left:-18px}._5O8MPa_cardsLeft{font-size:16px}._5O8MPa_bottom ._5O8MPa_cardFace,._5O8MPa_bottom ._5O8MPa_cardBack{width:44px;height:62px}}._5O8MPa_sidebarSlot{box-sizing:border-box;width:100%;margin:0 2px 8px}._5O8MPa_sidebarSlotRail{align-self:flex-start;width:36px;margin:0 0 12px}._5O8MPa_sidebarBtn{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-button-elevated-fill,#fff);width:100%;height:38px;color:inherit;cursor:pointer;box-sizing:border-box;font:inherit;border-radius:12px;justify-content:center;align-items:center;gap:6px;padding:8px 16px;font-size:14px;font-weight:500;display:flex}._5O8MPa_sidebarBtn:hover{background:var(--dsw-alias-button-floating-hover,#0000000a)}._5O8MPa_sidebarBtnRail{background:0 0;border-color:#0000;width:36px;height:36px;padding:0}._5O8MPa_inviteBox{flex-direction:column;gap:8px;margin-top:12px;display:flex}._5O8MPa_codeRow,._5O8MPa_codeBar,._5O8MPa_codeEdit{flex-wrap:nowrap;align-items:center;gap:8px;display:flex}._5O8MPa_codeBar{width:100%}._5O8MPa_codeValue{letter-spacing:.12em;background:var(--dsw-alias-bg-module-platform,#f3f3f3);user-select:all;cursor:pointer;border-radius:8px;padding:4px 10px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;font-weight:700}._5O8MPa_codeInput{letter-spacing:.12em;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;font-weight:700}._5O8MPa_codeInput::placeholder{letter-spacing:0;font-size:13px;font-weight:500}._5O8MPa_titleBtn{appearance:none;font:inherit;color:inherit;cursor:pointer;background:0 0;border:0;padding:0;font-weight:600}._5O8MPa_titleBtn:disabled{cursor:default}._5O8MPa_miniHand{max-width:320px;display:flex;overflow:hidden}._5O8MPa_miniHand>*{flex:none;width:52px;height:73px;margin-left:-26px}._5O8MPa_miniHand>:first-child{margin-left:0}._5O8MPa_qr{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:#fff;border-radius:8px;width:132px;height:132px}._5O8MPa_timer{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);max-width:280px;color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:999px;justify-content:center;align-items:baseline;gap:6px;margin:0 auto;padding:10px 16px;font-size:14px;display:flex}._5O8MPa_timer strong{font-variant-numeric:tabular-nums;font-size:28px;font-weight:650;line-height:1}._5O8MPa_timerUrgent{border-color:var(--dsw-alias-label-error,#d64545);color:var(--dsw-alias-label-error,#d64545)}._5O8MPa_banner{background:var(--dsw-alias-bg-module-platform,#f6f6f6);color:var(--dsw-alias-label-secondary,#5c5c5c);border-bottom:1px solid var(--dsw-alias-border-l2,#e8e8e8);padding:8px 16px;font-size:13px}._5O8MPa_chat{max-height:56px;color:var(--dsw-alias-label-tertiary,#8a8a8a);flex:none;font-size:12px;overflow:auto}._5O8MPa_settingsCard{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:12px;list-style:none}._5O8MPa_settingsCard:hover{border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_settingsOpen{background:var(--dsw-alias-bg-layer-2,#fafafa);border-color:var(--dsw-alias-label-dimmed,#c8c8c8)}._5O8MPa_settingsHeader{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}._5O8MPa_settingsHeader:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:-2px}._5O8MPa_settingsHeadText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}._5O8MPa_settingsName{color:var(--dsw-alias-label-primary,#1a1a1a);font-size:15px;font-weight:600;line-height:1.4}._5O8MPa_settingsDesc{color:var(--dsw-alias-label-tertiary,#8a8a8a);font-size:13px;line-height:1.5}._5O8MPa_settingsChevron{color:var(--dsw-alias-label-tertiary,#8a8a8a);flex:none;transition:transform .16s}._5O8MPa_settingsChevronOpen{transform:rotate(180deg)}._5O8MPa_settingsBody{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8);margin:0 16px;padding-bottom:8px}._5O8MPa_settingsHint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:12px 0 0;font-size:12px;line-height:1.5}._5O8MPa_settingsField{flex-direction:column;gap:6px;padding:12px 0;display:flex}._5O8MPa_settingsField+._5O8MPa_settingsField{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8)}._5O8MPa_settingsLabel{color:var(--dsw-alias-label-primary,#1a1a1a);font-size:13px;font-weight:500;line-height:1.5}._5O8MPa_settingsSwitchRow{justify-content:space-between;align-items:center;gap:12px;display:flex}._5O8MPa_settingsSwitch{appearance:none;cursor:pointer;background:0 0;border:0;flex:none;padding:0}._5O8MPa_settingsSwitch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4d6bfe);outline-offset:2px;border-radius:999px}._5O8MPa_settingsSwitchTrack{background:var(--dsw-alias-border-l2,#d9d9d9);border-radius:999px;width:36px;height:20px;transition:background .16s;display:block;position:relative}._5O8MPa_settingsSwitchTrack[data-on]{background:var(--dsw-alias-label-primary,#1a1a1a)}._5O8MPa_settingsSwitchThumb{background:#fff;border-radius:50%;width:16px;height:16px;transition:transform .16s;position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #00000029}._5O8MPa_settingsSwitchTrack[data-on] ._5O8MPa_settingsSwitchThumb{transform:translate(16px)}._5O8MPa_settingsInput{border:1px solid var(--dsw-alias-border-l2,#e8e8e8);background:var(--dsw-alias-bg-layer-3,#fff);height:34px;font:inherit;color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}._5O8MPa_settingsInput:focus-visible{border-color:var(--dsw-alias-brand-primary,#4d6bfe);outline:none}._5O8MPa_settingsFieldHint{color:var(--dsw-alias-label-tertiary,#8a8a8a);margin:0;font-size:12px;line-height:1.5}._5O8MPa_settingsActions{border-top:1px solid var(--dsw-alias-border-l2,#e8e8e8);flex-wrap:wrap;align-items:center;gap:8px 12px;padding:12px 0 16px;display:flex}._5O8MPa_settingsSaveOk{color:var(--dsw-alias-label-success,#2f8f62);margin:0;font-size:12px;line-height:1.5}._5O8MPa_settingsSaveError{color:var(--dsw-alias-label-error,#d64545);margin:0;font-size:12px;line-height:1.5}";
151
+ const tagId = "dsh-poker/styles.module.css";
152
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
153
+ const tag = document.createElement("style");
154
+ tag.dataset.plugin = "dsh-poker";
155
+ tag.dataset.pluginCss = tagId;
156
+ tag.textContent = css;
157
+ document.head.appendChild(tag);
158
+ }
159
+ var styles_module_css_default = {
160
+ "settlement": "_5O8MPa_settlement",
161
+ "field": "_5O8MPa_field",
162
+ "ghost": "_5O8MPa_ghost",
163
+ "warn": "_5O8MPa_warn",
164
+ "tabActive": "_5O8MPa_tabActive",
165
+ "badge": "_5O8MPa_badge",
166
+ "remainCompact": "_5O8MPa_remainCompact",
167
+ "bottom": "_5O8MPa_bottom",
168
+ "handInner": "_5O8MPa_handInner",
169
+ "seatRight": "_5O8MPa_seatRight",
170
+ "cardBackArt": "_5O8MPa_cardBackArt",
171
+ "timerUrgent": "_5O8MPa_timerUrgent",
172
+ "settingsField": "_5O8MPa_settingsField",
173
+ "error": "_5O8MPa_error",
174
+ "seatMeta": "_5O8MPa_seatMeta",
175
+ "sidebarSlot": "_5O8MPa_sidebarSlot",
176
+ "codeValue": "_5O8MPa_codeValue",
177
+ "cardsLeft": "_5O8MPa_cardsLeft",
178
+ "joinInline": "_5O8MPa_joinInline",
179
+ "selfRow": "_5O8MPa_selfRow",
180
+ "rank": "_5O8MPa_rank",
181
+ "select": "_5O8MPa_select",
182
+ "settingsChevronOpen": "_5O8MPa_settingsChevronOpen",
183
+ "suitCenter": "_5O8MPa_suitCenter",
184
+ "joinField": "_5O8MPa_joinField",
185
+ "settingsSwitchThumb": "_5O8MPa_settingsSwitchThumb",
186
+ "titleBtn": "_5O8MPa_titleBtn",
187
+ "settingsFieldHint": "_5O8MPa_settingsFieldHint",
188
+ "timer": "_5O8MPa_timer",
189
+ "joinForm": "_5O8MPa_joinForm",
190
+ "settingsSwitchRow": "_5O8MPa_settingsSwitchRow",
191
+ "table": "_5O8MPa_table",
192
+ "joinFields": "_5O8MPa_joinFields",
193
+ "cardFace": "_5O8MPa_cardFace",
194
+ "codeInput": "_5O8MPa_codeInput",
195
+ "settingsInput": "_5O8MPa_settingsInput",
196
+ "seatName": "_5O8MPa_seatName",
197
+ "settingsHint": "_5O8MPa_settingsHint",
198
+ "cardBack": "_5O8MPa_cardBack",
199
+ "qr": "_5O8MPa_qr",
200
+ "lobby": "_5O8MPa_lobby",
201
+ "avatar": "_5O8MPa_avatar",
202
+ "topbar": "_5O8MPa_topbar",
203
+ "roleHeroItem": "_5O8MPa_roleHeroItem",
204
+ "root": "_5O8MPa_root",
205
+ "tabs": "_5O8MPa_tabs",
206
+ "tab": "_5O8MPa_tab",
207
+ "suit": "_5O8MPa_suit",
208
+ "plainFace": "_5O8MPa_plainFace",
209
+ "backs": "_5O8MPa_backs",
210
+ "row": "_5O8MPa_row",
211
+ "sidebarSlotRail": "_5O8MPa_sidebarSlotRail",
212
+ "hint": "_5O8MPa_hint",
213
+ "createFields": "_5O8MPa_createFields",
214
+ "settingsLabel": "_5O8MPa_settingsLabel",
215
+ "opponentsThree": "_5O8MPa_opponentsThree",
216
+ "jokerArt": "_5O8MPa_jokerArt",
217
+ "faceArt": "_5O8MPa_faceArt",
218
+ "fieldWide": "_5O8MPa_fieldWide",
219
+ "miniHand": "_5O8MPa_miniHand",
220
+ "opponents": "_5O8MPa_opponents",
221
+ "settingsCard": "_5O8MPa_settingsCard",
222
+ "seatCenter": "_5O8MPa_seatCenter",
223
+ "codeRow": "_5O8MPa_codeRow",
224
+ "settingsHeader": "_5O8MPa_settingsHeader",
225
+ "settingsDesc": "_5O8MPa_settingsDesc",
226
+ "settingsChevron": "_5O8MPa_settingsChevron",
227
+ "card": "_5O8MPa_card",
228
+ "selected": "_5O8MPa_selected",
229
+ "cardBackLandlord": "_5O8MPa_cardBackLandlord",
230
+ "inviteBox": "_5O8MPa_inviteBox",
231
+ "centerPlay": "_5O8MPa_centerPlay",
232
+ "settingsSwitchTrack": "_5O8MPa_settingsSwitchTrack",
233
+ "red": "_5O8MPa_red",
234
+ "wild": "_5O8MPa_wild",
235
+ "settingsSaveOk": "_5O8MPa_settingsSaveOk",
236
+ "settingsBody": "_5O8MPa_settingsBody",
237
+ "sidebarBtnRail": "_5O8MPa_sidebarBtnRail",
238
+ "settingsOpen": "_5O8MPa_settingsOpen",
239
+ "seat": "_5O8MPa_seat",
240
+ "ready": "_5O8MPa_ready",
241
+ "input": "_5O8MPa_input",
242
+ "roleHeroImg": "_5O8MPa_roleHeroImg",
243
+ "remain": "_5O8MPa_remain",
244
+ "sidebarBtn": "_5O8MPa_sidebarBtn",
245
+ "danger": "_5O8MPa_danger",
246
+ "codeEdit": "_5O8MPa_codeEdit",
247
+ "settingsSaveError": "_5O8MPa_settingsSaveError",
248
+ "chat": "_5O8MPa_chat",
249
+ "settingsActions": "_5O8MPa_settingsActions",
250
+ "backsCompact": "_5O8MPa_backsCompact",
251
+ "fieldRow": "_5O8MPa_fieldRow",
252
+ "banner": "_5O8MPa_banner",
253
+ "seatHead": "_5O8MPa_seatHead",
254
+ "selfSeat": "_5O8MPa_selfSeat",
255
+ "muted": "_5O8MPa_muted",
256
+ "roleHero": "_5O8MPa_roleHero",
257
+ "close": "_5O8MPa_close",
258
+ "jokerFace": "_5O8MPa_jokerFace",
259
+ "codeBar": "_5O8MPa_codeBar",
260
+ "settingsHeadText": "_5O8MPa_settingsHeadText",
261
+ "settingsName": "_5O8MPa_settingsName",
262
+ "brand": "_5O8MPa_brand",
263
+ "settingsSwitch": "_5O8MPa_settingsSwitch",
264
+ "hand": "_5O8MPa_hand",
265
+ "primary": "_5O8MPa_primary",
266
+ "actions": "_5O8MPa_actions"
267
+ };
268
+
269
+ //#endregion
270
+ //#region src/client/InviteDialog.tsx
271
+ function InviteDialog({ roomCode, sitUrl, shareable }) {
272
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
273
+ className: styles_module_css_default.inviteBox,
274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
275
+ className: styles_module_css_default.codeRow,
276
+ children: [
277
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "房间号" }),
278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
279
+ className: styles_module_css_default.codeValue,
280
+ onClick: () => {
281
+ navigator.clipboard.writeText(roomCode);
282
+ },
283
+ children: roomCode
284
+ }),
285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
286
+ type: "button",
287
+ className: styles_module_css_default.ghost,
288
+ onClick: () => {
289
+ navigator.clipboard.writeText(roomCode);
290
+ },
291
+ children: "复制房号"
292
+ })
293
+ ]
294
+ }), shareable ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
296
+ className: styles_module_css_default.hint,
297
+ children: "同一条链接。先到的人入座,坐满或开打后来的人观战。"
298
+ }),
299
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
300
+ type: "button",
301
+ className: styles_module_css_default.ghost,
302
+ onClick: () => {
303
+ navigator.clipboard.writeText(sitUrl);
304
+ },
305
+ children: "复制房间链接"
306
+ }),
307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
308
+ className: styles_module_css_default.qr,
309
+ alt: "room qr",
310
+ src: `https://api.qrserver.com/v1/create-qr-code/?size=132x132&data=${encodeURIComponent(sitUrl)}`
311
+ })
312
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
313
+ className: `${styles_module_css_default.hint} ${styles_module_css_default.warn}`,
314
+ children: [
315
+ "要给别的机器用,先在插件设置里填 publicBaseUrl(named tunnel 或反代到本机端口)。 生产环境的 dsh web 不能绑 0.0.0.0,所以局域网 IP 用不了。本机可以打开 ",
316
+ sitUrl || "loopback 加入页",
317
+ " 自己测。"
318
+ ]
319
+ })]
320
+ });
321
+ }
322
+ function RoomCodeBar({ title, roomCode, sitUrl, shareable, canRename, onRename }) {
323
+ const [draft, setDraft] = (0, react.useState)(title);
324
+ const [editing, setEditing] = (0, react.useState)(false);
325
+ (0, react.useEffect)(() => {
326
+ setDraft(title);
327
+ }, [title]);
328
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
329
+ className: styles_module_css_default.codeBar,
330
+ children: [
331
+ canRename && editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
332
+ className: styles_module_css_default.codeEdit,
333
+ onSubmit: (event) => {
334
+ event.preventDefault();
335
+ onRename?.(draft);
336
+ setEditing(false);
337
+ },
338
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
339
+ className: styles_module_css_default.input,
340
+ value: draft,
341
+ maxLength: 24,
342
+ onChange: (event) => {
343
+ setDraft(event.target.value);
344
+ }
345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
346
+ type: "submit",
347
+ className: styles_module_css_default.ghost,
348
+ children: "保存"
349
+ })]
350
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
351
+ type: "button",
352
+ className: styles_module_css_default.titleBtn,
353
+ disabled: !canRename,
354
+ onClick: () => {
355
+ if (canRename) setEditing(true);
356
+ },
357
+ children: title || "好友局"
358
+ }),
359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
360
+ className: styles_module_css_default.muted,
361
+ children: "房号"
362
+ }),
363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
364
+ className: styles_module_css_default.codeValue,
365
+ onClick: () => {
366
+ navigator.clipboard.writeText(roomCode);
367
+ },
368
+ children: roomCode
369
+ }),
370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
371
+ type: "button",
372
+ className: styles_module_css_default.ghost,
373
+ onClick: () => {
374
+ navigator.clipboard.writeText(roomCode);
375
+ },
376
+ children: "复制房号"
377
+ }),
378
+ shareable && sitUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
379
+ type: "button",
380
+ className: styles_module_css_default.ghost,
381
+ onClick: () => {
382
+ navigator.clipboard.writeText(sitUrl);
383
+ },
384
+ children: "复制链接"
385
+ }) : sitUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
386
+ className: styles_module_css_default.muted,
387
+ children: "未配置 publicBaseUrl,仅本机可加入"
388
+ }) : null
389
+ ]
390
+ });
391
+ }
392
+
393
+ //#endregion
394
+ //#region src/client/SeatAvatar.tsx
395
+ const ASSET$2 = "/doudizhu/assets";
396
+ const ROLE_ICONS = {
397
+ landlord: `${ASSET$2}/role-landlord.png`,
398
+ landlordB: `${ASSET$2}/role-landlord-b.png`,
399
+ farmer: `${ASSET$2}/role-farmer.png`,
400
+ farmerB: `${ASSET$2}/role-farmer-b.png`,
401
+ farmerC: `${ASSET$2}/role-farmer-c.png`,
402
+ spectator: `${ASSET$2}/role-spectator.png`
403
+ };
404
+ const FARMER_ICONS = [
405
+ ROLE_ICONS.farmer,
406
+ ROLE_ICONS.farmerB,
407
+ ROLE_ICONS.farmerC
408
+ ];
409
+ function roleIconSrc(role, seat = 0) {
410
+ if (role === "landlord") return ROLE_ICONS.landlord;
411
+ return FARMER_ICONS[Math.abs(seat) % FARMER_ICONS.length] ?? ROLE_ICONS.farmer;
412
+ }
413
+ function SeatAvatar({ avatarUrl, role = "empty", seat = 0, occupied = false, spectator = false }) {
414
+ if (spectator) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
415
+ className: styles_module_css_default.avatar,
416
+ src: ROLE_ICONS.spectator,
417
+ alt: "观战",
418
+ onError: (event) => {
419
+ event.currentTarget.remove();
420
+ }
421
+ });
422
+ const sitting = occupied || role === "landlord" || role === "farmer";
423
+ const src = sitting ? roleIconSrc(role === "empty" ? "farmer" : role, seat) : avatarUrl;
424
+ if (!src) return null;
425
+ const landlord = role === "landlord";
426
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
427
+ className: styles_module_css_default.avatar,
428
+ src,
429
+ alt: landlord ? "地主" : sitting ? "农民" : "",
430
+ onError: (event) => {
431
+ event.currentTarget.remove();
432
+ }
433
+ });
434
+ }
435
+
436
+ //#endregion
437
+ //#region src/client/JoinForm.tsx
438
+ function JoinForm({ code, invite, name: name$1, onCode, onInvite, onName, onJoin, joining = false, showInvite = true, error }) {
439
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
440
+ className: styles_module_css_default.joinForm,
441
+ children: [
442
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
443
+ className: styles_module_css_default.roleHero,
444
+ children: [
445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
446
+ className: styles_module_css_default.roleHeroItem,
447
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
448
+ className: styles_module_css_default.roleHeroImg,
449
+ src: ROLE_ICONS.landlord,
450
+ alt: "地主"
451
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "地主" })]
452
+ }),
453
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
454
+ className: styles_module_css_default.roleHeroItem,
455
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
456
+ className: styles_module_css_default.roleHeroImg,
457
+ src: ROLE_ICONS.farmer,
458
+ alt: "农民"
459
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
460
+ }),
461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
462
+ className: styles_module_css_default.roleHeroItem,
463
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
464
+ className: styles_module_css_default.roleHeroImg,
465
+ src: ROLE_ICONS.farmerB,
466
+ alt: "农民"
467
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
468
+ }),
469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
470
+ className: styles_module_css_default.roleHeroItem,
471
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
472
+ className: styles_module_css_default.roleHeroImg,
473
+ src: ROLE_ICONS.farmerC,
474
+ alt: "农民"
475
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", { children: "农民" })]
476
+ })
477
+ ]
478
+ }),
479
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
480
+ className: styles_module_css_default.hint,
481
+ children: "填房号就能进。先到的人入座,坐满或已经开打后来的人观战。出牌每手 120 秒。"
482
+ }),
483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
484
+ className: styles_module_css_default.joinFields,
485
+ children: [
486
+ showInvite ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
487
+ className: styles_module_css_default.field,
488
+ children: ["邀请", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
489
+ className: styles_module_css_default.input,
490
+ value: invite,
491
+ autoComplete: "off",
492
+ spellCheck: false,
493
+ onChange: (event) => {
494
+ onInvite(event.target.value);
495
+ }
496
+ })]
497
+ }) : null,
498
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
499
+ className: styles_module_css_default.joinInline,
500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
501
+ className: styles_module_css_default.joinField,
502
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
503
+ className: styles_module_css_default.settingsLabel,
504
+ children: "房号"
505
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
506
+ className: `${styles_module_css_default.input} ${styles_module_css_default.codeInput}`,
507
+ value: code,
508
+ inputMode: "numeric",
509
+ autoComplete: "off",
510
+ spellCheck: false,
511
+ placeholder: "6 位房号",
512
+ onChange: (event) => {
513
+ onCode(event.target.value);
514
+ }
515
+ })]
516
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
517
+ className: styles_module_css_default.joinField,
518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
519
+ className: styles_module_css_default.settingsLabel,
520
+ children: "昵称"
521
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
522
+ className: styles_module_css_default.input,
523
+ value: name$1,
524
+ maxLength: 16,
525
+ onChange: (event) => {
526
+ onName(event.target.value);
527
+ }
528
+ })]
529
+ })]
530
+ }),
531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
532
+ type: "button",
533
+ className: styles_module_css_default.primary,
534
+ disabled: joining || !code.trim(),
535
+ onClick: onJoin,
536
+ children: joining ? "加入中…" : "加入"
537
+ }),
538
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
539
+ className: styles_module_css_default.error,
540
+ children: error
541
+ }) : null
542
+ ]
543
+ })
544
+ ]
545
+ });
546
+ }
547
+
548
+ //#endregion
549
+ //#region src/client/LobbyView.tsx
550
+ const MAX_MULTIPLIER_OPTIONS = [
551
+ 8,
552
+ 16,
553
+ 32,
554
+ 64
555
+ ];
556
+ function LobbyView({ tab, onTab, title, stakeM, maxMultiplier, seatCount, laiZi, onTitle, onStake, onCap, onSeats, onLaiZi, onCreate, creating, error, roomCode, sitUrl, watchUrl, shareable, welcomeAtoms, join }) {
557
+ const cap = seatCapAtoms(stakeAtomsFromM(stakeM), maxMultiplier, seatCount);
558
+ const blocked = welcomeAtoms < cap;
559
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
560
+ className: styles_module_css_default.lobby,
561
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
562
+ className: styles_module_css_default.card,
563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
564
+ className: styles_module_css_default.tabs,
565
+ role: "tablist",
566
+ "aria-label": "开房或加入",
567
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
568
+ type: "button",
569
+ role: "tab",
570
+ "aria-selected": tab === "create",
571
+ className: `${styles_module_css_default.tab} ${tab === "create" ? styles_module_css_default.tabActive : ""}`,
572
+ onClick: () => {
573
+ onTab("create");
574
+ },
575
+ children: "创建"
576
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
577
+ type: "button",
578
+ role: "tab",
579
+ "aria-selected": tab === "join",
580
+ className: `${styles_module_css_default.tab} ${tab === "join" ? styles_module_css_default.tabActive : ""}`,
581
+ onClick: () => {
582
+ onTab("join");
583
+ },
584
+ children: "加入"
585
+ })]
586
+ }), tab === "join" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "加入房间" }),
588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
589
+ className: styles_module_css_default.hint,
590
+ children: "别人开的房填房号就能进。积分记在开房那台机器上,不是 DeepSeek 平台余额。"
591
+ }),
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(JoinForm, {
593
+ code: join.code,
594
+ invite: join.invite,
595
+ name: join.name,
596
+ onCode: join.onCode,
597
+ onInvite: join.onInvite,
598
+ onName: join.onName,
599
+ onJoin: join.onJoin,
600
+ joining: join.joining,
601
+ showInvite: false,
602
+ error
603
+ })
604
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: "创建房间" }),
606
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
607
+ className: styles_module_css_default.hint,
608
+ children: "好友局信房主这台机器上的账本。积分不是 DeepSeek 平台余额。"
609
+ }),
610
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
611
+ className: styles_module_css_default.createFields,
612
+ children: [
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
614
+ className: `${styles_module_css_default.field} ${styles_module_css_default.fieldWide}`,
615
+ children: ["房间名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
616
+ className: styles_module_css_default.input,
617
+ value: title,
618
+ maxLength: 24,
619
+ placeholder: "好友局",
620
+ onChange: (event) => {
621
+ onTitle(event.target.value);
622
+ }
623
+ })]
624
+ }),
625
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
626
+ className: styles_module_css_default.field,
627
+ children: ["人数", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
628
+ className: styles_module_css_default.select,
629
+ value: seatCount,
630
+ onChange: (event) => {
631
+ onSeats(Number(event.target.value) === 4 ? 4 : 3);
632
+ },
633
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
634
+ value: 3,
635
+ children: "3 人 · 一副牌"
636
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
637
+ value: 4,
638
+ children: "4 人 · 两副牌"
639
+ })]
640
+ })]
641
+ }),
642
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
643
+ className: styles_module_css_default.field,
644
+ children: ["玩法", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
645
+ className: styles_module_css_default.select,
646
+ value: laiZi ? "laizi" : "classic",
647
+ onChange: (event) => {
648
+ onLaiZi(event.target.value === "laizi");
649
+ },
650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
651
+ value: "classic",
652
+ children: "经典"
653
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
654
+ value: "laizi",
655
+ children: "癞子"
656
+ })]
657
+ })]
658
+ }),
659
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
660
+ className: styles_module_css_default.field,
661
+ children: ["底注", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
662
+ className: styles_module_css_default.select,
663
+ value: stakeM,
664
+ onChange: (event) => {
665
+ onStake(Number(event.target.value));
666
+ },
667
+ children: STAKE_LADDER_M.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
668
+ value,
669
+ children: [value, "M"]
670
+ }, value))
671
+ })]
672
+ }),
673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
674
+ className: styles_module_css_default.field,
675
+ children: ["倍数封顶", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
676
+ className: styles_module_css_default.select,
677
+ value: maxMultiplier,
678
+ onChange: (event) => {
679
+ onCap(Number(event.target.value));
680
+ },
681
+ children: MAX_MULTIPLIER_OPTIONS.map((value) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
682
+ value,
683
+ children: value
684
+ }, value))
685
+ })]
686
+ })
687
+ ]
688
+ }),
689
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
690
+ className: styles_module_css_default.hint,
691
+ children: [
692
+ seatCount,
693
+ " 人",
694
+ laiZi ? "癞子" : "经典",
695
+ "。人齐自动开打,后来的人观战。出牌 120 秒。入座冻结 ",
696
+ formatM(cap),
697
+ ",欢迎 ",
698
+ formatM(welcomeAtoms),
699
+ "。"
700
+ ]
701
+ }),
702
+ blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
703
+ className: styles_module_css_default.error,
704
+ children: "欢迎积分不够入座冻结,把底注或封顶调低,或者去设置里加欢迎积分。"
705
+ }) : null,
706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
707
+ type: "button",
708
+ className: styles_module_css_default.primary,
709
+ disabled: blocked || creating,
710
+ onClick: onCreate,
711
+ children: creating ? "创建中…" : "创建房间"
712
+ }),
713
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
714
+ className: styles_module_css_default.error,
715
+ children: error
716
+ }) : null,
717
+ roomCode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InviteDialog, {
718
+ roomCode,
719
+ sitUrl,
720
+ watchUrl,
721
+ shareable
722
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
723
+ className: styles_module_css_default.hint,
724
+ children: [
725
+ "默认底注 ",
726
+ DEFAULT_STAKE_M,
727
+ "M / 封顶 ",
728
+ DEFAULT_MAX_MULTIPLIER,
729
+ "。"
730
+ ]
731
+ })
732
+ ] })]
733
+ })
734
+ });
735
+ }
736
+
737
+ //#endregion
738
+ //#region src/types.ts
739
+ function parseAtoms(value) {
740
+ if (!/^-?\d+$/.test(value)) throw new Error(`invalid atom string: ${value}`);
741
+ return BigInt(value);
742
+ }
743
+ function asCardId(value) {
744
+ return value;
745
+ }
746
+ function decksFor(count) {
747
+ return count === 3 ? 1 : 2;
748
+ }
749
+ function holeCount(count) {
750
+ return count === 3 ? 3 : 8;
751
+ }
752
+
753
+ //#endregion
754
+ //#region src/client/SettlementView.tsx
755
+ function SettlementView({ settlement, onRematch }) {
756
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
757
+ className: styles_module_css_default.settlement,
758
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
759
+ className: styles_module_css_default.card,
760
+ children: [
761
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
762
+ className: styles_module_css_default.roleHero,
763
+ children: settlement.winner === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
764
+ className: styles_module_css_default.roleHeroImg,
765
+ src: ROLE_ICONS.landlord,
766
+ alt: "地主"
767
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
768
+ className: styles_module_css_default.roleHeroImg,
769
+ src: ROLE_ICONS.farmer,
770
+ alt: "农民"
771
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
772
+ className: styles_module_css_default.roleHeroImg,
773
+ src: ROLE_ICONS.farmerB,
774
+ alt: "农民"
775
+ })] })
776
+ }),
777
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: settlement.winner === "landlord" ? "地主胜" : "农民胜" }),
778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
779
+ className: styles_module_css_default.hint,
780
+ children: settlement.formula
781
+ }),
782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: settlement.deltas.map((delta) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
783
+ "座 ",
784
+ delta.seat,
785
+ ": ",
786
+ formatM(parseAtoms(delta.atoms))
787
+ ] }, delta.seat)) }),
788
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
789
+ className: `${styles_module_css_default.hint} ${styles_module_css_default.warn}`,
790
+ children: settlement.trustNote
791
+ }),
792
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
793
+ className: styles_module_css_default.hint,
794
+ children: "积分 ≠ 平台余额,也不是 API key。"
795
+ }),
796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
797
+ type: "button",
798
+ className: styles_module_css_default.primary,
799
+ onClick: onRematch,
800
+ children: "再来一局"
801
+ })
802
+ ]
803
+ })
804
+ });
805
+ }
806
+
807
+ //#endregion
808
+ //#region src/engine/cards.ts
809
+ const RANK_ORDER = [
810
+ "3",
811
+ "4",
812
+ "5",
813
+ "6",
814
+ "7",
815
+ "8",
816
+ "9",
817
+ "10",
818
+ "J",
819
+ "Q",
820
+ "K",
821
+ "A",
822
+ "2",
823
+ "BJ",
824
+ "RJ"
825
+ ];
826
+ const RANK_VALUE = Object.fromEntries(RANK_ORDER.map((rank, index) => [rank, index]));
827
+ function cardBase$1(card) {
828
+ return String(card).split("~")[0] ?? String(card);
829
+ }
830
+ function rankOf$1(card) {
831
+ const raw = cardBase$1(card);
832
+ if (raw === "BJ" || raw === "RJ") return raw;
833
+ if (raw.startsWith("S") || raw.startsWith("H") || raw.startsWith("C") || raw.startsWith("D")) return raw.slice(1);
834
+ throw new Error(`invalid card: ${raw}`);
835
+ }
836
+ function rankValue(rank) {
837
+ const value = RANK_VALUE[rank];
838
+ if (value === void 0) throw new Error(`invalid rank: ${rank}`);
839
+ return value;
840
+ }
841
+ function cardValue(card) {
842
+ return rankValue(rankOf$1(card));
843
+ }
844
+ function compareCards(a, b) {
845
+ const byRank = cardValue(a) - cardValue(b);
846
+ if (byRank !== 0) return byRank;
847
+ return a.localeCompare(b);
848
+ }
849
+ function sortCards(cards) {
850
+ return [...cards].sort(compareCards);
851
+ }
852
+ const WILDABLE_RANKS = RANK_ORDER.filter((rank) => rank !== "BJ" && rank !== "RJ");
853
+ function isJokerRank(rank) {
854
+ return rank === "BJ" || rank === "RJ";
855
+ }
856
+ function isWildCard$1(card, wildRanks) {
857
+ if (wildRanks.length === 0) return false;
858
+ const rank = rankOf$1(card);
859
+ return !isJokerRank(rank) && wildRanks.includes(rank);
860
+ }
861
+ function countRanks(cards) {
862
+ const groups = /* @__PURE__ */ new Map();
863
+ for (const card of cards) {
864
+ const rank = rankOf$1(card);
865
+ const bucket = groups.get(rank);
866
+ if (bucket) bucket.push(card);
867
+ else groups.set(rank, [card]);
868
+ }
869
+ return groups;
870
+ }
871
+ function isNaturalSeqRank(rank) {
872
+ return rank !== "2" && rank !== "BJ" && rank !== "RJ";
873
+ }
874
+
875
+ //#endregion
876
+ //#region src/engine/hands.ts
877
+ function classify(cards, laiZiRanks = []) {
878
+ if (laiZiRanks.length === 0) return classifyNatural(cards);
879
+ const wilds = [];
880
+ const naturals = [];
881
+ for (const card of cards) if (isWildCard$1(card, laiZiRanks)) wilds.push(card);
882
+ else naturals.push(card);
883
+ if (wilds.length === 0) return classifyNatural(cards);
884
+ return classifyWithWilds(cards, naturals, wilds.length);
885
+ }
886
+ function classifyNatural(cards) {
887
+ const sorted = sortCards(cards);
888
+ if (sorted.length === 0) return null;
889
+ const ranks = countRanks(sorted);
890
+ const entries = [...ranks.entries()].sort((a, b) => rankValue(a[0]) - rankValue(b[0]));
891
+ if (jokerCount(ranks) === sorted.length && sorted.length >= 2) return {
892
+ type: "rocket",
893
+ cards: sorted,
894
+ key: rankValue("RJ"),
895
+ length: sorted.length,
896
+ wingLength: 0
897
+ };
898
+ if (sorted.length === 1) return {
899
+ type: "solo",
900
+ cards: sorted,
901
+ key: cardValue(sorted[0]),
902
+ length: 1,
903
+ wingLength: 0
904
+ };
905
+ if (sorted.length === 2 && entries.length === 1) return {
906
+ type: "pair",
907
+ cards: sorted,
908
+ key: rankValue(entries[0][0]),
909
+ length: 2,
910
+ wingLength: 0
911
+ };
912
+ if (sorted.length === 3 && entries.length === 1) return {
913
+ type: "trio",
914
+ cards: sorted,
915
+ key: rankValue(entries[0][0]),
916
+ length: 3,
917
+ wingLength: 0
918
+ };
919
+ if (sorted.length >= 4 && entries.length === 1) return {
920
+ type: "bomb",
921
+ cards: sorted,
922
+ key: rankValue(entries[0][0]),
923
+ length: sorted.length,
924
+ wingLength: 0
925
+ };
926
+ const trioSolo = classifyTrioSolo(sorted, entries);
927
+ if (trioSolo) return trioSolo;
928
+ const trioPair = classifyTrioPair(sorted, entries);
929
+ if (trioPair) return trioPair;
930
+ const seq = classifySeq(sorted, entries);
931
+ if (seq) return seq;
932
+ const seqPair = classifySeqPair(sorted, entries);
933
+ if (seqPair) return seqPair;
934
+ const plane = classifyBarePlane(sorted, entries);
935
+ if (plane) return plane;
936
+ const planePair = classifyPlanePair(sorted, entries);
937
+ if (planePair) return planePair;
938
+ const planeSolo = classifyPlaneSolo(sorted, entries);
939
+ if (planeSolo) return planeSolo;
940
+ const fourDualPair = classifyFourDualPair(sorted, entries);
941
+ if (fourDualPair) return fourDualPair;
942
+ const fourDualSolo = classifyFourDualSolo(sorted, entries);
943
+ if (fourDualSolo) return fourDualSolo;
944
+ return null;
945
+ }
946
+ function beats(prev, next, decks = 1) {
947
+ const nextBomb = next.type === "bomb" || next.type === "rocket";
948
+ const prevBomb = prev.type === "bomb" || prev.type === "rocket";
949
+ if (next.type === "rocket" && decks === 1) return prev.type !== "rocket";
950
+ if (prev.type === "rocket" && decks === 1) return false;
951
+ if (nextBomb && prevBomb) {
952
+ if (next.length !== prev.length) return next.length > prev.length;
953
+ if (!!next.wild !== !!prev.wild) return !next.wild && !!prev.wild;
954
+ if (next.type !== prev.type) return next.type === "rocket";
955
+ return next.key > prev.key;
956
+ }
957
+ if (nextBomb) return true;
958
+ if (prevBomb) return false;
959
+ if (next.type !== prev.type || next.length !== prev.length || next.wingLength !== prev.wingLength) return false;
960
+ return next.key > prev.key;
961
+ }
962
+ function classifyTrioSolo(cards, entries) {
963
+ if (cards.length !== 4) return null;
964
+ const trio = entries.find(([, group]) => group.length === 3);
965
+ const kicker = entries.find(([, group]) => group.length === 1);
966
+ if (!trio || !kicker) return null;
967
+ if (kicker[0] === "BJ" && entries.some(([rank]) => rank === "RJ")) return null;
968
+ if (kicker[0] === "RJ" && entries.some(([rank]) => rank === "BJ")) return null;
969
+ return {
970
+ type: "trioSolo",
971
+ cards,
972
+ key: rankValue(trio[0]),
973
+ length: 4,
974
+ wingLength: 1
975
+ };
976
+ }
977
+ function classifyTrioPair(cards, entries) {
978
+ if (cards.length !== 5) return null;
979
+ const trio = entries.find(([, group]) => group.length === 3);
980
+ const pair = entries.find(([, group]) => group.length === 2);
981
+ if (!trio || !pair || pair[0] === trio[0]) return null;
982
+ return {
983
+ type: "trioPair",
984
+ cards,
985
+ key: rankValue(trio[0]),
986
+ length: 5,
987
+ wingLength: 2
988
+ };
989
+ }
990
+ function classifySeq(cards, entries) {
991
+ if (cards.length < 5) return null;
992
+ if (!entries.every(([, group]) => group.length === 1)) return null;
993
+ if (!isConsecutive(entries.map(([rank]) => rank), 1)) return null;
994
+ return {
995
+ type: "seq",
996
+ cards,
997
+ key: rankValue(entries[entries.length - 1][0]),
998
+ length: cards.length,
999
+ wingLength: 0
1000
+ };
1001
+ }
1002
+ function classifySeqPair(cards, entries) {
1003
+ if (cards.length < 6 || cards.length % 2 !== 0) return null;
1004
+ if (!entries.every(([, group]) => group.length === 2)) return null;
1005
+ if (!isConsecutive(entries.map(([rank]) => rank), 1)) return null;
1006
+ if (entries.length < 3) return null;
1007
+ return {
1008
+ type: "seqPair",
1009
+ cards,
1010
+ key: rankValue(entries[entries.length - 1][0]),
1011
+ length: cards.length,
1012
+ wingLength: 0
1013
+ };
1014
+ }
1015
+ function classifyBarePlane(cards, entries) {
1016
+ const trios = entries.filter(([, group]) => group.length === 3).map(([rank]) => rank);
1017
+ if (trios.length < 2) return null;
1018
+ if (entries.some(([, group]) => group.length !== 3)) return null;
1019
+ if (!isConsecutive(trios, 1)) return null;
1020
+ return {
1021
+ type: "plane",
1022
+ cards,
1023
+ key: rankValue(trios[trios.length - 1]),
1024
+ length: cards.length,
1025
+ wingLength: 0
1026
+ };
1027
+ }
1028
+ function classifyPlaneSolo(cards, entries = [...countRanks(cards).entries()].sort((a, b) => rankValue(a[0]) - rankValue(b[0]))) {
1029
+ const available = entries.filter(([rank, group]) => group.length >= 3 && isNaturalSeqRank(rank) && rank !== "2").map(([rank]) => rank).sort((a, b) => rankValue(a) - rankValue(b));
1030
+ const maxK = Math.min(available.length, Math.floor(cards.length / 4));
1031
+ for (let k = maxK; k >= 2; k -= 1) {
1032
+ if (cards.length !== k * 4) continue;
1033
+ const found = [];
1034
+ for (const body of consecutiveRuns(available, k)) {
1035
+ const leftover = leftoverAfterBody(cards, body, 3);
1036
+ if (leftover.length !== k) continue;
1037
+ if (containsConsecutiveTrios(leftover)) continue;
1038
+ found.push({
1039
+ type: "planeSolo",
1040
+ cards,
1041
+ key: rankValue(body[body.length - 1]),
1042
+ length: cards.length,
1043
+ wingLength: k
1044
+ });
1045
+ }
1046
+ if (found.length > 0) {
1047
+ found.sort((a, b) => b.key - a.key);
1048
+ return found[0] ?? null;
1049
+ }
1050
+ }
1051
+ return null;
1052
+ }
1053
+ function classifyPlanePair(cards, entries) {
1054
+ entries.filter(([, group]) => group.length >= 3 && isNaturalSeqRank(group[0] ? rankOf$1(group[0]) : "3") && rankOf$1(group[0]) !== "2");
1055
+ const trioRanks = entries.filter(([rank, group]) => group.length >= 3 && isNaturalSeqRank(rank) && rank !== "2").map(([rank]) => rank).sort((a, b) => rankValue(a) - rankValue(b));
1056
+ for (let k = Math.min(trioRanks.length, Math.floor(cards.length / 5)); k >= 2; k -= 1) {
1057
+ if (cards.length !== k * 5) continue;
1058
+ for (const body of consecutiveRuns(trioRanks, k)) if (pairWings(cards, body) === k) return {
1059
+ type: "planePair",
1060
+ cards,
1061
+ key: rankValue(body[body.length - 1]),
1062
+ length: cards.length,
1063
+ wingLength: k
1064
+ };
1065
+ }
1066
+ return null;
1067
+ }
1068
+ function classifyFourDualSolo(cards, entries) {
1069
+ if (cards.length !== 6) return null;
1070
+ const quad = entries.find(([, group]) => group.length === 4);
1071
+ if (!quad) return null;
1072
+ const leftovers = cards.filter((card) => rankOf$1(card) !== quad[0]);
1073
+ if (leftovers.length !== 2) return null;
1074
+ if (hasRocket(countRanks(leftovers))) return null;
1075
+ return {
1076
+ type: "fourDualSolo",
1077
+ cards,
1078
+ key: rankValue(quad[0]),
1079
+ length: 6,
1080
+ wingLength: 2
1081
+ };
1082
+ }
1083
+ function classifyFourDualPair(cards, entries) {
1084
+ if (cards.length !== 8) return null;
1085
+ const quad = entries.find(([, group]) => group.length === 4);
1086
+ if (!quad) return null;
1087
+ const leftoverGroups = [...countRanks(leftoverAfterBody(cards, [quad[0]], 4)).entries()];
1088
+ if (leftoverGroups.some(([rank, group]) => rank === quad[0] || group.length !== 2)) return null;
1089
+ if (leftoverGroups.length !== 2) return null;
1090
+ return {
1091
+ type: "fourDualPair",
1092
+ cards,
1093
+ key: rankValue(quad[0]),
1094
+ length: 8,
1095
+ wingLength: 4
1096
+ };
1097
+ }
1098
+ function hasRocket(ranks) {
1099
+ return (ranks.get("BJ")?.length ?? 0) >= 1 && (ranks.get("RJ")?.length ?? 0) >= 1;
1100
+ }
1101
+ function jokerCount(ranks) {
1102
+ return (ranks.get("BJ")?.length ?? 0) + (ranks.get("RJ")?.length ?? 0);
1103
+ }
1104
+ function classifyWithWilds(original, naturals, wildCount) {
1105
+ if (wildCount <= 0) return classifyNatural(original);
1106
+ const ranksToTry = wildAssignmentRanks(naturals, wildCount);
1107
+ let best = null;
1108
+ const choice = new Array(wildCount).fill(0);
1109
+ const walk = () => {
1110
+ const virtuals = choice.map((index) => asCardId(`S${ranksToTry[index]}`));
1111
+ const result = classifyNatural([...naturals, ...virtuals]);
1112
+ if (!result) return;
1113
+ const hand = {
1114
+ ...result,
1115
+ cards: sortCards(original),
1116
+ wild: true
1117
+ };
1118
+ if (!best || wildPreference(hand, best) > 0) best = hand;
1119
+ };
1120
+ const rec = (depth) => {
1121
+ if (depth === wildCount) {
1122
+ walk();
1123
+ return;
1124
+ }
1125
+ for (let i = 0; i < ranksToTry.length; i += 1) {
1126
+ choice[depth] = i;
1127
+ rec(depth + 1);
1128
+ }
1129
+ };
1130
+ rec(0);
1131
+ return best;
1132
+ }
1133
+ function wildAssignmentRanks(naturals, wildCount) {
1134
+ if (naturals.length === 0) return wildCount >= 4 ? ["2"] : [...WILDABLE_RANKS];
1135
+ const present = new Set(naturals.map((card) => rankOf$1(card)));
1136
+ if (wildCount >= 5) return WILDABLE_RANKS.filter((rank) => present.has(rank) || neighborsOf(present).has(rank));
1137
+ return [...WILDABLE_RANKS];
1138
+ }
1139
+ function neighborsOf(present) {
1140
+ const out = /* @__PURE__ */ new Set();
1141
+ for (const rank of present) {
1142
+ const value = rankValue(rank);
1143
+ for (const candidate of WILDABLE_RANKS) if (Math.abs(rankValue(candidate) - value) === 1) out.add(candidate);
1144
+ }
1145
+ return out;
1146
+ }
1147
+ function wildPreference(next, prev) {
1148
+ const bombScore = (hand) => {
1149
+ if (hand.type === "rocket") return 200 + hand.length;
1150
+ if (hand.type === "bomb") return 100 + hand.length;
1151
+ return 0;
1152
+ };
1153
+ const byBomb = bombScore(next) - bombScore(prev);
1154
+ if (byBomb !== 0) return byBomb;
1155
+ if (next.length !== prev.length) return next.length - prev.length;
1156
+ return next.key - prev.key;
1157
+ }
1158
+ function isConsecutive(ranks, step) {
1159
+ if (ranks.some((rank) => !isNaturalSeqRank(rank))) return false;
1160
+ for (let i = 1; i < ranks.length; i += 1) if (rankValue(ranks[i]) - rankValue(ranks[i - 1]) !== step) return false;
1161
+ return ranks.length > 0;
1162
+ }
1163
+ function consecutiveRuns(ranks, length) {
1164
+ const runs = [];
1165
+ for (let i = 0; i + length <= ranks.length; i += 1) {
1166
+ const slice = ranks.slice(i, i + length);
1167
+ if (isConsecutive(slice, 1)) runs.push(slice);
1168
+ }
1169
+ return runs;
1170
+ }
1171
+ function containsConsecutiveTrios(cards) {
1172
+ return consecutiveRuns([...countRanks(cards).entries()].filter(([, group]) => group.length >= 3).map(([rank]) => rank).filter((rank) => isNaturalSeqRank(rank) && rank !== "2").sort((a, b) => rankValue(a) - rankValue(b)), 2).length > 0;
1173
+ }
1174
+ function pairWings(cards, body) {
1175
+ const groups = countRanks(leftoverAfterBody(cards, body, 3));
1176
+ if ([...groups.values()].some((group) => group.length !== 2 && group.length !== 4)) return null;
1177
+ let pairs = 0;
1178
+ for (const [rank, group] of groups) {
1179
+ if (body.includes(rank)) return null;
1180
+ if (group.length === 2) pairs += 1;
1181
+ else if (group.length === 4) pairs += 2;
1182
+ else return null;
1183
+ }
1184
+ return pairs;
1185
+ }
1186
+ function leftoverAfterBody(cards, body, take) {
1187
+ const used = /* @__PURE__ */ new Map();
1188
+ for (const rank of body) used.set(rank, take);
1189
+ const leftover = [];
1190
+ for (const card of cards) {
1191
+ const rank = rankOf$1(card);
1192
+ const remaining = used.get(rank) ?? 0;
1193
+ if (remaining > 0) used.set(rank, remaining - 1);
1194
+ else leftover.push(card);
1195
+ }
1196
+ return leftover;
1197
+ }
1198
+
1199
+ //#endregion
1200
+ //#region src/client/store.ts
1201
+ function emptyState() {
1202
+ return {
1203
+ view: null,
1204
+ settlement: null,
1205
+ selected: [],
1206
+ error: null,
1207
+ shareable: false,
1208
+ sitUrl: "",
1209
+ watchUrl: "",
1210
+ roomCode: ""
1211
+ };
1212
+ }
1213
+ function toggleCard(selected, card) {
1214
+ return selected.includes(card) ? selected.filter((item) => item !== card) : [...selected, card];
1215
+ }
1216
+ function selectionLegal(view, selected) {
1217
+ if (!view || selected.length === 0) return false;
1218
+ const key = [...selected].sort().join(",");
1219
+ if (view.legal.combos.some((combo) => [...combo.cards].sort().join(",") === key)) return true;
1220
+ const ranks = view.laiZiRanks ?? [];
1221
+ const classified = classify(selected, ranks);
1222
+ if (!classified) return false;
1223
+ const last = [...view.lastPlays].reverse().find((play) => play.type !== "pass");
1224
+ if (!last || view.leadSeat === view.you.seat) return true;
1225
+ const lead = classify(last.cards, ranks);
1226
+ if (!lead) return true;
1227
+ return beats(lead, classified, decksFor(view.room.seatCount ?? (view.room.seats.length >= 4 ? 4 : 3)));
1228
+ }
1229
+
1230
+ //#endregion
1231
+ //#region src/client/card-stack.ts
1232
+ const COMPACT_STACK_MAX = 5;
1233
+ function visibleBackCount(remaining, maxVisible = COMPACT_STACK_MAX) {
1234
+ if (remaining <= 0) return 0;
1235
+ return Math.min(remaining, maxVisible);
1236
+ }
1237
+
1238
+ //#endregion
1239
+ //#region src/client/CardBack.tsx
1240
+ const ASSET$1 = "/doudizhu/assets";
1241
+ function CardBack({ count = 1, compact = false, landlord = false }) {
1242
+ const remaining = Math.max(0, count);
1243
+ const n = compact ? visibleBackCount(remaining) : Math.min(remaining, 20);
1244
+ const src = landlord ? `${ASSET$1}/card-back-landlord.png` : `${ASSET$1}/card-back.png`;
1245
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1246
+ className: `${styles_module_css_default.remain} ${compact ? styles_module_css_default.remainCompact : ""}`,
1247
+ children: [compact && remaining > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1248
+ className: styles_module_css_default.cardsLeft,
1249
+ "aria-label": `剩余 ${remaining} 张`,
1250
+ children: remaining
1251
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1252
+ className: `${styles_module_css_default.backs} ${compact ? styles_module_css_default.backsCompact : ""}`,
1253
+ "aria-hidden": "true",
1254
+ children: Array.from({ length: n }, (_, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1255
+ className: `${styles_module_css_default.cardBack} ${landlord ? styles_module_css_default.cardBackLandlord : ""}`,
1256
+ style: { zIndex: index },
1257
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1258
+ className: styles_module_css_default.cardBackArt,
1259
+ src,
1260
+ alt: ""
1261
+ })
1262
+ }, index))
1263
+ })]
1264
+ });
1265
+ }
1266
+
1267
+ //#endregion
1268
+ //#region src/client/CardFace.tsx
1269
+ const SUIT_GLYPH = {
1270
+ S: "♠",
1271
+ H: "♥",
1272
+ C: "♣",
1273
+ D: "♦"
1274
+ };
1275
+ const ASSET = "/doudizhu/assets";
1276
+ const FACE_ART_RANKS = [
1277
+ "J",
1278
+ "Q",
1279
+ "K",
1280
+ "A"
1281
+ ];
1282
+ const FACE_ART = Object.fromEntries(FACE_ART_RANKS.map((rank) => [rank, `${ASSET}/face-${rank.toLowerCase()}.png`]));
1283
+ function cardBase(card) {
1284
+ return String(card).split("~")[0] ?? String(card);
1285
+ }
1286
+ function rankOf(card) {
1287
+ const raw = cardBase(card);
1288
+ if (raw === "BJ" || raw === "RJ") return raw;
1289
+ return raw.slice(1);
1290
+ }
1291
+ function isWildCard(card, wildRanks) {
1292
+ const rank = rankOf(card);
1293
+ return wildRanks.length > 0 && rank !== "BJ" && rank !== "RJ" && wildRanks.includes(rank);
1294
+ }
1295
+ function CardFace({ card, selected = false, laiZiRanks = [] }) {
1296
+ const raw = cardBase(card);
1297
+ const wild = isWildCard(card, laiZiRanks);
1298
+ if (raw === "BJ" || raw === "RJ" || rankOf(card) === "BJ" || rankOf(card) === "RJ") {
1299
+ const red$1 = rankOf(card) === "RJ";
1300
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1301
+ className: `${styles_module_css_default.cardFace} ${styles_module_css_default.jokerFace} ${red$1 ? styles_module_css_default.red : ""} ${selected ? styles_module_css_default.selected : ""}`,
1302
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1303
+ className: styles_module_css_default.jokerArt,
1304
+ src: red$1 ? `${ASSET}/joker-red.png` : `${ASSET}/joker-black.png`,
1305
+ alt: ""
1306
+ })
1307
+ });
1308
+ }
1309
+ const suit = raw[0] ?? "S";
1310
+ const rank = raw.slice(1);
1311
+ const red = suit === "H" || suit === "D";
1312
+ const art = FACE_ART[rank];
1313
+ const glyph = SUIT_GLYPH[suit] ?? suit;
1314
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1315
+ className: `${styles_module_css_default.cardFace} ${red ? styles_module_css_default.red : ""} ${selected ? styles_module_css_default.selected : ""} ${wild ? styles_module_css_default.wild : ""} ${art ? "" : styles_module_css_default.plainFace}`,
1316
+ children: [
1317
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1318
+ className: styles_module_css_default.rank,
1319
+ children: [rank, wild ? "*" : ""]
1320
+ }),
1321
+ art ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1322
+ className: styles_module_css_default.faceArt,
1323
+ src: art,
1324
+ alt: ""
1325
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1326
+ className: styles_module_css_default.suitCenter,
1327
+ children: glyph
1328
+ }),
1329
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1330
+ className: styles_module_css_default.suit,
1331
+ children: glyph
1332
+ })
1333
+ ]
1334
+ });
1335
+ }
1336
+
1337
+ //#endregion
1338
+ //#region src/client/SeatRow.tsx
1339
+ function SeatRow({ seat, align = "left", revealed, laiZiRanks = [] }) {
1340
+ const alignClass = align === "right" ? styles_module_css_default.seatRight : align === "center" ? styles_module_css_default.seatCenter : "";
1341
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1342
+ className: `${styles_module_css_default.seat} ${alignClass}`,
1343
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1344
+ className: styles_module_css_default.seatHead,
1345
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, {
1346
+ avatarUrl: seat.avatarUrl,
1347
+ role: seat.role,
1348
+ seat: seat.seat,
1349
+ occupied: Boolean(seat.playerId)
1350
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1351
+ className: styles_module_css_default.seatMeta,
1352
+ children: [
1353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1354
+ className: styles_module_css_default.seatName,
1355
+ children: [
1356
+ seat.displayName ?? "空座",
1357
+ seat.role === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1358
+ className: styles_module_css_default.badge,
1359
+ children: "地主"
1360
+ }) : null,
1361
+ seat.role === "farmer" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1362
+ className: styles_module_css_default.badge,
1363
+ children: "农民"
1364
+ }) : null,
1365
+ revealed && revealed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1366
+ className: styles_module_css_default.badge,
1367
+ children: "明牌"
1368
+ }) : null
1369
+ ]
1370
+ }),
1371
+ seat.ready && seat.role === "empty" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1372
+ className: styles_module_css_default.ready,
1373
+ children: "准备"
1374
+ }) : null,
1375
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1376
+ className: styles_module_css_default.muted,
1377
+ children: seat.connected ? "在线" : seat.playerId ? "离线" : ""
1378
+ })
1379
+ ]
1380
+ })]
1381
+ }), revealed && revealed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1382
+ className: styles_module_css_default.miniHand,
1383
+ children: revealed.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
1384
+ card,
1385
+ laiZiRanks
1386
+ }, card))
1387
+ }) : seat.playerId && seat.cardsLeft > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, {
1388
+ count: seat.cardsLeft,
1389
+ compact: true,
1390
+ landlord: seat.role === "landlord"
1391
+ }) : null]
1392
+ });
1393
+ }
1394
+
1395
+ //#endregion
1396
+ //#region src/client/TableView.tsx
1397
+ function TableView({ view, selected, onToggle, onBid, onDouble, onMingPai, onPlay, onPass, onChat }) {
1398
+ const seats = view.room.seats;
1399
+ const count = view.room.seatCount ?? (seats.length >= 4 ? 4 : 3);
1400
+ const self = view.you.seat ?? 0;
1401
+ const opponents = opponentSeats(self, count).map((seat) => seatAt(seats, seat));
1402
+ const mine = seatAt(seats, self);
1403
+ const legal = selectionLegal(view, selected);
1404
+ const phase = view.room.phase;
1405
+ const laiZi = view.laiZiRanks ?? [];
1406
+ const seconds = useDeadline(phase === "playing" ? view.deadlineAt : null);
1407
+ const seated = seats.filter((seat) => seat.playerId).length;
1408
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1409
+ className: styles_module_css_default.table,
1410
+ children: [
1411
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1412
+ className: styles_module_css_default.topbar,
1413
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1414
+ view.room.title || "好友局",
1415
+ " · ",
1416
+ count,
1417
+ "人",
1418
+ view.room.laiZi ? "癞子" : "经典",
1419
+ " · 底注 ",
1420
+ formatM(parseAtoms(view.room.stakeAtoms)),
1421
+ " · ",
1422
+ view.bid ? `${view.bid}倍` : "未叫",
1423
+ " · 阶段 ",
1424
+ phaseLabel(phase)
1425
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1426
+ className: styles_module_css_default.muted,
1427
+ children: [
1428
+ "余额 ",
1429
+ formatM(parseAtoms(view.yourAvailableAtoms)),
1430
+ " · 冻结 ",
1431
+ formatM(parseAtoms(view.yourEscrowAtoms))
1432
+ ]
1433
+ })]
1434
+ }),
1435
+ seconds !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1436
+ className: `${styles_module_css_default.timer} ${seconds <= 10 ? styles_module_css_default.timerUrgent : ""}`,
1437
+ children: [
1438
+ "出牌剩余 ",
1439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: seconds }),
1440
+ " / 120 秒"
1441
+ ]
1442
+ }) : phase === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1443
+ className: styles_module_css_default.hint,
1444
+ children: [
1445
+ seated,
1446
+ "/",
1447
+ count,
1448
+ " 人入座,人齐开打。后来的人观战。"
1449
+ ]
1450
+ }) : null,
1451
+ laiZi.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1452
+ className: styles_module_css_default.muted,
1453
+ children: ["癞子:", laiZi.join("、")]
1454
+ }) : null,
1455
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1456
+ className: `${styles_module_css_default.opponents} ${opponents.length > 2 ? styles_module_css_default.opponentsThree : ""}`,
1457
+ children: opponents.map((seat, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatRow, {
1458
+ seat,
1459
+ align: index === opponents.length - 1 ? "right" : index === 0 ? "left" : "center",
1460
+ ...view.revealedBySeat?.[seat.seat] ? { revealed: view.revealedBySeat[seat.seat] } : {},
1461
+ laiZiRanks: laiZi
1462
+ }, seat.seat))
1463
+ }),
1464
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1465
+ className: styles_module_css_default.centerPlay,
1466
+ children: view.lastPlays.slice(-1).map((play, index) => play.type === "pass" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1467
+ className: styles_module_css_default.muted,
1468
+ children: "过"
1469
+ }, index) : play.cards.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
1470
+ card,
1471
+ laiZiRanks: laiZi
1472
+ }, card)))
1473
+ }),
1474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1475
+ className: styles_module_css_default.bottom,
1476
+ children: view.bottom ? view.bottom.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
1477
+ card,
1478
+ laiZiRanks: laiZi
1479
+ }, card)) : phase !== "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, { count: holeCount(count) }) : null
1480
+ }),
1481
+ view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1482
+ className: styles_module_css_default.selfRow,
1483
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1484
+ className: styles_module_css_default.selfSeat,
1485
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, { spectator: true }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1486
+ className: styles_module_css_default.seatName,
1487
+ children: ["观战", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1488
+ className: styles_module_css_default.badge,
1489
+ children: "观战"
1490
+ })]
1491
+ })]
1492
+ })
1493
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1494
+ className: styles_module_css_default.hint,
1495
+ children: "观战中。只能看见已打出的牌与公开区。"
1496
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1497
+ className: styles_module_css_default.selfRow,
1498
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1499
+ className: styles_module_css_default.selfSeat,
1500
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SeatAvatar, {
1501
+ avatarUrl: mine?.avatarUrl ?? null,
1502
+ role: mine?.role ?? "empty",
1503
+ seat: self,
1504
+ occupied: Boolean(mine?.playerId)
1505
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1506
+ className: styles_module_css_default.seatName,
1507
+ children: [
1508
+ mine?.displayName,
1509
+ mine?.role === "landlord" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1510
+ className: styles_module_css_default.badge,
1511
+ children: "地主"
1512
+ }) : null,
1513
+ mine?.role === "farmer" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1514
+ className: styles_module_css_default.badge,
1515
+ children: "农民"
1516
+ }) : null,
1517
+ view.mingPaiBySeat?.[self] ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1518
+ className: styles_module_css_default.badge,
1519
+ children: "明牌"
1520
+ }) : null
1521
+ ]
1522
+ })]
1523
+ }), phase === "waiting" && !view.bottom ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBack, {
1524
+ count: holeCount(count),
1525
+ compact: true
1526
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1527
+ className: styles_module_css_default.hand,
1528
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1529
+ className: styles_module_css_default.handInner,
1530
+ children: view.you.cards.map((card) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1531
+ type: "button",
1532
+ onClick: () => {
1533
+ onToggle(card);
1534
+ },
1535
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFace, {
1536
+ card,
1537
+ selected: selected.includes(card),
1538
+ laiZiRanks: laiZi
1539
+ })
1540
+ }, card))
1541
+ })
1542
+ })]
1543
+ }),
1544
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1545
+ className: styles_module_css_default.actions,
1546
+ children: [
1547
+ phase === "waiting" && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1548
+ className: styles_module_css_default.hint,
1549
+ children: [
1550
+ "等满 ",
1551
+ count,
1552
+ " 人自动开打。"
1553
+ ]
1554
+ }) : null,
1555
+ phase === "bidding" && isTurn(view) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: view.auction?.kind === "rob" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1556
+ type: "button",
1557
+ className: styles_module_css_default.ghost,
1558
+ onClick: () => {
1559
+ onBid("pass");
1560
+ },
1561
+ children: "不抢"
1562
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1563
+ type: "button",
1564
+ className: styles_module_css_default.primary,
1565
+ onClick: () => {
1566
+ onBid("rob");
1567
+ },
1568
+ children: "抢地主"
1569
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1570
+ type: "button",
1571
+ className: styles_module_css_default.ghost,
1572
+ onClick: () => {
1573
+ onBid("pass");
1574
+ },
1575
+ children: "不叫"
1576
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1577
+ type: "button",
1578
+ className: styles_module_css_default.primary,
1579
+ onClick: () => {
1580
+ onBid("call");
1581
+ },
1582
+ children: "叫地主"
1583
+ })] }) }) : null,
1584
+ canMingPai(view) && onMingPai ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1585
+ type: "button",
1586
+ className: styles_module_css_default.ghost,
1587
+ onClick: onMingPai,
1588
+ children: "明牌"
1589
+ }) : null,
1590
+ phase === "doubling" && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1592
+ type: "button",
1593
+ className: styles_module_css_default.ghost,
1594
+ onClick: () => {
1595
+ onDouble("pass");
1596
+ },
1597
+ children: "不加倍"
1598
+ }),
1599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1600
+ type: "button",
1601
+ className: styles_module_css_default.primary,
1602
+ onClick: () => {
1603
+ onDouble("double");
1604
+ },
1605
+ children: "加倍 ×2"
1606
+ }),
1607
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1608
+ type: "button",
1609
+ className: styles_module_css_default.primary,
1610
+ onClick: () => {
1611
+ onDouble("reDouble");
1612
+ },
1613
+ children: "超级加倍 ×4"
1614
+ })
1615
+ ] }) : null,
1616
+ phase === "playing" && isTurn(view) && !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1617
+ type: "button",
1618
+ className: styles_module_css_default.primary,
1619
+ disabled: !legal,
1620
+ onClick: onPlay,
1621
+ children: "出牌"
1622
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1623
+ type: "button",
1624
+ className: styles_module_css_default.ghost,
1625
+ disabled: !view.legal.canPass,
1626
+ onClick: onPass,
1627
+ children: "过"
1628
+ })] }) : null
1629
+ ]
1630
+ }),
1631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1632
+ className: styles_module_css_default.chat,
1633
+ children: [view.chat.map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1634
+ line.displayName,
1635
+ ": ",
1636
+ line.text
1637
+ ] }, line.ts)), !view.you.spectator ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("form", {
1638
+ onSubmit: (event) => {
1639
+ event.preventDefault();
1640
+ const data = new FormData(event.currentTarget);
1641
+ const text = String(data.get("chat") ?? "");
1642
+ if (text) onChat(text);
1643
+ event.currentTarget.reset();
1644
+ },
1645
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1646
+ className: styles_module_css_default.input,
1647
+ name: "chat",
1648
+ maxLength: 80,
1649
+ placeholder: "聊天"
1650
+ })
1651
+ }) : null]
1652
+ })
1653
+ ]
1654
+ });
1655
+ }
1656
+ function opponentSeats(self, count) {
1657
+ const out = [];
1658
+ for (let step = 1; step < count; step += 1) out.push((self + step) % count);
1659
+ return out;
1660
+ }
1661
+ function seatAt(seats, index) {
1662
+ return seats[index] ?? {
1663
+ seat: index,
1664
+ playerId: null,
1665
+ displayName: null,
1666
+ avatarUrl: null,
1667
+ ready: false,
1668
+ connected: false,
1669
+ role: "empty",
1670
+ grantId: null,
1671
+ cardsLeft: 0
1672
+ };
1673
+ }
1674
+ function isTurn(view) {
1675
+ return view.you.seat !== null && view.turnSeat === view.you.seat;
1676
+ }
1677
+ function useDeadline(deadlineAt) {
1678
+ const [now, setNow] = (0, react.useState)(() => Date.now());
1679
+ (0, react.useEffect)(() => {
1680
+ if (!deadlineAt) return void 0;
1681
+ const tick = setInterval(() => {
1682
+ setNow(Date.now());
1683
+ }, 250);
1684
+ return () => {
1685
+ clearInterval(tick);
1686
+ };
1687
+ }, [deadlineAt]);
1688
+ if (!deadlineAt) return null;
1689
+ return Math.max(0, Math.ceil((Date.parse(deadlineAt) - now) / 1e3));
1690
+ }
1691
+ function canMingPai(view) {
1692
+ if (view.you.spectator || view.you.seat === null) return false;
1693
+ if (view.mingPaiBySeat?.[view.you.seat]) return false;
1694
+ return view.room.phase === "bidding" || view.room.phase === "doubling" || view.room.phase === "playing";
1695
+ }
1696
+ function phaseLabel(phase) {
1697
+ if (phase === "waiting") return "等人";
1698
+ if (phase === "bidding") return "叫抢";
1699
+ if (phase === "doubling") return "加倍";
1700
+ if (phase === "playing") return "出牌";
1701
+ if (phase === "settling") return "结算";
1702
+ return phase;
1703
+ }
1704
+
1705
+ //#endregion
1706
+ //#region src/client/HostApp.tsx
1707
+ function HostApp({ onClose }) {
1708
+ const [stakeM, setStakeM] = (0, react.useState)(DEFAULT_STAKE_M);
1709
+ const [maxMultiplier, setMaxMultiplier] = (0, react.useState)(DEFAULT_MAX_MULTIPLIER);
1710
+ const [seatCount, setSeatCount] = (0, react.useState)(3);
1711
+ const [laiZi, setLaiZi] = (0, react.useState)(false);
1712
+ const [title, setTitle] = (0, react.useState)("好友局");
1713
+ const [tab, setTab] = (0, react.useState)("create");
1714
+ const [joinCode, setJoinCode] = (0, react.useState)("");
1715
+ const [joinInvite, setJoinInvite] = (0, react.useState)("");
1716
+ const [joinName, setJoinName] = (0, react.useState)("好友");
1717
+ const [state, setState] = (0, react.useState)(emptyState);
1718
+ const [creating, setCreating] = (0, react.useState)(false);
1719
+ const [joining, setJoining] = (0, react.useState)(false);
1720
+ const [roomId, setRoomId] = (0, react.useState)(null);
1721
+ const seq = (0, react.useMemo)(() => ({ value: 0 }), []);
1722
+ (0, react.useEffect)(() => {
1723
+ const onHash = () => {
1724
+ const match = /#\/doudizhu\/room\/([^/]+)/.exec(location.hash);
1725
+ if (match) setRoomId(match[1] ?? null);
1726
+ };
1727
+ onHash();
1728
+ window.addEventListener("hashchange", onHash);
1729
+ return () => {
1730
+ window.removeEventListener("hashchange", onHash);
1731
+ };
1732
+ }, []);
1733
+ (0, react.useEffect)(() => {
1734
+ if (!roomId || !state.view) return;
1735
+ }, [roomId, state.view]);
1736
+ const applyView = (view) => {
1737
+ setState((prev) => ({
1738
+ ...prev,
1739
+ view,
1740
+ selected: prev.selected.filter((card) => view.you.cards.includes(card))
1741
+ }));
1742
+ };
1743
+ const startRoom = async () => {
1744
+ setCreating(true);
1745
+ setState((prev) => ({
1746
+ ...prev,
1747
+ error: null
1748
+ }));
1749
+ try {
1750
+ const created = await createRoom({
1751
+ stakeM,
1752
+ maxMultiplier,
1753
+ seatCount,
1754
+ laiZi,
1755
+ hostDisplayName: "房主",
1756
+ title
1757
+ });
1758
+ setRoomId(created.roomId);
1759
+ setState((prev) => ({
1760
+ ...prev,
1761
+ view: created.view,
1762
+ shareable: created.shareable,
1763
+ sitUrl: created.sitUrl,
1764
+ watchUrl: created.watchUrl,
1765
+ roomCode: created.roomCode
1766
+ }));
1767
+ location.hash = `#/doudizhu/room/${created.roomId}`;
1768
+ listenRoom(created.roomId, created.wsTicket);
1769
+ } catch (error) {
1770
+ setState((prev) => ({
1771
+ ...prev,
1772
+ error: error instanceof Error ? error.message : "create failed"
1773
+ }));
1774
+ } finally {
1775
+ setCreating(false);
1776
+ }
1777
+ };
1778
+ const enterRoom = async () => {
1779
+ setJoining(true);
1780
+ setState((prev) => ({
1781
+ ...prev,
1782
+ error: null
1783
+ }));
1784
+ try {
1785
+ const result = await joinRoom({
1786
+ roomCode: joinCode.trim(),
1787
+ invite: joinInvite.trim(),
1788
+ displayName: joinName,
1789
+ role: "sit"
1790
+ });
1791
+ setRoomId(result.roomId);
1792
+ applyView(result.view);
1793
+ setState((prev) => ({
1794
+ ...prev,
1795
+ roomCode: result.view.room.roomCode
1796
+ }));
1797
+ location.hash = `#/doudizhu/room/${result.roomId}`;
1798
+ listenRoom(result.roomId, result.wsTicket);
1799
+ } catch (error) {
1800
+ setState((prev) => ({
1801
+ ...prev,
1802
+ error: error instanceof Error ? error.message : "join failed"
1803
+ }));
1804
+ } finally {
1805
+ setJoining(false);
1806
+ }
1807
+ };
1808
+ const listenRoom = (nextRoomId, ticket) => {
1809
+ connectChannel(ticket, (event) => {
1810
+ if (event.type === "snapshot") {
1811
+ seq.value = event.seq;
1812
+ applyView(event.view);
1813
+ }
1814
+ if (event.type === "settled") {
1815
+ seq.value = event.seq;
1816
+ setState((prev) => ({
1817
+ ...prev,
1818
+ settlement: event.settlement
1819
+ }));
1820
+ }
1821
+ if (event.type === "reject") setState((prev) => ({
1822
+ ...prev,
1823
+ error: event.reason
1824
+ }));
1825
+ }, {
1826
+ roomId: nextRoomId,
1827
+ seq: () => seq.value
1828
+ });
1829
+ };
1830
+ const command = (cmd) => {
1831
+ if (!roomId) return;
1832
+ sendCommand(roomId, cmd).catch((error) => {
1833
+ setState((prev) => ({
1834
+ ...prev,
1835
+ error: error instanceof Error ? error.message : "command failed"
1836
+ }));
1837
+ });
1838
+ };
1839
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1840
+ className: styles_module_css_default.root,
1841
+ children: [
1842
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1843
+ className: styles_module_css_default.topbar,
1844
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1845
+ className: styles_module_css_default.brand,
1846
+ children: "斗地主"
1847
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1848
+ type: "button",
1849
+ className: styles_module_css_default.close,
1850
+ onClick: onClose,
1851
+ children: "关闭"
1852
+ })]
1853
+ }),
1854
+ state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1855
+ className: styles_module_css_default.banner,
1856
+ children: state.error
1857
+ }) : null,
1858
+ !state.view ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LobbyView, {
1859
+ tab,
1860
+ onTab: setTab,
1861
+ title,
1862
+ stakeM,
1863
+ maxMultiplier,
1864
+ seatCount,
1865
+ laiZi,
1866
+ onTitle: setTitle,
1867
+ onStake: setStakeM,
1868
+ onCap: setMaxMultiplier,
1869
+ onSeats: setSeatCount,
1870
+ onLaiZi: setLaiZi,
1871
+ onCreate: () => {
1872
+ startRoom();
1873
+ },
1874
+ creating,
1875
+ error: state.error,
1876
+ roomCode: state.roomCode,
1877
+ sitUrl: state.sitUrl,
1878
+ watchUrl: state.watchUrl,
1879
+ shareable: state.shareable,
1880
+ welcomeAtoms: DEFAULT_WELCOME_ATOMS,
1881
+ join: {
1882
+ code: joinCode,
1883
+ invite: joinInvite,
1884
+ name: joinName,
1885
+ joining,
1886
+ onCode: setJoinCode,
1887
+ onInvite: setJoinInvite,
1888
+ onName: setJoinName,
1889
+ onJoin: () => {
1890
+ enterRoom();
1891
+ }
1892
+ }
1893
+ }) : state.settlement && state.view.room.phase === "waiting" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettlementView, {
1894
+ settlement: state.settlement,
1895
+ onRematch: () => {
1896
+ setState((prev) => ({
1897
+ ...prev,
1898
+ settlement: null
1899
+ }));
1900
+ command({ type: "rematch" });
1901
+ }
1902
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [state.roomCode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1903
+ className: styles_module_css_default.banner,
1904
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RoomCodeBar, {
1905
+ title: state.view.room.title,
1906
+ roomCode: state.roomCode,
1907
+ sitUrl: state.sitUrl,
1908
+ shareable: state.shareable,
1909
+ canRename: true,
1910
+ onRename: (next) => {
1911
+ command({
1912
+ type: "rename",
1913
+ title: next
1914
+ });
1915
+ }
1916
+ })
1917
+ }) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableView, {
1918
+ view: state.view,
1919
+ selected: state.selected,
1920
+ onToggle: (card) => {
1921
+ setState((prev) => ({
1922
+ ...prev,
1923
+ selected: toggleCard(prev.selected, card)
1924
+ }));
1925
+ },
1926
+ onBid: (action) => {
1927
+ command({
1928
+ type: "bid",
1929
+ action
1930
+ });
1931
+ },
1932
+ onDouble: (action) => {
1933
+ command({
1934
+ type: "double",
1935
+ action
1936
+ });
1937
+ },
1938
+ onMingPai: () => {
1939
+ command({ type: "mingPai" });
1940
+ },
1941
+ onPlay: () => {
1942
+ command({
1943
+ type: "play",
1944
+ cards: state.selected,
1945
+ nonce: crypto.randomUUID()
1946
+ });
1947
+ },
1948
+ onPass: () => {
1949
+ command({
1950
+ type: "pass",
1951
+ nonce: crypto.randomUUID()
1952
+ });
1953
+ },
1954
+ onReady: (ready) => {
1955
+ command({
1956
+ type: "ready",
1957
+ ready
1958
+ });
1959
+ },
1960
+ onChat: (text) => {
1961
+ command({
1962
+ type: "chat",
1963
+ text
1964
+ });
1965
+ }
1966
+ })] })
1967
+ ]
1968
+ });
1969
+ }
1970
+
1971
+ //#endregion
1972
+ //#region src/client/SettingsCard.tsx
1973
+ function draftFrom(value) {
1974
+ return {
1975
+ publicBaseUrl: value.publicBaseUrl ?? "",
1976
+ welcomeAtoms: value.welcomeAtoms ?? "200000000",
1977
+ defaultSeatCount: value.defaultSeatCount === 4 ? 4 : 3,
1978
+ defaultMaxMultiplier: value.defaultMaxMultiplier ?? 8,
1979
+ defaultLaiZi: value.defaultLaiZi === true,
1980
+ spectatorCardCounter: value.spectatorCardCounter === true
1981
+ };
1982
+ }
1983
+ function draftsEqual(left, right) {
1984
+ return left.publicBaseUrl === right.publicBaseUrl && left.welcomeAtoms === right.welcomeAtoms && left.defaultSeatCount === right.defaultSeatCount && left.defaultMaxMultiplier === right.defaultMaxMultiplier && left.defaultLaiZi === right.defaultLaiZi && left.spectatorCardCounter === right.spectatorCardCounter;
1985
+ }
1986
+ function validateDraft(draft) {
1987
+ const publicBaseUrl = draft.publicBaseUrl.trim().replace(/\/+$/, "");
1988
+ if (publicBaseUrl !== "") {
1989
+ let parsed;
1990
+ try {
1991
+ parsed = new URL(publicBaseUrl);
1992
+ } catch {
1993
+ return "publicBaseUrl 必须是绝对 http(s) origin,不要路径或尾斜杠";
1994
+ }
1995
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return "publicBaseUrl 必须是 http 或 https";
1996
+ if (parsed.origin !== publicBaseUrl) return "publicBaseUrl 必须是 origin,不要路径、查询或尾斜杠";
1997
+ }
1998
+ if (!/^\d+$/.test(draft.welcomeAtoms)) return "欢迎积分必须是非负整数";
1999
+ if (![
2000
+ 8,
2001
+ 16,
2002
+ 32,
2003
+ 64
2004
+ ].includes(draft.defaultMaxMultiplier)) return "倍数封顶只能是 8、16、32 或 64";
2005
+ return null;
2006
+ }
2007
+ function SettingsCard({ scope }) {
2008
+ const [snap, setSnap] = (0, react.useState)(() => scope.getSnapshot());
2009
+ const [open, setOpen] = (0, react.useState)(false);
2010
+ const [draft, setDraft] = (0, react.useState)(() => snap.value ? draftFrom(snap.value) : null);
2011
+ const [saving, setSaving] = (0, react.useState)(false);
2012
+ const [saved, setSaved] = (0, react.useState)(false);
2013
+ const [error, setError] = (0, react.useState)(null);
2014
+ (0, react.useEffect)(() => scope.subscribe(() => {
2015
+ const next = scope.getSnapshot();
2016
+ setSnap((prevSnap) => {
2017
+ if (next.value) {
2018
+ const incoming = draftFrom(next.value);
2019
+ const previousStored = prevSnap.value ? draftFrom(prevSnap.value) : incoming;
2020
+ setDraft((prev) => {
2021
+ if (!prev || draftsEqual(prev, previousStored)) return incoming;
2022
+ return prev;
2023
+ });
2024
+ }
2025
+ return next;
2026
+ });
2027
+ }), [scope]);
2028
+ (0, react.useEffect)(() => {
2029
+ if (draft || !snap.value) return;
2030
+ setDraft(draftFrom(snap.value));
2031
+ }, [draft, snap.value]);
2032
+ const value = snap.value;
2033
+ if (!value || !draft) return null;
2034
+ const stored = draftFrom(value);
2035
+ const dirty = !draftsEqual(draft, stored);
2036
+ const patch = (field, next) => {
2037
+ setSaved(false);
2038
+ setError(null);
2039
+ setDraft((prev) => prev ? {
2040
+ ...prev,
2041
+ [field]: next
2042
+ } : prev);
2043
+ };
2044
+ const save = async () => {
2045
+ const invalid = validateDraft(draft);
2046
+ if (invalid) {
2047
+ setError(invalid);
2048
+ return;
2049
+ }
2050
+ const next = {
2051
+ ...draft,
2052
+ publicBaseUrl: draft.publicBaseUrl.trim().replace(/\/+$/, "")
2053
+ };
2054
+ setDraft(next);
2055
+ setSaving(true);
2056
+ setError(null);
2057
+ try {
2058
+ const writes = [
2059
+ ["publicBaseUrl", next.publicBaseUrl],
2060
+ ["welcomeAtoms", next.welcomeAtoms],
2061
+ ["defaultSeatCount", next.defaultSeatCount],
2062
+ ["defaultMaxMultiplier", next.defaultMaxMultiplier],
2063
+ ["defaultLaiZi", next.defaultLaiZi],
2064
+ ["spectatorCardCounter", next.spectatorCardCounter]
2065
+ ];
2066
+ for (const [field, fieldValue] of writes) {
2067
+ if (stored[field] === fieldValue) continue;
2068
+ await scope.set(String(field), fieldValue);
2069
+ }
2070
+ setSaved(true);
2071
+ } catch (cause) {
2072
+ setError(cause instanceof Error ? cause.message : "保存失败");
2073
+ } finally {
2074
+ setSaving(false);
2075
+ }
2076
+ };
2077
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2078
+ className: `${styles_module_css_default.settingsCard} ${open ? styles_module_css_default.settingsOpen : ""}`,
2079
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2080
+ type: "button",
2081
+ className: styles_module_css_default.settingsHeader,
2082
+ "aria-expanded": open,
2083
+ onClick: () => {
2084
+ setOpen(!open);
2085
+ },
2086
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2087
+ className: styles_module_css_default.settingsHeadText,
2088
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2089
+ className: styles_module_css_default.settingsName,
2090
+ children: "dsh-poker"
2091
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2092
+ className: styles_module_css_default.settingsDesc,
2093
+ children: "本机房间默认:人数、癞子、邀请地址和欢迎积分。同一房间先到入座,后来观战。"
2094
+ })]
2095
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2096
+ className: `${styles_module_css_default.settingsChevron} ${open ? styles_module_css_default.settingsChevronOpen : ""}`,
2097
+ width: "14",
2098
+ height: "14",
2099
+ viewBox: "0 0 14 14",
2100
+ "aria-hidden": "true",
2101
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2102
+ d: "M3.2 5.1 7 8.9l3.8-3.8",
2103
+ fill: "none",
2104
+ stroke: "currentColor",
2105
+ strokeWidth: "1.4",
2106
+ strokeLinecap: "round",
2107
+ strokeLinejoin: "round"
2108
+ })
2109
+ })]
2110
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2111
+ className: styles_module_css_default.settingsBody,
2112
+ children: [
2113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2114
+ className: styles_module_css_default.settingsHint,
2115
+ children: "开房在侧栏 New Session 下面的「斗地主」,这里只改下次开房的默认值。点保存后生效,已经开着的房间不动。"
2116
+ }),
2117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2118
+ className: styles_module_css_default.settingsField,
2119
+ children: [
2120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2121
+ className: styles_module_css_default.settingsLabel,
2122
+ children: "对外地址 publicBaseUrl"
2123
+ }),
2124
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2125
+ className: styles_module_css_default.settingsInput,
2126
+ value: draft.publicBaseUrl,
2127
+ placeholder: "https://your-named-tunnel.example",
2128
+ onChange: (event) => {
2129
+ patch("publicBaseUrl", event.target.value);
2130
+ }
2131
+ }),
2132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2133
+ className: styles_module_css_default.settingsFieldHint,
2134
+ children: "填能打到本机的 https origin,好友才能用邀请链接进桌。空着只能本机标签页互打。"
2135
+ })
2136
+ ]
2137
+ }),
2138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2139
+ className: styles_module_css_default.settingsField,
2140
+ children: [
2141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2142
+ className: styles_module_css_default.settingsLabel,
2143
+ children: "欢迎积分"
2144
+ }),
2145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2146
+ className: styles_module_css_default.settingsInput,
2147
+ inputMode: "numeric",
2148
+ value: draft.welcomeAtoms,
2149
+ onChange: (event) => {
2150
+ patch("welcomeAtoms", event.target.value);
2151
+ }
2152
+ }),
2153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2154
+ className: styles_module_css_default.settingsFieldHint,
2155
+ children: "进房时账本里的起始积分,不是 DeepSeek 平台余额。默认 200M。"
2156
+ })
2157
+ ]
2158
+ }),
2159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2160
+ className: styles_module_css_default.settingsField,
2161
+ children: [
2162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2163
+ className: styles_module_css_default.settingsLabel,
2164
+ children: "默认人数"
2165
+ }),
2166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2167
+ className: styles_module_css_default.settingsInput,
2168
+ value: draft.defaultSeatCount,
2169
+ onChange: (event) => {
2170
+ patch("defaultSeatCount", Number(event.target.value) === 4 ? 4 : 3);
2171
+ },
2172
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2173
+ value: 3,
2174
+ children: "3 人"
2175
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2176
+ value: 4,
2177
+ children: "4 人"
2178
+ })]
2179
+ }),
2180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2181
+ className: styles_module_css_default.settingsFieldHint,
2182
+ children: "3 人一副牌 3 张底牌;4 人两副牌 8 张底牌。"
2183
+ })
2184
+ ]
2185
+ }),
2186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2187
+ className: styles_module_css_default.settingsField,
2188
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2189
+ className: styles_module_css_default.settingsLabel,
2190
+ children: "默认倍数封顶"
2191
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2192
+ className: styles_module_css_default.settingsInput,
2193
+ inputMode: "numeric",
2194
+ value: String(draft.defaultMaxMultiplier),
2195
+ onChange: (event) => {
2196
+ patch("defaultMaxMultiplier", Number(event.target.value));
2197
+ }
2198
+ })]
2199
+ }),
2200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2201
+ className: styles_module_css_default.settingsField,
2202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2203
+ className: styles_module_css_default.settingsSwitchRow,
2204
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2205
+ className: styles_module_css_default.settingsLabel,
2206
+ children: "默认开癞子"
2207
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2208
+ type: "button",
2209
+ className: styles_module_css_default.settingsSwitch,
2210
+ role: "switch",
2211
+ "aria-checked": draft.defaultLaiZi,
2212
+ onClick: () => {
2213
+ patch("defaultLaiZi", !draft.defaultLaiZi);
2214
+ },
2215
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2216
+ className: styles_module_css_default.settingsSwitchTrack,
2217
+ "data-on": draft.defaultLaiZi ? "" : void 0,
2218
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: styles_module_css_default.settingsSwitchThumb })
2219
+ })
2220
+ })]
2221
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2222
+ className: styles_module_css_default.settingsFieldHint,
2223
+ children: "下次开房默认癞子。翻两张万能牌,大小王不当癞子。"
2224
+ })]
2225
+ }),
2226
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2227
+ className: styles_module_css_default.settingsField,
2228
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2229
+ className: styles_module_css_default.settingsSwitchRow,
2230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2231
+ className: styles_module_css_default.settingsLabel,
2232
+ children: "旁观记牌器"
2233
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2234
+ type: "button",
2235
+ className: styles_module_css_default.settingsSwitch,
2236
+ role: "switch",
2237
+ "aria-checked": draft.spectatorCardCounter,
2238
+ onClick: () => {
2239
+ patch("spectatorCardCounter", !draft.spectatorCardCounter);
2240
+ },
2241
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2242
+ className: styles_module_css_default.settingsSwitchTrack,
2243
+ "data-on": draft.spectatorCardCounter ? "" : void 0,
2244
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: styles_module_css_default.settingsSwitchThumb })
2245
+ })
2246
+ })]
2247
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2248
+ className: styles_module_css_default.settingsFieldHint,
2249
+ children: "观战可见剩余牌张。默认关。"
2250
+ })]
2251
+ }),
2252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2253
+ className: styles_module_css_default.settingsActions,
2254
+ children: [
2255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2256
+ type: "button",
2257
+ className: styles_module_css_default.primary,
2258
+ disabled: saving || !dirty || snap.writable === false,
2259
+ onClick: () => {
2260
+ save();
2261
+ },
2262
+ children: saving ? "保存中…" : "Save"
2263
+ }),
2264
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2265
+ type: "button",
2266
+ className: styles_module_css_default.ghost,
2267
+ disabled: saving || !dirty,
2268
+ onClick: () => {
2269
+ setDraft(stored);
2270
+ setSaved(false);
2271
+ setError(null);
2272
+ },
2273
+ children: "Discard"
2274
+ }),
2275
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2276
+ className: styles_module_css_default.settingsSaveError,
2277
+ children: error
2278
+ }) : saved && !dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2279
+ className: styles_module_css_default.settingsSaveOk,
2280
+ children: "已保存,下次开房生效"
2281
+ }) : dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2282
+ className: styles_module_css_default.settingsFieldHint,
2283
+ children: "有未保存的更改"
2284
+ }) : null
2285
+ ]
2286
+ }),
2287
+ snap.status === "unavailable" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2288
+ className: styles_module_css_default.settingsHint,
2289
+ children: "没挂 storageDomain 时房间只在内存里,重启就没了。"
2290
+ }) : null
2291
+ ]
2292
+ }) : null]
2293
+ });
2294
+ }
2295
+
2296
+ //#endregion
2297
+ //#region src/client/slots.tsx
2298
+ function registerClientSlots(ctx) {
2299
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
2300
+ name: "sidebar.footer.action",
2301
+ id: "doudizhu"
2302
+ }, DouDizhuSidebarButton));
2303
+ ctx.slots.inject("shell.overlay", () => ctx.slots.register({
2304
+ name: "shell.overlay",
2305
+ id: "doudizhu"
2306
+ }, DouDizhuOverlay));
2307
+ ctx.slots.inject("settings.plugin.item", () => {
2308
+ const scope = ctx.settingsScope.bind({ namespace: "doudizhu" });
2309
+ return ctx.slots.register({
2310
+ name: "settings.plugin.item",
2311
+ key: "doudizhu"
2312
+ }, function DouDizhuSettingsCard() {
2313
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, { scope });
2314
+ });
2315
+ });
2316
+ }
2317
+ function DouDizhuSidebarButton({ wide }) {
2318
+ const [mount, setMount] = (0, react.useState)(null);
2319
+ (0, react.useEffect)(() => {
2320
+ let host = null;
2321
+ const place = () => {
2322
+ const after = findNewSessionAnchor();
2323
+ if (!after) return;
2324
+ if (!host) {
2325
+ host = document.createElement("div");
2326
+ host.dataset.doudizhuEntry = "";
2327
+ }
2328
+ if (host.previousElementSibling !== after || !host.isConnected) after.insertAdjacentElement("afterend", host);
2329
+ setMount((prev) => prev === host ? prev : host);
2330
+ };
2331
+ place();
2332
+ const observer = new MutationObserver(place);
2333
+ observer.observe(document.body, {
2334
+ childList: true,
2335
+ subtree: true
2336
+ });
2337
+ return () => {
2338
+ observer.disconnect();
2339
+ host?.remove();
2340
+ };
2341
+ }, []);
2342
+ (0, react.useEffect)(() => {
2343
+ if (!mount) return;
2344
+ const slot = styles_module_css_default.sidebarSlot ?? "";
2345
+ const rail = styles_module_css_default.sidebarSlotRail ?? "";
2346
+ mount.className = wide ? slot : `${slot} ${rail}`;
2347
+ }, [mount, wide]);
2348
+ if (!mount) return null;
2349
+ return (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2350
+ type: "button",
2351
+ className: `${styles_module_css_default.sidebarBtn} ${wide ? "" : styles_module_css_default.sidebarBtnRail}`,
2352
+ onClick: () => {
2353
+ location.hash = "#/doudizhu";
2354
+ },
2355
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: wide ? "斗地主" : "斗" }), wide ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2356
+ className: styles_module_css_default.muted,
2357
+ children: "dsh-poker"
2358
+ }) : null]
2359
+ }), mount);
2360
+ }
2361
+ function findNewSessionAnchor() {
2362
+ const labeled = document.querySelectorAll("button[aria-label=\"New session\"], button[aria-label=\"新建会话\"]");
2363
+ const button = labeled[labeled.length - 1] ?? [...document.querySelectorAll("button")].find((el) => {
2364
+ const text = el.textContent?.replace(/\s+/g, " ").trim();
2365
+ return text === "New Session" || text === "新会话";
2366
+ }) ?? null;
2367
+ if (!button) return null;
2368
+ let node = button;
2369
+ let parent = node.parentElement;
2370
+ while (parent) {
2371
+ const style = getComputedStyle(parent);
2372
+ if (style.display === "flex" && style.flexDirection === "column") return node;
2373
+ node = parent;
2374
+ parent = node.parentElement;
2375
+ }
2376
+ return button;
2377
+ }
2378
+ function DouDizhuOverlay() {
2379
+ const [open, setOpen] = (0, react.useState)(() => location.hash.startsWith("#/doudizhu"));
2380
+ useHashOpen(setOpen);
2381
+ if (!open) return null;
2382
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HostApp, { onClose: () => {
2383
+ location.hash = "#/";
2384
+ } });
2385
+ }
2386
+ function useHashOpen(setOpen) {
2387
+ (0, react.useEffect)(() => {
2388
+ const sync = () => {
2389
+ setOpen(location.hash.startsWith("#/doudizhu"));
2390
+ };
2391
+ window.addEventListener("hashchange", sync);
2392
+ return () => {
2393
+ window.removeEventListener("hashchange", sync);
2394
+ };
2395
+ }, [setOpen]);
2396
+ }
2397
+
2398
+ //#endregion
2399
+ //#region src/client/index.tsx
2400
+ const name = "doudizhu-client";
2401
+ const inject = [
2402
+ "slots",
2403
+ "connection",
2404
+ "remote",
2405
+ "settingsScope"
2406
+ ];
2407
+ function apply(ctx) {
2408
+ ctx.effect(() => {
2409
+ registerClientSlots(ctx);
2410
+ return () => {};
2411
+ });
2412
+ }
2413
+
2414
+ //#endregion
2415
+ exports.apply = apply;
2416
+ exports.inject = inject;
2417
+ exports.name = name;
2418
+ return module.exports; } });
2419
+ //# sourceMappingURL=client.js.map