Add BugSweeper sample

This commit is contained in:
Frank A. Krueger 2017-12-09 19:18:34 -08:00
parent 0ecc2d19b5
commit d0734ae795
10 changed files with 714 additions and 1 deletions

View File

@ -1,5 +1,5 @@
var debug = true; var debug = false;
const nodes = {}; const nodes = {};

13
Samples/BugSweeper/App.cs Executable file
View File

@ -0,0 +1,13 @@
using System;
using Xamarin.Forms;
namespace BugSweeper
{
public class App : Application
{
public App ()
{
MainPage = new BugSweeperPage();
}
}
}

235
Samples/BugSweeper/Board.cs Executable file
View File

@ -0,0 +1,235 @@
using System;
using Xamarin.Forms;
namespace BugSweeper
{
class Board : AbsoluteLayout
{
// Alternative sizes make the tiles a tad small.
const int COLS = 9; // 16
const int ROWS = 9; // 16
const int BUGS = 10; // 40
Tile[,] tiles = new Tile[ROWS, COLS];
int flaggedTileCount;
bool isGameInProgress; // on first tap
bool isGameInitialized; // on first double-tap
bool isGameEnded;
// Events to notify page.
public event EventHandler GameStarted;
public event EventHandler<bool> GameEnded;
public Board()
{
for (int row = 0; row < ROWS; row++)
for (int col = 0; col < COLS; col++)
{
Tile tile = new Tile(row, col);
tile.TileStatusChanged += OnTileStatusChanged;
this.Children.Add(tile);
tiles[row, col] = tile;
}
SizeChanged += (sender, args) =>
{
double tileWidth = this.Width / COLS;
double tileHeight = this.Height / ROWS;
foreach (Tile tile in tiles)
{
Rectangle bounds = new Rectangle(tile.Col * tileWidth,
tile.Row * tileHeight,
tileWidth, tileHeight);
AbsoluteLayout.SetLayoutBounds(tile, bounds);
}
};
NewGameInitialize();
}
public void NewGameInitialize()
{
// Clear all the tiles.
foreach (Tile tile in tiles)
tile.Initialize();
isGameInProgress = false;
isGameInitialized = false;
isGameEnded = false;
this.FlaggedTileCount = 0;
}
public int FlaggedTileCount
{
set
{
if (flaggedTileCount != value)
{
flaggedTileCount = value;
OnPropertyChanged();
}
}
get
{
return flaggedTileCount;
}
}
public int BugCount
{
get
{
return BUGS;
}
}
// Not called until the first tile is double-tapped.
void DefineNewBoard(int tappedRow, int tappedCol)
{
// Begin the assignment of bugs.
Random random = new Random();
int bugCount = 0;
while (bugCount < BUGS)
{
// Get random row and column.
int row = random.Next(ROWS);
int col = random.Next(COLS);
// Skip it if it's already a bug.
if (tiles[row, col].IsBug)
{
continue;
}
// Avoid the tappedRow & Col & surrounding ones.
if (row >= tappedRow - 1 &&
row <= tappedRow + 1 &&
col >= tappedCol - 1 &&
col <= tappedCol + 1)
{
continue;
}
// It's a bug!
tiles[row, col].IsBug = true;
// Calculate the surrounding bug count.
CycleThroughNeighbors(row, col,
(neighborRow, neighborCol) =>
{
++tiles[neighborRow, neighborCol].SurroundingBugCount;
});
bugCount++;
}
}
void CycleThroughNeighbors(int row, int col, Action<int, int> callback)
{
int minRow = Math.Max(0, row - 1);
int maxRow = Math.Min(ROWS - 1, row + 1);
int minCol = Math.Max(0, col - 1);
int maxCol = Math.Min(COLS - 1, col + 1);
for (int neighborRow = minRow; neighborRow <= maxRow; neighborRow++)
for (int neighborCol = minCol; neighborCol <= maxCol; neighborCol++)
{
if (neighborRow != row || neighborCol != col)
callback(neighborRow, neighborCol);
}
}
void OnTileStatusChanged(object sender, TileStatus tileStatus)
{
if (isGameEnded)
return;
// With a first tile tapped, the game is now in progress.
if (!isGameInProgress)
{
isGameInProgress = true;
// Fire the GameStarted event.
if (GameStarted != null)
{
GameStarted(this, EventArgs.Empty);
}
}
// Update the "flagged" bug count before checking for a loss.
int flaggedCount = 0;
foreach (Tile tile in tiles)
if (tile.Status == TileStatus.Flagged)
flaggedCount++;
this.FlaggedTileCount = flaggedCount;
// Get the tile whose status has changed.
Tile changedTile = (Tile)sender;
// If it's exposed, some actions are required.
if (tileStatus == TileStatus.Exposed)
{
if (!isGameInitialized)
{
DefineNewBoard(changedTile.Row, changedTile.Col);
isGameInitialized = true;
}
if (changedTile.IsBug)
{
isGameInProgress = false;
isGameEnded = true;
// Fire the GameEnded event!
if (GameEnded != null)
{
GameEnded(this, false);
}
return;
}
// Auto expose for zero surrounding bugs.
if (changedTile.SurroundingBugCount == 0)
{
CycleThroughNeighbors(changedTile.Row, changedTile.Col,
(neighborRow, neighborCol) =>
{
// Expose all the neighbors.
tiles[neighborRow, neighborCol].Status = TileStatus.Exposed;
});
}
}
// Check for a win.
bool hasWon = true;
foreach (Tile til in tiles)
{
if (til.IsBug && til.Status != TileStatus.Flagged)
hasWon = false;
if (!til.IsBug && til.Status != TileStatus.Exposed)
hasWon = false;
}
// If there's a win, celebrate!
if (hasWon)
{
isGameInProgress = false;
isGameEnded = true;
// Fire the GameEnded event!
if (GameEnded != null)
{
GameEnded(this, true);
}
return;
}
}
}
}

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:BugSweeper"
x:Class="BugSweeper.BugSweeperPage"
Title="BugSweeper">
<ContentPage.Padding>
<OnPlatform x:TypeArguments="Thickness" iOS="0, 20, 0, 0" Android="0, 0, 0, 0" WinPhone="0, 0, 0, 0" />
</ContentPage.Padding>
<ContentView SizeChanged="OnMainContentViewSizeChanged">
<Grid x:Name="mainGrid" ColumnSpacing="0" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="7*" />
<RowDefinition Height="4*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout x:Name="textStack" Grid.Row="0" Grid.Column="1" Spacing="0">
<StackLayout HorizontalOptions="Center" Spacing="0">
<Label Text="BugSweeper" Font="Bold, Large" TextColor="Accent" />
<BoxView Color="Accent" HeightRequest="3" />
</StackLayout>
<Label Text="Tap to flag/unflag a potential bug." VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
<Label Text="Double-tap if you're sure it's not a bug.&#xA;The first double-tap is always safe!" VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
<StackLayout Orientation="Horizontal" Spacing="0" VerticalOptions="CenterAndExpand" HorizontalOptions="Center">
<Label BindingContext="{x:Reference board}" Text="{Binding FlaggedTileCount, StringFormat='Flagged {0} '}" />
<Label BindingContext="{x:Reference board}" Text="{Binding BugCount, StringFormat=' out of {0} bugs.'}" />
</StackLayout>
<!-- Make this a binding??? -->
<Label x:Name="timeLabel" Text="0:00" VerticalOptions="CenterAndExpand" HorizontalTextAlignment="Center" />
</StackLayout>
<ContentView Grid.Row="1" Grid.Column="1" SizeChanged="OnBoardContentViewSizeChanged">
<!-- Single-cell Grid for Board and overlays. -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<local:Board x:Name="board" />
<StackLayout x:Name="congratulationsText" Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center" Spacing="0">
<Label Text="C" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="N" TextColor="Red" />
<Label Text="G" TextColor="Red" />
<Label Text="R" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="T" TextColor="Red" />
<Label Text="U" TextColor="Red" />
<Label Text="L" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="T" TextColor="Red" />
<Label Text="I" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="N" TextColor="Red" />
<Label Text="S" TextColor="Red" />
<Label Text="!" TextColor="Red" />
</StackLayout>
<StackLayout x:Name="consolationText" Orientation="Horizontal" Spacing="0" HorizontalOptions="Center" VerticalOptions="Center">
<Label Text="T" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text="O" TextColor="Red" />
<Label Text=" " TextColor="Red" />
<Label Text="B" TextColor="Red" />
<Label Text="A" TextColor="Red" />
<Label Text="D" TextColor="Red" />
<Label Text="!" TextColor="Red" />
</StackLayout>
<Button x:Name="playAgainButton" Text=" Play Another Game? " HorizontalOptions="Center" VerticalOptions="Center" Clicked="OnplayAgainButtonClicked"
BorderColor="Black" BorderWidth="2" BackgroundColor="White" TextColor="Black" />
</Grid>
</ContentView>
</Grid>
</ContentView>
</ContentPage>

