Universal Windows Platform – Mahjong

Mahjong demonstrates how to create a game where you can play a simple game of Mahjong which is a Game where you need to match pairs of Tiles until the board has been cleared.

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 Mahjong and select a Location and then select Ok to create the Project
vs2017-new-project-mahjong

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-mahjong

Step 7

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

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
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.Data;
using Windows.UI.Xaml.Markup;
using Windows.UI.Xaml.Media;

namespace Mahjong
{
    public enum MahjongSelect
    {
        None, Selected, Disabled, Incorrect, Hint
    }

    public enum MahjongState
    {
        DifferentType, NotMove, ValidMove, InvalidMove, NoMoves, Won
    }

    public enum MahjongType
    {
        Joker, RedDragon, GreenDragon, WhiteDragon, EastWind, SouthWind, WestWind, NorthWind,
        Spring, Summer, Autumn, Winter, Plum, Orchid, Chrysanthemum, Bamboo,
        OneOfCircles, TwoOfCircles, ThreeOfCircles, FourOfCircles, FiveOfCircles,
        SixOfCircles, SevenOfCircles, EightOfCircles, NineOfCircles,
        OneOfBamboos, TwoOfBamboos, ThreeOfBamboos, FourOfBamboos, FiveOfBamboos,
        SixOfBamboos, SevenOfBamboos, EightOfBamboos, NineOfBamboos,
        OneOfCharacters, TwoOfCharacters, ThreeOfCharacters, FourOfCharacters, FiveOfCharacters,
        SixOfCharacters, SevenOfCharacters, EightOfCharacters, NineOfCharacters
    }

