g2 5.0 实现简单扫雷

103 阅读1分钟

原始的扫雷使用 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 进行格子的数据更新。