5
\$\begingroup\$

About a year ago, my brother and I came up with this cool idea for how to solve the problem of determining if any given "cell" on a Chinese Checkers board was adjacent to the cell in question.

Recently we started digging our teeth back into the idea and how we could work on variations of the game in which more than directly adjacent cells are of interest. I took up the task of figuring out how to adjust the grid system.

We'd previously developed some concept code in both C++ and C#, but decided for this project to work in C# (which I'm a little rusty in; been working with C and C++ mostly lately). To start, I merged the concepts of coordinates and direction into a more mathematical "vector" concept. However, it quickly became apparent that it is valuable to have a type-enforced way of separating unit vectors from vectors of non-unit magnitude.

For instance, suppose I want Grid.Cell.Neighbor to require a unit vector as a parameter. I could include conditional code, and either return null or throw exceptions, but that kind of overhead seems excessive for this simple task (not to mention forcing calling code to add potential run-time error checking for something that should be able to be validated at compile-time).

That said, I like the idea of keeping the Vector struct/class as simple as possible because it's not a terribly complex structure (and wouldn't warrant a polymorphic class in my typical C++ projects; but instead a simple typedef).

I'm looking for suggestions on the cleanest, lightest (is that an oxymoron?), or in other words most preferred C# way of enforcing the use of "unit vectors". Additionally, I'm open to any comments about the interfaces publicly accessible outside of the "Grid" class as far as "good practice" for C# goes.

using System;
using System.Collections.Generic;

namespace Cs_Console_Test {
    public class Program {
        const string TableHeader = " X  \t YL \t YR ";
        static string TableRow(Grid.Vector vec) {
            return String.Format("{0,4}\t{1,4}\t{2,4}", vec.X, vec.YL, vec.YR);
        }

        // Tests the Neighbor member function
        static void PrintNeighbors(Grid.ICell Center) {
            Console.WriteLine("    Neighbors of ({0}, {1}, {2})",
                Center.Location.X, Center.Location.YL, Center.Location.YR);
            Console.WriteLine(TableHeader + "\t->\tValue");

            foreach (Grid.Vector unit in Grid.Vector.Units) {
                Grid.Vector? Translated = Center.Location + unit;
                Grid.ICell Neighbor = Center.Neighbor(unit);

                string Location =
                    Translated == null
                    ? "X: null,\tYL: null,\tYZ: null"
                    : TableRow(Translated.Value);

                string Value =
                    Neighbor == null
                    ? "null"
                    : "Grid.ICell";

                Console.WriteLine(Location + "\t->\t" + Value);
            }
            Console.WriteLine();
        }

        static void PrintNeighborsOf(Grid Board, Grid.Vector Direction) {
            Grid.Vector Center = new Grid.Vector(0, 0, 0);

            // Test the addition operator
            Grid.Vector? Translated = Center + Direction;
            if (Translated != null) {
                Grid.ICell Cell = Board[Translated.Value];
                if (Cell != null) {
                    PrintNeighbors(Cell);
                } else {
                    Console.WriteLine("No Grid.ICell found at ({0}, {1}, {2})\n",
                        Translated.Value.X, Translated.Value.YL, Translated.Value.YR);
                }
            } else {
                Console.WriteLine("Unable to translate by ({0}, {1}, {2}): Intregal Overflow\n",
                        Direction.X, Direction.YL, Direction.YR);
            }

        }

        static void PrintLocation(Grid.Vector lhs, Grid.Vector rhs) {
            Grid.Vector? Translated = lhs + rhs;
            if (Translated == null) {
                Console.WriteLine("Unable to translate by ({0}, {1}, {2}): Intregal Overflow\n",
                    rhs.X, rhs.YL, rhs.YR);
            } else {
                Console.WriteLine("This should not get printed\n");
            }
        }

        public static void Main(string[] args) {
            Grid Board = new Grid();

            PrintNeighborsOf(Board, new Grid.Vector(0, 0, 0));
            PrintNeighborsOf(Board, new Grid.Vector(4, 4, 4));
            PrintNeighborsOf(Board, new Grid.Vector(5, 0, 0));
            PrintLocation(new Grid.Vector(1, 0, 0), new Grid.Vector(127, 0, 0));

            Console.Write("Press any key to continue . . . ");
            Console.ReadKey();
        }
    }

    public partial class Grid {
        public struct Vector {
            // Enumerate all six of the cardinal unit vectors
            public static readonly Vector[] Units = new Vector[6]{
                new Vector( 1,  0,  0), new Vector(-1,  0,  0),
                new Vector( 0,  1,  0), new Vector( 0, -1,  0),
                new Vector( 0,  0,  1), new Vector( 0,  0, -1),
            };

