Press "Enter" to skip to content

Lesson 20.4: Create floating game messages canvas

Lesson 20.4: Create floating game messages canvas

Now we’ll add a popup canvas for game messages.

Step 1: Modify SOSCSRPG.ViewModels\GameSession.cs

We’re going to add a new property that will be an ObservableCollection of strings – to hold the game messages. The popup will bind to this new property.

To start, we need to add new “using” directives for System and System.Collections.ObjectModel.

Next, on line 13, we’ll add “IDisposable” as an interface this class will implement. This is a better way for us to handle creating new GameSession objects and un-subscribing from the message broker.

Add the new ObservableCollection<string> property for the game messages on lines 95-97 and add its new PopupDetails property to line 103.

In the constructor, on lines 182-191, add the setup values for the game message PopupDetails.

On line 193, subscribe to the message broker’s OnMessageRaised event. Have the event call a new OnGameMessageRaised function.

Add the new OnGameMessageRaised function on lines 233-241. Now, when the message broker reports a new message, this function will add it to the new GamesMessages property that is bound to the PopuDetails.

On lines 235-238, I added a filter that will delete the oldest message (GameMessages item 0) if there are over 250 items already in the GameMessages collection. This will just keep the UI a little more manageable.

In lines 388-392, implement the Dispose function for IDisposable.  This function will dispose of the _currentBattle object and unsubscribe from the mess broker’s OnMessageRaised. This will prevent duplicated messages.

GameSession.cs

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using SOSCSRPG.Services.Factories;
using SOSCSRPG.Models;
using SOSCSRPG.Services;
using Newtonsoft.Json;
using SOSCSRPG.Core;

namespace SOSCSRPG.ViewModels
{
    public class GameSession : INotifyPropertyChanged, IDisposable
    {
        private readonly MessageBroker _messageBroker = MessageBroker.GetInstance();

        #region Properties

        private Player _currentPlayer;
        private Location _currentLocation;
        private Battle _currentBattle;
        private Monster _currentMonster;

        public event PropertyChangedEventHandler PropertyChanged;

        [JsonIgnore]
        public GameDetails GameDetails { get; private set; }

        [JsonIgnore]
        public World CurrentWorld { get; }

        public Player CurrentPlayer
        {
            get => _currentPlayer;
            set
            {
                if(_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp -= OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled -= OnPlayerKilled;
                }

                _currentPlayer = value;

                if(_currentPlayer != null)
                {
                    _currentPlayer.OnLeveledUp += OnCurrentPlayerLeveledUp;
                    _currentPlayer.OnKilled += OnPlayerKilled;
                }
            }
        }

        public Location CurrentLocation
        {
            get => _currentLocation;
            set
            {
                _currentLocation = value;

                CompleteQuestsAtLocation();
                GivePlayerQuestsAtLocation();
                CurrentMonster = MonsterFactory.GetMonsterFromLocation(CurrentLocation);

                CurrentTrader = CurrentLocation.TraderHere;
            }
        }

        [JsonIgnore]
        public Monster CurrentMonster
        {
            get => _currentMonster;
            set
            {
                if(_currentBattle != null)
                {
                    _currentBattle.OnCombatVictory -= OnCurrentMonsterKilled;
                    _currentBattle.Dispose();
                    _currentBattle = null;
                }

                _currentMonster = value;

                if(_currentMonster != null)
                {
                    _currentBattle = new Battle(CurrentPlayer, CurrentMonster);

                    _currentBattle.OnCombatVictory += OnCurrentMonsterKilled;
                }
            }
        }

        [JsonIgnore]
        public Trader CurrentTrader { get; private set; }

        [JsonIgnore]
        public ObservableCollection<string> GameMessages { get; } =
            new ObservableCollection<string>();

        public PopupDetails PlayerDetails { get; set; }
        public PopupDetails InventoryDetails { get; set; }
        public PopupDetails QuestDetails { get; set; }
        public PopupDetails RecipesDetails { get; set; }
        public PopupDetails GameMessagesDetails { get; set; }

        [JsonIgnore]
        public bool HasLocationToNorth =>
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null;

        [JsonIgnore]
        public bool HasLocationToEast =>
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null;

        [JsonIgnore]
        public bool HasLocationToSouth =>
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null;

        [JsonIgnore]
        public bool HasLocationToWest =>
            CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null;

        [JsonIgnore]
        public bool HasMonster => CurrentMonster != null;

        [JsonIgnore]
        public bool HasTrader => CurrentTrader != null;

        #endregion

        public GameSession(Player player, int xCoordinate, int yCoordinate)
        {
            PopulateGameDetails();

            CurrentWorld = WorldFactory.CreateWorld();
            CurrentPlayer = player;
            CurrentLocation = CurrentWorld.LocationAt(xCoordinate, yCoordinate);

            // Setup popup window properties
            PlayerDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 10,
                Left = 10,
                MinHeight = 75,
                MaxHeight = 400,
                MinWidth = 265,
                MaxWidth = 400
            };

            InventoryDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 10,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };

            QuestDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 275,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };

            RecipesDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 500,
                Left = 575,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 250,
                MaxWidth = 400
            };

            GameMessagesDetails = new PopupDetails
            {
                IsVisible = false,
                Top = 250,
                Left = 10,
                MinHeight = 75,
                MaxHeight = 175,
                MinWidth = 350,
                MaxWidth = 400
            };

            _messageBroker.OnMessageRaised += OnGameMessageRaised;
        }

        public void MoveNorth()
        {
            if(HasLocationToNorth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1);
            }
        }

        public void MoveEast()
        {
            if(HasLocationToEast)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate);
            }
        }

        public void MoveSouth()
        {
            if(HasLocationToSouth)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1);
            }
        }

        public void MoveWest()
        {
            if(HasLocationToWest)
            {
                CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate);
            }
        }

        private void PopulateGameDetails()
        {
            GameDetails = GameDetailsService.ReadGameDetails();
        }

        private void OnGameMessageRaised(object sender, GameMessageEventArgs e)
        {
            if (GameMessages.Count > 250)
            {
                GameMessages.RemoveAt(0);
            }

            GameMessages.Add(e.Message);
        }

        private void CompleteQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                QuestStatus questToComplete =
                    CurrentPlayer.Quests.FirstOrDefault(q => q.PlayerQuest.ID == quest.ID &&
                                                             !q.IsCompleted);

                if(questToComplete != null)
                {
                    if(CurrentPlayer.Inventory.HasAllTheseItems(quest.ItemsToComplete))
                    {
                        CurrentPlayer.RemoveItemsFromInventory(quest.ItemsToComplete);

                        _messageBroker.RaiseMessage("");
                        _messageBroker.RaiseMessage($"You completed the '{quest.Name}' quest");

                        // Give the player the quest rewards
                        _messageBroker.RaiseMessage($"You receive {quest.RewardExperiencePoints} experience points");
                        CurrentPlayer.AddExperience(quest.RewardExperiencePoints);

                        _messageBroker.RaiseMessage($"You receive {quest.RewardGold} gold");
                        CurrentPlayer.ReceiveGold(quest.RewardGold);

                        foreach(ItemQuantity itemQuantity in quest.RewardItems)
                        {
                            GameItem rewardItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);

                            _messageBroker.RaiseMessage($"You receive a {rewardItem.Name}");
                            CurrentPlayer.AddItemToInventory(rewardItem);
                        }

                        // Mark the Quest as completed
                        questToComplete.IsCompleted = true;
                    }
                }
            }
        }

        private void GivePlayerQuestsAtLocation()
        {
            foreach(Quest quest in CurrentLocation.QuestsAvailableHere)
            {
                if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID))
                {
                    CurrentPlayer.Quests.Add(new QuestStatus(quest));

                    _messageBroker.RaiseMessage("");
                    _messageBroker.RaiseMessage($"You receive the '{quest.Name}' quest");
                    _messageBroker.RaiseMessage(quest.Description);

                    _messageBroker.RaiseMessage("Return with:");
                    foreach(ItemQuantity itemQuantity in quest.ItemsToComplete)
                    {
                        _messageBroker
                            .RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }

                    _messageBroker.RaiseMessage("And you will receive:");
                    _messageBroker.RaiseMessage($"   {quest.RewardExperiencePoints} experience points");
                    _messageBroker.RaiseMessage($"   {quest.RewardGold} gold");
                    foreach(ItemQuantity itemQuantity in quest.RewardItems)
                    {
                        _messageBroker
                            .RaiseMessage($"   {itemQuantity.Quantity} {ItemFactory.CreateGameItem(itemQuantity.ItemID).Name}");
                    }
                }
            }
        }

        public void AttackCurrentMonster()
        {
            _currentBattle?.AttackOpponent();
        }

        public void UseCurrentConsumable()
        {
            if(CurrentPlayer.CurrentConsumable != null)
            {
                if (_currentBattle == null)
                {
                    CurrentPlayer.OnActionPerformed += OnConsumableActionPerformed;
                }

                CurrentPlayer.UseCurrentConsumable();

                if (_currentBattle == null)
                {
                    CurrentPlayer.OnActionPerformed -= OnConsumableActionPerformed;
                }
            }
        }

        private void OnConsumableActionPerformed(object sender, string result)
        {
            _messageBroker.RaiseMessage(result);
        }

        public void CraftItemUsing(Recipe recipe)
        {
            if(CurrentPlayer.Inventory.HasAllTheseItems(recipe.Ingredients))
            {
                CurrentPlayer.RemoveItemsFromInventory(recipe.Ingredients);

                foreach(ItemQuantity itemQuantity in recipe.OutputItems)
                {
                    for(int i = 0; i < itemQuantity.Quantity; i++)
                    {
                        GameItem outputItem = ItemFactory.CreateGameItem(itemQuantity.ItemID);
                        CurrentPlayer.AddItemToInventory(outputItem);
                        _messageBroker.RaiseMessage($"You craft 1 {outputItem.Name}");
                    }
                }
            }
            else
            {
                _messageBroker.RaiseMessage("You do not have the required ingredients:");
                foreach(ItemQuantity itemQuantity in recipe.Ingredients)
                {
                    _messageBroker
                        .RaiseMessage($"  {itemQuantity.QuantityItemDescription}");
                }
            }
        }

        private void OnPlayerKilled(object sender, System.EventArgs e)
        {
            _messageBroker.RaiseMessage("");
            _messageBroker.RaiseMessage("You have been killed.");

            CurrentLocation = CurrentWorld.LocationAt(0, -1);
            CurrentPlayer.CompletelyHeal();
        }

        private void OnCurrentMonsterKilled(object sender, System.EventArgs eventArgs)
        {
            // Get another monster to fight
            CurrentMonster = MonsterFactory.GetMonsterFromLocation(CurrentLocation);
        }

        private void OnCurrentPlayerLeveledUp(object sender, System.EventArgs eventArgs)
        {
            _messageBroker.RaiseMessage($"You are now level {CurrentPlayer.Level}!");
        }

        public void Dispose()
        {
            _currentBattle?.Dispose();
            _messageBroker.OnMessageRaised -= OnGameMessageRaised;
        }
    }
}

