CSCI 1111, Fall 2014: Introduction to Software Development

Homework Webpage

Please see Piazza, and the course webpage for the deadlines of each homework. If there are any ambiguities about the homeworks, please ask on Piazza.

Please click on the homework sections below to see the contents.

Homework 1

This homework has two parts. Each will be implemented in a separate file as a separate class.


Coordinate Conversion

First, you will implement a class called CoordinateConversion (in a CoordinateConversion.java file). This class will include a method int coordConversion(double x, double minCoord, double maxCoord, int pixelSize). You cannot change the signature for the method. This method must be public static. This method is similar to the ones we've discussed in lab, and will convert between a point (x) within a bounding coordinate plane bounded by minCoord as the minimum coordinate value, and maxCoord is the maximum coordinate value. The method must determine which pixel should represent that point within a screen of a given pixelSize. That coordinate within the screen is returned. Recall that this is the problem we need to solve for OpenMaps where we have a bounding box and a point within it, and we need to figure out where to draw that point on the screen. Note that your method is only dealing with a single dimension (x or y/longitude or latitude). For example, if we have a bounds on the coordinates between 5 and 15, we're trying to plot a point at 10, and we're plotting it on a screen that is 800 pixels wide, we want to plot it at 400 pixels. So, coordConversion(10, 5, 15, 800) == 400. Please see the assertions below for more details.

Error Checking: Your implementation must include error checking. If x is outside of the maximum and minimum boundaries, it must treat x as if it is the minimum boundary, or the maximum boundary, depending on if it is larger than, or less than the bounds, respectively. If the minimum coordinate is greater than, or equal to the maximum coordinate, or if the pixel size is not positive (i.e. if it is <= 0), your implementation must return -1 to indicate an error due to invalid input.

Example Assertions: We include a number of assertions that your implementation must past below. In the future, we might not include all of the assertions, instead relying on you to test your program thoroughly yourself.

// Error cases
assert coordConversion(5, 10, 15, 800) == 0    : "5 in 10x15 to 800px should be 0";
assert coordConversion(20, 10, 15, 800) == 800 : "20 in 10x15 to 800px should be 800";
assert coordConversion(10, 15, 10, 800) == -1  : "10 in 15x10 to 800px should be -1";
assert coordConversion(10, 10, 15, 0) == -1    : "10 in 10x15 to 0px should be -1";
assert coordConversion(10, 10, 15, -800) == -1 : "10 in 10x15 to -800px should be -1";

// Edge cases
assert coordConversion(5, 5, 15, 5) == 0      : "5 in 5x15 to 5px should be 0";
assert coordConversion(15, 5, 15, 5) == 5     : "15 in 5x15 to 5px should be 5";
assert coordConversion(15, 0, 300, 300) == 15 : "15 in 0x300 to 300px should be 15";

// Correctness tests
assert coordConversion(30, 0, 300, 200) == 20    : "30 in 0x300 to 200px should be 20";
assert coordConversion(130, 100, 400, 200) == 20 : "130 in 100x400 to 200px should be 20";
assert coordConversion(10, 5, 15, 800) == 400    : "10 in 5x15 to 800px should be 400";


Simplified 2048 Left Shift

Second, you'll implement a program that emulates some of the logic in 2048. You will implement a Combine class (in a Combine.java file) with a number of methods specified below. First, have a look at and play a few games of 2048 to get an idea of what we're eventually going to be shooting for. 2048 can be a little like a drug addiction, so be sure to pry yourself away, and back to this homework. We aren't at the level yet where all of it can be implemented, but you'll implement the shuffling left of blocks with the collapsing of two blocks of like values together into a single block of double the value.

We will represent a single row of the 2048 square that includes only three blocks with a 3-length string. Each character in the string is either a '4', a '2', or a '_' which designates a "blank" space. Example rows are "42_", "4_4", and "242". Your program will execute a "shift blocks left" operation which will include combining like blocks into one of double the value. Note that newly created (double valued) blocks are not combined with other blocks. Examples of proper shift left operations include:

  • "__4" ⇒ "4__"
  • "_44" ⇒ "8__"
  • "222" ⇒ "42_"
  • "2_2" ⇒ "4__"
  • "242" ⇒ "242"
  • "44_" ⇒ "422"

Note that for this homework, you cannot use loops. We haven't learned them yet, and you should be able to complete the assignment with only concepts we've learned so far.

Error Checking: Your code must handle quite a bit of error checking. The strings being passed in must:

  • include only the characters '4', '2' and '_',
  • have exactly a length of three.
If any input string is not formatted according to this, you must return the string, unmodified.

You will implement four methods:

  • public static boolean validateChar(char ch)
    This method takes a character, and validates that it is one of the allowed characters in the input string. It returns true if it is, and false otherwise.
  • public static boolean validateRow(String row)
    This method takes a string as an argument, and checks that it adheres to the requirements for the input string above in "Error Checking". Return true if it the string is valid, and false otherwise. It must invoke the validateChar method for each character.
  • public static String moveLeft(String row)
    If the row is not valid (you must invoke the validateRow method to determine this), return the string unmodified. Otherwise, return a new string with all of the non-blank spaces (i.e. '2's and '4's) shifted all the way to the left. This method does not combine blocks.
  • public static String combineLeft(String row)
    This method first calls validateRow to determine that the input string is formatted correctly. If it isn't, then it returns the input unchanged. Otherwise, it calls moveLeft to move all blocks to the far left (this simplifies the problem). Then it must combine any blocks of comparable value starting from the left of the string. When two blocks are combined into a block of twice the value, any other blocks will need to be shifted left. Return the new string with blocks shifted left, and combined appropriately.

Example Assertions:

assert validateChar('2')    : "2 should be a valid char";
assert validateChar('4')    : "4 should be a valid char";
assert validateChar('_')    : "_ should be a valid char";
assert !validateChar(' ')    : "space should not be a valid char";
assert !validateChar('3')    : "3 should not be a valid char";

assert !validateRow("2222") : "2222 should not be valid";
assert !validateRow("__")   : "__ should not be valid";
assert !validateRow("aaa")  : "aaa should not be valid";
assert !validateRow("333")  : "333 should not be valid";
assert validateRow("22_")   : "22_ should be valid";
assert validateRow("_2_")   : "_2_ should be valid";
assert validateRow("242")   : "242 should be valid";

