RandomPlayer / GameDriver: 2007 FRQ 4
A step-by-step solution to the 2007 AP CSA FRQ 4 (RandomPlayer / GameDriver), covering extending a class to add a new strategy and driving a generic game loop through an interface in Java.
Designing a game-playing strategy without knowing anything about the game itself is the twist in this AP Computer Science A free-response question — everything runs purely through a GameState interface and a Player class, so the exact same code could end up driving tic-tac-toe, checkers, or any other turn-based game.
What This FRQ Tests
- AP CSA units: Unit 9 (Inheritance/Interfaces) and Unit 5 (Writing Classes)
- Core skill: extending an existing class to add one new, overridden behavior, without needing to know how the rest of that class works internally
- Secondary skill: writing a loop against an interface's method contract, so the same code correctly drives any class that implements it
- Official category: "Classes" (leaning on Unit 9 inheritance), which on the 2007 exam was FRQ 4 — the fixed FRQ 1–4 category order used in more recent years (Methods and Control Structures, Classes, Array/ArrayList, 2D Array, always in that sequence) wasn't standardized until the 2019–2020 Course and Exam Description redesign. 2007's actual printed order was SelfDivisor (FRQ 1, Methods and Control Structures), a Marine Biology Simulation case-study question about a
PounceFishclass (FRQ 2 — skipped on this site, since it depends on case-study classes this site has no other context for), StudentAnswerSheet/TestResults (FRQ 3, Array/ArrayList), and this question, RandomPlayer/GameDriver (FRQ 4, Classes)
The Setup
- The
GameStateinterface (fully specified already, not written here) provides:boolean isGameOver()Player getWinner()— precondition:isGameOver()istrue; returnsnullif there was no winnerPlayer getCurrentPlayer()— precondition:isGameOver()isfalseArrayList<String> getCurrentMoves()— the valid moves for the current player; an empty list means there are nonevoid makeMove(String move)String toString()
- The given
Playerclass has:private String namePlayer(String aName)— the constructorString getName()String getNextMove(GameState state)— the default version just picks the first valid move; subclasses override it to define other strategies
- You're asked to write two separate things:
- A brand-new class,
RandomPlayer, extendingPlayer - One method,
play(), on the already-existingGameDriverclass
- A brand-new class,
Part (a): Building the RandomPlayer Class
The Rule, Broken Down
RandomPlayeris a subclass ofPlayer, with a constructor that just takes the player's name.- It overrides
getNextMove(GameState state)to pick uniformly at random from whatevergetCurrentMoves()returns. - If there are no valid moves, it returns
"no move"instead of picking anything.
Step-by-Step Approach
- Declare the class as
public class RandomPlayer extends Player. - Give it a constructor that takes the player's name and forwards it straight to
Player's own constructor withsuper(...)—Playeralready knows how to store a name, soRandomPlayerdoesn't need a field of its own for it. - Override
getNextMove: first ask the game state for its list of currently valid moves. - If that list is empty, return
"no move"immediately. - Otherwise, pick a random valid index into that list, and return the move stored there.
The Code
public class RandomPlayer extends Player
{
public RandomPlayer(String aName)
{
super(aName);
}
public String getNextMove(GameState state)
{
ArrayList<String> moves = state.getCurrentMoves();
if (moves.size() == 0)
{
return "no move";
}
int index = (int) (Math.random() * moves.size());
return moves.get(index);
}
}
Why Each Piece Matters
super(aName)—nameis aprivatefield ofPlayer, soRandomPlayerhas no way to set it directly; the constructor's only job here is to hand the name up to the parent.- Checking
moves.size() == 0before generating a random index — callingMath.random()and indexing into an empty list would throw anIndexOutOfBoundsExceptioninstead of cleanly returning"no move". Math.random() * moves.size()—Math.random()returns a value from0.0up to (but never including)1.0, so multiplying by the list's size scales that into the range0.0up to (but not including)moves.size().- The
(int)cast — truncates the scaled random value down to a whole number, guaranteeing an index between0andmoves.size() - 1inclusive, which is exactly the valid range formoves.get(...).
Tracing the Example
The FRQ doesn't supply a concrete list of moves or a single numeric result to trace here — getNextMove is deliberately random, so there's no one "expected output" the way there is for other FRQs this year. What can be checked is that the logic always lands on a valid result:
getCurrentMoves() returns |
moves.size() |
What happens |
|---|---|---|
| an empty list | 0 | the if catches it immediately and returns "no move" — Math.random() is never even called |
["X-0-0", "X-1-1", "X-2-2"] |
3 | Math.random() * 3 produces a value in [0.0, 3.0); casting to int always gives 0, 1, or 2 — every one a valid index into the 3-element list |
a single move, ["X-1-1"] |
1 | Math.random() * 1 is in [0.0, 1.0); casting to int always gives 0, the only valid index — so the one available move is always returned |
Every branch produces either a move that genuinely exists in the list, or exactly the string "no move" when there isn't one — which is the actual requirement, since which specific move gets picked is intentionally left up to chance.
Common Mistakes to Avoid
- Checking
moves.size() == 0after trying to index into the list instead of before — the order matters, since indexing an empty list throws an exception rather than returning cleanly. - Writing
Math.random() * moves.size() + 1, or forgetting the(int)cast entirely — either mistake can produce an index equal tomoves.size(), one past the end of the list. - Returning
"No move"or"NO MOVE"instead of exactly"no move"— the problem specifies this precise string, and other game logic (likeGameDriver.play()) is guaranteed to handle only that exact value correctly. - Forgetting
super(aName)in the constructor, which either fails to compile (ifPlayerhas no no-argument constructor) or leaves the name unset.
Part (b): Writing GameDriver's play()
The Rule, Broken Down
- Print the initial state of the game before anything else happens.
- While the game isn't over: find the current player and their next move, print both, then make that move.
- Once the game is over, print the winner's name followed by
"wins", or"Game ends in a draw"if there was no winner.
Step-by-Step Approach
- Print
stateright away —GameStateguarantees atoString(), soSystem.out.println(state)automatically uses it. - Loop for as long as
state.isGameOver()isfalse. - Inside the loop: ask
statefor the current player, then ask that player for their next move (passingstateitself in, sincegetNextMoveneeds to see the current game state). - Print the player's name and their move, then apply the move with
state.makeMove(move). - Once the loop ends, the game is over — ask
statefor the winner. - If the winner isn't
null, print their name followed by"wins"; otherwise print"Game ends in a draw".
The Code
public void play()
{
System.out.println(state);
while (!state.isGameOver())
{
Player current = state.getCurrentPlayer();
String move = current.getNextMove(state);
System.out.println(current.getName() + ": " + move);
state.makeMove(move);
}
Player winner = state.getWinner();
if (winner != null)
{
System.out.println(winner.getName() + " wins");
}
else
{
System.out.println("Game ends in a draw");
}
}
Why Each Piece Matters
System.out.println(state)before the loop even starts — this is what satisfies "first print the initial state of the game," and it works because everyGameStateimplementation is required to provide atoString().while (!state.isGameOver()), checked at the top of the loop — guarantees no move is ever attempted once the game has already ended, and skips the loop body entirely if the game somehow starts already over.current.getNextMove(state), notstate.getNextMove()—getNextMovebelongs toPlayer(and its subclasses, likeRandomPlayer), not toGameState; the state object is only ever the argument passed in, never the receiver.- Checking
winner != null— the problem is explicit thatgetWinner()can returnnullwhen the game ends without a winner, which is exactly the draw case. - Calling
state.makeMove(move)unconditionally, even whenmoveis"no move"— the problem guaranteesmakeMovealready handles that exact string correctly, soplay()doesn't need any special-case logic of its own for it.
Tracing the Example
The FRQ doesn't give a worked, printable trace for play() either — no sample GameState implementation, board, or console output is provided to check line by line. Using the tic-tac-toe move format the question itself mentions ("X-1-1") purely as an illustration, here's how the logic would play out for a short, already-decided two-move game:
| Step | isGameOver() |
Action |
|---|---|---|
| start | — | prints the initial board state |
| iteration 1 | false |
current player "X" moves; prints "X: X-1-1"; makeMove("X-1-1") applied |
| iteration 2 | false |
current player "O" moves; prints "O: O-0-0"; makeMove("O-0-0") applied |
| after iteration 2 | true |
loop exits |
| end | — | getWinner() returns the Player object for "X"; prints "X wins" |
If getWinner() had instead returned null at that final step, the last printed line would be "Game ends in a draw" instead — the two outcomes are mutually exclusive, and both are handled by the same if/else.
Common Mistakes to Avoid
- Printing the initial state inside the loop instead of once before it — this either skips the very first board state entirely or reprints it redundantly on the first iteration.
- Using a
do-whileloop instead of awhileloop — a game that's already over the momentplay()is called should print zero moves, not one. - Calling
state.getNextMove(...)instead ofcurrent.getNextMove(state)—getNextMoveis aPlayermethod, not aGameStatemethod; the state object is the parameter being passed in, not the object the method is called on. - Forgetting the
nullcheck ongetWinner()and assuming there's always a winner once the game is over — draws are an explicitly documented possibility.
Key Takeaways
- Extending a class to add one new capability only requires overriding the specific method that changes — everything else (fields, other methods) is inherited automatically, reached through
super. - Code that only interacts with an interface's methods (here,
GameState's contract) works identically no matter which concrete class actually implements it — that's the entire point of programming against an interface instead of a specific class. - When a method's documented return value can be either a real object or
null(likegetWinner()), check fornullexplicitly rather than assuming the "normal" case will always happen.