Minesweeper in PHP

Whilst my Sudoku solver is up to a basic level, I just need to implement so more logic to reduce possible values, I started to think about the game Minesweeper.  I’ve played it since it was in the Microsoft Entertainment Pack 1, which was installed in to Windows 3.

Minesweeper is another grid based game, the player has to find all the mines which are hidden.  The basic game is an 8×8 grid with 10 mines. The first reveal is never a mine, but will show either a blank grid or a number. If it’s a number, it means in the 8 surrounding cells, there are that many mines. If it’s blank, all the blocks around will be revealed until a number is shown, indicating there is a mine in a surrounding cell.  If you think a cell is a mine, you can mark so it can’t be clicked.

The game is over when the number of remaining cells are the same as the number of mines, whether they are marked or not.

I actually started to think about this while I was trying to work out better ways to handle the logic in the Sudoku solver.  I was thinking, how are the minesweeper grids generated. I read one site where it say when the grid is presented, on the first click, if it’s a mine, the mine is moved to another cell and the grid is recalculated. I started to think the other way around. Generate the grid AFTER the first click, eliminating that cell as a possible mine.  So it would be a case of creating an array with all the cell references, remove the click cell, randomise the remaining references and then trimming the array to the number of bombs.  So that’s where I started.

I started coding and was able to quickly generate the grid, values and the numbers to indicate the number of mines surrounding a cell.  I did have to think of a better way to identify the numbers on the grid, mainly because of the function to reveal blank cells could go out of the region by using a 2 digit identification number.  So the grid numbers are identified by 4 digits, starting from 1010.  This method might not have been the best, but it worked for me.

I had a rough idea how I wanted to handle what happens when a blank cell is clicked, but that’s what took me the most time. Given that the surrounding cells have to be checked, then those have to be checked as well if they are blank.

The code is commented and I’ve tried using variable names that make things obvious. I’ve still used the similar classes as Sudoku. An input handler and a grid generator.  It’s a basic interface, but the core game mechanics are there.

When you run it, you get to choose difficulty or specify a custom sized grid:

Then when you start the game, you are submitting values from a form over and over again.

Until the game is lost

Or won

Below is the code. I’ve tried to factor as best I could without going too far.  Let me know if you have any suggestions.

index.php

<?php  
// Minesweeper in PHP by Daniel Porter - thisismywww.com 
// Version 1.0 - 28 Sept 2018
?>
<html>
  <head>
    <title>Minesweeper</title>
  </head>
  <body>
    <h1>Minesweeper</h1>
    <?php
      $output = "";
      include('input_handler.php');
      include('grid_gen.php');
      $grid_gen->generate();
    ?>
  </body>
</html>

input_handler.php

<?php
// Minesweeper in PHP by Daniel Porter - thisismywww.com
// Version 1.0 - 28 Sept 2018

// class to handle input
class data_handler{
  public $mode = "";          // Specifies game mode - new/start/game/game_won/game_over
  public $grid_reference = array();   // Array of all grid references
  public $cell_values = array();    // Values in each cell, only for display
  public $mine_cells = array();    // Array of cells with mines in them
  public $visible_cells = array();  // Array of grid_references which are 'visible' 
  public $marked_cells = array();    // Array of grid_references which are 'marked'
  public $difficulty = "easy";      // Difficulty for grid: easy, medium, hard, custom
  public $num_rows;          // Number of rows in grid
  public $num_cols;          // Number of columns in grid
  public $num_mines;          // Number of mines in grid
  public $submitted_block;      // Block which is submitted each click
  public $mark_toggle;        // Variable if marked is checked
  
  function __construct(){
    // Mode = config  Config is submitted
    // Mode = game     Game is being played
      // If no cells are posted, generate grid and then play with submitted cell
      // If cells are posted, set variables and play game
    // Mode = new    Nothing is posted
      
    if(isset($_POST['mode'])){
      $this->mode=$_POST['mode']; 
      if($this->mode == "game"){
        $this->submitted_block = $_POST['submitted_block'];
        $this->num_rows = $_POST['num_rows'];
        $this->num_cols = $_POST['num_cols'];
                $this->num_mines = $_POST['num_mines'];
        
        if(!isset($_POST['cell_values'])){
        $this->grid_reference = unserialize($_POST['grid_reference']);
        $this->generate_values();
        $this->play_game();
        }else{
          $this->grid_reference = unserialize($_POST['grid_reference']);
          $this->cell_values = unserialize($_POST['cell_values']);
          $this->mine_cells = unserialize($_POST['mine_cells']);
          $this->visible_cells = unserialize($_POST['visible_cells']);
          $this->marked_cells = unserialize($_POST['marked_cells']);
          if(isset($_POST['mark_toggle'])){
            $this->mark_toggle = $_POST['mark_toggle'];
          }else{
            $this->mark_toggle = false;
          }
          $this->play_game();
        }
      }
      if($this->mode == "start"){
        $this->generate_grid();
      }
    }else{
      $this->mode="new";
    }
  }

