diff --git a/src/components/GameBoard/GameBoard.test.ts b/src/components/GameBoard/GameBoard.test.ts
index 59ca5ad..0b14573 100644
--- a/src/components/GameBoard/GameBoard.test.ts
+++ b/src/components/GameBoard/GameBoard.test.ts
@@ -6,6 +6,8 @@ import GameBoard from "./GameBoard.vue";
vi.spyOn(document, "addEventListener");
vi.spyOn(document, "removeEventListener");
+// TODO: add more tests (unit)
+// TODO: add more tests (e2e)
describe("GameBoard", () => {
beforeEach(() => {
setActivePinia(createPinia());
diff --git a/src/components/GameBoard/GameBoard.vue b/src/components/GameBoard/GameBoard.vue
index 76629e6..f294717 100644
--- a/src/components/GameBoard/GameBoard.vue
+++ b/src/components/GameBoard/GameBoard.vue
@@ -4,7 +4,7 @@
-
+
@@ -18,8 +18,9 @@
{{ gameOverDialog.message }}
- Cancel
- New Game
+
+ New game
+ Continue playing
@@ -37,41 +38,63 @@ import TileItem from "@/components/Tile/Item/TileItem.vue";
import GameBoardHeader from "@/components/GameBoard/Header/GameBoardHeader.vue";
import GameBoardControls from "@/components/GameBoard/Controls/GameBoardControls.vue";
-import { useGridCells } from "@/composables/useGridCells";
-import { useTiles } from "@/composables/useTiles";
+import { useGridCellsStore } from "@/stores/gridCells";
+import { useTilesStore } from "@/stores/tiles";
import { useGameStateStore } from "@/stores/gameState";
import { useUserInput } from "@/composables/useUserInput";
import { generateNumArray } from "@/utils";
-const gameStateStore = useGameStateStore();
-const { resetGridCells } = useGridCells();
-const { renderedTiles, hasReachedHighestValue, addTileToCell, setRenderedTiles } = useTiles();
-const { score, bestScore, gameOverDialog, gridSize, numObstacles } = storeToRefs(gameStateStore);
-const { endGame, setCanAcceptUserInput, setScore, hideGameOverDialog } = gameStateStore;
+const { gridCells } = storeToRefs(useGridCellsStore());
+const { resetGridCells } = useGridCellsStore();
+const { renderedTiles, hasReachedHighestValue } = storeToRefs(useTilesStore());
+const { addTileToCell, setRenderedTiles } = useTilesStore();
+const { score, bestScore, gameOverDialog, gridSize, numObstacles } =
+ storeToRefs(useGameStateStore());
+const { endGame, setCanAcceptUserInput, setScore, hideGameOverDialog } = useGameStateStore();
const { handleUserInput } = useUserInput();
-function startGame() {
- // Reset game
- hideGameOverDialog();
+function startNewGame() {
setScore(0);
- setRenderedTiles([]);
resetGridCells(gridSize.value);
addTileToCell();
- setCanAcceptUserInput(true);
// Add obstacles
generateNumArray(numObstacles.value).forEach(() => addTileToCell({ isObstacle: true }));
}
+function handleNewGameClick() {
+ // Reset game
+ hideGameOverDialog();
+ setRenderedTiles([]);
+
+ startNewGame();
+}
+
watch(hasReachedHighestValue, (current) => {
if (current) {
- setCanAcceptUserInput(false);
endGame("win");
}
});
+watch(
+ () => gameOverDialog.value.show,
+ (current) => {
+ setCanAcceptUserInput(!current);
+ },
+);
+
onMounted(() => {
- startGame();
+ const canContinueSavedGame =
+ renderedTiles.value.length - numObstacles.value > 1 &&
+ renderedTiles.value.length < gridCells.value.length;
+
+ if (!canContinueSavedGame) {
+ startNewGame();
+ }
+
+ setRenderedTiles(gridCells.value.filter((cell) => cell.tile).map((cell) => cell.tile!));
+ setCanAcceptUserInput(true);
+
document.addEventListener("keyup", handleUserInput);
});
diff --git a/src/composables/useGridCells.ts b/src/composables/useGridCells.ts
deleted file mode 100644
index 944b828..0000000
--- a/src/composables/useGridCells.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { ref, computed } from "vue";
-import { generateNumArray } from "@/utils";
-
-import type { Cell, Tile } from "@/types";
-
-// Shared state
-const gridCells = ref([]);
-
-export function useGridCells() {
- const _gridCellsByColumn = computed(() => {
- return gridCells.value.reduce((acc, cell) => {
- acc[cell.col - 1] = acc[cell.col - 1] || [];
- acc[cell.col - 1][cell.row - 1] = cell;
-
- return acc;
- }, []);
- });
-
- const _gridCellsByRow = computed(() => {
- return gridCells.value.reduce((acc, cell) => {
- acc[cell.row - 1] = acc[cell.row - 1] || [];
- acc[cell.row - 1][cell.col - 1] = cell;
-
- return acc;
- }, []);
- });
-
- const gridCellsByDirection = computed>(() => ({
- ArrowLeft: _gridCellsByRow.value,
- ArrowRight: _gridCellsByRow.value.map((row) => [...row].reverse()),
- ArrowUp: _gridCellsByColumn.value,
- ArrowDown: _gridCellsByColumn.value.map((col) => [...col].reverse()),
- }));
-
- function getRandomEmptyGridCell(): Cell {
- const emptyCells = gridCells.value.filter((cell) => !cell.tile);
- const randomIndex = Math.floor(Math.random() * emptyCells.length);
-
- return emptyCells[randomIndex];
- }
-
- function resetGridCells(size: number) {
- gridCells.value = generateNumArray(Math.pow(size, 2)).map((index) => ({
- col: (index % size) + 1,
- row: Math.floor(index / size) + 1,
- }));
- }
-
- function getTilesFromGridCells() {
- return gridCells.value.reduce((acc, cell) => {
- if (cell.tile) {
- acc.push(cell.tile);
- }
-
- return acc;
- }, []);
- }
-
- return {
- gridCells,
- gridCellsByDirection,
- getRandomEmptyGridCell,
- getTilesFromGridCells,
- resetGridCells,
- };
-}
diff --git a/src/composables/useTiles.ts b/src/composables/useTiles.ts
deleted file mode 100644
index 29b9f1b..0000000
--- a/src/composables/useTiles.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { computed, ref } from "vue";
-import { useGameStateStore } from "@/stores/gameState";
-import { useGridCells } from "@/composables/useGridCells";
-
-import type { Cell, Tile } from "@/types";
-
-interface _SetTileInCellPayload {
- cell: Cell;
- tile: Partial;
- isTileToMerge?: boolean;
-}
-
-const TILE_HIGHEST_VALUE = 2048;
-
-// Shared state
-const renderedTiles = ref([]);
-
-export function useTiles() {
- const gameStateStore = useGameStateStore();
- const { getRandomEmptyGridCell } = useGridCells();
-
- const hasReachedHighestValue = computed(() => {
- return Math.max(...renderedTiles.value.map((tile) => tile.value)) >= TILE_HIGHEST_VALUE;
- });
-
- // Private methods
- function _setTileInCell({ cell, tile, isTileToMerge }: _SetTileInCellPayload) {
- cell[isTileToMerge ? "tileToMerge" : "tile"] = {
- value: tile.value ?? 2, // set default value to 2
- id: tile.id ?? crypto.randomUUID(),
- isObstacle: tile.isObstacle,
- col: cell.col,
- row: cell.row,
- };
- }
-
- async function _moveTiles(gridCellsMatrix: Cell[][]) {
- return Promise.all(
- gridCellsMatrix.flatMap((group) => {
- const promises: Promise[] = [];
-
- // Starting from the second row since the first row cannot move
- for (let i = 1; i < group.length; i++) {
- const currentCell = group[i];
-
- if (!currentCell.tile || currentCell.tile.isObstacle) continue;
-
- let lastAvailableCell: Cell | null = null;
-
- for (let j = i - 1; j >= 0; j--) {
- const targetCell = group[j];
-
- if (!canCellAcceptTile(targetCell, currentCell.tile)) break;
-
- lastAvailableCell = targetCell;
- }
-
- if (lastAvailableCell) {
- const tileElem = getTileElemById(currentCell.tile.id);
-
- if (tileElem) {
- promises.push(
- new Promise((resolve) => {
- tileElem.addEventListener("transitionend", () => resolve(), { once: true });
- }),
- );
- }
-
- _setTileInCell({
- cell: lastAvailableCell,
- tile: currentCell.tile,
- isTileToMerge: !!lastAvailableCell.tile,
- });
-
- delete currentCell.tile;
-
- const tileIds = [lastAvailableCell.tile?.id, lastAvailableCell.tileToMerge?.id];
-
- renderedTiles.value.forEach((tile) => {
- if (tileIds.includes(tile.id)) {
- tile.col = lastAvailableCell.col;
- tile.row = lastAvailableCell.row;
- }
- });
- }
- }
-
- return promises;
- }),
- );
- }
-
- function getTileElemById(id: string) {
- return document.querySelector(`[data-tile-id="${id}"]`);
- }
-
- function canCellAcceptTile(cell: Cell, tile?: Tile) {
- return (
- !cell.tile || (!cell.tile.isObstacle && !cell.tileToMerge && cell.tile.value === tile?.value)
- );
- }
-
- function canTileSlide(gridCellsMatrix: Cell[][]) {
- return gridCellsMatrix.some((column) =>
- column.some((cell, cellIndex) => {
- const targetCell = column[cellIndex - 1];
- const restrictedCell = !cellIndex || !cell.tile || cell.tile.isObstacle;
-
- return restrictedCell ? false : canCellAcceptTile(targetCell, cell.tile);
- }),
- );
- }
-
- function setRenderedTiles(tiles: Tile[]) {
- renderedTiles.value = tiles;
- }
-
- function mergeTilesInGridCells(gridCells: Cell[]) {
- gridCells
- .filter((cell) => cell.tileToMerge)
- .forEach((cell) => {
- if (cell.tile) {
- cell.tile.value *= 2;
- gameStateStore.setScore(gameStateStore.score + cell.tile.value);
- }
-
- delete cell.tileToMerge;
- });
- }
-
- async function moveTilesIfPossible(gridCellsMatrix: Cell[][]) {
- if (!canTileSlide(gridCellsMatrix)) {
- return false;
- }
-
- await _moveTiles(gridCellsMatrix);
- return true;
- }
-
- function addTileToCell({ isObstacle }: { isObstacle?: boolean } = {}) {
- const cell = getRandomEmptyGridCell();
-
- _setTileInCell({ cell, tile: { value: isObstacle ? 0 : 2, isObstacle } });
- setRenderedTiles([...renderedTiles.value, cell.tile!]);
-
- return cell;
- }
-
- return {
- renderedTiles,
- hasReachedHighestValue,
- addTileToCell,
- canCellAcceptTile,
- canTileSlide,
- moveTilesIfPossible,
- setRenderedTiles,
- mergeTilesInGridCells,
- getTileElemById,
- };
-}
diff --git a/src/composables/useUserInput.ts b/src/composables/useUserInput.ts
index 48aa23f..3f84af4 100644
--- a/src/composables/useUserInput.ts
+++ b/src/composables/useUserInput.ts
@@ -1,26 +1,25 @@
import { nextTick } from "vue";
import { storeToRefs } from "pinia";
-import { useGridCells } from "@/composables/useGridCells";
-import { useTiles } from "@/composables/useTiles";
+import { useGridCellsStore } from "@/stores/gridCells";
+import { useTilesStore } from "@/stores/tiles";
import { useGameStateStore } from "@/stores/gameState";
import type { Tile } from "@/types";
export function useUserInput() {
- const gameStateStore = useGameStateStore();
- const { gridCells, gridCellsByDirection } = useGridCells();
+ const { canAcceptUserInput } = storeToRefs(useGameStateStore());
+ const { endGame, setCanAcceptUserInput } = useGameStateStore();
+ const { gridCells, gridCellsByDirection } = storeToRefs(useGridCellsStore());
+ const { renderedTiles } = storeToRefs(useTilesStore());
const {
- renderedTiles,
addTileToCell,
canTileSlide,
moveTilesIfPossible,
setRenderedTiles,
mergeTilesInGridCells,
getTileElemById,
- } = useTiles();
- const { canAcceptUserInput } = storeToRefs(gameStateStore);
- const { endGame, setCanAcceptUserInput } = gameStateStore;
+ } = useTilesStore();
async function handleUserInput(event: KeyboardEvent) {
if (!canAcceptUserInput.value) return;
diff --git a/src/stores/gameState.test.ts b/src/stores/gameState.test.ts
index 61bca59..eea0b48 100644
--- a/src/stores/gameState.test.ts
+++ b/src/stores/gameState.test.ts
@@ -1,4 +1,4 @@
-import { setActivePinia, createPinia } from "pinia";
+import { setActivePinia, createPinia, storeToRefs } from "pinia";
import { useGameStateStore } from "./gameState";
describe("gameState", () => {
@@ -9,24 +9,26 @@ describe("gameState", () => {
describe("endGame", () => {
test("ends game with Win scenario", () => {
const { endGame } = useGameStateStore();
+ const { gameOverDialog } = storeToRefs(useGameStateStore());
endGame("win");
- expect(useGameStateStore().gameOverDialog.show).toBe(true);
- expect(useGameStateStore().gameOverDialog.title).toBe("Congratulations!");
- expect(useGameStateStore().gameOverDialog.message).toBe(
+ expect(gameOverDialog.value.show).toBe(true);
+ expect(gameOverDialog.value.title).toBe("Congratulations!");
+ expect(gameOverDialog.value.message).toBe(
"You've reached the 2048 tile and won the game! Do you want to keep going and aim for an even higher score?",
);
});
test("ends game with Lose scenario", () => {
const { endGame } = useGameStateStore();
+ const { gameOverDialog } = storeToRefs(useGameStateStore());
endGame("lose");
- expect(useGameStateStore().gameOverDialog.show).toBe(true);
- expect(useGameStateStore().gameOverDialog.title).toBe("Game Over");
- expect(useGameStateStore().gameOverDialog.message).toBe(
+ expect(gameOverDialog.value.show).toBe(true);
+ expect(gameOverDialog.value.title).toBe("Game Over");
+ expect(gameOverDialog.value.message).toBe(
"You've reached a full board with no possible moves left! Would you like to try again and beat your highest score?",
);
});
diff --git a/src/stores/gameState.ts b/src/stores/gameState.ts
index 12393ca..cb94c9a 100644
--- a/src/stores/gameState.ts
+++ b/src/stores/gameState.ts
@@ -23,7 +23,7 @@ const GAME_OVER_DIALOG_CONTENT: Record
export const useGameStateStore = defineStore(
"gameState",
() => {
- const gridSize = ref(6);
+ const gridSize = ref(4);
const numObstacles = ref(0);
const score = ref(0);
const bestScore = ref(0);
@@ -88,7 +88,7 @@ export const useGameStateStore = defineStore(
{
persist: {
storage: localStorage,
- pick: ["gridSize", "numObstacles", "bestScore"],
+ pick: ["gridSize", "numObstacles", "bestScore", "score"],
},
},
);
diff --git a/src/composables/useGridCells.test.ts b/src/stores/gridCells.test.ts
similarity index 70%
rename from src/composables/useGridCells.test.ts
rename to src/stores/gridCells.test.ts
index 6d78397..18a4665 100644
--- a/src/composables/useGridCells.test.ts
+++ b/src/stores/gridCells.test.ts
@@ -1,9 +1,14 @@
-import { useGridCells } from "./useGridCells";
+import { createPinia, setActivePinia, storeToRefs } from "pinia";
+import { useGridCellsStore } from "./gridCells";
+
+describe("useGridCellsStore", () => {
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ });
-describe("useGridCells", () => {
describe("gridCells", () => {
test("should generate empty grid cells", () => {
- const { gridCells } = useGridCells();
+ const { gridCells } = storeToRefs(useGridCellsStore());
expect(gridCells.value).toHaveLength(0);
});
@@ -11,18 +16,18 @@ describe("useGridCells", () => {
describe("gridCellsByDirection", () => {
beforeEach(() => {
- const { resetGridCells } = useGridCells();
+ const { resetGridCells } = useGridCellsStore();
resetGridCells(2);
});
test("should generate grid cells by direction", () => {
- const { gridCellsByDirection } = useGridCells();
+ const { gridCellsByDirection } = storeToRefs(useGridCellsStore());
expect(Object.keys(gridCellsByDirection.value)).toHaveLength(4);
});
test("should have correct values order for ArrowLeft", () => {
- const { gridCellsByDirection } = useGridCells();
+ const { gridCellsByDirection } = storeToRefs(useGridCellsStore());
expect(gridCellsByDirection.value.ArrowLeft).toEqual([
[
@@ -37,7 +42,7 @@ describe("useGridCells", () => {
});
test("should have correct values order for ArrowRight", () => {
- const { gridCellsByDirection } = useGridCells();
+ const { gridCellsByDirection } = storeToRefs(useGridCellsStore());
expect(gridCellsByDirection.value.ArrowRight).toEqual([
[
@@ -52,7 +57,7 @@ describe("useGridCells", () => {
});
test("should have correct values order for ArrowUp", () => {
- const { gridCellsByDirection } = useGridCells();
+ const { gridCellsByDirection } = storeToRefs(useGridCellsStore());
expect(gridCellsByDirection.value.ArrowUp).toEqual([
[
@@ -67,7 +72,7 @@ describe("useGridCells", () => {
});
test("should have correct values order for ArrowDown", () => {
- const { gridCellsByDirection } = useGridCells();
+ const { gridCellsByDirection } = storeToRefs(useGridCellsStore());
expect(gridCellsByDirection.value.ArrowDown).toEqual([
[
@@ -84,7 +89,9 @@ describe("useGridCells", () => {
describe("getRandomEmptyGridCell", () => {
test("should return random empty grid cell", () => {
- const { getRandomEmptyGridCell, gridCells, resetGridCells } = useGridCells();
+ const { gridCells } = storeToRefs(useGridCellsStore());
+ const { getRandomEmptyGridCell, resetGridCells } = useGridCellsStore();
+
resetGridCells(2);
const cell = getRandomEmptyGridCell();
@@ -97,7 +104,7 @@ describe("useGridCells", () => {
describe("getTilesFromGridCells", () => {
test("should return tiles from grid cells", () => {
- const { getTilesFromGridCells, resetGridCells } = useGridCells();
+ const { getTilesFromGridCells, resetGridCells } = useGridCellsStore();
resetGridCells(2);
const tiles = getTilesFromGridCells();
@@ -108,7 +115,8 @@ describe("useGridCells", () => {
describe("resetGridCells", () => {
test("should reset grid cells", () => {
- const { resetGridCells, gridCells } = useGridCells();
+ const { gridCells } = storeToRefs(useGridCellsStore());
+ const { resetGridCells } = useGridCellsStore();
resetGridCells(2);
@@ -116,7 +124,8 @@ describe("useGridCells", () => {
});
test("should overwrite grid cells", () => {
- const { resetGridCells, gridCells } = useGridCells();
+ const { gridCells } = storeToRefs(useGridCellsStore());
+ const { resetGridCells } = useGridCellsStore();
resetGridCells(2);
resetGridCells(3);
diff --git a/src/stores/gridCells.ts b/src/stores/gridCells.ts
new file mode 100644
index 0000000..634b412
--- /dev/null
+++ b/src/stores/gridCells.ts
@@ -0,0 +1,75 @@
+import { defineStore } from "pinia";
+import { computed, ref } from "vue";
+
+import { generateNumArray } from "@/utils";
+import type { Cell, Tile } from "@/types";
+
+export const useGridCellsStore = defineStore(
+ "gridCells",
+ () => {
+ const gridCells = ref([]);
+
+ const _gridCellsByColumn = computed(() => {
+ return gridCells.value.reduce((acc, cell) => {
+ acc[cell.col - 1] = acc[cell.col - 1] || [];
+ acc[cell.col - 1][cell.row - 1] = cell;
+
+ return acc;
+ }, []);
+ });
+
+ const _gridCellsByRow = computed(() => {
+ return gridCells.value.reduce((acc, cell) => {
+ acc[cell.row - 1] = acc[cell.row - 1] || [];
+ acc[cell.row - 1][cell.col - 1] = cell;
+
+ return acc;
+ }, []);
+ });
+
+ const gridCellsByDirection = computed>(() => ({
+ ArrowLeft: _gridCellsByRow.value,
+ ArrowRight: _gridCellsByRow.value.map((row) => [...row].reverse()),
+ ArrowUp: _gridCellsByColumn.value,
+ ArrowDown: _gridCellsByColumn.value.map((col) => [...col].reverse()),
+ }));
+
+ function getRandomEmptyGridCell(): Cell {
+ const emptyCells = gridCells.value.filter((cell) => !cell.tile);
+ const randomIndex = Math.floor(Math.random() * emptyCells.length);
+
+ return emptyCells[randomIndex];
+ }
+
+ function resetGridCells(size: number) {
+ gridCells.value = generateNumArray(Math.pow(size, 2)).map((index) => ({
+ col: (index % size) + 1,
+ row: Math.floor(index / size) + 1,
+ }));
+ }
+
+ function getTilesFromGridCells() {
+ return gridCells.value.reduce((acc, cell) => {
+ if (cell.tile) {
+ acc.push(cell.tile);
+ }
+
+ return acc;
+ }, []);
+ }
+
+ return {
+ gridCells,
+ gridCellsByDirection,
+ getRandomEmptyGridCell,
+ getTilesFromGridCells,
+ resetGridCells,
+ };
+ },
+ {
+ persist: {
+ storage: localStorage,
+ pick: ["gridCells"],
+ },
+ },
+);
diff --git a/src/composables/useTiles.test.ts b/src/stores/tiles.test.ts
similarity index 78%
rename from src/composables/useTiles.test.ts
rename to src/stores/tiles.test.ts
index 2305be6..b089931 100644
--- a/src/composables/useTiles.test.ts
+++ b/src/stores/tiles.test.ts
@@ -1,6 +1,6 @@
-import { setActivePinia, createPinia } from "pinia";
-import { useGridCells } from "./useGridCells";
-import { useTiles } from "./useTiles";
+import { setActivePinia, createPinia, storeToRefs } from "pinia";
+import { useGridCellsStore } from "./gridCells";
+import { useTilesStore } from "./tiles";
const mockCell = { col: 1, row: 1 };
const mockTile = { value: 2, col: 2, row: 1, id: "id-1" };
@@ -13,11 +13,11 @@ vi.mock("@/stores/gameState", () => ({
})),
}));
-describe("useTiles", () => {
+describe("useTilesStore", () => {
beforeEach(() => {
- const { setRenderedTiles } = useTiles();
-
setActivePinia(createPinia());
+
+ const { setRenderedTiles } = useTilesStore();
setRenderedTiles([]);
vi.clearAllMocks();
@@ -25,7 +25,7 @@ describe("useTiles", () => {
describe("renderedTiles", () => {
test("should be initialized with empty array", () => {
- const { renderedTiles } = useTiles();
+ const { renderedTiles } = storeToRefs(useTilesStore());
expect(renderedTiles.value).toHaveLength(0);
});
@@ -33,7 +33,7 @@ describe("useTiles", () => {
describe("getTileElemById", () => {
test("should return tile element by id", () => {
- const { getTileElemById } = useTiles();
+ const { getTileElemById } = useTilesStore();
const tileElem = document.createElement("div");
tileElem.setAttribute("data-tile-id", "test-id");
@@ -46,7 +46,7 @@ describe("useTiles", () => {
});
test("should return null if tile element not found", () => {
- const { getTileElemById } = useTiles();
+ const { getTileElemById } = useTilesStore();
expect(getTileElemById("test-id")).toBeNull();
});
@@ -54,19 +54,19 @@ describe("useTiles", () => {
describe("canCellAcceptTile", () => {
test("should return true if cell has no tile", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(canCellAcceptTile(mockCell, mockTile)).toBe(true);
});
test("should return false if cell has tile", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(canCellAcceptTile({ ...mockCell, tile: mockTile })).toBe(false);
});
test("should return false if tile in cell is obstacle", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(canCellAcceptTile({ ...mockCell, tile: { ...mockTile, isObstacle: true } })).toBe(
false,
@@ -74,7 +74,7 @@ describe("useTiles", () => {
});
test("should return false if cell has tile to merge", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(
canCellAcceptTile({ ...mockCell, tile: { ...mockTile }, tileToMerge: mockTile }, mockTile),
@@ -82,7 +82,7 @@ describe("useTiles", () => {
});
test("should return false if tile in cell has different value", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(canCellAcceptTile({ ...mockCell, tile: { ...mockTile, value: 3 } }, mockTile)).toBe(
false,
@@ -90,7 +90,7 @@ describe("useTiles", () => {
});
test("should return true if tile in cell has same value", () => {
- const { canCellAcceptTile } = useTiles();
+ const { canCellAcceptTile } = useTilesStore();
expect(canCellAcceptTile({ ...mockCell, tile: { ...mockTile, value: 2 } }, mockTile)).toBe(
true,
@@ -100,7 +100,7 @@ describe("useTiles", () => {
describe("canTileSlide", () => {
test("should return true if tile can slide", () => {
- const { canTileSlide } = useTiles();
+ const { canTileSlide } = useTilesStore();
const mockCellWithTile = { ...mockCell, tile: mockTile };
expect(
@@ -112,7 +112,7 @@ describe("useTiles", () => {
});
test("should return false if tile cannot slide", () => {
- const { canTileSlide } = useTiles();
+ const { canTileSlide } = useTilesStore();
expect(canTileSlide([[mockCell, mockCell], [mockCell]])).toBe(false);
});
@@ -120,7 +120,8 @@ describe("useTiles", () => {
describe("setRenderedTiles", () => {
test("should set rendered tiles", () => {
- const { setRenderedTiles, renderedTiles } = useTiles();
+ const { renderedTiles } = storeToRefs(useTilesStore());
+ const { setRenderedTiles } = useTilesStore();
setRenderedTiles([mockTile]);
@@ -128,7 +129,8 @@ describe("useTiles", () => {
});
test("should update rendered tiles", () => {
- const { setRenderedTiles, renderedTiles } = useTiles();
+ const { renderedTiles } = storeToRefs(useTilesStore());
+ const { setRenderedTiles } = useTilesStore();
setRenderedTiles([mockTile]);
setRenderedTiles([mockTile, mockTile]); // Update rendered tiles
@@ -139,7 +141,7 @@ describe("useTiles", () => {
describe("mergeTilesInGridCells", () => {
test("should merge tiles in grid cells", () => {
- const { mergeTilesInGridCells } = useTiles();
+ const { mergeTilesInGridCells } = useTilesStore();
const gridCells = [
{ ...mockCell, tile: { ...mockTile }, tileToMerge: { ...mockTile } },
@@ -156,7 +158,7 @@ describe("useTiles", () => {
});
test("should not merge tiles in grid cells if no tiles to merge", () => {
- const { mergeTilesInGridCells } = useTiles();
+ const { mergeTilesInGridCells } = useTilesStore();
const gridCells = [{ ...mockCell, tile: { ...mockTile } }];
@@ -169,7 +171,7 @@ describe("useTiles", () => {
describe("moveTilesIfPossible", () => {
test("should move tiles if possible", async () => {
- const { moveTilesIfPossible } = useTiles();
+ const { moveTilesIfPossible } = useTilesStore();
const gridCellsMatrix = [
[
@@ -186,7 +188,7 @@ describe("useTiles", () => {
});
test("should not move tiles if not possible", async () => {
- const { moveTilesIfPossible } = useTiles();
+ const { moveTilesIfPossible } = useTilesStore();
const gridCellsMatrix = [
[
@@ -205,19 +207,21 @@ describe("useTiles", () => {
describe("addTileToCell", () => {
beforeEach(() => {
- const { resetGridCells } = useGridCells();
+ const { resetGridCells } = useGridCellsStore();
resetGridCells(4);
});
test("should add tile to cell", () => {
- const { addTileToCell, renderedTiles } = useTiles();
+ const { renderedTiles } = storeToRefs(useTilesStore());
+ const { addTileToCell } = useTilesStore();
const cell = addTileToCell();
expect(cell.tile).toEqual(renderedTiles.value[0]);
});
test("should add obstacle tile to cell", () => {
- const { addTileToCell, renderedTiles } = useTiles();
+ const { renderedTiles } = storeToRefs(useTilesStore());
+ const { addTileToCell } = useTilesStore();
const cell = addTileToCell({ isObstacle: true });
expect(cell.tile).toEqual(renderedTiles.value[0]);
diff --git a/src/stores/tiles.ts b/src/stores/tiles.ts
new file mode 100644
index 0000000..238db48
--- /dev/null
+++ b/src/stores/tiles.ts
@@ -0,0 +1,171 @@
+import { defineStore } from "pinia";
+import { computed, ref } from "vue";
+
+import { useGameStateStore } from "@/stores/gameState";
+import { useGridCellsStore } from "@/stores/gridCells";
+
+import type { Cell, Tile } from "@/types";
+
+interface _SetTileInCellPayload {
+ cell: Cell;
+ tile: Partial;
+ isTileToMerge?: boolean;
+}
+
+const TILE_HIGHEST_VALUE = 2048;
+
+export const useTilesStore = defineStore(
+ "tiles",
+ () => {
+ const renderedTiles = ref([]);
+
+ const hasReachedHighestValue = computed(() => {
+ return Math.max(...renderedTiles.value.map((tile) => tile.value)) >= TILE_HIGHEST_VALUE;
+ });
+
+ // Private methods
+ function _setTileInCell({ cell, tile, isTileToMerge }: _SetTileInCellPayload) {
+ cell[isTileToMerge ? "tileToMerge" : "tile"] = {
+ value: tile.value ?? 2, // set default value to 2
+ id: tile.id ?? crypto.randomUUID(),
+ isObstacle: tile.isObstacle,
+ col: cell.col,
+ row: cell.row,
+ };
+ }
+
+ async function _moveTiles(gridCellsMatrix: Cell[][]) {
+ return Promise.all(
+ gridCellsMatrix.flatMap((group) => {
+ const promises: Promise[] = [];
+
+ // Starting from the second row since the first row cannot move
+ for (let i = 1; i < group.length; i++) {
+ const currentCell = group[i];
+
+ if (!currentCell.tile || currentCell.tile.isObstacle) continue;
+
+ let lastAvailableCell: Cell | null = null;
+
+ for (let j = i - 1; j >= 0; j--) {
+ const targetCell = group[j];
+
+ if (!canCellAcceptTile(targetCell, currentCell.tile)) break;
+
+ lastAvailableCell = targetCell;
+ }
+
+ if (lastAvailableCell) {
+ const tileElem = getTileElemById(currentCell.tile.id);
+
+ if (tileElem) {
+ promises.push(
+ new Promise((resolve) => {
+ tileElem.addEventListener("transitionend", () => resolve(), { once: true });
+ }),
+ );
+ }
+
+ _setTileInCell({
+ cell: lastAvailableCell,
+ tile: currentCell.tile,
+ isTileToMerge: !!lastAvailableCell.tile,
+ });
+
+ delete currentCell.tile;
+
+ const tileIds = [lastAvailableCell.tile?.id, lastAvailableCell.tileToMerge?.id];
+
+ renderedTiles.value.forEach((tile) => {
+ if (tileIds.includes(tile.id)) {
+ tile.col = lastAvailableCell.col;
+ tile.row = lastAvailableCell.row;
+ }
+ });
+ }
+ }
+
+ return promises;
+ }),
+ );
+ }
+
+ function getTileElemById(id: string) {
+ return document.querySelector(`[data-tile-id="${id}"]`);
+ }
+
+ function canCellAcceptTile(cell: Cell, tile?: Tile) {
+ return (
+ !cell.tile ||
+ (!cell.tile.isObstacle && !cell.tileToMerge && cell.tile.value === tile?.value)
+ );
+ }
+
+ function canTileSlide(gridCellsMatrix: Cell[][]) {
+ return gridCellsMatrix.some((column) =>
+ column.some((cell, cellIndex) => {
+ const targetCell = column[cellIndex - 1];
+ const restrictedCell = !cellIndex || !cell.tile || cell.tile.isObstacle;
+
+ return restrictedCell ? false : canCellAcceptTile(targetCell, cell.tile);
+ }),
+ );
+ }
+
+ function setRenderedTiles(tiles: Tile[]) {
+ renderedTiles.value = tiles;
+ }
+
+ function mergeTilesInGridCells(gridCells: Cell[]) {
+ const gameStateStore = useGameStateStore();
+
+ gridCells
+ .filter((cell) => cell.tileToMerge)
+ .forEach((cell) => {
+ if (cell.tile) {
+ cell.tile.value *= 2;
+ gameStateStore.setScore(gameStateStore.score + cell.tile.value);
+ }
+
+ delete cell.tileToMerge;
+ });
+ }
+
+ async function moveTilesIfPossible(gridCellsMatrix: Cell[][]) {
+ if (!canTileSlide(gridCellsMatrix)) {
+ return false;
+ }
+
+ await _moveTiles(gridCellsMatrix);
+ return true;
+ }
+
+ function addTileToCell({ isObstacle }: { isObstacle?: boolean } = {}) {
+ const gridCellsStore = useGridCellsStore();
+ const cell = gridCellsStore.getRandomEmptyGridCell();
+
+ _setTileInCell({ cell, tile: { value: isObstacle ? 0 : 2, isObstacle } });
+ setRenderedTiles([...renderedTiles.value, cell.tile!]);
+
+ return cell;
+ }
+
+ return {
+ renderedTiles,
+ hasReachedHighestValue,
+ addTileToCell,
+ canCellAcceptTile,
+ canTileSlide,
+ moveTilesIfPossible,
+ setRenderedTiles,
+ mergeTilesInGridCells,
+ getTileElemById,
+ };
+ },
+ {
+ persist: {
+ storage: localStorage,
+ pick: ["renderedTiles"],
+ },
+ },
+);
diff --git a/src/utils/generateNumArray.test.ts b/src/utils/generateNumArray.test.ts
new file mode 100644
index 0000000..b50eedb
--- /dev/null
+++ b/src/utils/generateNumArray.test.ts
@@ -0,0 +1,17 @@
+import { generateNumArray } from "./generateNumArray";
+
+describe("generateNumArray", () => {
+ test("should generate an array of numbers from 0 to length - 1", () => {
+ const length = 5;
+ const result = generateNumArray(length);
+
+ expect(result).toEqual([0, 1, 2, 3, 4]);
+ });
+
+ test("should generate an empty array if length is 0", () => {
+ const length = 0;
+ const result = generateNumArray(length);
+
+ expect(result).toEqual([]);
+ });
+});
| | | | | |