This site uses cookies to ensure you get the best experience. More info
Got it!

A short refactoring story



The problem

As I have shortly described in my previous post, lately I have focused all of the little amount of time I had left on refactoring the Malison library which I now maintain.

The problem I had encountered was a result of introducing a Glyph abstraction to the library. It’s main goal was to map all characters, along with the fancy non-standard ones to a single enumeration type for easy access and use.

The Glyph enum values were directly translated into a number, which represented an index of a character in the font spritesheet embedded in Malison itself.

While the approach of wrapping all known characters into an enum is not a bad choice per se, it introduces a tightly coupled abstractions which make supplying your custom font to use along the library basically impossible.

The character limit

Going through most important issues, the first one are the supplied enum values in the Glyph.
There were roughly 180 values, from which some were unused, which gives us less than 180 characters.

Malison's built in font spritesheet Malison’s built in font spritesheet. Notice empty tiles on the bottom along with custom non-standard characters

While you could really go and make a text-based game with such a number of characters, this fixed limit makes it impossible to supply new, different characters not present there in the enumerator.

Technically you could expand the enum and add more values but that’s not a good choice, considering the fact that fonts commonly have more than a thousand of characters.

Character encoding

Another problem is the character encoding. The embedded custom font in Malison is based on ASCII encoding.
That means that it gets the proper index of a character in spritesheet based on it’s ASCII encoding value.
But, if I would like to use the earlier mentioned CP437, I would have to use it’s encoding too if I wanted to use it with it’s proper spritesheet.

CP437 font's spritesheet CP437 font’s spritesheet. Notice a bigger amount of characters and different order of certain characters.

Without the ability to supply the proper encoding, the code values would be incorrect which then would require me to change the spritesheet to follow the ASCII encoding further down limiting the Malison’s abilities.

The solution

The problems put us in the place where we need to refactor this. The coupling is tight, the library’s capabilities are limited.

As I have said before, such abstraction can prove to be handy, but definitely not in the library layer. The user should map his own supplied font in the way he wants.
An universal abstraction which would work for all fonts and encodings is kind of impossible to create here or at least it requires too much effort.

So, do we even need this abstraction in library? Definitely not.

My choice here is to just unwrap the underlying int values.
In the end, that’s what the enumeration type does - give certain numbers some fancy hats and names.

We would now operate on the code of the character and don’t really care what character it is - the library doesn’t need to know. What we need is just an integer or a char variable along with proper encoding to translate it into the correct number.

So the first step is to of course remove this enum once and for all.

Unfortunately the enum was coupled very tightly, scoring 100 references in the Malison project.

Glyph's references Seeing 100 references on abstraction you want to remove is surely motivating.

Leaving you without the boring details and having done all the dirty work I’ll just go ahead and walk you through the results of the refactoring.

Starting from the smaller changes, the Character structure that is a combination of a foreground colour, background colour and text character now keeps the character code instead.
It also has a static Encode method, encoding the given char variable and returning proper code.
By default it returns the character in Unicode encoding, but it can be supplied with any System.Text.Encoding instance.

Secondly, our TerminalBase abstract class now also has the Encoding property to keep currently used encoding, set in the constructor.

Having done that, I also changed the Terminal and WindowTerminal classes to keep track of the encoding too.

The next step was to change all the Write methods to act accordingly and encode any char that’s passed inside.

I also had to make a few refinements in the GlyphSheet class beyond changing the Glyph into a code. The GlyphSheet helps in caching the characters and operating on the given spritesheet for easy bitmap rendering.

I made it so it now works for any spritesheet sizes, which was previously set as a constant.

There was also a number of small changes here and there, but now let’s get onto the example of library’s new API.

Example usage

If we would now take the Malison’s WindowsForms renderer and made a new form inherting from TerminalForm and want to supply custom font, here’s what we gonna do.

Firstly, in the constructor we set the TerminalControl’s glyphsheet to a new one, created from our custom font, with given number of rows and columns.

The next step is to set the form’s terminal to a new one, with given size and specified encoding for our font.

public MainForm()
{
    InitializeComponent();

    // Initialize the terminal with a custom glyph sheet
    TerminalControl.GlyphSheet = new GlyphSheet(Properties.Resources.cp437_16x16, 16, 16);

    // Initialize terminal with proper character encoding
    Terminal = new Terminal(80, 30, Encoding.GetEncoding(437));
}

And that’s it! Having done that we can go on and test writing with our new font just like before refactoring the library.

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    Terminal[2, 2].Write("Hi, this is an example app ☺");

    Terminal[2, 4].Write("Here are some lines and boxes:");

    Terminal[2, 6, 10, 3][TermColor.White].DrawBox();

    Terminal[13, 6, 10, 3][TermColor.White].DrawBox(DrawBoxOptions.DoubleLines);
    
    Terminal[2, 10].Write("Because this is tailored for games, there's some fun glyphs in here:");
    int[] glyphs = new int[]
    {
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
    };

    int x = 4;
    for (int i = 0; i < glyphs.Length; i++)
    {
        Terminal[x, 12][TermColor.Orange].Write(glyphs[i]);
        x += 2;
    }

    Terminal[2, 14].Write("Background and foreground colors are supported:");

    TermColor[] colors = (TermColor[])Enum.GetValues(typeof(TermColor));
    for (int i = 0; i < colors.Length; i++)
    {
        Terminal[i + 3, 16][colors[i], TermColor.Black].Write('a');
        Terminal[i + 3, 17][TermColor.Black, colors[i]].Write('b');
    }
}

Working example And the result of code above is this. We have our font and everything works fine, starting from strings, chars, non-standard char values and ending in integer values passed to Write

Wrapping it up

To sum it up, the refactoring was successful. The library users can now supply any font as long as they give proper spritesheet for given encoding and just roll with it.

If you have a good eye for details you might have noticed another thing I’ll be working on to refactor.

That thing is the TermColor enumerator. Oh dear here we go again…

Basically it’s the same problem, there are fixed values which user cannot change, that are then translated to some colours inside the implemented renderer. That’s bad, the user cannot set his own colours so I’ll have to change that into some RGBA structure independent of the platform or framework along with some static methods to easily get the Red, Blue, Green, Black and so on.

I could even go crazy and implement some fancy methods for colour operations but that’s definitely not high on my roadmap.

That’s it for today, I wanted to keep it short but failed miserably so forgive me for that.
Hopefully you learned a few things or maybe got some ideas about tight coupling in your projects?

See you next time!