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

Sat, Jan 18 2:00pm

Mon, Jan 19 12:30pm

Mon, Jan 19 3:30pm

Tue, Jan 20 12:00am

Wed, Jan 21 10:00pm

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:

  1. Sudoku
    Complete a grid of numbers so that each column, row, and block contain the unique digits from 1 to 9.

  2. Minesweeper
    Reveal all cells in a grid without hitting any hidden mines. Each revealed cell shows the number of neighboring mines as a clue.

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

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:

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:

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:

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:

Scoring:

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:

  1. Update README.md with your name, uniqname, and puzzle choice.
  2. Update the run command in README.md appropriately.
  3. Document how you used AI coding tools (if any) in README.md (see below).
  4. Verify all your code is committed and pushed to the main branch 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:

  1. Use the 3rd-party prompt-sync package (we’ve already included this in the package.json dependencies).
  2. Use a callback-based approach with the built-in node:readline module.
  3. Use a promise-based asynchronous approach with the built-in node:readline/promises module.

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.

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:

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.