  function generate_grid(){
    // Set the number of rows, columns and mines based on difficulty
    switch ($_POST['difficulty']){
      case "easy":
        $this->num_rows = "8";
        $this->num_cols = "8";
        $this->num_mines = "10";
      break;
      case "medium":
        $this->num_rows = "16";
        $this->num_cols = "16";
        $this->num_mines = "40";
      break;
      case "hard":
        $this->num_rows = "24";
        $this->num_cols = "24";
        $this->num_mines = "99";
      break;
      case "custom":
        $this->num_rows = $_POST['num_rows'];
        $this->num_cols = $_POST['num_cols'];
        $this->num_mines = $_POST['num_mines'];
      break;
    }
    // Generate grid)reference array
    for ($x=10;$x<($this->num_rows+10);$x++){
      for ($y=10;$y<($this->num_cols+10);$y++){
        array_push($this->grid_reference,$x.$y);
      }
    }
  }

  function generate_values(){
    // Generate mine_cells and cell_values - need to know mine_cells before cell values
    // Mine Cells are created by using the grid references, minus clicked cell then trimmed
    $this->mine_cells = $this->grid_reference;
    $key = array_search($this->submitted_block, $this->mine_cells);
    unset($this->mine_cells[$key]);
    shuffle($this->mine_cells);
    $this->mine_cells = array_values(array_slice($this->mine_cells, 0, $this->num_mines));
    
    // Calculate how many mines are surrounding each cell if cell isn't a bomb
    // If none, don't set value
    
    foreach($this->grid_reference as $cell){
      if (!in_array($cell,$this->mine_cells)){
        $cells_to_check = array();
        $cells_to_check = $this->get_surrounding_cells($cell);
        $number = count(array_intersect($cells_to_check,$this->mine_cells));
        if ($number>0){
        $this->cell_values[$cell]=$number;
        }
      }
    }
    // On first round, make the submitted block visible
    $this->process_cell($this->submitted_block);  
  }
  
  function play_game(){
    // If toggle marked is true, run add to visible function then check if game is won
    // otherwise, if the clicked cell 
    // It the submitted isn't marked, run click function based on if it's a mine, number of blank
    
    if ($this->mark_toggle == true){
      $this->process_cell($this->submitted_block);
      $this->is_game_won();
      return;
    }else{
      if (!in_array($this->submitted_block,$this->marked_cells)){
        if (in_array($this->submitted_block,$this->mine_cells)){
          $this->click_mine();
          return;
        }elseif (isset($this->cell_values[$this->submitted_block])){
          $this->click_number();
          return;
        }else{
          $this->click_blank();
          return;
        }
      }
    }
  }

  function click_mine(){
    // If a mine is clicked, it's game over
    $this->game_over();
  }
  
  function click_number(){
    // If a number is clicked, only that cell is made visible
    $this->process_cell($this->submitted_block);
  }

  function click_blank(){
    // When a blank cell is clicked, each surrounding block is made visible.
    // Repeating outwards each time there are blank cells made visible.
    // To reduce number of checks, each time a cell is checked, its in an array
    // so that it's not checked again.
    
    $cells_to_check = $this->get_surrounding_cells($this->submitted_block);  
    $cells_checked = array(); 
    $this->process_cell($this->submitted_block);
    $x=1;
    while ($x>0){
      $x=0;
      foreach ($cells_to_check as $cell){
        $this->process_cell($cell);
        // If the cell is empty and it hasn't been checked we add it to checked cells
        // add the surrounding blank cells to the array to be checked. Increasing x so it will loop again.
         if((!isset($this->cell_values[$cell])) && (!in_array($cell,$cells_checked))){
          array_push($cells_checked,$cell);
          $cells_to_check = array_merge($cells_to_check,$this->get_surrounding_cells($cell));
          array_diff($cells_to_check,array($cell));
          $x++;
        }
      }
    }
  }

  function game_over(){
    // Set all cells visible and set mode to game_over
    $this->visible_cells = $this->grid_reference;
    $this->mode = "game_over";
  }
  
