0

I'm working on a Minesweeper game for my girlfriend as a surprise. In this stage of the process, I set the width and the height of the grid in the code-behind of the user control that declares the board (I was thinking of making a litte extra configuration window in the menu where she would be able to select different sizes for the board later).

The code for this is really straight forward and based on the JavaScript tutorial given here by Ania Kubow: https://www.youtube.com/watch?v=W0No1JDc6vE

namespace Minesweeper.Views;

public partial class BoardView : UserControl {
  public int BoardWidth { get; set; } = 10;

  public int BoardHeight { get; set; } = 10;

  public int BombCount { get; set; } = 20;

  public List<BoardTileView> Tiles { get; set; } = new();

  public BoardView() => InitializeComponent();

  private void BoardView_Loaded(object sender, RoutedEventArgs e) {
    List<string> bombs = Enumerable.Repeat(Constants.Bomb, BombCount).ToList();

    List<string> valid = Enumerable.Repeat(
      Constants.Valid, BoardWidth * BoardHeight - BombCount).ToList();

    List<string> tiles = bombs.Concat(valid).ToList();
    tiles.FisherYatesShuffle();

    AddRowDefinitions();
    AddColumnDefinitions();
      
    // Add tiles to board grid
    for (int row = 0; row < BoardWidth; row++) {
      var isLeftEdge = row == 0;
      var isRightEdge = row == BoardWidth - 1;

      for (int col = 0; col < BoardHeight; col++) {
        int index = row * (col + 1);

        BoardTileView boardTile = new() {
          Name = $"Tile{index}",
        };

        if (tiles[index].Equals(
          Constants.Bomb,
          StringComparison.InvariantCultureIgnoreCase)) {
          boardTile.testName.Background = Brushes.HotPink;
          boardTile.IsBomb = true;
        }

        Grid.SetRow(boardTile, row);
        Grid.SetColumn(boardTile, col);

        boardGrid.Children.Add(boardTile);
        Tiles.Add(boardTile);
        // Check for adjacent bomb count
      }
    }
  }

  private void AddRowDefinitions() {
    for (int i = 0; i < BoardWidth; i++) {
      boardGrid.RowDefinitions.Add(new RowDefinition());
    }
  }

  private void AddColumnDefinitions() {
    for (int i = 0; i < BoardHeight; i++) {
      boardGrid.ColumnDefinitions.Add(new ColumnDefinition());
    }
  }

  private bool IsBomb(string tile) =>
    tile.Equals(Constants.Bomb, StringComparison.InvariantCultureIgnoreCase);
}

The XAML is just a UserControl with a Grid named boardGrid.

The BoardTileView is just a UserControl with a Canvas with a PeachPuff Background and a public property IsBomb. In its XAML it has a x:Name="testName", so I can change its Background color to HotPink when it is a bomb for testing purposes.

The Fisher-Yates shuffle extension method is the following replacing the RNGCryptoServiceProvider with RandomNumberGenerator: Randomize a List<T>

The actual problem Whenever I change the BoardWidth and BoardHeight property to something that is greater than 15, the application crashes without throwing an exception in Debug mode, which made me think, is there a limit to the amount of children a WPF Grid can have that I'm not aware of, or is something else going on here?

The code above works fine up until a 15 by 15 grid. I was expecting I could make this grid even larger without consequences. I tried increasing the MainWindow in size to no avail.

The solution can be found on my Github here, because there is a lot of moving parts to a WPF application of course: https://github.com/binarybotany/Minesweeper

xatja
  • 11
  • 2
  • FWIW, I don't think there is a limit as such. Your `index` calculation looks off to me, though I doubt that's the reason for your error. On second thought, that might actually be the problem. – 500 - Internal Server Error Jul 24 '23 at 22:05
  • Have you checked your FisherYatesShuffle works with IList longer than Byte.MaxValue which is used in that method? – emoacht Jul 24 '23 at 22:54
  • @emoacht This is indeed the problem. When I use a `List.Count` of `Byte.MaxValue + 1`, the test enters an endless loop. – xatja Jul 25 '23 at 06:48

1 Answers1

0

Like @emoacht said. The Fisher-Yates Shuffle extension was the problem. Should you ever need a Fisher-Yates Shuffle that works with any size of elements, it is right here:

public static class Extensions {
  private static RandomNumberGenerator rng = RandomNumberGenerator.Create();

  public static List<T> FisherYatesShuffle<T>(
    this IEnumerable<T> source) {
    List<T> items = new(source);

    for (int i = items.Count - 1; i > 0; i--) {
      int j = GetRandomNumber(i + 1);
      items.Swap(i, j);
    }

    return items;
  }

  private static void Swap<T>(this IList<T> list, int x, int y) {
    if (x == y) return;

    T temp = list[x];
    list[x] = list[y];
    list[y] = temp;
  }

  private static int GetRandomNumber(int maxExclusive) {
    byte[] randomNumber = new byte[4];
    rng.GetBytes(randomNumber);
    int result = Math.Abs(BitConverter.ToInt32(randomNumber, 0));

    return result % maxExclusive;
  }
}

And of course, the index calculation is also completely off like @500 - Internal Server Error said, and here is a correct version below

for (int index = 0, row = 0; index < GridWidth * GridHeight; index++) {
  MinesweeperCell cell = new() { Name = $"Cell{index}" };

  if (IsBomb(cells[index])) {
    cell.IsBomb = true;
    cell.test.Background = Brushes.HotPink;
  }

  if (index != 0 && index % GridWidth == 0) row++;
  
  Grid.SetRow(cell, row);
  Grid.SetColumn(cell, index - GridWidth * row);

  grid.Children.Add(cell);
}

Pretty silly Stack Overflow entry like this, but I hope it's useful for someone.

xatja
  • 11
  • 2