原始的扫雷使用 canvas 也可以实现,但使用 g2 5.0 更快捷,实现的目的是为了熟悉 g2 5.0 的配置和交互,具体的方法不进行说明。
import { Chart } from '@antv/g2';
const chart = new Chart({
container: 'container',
theme: 'classic',
height: 640,
});
const x = 24;
const y = 24;
// 炸弹数
const bombs = 90;
function getIndex(bombX, bombY, x) {
return bombX + bombY * x;
}
// 开启所有
function openAll(data) {
return data.map(i => ({ ...i, open: true }));
}
// 开启空边缘的cell
function openEmpty(data, idx, x) {
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
const index = idx + i + j * x;
const d = data[index];
if (d && !d.open) {
d.open = true;
if (d.value === 0 && index !== idx) {
openEmpty(data, index, x);
}
};
}
}
}
// 创建初始化空数据
function createData(x, y) {
return new Array(x * y).fill(0).map((i, idx) => {
return {
// x 轴位置
x: idx % x + 1,
// y 轴位置
y: Math.floor(idx / x) + 1,
// 所含内容
value: 0,
// 当前开/关状态
open: false,
// 是否为炸弹
bomb: false,
}
});
}
// 创建雷
function createBombs(data, x, y, bombs) {
const bombData = [...data];
for (let i = 0; i < bombs; i++) {
const bombX = Math.floor(Math.random() * x);
const bombY = Math.floor(Math.random() * y);
const idx = getIndex(bombX, bombY, x);
const d = data[idx];
if (d.bomb) {
i--;
} else {
bombData[idx] = {
...d,
bomb: true,
};
}
}
return bombData;
}
// 创建数值
function createValues(data, x) {
return data.map((item, idx) => {
if (item.bomb) return item;
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
const d = data?.[idx + i + j * x];
if (d?.bomb) {
item.value++;
}
}
}
return item;
});
}
// 创建初始化数据
function getDefaultData(x = 12, y = 12, bombs = 10) {
const data = createData(x, y);
const bombsData = createBombs(data, x, y, bombs);
return createValues(bombsData, x);
}
let data = getDefaultData(x, y, bombs);
chart
.text()
.data([{ idx: -1, text: 'start' }])
.encode('x', 'idx')
.encode('y', -1)
.encode('text', 'text')
.style({
fontSize: 20,
});
const cells = chart
.cell()
.data(data)
.scale('color', { type: 'ordinal' })
.encode('x', 'x')
.encode('y', 'y')
.encode('value', 'value')
.style('stroke', '#000')
.style('inset', 2)
.style('lineWidth', .3)
.style('cursor', (v) => v.open ? '' : 'pointer')
.label({
position: 'inside',
formatter: (v, d) => d.open && d.value || '',
style: {
fill: '#000'
}
})
.axis(false)
.style('fill', (v) => v.open ? (v.bomb ? 'red' : '#fff') : '#eee')
.tooltip(false)
.animate(false);
// 点击交互cell
chart.on('cell:click', (d) => {
const { x: bombX, y: bombY, bomb, open, value } = d.data.data;
if (open) return;
if (bomb) return cells.changeData(openAll(data));
const idx = getIndex(bombX - 1, bombY - 1, x);
if (value === 0) {
openEmpty(data, idx, x);
};
data[idx].open = true;
cells.changeData([...data]);
});
// 点击交互text
chart.on('text:click', () => {
data = getDefaultData(x, y, bombs);
cells.changeData(data);
});
chart.render();
简单说明
通过 mask.cell 创建每一个格子,通过数据记录所有格子属性,格子的展示效果通过 style 和 label 的回调显示。
然后是交互,cell:click 为盒子的点击交互,通过点击 cell 的属性进行游戏运算的操作。text:click 为文本点击交互,用于重新开始。所有显示的改变是通过 mask.changeData 实现,chart.changeData 会更新所有图表下 mask 的数据,但因为 text 的 data 和主要更新的数据不一致,所以直接 cells.changeData 进行格子的数据更新。