EECS 498 APSD P1
Project 1: Single Puzzle Game
Project Deliverables: Due Fri Jan 23 Mon Jan 26 at 11:59pm via pull request on your provisioned GitHub repository.
Feature Demo: Due in Lab either Fri Jan 23 or Fri Jan 30.
Changelog
We’ll record updates and fixes to the project specification here.
Fri, Jan 16 10:30am
- Fixed number of valid words and points possible in the spelling bee example.
- Added note about unintentional duplicate word
"chaplain"in words.txt. - Added references with examples of user input and command-line arguments.
Sat, Jan 18 2:00pm
- Added a reference for provided randomization library.
Mon, Jan 19 12:30pm
- Added a reference with an example of reading an input file.
Mon, Jan 19 3:30pm
- Added a reference on error handling and user input validation.
Tue, Jan 20 12:00am
- Updated the
prompt-syncexample to create thePromptSyncinstance withsigint: true, which allows Ctrl-C to interrupt and terminate the program as expected.
Wed, Jan 21 10:00pm
- Updated submission instructions.
Introduction
Welcome to Project 1! In this project, you will implement a puzzle game in TypeScript with a simple, terminal-based interface.
You have three puzzle options to choose from:
-
Sudoku
Complete a grid of numbers so that each column, row, and block contain the unique digits from 1 to 9. -
Minesweeper
Reveal all cells in a grid without hitting any hidden mines. Each revealed cell shows the number of neighboring mines as a clue. -
Spelling Bee
Earn points by spelling words using a set of 7 letters. Your words must contain the “center” letter and you earn extra points for using all letters in a word.
Choose only one puzzle to implement. When you begin working with a group of 3 for future projects, you will combine codebases to make the complete set of puzzles.
Your Project 1 Repository
We’ve provisioned a GitHub repository for you to use for this project. First, you’ll need to join the course GitHub organization. We’ve sent an invite via your @umich.edu email. If you have added the course recently and did not receive an invitation, please contact the course staff.
In order to accept the invitation, you’ll need a GitHub account associated with that email address. If you don’t have such an account, try going to github.umich.edu. Or, you can add your @umich.edu email to an existing GitHub account at https://github.com/settings/emails.
Accept the emailed invitation. If you can’t find the invitation in your email, try visiting https://github.com/orgs/eecs498-software-design/invitation in a web browser while logged into GitHub.
Once you join the organization, you’ll automatically be added to a GitHub “team” named student-<youruniqname>, which we use to grant you access to provisioned repositories for course projects. You don’t need to do anything with this, but don’t leave the team or organization or you’ll lose access to your repositories.
Your project 1 repository is named p1-student-<youruniqname>. It should be listed at: https://github.com/orgs/eecs498-software-design/repositories.
Setup
Prerequisites
Make sure you have installed the following (if you completed the lab 1 exercises, you should be all set).
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”.
Clone Your Repository
Find your project repository at: https://github.com/orgs/eecs498-software-design/repositories
Clone your repository to your local machine using git:
$ git clone <your-repository-url>
Open the cloned repository folder in VS Code:
$ code p1-student-<youruniqname>
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:
$ npm install
Project Organization
We’ll cover the essential structure and configuration of the project repository.
You’ll find the following files and folders:
p1-student-<youruniqname>/
├── .vscode/
│ └── launch.json # VS Code debugging configurations
├── node_modules/ # Installed dependencies
├── src/
│ ├── main.ts # Example program entry point
│ ├── util.ts # Provided utility functions
│ ├── randomization.ts # Provided randomization library
│ └── ... # Your source files go here
├── package-lock.json # Exact dependency versions (auto-generated)
├── package.json # Project configuration and dependencies
├── README.md # Project readme (update before submitting)
├── sudoku_puzzles.txt # Sudoku puzzle data (if you choose Sudoku)
├── tsconfig.json # TypeScript compiler configuration
└── words.txt # Word list data (if you choose Spelling Bee)
package.json
node is a JavaScript runtime environment and npm is its package manager.
In a node/npm project, a package.json file contains metadata, configuration,
scripts, and a manifest of dependencies. You probably don’t need to edit this file
for this project, but if you’re curious there’s more info at
https://nodesource.com/blog/the-basics-of-package-json.
The package.json file is encoded in JSON. If you haven’t encountered JSON before, it’s basically a string representation of the same kind of data you can store in a javascript object. It’s commonly used for configuration files and data interchange because it’s both human-readable and easy for programs to parse.
The package-lock.json file is a lockfile that tracks the exact versions of dependencies installed in your project. The basic idea is to ensure consistency across different installations over time and in different environments. Don’t edit package-lock.json manually.
Dependencies
The dependencies and devDependencies properties of package.json list the packages your project depends on. When you ran npm install earlier, these dependencies (and their dependencies, and so on, recursively) were installed into the node_modules/ folder. This is a “local” installation for this project, not a global installation on your machine.
We’ve already included several dependencies for you:
tsx- runs TypeScript code onnodewithout a separate compilation steptypescript- The TypeScript compiler and language toolsprompt-sync- A library for reading user input from the terminalcolors- A library for printing colored text output to the terminalseedrandom- A dependency of therandomization.tslibrary we provide for you@typespackages - 3rd-party TypeScript type definitions for libraries originally written in JavaScript
You may install additional dependencies if you want, but you probably don’t need to for this project. To install a new dependency, you would run:
$ npm install <package-name>
Or, if it’s a development-only dependency (like @types type definitions) not actually used at runtime, you would run:
$ npm install --save-dev <package-name>
These commands add the package to your node_modules/ folder and update package.json and package-lock.json accordingly. To remove a package, run npm uninstall <package-name>.
tsconfig.json
A tsconfig.json file is used to customize what errors the TypeScript compiler checks for and also to configure the target JavaScript version and module system.
We recommend you do not modify this file, at least not for this project.
Running Your Code
Running from the Terminal
To run a TypeScript file, use npx tsx followed by the path to your file. For example, try this:
$ npx tsx src/main.ts
Briefly, the npx command runs locally installed tools, such as tsx, which is a convenient tool that allows running TypeScript programs on node without a separate compilation step.
Debugging in VS Code
The VS Code debugger is configured via the launch.json file, found in the hidden .vscode folder. We’ve included a starter launch.json file for you with two configurations:
- Debug Current File (tsx) - Debug whatever TypeScript file you currently have open in the editor
- Debug EXAMPLE (tsx) - Debug a specific file. You’ll need to replace “EXAMPLE” in the config.
For example, try setting a breakpoint somewhere in src/main.ts and use either of the configurations to start debugging (open the debug panel, select the configuration, and click the green “play” button). You can step through your code, inspect variables, explore the call stack, etc. Note the checkboxes at the bottom left of the debug panel that control whether the debugger pauses on certain errors.
TypeScript Modules
Modern TypeScript (and JavaScript) uses ES modules to manage code across multiple files.
Named Exports
Named exports allow you to export multiple functions, classes, or variables from a single file.
For example, the provided src/util.ts exports individual utility functions using named exports:
// src/util.ts
export function assert(condition: any, msg?: string): asserts condition {
if (!condition) {
throw new Error(msg ? `Assertion failed: ${msg}` : "Assertion failed.");
}
}
export function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
Then, the provided src/main.ts imports specific items from src/util.ts and src/randomization.ts using named imports. (Note that you don’t need to include the .ts file extension when importing local files.):
// src/main.ts
import { assert } from "./util";
import { Randomizer } from "./randomization";
// Now you can use them in your code
assert(x + y === 3, "Math is broken!");
const rand = Randomizer.create_autoseeded();
console.log(`Random element: ${rand.choose_one(data)}`);
You can import multiple named exports on the same line:
import { assert, assertNever } from "./util";
Or import as a different name:
import { assert as iReallyHopeThisIsTrue } from "./util";
Default Exports
It’s also possible to have a single default export from a file, though this is less common. For example:
// src/game.ts
export default class Game {
// class implementation
};
Importing:
When importing a default export, you don’t use curly braces, and you can name it whatever you like:
import Game from "./game";
import MyGame from "./game";
Importing from External Packages
You can also import from external packages/libraries installed via npm. For example, src/main.ts imports the colors package (which uses a default export). Note that you don’t specify a path (the ./ part) when importing from external packages.
// src/main.ts
import colors from "colors";
console.log(colors.red(`The mean of [${data}] is ${result}.`));
console.log(colors.blue(`Random element: ${rand.choose_one(data)}`));
Puzzle Specifications
Choose one of the following puzzles to implement.
Sudoku
Sudoku is a number-placement puzzle where the goal is to fill a 9x9 grid so that each column, row, and 3x3 block contain all digits from 1 to 9 exactly once. A subset of numbers are provided as a starting point. We’ve provided several puzzles in a file with a difficulty level and unique solution provided - you don’t need to implement puzzle generation or solving.
Program Specification
The program is run with one command line argument, an integer from 0 to 9 representing the difficulty level (0 = easiest, 9 = hardest). For example:
$ npx tsx src/main.ts 0
The program reads from the provided sudoku_puzzles.txt file, which contains a collection of Sudoku puzzles categorized by difficulty level. The file contains one puzzle per line. Each line contains the difficulty level, the starting board, and the solution board. Each of these elements is separated from the other with a space. The encodings of the board are in row-major order (i.e. row 0 left-to-right, then row 1, etc.) with . representing empty cells.
The program selects a random Sudoku puzzle of the requested difficulty. It displays a welcome message and the difficulty level. Then, it displays the sudoku board as shown below and prompts the user to enter a move in the format x y [1-9].
Welcome to Sudoku! Difficulty Level: 0
+-------+-------+-------+
8 | 5 2 3 | 4 6 9 | 7 1 8 |
7 | 8 . 1 | 5 7 2 | 6 9 3 |
6 | 9 7 6 | 3 8 1 | 2 5 4 |
+-------+-------+-------+
5 | 7 3 2 | 1 4 5 | . 8 6 |
4 | 4 8 5 | 6 9 7 | 1 3 2 |
3 | 6 1 . | 2 3 8 | 5 4 7 |
+-------+-------+-------+
2 | 2 9 4 | 8 1 6 | 3 7 5 |
1 | 3 6 7 | 9 5 4 | 8 2 1 |
0 | 1 5 8 | 7 2 3 | . 6 9 |
+-------+-------+-------+
0 1 2 | 3 4 5 | 6 7 8
Enter your move as x y [1-9] (or "quit"): 1 7 4
After each move, the updated board is displayed and the user is prompted for another move. If the user enters “quit”, the program exits.
Important! The board should be displayed with colored text for each number. Numbers that were part of the original puzzle (i.e. not entered by the player) are displayed in blue. Numbers entered by the player are displayed in white if they are correct and in red if they are incorrect. (All provided puzzles have exactly one solution, so it is sufficient to check against the solution from the file - you do not need to implement Sudoku solving or validation logic.)
If the player enters a malformed (e.g. “cat dog”) or invalid move (e.g. out-of-bounds coordinates, trying to overwrite a starter number, or entering numbers outside the 1-9 range), your program should handle it gracefully. You may display an error message or simply ignore the input and re-prompt the player.
If all cells are correctly filled, the program should display a winning message and exit. It’s not possible to lose the game.
Feature Demo
Interactively play a difficulty 0 puzzle. Try malformed, invalid, correct, and incorrect moves. The program should respond appropriately and display the updated board after each move, with the correct format and colors.
Run the program again on a few other difficulty levels. Check that a different random puzzle is selected each time.
Minesweeper
Minesweeper is a grid-based, numerical logic puzzle where you attempt to reveal all cells without hitting any mines. The mine locations are not known, but each revealed cell shows the number of adjacent mines (if any) as a clue.
Program Specification
The program is run with three command line arguments: the grid width, the grid height, and the number of mines. For example, a 15x10 grid with 20 mines:
$ npx tsx src/minesweeper.ts 15 10 20
The maximum allowed width is 26 and the maximum allowed height is 10. The number of mines must be less than the total number of cells (width * height). If these rules are violated, the program should print an error message and exit.
The program places the specified number of mines in random locations on the grid. It displays a welcome message showing the grid size and mine count. Then, it displays the board and prompts the user for a move. The two basic moves are revealing a cell or placing a “flag” on a cell to mark it as a suspected mine.
On the game board, cells are represented as:
_- Unrevealed cellF- Flagged cell- (a space) Revealed cell with no adjacent mines1-8- Revealed cell showing with adjacent mines*- Mine
You’re welcome to use colored text, but it’s not required.
Let’s walk through an example.
Welcome to Minesweeper! Grid: 15x10, Mines: 20
+-------------------------------+
9 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
8 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
7 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
6 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
5 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
4 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
3 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
2 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
1 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
0 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
+-------------------------------+
A B C D E F G H I J K L M N O
Enter your move (reveal x y, flag x y, or "quit"):
After each move, the updated board is displayed and the user is prompted for another move. Let’s say the player reveals cell (N, 8), which turns out to have 2 adjacent mines.
Enter your move (reveal x y, flag x y, or "quit"): reveal N 8
+-------------------------------+
9 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
8 | _ _ _ _ _ _ _ _ _ _ _ _ _ 2 _ |
7 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
6 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
5 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
4 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
3 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
2 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
1 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
0 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
+-------------------------------+
A B C D E F G H I J K L M N O
Let’s say the player continues by revealing cell (G, 4):
Enter your move (reveal x y, flag x y, or "quit"): reveal G 4
+-------------------------------+
9 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
8 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
7 | _ _ 1 1 2 _ 3 1 2 _ _ _ _ _ _ |
6 | _ _ 1 2 _ 2 1 _ _ _ _ _ _ |
5 | _ _ 1 1 1 1 1 1 1 1 2 _ _ |
4 | _ _ 2 1 _ _ |
3 | _ _ 1 1 1 1 1 1 2 _ _ |
2 | 2 2 1 1 _ 1 1 2 3 _ _ _ _ |
1 | 1 1 1 1 _ _ _ _ _ _ |
0 | 1 _ _ _ _ _ _ |
+-------------------------------+
As a special case, if a cell with no adjacent mines is revealed, the program automatically reveals all 8 neighboring cells (if they are not already revealed). The player would have just done this manually since they’re all safe. If any of those cells have no neighboring mines, their neighbors are also revealed, and so on. This seems like a great use case for recursion :).
Next, the player flags cell (F, 2) because they are sure it has a mine. Placing a flag keeps track of the suspected mine location and prevents accidentally revealing that cell.
Enter your move (reveal x y, flag x y, or "quit"): flag F 2
A B C D E F G H I J K L M N O
+-------------------------------+
9 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
8 | _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |
7 | _ _ 1 1 2 _ 3 1 2 _ _ _ _ _ _ |
6 | _ _ 1 2 _ 2 1 _ _ _ _ _ _ |
5 | _ _ 1 1 1 1 1 1 1 1 2 _ _ |
4 | _ _ 2 1 _ _ |
3 | _ _ 1 1 1 1 1 1 2 _ _ |
2 | 2 2 1 1 F 1 1 2 3 _ _ _ _ |
1 | 1 1 1 1 _ _ _ _ _ _ |
0 | 1 _ _ _ _ _ _ |
+-------------------------------+
A B C D E F G H I J K L M N O
Note that flagging cell (F, 2) again would toggle to remove the flag.
If the player’s input is malformed (e.g. “hello world”) or invalid (e.g. out-of-bounds coordinates, attempting to reveal a cell that has already been revealed, or attempting to reveal a flagged cell), your program should handle it gracefully. You may display an error message or simply ignore the input and re-prompt the player.
If the player reveals all non-mine cells, the game ends and prints a winning message. If the player reveals a mine, the game board is shown with all cells revealed and a losing message is displayed.
Feature Demo
Play a game on a small grid (e.g. 8x8 with 5 mines). Try malformed and invalid moves to verify error handling. Reveal cells to see the adjacent mine counts and verify the correct behavior when revealing a cell with no adjacent mines. Place and remove flags. Play until you win or lose and verify the appropriate end-game message is displayed.
Run the program again with the same parameters. The mines should be placed in different random locations.
Run the program again with different grid sizes and mine counts to verify correct behavior and error handling.
Spelling Bee
Spelling Bee is a word puzzle where you form words using a set of 7 letters. Every word must include the center letter, and you earn points based on word length. Words that use all 7 letters (pangrams) earn bonus points.
Program Specification
The program is run with a single command line argument: a string representing the set of 7 unique letters to use as the puzzle seed. The first letter is presumed to be the center letter. For example:
$ npx tsx src/spelling_bee.ts oplaebc
If the argument does not contain exactly 7 unique letters, the program should print an error message and exit.
Then it The program loads words from the provided words.txt to use as a dictionary. It prints a welcome message, including the number of words loaded from the dictionary. (Note: The words.txt file contains the word "chaplain" twice, meaning there are 58,110 words but only 58,109 unique words. This was unintentional. You may handle this however you like, but our suggestion is to directly edit the words.txt file to remove the duplicate, and also use a Set<string> if you store the full set of words, since that will automatically ensure no duplicates in your internal representation.)
The “center” letter is displayed in the center of a honeycomb pattern in a yellow color. The other 6 letters are the “outer” letters and are displayed in a random ordering around the center letter.
Welcome to Spelling Bee!
Dictionary loaded with 58109 words.
How many words can you make with 7 letters?
The center letter "O" must be used in every word.
___
___/ C \___ 0 / 42 words
/ B \___/ P \ 0 / 123 points
\___/ O \___/
/ E \___/ L \
\___/ A \___/
\___/
Words found: []
Enter a word (or "_shuffle", "_quit"): COAL
COAL is worth 1 point!
___
___/ C \___ 1 / 42 words
/ B \___/ P \ 1 / 123 points
\___/ O \___/
/ E \___/ L \
\___/ A \___/
\___/
Words found: [COAL]
Enter a word (or "_shuffle", "_quit"): PALE
INVALID WORD: PALE does not contain the center letter O.
___
___/ C \___ 1 / 42 words
/ B \___/ P \ 1 / 123 points
\___/ O \___/
/ E \___/ L \
\___/ A \___/
\___/
Words found: [COAL]
Enter a word (or "_shuffle", "_quit"): PLACEBO
NICE! You found a PANGRAM: PLACEBO is worth 7 + 7 = 14 points!
___
___/ C \___ 2 / 42 words
/ B \___/ P \ 15 / 123 points
\___/ O \___/
/ E \___/ L \
\___/ A \___/
\___/
Words found: [COAL, PLACEBO]
As the player enters words, the program checks them against the rules and scoring policy below.
Word rules:
- Must be at least 4 letters long
- Must contain the center letter
- Can only use the 7 available letters (letters may be reused)
- Must be in the dictionary
- Each word can only be found once
Scoring:
- Only words that follow the rules are recorded and earn points
- 4 letters: 1 point
- 5+ letters : 1 point per letter
- Pangram (uses all 7 letters): 1 point per letter, plus 7 bonus points
If an entered word is invalid, display a short message indicating which rule it broke. If the word is valid, print a message including the word and its point value. Also update the words found and score. If the word is a pangram (uses all 7 unique letters), print a special message. Input should be case-insensitive.
If the player types _shuffle, the outer letters are randomly rearranged and the honeycomb is re-displayed. If the player types _quit, the game ends.
If the player finds all valid words, earning full points, the game ends with a winning message.
Feature Demo
Run the game with a valid seed. Try entering words that are too short, missing the center letter, contain invalid letters, not in the dictionary, and already found. Verify each shows an appropriate error message. Enter valid words and verify the score updates correctly. Find a pangram and verify the bonus points are awarded. Use _shuffle to rearrange the outer letters and _quit to exit.
Submission (Updated Wed, Jan 21 10:00pm)
We’ve simplified submission for project 1. Now, it is sufficient to ensure all your code is committed and pushed to the main branch of your GitHub repository before the deadline. (The old instructions included creating a pull request - this is no longer necessary for project 1 initial deliverables, and we’ll provide updated instructions for project 2 and beyond.)
Before the deadline, ensure you have completed the following steps:
- Update
README.mdwith your name, uniqname, and puzzle choice. - Update the run command in
README.mdappropriately. - Document how you used AI coding tools (if any) in
README.md(see below). - Verify all your code is committed and pushed to the
mainbranch of your GitHub repository.
Documenting AI Usage
Please document how you used (or didn’t use) AI tools in README.md. Remember that using AI tools is not prohibited, and we won’t grade your code according to different criteria or more harshly, but it is helpful for us to understand how students are using these tools and may allow us to provide better written feedback.
A sentence or short paragraph is fine. For example, “I didn’t use any AI tools”, “I used AI tools for brainstorming but not coding”, “I used in-line suggestions from Copilot throughout the codebase”, or “I used VS Code Chat in agent mode (with Claude Opus 4.5) to generate an initial draft of the game class, then iterated and revised the code myself”.
Git Details
Ensure that your code is committed and pushed to the main branch of your repository. You can check your current branch with git status. If you’ve been working on main and committing there, that’s fine. Just make sure to git push as well before the deadline.
If you’ve been working on a different branch, you’ll need to merge your changes into main and push. Here’s a quick summary of the commands you might use:
git checkout main
git merge <your-branch-name>
git push origin main
If you run git status again, make sure that it shows something like this:
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
If you see any changes listed, or that your branch is ahead of origin/main, you’ll need to commit and push those changes before the deadline.
You can also check your repository on GitHub to confirm that your latest changes are present in the main branch. Find your repository at: https://github.com/orgs/eecs498-software-design/repositories.
If you need any assistance with git, please reach out in lab, office hours, or on Discord well before the deadline!
Feature Demo Before your feature demo in Lab, review the corresponding section for your chosen puzzle and ensure your implementation meets the requirements.
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 not receive any credit and at 50% for the feature demo.
UPDATE
We’ll announce a few more specifics on the format of the pull request.
References
User Input Examples
For interactive user input via the terminal, there are several options:
- Use the 3rd-party
prompt-syncpackage (we’ve already included this in thepackage.jsondependencies). - Use a callback-based approach with the built-in
node:readlinemodule. - Use a promise-based asynchronous approach with the built-in
node:readline/promisesmodule.
The prompt-sync approach is the simplest, but unfortunately does not work correctly with standard input redirection from a file (e.g. npx tsx src/main.ts < test_input.txt). This interferes with writing automated tests for a full game session.
Any of the approaches are acceptable for your initial project 1 implementation. We’ll cover options 2 and 3 in lecture in the near future, but not before project 1 initial deliverables are due. Option 3, using node:readline/promises, is generally most aligned with modern Typescript/JavaScript practices.
For now, here are examples of each approach used to implement a simple program that interactively prompts the user for words to add to a sentence. You may adapt these for project 1.
Using prompt-sync (simple, but no input redirection)
import PromptSync from "prompt-sync";
class SentenceBuilder {
private sentence: string = "";
public user_input(prompt: PromptSync.Prompt): void {
console.log(`Current sentence: "${this.sentence}"`);
while (true) {
// Prompt the user. This essentially pauses and waits.
const word = prompt(`Enter the next word in the sentence (or "_quit"): `);
if (word === "_quit") {
console.log("Final sentence:", this.sentence);
break;
}
else {
this.sentence += word + " ";
}
}
}
}
function main() {
// Create a prompt instance from the imported library.
// Do this only once. Setting sigint: true allows Ctrl-C at
// the terminal to interrupt the program as expected.
const prompt = PromptSync({ sigint: true });
const sb = new SentenceBuilder();
sb.user_input(prompt);
}
main();
Using node:readline (asynchronous, callback-based)
import { createInterface, Interface } from "node:readline";
/**
* A class that builds a sentence from interactive user input.
*/
class SentenceBuilder {
private sentence: string = "";
public user_input(rl: Interface, onFinish: () => void): void {
console.log(`Current sentence: "${this.sentence}"`);
// Prompt via the provided readline interface
rl.question(`Enter the next word in the sentence (or "_quit"): `, (word) => {
if (word === "_quit") {
console.log("Final sentence:", this.sentence);
onFinish(); // call provided callback once we stop user input
}
else {
this.sentence += word + " ";
// Register a new input request. This is not technically recursion. O.o
this.user_input(rl, onFinish);
}
});
}
}
function main() {
// Create a readline interface with standard input
// and output streams. Do this only once.
const rl = createInterface({
input: process.stdin,
output: process.stdout
});
// Start sb.user_input() with the readline interface. The 2nd
// argument is a callback to close the interface when it's done.
const sb = new SentenceBuilder();
sb.user_input(rl, () => rl.close());
}
main();
Using node:readline/promises (asynchronous, promise-based)
import { createInterface, Interface } from "node:readline/promises";
class SentenceBuilder {
private sentence: string = "";
// Only functions marked 'async' can use 'await'.
public async user_input(rl: Interface) {
console.log(`Current sentence: "${this.sentence}"`);
while (true) {
// Prompt via the provided readline interface.
// The 'await' here means the rest of the function
// code is treated like a callback that runs later.
// That even includes future iterations of the loop!
const word = await rl.question(`Enter the next word in the sentence (or "_quit"): `);
// This code only runs after the user provides input.
if (word === "_quit") {
console.log("Final sentence:", this.sentence);
break;
}
else {
this.sentence += word + " ";
}
}
}
}
// Only functions marked 'async' can use 'await'.
async function main() {
// Create a readline interface with standard input
// and output streams. Do this only once. The 'await using'
// syntax ensures rl.close() is called automatically when
// we exit the containing block at the closing }.
await using rl = createInterface({
input: process.stdin,
output: process.stdout
});
const sb = new SentenceBuilder();
await sb.user_input(rl); // Important! Must use 'await' here.
}
main();
Command-Line Arguments
Use process.argv to access command-line arguments. The first two elements are the node executable path and your script path, so actual arguments start from index 2!
For example, if you run:
$ npx tsx src/main.ts cat dog
You can access cat and dog like this:
const arg1 = process.argv[2]; // "cat"
const arg2 = process.argv[3]; // "dog"
The process object is provided by default in node environments, and you don’t need to import anything to use it. (We do need the devDependency @types/node in package.json so that TypeScript recognizes process and knows its type.)
Reading Files
For a TypeScript program running on node, the simplest approach is to use readFileSync from the built-in node:fs module. This reads the entire file contents all at once, which you can then split to process line-by-line. Here’s an example of reading a file line-by-line:
import { readFileSync } from "node:fs";
// Read entire file as a string.
// Make sure to specify the "utf-8", otherwise
// you'll get a Buffer object instead of a string.
const contents = readFileSync("words.txt", { encoding: "utf-8" });
// Trim the trailing newline generally present at the end of
// text files, then split the file contents into lines.
const lines = contents.trimEnd().split("\n");
// Lines is now a string[] where each element is a line from the file.
// Process each line however you need.
Note: File paths are relative to the current working directory when you run the program. So if you run npx tsx src/main.ts from your project root directory, a file path like "words.txt" refers to a file in that root directory.
Randomization Library
We’ve provided a library in src/randomization.ts that you’re encouraged to use for any tasks that involve randomness, such as generating random numbers, making random selections, or shuffling items. (Our library is a wrapper of utility functions around the 3rd-party seedrandom package, which is already included in the package.json dependencies.)
The library is centered on a Randomizer class. Generally speaking, you should create, store, and reuse a Randomizer instance over a sequence of operations (rather than creating a new one for every operation). For example, you might store it as a private member within a class representing a puzzle game and initialize it in the constructor.
A Randomizer is based on an internal pseudorandom number generator, which produces values that appear random, but are actually generated from an underlying deterministic sequence. The sequence can be initialized with “seed”, and the same seed always leads to the same sequence (assuming the same random operations are requested).
This is actually desirable for many applications, including our puzzle games, because it makes it easy to reproduce the same “random” behavior for testing and debugging, or even to allow users to share a seed with each other and play through the same random puzzle. On the other hand, this library is not suitable for cryptography or other security-related operations.
Creating a Randomizer
You can create a Randomizer instance in one of two ways:
import { Randomizer } from "./randomization";
// Create an explicitly seeded randomizer. The seed can be any string.
const seededRng = Randomizer.create_from_seed("some_seed_string");
// Create an auto-seeded randomizer. The seed is chosen automatically
// and is different on each run.
const autoRng = Randomizer.create_autoseeded();
Generating Random Numbers
For these examples, the notation [a, b) indicates a range with an inclusive lower bound a and an exclusive upper bound b.
const rng = Randomizer.create_from_seed("example");
// Random floating-point value in [0, 1)
const f = rng.float();
// Random 32-bit integer in [-2^31, 2^31)
const n1 = rng.signed_int();
// Random 31-bit nonnegative integer in [0, 2^31)
const n2 = rng.unsigned_int();
// Random integer in [0, n)
const r1 = rng.range(10);
// Random integer in [5, 15)
const r2 = rng.range(5, 15);
Making Random Selections
const rng = Randomizer.create_from_seed("example");
const items = ["apple", "banana", "cherry", "date"];
// Choose one random element
const fruit = rng.choose_one(items);
// returns a single string, e.g. "cherry"
// Choose n random elements
const fruits1 = rng.choose_n(items, 2);
// returns a string[], e.g. ["banana", "date"]
// Choose n random elements (may have duplicates)
const fruits2 = rng.choose_n_with_replacement(items, 3);
// returns a string[], e.g. ["apple", "apple", "cherry"]
Shuffling
const rng = Randomizer.create_from_seed("example");
const arr = [1, 2, 3, 4, 5];
// Shuffle in-place
rng.shuffle(arr); // e.g. arr itself becomes [3, 1, 5, 2, 4]
// Get a shuffled copy
const original = [1, 2, 3, 4, 5];
const shuffled = rng.shuffled_copy(original);
// shuffled is a new array, e.g. [3, 1, 5, 2, 4]
// original is still [1, 2, 3, 4, 5]
Error Handling
Aim for reasonably high quality code that includes basic error handling. For example, if the player enters a malformed or invalid move, the program should do something reasonable and not crash.
Recommendations for Project 1
The following recommendations apply to project 1 specifically. You’re welcome to handle errors in other ways if you prefer (including via exceptions), but a complex approach is not necessary at this stage, nor is it necessary to ensure graceful evolution of the codebase for future projects.
-
Check for invalid input early.
Check for invalid input at the point where it’s received and before it is used. For example, make sure the code reading user input forxandycoordinates checks whether they are in-bounds before callingget_cell(x,y). If it’s malformed or invalid, show a helpful message and re-prompt the user. -
Use assertions to detect programming bugs.
As a defensive programming technique, write functions with preconditions and document them (e.g.// REQUIRES: x and y are in bounds), and verify them withassert()at the start of the function implementation. -
Don’t catch exceptions for broken preconditions.
If an assertion fails due to a broken precondition, it indicates a bug in your code. Don’t add atry/catchto catch the failed assertion and recover from it, instead fix the bug. -
Don’t create custom exception types.
At this point, it’s likely not helpful to create custom error types likeOutOfBoundsErrorfor communicating failures across your code. In most cases, using preconditions is cleaner for the code you’re writing now.
Examples
You often need to parse and validate user input before using it. In Typescript, this generally means checking for specific error values like undefined or NaN (“Not a Number”) rather than catching exceptions.
For example, let’s read a command that specifies a kind of pet, how many pets there are, and their names. The command could look like either of these:
dog 3 Fido Bowser Rexcat 2 Whiskers Mittens
Here’s how we would read and check the input. In the example code below, we’ll just “return false;” on an error - your program might be structured differently.
function check_pet_input(input: string): boolean {
// Assume 'input' is the string read from e.g. prompt-sync or readline.
// Remove leading/trailing whitespace and split into parts.
// (Otherwise, you can get an extra empty string on either end.)
const parts = input.trim().split(" ");
// Check that there are at least 2 parts: pet type and count.
// (We could omit this check if we were OK with handling undefined later.)
if (parts.length < 2) {
console.log("Invalid input: must specify pet type and count.");
return false;
}
if (parts[0] !== "dog" && parts[0] !== "cat") {
console.log("Invalid input: pet type must be 'dog' or 'cat'.");
return false;
}
// Parse and validate the pet count.
// parseInt() returns NaN if the input is not a valid integer.
// CAUTION - parseInt() will accept things like "3abc" and return 3.
// CAUTION - don't check num_pets === NaN, because NaN === NaN is false :(.
const num_pets = parseInt(parts[1]);
if (Number.isNaN(num_pets) || num_pets < 0) {
console.log("Invalid input: pet count must be a nonnegative integer.");
return false;
}
// Alternative - use a regular expression like /^\d+$/ to validate the string.
// The expression /^\d+$/ creates a RegExp object that will match strings
// starting (^) and ending ($) with one or more digits (\d+). If you haven't
// used or regular expressions before, they're worth looking up sometime!
const count_str = parts[1];
if (!/^\d+$/.test(count_str)) {
console.log("Invalid input: pet count must be a nonnegative integer.");
return false;
}
const num_pets = parseInt(count_str); // safely a number
const pet_names = parts.slice(2); // get all parts after the first two
// Check that the correct number of pet names were provided.
if (pet_names.length !== num_pets) {
console.log(`Invalid input: expected ${num_pets} pet names, but got ${pet_names.length}.`);
return false;
}
// If the individual pet names required additional validation, we could
// do that here, e.g. check for inappropriate words, length limits, etc.
}
You’ll also want to check that command-line arguments are valid before using them.
For example, to check that exactly 3 numbers between 0 and 100 are provided, as in npx tsx src/main.ts 25 98 2:
// There is no separate process.argc. Just check process.argv.length.
// Note that there are 2 extra arguments for the node executable path
// and the script path, so if we have 3 user arguments, the length is 5.
if (process.argv.length !== 5) {
console.error("Usage: npx tsx src/main.ts <num1> <num2> <num3>");
process.exit(1); // exit with error code
}
// Parse and check each numeric argument. This can be done
// in a similar way to the user input example above.
Finally, when reading input files with readFileSync(), it does turn out that an exception is thrown if the file can’t be opened successfully (e.g. file not found). If you want, you can catch that exception with a try/catch block:
import { readFileSync } from "node:fs";
function main() {
try {
const file_contents = readFileSync("data.txt", { encoding: "utf-8" });
// Process the file contents...
}
catch (e: unknown) {
console.error(e);
process.exit(1); // exit with error code
}
}
main();
Writing the above code will catch an exception if the file can’t be opened, print the exception (showing an error message and a stack trace), and exit with a non-zero error code.
However, for project 1, you don’t need the try/catch. It OK to just let the exception propagate and terminate the program. In fact, when the exception escapes from main(), the effect is exactly the same: node prints the exception with an error message and a stack trace and exits with a non-zero error code.