@my-games/2048-vue 0.1.0 → 0.3.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
@@ -13,6 +13,7 @@ pnpm add @my-games/2048-vue vue
13
13
  ```vue
14
14
  <script setup lang="ts">
15
15
  import { Game2048 } from '@my-games/2048-vue';
16
+ import '@my-games/2048-vue/style.css';
16
17
 
17
18
  function onMove(dir, gained) {
18
19
  console.log(dir, gained);
@@ -30,7 +31,9 @@ function onMove(dir, gained) {
30
31
  </template>
31
32
  ```
32
33
 
33
- 组件样式已经内嵌(`import '@my-games/2048-core/styles.css'`),不用额外引 CSS。
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'`,内容一致)。
34
37
 
35
38
  ## Props
36
39
 
@@ -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*/
package/dist/index.d.ts CHANGED
@@ -1,131 +1,5 @@
1
- import { BoardSize } from '@my-games/2048-core';
2
- import { ComponentOptionsMixin } from 'vue';
3
- import { ComponentProvideOptions } from 'vue';
4
- import { DefineComponent } from 'vue';
5
- import { Direction } from '@my-games/2048-core';
6
- import { Game2048 as Game2048_2 } from '@my-games/2048-core';
7
- import { GameOptions } from '@my-games/2048-core';
8
- import { GameState } from '@my-games/2048-core';
9
- import { GameStatus } from '@my-games/2048-core';
10
- import { PublicProps } from 'vue';
11
- import { SaveStorage } from '@my-games/2048-core';
12
- import { ShallowRef } from 'vue';
13
- import { Tile } from '@my-games/2048-core';
14
-
15
- export { BoardSize }
16
-
17
- export { Direction }
18
-
19
- export declare const Game2048: DefineComponent<Props, {
20
- suggestMove: () => Direction | null;
21
- aiMove: () => Direction | null;
22
- newGame: () => void;
23
- undo: () => boolean;
24
- move: (dir: Direction) => boolean;
25
- continuePlaying: () => void;
26
- }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {} & {
27
- move: (dir: Direction, gained: number) => any;
28
- won: () => any;
29
- lost: () => any;
30
- }, string, PublicProps, Readonly<Props> & Readonly<{
31
- onMove?: ((dir: Direction, gained: number) => any) | undefined;
32
- onWon?: (() => any) | undefined;
33
- onLost?: (() => any) | undefined;
34
- }>, {
35
- aiDepth: number;
36
- size: 3 | 4 | 5 | 6;
37
- storage: SaveStorage | null;
38
- saveKey: string;
39
- bestKey: string;
40
- freshStart: boolean;
41
- autofocus: boolean;
42
- enableAI: boolean;
43
- title: string;
44
- }, {}, {}, {}, string, ComponentProvideOptions, false, {
45
- boardEl: HTMLDivElement;
46
- }, HTMLDivElement>;
47
-
48
- /**
49
- * 组件 `ref` 上暴露的方法(`defineExpose` 的对应类型)。
50
- * 供父组件用 `const g = ref<Game2048Instance>()` 精确声明类型。
51
- */
52
- export declare interface Game2048Instance {
53
- suggestMove(): Direction | null;
54
- aiMove(): Direction | null;
55
- newGame(): void;
56
- undo(): boolean;
57
- move(dir: Direction): boolean;
58
- continuePlaying(): void;
59
- }
60
-
61
- export { GameState }
62
-
63
- export { GameStatus }
64
-
65
- /**
66
- * Game2048 —— Vue 3 官方组件。
67
- *
68
- * 输入模型:
69
- * - 键盘:window 监听 keydown,但只有 board 元素拥有 DOM 焦点时才处理。
70
- * 多实例并存时用户点击 / 触摸哪个,哪个成为「活动」实例;`autofocus=true`
71
- * (默认)让第一个实例在 mount 时自动 focus,符合单实例场景直觉。
72
- * - 触摸:board 上的单指 swipe,主导轴决定方向;阈值 24px。
73
- *
74
- * 动画:
75
- * - 新块:scale-in
76
- * - 合并:pop 弹跳
77
- * - 滑动:transform transition
78
- *
79
- * AI:`enableAI=true` 打开「提示」/「AI 帮走」按钮;未打开时依然可以通过
80
- * template ref 拿到 `suggestMove` / `aiMove` 方法(`defineExpose`)。
81
- */
82
- declare interface Props {
83
- size?: 3 | 4 | 5 | 6;
84
- storage?: SaveStorage | null;
85
- saveKey?: string;
86
- bestKey?: string;
87
- freshStart?: boolean;
88
- autofocus?: boolean;
89
- enableAI?: boolean;
90
- aiDepth?: number;
91
- title?: string;
92
- }
93
-
94
- export { SaveStorage }
95
-
96
- export { Tile }
97
-
98
- export declare function useGame(options?: UseGameOptions): UseGameReturn;
99
-
100
- /**
101
- * `useGame` —— Vue 侧连接 Game2048 引擎的 composable。
102
- *
103
- * 用法:
104
- * ```ts
105
- * const { state, move, newGame, undo } = useGame({ size: 4 });
106
- * ```
107
- *
108
- * 生命周期:内部订阅引擎;`onBeforeUnmount` 时自动退订,防止内存泄漏。
109
- * state 用 `shallowRef` 存 —— 引擎每次都推新对象,深度 reactive 无收益。
110
- */
111
- export declare interface UseGameOptions extends GameOptions {
112
- /** AI 搜索深度;仅 `aiMove` 用到。 */
113
- aiDepth?: number;
114
- }
115
-
116
- export declare interface UseGameReturn {
117
- /** 引擎的完整状态快照;用 ShallowRef 声明让 <script setup> 的模板能自动 unwrap。 */
118
- state: ShallowRef<GameState>;
119
- raw: Game2048_2;
120
- move: (dir: Direction) => boolean;
121
- newGame: () => void;
122
- undo: () => boolean;
123
- continuePlaying: () => void;
124
- canUndo: () => boolean;
125
- /** 让 AI 走一步(内部仍走 game.move 触发订阅)。 */
126
- aiMove: () => Direction | null;
127
- /** 只查询、不走子。 */
128
- suggestMove: () => Direction | null;
129
- }
130
-
131
- export { }
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"}