@my-games/2048-react 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # @my-games/2048-react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > 2048 游戏的 React 18 组件适配器(函数组件 + Hooks + forwardRef)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @my-games/2048-react react react-dom
9
+ ```
10
+
11
+ ## 用法
12
+
13
+ ```tsx
14
+ import { Game2048 } from '@my-games/2048-react';
15
+ import '@my-games/2048-react/style.css';
16
+
17
+ export function Demo() {
18
+ return (
19
+ <Game2048
20
+ size={4}
21
+ saveKey="my-app:2048:save"
22
+ bestKey="my-app:2048:best"
23
+ enableAI={false}
24
+ onMove={(dir, gained) => console.log(dir, gained)}
25
+ onWon={() => console.log('won')}
26
+ />
27
+ );
28
+ }
29
+ ```
30
+
31
+ **样式必须显式引入**:vite lib 模式把内部 CSS import 抽到 `dist/index.css`,
32
+ JS bundle 不再引用。消费者入口加一次 `import '@my-games/2048-react/style.css'`
33
+ (或从 core 引 `import '@my-games/2048-core/styles.css'`,内容一致)。
34
+
35
+ ## Props
36
+
37
+ | Prop | 类型 | 默认 | 说明 |
38
+ | --- | --- | --- | --- |
39
+ | `size` | `3 \| 4 \| 5 \| 6` | `4` | 棋盘边长 |
40
+ | `storage` | `SaveStorage \| null` | `localStorage` | 传 `null` 关闭持久化(SSR) |
41
+ | `saveKey` | `string` | `game-2048:save` | 存档键,多实例分开 |
42
+ | `bestKey` | `string` | `game-2048:best` | 最高分独立键 |
43
+ | `freshStart` | `boolean` | `false` | 忽略已有存档开新局 |
44
+ | `autofocus` | `boolean` | `true` | mount 后自动 focus 让键盘立刻可用 |
45
+ | `enableAI` | `boolean` | `false` | 是否渲染 AI 按钮(API 一直可用) |
46
+ | `aiDepth` | `number` | `3` | expectimax 搜索深度 |
47
+ | `title` | `string` | `'2048'` | 传空串隐藏 header |
48
+ | `onMove` | `(dir, gained) => void` | — | 一次成功的滑动后触发 |
49
+ | `onWon` | `() => void` | — | 首次凑出目标值 |
50
+ | `onLost` | `() => void` | — | 无合法方向 |
51
+ | `className` | `string` | — | 追加到最外层 `<div className="game-2048">` |
52
+
53
+ ## Ref 命令式方法
54
+
55
+ ```tsx
56
+ import { useRef } from 'react';
57
+ import { Game2048, type Game2048Handle } from '@my-games/2048-react';
58
+
59
+ const g = useRef<Game2048Handle>(null);
60
+ // g.current?.move('left');
61
+ // g.current?.suggestMove();
62
+ // g.current?.aiMove();
63
+ ```
64
+
65
+ ## Hook(想自己画 UI)
66
+
67
+ ```tsx
68
+ import { useGame } from '@my-games/2048-react';
69
+
70
+ function MyBoard() {
71
+ const { state, move, suggestMove } = useGame({ size: 4 });
72
+ return <div>{state.score}</div>;
73
+ }
74
+ ```
75
+
76
+ ---
77
+
78
+ 完整玩法、引擎 API、设计取舍见仓库根 README:
79
+ <https://github.com/my-gam/2048#readme>
80
+
81
+ ## License
82
+
83
+ MIT.
@@ -0,0 +1,34 @@
1
+ import { Direction, SaveStorage } from '@my-games/2048-core';
2
+ /**
3
+ * Game2048 —— React 18 组件。与 Vue 版行为对齐:
4
+ * - 键盘:window keydown + 严格 focus 判定(多实例并存只活动那个响应);
5
+ * 方向键 / WASD。
6
+ * - 触摸:单指 swipe 主导轴 ≥24px;touchstart 时把焦点抢过来。
7
+ * - 动画:新块 / 合并 / 位移全走 core 的 styles.css。
8
+ * - AI:`enableAI` prop 控制按钮渲染;通过 ref 一直可以命令式访问。
9
+ */
10
+ export interface Game2048Props {
11
+ size?: 3 | 4 | 5 | 6;
12
+ storage?: SaveStorage | null;
13
+ saveKey?: string;
14
+ bestKey?: string;
15
+ freshStart?: boolean;
16
+ autofocus?: boolean;
17
+ enableAI?: boolean;
18
+ aiDepth?: number;
19
+ title?: string;
20
+ onMove?: (dir: Direction, gained: number) => void;
21
+ onWon?: () => void;
22
+ onLost?: () => void;
23
+ className?: string;
24
+ }
25
+ export interface Game2048Handle {
26
+ suggestMove(): Direction | null;
27
+ aiMove(): Direction | null;
28
+ newGame(): void;
29
+ undo(): boolean;
30
+ move(dir: Direction): boolean;
31
+ continuePlaying(): void;
32
+ }
33
+ export declare const Game2048: import('react').ForwardRefExoticComponent<Game2048Props & import('react').RefAttributes<Game2048Handle>>;
34
+ //# sourceMappingURL=Game2048.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Game2048.d.ts","sourceRoot":"","sources":["../src/Game2048.tsx"],"names":[],"mappings":"AAYA,OAAO,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClD,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,IAAI,SAAS,GAAG,IAAI,CAAC;IAChC,MAAM,IAAI,SAAS,GAAG,IAAI,CAAC;IAC3B,OAAO,IAAI,IAAI,CAAC;IAChB,IAAI,IAAI,OAAO,CAAC;IAChB,IAAI,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC;IAC9B,eAAe,IAAI,IAAI,CAAC;CACzB;AA0UD,eAAO,MAAM,QAAQ,0GAA4B,CAAC"}
package/dist/index.css ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * game-2048 通用样式表。
3
+ *
4
+ * 由 `@my-games/2048-core/styles.css` 暴露,Vue / React 适配器都直接 import 这一份,
5
+ * 保持视觉一致(避免两包各自漂移)。
6
+ *
7
+ * 关键约定:
8
+ * - 棋盘外圈 .game-2048-board 承担 pointer 事件(触摸/鼠标),内部 .tile 全部 pointer-events:none
9
+ * 防止子元素吞掉滑动检测。
10
+ * - Tile 定位用 translate,row/col 通过 --row / --col CSS 变量注入到 style 里;
11
+ * `transition: transform 120ms` 让滑动有平滑感,scale 关键帧负责新块与合并 pop。
12
+ * - 尺寸自适应:容器宽度决定一切;cell 与 gap 用比例 + clamp() 缩放,
13
+ * 最小 320px、最大 520px 之间保持视觉合理。
14
+ */
15
+
16
+ .game-2048 {
17
+ --board-size: min(90vw, 520px);
18
+ --gap: clamp(6px, 2.5vw, 12px);
19
+ --radius: clamp(4px, 1.2vw, 8px);
20
+ --bg-board: #bbada0;
21
+ --bg-empty: rgba(238, 228, 218, 0.35);
22
+ --text-dark: #776e65;
23
+ --text-light: #f9f6f2;
24
+ --accent: #8f7a66;
25
+
26
+ display: inline-flex;
27
+ flex-direction: column;
28
+ gap: var(--gap);
29
+ font-family: 'Helvetica Neue', 'Segoe UI', system-ui, -apple-system, sans-serif;
30
+ color: var(--text-dark);
31
+ user-select: none;
32
+ -webkit-tap-highlight-color: transparent;
33
+ }
34
+
35
+ .game-2048-header {
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ gap: var(--gap);
40
+ width: var(--board-size);
41
+ max-width: 90vw;
42
+ }
43
+
44
+ .game-2048-title {
45
+ margin: 0;
46
+ font-size: clamp(28px, 8vw, 44px);
47
+ font-weight: 800;
48
+ letter-spacing: -1px;
49
+ color: var(--text-dark);
50
+ }
51
+
52
+ .game-2048-scores {
53
+ display: flex;
54
+ gap: 6px;
55
+ }
56
+
57
+ .game-2048-score-box {
58
+ position: relative;
59
+ min-width: 74px;
60
+ padding: 6px 12px 8px;
61
+ border-radius: var(--radius);
62
+ background: var(--bg-board);
63
+ color: var(--text-light);
64
+ text-align: center;
65
+ font-variant-numeric: tabular-nums;
66
+ }
67
+
68
+ .game-2048-score-box .label {
69
+ display: block;
70
+ font-size: 11px;
71
+ letter-spacing: 1px;
72
+ color: #eee4da;
73
+ text-transform: uppercase;
74
+ }
75
+
76
+ .game-2048-score-box .value {
77
+ display: block;
78
+ font-size: 22px;
79
+ font-weight: 700;
80
+ line-height: 1.2;
81
+ }
82
+
83
+ .game-2048-score-box .delta {
84
+ position: absolute;
85
+ top: 4px;
86
+ right: 8px;
87
+ color: rgba(119, 110, 101, 1);
88
+ font-weight: 700;
89
+ font-size: 14px;
90
+ animation: game-2048-score-pop 700ms ease-out forwards;
91
+ pointer-events: none;
92
+ }
93
+
94
+ @keyframes game-2048-score-pop {
95
+ 0% { opacity: 1; transform: translateY(0); }
96
+ 100% { opacity: 0; transform: translateY(-24px); }
97
+ }
98
+
99
+ .game-2048-toolbar {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: space-between;
103
+ gap: var(--gap);
104
+ width: var(--board-size);
105
+ max-width: 90vw;
106
+ }
107
+
108
+ .game-2048-desc {
109
+ margin: 0;
110
+ font-size: 13px;
111
+ line-height: 1.5;
112
+ color: var(--text-dark);
113
+ opacity: 0.9;
114
+ }
115
+
116
+ .game-2048-actions {
117
+ display: flex;
118
+ gap: 6px;
119
+ flex-shrink: 0;
120
+ }
121
+
122
+ .game-2048-button {
123
+ padding: 8px 14px;
124
+ border: 0;
125
+ border-radius: var(--radius);
126
+ background: var(--accent);
127
+ color: var(--text-light);
128
+ font: inherit;
129
+ font-weight: 700;
130
+ font-size: 14px;
131
+ cursor: pointer;
132
+ transition: background 120ms, transform 80ms;
133
+ }
134
+
135
+ .game-2048-button:hover:not(:disabled) {
136
+ background: #a68472;
137
+ }
138
+
139
+ .game-2048-button:active:not(:disabled) {
140
+ transform: translateY(1px);
141
+ }
142
+
143
+ .game-2048-button:disabled {
144
+ opacity: 0.4;
145
+ cursor: not-allowed;
146
+ }
147
+
148
+ .game-2048-button.is-primary {
149
+ background: #f2b179;
150
+ color: #776e65;
151
+ }
152
+
153
+ .game-2048-button.is-primary:hover:not(:disabled) {
154
+ background: #f59563;
155
+ }
156
+
157
+ .game-2048-board {
158
+ position: relative;
159
+ width: var(--board-size);
160
+ max-width: 90vw;
161
+ aspect-ratio: 1;
162
+ padding: var(--gap);
163
+ border-radius: var(--radius);
164
+ background: var(--bg-board);
165
+ touch-action: none;
166
+ outline: none;
167
+ box-sizing: content-box;
168
+ }
169
+
170
+ .game-2048-board:focus-visible {
171
+ box-shadow: 0 0 0 3px rgba(143, 122, 102, 0.5);
172
+ }
173
+
174
+ .game-2048-grid {
175
+ display: grid;
176
+ width: 100%;
177
+ height: 100%;
178
+ gap: var(--gap);
179
+ }
180
+
181
+ .game-2048-cell {
182
+ border-radius: var(--radius);
183
+ background: var(--bg-empty);
184
+ }
185
+
186
+ .game-2048-tile-layer {
187
+ position: absolute;
188
+ inset: var(--gap);
189
+ pointer-events: none;
190
+ }
191
+
192
+ .game-2048-tile {
193
+ --pitch-x: calc(100% + var(--gap));
194
+ --pitch-y: calc(100% + var(--gap));
195
+ position: absolute;
196
+ top: 0;
197
+ left: 0;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ border-radius: var(--radius);
202
+ background: #eee4da;
203
+ color: var(--text-dark);
204
+ font-weight: 800;
205
+ font-size: clamp(24px, 8vw, 44px);
206
+ line-height: 1;
207
+ /* 注意 --cols 由 board 内联 style 提供 */
208
+ width: calc((100% - var(--gap) * (var(--cols) - 1)) / var(--cols));
209
+ height: calc((100% - var(--gap) * (var(--cols) - 1)) / var(--cols));
210
+ transform: translate(
211
+ calc(var(--col) * var(--pitch-x)),
212
+ calc(var(--row) * var(--pitch-y))
213
+ );
214
+ transition: transform 120ms ease-in-out;
215
+ will-change: transform;
216
+ }
217
+
218
+ .game-2048-tile.is-new {
219
+ animation: game-2048-tile-spawn 180ms ease-out;
220
+ }
221
+
222
+ .game-2048-tile.is-merged {
223
+ animation: game-2048-tile-pop 180ms ease-out;
224
+ z-index: 2;
225
+ }
226
+
227
+ @keyframes game-2048-tile-spawn {
228
+ 0% { opacity: 0; transform: translate(calc(var(--col) * var(--pitch-x)), calc(var(--row) * var(--pitch-y))) scale(0.3); }
229
+ 100% { opacity: 1; transform: translate(calc(var(--col) * var(--pitch-x)), calc(var(--row) * var(--pitch-y))) scale(1); }
230
+ }
231
+
232
+ @keyframes game-2048-tile-pop {
233
+ 0% { transform: translate(calc(var(--col) * var(--pitch-x)), calc(var(--row) * var(--pitch-y))) scale(1); }
234
+ 50% { transform: translate(calc(var(--col) * var(--pitch-x)), calc(var(--row) * var(--pitch-y))) scale(1.18); }
235
+ 100% { transform: translate(calc(var(--col) * var(--pitch-x)), calc(var(--row) * var(--pitch-y))) scale(1); }
236
+ }
237
+
238
+ /* 数值配色(沿用 2048 官方色板) */
239
+ .game-2048-tile[data-value='2'] { background: #eee4da; color: var(--text-dark); }
240
+ .game-2048-tile[data-value='4'] { background: #ede0c8; color: var(--text-dark); }
241
+ .game-2048-tile[data-value='8'] { background: #f2b179; color: var(--text-light); }
242
+ .game-2048-tile[data-value='16'] { background: #f59563; color: var(--text-light); }
243
+ .game-2048-tile[data-value='32'] { background: #f67c5f; color: var(--text-light); }
244
+ .game-2048-tile[data-value='64'] { background: #f65e3b; color: var(--text-light); }
245
+ .game-2048-tile[data-value='128'] { background: #edcf72; color: var(--text-light); font-size: clamp(20px, 6.5vw, 38px); }
246
+ .game-2048-tile[data-value='256'] { background: #edcc61; color: var(--text-light); font-size: clamp(20px, 6.5vw, 38px); }
247
+ .game-2048-tile[data-value='512'] { background: #edc850; color: var(--text-light); font-size: clamp(20px, 6.5vw, 38px); }
248
+ .game-2048-tile[data-value='1024'] { background: #edc53f; color: var(--text-light); font-size: clamp(16px, 5.5vw, 32px); }
249
+ .game-2048-tile[data-value='2048'] { background: #edc22e; color: var(--text-light); font-size: clamp(16px, 5.5vw, 32px); box-shadow: 0 0 24px rgba(243, 215, 116, 0.6); }
250
+ .game-2048-tile[data-super='1'] { background: #3c3a33; color: var(--text-light); font-size: clamp(14px, 5vw, 28px); }
251
+
252
+ .game-2048-overlay {
253
+ position: absolute;
254
+ inset: 0;
255
+ display: flex;
256
+ flex-direction: column;
257
+ align-items: center;
258
+ justify-content: center;
259
+ gap: 12px;
260
+ border-radius: var(--radius);
261
+ background: rgba(238, 228, 218, 0.73);
262
+ color: var(--text-dark);
263
+ text-align: center;
264
+ opacity: 0;
265
+ pointer-events: none;
266
+ transition: opacity 220ms ease-in;
267
+ z-index: 10;
268
+ }
269
+
270
+ .game-2048-overlay.is-visible {
271
+ opacity: 1;
272
+ pointer-events: auto;
273
+ }
274
+
275
+ .game-2048-overlay.is-lost {
276
+ background: rgba(238, 228, 218, 0.5);
277
+ }
278
+
279
+ .game-2048-overlay h2 {
280
+ margin: 0;
281
+ font-size: clamp(32px, 9vw, 52px);
282
+ font-weight: 800;
283
+ color: var(--text-dark);
284
+ }
285
+
286
+ .game-2048-overlay.is-won h2 {
287
+ color: #f9f6f2;
288
+ text-shadow: 0 0 12px rgba(237, 194, 46, 0.6);
289
+ }
290
+
291
+ .game-2048-overlay .row {
292
+ display: flex;
293
+ gap: 8px;
294
+ }
295
+
296
+ .game-2048-ai-indicator {
297
+ display: inline-flex;
298
+ align-items: center;
299
+ gap: 6px;
300
+ padding: 4px 10px;
301
+ border-radius: 999px;
302
+ background: rgba(143, 122, 102, 0.14);
303
+ font-size: 12px;
304
+ color: var(--text-dark);
305
+ }
306
+
307
+ @media (prefers-reduced-motion: reduce) {
308
+ .game-2048-tile,
309
+ .game-2048-overlay {
310
+ transition: none;
311
+ }
312
+ .game-2048-tile.is-new,
313
+ .game-2048-tile.is-merged {
314
+ animation: none;
315
+ }
316
+ }
317
+ /*$vite$:1*/
@@ -0,0 +1,4 @@
1
+ export { Game2048, type Game2048Props, type Game2048Handle } from './Game2048';
2
+ export { useGame, type UseGameOptions, type UseGameReturn } from './useGame';
3
+ export type { BoardSize, Direction, GameState, GameStatus, SaveStorage, Tile } from '@my-games/2048-core';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE7E,YAAY,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,UAAU,EACV,WAAW,EACX,IAAI,EACL,MAAM,qBAAqB,CAAC"}