@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 +4 -1
- package/dist/Game2048.vue.d.ts +58 -0
- package/dist/Game2048.vue.d.ts.map +1 -0
- package/dist/index.css +317 -0
- package/dist/index.d.ts +5 -131
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +331 -249
- package/dist/index.js.map +1 -1
- package/dist/instance.d.ts +14 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/useGame.d.ts +33 -0
- package/dist/useGame.d.ts.map +1 -0
- package/package.json +12 -10
- package/dist/style.css +0 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/useGame.ts","../src/Game2048.vue"],"sourcesContent":["import { onBeforeUnmount, shallowRef, type ShallowRef } from 'vue';\nimport { bestMove as aiBestMove, Game2048, type Direction, type GameOptions, type GameState } from '@my-games/2048-core';\n\n/**\n * `useGame` —— Vue 侧连接 Game2048 引擎的 composable。\n *\n * 用法:\n * ```ts\n * const { state, move, newGame, undo } = useGame({ size: 4 });\n * ```\n *\n * 生命周期:内部订阅引擎;`onBeforeUnmount` 时自动退订,防止内存泄漏。\n * state 用 `shallowRef` 存 —— 引擎每次都推新对象,深度 reactive 无收益。\n */\nexport interface UseGameOptions extends GameOptions {\n /** AI 搜索深度;仅 `aiMove` 用到。 */\n aiDepth?: number;\n}\n\nexport interface UseGameReturn {\n /** 引擎的完整状态快照;用 ShallowRef 声明让 <script setup> 的模板能自动 unwrap。 */\n state: ShallowRef<GameState>;\n raw: Game2048;\n move: (dir: Direction) => boolean;\n newGame: () => void;\n undo: () => boolean;\n continuePlaying: () => void;\n canUndo: () => boolean;\n /** 让 AI 走一步(内部仍走 game.move 触发订阅)。 */\n aiMove: () => Direction | null;\n /** 只查询、不走子。 */\n suggestMove: () => Direction | null;\n}\n\nexport function useGame(options: UseGameOptions = {}): UseGameReturn {\n const { aiDepth = 3, ...engineOptions } = options;\n const game = new Game2048(engineOptions);\n const state = shallowRef<GameState>(game.getState());\n const unsubscribe = game.subscribe((s) => {\n state.value = s;\n });\n onBeforeUnmount(unsubscribe);\n\n return {\n state,\n raw: game,\n move: (dir) => game.move(dir),\n newGame: () => game.newGame(),\n undo: () => game.undo(),\n continuePlaying: () => game.continuePlaying(),\n canUndo: () => game.canUndo(),\n suggestMove: () => aiBestMove(game.getGridValues(), { depth: aiDepth }),\n aiMove: () => {\n const dir = aiBestMove(game.getGridValues(), { depth: aiDepth });\n if (dir) game.move(dir);\n return dir;\n }\n };\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport '@my-games/2048-core/styles.css';\nimport { useGame } from './useGame';\nimport type { Direction } from '@my-games/2048-core';\n\n/**\n * Game2048 —— Vue 3 官方组件。\n *\n * 输入模型:\n * - 键盘:window 监听 keydown,但只有 board 元素拥有 DOM 焦点时才处理。\n * 多实例并存时用户点击 / 触摸哪个,哪个成为「活动」实例;`autofocus=true`\n * (默认)让第一个实例在 mount 时自动 focus,符合单实例场景直觉。\n * - 触摸:board 上的单指 swipe,主导轴决定方向;阈值 24px。\n *\n * 动画:\n * - 新块:scale-in\n * - 合并:pop 弹跳\n * - 滑动:transform transition\n *\n * AI:`enableAI=true` 打开「提示」/「AI 帮走」按钮;未打开时依然可以通过\n * template ref 拿到 `suggestMove` / `aiMove` 方法(`defineExpose`)。\n */\n\ninterface Props {\n size?: 3 | 4 | 5 | 6;\n storage?: import('@my-games/2048-core').SaveStorage | null;\n saveKey?: string;\n bestKey?: string;\n freshStart?: boolean;\n autofocus?: boolean;\n enableAI?: boolean;\n aiDepth?: number;\n title?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n size: 4,\n storage: undefined,\n saveKey: undefined,\n bestKey: undefined,\n freshStart: false,\n autofocus: true,\n enableAI: false,\n aiDepth: 3,\n title: '2048'\n});\n\nconst emit = defineEmits<{\n (e: 'move', dir: Direction, gained: number): void;\n (e: 'won'): void;\n (e: 'lost'): void;\n}>();\n\nconst { state, move, newGame, undo, continuePlaying, suggestMove, aiMove } = useGame({\n size: props.size,\n storage: props.storage,\n saveKey: props.saveKey,\n bestKey: props.bestKey,\n freshStart: props.freshStart,\n aiDepth: props.aiDepth\n});\n\nconst boardEl = ref<HTMLDivElement | null>(null);\nconst scoreDelta = ref<{ amount: number; key: number } | null>(null);\nconst aiHint = ref<Direction | null>(null);\nconst lastScore = ref(state.value.score);\nconst lastStatus = ref(state.value.status);\nlet deltaTimer: number | undefined;\n\n// 手动 watch:Vue 的 watch 需要在 setup 里 import;这里用普通 getter 触发\n// 每次订阅推来的新 state 时,state.value 会变,computed 重算 → DOM 更新。\n// 分数变化与 status 变化通过在 update 回调里对比 last 值实现(见下方 subscribe 二次挂载)。\n// 简化:把状态变化处理放在 useGame 的订阅里;这里只保留必要的 reactive。\n\n// --- 键盘 ---\nconst KEY_TO_DIR: Record<string, Direction> = {\n ArrowUp: 'up',\n ArrowDown: 'down',\n ArrowLeft: 'left',\n ArrowRight: 'right',\n w: 'up',\n a: 'left',\n s: 'down',\n d: 'right',\n W: 'up',\n A: 'left',\n S: 'down',\n D: 'right'\n};\n\nfunction isBoardFocused(): boolean {\n if (typeof document === 'undefined') return false;\n return document.activeElement === boardEl.value;\n}\n\nfunction onKey(ev: KeyboardEvent) {\n const dir = KEY_TO_DIR[ev.key];\n if (!dir) return;\n if (!isBoardFocused()) return;\n ev.preventDefault();\n performMove(dir);\n}\n\nfunction performMove(dir: Direction) {\n const before = state.value.score;\n const ok = move(dir);\n if (ok) {\n emit('move', dir, state.value.score - before);\n if (props.enableAI) aiHint.value = null;\n }\n}\n\n// --- 触摸 ---\nlet touchStartPoint: { x: number; y: number } | null = null;\nfunction onTouchStart(ev: TouchEvent) {\n if (ev.touches.length !== 1) return;\n const t = ev.touches[0];\n if (!t) return;\n touchStartPoint = { x: t.clientX, y: t.clientY };\n // 触摸时把焦点转到本 board,让后续键盘操作也生效\n boardEl.value?.focus({ preventScroll: true });\n}\nfunction onTouchEnd(ev: TouchEvent) {\n const start = touchStartPoint;\n touchStartPoint = null;\n if (!start) return;\n const t = ev.changedTouches[0];\n if (!t) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n const adx = Math.abs(dx);\n const ady = Math.abs(dy);\n if (Math.max(adx, ady) < 24) return;\n const dir: Direction = adx > ady ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up';\n performMove(dir);\n}\n\nonMounted(() => {\n if (props.autofocus) boardEl.value?.focus({ preventScroll: true });\n window.addEventListener('keydown', onKey, { passive: false });\n});\n\nonBeforeUnmount(() => {\n window.removeEventListener('keydown', onKey);\n if (deltaTimer) window.clearTimeout(deltaTimer);\n});\n\n// --- 分数 & 状态 side-effect ---\nwatch(\n () => state.value.score,\n (now, prev) => {\n const gained = now - (prev ?? lastScore.value);\n if (gained > 0) {\n scoreDelta.value = { amount: gained, key: Date.now() + Math.random() };\n if (deltaTimer) window.clearTimeout(deltaTimer);\n deltaTimer = window.setTimeout(() => (scoreDelta.value = null), 700);\n }\n lastScore.value = now;\n }\n);\nwatch(\n () => state.value.status,\n (s) => {\n if (s === lastStatus.value) return;\n if (s === 'won') emit('won');\n else if (s === 'lost') emit('lost');\n lastStatus.value = s;\n }\n);\n\n// --- 渲染辅助 ---\nconst gridStyle = computed(() => ({\n gridTemplateColumns: `repeat(${state.value.size}, 1fr)`,\n gridTemplateRows: `repeat(${state.value.size}, 1fr)`\n}));\n\nconst boardVars = computed(() => ({ '--cols': String(state.value.size) }));\n\nfunction tileVars(row: number, col: number) {\n return { '--row': String(row), '--col': String(col) } as Record<string, string>;\n}\n\nfunction tileClass(t: { value: number; isNew: boolean; merged: boolean }) {\n return {\n 'is-new': t.isNew,\n 'is-merged': t.merged,\n 'is-super': t.value > 2048\n };\n}\n\nfunction tileAttrs(t: { value: number }) {\n return t.value > 2048\n ? { 'data-super': '1' }\n : {};\n}\n\nconst overlayVisible = computed(() => state.value.status !== 'playing');\nconst isWon = computed(() => state.value.status === 'won');\nconst isLost = computed(() => state.value.status === 'lost');\nconst undoDisabled = computed(() => state.value.historySize === 0);\n\n// --- AI ---\nfunction onAskAI() {\n aiHint.value = suggestMove();\n}\nfunction onDoAI() {\n const before = state.value.score;\n const dir = aiMove();\n if (dir) {\n emit('move', dir, state.value.score - before);\n aiHint.value = null;\n }\n}\n\ndefineExpose({ suggestMove, aiMove, newGame, undo, move, continuePlaying });\n</script>\n\n<template>\n <div class=\"game-2048\">\n <header v-if=\"title\" class=\"game-2048-header\">\n <h1 class=\"game-2048-title\">{{ title }}</h1>\n <div class=\"game-2048-scores\">\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Score</span>\n <span class=\"value\">{{ state.score }}</span>\n <span\n v-if=\"scoreDelta\"\n :key=\"scoreDelta.key\"\n class=\"delta\"\n >+{{ scoreDelta.amount }}</span\n >\n </div>\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Best</span>\n <span class=\"value\">{{ state.bestScore }}</span>\n </div>\n </div>\n </header>\n\n <div class=\"game-2048-toolbar\">\n <p class=\"game-2048-desc\">\n 合并相同数字,冲过 <b>2048</b>。方向键 / WASD / 触屏滑动。\n </p>\n <div class=\"game-2048-actions\">\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onAskAI\"\n >\n 提示\n </button>\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onDoAI\"\n >\n AI 帮走\n </button>\n <button class=\"game-2048-button\" type=\"button\" :disabled=\"undoDisabled\" @click=\"undo()\">\n 撤销\n </button>\n <button class=\"game-2048-button is-primary\" type=\"button\" @click=\"newGame()\">\n 新游戏\n </button>\n </div>\n </div>\n\n <div\n ref=\"boardEl\"\n class=\"game-2048-board\"\n :style=\"boardVars\"\n tabindex=\"0\"\n role=\"application\"\n :aria-label=\"`2048 游戏板 ${state.status}`\"\n @touchstart.passive=\"onTouchStart\"\n @touchend.prevent=\"onTouchEnd\"\n >\n <div class=\"game-2048-grid\" :style=\"gridStyle\">\n <div v-for=\"i in state.size * state.size\" :key=\"i\" class=\"game-2048-cell\" />\n </div>\n <div class=\"game-2048-tile-layer\">\n <div\n v-for=\"t in state.tiles\"\n :key=\"t.id\"\n class=\"game-2048-tile\"\n :class=\"tileClass(t)\"\n :style=\"tileVars(t.row, t.col)\"\n :data-value=\"t.value\"\n v-bind=\"tileAttrs(t)\"\n >\n {{ t.value }}\n </div>\n </div>\n\n <div\n v-if=\"enableAI && aiHint\"\n class=\"game-2048-ai-indicator\"\n style=\"position: absolute; top: 8px; left: 8px; z-index: 3\"\n >\n 推荐:{{ aiHint }}\n </div>\n\n <div\n class=\"game-2048-overlay\"\n :class=\"{ 'is-visible': overlayVisible, 'is-won': isWon, 'is-lost': isLost }\"\n >\n <h2 v-if=\"isWon\">你赢了!</h2>\n <h2 v-else-if=\"isLost\">游戏结束</h2>\n <div class=\"row\">\n <button v-if=\"isWon\" class=\"game-2048-button\" type=\"button\" @click=\"continuePlaying()\">\n 继续游戏\n </button>\n <button\n v-if=\"isLost && !undoDisabled\"\n class=\"game-2048-button\"\n type=\"button\"\n @click=\"undo()\"\n >\n 悔一步\n </button>\n <button\n v-if=\"overlayVisible\"\n class=\"game-2048-button is-primary\"\n type=\"button\"\n @click=\"newGame()\"\n >\n 再来一局\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n"],"names":["useGame","options","aiDepth","engineOptions","game","Game2048","state","shallowRef","unsubscribe","s","onBeforeUnmount","dir","aiBestMove","props","__props","emit","__emit","move","newGame","undo","continuePlaying","suggestMove","aiMove","boardEl","ref","scoreDelta","aiHint","lastScore","lastStatus","deltaTimer","KEY_TO_DIR","isBoardFocused","onKey","ev","performMove","before","touchStartPoint","onTouchStart","t","_a","onTouchEnd","start","dx","dy","adx","ady","onMounted","watch","now","prev","gained","gridStyle","computed","boardVars","tileVars","row","col","tileClass","tileAttrs","overlayVisible","isWon","isLost","undoDisabled","onAskAI","onDoAI","__expose","_openBlock","_createElementBlock","_hoisted_1","_hoisted_2","_createElementVNode","_hoisted_3","_toDisplayString","_hoisted_4","_hoisted_5","_cache","_hoisted_6","_unref","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","_Fragment","_renderList","i","_hoisted_15","_mergeProps","_hoisted_16","_hoisted_17","_hoisted_20"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["aiBestMove","_openBlock","_createElementBlock","_createElementVNode","_toDisplayString","_unref","_createCommentVNode","_createTextVNode","_normalizeStyle","_withModifiers","_Fragment","_renderList","_mergeProps","_normalizeClass","_sfc_main"],"sources":["../src/useGame.ts","../src/Game2048.vue","../src/Game2048.vue"],"sourcesContent":["import { onBeforeUnmount, shallowRef, type ShallowRef } from 'vue';\nimport { bestMove as aiBestMove, Game2048, type Direction, type GameOptions, type GameState } from '@my-games/2048-core';\n\n/**\n * `useGame` —— Vue 侧连接 Game2048 引擎的 composable。\n *\n * 用法:\n * ```ts\n * const { state, move, newGame, undo } = useGame({ size: 4 });\n * ```\n *\n * 生命周期:内部订阅引擎;`onBeforeUnmount` 时自动退订,防止内存泄漏。\n * state 用 `shallowRef` 存 —— 引擎每次都推新对象,深度 reactive 无收益。\n */\nexport interface UseGameOptions extends GameOptions {\n /** AI 搜索深度;仅 `aiMove` 用到。 */\n aiDepth?: number;\n}\n\nexport interface UseGameReturn {\n /** 引擎的完整状态快照;用 ShallowRef 声明让 <script setup> 的模板能自动 unwrap。 */\n state: ShallowRef<GameState>;\n raw: Game2048;\n move: (dir: Direction) => boolean;\n newGame: () => void;\n undo: () => boolean;\n continuePlaying: () => void;\n canUndo: () => boolean;\n /** 让 AI 走一步(内部仍走 game.move 触发订阅)。 */\n aiMove: () => Direction | null;\n /** 只查询、不走子。 */\n suggestMove: () => Direction | null;\n}\n\nexport function useGame(options: UseGameOptions = {}): UseGameReturn {\n const { aiDepth = 3, ...engineOptions } = options;\n const game = new Game2048(engineOptions);\n const state = shallowRef<GameState>(game.getState());\n const unsubscribe = game.subscribe((s) => {\n state.value = s;\n });\n onBeforeUnmount(unsubscribe);\n\n return {\n state,\n raw: game,\n move: (dir) => game.move(dir),\n newGame: () => game.newGame(),\n undo: () => game.undo(),\n continuePlaying: () => game.continuePlaying(),\n canUndo: () => game.canUndo(),\n suggestMove: () => aiBestMove(game.getGridValues(), { depth: aiDepth }),\n aiMove: () => {\n const dir = aiBestMove(game.getGridValues(), { depth: aiDepth });\n if (dir) game.move(dir);\n return dir;\n }\n };\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport '@my-games/2048-core/styles.css';\nimport { useGame } from './useGame';\nimport type { Direction } from '@my-games/2048-core';\n\n/**\n * Game2048 —— Vue 3 官方组件。\n *\n * 输入模型:\n * - 键盘:window 监听 keydown,但只有 board 元素拥有 DOM 焦点时才处理。\n * 多实例并存时用户点击 / 触摸哪个,哪个成为「活动」实例;`autofocus=true`\n * (默认)让第一个实例在 mount 时自动 focus,符合单实例场景直觉。\n * - 触摸:board 上的单指 swipe,主导轴决定方向;阈值 24px。\n *\n * 动画:\n * - 新块:scale-in\n * - 合并:pop 弹跳\n * - 滑动:transform transition\n *\n * AI:`enableAI=true` 打开「提示」/「AI 帮走」按钮;未打开时依然可以通过\n * template ref 拿到 `suggestMove` / `aiMove` 方法(`defineExpose`)。\n */\n\ninterface Props {\n size?: 3 | 4 | 5 | 6;\n storage?: import('@my-games/2048-core').SaveStorage | null;\n saveKey?: string;\n bestKey?: string;\n freshStart?: boolean;\n autofocus?: boolean;\n enableAI?: boolean;\n aiDepth?: number;\n title?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n size: 4,\n storage: undefined,\n saveKey: undefined,\n bestKey: undefined,\n freshStart: false,\n autofocus: true,\n enableAI: false,\n aiDepth: 3,\n title: '2048'\n});\n\nconst emit = defineEmits<{\n (e: 'move', dir: Direction, gained: number): void;\n (e: 'won'): void;\n (e: 'lost'): void;\n}>();\n\nconst { state, move, newGame, undo, continuePlaying, suggestMove, aiMove } = useGame({\n size: props.size,\n storage: props.storage,\n saveKey: props.saveKey,\n bestKey: props.bestKey,\n freshStart: props.freshStart,\n aiDepth: props.aiDepth\n});\n\nconst boardEl = ref<HTMLDivElement | null>(null);\nconst scoreDelta = ref<{ amount: number; key: number } | null>(null);\nconst aiHint = ref<Direction | null>(null);\nconst lastScore = ref(state.value.score);\nconst lastStatus = ref(state.value.status);\nlet deltaTimer: number | undefined;\n\n// 手动 watch:Vue 的 watch 需要在 setup 里 import;这里用普通 getter 触发\n// 每次订阅推来的新 state 时,state.value 会变,computed 重算 → DOM 更新。\n// 分数变化与 status 变化通过在 update 回调里对比 last 值实现(见下方 subscribe 二次挂载)。\n// 简化:把状态变化处理放在 useGame 的订阅里;这里只保留必要的 reactive。\n\n// --- 键盘 ---\nconst KEY_TO_DIR: Record<string, Direction> = {\n ArrowUp: 'up',\n ArrowDown: 'down',\n ArrowLeft: 'left',\n ArrowRight: 'right',\n w: 'up',\n a: 'left',\n s: 'down',\n d: 'right',\n W: 'up',\n A: 'left',\n S: 'down',\n D: 'right'\n};\n\nfunction isBoardFocused(): boolean {\n if (typeof document === 'undefined') return false;\n return document.activeElement === boardEl.value;\n}\n\nfunction onKey(ev: KeyboardEvent) {\n const dir = KEY_TO_DIR[ev.key];\n if (!dir) return;\n if (!isBoardFocused()) return;\n ev.preventDefault();\n performMove(dir);\n}\n\nfunction performMove(dir: Direction) {\n const before = state.value.score;\n const ok = move(dir);\n if (ok) {\n emit('move', dir, state.value.score - before);\n if (props.enableAI) aiHint.value = null;\n }\n}\n\n// --- 触摸 ---\nlet touchStartPoint: { x: number; y: number } | null = null;\nfunction onTouchStart(ev: TouchEvent) {\n if (ev.touches.length !== 1) return;\n const t = ev.touches[0];\n if (!t) return;\n touchStartPoint = { x: t.clientX, y: t.clientY };\n // 触摸时把焦点转到本 board,让后续键盘操作也生效\n boardEl.value?.focus({ preventScroll: true });\n}\nfunction onTouchEnd(ev: TouchEvent) {\n const start = touchStartPoint;\n touchStartPoint = null;\n if (!start) return;\n const t = ev.changedTouches[0];\n if (!t) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n const adx = Math.abs(dx);\n const ady = Math.abs(dy);\n if (Math.max(adx, ady) < 24) return;\n const dir: Direction = adx > ady ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up';\n performMove(dir);\n}\n\nonMounted(() => {\n if (props.autofocus) boardEl.value?.focus({ preventScroll: true });\n window.addEventListener('keydown', onKey, { passive: false });\n});\n\nonBeforeUnmount(() => {\n window.removeEventListener('keydown', onKey);\n if (deltaTimer) window.clearTimeout(deltaTimer);\n});\n\n// --- 分数 & 状态 side-effect ---\nwatch(\n () => state.value.score,\n (now, prev) => {\n const gained = now - (prev ?? lastScore.value);\n if (gained > 0) {\n scoreDelta.value = { amount: gained, key: Date.now() + Math.random() };\n if (deltaTimer) window.clearTimeout(deltaTimer);\n deltaTimer = window.setTimeout(() => (scoreDelta.value = null), 700);\n }\n lastScore.value = now;\n }\n);\nwatch(\n () => state.value.status,\n (s) => {\n if (s === lastStatus.value) return;\n if (s === 'won') emit('won');\n else if (s === 'lost') emit('lost');\n lastStatus.value = s;\n }\n);\n\n// --- 渲染辅助 ---\nconst gridStyle = computed(() => ({\n gridTemplateColumns: `repeat(${state.value.size}, 1fr)`,\n gridTemplateRows: `repeat(${state.value.size}, 1fr)`\n}));\n\nconst boardVars = computed(() => ({ '--cols': String(state.value.size) }));\n\nfunction tileVars(row: number, col: number) {\n return { '--row': String(row), '--col': String(col) } as Record<string, string>;\n}\n\nfunction tileClass(t: { value: number; isNew: boolean; merged: boolean }) {\n return {\n 'is-new': t.isNew,\n 'is-merged': t.merged,\n 'is-super': t.value > 2048\n };\n}\n\nfunction tileAttrs(t: { value: number }) {\n return t.value > 2048\n ? { 'data-super': '1' }\n : {};\n}\n\nconst overlayVisible = computed(() => state.value.status !== 'playing');\nconst isWon = computed(() => state.value.status === 'won');\nconst isLost = computed(() => state.value.status === 'lost');\nconst undoDisabled = computed(() => state.value.historySize === 0);\n\n// --- AI ---\nfunction onAskAI() {\n aiHint.value = suggestMove();\n}\nfunction onDoAI() {\n const before = state.value.score;\n const dir = aiMove();\n if (dir) {\n emit('move', dir, state.value.score - before);\n aiHint.value = null;\n }\n}\n\ndefineExpose({ suggestMove, aiMove, newGame, undo, move, continuePlaying });\n</script>\n\n<template>\n <div class=\"game-2048\">\n <header v-if=\"title\" class=\"game-2048-header\">\n <h1 class=\"game-2048-title\">{{ title }}</h1>\n <div class=\"game-2048-scores\">\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Score</span>\n <span class=\"value\">{{ state.score }}</span>\n <span\n v-if=\"scoreDelta\"\n :key=\"scoreDelta.key\"\n class=\"delta\"\n >+{{ scoreDelta.amount }}</span\n >\n </div>\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Best</span>\n <span class=\"value\">{{ state.bestScore }}</span>\n </div>\n </div>\n </header>\n\n <div class=\"game-2048-toolbar\">\n <p class=\"game-2048-desc\">\n 合并相同数字,冲过 <b>2048</b>。方向键 / WASD / 触屏滑动。\n </p>\n <div class=\"game-2048-actions\">\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onAskAI\"\n >\n 提示\n </button>\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onDoAI\"\n >\n AI 帮走\n </button>\n <button class=\"game-2048-button\" type=\"button\" :disabled=\"undoDisabled\" @click=\"undo()\">\n 撤销\n </button>\n <button class=\"game-2048-button is-primary\" type=\"button\" @click=\"newGame()\">\n 新游戏\n </button>\n </div>\n </div>\n\n <div\n ref=\"boardEl\"\n class=\"game-2048-board\"\n :style=\"boardVars\"\n tabindex=\"0\"\n role=\"application\"\n :aria-label=\"`2048 游戏板 ${state.status}`\"\n @touchstart.passive=\"onTouchStart\"\n @touchend.prevent=\"onTouchEnd\"\n >\n <div class=\"game-2048-grid\" :style=\"gridStyle\">\n <div v-for=\"i in state.size * state.size\" :key=\"i\" class=\"game-2048-cell\" />\n </div>\n <div class=\"game-2048-tile-layer\">\n <div\n v-for=\"t in state.tiles\"\n :key=\"t.id\"\n class=\"game-2048-tile\"\n :class=\"tileClass(t)\"\n :style=\"tileVars(t.row, t.col)\"\n :data-value=\"t.value\"\n v-bind=\"tileAttrs(t)\"\n >\n {{ t.value }}\n </div>\n </div>\n\n <div\n v-if=\"enableAI && aiHint\"\n class=\"game-2048-ai-indicator\"\n style=\"position: absolute; top: 8px; left: 8px; z-index: 3\"\n >\n 推荐:{{ aiHint }}\n </div>\n\n <div\n class=\"game-2048-overlay\"\n :class=\"{ 'is-visible': overlayVisible, 'is-won': isWon, 'is-lost': isLost }\"\n >\n <h2 v-if=\"isWon\">你赢了!</h2>\n <h2 v-else-if=\"isLost\">游戏结束</h2>\n <div class=\"row\">\n <button v-if=\"isWon\" class=\"game-2048-button\" type=\"button\" @click=\"continuePlaying()\">\n 继续游戏\n </button>\n <button\n v-if=\"isLost && !undoDisabled\"\n class=\"game-2048-button\"\n type=\"button\"\n @click=\"undo()\"\n >\n 悔一步\n </button>\n <button\n v-if=\"overlayVisible\"\n class=\"game-2048-button is-primary\"\n type=\"button\"\n @click=\"newGame()\"\n >\n 再来一局\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport '@my-games/2048-core/styles.css';\nimport { useGame } from './useGame';\nimport type { Direction } from '@my-games/2048-core';\n\n/**\n * Game2048 —— Vue 3 官方组件。\n *\n * 输入模型:\n * - 键盘:window 监听 keydown,但只有 board 元素拥有 DOM 焦点时才处理。\n * 多实例并存时用户点击 / 触摸哪个,哪个成为「活动」实例;`autofocus=true`\n * (默认)让第一个实例在 mount 时自动 focus,符合单实例场景直觉。\n * - 触摸:board 上的单指 swipe,主导轴决定方向;阈值 24px。\n *\n * 动画:\n * - 新块:scale-in\n * - 合并:pop 弹跳\n * - 滑动:transform transition\n *\n * AI:`enableAI=true` 打开「提示」/「AI 帮走」按钮;未打开时依然可以通过\n * template ref 拿到 `suggestMove` / `aiMove` 方法(`defineExpose`)。\n */\n\ninterface Props {\n size?: 3 | 4 | 5 | 6;\n storage?: import('@my-games/2048-core').SaveStorage | null;\n saveKey?: string;\n bestKey?: string;\n freshStart?: boolean;\n autofocus?: boolean;\n enableAI?: boolean;\n aiDepth?: number;\n title?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n size: 4,\n storage: undefined,\n saveKey: undefined,\n bestKey: undefined,\n freshStart: false,\n autofocus: true,\n enableAI: false,\n aiDepth: 3,\n title: '2048'\n});\n\nconst emit = defineEmits<{\n (e: 'move', dir: Direction, gained: number): void;\n (e: 'won'): void;\n (e: 'lost'): void;\n}>();\n\nconst { state, move, newGame, undo, continuePlaying, suggestMove, aiMove } = useGame({\n size: props.size,\n storage: props.storage,\n saveKey: props.saveKey,\n bestKey: props.bestKey,\n freshStart: props.freshStart,\n aiDepth: props.aiDepth\n});\n\nconst boardEl = ref<HTMLDivElement | null>(null);\nconst scoreDelta = ref<{ amount: number; key: number } | null>(null);\nconst aiHint = ref<Direction | null>(null);\nconst lastScore = ref(state.value.score);\nconst lastStatus = ref(state.value.status);\nlet deltaTimer: number | undefined;\n\n// 手动 watch:Vue 的 watch 需要在 setup 里 import;这里用普通 getter 触发\n// 每次订阅推来的新 state 时,state.value 会变,computed 重算 → DOM 更新。\n// 分数变化与 status 变化通过在 update 回调里对比 last 值实现(见下方 subscribe 二次挂载)。\n// 简化:把状态变化处理放在 useGame 的订阅里;这里只保留必要的 reactive。\n\n// --- 键盘 ---\nconst KEY_TO_DIR: Record<string, Direction> = {\n ArrowUp: 'up',\n ArrowDown: 'down',\n ArrowLeft: 'left',\n ArrowRight: 'right',\n w: 'up',\n a: 'left',\n s: 'down',\n d: 'right',\n W: 'up',\n A: 'left',\n S: 'down',\n D: 'right'\n};\n\nfunction isBoardFocused(): boolean {\n if (typeof document === 'undefined') return false;\n return document.activeElement === boardEl.value;\n}\n\nfunction onKey(ev: KeyboardEvent) {\n const dir = KEY_TO_DIR[ev.key];\n if (!dir) return;\n if (!isBoardFocused()) return;\n ev.preventDefault();\n performMove(dir);\n}\n\nfunction performMove(dir: Direction) {\n const before = state.value.score;\n const ok = move(dir);\n if (ok) {\n emit('move', dir, state.value.score - before);\n if (props.enableAI) aiHint.value = null;\n }\n}\n\n// --- 触摸 ---\nlet touchStartPoint: { x: number; y: number } | null = null;\nfunction onTouchStart(ev: TouchEvent) {\n if (ev.touches.length !== 1) return;\n const t = ev.touches[0];\n if (!t) return;\n touchStartPoint = { x: t.clientX, y: t.clientY };\n // 触摸时把焦点转到本 board,让后续键盘操作也生效\n boardEl.value?.focus({ preventScroll: true });\n}\nfunction onTouchEnd(ev: TouchEvent) {\n const start = touchStartPoint;\n touchStartPoint = null;\n if (!start) return;\n const t = ev.changedTouches[0];\n if (!t) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n const adx = Math.abs(dx);\n const ady = Math.abs(dy);\n if (Math.max(adx, ady) < 24) return;\n const dir: Direction = adx > ady ? (dx > 0 ? 'right' : 'left') : dy > 0 ? 'down' : 'up';\n performMove(dir);\n}\n\nonMounted(() => {\n if (props.autofocus) boardEl.value?.focus({ preventScroll: true });\n window.addEventListener('keydown', onKey, { passive: false });\n});\n\nonBeforeUnmount(() => {\n window.removeEventListener('keydown', onKey);\n if (deltaTimer) window.clearTimeout(deltaTimer);\n});\n\n// --- 分数 & 状态 side-effect ---\nwatch(\n () => state.value.score,\n (now, prev) => {\n const gained = now - (prev ?? lastScore.value);\n if (gained > 0) {\n scoreDelta.value = { amount: gained, key: Date.now() + Math.random() };\n if (deltaTimer) window.clearTimeout(deltaTimer);\n deltaTimer = window.setTimeout(() => (scoreDelta.value = null), 700);\n }\n lastScore.value = now;\n }\n);\nwatch(\n () => state.value.status,\n (s) => {\n if (s === lastStatus.value) return;\n if (s === 'won') emit('won');\n else if (s === 'lost') emit('lost');\n lastStatus.value = s;\n }\n);\n\n// --- 渲染辅助 ---\nconst gridStyle = computed(() => ({\n gridTemplateColumns: `repeat(${state.value.size}, 1fr)`,\n gridTemplateRows: `repeat(${state.value.size}, 1fr)`\n}));\n\nconst boardVars = computed(() => ({ '--cols': String(state.value.size) }));\n\nfunction tileVars(row: number, col: number) {\n return { '--row': String(row), '--col': String(col) } as Record<string, string>;\n}\n\nfunction tileClass(t: { value: number; isNew: boolean; merged: boolean }) {\n return {\n 'is-new': t.isNew,\n 'is-merged': t.merged,\n 'is-super': t.value > 2048\n };\n}\n\nfunction tileAttrs(t: { value: number }) {\n return t.value > 2048\n ? { 'data-super': '1' }\n : {};\n}\n\nconst overlayVisible = computed(() => state.value.status !== 'playing');\nconst isWon = computed(() => state.value.status === 'won');\nconst isLost = computed(() => state.value.status === 'lost');\nconst undoDisabled = computed(() => state.value.historySize === 0);\n\n// --- AI ---\nfunction onAskAI() {\n aiHint.value = suggestMove();\n}\nfunction onDoAI() {\n const before = state.value.score;\n const dir = aiMove();\n if (dir) {\n emit('move', dir, state.value.score - before);\n aiHint.value = null;\n }\n}\n\ndefineExpose({ suggestMove, aiMove, newGame, undo, move, continuePlaying });\n</script>\n\n<template>\n <div class=\"game-2048\">\n <header v-if=\"title\" class=\"game-2048-header\">\n <h1 class=\"game-2048-title\">{{ title }}</h1>\n <div class=\"game-2048-scores\">\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Score</span>\n <span class=\"value\">{{ state.score }}</span>\n <span\n v-if=\"scoreDelta\"\n :key=\"scoreDelta.key\"\n class=\"delta\"\n >+{{ scoreDelta.amount }}</span\n >\n </div>\n <div class=\"game-2048-score-box\">\n <span class=\"label\">Best</span>\n <span class=\"value\">{{ state.bestScore }}</span>\n </div>\n </div>\n </header>\n\n <div class=\"game-2048-toolbar\">\n <p class=\"game-2048-desc\">\n 合并相同数字,冲过 <b>2048</b>。方向键 / WASD / 触屏滑动。\n </p>\n <div class=\"game-2048-actions\">\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onAskAI\"\n >\n 提示\n </button>\n <button\n v-if=\"enableAI\"\n class=\"game-2048-button\"\n type=\"button\"\n :disabled=\"isLost\"\n @click=\"onDoAI\"\n >\n AI 帮走\n </button>\n <button class=\"game-2048-button\" type=\"button\" :disabled=\"undoDisabled\" @click=\"undo()\">\n 撤销\n </button>\n <button class=\"game-2048-button is-primary\" type=\"button\" @click=\"newGame()\">\n 新游戏\n </button>\n </div>\n </div>\n\n <div\n ref=\"boardEl\"\n class=\"game-2048-board\"\n :style=\"boardVars\"\n tabindex=\"0\"\n role=\"application\"\n :aria-label=\"`2048 游戏板 ${state.status}`\"\n @touchstart.passive=\"onTouchStart\"\n @touchend.prevent=\"onTouchEnd\"\n >\n <div class=\"game-2048-grid\" :style=\"gridStyle\">\n <div v-for=\"i in state.size * state.size\" :key=\"i\" class=\"game-2048-cell\" />\n </div>\n <div class=\"game-2048-tile-layer\">\n <div\n v-for=\"t in state.tiles\"\n :key=\"t.id\"\n class=\"game-2048-tile\"\n :class=\"tileClass(t)\"\n :style=\"tileVars(t.row, t.col)\"\n :data-value=\"t.value\"\n v-bind=\"tileAttrs(t)\"\n >\n {{ t.value }}\n </div>\n </div>\n\n <div\n v-if=\"enableAI && aiHint\"\n class=\"game-2048-ai-indicator\"\n style=\"position: absolute; top: 8px; left: 8px; z-index: 3\"\n >\n 推荐:{{ aiHint }}\n </div>\n\n <div\n class=\"game-2048-overlay\"\n :class=\"{ 'is-visible': overlayVisible, 'is-won': isWon, 'is-lost': isLost }\"\n >\n <h2 v-if=\"isWon\">你赢了!</h2>\n <h2 v-else-if=\"isLost\">游戏结束</h2>\n <div class=\"row\">\n <button v-if=\"isWon\" class=\"game-2048-button\" type=\"button\" @click=\"continuePlaying()\">\n 继续游戏\n </button>\n <button\n v-if=\"isLost && !undoDisabled\"\n class=\"game-2048-button\"\n type=\"button\"\n @click=\"undo()\"\n >\n 悔一步\n </button>\n <button\n v-if=\"overlayVisible\"\n class=\"game-2048-button is-primary\"\n type=\"button\"\n @click=\"newGame()\"\n >\n 再来一局\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n"],"mappings":";;;AAkCA,SAAgB,QAAQ,UAA0B,CAAC,GAAkB;CACnE,MAAM,EAAE,UAAU,GAAG,GAAG,kBAAkB;CAC1C,MAAM,OAAO,IAAI,SAAS,aAAa;CACvC,MAAM,QAAQ,WAAsB,KAAK,SAAS,CAAC;CACnD,MAAM,cAAc,KAAK,WAAW,MAAM;EACxC,MAAM,QAAQ;CAChB,CAAC;CACD,gBAAgB,WAAW;CAE3B,OAAO;EACL;EACA,KAAK;EACL,OAAO,QAAQ,KAAK,KAAK,GAAG;EAC5B,eAAe,KAAK,QAAQ;EAC5B,YAAY,KAAK,KAAK;EACtB,uBAAuB,KAAK,gBAAgB;EAC5C,eAAe,KAAK,QAAQ;EAC5B,mBAAmBA,SAAW,KAAK,cAAc,GAAG,EAAE,OAAO,QAAQ,CAAC;EACtE,cAAc;GACZ,MAAM,MAAMA,SAAW,KAAK,cAAc,GAAG,EAAE,OAAO,QAAQ,CAAC;GAC/D,IAAI,KAAK,KAAK,KAAK,GAAG;GACtB,OAAO;EACT;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ECtBA,MAAM,QAAQ;EAYd,MAAM,OAAO;EAMb,MAAM,EAAE,OAAO,MAAM,SAAS,MAAM,iBAAiB,aAAa,WAAW,QAAQ;GACnF,MAAM,MAAM;GACZ,SAAS,MAAM;GACf,SAAS,MAAM;GACf,SAAS,MAAM;GACf,YAAY,MAAM;GAClB,SAAS,MAAM;EACjB,CAAC;EAED,MAAM,UAAU,IAA2B,IAAI;EAC/C,MAAM,aAAa,IAA4C,IAAI;EACnE,MAAM,SAAS,IAAsB,IAAI;EACzC,MAAM,YAAY,IAAI,MAAM,MAAM,KAAK;EACvC,MAAM,aAAa,IAAI,MAAM,MAAM,MAAM;EACzC,IAAI;EAQJ,MAAM,aAAwC;GAC5C,SAAS;GACT,WAAW;GACX,WAAW;GACX,YAAY;GACZ,GAAG;GACH,GAAG;GACH,GAAG;GACH,GAAG;GACH,GAAG;GACH,GAAG;GACH,GAAG;GACH,GAAG;EACL;EAEA,SAAS,iBAA0B;GACjC,IAAI,OAAO,aAAa,aAAa,OAAO;GAC5C,OAAO,SAAS,kBAAkB,QAAQ;EAC5C;EAEA,SAAS,MAAM,IAAmB;GAChC,MAAM,MAAM,WAAW,GAAG;GAC1B,IAAI,CAAC,KAAK;GACV,IAAI,CAAC,eAAe,GAAG;GACvB,GAAG,eAAe;GAClB,YAAY,GAAG;EACjB;EAEA,SAAS,YAAY,KAAgB;GACnC,MAAM,SAAS,MAAM,MAAM;GAE3B,IADW,KAAK,GACZ,GAAI;IACN,KAAK,QAAQ,KAAK,MAAM,MAAM,QAAQ,MAAM;IAC5C,IAAI,MAAM,UAAU,OAAO,QAAQ;GACrC;EACF;EAGA,IAAI,kBAAmD;EACvD,SAAS,aAAa,IAAgB;GACpC,IAAI,GAAG,QAAQ,WAAW,GAAG;GAC7B,MAAM,IAAI,GAAG,QAAQ;GACrB,IAAI,CAAC,GAAG;GACR,kBAAkB;IAAE,GAAG,EAAE;IAAS,GAAG,EAAE;GAAQ;GAE/C,QAAQ,OAAO,MAAM,EAAE,eAAe,KAAK,CAAC;EAC9C;EACA,SAAS,WAAW,IAAgB;GAClC,MAAM,QAAQ;GACd,kBAAkB;GAClB,IAAI,CAAC,OAAO;GACZ,MAAM,IAAI,GAAG,eAAe;GAC5B,IAAI,CAAC,GAAG;GACR,MAAM,KAAK,EAAE,UAAU,MAAM;GAC7B,MAAM,KAAK,EAAE,UAAU,MAAM;GAC7B,MAAM,MAAM,KAAK,IAAI,EAAE;GACvB,MAAM,MAAM,KAAK,IAAI,EAAE;GACvB,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,IAAI;GAE7B,YADuB,MAAM,MAAO,KAAK,IAAI,UAAU,SAAU,KAAK,IAAI,SAAS,IACpE;EACjB;EAEA,gBAAgB;GACd,IAAI,MAAM,WAAW,QAAQ,OAAO,MAAM,EAAE,eAAe,KAAK,CAAC;GACjE,OAAO,iBAAiB,WAAW,OAAO,EAAE,SAAS,MAAM,CAAC;EAC9D,CAAC;EAED,sBAAsB;GACpB,OAAO,oBAAoB,WAAW,KAAK;GAC3C,IAAI,YAAY,OAAO,aAAa,UAAU;EAChD,CAAC;EAGD,YACQ,MAAM,MAAM,QACjB,KAAK,SAAS;GACb,MAAM,SAAS,OAAO,QAAQ,UAAU;GACxC,IAAI,SAAS,GAAG;IACd,WAAW,QAAQ;KAAE,QAAQ;KAAQ,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO;IAAE;IACrE,IAAI,YAAY,OAAO,aAAa,UAAU;IAC9C,aAAa,OAAO,iBAAkB,WAAW,QAAQ,MAAO,GAAG;GACrE;GACA,UAAU,QAAQ;EACpB,CACF;EACA,YACQ,MAAM,MAAM,SACjB,MAAM;GACL,IAAI,MAAM,WAAW,OAAO;GAC5B,IAAI,MAAM,OAAO,KAAK,KAAK;QACtB,IAAI,MAAM,QAAQ,KAAK,MAAM;GAClC,WAAW,QAAQ;EACrB,CACF;EAGA,MAAM,YAAY,gBAAgB;GAChC,qBAAqB,UAAU,MAAM,MAAM,KAAK;GAChD,kBAAkB,UAAU,MAAM,MAAM,KAAK;EAC/C,EAAE;EAEF,MAAM,YAAY,gBAAgB,EAAE,UAAU,OAAO,MAAM,MAAM,IAAI,EAAE,EAAE;EAEzE,SAAS,SAAS,KAAa,KAAa;GAC1C,OAAO;IAAE,SAAS,OAAO,GAAG;IAAG,SAAS,OAAO,GAAG;GAAE;EACtD;EAEA,SAAS,UAAU,GAAuD;GACxE,OAAO;IACL,UAAU,EAAE;IACZ,aAAa,EAAE;IACf,YAAY,EAAE,QAAQ;GACxB;EACF;EAEA,SAAS,UAAU,GAAsB;GACvC,OAAO,EAAE,QAAQ,OACb,EAAE,cAAc,IAAI,IACpB,CAAC;EACP;EAEA,MAAM,iBAAiB,eAAe,MAAM,MAAM,WAAW,SAAS;EACtE,MAAM,QAAQ,eAAe,MAAM,MAAM,WAAW,KAAK;EACzD,MAAM,SAAS,eAAe,MAAM,MAAM,WAAW,MAAM;EAC3D,MAAM,eAAe,eAAe,MAAM,MAAM,gBAAgB,CAAC;EAGjE,SAAS,UAAU;GACjB,OAAO,QAAQ,YAAY;EAC7B;EACA,SAAS,SAAS;GAChB,MAAM,SAAS,MAAM,MAAM;GAC3B,MAAM,MAAM,OAAO;GACnB,IAAI,KAAK;IACP,KAAK,QAAQ,KAAK,MAAM,MAAM,QAAQ,MAAM;IAC5C,OAAO,QAAQ;GACjB;EACF;EAEA,SAAa;GAAE;GAAa;GAAQ;GAAS;GAAM;GAAM;EAAgB,CAAC;;GAIxE,OAAAC,UAAA,GAAAC,mBAqHM,OArHN,YAqHM;IApHU,QAAA,SAAdD,UAAA,GAAAC,mBAkBS,UAlBT,YAkBS,CAjBPC,mBAA4C,MAA5C,YAA4CC,gBAAb,QAAA,KAAK,GAAA,CAAA,GACpCD,mBAeM,OAfN,YAeM,CAdJA,mBASM,OATN,YASM;KARJ,OAAA,OAAA,OAAA,KAAAA,mBAAgC,QAAA,EAA1B,OAAM,QAAO,GAAC,SAAK,EAAA;KACzBA,mBAA4C,QAA5C,YAA4CC,gBAArBC,MAAA,KAAA,CAAK,CAAC,KAAK,GAAA,CAAA;KAE1B,WAAA,SADRJ,UAAA,GAAAC,mBAKC,QAAA;MAHE,KAAK,WAAA,MAAW;MACjB,OAAM;KACL,GAAA,MAACE,gBAAG,WAAA,MAAW,MAAM,GAAA,CAAA,K;I,IAG1BD,mBAGM,OAHN,YAGM,CAFJ,OAAA,OAAA,OAAA,KAAAA,mBAA+B,QAAA,EAAzB,OAAM,QAAO,GAAC,QAAI,EAAA,IACxBA,mBAAgD,QAAhD,YAAgDC,gBAAzBC,MAAA,KAAA,CAAK,CAAC,SAAS,GAAA,CAAA,C;IAK5CF,mBA8BM,OA9BN,YA8BM,CA7BJ,OAAA,OAAA,OAAA,KAAAA,mBAEI,KAAA,EAFD,OAAM,iBAAgB,GAAA;KAACI,gBAAA,aACd;KAAAJ,mBAAW,KAAA,MAAR,MAAI;KAAII,gBAAA,sBACvB;I,SACAJ,mBAyBM,OAzBN,aAyBM;KAvBI,QAAA,YADRF,UAAA,GAAAC,mBAQS,UAAA;;MANP,OAAM;MACN,MAAK;MACJ,UAAU,OAAA;MACV,SAAO;KACT,GAAA,QAED,GAAA,WAAA,K;KAEQ,QAAA,YADRD,UAAA,GAAAC,mBAQS,UAAA;;MANP,OAAM;MACN,MAAK;MACJ,UAAU,OAAA;MACV,SAAO;KACT,GAAA,WAED,GAAA,WAAA,K;KACAC,mBAES,UAAA;MAFD,OAAM;MAAmB,MAAK;MAAU,UAAU,aAAA;MAAe,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEE,MAAA,IAAA,CAAI,CAAA;KAAI,GAAA,QAExF,GAAA,WAAA;KACAF,mBAES,UAAA;MAFD,OAAM;MAA8B,MAAK;MAAU,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEE,MAAA,OAAA,CAAO,CAAA;KAAI,GAAA,OAE7E;;IAIJF,mBA+DM,OAAA;KA9DA,SAAA;KAAJ,KAAI;KACJ,OAAM;KACL,OAAKK,eAAE,UAAA,KAAS;KACjB,UAAS;KACT,MAAK;KACJ,cAAU,YAAcH,MAAA,KAAA,CAAK,CAAC;KACV,qBAAA;KACpB,YAAQI,cAAU,YAAU,CAAA,SAAA,CAAA;;KAE7BN,mBAEM,OAAA;MAFD,OAAM;MAAkB,OAAKK,eAAE,UAAA,KAAS;K,KAC3CP,UAAA,IAAA,GAAAC,mBAA4EQ,UAAA,MAAAC,WAA3DN,MAAA,KAAA,CAAK,CAAC,OAAOA,MAAA,KAAA,CAAK,CAAC,OAAxB,MAAC;MAAb,OAAAJ,UAAA,GAAAC,mBAA4E,OAAA;OAAjC,KAAK;OAAG,OAAM;;;KAE3DC,mBAYM,OAZN,aAYM,EAXJF,UAAA,IAAA,GAAAC,mBAUMQ,UAAA,MAAAC,WATQN,MAAA,KAAA,CAAK,CAAC,QAAX,MAAC;MADV,OAAAJ,UAAA,GAAAC,mBAUM,OAVNU,WAUM;OARH,KAAK,EAAE;OACR,OAAK,CAAC,kBACE,UAAU,CAAC,CAAA;OAClB,OAAO,SAAS,EAAE,KAAK,EAAE,GAAG;OAC5B,cAAY,EAAE;MACP,GAAA,EAAA,SAAA,KAAA,GAAA,UAAU,CAAC,CAAA,GAAAR,gBAEhB,EAAE,KAAK,GAAA,IAAA,WAAA;;KAKN,QAAA,YAAY,OAAA,SADpBH,UAAA,GAAAC,mBAMM,OANN,aAIC,SACIE,gBAAG,OAAA,KAAM,GAAA,CAAA,K;KAGdD,mBA2BM,OAAA,EA1BJ,OAAKU,eAAA,CAAC,qBAAmB;MAAA,cACD,eAAA;MAAc,UAAY,MAAA;MAAK,WAAa,OAAA;KAAM,CAAA,CAAA,E,IAEhE,MAAA,SAAVZ,UAAA,GAAAC,mBAA0B,MAAA,aAAT,MAAI,KACN,OAAA,SAAfD,UAAA,GAAAC,mBAAgC,MAAA,aAAT,MAAI,K,8BAC3BC,mBAoBM,OApBN,aAoBM;MAnBU,MAAA,SAAdF,UAAA,GAAAC,mBAES,UAAA;;OAFY,OAAM;OAAmB,MAAK;OAAU,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEG,MAAA,eAAA,CAAe,CAAA;MAAI,GAAA,QAEvF,K;MAEQ,OAAA,SAAM,CAAK,aAAA,SADnBJ,UAAA,GAAAC,mBAOS,UAAA;;OALP,OAAM;OACN,MAAK;OACJ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEG,MAAA,IAAA,CAAI,CAAA;MACb,GAAA,OAED,K;MAEQ,eAAA,SADRJ,UAAA,GAAAC,mBAOS,UAAA;;OALP,OAAM;OACN,MAAK;OACJ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAEG,MAAA,OAAA,CAAO,CAAA;MAChB,GAAA,QAED,K"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Direction } from '@my-games/2048-core';
|
|
2
|
+
/**
|
|
3
|
+
* 组件 `ref` 上暴露的方法(`defineExpose` 的对应类型)。
|
|
4
|
+
* 供父组件用 `const g = ref<Game2048Instance>()` 精确声明类型。
|
|
5
|
+
*/
|
|
6
|
+
export interface Game2048Instance {
|
|
7
|
+
suggestMove(): Direction | null;
|
|
8
|
+
aiMove(): Direction | null;
|
|
9
|
+
newGame(): void;
|
|
10
|
+
undo(): boolean;
|
|
11
|
+
move(dir: Direction): boolean;
|
|
12
|
+
continuePlaying(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=instance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../src/instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ShallowRef } from 'vue';
|
|
2
|
+
import { Game2048, Direction, GameOptions, GameState } from '@my-games/2048-core';
|
|
3
|
+
/**
|
|
4
|
+
* `useGame` —— Vue 侧连接 Game2048 引擎的 composable。
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* ```ts
|
|
8
|
+
* const { state, move, newGame, undo } = useGame({ size: 4 });
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* 生命周期:内部订阅引擎;`onBeforeUnmount` 时自动退订,防止内存泄漏。
|
|
12
|
+
* state 用 `shallowRef` 存 —— 引擎每次都推新对象,深度 reactive 无收益。
|
|
13
|
+
*/
|
|
14
|
+
export interface UseGameOptions extends GameOptions {
|
|
15
|
+
/** AI 搜索深度;仅 `aiMove` 用到。 */
|
|
16
|
+
aiDepth?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface UseGameReturn {
|
|
19
|
+
/** 引擎的完整状态快照;用 ShallowRef 声明让 <script setup> 的模板能自动 unwrap。 */
|
|
20
|
+
state: ShallowRef<GameState>;
|
|
21
|
+
raw: Game2048;
|
|
22
|
+
move: (dir: Direction) => boolean;
|
|
23
|
+
newGame: () => void;
|
|
24
|
+
undo: () => boolean;
|
|
25
|
+
continuePlaying: () => void;
|
|
26
|
+
canUndo: () => boolean;
|
|
27
|
+
/** 让 AI 走一步(内部仍走 game.move 触发订阅)。 */
|
|
28
|
+
aiMove: () => Direction | null;
|
|
29
|
+
/** 只查询、不走子。 */
|
|
30
|
+
suggestMove: () => Direction | null;
|
|
31
|
+
}
|
|
32
|
+
export declare function useGame(options?: UseGameOptions): UseGameReturn;
|
|
33
|
+
//# sourceMappingURL=useGame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGame.d.ts","sourceRoot":"","sources":["../src/useGame.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,KAAK,UAAU,EAAE,MAAM,KAAK,CAAC;AACnE,OAAO,EAA0B,QAAQ,EAAE,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEzH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,6BAA6B;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,KAAK,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC;IAC7B,GAAG,EAAE,QAAQ,CAAC;IACd,IAAI,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,qCAAqC;IACrC,MAAM,EAAE,MAAM,SAAS,GAAG,IAAI,CAAC;IAC/B,eAAe;IACf,WAAW,EAAE,MAAM,SAAS,GAAG,IAAI,CAAC;CACrC;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CAwBnE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@my-games/2048-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "2048 游戏的 Vue 3 组件适配器(Composition API)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
-
"./style.css": "
|
|
14
|
+
"./style.css": "./dist/index.css",
|
|
15
|
+
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
17
18
|
"dist",
|
|
@@ -23,15 +24,15 @@
|
|
|
23
24
|
"vue": "^3.4.0"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@my-games/2048-core": "0.
|
|
27
|
+
"@my-games/2048-core": "0.3.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
"@vitejs/plugin-vue": "^
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"vite
|
|
33
|
-
"
|
|
34
|
-
"vue
|
|
30
|
+
"@vitejs/plugin-vue": "^6.0.10",
|
|
31
|
+
"@vue/language-core": "^3.3.13",
|
|
32
|
+
"typescript": "^7.0.2",
|
|
33
|
+
"vite": "^8.3.5",
|
|
34
|
+
"vite-plugin-dts": "^5.1.2",
|
|
35
|
+
"vue": "^3.5.44"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
38
|
"2048",
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
},
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "vite build --mode lib",
|
|
62
|
-
"
|
|
63
|
+
"dev": "vite build --mode lib --watch",
|
|
64
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
63
65
|
}
|
|
64
66
|
}
|
package/dist/style.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.game-2048{--board-size: min(90vw, 520px);--gap: clamp(6px, 2.5vw, 12px);--radius: clamp(4px, 1.2vw, 8px);--bg-board: #bbada0;--bg-empty: rgba(238, 228, 218, .35);--text-dark: #776e65;--text-light: #f9f6f2;--accent: #8f7a66;display:inline-flex;flex-direction:column;gap:var(--gap);font-family:Helvetica Neue,Segoe UI,system-ui,-apple-system,sans-serif;color:var(--text-dark);-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.game-2048-header{display:flex;align-items:center;justify-content:space-between;gap:var(--gap);width:var(--board-size);max-width:90vw}.game-2048-title{margin:0;font-size:clamp(28px,8vw,44px);font-weight:800;letter-spacing:-1px;color:var(--text-dark)}.game-2048-scores{display:flex;gap:6px}.game-2048-score-box{position:relative;min-width:74px;padding:6px 12px 8px;border-radius:var(--radius);background:var(--bg-board);color:var(--text-light);text-align:center;font-variant-numeric:tabular-nums}.game-2048-score-box .label{display:block;font-size:11px;letter-spacing:1px;color:#eee4da;text-transform:uppercase}.game-2048-score-box .value{display:block;font-size:22px;font-weight:700;line-height:1.2}.game-2048-score-box .delta{position:absolute;top:4px;right:8px;color:#776e65;font-weight:700;font-size:14px;animation:game-2048-score-pop .7s ease-out forwards;pointer-events:none}@keyframes game-2048-score-pop{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(-24px)}}.game-2048-toolbar{display:flex;align-items:center;justify-content:space-between;gap:var(--gap);width:var(--board-size);max-width:90vw}.game-2048-desc{margin:0;font-size:13px;line-height:1.5;color:var(--text-dark);opacity:.9}.game-2048-actions{display:flex;gap:6px;flex-shrink:0}.game-2048-button{padding:8px 14px;border:0;border-radius:var(--radius);background:var(--accent);color:var(--text-light);font:inherit;font-weight:700;font-size:14px;cursor:pointer;transition:background .12s,transform 80ms}.game-2048-button:hover:not(:disabled){background:#a68472}.game-2048-button:active:not(:disabled){transform:translateY(1px)}.game-2048-button:disabled{opacity:.4;cursor:not-allowed}.game-2048-button.is-primary{background:#f2b179;color:#776e65}.game-2048-button.is-primary:hover:not(:disabled){background:#f59563}.game-2048-board{position:relative;width:var(--board-size);max-width:90vw;aspect-ratio:1;padding:var(--gap);border-radius:var(--radius);background:var(--bg-board);touch-action:none;outline:none;box-sizing:content-box}.game-2048-board:focus-visible{box-shadow:0 0 0 3px #8f7a6680}.game-2048-grid{display:grid;width:100%;height:100%;gap:var(--gap)}.game-2048-cell{border-radius:var(--radius);background:var(--bg-empty)}.game-2048-tile-layer{position:absolute;inset:var(--gap);pointer-events:none}.game-2048-tile{--pitch-x: calc(100% + var(--gap));--pitch-y: calc(100% + var(--gap));position:absolute;top:0;left:0;display:flex;align-items:center;justify-content:center;border-radius:var(--radius);background:#eee4da;color:var(--text-dark);font-weight:800;font-size:clamp(24px,8vw,44px);line-height:1;width:calc((100% - var(--gap) * (var(--cols) - 1)) / var(--cols));height:calc((100% - var(--gap) * (var(--cols) - 1)) / var(--cols));transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y)));transition:transform .12s ease-in-out;will-change:transform}.game-2048-tile.is-new{animation:game-2048-tile-spawn .18s ease-out}.game-2048-tile.is-merged{animation:game-2048-tile-pop .18s ease-out;z-index:2}@keyframes game-2048-tile-spawn{0%{opacity:0;transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y))) scale(.3)}to{opacity:1;transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y))) scale(1)}}@keyframes game-2048-tile-pop{0%{transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y))) scale(1)}50%{transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y))) scale(1.18)}to{transform:translate(calc(var(--col) * var(--pitch-x)),calc(var(--row) * var(--pitch-y))) scale(1)}}.game-2048-tile[data-value="2"]{background:#eee4da;color:var(--text-dark)}.game-2048-tile[data-value="4"]{background:#ede0c8;color:var(--text-dark)}.game-2048-tile[data-value="8"]{background:#f2b179;color:var(--text-light)}.game-2048-tile[data-value="16"]{background:#f59563;color:var(--text-light)}.game-2048-tile[data-value="32"]{background:#f67c5f;color:var(--text-light)}.game-2048-tile[data-value="64"]{background:#f65e3b;color:var(--text-light)}.game-2048-tile[data-value="128"]{background:#edcf72;color:var(--text-light);font-size:clamp(20px,6.5vw,38px)}.game-2048-tile[data-value="256"]{background:#edcc61;color:var(--text-light);font-size:clamp(20px,6.5vw,38px)}.game-2048-tile[data-value="512"]{background:#edc850;color:var(--text-light);font-size:clamp(20px,6.5vw,38px)}.game-2048-tile[data-value="1024"]{background:#edc53f;color:var(--text-light);font-size:clamp(16px,5.5vw,32px)}.game-2048-tile[data-value="2048"]{background:#edc22e;color:var(--text-light);font-size:clamp(16px,5.5vw,32px);box-shadow:0 0 24px #f3d77499}.game-2048-tile[data-super="1"]{background:#3c3a33;color:var(--text-light);font-size:clamp(14px,5vw,28px)}.game-2048-overlay{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;border-radius:var(--radius);background:#eee4daba;color:var(--text-dark);text-align:center;opacity:0;pointer-events:none;transition:opacity .22s ease-in;z-index:10}.game-2048-overlay.is-visible{opacity:1;pointer-events:auto}.game-2048-overlay.is-lost{background:#eee4da80}.game-2048-overlay h2{margin:0;font-size:clamp(32px,9vw,52px);font-weight:800;color:var(--text-dark)}.game-2048-overlay.is-won h2{color:#f9f6f2;text-shadow:0 0 12px rgba(237,194,46,.6)}.game-2048-overlay .row{display:flex;gap:8px}.game-2048-ai-indicator{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;background:#8f7a6624;font-size:12px;color:var(--text-dark)}@media (prefers-reduced-motion: reduce){.game-2048-tile,.game-2048-overlay{transition:none}.game-2048-tile.is-new,.game-2048-tile.is-merged{animation:none}}
|