@my-games/2048-vue 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,82 @@
1
- # Temporary Holding Version
1
+ # @my-games/2048-vue
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 游戏的 Vue 3 组件适配器(Composition API + SFC)。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pnpm add @my-games/2048-vue vue
9
+ ```
10
+
11
+ ## 用法
12
+
13
+ ```vue
14
+ <script setup lang="ts">
15
+ import { Game2048 } from '@my-games/2048-vue';
16
+ import '@my-games/2048-vue/style.css';
17
+
18
+ function onMove(dir, gained) {
19
+ console.log(dir, gained);
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <Game2048
25
+ :size="4"
26
+ save-key="my-app:2048:save"
27
+ best-key="my-app:2048:best"
28
+ :enable-ai="false"
29
+ @move="onMove"
30
+ />
31
+ </template>
32
+ ```
33
+
34
+ **样式必须显式引入**:vite lib 模式把内部 CSS import 抽到 `dist/index.css`,
35
+ JS bundle 不再引用。消费者入口加一次 `import '@my-games/2048-vue/style.css'`
36
+ (或从 core 引 `import '@my-games/2048-core/styles.css'`,内容一致)。
37
+
38
+ ## Props
39
+
40
+ | Prop | 类型 | 默认 | 说明 |
41
+ | --- | --- | --- | --- |
42
+ | `size` | `3 \| 4 \| 5 \| 6` | `4` | 棋盘边长 |
43
+ | `storage` | `SaveStorage \| null` | `localStorage` | 传 `null` 关闭持久化(SSR) |
44
+ | `saveKey` | `string` | `game-2048:save` | 存档键,多实例分开 |
45
+ | `bestKey` | `string` | `game-2048:best` | 最高分独立键 |
46
+ | `freshStart` | `boolean` | `false` | 忽略已有存档开新局 |
47
+ | `autofocus` | `boolean` | `true` | mount 后自动 focus 让键盘立刻可用 |
48
+ | `enableAI` | `boolean` | `false` | 是否渲染 AI 按钮(API 一直可用) |
49
+ | `aiDepth` | `number` | `3` | expectimax 搜索深度 |
50
+ | `title` | `string` | `'2048'` | 传空串隐藏 header |
51
+
52
+ ## 事件
53
+
54
+ `move(dir, gained)` / `won()` / `lost()`
55
+
56
+ ## Ref 命令式方法
57
+
58
+ `<script setup>` 里 `const g = ref<Game2048Instance>();` 后:
59
+
60
+ - `g.value?.move(dir)`
61
+ - `g.value?.newGame()` / `undo()` / `continuePlaying()`
62
+ - `g.value?.suggestMove(): Direction | null` — 只查询建议
63
+ - `g.value?.aiMove(): Direction | null` — 让 AI 走一步
64
+
65
+ ## Composable(想自己画 UI)
66
+
67
+ ```ts
68
+ import { useGame } from '@my-games/2048-vue';
69
+
70
+ const { state, move, suggestMove } = useGame({ size: 4 });
71
+ ```
72
+
73
+ `state` 是 `ShallowRef<GameState>`,模板里 auto-unwrap 直接用。
74
+
75
+ ---
76
+
77
+ 完整玩法、引擎 API、设计取舍见仓库根 README:
78
+ <https://github.com/my-gam/2048#readme>
79
+
80
+ ## License
81
+
82
+ MIT.
@@ -0,0 +1,58 @@
1
+ import { Direction } from '@my-games/2048-core';
2
+ /**
3
+ * Game2048 —— Vue 3 官方组件。
4
+ *
5
+ * 输入模型:
6
+ * - 键盘:window 监听 keydown,但只有 board 元素拥有 DOM 焦点时才处理。
7
+ * 多实例并存时用户点击 / 触摸哪个,哪个成为「活动」实例;`autofocus=true`
8
+ * (默认)让第一个实例在 mount 时自动 focus,符合单实例场景直觉。
9
+ * - 触摸:board 上的单指 swipe,主导轴决定方向;阈值 24px。
10
+ *
11
+ * 动画:
12
+ * - 新块:scale-in
13
+ * - 合并:pop 弹跳
14
+ * - 滑动:transform transition
15
+ *
16
+ * AI:`enableAI=true` 打开「提示」/「AI 帮走」按钮;未打开时依然可以通过
17
+ * template ref 拿到 `suggestMove` / `aiMove` 方法(`defineExpose`)。
18
+ */
19
+ interface Props {
20
+ size?: 3 | 4 | 5 | 6;
21
+ storage?: import('@my-games/2048-core').SaveStorage | null;
22
+ saveKey?: string;
23
+ bestKey?: string;
24
+ freshStart?: boolean;
25
+ autofocus?: boolean;
26
+ enableAI?: boolean;
27
+ aiDepth?: number;
28
+ title?: string;
29
+ }
30
+ declare const __VLS_export: import('vue').DefineComponent<Props, {
31
+ suggestMove: () => Direction | null;
32
+ aiMove: () => Direction | null;
33
+ newGame: () => void;
34
+ undo: () => boolean;
35
+ move: (dir: Direction) => boolean;
36
+ continuePlaying: () => void;
37
+ }, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
38
+ move: (dir: Direction, gained: number) => any;
39
+ won: () => any;
40
+ lost: () => any;
41
+ }, string, import('vue').PublicProps, Readonly<Props> & Readonly<{
42
+ onMove?: ((dir: Direction, gained: number) => any) | undefined;
43
+ onWon?: (() => any) | undefined;
44
+ onLost?: (() => any) | undefined;
45
+ }>, {
46
+ aiDepth: number;
47
+ size: 3 | 4 | 5 | 6;
48
+ storage: import('@my-games/2048-core').SaveStorage | null;
49
+ saveKey: string;
50
+ bestKey: string;
51
+ freshStart: boolean;
52
+ autofocus: boolean;
53
+ enableAI: boolean;
54
+ title: string;
55
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
56
+ declare const _default: typeof __VLS_export;
57
+ export default _default;
58
+ //# sourceMappingURL=Game2048.vue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Game2048.vue.d.ts","sourceRoot":"","sources":["../src/Game2048.vue"],"names":[],"mappings":"AAsVA,OAAO,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD;;;;;;;;;;;;;;;;GAgBG;AAEH,UAAU,KAAK;IACb,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,qBAAqB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3D,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;CAChB;AA0cD,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;6EAKhB,CAAC;wBACkB,OAAO,YAAY;AAAxC,wBAAyC"}
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,5 @@
1
+ export { default as Game2048 } from './Game2048.vue';
2
+ export { useGame, type UseGameOptions, type UseGameReturn } from './useGame';
3
+ export type { Game2048Instance } from './instance';
4
+ export type { BoardSize, Direction, GameState, GameStatus, SaveStorage, Tile } from '@my-games/2048-core';
5
+ //# 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,OAAO,IAAI,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAC7E,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,YAAY,EACV,SAAS,EACT,SAAS,EACT,SAAS,EACT,UAAU,EACV,WAAW,EACX,IAAI,EACL,MAAM,qBAAqB,CAAC"}