关于前端:写个羊了个羊der了个der

一、前言

随着羊了个羊爆火,最近也看到他人写了狗了个狗、猪了个猪之类的,本人也手痒了,因为懒得找图标素材,就从 antd icon 中轻易 copy 了一些,名字就叫 der 了个 der 吧,纯属娱乐

github地址

点击体验

二、实现

咱们用类Panel保护放方块的面板,用类ResolveBar来保护下方打消条

1. PanelResolveBar定义

1.1 首先在Panel中定义可配置的数据

class Panel extends EventEmitter {
  private panelWidth; // 面板宽度
  private panelHeight; // 面板高度
  private cubeTypeCount; // 方块品种数,
  private cubeCount; // 方块数量
  private slotCount; // 下方打消条可放多少个方块
  private cubeWidth; // 方块宽度,长宽一样
  private destroyCount; // 同品种方块多少个可打消 默认就是3

  constructor(config: IConfig) {
    super();
    this.panelWidth = config.panelWidth;
    this.panelHeight = config.panelHeight;
    this.cubeTypeCount = config.cubeTypeCount;
    this.cubeCount = config.cubeCount;
    this.slotCount = config.slotCount;
    this.cubeWidth = config.cubeWidth;
    this.destroyCount = config.destroyCount || 3;
  }
}

1.2 增加自定义数据

class Panel extends EventEmitter {
  ...
  // 自定义数据
  private cubesInfo: ICubeInfo[] = []; // 小方块详细信息
  private coordinateRange: ICoordinateRange; // 坐标范畴
  private slot: ResolveBar;
  constructor(config: IConfig) {
    ...
    // 坐标范畴
    this.coordinateRange = {
      x: [this.cubeWidth / 2, this.panelWidth - this.cubeWidth / 2],
      y: [this.cubeWidth / 2, this.panelHeight - this.cubeWidth / 2],
    }

    this.slot = new ResolveBar({
      cubeCount: this.slotCount,
      destroyCount: this.destroyCount
    })
  }
}

