Compare commits

...

15 Commits

Author SHA1 Message Date
0f19b5b19b [add] first 2022-09-30 09:48:41 +08:00
yupi
e8f95a4d4d add 添加统计 2022-09-18 00:47:09 +08:00
yupi
02e38aca6a update README.md 2022-09-18 00:42:16 +08:00
yupi
ce8bd53827 add 新增圣光、透视技能 2022-09-18 00:41:14 +08:00
yupi
0cf6b3a9c2 add 支持保存自定义配置
add 新增 2 种难度
2022-09-18 00:20:52 +08:00
yupi
e57adbc824 fix 修复移出道具 bug 2022-09-18 00:09:22 +08:00
yupi
06bc94fa1b update 补充 README.md 文档 2022-09-17 23:59:15 +08:00
yupi
9b91330a83 update 补充 README.md 文档 2022-09-17 23:37:55 +08:00
yupi
fc74efac2d add 支持自定义动物图案、随机区情况 2022-09-17 23:32:26 +08:00
yupi
75d97b11c3 bug fix 2022-09-17 17:30:42 +08:00
yupi
9b8077c536 add 添加 4 种道具 2022-09-17 12:09:46 +08:00
yupi
200b2a47d0 add 添加游戏胜利标识 2022-09-16 22:41:26 +08:00
yupi
7d73c64d62 add 添加自定义游戏配置 2022-09-16 22:17:31 +08:00
yupili
d03ef31503 add 支持选关 2022-09-16 19:06:06 +08:00
yupili
c3da9ecf1b update README.md 2022-09-16 18:22:28 +08:00
20 changed files with 8710 additions and 2607 deletions

View File

@ -1,4 +1,34 @@
被羊了个羊虐了后,我自己做了一个! # 鱼了个鱼
> 被羊了个羊虐了百遍后,我自己做了一个!
在线体验https://yulegeyu.cn
游戏视频https://www.bilibili.com/video/BV1Pe411M7wh
相关文章https://mp.weixin.qq.com/s/D_I1Tq-ofhKhlp0rkOpaLA
游戏截图(自定义了图案):
![游戏截图](doc/img.png)
游戏特色:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

View File

@ -5,6 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/logo.png" /> <link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>鱼了个鱼</title> <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> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

6473
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^6.1.0",
"ant-design-vue": "^3.2.11", "ant-design-vue": "^3.2.11",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"pinia": "^2.0.19", "pinia": "^2.0.19",

View File

@ -3,39 +3,19 @@
<div class="content"> <div class="content">
<router-view /> <router-view />
</div> </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> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts"></script>
<style scoped> <style scoped>
#app { #app {
padding: 16px 16px 50px;
background: url("assets/bg.jpeg"); background: url("assets/bg.jpeg");
height: 100vh; padding: 16px 16px 50px;
min-height: 100vh;
background-size: 100% 100%; background-size: 100% 100%;
} }
.footer { .content {
background: rgba(0, 0, 0, 0.6); max-width: 480px;
color: #fff; margin: 0 auto;
padding: 12px;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
} }
</style> </style>

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
View 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>

View File

@ -1,9 +1,19 @@
import { RouteRecordRaw } from "vue-router"; import { RouteRecordRaw } from "vue-router";
import IndexPage from "../pages/IndexPage.vue"; import IndexPage from "../pages/IndexPage.vue";
import GamePage from "../pages/GamePage.vue";
import ConfigPage from "../pages/ConfigPage.vue";
export default [ export default [
{ {
path: "/", path: "/",
component: IndexPage, component: IndexPage,
}, },
{
path: "/game",
component: GamePage,
},
{
path: "/config",
component: ConfigPage,
},
] as RouteRecordRaw[]; ] as RouteRecordRaw[];

View File

