”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > Svelte 的扫雷

Svelte 的扫雷

发布于2024-11-01
浏览:241

Background

I never understood as a kid how this game worked, I used to open this game up in Win95 IBM PC, and just click randomly around until I hit a mine. Now that I do, I decided to recreate the game with Svelte.

Lets break it down - How the game works ?

Minesweeper is a single player board game with a goal of clearing all tiles of the board. The player wins if he clicks all the tiles on the board without mines and loses if he clicks on a tile that holds a mine.

Minesweeper in Svelte

Game starts with all tiles concealing what is beneath it, so its a generally a lucky start to the game, you could click on either a empty tile or a mine. In case its not a empty tile or a mine, it could hold a count of mine that are present adjacent to the currently clicked tile.

Minesweeper in Svelte

As an example here 1 at the top left indicates there is 1 mine in of its adjacent cells. Now what do we mean by adjacent cells ? All cells that surround a cell become its adjacent cells. This allows players to strategize about clicking on which tile next. In case a user is not sure whether there is a mine or not underneath a tile, he can flag it by right clicking on the tile.

Minesweeper in Svelte

Thinking about the game logic

The game is pretty simple at first glance, just give a board of n x m rows and keep switching the cell state to display the content that each cell holds. But there is a case where if multiple empty cells are connected and you click on it, the game should keep opening adjacent cells if they are empty too, that gives its iconic ripple effect.

Minesweeper in Svelte

Gets quite tricky building all these conditions in, so lets break down into smaller tasks at hand.

  1. Create a board state - a n x m array (2d array)
  2. A cell inside a board could be / can hold: a mine, a count, or empty.
  3. A cell can be: clicked / not clicked.
  4. A cell can be: flagged / not flagged.
  5. Represent cell with these properties: row, col, text, type(mine, count, empty), clicked(clicked / not clicked), flagged (flagged / not flagged).
  6. Finally we could represent a game state like this: on, win, lose
  7. We also need to have a way to define bounds, while we create that ripple, we don't want the ripple to open up mines too! So we stop open tiles once we hit a boundary of mine!
  8. A game config: row, cols, mine count -> This could help us add difficulty levels to our game easily. Again this is optional step.
  9. This game is just calling a bunch of functions...on click event.

Creating board state

Creating a board is simple task of creating a 2d array given that we know the number of rows and columns. But along with that we also want to put mines at random spots in the board and annotate the board with the mine count in the adjacent cells of mine.

We create a list of unique random indices to put mines at, below function is used to do that,



  function uniqueRandomIndices(
    row_count: number,
    col_count: number,
    range: number,
  ) {
    const upper_row = row_count - 1;
    const upper_col = col_count - 1;
    const idxMap = new Map();
    while (idxMap.size !== range) {
      const rowIdx = Math.floor(Math.random() * upper_row);
      const colIdx = Math.floor(Math.random() * upper_col);
      idxMap.set(`${rowIdx}_${colIdx}`, [rowIdx, colIdx]);
    }
    return [...idxMap.values()];
  }



So here is function that we use to create board state



     function createBoard(
    rows: number,
    cols: number,
    minePositions: Array>,
  ) {
    const minePositionsStrings = minePositions.map(([r, c]) => `${r}_${c}`);
    let boardWithMines = Array.from({ length: rows }, (_, row_idx) =>
      Array.from({ length: cols }, (_, col_idx) => {
        let cell: Cell = {
          text: "",
          row: row_idx,
          col: col_idx,
          type: CellType.empty,
          clicked: CellClickState.not_clicked,
          flagged: FlagState.not_flagged,
        };
        if (minePositionsStrings.includes(`${row_idx}_${col_idx}`)) {
          cell.type = CellType.mine;
          cell.text = CellSymbols.mine;
        }
        return cell;
      }),
    );
    return boardWithMines;
  }



Once we do have a board with mines in random spots, we will end up with a 2d array. Now the important part that makes it possible to play at all, adding mine count to adjacent cells of a mine. For this we have couple of things to keep in mind before we proceed with it.

We have to work within the bounds for any given cell, what are these bounds ?

Bounds here simply means range of rows and cols through which we can iterate through to get all adjacent cells. We need to make sure these bounds never cross the board, else we will get an error or things might not work as expected.

So adjacent cells means each cell that touches current cell on sides or vertices. All the red cells are adjacent cells to the green cell in the middle as per the figure below.

