Mahjong
Learn creating a Mahjong game using Windows App SDK with this Tutorial
Mahjong shows how you can create the game of Mahjong based on the work by cry-inc, using game assets and a toolkit from NuGet using the Windows App SDK.
Step 1
Follow Setup and Start on how to get Setup and Install what you need for Visual Studio 2022 and Windows App SDK.
Step 2
Then in Visual Studio within Solution Explorer for the Solution, right click on the Project shown below the Solution and then select Manage NuGet Packages...
Step 3
Then in the NuGet Package Manager from the Browse tab search for Comentsys.Toolkit.WindowsAppSdk and then select Comentsys.Toolkit.WindowsAppSdk by Comentsys as indicated and select Install
This will add the package for Comentsys.Toolkit.WindowsAppSdk to your Project. If you get the Preview Changes screen saying Visual Studio is about to make changes to this solution. Click OK to proceed with the changes listed below. You can read the message and then select OK to Install the package.
Step 4
Then while still in the NuGet Package Manager from the Browse tab search for Comentsys.Assets.Games and then select Comentsys.Assets.Games by Comentsys as indicated and select Install
This will add the package for Comentsys.Assets.Games to your Project. If you get the Preview Changes screen saying Visual Studio is about to make changes to this solution. Click OK to proceed with the changes listed below. You can read the message and then select OK to Install the package, then you can close the tab for Nuget: Games by selecting the x next to it.
Step 5
Then in Visual Studio within Solution Explorer for the Solution, right click on the Project shown below the Solution and then select Add then New Item…
Step 6
Then in Add New Item from the C# Items list, select Code and then select Code File from the list next to this, then type in the name of Library.cs and then Click on Add.
Step 7
You will now be in the View for the Code of Library.cs then define a namespace
allowing classes to be defined together,
usually each is separate but will be defined in Library.cs by typing the following Code:
using Comentsys.Assets.Games;
using Comentsys.Toolkit.Binding;
using Comentsys.Toolkit.WindowsAppSdk;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Mahjong;
// State & Result Enums and Position & Tile Class
// Pair Class
public class Board
{
// Board Constants, Variables, Event & Get Methods
// Can Move, Can Move Up, Can Move Right, Can Move Left, Can Move & Next Move
// Add, Remove, Removable & Structure
// Get Hint & Scramble
// Constructor, Play, Set Hint & Set Disabled
}
// State to Brush Converter
public class Library
{
// Constants, Variables, Get Source, Set Sources, Get Tile & Shuffle
// Play
// Add
// Layout, Remove, New & Hint
}
Step 8
Still in Library.cs for the namespace
of Mahjong
you can define an enum
for State
and Result
along with a
Class
for Position
and Tile
to represent the Mahjong after the Comment of // State & Result Enums and Position & Tile Class
by typing the following:
public enum State
{
None,
Selected,
Disabled,
Hint
}
public enum Result
{
DifferentTypes = 1,
UnableToMove = 2,
InvalidMove = 4,
ValidMove = 8,
NoMoves = 16,
Winner = 32,
}
public class Position
{
public int Row { get; set; }
public int Column { get; set; }
public int Index { get; set; }
public Position(int row, int column, int index) =>
(Row, Column, Index) = (row, column, index);
}
public class Tile : ObservableBase
{
private State _state;
public Position Position { get; }
public MahjongTileType? Type { get; set; }
public State State { get => _state; set => SetProperty(ref _state, value); }
public Tile(MahjongTileType type, Position position) =>
(Type, Position) = (type, Position = position);
public Tile(Position position) =>
(Type, Position) = (null, Position = position);
}
Step 9
Still in the namespace
of Mahjong
in Library.cs after the Comment of // Pair Class
type the following:
public class Pair
{
private static readonly Random _random = new((int)DateTime.UtcNow.Ticks);
public Tile TileOne { get; set; }
public Tile TileTwo { get; set; }
public Pair(Tile tileOne, Tile tileTwo) =>
(TileOne, TileTwo) = (tileOne, tileTwo);
public static Pair Get(List tiles)
{
if(tiles.Count < 2)
throw new Exception();
var index = _random.Next() % tiles.Count;
var tileOne = tiles[index];
tiles.RemoveAt(index);
index = _random.Next() % tiles.Count;
var tileTwo = tiles[index];
tiles.RemoveAt(index);
return new Pair(tileOne, tileTwo);
}
}
Pair
will represent a couple of Tile
classes with a Property for each one it will also from a List
of them, get a randomly selected set of them.
Step 10
While still in the namespace
of Mahjong
in Library.cs and the class
of Board
and after the Comment
of // Board Constants, Variables, Event & Get Methods
type the following:
private const int rows = 8;
private const int columns = 10;
private const int indexes = 5;
private static 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 static readonly Random _random = new((int)DateTime.UtcNow.Ticks);
private static readonly List<MahjongTileType> _types =
Enum.GetValues(typeof(MahjongTileType))
.Cast<MahjongTileType>()
.Where(w => w != MahjongTileType.Back)
.ToList();
private readonly List<Tile> _tiles;
public delegate void RemovedEventHandler(Tile tile);
public event RemovedEventHandler Removed;
public IEnumerable<Tile> Get(int row, int column) =>
_tiles.Where(w => w.Position.Row == row
&& w.Position.Column == column)
.OrderBy(o => o.Position.Index);
private Tile Get(int row, int column, int index) =>
_tiles.FirstOrDefault(
f => f.Position.Row == row
&& f.Position.Column == column
&& f.Position.Index == index);
private Tile Get(Tile tile) =>
Get(tile.Position.Row, tile.Position.Column, tile.Position.Index);
Constants define the layout and configuration of the board for the game, there are Variables for both the types and tiles
themselves along with an Event Handler which will be used when playing the game when a tile is removed from the board.
Then there are Methods that are used to obtain a Tile
by row
& column
along with those plus index
as well as by Tile
.
Step 11
While still in the namespace
of Mahjong
in Library.cs and the class
of Board
and after
the Comment of // Can Move, Can Move Up, Can Move Right, Can Move Left, Can Move & Next Move
type the following Methods:
private bool CanMove(Tile tile, int rowOffset, int columnOffset, int indexOffset)
{
var found = Get(
tile.Position.Row + rowOffset,
tile.Position.Column + columnOffset,
tile.Position.Index + indexOffset
);
return found == null || tile == found;
}
private bool CanMoveUp(Tile tile) =>
CanMove(tile, 0, 0, 1);
private bool CanMoveRight(Tile tile) =>
CanMove(tile, 1, 0, 0);
private bool CanMoveLeft(Tile tile) =>
CanMove(tile, -1, 0, 0);
public bool CanMove(Tile tile)
{
bool up = CanMoveUp(tile);
bool upLeft = up && CanMoveLeft(tile);
bool upRight = up && CanMoveRight(tile);
return upLeft || upRight;
}
private bool NextMove()
{
var removable = new List<Tile>();
foreach (var 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;
}
CanMove
, CanMoveUp
, CanMoveRight
and CanMoveLeft
will be used to determine if a Tile
can be moved in those directions
and CanMove
will be used by NextMove
to determine the next available move.
Step 12
While still in the namespace
of Mahjong
in Library.cs and the class
of Boeard
and after
the Comment of // Add, Remove, Removable & Structure
type the following Methods:
private void Add(Tile tile) =>
_tiles.Add(tile);
private void Remove(Tile tile)
{
if (tile == Get(tile))
{
_tiles.Remove(tile);
Removed?.Invoke(tile);
}
}
private List<Tile> Removable()
{
List<Tile> removable = new();
foreach (var tile in _tiles)
if (CanMove(tile))
removable.Add(tile);
foreach (Tile tile in removable)
Remove(tile);
return removable;
}
private void Structure()
{
for (int index = 0; index < indexes; index++)
{
for (int row = 0; row < rows; row++)
{
for (int column = 0; column < columns; column++)
{
var current = _layout[row * columns + column];
if (current > 0 && index < current)
Add(new Tile(new Position(row, column, index)));
}
}
}
}
Add
will be used to add a Tile
to the List
of them and Remove
will not only remove it from the
List
but it will also Invoke
the Event Handler. Removable
will determine which tiles can be
removed and Structure
will create the structure for the tiles in the game.
Step 13
While still in the namespace
of Mahjong
in Library.cs and the class
of Board
after
the Comment of // Get Hint & Scramble
type the following Methods:
private Pair GetHint()
{
var tiles = new List<Tile>();
foreach (var 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 Pair(tiles[i], tiles[j]);
}
}
return null;
}
public void Scramble()
{
List<Pair> reversed = new();
while (_tiles.Count > 0)
{
List<Tile> removable = new();
removable.AddRange(Removable());
while (removable.Count > 1)
reversed.Add(Pair.Get(removable));
foreach (var tile in removable)
Add(tile);
}
for (int i = reversed.Count - 1; i >= 0; i--)
{
int index = _random.Next() % _types.Count;
reversed[i].TileOne.Type = _types[index];
reversed[i].TileTwo.Type = _types[index];
Add(reversed[i].TileOne);
Add(reversed[i].TileTwo);
}
}
GetHint
will be used to determine which are the next set of tiles that can be moved anywhere on the gameboard to
give a hint to the player of the game by checking which ones can be moved with CanMove
and Scramble
will be used to randomise the tiles used in the game.
Step 14
While still in the namespace
of Mahjong
in Library.cs and the class
of Board
after
the Comment of // Constructor, Play, Set Hint & Set Disabled
type the following Methods:
public Board()
{
_tiles = new List<Tile>();
Structure();
Scramble();
}
public Result Play(Tile tileOne, Tile tileTwo)
{
if (tileOne == tileTwo)
return Result.InvalidMove;
if (tileOne.Type != tileTwo.Type)
return Result.DifferentTypes;
if(!CanMove(tileOne) || !CanMove(tileTwo))
return Result.UnableToMove;
Remove(tileOne);
Remove(tileTwo);
if(_tiles.Count == 0)
return Result.Winner;
var result = Result.ValidMove;
if(!NextMove())
result |= Result.NoMoves;
return result;
}
public void SetHint()
{
if (_tiles.Count > 0)
{
var hint = GetHint();
if (hint != null)
{
hint.TileOne.State = State.Hint;
hint.TileTwo.State = State.Hint;
}
}
}
public void SetDisabled()
{
if(_tiles.Count > 0)
foreach(var tile in _tiles)
tile.State = CanMove(tile) ?
State.None : State.Disabled;
}
Play
will be used to check if the action is valid and produce the necessary outcome as well as checking if the
action is valid and if the game has been won or no further actions are valid, SetHint
will be used to indicate
which tiles are the hint ones and SetDisabled
will show which tiles are not valid.
Step 15
While still in the namespace
of Mahjong
in Library.cs after the
Comment of // State to Brush Converter
type the following Class:
public class StateToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, string language) =>
new SolidColorBrush((State)value switch
{
State.None => Colors.Transparent,
State.Selected => Colors.ForestGreen,
State.Disabled => Colors.DarkSlateGray,
State.Hint => Colors.CornflowerBlue,
_ => Colors.Transparent
});
public object ConvertBack(object value, Type targetType,
object parameter, string language) =>
throw new NotImplementedException();
}
StateToBrushConverter
defines an IValueConverter
that will be used with Data Binding, and this will
return a SolidColorBrush
of a given colour depending on the value of the State
used.
Step 16
While still in the namespace
of Mahjong
in Library.cs and the class
of Library
after
the Comment of // Constants, Variables, Get Source, Set Sources, Get Tile & Shuffle
type the following Constants, Variables and Methods:
private const string title = "Mahjong";
private const int rows = 8;
private const int columns = 10;
private const int tile_width = 74;
private const int tile_height = 95;
private const int square_height = 120;
private const int square_width = 90;
private readonly Dictionary<MahjongTileType, ImageSource> _sources = new();
private Board _board = new();
private Dialog _dialog;
private Grid _grid;
private Tile _selected;
private bool _gameOver;
private static async Task<ImageSource> GetSourceAsync(MahjongTileType type) =>
await MahjongTile.Get(type)
.AsImageSourceAsync();
private async Task SetSourcesAsync()
{
if (_sources.Count == 0)
foreach (var mahjongTileType in Enum.GetValues<MahjongTileType>())
_sources.Add(mahjongTileType, await GetSourceAsync(mahjongTileType));
}
private ImageSource GetTile(MahjongTileType? type) =>
type == null ? null : _sources[type.Value];
private void Shuffle()
{
_board.Scramble();
_grid.Children.Clear();
for (int column = 0; column < columns; column++)
for (int row = 0; row < rows; row++)
Add(row, column);
}
Constants are values that are used in the game that will not change and Variables are used to store various
values and controls needed for the game. GetSourceAsync
, SetSourcesAsync
and GetTile
are used for the assets
for the Mahjong tiles and Shuffle
is used to randomise the tiles displayed in the game the Method of Add
will be defined later.
Step 17
While still in the namespace
of Mahjong
in Library.cs and the class
of Library
after
the Comment of // Play
type the following Method:
private async void Play(Tile tile)
{
if(!_gameOver)
{
if (!_board.CanMove(tile))
return;
if (_selected == null || tile == _selected)
{
if (_selected == tile)
{
tile.State = State.None;
_selected = null;
}
else
{
tile.State = State.Selected;
_selected = tile;
}
}
else
{
var state = _board.Play(_selected, tile);
_board.SetDisabled();
if (state == Result.Winner)
_gameOver = true;
else if ((state & Result.NoMoves) != 0)
{
if (await _dialog.ConfirmAsync(
"No further moves. Shuffle?", "Yes", "No"))
Shuffle();
}
_selected = null;
}
}
if(_gameOver)
_dialog.Show("You Won, Game Over!");
}
Play
will check if the game is over, then will check if there is a valid move then will update the selected Tile
and then once
there are two the turn will be checked to see if it is valid then if the game is not over or there are moves still available
the game will continue otherwise the game will be over or if no more moves then there will be the option to shuffle the tiles.
Step 18
While still in the namespace
of Mahjong
in Library.cs and the class
of Library
after
the Comment of // Add
type the following Method:
private void Add(int row, int column)
{
Canvas square = new()
{
Width = square_width, Height = square_height
};
var tiles = _board.Get(row, column);
foreach (var tile in tiles)
{
Canvas canvas = new()
{
Tag = tile, Width = tile_width, Height = tile_height,
};
Image image = new()
{
Tag = tile,
Width = tile_width,
Height = tile_height,
Source = GetTile(tile.Type)
};
image.Tapped += (object sender, TappedRoutedEventArgs e) =>
Play((sender as Image).Tag as Tile);
canvas.Children.Add(image);
var rectangle = new Rectangle()
{
Tag = tile,
Opacity = 0.25,
Width = tile_width,
Height = tile_height,
IsHitTestVisible = false
};
var binding = new Binding()
{
Source = tile,
Mode = BindingMode.OneWay,
Converter = new StateToBrushConverter(),
Path = new PropertyPath(nameof(tile.State)),
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
BindingOperations.SetBinding(rectangle, Shape.FillProperty, binding);
if (!_board.CanMove(tile))
tile.State = State.Disabled;
canvas.Children.Add(rectangle);
Canvas.SetTop(canvas, -(tile.Position.Index * 5));
square.Children.Add(canvas);
}
square.SetValue(Grid.RowProperty, row);
square.SetValue(Grid.ColumnProperty, column);
_grid.Children.Add(square);
}
Step 19
While still in the namespace
of Reversi
in Library.cs and the class
of Library
after
the Comment of // Layout, Remove, New & Hint
type the following Methods:
private void Layout(Grid grid)
{
grid.Children.Clear();
_grid = new Grid();
for (int column = 0; column < columns; column++)
{
_grid.RowDefinitions.Add(new RowDefinition());
for (int row = 0; row < rows; row++)
{
if (row == 0)
_grid.ColumnDefinitions.Add(new ColumnDefinition());
Add(row, column);
}
}
grid.Children.Add(_grid);
}
private void Remove(Tile tile)
{
foreach (var item in _grid.Children.Cast<Canvas>()
.Where(w => w.Children.Any()))
{
var canvas = item.Children
.Cast<Canvas>()
.FirstOrDefault(w => w.Tag as Tile == tile);
if (canvas != null)
canvas.Children.Clear();
}
}
public async void New(Grid grid)
{
_gameOver = false;
_board = new Board();
_board.Removed += (Tile tile) =>
Remove(tile);
await SetSourcesAsync();
Layout(grid);
_dialog = new Dialog(grid.XamlRoot, title);
}
public void Hint() =>
_board.SetHint();
Layout
will create the look-and-feel of the game by setting up all the elements, Remove
will be set to use the Event Handler,
New
will setup and start a new game and assign the Event Handler and Hint
will be used to display the hint for the player.
Step 20
Step 21
In the XAML for MainWindow.xaml there will be some XAML for a StackPanel
, this should be Removed:
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
</StackPanel>
Step 22
While still in the XAML for MainWindow.xaml above </Window>
, type in the following XAML:
<Grid>
<Viewbox>
<Grid Margin="50" Name="Display"
HorizontalAlignment="Center"
VerticalAlignment="Center" Loaded="New">
<ProgressRing/>
</Grid>
</Viewbox>
<CommandBar VerticalAlignment="Bottom">
<AppBarButton Icon="Page2" Label="New" Click="New"/>
<AppBarButton Icon="Help" Label="Hint" Click="Hint"/>
</CommandBar>
</Grid>
This XAML contains a Grid
with a Viewbox
which will Scale a Grid
which contains a ProgressRing
which will display
until all assets have been set and it has a Loaded
event handler for New
which is also shared by an AppBarButton
along with another for Hint
.
Step 23
Step 24
In the Code for MainWindow.xaml.cs
there be a Method of myButton_Click(...)
this should be Removed by removing the following:
private void myButton_Click(object sender, RoutedEventArgs e)
{
myButton.Content = "Clicked";
}
Step 25
Once myButton_Click(...)
has been removed, within the Constructor of public MainWindow() { ... }
and below the line of this.InitializeComponent();
type in the following Code:
private readonly Library _library = new();
private void New(object sender, RoutedEventArgs e) =>
_library.New(Display);
private void Hint(object sender, RoutedEventArgs e) =>
_library.Hint();
Here an Instance of Library
is created then below this is the Method of New
and Hint
that will be used with Event Handler from the XAML, this Method
uses Arrow Syntax with the =>
for an Expression Body which is useful when a Method only has one line.
Step 26
Step 27
Once running you can then 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 which to choose, if no more can be moved then you'll get the option to Shuffle them, and you win when all the tiles have been removed or select New to start a new game.