    public class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected bool SetProperty<T>(ref T field, T value,
        [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        public void OnPropertyChanged(string name)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }

    public class MahjongPosition : BindableBase
    {
        public int Row { get; set; }
        public int Column { get; set; }
        public int Index { get; set; }

        public MahjongPosition(int column, int row)
        {
            Row = row;
            Column = column;
        }

        public MahjongPosition(int column, int row, int index)
        {
            Row = row;
            Column = column;
            Index = index;
        }
    }

    public class MahjongTile : BindableBase
    {
        private MahjongType? _type;
        private MahjongSelect _select;
        private MahjongPosition _position;

        public MahjongType? Type
        {
            get { return _type; }
            set { SetProperty(ref _type, value); }
        }

        public MahjongSelect Select
        {
            get { return _select; }
            set { SetProperty(ref _select, value); }
        }

        public MahjongPosition Position
        {
            get { return _position; }
            set { SetProperty(ref _position, value); }
        }

        public MahjongTile(MahjongType? type, int column, int row, int index)
        {
            Type = type;
            Position = new MahjongPosition(column, row, index);
        }
    }

    public class MahjongPair : BindableBase
    {
        private static readonly Random random = new Random((int)DateTime.Now.Ticks);
        private MahjongTile _tileOne;
        private MahjongTile _tileTwo;

        public MahjongTile TileOne
        {
            get { return _tileOne; }
            set { SetProperty(ref _tileOne, value); }
        }

        public MahjongTile TileTwo
        {
            get { return _tileTwo; }
            set { SetProperty(ref _tileTwo, value); }
        }

        public MahjongPair(MahjongTile tileOne, MahjongTile tileTwo)
        {
            _tileOne = tileOne;
            _tileTwo = tileTwo;
        }

        public static MahjongPair GetPair(List<MahjongTile> tiles)
        {
            if (tiles.Count < 2) throw new InvalidOperationException();
            int index = random.Next() % tiles.Count;
            MahjongTile tileOne = tiles[index];
            tiles.RemoveAt(index);
            index = random.Next() % tiles.Count;
            MahjongTile tileTwo = tiles[index];
            tiles.RemoveAt(index);
            return new MahjongPair(tileOne, tileTwo);
        }
    }

    public class MahjongBoard : BindableBase
    {
        public const int Rows = 8;
        public const int Columns = 10;
        public const int Indexes = 5;

        private readonly byte[] layout =
        {
            0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
            1, 1, 2, 2, 2, 2, 2, 2, 1, 1,
            1, 1, 2, 3, 4, 4, 3, 2, 1, 1,
            1, 1, 2, 4, 5, 5, 4, 2, 1, 1,
            1, 1, 2, 4, 5, 5, 4, 2, 1, 1,
            1, 1, 2, 3, 4, 4, 3, 2, 1, 1,
            1, 1, 2, 2, 2, 2, 2, 2, 1, 1,
            0, 1, 1, 1, 1, 1, 1, 1, 1, 0
        };
        private readonly List<MahjongType> types =
        Enum.GetValues(typeof(MahjongType)).Cast<MahjongType>().ToList();
        private readonly Random random = new Random((int)DateTime.Now.Ticks);

        private bool _started;
        private MahjongState _state;
        private ObservableCollection<MahjongTile> _tiles;

        private MahjongTile GetTile(int column, int row, int index)
        {
            return _tiles.FirstOrDefault(w =>
            w.Position.Row == row &&
            w.Position.Column == column &&
            w.Position.Index == index);
        }

        private MahjongPosition[] GetPositions(int column, int row)
        {
            MahjongPosition[] positions = new MahjongPosition[Columns * Rows];
            int p = 0;
            for (int c = 0; c < Columns; c++)
            {
                for (int r = 0; r < Rows; r++)
                {
                    positions[p++] = new MahjongPosition(column + c, row + r);
                }
            }
            return positions;
        }

        private bool CanMove(MahjongTile tile, int column, int row, int index)
        {
            MahjongPosition[] positions = GetPositions(tile.Position.Column, tile.Position.Row);
            int i = tile.Position.Index + index;
            for (int p = 0; p < positions.Length; p++)
            {
                int c = positions[p].Column + column;
                int r = positions[p].Row + row;
                MahjongTile found = GetTile(c, r, i);
                if (found != null && tile != found) return false;
            }
            return true;
        }

        private bool CanMoveUp(MahjongTile tile) => CanMove(tile, 0, 0, 1);

        private bool CanMoveRight(MahjongTile tile) => CanMove(tile, 1, 0, 0);

        private bool CanMoveLeft(MahjongTile tile) => CanMove(tile, -1, 0, 0);

        public bool CanMove(MahjongTile tile)
        {
            bool up = CanMoveUp(tile);
            bool leftUp = up && CanMoveLeft(tile);
            bool rightUp = up && CanMoveRight(tile);
            return leftUp || rightUp;
        }

        private bool NextMovePossible()
        {
            List<MahjongTile> removable = new List<MahjongTile>();
            foreach (MahjongTile tile in _tiles)
            {
                if (CanMove(tile)) removable.Add(tile);
            }
            for (int i = 0; i < removable.Count; i++)
            {
                for (int j = 0; j < removable.Count; j++)
                {
                    if (j != i && removable[i].Type == removable[j].Type)
                    {
                        return true;
                    }
                }
            }
            return false;
        }

        private MahjongTile AddTile(MahjongType? type, int column, int row, int index)
        {
            byte current = layout[row * Columns + column];
            return (current > 0 && index < current) ?
            new MahjongTile(type, column, row, index) : null;
        }

        private void Add(MahjongTile tile) => _tiles.Add(tile);

        private void Remove(MahjongTile tile) => _tiles.Remove(tile);

        private List<MahjongTile> ExtractRemovableTiles(MahjongBoard board)
        {
            List<MahjongTile> removable = new List<MahjongTile>();
            MahjongTile[] tiles = new MahjongTile[board.Tiles.Count];
            board.Tiles.CopyTo(tiles, 0);
            foreach (MahjongTile tile in tiles)
            {
                if (board.CanMove(tile)) removable.Add(tile);
            }
            foreach (MahjongTile tile in removable)
            {
                board.Remove(tile);
            }
            return removable;
        }

        private void Scramble(MahjongBoard board)
        {
            List<MahjongPair> reversed = new List<MahjongPair>();
            while (board.Tiles.Count > 0)
            {
                List<MahjongTile> removable = new List<MahjongTile>();
                removable.AddRange(ExtractRemovableTiles(board));
                while (removable.Count > 1)
                {
                    reversed.Add(MahjongPair.GetPair(removable));
                }
                foreach (MahjongTile tile in removable)
                {
                    board.Add(tile);
                }
            }
            for (int i = reversed.Count - 1; i >= 0; i--)
            {
                int typeIndex = random.Next() % types.Count;
                reversed[i].TileOne.Type = board.types[typeIndex];
                reversed[i].TileTwo.Type = board.types[typeIndex];
                board.Add(reversed[i].TileOne);
                board.Add(reversed[i].TileTwo);
            }
        }

        private void Structure(MahjongBoard board)
        {
            for (int index = 0; index < Indexes; index++)
            {
                for (int column = 0; column < Columns; column++)
                {
                    for (int row = 0; row < Rows; row++)
                    {
                        MahjongTile tile = AddTile(null, column, row, index);
                        if (tile != null) board.Tiles.Add(tile);
                    }
                }
            }
        }

        private void Generate(MahjongBoard board)
        {
            board.Tiles = new ObservableCollection<MahjongTile>();
            Structure(board);
            Scramble(board);
        }

        private MahjongPair GetHint()
        {
            List<MahjongTile> tiles = new List<MahjongTile>();
            foreach (MahjongTile tile in _tiles)
            {
                if (CanMove(tile)) tiles.Add(tile);
            }
            for (int i = 0; i < tiles.Count; i++)
            {
                for (int j = 0; j < tiles.Count; j++)
                {
                    if (i == j) continue;
                    if (tiles[i].Type == tiles[j].Type)
                    {
                        return new MahjongPair(tiles[i], tiles[j]);
                    }
                }
            }
            return null;
        }

        public MahjongState State
        {
            get { return _state; }
            set { SetProperty(ref _state, value); }
        }

        public ObservableCollection<MahjongTile> Tiles
        {
            get { return _tiles; }
            set { SetProperty(ref _tiles, value); }
        }

        public void Start() => Generate(this);

        public void Shuffle() => Scramble(this);

        public MahjongState Play(MahjongTile tileOne, MahjongTile tileTwo)
        {
            if (!_started) _started = true;
            if (tileOne == tileTwo) return MahjongState.InvalidMove;
            if (tileOne.Type != tileTwo.Type) return MahjongState.DifferentType;
            if (!CanMove(tileOne) || !CanMove(tileTwo)) return MahjongState.NotMove;
            Remove(tileOne);
            Remove(tileTwo);
            if (_tiles.Count == 0) return MahjongState.Won;
            MahjongState result = MahjongState.ValidMove;
            if (!NextMovePossible()) result |= MahjongState.NoMoves;
            return result;
        }

        public void SetHint()
        {
            if (Tiles.Count > 0)
            {
                SetNone();
                MahjongPair pair = GetHint();
                pair.TileOne.Select = MahjongSelect.Hint;
                pair.TileTwo.Select = MahjongSelect.Hint;
            }
        }

        public void SetNone()
        {
            if (Tiles.Count > 0)
            {
                foreach (MahjongTile tile in Tiles)
                {
                    tile.Select = MahjongSelect.None;
                }
            }
        }

        public void SetDisabled()
        {
            if (Tiles.Count > 0)
            {
                foreach (MahjongTile tile in Tiles)
                {
                    tile.Select = CanMove(tile) ?
                    MahjongSelect.None : MahjongSelect.Disabled;
                }
            }
        }
    }

    public class MahjongSelectToBrushConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is MahjongSelect select)
            {
                switch (select)
                {
                    case MahjongSelect.None:
                        return new SolidColorBrush(Colors.Transparent);
                    case MahjongSelect.Selected:
                        return new SolidColorBrush(Colors.ForestGreen);
                    case MahjongSelect.Disabled:
                        return new SolidColorBrush(Colors.DarkSlateGray);
                    case MahjongSelect.Incorrect:
                        return new SolidColorBrush(Colors.IndianRed);
                    case MahjongSelect.Hint:
                        return new SolidColorBrush(Colors.CornflowerBlue);
                }
            }
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

    public class MahjongTypeToGlyphConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is MahjongType type)
            {
                switch (type)
                {
                    case MahjongType.Joker:
                        return "\U0001F02A";
                    case MahjongType.RedDragon:
                        return "\U0001F004";
                    case MahjongType.GreenDragon:
                        return "\U0001F005";
                    case MahjongType.WhiteDragon:
                        return "\U0001F006";
                    case MahjongType.EastWind:
                        return "\U0001F000";
                    case MahjongType.SouthWind:
                        return "\U0001F001";
                    case MahjongType.WestWind:
                        return "\U0001F002";
                    case MahjongType.NorthWind:
                        return "\U0001F003";
                    case MahjongType.Spring:
                        return "\U0001F026";
                    case MahjongType.Summer:
                        return "\U0001F027";
                    case MahjongType.Autumn:
                        return "\U0001F028";
                    case MahjongType.Winter:
                        return "\U0001F029";
                    case MahjongType.Plum:
                        return "\U0001F022";
                    case MahjongType.Orchid:
                        return "\U0001F023";
                    case MahjongType.Chrysanthemum:
                        return "\U0001F025";
                    case MahjongType.Bamboo:
                        return "\U0001F024";
                    case MahjongType.OneOfCircles:
                        return "\U0001F019";
                    case MahjongType.TwoOfCircles:
                        return "\U0001F01A";
                    case MahjongType.ThreeOfCircles:
                        return "\U0001F01B";
                    case MahjongType.FourOfCircles:
                        return "\U0001F01C";
                    case MahjongType.FiveOfCircles:
                        return "\U0001F01D";
                    case MahjongType.SixOfCircles:
                        return "\U0001F01E";
                    case MahjongType.SevenOfCircles:
                        return "\U0001F01F";
                    case MahjongType.EightOfCircles:
                        return "\U0001F020";
                    case MahjongType.NineOfCircles:
                        return "\U0001F021";
                    case MahjongType.OneOfBamboos:
                        return "\U0001F010";
                    case MahjongType.TwoOfBamboos:
                        return "\U0001F011";
                    case MahjongType.ThreeOfBamboos:
                        return "\U0001F012";
                    case MahjongType.FourOfBamboos:
                        return "\U0001F013";
                    case MahjongType.FiveOfBamboos:
                        return "\U0001F014";
                    case MahjongType.SixOfBamboos:
                        return "\U0001F015";
                    case MahjongType.SevenOfBamboos:
                        return "\U0001F016";
                    case MahjongType.EightOfBamboos:
                        return "\U0001F017";
                    case MahjongType.NineOfBamboos:
                        return "\U0001F018";
                    case MahjongType.OneOfCharacters:
                        return "\U0001F007";
                    case MahjongType.TwoOfCharacters:
                        return "\U0001F008";
                    case MahjongType.ThreeOfCharacters:
                        return "\U0001F009";
                    case MahjongType.FourOfCharacters:
                        return "\U0001F00A";
                    case MahjongType.FiveOfCharacters:
                        return "\U0001F00B";
                    case MahjongType.SixOfCharacters:
                        return "\U0001F00C";
                    case MahjongType.SevenOfCharacters:
                        return "\U0001F00D";
                    case MahjongType.EightOfCharacters:
                        return "\U0001F00E";
                    case MahjongType.NineOfCharacters:
                        return "\U0001F00F";
                }
            }
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

    public class IntToThicknessConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is int index) return new Thickness(0, 0, 0, index * 3);
            return null;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

    public class MahjongTileStyle : StyleSelector
    {
        public Style Tile { get; set; }

        protected override Style SelectStyleCore(object item, DependencyObject container)
        {
            if (item is MahjongTile tile) return Tile;
            return base.SelectStyleCore(item, container);
        }
    }

    public class Binder
    {
        public static readonly DependencyProperty GridColumnBindingPathProperty =
        DependencyProperty.RegisterAttached("GridColumnBindingPath", typeof(string), typeof(Binder),
        new PropertyMetadata(null, BindingPathPropertyChanged));

        public static readonly DependencyProperty GridRowBindingPathProperty =
        DependencyProperty.RegisterAttached("GridRowBindingPath", typeof(string), typeof(Binder),
        new PropertyMetadata(null, BindingPathPropertyChanged));

        public static readonly DependencyProperty CanvasZIndexBindingPathProperty =
        DependencyProperty.RegisterAttached("CanvasZIndexBindingPath", typeof(string), typeof(Binder),
        new PropertyMetadata(null, BindingPathPropertyChanged));

        public static string GetGridColumnBindingPath(DependencyObject obj)
        {
            return (string)obj.GetValue(GridColumnBindingPathProperty);
        }

        public static void SetGridColumnBindingPath(DependencyObject obj, string value)
        {
            obj.SetValue(GridColumnBindingPathProperty, value);
        }

        public static string GetGridRowBindingPath(DependencyObject obj)
        {
            return (string)obj.GetValue(GridRowBindingPathProperty);
        }

        public static void SetGridRowBindingPath(DependencyObject obj, string value)
        {
            obj.SetValue(GridRowBindingPathProperty, value);
        }

        public static string GetCanvasZIndexBindingPath(DependencyObject obj)
        {
            return (string)obj.GetValue(CanvasZIndexBindingPathProperty);
        }

        public static void SetCanvasZIndexBindingPath(DependencyObject obj, string value)
        {
            obj.SetValue(CanvasZIndexBindingPathProperty, value);
        }

        private static void BindingPathPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue is string path)
            {
                DependencyProperty property = null;
                if (e.Property == GridColumnBindingPathProperty)
                    property = Grid.ColumnProperty;
                else if (e.Property == GridRowBindingPathProperty)
                    property = Grid.RowProperty;
                else if (e.Property == CanvasZIndexBindingPathProperty)
                    property = Canvas.ZIndexProperty;

                BindingOperations.SetBinding(obj, property,
                new Binding { Path = new PropertyPath(path) });
            }
        }
    }

    public class Library
    {
        private const string app_title = "Mahjong";

        private bool _gameOver = false;
        private MahjongTile _selected = null;
        private IAsyncOperation<IUICommand> _dialogCommand;

        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 ItemsPanelTemplate Layout()
        {
            StringBuilder rows = new StringBuilder();
            StringBuilder columns = new StringBuilder();
            for (int r = 0; r < MahjongBoard.Rows; r++)
            {
                rows.Append($"<RowDefinition Height=\"Auto\"/>");
            }
            for (int c = 0; c < MahjongBoard.Columns; c++)
            {
                columns.Append($"<ColumnDefinition Width=\"Auto\"/>");
            }
            return (ItemsPanelTemplate)
            XamlReader.Load($@"<ItemsPanelTemplate 
            xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' 
            xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
                <Grid>
                    <Grid.ColumnDefinitions>{columns}</Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>{rows}</Grid.RowDefinitions>
                </Grid>
            </ItemsPanelTemplate>");
        }

        public MahjongBoard Board { get; set; } = new MahjongBoard();

        public void Init(ref ItemsControl display)
        {
            _gameOver = false;
            Board.Start();
            display.ItemsPanel = Layout();
            display.ItemsSource = Board.Tiles;
        }

        public async void Tapped(ItemsControl display, ContentPresenter container)
        {
            if (!_gameOver)
            {
                MahjongTile tile = (MahjongTile)display.ItemFromContainer(container);
                if (_selected == null || _selected == tile)
                {
                    Board.SetNone();
                    if (_selected == tile)
                    {
                        tile.Select = MahjongSelect.None;
                        _selected = null;
                    }
                    else
                    {
                        tile.Select = MahjongSelect.Selected;
                        _selected = tile;
                    }
                }
                else
                {
                    Board.State = Board.Play(_selected, tile);
                    switch (Board.State)
                    {
                        case MahjongState.DifferentType:
                            tile.Select = MahjongSelect.Incorrect;
                            break;
                        case MahjongState.NotMove:
                            tile.Select = MahjongSelect.Incorrect;
                            break;
                        case MahjongState.ValidMove:
                            tile.Select = MahjongSelect.None;
                            break;
                        case MahjongState.InvalidMove:
                            tile.Select = MahjongSelect.Incorrect;
                            break;
                        case MahjongState.NoMoves:
                            Board.Shuffle();
                            break;
                        case MahjongState.Won:
                            await ShowDialogAsync("You Won, Game Over!");
                            _gameOver = true;
                            break;
                    }
                    _selected = null;
                }
            }
            else
            {
                await ShowDialogAsync("You Won, Game Over!");
            }
        }

        public void New(ref ItemsControl display)
        {
            _gameOver = false;
            Board.Start();
            display.ItemsSource = Board.Tiles;
        }

        public void Hint() => Board.SetHint();

        public void Show() => Board.SetDisabled();

        public void Shuffle() => Board.Shuffle();
    }
}