Minesweeper in Svelte


  const getMinIdx = (idx: number) => {
    if (idx > 0) {
      return idx - 1;
    }
    return 0;
  };
  const getMaxIdx = (idx: number, maxLen: number) => {
    if (idx   1 > maxLen - 1) {
      return maxLen - 1;
    }
    return idx   1;
  };

  const getBounds = (row_idx: number, col_idx: number) => {
    return {
      row_min: getMinIdx(row_idx),
      col_min: getMinIdx(col_idx),
      row_max: getMaxIdx(row_idx, rows),
      col_max: getMaxIdx(col_idx, cols),
    };
  };

  function annotateWithMines(
    boardWithMines: Cell[][],
    minePositions: number[][],
  ) {
    for (let minePosition of minePositions) {
      const [row, col] = minePosition;
      const bounds = getBounds(row, col);
      const { row_min, row_max, col_min, col_max } = bounds;
      for (let row_idx = row_min; row_idx 

We now have a board with mines and now we need to display this board with some html/ CSS,



  
{#each board as rows} {#each rows as cell} {/each} {/each}

This renders a grid on page, and certain styles applied conditionally if a cell is clicked, flagged. You get a good old grid like in the screenshot below

Minesweeper in Svelte

Cell and the clicks...

Cell and its state is at the heart of the game. Let's see how to think in terms of various state a cell can have

A cell can have:

  1. Empty content
  2. Mine
  3. Count - adjacent to a mine

A cell can be:

  1. Open
  2. Close

A cell can be:

  1. Flagged
  2. Not Flagged

export enum CellType {
    empty = 0,
    mine = 1,
    count = 2,
}

export enum CellClickState {
    clicked = 0,
    not_clicked = 1,
}
export enum CellSymbols {
    mine = "?",
    flag = "?",
    empty = "",
    explode = "?",
}

export enum FlagState {
    flagged = 0,
    not_flagged = 1,
}

export type Cell = {
    text: string;
    row: number;
    col: number;
    type: CellType;
    clicked: CellClickState;
    flagged: FlagState;
};


Rules for a cell:

  1. On left click, click a cell opens and on a right click cell is flagged.
  2. A flagged cell cannot be opened but only unflagged and then opened.
  3. A click on empty cell should open all its adjacent empty cells until it hits a boundary of mine cells.
  4. A click on cell with mine, should open all the mine with cell and that end the game

With this in mind, we can proceed with implementing a click handler for our cells



 const handleCellClick = (event: MouseEvent) => {
    switch (event.button) {
      case ClickType.left:
        handleLeftClick(event);
        break;
      case ClickType.right:
        handleRightClickonCell(event);
        break;
    }
    return;
  };



Simple enough to understand above function calls respective function mapped to each kind of click , left / right click



  const explodeAllMines = () => {
    for (let [row, col] of minePositions) {
      board[row][col] = {
        ...board[row][col],
        text: CellSymbols.explode,
      };
    }
  };

  const setGameLose = () => {
    game_state = GameState.lose;
  };


  const handleMineClick = () => {
    for (let [row, col] of minePositions) {
      board[row][col] = {
        ...board[row][col],
        clicked: CellClickState.clicked,
      };
    }
    setTimeout(() => {
      explodeAllMines();
      setGameLose();
    }, 300);
  };

  const clickEmptyCell = (row_idx: number, col_idx: number) => {
    // recursively click adjacent cells until:
    // 1. hit a boundary of mine counts - is it same as 3 ?
    // 2. cells are already clicked
    // 3. cells have mines - same as 1 maybe
    // 4. cells that are flagged - need to add a flag feature as well
    if (board[row_idx][col_idx].type === CellType.count) {
      return;
    }

    const { row_min, row_max, col_min, col_max } = getBounds(row_idx, col_idx);
    // loop over bounds to click each cell within the bounds
    for (let r_idx = row_min; r_idx  {
    if (event.target instanceof HTMLButtonElement) {
      const row_idx = Number(event.target.dataset.row);
      const col_idx = Number(event.target.dataset.col);
      const cell = board[row_idx][col_idx];
      if (
        cell.clicked === CellClickState.not_clicked &&
        cell.flagged === FlagState.not_flagged
      ) {
        board[row_idx][col_idx] = {
          ...cell,
          clicked: CellClickState.clicked,
        };
        switch (cell.type) {
          case CellType.mine:
            handleMineClick();
            break;
          case CellType.empty:
            clickEmptyCell(row_idx, col_idx);
            break;
          case CellType.count:
            break;
          default:
            break;
        }
      }
    } else {
      return;
    }
  };


Left click handler - handles most of the game logic, it subdivided into 3 sections based on kind of cell the player clicks on:

  1. Mine cell is clicked on
    If a mine cell is clicked we call handleMineClick() function, that will open up all the mine cells, and after certain timeout we display an explosion icon, we stop the clock and set the game state to lost.

  2. Empty cell is clicked on
    If a empty cell is clicked on we need to recursively click adjacent empty cells until we hit a boundary of first counts. As per the screenshot, you could see when I click on the bottom corner cell, it opens up all the empty cells until the first boundary of counts.

  3. Count cell is clicked on
    Handling count cell is simply revealing the cell content beneath it.

Minesweeper in Svelte

Game state - Final bits and pieces

Game Difficulty can be configured on the basis of ratio of empty cells to mines, if the mines occupy 30% of the board, the game is too difficult for anyone to play, so we set it up incrementally higher up to 25%, which is still pretty high


export const GameDifficulty: Record = {
    baby: { rows: 5, cols: 5, mines: 2, cellSize: "40px" }, // 8% board covered with mines
    normal: { rows: 9, cols: 9, mines: 10, cellSize: "30px" }, // 12% covered with mines
    expert: { rows: 16, cols: 16, mines: 40, cellSize: "27px" }, // 15% covered with mines
    "cheat with an AI": { rows: 16, cols: 46, mines: 180, cellSize: "25px" }, // 25% covered with mines - u need to be only lucky to beat this
};


Game state is divided into 3 states - win, lose and on


export enum GameState {
    on = 0,
    win = 1,
    lose = 2,
}

export type GameConfig = {
    rows: number;
    cols: number;
    mines: number;
    cellSize: string;
};


We also add a timer for the game, that start as soon as the game starts, I have separated it in a timer.worker.js, but it might be an overkill for a small project like this. We also have a function to find the clicked cells count, to check if user has clicked all the cells without mines.


  let { rows, cols, mines, cellSize } = GameDifficulty[difficulty];
  let minePositions = uniqueRandomIndices(rows, cols, mines);
  let game_state: GameState = GameState.on;
  let board = annotateWithMines(
    createBoard(rows, cols, minePositions),
    minePositions,
  );
  let clickedCellsCount = 0;
  let winClickCount = rows * cols - minePositions.length;
  $: flaggedCellsCount = mines;
  $: clickedCellsCount = calculateClickedCellsCount(board);
  $: if (clickedCellsCount === winClickCount) {
    game_state = GameState.win;
  }
  let timer = 0;
  let intervalWorker: Worker;
  let incrementTimer = () => {
    timer  = 1;
  };
  $: if (clickedCellsCount >= 1 && timer === 0) {
    intervalWorker = new Worker("timer.worker.js");
    intervalWorker.addEventListener("message", incrementTimer);
    intervalWorker.postMessage({
      type: "START_TIMER",
      payload: { interval: 1000 },
    });
  }
  $: timerDisplay = {
    minute: Math.round(timer / 60),
    seconds: Math.round(timer % 60),
  };

  $: if (game_state !== GameState.on) {
    intervalWorker?.postMessage({ type: "STOP_TIMER" });
  }

  const calculateClickedCellsCount = (board: Array>) => {
    return board.reduce((acc, arr) => {
      acc  = arr.reduce((count, cell) => {
        count  = cell.clicked === CellClickState.clicked ? 1 : 0;
        return count;
      }, 0);
      return acc;
    }, 0);
  };


And we have got a great minesweeper game now !!!

Minesweeper in Svelte

This is a very basic implementation of Minesweeper, we could do more with it, for starters we could represent the board state with bitmaps, which makes it infinitely...Could be a great coding exercise. Color coding the mine count could be a good detail to have, there are so many things to do with it...this should be a good base to work with...

In case you want to look at the complete code base you could fork / clone the repo from here:

https://github.com/ChinmayMoghe/svelte-minesweeper

版本声明 本文转载于:https://dev.to/chinmaymoghe/minesweeper-in-svelte-21km?1如有侵犯,请联系[email protected]删除
最新教程 更多>
  • 如何在Chrome中居中选择框文本?
    如何在Chrome中居中选择框文本?
    选择框的文本对齐:局部chrome-inly-ly-ly-lyly solument 您可能希望将文本中心集中在选择框中,以获取优化的原因或提高可访问性。但是,在CSS中的选择元素中手动添加一个文本 - 对属性可能无法正常工作。初始尝试 state)</option> < op...
    编程 发布于2025-07-19
  • 如何将来自三个MySQL表的数据组合到新表中?
    如何将来自三个MySQL表的数据组合到新表中?
    mysql:从三个表和列的新表创建新表 答案:为了实现这一目标,您可以利用一个3-way Join。 选择p。*,d.content作为年龄 来自人为p的人 加入d.person_id = p.id上的d的详细信息 加入T.Id = d.detail_id的分类法 其中t.taxonomy =...
    编程 发布于2025-07-19
  • 如何使用PHP将斑点(图像)正确插入MySQL?
    如何使用PHP将斑点(图像)正确插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call ...
    编程 发布于2025-07-19
  • Python高效去除文本中HTML标签方法
    Python高效去除文本中HTML标签方法
    在Python中剥离HTML标签,以获取原始的文本表示Achieving Text-Only Extraction with Python's MLStripperTo streamline the stripping process, the Python standard librar...
    编程 发布于2025-07-19
  • 如何实时捕获和流媒体以进行聊天机器人命令执行?
    如何实时捕获和流媒体以进行聊天机器人命令执行?
    在开发能够执行命令的chatbots的领域中,实时从命令执行实时捕获Stdout,一个常见的需求是能够检索和显示标准输出(stdout)在cath cath cant cant cant cant cant cant cant cant interfaces in Chate cant inter...
    编程 发布于2025-07-19
  • 解决Spring Security 4.1及以上版本CORS问题指南
    解决Spring Security 4.1及以上版本CORS问题指南
    弹簧安全性cors filter:故障排除常见问题 在将Spring Security集成到现有项目中时,您可能会遇到与CORS相关的错误,如果像“访问Control-allo-allow-Origin”之类的标头,则无法设置在响应中。为了解决此问题,您可以实现自定义过滤器,例如代码段中的MyFi...
    编程 发布于2025-07-19
  • 如何在其容器中为DIV创建平滑的左右CSS动画?
    如何在其容器中为DIV创建平滑的左右CSS动画?
    通用CSS动画,用于左右运动 ,我们将探索创建一个通用的CSS动画,以向左和右移动DIV,从而到达其容器的边缘。该动画可以应用于具有绝对定位的任何div,无论其未知长度如何。问题:使用左直接导致瞬时消失 更加流畅的解决方案:混合转换和左 [并实现平稳的,线性的运动,我们介绍了线性的转换。这...
    编程 发布于2025-07-19
  • \“(1)vs.(;;):编译器优化是否消除了性能差异?\”
    \“(1)vs.(;;):编译器优化是否消除了性能差异?\”
    答案: 在大多数现代编译器中,while(1)和(1)和(;;)之间没有性能差异。编译器: perl: 1 输入 - > 2 2 NextState(Main 2 -E:1)V-> 3 9 Leaveloop VK/2-> A 3 toterloop(next-> 8 last-> 9 ...
    编程 发布于2025-07-19
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-07-19
  • Java中如何使用观察者模式实现自定义事件?
    Java中如何使用观察者模式实现自定义事件?
    在Java 中创建自定义事件的自定义事件在许多编程场景中都是无关紧要的,使组件能够基于特定的触发器相互通信。本文旨在解决以下内容:问题语句我们如何在Java中实现自定义事件以促进基于特定事件的对象之间的交互,定义了管理订阅者的类界面。以下代码片段演示了如何使用观察者模式创建自定义事件: args)...
    编程 发布于2025-07-19
  • 如何限制动态大小的父元素中元素的滚动范围?
    如何限制动态大小的父元素中元素的滚动范围?
    在交互式接口中实现垂直滚动元素的CSS高度限制问题:考虑一个布局,其中我们具有与用户垂直滚动一起移动的可滚动地图div,同时与固定的固定sidebar保持一致。但是,地图的滚动无限期扩展,超过了视口的高度,阻止用户访问页面页脚。$("#map").css({ marginT...
    编程 发布于2025-07-19
  • 如何处理PHP文件系统功能中的UTF-8文件名?
    如何处理PHP文件系统功能中的UTF-8文件名?
    在PHP的Filesystem functions中处理UTF-8 FileNames 在使用PHP的MKDIR函数中含有UTF-8字符的文件很多flusf-8字符时,您可能会在Windows Explorer中遇到comploreer grounder grounder grounder gro...
    编程 发布于2025-07-19
  • 在UTF8 MySQL表中正确将Latin1字符转换为UTF8的方法
    在UTF8 MySQL表中正确将Latin1字符转换为UTF8的方法
    在UTF8表中将latin1字符转换为utf8 ,您遇到了一个问题,其中含义的字符(例如,“jáuòiñe”)在utf8 table tabled tablesset中被extect(例如,“致电。The recommended approach to correct the data is t...
    编程 发布于2025-07-19
  • Java的Map.Entry和SimpleEntry如何简化键值对管理?
    Java的Map.Entry和SimpleEntry如何简化键值对管理?
    A Comprehensive Collection for Value Pairs: Introducing Java's Map.Entry and SimpleEntryIn Java, when defining a collection where each element com...
    编程 发布于2025-07-19
  • 在Pandas中如何将年份和季度列合并为一个周期列?
    在Pandas中如何将年份和季度列合并为一个周期列?
    pandas data frame thing commans date lay neal and pree pree'和pree pree pree”,季度 2000 q2 这个目标是通过组合“年度”和“季度”列来创建一个新列,以获取以下结果: [python中的concate...
    编程 发布于2025-07-19

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3