Last updated on

Codebreaking puzzles

Points
100 pts
Topics
Specifications, Higher-order functions, Modularity
Files to submit
src/main/scala/codebreaking/Common.scala
src/main/scala/codebreaking/Wordle.scala
src/main/scala/codebreaking/BullsAndCows.scala
src/main/scala/codebreaking/MasterMind.scala

The aim of this exercise is to build solvers for multiple closely-related code-breaking and word-guessing games. In these games, one player, the secret-holder, picks a sequence of $n$ digits or letters, and the other, the code-breaker, tries to guess the sequence.

On every turn, the code-breaker makes a guess, and the secret holder gives it a score, revealing partial information about the secret. Each game of this form is represented as an instance of the Puzzle abstract class:

abstract class Puzzle[Word, Score]:
  def score(guess: Word, secret: Word): Score
  def guess(previousGuesses: List[ScoredGuess[Word, Score]]): GuessResult[Word]

src/main/scala/codebreaking/Common.scala

We will study three games:

Part 1: Scoring functions (60 pts)

Your first task is to write a scoring function for each of these games. The scoring function takes a guess and a secret, and returns a score:

def score(guess: Word, secret: Word): Score =
  require(guess.length == wordLength, f"Guess length must be $wordLength.")
  require(secret.length == wordLength, f"Secret length must be $wordLength.")
  require(guess.distinct == guess, f"Guess $guess cannot have duplicates.")
  require(secret.distinct == secret, f"Secret $secret cannot have duplicates.")
  ???

src/main/scala/codebreaking/Wordle.scala

def score(guess: Word, secret: Word): Score =
  require(guess.length == wordLength, f"Guess must have length $wordLength.")
  require(secret.length == wordLength, f"Secret must have length $wordLength.")
  require(secret.distinct == secret, f"Secret $secret cannot have duplicates.")
  ???

src/main/scala/codebreaking/BullsAndCows.scala

def score(guess: Word, secret: Word): Score =
  require(guess.length == wordLength, f"Guess must have length $wordLength.")
  require(secret.length == wordLength, f"Secret must have length $wordLength.")
  ???

src/main/scala/codebreaking/MasterMind.scala

You can run specific tests using the testOnly command. As the tests contain spaces and special characters, make sure to quote your patterns appropriately. For example:

sbt:codebreaking> testOnly -- "*BullsAndCows: *"
sbt:codebreaking> testOnly -- "*Wordle.score(\\\"purchasing\\\", \\\"bracketing\\\")*"

Part 2: Guessing functions (40 pts)

A guessing function is a function which takes a sequence of previous guesses and their scores and either proposes a new guess or reports that the puzzle is solved and gives its solution.

With a guessing function, solving a game is simple. We start with an empty list of guesses, then repeatedly call the guessing function. If it reports that the puzzle is solved, then we are done. Otherwise we score the guess that it returns and call the guessing function again with this additional scored guess. Our testing code includes an implementation of this algorithm.

Example

Imagine that we are given the following guesses:

List(1, 2, 3): 1 bull, 1 cow
List(5, 3, 2): 2 bulls, 0 cows

At this point, only two possible solutions remain: List(7, 2, 1) and List(3, 2, 5).

The guessing function for Bulls and Cows could guess List(7, 2, 1). The solver would then submit this guess to the secret holder, who would return a score. Assume that score is “1 bull, 0 cows”: the solver would then call the guessing function again with the additional guess and its score:

List(1, 2, 3): 1 bull, 1 cow
List(5, 3, 2): 2 bulls, 0 cows
List(7, 2, 1): 1 bull, 0 cows # Added

With this additional information, the guessing function would then return List(3, 2, 5) as the solution (the one possibility left).

Of course, the guessing function could pick a different combination (any sequence of three digits would be valid):

  • If it had picked List(1, 3, 7), then the secret holder would have reported “0 bulls, 1 cow”, and the guessing function could have again solved the game on the next round.

  • If it had picked List(0, 0, 8), then the secret holder would have reported “0 bulls, 0 cows”; this time the guessing function could not have completed the game on the next round, since more than one possible secret would remain.

Your second task is to implement guessing functions for our three games:

def guess(previousGuesses: List[ScoredGuess[Word, Score]]): GuessResult[Word] =
  require(
    previousGuesses.forall(_._1.length == wordLength),
    f"Previous guesses must have length $wordLength."
  )
  require(
    previousGuesses.map(_._1).forall(wordList.contains),
    "Previous guesses must all be valid words."
  )
  ???

src/main/scala/codebreaking/Wordle.scala

def guess(previousGuesses: List[ScoredGuess[Word, Score]]): GuessResult[Word] =
  require(
    previousGuesses.forall(_._1.length == wordLength),
    f"Previous guesses must have length $wordLength"
  )
  ???

src/main/scala/codebreaking/BullsAndCows.scala

def guess(previousGuesses: List[ScoredGuess[Word, Score]]): GuessResult[Word] =
  require(
    previousGuesses.forall(_._1.length == wordLength),
    f"Previous guesses must have length $wordLength"
  )
  ???

src/main/scala/codebreaking/MasterMind.scala

In each case, you receive a list of previous guesses and their scores, and you must return one of three results:

enum GuessResult[+Word]:
  case StillGuessing(nextGuess: Word)
  case FoundSolution(solution: Word)
  case NoSolution extends GuessResult[Nothing]

src/main/scala/codebreaking/Common.scala

  • Each complete game (repeated guesses until the secret is found) may not take more than 10 seconds. Since our CI machine may not run as fast as the VDI machines, we recommend staying well below that limit to be safe.

  • The tests are increasingly difficult to pass: the last tests require an efficient guessing strategy. They are hard to pass and worth very few points: do not waste time on them!

  • Your code must be deterministic. If you use Random, initialize it with a seed: using val rng = scala.util.Random(0) will make sure that running your code twice produces the same results.