2025 C# Advent - A Christmas Trivia Game
Note: This post is part of the 2025 C# Advent! Check out the rest of the entries here. My post is scheduled to go live on the site on Monday, December 8, 2025.
For the past few years, my C# Advent posts have always included some Spectre.Console in them and this year keeps that trend going. I’ve been thinking about the “what” of this post for a couple months, but I’ve always known it would include some Spectre.Console just because I enjoy using it.
I decided to go back to Brian Hogan’s excellent Exercises for Programmers book. It’s hard to believe it came out 10 years ago!! I think it’s been the source for most of my Advent post ideas.
The Problem
This year I’m building a trivia app (Exercise 57 - Trivia App), and to keep with the season, it’s focused on Christmas.
From the book:
Create a multiple-choice trivia application.
- Read questions, answers, and distractors (wrong answers) from a file.
- Choose questions at random.
Display the answer and distractors in a random order.- Track the number of correct answers.
- End the game when the player answers all of the questions
selects a wrong answer.
I’m modifying the requirements slightly and remove one of them because of how the data is structured. I can’t easily randomize the answers, but that’s ok - I can save that for another iteration. I’m also not ending on a wrong answer because that seems bogus. I’m going to specify a number of questions to use and when all have been answered, incorrect or not, then the program will end.
Setup
I’ll be using .NET 10 and C# 14 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, especially on my Mac, continues to be JetBrains Rider!
I did ask Copilot to create a list of questions for me.
I’d like 100 trivia questions related to Christmas. I’d like them in json format so I can use them in a program I’m writing.
Copilot was being a pain in the ass that day and I ended up getting the questions in batches of 25, which was fine because it gave me an opportunity to review them.
Here’s an example showing how the json is structured
[
{
"type": "singleSelect",
"question": "Which country is credited with starting the tradition of the Christmas tree?",
"options": ["Germany", "United States", "France", "Norway"],
"answer": [0],
"explanation": "Germany popularized the Christmas tree tradition in the 16th century."
},
{
"type": "singleSelect",
"question": "In the song 'The Twelve Days of Christmas', what is given on the fifth day?",
"options": ["Golden rings", "French hens", "Turtle doves", "Calling birds"],
"answer": [0],
"explanation": "The fifth day gift is 'five golden rings'."
},
{
"type": "singleSelect",
"question": "What beverage is traditionally left out for Santa Claus on Christmas Eve?",
"options": ["Apple cider", "Milk", "Eggnog", "Hot chocolate"],
"answer": [1],
"explanation": "Milk is commonly left out with cookies for Santa."
}
]
The Code
Get started by creating a new console application and adding Spectre.Console.
dotnet new console -o ChristmasTrivia
cd ChristmasTrivia
dotnet add package Spectre.Console
The first step is to replace the default “Hello, World” with “Christmas Trivia”, but spiced up with some Spectre.Console. And yes, I used the same style in last year’s post, but the GetSeasonalString method has been updated.
AnsiConsole.MarkupLine($":christmas_tree: {GetSeasonalString("Christmas Trivia!")} :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.
static string GetSeasonalString(string input, string evenColor = "red", string oddColor = "green")
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var sb = new StringBuilder(input.Length * 8);
int visibleIndex = 0;
foreach (var ch in input)
{
if (char.IsWhiteSpace(ch))
{
sb.Append($"[on white]{Markup.Escape(ch.ToString())}[/]");
continue;
}
var baseColor = (visibleIndex % 2 == 0) ? evenColor : oddColor;
var colorWithBackground = $"{baseColor} on white";
sb.Append($"[{colorWithBackground}]{Markup.Escape(ch.ToString())}[/]");
visibleIndex++;
}
return sb.ToString();
}
Also notice I chose to output the Christmas tree emoji, something else Spectre.Console makes easy.
Because my data file has 100 questions, I want to prompt the user for the number of questions to display. I’ll default to 5 and will ensure that if a number larger than my set is entered that the user is notified.
The AnsiConsole.Ask method makes this extremely simple to do it all, but here’s the initial code to simply ask for the number. I’ll follow that up with some validation code.
var numberOfQuestions = AnsiConsole.Ask<int>("How many questions would you like to answer?", 5);
AnsiConsole.MarkupLine("Great! You will be answering [bold]{0}[/] questions.", numberOfQuestions);

