Object-Oriented Programming and Inheritance (Critters), CS312 – Study Notes
offline

Difficulty: Intermediate | Prerequisites: Java classes, constructors, methods, access modifiers

Big Picture

The Critters problem is CS312's flagship test of object-oriented programming. You are given a base class (Critter) and asked to write a subclass that overrides specific methods to produce a particular behaviour. The problem tests whether you can manage instance variables, use inheritance correctly, and keep each method call stateless (one move per call, no loops inside move()). If you have not yet worked with extends, constructors, and method overriding, review those concepts first.


TL;DR

You write a class that extends Critter and overrides methods like eat(), move(), and fight() to define custom behaviour. The key challenge is tracking state across multiple calls using private instance variables, without trying to do too much in a single method call.


Key Terms

Inheritance (extends)

A mechanism where a new class (subclass) inherits fields and methods from an existing class (superclass). In the Critters framework, your class extends Critter to inherit the simulation infrastructure.

Think of it as: your class gets everything Critter already does for free, and you only write the parts that differ.

Method overriding

Defining a method in a subclass with the same signature as one in the superclass. When the simulation calls eat(), it runs your version, not the default. The method must match the return type and parameters exactly.

Direction enum

A fixed set of four values: Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST. The move() method returns one of these to tell the simulation which way to go. Direction.values() returns an array of all four.

Attack enum

A fixed set of values representing fight actions: Attack.ROAR, Attack.POUNCE, Attack.SCRATCH. The fight() method returns one of these.

Instance variable

A field declared inside the class but outside any method. It persists between method calls, which is how your critter "remembers" its state (current direction, step count, leg length) across multiple calls to move().

In simple terms, instance variables are your critter's memory between turns.

Constructor

The special method called when a new instance is created. In a Critter subclass, the constructor initialises instance variables to their starting values. If you do not call super(), Java inserts it automatically, which calls the no-argument constructor of Critter.


Core Content: Writing a Critter Subclass

Class header

  • Always public class YourCritter extends Critter. Forgetting extends Critter means none of the overridden methods connect to the simulation.

Instance variables

  • Declare all instance variables private. The exam deducts points for non-private fields.

  • Decide what state the critter needs to track. For a pattern like "walk 1 step in a random direction, then 2 steps, then 3 steps...", you need at minimum: current direction, steps taken so far on this leg, and the length of the current leg.

Constructor

  • Initialise all instance variables. For a random starting direction, call a helper method or use Direction.values()[(int)(Math.random() * 4)].

Picking a random direction

  • Direction.values() returns an array of all four directions.

  • (int)(Math.random() * 4) gives a random index from 0 to 3.

  • Store the result in an instance variable so the critter keeps heading that way until it is time to change.

Overriding eat()

  • Return true if the critter should always eat, false if it should never eat. The default in Critter is false.

Overriding move()

  • This method is called once per simulation step. It must return a single Direction.

  • Do not put a loop inside move(). Each call produces one move. The simulation calls move() repeatedly; your job is to return the right direction for this particular step.

  • Update your step counter and check whether it is time to change direction, all within the single call.

  • A common pattern: save the current direction as the return value, increment your step counter, and if the counter has reached the leg length, reset the counter, increase the leg length, and pick a new random direction.

Overriding fight(String opponent)

  • Return an Attack value based on whatever condition the problem specifies.

  • Do not call move() from inside fight(). The exam deducts 4 points for this. The fight decision should be based on the critter's current state (e.g. its current direction), not on advancing the movement.


Worked Example: The Yak Class

The Yak walks 1 step in a random direction, then 2 steps in a new random direction, then 3, and so on. It always eats. When fighting, it pounces if currently heading east or west, and roars otherwise.

Instance variables needed:

  • Direction dir for the current direction

  • int maxSteps for the current leg length (starts at 1)

  • int steps for how many steps have been taken on this leg

  • A static array of all directions for random selection: Direction.values()

Constructor logic:

Pick a random starting direction. Set maxSteps to 1. steps defaults to 0.

