255 lines
6.4 KiB
Vue
255 lines
6.4 KiB
Vue
<script setup lang="ts">
|
|
// TODO: store state in local storage
|
|
|
|
const store = useSettingsStore();
|
|
const confirmationModal = useConfirmationModal();
|
|
const { assets } = useAssets();
|
|
const { onRender } = useScreen();
|
|
|
|
const handleActivateB = () => {
|
|
store.closeSubMenu();
|
|
};
|
|
|
|
const handleActivateA = () => {
|
|
if (isDead()) {
|
|
resetBoard();
|
|
confirmationModal.close();
|
|
return;
|
|
}
|
|
confirmationModal.open({
|
|
text: "Restart game?",
|
|
onConfirm: () => {
|
|
resetBoard();
|
|
confirmationModal.close();
|
|
},
|
|
onCancel: () => {
|
|
confirmationModal.close();
|
|
},
|
|
});
|
|
};
|
|
|
|
const TILE_COLORS: Record<number, { bg: string; fg: string }> = {
|
|
[0]: { bg: "#ccc0b4", fg: "#776e65" },
|
|
[2]: { bg: "#eee4db", fg: "#776e65" },
|
|
[4]: { bg: "#eee0cb", fg: "#776e65" },
|
|
[8]: { bg: "#f3b27a", fg: "#f9f6f2" },
|
|
[16]: { bg: "#f69664", fg: "#f9f6f2" },
|
|
[32]: { bg: "#f67c5f", fg: "#f9f6f2" },
|
|
[64]: { bg: "#f7603c", fg: "#f9f6f2" },
|
|
[128]: { bg: "#ecd072", fg: "#f9f6f2" },
|
|
[256]: { bg: "#eecc62", fg: "#f9f6f2" },
|
|
[512]: { bg: "#eec950", fg: "#f9f6f2" },
|
|
[1024]: { bg: "#edc53f", fg: "#f9f6f2" },
|
|
[2048]: { bg: "#edc12e", fg: "#f9f6f2" },
|
|
[4046]: { bg: "#3c3a33", fg: "#f9f6f2" },
|
|
};
|
|
const LAST_TILE_COLOR =
|
|
Object.values(TILE_COLORS)[Object.values(TILE_COLORS).length - 1]!;
|
|
const TILE_SIZE = 28;
|
|
|
|
const BORDER_COLOR = "#bbada0";
|
|
const BORDER_SIZE = 3;
|
|
|
|
const BOARD_X = 64;
|
|
const BOARD_Y = 32;
|
|
const BOARD_SIZE = 4;
|
|
|
|
const board = Array.from({ length: BOARD_SIZE }, () =>
|
|
Array.from({ length: BOARD_SIZE }, () => 0),
|
|
);
|
|
|
|
onRender((ctx) => {
|
|
assets.images.home.topScreen.background.draw(ctx, 0, 0);
|
|
|
|
ctx.font = "10px NDS10";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.translate(BOARD_X, BOARD_Y);
|
|
|
|
ctx.fillStyle = BORDER_COLOR;
|
|
ctx.fillRect(
|
|
0,
|
|
0,
|
|
BORDER_SIZE * (BOARD_SIZE + 1) + TILE_SIZE * BOARD_SIZE,
|
|
BORDER_SIZE * (BOARD_SIZE + 1) + TILE_SIZE * BOARD_SIZE,
|
|
);
|
|
|
|
const renderTile = (col: number, row: number) => {
|
|
const value = board[row]![col]!;
|
|
const color = TILE_COLORS[value] ?? LAST_TILE_COLOR;
|
|
|
|
const x = BORDER_SIZE + col * (TILE_SIZE + BORDER_SIZE);
|
|
const y = BORDER_SIZE + row * (TILE_SIZE + BORDER_SIZE);
|
|
|
|
ctx.fillStyle = color.bg;
|
|
ctx.fillRect(x, y, TILE_SIZE, TILE_SIZE);
|
|
|
|
if (value === 0) return;
|
|
|
|
ctx.fillStyle = color.fg;
|
|
ctx.font = value <= 2048 ? "10px NDS10" : "7px NDS7";
|
|
fillTextHCentered(
|
|
ctx,
|
|
value.toString(),
|
|
x + 1,
|
|
y + (value <= 2048 ? 9 : 10),
|
|
TILE_SIZE,
|
|
);
|
|
};
|
|
|
|
for (let row = 0; row < BOARD_SIZE; row += 1) {
|
|
for (let col = 0; col < BOARD_SIZE; col += 1) {
|
|
renderTile(col, row);
|
|
}
|
|
}
|
|
});
|
|
|
|
const getCell = (row: number, col: number) => board[row]![col]!;
|
|
const setCell = (row: number, col: number, val: number) => {
|
|
board[row]![col] = val;
|
|
};
|
|
|
|
const compact = (rowDir: number, colDir: number) => {
|
|
const reversed = rowDir > 0 || colDir > 0;
|
|
const start = reversed ? BOARD_SIZE - 1 : 0;
|
|
const step = reversed ? -1 : 1;
|
|
|
|
for (let major = 0; major < BOARD_SIZE; major += 1) {
|
|
for (let times = 0; times < BOARD_SIZE - 1; times += 1) {
|
|
for (let minor = start; minor >= 0 && minor < BOARD_SIZE; minor += step) {
|
|
const row = rowDir !== 0 ? minor : major;
|
|
const col = colDir !== 0 ? minor : major;
|
|
const nextRow = row - rowDir;
|
|
const nextCol = col - colDir;
|
|
|
|
if (nextRow < 0 || nextRow >= BOARD_SIZE) continue;
|
|
if (nextCol < 0 || nextCol >= BOARD_SIZE) continue;
|
|
|
|
if (getCell(row, col) === 0 && getCell(nextRow, nextCol) !== 0) {
|
|
setCell(row, col, getCell(nextRow, nextCol));
|
|
setCell(nextRow, nextCol, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const merge = (rowDir: number, colDir: number) => {
|
|
const reversed = rowDir > 0 || colDir > 0;
|
|
const start = reversed ? BOARD_SIZE - 1 : 0;
|
|
const step = reversed ? -1 : 1;
|
|
|
|
for (let major = 0; major < BOARD_SIZE; major += 1) {
|
|
for (let minor = start; minor >= 0 && minor < BOARD_SIZE; minor += step) {
|
|
const row = rowDir !== 0 ? minor : major;
|
|
const col = colDir !== 0 ? minor : major;
|
|
const nextRow = row - rowDir;
|
|
const nextCol = col - colDir;
|
|
|
|
if (nextRow < 0 || nextRow >= BOARD_SIZE) continue;
|
|
if (nextCol < 0 || nextCol >= BOARD_SIZE) continue;
|
|
|
|
if (
|
|
getCell(row, col) !== 0 &&
|
|
getCell(row, col) === getCell(nextRow, nextCol)
|
|
) {
|
|
setCell(row, col, getCell(row, col) * 2);
|
|
setCell(nextRow, nextCol, 0);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const spawnTile = () => {
|
|
const empty: [number, number][] = [];
|
|
for (let row = 0; row < BOARD_SIZE; row += 1) {
|
|
for (let col = 0; col < BOARD_SIZE; col += 1) {
|
|
if (getCell(row, col) === 0) empty.push([row, col]);
|
|
}
|
|
}
|
|
if (empty.length === 0) return;
|
|
|
|
const [row, col] = empty[Math.floor(Math.random() * empty.length)]!;
|
|
setCell(row, col, Math.random() < 0.9 ? 2 : 4);
|
|
};
|
|
|
|
const resetBoard = () => {
|
|
for (let row = 0; row < BOARD_SIZE; row += 1) {
|
|
for (let col = 0; col < BOARD_SIZE; col += 1) {
|
|
setCell(row, col, 0);
|
|
}
|
|
}
|
|
|
|
spawnTile();
|
|
spawnTile();
|
|
};
|
|
|
|
const isDead = () => {
|
|
for (let row = 0; row < BOARD_SIZE; row += 1) {
|
|
for (let col = 0; col < BOARD_SIZE; col += 1) {
|
|
if (
|
|
getCell(row, col) === 0 ||
|
|
(col < BOARD_SIZE - 1 && getCell(row, col) === getCell(row, col + 1)) ||
|
|
(row < BOARD_SIZE - 1 && getCell(row, col) === getCell(row + 1, col))
|
|
)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
resetBoard();
|
|
|
|
const slide = (rowDir: number, colDir: number) => {
|
|
const before = board.map((r) => [...r]);
|
|
|
|
compact(rowDir, colDir);
|
|
merge(rowDir, colDir);
|
|
compact(rowDir, colDir);
|
|
|
|
const moved = board.some((r, i) => r.some((c, j) => c !== before[i]![j]));
|
|
if (!moved) return;
|
|
|
|
spawnTile();
|
|
if (isDead()) {
|
|
confirmationModal.open({
|
|
text: "Game Over!\nRestart?",
|
|
onConfirm: () => {
|
|
resetBoard();
|
|
confirmationModal.close();
|
|
},
|
|
onCancel: () => {
|
|
confirmationModal.close();
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
useKeyDown((key) => {
|
|
switch (key) {
|
|
case "NDS_UP":
|
|
slide(-1, 0);
|
|
break;
|
|
case "NDS_DOWN":
|
|
slide(1, 0);
|
|
break;
|
|
case "NDS_LEFT":
|
|
slide(0, -1);
|
|
break;
|
|
case "NDS_RIGHT":
|
|
slide(0, 1);
|
|
break;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<CommonButtons
|
|
:y-offset="confirmationModal.buttonsYOffset"
|
|
b-label="Cancel"
|
|
a-label="Restart"
|
|
@activate-b="handleActivateB()"
|
|
@activate-a="handleActivateA()"
|
|
/>
|
|
</template>
|