In this mini-project, you will continue our AsciiBlock examples.
a. Fork and clone the repository at https://github.com/Grinnell-CSC207/mp-blocks-maven.
b. Line
and Rect
(formerly Rectangle
) now permit you to mutate the text blocks. Read through those two classes to see that you understand the differences.
c. One of the challenges of mutable blocks is that the blocks that contain them must also be mutable. Verify that Boxed
continues to work correctly as we mutate blocks. (You can do so by reading the experiments and checking the output.)
d. If you haven’t done so yet, fill in the pre-assessment for this project on Gradescope.
The assignment repository does not include a working Surrounded
. Please finish the implementation and make sure that works as we change the things we surround.
Implement a more general version of Rectangle
called Grid
. Instead of an n
-by-m
grid of characters, Grid
has an n
-by-m
grid of AsciiBlock
objects. That is, it consists of m
rows of n
objects each.
For example, new Grid(new Line("Hello"), 3, 4)
should give
HelloHelloHello
HelloHelloHello
HelloHelloHello
HelloHelloHello
Similarly, new Grid(new Boxed(new Empty()), 3, 2)
should give
/\/\/\
\/\/\/
/\/\/\
\/\/\/
As you might expect, Grid
should adapt to changes in the underlying block.
We have replaced HorizontalCompositionTop
and HorizontalCompositionCenter
with a more general (and more concise) HComp
. In the new version, one can compose as many or as few objects as they’d like. In addition, they specify the alignment as part of the constructor, using VAlignment.TOP
, VAlignment.CENTER
, or VAlignment.BOTTOM
.
For example, suppose a
is a 5x2 rectangle of A
, b
is a 3x3 rectangle of B
, and c
is a 2x6 rectangle of C
.
new HComp(VAlignment.TOP, new AsciiBlock[] {a, b, c})
should give us
AAAAABBBCC
AAAAABBBCC
BBBCC
CC
CC
CC
new HComp(VAlignment.CENTER, new AsciiBlock[] {a, b, c})
should give us
CC
BBBCC
AAAAABBBCC
AAAAABBBCC
CC
CC
Note that when we could not perfectly center a block, we prioritized being higher than lower.
new HComp(VAlignment.BOTTOM, new AsciiBlock[] {a, b, c})
should give us
CC
CC
CC
BBBCC
AAAAABBBCC
AAAAABBBCC
Implement HComp
. Make sure it adapts to blocks that change size.
Similarly, we have replaced VerticalCompositionLeft
with a more general (and more concise) VComp
. Once again, we will provide an alignment (this time, a horizontal alignment rather than a vertical alignment) along with an array of blocks.
Suppose v1
is the line "One"
, v7
is the line "Seven"
, v11
is the line "Eleven"
, and v19
is the line "Nineteen"
.
new VComp(HAlignment.LEFT, new AsciiBlock {v1, v7, v11, v19})
should give us
One
Seven
Eleven
Nineteen
new VComp(HAlignment.CENTER, new AsciiBlock {v1, v7, v11, v19, v1})
should give us
One
Seven
Eleven
Nineteen
One
As that example shows, if we can’t perfectly center a line, we should preferentially move it to the left by half a space.
new VComp(HAlignment.RIGHT, new AsciiBlock {v1, v7, v11, v19})
should give us
One
Seven
Eleven
Nineteen
Implement VComp
. Make sure that it adapts to blocks that change size.
Implement a new class, HFlip
, which presents a horizontally flipped version of its parameter. That is, each line should appear in reverse order.
For example, if block
is
this
and
that
new HFlip(block)
should be
siht
dna
taht
As always, make sure that HFlip
adapts to changes to the underlying block.
Implement a new class, VFlip
, which presents a vertically flipped version of its parameter. That is, the lines should appear in reverse order.
For example, if block
is
this
and
that
new VFlip(block)
should be
that
and
this
As always, make sure that VFlip
adapts to changes in the underlying block.
This part is completely optional. Doing it provides no grade benefit. Skipping it incurs no grade penalty.
Sometimes blocks get too big, and we want to make them smaller. Implement a new class Trimmed
, that trims the underlying block to a particular width and height. Since we may want to trim from different sides (or keep different sides), the Trimmed
constructor will also take alignments as parameters.
For example, if the original block is
abcde
fghij
klmno
pqrst
uvwxy
new Trimmed(block, HAlignment.LEFT, VAlignment.TOP, 2, 2)
should be
ab
fg
new Trimmed(block, HAlignment.RIGHT, VAlignment.TOP, 2, 2)
should be
de
ij
new Trimmed(block, HAlignment.CENTER, VAlignment.TOP, 3, 2)
should be
bcd
ghi
new Trimmed(block, HAlignment.CENTER, VAlignment.TOP, 2, 2)
should be
bc
gh
That is, if the trimmed block cannot be exactly centered horizontally, we prioritize the left.
new Trimmed(block, HAlignment.CENTER, VAlignment.CENTER, 3, 3)
should be
ghi
lmn
qrs
new Trimmed(block, HAlignment.CENTER, VAlignment.CENTER, 3, 2)
should be
ghi
lmn
That is, if the trimmed block cannot be exactly centered vertically, we prioritize the top.
Make sure that your trimmed blocks adapt to changes in the underlying block.
If, when asked for a line, you ever find that the underlying blocks is not wide or high enough, you can do whatever you’d like. (That is, we will assume that people never try to trim too much.)
This part is completely optional. Doing it provides no grade benefit. Skipping it incurs no grade penalty.
We may also find that our boxes are not big enough and want to “pad” them to a larger size. Implement a new class, Padded
, that pads blocks to a given width and height.
For example, if our original block is X
,
new Padded(x, '.', HAlignment.LEFT, VAlignment.TOP, 3, 3)
should give us
X..
...
...
new Padded(x, '.', HAlignment.CENTER, VAlignment.TOP, 3, 3)
should give us
.X.
...
...
new Padded(x, '.', HAlignment.CENTER, VAlignment.TOP, 4, 3)
should give us
.X..
....
....
That is, if we can’t evenly pad something centered on both sides, we put more padding on the right.
new Padded(x, '.', HAlignment.RIGHT, VAlignment.CENTER, 3, 3)
should give us
...
.X.
...
new Padded(x, '.', HAlignment.RIGHT, VAlignment.CENTER, 3, 4)
should give us
...
.X.
...
...
That is, if we can’t evenly pad something verticaly centered, we put more padding on the bottom.
Make sure that your padded blocks adapt to changes in the underlying block.
If, when asked for a line, you ever find that the underlying blocks is too wide or too tall, you can do whatever you’d like. (That is, we will assume that people never try to trim too much.)
Design and implement at least one other new kind of text block.
Put tests for your new kind of text block in the file TestOurBlock.java
. Make sure that you test your new kind of text block in conjunction with some of the other kinds.
There are (at least) three models of equality for TextBlock
objects:
equal
.eqv
.eq
.We have implemented equal
and eq
. It’s up to you to implement eqv
.
What does it mean to be “built the same way”? As we’ve seen, you can have two textblocks that look the same (are equal
) but are constructed in different ways. For example, new VFlip(new Flip(block))
looks exactly the same as block
, but it was constructed differently.
Or consider the following:
XXX
XXX
How many ways are there to build that? Let’s see …
For the eqv
comparison, we want all of those to be treated as different objects.
How might you implement eqv
? The best strategy we’ve seen is to require that every AsciiBlock
also implement eqv(AsciiBlock other)
. That method should check whether other
is the same type of block. If not, it can return false. If so, it should decompose the other block and compare each field to the current one. For example, here’s what we might have within the Grid
class.
public boolean eqv(AsciiBlock other) {
return ((other instanceof Grid) && (this.eqv((Grid) other)));
} // eqv(AsciiBlock)
public boolean eqv(Grid other) {
return (this.hreps == other.hreps) && (this.vreps == other.hreps)
&& (this.element.eqv(other.element));
} // eqv(Grid)
In Art80x24.java
(in package edu.grinnell.csc207.main
), make the main
method print out an “interesting” 80x24 ASCII block created by using the various block types above.
Warning! I may demo your artworks in class.
Students have found multiple modes of collaboration work well in CSC-207. I prefer that at least some of the collaboration involve pair programming, but you will also find that you are more efficient if you split up some of the work.
I would suggest that you begin by reading through the problems together and discussing strategies for each. You might implement a few things together to start with, such as Surrounded
, Grid
, the new text block, its tests, and eqv`.
To help ensure that everyone understands all the parts, you should also come back together to discuss what you’ve done.
In between, you might assign one person to HComp
, HFlip
, and Trimmed
and the other to VComp
, VFlip
, and Padded
.
Or you could just do everything together.
The rubric is not yet fully developed. What you see below is mostly the rubric from an old assignment that helped inspire this assignment.
Submissions that fail to meet any of these requirements will get an I.
[ ] Passes all the R tests.
[ ] Constructors don't crash.
[ ] Includes the specified `.java` files, correctly named.
[ ] Each class has an introductory Javadoc comment that indicates
the author and purpose.
[ ] Includes a `README.md` file that contains the appropriate information
(authors, purpose, acknowledgements if appropriate)
[ ] All files compile correctly.
[ ] Includes one more text block.
[ ] The `README.md` includes a link to the GitHub repo.
[ ] The GitHub repo has at least ten commits.
Submissions that fail to meet any of these requirements but meet all previous requirements will receive an R.
[ ] Passes all the M tests.
[ ] No more than ten errors from `mvn checkstyle:check`.
[ ] The GitHub repo has at least ten meaningful commits.
[ ] Each commit has an appropriate commit message.
[ ] At least three tests in `TestNewBlock.java`.
Submissions that fail to meet any of these requirements but meet all previous requirements will receive an M.
[ ] Passes all the E tests.
[ ] Handle empty blocks appropriately.
[ ] Handle mutated blocks appropriately.
[ ] All (or most) repeated code has been factored out into individual methods.
[ ] All or most variable names are appropriate.
[ ] At least six tests in `TestNewBlock.java`.
[ ] Tests in `TestNewBlock.java` include interesting edge cases, such as
empty blocks.
The tests are not yet available and will be distributed by Tuesday. There is a placeholder for the tests in the repository.
You will pull the new tests into your repository as follows.
First, type the following. (You will only do this once.)
git remote add upstream https://github.com/Grinnell-CSC207/mp-blocks-maven
Then, each time I tell you that I’ve updated the tests, you will type the following.
git fetch upstream
git merge upstream/main
For example,
$ git fetch upstream
remote: Enumerating objects: 17, done.
remote: Counting objects: 100% (17/17), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 9 (delta 2), reused 9 (delta 2), pack-reused 0 (from 0)
Unpacking objects: 100% (9/9), 1.07 KiB | 91.00 KiB/s, done.
From https://github.com/Grinnell-CSC207/mp-blocks-maven
c4f88de..e2d7ec8 main -> upstream/main
$ git merge upstream/main
Updating c4f88de..e2d7ec8
Fast-forward
src/test/java/edu/grinnell/csc207/TestBlocks.java | 45 ++++++++++++++++++++++-
1 file changed, 44 insertions(+), 1 deletion(-)
How do I let someone else push commits to my GitHub repo?
Choose “Settings” (a gear) from along the top (usually top right).
Choose “Collaborators and Teams” from the list of settings on the left.
Click on “Add People”.
Search for your partner’s GitHub username.
Give them write access.
They should get an email invitation that they will need to accept.
Are we going to end up with merge conflicts?
If you are working on different files, you shouldn’t have merge conflicts.
If you’ve divided the work reasonably, the only place you will likely have a merge conflict is on the experiment. You can avoid that problem by creating separate experiment files.
How does pushing or pulling work with a partner in mind?
Don’t push non-working code. (Or at least don’t push code that doesn’t compile.)
Always pull as soon as you sit down for a new session.
Should we only do it on one computer or use two and merge somehow?
You should be able to work separately; that’s the whole point of a system like GitHub. If you try not to work on the same file, you are unlikely to have merge conflicts. And most merge conflicts are easy to fix.
How do you recommend collaboration for Art80x24?
Once you’ve built your blocks, experiment a bit with your partner and talk about what you might try.
Does the whole team get graded together?
Yes, the project gets one grade.
How do we ensure that both partners learn all the aspects?
If you want to ensure that you both learn the material, I’d suggest doing this as a pure pair programming exercise. (Perhaps you can alternate accounts so that we see pushes from both people.)
Alternately, you can sketch the main ideas of each part together, implement separately, and then come back together to discuss the unexpected complications.
Should we collaborate on a single repository with our partner?
Yes.
I also want to understand the best way to break down responsibilities between my partner and me be most efficient.
I’m not sure “most efficient” should be your goal (although with the amount of time some projects have taken, I sympathize). I’d shoot for “best ratio of understanding to time”.
What does it mean to have two structurally equivalent blocks?
They were formed in the same way. That is, they have the same outermost block and all constituent blocks are also equivalent.
For example, if the first block was formed with
HFlip
, the second block must also have been formed withHFlip
. And the block that the first block flips must be structurally equivalent to the block that the second block flips.
Similarly, if the first block is a right-aligned horizontal composition of five blocks, the second must also be a right-aligned horizontal composition of five blocks, and each of the corresponding blocks must also be structurally equivalent.
It sounds like structural equivalence requires recursion. Is that correct?
Yes. Although it’s effectively a kind of “mutual recursion”. Each object will provide its own
eqv
method and they will end up calling each-other’s methods.
What mistakes should I avoid when implementing the eqv model?
There’s an amazingly large range of possible mistakes, so perhaps I’ll respond with what you should do.
Make sure that you understand the examples. Ask questions if you don’t.
Be sure to recurse on sub-blocks.
Use
.equals
to compare strings rather than==
. (Okay, that’s one mistake to avoid.)
Can I use AsciiBlock.equal
in defining the individual eqv
methods?
You can, but I wouldn’t recommend it.
Aren’t equal
and eqv
the same?
Nope.
equal
deals with appearance,eqv
deals with construction. For example …
AsciiBlock exes0 = new Rect('X', 4, 3); AsciiBlock exes1 = new Grid(new Line("X"), 4, 3); AsciiBlock exes2a = new Rect('X', 1, 3); AsciiBlock exes2 = new HComp(VAlignment.LEFT, new AsciiBlock[] {exes2a, exes2a, exes2a, exes2a}); AsciiBlock exes3 = new HComp(VAlignment.LEFT, new AsciiBlock[] {new Empty(), exes0, new Empty(), new Empty()}); AsciiBlock exes4 = new HComp(VAlignment.RIGHT, new AsciiBlock[] {new Empty(), exes0, new Empty(), new Empty()}); AsciiBlock exes5 = new HComp(VAlignment.RIGHT, new AsciiBlock[] {new Empty(), new Empty(), exes0, new Empty()});
assertTrue(AsciiBlock.equals(exes0, exes1)); assertFalse(AsciiBlock.eqv(exes0, exes1), "Rect vs. Grid"); assertTrue(AsciiBlock.equals(exes0, exes2)); assertFalse(AsciiBlock.eqv(exes0, exes2), "Rect vs. HComp"); assertTrue(AsciiBlock.equals(exes2, exes3)); assertFalse(AsciiBlock.eqv(exes2, exes3), "Two HComps with different elements"); assertFalse(AsciiBlock.eqv(exes3, exes4), "Two HComps with different alignments"); assertFalse(AsciiBlock.eqv(exes4, exes5), "Two HComps with different elements");
What is the minimum you expect for our own block?
Don’t worry too much about your own block. We’re going to be fairly generous in the grading. But you should strive for something you’d be proud of.
Also make sure that it’s something that you can test.
For the Art80x24.java, what is the E standard for “interesting”?
Whatever satisfies you, as long as it’s not a grid of identical characters.
Warning! I may be showing them off in class.
Do we submit our assignment together or separately?
Please do only one submission for the two (or three) of you.
It seems this “composite pattern,” in more official words, plays really well to the strengths of OOP. Is this something that can even be easily achieved in other paradigms?
Is that the official term for this? I didn’t know.
We should be able to achieve something similar in a functional paradigm (although we’d likely avoid having mutable blocks). If we think of a “block” as a function from some input to an ASCII block, we could write something that takes other block functions as input and returns a new block function.
It would be hard to implement this well in a more imperative language. Not impossible, but harder.
I am concerned about the instructions mentioning that the functions should be able to “adapt to changes in the underlying block”. Are we not passing an object that, while it is being used to make another object, does not change, and if we were to change that underlying object and make another object with the changes, would be an entirely new object? The created object does not update in place if changes to its elements are made? Or should the created objects be changed if their constituents are changed? Phrased differently perhaps, should the new object ‘know’ it was made from A and B(and update if they are changed), or simply that it was made and it is now AB?
We are working with objects, not functions. I agree it would be difficult if we were using a purely imperative model. And yes, if
AB
is made fromA
andB
, andA
changes, thenAB
should change.
However, the changes are restricted. Objects only change when certain methods are called (or when their subobjects change when certain methods are called). Right now, those methods are the
Line
object’supdate
method and theRect
objectswider
,narrower
,taller
, andshorter
methods.
More concretely, if we set
a
to the line"eh"
andb
to the line"bee"
, the right-aligned vertical composition ofa
andb
will be
eh bee
If we then change
a
to"alphabet"
(usinga.update("alphabet")
), our vertical alignment should now be
alphabet bee
Note that you are only expected to support changes based on the underlying objects. There is no natural way for you to tell if someone reassigns a variable.
What should the Surrounded
class do to work well with mutable blocks?
Follow the lead of
Boxed
and only look at the underlying blocks when someone callsrow
,width
, orheight
.
Are we allowed to create helper functions for each AsciiBlock?
Of course. You should always feel free to create helpers.
Are there any built-in functions for reversing the strings?
No. But you should be able to figure this out. There’s also a note in eboard 8.
Are there any built-in functions for centering strings?
No. But you should be able to figure this out. For horizontal centering, you’ll need to add spaces on the left and right. For vertical centering, you’ll need to figure out which row to take from each block and whether to use spaces instead of a row.
Can you show us an example of Part 12: Make an Art?
I’ll add that to my “to-do” list.
Are enums in Java similar to how they work in C?
Pretty much, except that you have to put the enum name in front of the value. You should write things like
HAlignment.LEFT
.
How do I tell what method was used to create an object?
You can’t. But you can tell whether it’s a particular kind of object with the
instanceof
operator. E.g.(block instanceof HFlip)
returns true ifblock
is anHFlip
and false otherwise.
How do I tell what method was used to modify an object?
You can’t. You can just get the characteristics of the object after the change. For what we’re doing, knowing the width, height, and number of rows in the mutated object should suffice.
What do you mean by “an n
-by-m
grid?
It’s an arrangement in which we have
n
columns andm
rows, each of which contains the same thing. For example, if we’ve managed to make an ASCII cat,new Grid(cat, 5, 3)
would create fifteen cats, arranged in three rows of five each.
What should we do if there is an odd number for flipping? Do we just leave the middle alone?
Yes. I think the examples show that.
What should we do if there’s an odd difference in heights for a centered HComp
or an odd difference in widths for a centered VComp
?
Prioritize the top for a centered
HComp
. Prioritize the left for a centeredVComp
. There are examples in the assignment. Let me know if you need more explanation after looking at them.