Step 2: Modify WPUI\MainWindow.xaml.cs

Start by adding a “using” directive for System.Collections.Specialized and delete the directives for System.Windows.Documents and SOSCSRPG.Core.

On line 21, delete the declaration of the class-level _messageBroker variable. This is handled in the GameSession class now.

On lines 78-82, delete the OnMessageRaised function that scrolled to the end of the messages. We’ll handle this using a different method.

On line 105, add a user input action so pressing the “M” key opens and closes the chat messages popup.

Inside the SetActiveGameSessionTo function (starting on line 119), remove the code that worked with the _messageBroker variable and GameMessages. Replace it with the new code shown for the function.

This new code handles the notifications from the GameSesssion object’s GamesMessages ObservableCollection property. If there’s an existing _gameSession object (a game has already been started and the player is starting/loading a new game), this function unsubscribes from the CollectionChanged event (lines 121-125).

Then, on lines 130-131, MainWindow subscribes to the CollectionChanged event on the new _gameSession object.

On lines 134-141, add the new GameMessages_CollectionChanged function. This is the function that will run whenever an item is added to, or removed from, the GameMessages property – due to subscribing to the GameMessages.CollectionChanged event on lines 130-131. This function scrolls to the bottom of the new FlowDocumentScrollViewer whenever a message is added.

Inside the StartNewGame_OnClick function (line 145), we’ll call the Dispose function on the existing _gameSession object – if there is one (the question mark prevents an error from happening if _gameSession is null).

Finally, on lines 217-220, add the function to hide the game messages popup when the player clicks the “X” button on the popup canvas.

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using SOSCSRPG.Models;
using SOSCSRPG.Services;
using SOSCSRPG.ViewModels;
using Microsoft.Win32;
using WPFUI.Windows;

namespace WPFUI
{
    public partial class MainWindow : Window
    {
        private const string SAVE_GAME_FILE_EXTENSION = "soscsrpg";

        private readonly Dictionary<Key, Action> _userInputActions = 
            new Dictionary<Key, Action>();

        private GameSession _gameSession;
        private Point? _dragStart;

        public MainWindow(Player player, int xLocation = 0, int yLocation = 0)
        {
            InitializeComponent();

            InitializeUserInputActions();

            SetActiveGameSessionTo(new GameSession(player, xLocation, yLocation));

            // Enable drag for popup details canvases
            foreach (UIElement element in GameCanvas.Children)
            {
                if (element is Canvas)
                {
                    element.MouseDown += GameCanvas_OnMouseDown;
                    element.MouseMove += GameCanvas_OnMouseMove;
                    element.MouseUp += GameCanvas_OnMouseUp;
                }
            }
        }

        private void OnClick_MoveNorth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveNorth();
        }

        private void OnClick_MoveWest(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveWest();
        }

        private void OnClick_MoveEast(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveEast();
        }

        private void OnClick_MoveSouth(object sender, RoutedEventArgs e)
        {
            _gameSession.MoveSouth();
        }

        private void OnClick_AttackMonster(object sender, RoutedEventArgs e)
        {
            _gameSession.AttackCurrentMonster();
        }

        private void OnClick_UseCurrentConsumable(object sender, RoutedEventArgs e)
        {
            _gameSession.UseCurrentConsumable();
        }

        private void OnClick_DisplayTradeScreen(object sender, RoutedEventArgs e)
        {
            if(_gameSession.CurrentTrader != null)
            {
                TradeScreen tradeScreen = new TradeScreen();
                tradeScreen.Owner = this;
                tradeScreen.DataContext = _gameSession;
                tradeScreen.ShowDialog();
            }
        }

