public List<String> print() {
        String header = "ColumnNames   S[T]ack        ";
        for (int i = 1; i <= NUM_COLUMNS; i++) {
            header += "[" + i + "] ";
        }
        for (char s : Suit.getValidInputs()) {
            header += "[" + s + "] ";
        }

        String separator = "";
        for (int i = 0; i < header.length(); i++) {
            separator += "-";
        }

        // If there are any cards in the draw stack, get the top one.  Otherwise display blanks.
        // (The draw stack will be empty if all the cards are in the discard piles, i.e. if the game
        // is won.)
        String topDrawStackValue = drawStack.size() > 0 ?
                drawStack.get(topDrawStackIndex).getValue() :
                BLANK_CARD;

        // The first line of actual card data contains the draw stack and the discard piles.
        String firstRow = FIRST_ROW_BUFFER + topDrawStackValue + FIRST_ROW_DRAW_STACK_BUFFER;
        firstRow += getColumnsSlice(0);
        for (char s : Suit.getValidInputs()) {
            firstRow += " ";
            if (discardPiles.containsKey(String.valueOf(s))) {
                firstRow += discardPiles.get(String.valueOf(s)).getValue();
            }
            else {
                firstRow += BLANK_CARD;
            }
        }

        // Subsequent lines of card data don't need to worry about the draw stack or discard piles.
        List<String> moreRows = new ArrayList<>();
        int longestColumn = longestColumnLength();
        for (int i = 1; i < longestColumn; i++) {
            String thisRow = LATER_ROW_BUFFER;
            thisRow += getColumnsSlice(i);
            moreRows.add(thisRow);
        }

        List<String> output = new ArrayList<>();
        output.add(header);
        output.add(separator);
        output.add(firstRow);
        for (String row : moreRows) {
            output.add(row);
        }

        return output;
    }