具体解读:

  • cubesInfo:保护所有在面板中的方块

    interface ICubeInfo {
      coordinate: [number, number]; // 方块坐标 x,y
      cubeTypeKey: number; // 方块品种,咱们就用 数字标识 0,1,2,3,4,...
      zIndex: number; // 搁置的层级
      id: string; // 惟一标识,会用随机数标识
      coveredCubes: ICubeInfo[]; // 被哪些方块笼罩
    }
  • coordinateRange: 方块可搁置的坐标范畴

    留神:x 和 y 各有一个区间
    因为方块是有宽高的,而坐标只是一个点,因为搁置的范畴并不是间接面板的快高,而是要减掉本身宽高的一半

    export interface ICoordinateRange {
      x: [number, number];
      y: [number, number];
    }
    
    this.coordinateRange = {
      x: [this.cubeWidth / 2, this.panelWidth - this.cubeWidth / 2],
      y: [this.cubeWidth / 2, this.panelHeight - this.cubeWidth / 2],
    };
  • slot就是打消条
    咱们间接 new 一个 ResolveBar, 打消条保护的数据比较简单

    class ResolveBar {
      private cubeCount;
      private destroyCount;
      private keyCount: Map<number, number>; // 统计不同方块品种的数量
      private slotArr: ICubeInfo[]; // 搁置的方块列表,是有程序的
      constructor(config: IResolveBarConfig) {
        this.cubeCount = config.cubeCount;
        this.destroyCount = config.destroyCount;
        this.keyCount = new Map();
        this.slotArr = [];
      }

2. drawPanel: 在panel中搁置方块

咱们心愿方块搁置的坐标、以及搁置方块的品种都是是随机的,这须要一些计算

2.1 方块品种随机计算方法

首先方块的品种的随机的,这里思考了不同类型方块的数量要尽量大抵相等。

因而首先咱们平均分配,先计算出每种类型放多少个方块(当然并不能齐全均匀,须要把余数先排除),并放到数组averageCubeTypeKeyArr

// 能够放多少组, 默认每组3个方块, 就是destroyCount
const groupCount = Math.floor(this.cubeCount / this.destroyCount);
// 每种类型放多少组
const groupCountOfPerType = Math.floor(groupCount / this.cubeTypeCount);

const averageCubeTypeKeyArr = new Array(
  groupCountOfPerType * this.cubeTypeCount * this.destroyCount
);

for (let i = 0; i < this.cubeTypeCount; i++) {
  const start = i * this.destroyCount * groupCountOfPerType;
  const end = start + this.destroyCount * groupCountOfPerType;
  averageCubeTypeKeyArr.fill(i, start, end);
}

接下来,就是要解决余数,余数不能平均分配,咱们就只能采取抽签形式

// 还剩多少组能够放, 剩下的随机放
const leftGroupCount = groupCount % this.cubeTypeCount;

const cubeTypeArr = Array.from(
  new Array(this.cubeTypeCount),
  (item, index) => index
);

const leftCubeTypeKeyArr = new Array(leftGroupCount * this.destroyCount);
const choujiang = new Choujiang(cubeTypeArr);
for (let i = 0; i < leftGroupCount; i++) {
  let key = choujiang.choujiang();
  const start = i * this.destroyCount;
  const end = start + this.destroyCount;
  leftCubeTypeKeyArr.fill(key, start, end);
}

这里用到了一个不反复抽奖逻辑,间接看代码

class Choujiang {
  private cacheList: Array<any> = [];
  todos: Array<any> = [];
  deleteIndex: number | undefined;

  constructor(list: Array<any>) {
    this.cacheList = list;
  }

  choujiang(): any {
    if (this.deleteIndex) {
      this.todos.splice(this.deleteIndex, 1);
    }
    const count = this.todos.length - 1;
    const index = Math.round(count * Math.random());
    this.deleteIndex = index;

    console.log(index + "中奖了");

    return this.todos[index];
  }
}

而后,咱们把平均分配的的方块类型和通过抽奖形式调配的方块类型拼到一块,并且通过随机排序的形式把数组程序打乱,就失去了最终的方块品种数组

const finalCubeTypeKeyArr = [
  ...averageCubeTypeKeyArr,
  ...leftCubeTypeKeyArr,
].sort(() => {
  return Math.random() < 0.5 ? 1 : -1;
});

整体的方块类型数组计算如下

private calcCubeTypeKey() {
  // 能够放多少组, 默认每组3个方块
  const groupCount = Math.floor(this.cubeCount / this.destroyCount);
  // 每种类型放多少组
  const groupCountOfPerType = Math.floor(groupCount / this.cubeTypeCount);

  const averageCubeTypeKeyArr = new Array(groupCountOfPerType * this.cubeTypeCount * this.destroyCount);

  for (let i = 0; i < this.cubeTypeCount; i++) {
    const start = i * this.destroyCount * groupCountOfPerType;
    const end = start + this.destroyCount * groupCountOfPerType;
    averageCubeTypeKeyArr.fill(i, start, end);
  }

  // 还剩多少组能够放, 剩下的随机放
  const leftGroupCount = groupCount % this.cubeTypeCount;

  const cubeTypeArr = Array.from(new Array(this.cubeTypeCount), (item, index) => index);

  const leftCubeTypeKeyArr = new Array(leftGroupCount * this.destroyCount);
  const choujiang = new Choujiang(cubeTypeArr);
  for (let i = 0; i < leftGroupCount; i++) {
    let key = choujiang.choujiang();
    const start = i * this.destroyCount;
    const end = start + this.destroyCount;
    leftCubeTypeKeyArr.fill(key, start, end);
  }

  // 拼接并打乱
  const finalCubeTypeKeyArr = [...averageCubeTypeKeyArr, ...leftCubeTypeKeyArr].sort(() => {
    return Math.random() < 0.5 ? 1 : -1
  })

  return finalCubeTypeKeyArr;
}

2.2 坐标的随机算法

坐标的随机比较简单

const randomX =
  this.coordinateRange.x[0] +
  Math.random() * (this.coordinateRange.x[1] - this.coordinateRange.x[0]);
const randomY =
  this.coordinateRange.y[0] +
  Math.random() * (this.coordinateRange.y[1] - this.coordinateRange.y[0]);

2.3 残缺的drawPanel方块办法如下

public drawPanel() {
  const cubeTypeKeyArr = this.calcCubeTypeKey();

  for (let i = 0; i < this.cubeCount; i++) {
    const randomX = this.coordinateRange.x[0] + Math.random() * (this.coordinateRange.x[1] - this.coordinateRange.x[0]);
    const randomY = this.coordinateRange.y[0] + Math.random() * (this.coordinateRange.y[1] - this.coordinateRange.y[0]);
    const cubeTypeKey = cubeTypeKeyArr[i];
    const id = Math.random().toString();

    const currentCube: ICubeInfo = {
      id,
      zIndex: i,
      coordinate: [randomX, randomY],
      cubeTypeKey,
      coveredCubes: []
    };

    // 解决笼罩逻辑
    this.calcCoveredCubes(currentCube);

    this.cubesInfo.push(currentCube);
  }

  this.emit('updatePanelCubes', this.cubesInfo);
}

须要留神到calcCoveredCubes这个办法,这是解决方块间接笼罩的,因为每搁置一个方块,咱们要思考是否笼罩了曾经在面板中的某些方块

逻辑就是如果笼罩了某个方块,咱们就把以后新增加的方块push到被笼罩方块的coveredCubes

private calcCoveredCubes(currentCube: ICubeInfo) {
  const { coordinate } = currentCube;
  this.cubesInfo.forEach(item => {
    if (this.isCover(coordinate, item.coordinate)) {
      item.coveredCubes.push(currentCube)
    }
  })
}

private isCover(coordinate2: [number, number], coordinate1: [number, number]) {
  const dx = Math.abs(coordinate2[0] - coordinate1[0]) - this.cubeWidth;
  const dy = Math.abs(coordinate2[1] - coordinate1[1]) - this.cubeWidth;

  return dx < 0 && dy < 0
}

3 removeCube 方块打消

接下来就是打消方块了,当咱们点击面板某个没有被笼罩的方块时,须要进行打消逻辑,这里有三件事件要做

  1. 要将以后方块从面板中打消掉
  2. 遍历残余的方块,如果它的coveredCubes存在此方块,也要删掉
  3. 在打消条中增加这个方块

3.1 面板中方块打消逻辑如下:

public removeCube(id: string) {
  const currentCubeIndex = this.cubesInfo.findIndex(item => item.id === id);
  if (currentCubeIndex === -1) {
    throw new Error('找不到以后id')
  }

  const currentCube = this.cubesInfo[currentCubeIndex]
  if (currentCube.coveredCubes.length > 0) {
    throw new Error('以后cube被笼罩,不能移除')
  }
  // 从面板中删除方块
  this.cubesInfo.splice(currentCubeIndex, 1);

  // 更新残余方块的coveredCubes
  this.cubesInfo.forEach(item => {
    const findCubeIndex = item.coveredCubes.findIndex(item => item.id === id);
    if (findCubeIndex > -1) {
      item.coveredCubes.splice(findCubeIndex, 1)
    }
  })

  this.emit('updatePanelCubes', this.cubesInfo);
  // 打消条中增加方块
  const slotArr = this.slot.add(currentCube);

  this.emit('updateSlotCubes', slotArr)
}

3.2 打消条方块增加

在上一步中开端咱们也看到,方块被从面板删除后,同时也要将此方块增加到打消条中

打消条中增加方块逻辑如下,

  1. 查看要增加的方块品种曾经存在多少个
  2. 如果曾经存在,并且再加一个就须要打消了,那就间接把打消条中此品种的方块全删掉,实现打消逻辑
  3. 如果曾经存在,然而还没到打消的个数,那就找到此品种方块最初呈现的地位,把以后方块插进去
  4. 如果此品种方块并不存在,咱们把以后方块间接push进去就好了

具体实现逻辑如下

public add(cube: ICubeInfo) {
  if (this.slotArr.length >= this.cubeCount) {
    return;
  }

  const findKeyCount = this.keyCount.get(cube.cubeTypeKey) || 0;

  if (findKeyCount >= this.destroyCount - 1) {
    const index = this.slotArr.findIndex(item => item.cubeTypeKey === cube.cubeTypeKey);

    this.slotArr.splice(index, findKeyCount);

    this.keyCount.set(cube.cubeTypeKey, 0);
  } else {
    const lastIndex = this.slotArr.map(item => item.cubeTypeKey).lastIndexOf(cube.cubeTypeKey);
    if (lastIndex > -1) {
      this.slotArr.splice(lastIndex + 1, 0, cube);
    } else {
      this.slotArr.push(cube);
    }

    this.keyCount.set(cube.cubeTypeKey, findKeyCount + 1);
  }

  return this.slotArr
}

三、集成

const bodyWidth = document.body.clientWidth;
const defaultConfig = {
  cubeTypeCount: 15,
  cubeCount: 60,
  slotCount: 7,
  destroyCount: 3,
  cubeWidth: Math.floor(bodyWidth / (bodyWidth > 750 ? 20 : 8))
}

function App() {
  const app = useRef<any>(null);
  const pig = useRef<Pig | null>(null);
  const stepCount = useRef(0);
  const [cubesInfo, setCubesInfo] = useState<ICubeInfo[]>([]);
  const [slotCubesInfo, setSlotCubesInfo] = useState<ICubeInfo[]>([]);

  const onMountApp = (ele: HTMLDivElement) => {
    app.current = ele;
  }

  const newPig = () => {
    setCubesInfo([])
    setSlotCubesInfo([])
    stepCount.current = 0;
    if (app.current) {
      pig.current = new Pig({
        panelWidth: app.current.clientWidth,
        panelHeight: app.current.clientHeight,
        cubeWidth: defaultConfig.cubeWidth,
        cubeTypeCount: defaultConfig.cubeTypeCount,
        cubeCount: defaultConfig.cubeCount,
        slotCount: defaultConfig.slotCount,
        destroyCount: defaultConfig.destroyCount
      });

      pig.current.on('updatePanelCubes', (cubes: ICubeInfo[]) => {
        setCubesInfo([...cubes]);
      });

      pig.current.on('updateSlotCubes', (cubes: ICubeInfo[]) => {
        setSlotCubesInfo([...cubes]);
      });

      pig.current.drawPanel()
    }
  }

  useEffect(() => {
    newPig();
    return () => {
      pig.current?.removeAllListener()
    }
  }, [])

  const onClickCube = (item: ICubeInfo, event: any) => {
    if (item.coveredCubes.length > 0) {
      return;
    }
    stepCount.current++;
    pig.current?.removeCube(item.id);
  }

  useEffect(() => {
    if (slotCubesInfo.length === defaultConfig.slotCount) {
      alert('挑战失败,点击确认从新开始');
      newPig()
    }

    if (stepCount.current === defaultConfig.cubeCount && cubesInfo.length === 0 && slotCubesInfo.length === 0) {
      alert('祝贺通关,点击确认再从新玩')
      newPig()
    }

  }, [cubesInfo, slotCubesInfo])

  return (
    <div className="container">
      <div className="App" ref={onMountApp} style={{ height: `calc(100% - ${defaultConfig.cubeWidth + 20}px)` }}>
        {
          cubesInfo.map((item: ICubeInfo) => {
            return <div key={item.id} onClick={(event) => onClickCube(item, event)} className={cn("item", item.coveredCubes.length > 0 && 'cover')} style={{ width: defaultConfig.cubeWidth, height: defaultConfig.cubeWidth, left: item.coordinate[0] - defaultConfig.cubeWidth / 2, top: item.coordinate[1] - defaultConfig.cubeWidth / 2, zIndex: item.zIndex }}>
              {
                renderIconList(item.cubeTypeKey, item.coveredCubes.length > 0, { fontSize: defaultConfig.cubeWidth })
              }
            </div>
          })
        }
      </div>
      <div className="footer">
        <div className="bar" style={{ width: defaultConfig.slotCount * defaultConfig.cubeWidth, height: defaultConfig.cubeWidth }}>
          {
            slotCubesInfo.map((item: ICubeInfo) => {
              return <div key={item.id} className={cn("slot-item")} style={{ width: defaultConfig.cubeWidth, height: defaultConfig.cubeWidth }}>
                {
                  renderIconList(item.cubeTypeKey, false, { fontSize: defaultConfig.cubeWidth })
                }
              </div>
            })
          }
        </div>
      </div>
    </div>
  );
}

const renderIconList = (i: number, isCover: boolean, style: any) => {
  const Icon = IconList[i];
  const colorProps = isCover ? { twoToneColor: "rgba(0,0,0)" } : {};
  return <Icon style={style} {...colorProps} />
}

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理