@ -6,12 +6,12 @@
import { useGlobalStore } from "./globalStore"; import { useGlobalStore } from "./globalStore";
// @ts-ignore // @ts-ignore
import _ from "lodash"; import _ from "lodash";
import { nextTick, ref } from "vue"; import { ref } from "vue";
const useGame = () => { const useGame = () => {
const { gameConfig } = useGlobalStore(); const { gameConfig } = useGlobalStore();
// 游戏状态0 - 初始化, 1 - 进行中, 2 - 结束 // 游戏状态0 - 初始化, 1 - 进行中, 2 - 失败结束, 3 - 胜利
const gameStatus = ref(0); const gameStatus = ref(0);
// 各层块 // 各层块
@ -24,8 +24,15 @@ const useGame = () => {
const currSlotNum = ref(0); const currSlotNum = ref(0);
// 保存所有块(包括随机块) // 保存所有块(包括随机块)
const allBlocks: BlockType[] = [];
const blockData: Record<number, BlockType> = {}; const blockData: Record<number, BlockType> = {};
// 总块数
let totalBlockNum = ref(0);
// 已消除块数
let clearBlockNum = ref(0);
// 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21 // 总共划分 24 x 24 的格子,每个块占 3 x 3 的格子,生成的起始 x 和 y 坐标范围均为 0 ~ 21
const boxWidthNum = 24; const boxWidthNum = 24;
const boxHeightNum = 24; const boxHeightNum = 24;
@ -37,6 +44,17 @@ const useGame = () => {
// 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标) // 保存整个 "棋盘" 的每个格子状态(下标为格子起始点横纵坐标)
let chessBoard: ChessBoardUnitType[][] = []; let chessBoard: ChessBoardUnitType[][] = [];
// 操作历史(存储点击的块)
let opHistory: BlockType[] = [];
// region 技能相关
const isHolyLight = ref(false);
const canSeeRandom = ref(false);
// endregion
/** /**
* *
* @param width * @param width
@ -74,9 +92,12 @@ const useGame = () => {
console.log("块数单位", blockNumUnit); console.log("块数单位", blockNumUnit);
// 随机生成的总块数 // 随机生成的总块数
const totalRandomBlockNum = gameConfig.randomBlocks.reduce((pre, curr) => { const totalRandomBlockNum = gameConfig.randomBlocks.reduce(
(pre: number, curr: number) => {
return pre + curr; return pre + curr;
}, 0); },
0
);
console.log("随机生成的总块数", totalRandomBlockNum); console.log("随机生成的总块数", totalRandomBlockNum);
// 需要的最小块数 // 需要的最小块数
@ -86,12 +107,12 @@ const useGame = () => {
// 补齐到 blockNumUnit 的倍数 // 补齐到 blockNumUnit 的倍数
// e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18 // e.g. minBlockNum = 14, blockNumUnit = 6, 补到 18
let totalBlockNum = minBlockNum; totalBlockNum.value = minBlockNum;
if (totalBlockNum % blockNumUnit !== 0) { if (totalBlockNum.value % blockNumUnit !== 0) {
totalBlockNum = totalBlockNum.value =
(Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit; (Math.floor(minBlockNum / blockNumUnit) + 1) * blockNumUnit;
} }
console.log("总块数", totalBlockNum); console.log("总块数", totalBlockNum.value);
// 2. 初始化块,随机生成块的内容 // 2. 初始化块,随机生成块的内容
// 保存所有块的数组 // 保存所有块的数组
@ -99,15 +120,14 @@ const useGame = () => {
// 需要用到的动物数组 // 需要用到的动物数组
const needAnimals = gameConfig.animals.slice(0, gameConfig.typeNum); 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]); animalBlocks.push(needAnimals[i % gameConfig.typeNum]);
} }
// 打乱数组 // 打乱数组
const randomAnimalBlocks = _.shuffle(animalBlocks); const randomAnimalBlocks = _.shuffle(animalBlocks);
// 初始化 // 初始化
const allBlocks: BlockType[] = []; for (let i = 0; i < totalBlockNum.value; i++) {
for (let i = 0; i < totalBlockNum; i++) {
const newBlock = { const newBlock = {
id: i, id: i,
status: 0, status: 0,
@ -124,7 +144,7 @@ const useGame = () => {
// 3. 计算随机生成的块 // 3. 计算随机生成的块
const randomBlocks: BlockType[][] = []; const randomBlocks: BlockType[][] = [];
gameConfig.randomBlocks.forEach((randomBlock, idx) => { gameConfig.randomBlocks.forEach((randomBlock: number, idx: number) => {
randomBlocks[idx] = []; randomBlocks[idx] = [];
for (let i = 0; i < randomBlock; i++) { for (let i = 0; i < randomBlock; i++) {
randomBlocks[idx].push(allBlocks[pos]); randomBlocks[idx].push(allBlocks[pos]);
@ -134,7 +154,7 @@ const useGame = () => {
}); });
// 剩余块数 // 剩余块数
let leftBlockNum = totalBlockNum - totalRandomBlockNum; let leftBlockNum = totalBlockNum.value - totalRandomBlockNum;
// 4. 计算有层级关系的块 // 4. 计算有层级关系的块
const levelBlocks: BlockType[] = []; const levelBlocks: BlockType[] = [];
@ -265,18 +285,19 @@ const useGame = () => {
/** /**
* *
* @param block * @param block
* @param e
* @param randomIdx >= 0 * @param randomIdx >= 0
* @param force
*/ */
const doClickBlock = (block: BlockType, e: Event, randomIdx = -1) => { const doClickBlock = (block: BlockType, randomIdx = -1, force = false) => {
// 已经输了 / 已经被点击 / 有上层块,不能再点击 // 已经输了 / 已经被点击 / 有上层块(且非强制和圣光),不能再点击
if ( if (
currSlotNum.value >= gameConfig.slotNum || currSlotNum.value >= gameConfig.slotNum ||
block.status !== 0 || block.status !== 0 ||
block.lowerThanBlocks.length > 0 (block.lowerThanBlocks.length > 0 && !force && !isHolyLight.value)
) { ) {
return; return;
} }
isHolyLight.value = false;
// 修改元素状态为已点击 // 修改元素状态为已点击
block.status = 1; block.status = 1;
// 移除当前元素 // 移除当前元素
@ -287,9 +308,8 @@ const useGame = () => {
randomBlocksVal.value[randomIdx].length randomBlocksVal.value[randomIdx].length
); );
} else { } else {
// 删除节点 // 非随机区才可撤回
// @ts-ignore opHistory.push(block);
e.target.remove();
// 移除覆盖关系 // 移除覆盖关系
block.higherThanBlocks.forEach((higherThanBlock) => { block.higherThanBlocks.forEach((higherThanBlock) => {
_.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => { _.remove(higherThanBlock.lowerThanBlocks, (lowerThanBlock) => {
@ -325,6 +345,10 @@ const useGame = () => {
if (map[slotBlock.type] >= gameConfig.composeNum) { if (map[slotBlock.type] >= gameConfig.composeNum) {
// 块状态改为已消除 // 块状态改为已消除
slotBlock.status = 2; slotBlock.status = 2;
// 已消除块数 +1
clearBlockNum.value++;
// 清除操作记录,防止撤回
opHistory = [];
return; return;
} }
newSlotAreaVal[tempSlotNum++] = slotBlock; newSlotAreaVal[tempSlotNum++] = slotBlock;
@ -338,6 +362,9 @@ const useGame = () => {
alert("你输了"); alert("你输了");
}, 2000); }, 2000);
} }
if (clearBlockNum.value >= totalBlockNum.value) {
gameStatus.value = 3;
}
}; };
/** /**
@ -353,6 +380,111 @@ const useGame = () => {
gameStatus.value = 1; 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 { return {
gameStatus, gameStatus,
levelBlocksVal, levelBlocksVal,
@ -360,8 +492,20 @@ const useGame = () => {
slotAreaVal, slotAreaVal,
widthUnit, widthUnit,
heightUnit, heightUnit,
currSlotNum,
opHistory,
totalBlockNum,
clearBlockNum,
isHolyLight,
canSeeRandom,
doClickBlock, doClickBlock,
doStart, doStart,
doShuffle,
doBroke,
doRemove,
doRevert,
doHolyLight,
doSeeRandom,
}; };
}; };

175
src/core/gameConfig.ts Normal file
View 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,
};

View File

@ -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;

View File

@ -1,49 +1,5 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { defaultGameConfig } from "./gameConfig";
const defaultGameConfig = {
// 槽容量
slotNum: 7,
// 需要多少个一样块的才能合成
composeNum: 3,
// 动物类别数
typeNum: 12,
// 每层块数(大致)
levelBlockNum: 30,
// 边界收缩步长
borderStep: 1,
// 总层数(最小为 2
levelNum: 8,
// 最上层块数
topBlockNum: 40,
// 最下层块数最小值
minBottomBlockNum: 20,
// 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块)
randomBlocks: [8, 8],
// 动物数组
animals: [
"🐔",
"🐟",
"🦆",
"🐶",
"🐱",
"🐴",
"🐑",
"🐦",
"🐧",
"🐊",
"🐺",
"🐒",
"🐳",
"🐬",
"🐢",
"🦖",
"🦒",
"🦁",
"🐍",
"🐭",
"🐂",
],
};
/** /**
* *
@ -52,6 +8,7 @@ const defaultGameConfig = {
*/ */
export const useGlobalStore = defineStore("global", { export const useGlobalStore = defineStore("global", {
state: () => ({ state: () => ({
customConfig: { ...defaultGameConfig },
gameConfig: { ...defaultGameConfig }, gameConfig: { ...defaultGameConfig },
}), }),
getters: {}, getters: {},
@ -67,6 +24,12 @@ export const useGlobalStore = defineStore("global", {
}, },
}, },
actions: { actions: {
setGameConfig(gameConfig: GameConfigType) {
this.gameConfig = gameConfig;
},
setCustomConfig(customConfig: GameConfigType) {
this.customConfig = customConfig;
},
reset() { reset() {
this.$reset(); this.$reset();
}, },

36
src/core/type.d.ts vendored
View File

@ -22,3 +22,39 @@ interface ChessBoardUnitType {
// 放到当前格子里的块(层级越高下标越大) // 放到当前格子里的块(层级越高下标越大)
blocks: BlockType[]; 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
View 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
View 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>

View File

@ -1,119 +1,110 @@
<template> <template>
<div id="indexPage"> <div id="indexPage">
<a-row align="center"> <h1>🐟 鱼了个鱼</h1>
<div style="margin-bottom: 16px">低配版羊了个羊小游戏仅供消遣</div>
<a-button <a-button
type="primary"
block block
style="margin-bottom: 16px" style="margin-bottom: 16px"
@click="doStart" @click="toGamePage(easyGameConfig)"
> >
开始 简单模式
</a-button> </a-button>
<!-- 分层选块 --> <a-button
<div class="level-board"> block
<div style="margin-bottom: 16px"
v-for="(block, idx) in levelBlocksVal" @click="toGamePage(middleGameConfig)"
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> </a-button>
</div> <a-button
<!-- 随机选块 --> block
<div class="random-board"> style="margin-bottom: 16px"
<div @click="toGamePage(hardGameConfig)"
v-for="(randomBlock, index) in randomBlocksVal"
:key="index"
class="random-area"
> >
<div 困难模式
v-if="randomBlock.length > 0" </a-button>
class="block" <a-button
@click="(e) => doClickBlock(randomBlock[0], e, index)" block
style="margin-bottom: 16px"
@click="toGamePage(lunaticGameConfig)"
> >
{{ randomBlock[0].type }} 地狱模式
</div> </a-button>
<div <a-button
v-for="num in Math.max(randomBlock.length - 1, 0)" block
:key="num" style="margin-bottom: 16px"
class="block disabled" @click="toGamePage(skyGameConfig)"
></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 }} 天域模式
</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>
</div> </div>
</div> </div>
</a-row>
</div>
</template> </template>
<script setup lang="ts"> <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 { const router = useRouter();
gameStatus,
levelBlocksVal, const { setGameConfig } = useGlobalStore();
randomBlocksVal,
slotAreaVal, const toGamePage = (config?: GameConfigType) => {
widthUnit, if (config) {
heightUnit, setGameConfig(config);
doClickBlock, router.push("/game");
doStart, } else {
} = useGame(); router.push("/config");
}
};
</script> </script>
<style scoped> <style scoped>
.level-board { #indexPage {
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; text-align: center;
vertical-align: top;
display: inline-block;
} }
.disabled { .footer {
background: grey; background: rgba(0, 0, 0, 0.6);
cursor: not-allowed; color: #fff;
padding: 12px;
text-align: center;
position: fixed;
bottom: 0;
left: 0;
right: 0;
} }
</style> </style>

View File

@ -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>

View File

@ -1,10 +1,11 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { defineConfig } from "vite";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue({ vue({
base: "./",
// 支持 Markdown 文件加载 // 支持 Markdown 文件加载
include: [/\.vue$/], include: [/\.vue$/],
}), }),

2909
yarn.lock

File diff suppressed because it is too large Load Diff