How do you feel about the use of ChatGPT and A.I. in general as presented in this article? Please let me know in the comments.
From the original explorer’s notes …
“For this tutorial, I’ve chosen to keep the original ASCII character set in order to capture the feeling of the original game and to keep the focus on the C# language itself rather than the sophistication of the graphics.”
Feburary 18, 2023
Yeah, that’s great but this still doesn’t cut it.
I’ll grant that it’s somewhat better than the prototype Rogue game from all those years ago …
… but it still doesn’t measure up to the DOS classic.
Even while focusing on the language rather than graphics, C# still offers better ways to render ASCII graphics than outputting a string to a label control. It was clever enough for a start but I want some color on the map and the label control doesn’t allow for that variation.
For my first upgrade to the code during this project rescue, I’m looking at how to draw text on the screen so that different items can be highlighted with different colors and be a little more distinctive.
I think I already had drawing text in mind when I turned to ChatGPT for ideas but wanted to get some specifics, including sample code.
As a second option, it recommended the TextRenderer class as a better alternative to Graphics.DrawString and I decided to try that.
This post is part of a series on creating a roguelike game in C#. For the latest chapters and more information, please visit the Official Project Page on AndrewComeau.com. The current code for this project is also available on Github.
Tracing the code
I had to do some actual review of the code after not touching it for so long and it was proof of what I’ve told others many times – comment your code because you won’t remember it next month, much less a year from now.
Tracing the display process backward from its end, the map on DungeonMain is supplied by the ScreenDisplay string property of the Game class. It’s updated both at the start of the game (DungeonMain.StartGame()) and in the KeyDown event for every key that’s pressed.
lblArray.Text = currentGame.ScreenDisplay;
This property is updated at a few points in the Game class by setting it to the output of one of two functions in the MapLevel class.
ScreenDisplay = DevMode ? this.CurrentMap.MapCheck() : this.CurrentMap.MapText();
Both of these functions read from the MapLevel.levelMap property which is an array of MapSpace objects which contain a number of optional characters to display based on conditions. MapCheck() is used in Developer Mode and just outputs a default priority character to show the player the entire map and everything happening on it. MapText() maintains the Fog of War and makes a lot more decisions about which character to display based on the player’s relative position to everything else on the map, whether spaces have been discovered, etc.
GPT’s suggestion of a 2D cell array made sense. I could have both functions return an array of char values rather than a string so that whatever routine draws the text can iterate through it. This still doesn’t provide instructions for what color to paint each character. Many of the characters in the game such as inventory items and monsters are defined by constants in their respective classes. The MapLevel class and its objects only supply the actual map spaces – walls, hallways, doors, etc..
private const char HORIZONTAL = '═';
private const char VERTICAL = '║';
private const char CORNER_NW = '╔';
private const char CORNER_SE = '╝';
private const char CORNER_NE = '╗';
private const char CORNER_SW = '╚';
GPT’s recommended using a struct, holding the character and color information. I could use a Tuple just to hold the character and foreground since the background is already defined but there’s always the possibility that a future update might require more information. I added the new MapGlyph struct to the MapLevel class.
public struct MapGlyph
{
public char DisplayChar;
public Color Foreground;
public Color Background;
}
A new instance is neeed for each map constant in the program. The problem is that constants in C# can’t be defined to new objects so the map constants will need to be changed to static readonly variables.
public static readonly MapGlyph HORIZONTAL
= new MapGlyph ('═', Color.Green, Color.Black);
So, now my first step will be to change all the declarations in each class, see what hell breaks loose in the Error List from invalid references and work my way forward from there.
The Inventory, Monster and Player classes all refer to their own char value constants in their constructors and the reference lists they use for inventory and monster types so those will need to be changed, too.
I got a little confused for a moment with the necessary relationship between the existing MapSpace and new MapGlyph classes. MapSpace represents the map in the computer’s memory with settings for each character based on discovered and hidden status, etc.. MapGlyph represents what will finally be output to the user at the end of each turn based on various decisions.
The best way to do this is for the MapSpace objects themselves to hold MapGlyph objects instead of char values. This creates a hierarchy of classes: MapLevel -> MapSpace -> MapGlyph. This also meant going through the rest of the code and using the DisplayCharacter property of the MapGlyph.MapSpace object in order to refer to the actual char values.
Then, the MapLevel class maintains lists of char values that will be visible, occur inside a room, etc.. For code brevity, I tried changing these to lists of MapGlyphs instead to see what coding changes were required but that would have made a number of the comparisons in the program more difficult. Once again, it meant referring to the DisplayChar property rather than the MapGlyph object itself.
Once everything was resolved in the classes, I needed a new array to hold the glyphs for output to the user. I’m using an array of MapGlyph objects since that’s all that’s needed.
/// <summary>
/// Array to hold map definition.
/// </summary>
private MapSpace[,] levelMap = new MapSpace[80, 25];
public MapGlyph[,] DisplayMap = new MapGlyph[80, 25];
You might be wondering – “Why not just copy the glyph objects from levelMap”.
MapText() and MapCheck() are still separate functions that make decisions about what character to place on the user’s map depending on the location of monsters inventory and other items and whether something is hidden so there’s still a lot of filtering to be done from levelMap.
Fortunately, changing these two functions was a breeze.
public MapGlyph[,] MapCheck()
{
// Iterate through the two-dimensional array and transfer the appropriate
// characters to the output map to display to the user.
for (int y = 0; y <= MAP_HT; y++)
{
for (int x = 0; x <= MAP_WD; x++)
DisplayMap[x, y] = PriorityChar(levelMap[x, y], true);
}
return DisplayMap;
}
Of course, that’s just the developer view. MapText() is a bit more involved but you can see the new version in the code on Github.
I also realized later on that these changes would make the original ScreenDisplay property obsolete. ScreenDisplay functioned by holding a string value assembled from the map which can be copied around as needed. Now we’re working with an array which is a reference type and the content of which is not copied when the reference to it is copied. MapText() and MapCheck() are updating the DisplayMap array directly, even though they’re still functions and technically returning it at the end. That’s why DisplayMap is public so it can be referenced by the main form.
This became really obvious when I realized the Help and Inventory screens were no longer working because their content was not being copied to the new DisplayMap. Their content is also different than the game map so they needed a special function in the MapLevel class to translate it to DisplayMap when necessary.
public void UpdateDisplayFromText(string TextOutput)
{
string[] lines = TextOutput.Split('\n');
int cx = 0, cy = 0, nx = 0, ny = 0;
// Clear existing text
for(cy = 0; cy < 25; cy++)
{
for(cx = 0; cx < 80; cx++)
{
DisplayMap[cx, cy] =
new MapGlyph(MapLevel.EMPTY.DisplayChar, Color.Black, Color.Black);
}
}
// Add new text
foreach(string line in lines)
{
foreach (char c in line)
{
DisplayMap[nx, ny] =
new MapGlyph(c, Color.Orange, Color.Black);
nx += 1;
}
nx = 0;
ny += 1;
}
}
Rendering the Text
I originally wanted to paint the characters on a Panel in order to have finer control over where the map would appear on the form and, with some encouragement from ChatGPT, used the Paint event to transfer the MapGlyphs from the array onto the panel.
Keep that phrase,“ideal WinForms approach”, in mind. It also suggested subclassing the Panel control because C# hides the DoubleBuffered property that is supposed to keep screen updates from flickering. I added the subclass to the code for the main form and then made sure it was referenced in the declarations.
public class RoguePanel : Panel
{
public RoguePanel()
{
DoubleBuffered = true;
ResizeRedraw = true;
}
}
Surprisingly, after all those code changes throughout the classes, the new rendering worked the first time, although not quite flawlessly. Click the image below to view the animation.
Checking in with ChatGPT …
So much for the “ideal WinForms approach”. I tried a few of the other suggestions for adjusting the performance and the flicker got marginally better but didn’t disappear so I ditched the panel and drew the text directly on the form. The StartGame() method and KeyDown() event also explicitly Invalidate the form in order to ensure a redraw.
(Form level)
Font mapFont = new Font("Consolas", 14, FontStyle.Regular);
protected override void OnPaint(PaintEventArgs e)
{
// Redraw the map from the ScreenDisplay array.
int cellWidth = 10;
int cellHeight = 18;
int px, py;
if (currentGame != null)
{
// Iterate through array cells and draw glyphs on screen.
for (int y = 0; y < currentGame.ScreenDisplay.GetLength(1); y++)
{
for (int x = 0; x < currentGame.ScreenDisplay.GetLength(0); x++)
{
MapGlyph g = currentGame.ScreenDisplay[x, y];
px = x * cellWidth;
// Add 150 to top to avoid game message display.
py = y * cellHeight + 150;
TextRenderer.DrawText(
e.Graphics,
g.DisplayChar.ToString(),
mapFont,
new Point(px, py),
g.Foreground,
g.Background,
TextFormatFlags.NoPadding);
}
}
}
}
After playing around with the placement and the colors a bit, I have it looking a lot closer to the classic Rogue that I know and love.
Next up …
For the Help and Inventory screens, I was originally adding a large top margin so as not to conflict with the status updates at the top but I was able to use the GameMode property of the currentGame object to solve that and hide the status list when not needed.
listStatus.Visible = (currentGame.GameMode == Game.DisplayMode.Primary);
The status updates and the stats display at the bottom might still need work, though. There’s no reason the stats display can’t be drawn on the screen with the rest of the game map as the map is 24 cells high and there are 25 available. As for the status updates, I still like having a history available rather than the single most recent status that the original game would show and then, often, remove before you could read it, but I might want the list to be a bit more selective about what it retains and to highlight some things.
Sign up for my newsletter to receive updates about new projects, including my new book "Self-Guided SQL: Build Your SQL Skils with SQLite"!
We respect your privacy and will never share your information with third-parties. See our privacy policy for more information.















