Universal Windows Platform – Tiles Game

Tiles Game demonstrates how to create a Game where you have to tap on all the Black Tiles in the quickest time, however tap any White Tile or a Black Tile in the wrong place then you lose!

Step 1

If not already, follow Setup and Start on how to Install and get Started with Visual Studio 2017 or in Windows 10 choose Start, and then from the Start Menu find and select Visual Studio 2017.

vs2017

Step 2

Once Visual Studio Community 2017 has started, from the Menu choose File, then New then Project…

vs2017-file-new-project

Step 3

From New Project choose Visual C# from Installed, Templates then choose Blank App (Universal Windows) and then type in the Name as TilesGame and select a Location and then select Ok to create the Project
vs2017-new-project-tiles-game

Step 4

Then in New Universal Windows Project you need to select the Target Version this should be at least the Windows 10, version 1803 (10.0; Build 17134) which is the April 2018 Update and the Minimum Version to be the same.

vs2017-target-platform

The Target Version will control what features your application can use in Windows 10 so by picking the most recent version you’ll be able to take advantage of those features. To make sure you always have the most recent version, in Visual Studio 2017 select Tools Extensions and Updates… then and then see if there are any Updates

Step 5

Once done select from the Menu, Project, then Add New Item…

vs2017-project-add-new-item

Step 6

From the Add New Item window select Visual C#, then Code from Installed then select Code File from the list, then type in the Name as Library.cs before selecting Add to add the file to the Project

vs2017-add-new-item-tiles-game

Step 7

Once in the Code View for Library.cs the following should be entered:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

namespace TilesGame
{
    public enum TilesType
    {
        White, Black, Start, Finish, Correct, Incorrect
    }

    public enum TilesState
    {
        Ready, Started, Lost, Complete
    }

    public class TilesItem
    {
        public int Row { get; set; }
        public TilesType Type { get; set; }

        public TilesItem(int row, TilesType type)
        {
            Row = row;
            Type = type;
        }
    }

    public class TilesBoard
    {
        private const int total = 32;
        private const int offset = 3;
        private const int columns = 4;
        private const int view_height = 400;
        private const int view_width = 200;
        private const int tile_height = view_height / 4;
        private const int tile_width = view_width / 4;
        private readonly Random random = new Random((int)DateTime.Now.Ticks);
        private readonly Color[] colours =
        {
            Colors.White, Colors.Black, Colors.Yellow,
            Colors.ForestGreen, Colors.Gray, Colors.IndianRed
        };

        private List<int> _selected = new List<int>();
        private DateTime _start;
        private double _height;
        private int _current;
        private Grid _layout;

        public delegate void StateChangedEvent(TilesState state);
        public event StateChangedEvent StateChanged;

        public TimeSpan Current { get { return DateTime.Now - _start; } }

        public TimeSpan Best { get; private set; }

        public TimeSpan Time { get; private set; }

        public TilesState State { get; private set; }

        private List<int> Choose()
        {
            List<int> list = new List<int>();
            while ((list.Count < total))
            {
                list.Add(random.Next(0, columns));
            }
            return list;
        }

        private SolidColorBrush GetBrush(TilesType type)
        {
            return new SolidColorBrush(colours[(int)type]);
        }

        private void Grid_Tapped(object sender, TappedRoutedEventArgs e)
        {
            if (State != TilesState.Lost)
            {
                Grid grid = (Grid)sender;
                TilesItem item = (TilesItem)grid.Tag;
                if (item.Type == TilesType.Black)
                {
                    _current--;
                    if (_current == item.Row)
                    {
                        if (State != TilesState.Started)
                        {
                            _start = DateTime.Now;
                            State = TilesState.Started;
                        }
                        grid.Background = GetBrush(TilesType.Correct);
                        _height = _height + tile_height;
                        Canvas.SetTop(_layout, _height);
                        if (_current == offset + 1)
                        {
                            _height = _height + tile_height;
                            Canvas.SetTop(_layout, _height);
                            Time = DateTime.Now - _start;
                            if (Best == TimeSpan.Zero || Time < Best)
                            {
                                Best = Time;
                            }
                            State = TilesState.Complete;
                        }
                        else
                        {
                            State = TilesState.Started;
                        }
                    }
                    else
                    {
                        grid.Background = GetBrush(TilesType.Incorrect);
                        State = TilesState.Lost;
                    }
                }
                else if (item.Type == TilesType.White)
                {
                    grid.Background = GetBrush(TilesType.Incorrect);
                    State = TilesState.Lost;
                }
                StateChanged?.Invoke(State);
            }
        }

