Last Updated: 2016-06-29 Wed 14:23

CS 310 HW2: Sparse, Listy, Bricky 2048

CHANGELOG:

Wed Jun 29 14:22:59 EDT 2016
The final deadline has been adjusted to Thu 7/7 as the university is on holiday on the original due date Tue 7/5.

1 Overview

This project continues our study of simple collections through development of the game 2048. This second assignment extends the game in two central directions.

The first major addition will be a Sparse Board class which has \(O(T)\) space complexity where \(T\) is the number of non-space tiles. This means that empty boards take a constant amount of space irrespective of how many rows and columns the board possesses. When the board fills up, \(T\) approaches \(R\times C\) and the memory usage of SparseBoard becomes \(O(R\times C)\). Contrast this to DenseBoard which always takes \(O(R\times C)\) space regardless of how many non-space tiles are present. Along with with \(O(T)\) space complexity, the time complexities of SparseBoard shift methods will also be close to \(O(T)\) so that shifting boards with small numbers of tiles will be faster than for DenseBoard.

The primary underlying component of the SparseBoard will be a doubly-linked list. This is not strictly necessary: an ArrayList would suffice for this purpose and perhaps provide better practical performance. However, in order to gain familiarity with how lists and iterators work, it is a requirement that SparseBoard use linked lists and iterators internally to store tiles. Provided as part of the HW distribution is a partial implementation of a doubly-linked list based on the implementation presented in the textbook. Several pieces of this WLinkedList must be completed in order for this version to be fully functional. The interface of WLinkedList is nearly identical to java.util.LinkedList so that while working on other parts of the project, one can use java.util.LinkedList and then later replace it with a completed WLinkedList implementation later.

The second major addition 2048 is immovable tiles which are just what they sound like: tiles that do not change position during shifts. Thus far, our TwoNTile is movable but HW2 introduces a second Tile called a Brick which stays still during shifts. In support of this new game mechanic, PlayText2048 now has additional command line arguments to populate the board with random bricks that prevent tiles from sliding all the way across the board. Immovable tiles require several parts of the existing infrastructure to be modified.

For those that did not complete HW1, the following reference implementation source files will be provided as they are required as is or in modified form to complete HW2: TwoNTile, Game2048. These classes are provided in the ref/ subdirectory of the distribution for those that need them. Using existing versions of these classes from HW1 is perfectly acceptable but be advised that they must be adapted to work with the new setup: minor changes to Game2048 will be required while TwoNTile should work without modification.

A functioning DenseBoard is not required to complete HW2 however some buffer credit will be provided for submitting a version of DenseBoard which correctly handles immovable tiles. This will be examined in a set of optional tests. Naturally completing this optional portion is easier if one already has a working DenseBoard implementation.

2 Project Files

The following files are relevant to the HW. When submitting, place them in your HW Directory, zip that directory and submit. *Always submit a zip file,* not tar.gz, not bzip, not 7zip, not your favorite compression). For additional instructions see Setup and Submission.

File State Notes
WLinkedList.java Modify A partial implementation of a doubly linked list, used by SparseBoard
     
Tile.java Provided Abstract base class for tiles, now includes an isMovable() method
TwoNTile.java Create Tiles with powers of 2, add an equals() method to the HW1 version
Brick.java Create An immovable tile which does not merge with anything
     
Board.java Provided Abstract base class for tile boards, now includes a debugString() method
SparseBoard.java Create A board taking O(T) space and near O(T) time shifts, works with Bricks
DenseBoard.java Optional Dense board of tiles adapted to work with Bricks
     
Game2048.java Create Game logic/state to run a game of 2048, includes constructor options to use
    either a Dense or Sparse board internally.
     
PlayText2048.java Provided Main loop to play 2048 using on the console, now allows Bricks
PlayGUI2048.java Provided Main loop to play 2048 graphically, now allows Bricks
     