I could write some code to manually check the number to ensure it doesn’t exceed the total questions in my json file, but I’ll add on to the .Ask prompt so it includes some validation. To make the code a bit cleaner, I’ve introduced some constants.
const int minimumQuestions = 5;
const int maximumQuestions = 100;
var numberOfQuestionsValidationMessage = $"Anything less than 5 questions disappoints Santa and we only have {maximumQuestions} questions total.";
Then, the Ask method turns into this:
AnsiConsole.WriteLine();
var numberOfQuestions = AnsiConsole.Prompt(
new TextPrompt<int>("How many questions would you like to answer?")
.DefaultValue(5)
.Validate(n => n is >= minimumQuestions and <= maximumQuestions, numberOfQuestionsValidationMessage)
);
I love how clean the pattern matching makes the .Validate call.
It’s time to load the data, but first I want to use Spectre.Console to provide some updates to what’s going on, so I’m using a Status control.
AnsiConsole.Status()
.Spinner(Spinner.Known.Christmas)
.Start("Loading questions...", ctx =>
{
// load questions
Thread.Sleep(2000);
AnsiConsole.MarkupLine("[green]Questions loaded![/]");
});
Loading the questions is going to be simple, so first I’ll start with another const:
const string dataFile = "data.json";
This file lives in the root of the project. If you want it somewhere else, adjust accordingly.
I want to deserialize the data into a list of questions, so I created a new class named TriviaQuestion which looks like this:
using System.Text.Json.Serialization;
namespace ChristmasTrivia;
public class TriviaQuestion
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("question")]
public string? Question { get; set; }
[JsonPropertyName("options")]
public List<string>? Options { get; set; }
[JsonPropertyName("answer")]
public List<int>? Answer { get; set; }
[JsonPropertyName("explanation")]
public string? Explanation { get; set; }
[JsonIgnore]
public bool IsMultiSelect => Answer is { Count: > 1 };
}
Pseudo code for how I think this will should work:
grab a random set of questions (numberOfQuestions)
for each question in the selected list
display the question
display the potential answers
wait for an answer
compare entered answer to the real answer
let the user know if they were correct or not and keep track of the number of correct/incorrect
Display a summary at the end and wish the user a Merry Christmas!
Back to loading - following the plan, I’ll pull the entire set of questions into an array and then grab a random set of no more than ‘numberOfQuestions’. I’m making sure that my random set doesn’t have any duplicates. The referenced extension (Shuffle) method will be shown in the final/full code at the end of this post.
IReadOnlyList<TriviaQuestion> allQuestions = Array.Empty<TriviaQuestion>();
AnsiConsole.Status()
.Spinner(Spinner.Known.Christmas)
.Start("Loading questions...", ctx =>
{
// load questions
allQuestions = LoadQuestions();
// keeping this in just so you can see the fancy Christmas tree
Thread.Sleep(2000);
AnsiConsole.MarkupLine($"[green]{allQuestions.Count} Questions loaded! Selecting {numberOfQuestions} for you![/]");
});
var random = new Random();
var pool = allQuestions.ToList();
pool.Shuffle(random);
var selectedQuestions = pool
.Take(Math.Min(numberOfQuestions, pool.Count))
.ToList();
The LoadQuestions method isn’t much. It verifies that the data file exists and then deserializes into a list of TriviaQuestions. Easy.
static IReadOnlyList<TriviaQuestion> LoadQuestions()
{
if (!File.Exists(dataFile))
{
throw new FileNotFoundException($"Could not find trivia data at '{dataFile}'.");
}
using var stream = File.OpenRead(dataFile);
var questions = JsonSerializer.Deserialize<List<TriviaQuestion>>(stream);
if (questions is null)
return Array.Empty<TriviaQuestion>();
return questions;
}
Now it’s time to display each question with it’s set of potential answers and let the user answer!
foreach (var question in selectedQuestions)
{
if (!AskQuestion(question))
break;
}
ShowSummary();
bool AskQuestion(TriviaQuestion question)
{
var options = question.Options ?? [];
// Display options with numbers
AnsiConsole.MarkupLine($"[bold]{question.Question}[/]");
for (int i = 0; i < options.Count; i++)
{
AnsiConsole.MarkupLine($" [yellow]{i + 1}[/]. {options[i]}");
}
while (true)
{
var input = AnsiConsole.Prompt(
new TextPrompt<string>("Select an option: ")
.AllowEmpty()
.DefaultValue("")
).Trim();
if (int.TryParse(input, out int num) && num >= 1 && num <= options.Count)
{
int selectedIndex = num - 1;
var isCorrect = question.Answer?.Contains(selectedIndex) == true;
if (isCorrect)
{
AnsiConsole.MarkupLine("[bold green]Correct![/]");
}
else
{
var correctOption = question.Answer?.FirstOrDefault() ?? -1;
var correctText = correctOption >= 0 && correctOption < options.Count
? options[correctOption]
: "Unknown";
AnsiConsole.MarkupLine($"[bold red]Incorrect.[/] The right answer is [green]{correctText}[/].");
}
if (!string.IsNullOrWhiteSpace(question.Explanation))
{
AnsiConsole.MarkupLine($"[italic]{question.Explanation}[/]");
}
AnsiConsole.WriteLine();
return true;
}
AnsiConsole.MarkupLine("[red]Invalid input. Please enter a number.[/]");
}
}
and finally, a nice summary after all the questions have been answered:
void ShowSummary()
{
var panel = new Panel($"You answered [green]{correct}[/] out of [yellow]{numberOfQuestions}[/] correctly!\nKeep spreading the cheer!")
{
Border = BoxBorder.Heavy,
BorderStyle = new Style(Color.Gold1)
};
AnsiConsole.Write(panel);
}
Here’s the running program:

Here’s the entire program in all its glory! Yes, it’s all jammed into a single .cs file. Put me on the naughty list. :-)
using Spectre.Console;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
const int minimumQuestions = 5;
const int maximumQuestions = 100;
var numberOfQuestionsValidationMessage = $"Anything less than 5 questions disappoints Santa and we only have {maximumQuestions} questions total.";
const string dataFile = "data.json";
AnsiConsole.MarkupLine($":christmas_tree: {GetSeasonalString("Christmas Trivia!")} :christmas_tree:");
AnsiConsole.WriteLine();
var numberOfQuestions = AnsiConsole.Prompt(
new TextPrompt<int>("How many questions would you like to answer?")
.DefaultValue(5)
.Validate(n => n is >= minimumQuestions and <= maximumQuestions, numberOfQuestionsValidationMessage)
);
AnsiConsole.MarkupLine("Great! You will be answering [bold]{0}[/] questions.", numberOfQuestions);
IReadOnlyList<TriviaQuestion> allQuestions = Array.Empty<TriviaQuestion>();
AnsiConsole.Status()
.Spinner(Spinner.Known.Christmas)
.Start("Loading questions...", ctx =>
{
// load questions
allQuestions = LoadQuestions();
Thread.Sleep(2000);
AnsiConsole.MarkupLine($"[green]{allQuestions.Count} Questions loaded! Selecting {numberOfQuestions} for you![/]");
});
var random = new Random();
var pool = allQuestions.ToList();
pool.Shuffle(random);
var selectedQuestions = pool
.Take(Math.Min(numberOfQuestions, pool.Count))
.ToList();
int correct = 0;
foreach (var question in selectedQuestions)
{
if (!AskQuestion(question))
break;
}
ShowSummary();
return;
bool AskQuestion(TriviaQuestion question)
{
var options = question.Options ?? [];
// Display options with numbers
AnsiConsole.MarkupLine($"[bold]{question.Question}[/]");
for (int i = 0; i < options.Count; i++)
{
AnsiConsole.MarkupLine($" [yellow]{i + 1}[/]. {options[i]}");
}
while (true)
{
var input = AnsiConsole.Prompt(
new TextPrompt<string>("Select an option: ")
.AllowEmpty()
.DefaultValue("")
).Trim();
if (int.TryParse(input, out int num) && num >= 1 && num <= options.Count)
{
int selectedIndex = num - 1;
var isCorrect = question.Answer?.Contains(selectedIndex) == true;
if (isCorrect)
{
correct++;
AnsiConsole.MarkupLine("[bold green]Correct![/]");
}
else
{
var correctOption = question.Answer?.FirstOrDefault() ?? -1;
var correctText = correctOption >= 0 && correctOption < options.Count
? options[correctOption]
: "Unknown";
AnsiConsole.MarkupLine($"[bold red]Incorrect.[/] The right answer is [green]{correctText}[/].");
}
if (!string.IsNullOrWhiteSpace(question.Explanation))
{
AnsiConsole.MarkupLine($"[italic]{question.Explanation}[/]");
}
AnsiConsole.WriteLine();
return true;
}
AnsiConsole.MarkupLine("[red]Invalid input. Please enter a number.[/]");
}
}
void ShowSummary()
{
var panel = new Panel($"You answered [green]{correct}[/] out of [yellow]{numberOfQuestions}[/] correctly!\nKeep spreading the cheer!")
{
Border = BoxBorder.Heavy,
BorderStyle = new Style(Color.Gold1)
};
AnsiConsole.Write(panel);
}
static IReadOnlyList<TriviaQuestion> LoadQuestions()
{
if (!File.Exists(dataFile))
{
throw new FileNotFoundException($"Could not find trivia data at '{dataFile}'.");
}
using var stream = File.OpenRead(dataFile);
var questions = JsonSerializer.Deserialize<List<TriviaQuestion>>(stream);
if (questions is null)
return Array.Empty<TriviaQuestion>();
return questions;
}
static string GetSeasonalString(string input, string evenColor = "red", string oddColor = "green")
{
if (string.IsNullOrEmpty(input))
return string.Empty;
var sb = new StringBuilder(input.Length * 8);
int visibleIndex = 0;
foreach (var ch in input)
{
if (char.IsWhiteSpace(ch))
{
sb.Append($"[on white]{Markup.Escape(ch.ToString())}[/]");
continue;
}
var baseColor = (visibleIndex % 2 == 0) ? evenColor : oddColor;
var colorWithBackground = $"{baseColor} on white";
sb.Append($"[{colorWithBackground}]{Markup.Escape(ch.ToString())}[/]");
visibleIndex++;
}
return sb.ToString();
}
public static class ListExtensions
{
public static void Shuffle<T>(this IList<T> list, Random rng)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = rng.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}
public class TriviaQuestion
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("question")]
public string? Question { get; set; }
[JsonPropertyName("options")]
public List<string>? Options { get; set; }
[JsonPropertyName("answer")]
public List<int>? Answer { get; set; }
[JsonPropertyName("explanation")]
public string? Explanation { get; set; }
[JsonIgnore]
public bool IsMultiSelect => Answer is { Count: > 1 };
}
I hope you enjoyed this year’s post. It was fun to write, and as usual, I can already think of some improvements. Unfortunately, the deadline for this post is looming and it IS Sunday night, so this is as good as it gets! Please leave a comment, or ping me on LI or Mastodon if you have questions or comments.
Make sure you check out the code on GitHub!
Merry Christmas!

Comments