        private void OnClick_Craft(object sender, RoutedEventArgs e)
        {
            Recipe recipe = ((FrameworkElement)sender).DataContext as Recipe;
            _gameSession.CraftItemUsing(recipe);
        }

        private void InitializeUserInputActions()
        {
            _userInputActions.Add(Key.W, () => _gameSession.MoveNorth());
            _userInputActions.Add(Key.A, () => _gameSession.MoveWest());
            _userInputActions.Add(Key.S, () => _gameSession.MoveSouth());
            _userInputActions.Add(Key.D, () => _gameSession.MoveEast());
            _userInputActions.Add(Key.Z, () => _gameSession.AttackCurrentMonster());
            _userInputActions.Add(Key.C, () => _gameSession.UseCurrentConsumable());
            _userInputActions.Add(Key.P, () => _gameSession.PlayerDetails.IsVisible = !_gameSession.PlayerDetails.IsVisible);
            _userInputActions.Add(Key.I, () => _gameSession.InventoryDetails.IsVisible = !_gameSession.InventoryDetails.IsVisible);
            _userInputActions.Add(Key.Q, () => _gameSession.QuestDetails.IsVisible = !_gameSession.QuestDetails.IsVisible);
            _userInputActions.Add(Key.R, () => _gameSession.RecipesDetails.IsVisible = !_gameSession.RecipesDetails.IsVisible);
            _userInputActions.Add(Key.M, () => _gameSession.GameMessagesDetails.IsVisible = !_gameSession.GameMessagesDetails.IsVisible);
            _userInputActions.Add(Key.T, () => OnClick_DisplayTradeScreen(this, new RoutedEventArgs()));
        }

        private void MainWindow_OnKeyDown(object sender, KeyEventArgs e)
        {
            if(_userInputActions.ContainsKey(e.Key))
            {
                _userInputActions[e.Key].Invoke();

                e.Handled = true;
            }
        }

        private void SetActiveGameSessionTo(GameSession gameSession)
        {
            if (_gameSession != null)
            {
                _gameSession.GameMessages.CollectionChanged -=
                    GameMessages_CollectionChanged;
            }

            _gameSession = gameSession;
            DataContext = _gameSession;

            _gameSession.GameMessages.CollectionChanged +=
                GameMessages_CollectionChanged;
        }

        private void GameMessages_CollectionChanged(object sender,
            NotifyCollectionChangedEventArgs e)
        {
            (GameMessagesFlowDocumentScrollViewer
                .Template
                .FindName("PART_ContentHost", GameMessagesFlowDocumentScrollViewer) as ScrollViewer)
                ?.ScrollToEnd();
        }

        private void StartNewGame_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession?.Dispose();

            Startup startup = new Startup();
            startup.Show();
            Close();
        }

        private void SaveGame_OnClick(object sender, RoutedEventArgs e)
        {
            SaveGame();
        }

        private void Exit_OnClick(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void MainWindow_OnClosing(object sender, CancelEventArgs e)
        {
            AskToSaveGame();
        }

        private void AskToSaveGame()
        {
            YesNoWindow message =
                new YesNoWindow("Save Game", "Do you want to save your game?");
            message.Owner = GetWindow(this);
            message.ShowDialog();

            if(message.ClickedYes)
            {
                SaveGame();
            }
        }

        private void SaveGame()
        {
            SaveFileDialog saveFileDialog =
                new SaveFileDialog
                {
                    InitialDirectory = AppDomain.CurrentDomain.BaseDirectory,
                    Filter = $"Saved games (*.{SAVE_GAME_FILE_EXTENSION})|*.{SAVE_GAME_FILE_EXTENSION}"
                };

            if (saveFileDialog.ShowDialog() == true)
            {
                SaveGameService.Save(new GameState(_gameSession.CurrentPlayer, 
                    _gameSession.CurrentLocation.XCoordinate, 
                    _gameSession.CurrentLocation.YCoordinate), saveFileDialog.FileName);
            }
        }

        private void ClosePlayerDetailsWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.PlayerDetails.IsVisible = false;
        }

        private void CloseInventoryWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.InventoryDetails.IsVisible = false;
        }

        private void CloseQuestsWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.QuestDetails.IsVisible = false;
        }

        private void CloseRecipesWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.RecipesDetails.IsVisible = false;
        }

        private void CloseGameMessagesDetailsWindow_OnClick(object sender, RoutedEventArgs e)
        {
            _gameSession.GameMessagesDetails.IsVisible = false;
        }

        private void GameCanvas_OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton != MouseButton.Left)
            {
                return;
            }

            UIElement movingElement = (UIElement)sender;
            _dragStart = e.GetPosition(movingElement);
            movingElement.CaptureMouse();

            e.Handled = true;
        }

        private void GameCanvas_OnMouseMove(object sender, MouseEventArgs e)
        {
            if (_dragStart == null || e.LeftButton != MouseButtonState.Pressed)
            {
                return;
            }

            Point mousePosition = e.GetPosition(GameCanvas);
            UIElement movingElement = (UIElement)sender;

            // Don't let player move popup details off the board
            if (mousePosition.X < _dragStart.Value.X ||
                mousePosition.Y < _dragStart.Value.Y ||
                mousePosition.X > GameCanvas.ActualWidth - ((Canvas)movingElement).ActualWidth + _dragStart.Value.X ||
                mousePosition.Y > GameCanvas.ActualHeight - ((Canvas)movingElement).ActualHeight + _dragStart.Value.Y)
            {
                return;
            }

            Canvas.SetLeft(movingElement, mousePosition.X - _dragStart.Value.X);
            Canvas.SetTop(movingElement, mousePosition.Y - _dragStart.Value.Y);

            e.Handled = true;
        }

        private void GameCanvas_OnMouseUp(object sender, MouseButtonEventArgs e)
        {
            var movingElement = (UIElement)sender;
            movingElement.ReleaseMouseCapture();
            _dragStart = null;

            e.Handled = true;
        }
    }
}

