This commit is contained in:
parent
6112bcb976
commit
e5e3864964
1
.env
1
.env
@ -2,3 +2,4 @@ VITE_REQUEST_BASE_URL=https://wzpmc.cn:18080
|
||||
# VITE_REQUEST_BASE_URL=http://172.16.114.84:58082
|
||||
VITE_WEBSOCKET_BASE_URL=wss://wzpmc.cn:18080
|
||||
# VITE_WEBSOCKET_BASE_URL=ws://172.16.114.84:58082
|
||||
VITE_GAME_2048_DEBUG=true
|
||||
|
@ -9,12 +9,11 @@ module.exports = {
|
||||
'@vue/eslint-config-typescript',
|
||||
'@vue/eslint-config-prettier/skip-formatting',
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
},
|
||||
parserOptions: { ecmaVersion: 'latest' },
|
||||
rules: {
|
||||
'vue/no-unused-vars': 'warn',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/valid-v-for': 'off',
|
||||
'no-redeclare': 'off',
|
||||
},
|
||||
};
|
||||
|
1
env.d.ts
vendored
1
env.d.ts
vendored
@ -5,4 +5,5 @@ type BooleanString = 'true' | 'false';
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_REQUEST_BASE_URL: string;
|
||||
readonly VITE_WEBSOCKET_BASE_URL: string;
|
||||
readonly VITE_GAME_2048_DEBUG: BooleanString;
|
||||
}
|
||||
|
@ -767,10 +767,10 @@ onMounted(async () => {
|
||||
onUnmounted(() => {
|
||||
locked.value = true;
|
||||
isUnmounted = true;
|
||||
for (const future of chainIterables([
|
||||
for (const future of chainIterables(
|
||||
tileAddFutureMap.values(),
|
||||
tileTransitionFutureMap.values(),
|
||||
])) {
|
||||
)) {
|
||||
future.resolve();
|
||||
}
|
||||
});
|
||||
|
@ -4,22 +4,21 @@
|
||||
<div
|
||||
v-for="x in width"
|
||||
:key="`${y}-${x}`"
|
||||
:style="[getTilePositionStyle(y - 1, x - 1)]"
|
||||
:style="[getTilePositionStyle([y - 1, x - 1])]"
|
||||
class="bg-tile"
|
||||
ref="backgroundTiles"
|
||||
></div>
|
||||
</template>
|
||||
<div
|
||||
v-for="tile of tiles"
|
||||
:key="tile.id"
|
||||
v-show="tile.renderState.exists"
|
||||
class="tile"
|
||||
:data-id="`tile-${tile.id}`"
|
||||
:style="[
|
||||
getTilePositionStyle(tile.renderState.y, tile.renderState.x),
|
||||
getNumberTileStyle(tile.renderState),
|
||||
]"
|
||||
:data-tile-id="tile.id"
|
||||
:style="[getNumberTileStyle(tile.renderState)]"
|
||||
>
|
||||
{{ tile.renderState.number }}
|
||||
<div v-if="GAME_2048_DEBUG" class="id">#{{ tile.id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -50,8 +49,183 @@
|
||||
.bg-tile {
|
||||
background-color: rgb(205, 193, 180);
|
||||
}
|
||||
.id {
|
||||
position: absolute;
|
||||
font-size: 15px;
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { GAME_2048_DEBUG } from '@/env';
|
||||
import { useGame2048Store } from '@/stores/2048';
|
||||
import { waitRef } from '@/utils';
|
||||
import { arrayIncludes, zip } from '@/utils/array';
|
||||
import { setDefault } from '@/utils/map';
|
||||
import { TableView, toPos, type Pos } from '@/utils/table-view';
|
||||
import type { Exposed } from '@/utils/types';
|
||||
import { Directions } from '@/views/Game2048PageV2.vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { sample, shuffle } from 'lodash-es';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, reactive, ref, useTemplateRef } from 'vue';
|
||||
const game2048Store = useGame2048Store();
|
||||
const { width, height } = storeToRefs(game2048Store);
|
||||
const table = new TableView<Tile>([game2048Store.height, game2048Store.width]);
|
||||
type TileRenderState = {
|
||||
number: number;
|
||||
exists: boolean;
|
||||
fromPos: Pos;
|
||||
toPos: Pos;
|
||||
showPos: Pos;
|
||||
zIndex: number;
|
||||
};
|
||||
class Tile {
|
||||
private static lastId = 0;
|
||||
readonly id: number;
|
||||
private _x!: number;
|
||||
private _y!: number;
|
||||
private _exists: boolean;
|
||||
number = 0;
|
||||
zIndex = 0;
|
||||
renderState: TileRenderState = {
|
||||
number: 0,
|
||||
exists: false,
|
||||
fromPos: [0, 0],
|
||||
toPos: [0, 0],
|
||||
showPos: [0, 0],
|
||||
zIndex: 0,
|
||||
};
|
||||
constructor() {
|
||||
this.id = Tile.lastId++;
|
||||
this._exists = false;
|
||||
this.setPos([0, 0], 'move', false);
|
||||
}
|
||||
get x() {
|
||||
return this._x;
|
||||
}
|
||||
get y() {
|
||||
return this._y;
|
||||
}
|
||||
get el() {
|
||||
return gameContainerRef.value?.querySelector(`.tile[data-tile-id="${this.id}"]`);
|
||||
}
|
||||
get exists() {
|
||||
return this._exists;
|
||||
}
|
||||
set exists(value) {
|
||||
if (!value && 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;
|
||||
}
|
||||
}
|
||||
setPos([y, x]: Pos, type: 'move' | 'show', changeTable: boolean) {
|
||||
if (changeTable && table.get(this.y, this.x) === this) {
|
||||
table.delete(this.y, this.x);
|
||||
}
|
||||
this._y = y;
|
||||
this._x = x;
|
||||
if (changeTable) {
|
||||
table.set(y, x, this);
|
||||
}
|
||||
if (type === 'move') {
|
||||
this.renderState.fromPos = this.renderState.toPos;
|
||||
this.renderState.toPos = [this.y, this.x];
|
||||
} else {
|
||||
this.renderState.showPos = [this.y, this.x];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
finish() {
|
||||
this.el?.getAnimations().forEach((animation) => animation.finish());
|
||||
return this;
|
||||
}
|
||||
async show(fromOthers: boolean) {
|
||||
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;
|
||||
}
|
||||
toString() {
|
||||
return `Tile#${this.id}`;
|
||||
}
|
||||
}
|
||||
const tiles = reactive<Exposed<Tile>[]>(
|
||||
Array.from({ length: game2048Store.width * game2048Store.height }, () => new Tile()),
|
||||
);
|
||||
const idleTiles = new Set(tiles);
|
||||
const createShowKeyframeCommon = (
|
||||
pos: Pos | undefined,
|
||||
{ scale = 0, opacity = true }: { scale?: number; opacity?: boolean } = {},
|
||||
) => {
|
||||
const posStyle = pos ? getTilePositionStyle(pos) : {};
|
||||
return [
|
||||
{ opacity: opacity ? 0 : 1, transform: `scale(${scale})`, ...posStyle },
|
||||
{ opacity: 1, transform: 'none', ...posStyle },
|
||||
];
|
||||
};
|
||||
const createKeyframeOptions = ({
|
||||
duration,
|
||||
delay,
|
||||
...options
|
||||
}: KeyframeAnimationOptions): KeyframeAnimationOptions => ({
|
||||
easing: 'ease',
|
||||
fill: 'both',
|
||||
duration: !breaking ? duration : 0,
|
||||
delay: !breaking ? delay : 0,
|
||||
...options,
|
||||
});
|
||||
const tileWidth = 105;
|
||||
const borderWidth = 15;
|
||||
const tileWidthStyle = `${tileWidth}px`;
|
||||
const gameContainerStyle = {
|
||||
minWidth: `${width.value * tileWidth + (width.value + 1) * borderWidth}px`,
|
||||
minHeight: `${height.value * tileWidth + (height.value + 1) * borderWidth}px`,
|
||||
};
|
||||
const tileBackgroundColorMapping: Record<number, string | undefined> = {
|
||||
2: 'rgb(238, 228, 218)',
|
||||
4: 'rgb(237, 224, 200)',
|
||||
@ -65,277 +239,49 @@ const tileBackgroundColorMapping: Record<number, string | undefined> = {
|
||||
1024: 'rgb(237, 197, 63)',
|
||||
2048: 'rgb(237, 194, 46)',
|
||||
};
|
||||
namespace TableView {
|
||||
export type GenKey = (first: number, second: number, xFirst: boolean) => string;
|
||||
export type Pos = [y: number, x: number];
|
||||
}
|
||||
class TableView<T> {
|
||||
static toPos(first: number, second: number, xFirst: boolean) {
|
||||
return xFirst ? [second, first] : [first, second];
|
||||
}
|
||||
private map = new Map<string, T>();
|
||||
constructor(
|
||||
private genKey: TableView.GenKey,
|
||||
public size: TableView.Pos,
|
||||
) {}
|
||||
get(first: number, second: number, xFirst: boolean = false) {
|
||||
return this.map.get(this.genKey(first, second, xFirst));
|
||||
}
|
||||
set(first: number, second: number, value: T, xFirst: boolean = false) {
|
||||
this.map.set(this.genKey(first, second, xFirst), value);
|
||||
}
|
||||
delete(first: number, second: number, xFirst: boolean = false) {
|
||||
this.map.delete(this.genKey(first, second, xFirst));
|
||||
}
|
||||
getLine(first: number, xFirst: boolean = false) {
|
||||
return new LineView(this, first, xFirst);
|
||||
}
|
||||
*iter(xFirst: boolean = false) {
|
||||
for (let i = 0; i < this.size[xFirst ? 1 : 0]; i++) {
|
||||
yield this.getLine(i, xFirst);
|
||||
}
|
||||
}
|
||||
toArray(xFirst: boolean = false) {
|
||||
return Array.from(this.iter(xFirst).map((line) => line.toArray()));
|
||||
}
|
||||
clear() {
|
||||
this.map.clear();
|
||||
}
|
||||
getEmptyPositions() {
|
||||
const emptyPositions: TableView.Pos[] = [];
|
||||
for (const [y, line] of entries(this.iter())) {
|
||||
for (const [x, tile] of entries(line)) {
|
||||
if (tile !== undefined) continue;
|
||||
emptyPositions.push([y, x]);
|
||||
}
|
||||
}
|
||||
return emptyPositions;
|
||||
}
|
||||
}
|
||||
class LineView<T> {
|
||||
constructor(
|
||||
private table: TableView<T>,
|
||||
private readonly first: number,
|
||||
private readonly xFirst: boolean,
|
||||
) {}
|
||||
get(second: number) {
|
||||
return this.table.get(this.first, second, this.xFirst);
|
||||
}
|
||||
set(second: number, value: T) {
|
||||
this.table.set(this.first, second, value, this.xFirst);
|
||||
}
|
||||
delete(second: number) {
|
||||
this.table.delete(this.first, second, this.xFirst);
|
||||
}
|
||||
get size() {
|
||||
return this.table.size[this.xFirst ? 0 : 1];
|
||||
}
|
||||
toArray() {
|
||||
return Array.from(this);
|
||||
}
|
||||
*[Symbol.iterator]() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
yield this.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
const showKeyframesEase = (scale: number = 0) => [
|
||||
{
|
||||
opacity: 0,
|
||||
transform: `scale(${scale})`,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: 'none',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { useGame2048Store } from '@/stores/2048';
|
||||
import { arrayIncludes, entries, zip } from '@/utils/array';
|
||||
import type { Exposed } from '@/utils/types';
|
||||
import { Directions } from '@/views/Game2048PageV2.vue';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { sample, shuffle } from 'lodash-es';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, reactive, ref, useTemplateRef } from 'vue';
|
||||
const game2048Store = useGame2048Store();
|
||||
const { width, height } = storeToRefs(game2048Store);
|
||||
const table = new TableView<Tile>(
|
||||
(first, second, xFirst) => (xFirst ? `${first}-${second}` : `${second}-${first}`),
|
||||
[game2048Store.width, game2048Store.height],
|
||||
);
|
||||
type TileState = {
|
||||
number: number;
|
||||
x: number;
|
||||
y: number;
|
||||
exists: boolean;
|
||||
};
|
||||
class Tile implements TileState {
|
||||
private static lastId = 0;
|
||||
readonly id: number;
|
||||
private _x!: number;
|
||||
private _y!: number;
|
||||
number: number = 0;
|
||||
renderState!: TileState;
|
||||
_exists!: boolean;
|
||||
toString() {
|
||||
return `Tile#${this.id}`;
|
||||
}
|
||||
constructor() {
|
||||
this.id = Tile.lastId++;
|
||||
this.x = -1;
|
||||
this.y = -1;
|
||||
this.syncAllState();
|
||||
}
|
||||
get x() {
|
||||
return this._x;
|
||||
}
|
||||
set x(value) {
|
||||
table.delete(this.y, this.x);
|
||||
this._x = value;
|
||||
table.set(this.y, this.x, this);
|
||||
}
|
||||
get y() {
|
||||
return this._y;
|
||||
}
|
||||
set y(value) {
|
||||
table.delete(this.y, this.x);
|
||||
this._y = value;
|
||||
table.set(this.y, this.x, this);
|
||||
}
|
||||
get el() {
|
||||
return gameContainerRef.value?.querySelector(`.tile[data-id="tile-${this.id}"]`);
|
||||
}
|
||||
get exists() {
|
||||
return this._exists;
|
||||
}
|
||||
set exists(value) {
|
||||
this._exists = value;
|
||||
if (value) {
|
||||
idleTiles.delete(this);
|
||||
} else {
|
||||
idleTiles.add(this);
|
||||
table.delete(this.y, this.x);
|
||||
}
|
||||
}
|
||||
syncAllState() {
|
||||
this.renderState = {
|
||||
number: this.number,
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
exists: this._exists,
|
||||
};
|
||||
}
|
||||
finish() {
|
||||
this.el?.getAnimations().forEach((animation) => animation.finish());
|
||||
}
|
||||
async show(fromOthers: boolean) {
|
||||
this.finish();
|
||||
this._exists = true;
|
||||
idleTiles.delete(this);
|
||||
this.syncAllState();
|
||||
await this.el?.animate(
|
||||
fromOthers
|
||||
? [
|
||||
{
|
||||
opacity: 0,
|
||||
transform: `scale(0.5)`,
|
||||
},
|
||||
{
|
||||
transform: `scale(1.1)`,
|
||||
offset: 0.8,
|
||||
},
|
||||
{
|
||||
opacity: 1,
|
||||
transform: `scale(1)`,
|
||||
},
|
||||
]
|
||||
: showKeyframesEase(),
|
||||
keyframeOptions({ duration: 150 }),
|
||||
).finished;
|
||||
}
|
||||
hide() {
|
||||
this.finish();
|
||||
this.exists = false;
|
||||
this.renderState.exists = false;
|
||||
}
|
||||
async move() {
|
||||
if (!this._exists) return;
|
||||
this.finish();
|
||||
const oldKeyframe = getTilePositionStyle(this.renderState.y, this.renderState.x);
|
||||
this.renderState.y = this.y;
|
||||
this.renderState.x = this.x;
|
||||
const newKeyframe = getTilePositionStyle(this.renderState.y, this.renderState.x);
|
||||
await this.el?.animate([oldKeyframe, newKeyframe], keyframeOptions({ duration: 100 })).finished;
|
||||
}
|
||||
}
|
||||
const tiles = reactive<Exposed<Tile>[]>(
|
||||
Array.from({ length: game2048Store.width * game2048Store.height }, () => new Tile()),
|
||||
);
|
||||
const idleTiles = new Set(tiles);
|
||||
const keyframeOptions = ({
|
||||
duration,
|
||||
delay,
|
||||
...options
|
||||
}: KeyframeAnimationOptions): KeyframeAnimationOptions => ({
|
||||
easing: 'ease',
|
||||
fill: 'forwards',
|
||||
duration: animate.value ? duration : 0,
|
||||
delay: animate.value ? delay : 0,
|
||||
...options,
|
||||
});
|
||||
const tileWidth = 105;
|
||||
const borderWidth = 15;
|
||||
const tileWidthStyle = `${tileWidth}px`;
|
||||
const gameContainerStyle = {
|
||||
minWidth: `${width.value * tileWidth + (width.value + 1) * borderWidth}px`,
|
||||
minHeight: `${height.value * tileWidth + (height.value + 1) * borderWidth}px`,
|
||||
};
|
||||
const getTilePositionStyle = (y: number, x: number) => ({
|
||||
const getTilePositionStyle = ([y, x]: Pos) => ({
|
||||
left: `${borderWidth + x * (tileWidth + borderWidth)}px`,
|
||||
top: `${borderWidth + y * (tileWidth + borderWidth)}px`,
|
||||
});
|
||||
const getNumberTileStyle = ({ number }: TileState) => ({
|
||||
const getNumberTileStyle = ({ number }: TileRenderState) => ({
|
||||
fontSize: `${number < 128 ? 55 : number < 1024 ? 45 : 35}px`,
|
||||
color: number < 8 ? 'rgb(119, 110, 101)' : 'white',
|
||||
backgroundColor: tileBackgroundColorMapping[number] ?? 'rgb(56, 56, 56)',
|
||||
});
|
||||
const gameContainerRef = useTemplateRef('gameContainer');
|
||||
const backgroundTilesRef = useTemplateRef('backgroundTiles');
|
||||
const addRandomTiles = async (count: number) => {
|
||||
const addRandomTiles = (count: number) => {
|
||||
const emptyPositions = shuffle(table.getEmptyPositions());
|
||||
const addedTiles: Exposed<Tile>[] = [];
|
||||
for (const [, tile] of zip(Array(count), Array.from(idleTiles))) {
|
||||
const pos = emptyPositions.pop();
|
||||
if (!pos) break;
|
||||
tile.number = sample([2, 4]);
|
||||
[tile.y, tile.x] = pos;
|
||||
tile.setPos(pos, 'show', true);
|
||||
addedTiles.push(tile);
|
||||
}
|
||||
await Promise.all(addedTiles.map((t) => t.show(false)));
|
||||
};
|
||||
const init = async () => {
|
||||
await addRandomTiles(8);
|
||||
console.log(table.toArray());
|
||||
return addedTiles;
|
||||
};
|
||||
onMounted(async () => {
|
||||
await Promise.all(
|
||||
[
|
||||
gameContainerRef.value?.animate(showKeyframesEase(0.95), keyframeOptions({ duration: 500 })),
|
||||
gameContainerRef.value?.animate(
|
||||
createShowKeyframeCommon(undefined, { scale: 0.95 }),
|
||||
createKeyframeOptions({ duration: 500 }),
|
||||
),
|
||||
backgroundTilesRef.value?.map((el, i) => {
|
||||
const x = i % width.value,
|
||||
y = Math.floor(i / width.value);
|
||||
return el.animate(
|
||||
showKeyframesEase(),
|
||||
keyframeOptions({ duration: 500, delay: (x + y) * 100 }),
|
||||
createShowKeyframeCommon(undefined),
|
||||
createKeyframeOptions({ duration: 500, delay: (x + y) * 100 }),
|
||||
);
|
||||
}),
|
||||
]
|
||||
.flat()
|
||||
.map((a) => a?.finished),
|
||||
);
|
||||
init();
|
||||
await Promise.all(addRandomTiles(2).map((t) => t.show(false)));
|
||||
});
|
||||
const firstIndices = (d: Directions) =>
|
||||
Array.from(
|
||||
@ -384,54 +330,81 @@ const secondIndices = (d: Directions) =>
|
||||
}
|
||||
})(),
|
||||
);
|
||||
const animate = ref(true);
|
||||
const once = true;
|
||||
const animating = ref(false);
|
||||
let breaking = false;
|
||||
const move = async (key: Directions) => {
|
||||
if (breaking) return;
|
||||
if (animating.value) {
|
||||
breaking = true;
|
||||
tiles.forEach((t) => t.finish());
|
||||
await waitRef(animating, false);
|
||||
breaking = false;
|
||||
}
|
||||
animating.value = true;
|
||||
const first = Array.from(firstIndices(key)),
|
||||
second = Array.from(secondIndices(key).toReversed()),
|
||||
xFirst = arrayIncludes([Directions.UP, Directions.DOWN], key);
|
||||
const tilesToMove = new Set<Tile>(),
|
||||
tilesToShow = new Set<Tile>(),
|
||||
tilesToHide = new Set<Tile>();
|
||||
console.log(table.toArray());
|
||||
const tilesToShow = new Set<Tile>(),
|
||||
tilesToHide = new Set<Tile>(),
|
||||
tilesToMove = new Set<Tile>(),
|
||||
relationMap = new Map<Tile, Tile[]>();
|
||||
let changed = false;
|
||||
let finished: boolean;
|
||||
for (const f of first) {
|
||||
let prev: [s: number, Tile] | undefined,
|
||||
changed = false,
|
||||
finished: boolean;
|
||||
let prev: Tile | undefined;
|
||||
do {
|
||||
finished = true;
|
||||
for (const s of second) {
|
||||
const tile = table.get(f, s, xFirst);
|
||||
if (!tile) continue;
|
||||
if (!prev) {
|
||||
prev = [s, tile];
|
||||
prev = tile;
|
||||
continue;
|
||||
}
|
||||
const [precSecond, prevTile] = prev;
|
||||
const prevTile = prev;
|
||||
if (prevTile.number === tile.number) {
|
||||
finished = false;
|
||||
changed = true;
|
||||
tile.exists = false;
|
||||
prevTile.number *= 2;
|
||||
tilesToMove.add(tile);
|
||||
tilesToShow.add(prevTile);
|
||||
tilesToHide.add(tile);
|
||||
prev = once ? undefined : [s, prevTile];
|
||||
prevTile.exists = false;
|
||||
tile.number *= 2;
|
||||
setDefault(relationMap, tile, []).push(prevTile);
|
||||
tilesToShow.add(tile);
|
||||
tilesToHide.add(prevTile);
|
||||
prev = once ? undefined : tile;
|
||||
} else {
|
||||
prev = [s, tile];
|
||||
prev = tile;
|
||||
}
|
||||
}
|
||||
const dumpIndexIt = Iterator.from(second);
|
||||
for (const s of second) {
|
||||
const tile = table.get(f, s, xFirst);
|
||||
if (!tile) continue;
|
||||
}
|
||||
finished = true;
|
||||
} while (!once && !finished);
|
||||
const dumpIndexIt = Iterator.from(second);
|
||||
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!,
|
||||
pos = toPos(f, dumpIndex, xFirst);
|
||||
if (s !== dumpIndex) {
|
||||
changed = true;
|
||||
tile.zIndex = zIndex--;
|
||||
tilesToMove.add(tile);
|
||||
}
|
||||
tile.setPos(pos, 'move', true).setPos(pos, 'show', false);
|
||||
for (const other of relationMap.get(tile) ?? []) {
|
||||
other.setPos(pos, 'move', false).setPos(pos, 'show', false);
|
||||
other.zIndex = zIndex--;
|
||||
tilesToMove.add(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('add', tilesToShow);
|
||||
console.log('move', tilesToMove);
|
||||
console.log('hide', tilesToHide);
|
||||
console.log(table.toArray());
|
||||
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)),
|
||||
]);
|
||||
animating.value = false;
|
||||
};
|
||||
useEventListener(document, 'keydown', async (e) => {
|
||||
if (!arrayIncludes(Object.values(Directions), e.key)) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const DEV = import.meta.env.DEV;
|
||||
export const REQUEST_BASE_URL = import.meta.env.VITE_REQUEST_BASE_URL;
|
||||
export const WEBSOCKET_BASE_URL = import.meta.env.VITE_WEBSOCKET_BASE_URL;
|
||||
export const GAME_2048_DEBUG = import.meta.env.VITE_GAME_2048_DEBUG;
|
||||
|
@ -11,7 +11,7 @@ export function useRefresh() {
|
||||
};
|
||||
}
|
||||
|
||||
export function* chainIterables<T>(iterables: Iterable<T>[]): Generator<T, void, undefined> {
|
||||
export function* chainIterables<T>(...iterables: Iterable<T>[]): Generator<T, void, undefined> {
|
||||
for (const iterable of iterables) {
|
||||
yield* iterable;
|
||||
}
|
||||
|
5
src/utils/map.ts
Normal file
5
src/utils/map.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function setDefault<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
if (map.has(key)) return map.get(key) as V;
|
||||
map.set(key, value);
|
||||
return value;
|
||||
}
|
83
src/utils/table-view.ts
Normal file
83
src/utils/table-view.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { entries } from './array';
|
||||
export type Pos = [y: number, x: number];
|
||||
export const toPos = (first: number, second: number, xFirst: boolean): Pos =>
|
||||
xFirst ? [second, first] : [first, second];
|
||||
function key(first: number, second: number, xFirst: boolean) {
|
||||
const [y, x] = toPos(first, second, xFirst);
|
||||
return `${y}-${x}`;
|
||||
}
|
||||
export class TableView<T> {
|
||||
private map: Map<string, T>;
|
||||
size: Pos;
|
||||
constructor(size: Pos);
|
||||
constructor(tableView: TableView<T>);
|
||||
constructor(arg: Pos | TableView<T>) {
|
||||
if (arg instanceof TableView) {
|
||||
this.map = new Map(arg.map);
|
||||
this.size = [...arg.size];
|
||||
} else {
|
||||
this.map = new Map();
|
||||
this.size = [...arg];
|
||||
}
|
||||
}
|
||||
get(first: number, second: number, xFirst: boolean = false) {
|
||||
return this.map.get(key(first, second, xFirst));
|
||||
}
|
||||
set(first: number, second: number, value: T, xFirst: boolean = false) {
|
||||
this.map.set(key(first, second, xFirst), value);
|
||||
}
|
||||
delete(first: number, second: number, xFirst: boolean = false) {
|
||||
this.map.delete(key(first, second, xFirst));
|
||||
}
|
||||
getLine(first: number, xFirst: boolean = false) {
|
||||
return new LineView(this, first, xFirst);
|
||||
}
|
||||
*iter(xFirst: boolean = false) {
|
||||
for (let i = 0; i < this.size[xFirst ? 1 : 0]; i++) {
|
||||
yield this.getLine(i, xFirst);
|
||||
}
|
||||
}
|
||||
toArray(xFirst: boolean = false) {
|
||||
return Array.from(this.iter(xFirst).map((line) => line.toArray()));
|
||||
}
|
||||
clear() {
|
||||
this.map.clear();
|
||||
}
|
||||
getEmptyPositions() {
|
||||
const emptyPositions: Pos[] = [];
|
||||
for (const [y, line] of entries(this.iter())) {
|
||||
for (const [x, tile] of entries(line)) {
|
||||
if (tile !== undefined) continue;
|
||||
emptyPositions.push([y, x]);
|
||||
}
|
||||
}
|
||||
return emptyPositions;
|
||||
}
|
||||
}
|
||||
export class LineView<T> {
|
||||
constructor(
|
||||
private table: TableView<T>,
|
||||
private readonly first: number,
|
||||
private readonly xFirst: boolean,
|
||||
) {}
|
||||
get(second: number) {
|
||||
return this.table.get(this.first, second, this.xFirst);
|
||||
}
|
||||
set(second: number, value: T) {
|
||||
this.table.set(this.first, second, value, this.xFirst);
|
||||
}
|
||||
delete(second: number) {
|
||||
this.table.delete(this.first, second, this.xFirst);
|
||||
}
|
||||
get size() {
|
||||
return this.table.size[this.xFirst ? 0 : 1];
|
||||
}
|
||||
toArray() {
|
||||
return Array.from(this);
|
||||
}
|
||||
*[Symbol.iterator]() {
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
yield this.get(i);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user