Christmas MadLib using C# and Spectre.Console (2024 C# Advent)
Note: This post is part of the 2024 C# Advent! Check out the rest of the entries here.
I am once again reaching into Brian P. Hogan’s excellent Exercises for Programmers book for a fun exercise that I tweaked for the season!
The Problem
I’m tackling Exercise 4 from the book, Mad Lib! As a child, my sister would buy Mad Lib books and do them with me in the car on family trips, or when we were bored and had nothing else to do.
The gist of this exercise is:
Create a simple mad-lib program that prompts for a noun, a verb, an adverb, and an adjective and injects those into a story that you create.
Setup
I’ll be using .NET 8 and C# 12 for this project, and I’ll also be using Spectre.Console again to help with both the input and output of the program. All screenshots will be from my Mac, but the code should work on Windows and Linux as well. Oh, and one final note - my IDE of choice lately is JetBrains Rider!
Given the season, I asked GitHub Copilot to help me write the story. The prompt I used:
I want to build a MadLib type program, but I need a story. I want it to be related to Christmas. Can you help?
I have modified what it generated, but it was a good start giving me a general story with ideas for placeholders. I’m also expanding a bit on Brian’s exercise to prompt for more words since my story is a bit longer.
The Code
Get started by creating a new console application and adding Spectre.Console.
dotnet new console -o ChristmasMadLib
cd ChristmasMadLib
dotnet add package Spectre.Console
The first step is to replace the default “Hello, World” with “A Christmas Tale, MadLib Style”
AnsiConsole.MarkupLine($":christmas_tree: {getSeasonalString("A Christmas Tale, MadLib Style!")} :christmas_tree:");
Here’s what it looks like in my terminal
The helper method looks at the string and alternates colors, in this case red and green, both on a white background.
string getSeasonalString(string input)
{
var inColor = "";
for (int i = 0; i < input.Length; i++)
{
if (i % 2 == 0)
{
inColor += $"[red on white]{input[i]}[/]";
}
else
{
inColor += $"[green on white]{input[i]}[/]";
}
}
return inColor;
}
Also notice I chose to output the Christmas tree emoji, something else Spectre.Console makes easy.
The program itself is simple, especially since C# added string interpolation in 6.0. I used Spectre.Console to gather the nouns, verbs, adverbs, adjectives, and other input I needed for the story to be generated. To streamline things, I pulled the prompts into constants.
const string nounPrompt = "Enter a noun:";
const string namePrompt = "Enter a name:";
const string pluralNounPrompt = "Enter a plural noun:";
const string adjectivePrompt = "Enter an adjective:";
const string placeInHousePrompt = "Enter a place in a house:";
const string greetingPrompt = "Enter a greeting:";
const string adverbPrompt = "Enter an adverb:";
const string vehiclePrompt = "Enter a vehicle:";
const string placePrompt = "Enter a place:";
const string phrasePrompt = "Enter a phrase:";
string WhatTheVillageIsCoveredIn = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "Snow");
string ElfName = AnsiConsole.Ask<string>(redOnWhiteText(namePrompt), "Elf");
string ThingsElvesMake = AnsiConsole.Ask<string>(greenOnWhiteText(pluralNounPrompt), "toys");
string DescriptionOfChildren = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "good");
string Workshop = AnsiConsole.Ask<string>(greenOnWhiteText(placeInHousePrompt), "workshop");
string SantasGreeting = AnsiConsole.Ask<string>(redOnWhiteText(greetingPrompt), "Ho, ho, ho!");
string TargetOfGift = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "child");
string ChildName = AnsiConsole.Ask<string>(redOnWhiteText(namePrompt), "Timmy");
string ElfFeeling = AnsiConsole.Ask<string>(greenOnWhiteText(adjectivePrompt), "excited");
string HowDidTheElfWork = AnsiConsole.Ask<string>(redOnWhiteText(adverbPrompt), "immediately");
string Material1 = AnsiConsole.Ask<string>(redOnWhiteText(nounPrompt), "wood");
string Material2 = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "paint");
string Material3 = AnsiConsole.Ask<string>(redOnWhiteText(nounPrompt), "glitter");
string Tool = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "hammer");
string ToyDescription = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "shiny");
string Toy = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "train");
string Vehicle = AnsiConsole.Ask<string>(redOnWhiteText(vehiclePrompt), "sleigh");
string Destination = AnsiConsole.Ask<string>(greenOnWhiteText(placePrompt), "the night sky");
string ChristmasDescription = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "magical");
string CatchPhrase = AnsiConsole.Ask<string>(greenOnWhiteText(phrasePrompt), "Merry Christmas!");
Since I wanted to the prompts to be just as festive as the rest of the app, here are the helper methods I wrote to avoid sprinkling style markup through the text since it was making it really tough when I wanted to make a change.
string greenOnWhiteText(string input)
{
return $"[green on white]{input}[/]";
}
string redOnWhiteText(string input)
{
return $"[red on white]{input}[/]";
}
As I was writing the code and testing with my wife and son, we definitely came up with some funny stories. My wife wanted me to be more specific about some of the input
don’t just say ‘noun’, say ‘material’ or something
I did get specific in a couple instances, but I also decided to include default values for each of the .Ask<T> calls. Spectre.Console really does make this kind of thing simple.
Before I get to the story itself, I used a Spectre.Console Panel for the output, setting the border to red, and then printed a final message with more Christmas trees.
var storyOutput = new Panel(new Markup(greenOnWhite(story)))
.BorderColor(Color.Red);
AnsiConsole.Write(storyOutput);
Console.WriteLine();
AnsiConsole.MarkupLine($":christmas_tree: {getSeasonalString("Merry Christmas!")} :christmas_tree:");
Ok, so, now the story. To make it easy, I took what GitHub Copilot generated for me, tweaked it in a separate text editor, and then created an interpolated verbatim string literal, something introduced in C# 8.0.
string story = @$"Once upon a time, in a small village covered in [red on white]{WhatTheVillageIsCoveredIn}[/], there was a little elf named [red on white]{ElfName}[/]. [red on white]{ElfName}[/] was very excited because Christmas was just around the corner. Every year, [red on white]{ElfName}[/] and the other elves worked hard to make [red on white]{ThingsElvesMake}[/] for all the [red on white]{DescriptionOfChildren}[/] children around the world.
One day, Santa Claus called [red on white]{ElfName}[/] to his [red on white]{Workshop}[/]. ""[red on white]{SantasGreeting}[/], [red on white]{ElfName}[/]! I need your help with a special task,"" Santa said. ""We need to make a special toy for a {TargetOfGift} named [red on white]{ChildName}[/].""
[red on white]{ElfName}[/] was [red on white]{ElfFeeling}[/] and {HowDidTheElfWork} got to work. First, [red on white]{ElfName}[/] gathered all the materials: [red on white]{Material1}[/], [red on white]{Material2}[/], and [red on white]{Material3}[/]. Then, [red on white]{ElfName} used {Tool}[/] to put everything together. It was hard work, but [red on white]{ElfName}[/] didn't mind because it was for a very special child.
Finally, the toy was ready. It was a [red on white]{ToyDescription} {Toy}[/]. Santa was very pleased. ""Thank you, [red on white]{ElfName}[/]! This is perfect,"" Santa said. ""Now, let's get ready for Christmas Eve!""
On Christmas Eve, [red on white]{ElfName}[/] and the other elves loaded the [red on white]{Vehicle}[/] with all the toys. Santa climbed into the [red on white]{Vehicle}[/] and waved goodbye. ""[red on white underline]{CatchPhrase}[/]"" he shouted as the {Vehicle} took off into the night sky.
[red on white]{ElfName}[/] watched as Santa and the reindeer disappeared into [red on white]{Destination}[/]. It had been a [red on white]{ChristmasDescription}[/] Christmas, and [red on white]{ElfName}[/] couldn't wait to do it all again next year.
The End.";
The descriptive variable names I used to gather the data are really helpful because I can read the story and and understand it without having to run the program and fill things in. The alternative was to name them with non-descriptive terms like “noun1”, “noun2”.
I need to clean it up a bit to reduce the hard-coded style markup through the text. It’s nice in the output, but it’s a bit difficult to manage in code.
Here it is running on my Mac using the defaults provided.
And here’s what the story looks like printed out at the end
Ok, so here’s the whole thing!
using Spectre.Console;
Console.WriteLine();
Console.WriteLine();
AnsiConsole.MarkupLine($":christmas_tree: {getSeasonalString("A Christmas Tale, MadLib Style!")} :christmas_tree:");
Console.WriteLine();
Console.WriteLine();
const string nounPrompt = "Enter a noun:";
const string namePrompt = "Enter a name:";
const string pluralNounPrompt = "Enter a plural noun:";
const string adjectivePrompt = "Enter an adjective:";
const string placeInHousePrompt = "Enter a place in a house:";
const string greetingPrompt = "Enter a greeting:";
const string adverbPrompt = "Enter an adverb:";
const string vehiclePrompt = "Enter a vehicle:";
const string placePrompt = "Enter a place:";
const string phrasePrompt = "Enter a phrase:";
string WhatTheVillageIsCoveredIn = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "Snow");
string ElfName = AnsiConsole.Ask<string>(redOnWhiteText(namePrompt), "Elf");
string ThingsElvesMake = AnsiConsole.Ask<string>(greenOnWhiteText(pluralNounPrompt), "toys");
string DescriptionOfChildren = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "good");
string Workshop = AnsiConsole.Ask<string>(greenOnWhiteText(placeInHousePrompt), "workshop");
string SantasGreeting = AnsiConsole.Ask<string>(redOnWhiteText(greetingPrompt), "Ho, ho, ho!");
string TargetOfGift = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "child");
string ChildName = AnsiConsole.Ask<string>(redOnWhiteText(namePrompt), "Timmy");
string ElfFeeling = AnsiConsole.Ask<string>(greenOnWhiteText(adjectivePrompt), "excited");
string HowDidTheElfWork = AnsiConsole.Ask<string>(redOnWhiteText(adverbPrompt), "immediately");
string Material1 = AnsiConsole.Ask<string>(redOnWhiteText(nounPrompt), "wood");
string Material2 = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "paint");
string Material3 = AnsiConsole.Ask<string>(redOnWhiteText(nounPrompt), "glitter");
string Tool = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "hammer");
string ToyDescription = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "shiny");
string Toy = AnsiConsole.Ask<string>(greenOnWhiteText(nounPrompt), "train");
string Vehicle = AnsiConsole.Ask<string>(redOnWhiteText(vehiclePrompt), "sleigh");
string Destination = AnsiConsole.Ask<string>(greenOnWhiteText(placePrompt), "the night sky");
string ChristmasDescription = AnsiConsole.Ask<string>(redOnWhiteText(adjectivePrompt), "magical");
string CatchPhrase = AnsiConsole.Ask<string>(greenOnWhiteText(phrasePrompt), "Merry Christmas!");
Console.WriteLine();
string story = @$"Once upon a time, in a small village covered in [red on white]{WhatTheVillageIsCoveredIn}[/], there was a little elf named [red on white]{ElfName}[/]. [red on white]{ElfName}[/] was very excited because Christmas was just around the corner. Every year, [red on white]{ElfName}[/] and the other elves worked hard to make [red on white]{ThingsElvesMake}[/] for all the [red on white]{DescriptionOfChildren}[/] children around the world.
One day, Santa Claus called [red on white]{ElfName}[/] to his [red on white]{Workshop}[/]. ""[red on white]{SantasGreeting}[/], [red on white]{ElfName}[/]! I need your help with a special task,"" Santa said. ""We need to make a special toy for a {TargetOfGift} named [red on white]{ChildName}[/].""
[red on white]{ElfName}[/] was [red on white]{ElfFeeling}[/] and {HowDidTheElfWork} got to work. First, [red on white]{ElfName}[/] gathered all the materials: [red on white]{Material1}[/], [red on white]{Material2}[/], and [red on white]{Material3}[/]. Then, [red on white]{ElfName} used {Tool}[/] to put everything together. It was hard work, but [red on white]{ElfName}[/] didn't mind because it was for a very special child.
Finally, the toy was ready. It was a [red on white]{ToyDescription} {Toy}[/]. Santa was very pleased. ""Thank you, [red on white]{ElfName}[/]! This is perfect,"" Santa said. ""Now, let's get ready for Christmas Eve!""
On Christmas Eve, [red on white]{ElfName}[/] and the other elves loaded the [red on white]{Vehicle}[/] with all the toys. Santa climbed into the [red on white]{Vehicle}[/] and waved goodbye. ""[red on white underline]{CatchPhrase}[/]"" he shouted as the {Vehicle} took off into the night sky.
[red on white]{ElfName}[/] watched as Santa and the reindeer disappeared into [red on white]{Destination}[/]. It had been a [red on white]{ChristmasDescription}[/] Christmas, and [red on white]{ElfName}[/] couldn't wait to do it all again next year.
The End.";
var storyOutput = new Panel(new Markup(whiteOn))
.BorderColor(Color.Red);
AnsiConsole.Write(storyOutput);
Console.WriteLine();
AnsiConsole.MarkupLine($":christmas_tree: {getSeasonalString("Merry Christmas!")} :christmas_tree:");
string greenOnWhiteText(string input)
{
return $"[green on white]{input}[/]";
}
string redOnWhiteText(string input)
{
return $"[red on white]{input}[/]";
}
string getSeasonalString(string input)
{
var inColor = "";
for (int i = 0; i < input.Length; i++)
{
if (i % 2 == 0)
{
inColor += $"[red on white]{input[i]}[/]";
}
else
{
inColor += $"[green on white]{input[i]}[/]";
}
}
return inColor;
}
This was another fun one to write, and testing with my wife and son made for a lot of laughs.
I hope you enjoyed it!
Make sure you check out the code on GitHub!
Comments