Day 5 - States

Objectives

  • Adding states to the Game class using the "State" Design Pattern
  • Refactor using those states. This means nothing will change, only our code will be more ready for the future.

The reason why we are doing this is because it will become very difficult to manage the Game's state in the future if we don't do this. A simple example is what needs to happen when the player clicks a Gem. This depends on the state of the Game. If no Gem is selected (state 1) yet, then the Gem should be selected. If a Gem is selected (state 2) already, then that Gem and the selected Gem should be swapped. While swapping (state 3) the player should not be able to click any Gem. You see, the behaviour of clicking a Gem depends on the state of the Game and that's why we need a proper way to handle this. Luckily there is a Design Pattern that gives us a standard way of doing this.

Abstract GameState class

Remark: I created a folder "States" under the root folder of my project, so the namespace of all state classes is Bejeweled.States. Take this into account when trying out the tutorial ;-)

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Bejeweled.States
{
    abstract class GameState
    {
        protected Game _game;
 
        public GameState(Game game)
        {
            _game = game;
        }
 
        public virtual void OnGemClicked(Gem gem)
        {
            //By default, nothing happens when clicking a Gem
        }
    }
}

All Game states will inherit from this class and for now it only contains one method "OnGemClicked" that will be called when a player clicks a Gem. It's up to the state's implementation to decide what needs to happen and possibly change the Game's state. By default (if the derived class doesn't override this method), nothing will happen when a player clicks a Gem, as we leave the implementation empty (see comment inside the method).

The SelectingFirstGem class (our first real State!)

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Bejeweled.States
{
    class SelectingFirstGem : GameState
    {
        public SelectingFirstGem(Game game)
            : base(game)
        {
        }
 
        public override void OnGemClicked(Gem gem)
        {
            gem.IsSelected = true;
            _game.CurrentState = new SelectingSecondGem(_game, gem);
        }
    }
}

As you can see we override the OnGemClicked method adding state specific code: When no Gem is selected, we select the Gem and change the Game's state to "SelectingSecondGem" passing the Gem selected. In this second state, the Gem will be kept as an attribute to be able to properly handle it's state transition.

The SelectingSecondGem class

using System;
using System.Collections.Generic;
using System.Text;
 
namespace Bejeweled.States
{
    class SelectingSecondGem : GameState
    {
        private Gem _firstGem;
 
        public SelectingSecondGem(Game game, Gem firstGem)
            : base(game)
        {
            _firstGem = firstGem;
        }
 
        public override void OnGemClicked(Gem gem)
        {
            if (gem == _firstGem) //Is same as first selected Gem?
            {
                gem.IsSelected = false;
                _game.CurrentState = new SelectingFirstGem(_game);
            }
            else
            {
                _firstGem.IsSelected = false;
                _firstGem = gem;
                _firstGem.IsSelected = true;
            }
        }
    }
}

In this state we also override the "OnGemClicked" method, only here we check if the previous selected Gem that was passed in the constructor (by the previous state) is the same as the clicked Gem. If yes, we deselect the Gem and go back to the "SelectingFirstGem" state. If no, we just deselect the previous one and select the clicked one. This will change in the future when we start swapping Gems, but we are not there yet.

The Game class

Finally we update the Game class, as it will work with the new State mechanism now. Actually it became alot simpler as you see in the next code fragment:

using System.Drawing;
using SdlDotNet.Core;
using FrialaSoft.Bejeweled.States;
 
namespace Bejeweled
{
    class Game
    {
        private readonly Canvas _canvas;
        private GameState _currentState;
 
        public Game()
        {
            _canvas = new Canvas(320, 240);
            _currentState = new SelectingFirstGem(this);
            WireEvents();
            CreateGems();
        }
 
        internal GameState CurrentState
        {
            get { return _currentState; }
            set { _currentState = value; }
        }
 
        private void WireEvents()
        {
            Events.Quit += Events_Quit;
            Events.Tick += Events_Tick;
        }
 
        private void CreateGems()
        {
            GemFactory gemFactory = new GemFactory();
 
            for (int i = 0; i < 10; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    Gem g = gemFactory.CreateRandom(new Point(16 + i * 32, 12 + j * 24));
                    g.MouseClick += Gem_MouseClick;
                    _canvas.Add(g);
                }
            }
        }
 
        private void Gem_MouseClick(object sender, SdlDotNet.Input.MouseButtonEventArgs e)
        {
            Gem g = (Gem)sender;
            CurrentState.OnGemClicked(g);
        }
 
        private static void Events_Quit(object sender, QuitEventArgs e)
        {
            Events.QuitApplication();
        }
 
        private void Events_Tick(object sender, TickEventArgs e)
        {
            _canvas.Draw();
        }
 
        public void Play()
        {
            Events.Run();
        }
    }
}

As you can see, we don't do any logic in the "Gem_MouseClick" event handler anymore. We just ask our current state to handle this event. Nice isn't it? Not using states would have caused us having to write hundreds of lines of code in that same event handler doing alot of checks and messing up in the end as it becomes too complicated. This is a good example of the Open/Closed principle: The Game class is open for extension, but closed for modification which is a good practice.

UML Diagrams

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License