CS 310 HW2: Sparse, Listy, Bricky 2048
- Deadlines
- Milestone Deadline: 11:59pm Tuesday 6/27/2016
- Final Deadline: 11:59pm Thursday 7/7/2016
- Submit to Blackboard
- Approximately 8.75% of total grade
- Code for this assignment is here: distrib-hw2.zip
- Test Cases are in the
distrib-hw2.zip
file for this HW.
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
andBrick.java
with the update versions which contain a couple new methods. - Start work on
WLinkedList
as it is partially complete but requires methods markedREQUIRED
to be completed. Most of the tests inHW2MilestoneTests
concernWLinkedList
. - Work next on
Brick
which is an immovable, unmergableTile
.HW2MilestoneTests
also include tests ofBrick
, SparseBoard
andBrick
must be completed- Some minor updates to existing classes such as
Game2048
constructors that useSparseBoard
also markedREQUIRED
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 fewjava.util
classes such asjava.util.Iterator
andjava.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
andendMarker
. The are used to denote the front and back of the list and do not contain any data. You will need to carefully manipulate theirprev/next
pointers and the pointers of other nodes to complete requiredWLinkedList
methods. - The internal iterator class,
LinkedListIterator
is incomplete: methods marked asREQUIRED
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()
andcoalesce()
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 ofWLinkedList
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
andListIterators
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 inSparseBoard
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 theWLinkedList
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.