        private Grid Add(int row, int column)
        {
            TilesType type = TilesType.White;
            if (row <= offset)
            {
                type = TilesType.Finish;
            }
            else if (row >= total)
            {
                type = TilesType.Start;
            }
            else
            {
                type = (_selected[row] == column) ?
                TilesType.Black : TilesType.White;
            }
            Grid grid = new Grid()
            {
                Background = GetBrush(type),
                Tag = new TilesItem(row, type),
                BorderThickness = new Thickness(1),
                BorderBrush = new SolidColorBrush(Colors.WhiteSmoke)
            };
            Grid.SetRow(grid, row);
            Grid.SetColumn(grid, column);
            grid.Tapped += Grid_Tapped;
            return grid;
        }

        private void Layout(ref Canvas canvas)
        {
            canvas.Children.Clear();
            int rows = total + offset;
            _layout = new Grid()
            {
                VerticalAlignment = VerticalAlignment.Top,
                HorizontalAlignment = HorizontalAlignment.Center
            };
            _layout.Children.Clear();
            _layout.ColumnDefinitions.Clear();
            _layout.RowDefinitions.Clear();
            for (int row = 0; (row < rows); row++)
            {
                _layout.RowDefinitions.Add(new RowDefinition()
                {
                    Height = new GridLength(tile_height)
                });
            }
            for (int column = 0; (column < columns); column++)
            {
                _layout.ColumnDefinitions.Add(new ColumnDefinition()
                {
                    Width = new GridLength(tile_width)
                });
            }
            for (int row = 0; (row < total + offset); row++)
            {
                for (int column = 0; (column < columns); column++)
                {
                    _layout.Children.Add(Add(row, column));
                }
            }
            _height = -tile_height * total + (tile_height * offset);
            Canvas.SetTop(_layout, _height);
            canvas.Children.Add(_layout);
        }

        public void Init(ref Canvas canvas)
        {
            _current = total;
            Time = TimeSpan.Zero;
            State = TilesState.Ready;
            _selected = Choose();
            Layout(ref canvas);
        }
    }

    public class Library
    {
        private const string app_title = "Tiles Game";

        private TilesBoard _board = new TilesBoard();
        private IAsyncOperation<IUICommand> _dialogCommand;
        private DispatcherTimer _timer;

        private async Task<bool> ShowDialogAsync(string content, string title = app_title)
        {
            try
            {
                if (_dialogCommand != null)
                {
                    _dialogCommand.Cancel();
                    _dialogCommand = null;
                }
                _dialogCommand = new MessageDialog(content, title).ShowAsync();
                await _dialogCommand;
                return true;
            }
            catch (TaskCanceledException)
            {
                return false;
            }
        }

        private void SetTextBlock(TimeSpan value, TextBlock time, TextBlock best)
        {
            time.Text = $@"Time:{Environment.NewLine}{value:ss\.fff}";
            best.Text = $@"Best:{Environment.NewLine}{_board.Best:ss\.fff}";
        }

        private async void Board_StateChanged(TilesState state)
        {
            switch (state)
            {
                case TilesState.Lost:
                    await ShowDialogAsync($@"Game Over, You Lost! Best Time: {_board.Best:ss\.fff}");
                    break;
                case TilesState.Complete:
                    await ShowDialogAsync($@"Completion Time: {_board.Time:ss\.fff}, Best Time: {_board.Best:ss\.fff}");
                    break;
            }
        }

        public void Timer(TextBlock time, TextBlock best)
        {
            if (_timer != null)
            {
                _timer.Stop();
            }
            _timer = new DispatcherTimer()
            {
                Interval = TimeSpan.FromMilliseconds(100)
            };
            _timer.Tick += (object sender, object e) =>
            {
                if (_board.State == TilesState.Ready)
                {
                    SetTextBlock(_board.Time, time, best);
                }
                else if (_board.State == TilesState.Started)
                {
                    SetTextBlock(_board.Current, time, best);
                }
                else if (_board.State == TilesState.Complete)
                {
                    SetTextBlock(_board.Time, time, best);
                    _timer.Stop();
                }
                else
                {
                    _timer.Stop();
                }
            };
            _timer.Start();
        }

        public void Init(ref Canvas display, TextBlock time, TextBlock best)
        {
            _board.StateChanged += Board_StateChanged;
            _board.Init(ref display);
            Timer(time, best);
        }

        public void New(ref Canvas display, TextBlock time, TextBlock best)
        {
            _board.Init(ref display);
            Timer(time, best);
        }
    }
}

In the Code File for Library there are using statements to include the necessary functionality. There is enum values for the Game for TilesType which are the various Tiles Types, TilesState which are the various Game States. There is a TilesItem Class which has int and TilesType Properties