View File

@ -0,0 +1,174 @@
#define FIX_WINPHONE_BUTTON // IsEnabled = false doesn't disable button
#pragma warning disable 4014 // for non-await'ed async call
using System;
using System.Linq;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace BugSweeper
{
public partial class BugSweeperPage : ContentPage
{
const string timeFormat = @"%m\:ss";
bool isGameInProgress;
DateTime gameStartTime;
public BugSweeperPage()
{
InitializeComponent();
board.GameStarted += (sender, args) =>
{
isGameInProgress = true;
gameStartTime = DateTime.Now;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
timeLabel.Text = (DateTime.Now - gameStartTime).ToString(timeFormat);
return isGameInProgress;
});
};
board.GameEnded += (sender, hasWon) =>
{
isGameInProgress = false;
if (hasWon)
{
DisplayWonAnimation();
}
else
{
DisplayLostAnimation();
}
};
PrepareForNewGame();
}
void PrepareForNewGame()
{
board.NewGameInitialize();
congratulationsText.IsVisible = false;
consolationText.IsVisible = false;
playAgainButton.IsVisible = false;
playAgainButton.IsEnabled = false;
timeLabel.Text = new TimeSpan().ToString(timeFormat);
isGameInProgress = false;
}
void OnMainContentViewSizeChanged(object sender, EventArgs args)
{
ContentView contentView = (ContentView)sender;
double width = contentView.Width;
double height = contentView.Height;
bool isLandscape = width > height;
if (isLandscape)
{
mainGrid.RowDefinitions[0].Height = 0;
mainGrid.RowDefinitions[1].Height = new GridLength(1, GridUnitType.Star);
mainGrid.ColumnDefinitions[0].Width = new GridLength(1, GridUnitType.Star);
mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
Grid.SetRow(textStack, 1);
Grid.SetColumn(textStack, 0);
}
else // portrait
{
mainGrid.RowDefinitions[0].Height = new GridLength(3, GridUnitType.Star);
mainGrid.RowDefinitions[1].Height = new GridLength(5, GridUnitType.Star);
mainGrid.ColumnDefinitions[0].Width = 0;
mainGrid.ColumnDefinitions[1].Width = new GridLength(1, GridUnitType.Star);
Grid.SetRow(textStack, 0);
Grid.SetColumn(textStack, 1);
}
}
// Maintains a square aspect ratio for the board.
void OnBoardContentViewSizeChanged(object sender, EventArgs args)
{
ContentView contentView = (ContentView)sender;
double width = contentView.Width;
double height = contentView.Height;
double dimension = Math.Min(width, height);
double horzPadding = (width - dimension) / 2;
double vertPadding = (height - dimension) / 2;
contentView.Padding = new Thickness(horzPadding, vertPadding);
}
async void DisplayWonAnimation()
{
congratulationsText.Scale = 0;
congratulationsText.IsVisible = true;
// Because IsVisible has been false, the text might not have a size yet,
// in which case Measure will return a size.
double congratulationsTextWidth = congratulationsText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
congratulationsText.Rotation = 0;
congratulationsText.RotateTo(3 * 360, 1000, Easing.CubicOut);
double maxScale = 0.9 * board.Width / congratulationsTextWidth;
await congratulationsText.ScaleTo(maxScale, 1000);
foreach (View view in congratulationsText.Children)
{
view.Rotation = 0;
view.RotateTo(180);
await view.ScaleTo(3, 100);
view.RotateTo(360);
await view.ScaleTo(1, 100);
}
await DisplayPlayAgainButton();
}
async void DisplayLostAnimation()
{
consolationText.Scale = 0;
consolationText.IsVisible = true;
// (See above for rationale)
double consolationTextWidth = consolationText.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
double maxScale = 0.9 * board.Width / consolationTextWidth;
await consolationText.ScaleTo(maxScale, 1000);
await Task.Delay(1000);
await DisplayPlayAgainButton();
}
async Task DisplayPlayAgainButton()
{
playAgainButton.Scale = 0;
playAgainButton.IsVisible = true;
playAgainButton.IsEnabled = true;
// (See above for rationale)
double playAgainButtonWidth = playAgainButton.Measure(Double.PositiveInfinity, Double.PositiveInfinity).Request.Width;
double maxScale = board.Width / playAgainButtonWidth;
await playAgainButton.ScaleTo(maxScale, 1000, Easing.SpringOut);
}
void OnplayAgainButtonClicked(object sender, object EventArgs)
{
#if FIX_WINPHONE_BUTTON
if (Device.OS == TargetPlatform.WinPhone && !((Button)sender).IsEnabled)
return;
#endif
PrepareForNewGame();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

191
Samples/BugSweeper/Tile.cs Executable file
View File

@ -0,0 +1,191 @@
#define FIX_WINDOWS_DOUBLE_TAPS // Double-taps don't work well on Windows Runtime as of 2.3.0
#define FIX_WINDOWS_PHONE_NULL_CONTENT // Set Content of Frame to null doesn't work in Windows as of 2.3.0
using System;
using Xamarin.Forms;
namespace BugSweeper
{
enum TileStatus
{
Hidden,
Flagged,
Exposed
}
class Tile : Frame
{
TileStatus tileStatus = TileStatus.Hidden;
Label label;
Image flagImage, bugImage;
static ImageSource flagImageSource;
static ImageSource bugImageSource;
bool doNotFireEvent;
public event EventHandler<TileStatus> TileStatusChanged;
static Tile()
{
flagImageSource = ImageSource.FromResource("Samples.BugSweeper.Images.Xamarin120.png", System.Reflection.Assembly.GetCallingAssembly());
bugImageSource = ImageSource.FromResource("Samples.BugSweeper.Images.RedBug.png", System.Reflection.Assembly.GetCallingAssembly ());
}
public Tile(int row, int col)
{
this.Row = row;
this.Col = col;
this.BackgroundColor = Color.Yellow;
this.OutlineColor = Color.Blue;
this.Padding = 2;
label = new Label {
Text = " ",
TextColor = Color.Yellow,
BackgroundColor = Color.Blue,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
};
flagImage = new Image {
Source = flagImageSource,
};
bugImage = new Image {
Source = bugImageSource
};
TapGestureRecognizer singleTap = new TapGestureRecognizer {
NumberOfTapsRequired = 1
};
singleTap.Tapped += OnSingleTap;
this.GestureRecognizers.Add(singleTap);
#if FIX_WINDOWS_DOUBLE_TAPS
if (Device.OS != TargetPlatform.Windows && Device.OS != TargetPlatform.WinPhone) {
#endif
TapGestureRecognizer doubleTap = new TapGestureRecognizer {
NumberOfTapsRequired = 2
};
doubleTap.Tapped += OnDoubleTap;
this.GestureRecognizers.Add(doubleTap);
#if FIX_WINDOWS_DOUBLE_TAPS
}
#endif
}
public int Row { private set; get; }
public int Col { private set; get; }
public bool IsBug { set; get; }
public int SurroundingBugCount { set; get; }
public TileStatus Status {
set {
if (tileStatus != value) {
tileStatus = value;
switch (tileStatus) {
case TileStatus.Hidden:
this.Content = null;
#if FIX_WINDOWS_PHONE_NULL_CONTENT
if (Device.OS == TargetPlatform.WinPhone || Device.OS == TargetPlatform.Windows) {
this.Content = new Label { Text = " " };
}
#endif
break;
case TileStatus.Flagged:
this.Content = flagImage;
break;
case TileStatus.Exposed:
if (this.IsBug) {
this.Content = bugImage;
} else {
this.Content = label;
label.Text =
(this.SurroundingBugCount > 0) ?
this.SurroundingBugCount.ToString() : " ";
}
break;
}
if (!doNotFireEvent && TileStatusChanged != null) {
TileStatusChanged(this, tileStatus);
}
}
}
get {
return tileStatus;
}
}
// Does not fire TileStatusChanged events.
public void Initialize()
{
doNotFireEvent = true;
this.Status = TileStatus.Hidden;
this.IsBug = false;
this.SurroundingBugCount = 0;
doNotFireEvent = false;
}
#if FIX_WINDOWS_DOUBLE_TAPS
bool lastTapSingle;
DateTime lastTapTime;
#endif
void OnSingleTap(object sender, object args)
{
#if FIX_WINDOWS_DOUBLE_TAPS
if (Device.OS == TargetPlatform.Windows || Device.OS == TargetPlatform.WinPhone) {
if (lastTapSingle && DateTime.Now - lastTapTime < TimeSpan.FromMilliseconds (500)) {
OnDoubleTap (sender, args);
lastTapSingle = false;
} else {
lastTapTime = DateTime.Now;
lastTapSingle = true;
}
}
#endif
switch (this.Status) {
case TileStatus.Hidden:
this.Status = TileStatus.Flagged;
break;
case TileStatus.Flagged:
this.Status = TileStatus.Hidden;
break;
case TileStatus.Exposed:
// Do nothing
break;
}
}
void OnDoubleTap (object sender, object args)
{
this.Status = TileStatus.Exposed;
}
}
}

View File

@ -0,0 +1,16 @@
using Ooui;
using Xamarin.Forms;
namespace Samples
{
public class BugSweeperSample : ISample
{
public string Title => "Xamarin.Forms BugSweeper";
public Ooui.Element CreateElement ()
{
var page = new BugSweeper.BugSweeperPage ();
return page.GetOouiElement ();
}
}
}

View File

@ -15,6 +15,8 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="**/*.xaml" /> <EmbeddedResource Include="**/*.xaml" />
<EmbeddedResource Include="BugSweeper\Images\RedBug.png" />
<EmbeddedResource Include="BugSweeper\Images\Xamarin120.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -41,6 +43,10 @@
</EmbeddedResource> </EmbeddedResource>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="BugSweeper\Images\RedBug.png" />
<None Remove="BugSweeper\Images\Xamarin120.png" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework> <TargetFramework>netcoreapp2.0</TargetFramework>