」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > Svelte 的掃雷

Svelte 的掃雷

發佈於2024-11-01
瀏覽:925

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]刪除
最新教學 更多>
  • ## 如何有效分析 PHP 記憶體使用:Xdebug 替代方案和最佳實踐
    ## 如何有效分析 PHP 記憶體使用:Xdebug 替代方案和最佳實踐
    分析 PHP 記憶體消耗您尋求一種方法來檢查 PHP 頁面的記憶體使用量。具體來說,您的目標是確定資料的記憶體分配並識別導致大量記憶體消耗的函數呼叫。 Xdebug 的限制雖然 Xdebug 提供了跟踪功能,提供內存增量信息,其豐富的數據可能令人難以承受。如果細粒度過濾選項可用,問題就可以解決。然而...
    程式設計 發佈於2024-11-07
  • 如何在虛擬 DOM 中渲染元件以及如何最佳化重新渲染
    如何在虛擬 DOM 中渲染元件以及如何最佳化重新渲染
    构建现代 Web 应用程序时,高效更新 UI(用户界面)对于保持应用程序快速响应至关重要。许多框架(如 React)中使用的常见策略是使用 虚拟 DOM 和 组件。本文将解释如何使用 Virtual DOM 渲染组件,以及如何优化重新渲染以使 Web 应用程序不会变慢。 1.什么是虚...
    程式設計 發佈於2024-11-07
  • CRUD 操作:它們是什麼以及如何使用它們?
    CRUD 操作:它們是什麼以及如何使用它們?
    CRUD 操作:它們是什麼以及如何使用它們? CRUD 操作(建立、讀取、更新和刪除)是任何需要資料管理的應用程式的基礎。對於開發人員來說,了解這些操作非常重要,因為它們提供了我們有效與資料庫互動所需的基本功能。在這篇文章中,我將透過展示如何將 CRUD 操作整合到我的 Yoga ...
    程式設計 發佈於2024-11-07
  • 推出免費 Java 實用程式包
    推出免費 Java 實用程式包
    面向 Java 后端开发人员的快速且易于使用的编程工具包 在我作为管理员和开发人员的职业生涯中,我多次从无数的免费软件和开源产品中受益。因此,我很自然地也为这个社区做出贡献。 这个 Java 类集合是在各种项目过程中创建的,并将进一步开发。我希望这个工具也能为您服务。 https://java-ut...
    程式設計 發佈於2024-11-07
  • 如何在 PHP Foreach 迴圈中檢索巢狀數組的數組鍵?
    如何在 PHP Foreach 迴圈中檢索巢狀數組的數組鍵?
    PHP:在Foreach 循環中檢索數組鍵在PHP 中,使用foreach 循環迭代關聯數組可以存取這兩個值和鑰匙。但是, key() 函數僅傳回目前值的鍵,這在處理巢狀數組時可能是不夠的。 例如,考慮這樣的陣列:<?php $samplearr = array( 4722 =>...
    程式設計 發佈於2024-11-07
  • 如何將 MySQL 表中的 Latin1 字元轉換為 UTF-8?
    如何將 MySQL 表中的 Latin1 字元轉換為 UTF-8?
    將UTF8 表上的Latin1 字元轉換為UTF8您已確定您的PHP 腳本缺少必要的mysql_set_charset 函數以確保正確處理UTF-8 字元。儘管實施了此修復,您現在仍面臨著修正包含儲存在 UTF8 表中的 Latin1 字元的現有行的挑戰。 要解決此問題,您可以利用 MySQL 函數...
    程式設計 發佈於2024-11-07
  • 如何使用 Zapcap API(字幕 API)
    如何使用 Zapcap API(字幕 API)
    將 ZapCap 的自動視訊處理 API 整合到您現有的系統中是一個簡單的過程,旨在最大限度地降低複雜性並最大限度地提高效率。 ZapCap 提供開發人員友好的 API 文檔,以確保無縫入門。 逐步整合指南 步驟 1:在 ZapCap 取得您的 API 金鑰 在開始之前...
    程式設計 發佈於2024-11-07
  • 探索引導元件
    探索引導元件
    Bootstrap 5 是最受歡迎的前端框架之一,它帶來了一系列有用的組件和實用程序,可幫助開發人員快速構建響應靈敏且具有視覺吸引力的網站。 牌 卡片是 Bootstrap 5 中的多功能元件,可讓您以乾淨、有組織的方式顯示內容。它們非常適合以美觀且實用的方式展示資訊。 ...
    程式設計 發佈於2024-11-07
  • 簡化 SVG 管理:將路徑轉換為單一 JS 常數文件
    簡化 SVG 管理:將路徑轉換為單一 JS 常數文件
    构建 React.js 应用程序时,有效管理 SVG 图标至关重要。 SVG 提供了响应式设计所需的可扩展性和灵活性,但在大型项目中处理它们可能会变得很麻烦。这就是 svg-path-constants 的用武之地,它是一个 CLI 工具,旨在通过将 SVG 路径转换为可重用常量来简化 SVG 工作...
    程式設計 發佈於2024-11-07
  • 如何管理 JavaScript 程式碼結構
    如何管理 JavaScript 程式碼結構
    出色地!維護乾淨且有組織的 JavaScript 程式碼庫對於專案的長期成功至關重要。結構良好的程式碼庫可以增強可讀性,減少技術債務,並促進更輕鬆的調試和擴展。無論您正在開發小型專案還是大型應用程序,遵循建立 JavaScript 程式碼的最佳實踐都可以顯著改善您的開發流程。以下是如何管理 Java...
    程式設計 發佈於2024-11-07
  • 溢出可以配置向左流嗎?
    溢出可以配置向左流嗎?
    溢出可以配置為向左流動嗎? 溢流通常透過強制內容向右流動來處理,導致最左邊的內容被裁切。但是,可以透過套用特定的 CSS 樣式來扭轉此行為。 解決方案要啟用向左溢出,請按照給定的步驟操作:將溢出設定為隱藏:將溢出:隱藏應用到容器以防止內容超出其邊界。 文字右對齊:使用text-align: righ...
    程式設計 發佈於2024-11-07
  • 如何在保留資料類型的同時將 NumPy 陣列與不同資料類型組合?
    如何在保留資料類型的同時將 NumPy 陣列與不同資料類型組合?
    在NumPy 中組合具有多種資料類型的陣列將包含不同資料類型的陣列連接成單一陣列,每列中具有相應的數據類型一個挑戰。不幸的是,使用 np.concatenate() 的常見方法會將整個數組轉換為字串資料類型,從而導致記憶體效率低下。 要克服此限制,一個可行的解決方案是使用記錄數組或結構化數組。 記錄...
    程式設計 發佈於2024-11-07
  • 如何在同一行水平對齊內聯塊?
    如何在同一行水平對齊內聯塊?
    在同一行水平對齊內聯塊問題內聯塊比浮動元素具有優勢,例如基線對齊和自動居中當視口變窄時。然而,在同一行上水平對齊兩個內聯區塊可能會帶來挑戰。 內聯塊對齊的挑戰浮動可能會幹擾基線對齊並導致不必要的環繞.相對和絕對定位會導致間距問題,類似於浮動。 解決方案:使用文字對齊一個有效的解決方案是利用文字對齊:...
    程式設計 發佈於2024-11-07
  • 感到沒有動力
    感到沒有動力
    覺得自己像個菜鳥,放棄了幾次。 我第一次開始考慮編碼是在我還是個孩子的時候,但我選擇成為一名社交蝴蝶,現在我已經26 歲了,嘗試了很多次學習編碼python、JS、React、DB 等但最終,我感到不知所措並放棄了它。 現在,正因為如此,我感覺自己像個失敗的鬆手,我想解決這個問題...
    程式設計 發佈於2024-11-07

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3