There is a TilesBoard Class which has various const Values do define the sizes for the look-and feel of the Game, plus readonly for Random for creating randomised values and an Array of Color values for the Game. There are Members for a List of int as well as other values for the Game. There is an event for StateChangedEvent and there are TimeSpan Properties and a TilesState Property.

Also in the TilesBoard Class there are Methods including Choose which is used to select a randomised List of int, GetBrush is used to return a SolidColorBrush from the Array of Color Defined. There is a Grid_Tapped Event Handler which will respond to events from Grid, it will check if the current State is Not Lost. If the TilesType is Black then will begin the game by setting the State to Started and then if Not already set then this is set to the TilesType of Correct and will calculate the offset to move itself within a Canvas by using SetTop and if the selected item was the last one it will calclate the TimeSpan Property Values and set the TilesState to Complete or for any other TilesType of Black will set the TilesState to Started tapping on any other than the Current Black Tile will set the TilesState to Lost and the TilesType of Incorrect – this will also happen if the TilesType is White, any change of State value will trigger the StateChanged Event Handler.

Also while still in TilesBoard Class there is a Add Method which will set the appropriate TilesType and then will create a Grid Element and Position it according to the row and column passed in. The Layout Method helps create the look-and-feel of the Game including the Size of the elements and their position. The Init Merthod is used to configure the Game to start and calls the Choose Method to pick the configuration of the items and then calls Layout Method which will use this to create a randomised set of Tiles.

The Library Class has various const for values for the Game and a Random to select randomised numbers. There is a Member for TilesBoard and DispatcherTimer and a Member for IAsyncOperation of ContentDialogResult. There is ShowDialogAsync Method to display a ContentDialog. The SetTextBlock Method is used to display values from the TimeSpan Properties. The Board_StateChanged Event Handler responds to the TilesState of Lost and Complete and will call ShowDialogAsync to show an appropriate message. The Timer Method is used to configure a DispatcherTimer which will be used to set the various TextBlock Elements to display the current TimeSpan Values. The Init Event is used to initialise the game and call the Init and Timer Methods which are also called by the New Method.

Step 8

In the Solution Explorer select MainPage.xaml

vs2017-mainpage-tiles-game

Step 9

From the Menu choose View and then Designer

vs2017-view-designer

Step 10

The Design View will be displayed along with the XAML View and in this between the Grid and /Grid elements,

<Viewbox>
	<StackPanel Orientation="Horizontal">
		<TextBlock Name="Time" Width="100" Foreground="{ThemeResource AccentButtonBackground}"
		VerticalAlignment="Center" HorizontalTextAlignment="Center"/>
		<Canvas Name="Display" Height="400" Width="200" 
		VerticalAlignment="Center" HorizontalAlignment="Center"/>
		<TextBlock Name="Best" Width="100" Foreground="{ThemeResource AccentButtonBackground}"
		VerticalAlignment="Center" HorizontalTextAlignment="Center"/>
	</StackPanel>
</Viewbox>
<CommandBar VerticalAlignment="Bottom">
	<AppBarButton Icon="Page2" Label="New" Click="New_Click"/>
</CommandBar>

Within the main Grid Element, the first block of XAML is a Viewbox Control which contains an StackPanel which is where the Game will appear, it also contains TextBlock Elements to display the values of the TimeSpan Properties. The second block of XAML is a CommandBar with AppBarButton for New which calls New_Click.

Step 11

From the Menu choose View and then Code

vs2017-view-code

Step 12

Once in the Code View, below the end of public MainPage() { … } the following Code should be entered:

Library library = new Library();

protected override void OnNavigatedTo(NavigationEventArgs e)
{
	library.Init(ref Display, Time, Best);
}

private void New_Click(object sender, RoutedEventArgs e)
{
	library.New(ref Display, Time, Best);
}

There is an OnNavigatedTo Event Handler which will call the Init Method from the Library Class, New_Click which will call the New Method of the Library Class.

Step 13

That completes the Universal Windows Platform Application so Save the Project then in Visual Studio select the Local Machine to run the Application

vs2017-local-machine

Step 14

Once the Application has started running you can tap on the first Black Tile to begin the Game, tap on them all then you win the game, but tap on any White Tile then you lose the game!

uwp-ran-tiles-game

Step 15

To Exit the Application select the Close button in the top right of the Application

vs2017-close

Tiles Game is a simple game that produces a Random set of Grid Elements that can be tapped to see if you can tap on them all as quickly as possible, this game is sometimes called Piano Game, Piano Tiles or Don’t tap the White Tile and is a simple game to play but a tricky one to master!

Creative Commons License

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s