            // sbyte => range of [-127, 128]; plenty big for this app, and small enough to GetHashCode()
            public sbyte X  { get; set; } // Horizontal Axis
            public sbyte YL { get; set; } // Left-slanted Vertical Axis
            public sbyte YR { get; set; } // Right-slanted Vertical Axis
            public Vector(sbyte x, sbyte yl, sbyte yr) { X = x; YL = yl; YR = yr; }

            // Overloaded Arithmetic Operators; Prototypes found at: http://stackoverflow.com/a/20596352/310560
            // Since we don't have a clear cut way to give a comparable "numerical" value to a set of 3 signed integers
            // The logic for `Compare` will be based on `==` and `<` (the typical C++ approach)
            // Where the typical C# approach would base `==` and `<` off `Compare`
            public static bool operator==(Vector lhs, Vector rhs) {
                return
                    lhs.X  == rhs.X  &&
                    lhs.YL == rhs.YL &&
                    lhs.YR == rhs.YR;
            }

            // The < operator is important for any data algorithms which require sorting
            public static bool operator< (Vector lhs, Vector rhs) {
                return
                    lhs.X  < rhs.X  || (lhs.X  == rhs.X  &&
                    lhs.YL < rhs.YL || (lhs.YL == rhs.YL &&
                    lhs.YR < rhs.YR));
            }

            // We'll just implement the rest of the comparison operators simply because we can,  and complementary
            // operators should always be implemented together (though why C# doesn't do this already I'm not sure)
            public static bool operator> (Vector lhs, Vector rhs) { return   rhs <  lhs; }
            public static bool operator<=(Vector lhs, Vector rhs) { return !(lhs >  rhs); }
            public static bool operator>=(Vector lhs, Vector rhs) { return !(lhs <  rhs); }
            public static bool operator!=(Vector lhs, Vector rhs) { return !(lhs == rhs); }

            // Attached Object Comparison Overloads
            public override bool Equals(object obj) { return (obj is Vector) && (this == (Vector)obj); }
            public bool Equals(Vector rhs) { return Equals(this, rhs); }
            public int CompareTo(Vector rhs) { return CompareTo(this, rhs); }

            // Detached Object Comparison Overloads
            public static bool Equals(Vector lhs, Vector rhs) { return lhs == rhs; }
            private static int CompareTo(Vector lhs, Vector rhs) {
                return lhs == rhs ?  0
                    : (lhs <  rhs ? -1
                    :                1);
            }

            // Used for indexing in hash tables (i.e. Dictionary<Vector, Cell>)
            public override int GetHashCode() {
                return (X << 16) | (YL << 8) | (YR << 0);
            }

            // Addition operator used for translation
            public static Vector? operator+(Vector lhs, Vector rhs) {
                int x  = lhs.X  + rhs.X,
                    yl = lhs.YL + rhs.YL,
                    yr = lhs.YR + rhs.YR;

                try {
                    Vector shifted  = new Vector(
                        checked((sbyte)(lhs.X + rhs.X)),
                        checked((sbyte)(lhs.YL + rhs.YL)),
                        checked((sbyte)(lhs.YR + rhs.YR)));
                    return shifted;
                } catch {
                    return null;
                }
            }
        }
    }

    public partial class Grid {
        private readonly Dictionary<Vector, ICell> Board;
        public ICell this[Vector i] {
            get { if (Board.ContainsKey(i)) return Board[i]; return null; }
            private set { Board[i] = value; }
        }

        public Grid() {
            Board = new Dictionary<Vector, ICell>();

            // Add the inner hexagonal part of the Chinese Checkers board
            // Will add logic for the outer triangles after finished testing
            for (sbyte i = -4; i < 5; ++i) {
                for (sbyte j = -4; j < 5; ++j) {
                    for (sbyte k = -4; k < 5; ++k) {
                        new Cell(this, new Vector(i, j, k));
                    }
                }
            }
        }
    }

    public partial class Grid {
        public interface ICell {
            Grid Parent { get; }
            Vector Location { get; }
            ICell Neighbor(Vector direction);
        }

        // It's important this class is private to insure Grid exclusively manages all cells
        // Thus it is important to maintain the interface ICell for public interaction
        private class Cell : ICell {
            private readonly Grid _parent;
            public Grid Parent { get { return _parent; } }

            private readonly Vector _location;
            public Vector Location { get { return _location; } }

            public ICell Neighbor(Vector direction) {
                Vector? loc = Location + direction;
                if (loc == null) { return null; }
                return Parent[loc.Value];
            }