Step 3: Modify WPUI\MainWindow.xaml

Add the new GameMessages Canvas XAML that’s on lines 434-493.

The Canvas part of the code is like the other popup canvases, but I switched from using a RichTextBox to a FlowDocumentScrollViewer to display the messages. This way, we can bind to the GameMessages ObservableCollection property.

If you don’t like the font for the game messages, you can change the FontFamily value on line 483 from “Calibri” to whatever font you prefer for your version of the game.

Finally, remove the original game messages section that was on lines 451-467.

MainWindow.xaml

<Window x:Class="WPFUI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:SOSCSRPG.ViewModels;assembly=SOSCSRPG.ViewModels"
        d:DataContext="{d:DesignInstance viewModels:GameSession}"
        mc:Ignorable="d"
        FontSize="11pt"
        Title="{Binding GameDetails.Title}" Height="768" Width="1024"
        KeyDown="MainWindow_OnKeyDown"
        Closing="MainWindow_OnClosing">

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibility" />
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="225"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="250"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Menu -->
        <Menu Grid.Row="0" Grid.Column="0"
              Grid.ColumnSpan="2"
              FontSize="11pt"
              Background="AliceBlue">
            <MenuItem Header="File">
                <MenuItem Header="New Game"
                          Click="StartNewGame_OnClick"/>
                <MenuItem Header="Save Game"
                          Click="SaveGame_OnClick"/>
                <Separator/>
                <MenuItem Header="Exit"
                          Click="Exit_OnClick"/>
            </MenuItem>
            <MenuItem Header="Help">
                <MenuItem Header="Help"
                          IsEnabled="False"/>
                <Separator/>
                <MenuItem Header="About"
                          IsEnabled="False"/>
            </MenuItem>
        </Menu>

        <!-- Main game canvas (full window) -->
        <Canvas Grid.Row="1" Grid.Column="0"
                Grid.RowSpan="2"
                Grid.ColumnSpan="2"
                x:Name="GameCanvas"
                ZIndex="99">

            <!-- Player Details -->
            <Canvas Top="{Binding PlayerDetails.Top}" Left="{Binding PlayerDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding PlayerDetails.MinHeight}"
                    MaxHeight="{Binding PlayerDetails.MaxHeight}"
                    MinWidth="{Binding PlayerDetails.MinWidth}"
                    MaxWidth="{Binding PlayerDetails.MaxWidth}"
                    Visibility="{Binding PlayerDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">

                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">

                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="60"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>

                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Player Details"/>

                        <Button Grid.Row="0" Grid.Column="1"
                                HorizontalAlignment="Right"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="ClosePlayerDetailsWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>

                        <!-- Sets the background color for the two player data rows -->
                        <Border Grid.Row="2" Grid.Column="0"
                                Grid.ColumnSpan="2"
                                Grid.RowSpan="2"
                                Background="WhiteSmoke">
                        </Border>

                        <!-- Player level and name -->
                        <Canvas Grid.Row="2" Grid.Column="0"
                                Grid.ColumnSpan="2"
                                HorizontalAlignment="Left"
                                MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">

                            <Ellipse Canvas.Top="3" Canvas.Left="3"
                                     Width="50"
                                     Height="50"
                                     StrokeThickness="1"
                                     Stroke="SteelBlue"/>

                            <Ellipse Canvas.Top="5" Canvas.Left="5"
                                     Width="46"
                                     Height="46"
                                     StrokeThickness="1"
                                     Stroke="SteelBlue"/>

                            <Label Canvas.Top="5" Canvas.Left="5"
                                   Width="46"
                                   HorizontalContentAlignment="Center"
                                   VerticalContentAlignment="Center"
                                   FontSize="18pt"
                                   FontWeight="Bold"
                                   Content="{Binding CurrentPlayer.Level}"/>

                            <Label Canvas.Top="5" Canvas.Left="55"
                                   Width="200"
                                   FontSize="18pt"
                                   FontWeight="Bold"
                                   Content="{Binding CurrentPlayer.Name}"/>

                        </Canvas>

                        <Grid Grid.Row="3" Grid.Column="0"
                              HorizontalAlignment="Left"
                              VerticalAlignment="Top"
                              Margin="5,5,5,5">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                                <RowDefinition Height="Auto"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>

                            <Label Grid.Row="0" Grid.Column="0"
                                   FontWeight="Bold"
                                   Content="Exp:"/>
                            <Label Grid.Row="0" Grid.Column="1"
                                   Content="{Binding CurrentPlayer.ExperiencePoints}"/>

                            <Label Grid.Row="1" Grid.Column="0"
                                   FontWeight="Bold"
                                   Content="Gold:"/>
                            <Label Grid.Row="1" Grid.Column="1"
                                   Content="{Binding CurrentPlayer.Gold}"/>

                            <Label Grid.Row="2" Grid.Column="0"
                                   FontWeight="Bold"
                                   Content="HP:"/>
                            <Label Grid.Row="2" Grid.Column="1"
                                   Content="{Binding CurrentPlayer.HitPoints}"/>
                        </Grid>

                        <!-- Player Attributes -->
                        <ListBox Grid.Row="3" Grid.Column="1"
                                 Margin="5,5,5,5"
                                 ItemsSource="{Binding CurrentPlayer.Attributes}">
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <StackPanel Orientation="Horizontal">
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition SharedSizeGroup="Description"/>
                                            </Grid.ColumnDefinitions>
                                            <TextBlock Text="{Binding DisplayName}" 
                                                       HorizontalAlignment="Left"
                                                       MinWidth="100"/>
                                        </Grid>
                                        <Grid>
                                            <Grid.ColumnDefinitions>
                                                <ColumnDefinition SharedSizeGroup="ModifiedValue"/>
                                            </Grid.ColumnDefinitions>
                                            <TextBlock Text="{Binding ModifiedValue}"
                                                       HorizontalAlignment="Right"/>
                                        </Grid>
                                    </StackPanel>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>

                    </Grid>

                </Border>

            </Canvas>

            <!-- Player Inventory Details -->
            <Canvas Top="{Binding InventoryDetails.Top}" Left="{Binding InventoryDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding InventoryDetails.MinHeight}"
                    MaxHeight="{Binding InventoryDetails.MaxHeight}"
                    MinWidth="{Binding InventoryDetails.MinWidth}"
                    MaxWidth="{Binding InventoryDetails.MaxWidth}"
                    Visibility="{Binding InventoryDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">

                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">

                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>

                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Inventory"/>

                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseInventoryWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>

                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Inventory.GroupedInventory}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Description"
                                                    Binding="{Binding Item.Name, Mode=OneWay}"
                                                    Width="*"/>
                                <DataGridTextColumn Header="Qty"
                                                    IsReadOnly="True"
                                                    Width="Auto"
                                                    Binding="{Binding Quantity, Mode=OneWay}"/>
                                <DataGridTextColumn Header="Price"
                                                    Binding="{Binding Item.Price, Mode=OneWay}"
                                                    Width="Auto"/>
                            </DataGrid.Columns>
                        </DataGrid>

                    </Grid>

                </Border>

            </Canvas>

            <!-- Player Quests Details -->
            <Canvas Top="{Binding QuestDetails.Top}" Left="{Binding QuestDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding QuestDetails.MinHeight}"
                    MaxHeight="{Binding QuestDetails.MaxHeight}"
                    MinWidth="{Binding QuestDetails.MinWidth}"
                    MaxWidth="{Binding QuestDetails.MaxWidth}"
                    Visibility="{Binding QuestDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">

                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">

                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>

                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Quests"/>

                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseQuestsWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>

                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Quests}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Name"
                                                    Binding="{Binding PlayerQuest.Name, Mode=OneWay}"
                                                    Width="*">
                                    <DataGridTextColumn.CellStyle>
                                        <Style TargetType="DataGridCell">
                                            <Setter Property="ToolTip" 
                                                    Value="{Binding PlayerQuest.ToolTipContents}"/>
                                        </Style>
                                    </DataGridTextColumn.CellStyle>
                                </DataGridTextColumn>
                                <DataGridTextColumn Header="Done?"
                                                    Binding="{Binding IsCompleted, Mode=OneWay}"
                                                    Width="Auto"/>
                            </DataGrid.Columns>
                        </DataGrid>

                    </Grid>

                </Border>

            </Canvas>

            <!-- Player Recipes Details -->
            <Canvas Top="{Binding RecipesDetails.Top}" Left="{Binding RecipesDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding RecipesDetails.MinHeight}"
                    MaxHeight="{Binding RecipesDetails.MaxHeight}"
                    MinWidth="{Binding RecipesDetails.MinWidth}"
                    MaxWidth="{Binding RecipesDetails.MaxWidth}"
                    Visibility="{Binding RecipesDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">

                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">

                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>

                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Recipes"/>

                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseRecipesWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>

                        <DataGrid Grid.Row="2" Grid.Column="0"
                                  Grid.ColumnSpan="2"
                                  ItemsSource="{Binding CurrentPlayer.Recipes}"
                                  AutoGenerateColumns="False"
                                  HeadersVisibility="Column"
                                  VerticalScrollBarVisibility="Auto"
                                  MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=MaxHeight}"
                                  Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                          AncestorType={x:Type Canvas}},Path=ActualWidth}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Name"
                                                    Binding="{Binding Name, Mode=OneWay}"
                                                    Width="*">
                                    <DataGridTextColumn.CellStyle>
                                        <Style TargetType="DataGridCell">
                                            <Setter Property="ToolTip" 
                                                    Value="{Binding ToolTipContents}"/>
                                        </Style>
                                    </DataGridTextColumn.CellStyle>
                                </DataGridTextColumn>
                                <DataGridTemplateColumn MinWidth="75">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <Button Click="OnClick_Craft"
                                                    Width="55"
                                                    Content="Craft"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>

                    </Grid>

                </Border>

            </Canvas>

            <!-- Game Messages -->
            <Canvas Top="{Binding GameMessagesDetails.Top}" Left="{Binding GameMessagesDetails.Left}"
                    Width="Auto" Height="Auto"
                    MinHeight="{Binding GameMessagesDetails.MinHeight}"
                    MaxHeight="{Binding GameMessagesDetails.MaxHeight}"
                    MinWidth="{Binding GameMessagesDetails.MinWidth}"
                    MaxWidth="{Binding GameMessagesDetails.MaxWidth}"
                    Visibility="{Binding GameMessagesDetails.IsVisible, Converter={StaticResource BooleanToVisibility}}">

                <Border BorderBrush="Navy" BorderThickness="3"
                        Background="LightSteelBlue">

                    <Grid Margin="2,2,2,2">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="4"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>

                        <Label Grid.Row="0" Grid.Column="0"
                               HorizontalAlignment="Left"
                               FontWeight="Bold"
                               Content="Game Messages"/>

                        <Button Grid.Row="0" Grid.Column="1"
                                Width="25"
                                FontWeight="Bold"
                                Content="X"
                                Click="CloseGameMessagesDetailsWindow_OnClick">
                            <Button.Resources>
                                <Style TargetType="Border">
                                    <Setter Property="CornerRadius" Value="3"/>
                                </Style>
                            </Button.Resources>
                        </Button>

                        <FlowDocumentScrollViewer
                            Grid.Row="2" Grid.Column="0"
                            Grid.ColumnSpan="2"
                            MaxHeight="{Binding RelativeSource={RelativeSource FindAncestor,
                                                AncestorType={x:Type Canvas}},Path=MaxHeight}"
                            Width="{Binding RelativeSource={RelativeSource FindAncestor,
                                            AncestorType={x:Type Canvas}},Path=ActualWidth}"
                            x:Name="GameMessagesFlowDocumentScrollViewer" >
                            <FlowDocument Background="WhiteSmoke">
                                <Paragraph FontFamily="Calibri">
                                    <ItemsControl ItemsSource="{Binding GameMessages, Mode=OneWay}" />
                                </Paragraph>
                            </FlowDocument>
                        </FlowDocumentScrollViewer>

                    </Grid>

                </Border>

            </Canvas>

        </Canvas>

        <!-- Gameplay -->
        <Grid Grid.Row="1" Grid.Column="0"
              Grid.ColumnSpan="2"
              Background="Beige"
              ZIndex="1">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="3*"/>
                <ColumnDefinition Width="2*"/>
            </Grid.ColumnDefinitions>

            <!-- Location information -->
            <Border Grid.Row="0" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid Margin="3">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>
                        <RowDefinition Height="*"/>
                        <RowDefinition Height="Auto"/>
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Name}"/>

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentLocation.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>

                    <TextBlock Grid.Row="2"
                               HorizontalAlignment="Center"
                               Text="{Binding CurrentLocation.Description}"
                               TextWrapping="Wrap"/>
                </Grid>

            </Border>

            <!-- Monster information -->
            <Border Grid.Row="1" Grid.Column="1"
                    BorderBrush="Gainsboro"
                    BorderThickness="1">

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <TextBlock Grid.Row="0"
                               HorizontalAlignment="Center"
                               Height="Auto"
                               Text="{Binding CurrentMonster.Name}" />

                    <Image Grid.Row="1"
                           HorizontalAlignment="Center"
                           VerticalAlignment="Center"
                           Height="125"
                           Width="125"
                           Source="{Binding CurrentMonster.ImageName, 
                                            Converter={StaticResource FileToBitmapConverter}}"/>

                    <StackPanel Grid.Row="2"
                                Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                                HorizontalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock>Current Hit Points:</TextBlock>
                        <TextBlock Text="{Binding CurrentMonster.CurrentHitPoints}" />
                    </StackPanel>

                </Grid>

            </Border>

        </Grid>

        <!-- Action controls -->
        <Grid Grid.Row="2" Grid.Column="0"
              Grid.ColumnSpan="2"

              Background="Lavender"
              ZIndex="1">

            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="255" />
            </Grid.ColumnDefinitions>

            <!-- Combat Controls -->
            <Grid Grid.Row="0" Grid.Column="0"
                  HorizontalAlignment="Right"
                  VerticalAlignment="Center">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition Width="10"/>
                    <ColumnDefinition Width="50"/>
                </Grid.ColumnDefinitions>

                <ComboBox Grid.Row="0" Grid.Column="0"
                          Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Weapons}"
                          SelectedItem="{Binding CurrentPlayer.CurrentWeapon}"
                          DisplayMemberPath="Name"/>

                <Button Grid.Row="0" Grid.Column="2"
                        Visibility="{Binding HasMonster, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_AttackMonster"/>

                <ComboBox Grid.Row="1" Grid.Column="0"
                          Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                          ItemsSource="{Binding CurrentPlayer.Inventory.Consumables}"
                          SelectedItem="{Binding CurrentPlayer.CurrentConsumable}"
                          DisplayMemberPath="Name"/>

                <Button Grid.Row="1" Grid.Column="2"
                        Visibility="{Binding CurrentPlayer.Inventory.HasConsumable, Converter={StaticResource BooleanToVisibility}}"
                        Content="Use"
                        Click="OnClick_UseCurrentConsumable"/>
            </Grid>

            <!-- Movement Controls -->
            <Grid Grid.Row="0" Grid.Column="1">

                <Grid.RowDefinitions>
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>

                <Button Grid.Row="0" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveNorth"
                        Visibility="{Binding HasLocationToNorth, Converter={StaticResource BooleanToVisibility}}"
                        Content="North"/>
                <Button Grid.Row="1" Grid.Column="0" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveWest"
                        Visibility="{Binding HasLocationToWest, Converter={StaticResource BooleanToVisibility}}"
                        Content="West"/>
                <Button Grid.Row="1" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_DisplayTradeScreen"
                        Visibility="{Binding HasTrader, Converter={StaticResource BooleanToVisibility}}"
                        Content="Trade"/>
                <Button Grid.Row="1" Grid.Column="2" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveEast"
                        Visibility="{Binding HasLocationToEast, Converter={StaticResource BooleanToVisibility}}"
                        Content="East"/>
                <Button Grid.Row="2" Grid.Column="1" 
                        Height="25" Width="65" Margin="10" 
                        Click="OnClick_MoveSouth"
                        Visibility="{Binding HasLocationToSouth, Converter={StaticResource BooleanToVisibility}}"
                        Content="South"/>

            </Grid>

        </Grid>

    </Grid>
