TileGame: 2009 FRQ 4
A step-by-step solution to the 2009 AP CSA FRQ 4 (TileGame), covering matching tile edges in an ArrayList and trying every rotation of a tile before giving up on placing it in Java.
Snapping numbered puzzle tiles together so their touching edges always match is the premise of this AP Computer Science A free-response question — the twist is that a tile that doesn't fit right away might still fit after being spun in place a few times.
What This FRQ Tests
- AP CSA units: Unit 7 (ArrayList) and Unit 4 (Iteration)
- Core skill: searching an
ArrayListfor a valid insertion point by comparing a new item against its would-be neighbors on both sides - Secondary skill: retrying an operation across a fixed, small number of states (here, a tile's four possible rotations) until one works or all have been tried
- Official category: this year's FRQ 4 tests
ArrayList(Unit 7) rather than the 2D-array slot used in today's fixed FRQ-number-to-category pattern. 2009 predates the 2019–2020 CED redesign that formalized that ordering, and this particular year's exam doesn't include a 2D-array question at all — the exam's own printed "4." next to this question is what actually determines the "FRQ 4" label here, not the category.
The Setup
- The given
NumberTileclass provides methods you call but never modify:void rotate()— rotates the tile 90 degrees clockwise (calling it 4 times returns a tile to its original orientation)int getLeft()— the value on the tile's current left edgeint getRight()— the value on the tile's current right edge
TileGameholds:private ArrayList<NumberTile> board— the tiles currently placed, left to right, in order; guaranteed nevernull
- Tiles are meant to line up so that the touching edges of any two neighbors show the same number — the tile at a given position's right edge must equal the next tile's left edge.
- You're asked to write two methods:
private int getIndexForFit(NumberTile tile)— finds a validArrayListindex for the tile in its current orientation only (no rotating here); returns-1if it doesn't fit anywherepublic boolean insertTile(NumberTile tile)— tries to actually place the tile, rotating it as needed and checking all four orientations before giving up
Part (a): Writing getIndexForFit
The Rule, Broken Down
- If the board is empty, any tile fits — at position
0. - The tile fits at the front if its right edge matches the current first tile's left edge.
- The tile fits at the end if its left edge matches the current last tile's right edge.
- The tile fits between two existing tiles at positions
iandi + 1if the tile's left edge matches positioni's right edge, and the tile's right edge matches positioni + 1's left edge — both have to hold at once. - If none of the above match anywhere, the tile doesn't fit in this orientation, and the method returns
-1.
Step-by-Step Approach
- Handle the empty-board case immediately: if
board.size() == 0, return0right away. - Check the front-of-board case: compare the new tile's right edge to the first tile's left edge.
- Loop through every adjacent pair of existing tiles, checking whether the new tile fits snugly between them.
- Check the end-of-board case: compare the new tile's left edge to the last tile's right edge.
- If none of these checks succeeded, return
-1.
The Code
private int getIndexForFit(NumberTile tile)
{
if (board.size() == 0)
{
return 0;
}
if (tile.getRight() == board.get(0).getLeft())
{
return 0;
}
for (int i = 0; i < board.size() - 1; i++)
{
if (board.get(i).getRight() == tile.getLeft()
&& tile.getRight() == board.get(i + 1).getLeft())
{
return i + 1;
}
}
if (tile.getLeft() == board.get(board.size() - 1).getRight())
{
return board.size();
}
return -1;
}
Why Each Piece Matters
- The empty-board check comes first and returns immediately. Every other check in the method calls
board.get(...), which would throw an exception on an empty list — so this case has to be handled before anything else runs. - The "between" check tests both edges at once with
&&. A tile could easily match one neighboring edge without matching the other; both conditions have to hold for the insertion point to actually work. - The loop returns
i + 1, noti. Inserting "between positioniand positioni + 1" means the new tile ends up occupying positioni + 1(and everything from the oldi + 1onward shifts right) — this is exactly howArrayList.add(int index, E obj)behaves. - The front and end checks are separate from the loop, since "before the first tile" and "after the last tile" only involve one existing neighbor, not two.
Tracing the Example
Using the question's own game board (edges shown as left/right only, since those are the only two exposed by NumberTile):
| Position | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Left edge | 4 | 3 | 4 | 2 | 2 |
| Right edge | 3 | 4 | 2 | 2 | 9 |
For tile1 (left 2, right 2, in its original orientation):
- Front check:
tile1.getRight()(2) vs.board.get(0).getLeft()(4) — no match. - Loop
i = 0:board.get(0).getRight()(3) vs.tile1.getLeft()(2) — no match. - Loop
i = 1:board.get(1).getRight()(4) vs.tile1.getLeft()(2) — no match. - Loop
i = 2:board.get(2).getRight()(2) equalstile1.getLeft()(2), andtile1.getRight()(2) equalsboard.get(3).getLeft()(2) — both match! Returnsi + 1 = 3.
That matches the question directly: "the call getIndexForFit(tile1) can return either 3 or 4." The loop happens to find the position-3 fit first and returns it immediately, which is one of the two accepted answers — the position-4 fit (between positions 3 and 4) also exists, but the method never needs to check further once a valid match is found.
For tile2 (left 8, right 2): the front check fails (2 ≠ 4), every loop iteration fails because no board tile's right edge is 8, and the end check also fails (8 ≠ 9). The method falls through to return -1; — matching the question's stated result exactly.
Common Mistakes to Avoid
- Calling
board.get(0)before checking whether the board is empty. This throws anIndexOutOfBoundsExceptionthe moment the board has no tiles at all. - Checking only one edge of a "between" fit. A tile matching the left neighbor but not the right one (or vice versa) is not a valid fit — both comparisons need
&&, not two separateifstatements that eachreturnon their own. - Returning
iinstead ofi + 1from the loop — this points at the existing tile being compared against, not the slot the new tile will actually occupy after insertion. - Looping all the way to
board.size() - 1instead of stopping atboard.size() - 2(i.e., usingi < board.size() - 1). Comparing positionito positioni + 1only makes sense whilei + 1is still a valid index.
Part (b): Writing insertTile
The Idea
getIndexForFit(from part (a), assumed to work correctly regardless of what was written above) only checks the tile's current orientation — it never rotates anything itself.insertTileis responsible for trying all four possible orientations of the tile (its original orientation, plus after 1, 2, and 3 rotations) until one of them fits somewhere, or all four have failed.- Since
rotate()always turns the tile 90 degrees clockwise, calling it 4 times in a row returns the tile to exactly the orientation it started in — a detail that makes the "try every orientation" loop clean to write.
Step-by-Step Approach
- Repeat up to 4 times (once per possible orientation).
- On each attempt, call
getIndexForFiton the tile in whatever orientation it's currently in. - If a valid index comes back (anything other than
-1), insert the tile there withboard.add(index, tile)and returntrueimmediately. - If it didn't fit, rotate the tile once and try again.
- If all 4 orientations have been tried and none worked, return
false.
The Code
public boolean insertTile(NumberTile tile)
{
for (int attempt = 0; attempt < 4; attempt++)
{
int index = getIndexForFit(tile);
if (index != -1)
{
board.add(index, tile);
return true;
}
tile.rotate();
}
return false;
}
Why Each Piece Matters
- The loop checks the tile's orientation before rotating, every time. This guarantees the tile's original orientation gets checked first (on
attempt = 0), matching the natural expectation that a tile which already fits shouldn't need to be spun at all. board.add(index, tile)is the exact method listed on the AP Quick Reference sheet for inserting into anArrayListat a specific position — it automatically shifts every tile fromindexonward one position to the right, which is exactly the postcondition the problem describes ("the order of the other tiles on the board relative to each other is not changed").- Returning immediately after a successful
add. The problem states the tile should be placed "at most 1 time" — stopping as soon as one orientation works prevents ever inserting it a second time in a different orientation. - Four full rotations, not fewer. A tile has exactly 4 distinct orientations (0, 1, 2, or 3 rotations); checking fewer than that could miss an orientation that actually would have fit.
Tracing the Example
Placing the question's tile1 (left 2, right 2 originally) onto the same 5-tile board from part (a):
- Attempt 0 (no rotation yet):
getIndexForFit(tile1)returns3, exactly as traced above. Since this isn't-1, the method callsboard.add(3, tile1)and returnstrueimmediately — the tile never needs to rotate at all.
This matches the scenario the question walks through: "Assume that the new tile, in its original orientation, is inserted between the tiles at positions 2 and 3." Because the very first orientation checked already fits, insertTile places it there without ever calling rotate().
The question separately notes that if this same tile were rotated once, it would fit either before position 0 or after position 4 instead — since the original orientation already succeeds first, that rotated possibility is simply never reached in this particular trace. (The question doesn't give the tile's exact left/right values after a rotation, so that alternate placement isn't independently re-verified here — only the original-orientation placement that the worked example actually walks through.)
Common Mistakes to Avoid
- Rotating the tile before the first check. This would skip testing the tile's original orientation entirely, potentially missing a fit that didn't need any rotation.
- Forgetting to actually rotate on a failed attempt — an infinite loop isn't possible here since the loop is bounded at 4 iterations, but omitting
tile.rotate()means every attempt after the first checks the exact same (failing) orientation over and over. - Calling
getIndexForFita 5th time or rotating a 5th time. Only 4 distinct orientations exist; a 5th attempt would just repeat the first one. - Not returning
trueimmediately after a successfuladd. Continuing to loop afterward risks inserting the same tile again in a different orientation, violating "the tile should be placed at most 1 time."
Key Takeaways
- Inserting into a sorted or matched sequence almost always means checking three separate cases — fits at the front, fits at the end, and fits between two existing elements — with the "between" case needing both neighboring comparisons to hold at once.
ArrayList.add(int index, E obj)does the position-shifting work automatically; once a valid index is found, placing the item is a single call.- "Try every possible state until one works" is a bounded loop over a known, small number of variations (here, a tile's 4 rotations) — check the current state first, then transform and retry, and stop as soon as something succeeds.