            public Cell(Grid par, Vector loc) {
                _parent = par; _location = loc;
                Parent[Location] = this;
            }
        }
    }
}
\$\endgroup\$
5
  • \$\begingroup\$ please show code of client which creates and uses Grid class \$\endgroup\$ – Disappointed Jul 26 '16 at 12:57
  • \$\begingroup\$ @Disappointed I didn't want to include a GUI (seemed too much) or my Unit Tests (forcing someone to create a test project). If more code is desired, I'm thinking of moving some of my unit tests into a Main() for console application. How does that sound? Will include in the next iteration (as this question has an answer) \$\endgroup\$ – Assimilater Jul 26 '16 at 15:20
  • \$\begingroup\$ i just tried to use your class and i couldn't do it :) var vector1 = new Grid.Vector(50, 50, 50); var newGrid = new Grid();newGrid[vector1]=//cannot create sell here That's why it's interesting how do you use it. And by the way a lot of code issues could be observer from clien point of view. \$\endgroup\$ – Disappointed Jul 26 '16 at 15:27
  • \$\begingroup\$ @Disappointed Ah, I see why that would be good to include. As for why yours failed, the idea is that the user isn't meant to create cells (notice the [] operator is declared private set), the constructor for Grid creates all the cells that make up the board. I have future plans to make a configuration interface that can be used to describe custom board layouts. The current model creates a hexagon with each axis ranging from -4 to 4 (so 50 is out of range) \$\endgroup\$ – Assimilater Jul 26 '16 at 15:32
  • \$\begingroup\$ @Disappointed With some effort I was able to produce some use case code that doesn't invalidate the current answers. See the update :) \$\endgroup\$ – Assimilater Jul 26 '16 at 18:41
5
\$\begingroup\$

Only scratching the surface of struct Vector

A struct should be immutable if possible. You have public setters

public sbyte X  { get; set; } // Horizontal Axis
public sbyte YL { get; set; } // Left-slanted Vertical Axis
public sbyte YR { get; set; } // Right-slanted Vertical Axis  

and in addition if you need a comment for a variable/property then it should be named better.


public static Vector? operator+(Vector lhs, Vector rhs) {
    int x  = lhs.X  + rhs.X,
        yl = lhs.YL + rhs.YL,
        yr = lhs.YR + rhs.YR;

    try {
        Vector shifted  = new Vector(
            checked((sbyte)(lhs.X + rhs.X)),
            checked((sbyte)(lhs.YL + rhs.YL)),
            checked((sbyte)(lhs.YR + rhs.YR)));
        return shifted;
    } catch {
        return null;
    }
}  

Please don't declare and/or initialize multiple variables on the same line. This becomes just unreadable. Nevertheless this variables x,yl and yr are never used so get rid of them.


Although I place simple properties which are having only get; and/or set; on the same line I wouldn't do it for public Vector(sbyte x, sbyte yl, sbyte yr) { X = x; YL = yl; YR = yr; } for the sake of readability.


As a little side note, almost every C# developer would expect to see the opening brace { at a new line.

\$\endgroup\$
4
  • \$\begingroup\$ I would expect the { on a new like ;-) however I must admit there was a time when I tried the OP's formatting but I didn't like it... there was no { to put a breakpoint there haha ;-] \$\endgroup\$ – t3chb0t Jul 26 '16 at 11:41
  • \$\begingroup\$ C# style is the Allman style? Dang, I forget (Visual Studio just adheres to my format). Yuck aside (vertical space is precious :P ) I will update this in my next iteration (assuming a bunch of people don't come in and disagree with you in the next day or so). And oops on the x, yl, yr variables; a relic from an old way of trying to do things with that operator. Will Fix. Will make struct immutable as you say (this was my intention as well). \$\endgroup\$ – Assimilater Jul 26 '16 at 15:28
  • \$\begingroup\$ Any suggestions on better property names? Since they're "mathematical axis" I felt it was ok to have them named as they are; additionally at this point typical usage is to call the constructor, and not directly use those properties outside of the Grid class. Though this may change over time so I left them publicly exposed for now. \$\endgroup\$ – Assimilater Jul 26 '16 at 15:29
  • \$\begingroup\$ @t3chb0t you can always put a breakpoint on the first line in the body :/ (though let's not start a debate on preference ;) if the C# standard is Allman I will adhere :) ) \$\endgroup\$ – Assimilater Jul 26 '16 at 16:56
3
\$\begingroup\$

In addition to Heslacher's Answer, it is useful for debugging and output to override the ToString member function for struct Vector

public override string ToString()
{
    return String.Format("Grid.Vector({0}, {1}, {2})", X, YL, YR);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.