assert "4__".equals(moveLeft("__4"))    : "__4 doesn't change to 4__";
assert "4__".equals(moveLeft("_4_"))    : "_4_ doesn't change to 4__";
assert "42_".equals(moveLeft("4_2"))    : "4_2 doesn't change to 42_";

assert "4__".equals(combineLeft("__4")) : "__4 doesn't change to 4__";
assert "8__".equals(combineLeft("_44")) : "_44 doesn't change to 8__";
assert "4__".equals(combineLeft("2_2")) : "2_2 doesn't change to 4__";
assert "4__".equals(combineLeft("22_")) : "22_ doesn't change to 4__";
assert "42_".equals(combineLeft("222")) : "222 doesn't change to 42_";
assert "44_".equals(combineLeft("422")) : "422 doesn't change to 44_";
assert "242".equals(combineLeft("242")) : "242 doesn't change to 242";

assert "8__".equals(combineLeft(combineLeft("422"))) : "Double invocation doesn't work!";

// You should be using your validate methods to check for erroneous inputs!
assert "_222".equals(combineLeft("_222")) : "2222 should be invalid!";
assert "333".equals(combineLeft("333"))   : "333 should be invalid!";
assert "__".equals(combineLeft("__"))     : "__ should be invalid!";

Notes and grading: Your implementations must compile to get any credit. Please develop these in small chunks. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit proportional to how many assertions they pass. The methods they implement must be public static, and have the specified signatures/prototypes to get any credit. Your main method will not be invoked, so do not assume that it executes. Make sure to submit your .java file, and that it has the correct class name.

If you simply implement your methods in such a way that they are customized to the provided specific test cases (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit both .java files via blackboard.

Homework 2

This homework has two parts. Each will be implemented in a separate file as a separate class. Submit only the two .java files you complete.



Can Haz Maths?

Implement a few methods to do some mathematical computations. You'll create a HazMaths class in a corresponding .java file. These should have the following definitions:

  • public static boolean isPrime(int n)
    Return true or false depending on if the passed in integer values is prime or not, respectively. Return false, if the invoker passes in a number less than 1. A prime number is one that is not evenly divisible by any other number. Some sample assertions:
    assert isPrime(2);
    assert isPrime(53);
    assert !isPrime(55);
    assert !isPrime(24);
    assert !isPrime(-37337);
    
    Prime numbers form the basis of cryptography! Read into why this is a little bit. Really cool stuff.
  • public static int sumOfSums(int n)
    This method takes an integer, n, and computes the sum of each of the sums of each integer between 1 and n. For example, the sum of the sums of 3 is (1) + (1 + 2) + (1 + 2 + 3) = 10.
    assert sumOfSums(1) == 1;
    assert sumOfSums(3) == 10;
    assert sumOfSums(6) == 56;
    assert sumOfSums(25) == 2925;
    assert sumOfSums(-5) == 0;
    


2048 Left Shift

Similar to in HW1, you'll implement a left shift of decimals in a string, with combining of multiple like digits into a single digit of double the value. The main difference between HW1 and this, is that now the string representing a row in 2048 can be of any length! You'll implement a Combine class in a comparable .java file.

As before, the input string can only include the characters '_', '2', and '4'. Unlike before, it can be of any length (including length 0). Any '2's that have no other characters, or any number of '_'s between them combine to become a '4' (i.e. two '2's disappear, and a '4' is left in one of their space). Likewise, '4's become '8's. Any digit that is the result of a combination cannot be combined with another digit. All digits are combined in this way, and shifted to the far left so that any '_' appear on the right of the string. The length of the string remains unchanged.

This functionality is implemented across multiple methods that you must implement.

  • public static boolean validateChar(char ch)
    Please see the unchanged requirements from HW1.
  • public static boolean validateRow(String s)
    The string must include only the characters discussed above, but it can be of any length. Return true if it includes only valid characters, false otherwise. You must invoke validateChar to determine validity of each character.
  • public static String repeat(char ch, int num)
    Create a string that consists of num, ch characters.
  • public static String eliminateUnderscores(String s)
    Given a String s, return a new String that has all '_' characters removed. You cannot use the replace method in the String Class. Note that the returned string will be of the same or lesser length than the argument.
  • public static String moveLeft(String s)
    All non-'_' characters in the input string are shifted to the left of the string, leaving only '_'s on the right-hand side of the string. The resulting "left shifted" String is returned. It is highly suggested that you first use eliminateUnderscores to cluster all the non-'_' characters, and then to use repeat to concatenate the appropriate number of '_' on to the right of the String that is returned. Most other ways to implement this method are far more complicated.

    You must invoke the validateRow method to test if the input String is valid. If it is not, return the input string directly without change.

  • public static String combineLeft(String s)
    Given the input string, the output should be a string of the same length, with all '_'s removed from the left side of the string, and with all pairs of like digits with only '_'s in between them, combined into a digit of twice the value. Return the resulting String.

    It is highly suggested that you use the moveLeft method to help you with this implementation. When generating your new string, it is much easier to be able to ignore all '_' between digits.

    You must invoke the validateRow method to test if the input String is valid. If it is not, return the input string directly without change.

Some example assertions:
assert validateChar('_');
assert !validateChar(' ');

assert validateRow("2222");
assert validateRow("_24_");
assert !validateRow("2234_");
assert validateRow("");
assert !validateRow("*");

assert "***".equals(repeat('*', 3));
assert "".equals(repeat('*', 0));
assert "44444444".equals(repeat('4', 8));

assert "4".equals(eliminateUnderscores("__4__"));
assert "244".equals(eliminateUnderscores("2_4_4_"));
assert "".equals(eliminateUnderscores("___"));
assert "242442".equals(eliminateUnderscores("242442"));

assert "4____".equals(moveLeft("__4__"));
assert "244___".equals(moveLeft("2_4_4_"));
assert "___".equals(moveLeft("___"));
assert "_3_".equals(moveLeft("_3_"));
assert "242442".equals(moveLeft("242442"));

assert "484___".equals(combineLeft("224422"));
assert "484________".equals(combineLeft("2_2_4_4_2_2"));
assert "242424__".equals(combineLeft("242_42_4"));
assert "828____".equals(combineLeft("__44244"));
assert "".equals(combineLeft(""));
assert "22344".equals(combineLeft("22344"));

assert "88__________".equals(combineLeft(combineLeft("_2_22_222_4_")));


An Implementation Strategy and General Advice:

Please reread the "course philosophies" on the main course's webpage. You always want to implement and test the smallest chunk of code you can. If you try and write the whole program, and then start debugging/testing, your homeworks will take a ton of time. I'm breaking up these homeworks into separate methods explicitly so that you can implement the methods one by one, and test them before moving on to the next. If you implement a method, and test it (so you have confidence it works), then you should think "this method provides me the functionality to do X" for whatever X the method is implemented to do. Now implementing the next methods that might use that method are easier to think about because if you need that functionality X, then you can just invoke the method! Easy-peasy. Once you have the repeat method done, when you need to generate a number of '_', you don't need to think about a loop, only that you need to invoke that method! This is abstraction.

You know variables, conditionals, loops, math, and methods. Those are the building blocks for every single program that is written! With practice, you'll be able to master these building blocks, and create awesome programs. But if the core concepts for all programming can be learned in a single semester, what are you possibly going to learn in subsequent CS classes??? Put another way, what do you think defines a "good" software developer from one that is not?

Programs very quickly become too complicated for even a good programmer to understand. Our short-term memory holds 7 ± 2 items. Every conditional, variable, and loop takes up one of those! No wonder we can't keep the whole program in our heads!!! "Good" programmers, then are good at using abstraction to hide complexity. Methods are the main tool we have right now for abstraction. A method hides its own, possibly complex, functionality behind an method name, a return value, and a set of arguments. You just need to invoke the method to harness its power, and forget about all the details that would use up those 7 ± 2 slots: abstraction! The best programmers always seek out abstraction to make it easier to understand complicated programs.

It will be difficult for you to understand at this point when to use abstraction, or even how it can be helpful. As you become more familiar with coding, and as you get to the point when it all doesn't feel quite so foreign (with hard work, you'll get there!), you'll start to get a good understand of how to use methods and abstractions. For now, hold in there, and try and heed this advice on how to write your programs methodically! It's only a matter of time and sweat till you're a master of abstraction and all things programming!


