diff --git a/src/components/game2048/Game2048V2.vue b/src/components/game2048/Game2048V2.vue
index 6adedd3..76de990 100644
--- a/src/components/game2048/Game2048V2.vue
+++ b/src/components/game2048/Game2048V2.vue
@@ -12,7 +12,6 @@
([game2048Store.height, game2048Store.width]);
type TileRenderState = {
number: number;
- exists: boolean;
fromPos: Pos;
toPos: Pos;
showPos: Pos;
zIndex: number;
+ shouldMove: boolean;
+ shouldHide: boolean;
+ shouldShow: boolean;
};
class Tile {
private static lastId = 0;
- readonly id: number;
+ readonly id: number = Tile.lastId++;
private _x!: number;
private _y!: number;
- private _exists: boolean;
number = 0;
zIndex = 0;
+ fromOthers = false;
renderState: TileRenderState = {
number: 0,
- exists: false,
fromPos: [0, 0],
toPos: [0, 0],
showPos: [0, 0],
zIndex: 0,
+ shouldMove: false,
+ shouldHide: false,
+ shouldShow: false,
};
constructor() {
- this.id = Tile.lastId++;
- this._exists = false;
this.setPos([0, 0], 'move', false);
}
get x() {
@@ -111,20 +112,14 @@ class Tile {
get el() {
return gameContainerRef.value?.querySelector(`.tile[data-tile-id="${this.id}"]`);
}
- get exists() {
- return this._exists;
+ get exists(): boolean {
+ return !idleTiles.has(this);
}
- set exists(value) {
- if (!value && table.get(this.y, this.x) === this) {
+ remove() {
+ if (table.get(this.y, this.x) === this) {
table.delete(this.y, this.x);
}
- this._exists = value;
- if (value) {
- idleTiles.delete(this);
- } else {
- idleTiles.add(this);
- this.renderState.zIndex = 0;
- }
+ idleTiles.add(this);
}
setPos([y, x]: Pos, type: 'move' | 'show', changeTable: boolean) {
if (changeTable && table.get(this.y, this.x) === this) {
@@ -134,6 +129,7 @@ class Tile {
this._x = x;
if (changeTable) {
table.set(y, x, this);
+ idleTiles.delete(this);
}
if (type === 'move') {
this.renderState.fromPos = this.renderState.toPos;
@@ -147,48 +143,49 @@ class Tile {
this.el?.getAnimations().forEach((animation) => animation.finish());
return this;
}
- async show(fromOthers: boolean) {
+ async animate() {
this.finish();
- // 避免新增块和移除块复用带来的位置冲突问题,
- // 所以把设置toPos放在这
- this.renderState.toPos = [...this.renderState.showPos];
- this.exists = true;
- this.renderState.exists = true;
- this.renderState.number = this.number;
- const posStyle = getTilePositionStyle(this.renderState.showPos);
- await this.el?.animate(
- fromOthers
- ? [
- { opacity: 1, transform: `scale(0.5)`, ...posStyle },
- { transform: `scale(1.1)`, offset: 0.8 },
- { opacity: 1, transform: `scale(1)`, ...posStyle },
- ]
- : Object.assign(
- createShowKeyframeCommon(this.renderState.showPos, { opacity: false }),
- posStyle,
- ),
- createKeyframeOptions({ duration: 500 }),
- ).finished;
- return this;
- }
- hide() {
- this.finish();
- this.renderState.exists = false;
- return this;
- }
- async move() {
- this.finish();
- this.renderState.zIndex = this.zIndex;
- this.zIndex = 0;
- const ext = { zIndex: this.renderState.zIndex };
- await this.el?.animate(
- [
- Object.assign(getTilePositionStyle(this.renderState.fromPos), ext),
- Object.assign(getTilePositionStyle(this.renderState.toPos), ext),
- ],
- createKeyframeOptions({ easing: 'ease-in', duration: 500 }),
- ).finished;
- return this;
+ const { shouldMove, shouldHide, shouldShow } = this.renderState;
+ if (shouldMove) {
+ this.renderState.shouldMove = false;
+ this.renderState.zIndex = this.zIndex;
+ this.zIndex = 0;
+ const ext = { zIndex: this.renderState.zIndex };
+ await this.el?.animate(
+ [
+ Object.assign(getTilePositionStyle(this.renderState.fromPos), ext),
+ Object.assign(getTilePositionStyle(this.renderState.toPos), ext),
+ ],
+ createKeyframeOptions({ easing: 'ease-in', duration: tileMoveDuration }),
+ ).finished;
+ }
+ if (shouldHide) {
+ this.renderState.shouldHide = false;
+ await this.el?.animate({ display: 'none' }, createKeyframeOptions()).finished;
+ }
+ if (shouldShow) {
+ this.renderState.shouldShow = false;
+ this.renderState.toPos = [...this.renderState.showPos];
+ this.renderState.number = this.number;
+ await this.el?.animate({ display: 'block' }, createKeyframeOptions()).finished;
+ const posStyle = getTilePositionStyle(this.renderState.showPos);
+ await this.el?.animate(
+ this.fromOthers
+ ? [
+ { transform: `scale(0.5)`, ...posStyle },
+ { transform: `scale(1.1)`, offset: 0.8 },
+ { transform: `scale(1)`, ...posStyle },
+ ]
+ : Object.assign(
+ createShowKeyframeCommon(this.renderState.showPos, { opacity: false }),
+ posStyle,
+ ),
+ createKeyframeOptions({
+ duration: tileShowDuration,
+ delay: shouldMove ? 0 : tileMoveDuration,
+ }),
+ ).finished;
+ }
}
toString() {
return `Tile#${this.id}`;
@@ -212,7 +209,7 @@ const createKeyframeOptions = ({
duration,
delay,
...options
-}: KeyframeAnimationOptions): KeyframeAnimationOptions => ({
+}: KeyframeAnimationOptions = {}): KeyframeAnimationOptions => ({
easing: 'ease',
fill: 'both',
duration: !breaking ? duration : 0,
@@ -239,6 +236,8 @@ const tileBackgroundColorMapping: Record = {
1024: 'rgb(237, 197, 63)',
2048: 'rgb(237, 194, 46)',
};
+const tileMoveDuration = 150,
+ tileShowDuration = 150;
const getTilePositionStyle = ([y, x]: Pos) => ({
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
top: `${borderWidth + y * (tileWidth + borderWidth)}px`,
@@ -253,11 +252,13 @@ const backgroundTilesRef = useTemplateRef('backgroundTiles');
const addRandomTiles = (count: number) => {
const emptyPositions = shuffle(table.getEmptyPositions());
const addedTiles: Exposed[] = [];
- for (const [, tile] of zip(Array(count), Array.from(idleTiles))) {
+ for (const tile of Iterator.from(idleTiles).take(count)) {
const pos = emptyPositions.pop();
if (!pos) break;
tile.number = sample([2, 4]);
tile.setPos(pos, 'show', true);
+ tile.fromOthers = false;
+ tile.renderState.shouldShow = true;
addedTiles.push(tile);
}
return addedTiles;
@@ -281,7 +282,8 @@ onMounted(async () => {
.flat()
.map((a) => a?.finished),
);
- await Promise.all(addRandomTiles(2).map((t) => t.show(false)));
+ fading = false;
+ await Promise.all(addRandomTiles(2).map((t) => t.animate()));
});
const firstIndices = (d: Directions) =>
Array.from(
@@ -331,10 +333,30 @@ const secondIndices = (d: Directions) =>
})(),
);
const once = true;
+let fading = true;
const animating = ref(false);
let breaking = false;
+const canMove = () => {
+ if (table.filledSize < width.value * height.value) return true;
+ for (const xFirst of [false, true]) {
+ for (const line of table.iter(xFirst)) {
+ let prev: Tile | undefined;
+ for (const tile of Iterator.from(line).filter((t) => !!t)) {
+ if (!prev) {
+ prev = tile;
+ continue;
+ }
+ if (prev.number === tile.number) {
+ return true;
+ }
+ prev = tile;
+ }
+ }
+ }
+ return false;
+};
const move = async (key: Directions) => {
- if (breaking) return;
+ if (fading || breaking || !canMove()) return;
if (animating.value) {
breaking = true;
tiles.forEach((t) => t.finish());
@@ -345,7 +367,7 @@ const move = async (key: Directions) => {
const first = Array.from(firstIndices(key)),
second = Array.from(secondIndices(key).toReversed()),
xFirst = arrayIncludes([Directions.UP, Directions.DOWN], key);
- const tilesToShow = new Set(),
+ const tilesToReshow = new Set(),
tilesToHide = new Set(),
tilesToMove = new Set(),
relationMap = new Map();
@@ -366,10 +388,11 @@ const move = async (key: Directions) => {
if (prevTile.number === tile.number) {
finished = false;
changed = true;
- prevTile.exists = false;
+ prevTile.remove();
tile.number *= 2;
+ tile.fromOthers = true;
setDefault(relationMap, tile, []).push(prevTile);
- tilesToShow.add(tile);
+ tilesToReshow.add(tile);
tilesToHide.add(prevTile);
prev = once ? undefined : tile;
} else {
@@ -377,12 +400,12 @@ const move = async (key: Directions) => {
}
}
} while (!once && !finished);
- const dumpIndexIt = Iterator.from(second);
+ let i = 0;
let zIndex = second.length - 1;
for (const s of second) {
const tile = table.get(f, s, xFirst);
if (!tile) continue;
- const dumpIndex = dumpIndexIt.next().value!,
+ const dumpIndex = second[i++],
pos = toPos(f, dumpIndex, xFirst);
if (s !== dumpIndex) {
changed = true;
@@ -397,13 +420,19 @@ const move = async (key: Directions) => {
}
}
}
- const tilesToAdd = changed ? addRandomTiles(2) : [];
- await Promise.all(Iterator.from(tilesToMove).map((t) => t.move()));
- Iterator.from(tilesToHide).forEach((t) => t.hide());
- await Promise.all([
- ...Iterator.from(tilesToShow).map((t) => t.show(true)),
- tilesToAdd.map((t) => t.show(false)),
- ]);
+ if (tilesToMove.size) {
+ addRandomTiles(2);
+ }
+ tilesToMove.forEach((t) => {
+ t.renderState.shouldMove = true;
+ });
+ tilesToHide.forEach((t) => {
+ t.renderState.shouldHide = true;
+ });
+ tilesToReshow.forEach((t) => {
+ t.renderState.shouldShow = true;
+ });
+ await Promise.all([tiles.map((t) => t.animate())]);
animating.value = false;
};
useEventListener(document, 'keydown', async (e) => {
diff --git a/src/utils/table-view.ts b/src/utils/table-view.ts
index 2b06559..e362e4b 100644
--- a/src/utils/table-view.ts
+++ b/src/utils/table-view.ts
@@ -20,6 +20,9 @@ export class TableView {
this.size = [...arg];
}
}
+ get filledSize() {
+ return this.map.size;
+ }
get(first: number, second: number, xFirst: boolean = false) {
return this.map.get(key(first, second, xFirst));
}