Assignment: Playbook
Build a small single-player browser game in Blazor. The player reads a short passage of a story and picks what the main character does next. Each choice leads to a different passage. The story ends when a passage has no choices.
You are given a SQL seed file (SimplePlaybookSeed) that populates the story. Everything else — the project setup, data access layer, and UI — is yours to build.
Data model
The entire story lives in a single table:
| Column | Type | Notes |
|---|---|---|
Id | INT PK AUTO_INCREMENT | |
StoryText | TEXT | The passage the player reads |
Choice1Text | VARCHAR(255) | Label for the first choice, nullable |
Choice1Next | INT | Id of the section to jump to, nullable |
Choice2Text | VARCHAR(255) | Label for the second choice, nullable |
Choice2Next | INT | Id of the section to jump to, nullable |
A section where both Choice1Text and Choice2Text are NULL is an ending.
Tasks
1. Project setup
Create a new Blazor Web App project targeting .NET 9 or later with Interactive Server render mode.
Add the following NuGet packages:
Pomelo.EntityFrameworkCore.MySqlMicrosoft.EntityFrameworkCore.Design
Add a connection string for your local MySQL database to appsettings.json.
2. Data layer
Create a Data/ folder and implement the following:
AppDbContext — a DbContext with a single DbSet<Section>.
Section — an EF Core entity class that matches the table above and implements IHasId.
ARepository<TEntity> — an abstract generic repository base class. It must:
- Be constrained to
where TEntity : class, IHasId - Receive an
AppDbContextvia constructor and store the matchingDbSet<TEntity>usingdbContext.Set<TEntity>() - Provide at minimum:
GetById(int id),GetAll()
Use the Repository Pattern as described in Repository Pattern
Register AppDbContext and SectionRepository as scoped services in Program.cs.
3. Database migration and seeding
Run the EF Core migration to create the table:
dotnet ef migrations add Init
dotnet ef database updateThen seed the story by running the provided SQL script against your database.
4. Home page
Replace the default home page (/) with a start screen. It should:
- Display the game title and a short description
- Load the first section from the database via
SectionRepository - Show a Begin button that navigates to
/play/{id}where{id}is the first section’sId
Inject SectionRepository directly into the page.
5. Play page
Create a new page at the route /play/{Id:int}. It should:
- Declare a
[Parameter]property forId - Load the matching
SectionfromSectionRepositorywheneverIdchanges (useOnParametersSet) - Pass the loaded section to the
StorySectioncomponent (see task 6) - Navigate to
/play/{nextId}when the player picks a choice
6. StorySection component
Create a reusable component in Components/Shared/ that handles all rendering of a story section. It must:
- Accept the current
Sectionas a required[Parameter] - Accept an
EventCallback<int>parameter namedOnChoiceSelected - Render the story text
- For each non-null choice, render a button that invokes
OnChoiceSelectedwith the target section id - When both choices are null, render an ending message and a link back to the home page
The Play page renders this component and handles the OnChoiceSelected callback.
7. Scoped CSS
Add a .razor.css scoped stylesheet alongside each component you create. At minimum style:
- The story text (readable font size, good line height)
- The choice buttons (clearly separated, some hover effect)
- The ending state (visually distinct from the choice state)
Expected navigation flow
/ → Begin button → /play/1
/play/1 → pick a choice → /play/2 (or /play/3)
/play/2 → pick a choice → /play/4 (or /play/5)
...
/play/8 → ending, no choices, link back to /