Introduction

This is a familiar game with a new twist. To win, you must find and flag all ten bugs. Ten bugs are hidden in a 9-by-9 grid of tiles.That first double tab is always safe. Thereafter, numbers indicate the number of surrounding tiles with bugs. Flag it with a single tab. Avoid double-tabbing a tile with bug!.
Let's start.
Step 1

You can create Xamarin.Forms app by going to File >> New >> Visual C# >> Cross-platform >> Cross-Platform App (Xamarin.Native or Xamarin.Forms), enter desired name for your project, and press OK.

(Project name- BugSweeper)

Step 2

After the project creation, go to Solution Explorer >> BugSweeper(PCL) app >> click open MainPage.xaml and add the following code.

Used ToolBox items,
  • StackLayout
  • Label
  • BoxView
  • Grid
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
  3. xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
  4. xmlns:local="clr-namespace:BugSwapperApp"
  5. x:Class="BugSwapperApp.MainPage">
  6. <ContentView SizeChanged="OnMainContentView_SizeChanged">
  7. <Grid x:Name="mainGrid" ColumnSpacing="0" RowSpacing="0">
  8. <Grid.RowDefinitions>
  9. <RowDefinition Height="7*"/>
  10. <RowDefinition Height="4*"/>
  11. </Grid.RowDefinitions>
  12. <Grid.ColumnDefinitions>
  13. <ColumnDefinition Width="0"/>
  14. <ColumnDefinition Width="*"/>
  15. </Grid.ColumnDefinitions>
  16. <StackLayout x:Name="textStack" Grid.Row="0" Grid.Column="1" Spacing="0">
  17. <StackLayout HorizontalOptions="Center" Spacing="0">
  18. <Label Text="BugSweeper" Font="Bold, Large" TextColor="Navy"/>
  19. <BoxView Color="Orange" HeightRequest="3"/>
  20. </StackLayout>
  21. <Label Text="Tab to flag/unflag a potential bug" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
  22. <Label Text="Double-tab if you're sure it's not a bug.
  23. The First Double tab be safe" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
  24. <StackLayout Orientation="Horizontal" Spacing="0" VerticalOptions="CenterAndExpand" HorizontalOptions="Center">
  25. <Label BindingContext="{x:Reference board}" Text="{Binding FlaggedTileCount,StringFormat='Flagged {0}'}"/>
  26. <Label BindingContext="{x:Reference board}" Text="{Binding BugCount, StringFormat=' out of {0} bugs.'}"/>
  27. </StackLayout>
  28. <Label x:Name="timeLabel" Text="0:00" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
  29. </StackLayout>
  30. <ContentView Grid.Row="1" Grid.Column="1" SizeChanged="OnBoardContentView_SizeChanged">
  31. <Grid>
  32. <Grid.RowDefinitions>
  33. <RowDefinition Height="*"/>
  34. </Grid.RowDefinitions>
  35. <Grid.ColumnDefinitions>
  36. <ColumnDefinition Width="*"/>
  37. </Grid.ColumnDefinitions>
  38. <local:Board x:Name="board"/>
  39. <StackLayout x:Name="congratulationsText" Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center" Spacing="0">
  40. <Label Text="C" TextColor="Black"/>
  41. <Label Text="O" TextColor="Black"/>
  42. <Label Text="N" TextColor="Black"/>
  43. <Label Text="G" TextColor="Black"/>
  44. <Label Text="R" TextColor="Black"/>
  45. <Label Text="A" TextColor="Black"/>
  46. <Label Text="T" TextColor="Black"/>
  47. <Label Text="U" TextColor="Black"/>
  48. <Label Text="L" TextColor="Black"/>
  49. <Label Text="A" TextColor="Black"/>
  50. <Label Text="T" TextColor="Black"/>
  51. <Label Text="I" TextColor="Black"/>
  52. <Label Text="O" TextColor="Black"/>
  53. <Label Text="N" TextColor="Black"/>
  54. <Label Text="S" TextColor="Black"/>
  55. <Label Text="!" TextColor="Black"/>
  56. <Label Text="!" TextColor="Black"/>
  57. </StackLayout>
  58. <StackLayout x:Name="consolationText" Orientation="Horizontal" Spacing="0" HorizontalOptions="Center" VerticalOptions="Center">
  59. <Label Text="T" TextColor="Black"/>
  60. <Label Text="O" TextColor="Black"/>
  61. <Label Text="O" TextColor="Black"/>
  62. <Label Text="L" TextColor="Black"/>
  63. <Label Text="O" TextColor="Black"/>
  64. <Label Text="S" TextColor="Black"/>
  65. <Label Text="T" TextColor="Black"/>
  66. <Label Text="!" TextColor="Black"/>
  67. <Label Text=":" TextColor="Gray"/>
  68. <Label Text=")" TextColor="Green"/>
  69. </StackLayout>
  70. <Button x:Name="playAgainButton" Text="Play Another Game?" VerticalOptions="Center" HorizontalOptions="Center" Clicked="onplayAgainButton_Clicked" BorderColor="Black" BorderWidth="2" BackgroundColor="White" TextColor="Blue"/>
  71. </Grid>
  72. </ContentView>
  73. </Grid>
  74. </ContentView>
  75. </ContentPage>


