Universal Windows Platform – Reversi

Reversi demonstrates how to create a game where you can play a simple game of Reversi also known as Othello for two players.

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

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

Step 7

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

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Markup;

namespace Reversi
{
    public enum ReversiState
    {
        Empty, Black, White
    }

    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 ReversiSquare : BindableBase
    {
        private ReversiState _state;
        private int _row;
        private int _column;

        public ReversiSquare(int row, int column)
        {
            _row = row;
            _column = column;
        }

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

        public int Row
        {
            get { return _row; }
            set { SetProperty(ref _row, value); }
        }

        public int Column
        {
            get { return _column; }
            set { SetProperty(ref _column, value); }
        }
    }

    public class ReversiBoard : BindableBase
    {
        private const int total = 8;

        delegate void Navigation(ref int row, ref int column);

        private static List<Navigation> _functions = new List<Navigation>()
        {
            new Navigation(delegate (ref int row, ref int column) { row++; }),
            new Navigation(delegate (ref int row, ref int column) { row++; }),
            new Navigation(delegate (ref int row, ref int column) { row--; }),
            new Navigation(delegate (ref int row, ref int column) { row++; column--; }),
            new Navigation(delegate (ref int row, ref int column) { row++; column++; }),
            new Navigation(delegate (ref int row, ref int column) { row--; column--; }),
            new Navigation(delegate (ref int row, ref int column) { row--; column++; }),
            new Navigation(delegate (ref int row, ref int column) { column++; }),
            new Navigation(delegate (ref int row, ref int column) { column--; }),
        };
        private ReversiState _nextMove = ReversiState.White;
        private int _whiteScore = 0;
        private int _blackScore = 0;
        private bool _gameOver = false;

        private ReversiSquare GetSquare(int row, int column)
        {
            return Squares.Single(s => s.Column == column && s.Row == row);
        }

        private IEnumerable<ReversiSquare> NavigateBoard(int row, int column,
            Navigation navigation)
        {
            navigation(ref column, ref row);
            while (row >= 0 && row < total && column >= 0 && column < total)
            {
                yield return GetSquare(row, column);
                navigation(ref column, ref row);
            }
        }

        private ReversiState ToggleState(ReversiState state)
        {
            return state == ReversiState.Black ?
            ReversiState.White : ReversiState.Black;
        }

        private bool IsMoveSurroundsCounters(int row, int column,
            Navigation navigation, ReversiState state)
        {
            int index = 1;
            var squares = NavigateBoard(row, column, navigation);
            foreach (var square in squares)
            {
                ReversiState currentState = square.State;
                if (index == 1)
                {
                    if (currentState != ToggleState(state)) return false;
                }
                else
                {
                    if (currentState == state) return true;
                    if (currentState == ReversiState.Empty) return false;
                }
                index++;
            }
            return false;
        }

        private void FlipOpponentCounters(int row, int column,
            ReversiState state)
        {
            foreach (Navigation navigation in _functions)
            {
                if (!IsMoveSurroundsCounters(row, column, navigation, state)) continue;
                ReversiState opponentState = ToggleState(state);
                IEnumerable<ReversiSquare> squares = NavigateBoard(row, column, navigation);
                foreach (var square in squares)
                {
                    if (square.State == state) break;
                    square.State = state;
                }
            }
        }

        private bool IsValidMove(int row, int col, ReversiState state)
        {
            if (GetSquare(row, col).State != ReversiState.Empty) return false;
            return _functions.Any(nav => IsMoveSurroundsCounters(row, col, nav, state));
        }

        private bool IsValidMove(int row, int col)
        {
            return IsValidMove(row, col, NextMove);
        }

        private bool HasPlayerMove(ReversiState state)
        {
            for (int row = 0; row < total; row++)
            {
                for (int column = 0; column < total; column++)
                {
                    if (IsValidMove(row, column, state)) return true;
                }
            }
            return false;
        }

        private bool IsGameOver()
        {
            return !HasPlayerMove(ReversiState.Black) &&
            !HasPlayerMove(ReversiState.White);
        }

        private void Setup()
        {
            foreach (ReversiSquare square in Squares)
            {
                square.State = ReversiState.Empty;
            }
            GetSquare(3, 4).State = ReversiState.Black;
            GetSquare(4, 3).State = ReversiState.Black;
            GetSquare(4, 4).State = ReversiState.White;
            GetSquare(3, 3).State = ReversiState.White;
            NextMove = ReversiState.Black;
            WhiteScore = 0;
            BlackScore = 0;
        }

