乐趣区

关于前端:写个羊了个羊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} />
}
退出移动版