Step 3

In this step, open Solution Explorer >> BugSweeper (PCL) >> MainPage.xaml.cs and double click to open MainPage.xaml.cs. Here is the code.
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Xamarin.Forms;
  7. namespace BugSwapperApp
  8. {
  9. public partial class MainPage : ContentPage
  10. {
  11. const string timeFormat = @"%m\:ss";
  12. bool isGameInProgress;
  13. DateTime gameStartTime;
  14. public MainPage()
  15. {
  16. InitializeComponent();
  17. board.GameStarted += (sender, args) =>
  18. {
  19. isGameInProgress = true;
  20. gameStartTime = DateTime.Now;
  21. Device.StartTimer(TimeSpan.FromSeconds(1), () =>
  22. {
  23. timeLabel.Text = (DateTime.Now - gameStartTime).ToString(timeFormat);
  24. return isGameInProgress;
  25. });
  26. }; board.GameEnded += (sender, hasWon) =>
  27. {
  28. isGameInProgress = false;
  29. if (hasWon)
  30. {
  31. DisplayWonAnimation();
  32. }
  33. else
  34. {
  35. DisplayLostAnimation();
  36. }
  37. };
  38. PrepareForNewGame();
  39. }
  40. void PrepareForNewGame()
  41. {
  42. board.NewGameInitialize();
  43. congratulationsText.IsVisible = false;
  44. consolationText.IsVisible = false;
  45. playAgainButton.IsVisible = false;
  46. playAgainButton.IsEnabled = false;
  47. timeLabel.Text = new TimeSpan().ToString(timeFormat);
  48. isGameInProgress = false;
  49. }
  50. void OnMainContentView_SizeChanged(object sender, EventArgs e)
  51. {
  52. ContentView contentView = (ContentView)sender;
  53. double width = contentView.Width;
  54. double height = contentView.Height;
  55. bool isLandscape = width > height;
  56. if (isLandscape)
  57. {
  58. mainGrid.RowDefinitions[0].Height = 0;
  59. mainGrid.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
  60. mainGrid.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
  61. mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
  62. Grid.SetRow(textStack, 1);
  63. Grid.SetColumn(textStack, 0);
  64. }
  65. else // portrait
  66. {
  67. mainGrid.RowDefinitions[0].Height = new GridLength(3, GridUnitType.Star);
  68. mainGrid.RowDefinitions[1].Height = new GridLength(5, GridUnitType.Star);
  69. mainGrid.ColumnDefinitions[0].Width = 0;
  70. mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
  71. Grid.SetRow(textStack, 0);
  72. Grid.SetColumn(textStack, 1);
  73. }
  74. }
  75. void OnBoardContentView_SizeChanged(object sender, EventArgs e)
  76. {
  77. ContentView contentView = (ContentView)sender;
  78. double width = contentView.Width;
  79. double height = contentView.Height;
  80. double dimension = Math.Min(width, height);
  81. double horzPadding = (width - dimension) / 2;
  82. double vertPadding = (height - dimension) / 2;
  83. contentView.Padding = new Thickness(horzPadding, vertPadding);
  84. }
  85. async void DisplayWonAnimation()
  86. {
  87. congratulationsText.Scale = 0;
  88. congratulationsText.IsVisible = true;
  89. // Because IsVisible has been false, the text might not have a size yet,
  90. // in which case Measure will return a size.
  91. double congratulationsTextWidth = congratulationsText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
  92. congratulationsText.Rotation = 0;
  93. congratulationsText.RotateTo(3 * 360, 1000, Easing.CubicOut);
  94. double maxScale = 0.9 * board.Width / congratulationsTextWidth;
  95. await congratulationsText.ScaleTo(maxScale, 1000);
  96. foreach (View view in congratulationsText.Children)
  97. {
  98. view.Rotation = 0;
  99. view.RotateTo(180);
  100. await view.ScaleTo(3, 100);
  101. view.RotateTo(360);
  102. await view.ScaleTo(1, 100);
  103. }
  104. await DisplayPlayAgainButton();
  105. }
  106. async void DisplayLostAnimation()
  107. {
  108. consolationText.Scale = 0;
  109. consolationText.IsVisible = true;
  110. // (See above for rationale)
  111. double consolationTextWidth = consolationText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
  112. double maxScale = 0.9 * board.Width / consolationTextWidth;
  113. await consolationText.ScaleTo(maxScale, 1000);
  114. await Task.Delay(1000);
  115. await DisplayPlayAgainButton();
  116. }
  117. async Task DisplayPlayAgainButton()
  118. {
  119. playAgainButton.Scale = 0;
  120. playAgainButton.IsVisible = true;
  121. playAgainButton.IsEnabled = true;
  122. // (See above for rationale)
  123. double playAgainButtonWidth = playAgainButton.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
  124. double maxScale = board.Width / playAgainButtonWidth;
  125. await playAgainButton.ScaleTo(maxScale, 1000, Easing.SpringOut);
  126. }
  127. void onplayAgainButton_Clicked(object sender, EventArgs e)
  128. {
  129. PrepareForNewGame();
  130. }
  131. }
  132. }