move() logic, step by step:

  1. Save the current direction as the result to return.

  1. Increment steps.

  1. If steps equals maxSteps: reset steps to 0, increment maxSteps, pick a new random direction.

  1. Return the saved direction.

The critical insight is that you save the direction before potentially changing it. The direction returned is the one the critter was heading before any update.

fight() logic:

Check the current value of dir. If it is EAST or WEST, return POUNCE. Otherwise return ROAR. Do not call move() here.

Grading breakdown (16 points):

  • Header with extends Critter: 1 point

  • Instance variables present and correct: 1 point

  • Instance variables declared private: 1 point

  • Random direction selection works correctly: 2 points

  • eat() returns true: 1 point

  • fight() checks direction and returns correct attack: 4 points

  • move() increments steps and returns correct direction: 2 points

  • move() handles end-of-leg reset (steps, leg length, new direction): 4 points

  • Deduction of 6 points if move() contains a loop


Common Misconceptions

  • Students often put a loop inside move() to handle multiple steps at once. Each call to move() should handle exactly one step. The simulation calls move() repeatedly. A loop inside move() costs 6 points.

  • Students often call move() from inside fight() to check the next direction. The fight method should use the current state (e.g. the stored dir variable), not advance the simulation. This costs 4 points.

  • Students often forget to save the current direction before updating it. If you change dir and then return dir, you return the new direction, not the one the critter was supposed to walk this step.

  • Students often make instance variables non-private. The exam explicitly checks for private on every instance variable.


Why It Matters / Exam Flags

  • ⚠️ The Critter problem is worth 16 points, one of the highest-value questions on the exam.

  • ⚠️ The grading rubric penalises structural errors (loops in move(), calling move() from fight()) more heavily than small logic bugs.

  • ⚠️ Math.random() * 4 gives a value in [0.0, 4.0). Casting to int gives 0, 1, 2, or 3, which maps neatly to the four directions.


Quick Self-Test

  1. True or False: A Critter subclass should contain a loop inside move() to handle all the steps in one leg. (False. Each call to move() handles one step.)

  1. Fill in the blank: To pick a random Direction, use Direction.values()[(int)(Math.random() * ____)]. (4)

  1. True or False: Calling move() inside fight() is acceptable because you need to know the next direction. (False. Use the stored direction variable.)

  1. True or False: If eat() is not overridden, the default Critter behaviour is to eat. (False. The default is not to eat.)


Practice Q&A

Q: A Critter subclass called Ant walks SOUTH for 1 step, then EAST for 1 step, then SOUTH for 1 step, then EAST for 1 step, forever. What instance variables do you need?

A: You need one boolean or Direction variable to track whether the next step is SOUTH or EAST. After each call to move(), toggle the variable. No counter is needed because the pattern always alternates.

Q: Why must instance variables in a Critter subclass be private?

A: Encapsulation. Other classes (including the simulation and other critters) should not be able to read or modify your critter's internal state. The exam also deducts points for non-private fields.

Q: A critter's fight() method needs to return SCRATCH if the critter is currently heading NORTH, and ROAR otherwise. Write the method.

A: public Attack fight(String opp) { if (dir == Direction.NORTH) { return Attack.SCRATCH; } return Attack.ROAR; } where dir is a private instance variable storing the current direction.

Q: What is the output of (int)(Math.random() * 4) and why?

A: An integer from 0 to 3 inclusive. Math.random() returns [0.0, 1.0), so multiplying by 4 gives [0.0, 4.0), and casting to int truncates to 0, 1, 2, or 3.


Connections to Other Topics

This connects to arrays because Direction.values() returns an array you index into for random selection. It connects to code tracing because exam questions on polymorphism and declared-type vs. runtime-type tracing use the same inheritance concepts tested here. Understanding access modifiers (private) also feeds into the data-processing and ArrayList questions where encapsulation matters.


Related Terms / Search Tags

Critter class, Java inheritance, extends keyword, method overriding, polymorphism, Direction enum, Attack enum, instance variables, private access modifier, constructor, Math.random, random direction, move method, fight method, eat method, CS312 UT Austin, credit by exam, object-oriented programming, OOP, subclass, superclass, encapsulation