        public ReversiBoard()
        {
            Squares = new List<ReversiSquare>();
            for (int row = 0; (row < total); row++)
            {
                for (int column = 0; (column < total); column++)
                {
                    Squares.Add(new ReversiSquare(row, column));
                }
            }
            Setup();
        }

        public int WhiteScore
        {
            get { return _whiteScore; }
            set { SetProperty(ref _whiteScore, value); }
        }

        public int BlackScore
        {
            get { return _blackScore; }
            set { SetProperty(ref _blackScore, value); }
        }

        public bool GameOver
        {
            get { return _gameOver; }
            set { SetProperty(ref _gameOver, value); }
        }
		
        public ReversiState NextMove
        {
            get { return _nextMove; }
            set { SetProperty(ref _nextMove, value); }
        }

        public List<ReversiSquare> Squares { get; }

        public void MakeMove(int row, int col)
        {
            if (!IsValidMove(row, col, NextMove)) return;
            GetSquare(row, col).State = NextMove;
            FlipOpponentCounters(row, col, NextMove);
            NextMove = ToggleState(NextMove);
            if (!HasPlayerMove(NextMove)) NextMove = ToggleState(NextMove);
            GameOver = IsGameOver();
            BlackScore = Squares.Count(s => s.State == ReversiState.Black);
            WhiteScore = Squares.Count(s => s.State == ReversiState.White);
        }

        public void Init()
        {
            Setup();
        }
    }

    public class ReversiStateToGlyphConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            if (value is ReversiState state)
            {
                switch (state)
                {
                    case ReversiState.Black: return "\u26AB";
                    case ReversiState.White: return "\u26AA";
                }
            }
            return null;
        }

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

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

        protected override Style SelectStyleCore(object item, DependencyObject container)
        {
            if (item is ReversiSquare square) 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, GridBindingPathPropertyChanged));

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

        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);
        }

        private static void GridBindingPathPropertyChanged(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;

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

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

        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 columns = new StringBuilder();
            StringBuilder rows = new StringBuilder();
            for (int i = 0; i < 8; i++)
            {
                columns.Append("<ColumnDefinition Width=\"*\"/>");
                rows.Append("<RowDefinition Height=\"*\"/>");
            }
            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 ReversiBoard Board { get; set; } = new ReversiBoard();

        public void Init(ref ItemsControl display, ref TextBlock scores)
        {
            display.ItemsPanel = Layout();
            display.ItemsSource = Board.Squares;
            scores.DataContext = Board;
        }

        public async void Tapped(ItemsControl display, ContentPresenter container)
        {
            if (!Board.GameOver)
            {
                ReversiSquare square = (ReversiSquare)display.ItemFromContainer(container);
                Board.MakeMove(square.Row, square.Column);
            }
            else
            {
                await ShowDialogAsync($"Game Over! White: {Board.WhiteScore}, Black: {Board.BlackScore}");
            }
        }

        public void New(ref ItemsControl display, ref TextBlock scores)
        {
            Board = new ReversiBoard();
            scores.DataContext = Board;
            display.ItemsSource = Board.Squares;
        }
    }
}

In the Code File for Library there are using statements to include the necessary functionality. There is enum values for the Game for ReversiState. There is a BindableBase Class which implements INotifyPropertyChanged there are SetProperty and OnPropertyChanged Methods. There is a ReversiSquare Class which represents a position in a Grid with an int Property for “Row” and “Column” and also has a Property for ReveriState.

The ReversiBoard Class represents the Board of the Game itself and it Inherits from BindableBase, it has a const int for the total size of the Board, there is a delegate for use with a List of Navigation Functions and there are Members for ReversiState and int for the Scores and a bool to indicate when the Game is over. There is a Method to return a ReversiSquare by “Row” and “Column” which will be a given Position, there is a NavigateBoard Method which returns an IEnumerable of Navigation for all possible Positions. There is a Method to Toggle the ReversiState, the IsMoveSurroundsCounters Method is used to determine if a given Move surrounds Counters already on the Board. The FlipOpponentCounters will use the IsMoveSurroundsCounters to set the related Counters to the correct ReversiState based on the Positions from NavigateBoard. There are IsValidMove Methods which will check if a given Move is valid using the GetSquare and IsMoveSurroundsCounters Methods. HasPlayerMove is used to check if any Move is possible, the IsGameOver Method is used to check if either Black or White has any possible Moves and the Setup Method is used to help create the layout for the Game.