In the Code File for Library there are using statements to include the necessary functionality. There is enum values for the Game for MahjongSelect which are the various visual selected States, MahjongState which are the various game States and MahjongType which are the types of Mahjong tile available. There is a BindableBase Class which implements INotifyPropertyChanged there are SetProperty and OnPropertyChanged Methods.

There is a MahjongPosition Class which Inherits from BindableBase and has int Properties for Row, Column and Index. The MahjongTile Class represents a Mahjong Tile and also Inherits from BindableBase and has Properties for MahjongType, MahjongSelect and MahjongPosition and there is a MahjongPair Class which represents a Pair of Tiles and also Inherits from BindableBase with Properties for MahjongTile and a Random to help select randomised numbers, within this Class is a GetPair Method which returns a set of two Tiles from a List of MahjongTile.

The MahjongBoard Class represents the Board of the Game itself and it Inherits from BindableBase. There are const int for Rows, Columns and Indexes and readonly Values for the byte[] Layout and List of MahjongType, Random, MahjongState and an ObservableCollection of MahjongTile. There is a GetTile Method to return a MahjongTile from the ObservableCollection of MahjongTile. The GetPositions Method returns an Array of MahjongPosition which is all the possible Positions. There is a CanMove Method which will return all possible Positions to see if a given Tile can be Moved, this will be called by the CanMoveUp, CanMoveRight, CanMoveLeft and CanMove by MahjongTile Methods. There is a NextMovePossible Method which will check if a Move is possible.

