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:

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:

The web frontend includes support for:

Prerequisites

This project continues from your existing group project codebase. From previous projects, you should already have:

  1. All three puzzles working with core game logic decoupled from I/O.
  2. The three new actions from Project 3 (chord, hint, fill).
  3. Minesweeper special items (shield, smoke bomb, scanner).
  4. 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:

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:

  1. Get it to launch: Run pnpm dev and verify you can open the frontend in a browser without immediate crashes.
  2. 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.
  3. 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:

  1. Open developer tools: Press F12 (or right-click and select “Inspect”) to open the browser’s developer tools panel.
  2. 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.
  3. Use debugger; statements: You can also add debugger; 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:

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:

Sudoku-specific events:

Spelling Bee-specific events:

Implementation Steps

  1. Add the @repo/observer dependency to your puzzle package (see Setup).

  2. Add an events property to each puzzle class:
    public readonly events = new Observable<MinesweeperObserver>();
    
  3. 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');
    
  4. Ensure onGameFinished is 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:

puzzles.ts - Puzzle data:

Implementation Steps

  1. Review the existing router implementations in packages/trpc/src/routers/.

  2. Ensure the GameDTOSchema is defined correctly at the top of players.ts (see Changelog above for the fix).

  3. 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.

  4. Autosave data format: When loading autosave data from the database, ensure it’s returned in the format expected by the GameDTO type (with kind, config, and state fields). 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)).

  5. 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:

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:

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:

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:

  1. Save/Load: If the tRPC backend fails to save or load.

  2. Configuration: If a user enters an invalid configuration for a game and attempts to start.

  3. 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.

  4. Puzzle loading: Loading puzzles from the Sudoku database or loading the spelling bee dictionary could fail.

  5. 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:

  1. Log in and play through a few Minesweeper and Sudoku games with friends.
  2. Play spelling bee on my own.
  3. Make a lot of really nice guesses and get pretty far through.
  4. Go back to the Home page, double check my autosave is there.
  5. Step away for a well-earned break.
  6. Come back and click “Resume Game” for Spelling Bee.
  7. Finally earn the title of “Queen Bee” after all these years!
  8. 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:

  1. Run instructions: How to run the web frontend.
  2. Project description: Brief description of your implementation.
  3. AI disclosure: How your group used AI tools (if at all).

Submit via GitHub release:

  1. Ensure all code is committed and pushed.
  2. Go to your repository’s “Releases” section on GitHub.
  3. Click “Draft a new release”.
  4. Create a new tag: p4-initial-deliverables.
  5. Set the title: “Project 4: Initial Deliverables”.
  6. 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.