Notes and grading: Your implementations must compile to get any credit. Please develop these in small chunks. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit proportional to how many assertions they pass. The methods they implement must be public static, and have the specified signatures/prototypes to get any credit. Make sure to submit your .java file, and that it has the correct class name. Your main method will not be invoked, so do not assume that it executes.

If you simply implement your methods in such a way that they are customized to the specific test cases (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit both .java files via blackboard.

Homework 3

This homework has two parts. Each will be implemented in a separate file as a separate class. Submit only the two .java files you complete.



Making sense of Open Maps data

As you've seen in many labs, open street maps is a wonderful resource! Not only can you browse geographically, but you can also download the data that backs the maps. As we're going to implement our own version of a mapping application, we need to figure out how to understand the data that is so diligently provided to us by open street maps. As a step toward providing our own mapping application, we will start parsing the .osm file. You can read about .osm files, and specifically the nodes within them. Read a little bit about the format to get a grip on it, and try and answer the questions:

  • What is XML and why is it used?
  • Where else is it used?
  • What types of features are represented as nodes?
  • What attributes do nodes have?
  • How are roads represented in a .osm file?
Feel free to discuss these freely on Piazza.

For this assignment, you'll implement a class called OSMParse in a corresponding OSMParse.java file. This implementation will be the basis for your mapping application. If you look at the osm format, it nests different types of items inside of each other just like java nests conditionals, loops, and methods inside of {...}. Examples of these items of the .osm file include:

  • <node .../> - which is used to describe a node.
  • <node ...> ... </node> - where everything between the begin node, and end (</...>) node is nested under it, and describes the node in some way.
  • <bounds ...> - which has all of the information about the longitude and latitude bounds of the map.
You will be using a representation of the file which is an array of Strings, one per line in the original file. In the API that follows, any String[] osm, is this representation of the .osm file.

You must implement the following methods. Please reference a simple test file that you'll be parsing through with these methods. You must understand the format to some degree to understand these methods. Also, you will be given a .java file (below) that will open a .osm file and return the array of Strings (the String[] osm below).

  • public static int numEntries(String[] osm, String type)
    Return the number of items in the osm representation of the specified type. type is one of the names of the items in the .osm file: "node", "bounds", "way".
  • public static int nthEntry(String[] osm, String type, int idx)
    osm is the representation of the .osm file described above. type is one of the names of the items in the .osm file: "node", "bounds", "way". idx is the index that the caller wants; the nth item of the specific type. For instance, if the caller passes idx of 3, and type of "node", they want to locate the fourth node item in the file. This method returns the index into the osm array of the idx item of the specific type.
  • public static String getAttribute(String s, String name)
    Given an input string s that is one of the lines in the osm data, this method searches for a name="value" pair where the name in the string matches the name argument to the method. The return value of the method is the value in the string.
  • public static double[] bounds(String[] osm)
    This method searches through the osm representation, and returns an array of doubles with information about the bounds of the file. The array is formatted as such: [maxlon, maxlat, minlon, minlat]. If the bounds cannot be found an array with four -1s is returned.

    I strongly recommend that you use the nthEntry method to get the bounds item, and the getAttribute method to find the values for maxlon, maxlat, minlon, and minlat. You will also want to look up the Double class in the javadocs, and find a way to take a string, and produce a double.

  • public static double[] nodeInfo(String[] osm, int idx)
    Look up the idxth node entry in the osm data, and return an array of doubles formatted as such: [lon,lat,nodeid]. Each of these values is taken from a node line in the .osm file. An example is: <node id="291675253" visible="true" version="3" changeset="8886698" timestamp="2011-08-01T01:48:06Z" user="emacsen" uid="74937" lat="38.8990372" lon="-77.0466533"/>. This method removes the doubles for the lon, lat, and id. This method returns an array with the nodeid=-1 if a node index is requested past the number of nodes that exist in the osm data.

    I strongly suggest that you use your numEntries method so that you know how many entries you can loop through, nthEntry to get the desired idx, and getAttribute to get the longitude, latitude, and node identifier.

Testing: Please see the OSMParse.java file. You can base your implementation on this. It includes a method called getOsmData that you can use to read in a .osm file into our format. We provide two test .osm files: a simple test file, and a file that includes SEAS at GWU.



One Dimensional 2048

Your own 2048 is edging closer to reality! Now we'll implement much of the functionality you already have, with arrays of integers instead of strings of digits. This will enable us to, among other things, go up to numbers greater than 8! You will implement a class called OneDimensional2048 in a OneDimensional2048.java file. You must implement a number of familiar methods:

  • public static boolean validateValue(int value, int maxPowerOfTwo)
    We must make sure that an integer value is a power of two between 2 and maxPowerOfTwo (e.g. 2048), or is set to 0. 0 denotes an empty square in the 2048 game.
  • public static boolean validateRow(int[] row)
    Validate that a row includes only value values. You can assume here that the maximum value we will represent is 2048. Use your validateValue method to test each integer in the row array.
  • public static boolean moveLeft(int[] row)
    All non-zero integers are shifted all the way to the left of the row array. Remember that arrays are different in that the row variable references the array itself. So if you modify any value in the array, the calling function will see those changes. Thus we do not return a new, modified array, instead modifying the row passed in as an argument. We return a boolean: false if the row is not valid, true otherwise. You must invoke validateRow to determine the input parameter's validity.
  • public static boolean combineLeft(int[] row)
    As in the previous implementations, we not only are moving all values left (no 0s are left of any non-zero values), but we are also combining like-values in values of double the value. Any value that is the result of such a combination will not be combined with another this invocation of this method. It is highly recommended that you use moveLeft to implement this as it should simply your implementation greatly; however it is not a requirement. You will be modifying the array that is passed in to this method.

    You must invoke the validateRow method to test if the input array of integers is valid. If it is not, return false. True otherwise.

Some assertions you can use to test your code. Note that for these assertions to work, you'll need to implement a method called identicalRows, (boolean identicalRows(int[] testRow, row))) that compares the passed in row against the first argument. If the contents are the same, it returns true. Otherwise, false.

int[] row;

assert(!validateValue(8, 4));
assert(validateValue(8, 8));
assert(validateValue(8, 16));
assert(validateValue(0, 16));
assert(validateValue(2, 2048));
assert(!validateValue(7, 2048));
assert(!validateValue(1023, 2048));
assert(!validateValue(1025, 2048));

assert(validateRow(new int[]{2, 2, 2, 2}));
assert(validateRow(new int[]{0, 2, 4, 0, 32}));
assert(!validateRow(new int[]{2, 2, 0, 3, 4, 0}));
assert(validateRow(new int[]{}));
assert(!validateRow(new int[]{8, 2, 64, 32, 30}));

row = new int[]{0,0,4,0,0};
assert(moveLeft(row) && identicalRows(new int[]{4,0,0,0,0}, row));
row = new int[]{0,0,4,0,0};
assert(moveLeft(row) && !identicalRows(new int[]{4,0,0,0,0,0}, row));
row = new int[]{2,0,4,0,0,16};
assert(moveLeft(row) && identicalRows(new int[]{2,4,16,0,0,0}, row));
row = new int[]{0,0,0};
assert(moveLeft(row) && identicalRows(new int[]{0,0,0}, row));
assert(!moveLeft(new int[]{2,0,31}));
row = new int[]{4,16,2048};
assert(moveLeft(row) && identicalRows(new int[]{4,16,2048}, row));

row = new int[]{8,8,16,16,32,32};
assert(combineLeft(row) && identicalRows(new int[]{16,32,64,0,0,0}, row));
row = new int[]{2,0,2,8,0,8,64,0,64,0};
assert(combineLeft(row) && identicalRows(new int[]{4,16,128,0,0,0,0,0,0,0}, row));
row = new int[]{2,0,8,2,0,64,4,0,64,0};
assert(combineLeft(row) && identicalRows(new int[]{2,8,2,64,4,64,0,0,0,0}, row));
row = new int[]{2,0,8,2,0,64,4,0,64,0};
assert(combineLeft(row) && identicalRows(new int[]{2,8,2,64,4,64,0,0,0,0}, row));
row = new int[]{0,0,2,2,128,64,0,64};
assert(combineLeft(row) && identicalRows(new int[]{4,128,128,0,0,0,0,0}, row));
row = new int[]{0,0,2,2,128,1234,64,0,64};
assert(!combineLeft(row));
row = new int[]{};
assert(combineLeft(row) && identicalRows(new int[]{}, row));

row = new int[]{0,1024,512,256,128,64,32,16,8,4,2,0,2,0};
assert(combineLeft(row) && combineLeft(row) && combineLeft(row) && combineLeft(row) && 
       combineLeft(row) && combineLeft(row) && combineLeft(row) && combineLeft(row) && 
       combineLeft(row) && combineLeft(row) && identicalRows(new int[]{2048,0,0,0,0,0,0,0,0,0,0,0,0,0}, row));


Notes and grading: Your implementations must compile to get any credit. You must include all of the required methods, and they must compile, even if they don't work. Please develop these in small chunks. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit proportional to how many assertions they pass. The methods they implement must be public static, and have the specified signatures/prototypes to get any credit. Make sure to submit your .java file, and that it has the correct class name. Your main method will not be invoked, so do not assume that it executes.

If you simply implement your methods in such a way that they are customized to the specific test cases we provide (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit both .java files via blackboard.

Homework 4


Playable 2048

The next step is to finish a playable version of 2048! Before you read the specification, and the set of methods, please think through how you would design this program given what you've implemented so far.

Summary: This assignment will break the problem of providing a 2048 playable environment up into a few pieces:

  • We need to be able to collapse in each direction. We do this not by providing a method for each direction, and instead we implement a method for rotating the board 90 degrees. If we want to collapse in a specific direction (e.g. right), we could rotate 90 degrees twice, then combineLeft, then rotate the result twice to get the original orientation back. You can use and extend your combineLeft from last homework.
  • We need to randomly add a 2 or 4 each time we move. We need to add that new value in a random square that has a zero in it. We come up with a method for determining randomly which of the zero squares to use for the new value (see randCoord).
  • We will use a few provided functions for generating 1) a 2 or a 4 randomly (you'll see we've hard-coded it to 2 for now), and 2) a method to generate a random number between 0 and some upper value. We'll use this to figure out which of the zero squares to populate with the new value.