Also in the MahjongBoard Class there is an AddTile Method to help create the layout of the Board, an Add Method which has a Lambda to add to the ObservableCollection of MahjongTile and Remove to remove from it. The ExtractRemovableTiles will determine all the Tiles that can be moved, the Scramble Method will from the ObservableCollection of MahjongTile determine which ones are removable and create a randomised set of MahjongTile for the Board. There is a Structure Method which will create the set of MahjongTile for the Game and is called along with Scramble in the Generate Method. Ther GetHint Method is used to obtain a MahjongPair of Tiles from the Board which can be moved to help the Player.

While still in the MahjongBoard Class there are Properties for MahjongState and the ObservableCollection of MahjongTile, there is a Start Method Lambda which calls the Generate Method and Shuffle Method Lambda which calls the Scramble Method. There is a Play Method which will when a Tile is selected determine which MahjongState currently applies if the Pair of Tiles selected can be moved will call Remove Method and again update the MahjongState accordingly after these have been removed. Ther is a SetHint Method which will set the MahjongSelect of the MahjongPair and set their MahjongSelect to Hint, SetNone will set all Tiles to have the MahjongSelect of None and SetDisabled will set those Tiles that cannot be moved to have the MahjongSelect of Disabled.

There is a MahjongSelectToBrushConverter Class which will Convert a MahjongSelect to a relevant SolidColorBrush, There is a MahjongTypeToGlyphConverter Class which will Convert a MahjongType to the relevant Emoji Character to represent the Tiles. There is a IntToThicknessConverter which will convert a given int Index to a Thickness and there is a MahjongTileStyle is used to determine the Style for the Board. The Binder Class is used to create the Bindings for the DependencyProperty of Grid.ColumnProperty, Grid.RowProperty and Canvas.ZIndexProperty.

