EECS 498 APSD Project 4: Web-Based Frontend and Multiplayer
Feature Demo: No official feature demo. You’re welcome to ask questions in lab. (You’ll also implement task 6 as part of lab on Fri Apr 17.)
Initial Deliverables: Due Tue Apr 21 at 11:59pm. This will be worth the full 9% of your grade for P4.
Revisions: There is no separate revision submission for this project.
This project is a combined version of material originally intended for projects 4 and 5. It includes a significant amount of starter code, which you should spend some time reviewing and understanding as you work through the tasks described below. Much of the work will be plugging in and adapting your code to work in the context of the full system. You’ll find several comments throughout the files, including some labeled TASK X, that provide specific guidance on what to implement and where.
Given the short timeline for the project, my hope is that this structure makes it feasible for you to engage with the interesting material in the second half of the course without getting bogged down in too much new implementation work. I also hope it gives you the satisfaction of seeing the full picture come together, supported at the core by the code you’ve developed in previous projects.
If you and your group find that this is taking an unreasonable amount of time or there are places where you’re not sure how to proceed, please reach out to me in office hours or otherwise. You’re also welcome to ask/answer questions of each other on Discord.
Changelog
We’ll record updates and fixes to the project specification here.
IMPORTANT! Changes from initial release:
We initially released some files in lab last week. There are a few important updates and clarifications since then:
- Correction to
saveAutosaveprocedure inpackages/trpc/src/routers/players.ts: Add a definition for the followingGameDTOSchemaat the top of the file:const GameDTOSchema = z.object({ kind: z.string(), config: z.record(z.string(), z.unknown()), state: z.record(z.string(), z.unknown()), });Then, in the
saveAutosaveprocedure’s input schema, useautosave: GameDTOSchemarather thanautosave: z.object(). (The latter is broken - it will discard all the autosave data.) -
The
onCellsChordedevent hasxandyparameters representing the original chord location. The type ofcellsRevealedshould beReadonlyArray<{ x: number; y: number; details: MinesweeperRevealDetails }>, updated to include full reveal details for each cell revealed as part of a chord rather than the previous definition that only included the contents of those cells. - Some events from specific games are redundant with
onGameFinishedfrom the basePuzzleObserverinterface:MinesweeperObserver:onGameWon()andonGameLost()SudokuObserver:onPuzzleSolved()SpellingBeeObserver:onAllWordsFound()
You may remove these redundant events altogether if you like. The frontend doesn’t use them at all. Do make sure that your games emit
onGameFinished()appropriately, since that’s what the provided frontend listens for.
Introduction
Welcome to Project 4! In this project, your group will connect your puzzle implementations to a web-based frontend using Vue.js. This involves several key tasks:
- Task 1 - Observer Pattern: Implement the observer pattern in your puzzle classes to emit events when game state changes.
- Task 2 - tRPC Backend: Ensure the API endpoints work correctly to allow the web frontend to interact with your database layer.
- Task 3 - Puzzle Configuration, State, and Creation: Define how game data is transferred between the database, multiplayer system, and frontend.
- Task 4 - Puzzle Game Integration: Connect your game logic to the provided Vue components.
- Task 5 - Error Handling and Resource Cleanup: Implement proper error handling and fix resource management bugs.
- Task 6 - Multiplayer Sync: (To be completed in lab on Fri Apr 17) Address concurrency issues in the multiplayer server.
The web frontend includes support for:
- Single-player games with autosave
- Multiplayer games via a WebSocket-based room system
- Animations and visual feedback for game events
Prerequisites
This project continues from your existing group project codebase. From previous projects, you should already have:
- All three puzzles working with core game logic decoupled from I/O.
- The three new actions from Project 3 (chord, hint, fill).
- Minesweeper special items (shield, smoke bomb, scanner).
- Database integration for player info, sudoku puzzles, and autosave.
Note: You won’t necessarily need the Sudoku and Spelling Bee variations from project 3. The proper place to implement these is in the UI layer. The Sudoku emoji themes from Project 3 are already implemented in the provided web-based interface. The Spelling Bee variations that used different ASCII art boards do not apply naturally for the web-based interface, which instead dynamically renders the board based on the number of letters. You will need to ensure your underlying spelling bee class can accommodate a variable number of letters.
Note: The provided web-based Sudoku interface has a “clear” action to remove numbers from cells. You may not have this action from previous projects, but it should be fairly straightforward to add.
Setup
Starter Files
You received batch 1 of the starter files in a previous pull request to your group repository.
You’ll find another pull request with batch 2. Merge that into your codebase. It only adds new folders/files, so it shouldn’t conflict with anything. The PR is targeted at the main branch, so if you’re working somewhere else, you’ll want to merge/rebase to get the new files to your desired branch.
Adding the Observer Package Dependency
To use the provided Observable class, add it as a dependency to your puzzle package. You likely already did this after receiving the first batch of starter files. If not, find packages/puzzle/package.json and add:
"dependencies": {
"@repo/observer": "workspace:*"
}
Install Dependencies
Install dependencies from the root of the repository. This will handle new apps/packages that were added from the starter files, as well as your update to the puzzle package.json.
$ pnpm install
Running the Application
Development Mode
To start all development servers (web-based frontend, tRPC backend for database access, and multiplayer server), run:
$ pnpm dev
This uses Turborepo to run multiple development servers in parallel. The terminal will show a tabbed interface - use arrow keys to switch between the different server outputs.
Default Ports: | Service | Port | URL | | ——- | —- | — | | Vue Frontend | 5173 | http://localhost:5173 | | tRPC Backend | 5000 | http://localhost:5000/trpc | | Multiplayer Server | 3001 | ws://localhost:3001 |
If you have another process using one of these ports, you can change the port in the respective configuration files:
- Frontend:
apps/local-frontend/vite.config.ts(addport: XXXXto theserverobject) - tRPC Backend:
packages/trpc/src/server.ts(change thePORTconstant) - Multiplayer Server:
apps/room-server/src/server.ts(change thePORTenvironment variable default)
Opening the Frontend
Once the development servers are running, open http://localhost:5173 (or your configured port) in a web browser.
Important: The frontend will not work completely out of the box! You’ll need to complete the tasks in this project to connect your puzzle implementations to the provided Vue interface. We recommend this progression:
- Get it to launch: Run
pnpm devand verify you can open the frontend in a browser without immediate crashes. - Get player login working: This involves ensuring the tRPC backend (Task 2) correctly connects to your database layer for player creation and lookup. If nothing seems to be working, check the terminal output for your tRPC backend and make sure it isn’t running into import issues or other errors.
- Get one puzzle working: Start with Sudoku or Spelling Bee (they’re a bit simpler than Minesweeper). This involves Tasks 1, 3, and 4 for that puzzle. Once one works, the others should follow a similar pattern.
Note: Hot Module Replacement (HMR) is enabled by default in Vite, which means changes to Vue components will be reflected without a full page reload. If you want to disable HMR (e.g., for debugging), uncomment the hmr: false line in vite.config.ts.
Debugging
You can debug frontend code using your browser’s built-in developer tools:
- Open developer tools: Press F12 (or right-click and select “Inspect”) to open the browser’s developer tools panel.
- Set breakpoints: In the “Sources” tab, navigate to your TypeScript/JavaScript files and click on line numbers to set breakpoints. Execution will pause when those lines are reached.
- Use
debugger;statements: You can also adddebugger;statements directly in your TypeScript code. When developer tools are open, execution will pause at these statements.
The “Console” tab is useful for viewing log messages and errors. The “Network” tab shows API requests, which can help debug tRPC calls.
Vue DevTools: There’s a Vue DevTools browser extension for inspecting Vue component state. You probably won’t need it for this project, but it’s available if you want to explore.
Project Structure
We briefly outline new apps and packages that are added with the starter files for this project.
The web frontend:
apps/local-frontend/
├── src/
│ ├── App.vue # Root Vue component
│ ├── dto.ts # Data Transfer Object definitions
│ ├── main.ts # Application entry point
│ ├── components/ # Shared UI components
│ ├── composables/ # Vue composables (shared reactive state)
│ │ └── localPlayer.ts # Player state management
│ ├── router/ # Vue Router configuration
│ ├── trpc/ # tRPC client setup
│ │ └── client.ts
│ └── views/ # Page-level components
│ ├── MinesweeperView.vue
│ ├── SudokuView.vue
│ ├── SpellingBeeView.vue
│ └── minesweeper/ # Minesweeper-specific components
│ └── sudoku/ # Sudoku-specific components
│ └── spelling_bee/ # Spelling Bee-specific components
The multiplayer room server:
apps/room-server/
├── src/
│ └── server.ts # Room-based multiplayer server
The tRPC backend API (mostly for serving database info):
packages/trpc/
├── src/
│ ├── server.ts # Fastify server setup
│ ├── router.ts # Main router combining all routes
│ ├── trpc.ts # tRPC initialization
│ └── routers/
│ ├── players.ts # Player and autosave endpoints
│ └── puzzles.ts # Puzzle data endpoints
The multiplayer client and some shared types:
packages/multiplayer/
├── src/
│ └── multiplayer.ts # Client library for multiplayer
A generic implementation of the observer pattern:
packages/observer/
├── src/
│ └── observable.ts # Generic Observer Pattern Implementation
Key Technologies
Vue.js
Vue.js is a progressive JavaScript framework for building user interfaces. The frontend uses Vue 3 with the Composition API and Single File Components (.vue files that contain template, script, and styles together). The <script setup> syntax is a compile-time shorthand that reduces boilerplate.
Key concepts you may encounter:
- Reactive State:
ref()andreactive()create data that automatically updates the UI when changed. - Computed Properties:
computed()creates derived values that update automatically. - Lifecycle Hooks:
onMounted()runs when a component is inserted into the DOM;onUnmounted()runs when it’s removed. These are important for resource management - cleaning up observers, timers, and connections.
How your games interact with Vue: The View components extract a view model from your game instance (a plain object representing what to render) and listen for observable events from your game to trigger animations. You shouldn’t need to write much Vue code yourself - the UI components are provided. Your main work is implementing the adapter functions that connect your game logic to the existing Vue code.
See the Vue.js Documentation for more details if you’re curious.
Fomantic UI
Fomantic UI is a CSS framework that provides styled UI components. The frontend uses Fomantic UI classes for buttons, forms, messages, etc. You probably don’t need to modify any Fomantic UI code unless you want to customize the look and feel.
Zod
You’ve already used Zod for schema validation in lab and/or previous projects. It will likely continue to be useful here for parsing/validating data at runtime, particularly as game configuration, state, and actions are passed around between application layers and over a network.
tRPC
tRPC enables type-safe remote procedure calls (RPCs) between client and server. Unlike REST APIs where you define HTTP endpoints and manually ensure types match, tRPC lets you define TypeScript functions on the server that can be called directly from the client with full type safety.
This is a reasonable choice when both frontend and backend are written in TypeScript. In a real-world system with multiple client languages or a public API, you might use a REST API or one of many other more flexible protocols.
You shouldn’t need to do much with tRPC beyond ensuring the provided backend routes are implemented correctly. The routes and schemas are already defined for you - you just need to connect them to your database layer.
Here’s a quick example:
Server side (defining endpoints):
export const playersRouter = router({
get: publicProcedure
.input(z.object({ player_id: z.string() }))
.query(async ({ input }) => {
return await getPlayerById(input.player_id);
}),
});
Client side (calling endpoints):
const player = await trpc.players.get.query({ player_id: '123' });
Task 1: Observer Pattern and Events
The puzzle games need to emit events so that the UI can react to state changes with appropriate animations and feedback.
The Observable Class
We provide a type-safe Observable class in packages/observer/src/observable.ts. It implements the observer pattern with a generic parameter for the observer interface. You can take a look at the source code and examples in the starter files for more details.
Observer Interfaces
Observer interfaces are defined in packages/puzzle/src/puzzle_observer.ts. You may leave them in this file or move them to your puzzle implementation files.
Base Interface (all puzzles should emit these):
interface PuzzleObserver {
onStateChanged?(): void; // Any state change (used for autosave)
onGameFinished?(hasWon: boolean): void; // Game ended
}
Minesweeper-specific events:
onCellRevealed(x, y, details)- When a cell is revealed, with details about contents and effectsonCellFlagged(x, y, isFlagged)- When a cell is flagged/unflaggedonCellsChorded(x, y, cellsRevealed)- When a chord action reveals multiple cells
Sudoku-specific events:
onCellPlaced(x, y, value, isCorrect)- When a number is placedonCellCleared(x, y)- When a cell is clearedonCellFilled(x, y, value)- When a cell is auto-filledonRegionCompleted(cells)- When a row, column, or subgrid is completed correctly
Spelling Bee-specific events:
onWordGuessed(word, result)- When a word is guessed, with evaluation resultonShuffled(order)- When letters are shuffledonHintGiven(hint)- When a hint is provided
Implementation Steps
-
Add the
@repo/observerdependency to your puzzle package (see Setup). - Add an
eventsproperty to each puzzle class:public readonly events = new Observable<MinesweeperObserver>(); - Emit events at the appropriate points in your game logic. For example:
// After revealing a cell this.events.emit('onCellRevealed', x, y, { contents: 'empty', cellsFloodFillRevealed: floodFilledCells, }); this.events.emit('onStateChanged'); - Ensure
onGameFinishedis emitted when the game ends (win or lose).
Important: The web UI depends on the event details. Include all necessary information so observers don’t need to re-implement game logic. For example, when an empty cell triggers flood-fill, include all revealed cells in the event details.
Look for TASK 1 comments in the starter code for additional guidance.
Task 2: tRPC Backend
The web frontend runs in a browser and cannot directly access your database layer. The tRPC backend provides API endpoints that the frontend can call.
Existing Implementation
The starter code includes basic implementations in packages/trpc/src/routers/:
players.ts - Player management:
list- Get all playersget- Get player by IDgetAutosave- Get player’s saved gamesaveAutosave- Save player’s gamecreate- Create new playerupdate- Update player info
puzzles.ts - Puzzle data:
get- Get puzzle by IDlistByDifficulty- List puzzles by difficultyrandom- Get random puzzlerandomByDifficulty- Get random puzzle of specific difficultygetDictionary- Get word list for Spelling Bee
Implementation Steps
-
Review the existing router implementations in
packages/trpc/src/routers/. -
Ensure the
GameDTOSchemais defined correctly at the top ofplayers.ts(see Changelog above for the fix). -
Connect the router endpoints to your database layer functions. The starter code imports from
@repo/database/players- adjust these imports if your database functions are exported differently. - Autosave data format: When loading autosave data from the database, ensure it’s returned in the format expected by the
GameDTOtype (withkind,config, andstatefields). Depending on how you store data in your database:- If stored as a JSON string:
JSON.parse(autosave)to get an object - If already an object: ensure it matches the expected structure
Similarly, when saving, ensure the data is serialized appropriately for your database (likely
JSON.stringify(autosave)). - If stored as a JSON string:
- Test your endpoints by starting the dev server and checking that the frontend can log in, save games, and load games.
Look for TASK 2 comments in the starter code for additional guidance.
Task 3: Puzzle Configuration, State, and Creation
Game data needs to be transferred between different parts of the application: the database (for autosave), the multiplayer system (for syncing between clients), and the frontend (for rendering). This task involves implementing the adapter functions that convert between your internal game representation and the common DTO format.
GameDTO Structure
The GameDTO type (defined in apps/local-frontend/src/dto.ts) represents a saved or synced game:
type GameDTO = {
kind: string; // "minesweeper", "sudoku", "spelling_bee"
config: Record<string, unknown>; // Game configuration
state: Record<string, unknown>; // Current game state
};
Key Considerations
The main question is where parsing and validation happen in your code:
- Option A: An internal memento pattern with encapsulated serialization
- Option B: Dedicated classes/abstractions for config/state
- Option C: Zod schemas or equivalent validation at parsing boundaries
Any of these approaches can work. The important thing is that the adapter functions in the View files correctly convert between your representation and the DTO structure, and that the tRPC backend correctly handles the DTO format for autosave.
Implementation Steps
In each puzzle View file (e.g., SudokuView.vue), you’ll find functions marked with TASK 3 comments. Implement these to work with your game classes:
getGameDTO(game)- Extract current game data as a DTOparseAndCreateGameFromDTO(dto)- Create a game instance from a DTOcreateNewGame(config)- Create a new game from configuration
See the detailed comments in apps/local-frontend/src/dto.ts and the View files for specific guidance.
Configuration Modals
Each puzzle has a configuration modal component (e.g., MinesweeperConfigModal.vue) that collects game settings from the user. The provided modals include form fields and UI, but you may need to add or modify code to match your config schema. Look for TASK 3 comments in the config modal files for guidance.
For example, if your Minesweeper config uses a different structure for specifying the number of mines or items, adjust the parseConfig or handleStartGame function accordingly.
Task 4: Puzzle Game Integration
Each puzzle has a View component (MinesweeperView.vue, SudokuView.vue, SpellingBeeView.vue) that connects your game logic to the UI.
Key Functions to Implement
State Conversion:
// Convert game instance to view model for rendering
function toViewModel(game: GameType): ViewModelType {
// Map your internal state to the view model structure
// defined in the corresponding Board component
}
Action Handling:
// Apply an action to the game
function applyAction(game: GameType, action: ActionType) {
// Call your action function, handle invalid actions gracefully
}
// Parse action from network DTO
function parseActionFromDTO(dto: ActionDTO): ActionType {
// Parse and validate using your action representation
}
Verifying Action Format
Search each View file for submitAction calls. These create action objects that are sent to your game. The provided code creates actions in a specific format - verify that this matches your game’s expected action representation.
For example, the Sudoku View creates actions like this:
submitAction({ action_kind: 'place', params: { x: col, y: row, guess: num } });
submitAction({ action_kind: 'clear', params: { x: col, y: row } });
submitAction({ action_kind: 'fill', params: { x: col, y: row } });
If your game uses a different action format, update this code to produce the right shape of data for your implementation..
Sudoku: Clear Action
The provided Sudoku Vue interface includes a button to clear a number previously placed in a cell. If your Sudoku implementation doesn’t have a “clear” action yet, add one now. You don’t necessarily need to add this to your separate terminal-based interface from previous projects.
Usage: clear x y
Behavior:
- May only be applied to a non-starter cell that has a number placed.
- Removes the number, returning the cell to empty.
Look for TASK 4 comments in the View files for additional guidance.
Task 5: Error Handling and Resource Management
Error Handling
The provided starter code does not handle errors correctly in all cases. In other cases, error handling code is missing (or is part of code that you need to fill in). You should explore the codebase and identify places where you can improve or add error handling. This could include code you add, but also parts of the provided starter code. Here are a few specific suggestions:
-
Save/Load: If the tRPC backend fails to save or load.
-
Configuration: If a user enters an invalid configuration for a game and attempts to start.
-
State/Action parsing: Game states or actions might be malformed, especially in multiplayer scenarios. For example, a player might attempt to join a Spelling Bee game room from the Sudoku page.
-
Puzzle loading: Loading puzzles from the Sudoku database or loading the spelling bee dictionary could fail.
-
Puzzle actions: What if the game UI sometimes generates an invalid action (e.g. it may not prevent attempting to click invalid cells), or what if in a multiplayer scenario a submitted action is no longer valid because another player’s action got in first?
Resource Cleanup Bug
There’s also one significant “resource” management bug in the provided starter code. We won’t tell you exactly what it is, but here’s a related bug report from one of our early access play testers. Your task is to investigate, identify the root cause, and implement a fix. (The fix will not involve writing a lot of code.)
Bug Report #427: Autosave keeps getting overwritten after leaving game
Reported by: Susan Doku
Severity: High
Component: Autosave
Description:
I was playing multiplayer with friends for a while in some shared games, but I decided to switch over to play Spelling Bee on my own. I got about halfway through all the words and decided to take a break. Before I left, I double checked on the Home page that it was showing a save for Spelling Bee, but when I came back a bit later and clicked to resume the game it took me to some random Sudoku game! I was honestly a bit upset.
Steps to Reproduce:
- Log in and play through a few Minesweeper and Sudoku games with friends.
- Play spelling bee on my own.
- Make a lot of really nice guesses and get pretty far through.
- Go back to the Home page, double check my autosave is there.
- Step away for a well-earned break.
- Come back and click “Resume Game” for Spelling Bee.
Finally earn the title of “Queen Bee” after all these years!- Wat. This is not my Spelling Bee game!
Expected Behavior: Have fun playing spelling bee.
Actual Behavior: Cry. My spelling bee is gone and I have no idea where it went.
Task 6: Multiplayer Sync
The multiplayer room server provides basic functionality for creating rooms and broadcasting actions between clients. However, the current implementation doesn’t handle concurrency in a fully robust way - clients could potentially process messages out of order or miss messages entirely.
We will address this in lab on Friday, April 17. No work is required on this task outside of lab.
Deliverables Submission
Before submitting, update the README.md file in your repository with:
- Run instructions: How to run the web frontend.
- Project description: Brief description of your implementation.
- AI disclosure: How your group used AI tools (if at all).
Submit via GitHub release:
- Ensure all code is committed and pushed.
- Go to your repository’s “Releases” section on GitHub.
- Click “Draft a new release”.
- Create a new tag:
p4-initial-deliverables. - Set the title: “Project 4: Initial Deliverables”.
- Click “Publish release”.
Make sure to create your release before 11:59pm on the deadline.
Revisions
There is no separate revision submission for this project. Revisions will be incorporated into subsequent project submissions.