diff --git a/README.md b/README.md index 1443333..903dc1f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ -# 被羊了个羊虐了后,我自己做了一个! +# 鱼了个鱼 + +> 被羊了个羊虐了百遍后,我自己做了一个! 在线体验:https://yulegeyu.cn +玩法: +1. 支持选关 +2. 支持自定义难度(待更新) +3. 无限道具(待更新) diff --git a/src/configs/routes.ts b/src/configs/routes.ts index 4002532..de29705 100644 --- a/src/configs/routes.ts +++ b/src/configs/routes.ts @@ -1,9 +1,14 @@ import { RouteRecordRaw } from "vue-router"; import IndexPage from "../pages/IndexPage.vue"; +import GamePage from "../pages/GamePage.vue"; export default [ { path: "/", component: IndexPage, }, + { + path: "/game", + component: GamePage, + }, ] as RouteRecordRaw[]; diff --git a/src/core/gameConfig.ts b/src/core/gameConfig.ts new file mode 100644 index 0000000..5c7ded1 --- /dev/null +++ b/src/core/gameConfig.ts @@ -0,0 +1,131 @@ +// 动物数组 +const animals = [ + "🐔", + "🐟", + "🦆", + "🐶", + "🐱", + "🐴", + "🐑", + "🐦", + "🐧", + "🐊", + "🐺", + "🐒", + "🐳", + "🐬", + "🐢", + "🦖", + "🦒", + "🦁", + "🐍", + "🐭", + "🐂", +]; + +export const defaultGameConfig: GameConfig = { + // 槽容量 + slotNum: 7, + // 需要多少个一样块的才能合成 + composeNum: 3, + // 动物类别数 + typeNum: 12, + // 每层块数(大致) + levelBlockNum: 24, + // 边界收缩步长 + borderStep: 1, + // 总层数(最小为 2) + levelNum: 6, + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: [8, 8], + // 动物数组 + animals, +}; + +/** + * 简单难度 + */ +export const easyGameConfig: GameConfig = { + // 槽容量 + slotNum: 7, + // 需要多少个一样块的才能合成 + composeNum: 3, + // 动物类别数 + typeNum: 8, + // 每层块数(大致) + levelBlockNum: 10, + // 边界收缩步长 + borderStep: 1, + // 总层数(最小为 2) + levelNum: 6, + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: [4, 4], + // 动物数组 + animals, +}; + +/** + * 中等难度 + */ +export const middleGameConfig: GameConfig = { + // 槽容量 + slotNum: 7, + // 需要多少个一样块的才能合成 + composeNum: 3, + // 动物类别数 + typeNum: 10, + // 每层块数(大致) + levelBlockNum: 12, + // 边界收缩步长 + borderStep: 1, + // 总层数(最小为 2) + levelNum: 7, + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: [5, 5], + // 动物数组 + animals, +}; + +/** + * 困难难度 + */ +export const hardGameConfig: GameConfig = { + // 槽容量 + slotNum: 7, + // 需要多少个一样块的才能合成 + composeNum: 3, + // 动物类别数 + typeNum: 12, + // 每层块数(大致) + levelBlockNum: 16, + // 边界收缩步长 + borderStep: 1, + // 总层数(最小为 2) + levelNum: 8, + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: [6, 6], + // 动物数组 + animals, +}; + +/** + * 地狱难度 + */ +export const lunaticGameConfig: GameConfig = { + // 槽容量 + slotNum: 7, + // 需要多少个一样块的才能合成 + composeNum: 3, + // 动物类别数 + typeNum: 14, + // 每层块数(大致) + levelBlockNum: 20, + // 边界收缩步长 + borderStep: 2, + // 总层数(最小为 2) + levelNum: 10, + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: [8, 8], + // 动物数组 + animals, +}; diff --git a/src/core/gameV1.ts b/src/core/gameV1.ts deleted file mode 100644 index 22c6232..0000000 --- a/src/core/gameV1.ts +++ /dev/null @@ -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([]); - // 随机区块 - const randomBlocksVal = ref([]); - // 插槽区 - const slotAreaVal = ref([]); - // 当前槽占用数 - const currSlotNum = ref(0); - - // 保存所有块(包括随机块) - const blockData: Record = {}; - - // 总共划分 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 = {}; - // 去除空槽 - 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; diff --git a/src/core/globalStore.ts b/src/core/globalStore.ts index c57df76..0f4f6d4 100644 --- a/src/core/globalStore.ts +++ b/src/core/globalStore.ts @@ -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: GameConfig) { + this.gameConfig = gameConfig; + }, + setCustomConfig(customConfig: GameConfig) { + this.customConfig = customConfig; + }, reset() { this.$reset(); }, diff --git a/src/core/type.d.ts b/src/core/type.d.ts index a0c1e7b..feccf2e 100644 --- a/src/core/type.d.ts +++ b/src/core/type.d.ts @@ -22,3 +22,29 @@ interface ChessBoardUnitType { // 放到当前格子里的块(层级越高下标越大) blocks: BlockType[]; } + +/** + * 游戏配置 + */ +interface GameConfig { + // 槽容量 + slotNum: number; + // 需要多少个一样块的才能合成 + composeNum: number; + // 动物类别数 + typeNum: number; + // 每层块数(大致) + levelBlockNum: number; + // 边界收缩步长 + borderStep: number; + // 总层数(最小为 2) + levelNum: number; + // 随机区块数(数组长度代表随机区数量,值表示每个随机区生产多少块) + randomBlocks: number[]; + // 动物数组 + animals: string[]; + // 最上层块数(已废弃) + // topBlockNum: 40, + // 最下层块数最小值(已废弃) + // minBottomBlockNum: 20, +} diff --git a/src/pages/GamePage.vue b/src/pages/GamePage.vue new file mode 100644 index 0000000..573c78a --- /dev/null +++ b/src/pages/GamePage.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/pages/IndexPage.vue b/src/pages/IndexPage.vue index a8d0adb..ec76031 100644 --- a/src/pages/IndexPage.vue +++ b/src/pages/IndexPage.vue @@ -1,119 +1,65 @@ diff --git a/src/pages/IndexPageV1.vue b/src/pages/IndexPageV1.vue deleted file mode 100644 index 6e552cd..0000000 --- a/src/pages/IndexPageV1.vue +++ /dev/null @@ -1,507 +0,0 @@ - - - - -