Le Cube ◇ Mass Psychosis

The Blog

ReaScripts for Cockos REAPER
FMOD
Test-Focused Development
TDD-ing Avatar Health in C# and C++
TDD-ing Chess in C#
Part 1 - The Position - C#
Lifting

TDD-ing Chess in C#

Part 1 - The Position - C#

by Egill Antonsson
published on 2023 Oct 23
updated on 2023 Nov 13

Introduction

This is the first post in a series of posts where I will implement a chess game in the TDD workflow. Though others have already done something similar (e.g. TDD and modeling a chess game by Erik Dietrich), I'm doing this to craft my TDD workflow and share its values and result with you. Some explanations will be skipped here as they are already covered in that series TTD-ing Avatar Health in C# and C++.

Using Unity

I will use Unity and its packages Test Framework, a Unity integration of the NUnit framework, and Code Coverage analysis tool. Other C# .NET framework could be used instead, but Unity was the handiest for me to use. The whole project is on GitHub.

Setup

I create a new Unity project (3D). Both packages Test Framework and Code Coverage are already installed as they are part of the already installed Engineering feature set. The package doc detail how to setup tests for a project. My project structure starts simple:

  • Test code: Scripts/Tests/EditMode/[ClassName]Test.cs
  • Production code: Scripts/Runtime/[ClassName].cs

As the folder structure implies I'll do EditMode tests for the Runtime production code. My main reasons to prefer EditMode (over PlayMode) tests are:

  • They run faster as there is no PlayMode startup cost (in line with What is a good Unit Test section in Part 1)
  • Tests for the domain logic should not need the PlayMode test feature to run as Coroutine to wait or step through frames."

The doc EditMode vs. PlayMode tests explains the difference further.

The rules

Chess has been played for over a thousand years and it evolved roughly into its current form by about 1500. The chess rules are known to many and the basic rules are:

  • The game of chess is a two player turn based game where each player controls sixteen pieces on a chessboard
  • The board is 8x8 squares, 8 horizontal rows that are called Ranks (and presented as letters from 'a' to 'h'), and 8 vertical columns that are called Files (presented numbers as 1 to 8)
  • Each piece has a position in one of the squares and can move to a new position, capturing an opponent piece if in that position
  • Each piece type (King, Queen, Rook, Knight, Bishop, Pawn) moves in its unique way
  • The object of the game is to checkmate the opponent's King

The Position

Each board square is a position (column, row)
Each board square is a position (column, row)

A Position data structure seems to be at the base of the domain model. The board squares are positions, and the pieces have a position and move into a new position. To streamline calculations in the domain model the Position will be defined as 0-based Column and Row, although the presentation / view will be 'a' to 'h' Rank and 1-based File. I deem it well fitting to define Position as a Struct.

Some upfront design like this fits well with the TDD workflow, as long as it does go towards wholistic water flow design phase, as the TDD cycles should drive most of the design by taking on the next rule / requirement, one at a time.

Cycling the Position

RED (does not compile)

I create the test file and write the first test and see it fail, as the code does not compile.

// PositionTest.cs
using NUnit.Framework;

namespace PositionTest
{
	public class Constructor
	{
		[Test]
		public void ColumnAndRow_AreInitialized()
		{
			var position = new Position(0, 1);
			Assert.That(position.Column, Is.EqualTo(0));
			Assert.That(position.Row, Is.EqualTo(1));
		}
	}
}

RED (test fails)

I write the minimal production code to compile successfully and intentionally make the test fail to verify that the test is not passing when it should not.

// Position.cs
public struct Position
{
	public int Column;
	public int Row;

	public Position(int column, int row)
	{
		Column = -1;
		Row = -1;
	}
}

GREEN

It make the test pass by providing the obvious solution that holds for all cases.

// Position.cs
public struct Position
{
	public int Row;
	public int Column;

	public Position(int column, int row)
	{
		Column = column;
		Row = row;
	}
}

REFACTOR

I encapsulate the properties with getters and declare the structure as immutable with the readonly keyword.

// Position.cs
public readonly struct Position
{
	public int Column { get; }
	public int Row { get; }

	public Position(int column, int row)
	{
		Column = column;
		Row = row;
	}
}

Comparing positions

I'm certain that the domain logic will be comparing positions thus I write the tests for it.

GREEN

// PositionTest.cs
public class Equals
{
	[Test]
	public void AreEqual_WhenWithSameRowAndSameColumn()
	{
		var pos1 = new Position(0, 0);
		var pos2 = new Position(0, 0);
		Assert.That(pos1.Equals(pos2), Is.True);
	}

	[Test]
	public void AreNotEqual_WhenNotWithSameRowAndSameColumn()
	{
		var pos1 = new Position(1, 0);
		var pos2 = new Position(0, 0);
		Assert.That(pos1.Equals(pos2), Is.False);
	}
}

I ran this test and expected it to fail as I had not implemented anything, but it passes. Then I remember that struct has a default implementation ofEquals that makes these tests pass, paraphrasing from the Microsoft doc: "Any struct has a default implementation of value equality that it inherits from the System.ValueType. This implementation uses reflection which is relatively slow compared to a custom implementation that you write specifically for the type."

Before I go any further I will add tests for the equals operator.

RED (does not compile)

// PositionTest.cs
[Test]
public void AreEqual_WhenWithSameRowAndSameColumn_UsingOperator()
{
	var pos1 = new Position(0, 0);
	var pos2 = new Position(0, 0);
	Assert.That(pos1 == pos2, Is.True);
}

[Test]
public void AreNotEqual_WhenNotWithSameRowAndSameColumn_UsingOperator()
{
	var pos1 = new Position(1, 0);
	var pos2 = new Position(0, 0);
	Assert.That(pos1 != pos2, Is.True);
}

These tests produce the compile errors: Cannot apply operator '==' / '!=' to operands of type 'Position' and 'Position'

GREEN and REFACTOR

Although my default approach is to use the profiler to identify performance bottlenecks and verify improvements, I will this time take the Microsoft doc words as gospel truth and thus do a custom equality implementation. I use the IDEA (Rider) to do it in an automatic manner and thus also deem it safe to jump over Red (tests fail) step

// Position.cs
public readonly struct Position : IEquatable<Position>
{
	// ...
	public bool Equals(Position other)
	{
		return Column == other.Column && Row == other.Row;
	}

	public override bool Equals(object obj)
	{
		return obj is Position other && Equals(other);
	}

	public override int GetHashCode()
	{
		return HashCode.Combine(Column, Row);
	}

	public static bool operator ==(Position left, Position right)
	{
		return left.Equals(right);
	}

	public static bool operator !=(Position left, Position right)
	{
		return !left.Equals(right);
	}
}

I make Position inherit IEquatable and execute the action generate equality members which makes the tests pass and provides the clear refactored solution.