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:

ColumnTypeNotes
IdINT PK AUTO_INCREMENT
StoryTextTEXTThe passage the player reads
Choice1TextVARCHAR(255)Label for the first choice, nullable
Choice1NextINTId of the section to jump to, nullable
Choice2TextVARCHAR(255)Label for the second choice, nullable
Choice2NextINTId 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.MySql
  • Microsoft.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 AppDbContext via constructor and store the matching DbSet<TEntity> using dbContext.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 update

Then 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’s Id

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 for Id
  • Load the matching Section from SectionRepository whenever Id changes (use OnParametersSet)
  • Pass the loaded section to the StorySection component (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 Section as a required [Parameter]
  • Accept an EventCallback<int> parameter named OnChoiceSelected
  • Render the story text
  • For each non-null choice, render a button that invokes OnChoiceSelected with 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 /