Step 4
Open Solution Explorer >> BugSweeper(PCL) >> right click and select New Item. In this popup window, select Cross Platform >> Class.

This way, you can add a new class. Now, create a new class named Board.cs and double-click to get into its design view and insert the code given below.
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Xamarin.Forms;
  7. namespace BugSwapperApp
  8. {
  9. class Board : AbsoluteLayout
  10. {
  11. const int COLS = 9;
  12. const int ROWS = 9;
  13. const int BUGS = 10;
  14. Tile[,] tiles = new Tile[ROWS, COLS];
  15. int flaggedTileCount;
  16. bool isGameInProgress;
  17. bool isGameInitialized;
  18. bool isGameEnded;
  19. public event EventHandler GameStarted;
  20. public event EventHandler<bool> GameEnded;
  21. public Board()
  22. {
  23. for (int row = 0; row < ROWS; row++)
  24. for (int col = 0; col < COLS; col++)
  25. {
  26. Tile tile = new Tile(row, col);
  27. tile.TileStatusChanged += OnTileStatusChanged;
  28. this.Children.Add(tile);
  29. tiles[row, col] = tile;
  30. }
  31. SizeChanged += (sender, args) =>
  32. {
  33. double tileWidth = this.Width / COLS;
  34. double tileHeight = this.Height / ROWS;
  35. foreach (Tile tile in tiles)
  36. {
  37. Rectangle bounds = new Rectangle(tile.Col * tileWidth,
  38. tile.Row * tileHeight,
  39. tileWidth, tileHeight);
  40. AbsoluteLayout.SetLayoutBounds(tile, bounds);
  41. }
  42. };
  43. NewGameInitialize();
  44. }
  45. public void NewGameInitialize()
  46. {
  47. // Clear all the tiles.
  48. foreach (Tile tile in tiles)
  49. tile.Initialize();
  50. isGameInProgress = false;
  51. isGameInitialized = false;
  52. isGameEnded = false;
  53. this.FlaggedTileCount = 0;
  54. }
  55. public int FlaggedTileCount
  56. {
  57. set
  58. {
  59. if (flaggedTileCount != value)
  60. {
  61. flaggedTileCount = value;
  62. OnPropertyChanged();
  63. }
  64. }
  65. get
  66. {
  67. return flaggedTileCount;
  68. }
  69. }
  70. public int BugCount
  71. {
  72. get
  73. {
  74. return BUGS;
  75. }
  76. }
  77. // Not called until the first tile is double-tapped.
  78. void DefineNewBoard(int tappedRow, int tappedCol)
  79. {
  80. // Begin the assignment of bugs.
  81. Random random = new Random();
  82. int bugCount = 0;
  83. while (bugCount < BUGS)
  84. {
  85. // Get random row and column.
  86. int row = random.Next(ROWS);
  87. int col = random.Next(COLS);
  88. // Skip it if it's already a bug.
  89. if (tiles[row, col].IsBug)
  90. {
  91. continue;
  92. }
  93. // Avoid the tappedRow & Col & surrounding ones.
  94. if (row >= tappedRow - 1 &&
  95. row <= tappedRow + 1 &&
  96. col >= tappedCol - 1 &&
  97. col <= tappedCol + 1)
  98. {
  99. continue;
  100. }
  101. // It's a bug!
  102. tiles[row, col].IsBug = true;
  103. // Calculate the surrounding bug count.
  104. CycleThroughNeighbors(row, col,
  105. (neighborRow, neighborCol) =>
  106. {
  107. ++tiles[neighborRow, neighborCol].SurroundingBugCount;
  108. });
  109. bugCount++;
  110. }
  111. }
  112. void CycleThroughNeighbors(int row, int col, Action<int, int> callback)
  113. {
  114. int minRow = Math.Max(0, row - 1);
  115. int maxRow = Math.Min(ROWS - 1, row + 1);
  116. int minCol = Math.Max(0, col - 1);
  117. int maxCol = Math.Min(COLS - 1, col + 1);
  118. for (int neighborRow = minRow; neighborRow <= maxRow; neighborRow++)
  119. for (int neighborCol = minCol; neighborCol <= maxCol; neighborCol++)
  120. {
  121. if (neighborRow != row || neighborCol != col)
  122. callback(neighborRow, neighborCol);
  123. }
  124. }
  125. void OnTileStatusChanged(object sender, TileStatus tileStatus)
  126. {
  127. if (isGameEnded)
  128. return;
  129. // With a first tile tapped, the game is now in progress.
  130. if (!isGameInProgress)
  131. {
  132. isGameInProgress = true;
  133. // Fire the GameStarted event.
  134. if (GameStarted != null)
  135. {
  136. GameStarted(this, EventArgs.Empty);
  137. }
  138. }
  139. // Update the "flagged" bug count before checking for a loss.
  140. int flaggedCount = 0;
  141. foreach (Tile tile in tiles)
  142. if (tile.Status == TileStatus.Flagged)
  143. flaggedCount++;
  144. this.FlaggedTileCount = flaggedCount;
  145. // Get the tile whose status has changed.
  146. Tile changedTile = (Tile)sender;
  147. // If it's exposed, some actions are required.
  148. if (tileStatus == TileStatus.Exposed)
  149. {
  150. if (!isGameInitialized)
  151. {
  152. DefineNewBoard(changedTile.Row, changedTile.Col);
  153. isGameInitialized = true;
  154. }
  155. if (changedTile.IsBug)
  156. {
  157. isGameInProgress = false;
  158. isGameEnded = true;
  159. // Fire the GameEnded event!
  160. if (GameEnded != null)
  161. {
  162. GameEnded(this, false);
  163. }
  164. return;
  165. }
  166. // Auto expose for zero surrounding bugs.
  167. if (changedTile.SurroundingBugCount == 0)
  168. {
  169. CycleThroughNeighbors(changedTile.Row, changedTile.Col,
  170. (neighborRow, neighborCol) =>
  171. {
  172. // Expose all the neighbors.
  173. tiles[neighborRow, neighborCol].Status = TileStatus.Exposed;
  174. });
  175. }
  176. }
  177. // Check for a win.
  178. bool hasWon = true;
  179. foreach (Tile til in tiles)
  180. {
  181. if (til.IsBug && til.Status != TileStatus.Flagged)
  182. hasWon = false;
  183. if (!til.IsBug && til.Status != TileStatus.Exposed)
  184. hasWon = false;
  185. }
  186. // If there's a win, celebrate!
  187. if (hasWon)
  188. {
  189. isGameInProgress = false;
  190. isGameEnded = true;
  191. // Fire the GameEnded event!
  192. if (GameEnded != null)
  193. {
  194. GameEnded(this, true);
  195. }
  196. return;
  197. }
  198. }
  199. }
  200. }