You'll implement a TwoDimensional2048 class in a comparably named file. It will include the following methods:

  • public static boolean validateBoard(int[][] b)
    I strongly recommend that you make this method invoke your validateRow method in your last assignment. You can do this using the following syntax. Your OneDimensional2048.java file must be in the current directory.
    OneDimensional2048.validateRow(row);
    
    Additionally, this method checks that each row is of the same length. Note that it does allow boards that are not square.
  • public static int[][] blankBoard(int x, int y)
    Return a board with the specified dimensions, i.e. with x number of rows, and y number of columns. All values should be initialized to zero.
  • public static boolean identicalBoard(int[][] a, int[][] b)
    Check if the two boards that are passed in have identical values in all of their squares. Return true or false accordingly.
  • public static int numUnoccupied(int[][] b)
    Return the number of 0 cells on the board. This is quite useful when finding a bound on the random number you'll need to generate (see next section) to determine where a new item needs to be placed.
  • public static int[] randCoord(int[][] b, int offset)
    Return the x,y coordinate (in an array of length 2) in the board that a new random value should be placed in that corresponds to a given offset into the zero squares. In the picture on the right, we have three examples. The red line is if we ask for the coordinates of the 0th empty square (offset = 0). That square is at [0,1]. The blue line is searching for the 4th empty square (offset = 4), thus will return [2,1]. The green line is searching for the 2nd empty square (offset = 2), thus will return [1,0].
    This method is one of the more challenging ones of this assignment.
  • public static boolean addNewValue(int[][] b)
    This method is supposed to perform the function in 2048 of adding a new random value into the board. We will do so in a very controlled manner. First, you must figure out where to put the new value. Of course it can only be in one of the spots with a zero. This method will
    1. Invoke Rnd2048.randValue() to get the value to be inserted (a 2 or a 4).
    2. Invoke Rnd2048.randNum(numUnoccupied(b)) to figure out which of the zeros (i.e. the offset of the zero square) to place the value we just generated into.
    3. Invoke randCoord(b, offset) to get the [x,y] coordinates in the board we'll insert the value into. This takes the randomly generated offset into the zeros from the previous step.
    4. Add the new value at the corresponding coordinates.
    This method is straight-forward, but it is important as it connects many other functionalities.
  • public static int[][] combineLeft(int[][] b)
    You can use your previous solution to combineLeft that only works on a 1D array. If you call this for each row in b, it should have the desired effect. You can call the combineLeft method in your previous OneDimensional2048 implementation as such:
    OneDimensional2048.combineLeft(row);
    
    Your previous implementation has to be in the same directory/folder as your current implementation. Though you don't have to do this, I strongly recommend it. You already did the hard work for combine; you might as well use it!
  • public static int[][] rotateLeft(int[][] b)
    Take a board as input, and return a new board that is the same contents, but rotated 90 degrees to the left (counter-clockwise). This method, and combineLeft will be used together to implement up, down, and right. For instance, to move the squares to the right, we can rotate left twice, then combineLeft, then rotate left twice. This means that we only need to implement the logic of combine once!
  • public static int[][] left(int[][] b)
  • public static int[][] right(int[][] b)
  • public static int[][] up(int[][] b)
  • public static int[][] down(int[][] b)
    Each of these move all blocks, and combine them, in the direction specified. They will just use a combination of rotateLeft and combineLeft, and they should be pretty short (e.g. 5 lines each).

