first init
This commit is contained in:
commit
9001cd7c25
17
.eslintrc.js
Normal file
17
.eslintrc.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: ["plugin:vue/vue3-recommended", "plugin:prettier/recommended"],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["vue", "@typescript-eslint", "prettier"],
|
||||||
|
rules: {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
},
|
||||||
|
};
|
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.vercel
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>鱼了个鱼</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "yulegeyu",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ant-design-vue": "^3.2.11",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"pinia": "^2.0.19",
|
||||||
|
"pinia-plugin-persistedstate": "^2.1.1",
|
||||||
|
"vue": "^3.2.37",
|
||||||
|
"vue-router": "4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/lodash": "^4.14.185",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||||
|
"@typescript-eslint/parser": "^5.23.0",
|
||||||
|
"@vitejs/plugin-vue": "^3.0.3",
|
||||||
|
"eslint": "^8.15.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"eslint-plugin-n": "^15.2.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-promise": "^6.0.0",
|
||||||
|
"eslint-plugin-vue": "^8.7.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"typescript": "^4.6.4",
|
||||||
|
"vite": "^3.0.7",
|
||||||
|
"vue-tsc": "^0.39.5"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
41
src/App.vue
Normal file
41
src/App.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<div class="content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
鱼了个鱼 ©2022 by
|
||||||
|
<a href="https://github.com/liyupi" target="_blank" style="color: #fff">
|
||||||
|
程序员鱼皮
|
||||||
|
</a>
|
||||||
|
|
|
||||||
|
<a
|
||||||
|
href="https://github.com/liyupi/yulegeyu"
|
||||||
|
target="_blank"
|
||||||
|
style="color: #fff"
|
||||||
|
>
|
||||||
|
代码开源
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<style scoped>
|
||||||
|
#app {
|
||||||
|
padding: 16px 16px 50px;
|
||||||
|
background: url("assets/bg.jpeg");
|
||||||
|
height: 100vh;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
</style>
|
BIN
src/assets/bg.jpeg
Normal file
BIN
src/assets/bg.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
9
src/configs/routes.ts
Normal file
9
src/configs/routes.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { RouteRecordRaw } from "vue-router";
|
||||||
|
import IndexPage from "../pages/IndexPage.vue";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
component: IndexPage,
|
||||||
|
},
|
||||||
|
] as RouteRecordRaw[];
|
368
src/core/game.ts
Normal file
368
src/core/game.ts
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* 游戏逻辑 V2(不固定 level)
|
||||||
|
*
|
||||||
|
* @author yupi https://github.com/liyupi
|
||||||
|
*/
|
||||||
|
import { useGlobalStore } from "./globalStore";
|
||||||
|
// @ts-ignore
|
||||||
|
import _ from "lodash";
|
||||||
|
import { nextTick, ref } from "vue";
|
||||||
|
|
||||||
|
const useGame = () => {
|
||||||
|
const { gameConfig } = useGlobalStore();
|
||||||
|
|
||||||
|
// 游戏状态:0 - 初始化, 1 - 进行中, 2 - 结束
|
||||||
|
const gameStatus = ref(0);
|
||||||
|
|
||||||
|
// 各层块
|
||||||
|
const levelBlocksVal = ref<BlockType[]>([]);
|
||||||
|
// 随机区块
|
||||||
|
const randomBlocksVal = ref<BlockType[][]>([]);
|
||||||
|
// 插槽区
|
||||||
|
const slotAreaVal = ref<BlockType[]>([]);
|
||||||
|
// 当前槽占用数
|
||||||
|
const currSlotNum = ref(0);
|
||||||
|
|
||||||
|
// 保存所有块(包括随机块)
|
||||||
|
const blockData: Record<number, BlockType> = {};
|
||||||
|
|
||||||
|
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
|
||||||
|
const boxWidthNum = 24;
|
||||||
|
const boxHeightNum = 24;
|
||||||
|
|
||||||
|
// 每个格子的宽高
|
||||||
|
const widthUnit = 14;
|
||||||
|
const heightUnit = 14;
|
||||||
|
|
||||||
|
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
|
||||||
|
let chessBoard: ChessBoardUnitType[][] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化指定大小的棋盘
|
||||||
|
* @param width
|
||||||
|
* @param height
|
||||||
|
*/
|
||||||
|
const initChessBoard = (width: number, height: number) => {
|
||||||
|
chessBoard = new Array(width);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
chessBoard[i] = new Array(height);
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
chessBoard[i][j] = {
|
||||||
|
blocks: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化棋盘
|
||||||
|
initChessBoard(boxWidthNum, boxHeightNum);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏初始化
|
||||||
|
*/
|
||||||
|
const initGame = () => {
|
||||||
|
console.log("initGame", gameConfig);
|
||||||
|
|
||||||
|
// 0. 设置父容器宽高
|
||||||
|
const levelBoardDom: any = document.getElementsByClassName("level-board");
|
||||||
|
levelBoardDom[0].style.width = widthUnit * boxWidthNum + "px";
|
||||||
|
levelBoardDom[0].style.height = heightUnit * boxHeightNum + "px";
|
||||||
|
|
||||||
|
// 1. 规划块数
|
||||||
|
// 块数单位(总块数必须是该值的倍数)
|
||||||
|
const blockNumUnit = gameConfig.composeNum * gameConfig.typeNum;
|
||||||
|
console.log("块数单位", blockNumUnit);
|
||||||
|
|
||||||
|
// 随机生成的总块数
|
||||||
|
const totalRandomBlockNum = gameConfig.randomBlocks.reduce((pre, curr) => {
|
||||||
|
return pre + curr;
|
||||||
|
}, 0);
|
||||||
|
console.log("随机生成的总块数", totalRandomBlockNum);
|
||||||
|
|
||||||
|
// 需要的最小块数
|
||||||
|
const minBlockNum =
|
||||||
|
gameConfig.levelNum * gameConfig.levelBlockNum + totalRandomBlockNum;
|
||||||
|
console.log("需要的最小块数", minBlockNum);
|
||||||
|
|
||||||
|
// 补齐到 blockNumUnit 的倍数
|
||||||
|
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
|
||||||
|
let totalBlockNum = minBlockNum;
|
||||||
|
if (totalBlockNum % blockNumUnit !== 0) {
|
||||||
|
totalBlockNum =
|
||||||
|
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
|
||||||
|
}
|
||||||
|
console.log("总块数", totalBlockNum);
|
||||||
|
|
||||||
|
// 2. 初始化块,随机生成块的内容
|
||||||
|
// 保存所有块的数组
|
||||||
|
const animalBlocks: string[] = [];
|
||||||
|
// 需要用到的动物数组
|
||||||
|
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
|
||||||
|
// 依次把块塞到数组里
|
||||||
|
for (let i = 0; i < totalBlockNum; i++) {
|
||||||
|
animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
|
||||||
|
}
|
||||||
|
// 打乱数组
|
||||||
|
const randomAnimalBlocks = _.shuffle(animalBlocks);
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
const allBlocks: BlockType[] = [];
|
||||||
|
for (let i = 0; i < totalBlockNum; i++) {
|
||||||
|
const newBlock = {
|
||||||
|
id: i,
|
||||||
|
status: 0,
|
||||||
|
level: 0,
|
||||||
|
type: randomAnimalBlocks[i],
|
||||||
|
higherThanBlocks: [] as BlockType[],
|
||||||
|
lowerThanBlocks: [] as BlockType[],
|
||||||
|
} as BlockType;
|
||||||
|
allBlocks.push(newBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下一个要塞入的块
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
// 3. 计算随机生成的块
|
||||||
|
const randomBlocks: BlockType[][] = [];
|
||||||
|
gameConfig.randomBlocks.forEach((randomBlock, idx) => {
|
||||||
|
randomBlocks[idx] = [];
|
||||||
|
for (let i = 0; i < randomBlock; i++) {
|
||||||
|
randomBlocks[idx].push(allBlocks[pos]);
|
||||||
|
blockData[pos] = allBlocks[pos];
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 剩余块数
|
||||||
|
let leftBlockNum = totalBlockNum - totalRandomBlockNum;
|
||||||
|
|
||||||
|
// 4. 计算有层级关系的块
|
||||||
|
const levelBlocks: BlockType[] = [];
|
||||||
|
let minX = 0;
|
||||||
|
let maxX = 22;
|
||||||
|
let minY = 0;
|
||||||
|
let maxY = 22;
|
||||||
|
// 分为 gameConfig.levelNum 批,依次生成,每批的边界不同
|
||||||
|
for (let i = 0; i < gameConfig.levelNum; i++) {
|
||||||
|
let nextBlockNum = Math.min(gameConfig.levelBlockNum, leftBlockNum);
|
||||||
|
// 最后一批,分配所有 leftBlockNum
|
||||||
|
if (i == gameConfig.levelNum - 1) {
|
||||||
|
nextBlockNum = leftBlockNum;
|
||||||
|
}
|
||||||
|
// 边界收缩
|
||||||
|
if (gameConfig.borderStep > 0) {
|
||||||
|
const dir = i % 4;
|
||||||
|
if (i > 0) {
|
||||||
|
if (dir === 0) {
|
||||||
|
minX += gameConfig.borderStep;
|
||||||
|
} else if (dir === 1) {
|
||||||
|
maxY -= gameConfig.borderStep;
|
||||||
|
} else if (dir === 2) {
|
||||||
|
minY += gameConfig.borderStep;
|
||||||
|
} else {
|
||||||
|
maxX -= gameConfig.borderStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextGenBlocks = allBlocks.slice(pos, pos + nextBlockNum);
|
||||||
|
levelBlocks.push(...nextGenBlocks);
|
||||||
|
pos = pos + nextBlockNum;
|
||||||
|
// 生成块的坐标
|
||||||
|
genLevelBlockPos(nextGenBlocks, minX, minY, maxX, maxY);
|
||||||
|
leftBlockNum -= nextBlockNum;
|
||||||
|
if (leftBlockNum <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("最终剩余块数", leftBlockNum);
|
||||||
|
|
||||||
|
// 4. 初始化空插槽
|
||||||
|
const slotArea: BlockType[] = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
console.log("随机块情况", randomBlocks);
|
||||||
|
|
||||||
|
return {
|
||||||
|
levelBlocks,
|
||||||
|
randomBlocks,
|
||||||
|
slotArea,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成一批层级块(坐标、层级关系)
|
||||||
|
* @param blocks
|
||||||
|
* @param minX
|
||||||
|
* @param minY
|
||||||
|
* @param maxX
|
||||||
|
* @param maxY
|
||||||
|
*/
|
||||||
|
const genLevelBlockPos = (
|
||||||
|
blocks: BlockType[],
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number
|
||||||
|
) => {
|
||||||
|
// 记录这批块的坐标,用于保证同批次元素不能完全重叠
|
||||||
|
const currentPosSet = new Set<string>();
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
const block = blocks[i];
|
||||||
|
// 随机生成坐标
|
||||||
|
let newPosX;
|
||||||
|
let newPosY;
|
||||||
|
let key;
|
||||||
|
while (true) {
|
||||||
|
newPosX = Math.floor(Math.random() * maxX + minX);
|
||||||
|
newPosY = Math.floor(Math.random() * maxY + minY);
|
||||||
|
key = newPosX + "," + newPosY;
|
||||||
|
// 同批次元素不能完全重叠
|
||||||
|
if (!currentPosSet.has(key)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chessBoard[newPosX][newPosY].blocks.push(block);
|
||||||
|
currentPosSet.add(key);
|
||||||
|
block.x = newPosX;
|
||||||
|
block.y = newPosY;
|
||||||
|
// 填充层级关系
|
||||||
|
genLevelRelation(block);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 给块绑定层级关系(用于确认哪些元素是当前可点击的)
|
||||||
|
* 核心逻辑:每个块压住和其坐标有交集棋盘格内所有 level 大于它的点,双向建立联系
|
||||||
|
* @param block
|
||||||
|
*/
|
||||||
|
const genLevelRelation = (block: BlockType) => {
|
||||||
|
// 确定该块附近的格子坐标范围
|
||||||
|
const minX = Math.max(block.x - 2, 0);
|
||||||
|
const minY = Math.max(block.y - 2, 0);
|
||||||
|
const maxX = Math.min(block.x + 3, boxWidthNum - 2);
|
||||||
|
const maxY = Math.min(block.y + 3, boxWidthNum - 2);
|
||||||
|
// 遍历该块附近的格子
|
||||||
|
let maxLevel = 0;
|
||||||
|
for (let i = minX; i < maxX; i++) {
|
||||||
|
for (let j = minY; j < maxY; j++) {
|
||||||
|
const relationBlocks = chessBoard[i][j].blocks;
|
||||||
|
if (relationBlocks.length > 0) {
|
||||||
|
// 取当前位置最高层的块
|
||||||
|
const maxLevelRelationBlock =
|
||||||
|
relationBlocks[relationBlocks.length - 1];
|
||||||
|
// 排除自己
|
||||||
|
if (maxLevelRelationBlock.id === block.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
maxLevel = Math.max(maxLevel, maxLevelRelationBlock.level);
|
||||||
|
block.higherThanBlocks.push(maxLevelRelationBlock);
|
||||||
|
maxLevelRelationBlock.lowerThanBlocks.push(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 比最高层的块再高一层(初始为 1)
|
||||||
|
block.level = maxLevel + 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击块事件
|
||||||
|
* @param block
|
||||||
|
* @param e
|
||||||
|
* @param randomIdx 随机区域下标,>= 0 表示点击的是随机块
|
||||||
|
*/
|
||||||
|
const doClickBlock = (block: BlockType, e: Event, randomIdx = -1) => {
|
||||||
|
// 已经输了 / 已经被点击 / 有上层块,不能再点击
|
||||||
|
if (
|
||||||
|
currSlotNum.value >= gameConfig.slotNum ||
|
||||||
|
block.status !== 0 ||
|
||||||
|
block.lowerThanBlocks.length > 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 修改元素状态为已点击
|
||||||
|
block.status = 1;
|
||||||
|
// 移除当前元素
|
||||||
|
if (randomIdx >= 0) {
|
||||||
|
// 移除所点击的随机区域的第一个元素
|
||||||
|
randomBlocksVal.value[randomIdx] = randomBlocksVal.value[randomIdx].slice(
|
||||||
|
1,
|
||||||
|
randomBlocksVal.value[randomIdx].length
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 删除节点
|
||||||
|
// @ts-ignore
|
||||||
|
e.target.remove();
|
||||||
|
// 移除覆盖关系
|
||||||
|
block.higherThanBlocks.forEach((higherThanBlock) => {
|
||||||
|
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
|
||||||
|
return lowerThanBlock.id === block.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 新元素加入插槽
|
||||||
|
let tempSlotNum = currSlotNum.value;
|
||||||
|
slotAreaVal.value[tempSlotNum] = block;
|
||||||
|
// 检查是否有可消除的
|
||||||
|
// block => 出现次数
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
// 去除空槽
|
||||||
|
const tempSlotAreaVal = slotAreaVal.value.filter(
|
||||||
|
(slotBlock) => !!slotBlock
|
||||||
|
);
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
const type = slotBlock.type;
|
||||||
|
if (!map[type]) {
|
||||||
|
map[type] = 1;
|
||||||
|
} else {
|
||||||
|
map[type]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("tempSlotAreaVal", tempSlotAreaVal);
|
||||||
|
console.log("map", map);
|
||||||
|
// 得到新数组
|
||||||
|
const newSlotAreaVal = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
tempSlotNum = 0;
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
// 成功消除(不添加到新数组中)
|
||||||
|
if (map[slotBlock.type] >= gameConfig.composeNum) {
|
||||||
|
// 块状态改为已消除
|
||||||
|
slotBlock.status = 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newSlotAreaVal[tempSlotNum++] = slotBlock;
|
||||||
|
});
|
||||||
|
slotAreaVal.value = newSlotAreaVal;
|
||||||
|
currSlotNum.value = tempSlotNum;
|
||||||
|
// 游戏结束
|
||||||
|
if (tempSlotNum >= gameConfig.slotNum) {
|
||||||
|
gameStatus.value = 2;
|
||||||
|
setTimeout(() => {
|
||||||
|
alert("你输了");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏
|
||||||
|
*/
|
||||||
|
const doStart = () => {
|
||||||
|
gameStatus.value = 0;
|
||||||
|
const { levelBlocks, randomBlocks, slotArea } = initGame();
|
||||||
|
console.log(levelBlocks, randomBlocks, slotArea);
|
||||||
|
levelBlocksVal.value = levelBlocks;
|
||||||
|
randomBlocksVal.value = randomBlocks;
|
||||||
|
slotAreaVal.value = slotArea;
|
||||||
|
gameStatus.value = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameStatus,
|
||||||
|
levelBlocksVal,
|
||||||
|
randomBlocksVal,
|
||||||
|
slotAreaVal,
|
||||||
|
widthUnit,
|
||||||
|
heightUnit,
|
||||||
|
doClickBlock,
|
||||||
|
doStart,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGame;
|
395
src/core/gameV1.ts
Normal file
395
src/core/gameV1.ts
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
/**
|
||||||
|
* 游戏逻辑 V1(划分层级生成,可能出现同层级互相覆盖的情况,故废弃)
|
||||||
|
*
|
||||||
|
* @author yupi https://github.com/liyupi
|
||||||
|
*/
|
||||||
|
import { useGlobalStore } from "./globalStore";
|
||||||
|
// @ts-ignore
|
||||||
|
import _ from "lodash";
|
||||||
|
import { nextTick, ref } from "vue";
|
||||||
|
|
||||||
|
const useGameV1 = () => {
|
||||||
|
const { gameConfig } = useGlobalStore();
|
||||||
|
|
||||||
|
// 游戏状态:0 - 初始化, 1 - 进行中, 2 - 结束
|
||||||
|
const gameStatus = ref(0);
|
||||||
|
|
||||||
|
// 各层块
|
||||||
|
const levelBlocksVal = ref<BlockType[][]>([]);
|
||||||
|
// 随机区块
|
||||||
|
const randomBlocksVal = ref<BlockType[][]>([]);
|
||||||
|
// 插槽区
|
||||||
|
const slotAreaVal = ref<BlockType[]>([]);
|
||||||
|
// 当前槽占用数
|
||||||
|
const currSlotNum = ref(0);
|
||||||
|
|
||||||
|
// 保存所有块(包括随机块)
|
||||||
|
const blockData: Record<number, BlockType> = {};
|
||||||
|
|
||||||
|
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
|
||||||
|
const boxWidthNum = 24;
|
||||||
|
const boxHeightNum = 24;
|
||||||
|
|
||||||
|
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
|
||||||
|
let chessBoard: ChessBoardUnitType[][] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化指定大小的棋盘
|
||||||
|
* @param width
|
||||||
|
* @param height
|
||||||
|
*/
|
||||||
|
const initChessBoard = (width: number, height: number) => {
|
||||||
|
chessBoard = new Array(width);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
chessBoard[i] = new Array(height);
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
chessBoard[i][j] = {
|
||||||
|
blocks: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化棋盘
|
||||||
|
initChessBoard(boxWidthNum, boxHeightNum);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏初始化
|
||||||
|
*/
|
||||||
|
const initGame = () => {
|
||||||
|
console.log("initGame", gameConfig);
|
||||||
|
|
||||||
|
// 1. 规划块数
|
||||||
|
// 块数单位(总块数必须是该值的倍数)
|
||||||
|
const blockNumUnit = gameConfig.composeNum * gameConfig.typeNum;
|
||||||
|
console.log("块数单位", blockNumUnit);
|
||||||
|
|
||||||
|
// 随机生成的总块数
|
||||||
|
const totalRandomBlockNum = gameConfig.randomBlocks.reduce((pre, curr) => {
|
||||||
|
return pre + curr;
|
||||||
|
}, 0);
|
||||||
|
console.log("随机生成的总块数", totalRandomBlockNum);
|
||||||
|
|
||||||
|
// 需要的最小块数
|
||||||
|
const minBlockNum = Math.ceil(
|
||||||
|
(gameConfig.levelNum *
|
||||||
|
(gameConfig.topBlockNum + gameConfig.minBottomBlockNum)) /
|
||||||
|
2 +
|
||||||
|
totalRandomBlockNum
|
||||||
|
);
|
||||||
|
console.log("需要的最小块数", minBlockNum);
|
||||||
|
|
||||||
|
// 补齐到 blockNumUnit 的倍数
|
||||||
|
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
|
||||||
|
let totalBlockNum = minBlockNum;
|
||||||
|
if (totalBlockNum % blockNumUnit !== 0) {
|
||||||
|
totalBlockNum =
|
||||||
|
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
|
||||||
|
}
|
||||||
|
console.log("总块数", totalBlockNum);
|
||||||
|
|
||||||
|
// 2. 随机生成块
|
||||||
|
// 保存所有块的数组
|
||||||
|
const animalBlocks: string[] = [];
|
||||||
|
// 需要用到的动物数组
|
||||||
|
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
|
||||||
|
// 依次把块塞到数组里
|
||||||
|
for (let i = 0; i < totalBlockNum; i++) {
|
||||||
|
animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
|
||||||
|
}
|
||||||
|
// 打乱数组
|
||||||
|
const randomAnimalBlocks = _.shuffle(animalBlocks);
|
||||||
|
// 下一个要塞入的块
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
// 3. 填充结果
|
||||||
|
// 计算随机生成的块
|
||||||
|
const randomBlocks: BlockType[][] = [];
|
||||||
|
gameConfig.randomBlocks.forEach((randomBlock, idx) => {
|
||||||
|
randomBlocks[idx] = [];
|
||||||
|
for (let i = 0; i < randomBlock; i++) {
|
||||||
|
const newBlock = {
|
||||||
|
id: pos,
|
||||||
|
level: i,
|
||||||
|
status: 0,
|
||||||
|
type: randomAnimalBlocks[pos],
|
||||||
|
higherThanBlocks: [] as BlockType[],
|
||||||
|
lowerThanBlocks: [] as BlockType[],
|
||||||
|
} as BlockType;
|
||||||
|
randomBlocks[idx].push(newBlock);
|
||||||
|
blockData[pos] = newBlock;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 剩余块数
|
||||||
|
let leftBlockNum = totalBlockNum - totalRandomBlockNum;
|
||||||
|
|
||||||
|
// 计算每层生成的块数
|
||||||
|
// 每层递减块数
|
||||||
|
// e.g. 最上层 38 块,最下层不小于 20 块,共 10 层,则每层递减块数为 18 / 9 = 2
|
||||||
|
const stepNum = Math.floor(
|
||||||
|
(gameConfig.topBlockNum - gameConfig.minBottomBlockNum) /
|
||||||
|
(gameConfig.levelNum - 1)
|
||||||
|
);
|
||||||
|
console.log("每层递减块数", stepNum);
|
||||||
|
|
||||||
|
// 下一层要分配的块数
|
||||||
|
let nextBlockNum = gameConfig.topBlockNum;
|
||||||
|
// 各层的块
|
||||||
|
const levelBlocks: BlockType[][] = [];
|
||||||
|
for (let i = 0; i < gameConfig.levelNum; i++) {
|
||||||
|
// 最后一层,所有的块都分配出去
|
||||||
|
if (i == gameConfig.levelNum - 1) {
|
||||||
|
nextBlockNum = leftBlockNum;
|
||||||
|
}
|
||||||
|
levelBlocks[i] = [];
|
||||||
|
// 添加新块
|
||||||
|
for (let j = 0; j < nextBlockNum; j++) {
|
||||||
|
if (pos >= totalBlockNum) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const newBlock = {
|
||||||
|
id: pos,
|
||||||
|
level: i,
|
||||||
|
status: 0,
|
||||||
|
type: randomAnimalBlocks[pos],
|
||||||
|
higherThanBlocks: [] as BlockType[],
|
||||||
|
lowerThanBlocks: [] as BlockType[],
|
||||||
|
} as BlockType;
|
||||||
|
levelBlocks[i].push(newBlock);
|
||||||
|
blockData[pos] = newBlock;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
leftBlockNum -= nextBlockNum;
|
||||||
|
nextBlockNum -= stepNum;
|
||||||
|
}
|
||||||
|
console.log("剩余块数", leftBlockNum);
|
||||||
|
|
||||||
|
// 4. 初始化空插槽
|
||||||
|
const slotArea: BlockType[] = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"各层数量",
|
||||||
|
levelBlocks
|
||||||
|
.map((levelBlock, idx) => `第${idx}层:${levelBlock.length} 块`)
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
console.log("随机块情况", randomBlocks);
|
||||||
|
|
||||||
|
return {
|
||||||
|
levelBlocks,
|
||||||
|
randomBlocks,
|
||||||
|
slotArea,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机生成块坐标
|
||||||
|
*/
|
||||||
|
const randomPos = () => {
|
||||||
|
const levelBoardDom: any = document.getElementsByClassName("level-board");
|
||||||
|
// 为方便给格子设置固定宽高,不动态计算了
|
||||||
|
// const totalWidth = levelBoardDom[0].clientWidth;
|
||||||
|
const blockDomList = document.getElementsByClassName("level-block");
|
||||||
|
// 每个格子的宽高
|
||||||
|
const widthUnit = 14;
|
||||||
|
const heightUnit = 14;
|
||||||
|
// 设置父容器宽高
|
||||||
|
levelBoardDom[0].style.width = widthUnit * boxWidthNum + "px";
|
||||||
|
levelBoardDom[0].style.height = heightUnit * boxHeightNum + "px";
|
||||||
|
// 遍历时层级递增
|
||||||
|
for (let i = 0; i < blockDomList.length; i++) {
|
||||||
|
let blockDom: any = blockDomList[i];
|
||||||
|
const blockId = blockDom.dataset.id;
|
||||||
|
const block = blockData[blockId];
|
||||||
|
blockDom.style.position = "absolute";
|
||||||
|
// 随机生成坐标,当前层级不能重复
|
||||||
|
let newPosX;
|
||||||
|
let newPosY;
|
||||||
|
while (true) {
|
||||||
|
newPosX = Math.floor(Math.random() * (boxWidthNum - 2));
|
||||||
|
newPosY = Math.floor(Math.random() * (boxHeightNum - 2));
|
||||||
|
const currChessBoardUnit = chessBoard[newPosX][newPosY];
|
||||||
|
// 同层级元素不能完全重叠
|
||||||
|
if (
|
||||||
|
currChessBoardUnit.blocks.length < 1 ||
|
||||||
|
currChessBoardUnit.blocks[currChessBoardUnit.blocks.length - 1]
|
||||||
|
.level != block.level
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chessBoard[newPosX][newPosY].blocks.push(block);
|
||||||
|
block.x = newPosX;
|
||||||
|
block.y = newPosY;
|
||||||
|
blockDom.style.left = newPosX * widthUnit + "px";
|
||||||
|
blockDom.style.top = newPosY * heightUnit + "px";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可能导致死循环,暂不使用
|
||||||
|
// /**
|
||||||
|
// * 判断某个坐标是否和其他块有重叠
|
||||||
|
// * @param x
|
||||||
|
// * @param y
|
||||||
|
// * @param block
|
||||||
|
// */
|
||||||
|
// const hasOverlap = (x: number, y: number, block: BlockType) => {
|
||||||
|
// // 确定该块附近的格子坐标范围
|
||||||
|
// const minX = Math.max(x - 2, 0);
|
||||||
|
// const minY = Math.max(y - 2, 0);
|
||||||
|
// const maxX = Math.min(x + 3, boxWidthNum - 2);
|
||||||
|
// const maxY = Math.min(y + 3, boxWidthNum - 2);
|
||||||
|
// // 遍历该块附近的格子
|
||||||
|
// for (let i = minX; i < maxX; i++) {
|
||||||
|
// for (let j = minY; j < maxY; j++) {
|
||||||
|
// const relationBlocks = chessBoard[i][j].blocks;
|
||||||
|
// for (const relationBlock of relationBlocks) {
|
||||||
|
// if (relationBlocks[relationBlocks.length - 1].level != block.level) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return true;
|
||||||
|
// };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定层级覆盖关系(用于确认哪些元素是当前可点击的)
|
||||||
|
*
|
||||||
|
* 核心逻辑:每个块压住和其坐标有交集棋盘格内所有 level 大于它的点,双向建立联系
|
||||||
|
*/
|
||||||
|
const bindLevelRelation = () => {
|
||||||
|
levelBlocksVal.value.forEach((levelBlock) => {
|
||||||
|
levelBlock.forEach((block) => {
|
||||||
|
const x = block.x;
|
||||||
|
const y = block.y;
|
||||||
|
// 确定该块附近的格子坐标范围
|
||||||
|
const minX = Math.max(x - 2, 0);
|
||||||
|
const minY = Math.max(y - 2, 0);
|
||||||
|
const maxX = Math.min(x + 3, boxWidthNum - 2);
|
||||||
|
const maxY = Math.min(y + 3, boxWidthNum - 2);
|
||||||
|
// 遍历该块附近的格子
|
||||||
|
for (let i = minX; i < maxX; i++) {
|
||||||
|
for (let j = minY; j < maxY; j++) {
|
||||||
|
const relationBlocks = chessBoard[i][j].blocks;
|
||||||
|
relationBlocks.forEach((relationBlock) => {
|
||||||
|
// 建立覆盖关系
|
||||||
|
if (relationBlock.level > block.level) {
|
||||||
|
block.higherThanBlocks.push(relationBlock);
|
||||||
|
relationBlock.lowerThanBlocks.push(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击块事件
|
||||||
|
* @param block
|
||||||
|
* @param e
|
||||||
|
* @param randomIdx 随机区域下标,>= 0 表示点击的是随机块
|
||||||
|
*/
|
||||||
|
const doClickBlock = (block: BlockType, e: Event, randomIdx = -1) => {
|
||||||
|
// 已经输了 / 已经被点击 / 有上层块,不能再点击
|
||||||
|
if (
|
||||||
|
currSlotNum.value >= gameConfig.slotNum ||
|
||||||
|
block.status !== 0 ||
|
||||||
|
block.lowerThanBlocks.length > 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 修改元素状态为已点击
|
||||||
|
block.status = 1;
|
||||||
|
// 移除当前元素
|
||||||
|
if (randomIdx >= 0) {
|
||||||
|
// 移除所点击的随机区域的第一个元素
|
||||||
|
randomBlocksVal.value[randomIdx] = randomBlocksVal.value[randomIdx].slice(
|
||||||
|
1,
|
||||||
|
randomBlocksVal.value[randomIdx].length
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 删除节点
|
||||||
|
// @ts-ignore
|
||||||
|
e.target.remove();
|
||||||
|
// 移除覆盖关系
|
||||||
|
block.higherThanBlocks.forEach((higherThanBlock) => {
|
||||||
|
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
|
||||||
|
return lowerThanBlock.id === block.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 新元素加入插槽
|
||||||
|
let tempSlotNum = currSlotNum.value;
|
||||||
|
slotAreaVal.value[tempSlotNum] = block;
|
||||||
|
// 检查是否有可消除的
|
||||||
|
// block => 出现次数
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
// 去除空槽
|
||||||
|
const tempSlotAreaVal = slotAreaVal.value.filter(
|
||||||
|
(slotBlock) => !!slotBlock
|
||||||
|
);
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
const type = slotBlock.type;
|
||||||
|
if (!map[type]) {
|
||||||
|
map[type] = 1;
|
||||||
|
} else {
|
||||||
|
map[type]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("tempSlotAreaVal", tempSlotAreaVal);
|
||||||
|
console.log("map", map);
|
||||||
|
// 得到新数组
|
||||||
|
const newSlotAreaVal = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
tempSlotNum = 0;
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
// 成功消除(不添加到新数组中)
|
||||||
|
if (map[slotBlock.type] >= gameConfig.composeNum) {
|
||||||
|
// 块状态改为已消除
|
||||||
|
slotBlock.status = 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newSlotAreaVal[tempSlotNum++] = slotBlock;
|
||||||
|
});
|
||||||
|
slotAreaVal.value = newSlotAreaVal;
|
||||||
|
currSlotNum.value = tempSlotNum;
|
||||||
|
// 游戏结束
|
||||||
|
if (tempSlotNum >= gameConfig.slotNum) {
|
||||||
|
gameStatus.value = 2;
|
||||||
|
setTimeout(() => {
|
||||||
|
alert("你输了");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏
|
||||||
|
*/
|
||||||
|
const doStart = () => {
|
||||||
|
gameStatus.value = 0;
|
||||||
|
const { levelBlocks, randomBlocks, slotArea } = initGame();
|
||||||
|
console.log(levelBlocks, randomBlocks, slotArea);
|
||||||
|
levelBlocksVal.value = levelBlocks;
|
||||||
|
randomBlocksVal.value = randomBlocks;
|
||||||
|
slotAreaVal.value = slotArea;
|
||||||
|
// 等 dom 更新后才刷新坐标
|
||||||
|
nextTick(() => {
|
||||||
|
randomPos();
|
||||||
|
bindLevelRelation();
|
||||||
|
gameStatus.value = 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameStatus,
|
||||||
|
levelBlocksVal,
|
||||||
|
randomBlocksVal,
|
||||||
|
slotAreaVal,
|
||||||
|
doClickBlock,
|
||||||
|
doStart,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGameV1;
|
74
src/core/globalStore.ts
Normal file
74
src/core/globalStore.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
const defaultGameConfig = {
|
||||||
|
// 槽容量
|
||||||
|
slotNum: 7,
|
||||||
|
// 需要多少个一样块的才能合成
|
||||||
|
composeNum: 3,
|
||||||
|
// 动物类别数
|
||||||
|
typeNum: 10,
|
||||||
|
// 每层块数(大致)
|
||||||
|
levelBlockNum: 30,
|
||||||
|
// 边界收缩步长
|
||||||
|
borderStep: 1,
|
||||||
|
// 总层数(最小为 2)
|
||||||
|
levelNum: 2,
|
||||||
|
// 最上层块数
|
||||||
|
topBlockNum: 40,
|
||||||
|
// 最下层块数最小值
|
||||||
|
minBottomBlockNum: 20,
|
||||||
|
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||||
|
randomBlocks: [8, 8],
|
||||||
|
// 动物数组
|
||||||
|
animals: [
|
||||||
|
"🐔",
|
||||||
|
"🐟",
|
||||||
|
"🦆",
|
||||||
|
"🐶",
|
||||||
|
"🐱",
|
||||||
|
"🐴",
|
||||||
|
"🐑",
|
||||||
|
"🐦",
|
||||||
|
"🐧",
|
||||||
|
"🐊",
|
||||||
|
"🐺",
|
||||||
|
"🐒",
|
||||||
|
"🐳",
|
||||||
|
"🐬",
|
||||||
|
"🐢",
|
||||||
|
"🦖",
|
||||||
|
"🦒",
|
||||||
|
"🦁",
|
||||||
|
"🐍",
|
||||||
|
"🐭",
|
||||||
|
"🐂",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局状态存储
|
||||||
|
*
|
||||||
|
* @author yupi
|
||||||
|
*/
|
||||||
|
export const useGlobalStore = defineStore("global", {
|
||||||
|
state: () => ({
|
||||||
|
gameConfig: { ...defaultGameConfig },
|
||||||
|
}),
|
||||||
|
getters: {},
|
||||||
|
// 持久化
|
||||||
|
persist: {
|
||||||
|
key: "global",
|
||||||
|
storage: window.localStorage,
|
||||||
|
beforeRestore: (context) => {
|
||||||
|
console.log("load globalStore data start");
|
||||||
|
},
|
||||||
|
afterRestore: (context) => {
|
||||||
|
console.log("load globalStore data end");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
reset() {
|
||||||
|
this.$reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
24
src/core/type.d.ts
vendored
Normal file
24
src/core/type.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 块类型
|
||||||
|
*/
|
||||||
|
interface BlockType {
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
level: number;
|
||||||
|
type: string;
|
||||||
|
// 0 - 正常, 1 - 已点击, 2 - 已消除
|
||||||
|
status: 0 | 1 | 2;
|
||||||
|
// 压住的其他块
|
||||||
|
higherThanBlocks: BlockType[];
|
||||||
|
// 被哪些块压住(为空表示可点击)
|
||||||
|
lowerThanBlocks: BlockType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个格子单元类型
|
||||||
|
*/
|
||||||
|
interface ChessBoardUnitType {
|
||||||
|
// 放到当前格子里的块(层级越高下标越大)
|
||||||
|
blocks: BlockType[];
|
||||||
|
}
|
21
src/main.ts
Normal file
21
src/main.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import Antd from "ant-design-vue";
|
||||||
|
import App from "./App.vue";
|
||||||
|
import * as VueRouter from "vue-router";
|
||||||
|
import routes from "./configs/routes";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
import "ant-design-vue/dist/antd.css";
|
||||||
|
import "./style.css";
|
||||||
|
|
||||||
|
// 路由
|
||||||
|
const router = VueRouter.createRouter({
|
||||||
|
history: VueRouter.createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
|
||||||
|
createApp(App).use(Antd).use(router).use(pinia).mount("#app");
|
119
src/pages/IndexPage.vue
Normal file
119
src/pages/IndexPage.vue
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div id="indexPage">
|
||||||
|
<a-row align="center">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
@click="doStart"
|
||||||
|
>
|
||||||
|
开始
|
||||||
|
</a-button>
|
||||||
|
<!-- 分层选块 -->
|
||||||
|
<div class="level-board">
|
||||||
|
<div
|
||||||
|
v-for="(block, idx) in levelBlocksVal"
|
||||||
|
v-show="gameStatus > 0"
|
||||||
|
:key="idx"
|
||||||
|
class="block level-block"
|
||||||
|
:class="{ disabled: block.lowerThanBlocks.length > 0 }"
|
||||||
|
:data-id="block.id"
|
||||||
|
:style="{
|
||||||
|
zIndex: 100 + block.level,
|
||||||
|
left: block.x * widthUnit + 'px',
|
||||||
|
top: block.y * heightUnit + 'px',
|
||||||
|
}"
|
||||||
|
@click="(e) => doClickBlock(block, e)"
|
||||||
|
>
|
||||||
|
{{ block.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 随机选块 -->
|
||||||
|
<div class="random-board">
|
||||||
|
<div
|
||||||
|
v-for="(randomBlock, index) in randomBlocksVal"
|
||||||
|
:key="index"
|
||||||
|
class="random-area"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="randomBlock.length > 0"
|
||||||
|
class="block"
|
||||||
|
@click="(e) => doClickBlock(randomBlock[0], e, index)"
|
||||||
|
>
|
||||||
|
{{ randomBlock[0].type }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="num in Math.max(randomBlock.length - 1, 0)"
|
||||||
|
:key="num"
|
||||||
|
class="block disabled"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 槽位 -->
|
||||||
|
<div v-if="slotAreaVal.length > 0" class="slot-board">
|
||||||
|
<div
|
||||||
|
v-for="(slotBlock, index) in slotAreaVal"
|
||||||
|
:key="index"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
{{ slotBlock?.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import useGame from "../core/game";
|
||||||
|
|
||||||
|
const {
|
||||||
|
gameStatus,
|
||||||
|
levelBlocksVal,
|
||||||
|
randomBlocksVal,
|
||||||
|
slotAreaVal,
|
||||||
|
widthUnit,
|
||||||
|
heightUnit,
|
||||||
|
doClickBlock,
|
||||||
|
doStart,
|
||||||
|
} = useGame();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.level-board {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-block {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.random-board {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.random-area {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-board {
|
||||||
|
margin-top: 24px;
|
||||||
|
border: 10px solid saddlebrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
font-size: 28px;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
line-height: 42px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background: white;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background: grey;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
507
src/pages/IndexPageV1.vue
Normal file
507
src/pages/IndexPageV1.vue
Normal file
@ -0,0 +1,507 @@
|
|||||||
|
<template>
|
||||||
|
<div id="indexPage">
|
||||||
|
<a-row align="center">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
@click="doStart"
|
||||||
|
>
|
||||||
|
开始
|
||||||
|
</a-button>
|
||||||
|
<!-- 分层选块 -->
|
||||||
|
<div class="level-board">
|
||||||
|
<div
|
||||||
|
v-for="(levelBlock, index) in levelBlocksVal"
|
||||||
|
:key="index"
|
||||||
|
class="level"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(block, idx) in levelBlock"
|
||||||
|
v-show="gameStatus > 0"
|
||||||
|
:key="idx"
|
||||||
|
class="block level-block"
|
||||||
|
:class="{ disabled: block.lowerThanBlocks.length > 0 }"
|
||||||
|
:data-id="block.id"
|
||||||
|
:style="{ zIndex: 10000 - index }"
|
||||||
|
@click="(e) => doClickBlock(block, e)"
|
||||||
|
>
|
||||||
|
{{ block.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 随机选块 -->
|
||||||
|
<div class="random-board">
|
||||||
|
<div
|
||||||
|
v-for="(randomBlock, index) in randomBlocksVal"
|
||||||
|
:key="index"
|
||||||
|
class="random-area"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="randomBlock.length > 0"
|
||||||
|
class="block"
|
||||||
|
@click="(e) => doClickBlock(randomBlock[0], e, index)"
|
||||||
|
>
|
||||||
|
{{ randomBlock[0].type }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="num in Math.max(randomBlock.length - 1, 0)"
|
||||||
|
:key="num"
|
||||||
|
class="block disabled"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 槽位 -->
|
||||||
|
<div v-if="slotAreaVal.length > 0" class="slot-board">
|
||||||
|
<div
|
||||||
|
v-for="(slotBlock, index) in slotAreaVal"
|
||||||
|
:key="index"
|
||||||
|
class="block"
|
||||||
|
>
|
||||||
|
{{ slotBlock?.type }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useGlobalStore } from "../core/globalStore";
|
||||||
|
// @ts-ignore
|
||||||
|
import _ from "lodash";
|
||||||
|
import { nextTick, ref } from "vue";
|
||||||
|
|
||||||
|
const { gameConfig } = useGlobalStore();
|
||||||
|
|
||||||
|
// 游戏状态:0 - 初始化, 1 - 进行中, 2 - 结束
|
||||||
|
const gameStatus = ref(0);
|
||||||
|
|
||||||
|
// 各层块
|
||||||
|
const levelBlocksVal = ref<BlockType[][]>([]);
|
||||||
|
// 随机区块
|
||||||
|
const randomBlocksVal = ref<BlockType[][]>([]);
|
||||||
|
// 插槽区
|
||||||
|
const slotAreaVal = ref<BlockType[]>([]);
|
||||||
|
// 当前槽占用数
|
||||||
|
const currSlotNum = ref(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 块类型
|
||||||
|
*/
|
||||||
|
interface BlockType {
|
||||||
|
id: number;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
level: number;
|
||||||
|
type: string;
|
||||||
|
// 0 - 正常, 1 - 已点击, 2 - 已消除
|
||||||
|
status: 0 | 1 | 2;
|
||||||
|
// 压住的其他块
|
||||||
|
higherThanBlocks: BlockType[];
|
||||||
|
// 被哪些块压住(为空表示可点击)
|
||||||
|
lowerThanBlocks: BlockType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个格子单元类型
|
||||||
|
*/
|
||||||
|
interface ChessBoardUnitType {
|
||||||
|
// 放到当前格子里的块(层级越高下标越小)
|
||||||
|
blocks: BlockType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存所有块(包括随机块)
|
||||||
|
const blockData: Record<number, BlockType> = {};
|
||||||
|
|
||||||
|
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
|
||||||
|
const boxWidthNum = 24;
|
||||||
|
const boxHeightNum = 24;
|
||||||
|
|
||||||
|
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
|
||||||
|
let chessBoard: ChessBoardUnitType[][] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化指定大小的棋盘
|
||||||
|
* @param width
|
||||||
|
* @param height
|
||||||
|
*/
|
||||||
|
const initChessBoard = (width: number, height: number) => {
|
||||||
|
chessBoard = new Array(width);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
chessBoard[i] = new Array(height);
|
||||||
|
for (let j = 0; j < height; j++) {
|
||||||
|
chessBoard[i][j] = {
|
||||||
|
blocks: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化棋盘
|
||||||
|
initChessBoard(boxWidthNum, boxHeightNum);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏初始化
|
||||||
|
*/
|
||||||
|
const initGame = () => {
|
||||||
|
console.log("initGame", gameConfig);
|
||||||
|
|
||||||
|
// 1. 规划块数
|
||||||
|
// 块数单位(总块数必须是该值的倍数)
|
||||||
|
const blockNumUnit = gameConfig.composeNum * gameConfig.typeNum;
|
||||||
|
console.log("块数单位", blockNumUnit);
|
||||||
|
|
||||||
|
// 随机生成的总块数
|
||||||
|
const totalRandomBlockNum = gameConfig.randomBlocks.reduce((pre, curr) => {
|
||||||
|
return pre + curr;
|
||||||
|
}, 0);
|
||||||
|
console.log("随机生成的总块数", totalRandomBlockNum);
|
||||||
|
|
||||||
|
// 需要的最小块数
|
||||||
|
const minBlockNum = Math.ceil(
|
||||||
|
(gameConfig.levelNum *
|
||||||
|
(gameConfig.topBlockNum + gameConfig.minBottomBlockNum)) /
|
||||||
|
2 +
|
||||||
|
totalRandomBlockNum
|
||||||
|
);
|
||||||
|
console.log("需要的最小块数", minBlockNum);
|
||||||
|
|
||||||
|
// 补齐到 blockNumUnit 的倍数
|
||||||
|
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
|
||||||
|
let totalBlockNum = minBlockNum;
|
||||||
|
if (totalBlockNum % blockNumUnit !== 0) {
|
||||||
|
totalBlockNum = (Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
|
||||||
|
}
|
||||||
|
console.log("总块数", totalBlockNum);
|
||||||
|
|
||||||
|
// 2. 随机生成块
|
||||||
|
// 保存所有块的数组
|
||||||
|
const animalBlocks: string[] = [];
|
||||||
|
// 需要用到的动物数组
|
||||||
|
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
|
||||||
|
// 依次把块塞到数组里
|
||||||
|
for (let i = 0; i < totalBlockNum; i++) {
|
||||||
|
animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
|
||||||
|
}
|
||||||
|
// 打乱数组
|
||||||
|
const randomAnimalBlocks = _.shuffle(animalBlocks);
|
||||||
|
// 下一个要塞入的块
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
// 3. 填充结果
|
||||||
|
// 计算随机生成的块
|
||||||
|
const randomBlocks: BlockType[][] = [];
|
||||||
|
gameConfig.randomBlocks.forEach((randomBlock, idx) => {
|
||||||
|
randomBlocks[idx] = [];
|
||||||
|
for (let i = 0; i < randomBlock; i++) {
|
||||||
|
const newBlock = {
|
||||||
|
id: pos,
|
||||||
|
level: i,
|
||||||
|
status: 0,
|
||||||
|
type: randomAnimalBlocks[pos],
|
||||||
|
higherThanBlocks: [] as BlockType[],
|
||||||
|
lowerThanBlocks: [] as BlockType[],
|
||||||
|
} as BlockType;
|
||||||
|
randomBlocks[idx].push(newBlock);
|
||||||
|
blockData[pos] = newBlock;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 剩余块数
|
||||||
|
let leftBlockNum = totalBlockNum - totalRandomBlockNum;
|
||||||
|
|
||||||
|
// 计算每层生成的块数
|
||||||
|
// 每层递减块数
|
||||||
|
// e.g. 最上层 38 块,最下层不小于 20 块,共 10 层,则每层递减块数为 18 / 9 = 2
|
||||||
|
const stepNum = Math.floor(
|
||||||
|
(gameConfig.topBlockNum - gameConfig.minBottomBlockNum) /
|
||||||
|
(gameConfig.levelNum - 1)
|
||||||
|
);
|
||||||
|
console.log("每层递减块数", stepNum);
|
||||||
|
|
||||||
|
// 下一层要分配的块数
|
||||||
|
let nextBlockNum = gameConfig.topBlockNum;
|
||||||
|
// 各层的块
|
||||||
|
const levelBlocks: BlockType[][] = [];
|
||||||
|
for (let i = 0; i < gameConfig.levelNum; i++) {
|
||||||
|
// 最后一层,所有的块都分配出去
|
||||||
|
if (i == gameConfig.levelNum - 1) {
|
||||||
|
nextBlockNum = leftBlockNum;
|
||||||
|
}
|
||||||
|
levelBlocks[i] = [];
|
||||||
|
// 添加新块
|
||||||
|
for (let j = 0; j < nextBlockNum; j++) {
|
||||||
|
if (pos >= totalBlockNum) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const newBlock = {
|
||||||
|
id: pos,
|
||||||
|
level: i,
|
||||||
|
status: 0,
|
||||||
|
type: randomAnimalBlocks[pos],
|
||||||
|
higherThanBlocks: [] as BlockType[],
|
||||||
|
lowerThanBlocks: [] as BlockType[],
|
||||||
|
} as BlockType;
|
||||||
|
levelBlocks[i].push(newBlock);
|
||||||
|
blockData[pos] = newBlock;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
leftBlockNum -= nextBlockNum;
|
||||||
|
nextBlockNum -= stepNum;
|
||||||
|
}
|
||||||
|
console.log("剩余块数", leftBlockNum);
|
||||||
|
|
||||||
|
// 4. 初始化空插槽
|
||||||
|
const slotArea: BlockType[] = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"各层数量",
|
||||||
|
levelBlocks
|
||||||
|
.map((levelBlock, idx) => `第${idx}层:${levelBlock.length} 块`)
|
||||||
|
.join("\n")
|
||||||
|
);
|
||||||
|
console.log("随机块情况", randomBlocks);
|
||||||
|
|
||||||
|
return {
|
||||||
|
levelBlocks,
|
||||||
|
randomBlocks,
|
||||||
|
slotArea,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机生成块坐标
|
||||||
|
*/
|
||||||
|
const randomPos = () => {
|
||||||
|
const levelBoardDom: any = document.getElementsByClassName("level-board");
|
||||||
|
// 为方便给格子设置固定宽高,不动态计算了
|
||||||
|
// const totalWidth = levelBoardDom[0].clientWidth;
|
||||||
|
const blockDomList = document.getElementsByClassName("level-block");
|
||||||
|
// 每个格子的宽高
|
||||||
|
const widthUnit = 14;
|
||||||
|
const heightUnit = 14;
|
||||||
|
// 设置父容器宽高
|
||||||
|
levelBoardDom[0].style.width = widthUnit * boxWidthNum + "px";
|
||||||
|
levelBoardDom[0].style.height = heightUnit * boxHeightNum + "px";
|
||||||
|
// 遍历时层级递增
|
||||||
|
for (let i = 0; i < blockDomList.length; i++) {
|
||||||
|
let blockDom: any = blockDomList[i];
|
||||||
|
const blockId = blockDom.dataset.id;
|
||||||
|
const block = blockData[blockId];
|
||||||
|
blockDom.style.position = "absolute";
|
||||||
|
// 随机生成坐标,当前层级不能重复
|
||||||
|
let newPosX;
|
||||||
|
let newPosY;
|
||||||
|
while (true) {
|
||||||
|
newPosX = Math.floor(Math.random() * (boxWidthNum - 2));
|
||||||
|
newPosY = Math.floor(Math.random() * (boxHeightNum - 2));
|
||||||
|
const currChessBoardUnit = chessBoard[newPosX][newPosY];
|
||||||
|
// 同层级元素不能完全重叠
|
||||||
|
if (
|
||||||
|
currChessBoardUnit.blocks.length < 1 ||
|
||||||
|
currChessBoardUnit.blocks[currChessBoardUnit.blocks.length - 1].level !=
|
||||||
|
block.level
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chessBoard[newPosX][newPosY].blocks.push(block);
|
||||||
|
block.x = newPosX;
|
||||||
|
block.y = newPosY;
|
||||||
|
blockDom.style.left = newPosX * widthUnit + "px";
|
||||||
|
blockDom.style.top = newPosY * heightUnit + "px";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可能导致死循环,暂不使用
|
||||||
|
// /**
|
||||||
|
// * 判断某个坐标是否和其他块有重叠
|
||||||
|
// * @param x
|
||||||
|
// * @param y
|
||||||
|
// * @param block
|
||||||
|
// */
|
||||||
|
// const hasOverlap = (x: number, y: number, block: BlockType) => {
|
||||||
|
// // 确定该块附近的格子坐标范围
|
||||||
|
// const minX = Math.max(x - 2, 0);
|
||||||
|
// const minY = Math.max(y - 2, 0);
|
||||||
|
// const maxX = Math.min(x + 3, boxWidthNum - 2);
|
||||||
|
// const maxY = Math.min(y + 3, boxWidthNum - 2);
|
||||||
|
// // 遍历该块附近的格子
|
||||||
|
// for (let i = minX; i < maxX; i++) {
|
||||||
|
// for (let j = minY; j < maxY; j++) {
|
||||||
|
// const relationBlocks = chessBoard[i][j].blocks;
|
||||||
|
// for (const relationBlock of relationBlocks) {
|
||||||
|
// if (relationBlocks[relationBlocks.length - 1].level != block.level) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return true;
|
||||||
|
// };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定层级覆盖关系(用于确认哪些元素是当前可点击的)
|
||||||
|
*
|
||||||
|
* 核心逻辑:每个块压住和其坐标有交集棋盘格内所有 level 大于它的点,双向建立联系
|
||||||
|
*/
|
||||||
|
const bindLevelRelation = () => {
|
||||||
|
levelBlocksVal.value.forEach((levelBlock) => {
|
||||||
|
levelBlock.forEach((block) => {
|
||||||
|
const x = block.x;
|
||||||
|
const y = block.y;
|
||||||
|
// 确定该块附近的格子坐标范围
|
||||||
|
const minX = Math.max(x - 2, 0);
|
||||||
|
const minY = Math.max(y - 2, 0);
|
||||||
|
const maxX = Math.min(x + 3, boxWidthNum - 2);
|
||||||
|
const maxY = Math.min(y + 3, boxWidthNum - 2);
|
||||||
|
// 遍历该块附近的格子
|
||||||
|
for (let i = minX; i < maxX; i++) {
|
||||||
|
for (let j = minY; j < maxY; j++) {
|
||||||
|
const relationBlocks = chessBoard[i][j].blocks;
|
||||||
|
relationBlocks.forEach((relationBlock) => {
|
||||||
|
// 建立覆盖关系
|
||||||
|
if (relationBlock.level > block.level) {
|
||||||
|
block.higherThanBlocks.push(relationBlock);
|
||||||
|
relationBlock.lowerThanBlocks.push(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始游戏
|
||||||
|
*/
|
||||||
|
const doStart = () => {
|
||||||
|
gameStatus.value = 0;
|
||||||
|
const { levelBlocks, randomBlocks, slotArea } = initGame();
|
||||||
|
console.log(levelBlocks, randomBlocks, slotArea);
|
||||||
|
levelBlocksVal.value = levelBlocks;
|
||||||
|
randomBlocksVal.value = randomBlocks;
|
||||||
|
slotAreaVal.value = slotArea;
|
||||||
|
// 等 dom 更新后才刷新坐标
|
||||||
|
nextTick(() => {
|
||||||
|
randomPos();
|
||||||
|
bindLevelRelation();
|
||||||
|
gameStatus.value = 1;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击块事件
|
||||||
|
* @param block
|
||||||
|
* @param e
|
||||||
|
* @param randomIdx 随机区域下标,>= 0 表示点击的是随机块
|
||||||
|
*/
|
||||||
|
const doClickBlock = (block: BlockType, e: Event, randomIdx = -1) => {
|
||||||
|
// 已经输了 / 已经被点击 / 有上层块,不能再点击
|
||||||
|
if (
|
||||||
|
currSlotNum.value >= gameConfig.slotNum ||
|
||||||
|
block.status !== 0 ||
|
||||||
|
block.lowerThanBlocks.length > 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 修改元素状态为已点击
|
||||||
|
block.status = 1;
|
||||||
|
// 移除当前元素
|
||||||
|
if (randomIdx >= 0) {
|
||||||
|
// 移除所点击的随机区域的第一个元素
|
||||||
|
randomBlocksVal.value[randomIdx] = randomBlocksVal.value[randomIdx].slice(
|
||||||
|
1,
|
||||||
|
randomBlocksVal.value[randomIdx].length
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 删除节点
|
||||||
|
// @ts-ignore
|
||||||
|
e.target.remove();
|
||||||
|
// 移除覆盖关系
|
||||||
|
block.higherThanBlocks.forEach((higherThanBlock) => {
|
||||||
|
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
|
||||||
|
return lowerThanBlock.id === block.id;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 新元素加入插槽
|
||||||
|
let tempSlotNum = currSlotNum.value;
|
||||||
|
slotAreaVal.value[tempSlotNum] = block;
|
||||||
|
// 检查是否有可消除的
|
||||||
|
// block => 出现次数
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
// 去除空槽
|
||||||
|
const tempSlotAreaVal = slotAreaVal.value.filter((slotBlock) => !!slotBlock);
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
const type = slotBlock.type;
|
||||||
|
if (!map[type]) {
|
||||||
|
map[type] = 1;
|
||||||
|
} else {
|
||||||
|
map[type]++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log("tempSlotAreaVal", tempSlotAreaVal);
|
||||||
|
console.log("map", map);
|
||||||
|
// 得到新数组
|
||||||
|
const newSlotAreaVal = new Array(gameConfig.slotNum).fill(null);
|
||||||
|
tempSlotNum = 0;
|
||||||
|
tempSlotAreaVal.forEach((slotBlock) => {
|
||||||
|
// 成功消除(不添加到新数组中)
|
||||||
|
if (map[slotBlock.type] >= gameConfig.composeNum) {
|
||||||
|
// 块状态改为已消除
|
||||||
|
slotBlock.status = 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newSlotAreaVal[tempSlotNum++] = slotBlock;
|
||||||
|
});
|
||||||
|
slotAreaVal.value = newSlotAreaVal;
|
||||||
|
currSlotNum.value = tempSlotNum;
|
||||||
|
// 游戏结束
|
||||||
|
if (tempSlotNum >= gameConfig.slotNum) {
|
||||||
|
gameStatus.value = 2;
|
||||||
|
setTimeout(() => {
|
||||||
|
alert("你输了");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.level-board {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level {
|
||||||
|
}
|
||||||
|
|
||||||
|
.random-board {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.random-area {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slot-board {
|
||||||
|
margin-top: 24px;
|
||||||
|
border: 10px solid saddlebrown;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
font-size: 28px;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
line-height: 42px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
background: white;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: top;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
background: grey;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
0
src/style.css
Normal file
0
src/style.css
Normal file
7
src/vite-env.d.ts
vendored
Normal file
7
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
12
vite.config.ts
Normal file
12
vite.config.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue({
|
||||||
|
// 支持 Markdown 文件加载
|
||||||
|
include: [/\.vue$/],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user