在SolidJs中创建扫雷游戏 - 得分,计时器和游戏状态
#javascript #教程 #gamedev #solidjs

欢迎在Solidjs Post系列中创建扫雷游戏的第三部分。在first part中,我们使用平坦的阵列和数学操作构建了游戏板。在second part中,我们集中于实施“零开放”功能所需的递归迭代过程。
在这篇文章中,我们旨在将游戏化元素纳入游戏中,以使其对用户更具吸引力和愉悦感。

我们将通过添加矿山计数器来跟踪那些讨厌的炸弹,并帮助您维持标记的炸弹来调整内容。另外,我们将介绍游戏结束的逻辑和方便的新计时器组件,该组件将跟踪您玩的时间,直到炸弹开始引爆或已标记所有矿山。

我们有很多事情要做,所以让我们继续!


嘿!有关您将要阅读的内容的更多内容,请在Twitter上查看@mattibarzeev ð»


代码可以在此GitHub存储库中找到:
https://github.com/mbarzeev/solid-minesweeper

地雷柜台

Minesweeper Game有一个游戏面板,其中包括地雷计数器。
计数器从板上的最初矿山的初始数量开始,每次玩家都标记瓷砖时,即使商标实际上并不在其中有一个地雷的瓷砖上,矿山的数量也会相应减少。

我们首先在标题中渲染游戏面板:

<header class={styles.header}>
               <div class={styles.gamePanel}></div>
               <div class={styles.board}>
                   <For each={tilesArray()}>
                       {(tileData: TileData) => (
                           <Tile data={tileData} onTileContextMenu={toggleTileMark} onTileClicked={onTileClicked} />
                       )}
                   </For>
               </div>
           </header>

为了显示剩余的矿山数量,我们需要创建一个固体信号,以使其具有反应性:

const TOTAL_MINES = 50;
const [remainingMines, setRemainingMines] = createSignal<number>(TOTAL_MINES);

,渲染将是:

<div class={styles.gamePanel}>
                   <span>{remainingMines()}</span>
               </div>

现在,要正确设置剩余地雷的总数,我们需要在每次播放器切换标记时进行更新,我想这样做,以此作为修改瓷砖数组时的副作用。
为了实现这一目标,我们需要聆听tilesArray的更改并检查标记的瓷砖以计算其余地雷。它可能似乎没有优化,但它会以一种确定的方式使用游戏状态来工作 - 我的意思是,如果我们给游戏一个状态,它将正确渲染。

为此,我们将使用solid s createefect():

createEffect(() => {
   const markedTiles = tilesArray().filter((tile) => tile.isMarked);
   setRemainingMines(TOTAL_MINES - markedTiles.length);


 });

这种效果每次更改瓷砖信号时都会触发这种效果,滤除标记的图块,然后从total_mines中扣除以获取剩余的矿山号。

引爆一个地雷

当玩家不小心打开一个拥有我的瓷砖时,游戏结束了。
这是瓷砖单击处理程序的代码:

const onTileClicked = (index: number) => {
   const tile: TileData = tilesArray()[index];
   // If the tile is marked, un-mark it
   tile.isMarked = false;
   // If the tile has a mine, it's game over
   if (tile.value === MINE_VALUE) {
       gameOver();
   } else {
       let indices = [index];
       const tileValue = tile.value;
       if (tileValue === 0) {
           // get the indices that need to be opened...
           indices = getTotalZeroTilesIndices(index);
       }
       openTiles(indices);
   }
};

您可以看到,当我们检测到该值是mine_value时,我们称为gameOver()函数。
我们要在游戏功能中要做的是引爆点击矿,然后在板上的其余地雷引爆。

我们首先将另一个属性添加到Tiledata,即isDetonated:boolean;

export type TileData = {
   index: number;
   value: TileValue;
   isOpen: boolean;
   isMarked: boolean;
   isDetonated: boolean;
};

这将有助于我们指出瓷砖被引爆。我们显然将其启动到错误:

// Convert the boardArray to TilesArray
const [tilesArray, setTilesArray] = createSignal<TileData[]>(
   boardArray.map((item, index) => ({
       index,
       value: getTileValue(index),
       isOpen: false,
       isMarked: false,
       isDetonated: false,
   }))
);

我们还添加了CSS类,以防瓷砖在瓷砖组件中引爆:

classList={{[styles.exposed]: data.isOpen || data.isMarked, [styles.detonated]: data.isDetonated}}
.value.detonated {
   display: block;
   background-color: red;
}

现在,当玩家引爆矿山时,我们将其中的所有瓷砖标记为引爆的,我们将它们全部打开,因此,当游戏超过整个游戏板上时,我们都会公开:

const gameOver = () => {
   setTilesArray((prevArray) => {
       const newArray = prevArray.map((tile) => {
           if (tile.value === MINE_VALUE) {
               return {...tile, isDetonated: true, isOpen: true};
           }
           return {...tile, isOpen: true};
       });


       return newArray;
   });
};

这是到目前为止的结果:

Image description

找到所有地雷

是时候处理玩家找到所有矿山的情况了。换句话说,这意味着每个带有矿山的瓷砖都标有一个标志,并且标记的瓷砖数量等于木板中的矿山总数。

createEffect中,我们以前做出了我们添加以下代码,以检查游戏是否赢了:

createEffect(() => {
  if (isGameOver) return;


   const markedTiles = tilesArray().filter((tile) => tile.isMarked);
   setRemainingMines(TOTAL_MINES - markedTiles.length);


   // If the marked tiles are actually mines and they equal to the total number
   // of mines in the board, the game is won
   if (markedTiles.length === TOTAL_MINES) {
       try {
           markedTiles.forEach((tile) => {
               if (tile.value !== MINE_VALUE) {
                   throw new Error();
               }
           });
           gameWon();
       } catch (error) {
           // Do nothing, the game is not won
       }
   }
});

简而言之,当标记瓷砖的数量等于总矿山数量时,我们检查它们是否都是矿山。如果不是,我们打破了循环。如果是,我们称为gamewon()方法。

请注意,我们在做任何事情之前先检查Isgameover。这是为了防止当我们想在游戏结束时对图块数组做些事情时,例如在gameWon函数中,我们打开整个瓷砖。

这是gameWon函数的代码:

const gameWon = () => {
   isGameOver = true;
   setTilesArray((prevArray) => {
       const newArray = prevArray.map((tile) => {
           return {...tile, isOpen: true};
       });


       return newArray;
   });
};

计时器

是时候添加计时器了。
目前,它将是愚蠢的 - 它将从页面的刷新开始,并在游戏结束时停止。

我们从信号和计时器间隔开始:

let timerInterval: number;
const [timerSeconds, setTimerSeconds] = createSignal(0);
const startTimer = () => {
   timerInterval = setInterval(() => {
       setTimerSeconds(timerSeconds() + 1);
   }, 1000);
};

,我们在主应用程序文件上调用startTimer()

对于计时器,我们构建了一个专用组件,该组件以不错的格式显示时间。请注意,传递给组件的秒数不是一个数字,而是固体访问者类型:

import {Accessor} from 'solid-js';


const Timer = ({seconds}: {seconds: Accessor<number>}) => {
   return <div>{getDisplayTimeBySeconds(seconds())}</div>;
};


const getDisplayTimeBySeconds = (seconds: number) => {
   const min = Math.floor(seconds / 60);
   const sec = seconds % 60;
   return `${getDisplayableTime(min)}:${getDisplayableTime(sec)}`;
};


function getDisplayableTime(timeValue: number): string {
   return timeValue < 10 ? `0${timeValue}` : `${timeValue}`;
}


export default Timer;


我们在主应用程序中使用这样的组件:

<div class={styles.scoring}>
                   <span>{remainingMines()}</span>
                   <Timer seconds={timerSeconds} />
               </div>

以及当游戏结束时,我们清除了间隔。拥有游戏状态并具有效果可能会更好,但是我们将其保存到重构阶段。

好吧,我认为我们做到了并实现了这篇文章的目标!

那么我们到底有什么?

Image description

我们有一个矿床,我们有一个计时器,我们有办法表明比赛何时结束或赢得胜利,甚至为矿山提供了一个不错的炸弹表情符号(对不起,无法找到我的矿山)。还不错ð

请继续关注下一篇文章,我们将放置一些模式并尝试完成游戏!

代码可以在此GitHub存储库中找到:
https://github.com/mbarzeev/solid-minesweeper


嘿!有关更多内容,例如您刚刚阅读的内容,请在Twitter上查看@mattibarzeev ð»