</Window>

Step 4: Test the game

NEXT LESSON: TO BE WRITTEN

PREVIOUS LESSON: Lesson 20.3: Create floating player details canvas

6 Comments

  1. Chris
    Chris 2023-09-28

    I’m sad that it’s over, but proud of myself for doing the whole thing!

    • SOSCSRPG
      SOSCSRPG 2023-09-29

      I’m happy to hear you enjoyed it. You can always come up with your own ideas on how to expand it – or write something even better. Good luck with your future programming!

  2. Meg Read
    Meg Read 2024-08-07

    Just wanted to say thanks for the great videos and written instructions. I used this to kickstart remembering my C# after being of the industry for a few years. (My last job worked with tons of WPF projects so this was perfect for me.)

    It was really nice to get right into code. There are lots of tutorials for beginners but they were too easy so this was nice to get a more complex example to help me refresh my memory. Going to keep modifying it a bit with my 8yo, he’s already loving using the coordinate system to plot out new locations and roads to keep them separated.

    Thanks Again!

    • SOSCSRPG
      SOSCSRPG 2024-08-08

      You’re welcome, Meg. That’s awesome to hear! I hope your son builds a cool world and enjoys playing a game he helped create.

  3. Henry Ward
    Henry Ward 2025-01-22

    Finished!

    Tysm for the written instructions – that made it much faster to complete.

    This was the best tutorial I’ve ever done!

    I hope to contribute to the GitHub project as and when you’re accepting contributions.

    Many thanks again!

    • SOSCSRPG
      SOSCSRPG 2025-01-22

      Congratulations! And, good luck on your future programming.

      I’m not sure if I want to have updates for my GitHub code – that might throw off some of the lessons. But, if you create your own version of the game, and want to share it here, please let me know about it.

Leave a Reply

Your email address will not be published. Required fields are marked *