Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
0f19b5b19b | |||
|
e8f95a4d4d | ||
|
02e38aca6a | ||
|
ce8bd53827 | ||
|
0cf6b3a9c2 | ||
|
e57adbc824 | ||
|
06bc94fa1b | ||
|
9b91330a83 | ||
|
fc74efac2d | ||
|
75d97b11c3 | ||
|
9b8077c536 | ||
|
200b2a47d0 | ||
|
7d73c64d62 | ||
|
d03ef31503 | ||
|
c3da9ecf1b |
32
README.md
32
README.md
@ -1,4 +1,34 @@
|
||||
被羊了个羊虐了后,我自己做了一个!
|
||||
# 鱼了个鱼
|
||||
|
||||
> 被羊了个羊虐了百遍后,我自己做了一个!
|
||||
|
||||
在线体验:https://yulegeyu.cn
|
||||
|
||||
游戏视频:https://www.bilibili.com/video/BV1Pe411M7wh
|
||||
|
||||
相关文章:https://mp.weixin.qq.com/s/D_I1Tq-ofhKhlp0rkOpaLA
|
||||
|
||||
游戏截图(自定义了图案):
|
||||
|
||||

|
||||
|
||||
游戏特色:
|
||||
|
||||
1. 支持选择难度(4 种)
|
||||
2. 支持自定义难度
|
||||
3. 支持自定义动物图案(比如 🐔🏀)
|
||||
4. 可以无限使用技能(道具)
|
||||
5. 不需要看广告
|
||||
6. 能通关
|
||||
|
||||
> 补一句:就出于兴趣做了几个小时,有 bug 正常哈哈,欢迎 PR~
|
||||
|
||||
简单说下实现原理,主要有 4 个点:
|
||||
|
||||
1. 游戏全局参数:做游戏的同学都知道,游戏中会涉及很多参数,比如槽位数量、层数等等。我们要将这些参数抽取成统一的全局变量,供其他变量使用。从而做到修改一处,游戏自动适配。还可以提供页面来让用户自定义这些参数,提高游戏的可玩性。
|
||||
2. 网格:为了让块的分布相对规整、并且为计算坐标提供方便,我将整个游戏画布分为 24 x 24 的虚拟网格,类似一个棋盘。一个块占用 3 x 3 的格子。
|
||||
3. 随机生成块:包括随机生成方块的图案和坐标。首先我根据全局参数计算出了总块数,然后用 shuffle 函数打乱存储所有动物图案的数组,再依次将数组中的图案填充到方块中。生成坐标的原理是随机选取坐标范围内的点,坐标范围可以随着层级的增加而递减,即生成的图案越来越挤,达到难度逐层加大的效果。
|
||||
4. 块的覆盖关系:怎么做到点击上层的块后,才能点下层的块呢?首先要给每个块指定一个层级属性。然后有两种思路,第 1 种是先逐层生成,然后每个格子里层级最高的块依次判断其周围格子有没有块层级大于它;第 2 种是在随机生成块的时候就给相互重叠的块绑定层级关系(即谁覆盖了我?我覆盖了谁?)。这里我选择了第 2 种方法,感觉效率会高一些。
|
||||
|
||||
|
||||
|
||||
|
BIN
doc/img.png
Normal file
BIN
doc/img.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 345 KiB |
@ -5,6 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>鱼了个鱼</title>
|
||||
<script charset="UTF-8" id="LA_COLLECT" src="//sdk.51.la/js-sdk-pro.min.js"></script>
|
||||
<script>LA.init({id: "JonPnywkOzKkLXdw",ck: "JonPnywkOzKkLXdw"})</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
6473
package-lock.json
generated
Normal file
6473
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"ant-design-vue": "^3.2.11",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.0.19",
|
||||
|
30
src/App.vue
30
src/App.vue
@ -3,39 +3,19 @@
|
||||
<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;
|
||||
padding: 16px 16px 50px;
|
||||
min-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;
|
||||
.content {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
BIN
src/assets/kunkun.png
Normal file
BIN
src/assets/kunkun.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
15
src/components/MyAd.vue
Normal file
15
src/components/MyAd.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="my-ad">
|
||||
<a href="https://github.com/liyupi/yulegeyu" target="_blank">
|
||||
<div style="background: rgba(0, 0, 0, 0.8); padding: 12px">
|
||||
<github-outlined />
|
||||
代码完全开源,欢迎 star
|
||||
</div>
|
||||
</a>
|
||||
<!-- <a href="https://space.bilibili.com/12890453/"> 欢迎关注程序员鱼皮 </a>-->
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { GithubOutlined } from "@ant-design/icons-vue";
|
||||
</script>
|
||||
<style></style>
|
@ -1,9 +1,19 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import IndexPage from "../pages/IndexPage.vue";
|
||||
import GamePage from "../pages/GamePage.vue";
|
||||
import ConfigPage from "../pages/ConfigPage.vue";
|
||||
|
||||
export default [
|
||||
{
|
||||
path: "/",
|
||||
component: IndexPage,
|
||||
},
|
||||
{
|
||||
path: "/game",
|
||||
component: GamePage,
|
||||
},
|
||||
{
|
||||
path: "/config",
|
||||
component: ConfigPage,
|
||||
},
|
||||
] as RouteRecordRaw[];
|
||||
|
186
src/core/game.ts
186
src/core/game.ts
@ -6,12 +6,12 @@
|
||||
import { useGlobalStore } from "./globalStore";
|
||||
// @ts-ignore
|
||||
import _ from "lodash";
|
||||
import { nextTick, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const useGame = () => {
|
||||
const { gameConfig } = useGlobalStore();
|
||||
|
||||
// 游戏状态:0 - 初始化, 1 - 进行中, 2 - 结束
|
||||
// 游戏状态:0 - 初始化, 1 - 进行中, 2 - 失败结束, 3 - 胜利
|
||||
const gameStatus = ref(0);
|
||||
|
||||
// 各层块
|
||||
@ -24,8 +24,15 @@ const useGame = () => {
|
||||
const currSlotNum = ref(0);
|
||||
|
||||
// 保存所有块(包括随机块)
|
||||
const allBlocks: BlockType[] = [];
|
||||
const blockData: Record<number, BlockType> = {};
|
||||
|
||||
// 总块数
|
||||
let totalBlockNum = ref(0);
|
||||
|
||||
// 已消除块数
|
||||
let clearBlockNum = ref(0);
|
||||
|
||||
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
|
||||
const boxWidthNum = 24;
|
||||
const boxHeightNum = 24;
|
||||
@ -37,6 +44,17 @@ const useGame = () => {
|
||||
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
|
||||
let chessBoard: ChessBoardUnitType[][] = [];
|
||||
|
||||
// 操作历史(存储点击的块)
|
||||
let opHistory: BlockType[] = [];
|
||||
|
||||
// region 技能相关
|
||||
|
||||
const isHolyLight = ref(false);
|
||||
|
||||
const canSeeRandom = ref(false);
|
||||
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 初始化指定大小的棋盘
|
||||
* @param width
|
||||
@ -74,9 +92,12 @@ const useGame = () => {
|
||||
console.log("块数单位", blockNumUnit);
|
||||
|
||||
// 随机生成的总块数
|
||||
const totalRandomBlockNum = gameConfig.randomBlocks.reduce((pre, curr) => {
|
||||
return pre + curr;
|
||||
}, 0);
|
||||
const totalRandomBlockNum = gameConfig.randomBlocks.reduce(
|
||||
(pre: number, curr: number) => {
|
||||
return pre + curr;
|
||||
},
|
||||
0
|
||||
);
|
||||
console.log("随机生成的总块数", totalRandomBlockNum);
|
||||
|
||||
// 需要的最小块数
|
||||
@ -86,12 +107,12 @@ const useGame = () => {
|
||||
|
||||
// 补齐到 blockNumUnit 的倍数
|
||||
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
|
||||
let totalBlockNum = minBlockNum;
|
||||
if (totalBlockNum % blockNumUnit !== 0) {
|
||||
totalBlockNum =
|
||||
totalBlockNum.value = minBlockNum;
|
||||
if (totalBlockNum.value % blockNumUnit !== 0) {
|
||||
totalBlockNum.value =
|
||||
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
|
||||
}
|
||||
console.log("总块数", totalBlockNum);
|
||||
console.log("总块数", totalBlockNum.value);
|
||||
|
||||
// 2. 初始化块,随机生成块的内容
|
||||
// 保存所有块的数组
|
||||
@ -99,15 +120,14 @@ const useGame = () => {
|
||||
// 需要用到的动物数组
|
||||
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum);
|
||||
// 依次把块塞到数组里
|
||||
for (let i = 0; i < totalBlockNum; i++) {
|
||||
for (let i = 0; i < totalBlockNum.value; i++) {
|
||||
animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
|
||||
}
|
||||
// 打乱数组
|
||||
const randomAnimalBlocks = _.shuffle(animalBlocks);
|
||||
|
||||
// 初始化
|
||||
const allBlocks: BlockType[] = [];
|
||||
for (let i = 0; i < totalBlockNum; i++) {
|
||||
for (let i = 0; i < totalBlockNum.value; i++) {
|
||||
const newBlock = {
|
||||
id: i,
|
||||
status: 0,
|
||||
@ -124,7 +144,7 @@ const useGame = () => {
|
||||
|
||||
// 3. 计算随机生成的块
|
||||
const randomBlocks: BlockType[][] = [];
|
||||
gameConfig.randomBlocks.forEach((randomBlock, idx) => {
|
||||
gameConfig.randomBlocks.forEach((randomBlock: number, idx: number) => {
|
||||
randomBlocks[idx] = [];
|
||||
for (let i = 0; i < randomBlock; i++) {
|
||||
randomBlocks[idx].push(allBlocks[pos]);
|
||||
@ -134,7 +154,7 @@ const useGame = () => {
|
||||
});
|
||||
|
||||
// 剩余块数
|
||||
let leftBlockNum = totalBlockNum - totalRandomBlockNum;
|
||||
let leftBlockNum = totalBlockNum.value - totalRandomBlockNum;
|
||||
|
||||
// 4. 计算有层级关系的块
|
||||
const levelBlocks: BlockType[] = [];
|
||||
@ -265,18 +285,19 @@ const useGame = () => {
|
||||
/**
|
||||
* 点击块事件
|
||||
* @param block
|
||||
* @param e
|
||||
* @param randomIdx 随机区域下标,>= 0 表示点击的是随机块
|
||||
* @param force 强制移除
|
||||
*/
|
||||
const doClickBlock = (block: BlockType, e: Event, randomIdx = -1) => {
|
||||
// 已经输了 / 已经被点击 / 有上层块,不能再点击
|
||||
const doClickBlock = (block: BlockType, randomIdx = -1, force = false) => {
|
||||
// 已经输了 / 已经被点击 / 有上层块(且非强制和圣光),不能再点击
|
||||
if (
|
||||
currSlotNum.value >= gameConfig.slotNum ||
|
||||
block.status !== 0 ||
|
||||
block.lowerThanBlocks.length > 0
|
||||
(block.lowerThanBlocks.length > 0 && !force && !isHolyLight.value)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
isHolyLight.value = false;
|
||||
// 修改元素状态为已点击
|
||||
block.status = 1;
|
||||
// 移除当前元素
|
||||
@ -287,9 +308,8 @@ const useGame = () => {
|
||||
randomBlocksVal.value[randomIdx].length
|
||||
);
|
||||
} else {
|
||||
// 删除节点
|
||||
// @ts-ignore
|
||||
e.target.remove();
|
||||
// 非随机区才可撤回
|
||||
opHistory.push(block);
|
||||
// 移除覆盖关系
|
||||
block.higherThanBlocks.forEach((higherThanBlock) => {
|
||||
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
|
||||
@ -325,6 +345,10 @@ const useGame = () => {
|
||||
if (map[slotBlock.type] >= gameConfig.composeNum) {
|
||||
// 块状态改为已消除
|
||||
slotBlock.status = 2;
|
||||
// 已消除块数 +1
|
||||
clearBlockNum.value++;
|
||||
// 清除操作记录,防止撤回
|
||||
opHistory = [];
|
||||
return;
|
||||
}
|
||||
newSlotAreaVal[tempSlotNum++] = slotBlock;
|
||||
@ -338,6 +362,9 @@ const useGame = () => {
|
||||
alert("你输了");
|
||||
}, 2000);
|
||||
}
|
||||
if (clearBlockNum.value >= totalBlockNum.value) {
|
||||
gameStatus.value = 3;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -353,6 +380,111 @@ const useGame = () => {
|
||||
gameStatus.value = 1;
|
||||
};
|
||||
|
||||
// region 技能
|
||||
|
||||
/**
|
||||
* 洗牌
|
||||
*
|
||||
* @desc 随机重洗所有未被点击的块
|
||||
*/
|
||||
const doShuffle = () => {
|
||||
// 遍历所有未消除的块
|
||||
const originBlocks = allBlocks.filter((block) => block.status === 0);
|
||||
const newBlockTypes = _.shuffle(originBlocks.map((block) => block.type));
|
||||
let pos = 0;
|
||||
originBlocks.forEach((block) => {
|
||||
block.type = newBlockTypes[pos++];
|
||||
});
|
||||
levelBlocksVal.value = [...levelBlocksVal.value];
|
||||
};
|
||||
|
||||
/**
|
||||
* 破坏
|
||||
*
|
||||
* @desc 消除一组层级块
|
||||
*/
|
||||
const doBroke = () => {
|
||||
// 类型,块列表映射
|
||||
const typeBlockMap: Record<string, BlockType[]> = {};
|
||||
const blocks = levelBlocksVal.value.filter((block) => block.status === 0);
|
||||
// 遍历所有未消除的层级块
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i];
|
||||
if (!typeBlockMap[block.type]) {
|
||||
typeBlockMap[block.type] = [];
|
||||
}
|
||||
typeBlockMap[block.type].push(block);
|
||||
// 有能消除的一组块
|
||||
if (typeBlockMap[block.type].length >= gameConfig.composeNum) {
|
||||
typeBlockMap[block.type].forEach((clickBlock) => {
|
||||
doClickBlock(clickBlock, -1, true);
|
||||
});
|
||||
console.log("doBroke", typeBlockMap[block.type]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 撤回
|
||||
*
|
||||
* @desc 后退一步
|
||||
*/
|
||||
const doRevert = () => {
|
||||
if (opHistory.length < 1) {
|
||||
return;
|
||||
}
|
||||
opHistory[opHistory.length - 1].status = 0;
|
||||
// @ts-ignore
|
||||
slotAreaVal.value[currSlotNum.value - 1] = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 移出块
|
||||
*/
|
||||
const doRemove = () => {
|
||||
// 移除第一个块
|
||||
const block = slotAreaVal.value[0];
|
||||
if (!block) {
|
||||
return;
|
||||
}
|
||||
// 槽移除块
|
||||
for (let i = 0; i < slotAreaVal.value.length - 1; i++) {
|
||||
slotAreaVal.value[i] = slotAreaVal.value[i + 1];
|
||||
}
|
||||
// @ts-ignore
|
||||
slotAreaVal.value[slotAreaVal.value.length - 1] = null;
|
||||
// 改变新块的坐标
|
||||
block.x = Math.floor(Math.random() * (boxWidthNum - 2));
|
||||
block.y = boxHeightNum - 2;
|
||||
block.status = 0;
|
||||
// 移除的是随机块的元素,移到层级区域
|
||||
if (block.level < 1) {
|
||||
block.level = 10000;
|
||||
levelBlocksVal.value.push(block);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 圣光
|
||||
*
|
||||
* @desc 下一个块可以任意点击
|
||||
*/
|
||||
const doHolyLight = () => {
|
||||
isHolyLight.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 透视
|
||||
*
|
||||
* @desc 可以看到随机块
|
||||
*/
|
||||
const doSeeRandom = () => {
|
||||
canSeeRandom.value = !canSeeRandom.value;
|
||||
};
|
||||
|
||||
// endregion
|
||||
|
||||
return {
|
||||
gameStatus,
|
||||
levelBlocksVal,
|
||||
@ -360,8 +492,20 @@ const useGame = () => {
|
||||
slotAreaVal,
|
||||
widthUnit,
|
||||
heightUnit,
|
||||
currSlotNum,
|
||||
opHistory,
|
||||
totalBlockNum,
|
||||
clearBlockNum,
|
||||
isHolyLight,
|
||||
canSeeRandom,
|
||||
doClickBlock,
|
||||
doStart,
|
||||
doShuffle,
|
||||
doBroke,
|
||||
doRemove,
|
||||
doRevert,
|
||||
doHolyLight,
|
||||
doSeeRandom,
|
||||
};
|
||||
};
|
||||
|
||||
|
175
src/core/gameConfig.ts
Normal file
175
src/core/gameConfig.ts
Normal file
@ -0,0 +1,175 @@
|
||||
// 动物数组
|
||||
const animals = [
|
||||
"🐔",
|
||||
"🐟",
|
||||
"🦆",
|
||||
"🐶",
|
||||
"🐱",
|
||||
"🐴",
|
||||
"🐑",
|
||||
"🐦",
|
||||
"🐧",
|
||||
"🐊",
|
||||
"🐺",
|
||||
"🐒",
|
||||
"🐳",
|
||||
"🐬",
|
||||
"🐢",
|
||||
"🦖",
|
||||
"🦒",
|
||||
"🦁",
|
||||
"🐍",
|
||||
"🐭",
|
||||
"🐂",
|
||||
];
|
||||
|
||||
export const defaultGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 12,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 24,
|
||||
// 边界收缩步长
|
||||
borderStep: 1,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 6,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [8, 8],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 简单难度
|
||||
*/
|
||||
export const easyGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 8,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 10,
|
||||
// 边界收缩步长
|
||||
borderStep: 1,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 6,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [4, 4],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 中等难度
|
||||
*/
|
||||
export const middleGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 10,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 12,
|
||||
// 边界收缩步长
|
||||
borderStep: 1,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 7,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [5, 5],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 困难难度
|
||||
*/
|
||||
export const hardGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 12,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 16,
|
||||
// 边界收缩步长
|
||||
borderStep: 1,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 8,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [6, 6],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 地狱难度
|
||||
*/
|
||||
export const lunaticGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 14,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 20,
|
||||
// 边界收缩步长
|
||||
borderStep: 2,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 10,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [8, 8],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 天狱难度
|
||||
*/
|
||||
export const skyGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 16,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 24,
|
||||
// 边界收缩步长
|
||||
borderStep: 2,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 12,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [8, 8],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
||||
|
||||
/**
|
||||
* 羊了个羊难度
|
||||
*/
|
||||
export const yangGameConfig: GameConfigType = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 18,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 28,
|
||||
// 边界收缩步长
|
||||
borderStep: 3,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 15,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [8, 8],
|
||||
// 动物数组
|
||||
animals,
|
||||
};
|
@ -1,395 +0,0 @@
|
||||
/**
|
||||
* 游戏逻辑 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;
|
@ -1,49 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const defaultGameConfig = {
|
||||
// 槽容量
|
||||
slotNum: 7,
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: 3,
|
||||
// 动物类别数
|
||||
typeNum: 12,
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: 30,
|
||||
// 边界收缩步长
|
||||
borderStep: 1,
|
||||
// 总层数(最小为 2)
|
||||
levelNum: 8,
|
||||
// 最上层块数
|
||||
topBlockNum: 40,
|
||||
// 最下层块数最小值
|
||||
minBottomBlockNum: 20,
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: [8, 8],
|
||||
// 动物数组
|
||||
animals: [
|
||||
"🐔",
|
||||
"🐟",
|
||||
"🦆",
|
||||
"🐶",
|
||||
"🐱",
|
||||
"🐴",
|
||||
"🐑",
|
||||
"🐦",
|
||||
"🐧",
|
||||
"🐊",
|
||||
"🐺",
|
||||
"🐒",
|
||||
"🐳",
|
||||
"🐬",
|
||||
"🐢",
|
||||
"🦖",
|
||||
"🦒",
|
||||
"🦁",
|
||||
"🐍",
|
||||
"🐭",
|
||||
"🐂",
|
||||
],
|
||||
};
|
||||
import { defaultGameConfig } from "./gameConfig";
|
||||
|
||||
/**
|
||||
* 全局状态存储
|
||||
@ -52,6 +8,7 @@ const defaultGameConfig = {
|
||||
*/
|
||||
export const useGlobalStore = defineStore("global", {
|
||||
state: () => ({
|
||||
customConfig: { ...defaultGameConfig },
|
||||
gameConfig: { ...defaultGameConfig },
|
||||
}),
|
||||
getters: {},
|
||||
@ -67,6 +24,12 @@ export const useGlobalStore = defineStore("global", {
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setGameConfig(gameConfig: GameConfigType) {
|
||||
this.gameConfig = gameConfig;
|
||||
},
|
||||
setCustomConfig(customConfig: GameConfigType) {
|
||||
this.customConfig = customConfig;
|
||||
},
|
||||
reset() {
|
||||
this.$reset();
|
||||
},
|
||||
|
36
src/core/type.d.ts
vendored
36
src/core/type.d.ts
vendored
@ -22,3 +22,39 @@ interface ChessBoardUnitType {
|
||||
// 放到当前格子里的块(层级越高下标越大)
|
||||
blocks: BlockType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏配置类型
|
||||
*/
|
||||
interface GameConfigType {
|
||||
// 槽容量
|
||||
slotNum: number;
|
||||
// 需要多少个一样块的才能合成
|
||||
composeNum: number;
|
||||
// 动物类别数
|
||||
typeNum: number;
|
||||
// 每层块数(大致)
|
||||
levelBlockNum: number;
|
||||
// 边界收缩步长
|
||||
borderStep: number;
|
||||
// 总层数(最小为 2)
|
||||
levelNum: number;
|
||||
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
|
||||
randomBlocks: number[];
|
||||
// 动物数组
|
||||
animals: string[];
|
||||
// 最上层块数(已废弃)
|
||||
// topBlockNum: 40,
|
||||
// 最下层块数最小值(已废弃)
|
||||
// minBottomBlockNum: 20,
|
||||
}
|
||||
|
||||
/**
|
||||
* 技能类型
|
||||
*/
|
||||
interface SkillType {
|
||||
name: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
action: function;
|
||||
}
|
||||
|
112
src/pages/ConfigPage.vue
Normal file
112
src/pages/ConfigPage.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div id="customConfigPage">
|
||||
<h2>
|
||||
自定义难度
|
||||
<a-button style="float: right" @click="doBack">返回</a-button>
|
||||
</h2>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
label-align="left"
|
||||
:label-col="{ style: { width: '120px' } }"
|
||||
:model="config"
|
||||
@finish="handleFinish"
|
||||
>
|
||||
<a-form-item label="槽容量" name="slotNum">
|
||||
<a-input-number v-model:value="config.slotNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="合成数" name="composeNum">
|
||||
<a-input-number v-model:value="config.composeNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="动物数" name="typeNum">
|
||||
<a-input-number v-model:value="config.typeNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="动物图案" name="animalStr">
|
||||
<a-input v-model:value="config.animalStr" />
|
||||
</a-form-item>
|
||||
<a-form-item label="总层数" name="levelNum">
|
||||
<a-input-number v-model:value="config.levelNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="每层块数" name="levelBlockNum">
|
||||
<a-input-number v-model:value="config.levelBlockNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="边界收缩" name="borderStep">
|
||||
<a-input-number v-model:value="config.borderStep" />
|
||||
</a-form-item>
|
||||
<a-form-item label="随机区数" name="randomAreaNum">
|
||||
<a-input-number v-model:value="config.randomAreaNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="随机区块数" name="randomBlockNum">
|
||||
<a-input-number v-model:value="config.randomBlockNum" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
style="margin-bottom: 12px"
|
||||
block
|
||||
>
|
||||
开始
|
||||
</a-button>
|
||||
<a-button block style="margin-bottom: 12px" @click="resetForm"
|
||||
>重置
|
||||
</a-button>
|
||||
<a-button block danger @click="resetConfig">还原最初配置</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { FormInstance } from "ant-design-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useGlobalStore } from "../core/globalStore";
|
||||
import { defaultGameConfig } from "../core/gameConfig";
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const router = useRouter();
|
||||
const { customConfig, setGameConfig, setCustomConfig, reset } =
|
||||
useGlobalStore();
|
||||
const initConfig = {
|
||||
randomAreaNum: 2,
|
||||
randomBlockNum: 8,
|
||||
animalStr: defaultGameConfig.animals.join(""),
|
||||
...customConfig,
|
||||
};
|
||||
const config = reactive<any>(initConfig);
|
||||
|
||||
/**
|
||||
* 表单提交
|
||||
* @param values
|
||||
*/
|
||||
const handleFinish = (values: any) => {
|
||||
config.randomBlocks = new Array(values.randomAreaNum).fill(
|
||||
values.randomBlockNum
|
||||
);
|
||||
if (values.animalStr) {
|
||||
config.animals = Array.from(values.animalStr);
|
||||
}
|
||||
setGameConfig(config);
|
||||
setCustomConfig(config);
|
||||
router.push("/game");
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
formRef?.value?.resetFields();
|
||||
};
|
||||
|
||||
/**
|
||||
* 还原至初始配置
|
||||
*/
|
||||
const resetConfig = () => {
|
||||
reset();
|
||||
router.go(0);
|
||||
};
|
||||
|
||||
/**
|
||||
* 回上一页
|
||||
*/
|
||||
const doBack = () => {
|
||||
router.back();
|
||||
};
|
||||
</script>
|
||||
<style></style>
|
169
src/pages/GamePage.vue
Normal file
169
src/pages/GamePage.vue
Normal file
@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div id="gamePage">
|
||||
<a-row align="space-between">
|
||||
<a-button style="margin-bottom: 8px" @click="doBack"> 返回</a-button>
|
||||
<a-button>块数:{{ clearBlockNum }} / {{ totalBlockNum }}</a-button>
|
||||
</a-row>
|
||||
<!-- 胜利 -->
|
||||
<a-row align="center">
|
||||
<div v-if="gameStatus === 3" style="text-align: center">
|
||||
<h2>恭喜,你赢啦!🎉</h2>
|
||||
<img alt="程序员鱼皮" src="../assets/kunkun.png" />
|
||||
<my-ad style="margin-top: 16px" />
|
||||
</div>
|
||||
</a-row>
|
||||
<!-- 分层选块 -->
|
||||
<a-row align="center">
|
||||
<div v-show="gameStatus > 0" class="level-board">
|
||||
<div v-for="(block, idx) in levelBlocksVal" :key="idx">
|
||||
<div
|
||||
v-if="block.status === 0"
|
||||
class="block level-block"
|
||||
:class="{
|
||||
disabled: !isHolyLight && block.lowerThanBlocks.length > 0,
|
||||
}"
|
||||
:data-id="block.id"
|
||||
:style="{
|
||||
zIndex: 100 + block.level,
|
||||
left: block.x * widthUnit + 'px',
|
||||
top: block.y * heightUnit + 'px',
|
||||
}"
|
||||
@click="() => doClickBlock(block)"
|
||||
>
|
||||
{{ block.type }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-row>
|
||||
<!-- 随机选块 -->
|
||||
<a-row align="space-between" class="random-board">
|
||||
<div
|
||||
v-for="(randomBlock, index) in randomBlocksVal"
|
||||
:key="index"
|
||||
class="random-area"
|
||||
>
|
||||
<div
|
||||
v-if="randomBlock.length > 0"
|
||||
:data-id="randomBlock[0].id"
|
||||
class="block"
|
||||
@click="() => doClickBlock(randomBlock[0], index)"
|
||||
>
|
||||
{{ randomBlock[0].type }}
|
||||
</div>
|
||||
<!-- 隐藏 -->
|
||||
<div
|
||||
v-for="num in Math.max(randomBlock.length - 1, 0)"
|
||||
:key="num"
|
||||
class="block disabled"
|
||||
>
|
||||
<span v-if="canSeeRandom">
|
||||
{{ randomBlock[num].type }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-row>
|
||||
<!-- 槽位 -->
|
||||
<a-row v-if="slotAreaVal.length > 0" align="center" class="slot-board">
|
||||
<div v-for="(slotBlock, index) in slotAreaVal" :key="index" class="block">
|
||||
{{ slotBlock?.type }}
|
||||
</div>
|
||||
</a-row>
|
||||
<!-- 技能 -->
|
||||
<div class="skill-board">
|
||||
<a-space>
|
||||
<a-button size="small" @click="doRevert">撤回</a-button>
|
||||
<a-button size="small" @click="doRemove">移出</a-button>
|
||||
<a-button size="small" @click="doShuffle">洗牌</a-button>
|
||||
<a-button size="small" @click="doBroke">破坏</a-button>
|
||||
<a-button size="small" @click="doHolyLight">圣光</a-button>
|
||||
<a-button size="small" @click="doSeeRandom">透视</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useGame from "../core/game";
|
||||
import { onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import MyAd from "../components/MyAd.vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
gameStatus,
|
||||
levelBlocksVal,
|
||||
randomBlocksVal,
|
||||
slotAreaVal,
|
||||
widthUnit,
|
||||
heightUnit,
|
||||
totalBlockNum,
|
||||
clearBlockNum,
|
||||
isHolyLight,
|
||||
canSeeRandom,
|
||||
doClickBlock,
|
||||
doStart,
|
||||
doShuffle,
|
||||
doBroke,
|
||||
doRemove,
|
||||
doRevert,
|
||||
doHolyLight,
|
||||
doSeeRandom,
|
||||
} = useGame();
|
||||
|
||||
/**
|
||||
* 回上一页
|
||||
*/
|
||||
const doBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
doStart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.level-board {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.level-block {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.random-board {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.random-area {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.slot-board {
|
||||
border: 10px solid saddlebrown;
|
||||
margin: 16px auto;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.skill-board {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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>
|
@ -1,119 +1,110 @@
|
||||
<template>
|
||||
<div id="indexPage">
|
||||
<a-row align="center">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="doStart"
|
||||
<h1>🐟 鱼了个鱼</h1>
|
||||
<div style="margin-bottom: 16px">低配版羊了个羊小游戏,仅供消遣</div>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(easyGameConfig)"
|
||||
>
|
||||
简单模式
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(middleGameConfig)"
|
||||
>
|
||||
中等模式
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(hardGameConfig)"
|
||||
>
|
||||
困难模式
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(lunaticGameConfig)"
|
||||
>
|
||||
地狱模式
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(skyGameConfig)"
|
||||
>
|
||||
天域模式
|
||||
</a-button>
|
||||
<a-button
|
||||
block
|
||||
style="margin-bottom: 16px"
|
||||
@click="toGamePage(yangGameConfig)"
|
||||
>
|
||||
羊了个羊模式
|
||||
</a-button>
|
||||
<a-button block style="margin-bottom: 16px" @click="() => toGamePage()">
|
||||
自定义 🔥
|
||||
</a-button>
|
||||
<my-ad />
|
||||
<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-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>
|
||||
代码开源
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useGame from "../core/game";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
easyGameConfig,
|
||||
middleGameConfig,
|
||||
hardGameConfig,
|
||||
lunaticGameConfig,
|
||||
skyGameConfig,
|
||||
yangGameConfig,
|
||||
} from "../core/gameConfig";
|
||||
import { useGlobalStore } from "../core/globalStore";
|
||||
import MyAd from "../components/MyAd.vue";
|
||||
|
||||
const {
|
||||
gameStatus,
|
||||
levelBlocksVal,
|
||||
randomBlocksVal,
|
||||
slotAreaVal,
|
||||
widthUnit,
|
||||
heightUnit,
|
||||
doClickBlock,
|
||||
doStart,
|
||||
} = useGame();
|
||||
const router = useRouter();
|
||||
|
||||
const { setGameConfig } = useGlobalStore();
|
||||
|
||||
const toGamePage = (config?: GameConfigType) => {
|
||||
if (config) {
|
||||
setGameConfig(config);
|
||||
router.push("/game");
|
||||
} else {
|
||||
router.push("/config");
|
||||
}
|
||||
};
|
||||
</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;
|
||||
#indexPage {
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
background: grey;
|
||||
cursor: not-allowed;
|
||||
.footer {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,507 +0,0 @@
|
||||
<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>
|
@ -1,12 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
// 支持 Markdown 文件加载
|
||||
include: [/\.vue$/],
|
||||
}),
|
||||
],
|
||||
plugins: [
|
||||
vue({
|
||||
base: "./",
|
||||
// 支持 Markdown 文件加载
|
||||
include: [/\.vue$/],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user