Appearance
🌈连连看
玩法
我对连连看的理解应该不会比你多 😉。
只是在变态模式下可能存在看不清的情况,建议使用游戏 右上角按钮
开启悬停显示 RGB
颜色功能;
不要玩 变态
神仙
难度
主要是眼睛受不了
算法
连接,消除算法
基于 广度优先搜索
算法,这个基础算法没什么好多说的;
为了短暂显示连接路径,因此需要记录下路径。
色值算法
平均分割 RGB
颜色,并选取其中的某一段(相邻色值更加难以辨析);
ts
import _ from "lodash";
export const divideColor = (count = 1) => {
let colors = [0xff0000, 0x00ff00, 0x0000ff];
if (count < 2) return colors.map(c => `#${c.toString(16).padStart(6, "0")}`);
colors.push(colors[0]);
let res = [];
for (let i = 0; i < colors.length - 1; i++) {
const cs = colors[i],
ce = colors[i + 1],
rg = (((ce >> 16) & 0xff) - ((cs >> 16) & 0xff)) / count,
gg = (((ce >> 8) & 0xff) - ((cs >> 8) & 0xff)) / count,
bg = ((ce & 0xff) - (cs & 0xff)) / count;
let c = count;
res.push(colors[i]);
while (--c > 0) {
const ls = res[res.length - 1],
i = count - c;
res.push(
(Math.floor(((cs >> 16) & 0xff) + i * rg) << 16) +
(Math.floor(((cs >> 8) & 0xff) + i * gg) << 8) +
(Math.floor(((cs >> 0) & 0xff) + i * bg) << 0)
);
}
}
return res.map(c => `#${c.toString(16).padStart(6, "0")}`);
};
export const sepColorsByDepth = (depth, color = [0xff0000, 0x00ff00, 0x0000ff], deep = 0) => {
if (depth === deep) return color.map(c => "#" + c.toString(16).padStart(6, "0"));
const c = [];
for (let i = 0; i < color.length; i++) {
const fst = color[i],
sec = color[(i + 1) % color.length];
c.push(fst);
const r = Math.round(_.mean([(fst & 0xff0000) / 0x10000, (sec & 0xff0000) / 0x10000]));
const g = Math.round(_.mean([(fst & 0xff00) / 0x100, (sec & 0xff00) / 0x100]));
const b = Math.round(_.mean([fst & 0xff, sec & 0xff]));
c.push(r * 0x10000 + g * 0x100 + b);
}
return sepColorsByDepth(depth, c, deep + 1);
};
源码
vue
<template>
<div class="link-link flex-column flex-center-all" @contextmenu.prevent="_ => _">
<div class="flex-column">
<div class="flex-horiz mb-xs">
<div class="">
<a-button type="primary" @click="startGame">重新开始</a-button>
</div>
<div class="ml-xs">
<a-select
:options="levelsOptions"
v-model="level"
style="width: 100px"
@update:value="startGame"
></a-select>
</div>
<div class="flex-grow"></div>
<div class="">
<a-tooltip
placement="top-end"
trigger="hover"
content="开启后将鼠标停留在色块上,将显示颜色色值。"
>
<a-switch v-model="showColorTitle"></a-switch>
</a-tooltip>
</div>
</div>
<div class="game-container">
<div class="flex-horiz grid-row" v-for="row in map">
<div class="grid-box" v-for="grid in row" :class="gridBoxClass(grid)">
<div
class="grid border-box"
:style="gridStyle(grid)"
:title="showColorTitle && grid.color ? grid.color : undefined"
@mousedown="onMouseDown(grid)"
></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { divideColor } from "../../utils/color.util";
import { getRandomRangeInArray } from "../../utils/array.util";
import _ from "lodash";
class Grid {
constructor(row, col) {
this.row = row;
this.col = col;
}
row: number;
col: number;
color: string;
}
export default defineComponent({
name: "link-link",
props: {},
setup(props: any, ctx: any) {
const ROW = 10,
COL = 16,
SIZE = ROW * COL,
PAIR = 20;
const DIR = [
[1, -1, 0, 0],
[0, 0, 1, -1],
];
const level = ref(0);
const levels: [string, number][] = [
["初级", 1],
["中级", 3],
["高级", 10],
["变态", 20],
["神仙", 50],
];
const getColors = () => getRandomRangeInArray(divideColor(levels[level.value][1]), PAIR, true);
const createMap = (): Grid[][] => {
return new Array(ROW).fill(0).map((_, row) =>
new Array(COL).fill(0).map((_, col) => {
return new Grid(row, col);
})
);
};
const refreshMapColors = (map: Grid[][]) => {
const colors = _.shuffle(getColors());
const pairGrids = _.sampleSize(_.flatten(map), PAIR * 2);
for (let i = 0; i < PAIR; i++) {
pairGrids[i * 2].color = pairGrids[i * 2 + 1].color = colors[i];
}
};
const gameStatus = ref(0),
selectedGrid = ref(null),
showColorTitle = ref(false);
const map = ref(createMap());
const findPath = (start, end) => {
if (start.color !== end.color) return [];
const queue = [[start, [start], -1, 0]];
while (queue.length) {
const [grid, path, dir, changeC] = queue.shift();
if (changeC > 3) continue;
if (grid === end) return path;
if (grid.color && grid !== start) continue;
for (let i = 0; i < 4; i++) {
const rr = grid.row + DIR[0][i],
cc = grid.col + DIR[1][i];
if (rr < 0 || rr >= ROW || cc < 0 || cc >= COL) continue;
const nextGrid = map.value[rr][cc];
if (path.find(g => g === nextGrid)) continue;
queue.push([nextGrid, path.concat([nextGrid]), i, i === dir ? changeC : changeC + 1]);
}
}
return [];
};
const startGame = () => {
map.value = createMap();
refreshMapColors(map.value);
gameStatus.value = 2;
};
startGame();
const onMouseDown = grid => {
if (!grid.color || selectedGrid.value === grid) return;
if (!selectedGrid.value) return (selectedGrid.value = grid);
const path = findPath(selectedGrid.value, grid);
if (!path.length) return (selectedGrid.value = grid);
const p = path.slice(1, -1);
selectedGrid.value.color = grid.color = "";
selectedGrid.value = null;
setTimeout(() => path.map(g => (g.color = "#e3e3e3")));
setTimeout(() => path.map(g => (g.color = "")), 50);
};
return {
map,
level,
levels,
ROW,
COL,
SIZE,
PAIR,
showColorTitle,
levelsOptions: levels.map((lv, value) => ({ label: lv[0], value })),
onMouseDown,
startGame,
gridStyle: grid => ({
"background-color": grid.color || "#fafafa",
}),
gridBoxClass: grid => [
grid.color ? "active" : "",
selectedGrid.value === grid ? "focus" : "",
],
};
},
});
</script>
<style lang="less" scoped>
.link-link {
}
.game-container {
border: 3px solid #ccc;
padding: 5px;
}
.grid-box {
margin: 0.5px;
@size: 40px;
.grid {
height: @size;
width: @size;
}
&.active .grid {
cursor: pointer;
}
&:not(.active) .grid {
transition: 0.6s background-color ease;
}
&.focus {
border: 2px solid #1890ff;
transform: scale(1.3);
.grid {
border: 1px solid #fff;
height: @size - 4;
width: @size - 4;
}
}
}
</style>