Words Game
Learn creating a Words Game using Windows App SDK with this Tutorial
Words Game shows how you can create a game based on Wordle where the aim is to guess the five-letter word with just five chances to guess correctly using 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, then you can close the tab for Nuget: WordsGame by selecting the x next to it.
Step 4
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 5
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 6
You will now be in the View for the Code of Library.cs to 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 along with using
for Comentsys.Toolkit.WindowsAppSdk
and others plus an enum
for State
.
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.Media;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace WordsGame;
public enum State
{
Key,
Empty,
Absent,
Present,
Correct
}
// Position Class
// Item Class
// StateToBrushConvertor Class
// ItemTemplateSelector Class
// Words Class
public class Library
{
// Library Constants, Variables & GetIndexes Method
// Library ListCurrent, GetCurrent, Set & Check Method
// Library Over & Select Method
// Layout Method
// Setup, Load, Accept & New Methods
}
Step 7
Still in Library.cs for the namespace
of WordsGame
you will define a class
for
Position
after the Comment of // Position Class
by typing the following:
public class Position : ObservableBase
{
private int _row;
private int _column;
private char _letter;
public Position(int row, int column, char letter) =>
(_column, _row, _letter) = (column, row, letter);
public int Row
{
get => _row;
set => SetProperty(ref _row, value);
}
public int Column
{
get => _column;
set => SetProperty(ref _column, value);
}
public char Letter
{
get => _letter;
set => SetProperty(ref _letter, value);
}
}
Position
represents a Row
and Column
along with the Letter
and uses ObservableBase
from the package of Comentsys.Toolkit.WindowsAppSdk
.
Step 8
Still in Library.cs for the namespace
of WordsGame
you will define a class
for
Item
after the Comment of // Item Class
by typing the following:
public class Item : ActionCommandObservableBase
{
private State _state;
private Position _position;
public Item(Position position, State state) : base(null) =>
(_position, State) = (position, state);
public Item(Position position, State state, Action<Position> action) :
base(new ActionCommandHandler((param) => action(position))) =>
(_position, State) = (position, state);
public Position Position
{
get => _position;
set => SetProperty(ref _position, value);
}
public State State
{
get => _state;
set => SetProperty(ref _state, value);
}
}
Item
has Properties for Position
and State
uses
ActionCommandObservableBase
from the package of Comentsys.Toolkit.WindowsAppSdk
.
Step 9
Still in Library.cs for the namespace
of WordsGame
you will define a class
after the Comment of // StateToBrushConverter Class
by typing the following:
public class StateToBrushConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, string language)
{
if (value is State state)
{
return new SolidColorBrush(value switch
{
State.Empty => Colors.White,
State.Absent => Colors.DarkGray,
State.Present => Colors.DarkKhaki,
State.Correct => Colors.DarkSeaGreen,
_ => Colors.LightGray
});
}
return null;
}
public object ConvertBack(object value, Type targetType,
object parameter, string language) =>
throw new NotImplementedException();
}
StateToBrushConverter
uses the interface
of IValueConverter
for Data Binding which will allow the colours of the
Item in the game to be represented from either White, Dark Grey, Dark Khaki, Dark Sea Green or Light Grey as a SolidColorBrush
.
Step 10
Still in Library.cs for the namespace
of WordsGame
you will define a class
after the Comment of // ItemTemplateSelector Class
by typing the following:
public class ItemTemplateSelector : DataTemplateSelector
{
public DataTemplate SpacerItem { get; set; }
public DataTemplate KeyItem { get; set; }
protected override DataTemplate SelectTemplateCore
(object value, DependencyObject container) =>
value is Item item ? item?.Command != null ?
KeyItem : SpacerItem : null;
}
ItemTemplateSelector
will be used to provide a different DataTemplate
depending on whether the Command
has been set on an Item
,
this will be useful when creating the Keyboard used in the game.
Step 11
Still in Library.cs for the namespace
of WordsGame
you will define a class
after the Comment of // Words Class
by typing the following which will use HttpClient
to get a list of Words for the game:
public class Words
{
private const string request = "https://raw.githubusercontent.com/tutorialr/winappsdk-tutorials/main/Code/WordsGame/words.txt";
private readonly List<string> _results = new();
private readonly HttpClient _client = new();
public async Task RequestAsync()
{
try
{
_results.Clear();
var response = await _client.GetStreamAsync(request);
using var reader = new StreamReader(response);
while (!reader.EndOfStream)
{
var word = await reader.ReadLineAsync();
if (word != null)
_results.Add(word);
}
}
catch { }
}
public List<string> Response => _results;
}
Step 12
While still in the namespace
of WordsGame
in Library.cs and in the class
of Library
after the
Comment of // Library Constants, Variables & GetIndexes Method
type the following Constants, Variables and Method:
private const string title = "Words Game";
private const char backspace = '⌫';
private const char empty = ' ';
private const int count = 5;
private const int keys = 11;
private const int rows = 3;
private readonly Words _words = new();
private readonly ObservableCollection<Item> _keys = new();
private readonly ObservableCollection<Item> _items = new();
private readonly Random _random = new((int)DateTime.UtcNow.Ticks);
private readonly List<char> _letters = new()
{
'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', backspace,
empty, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', empty,
empty, empty, 'Z', 'X', 'C', 'V', 'B', 'N', 'M', empty, empty
};
private Dialog _dialog;
private string _word;
private bool _winner;
private int _column;
private int _row;
public static IEnumerable<int> GetIndexes(string source, char target)
{
int index = source.IndexOf(target);
while (index != -1)
{
yield return index;
index = source.IndexOf(target, index + 1);
}
}
Constants are values that are used in the game that will not change, Variables
are values that will be changed in the game and the Method of GetIndex
is used to get the positions of characters in a string
.
Step 13
While still in the namespace
of WordsGame
in Library.cs and in the class
of Library
after the
Comment of // Library ListCurrent, GetCurrent, Set & Check Method
type the following Methods:
private IEnumerable<Item> ListCurrent() =>
_items.Where(f => f.Position.Row == _row);
private Item GetCurrent() =>
_items.FirstOrDefault(
f => f.Position.Row == _row
&& f.Position.Column == _column);
private void Set(Position position, State state)
{
var key = _keys.FirstOrDefault(
f => f.Position.Letter == position.Letter);
if (key != null)
key.State = state;
var item = _items.FirstOrDefault(
f => f.Position.Row == _row
&& f.Position.Column == position.Column
&& f.Position.Letter == position.Letter);
if (item != null)
{
item.Position.Letter = position.Letter;
item.State = state;
}
}
private bool Check()
{
var current = ListCurrent();
foreach(var item in current)
{
var state = State.Absent;
var indexes = GetIndexes(_word, item.Position.Letter);
if(indexes?.Any() == true)
{
foreach (var index in indexes)
{
state = item.Position.Column == index ?
State.Correct : State.Present;
}
}
Set(item.Position, state);
}
var word = string.Join(string.Empty, current.Select(s => s.Position.Letter));
_winner = _word.Equals(word, StringComparison.InvariantCultureIgnoreCase);
return _winner;
}
ListCurrent
is used to return the items for a given Row
with GetCurrent
returning an Item
for a
given Row
and Column
plus Set
is used to update the State
for the Keyboard and Display of items
and Check
is used to determine if the letters are there, in right place or not present at all in the Word to guess.
Step 14
While still in the namespace
of WordsGame
in Library.cs and in the class
of Library
after the
Comment of // Library Over & Select Method
type the following Methods:
private bool Over()
{
if (_row == count)
{
_dialog.Show($"Game Over! You did not get the word {_word}!");
return true;
}
else if(_winner)
{
_dialog.Show($"Game Over! You got the word {_word} correct!");
return true;
}
return false;
}
private void Select(Position position)
{
if (!Over())
{
if (position.Letter == backspace)
{
if (_column > 0)
{
_column--;
var current = GetCurrent();
if (current != null)
{
current.State = State.Empty;
current.Position.Letter = empty;
}
}
}
else
{
if (_column < count)
{
var current = GetCurrent();
if (current != null)
{
current.State = State.Key;
current.Position.Letter = position.Letter;
_column++;
}
}
}
}
}
Over
is used to check if the game has been completed and show the appropriate message using a Dialog
and
Select
is used when choosing a letter or using the backspace
option.
Step 15
While still in the namespace
of WordsGame
in Library.cs and in the class
of Library
after the
Comment of // Library Layout Method
type the following Method:
private void Layout(ItemsControl display, ItemsControl keyboard)
{
int index = 0;
_keys.Clear();
_items.Clear();
for (int row = 0; row < count; row++)
{
for (int column = 0; column < count; column++)
{
_items.Add(new Item(
new Position(column, row, empty),
State.Empty));
}
}
display.ItemsSource = _items;
for (int row = 0; row < rows; row++)
{
for (int column = 0; column < keys; column++)
{
var letter = _letters[index];
var position = new Position(row, column, letter);
if (letter == empty)
_keys.Add(new Item(position,
State.Empty));
else
_keys.Add(new Item(position,
State.Key, (Position p) => Select(p)));
index++;
}
}
keyboard.ItemsSource = _keys;
}
Layout
is used to create the look and feel of the game including configuring the Display and Keyboard elements used in the game which use an ItemsControl
.
Step 16
While still in the namespace
of EmojiGame
in Library.cs and in the class
of Library
after the
Comment of // Setup, Load, Accept & New Method
type the following Methods
for Setup
and Load
which will initialise the game and list of Words plus Accept
to confirm the input Word and New
to start a new game.
private void Setup()
{
_row = 0;
_column = 0;
_winner = false;
var total = _words.Response.Count;
if (total > 0)
{
var choice = _random.Next(0, total - 1);
_word = _words.Response[choice];
foreach (var key in _keys)
key.State = State.Key;
foreach (var item in _items)
{
item.State = State.Empty;
item.Position.Letter = empty;
}
}
else
_dialog.Show("Failed to load Word List!");
}
public async void Load(ItemsControl display, ItemsControl keyboard)
{
_dialog = new Dialog(display.XamlRoot, title);
await _words.RequestAsync();
Layout(display, keyboard);
Setup();
}
public void Accept()
{
if(_row < count)
{
if (_column == count)
{
if (!Check())
{
_column = 0;
_row++;
}
}
else
_dialog.Show("Not enough letters");
}
Over();
}
public void New() =>
Setup();
Step 17
Step 18
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 19
While still in the XAML for MainWindow.xaml below <Window
, type in the following XAML:
xmlns:ui="using:Comentsys.Toolkit.WindowsAppSdk"
The XAML for <Window>
should then look as follows:
<Window
xmlns:ui="using:Comentsys.Toolkit.WindowsAppSdk"
x:Class="WordsGame.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WordsGame"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
Step 20
While still in the XAML for MainWindow.xaml above </Window>
, type in the following XAML:
<Grid>
<Grid.Resources>
<local:StateToBrushConverter x:Key="StateToBrushConverter"/>
<DataTemplate x:Name="ItemTemplate">
<ui:Piece IsSquare="True"
Stroke="LightGray"
Value="{Binding Position.Letter}"
Fill="{Binding State, Mode=OneWay,
Converter={StaticResource StateToBrushConverter},
ConverterParameter=True}" />
</DataTemplate>
<DataTemplate x:Name="KeyTemplate">
<Button Command="{Binding Command}">
<ui:Piece IsSquare="True"
Value="{Binding Position.Letter}"
Fill="{Binding State, Mode=OneWay,
Converter={StaticResource StateToBrushConverter},
ConverterParameter=True}" />
</Button>
</DataTemplate>
<DataTemplate x:Name="SpacerTemplate">
<Grid/>
</DataTemplate>
<local:ItemTemplateSelector x:Key="ItemTemplateSelector"
KeyItem="{StaticResource KeyTemplate}"
SpacerItem="{StaticResource SpacerTemplate}"/>
</Grid.Resources>
<Viewbox>
<!-- StackPanel -->
</Viewbox>
<CommandBar VerticalAlignment="Bottom">
<AppBarButton Icon="Accept" Label="Accept" Click="Accept"/>
<AppBarButton Icon="Page2" Label="New" Click="New"/>
</CommandBar>
</Grid>
This XAML contains a Grid
with a Viewbox which will Scale a StackPanel
to be added in the next Step.
It has an event handler for Accept
and New
for each AppBarButton
and defines the Templates that will be used in the game.
Step 21
While still in the XAML for MainWindow.xaml below the Comment of <!-- StackPanel -->
type in the following XAML:
<StackPanel Margin="50" Orientation="Vertical" Loaded="Load">
<ItemsControl Name="Display" Margin="10"
HorizontalAlignment="Center"
ItemTemplate="{StaticResource ItemTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid MaximumRowsOrColumns="5"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ProgressRing/>
</ItemsControl>
<ItemsControl Name="Keyboard" Margin="10"
HorizontalAlignment="Center"
ItemTemplateSelector="{StaticResource ItemTemplateSelector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsWrapGrid MaximumRowsOrColumns="11"
Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
This XAML contains a StackPanel
with a Loaded
event handler for Load
with the
ItemsPanel
for it set to use a VariableSizedWrapGrid
and ItemsWrapGrid
and uses the ItemTemplate
and the previously defined class
of ItemTemplateSelector
.
Step 22
Step 23
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 24
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 Load(object sender, RoutedEventArgs e) =>
_library.Load(Display, Keyboard);
private void Accept(object sender, RoutedEventArgs e) =>
_library.Accept();
private void New(object sender, RoutedEventArgs e) =>
_library.New();
Here an Instance of Library
is created then below this are the Methods of Load
, Accept
and New
that will be used with Event Handler
from the XAML
, these Methods use Arrow Syntax with the =>
for an expression body which is useful when a Method only has one line.
Step 25
Step 26
Once running you can then use the on-screen Keyboard to enter a Word with 5 letters and then use Accept then you will see which letters are in the correct position in Green, are in the Word but in the wrong position in Yellow or Dark Grey if no letters are in the Word and you get 5 chances to guess or you lose so guess correctly to win, or select New to start a new game.