ref/* Provided Directory of reference implementations for some required HW1 classes
     
ID.txt Edit Identifying information

3 Class Architecture

The architecture for 2048 established in HW1 is re-used again here. You may re-use your own code or adopt some of the provide reference implementation files if needed.

  • Replace your copies of Tile.java and Brick.java with the update versions which contain a couple new methods.
  • Start work on WLinkedList as it is partially complete but requires methods marked REQUIRED to be completed. Most of the tests in HW2MilestoneTests concern WLinkedList.
  • Work next on Brick which is an immovable, unmergable Tile. HW2MilestoneTests also include tests of Brick,
  • SparseBoard and Brick must be completed
  • Some minor updates to existing classes such as Game2048 constructors that use SparseBoard also marked REQUIRED as they must be added to the existing classes.

Since you will be modifying, updating, and re-using your own code base, it is in your own best interest to code well.

  • Use simple techniques that are transparent on revisiting them.
  • Write comments to explain to yourself your intentions.
  • Use error checking, assertions, and exception throwing to fail early if assumptions are violated.

The remaining subsections give an overview of the concrete classes you must implement. All the listed public methods are required. You are free to write own additional internal methods that have any visibility modifier (public, private, proctected). Doing so will likely make the implementation of the project easier.

3.1 WLinkedList

Most of WLinkedList is already completed but a few REQUIRED parts require attention. SparseBoard will use linked lists for its implementation so start with this class and ensure it is solid.

// WLinkedList class implements a doubly-linked list that is Iterable
// and provides a ListIterator.  It is a drop-in replacement for
// java.util.LinkedList. The provided implementation is based on Mark
// Allen Weiss's code from Data Structures and Problem Solving Using
// Java 4th edition. SOME METHODS REQUIRE DEFINITIONS to complete the
// implementation and are marked REQUIRED.
public class WLinkedList<T> implements Iterable<T>{

  // Construct an empty LinkedList.
  public WLinkedList( ) ;

  // Change the size of this collection to zero.
  public void clear( );

  // Returns the number of items in this collection.
  // @return the number of items in this collection.
  public int size( );

  // Tests if some item is in this collection.
  // @param x any object.
  // @return true if this collection contains an item equal to x.
  public boolean contains( Object x );

  // Adds an item to this collection, at the end.
  // @param x any object.
  // @return true.
  public boolean add( T x );

  // Add all elements in the iterable object c
  public boolean addAll(Iterable<T> c);

  // Adds an item to this collection, at specified position.
  // Items at or after that position are slid one position higher.
  // @param x any object.
  // @param idx position to add at.
  // @throws IndexOutOfBoundsException if idx is not between 0 and size(), inclusive.
  public void add( int idx, T x );

  // Adds an item to this collection, at front.
  // Other items are slid one position higher.
  // @param x any object.
  public void addFirst( T x );

  // Adds an item to this collection, at end.
  // @param x any object.
  public void addLast( T x );

  // Returns the first item in the list.
  // @throws NoSuchElementException if the list is empty.
  public T getFirst( );

  // Returns the last item in the list.
  // @throws NoSuchElementException if the list is empty.
  public T getLast( );

  // Returns the item at position idx.
  // @param idx the index to search in.
  // @throws IndexOutOfBoundsException if index is out of range.
  public T get( int idx );

  // Changes the item at position idx.
  // @param idx the index to change.
  // @param newVal the new value.
  // @return the old value.
  // @throws IndexOutOfBoundsException if index is out of range.
  public T set( int idx, T newVal );

  // Removes the front item in the queue.
  // @return the front item.
  // @throws NoSuchElementException if the list is empty.
  public T remove( );

  // Removes the first item in the list.
  // @return the item was removed from the collection.
  // @throws NoSuchElementException if the list is empty.
  public T removeFirst( );

  // Removes the last item in the list.
  // @return the item was removed from the collection.
  // @throws NoSuchElementException if the list is empty.
  public T removeLast( );

  // Removes an item from this collection.
  // @param x any object.
  // @return true if this item was removed from the collection.
  public boolean remove( Object x );

  // Removes an item from this collection.
  // @param idx the index of the object.
  // @return the item was removed from the collection.
  public T remove( int idx );

  // Tests if this collection is empty.
  // @return true if the size of this collection is zero.
  public boolean isEmpty( );

  public final boolean equals( Object other );

  // Return the hashCode.
  public final int hashCode( );

  // Return a string representation of this collection.
  public String toString( );

  // REQUIRED: Transfer all contents of other to the end of this
  // list. Completely empties list other of all elements.
  // 
  // TARGET COMPLEXITY: O(1)
  public void transferFrom(WLinkedList<T> other);

  // REQUIRED: Produce a new list which combines all elements from the
  // lists in the parameter array.  All lists in the parameter array
  // lists[] are completely emptied.
  // 
  // TARGET COMPLEXITY: O(N)
  // N: the size of the parameter array lists[]
  public static <T> WLinkedList<T> coalesce(WLinkedList<T> lists[]);

  // Obtains an Iterator object used to traverse the collection.
  // @return an iterator positioned prior to the first element.
  public Iterator<T> iterator( );

  // Obtains a ListIterator object used to traverse the collection bidirectionally.
  // @return an iterator positioned prior to the requested element.
  // @param idx the index to start the iterator. Use size() to do complete
  // reverse traversal. Use 0 to do complete forward traversal.
  // @throws IndexOutOfBoundsException if idx is not between 0 and size(), inclusive.
  public ListIterator<T> listIterator( int idx );

  // Obtains a ListIterator object used to traverse the collection bidirectionally.
  // @return an iterator positioned prior to the first element
  public ListIterator<T> listIterator( );

  // This is the implementation of the LinkedListIterator.
  // It maintains a notion of a current position and of
  // course the implicit reference to the LinkedList.
  public class LinkedListIterator implements ListIterator<T>{

    // Construct an iterator
    public LinkedListIterator( int idx );

    // Can the iterator be moved to the next() element
    public boolean hasNext( );

    // Move the iterator forward and return the passed-over element
    public T next( );

    // Remove the item that was most recently returned by a call to
    // next() or previous().
    public void remove( ) ;

    // REQUIRED: Can the iterator be moved with previous()
    // 
    // TARGET COMPLEXITY: O(1)
    public boolean hasPrevious( );

    // REQUIRED: Move the iterator backward and return the passed-over
    // element
    // 
    // TARGET COMPLEXITY: O(1)
    public T previous( );

    // REQUIRED: Add the specified data to the list before the element
    // that would be returned by a call to next()
    // 
    // TARGET COMPLEXITY: O(1)
    public void add(T x);

    // OPTIONAL: Set the data associated with the last next() or
    // previous() call to the specified data
    public void set(T x);

    // OPTIONAL: Return the integer index associated with the element
    // that would be returned by next()
    public int nextIndex();

    // OPTIONAL: Return the integer index associated with the element
    // that would be returned by previous()
    public int previousIndex();

  }

}

3.2 Brick

// Concrete implementation of a Tile. Bricks do not merge with
// anything and do not move.
public class Brick extends Tile {

  // Create a Brick
  public Brick();  }

  // Should always return false
  public boolean mergesWith(Tile moving);

  // Should throw an informative runtime exception as bricks never
  // merge with anything
  public Tile merge(Tile moving);

  // Always 0
  public int getScore();

  // Always false
  public boolean isMovable();

  // Return the string "BRCK"
  public String toString();

  // true if the other is a Brick and false otherwise
  public boolean equals(Object other);

}

3.3 TwoNTile

An equals() method has been added.

// Concrete implementation of a Tile. TwoNTiles merge with each other
// but only if they have the same value.
public class TwoNTile extends Tile {

  // Create a tile with the given value of n; should be a power of 2
  // though no error checking is done
  public TwoNTile(int n);

  // Returns true if this tile merges with the given tile. "this"
  // (calling tile) is assumed to be the stationary tile while moving
  // is presumed to be the moving tile. TwoNTiles only merge with
  // other TwoNTiles with the same internal value.
  public boolean mergesWith(Tile moving);

  // Produce a new tile which is the result of merging this tile with
  // the other. For TwoNTiles, the new Tile will be another TwoNTile
  // and will have the sum of the two merged tiles for its value.
  // Throw a runtime exception with a useful error message if this
  // tile and other cannot be merged.
  public Tile merge(Tile moving);

  // Get the score for this tile. The score for TwoNTiles are its face
  // value.
  public int getScore();

  // Return a string representation of the tile
  public String toString();

  // REQUIRED: Determine if this TwoNTile is equal to another object
  // which is only true when the other object is a TwoNTile with the
  // same value as this tile. Required for tests to work correctly.
  public boolean equals(Object other);

}

3.4 SparseBoard

This is the main class that requires implementation in for HW2.

// Tracks the positions of an arbitrary 2D grid of Tiles.  SparseBoard
// uses internal linked lists of tile coordinates to track only the
// tiles that are non-empty.  A full-credit implementation will make
// use of a completed WLinkedList.
public class SparseBoard extends Board {

  // Build a Board of the specified size that is empty of any tiles
  public SparseBoard(int rows, int cols);

  // Build a board that copies the 2D array of tiles provided Tiles
  // are immutable so can be referenced without copying.  Use internal
  // lists of tile coordinates.  Ignore empty spaces in the array t
  // which are indicated by nulls.
  public SparseBoard(Tile t[][]);

  // Create a distinct copy of the board including its internal tile
  // positions and any other state
  // 
  // TARGET COMPLEXITY: O(T)
  // T: the number of non-empty tiles in the board
  public Board copy();

  // Return the number of rows in the Board
  // TARGET COMPLEXITY: O(1)
  public int getRows();

  // Return the number of columns in the Board
  // TARGET COMPLEXITY: O(1)
  public int getCols();

  // Return how many tiles are present in the board (non-empty spaces)
  // TARGET COMPLEXITY: O(1)
  public int getTileCount();

  // Return how many free spaces are in the board
  // TARGET COMPLEXITY: O(1)
  public int getFreeSpaceCount();

  // Get the tile at a particular location.  If no tile exists at the
  // given location (free space) then null is returned. Throw a
  // runtime exception with a useful error message if an out of bounds
  // index is requested.
  // 
  // TARGET COMPLEXITY: O(T)
  // T: The number of non-empty tiles in the board 
  public Tile tileAt(int i, int j) ;

  // true if the last shift operation moved any tile
  // false otherwise
  // TARGET COMPLEXITY: O(1)
  public boolean lastShiftMovedTiles();

  // Return true if a shift left, right, up, or down would merge any
  // tiles. If no shift would cause any tiles to merge, return false.
  // The inability to merge anything is part of determining if the
  // game is over.
  // 
  // TARGET COMPLEXITY: O(T)
  // T: The number of non-empty tiles in the board 
  public boolean mergePossible();

  // Add a the given tile to the board at the "freeL"th free space.
  // Free spaces are numbered 0,1,... from left to right accross the
  // columns of the zeroth row, then the first row, then the second
  // and so forth. For example the board with following configuration
  // 
  //    -    -    -    - 
  //    -    4    -    - 
  //   16    2    -    2 
  //    8    8    4    4 
  // 
  // has its 9 free spaces numbered as follows
  // 
  //    0    1    2    3 
  //    4    .    5    6 
  //    .    .    7    . 
  //    .    .    .    . 
  // 
  // where the dots (.) represent filled tiles on the board.
  // 
  // Calling addTileAtFreeSpace(6, new Tile(32) would leave the board in
  // the following state.
  // 
  //    -    -    -    - 
  //    -    4    -   32 
  //   16    2    -    2 
  //    8    8    4    4 
  // 
  // Throw a runtime exception with an informative error message if a
  // location that does not exist is requested.
  // 
  // TARGET RUNTIME COMPLEXITY: O(T + max(R,C))
  // TARGET SPACE COMPLEXITY:   O(T + max(R,C))
  // T: the number of non-empty tiles in the board
  // R: number of rows
  // C: number of columns
  public void addTileAtFreeSpace(int freeL, Tile tile) ;

  // Pretty-printed version of the board. Use the format "%4s " to
  // print the String version of each tile in a grid.
  // 
  // TARGET COMPLEXITY: O(R * C)
  // R: number of rows
  // C: number of columns
  public String toString();

  // OPTIONAL: A string that shows some internal state of the board
  // which you find useful for debugging. Will be shown in some test
  // cases on failure.  No required format. Suggestion: contents of
  // any internal lists.
  public String debugString();

  // Shift the tiles of Board in various directions.  Any tiles that
  // collide and should be merged should be changed internally in the
  // board.  Shifts only remove tiles, never add anything.  The shift
  // methods also set the state of the board internally so that a
  // subsequent call to lastShiftMovedTiles() will return true if any
  // Tile moved and false otherwise.  The methods return the score
  // that is generated from the shift which is the sum of the scores
  // all tiles merged during the shift. If no tiles are merged, the
  // return score is 0.
  // 
  // TARGET RUNTIME COMPLEXITY: O(T + max(R,C))
  // TARGET SPACE COMPLEXITY:   O(T + max(R,C))
  // T: the number of non-empty tiles in the board
  // R: number of rows
  // C: number of columns
  public int shiftLeft();

  public int shiftRight();

  public int shiftUp();

  public int shiftDown();

}

3.5 Game2048

Two new constructors that allow the use of SparseBoard internally during the game are REQUIRED to complete the implementation.

// Represents the internal state of a game of 2048 and allows various
// operations of game moves as methods. Uses TwoNTiles and DenseBoard
// to implement the game.
public class Game2048{

  // Create a game with a DenseBoard with the given number of rows and
  // columns. Initialize the game's internal random number generator
  // to the given seed.
  public Game2048(int rows, int cols, int seed) ;

  // Create a game with a DenseBoard which has the given arrangement
  // of tiles. Initialize the game's internal random number generator
  // to the given seed.
  public Game2048(Tile tiles[][], int seed) ;

  // REQUIRED: The final parameter indicates whether a SparseBoard
  // (true) or a DenseBoard (false) should be used during the game
  // 
  // Create a game with a DenseBoard with the given number of rows and
  // columns. Initialize the game's internal random number generator
  // to the given seed.
  public Game2048(int rows, int cols, int seed, boolean useSparse) ;

  // REQUIRED: The final parameter indicates whether a SparseBoard
  // (true) or a DenseBoard (false) should be used during the game
  // 
  // Create a game with a DenseBoard which has the given arrangement
  // of tiles. Initialize the game's internal random number generator
  // to the given seed.
  public Game2048(Tile tiles[][], int seed, boolean useSparse) ;

  // Return the number of rows in the Game
  public int getRows();

  // Return the number of columns in the Game
  public int getCols();

  // Return the current score of the game.
  public int getScore();

  // Return a string representation of the board; useful for text UIs
  // like PlayText2048
  public String boardString();

  // Return the tile at a given position in the grid; throws an
  // exception if the request is out of bounds. Potentially useful for
  // more complex UIs which want to lay out tiles individually.
  public Tile tileAt(int i, int j);

  // Shift tiles left and update the score
  public void shiftLeft();

  // Shift tiles right and update the score
  public void shiftRight();

  // Shift tiles up and update the score
  public void shiftUp();

  // Shift tiles down and update the score
  public void shiftDown();

  // Generate and return a random tile according to the probability
  // distribution. 
  //    70% 2-tile
  //    25% 4-tile
  //     5% 8-tile
  // Use the internal random number generator for the game.
  public Tile getRandomTile();

  // If the game board has F>0 free spaces, return a random integer
  // between 0 and F-1.  If there are no free spaces, throw an
  // exception.
  public int randomFreeLocation();

  // Add a random tile to a random free position. To adhere to the
  // automated tests, the order of calls to random methods MUST BE
  // 
  // 1. Generate a random location using randomFreeLocation()
  // 2. Generate a random tile using getRandomTile()
  // 3. Add the tile to board using one of its methods
  public void addRandomTile();

  // REQUIRED: Add a brick at a random location.
  // 
  // 1. Generate a random location using randomFreeLocation()
  // 2. Add the Brick to board using one of its methods
  public void addRandomBrick();

  // Returns true if the game over conditions are met (no free spaces,
  // no merge possible) and false otherwise
  public boolean isGameOver();

  // true if the last shift moved any tiles and false otherwise
  public boolean lastShiftMovedTiles();

  // Optional: pretty print some representation of the game. No
  // specific format is required, used mainly for debugging purposes.
  public String toString();

}

3.6 PlayText2048 and PlayGUI2048

No changes are necessary but the command line conventions in PlayText2048 have been altered to allow bricks in interactive games and allow the selection of sparse boards. Options for these are also now present in PlayText2048.

// Class to play a single game of 2048 with bricks
public class PlayText2048 {

  // Play a game of 2048 of the given size. Allows one to specify the
  // a number of random bricks, whether to use a sparse/dense board
  // and a random seed.
  // 
  // usage: java PlayText2048 rows cols bricks {sparse|dense} [random-seed]
  //   rows/cols: the size of the board [int]
  //   bricks: the number of immovable bricks to add to the board, 0 for none [int]
  //   {sparse|dense}: use either a sparse or dense board ["sparse" or "dense"]
  //   random-seed: used to initialize the random number generator [int]
  public static void main(String args[]);

}
> java PlayGUI2048

3.7 DenseBoard

There are no interface changes in DenseBoard from HW1. It is NOT REQUIRED to complete HW2. However, to garner the buffer credit, adjust the internal DenseBoard logic to account for immovable tiles such as Brick.

4 A Doubly Linked List: WLinkedList

public class WLinkedList<T> implements Iterable<T>

This class implements a doubly linked list and is based on Weiss's linked list from Chapter 17 of Data Structures and Problem Solving in Java, 4th ed. The following modifications have been made.

  • The class has been slightly rearranged to remove dependencies on Weiss's recreation of the collections class hierarchy. WLinkedList is mostly stand-alone except for dependencies on a few java.util classes such as java.util.Iterator and java.util.ListIterator
  • The class implements the Iterable interface so that it can be used in the java for-each loop.
  • Weiss uses headed lists so that there are always two special nodes in the list. These are called the beginMarker and endMarker. The are used to denote the front and back of the list and do not contain any data. You will need to carefully manipulate their prev/next pointers and the pointers of other nodes to complete required WLinkedList methods.
  • The internal iterator class, LinkedListIterator is incomplete: methods marked as REQUIRED must be written in order to complete the class. This will give you some familiarity with the inner workings of linked lists and iterators.
  • Two new methods require implementation. transferFrom() and coalesce() deal with moving elements from one list to another efficiently by manipulating node pointers.

4.1 List Iterators

public class WLinkedList<T> implements Iterable<T>{
  public Iterator<T> iterator( );
  public ListIterator<T> listIterator( int idx );
  public ListIterator<T> listIterator( );

  public class LinkedListIterator implements ListIterator<T>{
    ...
  }
}

WLinkedList provides a set of iteration capabilities that allow one to efficiently scan through the elements. As discussed in lecture, implementing the Iterable interface and providing and iterator() method enables efficient enumeration of elements using loops, including java's for-each loop. For example

WLinkedList<String> l = new WLinkedList<String>();
l.add("Goodbye");  l.add("cruel"); l.add("world");
for(String s : l){
  System.out.println(s);
}

For finer-grained manipulation, the listIterator() method provides a ListIterator that can move forward with next(), backward with previous(), and modify the list with add(x) and remove(). These capabilities will be required in order to efficiently implement the shifts in SparseBoard as repeated calls to get(i) will will not meet the runtime complexity target, An overview of the ListIterator concept is described in the official Java Collections Tutorial.

The iteration capabilities are implemented in an internal class called LinkedListIterator. It has several methods with required definitions concerning moving backwards through the list and addition to the list. All of these are necessary to allow for shifts in arbitrary directions in SparseBoard (both left and right, up and down).

4.2 The transferFrom() and coalesce() methods

// TARGET COMPLEXITY: O(1)
public void transferFrom(WLinkedList<T> other);

This is a simple method which takes all elements from another list called other and appends them to the calling list. The argument other is emptied of all elements. Examples are below.

Welcome to DrJava.  Working directory is /windows/Dropbox/teaching/cs310-F2014/hw/hw2/ckauffm2-hw2
> WLinkedList x = new WLinkedList();
> x.add("Earth"); x.add("Wind"); x.add("Water");
> WLinkedList y = new WLinkedList();
> y.add("Fire"); y.add("Heart");
> x
[Earth, Wind, Water ]
> y
[ Fire Heart ]
> x.transferFrom(y)
> x
[Earth, Wind, Water, Fire, Heart ]
> y
[]
> WLinkedList z = new WLinkedList();
> z.add("Captain Planet")
true
> x
[Earth, Wind, Water, Fire, Heart ]
> z
[Captain Planet ]
> z.transferFrom(x)
> x
[ ]
> z
[Captain Planet, Earth, Wind, Water, Fire, Heart ]

The naive way of performing transferFrom(other) is to iterate through all of the elements of other removing them one at a time and appending them to the calling list. However, in order to meet the \(O(1)\) runtime complexity target, one must instead directly manipulate pointers internal to the lists to complete the transfer. Don't forget to update other fields of the lists such as the sizes and whether the lists have been modified.

// TARGET COMPLEXITY: O(N)
// N: the size of the parameter array lists[]
public static <T> WLinkedList<T> coalesce(WLinkedList<T> lists[])

The coalesce() method creates a new list comprised of all elements from the lists in the parameter array lists[]. All lists within the array are emptied completely.

> WLinkedList x = new WLinkedList();
> x.add("Earth"); x.add("Wind"); x.add("Water");
> WLinkedList y = new WLinkedList();
> y.add("Fire"); y.add("Heart");
> WLinkedList z = new WLinkedList();
> z.add("Captain Planet")
> WLinkedList plists[] = new WLinkedList[]{x, y, z};
> plists
{ [ Earth Wind Water ], [ Fire Heart ], [ Captain Planet ] }
> WLinkedList planeteers = WLinkedList.coalesce(plists);
> x
[]
> y
[]
> z
[]
> planeteers
[ Earth, Wind, Water, Fire, Heart, Captain Planet ]

Notice that each of the lists x,y,z is added to an array of lists which is then coalesced into the list planeteers. This leaves planeteers with all of the contents of all of the lists while the lists x,y,z are all emptied.

Employing repeated calls to transferFrom() is an ideal means to implement coalesce() in order to meet the complexity bounds.

5 Immovable Tiles

Tiles are immovable if they answer false to the new method call isMovable() which has been added to the Tile parent class. The parent implementation returns true so TwoNTile will be movable by default. However, a new class called Brick should override this method to return false indicating it is not movable.

Bricks are displayed as the string BRCK. Bricks do not move during shifts and do not merge with other tiles. For example the following 5 by 3 board has 2 bricks.

   -    - BRCK 
   -    -    - 
   - BRCK    - 
   -    -    - 
   -    -    2

A shift LEFT leads to

   -    - BRCK 
   -    -    - 
   - BRCK    - 
   -    -    - 
   2    -    -

A subsequent shift UP leads to

   2    - BRCK 
   -    -    - 
   - BRCK    - 
   -    -    - 
   -    -    -

and a final shift RIGHT gives

   -    2 BRCK 
   -    -    - 
   - BRCK    - 
   -    -    - 
   -    -    -

Bricks serve as barriers preventing tiles from moving all the way across the board. Depending on brick placement, this can create a partitioned board where it is not possible for some tiles to ever reach others such as the following 2 by 7 example.

   -    -    - BRCK    -    -    - 
   -    2 BRCK    -    2 BRCK    -

During a game, TwoNTiles will be placed on either the left side or the right side but cannot cross over due to the bricks.

The provided class PlayText2048 allows one to play rounds of 2048 with a specified number of randomly placed bricks on the board. Below is a demo run of PlayText2048 with a 4 by 4 board and 1 brick. At the command line, one can also specify that a sparse or dense board is used though the end uses will not see any difference due to this choice during gameplay as the choice only affects game internals. The example uses a sparse board.

aphaedrus [ckauffm2-hw2]% java PlayText2048
usage: java PlayText2048 rows cols bricks {sparse|dense} [random-seed]
  rows/cols: the size of the board [int]
  bricks: the number of immovable bricks to add to the board, 0 for none [int]
  {sparse|dense}: use either a sparse or dense board ["sparse" or "dense"]
  random-seed: used to initialize the random number generator [int]

aphaedrus [ckauffm2-hw2]% java PlayText2048 4 4 1 sparse
Instructions
------------
Enter moves as l r u d q for
l: shift left
r: shift right
u: shift up
l: shift down
q: quit game

Score: 0
   -    -    -    - 
BRCK    -    -    2 
   -    -    -    - 
   -    -    4    2 

Move: u
u
Score: 4
   -    -    4    4 
BRCK    -    -    - 
   -    -    -    - 
   8    -    -    - 

Move: l
l
Score: 12
   8    -    -    - 
BRCK    -    -    - 
   -    -    -    - 
   8    -    -    4 

Move: u
u
Score: 12
   8    -    -    4 
BRCK    -    -    - 
   8    -    -    - 
   -    -    -    4 

Move: u
u
Score: 20
   8    2    -    8 
BRCK    -    -    - 
   8    -    -    - 
   -    -    -    - 

Move: r
r
Score: 20
   -    8    2    8 
BRCK    -    -    - 
   -    -    -    8 
   -    -    2    - 

Move: u
u
Score: 40
   -    8    4   16 
BRCK    4    -    - 
   -    -    -    - 
   -    -    -    - 

Move: d
d
Score: 40
   -    -    -    - 
BRCK    4    -    - 
   -    8    -    - 
   -    4    4   16 

Move: l
l
Score: 48
   -    -    -    - 
BRCK    4    -    - 
   8    -    2    - 
   8   16    -    - 

Move: d
d
Score: 64
   -    -    -    - 
BRCK    -    -    - 
   4    4    -    - 
  16   16    2    - 

Move: l
l
Score: 104
   -    2    -    - 
BRCK    -    -    - 
   8    -    -    - 
  32    2    -    - 

Move: q
Score: 104
   -    2    -    - 
BRCK    -    -    - 
   8    -    -    - 
  32    2    -    - 

Game Over! Final Score: 104

6 SparseBoard

public class SparseBoard extends Board

The SparseBoard class extends Board and thus behaves to the outside world identically to HW1's DenseBoard: it has constructors, tile retrieval, shift operations, the ability to check for tiles moving, and so on. The changes are all internal. Rather than use a 2D array to provide space for all tiles, SparseBoard will keep a list of only the non-empty (non-space) tiles presently on the board and where those tiles are located.

6.1 Internal Representation

Consider the 4 by 7 board below.

   -    2    -    -    -    4    8 
   -    2    -    -    - BRCK    - 
   -   16    -    -    -   32   16 
  64    -    -    -    -    -    -

Instead of using a 2D array with null in the empty spaces, a SparseBoard should simply track the locations of each non-empty tile. There are 9 tiles on the board which means the list likely contains the following triplets.

[( 0,  1,    2)( 0,  5,    4)( 0,  6,    8)( 1,  1,    2)( 1,  5, BRCK)
 ( 2,  1,   16)( 2,  5,   32)( 2,  6,   16)( 3,  0,   64)]

Each triplet is in the form (row, col, tile). Any row/col position not in this list is assumed to be empty.

The above listing has a specific ordering which is worth noting: everything in row 0 is listed first, then row 1, then row 2, and so on with the tiles being ordered within a row by their column. This arrangement is typically referred to as row major order as rows matter, then columns. It is complemented by column major order which orders by column first, then row. Both orderings for the board above are shown below.

rowMajor: [( 0,  1,    2)( 0,  5,    4)( 0,  6,    8)( 1,  1,    2)( 1,  5, BRCK)
           ( 2,  1,   16)( 2,  5,   32)( 2,  6,   16)( 3,  0,   64)]

colMajor: [( 3,  0,   64)( 0,  1,    2)( 1,  1,    2)( 2,  1,   16)( 0,  5,    4)
           ( 1,  5, BRCK)( 2,  5,   32)( 0,  6,    8)( 2,  6,   16)]

It is suggested but not required that SparseBoard keep track of both orderings at once. This is not strictly necessary but simplifies reasoning about the class somewhat. Tracking both will require \(O(2\times T) = O(T)\) space and will not be penalized as it adheres to the space complexity bound. You may attempt to keep track of only one of these representations at once to lower the memory footprint but be advised that this will not garner additional credit and will result in a more difficult implementation.

6.2 Representation of Tile Coordinates

A small auxiliary class to hold tiles and their coordinates is useful for tracking tile positions. As with Node inside LinkedList classes, consider adding some sort of tile holder class within the SparseBoard class which carries with it a row, column, and tile. Such a holder class can then be inserted into lists and will carry all tile information with it.

6.3 Lists of Coordinates

It is a requirement that the WLinkedList class be used for lists of tile coordinates. Failure to do so will result in a loss of credit.

  • Using java.util.ArrayList will be penalized heavily
  • Using java.util.LinkedList will be more modestly penalized

That said, while you are developing SparseBoard, you should feel free to use either of ArrayList or LinkedList initially to hold tile coordinates. WLinkedList is especially designed to be interchangeable with java.util.LinkedList: the two classes share many of the same method names and iterator capabilities. While working on SparseBoard, one can use java.util.LinkedList and later replace it with WLinkedList once that class is in working order. This is how the reference implementation was built: working with java.util.LinkedList to verify that SparseBoard was working correctly and then replacing LinkedList with WLinkedList and making corrections to WLinkedList when bugs arose.

The exact arrangement of coordinate lists is a design decision that is left to you. Options include two lists of tiles in row-major and column-major order, sets of nested (2D) lists for row/col major ordering, or a single list of coordinates that flips between row and column major order on demand. However, take care that you abide by the \(O(T)\) space constraint of SparseBoard. If you find that you have a list that is always the size of the number of rows or columns, you will have missed the target space complexity.

6.4 Tile Retrieval

  // TARGET COMPLEXITY: O(T)
  // T: The number of non-empty tiles in the board 
  public Tile tileAt(int i, int j);

Unlike DenseBoard, the tile at position (i,j) does not have a predefined memory location. Thus it must be searched for through the lists of tile coordinates. This means tile retrieval using tileAt(i,j) is not constant but \(O(T)\) in the worst case. You will need to avoid repeated calls to tileAt(i,j) in several places in order to adhere to the complexity bounds of other methods.

The best way to do avoid repeated tileAt(i,j) calls is to utilize a ListIterator (java docs) which abstracts away the position in a list. Read more about list iterators in the associated section.

6.5 Printing and Debug Strings

  // Pretty-printed version of the board. Use the format "%4s " to
  // print the String version of each tile in a grid.
  // 
  // TARGET COMPLEXITY: O(R * C)
  // R: number of rows
  // C: number of columns
  public String toString();

The toString() method of SparseBoard should produce the same kind of board representation that DenseBoard did which is a textual display of the board as a 2D grid. This display is appropriate for showing the board during a text game. Note the time complexity is \(O(R\times C)\) as it is necessary to populate the returned string with blanks in all spaces. However, repeated calls to tileAt(i,j) will no meet the time complexity of the method.

  // OPTIONAL: A string that shows some internal state of the board
  // which you find useful for debugging. Will be shown in some test
  // cases on failure.  No required format. Suggestion: contents of
  // any internal lists.
  public String debugString();

Since the internal representation of SparseBoards is quite different from the picture produced by its toString() method, inquiring about the internal state is somewhat more difficult. For that reason, the test cases will call the debugString() method when test fails and print its results. This method should be supplied and produce some useful display of how the board looks from the inside which can be used for debugging purposes. It is optional and need only be present to garner credit for it, but it is a hook which allows one to customize display or behavior to ease the burden of figuring out what is wrong. Good candidates for printing are any internal lists that store tile coordinates, the virtual size of the board (rows/cols), and whether the last shift moved tiles.

6.6 Shifting and Merging with Coordinate Lists

Shifting and merging are the significant operations of any Board. SparseBoard needs to implement shiftLeft()/shiftRight()/shiftUp()/shiftDown() using its internal lists of tile coordinates rather than by adjusting the positions of tiles in a 2D array. These methods have an interesting time complexity and also a space complexity constraint: \(O(T + \max(R,C))\).

// TARGET RUNTIME COMPLEXITY: O(T + max(R,C))
// TARGET SPACE COMPLEXITY:   O(T + max(R,C))
// T: the number of non-empty tiles in the board
// R: number of rows
// C: number of columns
public int shiftLeft();
public int shiftRight();
public int shiftUp();
public int shiftDown();

This time/space complexity has the following consequences:

  • While calculating a shift, only a constant number of passes through the list of non-space tiles is allowed (one or two passes is preferred). Repeated tileAt(i,j) calls will not meet the target complexity.
  • In addition, a constant number of passes over all rows or all columns is allowed.
  • For auxiliary space, one can create a second list of tiles or establish a list or array that is the size of the number of rows or columns.
  • A doubly nested for loops that visits each tile more than once will miss the target runtime complexity.
  • Creating a dense 2D grid of tiles and scanning through all of them (as done in DenseBoard) will miss both the time complexity target and the space complexity target.

The suggested approach for shifts is as follows

  • For horizontal shifts (left/right), work with a list that is in row major order.
  • Use one or two passes over the tile coordinates to merge any tiles that require it and change the positions of tiles to reflect their new location
  • Don't forget to account for immovable tiles like Brick
  • Once the row major list of tile coordinates has been adjusted for the shift, use it to completely rebuild the column major list.
  • Rebuilding the column major list is possible within the time and space complexity by making use of an auxiliary array sized by the number of columns. Each element of the array is a linked list to which tile coordinates are added.
  • The array of lists is finally merged together using the coalesce() method of WLinkedList and the result becomes the new column major tile list.
  • For vertical shifts (up/down) follow the same strategy except use the column major list of coordinates to perform shifts/merges then rebuild the row major tile list.

As in DenseBoard, the order of goodness for shift implementations for SparseBoard is roughly as follows

  • Compilable
  • Compiles against the test cases
  • Passes the test cases (including those for immovable tiles)
  • Correct and each of left/right/up/down is easy to follow
  • Correct and left/right/up/down share code in a master shift

Full credit will be awarded for finding ways for these similar methods to share code likely through the use of a special internal iterator-like class (though not necessarily a ListIterator).

6.7 Adding Tiles at Free Spaces

  // Add a the given tile to the board at the "freeL"th free space.
  // 
  // ALTERED FROM ORIGINAL VERSION OF SPEC Wed Oct  1 17:59:23 EDT 2014 
  // TARGET RUNTIME COMPLEXITY: O(T + max(R,C))
  // TARGET SPACE COMPLEXITY:   O(T + max(R,C))
  // T: the number of non-empty tiles in the board
  // R: number of rows
  // C: number of columns
  public void addTileAtFreeSpace(int freeL, Tile tile);

Addition of tiles to the board uses the same general scheme as was used in DenseBoard: free board spaces are numbered 0 to L-1 and tiles are placed by picking an integer in this range and requesting the addition. In order to get the tile added, the SparseBoard must scan through its list of non-space tiles and very carefully determine where to insert the new tile. This routine is deceptively difficult in that it requires careful attention to index counting.

As an example, consider the following board.

   -    4    -    - 
   - BRCK    -    2 
   -    -    8    -

It's free spaces are numbered

   0    .    1    2 
   3    .    4    . 
   5    6    .    7

It is likely that internally there is stored a list of the row major tiles that are not spaces. This list looks like the following.

[( 0,  1,    4)( 1,  1, BRCK)( 1,  3,    2)( 2,  2,    8)]

The general approach to complete a call such as

b.addTileAtFreeSpace(6, new TwoNTile(16))

is to begin scanning through the row major list of tile positions tracking how many free spaces have been passed over.

  • The first coordinate is (0,1, 4) which means 1 free space has been passed over bringing the total free spaces to 1.
  • The next coordinate is (1,1, BRCK) which means 3 free spaces have been passed over bringing the total free spaces to 4.
  • The next coordinate is (1,3, 2) which means 1 free space has been passed over bringing the total free spaces to 5.
  • The next coordinate is (2,2, 8) which means 2 free spaces have been passed over bringing the total free spaces to 7.
  • Since we are adding at free space 6 and 7 free spaces have been passed over, the entry (2,1, 16) should be added prior to the current coordinate.
  • The column major list (if present) should be updated or rebuilt after this change.

Getting this operation correct will require careful counting of the free spaces between non-space tiles the row major list.

Hint: While calculating spacing, consider converting the 2D row/col coordinates into a single 1D position as if everything were laid out in one long array. This can simplify calculating the number of "spaces" in between entries.

7 Grading

Grading for this HW will be divided into three distinct parts:

  • Part of your grade will be based on passing some automated test cases by an early "milestone" deadline. See the top of the HW specification for
  • Part of your grade will be based on passing all automated test cases by the final deadline
  • Part of your grad will be based on a manual inspection of your code and analysis documents by the teaching staff to determine quality and efficiency.

7.1 Milestone Automated Tests (5%)

  • Prior to the final HW deadline, some credit will be garnered by submitting portions of the HW and passing some automated test cases associated with that portion
  • The file HW1MilestoneTests.java contains tests which will be used for the HW milestone. These tests are duplicates of tests in some other testing files.
  • This early deadline is to encourage you to begin your work early on the project and make consistent incremental steps towards its completion.
  • For details of automated tests, see the next section
  • No manual inspection is done at the HW milestone, only automated tests.
  • Milestone tests will not be used for the final grading.
  • No late submissions are accepted for milestones and the submission link be unavailable shortly after the Milestone deadline.

7.2 Final Automated Tests (45%)

  • JUnit test cases will be provided to detect errors in your code. These will be run by a grader on submitted HW after the final deadline.
  • Tests may not be available on initial release of the HW but will be posted at a later time.
  • Tests may be expanded as the HW deadline approaches.
  • It is your responsibility to get and use the freshest set of tests available.
  • Tests will be provided in source form so that you will know what tests are doing and where you are failing.
  • It is up to you to run the tests to determine whether you are passing or not. If your code fails to compile against the tests, little credit will be garnered for this section
  • Most of the credit will be divide evenly among the tests; e.g. 50% / 25 tests = 2% per test. However, the teaching staff reserves the right to adjust the weight of test cases after the fact if deemed necessary.
  • Test cases are typically run from the command line using the following invocation which you should verify works as expected on your own code.
    UNIX Command line instructions
      Compile
    > javac -cp .:junit-cs310.jar *.java
    
    Run tests
    > java -cp .:junit-cs310.jar SomeTests
    
    WINDOWS Command line instructions: replace colon with semicolon
    Compile
    > javac -cp .;junit-cs310.jar *.java
    
    Run tests
    > java -cp .;junit-cs310.jar SomeTests
    

7.3 Final Manual Inspection (50%)

  • Graders will manually inspect your code and analysis documents looking for a specific set of features after the final deadline.
  • Most of the time the requirements for credit will be posted along with the assignment though these may be revised as the the HW deadline approaches.
  • Credit will be awarded for good coding style which includes
    • Good indentation and curly brace placement
    • Comments describing private internal fields
    • Comments describing a complex section of code and invariants which must be maintained for classes
    • Use of internal private methods to decompose the problem beyond what is required in the spec
  • Some credit will be awarded for clearly adhering to the target complexity bounds specified in certain methods. If the specification lists the target complexity of a method as O(N) but your implementation is actually O(N log N), credit will be deducted. If the implementation complexity is too difficult to determine due to poor coding style, credit will likely be deducted.

    All TARGET COMPLEXITIES are worst-case run-times.

  • Some credit will be awarded for turning in any analysis documents that are required by the HW specification. These typically involve analyzing how fast a method should run or how much memory a method requires and are reported in a text document submitted with your code.

8 Final Manual Inspection Criteria

The following features will be evaluated during manual inspection of your code and analysis documents.

8.1 Code Readability (5%)

  • Good indentation and curly brace placement is used.
  • Comments are present describing private internal class fields.
  • Comments describing a complex section of code and invariants which must be maintained for classes
  • Informative variable names are used especially for fields of classes and important local variables. Avoid single letter variable names for class fields.

8.2 Target Complexities of WLinked List Methods (10%)

The following methods of WLinkedList have target complexities. It should be clear from the code that your implementation adheres to the complexity to receive full credit.

public class WLinkedList<T> implements Iterable<T>{
  // TARGET COMPLEXITY: O(1)
  public void transferFrom(WLinkedList<T> other);

  // TARGET COMPLEXITY: O(N)
  // N: the size of the parameter array lists[]
  public static <T> WLinkedList<T> coalesce(WLinkedList<T> lists[]);

  public class LinkedListIterator implements ListIterator<T>{
    // TARGET COMPLEXITY: O(1)
    public boolean hasPrevious( );

    // TARGET COMPLEXITY: O(1)
    public T previous( );

    // TARGET COMPLEXITY: O(1)
    public void add(T x);
  }
}

8.3 SparseBoard Design Clarity (10%)

  • The design of the SparseBoard class is documented and apparent.
  • It is clear what information is tracked in which fields and how they are manipulated during various methods.
  • Internal classes are used to store tile coordinates.
  • Private methods are employed to maintain the correctness of the tile position lists after shifts.

8.4 Appropriate Use of WLinkedList in SparseBoard (10%)

  • Linked lists are used internally to store the tiles.
  • Use Iterators and ListIterators to scan through elements of these lists and avoid the complexity of re-scanning parts already visited.
  • The coalesce() method is employed at appropriate points in SparseBoard such as during shift operations to merge an array of lists together efficiently
  • Relying on java.util.LinkedList will result in loss of credit: it is required that the WLinkedList class be completed and employed.
  • Employing arrays or ArrayLists for anything except local variables in methods will result in large loss of credit.

8.5 Board Shift Methods: Code Clarity/Elegance and Complexity (10%)

  • The implementations of shift clearly adhere to the target runtime complexity of \(O(T + \max(R,C))\) and to the target space complexity of \(O(T + \max(R,C))\).
  • ListIterators are used during shifts in order to efficiently scan through the board.
  • Some practical efficiency is attempted by using coalesce() when possible to reduce the number of passes through the tile lists, particularly when rebuilding the row major tile coordinate list from column major list and vice verse.
  • Full credit will be given if the design is crafted to allow all four shift methods to share code in a master shift. This will likely require an additional iterator class (on top of the iterator classes) as discussed in the HW1 hints.

8.6 Target Complexities of other SparseBoard Methods (5%)

The following methods of SparseBoard have target complexities. It should be clear from the code that your implementation adheres to the complexity to receive full credit.

public class SparseBoard extends Board {
  // TARGET COMPLEXITY: O(T)
  // T: the number of non-empty tiles in the board
  public Board copy();

  // TARGET COMPLEXITY: O(T)
  // T: the number of non-empty tiles in the board
  public Tile tileAt(int i, int j) ;

  // TARGET COMPLEXITY: O(T)
  // T: the number of non-empty tiles in the board 
  public boolean mergePossible();

  // ALTERED FROM ORIGINAL VERSION OF SPEC Wed Oct  1 17:59:23 EDT 2014 
  // TARGET RUNTIME COMPLEXITY: O(T + max(R,C))
  // TARGET SPACE COMPLEXITY:   O(T + max(R,C))
  // T: the number of non-empty tiles in the board
  // R: number of rows
  // C: number of columns
  public void addTileAtFreeSpace(int freeL, Tile tile);

  // TARGET COMPLEXITY: O(R * C)
  // R: number of rows
  // C: number of columns
  public String toString();
}

9 Optional Buffer Credit: DenseBoard and Immovable Tiles (5%)

This section is optional but successful completion will mitigate errors in other required sections.

9.1 Buffer Credit (5%)

Buffer Credit is additional credit that can be garnered to make up for mistakes in other parts of the HW. If credit is lost in a required section, completing the Buffer Credit section replaces the lost credit. Buffer credit will not move the total score for an assignment above 100%.

A special set of tests will be provided to evaluate buffer credit (BufferCreditDenseBoardTests.java). Buffer credit will be proportional to the fraction of tests passed: if there is 5% buffer credit available and 50 tests to pass, passing 20/50 tests will provide 2% buffer credit. These tests are optional but successfully passing them will provide buffer credit for this HW.

9.2 Updates to DenseBoard

The rules of 2048 have been expanded in HW2 to allow for immovable tiles (the Brick). It is only required that a working SparseBoard be submitted for full credit on HW2. However, it is instructive to revisit HW1's DenseBoard and update it to adhere to the immovable tile game mechanic. This will certainly require updates to the shift() methods and perhaps other parts of DenseBoard. The buffer credit tests will examine whether DenseBoard correctly handles immovable tiles.

Interactive testing is facilitated by the PlayText2048 class whose main() method can be invoked to use a DenseBoard during the game as in

aphaedrus [ckauffm2-hw2]% java PlayText2048
usage: java PlayText2048 rows cols bricks {sparse|dense} [random-seed]

aphaedrus [ckauffm2-hw2]% java PlayText2048 4 4 1 dense
                                                  ^^^^^

which will start a 4 by 4 game with 1 brick using a dense board under the hood.

10 Setup and Submission

10.1 HW Distribution

Most programming assignments will have some code that is provided and should be used to complete the assignment.

Download it and extract all its contents; by default it will create a directory (folder) named distrib-hwX where X is the HW number.

10.2 HW Directory

Rename the distrib-hwX directory to masonid-hwX where masonid is your mason ID. My mason ID is ckauffm2 so I would rename HW1 to ckauffm2-hw2.

This is your HW directory. Everything concerning your assignment will go in this directory.

10.3 ID.txt

Create a text file in your HW directory called ID.txt which has identifying information in it. My ID.txt looks like.

Chris Kauffman
ckauffm2
G001234567

It contains my full name, my mason ID, and G# in it. The presence of a correct ID.txt helps immensely when grading lots of assignments.

10.4 Penalties

Make sure to

  • Set up your HW directory correctly
  • Include an ID.txt
  • Indent your code and make comments

Failure to do so may be penalized by a 5% deduction.

10.5 Submission: Blackboard

Do not e-mail the professor or TAs your code.

Create a ZIP file of your HW directory and submit it to the course blackboard page. Do not submit multiple files manually through blackboard as this makes it hard to unpack large numbers of assignments. Learn how to create a zip and submit only that file.

On Blackboard

  • Click on the Assignments section
  • Click on the HW1 link
  • Scroll down to "Attach a File"
  • Click "Browse My Computer"
  • Select you Zip file

You can resubmit to blackboard as many times as you like up to the deadline.


Author: Chris Kauffman (kauffman@cs.gmu.edu)
Date: 2016-06-29 Wed 14:23