Step 5

Now, we need to add images. For that, go to Solution Explorer >> BugSweeper(PCL) >> right-click on BugSweeper(PCL) project and select Add>> Existing Item. In this popup window, select images to insert and the images are used for tiles.
  1. BugSwapperApp.Microsoft.png
  2. BugSwapperApp.Microsoft.png
Step 6

In this step, similarly, create a new class named Tile.cs. Here is the code for this class.
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using Xamarin.Forms;
  7. namespace BugSwapperApp
  8. {
  9. enum TileStatus
  10. {
  11. Hidden,
  12. Flagged,
  13. Exposed
  14. }
  15. class Tile : Frame
  16. {
  17. TileStatus tileStatus = TileStatus.Hidden;
  18. Label label;
  19. Image flagImage, bugImage;
  20. static ImageSource flagImageSource;
  21. static ImageSource bugImageSource;
  22. bool doNotFireEvent;
  23. public event EventHandler<TileStatus> TileStatusChanged;
  24. static Tile()
  25. {
  26. flagImageSource = ImageSource.FromResource("BugSwapperApp.Microsoft.png");
  27. bugImageSource = ImageSource.FromResource("BugSwapperApp.Xbox.png");
  28. }
  29. public Tile(int row , int col)
  30. {
  31. this.Row = row;
  32. this.Col = col;
  33. this.BackgroundColor = Color.Yellow;
  34. this.OutlineColor = Color.Blue;
  35. this.Padding = 2;
  36. label = new Label
  37. {
  38. Text = " ",
  39. TextColor = Color.Yellow,
  40. BackgroundColor = Color.Blue,
  41. HorizontalTextAlignment = TextAlignment.Center,
  42. VerticalTextAlignment = TextAlignment.Center,
  43. };
  44. flagImage = new Image
  45. {
  46. Source = flagImageSource,
  47. };
  48. bugImage = new Image
  49. {
  50. Source = bugImageSource
  51. };
  52. TapGestureRecognizer singleTap = new TapGestureRecognizer
  53. {
  54. NumberOfTapsRequired = 1
  55. };
  56. singleTap.Tapped += OnSingleTap;
  57. this.GestureRecognizers.Add(singleTap);
  58. if (Device.OS != TargetPlatform.Windows && Device.OS != TargetPlatform.WinPhone)
  59. {
  60. TapGestureRecognizer doubleTap = new TapGestureRecognizer
  61. {
  62. NumberOfTapsRequired = 2
  63. };
  64. doubleTap.Tapped += OnDoubleTap;
  65. this.GestureRecognizers.Add(doubleTap);
  66. }
  67. }
  68. public int Row { private set; get; }
  69. public int Col { private set; get; }
  70. public bool IsBug { set; get; }
  71. public int SurroundingBugCount { set; get; }
  72. public TileStatus Status
  73. {
  74. set
  75. {
  76. if (tileStatus != value)
  77. {
  78. tileStatus = value;
  79. switch (tileStatus)
  80. {
  81. case TileStatus.Hidden:
  82. this.Content = null;
  83. #if FIX_WINDOWS_PHONE_NULL_CONTENT
  84. if (Device.OS == TargetPlatform.WinPhone || Device.OS == TargetPlatform.Windows) {
  85. this.Content = new Label { Text = " " };
  86. }
  87. #endif
  88. break;
  89. case TileStatus.Flagged:
  90. this.Content = flagImage;
  91. break;
  92. case TileStatus.Exposed:
  93. if (this.IsBug)
  94. {
  95. this.Content = bugImage;
  96. }
  97. else
  98. {
  99. this.Content = label;
  100. label.Text =
  101. (this.SurroundingBugCount > 0) ?
  102. this.SurroundingBugCount.ToString() : " ";
  103. }
  104. break;
  105. }
  106. if (!doNotFireEvent && TileStatusChanged != null)
  107. {
  108. TileStatusChanged(this, tileStatus);
  109. }
  110. }
  111. }
  112. get
  113. {
  114. return tileStatus;
  115. }
  116. }
  117. // Does not fire TileStatusChanged events.
  118. public void Initialize()
  119. {
  120. doNotFireEvent = true;
  121. this.Status = TileStatus.Hidden;
  122. this.IsBug = false;
  123. this.SurroundingBugCount = 0;
  124. doNotFireEvent = false;
  125. }
  126. bool lastTapSingle;
  127. DateTime lastTapTime;
  128. void OnSingleTap(object sender, object args)
  129. {
  130. switch (this.Status)
  131. {
  132. case TileStatus.Hidden:
  133. this.Status = TileStatus.Flagged;
  134. break;
  135. case TileStatus.Flagged:
  136. this.Status = TileStatus.Hidden;
  137. break;
  138. case TileStatus.Exposed:
  139. break;
  140. }
  141. }
  142. void OnDoubleTap(object sender, object args)
  143. {
  144. this.Status = TileStatus.Exposed;
  145. }
  146. }
  147. }
Step 7

Click ' F5 ' or Build to run your projects. Running this project, you will have the result like below.

Finally, we have successfully created a Xamarin.Forms BugSweeper Application.