CompSci.rocks
FRQcsapa

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 PounceFish class (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 GameState interface (fully specified already, not written here) provides:
    • boolean isGameOver()
    • Player getWinner() — precondition: isGameOver() is true; returns null if there was no winner
    • Player getCurrentPlayer() — precondition: isGameOver() is false
    • ArrayList<String> getCurrentMoves() — the valid moves for the current player; an empty list means there are none
    • void makeMove(String move)
    • String toString()
  • The given Player class has:
    • private String name
    • Player(String aName) — the constructor
    • String 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, extending Player
    • One method, play(), on the already-existing GameDriver class

Part (a): Building the RandomPlayer Class

The Rule, Broken Down

  1. RandomPlayer is a subclass of Player, with a constructor that just takes the player's name.
  2. It overrides getNextMove(GameState state) to pick uniformly at random from whatever getCurrentMoves() returns.
  3. If there are no valid moves, it returns "no move" instead of picking anything.

Step-by-Step Approach

  1. Declare the class as public class RandomPlayer extends Player.
  2. Give it a constructor that takes the player's name and forwards it straight to Player's own constructor with super(...)Player already knows how to store a name, so RandomPlayer doesn't need a field of its own for it.
  3. Override getNextMove: first ask the game state for its list of currently valid moves.
  4. If that list is empty, return "no move" immediately.
  5. 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)name is a private field of Player, so RandomPlayer has no way to set it directly; the constructor's only job here is to hand the name up to the parent.
  • Checking moves.size() == 0 before generating a random index — calling Math.random() and indexing into an empty list would throw an IndexOutOfBoundsException instead of cleanly returning "no move".
  • Math.random() * moves.size()Math.random() returns a value from 0.0 up to (but never including) 1.0, so multiplying by the list's size scales that into the range 0.0 up to (but not including) moves.size().
  • The (int) cast — truncates the scaled random value down to a whole number, guaranteeing an index between 0 and moves.size() - 1 inclusive, which is exactly the valid range for moves.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() == 0 after 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 to moves.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 (like GameDriver.play()) is guaranteed to handle only that exact value correctly.
  • Forgetting super(aName) in the constructor, which either fails to compile (if Player has no no-argument constructor) or leaves the name unset.

Part (b): Writing GameDriver's play()

The Rule, Broken Down

  1. Print the initial state of the game before anything else happens.
  2. While the game isn't over: find the current player and their next move, print both, then make that move.
  3. 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

  1. Print state right away — GameState guarantees a toString(), so System.out.println(state) automatically uses it.
  2. Loop for as long as state.isGameOver() is false.
  3. Inside the loop: ask state for the current player, then ask that player for their next move (passing state itself in, since getNextMove needs to see the current game state).
  4. Print the player's name and their move, then apply the move with state.makeMove(move).
  5. Once the loop ends, the game is over — ask state for the winner.
  6. 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 every GameState implementation is required to provide a toString().
  • 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), not state.getNextMove()getNextMove belongs to Player (and its subclasses, like RandomPlayer), not to GameState; the state object is only ever the argument passed in, never the receiver.
  • Checking winner != null — the problem is explicit that getWinner() can return null when the game ends without a winner, which is exactly the draw case.
  • Calling state.makeMove(move) unconditionally, even when move is "no move" — the problem guarantees makeMove already handles that exact string correctly, so play() 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-while loop instead of a while loop — a game that's already over the moment play() is called should print zero moves, not one.
  • Calling state.getNextMove(...) instead of current.getNextMove(state)getNextMove is a Player method, not a GameState method; the state object is the parameter being passed in, not the object the method is called on.
  • Forgetting the null check on getWinner() 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 (like getWinner()), check for null explicitly rather than assuming the "normal" case will always happen.

Related FRQs