Homework 3

Due Wednesday 10-Feb, at 8pm 10pm


To start

  1. Create a folder named ‘hw3’
  2. Download hw3.py to that folder
  3. Edit hw3.py and modify the functions as required
  4. When you have completed and fully tested hw3, submit hw3.py to Gradescope. For this hw, you may submit up to 20 times (which is way more than you should require), but only your last submission counts.

Some important notes

  1. This homework is solo. You may not collaborate or discuss it with anyone outside of the course, and your options for discussing with other students currently taking the course are limited. See the academic honesty policy for more details.
  2. After you submit to Gradescope, make sure you check your score. If you aren’t sure how to do this, then ask a CA or Professor.
  3. There is no partial credit on Gradescope testcases. Your Gradescope score is your Gradescope score.
  4. Read the last bullet point again. Seriously, we won’t go back later and increase your Gradescope score for any reason. Even if you worked really hard and it was only a minor error…
  5. Do not hardcode the test cases in your solutions.
  6. The starter hw3.py file includes test functions to help you test on your own before you submit to Gradescope. When you run your file, problems will be tested in order. If you wish to temporarily bypass specific tests (say, because you have not yet completed some functions), you can comment out individual test function calls at the bottom of your file in main(). However, be sure to uncomment and test everything together before you submit! Ask a CA if you need help with this.
  7. Remember the course’s academic integrity policy. Solving the homework yourself is your best preparation for exams and quizzes; cheating or short-cutting your learning process in order to improve your homework score will actually hurt your course grade long-term.

Limitations

Do not use string indexing, lists, list indexing, or recursion this week. The autograder will reject your submission entirely if you do.

A Note About Style Grading

Starting with this assignment, we will be grading your code based on whether it follows the 15-112 style guide. We may deduct up to 10 points from your overall grade for style errors. We highly recommend that you try to write clean code with good style all along, rather than fixing your style issues at the end. Good style helps you code faster and with fewer bugs. It is totally worth it. In any case, style grading starts this week, so please use good style from now on!