  function get_surrounding_cells($cell){
    // Sloppy, but generates array of all cells surrounding the submitted cell, making sure
    // the values are only those one in the grid reference array.
    // also removes marked cells so it's not included in a cell which is checked.
    $cells_to_check = array();
    array_push($cells_to_check, substr($cell,0,2)-1 .substr($cell,2,2)-1);
    array_push($cells_to_check, substr($cell,0,2)-1 .substr($cell,2,2));
    array_push($cells_to_check, substr($cell,0,2)-1 .substr($cell,2,2)+1);
    array_push($cells_to_check, substr($cell,0,2) .substr($cell,2,2)-1);
    array_push($cells_to_check, substr($cell,0,2) .substr($cell,2,2)+1);
    array_push($cells_to_check, substr($cell,0,2)+1 .substr($cell,2,2)-1);
    array_push($cells_to_check, substr($cell,0,2)+1 .substr($cell,2,2));
    array_push($cells_to_check, substr($cell,0,2)+1 .substr($cell,2,2)+1);
    $cells_to_check = array_intersect($cells_to_check,$this->grid_reference);
    $cells_to_check = array_diff($cells_to_check,$this->marked_cells);    
    return $cells_to_check;
  }

  function process_cell($cell){
    // general function to process mark a submitted cell.
    
    // If the cell needs to be marked, add it to marked, otherwise, if it is marked and mark
    // toggle is on, remove from marked cells
    if(($cell == $this->submitted_block) && ($this->mark_toggle == true) && (!in_array($this->submitted_block,$this->marked_cells))){
      array_push($this->marked_cells,$this->submitted_block);
      return;
    }elseif(($cell == $this->submitted_block) && ($this->mark_toggle == true) && (in_array($this->submitted_block,$this->marked_cells))){
      $key = array_search($this->submitted_block, $this->marked_cells);
      unset($this->marked_cells[$key]);
      return;
    }
    
    // if it's not in the marked cells, make it visible then check if the game is won.
    if (!in_array($cell,$this->marked_cells)){
      array_push($this->visible_cells,$cell);
      $this->visible_cells = array_unique($this->visible_cells);
      $this->is_game_won();
    }
  }
  
  function is_game_won(){
    // check to see if game is won. A simple calculation
    if((isset($_POST)) && ((count($this->grid_reference) - count($this->visible_cells)) == count($this->mine_cells))){
    $this->mode="game_won";
    }
  }
  
}
// instance input_handler class
$data = new data_handler();
?>

grid_gen.php

<?php
// Minesweeper in PHP by Daniel Porter - thisismywww.com
// Version 1.0 -  28 Sept 2018
// class to handle building the content
class grid_gen{
  // devlare variables
  public $table_html = "";
  public $pre_table = "";
  public $post_table = "";
  public $color = array(
    1=>"blue",
    2=>"green",
    3=>"red",
    4=>"purple",
    5=>"brown",
    6=>"pink",
    7=>"yellow",
    8=>"red");
  