The Library Class has an IAsyncOperation of IUICommand for use with the ShowDialogAsync Method which will display a MessageDialog. There is a Layout Method which will be used to create the ItemsPanelTemplate for the display Layout of the Game Board. There is a MahjongBoard Property and an Init Method which will be used to create the look-and-feel of the Game, there is a Tapped Event Handler which will get Tapped MahjongTile and will set any first MahjongTile to have the MahjongSelect of Selected, when a second MahjongTile has been selected the Play Method of the MahjongBoard will be called and either a MahjongSelect will be set for a given MahjongState or the Shuffle Method will be called or ShowDialogAsync to indicate the Game is Over. There is a New Method which will begin a new Game, Hint which will display a hint to the player Show which will indicate which Tiles can be selected and Shuffle which will call the Shuffle Method from MahjongBoard.

Step 8

In the Solution Explorer select MainPage.xaml

vs2017-mainpage-mahjong

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 above the Grid element, enter the following XAML:

<Page.Resources>
	<Style TargetType="ContentPresenter" x:Key="TileStyle">
		<Setter Property="local:Binder.GridRowBindingPath" Value="Position.Row"/>
		<Setter Property="local:Binder.GridColumnBindingPath" Value="Position.Column"/>
		<Setter Property="local:Binder.CanvasZIndexBindingPath" Value="Position.Index" />
		<Setter Property="Background" Value="Transparent"/>
	</Style>
	<local:MahjongTileStyle x:Key="MahjongTileStyle" Tile="{StaticResource TileStyle}"/>
	<local:MahjongTypeToGlyphConverter x:Key="MahjongTypeToGlyphConverter"/>
	<local:MahjongSelectToBrushConverter x:Key="MahjongSelectToBrushConverter"/>
	<local:IntToThicknessConverter x:Key="IntToThicknessConverter"/>
	<DataTemplate x:Key="MahjongTemplate" x:DataType="local:MahjongTile">
		<Grid IsHitTestVisible="False" HorizontalAlignment="Center" VerticalAlignment="Center"
			 Margin="{x:Bind Position.Index, Mode=OneWay, Converter={StaticResource IntToThicknessConverter}}">
			<Viewbox>
				<TextBlock IsColorFontEnabled="True" FontFamily="Segoe UI Emoji" VerticalAlignment="Center"                           
				Text="{x:Bind Type, Mode=OneWay, Converter={StaticResource MahjongTypeToGlyphConverter}}"/>
			</Viewbox>
			<Rectangle Opacity="0.25" Margin="1,1,1,-1" RadiusX="2" RadiusY="2"
			HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
			Fill="{x:Bind Select, Mode=OneWay, Converter={StaticResource MahjongSelectToBrushConverter}}">
			</Rectangle>
		</Grid>
	</DataTemplate>
