p2-combined-puzzles
EECS 498 APSD Project 2: Combined Puzzle Games
Feature Demo: Due in Lab Fri Feb 6.
Project Deliverables: Due Fri Feb 6 at 11:59pm.
Changelog
We’ll record updates and fixes to the project specification here.
Thu, Jan 29 2:50pm - Starter Files Bugfix
Two mistakes were found in the starter files:
- The
@repo/randompackage was missing as a dependency in thepuzzlepackage. - The .ts source files in
randomshould be within asrc/directory within the package rather than at the top level.
We submitted a pull request to each of your repositories to fix these issues. Take a look at the “Pull Requests” section of your repository on GitHub.
Sat, Feb 1 2:00pm - Additional Database Details
We’ve added more details about using the database in this project, including references to the relevant portions of the lab 4 database tutorial.
Sat, Feb 1 11:00pm - Clarification on Third-Party Libraries
We’ve clarified that you’re welcome to use third-party libraries in this project, subject to certain guidelines. See the “Third-Party Libraries” section for details.
Introduction
Welcome to Project 2! In this project, you and your group will combine your implementations from Project 1 into a single codebase and implement a few additional features:
- A unified command-line interface.
- Player accounts and login.
- Save/resume functionality.
You’ll also learn several new tools and techniques:
- A “monorepo” project structure.
- Basic database usage with SQLite and Kysely.
- Writing automated tests with Vitest.
Your Group Project Repository
We’ve provisioned a GitHub repository for your project group that you’ll use for this project and throughout the rest of the term. (This is not the same repository as you used for project 1.)
Find your group repository at: https://github.com/orgs/eecs498-software-design/repositories.
Setup
Prerequisites
The following are prerequisites you’ll already have installed from project 1. We’ve included them here for completeness.
Basic command-line tools from EECS 280 Setup
Follow the Windows+WSL or Mac instructions.
Visual Studio Code (recommended editor)
Download from code.visualstudio.com
Node.js (version 24 or higher) and npm
Follow the instructions at https://nodejs.org/en/download.
- For Mac, select “macOS”, “nvm”, and “npm”.
- For Windows+WSL, select “Linux” (not Windows), “nvm”, and “npm”.
- For Linux, select “Linux”, “nvm”, and “npm”.
Install pnpm
We’ll use pnpm (“performant npm”) for the remaining projects instead of npm. It turns out that pnpm is much better suited for monorepo projects than npm.
Ironically, you can install pnpm using npm. Run the following command in your terminal:
npm install -g pnpm@latest-10
Bye bye npm. We know you probably just got used to typing npm everywhere… sorry about that. If you try to install dependencies and the terminal just hangs for a while, you may have accidentally used npm instead of pnpm. Just stop the command (Ctrl+C) and try again with pnpm. We’ve configured the project’s package.json files so that hopefully catches this with an error in most cases.
Clone Your Repository
Find your project repository at: https://github.com/orgs/eecs498-software-design/repositories
If you haven’t already, clone your repository to your local machine using git:
$ git clone <your-repository-url>
Open the cloned repository folder in VS Code:
$ code group-project-<yourteamnumber>
We recommend working in the integrated VS Code terminal.
Install Dependencies
After cloning, install dependencies by running the following at the top-level of the project. Make sure to use pnpm, not npm:
$ pnpm install
Note: If you see TypeScript errors in VS Code after installing dependencies (e.g., red squiggles in tsconfig.json files or unresolved imports), try restarting the TypeScript server. Open the command palette (Ctrl+Shift+P or Cmd+Shift+P on Mac), search for “TypeScript: Restart TS Server”, and select it.
Project 1 Files
Your group will need to combine your individual Project 1 puzzle implementations into this repository. Copy your puzzle source files into the appropriate subdirectory within packages/puzzle/:
- Sudoku files go in
packages/puzzle/sudoku/ - Minesweeper files go in
packages/puzzle/minesweeper/ - Spelling Bee files go in
packages/puzzle/spelling_bee/
You may need to refactor your code to work within this structure. In particular, you’ll need to update import paths and remove any direct dependencies on node APIs (see “Decoupling Puzzles from I/O and node” below). You’re also welcome to use a different structure if you like.
Since each team member wrote their puzzle independently, you may have different coding styles or approaches. Take some time to discuss and agree on conventions (e.g., naming, error handling, how configuration is passed in). You might also find opportunities to share code. But, only do so where it provides some benefit (i.e. better abstraction, more favorable coupling) and doesn’t add unnecessary complexity.
Project Organization
We’ll cover the essential structure and configuration of the project repository.
For a refresher on package.json, dependencies, tsconfig.json, and ES module imports/exports, see the Project 1 Specification.
Monorepo Structure
This project is organized as a monorepo: a single repository that contains multiple packages and applications.
The essential idea is that shared code is extracted into separate packages (located in the packages/ folder), while individual applications live in the apps/ folder. Each package and app has its own package.json that specifies its dependencies, including dependencies on other packages in the same repository via the workspace:* syntax.
p2-group-<teamname>/
├── apps/
│ └── local-cli/ # Command-line application
│ ├── package.json
│ └── src/
├── packages/
│ ├── database/ # Database access layer
│ │ ├── package.json
│ │ └── src/
│ ├── puzzle/ # Shared puzzle game logic
│ │ ├── package.json
│ │ └── ...
│ ├── random/ # Randomization library
│ │ ├── package.json
│ │ └── src/
│ └── typescript-config/ # Shared TypeScript configuration
│ └── package.json
├── package.json # Root package.json (monorepo config)
├── pnpm-lock.yaml # Lockfile with exact dependency versions
├── pnpm-workspace.yaml # Workspace configuration for pnpm
├── turbo.json # Turborepo configuration
└── vitest.config.ts # Vitest test runner configuration
The monorepo structure is managed by pnpm workspaces, which allow packages to depend on each other without needing to publish them to a package registry. For example, the local-cli app has a dependency on the @repo/database and @repo/puzzle packages, specified in its package.json:
"dependencies": {
"@repo/database": "workspace:*",
"@repo/puzzle": "workspace:*",
}
The workspace:* syntax tells pnpm to look for the @repo/database package within the monorepo workspace rather than downloading it from npm.
Turborepo is a build system for monorepos that helps coordinate tasks (like building or testing) across packages. It’s configured in turbo.json. You don’t need to understand its details for this project, but if you’re curious, you can read more at https://turborepo.dev/docs.
Packages and Apps
Here’s a summary of each package and app in the repository:
| Package/App | Description |
|---|---|
apps/local-cli |
The main command-line application that provides a unified interface for playing puzzle games. |
packages/database |
Database access layer using SQLite and Kysely. Exports functions for managing player data. |
packages/puzzle |
Shared puzzle game logic. Contains core implementations for Sudoku, Minesweeper, and Spelling Bee. |
packages/random |
Randomization library (same as randomization.ts from project 1, but now as a separate package). |
packages/typescript-config |
Shared TypeScript compiler configuration that other packages extend. |
Package Exports
Packages explicitly declare what they export in their package.json file via the exports field. For example, the @repo/database package exports a players module:
"exports": {
"./players": "./src/players.ts"
}
This means other packages can import from @repo/database/players:
import { getPlayerById, insertPlayer } from "@repo/database/players";
Similarly, the @repo/random package exports its main module:
"exports": {
".": "./src/randomization.ts"
}
So you can import from it directly:
import { Randomizer } from "@repo/random";
You’ll need to update package.json in packages/puzzle to export your puzzle implementations so that local-cli can import them. You might choose to implement individual puzzles, a factory, or something else. There are several valid approaches.
Running Your Code
Running the Local CLI
To run the local CLI application, use the following command from the root of the repository:
$ pnpm start:local-cli
This runs the start script defined in apps/local-cli/package.json, which executes src/main.ts via tsx.
Running Tests with Vitest
This project uses Vitest as its test runner. Vitest is pretty complex, so we’ll highlight the main uses cases for this project.
To run all tests once:
$ pnpm test:run
One of the main benefits of using a modern test runner like Vitest is that you can run tests in “watch mode”, meaning it will detect changes to your source files and automatically re-run tests (i.e. make an edit, hit save, and the tests rerun):
$ pnpm test:watch
Even better, you can run tests though a browser-based UI that provides a nice interface and several other features, such as code coverage or a dependency graph:
$ pnpm test:ui
Debugging with VS Code
To debug your code in VS Code, use the JavaScript Debug Terminal instead of configuring launch.json.
- Open the command palette (
Ctrl+Shift+PorCmd+Shift+Pon Mac). - Search for and select “JavaScript Debug Terminal”.
- In the debug terminal that opens, run your code with
pnpmas usual (e.g.,pnpm start:local-cliorpnpm test:run). - The debugger should attach automatically and pause on any errors or breakpoints you’ve set.
This approach works for running the local-cli app as well as running tests with Vitest. If you’re debugging tests, you may need to add the --test-timeout=0 flag to avoid timeout issues when paused at a breakpoint, for example:
$ pnpm test:ui --test-timeout=0
If you run with pnpm test:watch or pnpm test:ui, Vitest will automatically re-run tests when you save files, so you can edit code, save it, and it will run right back to your breakpoint.
Decoupling Puzzles from I/O and node
The puzzles shouldn’t be directly coupled to command-line arguments, user input/output, or the file system. Eventually, our plan is that the puzzle logic could be used in different environments (e.g., a browser-based web app, on a server, etc.). We would also like to be able to test the puzzle logic in isolation.
In the packages/puzzle package, we’ve intentionally configured package.json not to include node libraries. If you look at your puzzle code, you’ll see some lines highlighted with red squiggles. These indicate code that was previously coupled to node (e.g., reading files, parsing command-line arguments). Any code that handles these things will need to move to the local-cli app (which we are ok with assuming will always be run via node).
Writing Puzzle Tests
You should write automated tests for your puzzle implementations using Vitest. Tests don’t need to be exhaustive - it would be arduous to write tests for every possible game scenario - but you should test the core logic of each puzzle. We also want you to have the experience of using an industry-strength testing framework.
Focus on testing things like:
- Valid and invalid moves
- Win/lose conditions
- Edge cases in game logic
When writing tests for your puzzle implementations, focus on testing the core logic of the puzzles without involving any input/output or file system operations.
Test Structure
Vitest tests are organized using describe blocks (to group related tests) and it blocks (for individual test cases). Use expect to make assertions about your code’s behavior. Each takes in a lambda function that contains the test logic, which is used as a callback to be executed when the test runs.
Here’s a simple example:
import { describe, it, expect } from "vitest";
describe("math", () => {
it("adds numbers correctly", () => {
expect(2 + 2).toBe(4);
});
it("subtracts numbers correctly", () => {
expect(2 - 2).toBe(0);
});
});
The expect function uses a fluent syntax (also called method chaining), where you call a series of methods on the result. You start with expect(value), then chain a matcher method like .toBe(), .toEqual(), or .toThrow():
expect(cell.isRevealed()).toBe(false); // Check for exact identity (===)
expect(result).toEqual({ x: 1, y: 2 }); // Check for deep equality (objects/arrays)
expect(() => doSomething()).toThrow(); // Check that a function throws an error
You can also add .not before the matcher to negate it:
expect(cell.isRevealed()).not.toBe(true);
When checking if a value is defined, use expect.assert(value) to narrow the type for subsequent code. This is useful when a function might return undefined:
const player = await getPlayerById("abc123");
expect.assert(player); // Asserts player is defined and narrows type
expect(player.display_name).toBe("Alice"); // Now TypeScript knows player is not undefined
For a full list of matchers, see the Vitest expect documentation.
Test files should be named with a .test.ts suffix (e.g., sudoku.test.ts) and placed alongside the code they test (or in a dedicated tests directory). When you run pnpm test:run, pnpm test:watch, or pnpm test:ui, Vitest will automatically find and run all test files.
Command-Line Puzzle Collection
Your application should be an “all-in-one” puzzle collection with a unified command-line interface in apps/local-cli.
As it is configured right now, running pnpm start:local-cli will launch the application written in apps/local-cli/src/main.ts. You’re welcome to adjust this if you like.
On your first pass through the project, we suggest you focus on getting the puzzles working first, then add save/load functionality after.
When run, the basic flow is:
- Login: The user logs in with their player ID or creates a new account.
- Main Menu: The user sees a main menu with options to start a new game, resume a saved game, or quit.
- Puzzle Selection: If starting a new game, the user chooses which puzzle to play (Sudoku, Minesweeper, or Spelling Bee).
- Configuration: The user configures the puzzle (e.g., difficulty level for Sudoku, grid size and mine count for Minesweeper, letter set for Spelling Bee).
- Gameplay: The user plays the puzzle until they win, lose, or quit.
- Return to Menu: After the game ends, the user returns to the main menu and can play again or exit.
There is no logout option. To switch users, quit the application and restart.
Here’s an example of what the login screen might look like:
Welcome to Puzzle Games!
[1] Log in
[2] Create new account
[q] Quit
Enter choice: 1
Enter your player ID: eddiecat
Welcome back, Eddie the Cat!
Players have both a player ID (used for login, must be unique) and a display name (shown in the UI). When creating a new account, prompt for both.
After logging in, the main menu might look like:
What would you like to do?
[1] New Game
[2] Resume Game (Sudoku)
[q] Quit
Enter choice:
And a puzzle selection menu:
Choose a puzzle:
[1] Sudoku
[2] Minesweeper
[3] Spelling Bee
[b] Back
Enter choice:
Note: The exact appearance and flow of the menus are up to you. It doesn’t need to be fancy, as long as it it functional. It’s ok if you’re still just printing lines to the terminal for the interface.
If you would like, strategic use of console.clear() can go a long way toward keeping the interface clean, but it’s not required. Doing this makes for a nicer interface but is kind of annoying if you’re using console.log() for debugging, since it may clear your debug outputs. If you’d like to temporarily disable console.clear(), you can override it by adding something like this at the top of your main file:
console.clear = () => console.log("console.clear() ignored");
Working with JSON and Plain Objects
When passing configuration or game state around your code, you’ll often use plain JavaScript objects. For example, a Sudoku configuration might look like:
const config = {
difficulty: 3,
puzzleData: "53..7....6..195..."
};
This is just a regular object - you can pass it directly to a constructor or function. There’s no need to convert it to a string.
JSON (JavaScript Object Notation) is a string format for representing data. It looks similar to JavaScript object syntax, but it’s text:
const jsonString = '{"difficulty":3,"puzzleData":"53..7....6..195..."}';
This is sometimes called a serialized format, because it’s a way to represent structured data in a linear or “serial” format, like a string. You only need JSON strings when handling external input/output (e.g. to a file, a database, over a network, etc.).
To convert between objects and JSON strings:
// Object → JSON string (for storage)
const jsonString = JSON.stringify(config);
// JSON string → Object (when loading)
const config = JSON.parse(jsonString);
Note: JSON.stringify() and JSON.parse() only work with data that can be represented in JSON. This includes objects, arrays, strings, numbers, booleans, and null. It does not include functions, undefined, or special types like Date or Map.
For this project, you’ll likely use JSON.stringify() when saving game state to the database and JSON.parse() when loading it back. But when passing data between functions within your code (e.g., from the CLI to a puzzle constructor), you may choose to can likely just use regular objects directly.
Autosave and Resume
Your application should automatically save the game state after every move. This means that even if the user forcefully terminates the application (e.g., with Ctrl+C), they can resume their game when they restart.
Each player can only have one saved game at a time (one total - not one per puzzle). When the player starts a new game, any existing saved game is replaced. When the player wins or loses a game, the saved game is cleared.
From the main menu, if there is a saved game for the current player, display a “Resume Game” option that shows which puzzle it is, e.g. “Resume Game (Sudoku)”. If there is no saved game, you can omit this option or disable it.
The game state should be saved to the database, not to the file system. See the Database section below for tips. You’ll need to store enough information to fully restore the game state, including:
- Which puzzle type it is
- The puzzle configuration (difficulty, grid size, letters, etc.)
- The current state of the puzzle (moves made, cells revealed, words found, etc.)
Accepting Configuration via Parameters
As described above, puzzle classes should accept configuration information via constructor parameters or method arguments rather than reading command-line arguments or files directly. The local-cli app is responsible for:
- Prompting the user for configuration options
- Reading necessary files (puzzle data, word lists, etc.)
- Creating puzzle instances with the appropriate configuration
- Handling user input and output during gameplay
- Saving and loading game state to/from the database
Hint: You don’t necessarily need a single common interface for all puzzles. It’s fine for the CLI to have separate code paths that “know” about each puzzle type and how to configure it, run it, and save/restore it. That said, if you find common patterns, you’re welcome to abstract them.
Running the Application
To start the application, simply run:
$ pnpm start:local-cli
The login screen and interactive menus will guide the user from there.
Database
The project uses SQLite as a lightweight, file-based database and Kysely as a type-safe query builder. You don’t need to write raw SQL - Kysely provides a typed “fluent interface” that is essentially a safer form of SQL.
In lab 4, we covered essential concepts related to using a database in this project, including schemas, migrations, queries, seeds, and a database layer in your overall application. The lab describes where database-related files live in this repository and how to import/export from your database package to your main app.
The lab 4 tutorial is available here: https://eecs498-software-design.github.io/lab04/database.html
Database Workflow
We’ve copied the essential workflow from the lab 4 tutorial here for convenience.
Whenever you need to change your database schema, follow this workflow:
- Create a migration - Create a new, sequentially numbered migration file.
- Write the
upanddownfunctions - Define the changes to apply and how to revert them. - Run the migration -
pnpm db:migrate - Regenerate types -
pnpm db:generate - Update your database layer - Add or modify functions in your layer files.
If you realize that you made a mistake in a migration that hasn’t been committed or applied beyond your local dev database yet, you can revert it with pnpm --filter database db:migrate:down, fix the migration file, and then run pnpm db:migrate again. Afterward, don’t forget to regenerate types with pnpm db:generate. If the migration has already been committed or applied to other databases, it’s better to create a new “forward” migration that corrects the mistake.
Adding Sudoku Puzzles to the Database
Lab 4 includes a walkthrough of adding sudoku puzzles to the database (rather than reading in from a file) - make sure to incorporate this into your project 2 implementation. Your main application should import the required functionality from the database package to load Sudoku puzzles from the database when configuring a new Sudoku game. (Your core Sudoku puzzle code in the puzzle package should not depend on the database package.)
Player Accounts
For managing player accounts through the database, refer to the relevant starter files. These contain much of what you’ll need.
packages/database/src/migrations/001_template.ts: A migration to create theplayerstable.packages/database/src/players.ts: Database layer functions for player accounts.apps/local-cli/src/db-example.ts: A basic example that imports and uses the player database layer functions.
You should also create a seed script for player data, similar to the Sudoku seed script provided in lab 4. This will help you test your implementation by pre-populating the database with some sample players.
Save/Resume Functionality
To implement autosave and resume functionality, you’ll need to add a migration that updates the database schema to include information about saved games.
There are a few options for how to store the saved game state. One straightforward approach is to serialize the game state as a JSON string (using JSON.stringify()) and store it in a single column. Then, use JSON.parse() to deserialize it after retrieving from the database. The final section of the lab 4 database tutorial addresses this.
You may either add a new column to the existing players table or create a new saved_games table that links to players via a foreign key. The former option is simpler, but is fundamentally restricted to a single save per player. The latter is more complex, but would in principle allow multiple games to be stored per player. For project 2, either approach is fine, and we won’t penalize in grading for the simpler option.
Third-Party Libraries
You’re welcome to use third-party libraries in your project, as long as:
- They are generally compatible with the given project environment and structure (e.g. TypeScript,
node, SQLite, etc.). - They do not implement large portions of the project’s application for you (e.g. don’t import a 3rd-party Sudoku implementation or a user management library)
- They do not require significant changes to the project structure or build process.
- They do not involve esoteric or complex paradigms or coding practices. We need to be able to read and understand your code with a minimal learning curve.
- They do not dramatically alter the learning objectives of the project.
Along these lines, please do not introduce additional “frameworks” that differ substantially from the given project structure. e.g. Don’t switch to a different test runner, database ORM, or UI framework.
If you’re not sure, please reach out and ask us!
You should add dependencies only to the package that needs them. For example, to add a dependency to the puzzle package, run the following command from the root of the repository (where <dependency-name> is the name of the dependency you want to add):
$ pnpm add <dependency-name> --filter puzzle
If you want to remove a dependency, likewise use:
$ pnpm remove <dependency-name> --filter puzzle
Finally, make sure to think about whether a dependency should be added to a particular package. For example, if you’re considering a library like colors for colored terminal output, it should probably go in the local-cli app rather than the puzzle package, since the puzzles shouldn’t depend on terminal-specific functionality. (This aligns with moving I/O code out of the core puzzle implementations.)
Feature Demo
During lab on Friday, Feb 6, your group will demonstrate the key features of your project. Be prepared to show:
- Menu navigation: Launch the application and demonstrate navigating between menus (main menu, puzzle selection, configuration). It’s OK if your menu structure is different from the examples above, as long as it supports the required functionality.
- All three puzzles working: Configure and play each of Sudoku, Minesweeper, and Spelling Bee briefly to show they are functional. Show that winning, losing, or quitting returns to the main menu.
- Player accounts: Create a new player, log in, and show that the player account persists on subsequent runs of your application. There’s no password authentication, and we haven’t specified any particular “log out” functionality, so nothing is required there.
- Autosave/resume: Log in, start a puzzle, make some moves, forcefully quit with
Ctrl+C, then restart and resume from where you left off. It’s OK if you chose to do something slightly different or more complex than the simple autosave/resume functionality we specified, as long as the essential nature of the feature is present and saves are made to the database.
Complete implementations receive full credit for the feature demo. Minor imperfections (e.g., small formatting issues) are not penalized.
Implementations with any missing features or incorrect behavior receive at most 80% for the feature demo.
Implementations that are substantially incomplete may receive at most 50% for the feature demo.
Initial Deliverables Submission
Before submitting, update the README.md file in your repository with the following:
- Run instructions: Provide clear instructions for how to run your code.
- Project description: A brief description (no more than a paragraph) of your project organization and any significant updates since the last submission. We’re not grading the description directly, but it can help us more quickly navigate and understand your codebase.
- Third-party libraries: List any notable third-party libraries you used, other than those provided with the project distribution.
- AI disclosure: Document how your group used (or didn’t use) AI coding tools. A sentence or short paragraph is fine. For example: “We didn’t use any AI tools”, “We used AI tools for brainstorming but not coding”, “We used Copilot suggestions throughout the codebase”, or “We used Claude Opus 4.5 in VS Code Chat to help refactor the puzzle classes and generate test cases”. Remember that using AI tools is not prohibited, and we won’t grade your code differently based on AI usage. However, it’s helpful for us to understand how students are using these tools.
Your group will submit your project via a GitHub release. This allows you to tag a specific commit as your final submission.
- Ensure all your code is committed and pushed to your repository.
- Go to your repository on GitHub.
- Click on “Releases” in the right sidebar (or navigate to
https://github.com/eecs498-software-design/<your-repo>/releases). - Click “Draft a new release”.
- Create a new tag for the release named
p2-initial-deliverables. - Set the release title to “Project 2: Initial Deliverables”.
- You don’t need to put anything specific in the release description.
- Click “Publish release”.
Make sure to create your release before 11:59pm on the deadline.
Revisions Submission
After receiving feedback on your initial submission, your group will have the opportunity to make revisions and improvements. We’ll provide specific instructions on how to submit your revised project once the revision period begins. (It will involve creating another GitHub release similar to the initial submission process.)