The following are methods that I found useful, but that you do not have to implement if you choose not to. Consider them when designing your program.
  • public static int numMax(int[][] b)
    Return the maximum number of cells in the board.
  • public static int numOccupied(int[][] b)
    Return the maximum number of cells that are non-zero in the board.
  • public static boolean addValue(int[][] b, int x, int y, int val)
    Try and set the zero-valued cell in a board at coordinate x, y to val. Return false if the coordinate is outside of the bounds of b, or if that coordinate is non-zero.
  • public static int[][] copyBoard(int[][] b)
    Create a new board, and copy an existing (argument) board into it.
  • public static void printBoard(int[][] b)
    Print out a board (with tabs between values). Used for debugging.

Other classes, and provided files: We are providing a class with assertions and some methods, and the Rnd2048.java class. You should base your implementation off of these. To get the assertions to work, you'll have to implement many of the methods above.

You will have to use methods from another class we provide to get both random values (e.g. 2 or 4) to insert into the 2048 board, and to give us an offset into the blank (empty) spaces into the board to insert a new value.

  int value  = Rnd2048.randValue();
  int offset = Rnd2048.randNum(numUnoccupied(b))

Note that it is very important for this assignment that we don't actually use random numbers. It would be difficult to test such a solution. Thus we provide the Rnd2048 class for your use. It is required to get the provided assertions to work. Please note that the assertions assume that you only call each of these random methods when required. If you call them additional times, then they might start giving values that will make the assertions fail. Think of both of these methods as returning a value that is at an offset into an array that increases by one every time you call it.


Suggested implementation strategy: You should try and, as much as possible, implement the methods one at a time, and comment out all of the assertions for methods you haven't finished. I've included the mandatory methods in an order that I believe will enable you to iteratively develop your solution. Please write and test each method separately, and try and resist writing code for latter methods until your former ones are in good shape. You will require a means to visualize your program, so I suggest writing the printBoard method ASAP.

Notes and grading: Your implementations must compile to get any credit. You must include all of the required methods, and they must compile, even if they don't work. Please develop these in small chunks. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit proportional to how many assertions they pass. The methods they implement must be public static, and have the specified signatures/prototypes to get any credit. Make sure to submit your .java files, and that TwoDimensional2048.java has the correct class name. Your main method will not be invoked, so do not assume that it executes.