</Page.Resources>

Then while still in the XAML View between the Grid and /Grid elements, enter the following XAML:

<Viewbox>
	<ItemsControl Name="Display" Margin="50"
		ItemContainerStyleSelector="{StaticResource MahjongTileStyle}" 
		ItemTemplate="{StaticResource MahjongTemplate}" Tapped="Display_Tapped">
	</ItemsControl>
</Viewbox>
<CommandBar VerticalAlignment="Bottom">
	<AppBarButton Icon="Page2" Label="New" Click="New_Click"/>
	<AppBarButton Label="Hint" Click="Hint_Click">
		<AppBarButton.Icon>
			<FontIcon FontFamily="Segoe MDL2 Assets" Glyph=""/>
		</AppBarButton.Icon>
	</AppBarButton>
	<AppBarButton Label="Show" Click="Show_Click">
		<AppBarButton.Icon>
			<FontIcon FontFamily="Segoe MDL2 Assets" Glyph=""/>
		</AppBarButton.Icon>
	</AppBarButton>
	<AppBarButton Icon="Shuffle" Label="Shuffle" Click="Shuffle_Click"/>
</CommandBar>

Within the Page.Resources block of XAML above the Grid Element contains Style definitions including a Property for Row, Column and ZIndex. There is also the MahjongTileStyle, MahjongTypeToGlyphConverter, MahjongSelectToBrushConverter and IntToThicknessConverter. There is a DataTemplate which is for a MahjongTile which is a Grid with its Margin Databound to the Index using the IntToThicknessConverter and inside of which is the Viewbox with a TextBlock with its Text Databound to the Type using the MahjongTypeToGlyphConverter, there is a Rectangle with its.Background Databound to the MahjongSelectToBrushConverter.

Within the main Grid Element, the first block of XAML is a Viewbox Control which contains an ItemsControl which is where the Board will be displayed using the given ItemTemplate. The second block of XAML is a CommandBar with AppBarButton for New which calls New_Click, Hint which calls Hint_Click, Show which calls Show_Click and Shuffle which calls Shuffle_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);
}

private void Display_Tapped(object sender, TappedRoutedEventArgs e)
{
	library.Tapped(sender as ItemsControl, e.OriginalSource as ContentPresenter);
}

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

private void Hint_Click(object sender, RoutedEventArgs e)
{
	library.Hint();
}

private void Show_Click(object sender, RoutedEventArgs e)
{
	library.Show();
}

private void Shuffle_Click(object sender, RoutedEventArgs e)
{
	library.Shuffle();
}

There is an OnNavigatedTo Event Handler which will call the Init Method from the Library Class, Display_Tapped which handles when the ItemsControl has been Tapped and New_Click which will call the New Method of the Library Class, Hint_Click will call the Hint Method, Show_Click will call the Show Method and Shuffle_Click will call the Shuffle 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 a Tile and then select another Tile that matches to remove it from the Board – if you’re not sure which two to pick then use Hint to indicate a Pair to choose, or use Show to indicate which Tiles can’t be moved or if you can’t pick any Tiles then use Shuffle to move them around and you Win when all the Tiles have been removed!

uwp-ran-mahjong

Step 15

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

vs2017-close

Mahjong is based upon Mahjong solitaire which is a single-player matching game that uses a set of Mahjong Tiles, there is room for improvment with this example to make the Shuffle more reliable but for a great-looking but simple example of the game it is hopefully an interesting game to create.

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 )

Connecting to %s