Problems

  1. hasConsecutiveDigits(n) [10 pts]
    Write the function hasConsecutiveDigits(n) that takes a possibly-negative int n and returns True if somewhere in n some digit occurs consecutively (so the same digit occurs twice in a row), and False otherwise. For example, these numbers have consecutive digits: 11, -543661, 1200, -1200, and these numbers do not: 1, 123, 12321, -12321.

  2. isPalindromicNumber(n) [10 pts]
    Write the function isPalindromicNumber(n) that takes a non-negative int n and returns True if that number is palindromic and False otherwise. A palindromic number is the same forwards as backwards. For example, these numbers are palindromic: 0, 1, 99, 12321, 123321, and these numbers are not: 1211, 112, 10010.

  3. nthPalindromicPrime(n) [10 pts]
    Write the function nthPalindromicPrime(n). See here for details. So nthPalindromicPrime(0) returns 2, and nthPalindromicPrime(10) returns 313.

  4. The 112 Game [70 pts]

    Note: This is a long problem with many parts. Carefully read this entire writeup first! Your goal is reading is to make sure you understand all of the following:

    1. How to play the game (on paper, for example).
    2. How the game information will be stored on the computer.
    3. How all the functions described here fit together into a cohesive design.

    Game Play

    In this problem, we will implement a version of the world-famous and endlessly amusing 112 game.

    Here is how the game works: Start with a board with N positions, something like a 1xN tic-tac-toe board, with all positions empty. There are two players who alternate turns. On each turn, the current player must place either a 1 or a 2 in any unoccupied position. If the move results in the number 112 among any three contiguous positions, then the player who just moved wins the game. If the board is full, without any 112's, then it is a tie.

    So, for example, here is a short game on a board of size 5:

    Start with an empty board - - - - -
    Player 1, at position 4, places a 2 - - - 2 -
    Player 2, at position 1, places a 2 2 - - 2 -
    Player 1, at position 3, places a 1 2 - 1 2 -
    Player 2, at position 2, places a 1
    And Player 2 wins!!!
    2 1 1 2 -

    Specifying a Match

    Now, while it would be interesting to write an interactive game (either text-based in the console or with graphics) that people could play, here we will instead slightly adapt the problem so that a computer autograder can easily autograde it.

    So instead of playing a game interactively, we will write a function called play112 that takes a specification of a game and simply returns the outcome of that game. A game specification has to include the size of the board, which we will restrict to being an integer between 1 and 9 inclusive, and then a series of moves, which will be two values -- the position (an integer from 1 to boardSize, where 1 is the leftmost position) and the value to place there (either a 1 or a 2).

    The natural representation in Python for this would be a list. So the game above would be represented by this list:
    [5, 4, 2, 1, 2, 3, 1, 2, 1]
    This list indicates that the board is of size 5, then specifies each move -- at position 4 place a 2, at position 1 place a 2, and so on. The list does not include the player numbers, because we know these alternate, starting from player 1. Using a list this way is natural, only we have not yet covered lists. We need to find a different, less natural, less ideal, but workable way to represent these values using the types that we currently know.

    We do have integers. So we can represent that same list of digits (and notice they are all single digits, and then all are non-zero) as a single integer value:
    542123121
    Again, this may not be ideal, but it definitely works.

    Storing the Board

    Now that we can specify game play, we also need a way to store the board as the game progresses. Again, we are limited to integers, so we will store the entire board as a single integer. We need to store 1's and 2's where they were played, but we also need to store empty values. It may seem like 0 is a good idea for these, only that can lead to some oddness due to leading 0's not being displayed. For example, if 0's were the empty spaces, then the board above would be represented like the values in the third column of this table:

    Start with an empty board - - - - - 0
    Player 1, at position 4, places a 2 - - - 2 - 20
    Player 2, at position 1, places a 2 2 - - 2 - 20020
    Player 1, at position 3, places a 1 2 - 1 2 - 20120
    Player 2, at position 2, places a 1
    And Player 2 wins!!!
    2 1 1 2 - 21120

    See how the board is initially 0, even though it represents 5 blank spaces? This works, because 0 is the same as 00000, but it's not necessarily intuitive. It's also worth noting that we are only talking about intuitive for the programmer (that is, you!). Users would never know how you chose to represent the board internally in your program. Even so, good decisions here will lead to clearer, easier to understand, easier to debug code.

    In any case, for these reasons, instead of 0, we will use 8 to represent a blank space. Thus, here are the integer representations of the boards in the example game:

    Start with an empty board - - - - - 88888
    Player 1, at position 4, places a 2 - - - 2 - 88828
    Player 2, at position 1, places a 2 2 - - 2 - 28828
    Player 1, at position 3, places a 1 2 - 1 2 - 28128
    Player 2, at position 2, places a 1
    And Player 2 wins!!!
    2 1 1 2 - 21128

    Return Values from play112

    Now that we have a game specification, and a way to store a board, what should play112(542123121) return? Any call to play112 will return a single string (ok, we'll bend our limitation about strings just so we can more easily autograde) composed of two parts: the board as it existed at the end of the game, and then the result of the game. And so:
    play112(542123121) returns: "21128: Player 2 wins!"
    Compare this result to the table above to confirm that 21128 represents the last board of the game, and also that Player 2 in fact won.

    Ok, so now we are ready for a formal problem description. Here it is: write the function play112(gameSpec). It takes a gameSpec as defined above, and it returns a string composed of the final board, a colon (:), a space, and then the outcome of the game. Note that your code must match this spec exactly, character-for-character (no extra spaces, no incorrect upper or lower cases, etc). Here are the possible outcomes of the game:

    Player 1 wins! Player 1 played a move that generated 112, and won!
    Player 2 wins! Player 2 played a move that generated 112, and won!
    Player 1: move must be 1 or 2! Player 1 played a piece other than a 1 or 2, and lost.
    Player 2: move must be 1 or 2! Player 2 played a piece other than a 1 or 2, and lost.
    Player 1: occupied! Player 1 played a piece into an occupied position, and lost.
    Player 2: occupied! Player 2 played a piece into an occupied position, and lost.
    Player 1: offboard! Player 1 played a piece into a position not on the board, and lost.
    Player 2: offboard! Player 2 played a piece into a position not on the board, and lost.
    Tie! Neither player has won or lost, and the board is full.
    Unfinished! None of the preceding cases applies.

    Note that the game stops immediately after a win or loss, even if the game specification contains more moves.

    Here is a test function for you:

    def testPlay112(): print("Testing play112()...", end='') assert(play112( 5 ) == "88888: Unfinished!") assert(play112( 521 ) == "81888: Unfinished!") assert(play112( 52112 ) == "21888: Unfinished!") assert(play112( 5211231 ) == "21188: Unfinished!") assert(play112( 521123142 ) == "21128: Player 2 wins!") assert(play112( 521123151 ) == "21181: Unfinished!") assert(play112( 52112315142 ) == "21121: Player 1 wins!") assert(play112( 523 ) == "88888: Player 1: move must be 1 or 2!") assert(play112( 51223 ) == "28888: Player 2: move must be 1 or 2!") assert(play112( 51211 ) == "28888: Player 2: occupied!") assert(play112( 5122221 ) == "22888: Player 1: occupied!") assert(play112( 51261 ) == "28888: Player 2: offboard!") assert(play112( 51122324152 ) == "12212: Tie!") print('Passed!')

    Study the test function carefully. Manually confirm every single line of it, so that you understand why each given call to play112 produces the expected result.

    Helper Functions and Top Down Design

    So now what? At this stage of the course, this is a daunting task, perhaps too daunting without some more guidance. But it gives you your first real taste of what we are trying to teach you. This course is not about how a mod operator or a for loop works. Instead, this course is meant to teach you how to solve problems. And that requires higher-level design, abstraction, problem-solving, and so on.

    In particular, given a problem such as this, we will use a top-down design approach. So we will try to break this problem down into smaller parts, and to break those down into even smaller parts, until we get to problems small enough that we can fairly easily write and test them.

    For this problem, we will provide the entire design for you, and you must use this design. The autograder will expect you to write every function specified here, in just the way it is specified. Of course, in the future, you will need to do this sort of design yourself.

    Along with each helper function below, we have also included some testcases that could be useful to test your code. You should add these to the appropriate place in the provided skeleton code for this assignment, and use it to test as you write each function.

    At the lowest level, we will need some functions that do some basic manipulations of our board:

    • makeBoard(moves)
      Write the function makeBoard(moves) that takes a positive integer number of moves, and returns an empty board (all 8's) for a game with that many moves. Here are some sample test cases for you:
      assert(makeBoard(1) == 8)
      assert(makeBoard(2) == 88)
      assert(makeBoard(3) == 888)

    • digitCount(n)
      Write the function digitCount(n) that takes a possibly-negative int value n and returns the number of digits in n.
      Test cases:
      assert(digitCount(0) == 1)
      assert(digitCount(5) == digitCount(-5) == 1)
      assert(digitCount(42) == digitCount(-42) == 2)
      assert(digitCount(121) == digitCount(-121) == 3)

    • getKthDigit(n, k)
      Write the function getKthDigit(n, k) that takes a possibly-negative int value n and a non-negative int value k, and returns the kth digit of n, counting right-to-left, and 0-based (so the 0th digit is the rightmost digit, that is, the 1's digit).
      Test cases:
      assert(getKthDigit(789, 0) == getKthDigit(-789, 0) == 9)
      assert(getKthDigit(789, 1) == getKthDigit(-789, 1) == 8)
      assert(getKthDigit(789, 2) == getKthDigit(-789, 2) == 7)
      assert(getKthDigit(789, 3) == getKthDigit(-789, 3) == 0)
      assert(getKthDigit(789, 4) == getKthDigit(-789, 4) == 0)

    • setKthDigit(n, k, d)
      Write the function setKthDigit(n, k, d) that takes a non-negative int n, a non-negative int k, and an int d where 0<=d<=9, and returns the number resulting by replacing the kth digit (where k is defined as in getKthDigit, above) of the number n with the digit d.
      Test cases:
      assert(setKthDigit(789, 0, 6) == 786)
      assert(setKthDigit(789, 1, 6) == 769)
      assert(setKthDigit(789, 2, 6) == 689)
      assert(setKthDigit(789, 3, 6) == 6789)
      assert(setKthDigit(789, 4, 6) == 60789)

    At the next-higher level, we need some functions that use the lower-level functions you just wrote to perform the individual steps of game play:

    • getLeftmostDigit(n)
      Write the function getLeftmostDigit(n) that takes a non-negative int n and returns the leftmost digit (that is, the one with the highest place value). You might find digitCount and getKthDigit useful here.
      Test cases:
      assert(getLeftmostDigit(7089) == 7)
      assert(getLeftmostDigit(89) == 8)
      assert(getLeftmostDigit(9) == 9)
      assert(getLeftmostDigit(0) == 0)

    • clearLeftmostDigit(n)
      Write the function clearLeftmostDigit(n) that takes a non-negative int n and returns the number resulting by deleting the leftmost digit. You might find digitCount and setKthDigit useful here. Test cases:
      assert(clearLeftmostDigit(789) == 89)
      assert(clearLeftmostDigit(89) == 9)
      assert(clearLeftmostDigit(9) == 0)
      assert(clearLeftmostDigit(0) == 0)

      assert(clearLeftmostDigit(60789) == 789)
    • makeMove(board, position, move)
      Write the function makeMove that takes a board as specified above, an int position on the board (where 1 is the leftmost position), and an int move (1 or 2), and returns the new board that results by making the given move in the given position. If the move is illegal, however, the function should instead return a specific error message indicating the nature of the problem. In particular, if the move is not a 1 or a 2, return "move must be 1 or 2!". If the position is not on the board, return "offboard!". And if the position is not empty, return "occupied!". Note: while strings are not allowed in general on this homework, you plainly can use strings here as your return values (seeing as the design requires this). Also, again, you might find several of the earlier functions to be useful here.
      Test cases:
      assert(makeMove(8, 1, 1) == 1)
      assert(makeMove(888888, 1, 1) == 188888)
      assert(makeMove(888888, 2, 1) == 818888)
      assert(makeMove(888888, 5, 2) == 888828)
      assert(makeMove(888888, 6, 2) == 888882)
      assert(makeMove(888888, 6, 3) == "move must be 1 or 2!")
      assert(makeMove(888888, 7, 1) == "offboard!")
      assert(makeMove(888881, 6, 1) == "occupied!")

    • isWin(board)
      Write the function isWin(board) that takes a board as specified above and returns True if that board is a win (contains 112), and False otherwise.
      Test cases:
      assert(isWin(888888) == False)
      assert(isWin(112888) == True)
      assert(isWin(811288) == True)
      assert(isWin(888112) == True)
      assert(isWin(211222) == True)
      assert(isWin(212212) == False)

    • isFull(board)
      Write the function isFull(board) that takes a board as specified above and returns True if that board is full (no empty spaces), and False otherwise.
      Test cases:
      assert(isFull(888888) == False)
      assert(isFull(121888) == False)
      assert(isFull(812188) == False)
      assert(isFull(888121) == False)
      assert(isFull(212122) == True)
      assert(isFull(212212) == True)

    And that finally leads to the top-level function that uses all the preceding functions to actually play the game:

    • play112(game)
      Write the function play112(game) that takes a game spec -- an int containing the board size followed by the position and piece of each move in the game -- and returns the result of playing that game of 112, again as described above. To do this, you must make appropriate use of all the "helper" functions you just wrote. First, you probably want to keep track of which player's turn it is. Then, you might want to use getLeftmostDigit to extract the size of the board, and then perhaps set game equal to the result of clearLeftmostDigit(game) to strip away the board size. Then you would loop through all the turns in the game, extracting the position and the move. For each move, you would make the move, and if the game ends for any reason (error, win, tie), just stop there, returning the appropriate result. Otherwise, change players and move on to the next turn. If you run out of turns and the game has not yet ended, then it is clearly unfinished, so return that. Test cases (same as above):
      assert(play112( 5 ) == "88888: Unfinished!")
      assert(play112( 521 ) == "81888: Unfinished!")
      assert(play112( 52112 ) == "21888: Unfinished!")
      assert(play112( 5211231 ) == "21188: Unfinished!")
      assert(play112( 521123142 ) == "21128: Player 2 wins!")
      assert(play112( 521123151 ) == "21181: Unfinished!")
      assert(play112( 52112315142 ) == "21121: Player 1 wins!")
      assert(play112( 523 ) == "88888: Player 1: move must be 1 or 2!")
      assert(play112( 51223 ) == "28888: Player 2: move must be 1 or 2!")
      assert(play112( 51211 ) == "28888: Player 2: occupied!")
      assert(play112( 5122221 ) == "22888: Player 1: occupied!")
      assert(play112( 51261 ) == "28888: Player 2: offboard!")
      assert(play112( 51122324152 ) == "12212: Tie!")

    Final Hint on Strings

    We haven't actually covered strings yet, but you need them (a little bit) for this problem, especially when returning from the play112() function. Consider the following sample code for a hint of how to do this easily:

    board = 21121 playerNum = 1 msg = "wins!" theString = f"{board}: Player {playerNum} {msg}" print(theString)

    And that's it! Good luck!