If you simply implement your methods in such a way that they are customized to the specific test cases we provide (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit your .java files (if you use your code for the previous assignment, please submit it as well) via blackboard.

Homework 5


Object Oriented GWMaps

In this homework, you will improve your GWMaps application to include information about traffic signals, and roads. You will also break your implementation into separate classes, and make it object oriented. The high-level idea is that you'll implement a number of classes:

  • OSMNode which includes all of the node-specific, private data including its latitude, longitude, identifier, and a boolean denoting if it is a traffic signal or not. It includes a number of methods to set and get those fields.
  • OSMRoad which includes all of the road-specific, private data including the list (array) of nodes that constitute the road, and the name of the road (i.e. the street name). It includes a number of methods to get and set that data, and a method to detect if the road intersections with another road (passed in as an argument), and if it does, it returns the intersection node.
  • GWMap is the class that users of your mapping functionality will use. It includes a constructor that takes a filename as an argument, and parses through that file assuming it is a .osm file from Open Street Maps. It will use your OSMParse class from HW3. It includes a list (array) of all the nodes in the map, and a list (array) of the roads, in addition to two nodes that maintain the maximum and minimum bounds. It includes methods for accessing/getting all of this data. This class includes a number of private utility methods that aid in parsing through the .osm file. It is the only class that knows about the .osm format. It creates the nodes and roads to represent the data in an easier to access manner that we will use in future assignments.

This homework should be completed in two phases. First, implement the OSMNode and OSMRoad classes, and test them. Second, implement the GWMap class which will include the more advanced parsing of the .osm file that uses the previous two classes to represent the data.

Provided files: Find the GWMapMain.java file, and two test files, OSMTest.osm and SEASatGWU.osm. The former is simplified, and includes much simpler data, while the latter includes the raw .osm output from Open Street Maps.

You must understand the tests, and the methods before starting to write code. Use the assertions as a guide, but you must implement all methods correctly (i.e. abiding by the specification).


The public methods you must implement for your OSMNode class include the following. Node that you can include any number of private methods for your own use. All methods, unless otherwise specified are not static.

  • OSMNode(long id): initialize the data for the node with the passed in identifier, and both longitude and latitude set to a default value of 0.
  • OSMNode(double longitude, double latitude, long id): initialize the data for the node with the passed in arguments.
  • boolean equals(OSMNode n): return true or false depending on if the passed in node has the same identifier as this node.
  • double getLon(): return the node's longitude.
  • double getLat(): return the node's latitude.
  • long getId(): return the node's identifier.
  • boolean isSignal(): return if the node is a traffic signal nor not.
  • void setAsSignal(): set the node to be a traffic signal. By default all nodes are not set to be traffic signals.

The public methods you must implement for the OSMRoad class include the following. Again, you can include any number of private methods, and the following are not static.

  • OSMRoad(String name, OSMNode[] nodes): is a constructor that initializes the data for the road. The street name for the road is set to the argument, and the array of nodes is set to be the list of nodes that constitute this road.
  • OSMNode getNode(int nth): return the nth node in the road. If nth is not within the bounds of the node array, then return null.
  • int getNumNodes(): returns the number of nodes that constitute this road.
  • String getName(): returns the street name for this road.
  • OSMNode intersects(OSMRoad r): compares nodes in the current road with those in the one that is passed in. If any of the nodes are shared between the two roads, return the shared node. Otherwise, return null.

The GWMap class is the main class that is to be used by developers using our map. Objects of it can be created, one per .osm file. The following methods are required in it, and are public and not static.

  • GWMap(String OSMfilename): A constructor that takes the filename of the .osm file in the current folder/directory, and parses through the file, add adds all of the relevant nodes and roads (ways). More details about parsing the file follow.
  • int numNodes(): return the number of nodes in our map.
  • OSMNode getNode(int nth): return the nth node in our map.
  • int numRoads(): return the number of roads in our map.
  • OSMRoad getRoad(int nth): return the nth road in our map.
  • OSMNode[] getBounds(): return an array of length 2 that includes two nodes: the maximum coordinate of our map, and the minimum.

A number of private methods might be helpful within your GWMap class (they were for me!). These include:

  • void populateNodes(String[] osm): This method parses through the .osm file using and extending your previous OSMParse class. We must also determine which nodes are traffic signals, and create them accordingly. This will require additions to your parsing functions to understand how the .osm file denotes traffic signals. This method generates the list of nodes that are tracked by the object of the GWMap class.
  • void populateRoads(String[] osm): This method parses through the "way"s in the .osm file by also using and extending your OSMParse class. Only some of the ways in the .osm file will end up being roads, as determined by the rules in the Parsing the .osm file section. This method generates the list of roads.
  • void populateBounds(String[] osm): Add the bounds into the GWMap object.

Each of these methods are pretty complicated, and include all of the parsing work done within this homework. They translate the .osm file into a set of nodes and roads that can be easily used by clients of your class.


Parsing the .osm file. Here we describe which of the nodes is a traffic signal, and how to find the roads within the .osm file.

  • Node identifiers are listed as the id="#" attribute within the node tag. For example:
     <node id="291675382" visible="true" version="3" changeset="5155510" timestamp="2010-07-07T01:34:45Z" user="aude" uid="12055" lat="38.8992236" lon="-77.0488323"/>
    
    The node's identifier is 291675382.
  • Traffic signals are nodes denoted by nodes who's tag is not termined with a />, and are followed by a <tag k="highway" v="traffic_signals">. What follows is a node that is not a traffic signal followed by one that is.
     <node id="291675382" visible="true" version="3" changeset="5155510" timestamp="2010-07-07T01:34:45Z" user="aude" uid="12055" lat="38.8992236" lon="-77.0488323"/>
     <node id="49824719" visible="true" version="7" changeset="3920604" timestamp="2010-02-20T06:15:27Z" user="NE2" uid="207745" lat="38.9020773" lon="-77.0488070">
      <tag k="highway" v="traffic_signals"/>
     </node>
    
  • The roads we wish to track in our application are defined as follows:
    1. Roads are denoted by way tags in the .osm format. However, not all way tags are roads we care about. Some denote parks, buildings, etc.
    2. In between the <way ...> tag, and the </way> tag, there are a number of other tags. An example follows:
       <way id="6056057" visible="true" version="14" changeset="19460319" timestamp="2013-12-15T08:51:55Z" user="michael colvin" uid="1329572">
        <nd ref="49777749"/>
        <nd ref="291675380"/>
        <nd ref="49777696"/>
        <nd ref="49777690"/>
        <nd ref="49741695"/>
        <nd ref="2383566163"/>
        <tag k="HFCS" v="Collector"/>
        <tag k="highway" v="residential"/>
        <tag k="name" v="H Street Northwest"/>
        <tag k="source:HFCS" v="District of Columbia (DC GIS)"/>
        <tag k="tiger:cfcc" v="A41"/>
        <tag k="tiger:county" v="District of Columbia, DC"/>
        <tag k="tiger:name_base" v="H"/>
        <tag k="tiger:name_direction_suffix" v="NW"/>
        <tag k="tiger:name_type" v="St"/>
        <tag k="tiger:reviewed" v="no"/>
        <tag k="tiger:zip_left" v="20037"/>
        <tag k="tiger:zip_right" v="20037"/>
       </way>
      
    3. Each of the <nd ...> tags includes a reference to the identifier of the node that is the next link in the road. You will populate the nodes for the road from this list. Note that you must find the node corresponding to the specified identifier, so that we can find its location, and if it is a traffic signal.
    4. Each of the <tag ...> includes a "key"/"value" pair. We are interested in two of these: those with the "key" set to "highway", and those with the "key" set to "name".
    5. Remember that you implemented getAttribute previously!
  • The roads you will use to populate your GWMap application are only those with the "highway" key, with the value set to anything other than "footway", "service", or "pedestrian". We want to avoid the sidewalks and pedestrian walkways! In the above example, the type of "highway" is "residential", so this is a road we will represent in our GWMap application!
  • The street name that we store in the OSMRoad objects is the value associated with the "name" key. In the above example, the name of the street is "H Street Northwest".

Notes and grading: Your implementations must compile to get any credit. You must submit at least GWMap.java, OSMNode.java, and OSMRoad.java. You must also submit any other .java files you use. You must include all of the required methods, and they must compile, even if they don't work. Please develop these in small chunks. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit proportional to how many assertions they pass. Any main methods you use will not be invoked, so do not assume that it executes.

If you simply implement your methods in such a way that they are customized to the specific test cases we provide (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit your .java files (if you use your code for the previous assignments, please submit it as well) via blackboard.

Homework 6


Object Oriented 2048 and AI

2048 is too exhausting to play on our own. So many moves. So many numbers. To remedy this situation, in this homework you'll implement an AI to automatically play 2048 for you. This will consist of two phases:

  1. Make an object oriented version of 2048 that will greatly ease your implementation of AIs that might want to "look into the future" with many different board objects, shifted in different directions.
  2. Write an AI to automatically play 2048 for you. You'll implement a simple, dumb AI, but you also have free reign to go wild and implement your own super-smart AI to take over the world -- or at least get an astronomically high score in 2048!

Object Oriented 2048

In this part of the homework, you're going to create a class called TwoThousandFourtyEight (in a TwoThousandFourtyEight.java file) that allows you to make 2048 objects that each contain a 2048 board, that can be played. This class must implement the following methods:

  • public int getScore(): Return the score of the 2048 game in this TwoThousandFourtyEight object. The score is calculated as the sum for each tile of the sum of the powers of two up to and including the value in the tile. Put in more understandable terms: If a tile has value 8, then the number of points from that tile is 8 + 4 + 2. A tile with value 32, contributes 32 + 16 + 8 + 4 + 2 to your score. All of these values are added up for all of the tiles, and this is returned as the overall score.
  • public int getHighestTile(): Return the highest value tile in your board.
  • public TwoThousandFourtyEight copy(): This method creates a new TwoThousandFourtyEight object, copies the contents of the current object, and returns it. Note that any changes to any of the state/data in the current object must be completely separate from that in the new object, and vice-versa.
  • public boolean up(), public boolean down(), public boolean left(), and public boolean right(): These methods execute two actions. First, they shift the board in the corresponding direction (e.g. using the methods in your previous implementation). Second, they add a 2 tile at a random location in the board using TwoDimensional2048.addNewValue. Note that this is a simplification of the normal 2048 game which adds 2s or 4s. These methods return false when the game is over, and true otherwise. The game is over if we cannot add a new value to the board in the second step, as the board is full.
  • public TwoThousandFourtyEight(int x, int y): The class' constructor. It takes the width and height of the board as arguments, and allocates the board for the new TwoThousandFourtyEight object. It also adds a 2 value to a random location in the board (using TwoDimensional2048.addNewValue).
  • public int[][] getBoard(): Return a copy of the 2048 board. Note that this must be a copy, and not the actual board used in subsequent movements, or else the client can easily cheat.

In addition to these, you can implement as many private methods as you want.

To test your implementation, find the provided Main2048.java and Rnd2048.java files. Note that for this homework, you want to make sure that you don't modify Rnd2048.java.

I highly suggest that you use your previous implementations of 2048 for most of the logic and board management. Any files that you do use, you must submit. We will use our own Rnd2048, but please submit the one you use regardless.


2048 AI

Time to let the machines rule the world! You must complete the playRandom and playAi methods in the Ai2048 class in the provided Ai2048.java file. In that file, you'll find a very dumb AI implemented already that simply moves the board left until the game is over. The AiMain2048.java file implements a main to run these different AI implementations many times, and prints out the average score and maximum tile value. This will let you test your AI implementations, and let you know how you're doing! Your implementation of these methods will use the Object Oriented implementation of 2048 from the previous section.

The playRandom method in Ai2048 simply randomly chooses if it should swipe up, down, left, or right. This is, in some sense, a base-line that you can use to measure how good your implementation is! Can you do better than rolling a dice? Your implementation of playRandom should be able to get a score of around 350 points with an average maximum tile of around 73. To get credit for this method, your implementation must achieve between 300 and 400 points.

Your playAI method must be more creative. To get credit for this method, it must get over 400 points on average. A very simple implementation (that is around 3 lines of code) can achieve around 840 points, so you don't need to get too complex here. However, it will take some critical thought to figure out a strategy for playing 2048.

Please submit your Ai2048.java file, and any others that you require for it to work. However, we will be using our own implementation of TwoThousandFourtyEight.java to prevent anyone cheating in that implementation. If you do not submit any required file, your implementation will not compile, and you will not receive credit.

Execution time limits. Your implementation of your AI must not take longer than 5 minutes to execute the AiMain2048.java main method. We must put an arbitrary limit on execution to be able to detect infinite loops.


AI Face-Off

The above instructions are part of the required homework. In addition to this, you can complete an optional implementation of your AI (still in the playAI method) that is significantly more intelligent. There is no minimum required score for you to achieve. Instead, your AI will compete with those of your classmates. What follows is a set of deadlines. At each of these deadlines, we will run a competition between all AIs that have been submitted for that deadline. At least the top five implementations will receive extra credit. In subsequent competitions, the same students cannot receive more extra credit, with one exception: the last deadline is open for anyone to compete in, regardless if they've been at the top in a previous competition. If you provide an implementation that beats 1250 points (Chester's implementation), extra points to you!

Note: The execution time limits for this competition are extended to 10 minutes.

The deadlines for these competitions follow:

  • 11/16 - Lightning round. Speed is king. Can you finish in time?
  • 11/23 - A day before the normal deadline. Everyone should compete in this!
  • 11/30 - Holidays are a perfect time for 2048!
  • 12/7 - 2048-ageddon! 2048-poclypse!

There will be separate submission links on blackboard for each deadline. Deadlines are at midnight EST. Please submit all .java files required to execute your solution. This must include TwoThousandFourtyEight.java and any supporting files.

Homework 7


GWMaps Directions

In this homework, you'll expand your Maps application to provide directions! Your implementation will have the following components:

  • GWDirections class: You'll implement a class that will use your GWMap implementation from previous assignments to provide directions through the streets on the map from point A to B! This class must be in a GWDirections.java file and must be submitted.
  • GWDirectionsMain is a class that is provided for you to help you test your implementation. You must test on your own as it is non-exhaustive; just because your implementation doesn't trigger assertions, doesn't mean it works! See the file GWDirectionsMain.java.
  • GWMapDraw is a class that will be provided for you to visualize the map, and your provided directions.

The directions consist of three components:

  1. Point A. This is an OSMNode that is the start location of the directions.
  2. Point B, which is an OSMNode that is the final destination that your directions are working toward.
  3. The result of your directions will be a sequence (array) of OSMNodes, the first being A, the last being B, and all nodes in-between being those that are adjacent on a road (as defined in Homework 5) to the previous.

Algorithm for finding directions. For simplicity your code for finding the directions, they must be found in a very specific manner that is, in Computer Science, called a greedy algorithm. We start at OSMNode A. Of all of the nodes that are adjacent to that node on any roads, the next node we move to is the one that is closest (in terms of distance as the bird flies) to the goal (OSMNode B). This process is repeated, by always making the next move toward the destination until we are either at the destination, or no adjacent node is closer than the current one, in which case, we return null.


This implementation requires a few "support" methods in the GWDirections class. As always, these must have the exact prototypes as below. These include:

  • public static double distance(OSMNode a, OSMNode b): Return the distance in miles between two OSMNodes. This is calculated as follows. First, we calculate the latitudinal distance (North/South distance): (lata - latb) * 69.172 converts the decimals of latitude into miles. lata is the latitude of node A. Longitudinal distance (East/West distance) is more complicated as it varies depending on the latitude: (lona - lonb) * 69.172 * cosine(averageLatitude). Here, averageLatitude is the average of the two latitudes of the nodes. Please read through the Javadocs for cosine, and make sure you're always using the correct one of radians or decimals, depending on what's required (note, the OSM format gives us lon and lat in decimals).
  • public static OSMNode closerNode(OSMNode a, OSMNode b, OSMNode dest): Takes as arguments two nodes, a and b (not to be mistaken with A and B above), and returns the one that is closer to the dest node.
  • public double totalDistance(OSMNode[] ns): This method takes an array of nodes as an argument, and returns the total distance of the sequence of nodes. That is, the distance of the 0th node from the 1st plus the distance from the 1st to the 2nd, etc.
  • public OSMRoad[] getNodesRoads(OSMNode n): This method returns an array of only the OSMRoads that have the OSMNode passed in as an argument in them. A node that is only in a single road, will return only that road. A node that is an intersection between roads will return all intersecting roads. The order of these roads is in the same order they appear in the .osm file.
  • public OSMNode[] getAdjacentNodes(OSMRoad r, OSMNode n): OSMNode n must be in road r, and this method returns an array of length 2 that includes the two nodes on the road that are adjacent to n, ordered so that a node earlier in this array is earlier in the list of node references in the road description in the .osm file. If n is either the first or last node in the road, then the first, or second node, respectively, is set to null.
  • public OSMNode[] getDirections(OSMNode a, OSMNode b): This is the big method. It uses many of the previous ones to generate the directions using the greedy algorithm spelled out above, starting at node a, and always moving toward node b until we reach it. The nodes that are directly connected via roads connecting a and b. The first node in the list is a, and the last is b. However, if at any point, no progress can be made from a current node toward the destination, we will return null. Please note, that this method can be relatively simple if you make use of the previous two methods to, at the current node, find all other connected nodes, and closerNode to find the one to move to next.
  • public GWDirections(GWMap m): The class's constructor. It takes as an argument, the map that can be used in the other methods.
  • public GWMap getMap(): Return the map that the directions are based on (i.e. the one that was passed into the constructor.


To test your program, the GWDirectionsMain class uses the OSMTest.osm and SEASatGWU.osm files.


One way to test your program, is to visualize your roads and directions. Find the GWMapDraw.java program to help you with that. A sample output with a working program is on the right that displays the SEASatGWU.osm file. Note that you will have to get most of your implementation working to be able to use this, but it simply will output all of the road's nodes (using methods from hw5), and will print out an array of nodes as the magenta and red path. This will help you visualize where your directions are going, and perhaps help you debug.

Please feel free to use this program to help you debug, and modify it as you wish. Please do not submit this file. We will not use it in evaluating your program, and it is only to help debugging.


Notes and grading: Your implementations must compile to get any credit. You must submit at least GWDirections.java and GWDirectionsMain.java (though we will use our own GWDirectionsMain.java). You can optionally submit your GWMap.java, OSMNode.java, and OSMRoad.java if you wish. Submit all files that you require for your program to work. You must include all of the required methods, and they must compile, even if they don't work. Code that doesn't compile will receive zero credit. Please develop the implementation in small chunks. Get the first methods to work before the last. Don't program everything, and hope it will work. Write each method separately, and test it separately. Your implementation will get credit in some manner proportional to how many assertions they pass. Any main methods you use will not be invoked, so do not assume that it executes.

If you simply implement your methods in such a way that they are customized to the specific test cases we provide (i.e. by returning the desired string, but not having any of the logic required by the assignment), you'll receive no credit.

Submission: Please submit your .java files (if you use your code for the previous assignments, please submit it as well) via blackboard.