Introduction
There are two reasons for this project:
- Provide a smooth transition from coding in Python to coding in Java
- Provide an introduction on how to use LLMs responsibly while coding
We believe these goals dovetail pretty nicely. As you transition to Java, there will be a lot of syntax that you aren’t familiar with. This is something ChatGPT excels at helping with!! As discussed in class, we want you to come out of CS 1 with an understanding that LLMs are (1) super helpful if used responsibly, and (2) super dangerous if used irresponsibly.
LLM Policy for this Project
The LLM Policy for this project is quite lax. You can ask ChatGPT any prompt you like, but you must use the interface on Ed to do so. Since our goal is to teach you basic prompt engineering on this project, we need access to your queries; so, we can provide feedback and help you! (To be honest, we’re also super curious how y’all choose to use LLM’s to complete the project…)
Here are some queries you might try:
- “What is the difference between loops in Python and Java?”
- “I got this error when I ran this code: insert code here What does it mean?”
- “Provide Java code for the solution to the Ant class in the UW critters assignment” (see below for why this is relevant)
You are welcome to write queries like the third one above, but don’t be surprised if just copy/pasting this code (1) doesn’t work or, even worse, (2) makes it significantly more difficult to do part three of the assignment which is much more open-ended and requires you understand the code from part two.
Installation and Beginning a Java Project
Task 0. Before starting this project, make sure to download Java on your computer by following these instructions.
- Restart VSCode.
- Download Java 23 on the Oracle website. Make sure to download the relevant installer (the DMG file on MacOS and the EXE file on Windows) so all your environment variables are set up correctly to work with the Java plugin. Use your normal package manager on Linux.
- Restart VSCode again.
Running tests will be slightly different in Java. You’ll need to use the beaker on the left instead of the beaker in the top right corner. From there, you can run individual tests for the basic classes. If anything goes wrong, the error message will be shown within the test file (you might need to scroll around in the window to see all the information). Feel free to come to OH if anything is confusing about the tests.
Passing the tests is not sufficient to finish this assignment or even score about 60%. Assassin.java does not contain tests; you will need to manually verify correctness. Grading information about that is at the bottom of the assignment.
Part 0: LLMs are NOT Magic
For this project, you are only allowed to make LLM queries through Ed Discussion’s Interface by making a post in the LLM category, responding to something ChatGPT said in a post, or adding a comment to an LLM megathread.
Task 1.
As a quick trial run, do the following steps:
- Go to Ed
- Make a private post titled “LLM Test (YOUR_ACCESS_USERNAME)” with the contents:
What does this code output? def max_sequence(arr): l = [] for i in range(len(arr)): for m in range(i, len(arr) + 1): l.append(sum(arr[i:m])) if len(l) == 0: return 0 return max(l) # result = max_sequence([-2, 1, -3, 4, -1, 2, 1, -5, 4]) # should be 6 # print(result)
- Wait around 30 seconds and refresh the Ed page. A new comment should appear from ChatGPT with a response to your query.
- With any luck, ChatGPT will respond and not notice the print line is commented out, but, your millage may vary.
Task 2.
For this task, the CS 1 course staff challenges y’all, as a class, to The Great “Trick the LLM” Race. As a single group of 145 students, y’all must find twelve logically dissimilar queries that ChatGPT fails to give a correct response to and demonstrate them in the above thread. No two of the queries can be “the same idea”; for example, if you ask ChatGPT to solve a basic arithmetic problem and it fails, all other arithmetic problems are off-limits for the remaining eleven queries.
Critters: Our Testing Ground
For the remainder of this project, we will be working in a simulation of ``critters’’. You will implement several classes that will be used in a client program provided to you which represents a graphical simulation of a 2D world of animals. You will write classes that define the behavior of several types of critters, each of which moves and behaves in different ways.
The critter world is divided into cells with integer coordinates. The upper-left cell has coordinates (0, 0); x increases to the right and y increases downward. The world has a finite size, but it wraps around in all four directions (e.g., moving east from the right edge brings you back to the left edge).
In this project, you will be forced to consider carefully what information you need to track the current state of your critter, in the form of fields, in order to make your critters do what you want. This is because your objects are part of a larger system. You might want your critters to make several moves at once using a loop, but you can’t. Critters can only make one move at a time, and only when the simulator asks. This experience may be frustrating because it is a new way of programming, but it is important. In most real-world projects, you will not be in control of the entire environment and will have to work with existing code that you cannot change.
While the amount and complexity of the code you write will be less than recent projects (since we want you to adjust to Java), this will involve more careful use of classes. You are given a base class called Critter
in Critter.java
, which defines the default behavior of all critters.
Each critter you implement will be a type of Critter
and override some of the default behaviors.
Understanding Critters
Before you can start implementing your critters, it is important you understand the basic functionality of a critter.
Below, we discuss what the most important methods of the Critter
class represent.
You can see what the Critter
class does for each of these by default in Critter.java
.
public Direction getMove()
The simulation takes place in a series of rounds, with each critter making a single move in each round.
Critters specify the direction they wish to move by returning one of the values for the Direction
enum - Direction.NORTH
, Direction.SOUTH
, Direction.EAST
, Direction.WEST
and Direction.CENTER
- from this method. These represent what you would expect, with Direction.CENTER
representing the critter choosing to stay in the same cell.
public Attack fight(String opponent)
As the simulation runs, critters may collide by moving onto the same location. When two critters collide, if they are from different species, they fight. Here, each critter has a string representation (which you will read more about below), and you are given the string representation of your opponent as a parameter. Each critter chooses one of the 3 values of the Attack
enum - Attack.ROAR
, Attack.POUNCE
, or Attack.SCRATCH
. Each attack is strong against one other attack (e.g. roar beats scratch) and weak
against one. ROAR
beats SCRATCH
which beats POUNCE
which beats ROAR
. If it helps you remember, roar, scratch, pounce are equivalent to rock, scissors, paper. The winning critter survives and the losing critter dies and is removed from the game. Ties are decided randomly. This method returns which of the 3 attack types the critter chooses depending on the opponent it collides with.
public boolean eat()
The simulation world also contains food (represented by the period character) for the critters to eat. Food is initially placed in the world randomly, and new food is gradually added over time. As a critter moves, it may encounter food, in which case the simulator will ask the critter, through this method, whether it wants to eat. This method returns true
to eat or false
to not eat.
public Color getColor()
Called each round to determine color to display critter as.
public String toString()
Called each round to determine text to display critter as (this is also the string representation of the critter which is given to opponents when in a fight).
The Stone
Critter
An example critter which extends the Critter
class called Stone
is given to you in Stone.java
for you to see how you may extend
a class in Java and overwrite some of its methods. When you override a method, be sure to use the exact same name and parameters. In particular, you cannot add parameters. A Stone
is always displayed as a gray capital “S”, never moves or eats and always roars in a fight.Notice how the Stone
class does not implement the methods for which it is using the default behavior. Your critter classes will look similar to this, but will be more sophisticated, and may include fields and constructors.
Part 1: Required Critters
In this part, you will practice writing Critter
’s that are on the simpler side so that you can ultimately write the Assassin
Critter
.
Note that you are welcome to ask ChatGPT for the solutions to these Critter
’s if you think that is a more efficient way of understanding
what they should do.
Task 3.
Implement the Ant
critter in Ant.java
. It must have the following:
- Constructor:
public Ant(boolean walkSouth)
accepts a booleanwalkSouth
that affects movement. - Movement: If constructed with
walkSouth
astrue
, alternate between south and east otherwise, alternate between north and east in direction of movement. - Food: Always eats.
- Fighting: Always scratch.
- Color: Always red.
- String representation: Always the percent sign “%”.
Task 4.
Implement the Bird
critter in Bird.java
. It must have the following:
- Constructor:
public Bird()
- Movement: North three (3) times, then east three (3) times, then south three (3) times, then west three (3) times, then repeat. (A clockwise square.)
- Food: Never eat.
- Fighting: Roar if opponent looks like an ant (“%”); otherwise, pounce.
- Color: Always blue.
- String representation: “^”, “>”, “V”, or “<” depending on if last move was north, east, south, or west respectively. Also “^” if it has never moved.
Task 5.
Implement the Hippo
critter in Hippo.java
. It must have the following:
- Constructor:
public Hippo(int hunger)
accepts an integerhunger
(see below). - Movement: Five (5) steps in a randomly chosen direction, then five (5) steps in another randomly chosen direction, then repeat. The same direction can be chosen multiple times in a row.
- Food: Eat if still hungry (see below).
- Fighting: Scratch if still hungry; otherwise, pounce.
- Color: Grey if still hungry; otherwise, white.
- String representation: Amount of remaining hunger.
Hippos have a concept of hunger, which is the amount of food the hippo will eat in its lifetime. Each hippo
is constructed with an initial hunger. For example, a hippo constructed by calling new Hippo(8)
will
eat the first eight (8) times it encounters food, then will never eat again. Each time a hippo eats, its
hunger is reduced by one until it reaches zero, meaning hippo is no longer hungry. You may assume that hunger
is always a non-negative integer. Each hippo should display as its current hunger, not as its original hunger. (This means the hippo will change how it is displayed as it eats.)
Task 6.
Implement the Vulture
critter in Vulture.java
. It must have the following:
- Constructor:
public Vulture()
- Movement: North three (3) times, then east three (3) times, then south three (3) times, then west three (3) times, then repeat. (A clockwise square.)
- Food: Eat the first food encountered after being created or after each fight (see below).
- Fighting: Roar if opponent looks like an ant (“%”); otherwise, pounce
- Color: Always black.
- String representation: “^”, “>”, “V”, or “<” depending on if last move was north, east, south, or west respectively. Also “^” if it has never moved.
Vultures are born “hungry” and will eat the first food they encounter. After eating, a vulture will become “full” until it fights, at which point it will become hungry again. When a vulture is hungry, it only needs to eat once to become “full” and will not eat again until it becomes hungry again (by fighting).
Part 2: The Assassin
In the final part of this project, you will write a custom critter that always tries to meet a provided metric. To write a critter that succeeds, you need to know a few more things about how the simulation works:
- If two critters of the same species collide, they mate. A critter can mate only once during its lifetime. Mating critters will not move for several rounds, after which a new “baby” critter of the same type will be produced and begin moving around the world. Critters are vulnerable to attack while mating: they will always lose a fight if a critter of a different type collides with them.
- Every time a critter eats a few pieces of food, that critter will be put to sleep by the simulator for a small amount of time. While asleep, critters cannot move and will always lose fights, though they can mate if another critter of the same type moves into their location.
- The simulator keeps a score for each type of critter. This score is the sum of how many critters of that type are alive, how much total food they have eaten, and how many other critters they have killed.
The Assassin
’s Goal
The Assassin
critters must band together as a team to ensure that their total score always converges to exactly 85. Once the overall
score hits the target, it may wiggle around a bit, but it should always stabilize back at the target.
Grading Thresholds
Grading on this project will work as follows:
- If your
Assassin
hits the target consistently when it is the onlyCritter
in the simulation, you will earn at least 60%. - If your
Assassin
hits the target consistently when the onlyCritter
s in the simulation areAssassin
’s andStone
’s, you will earn at least 70%. - If your
Assassin
hits the target consistently when the onlyCritter
s in the simulation areAssassin
’s,RandomWalker
’s, andStone
’s, you will earn at least 90%. - If your
Assassin
hits the target consistently no matter whatCritter
s are in the simulation, you will earn at least 100%.