  function form_content(){
    global $data;
    // Generate the form content, pre and post Populate the pre and post form data on on the grid, post is just for submit
    
    if (($data->mode == "game") || ($data->mode == "new") || ($data->mode=="start")){
    $this->pre_table.="<form action='index.php' method='post' id='minesweeper'>\n";
    }else{
    $this->post_table.="<a href='.'>New Game</a><br>";  
    }
    
    if ($data->mode == "new"){
      $this->pre_table.="<select name = 'difficulty'>\n";
      $this->pre_table.=" <option value='easy'>Easy</option>\n";
      $this->pre_table.=" <option value='medium'>Medium</option>\n";
      $this->pre_table.=" <option value='hard'>Hard</option>\n";
      $this->pre_table.=" <option value='custom'>Custom</option>\n";
      $this->pre_table.="</select>\n";
      $this->pre_table.="<br>Custom Settings<br>";
      $this->pre_table.="Rows: <select name = 'num_rows'>\n";
        $this->options_builder(50);
      $this->pre_table.="</select>\n";
      $this->pre_table.="Columns: <select name = 'num_cols'>\n";
        $this->options_builder(50);
      $this->pre_table.="</select>\n";
      $this->pre_table.="Mines: <select name = 'num_mines'>\n";
        $this->options_builder(50);
      $this->pre_table.="</select>\n";
      $this->pre_table.="<br><input type='submit' name='mode' value='start'>";
    }

    if ($data->mode == "start" || $data->mode == "game"){
      $this->pre_table.="<input type='hidden' name='grid_reference' value='" . htmlspecialchars(serialize($data->grid_reference)) . "'>\n";
      $this->pre_table.="<input type='hidden' name='num_cols' value='" . $data->num_cols . "'>\n";
      $this->pre_table.="<input type='hidden' name='num_rows' value='" . $data->num_rows . "'>\n";
      $this->pre_table.="<input type='hidden' name='num_mines' value='" . $data->num_mines . "'>\n";
      $this->pre_table.="Mines:". ($data->num_mines-count($data->marked_cells))."<br>\n";
    }

    
    if ($data->mode == "game"){
      $this->pre_table.="<input type='hidden' name='mode' value='game'>";
      $this->pre_table.="<input type='hidden' name='cell_values' value='" . htmlspecialchars(serialize($data->cell_values)) . "'>\n";
      $this->pre_table.="<input type='hidden' name='mine_cells' value='" . htmlspecialchars(serialize($data->mine_cells)) . "'>\n"; 
      $this->pre_table.="<input type='hidden' name='visible_cells' value='" . htmlspecialchars(serialize($data->visible_cells)) . "'>\n"; 
      $this->pre_table.="<input type='hidden' name='marked_cells' value='" . htmlspecialchars(serialize($data->marked_cells)) . "'>\n";
      $this->pre_table.="Toggle Marked <input type='checkbox' name='mark_toggle'";
      if($data->mark_toggle == true){
        $this->pre_table.=" checked='checked'";
      }
      $this->pre_table.=">\n";
    }

    if ($data->mode == "start"){
      $this->pre_table.="<input type='hidden' name='mode' value='game'>";
      $this->pre_table.="Toggle Marked\n";
    }
  
    if ($data->mode != "game_won"){
    $this->post_table.="</form>\n";
    }
    // if the game is over or game has been won, display message
    if ($data->mode == "game_over"){
      $this->pre_table.= "Game Over\n";
    }
    if ($data->mode == "game_won"){
      $this->pre_table.= "Congratulations, you've won!\n";
      
    }
  }
  
  function options_builder($number){
    // function to build options up to number specified
    for ($x=8;$x<=$number;$x++){
      $this->pre_table.=" <option value='$x'>$x</option>\n";
    }
  }
  
  function create_table(){
    // loop to build grid. Only extra element is to mark block as red if block is submitted
    global $data;
    $this->table_html .= "<table border='1'>\n";
      for ($x=10;$x<($data->num_rows+10);$x++){
        $this->table_html .= "<tr>\n";
        for ($y=10;$y<($data->num_cols+10);$y++){
          $block = $x.$y;
          if(($data->mode=="game_over") && ($data->submitted_block == $block)){
          $extra=" bgcolor='red'";
          }else{
          $extra="";
          }
          $this->table_html .= "<td width='18px' height='18px' border='0' align='center'$extra>";
          $this->cell_content($block);
          $this->table_html .= "</td>\n";
        }
        $this->table_html .= "</tr>\n";
      }
      $this->table_html .= "</table>\n";
  }
  
  function cell_content($block){
  // Case is when grid is just created
  // else if the cell is visible, display it's content, otherwise, create form button 
  global $data;
    if ($data->mode == "start"){
      $this->table_html .= "<input type='hidden' name='mode' value='game'><input type='submit' name='submitted_block' value='". $block ."' style='height:18px; width=18px; text-indent:-9999px' />";
    }else{
      if (in_array($block,$data->visible_cells)){
        if (array_key_exists($block,$data->cell_values)){
            $this->color_number($data->cell_values[$block]);
          }else{
            if (in_array($block,$data->mine_cells)){
              $this->table_html .= "<strong>*</strong>";
            }else{
              $this->table_html .= "";
          }
        }
      }else{
        $this->table_html .= "<input type='submit' name='submitted_block' value='". $block ."' style='height:15px; width=15px; text-indent:-9999px";
          if(in_array($block,$data->marked_cells)){
            $this->table_html .= "; background:red";
          }
        $this->table_html .= "'/>";
      }
    }
  }
  
  function color_number($number){
    $this->table_html .= "<font style='color:".$this->color[$number]."'>$number</font>";
  }
  
  
  function generate(){
  // function that builds for and table data as long as mode isn't new.
    global $data;
    $this->form_content();
    echo $this->pre_table;
    if((!isset($data->mode)) || ($data->mode != "new")){
    $this->create_table();
    echo $this->table_html;
    }
    echo $this->post_table;
  }
}
// instance grid_gen class
$grid_gen = new grid_gen();
?>

 

Leave a Reply