Also in the ReversiBoard Class there is the Constructor which will populate the List of ReversiSquare, there are int Properties for WhiteScore, BlackScore and a bool for GameOver and for ReversiState. There is the List of ReversiSquare, a Method for MakeMove which checks for a valid Move and will get the next possible move and will Toggle the State of the given Square and will check if the Game is Over and update the Player Scores and there is an Init Method which calls the Setup Method.

There is a ReversiStateToGlyphConverter Class which will Convert a ReversiState to the relevant Emoji Character to represent the Counters for Black and White. There is a ReversiSquareStyle is used to determine the Style for the Board. The Binder Class is used to create the Bindings for the DependencyProperty of Grid.ColumnProperty and Grid.RowProperty.

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 ReversiBoard 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 ReversiSquare and call the MakeMove Method of the ReversiBoard or if GameOver is true then it will use the ShowDialogAsync Method to show a MessageDialog to indicate the Game is Over. There is a New Method which will begin a new Game.

Step 8

In the Solution Explorer select MainPage.xaml

vs2017-mainpage-reversi

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="Row"/>
		<Setter Property="local:Binder.GridColumnBindingPath" Value="Column"/>
		<Setter Property="Background" Value="ForestGreen"/>
	</Style>
	<local:ReversiSquareStyle x:Key="ReversiSquareStyle" Tile="{StaticResource TileStyle}"/>
	<local:ReversiStateToGlyphConverter x:Key="ReversiStateToGlyphConverter"/>
	<DataTemplate x:Key="ReversiTemplate" x:DataType="local:ReversiSquare">
		<Grid IsHitTestVisible="False" BorderThickness="1" BorderBrush="Black">
			<Viewbox>
				<TextBlock IsColorFontEnabled="True" FontFamily="Segoe UI Emoji"
				Text="{x:Bind State, Mode=OneWay, Converter={StaticResource ReversiStateToGlyphConverter}}"/>
			</Viewbox>
		</Grid>
	</DataTemplate>
</Page.Resources>

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

<Viewbox>
	<StackPanel Margin="50">
		<TextBlock HorizontalAlignment="Center" Name="Scores">
			<Run FontFamily="Segoe UI">Score:</Run>
			<Run FontFamily="Segoe UI Emoji">⚫</Run>
			<Run Text="{Binding BlackScore}" FontFamily="Segoe UI" />
			<Run FontFamily="Segoe UI Emoji">⚪</Run>
			<Run Text="{Binding WhiteScore}" FontFamily="Segoe UI" />
			<Run FontFamily="Segoe UI">Move:</Run>
			<Run Text="{Binding NextMove, Converter={StaticResource ReversiStateToGlyphConverter}}" FontFamily="Segoe UI Emoji" />
		</TextBlock>
		<ItemsControl Name="Display" Width="400" Height="400" 
			ItemContainerStyleSelector="{StaticResource ReversiSquareStyle}" 
			ItemTemplate="{StaticResource ReversiTemplate}" Tapped="Display_Tapped">
		</ItemsControl>
	</StackPanel>
</Viewbox>
<CommandBar VerticalAlignment="Bottom">
	<AppBarButton Icon="Page2" Label="New" Click="New_Click"/>
</CommandBar>

Within the Page.Resources block of XAML above the Grid Element contains Style definitions including a Property for Row and Column. There is also the ReversiSquareStyle and ReversiStateToGlyphConverter. There is a DataTemplate which is for a ReversiSquare which is a TextBlock within a Viewbox with the Text Databound to the State using the ReversiStateToGlyphConverter.

Within the main Grid Element, the first block of XAML is a Viewbox Control which contains a StackPanel Control with a TextBlock with many Run Elements to help display the Scores, there is 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.

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, ref Scores);
}

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, ref Scores);
}

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.

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 appropriate Square to place either a White or Black Counter on the on the Board to play Reversi until the Game is Over and the Player with the highest Score wins!

uwp-ran-reversi

Step 15

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

vs2017-close

Reversi is based upon Reversi8 example by Colin Eberhardt and shows how to create a simple two-player Reversi Game including fully represented game logic, unchequered board and counters

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