Details
Due date
This assignment is due on Monday, June 3rd 11:30PM
Assignment Overview
This last Mini Project covers everything up through Week 9, and is the third (and final!) OOP
assignment, implemented in Java. You will find some similarities to your work in MP7,
though you will also practice working with Random
and arrays
in Java. Other than that, you will be reviewing a cumulation of fundamental concepts we've learned throughout the
term, including loops, string processing, and OOP (which we choose to implement in Java).
You will not need to write any documentation for this assignment, unless you implement
optional Part C features.
This assignment is an apt CS1-variant of the classic "Game of Life". The Game of Life is a simulation where cells follow some heuristics to overcome their neighbors. In this version, cells will represent "PokePixels", of which we will start with 5 types corresponding to a specific Pokemon type (e.g. "Fire"), each represented by a color.
The Game of Life is represented as a 2D grid of pixels (also referred to as cells); we've seen 2D lists in Python (and perhaps numpy
!)
and you will get just a basic introduction to the analog in Java, which is covered on Monday's lecture
and which will be walked through whenever you use them in this assignment. Lab 08 will also
be another motivating application of 2D arrays in Java!
How does the Game of Life work? The game first initializes a random board of pixels,
each representing a cell type (different variants will define cell types differently; if you really wanted to,
you could create a Game of Life variant using Caltech houses for cells). Then, a game loop
is started (this is managed in our Game.java
, which you won't modify in Parts A/B).
For each lifetime "cycle", a the 2D grid is looped through, row by row, cell by cell,
and applying an "update rule" for each cell to determine whether its type changes.
Some Game of Life simulations define their heuristics by looking at each pixels neighbors and changing the neighbors depending on the placement of types, but ours will focus on looking at the neighbors to determine whether the pixel of interest is changed.
Example screenshots of the first 3 cycles of the game of life variant, followed by the 35th, are given below:
![Game Cycle 1](game1.png)
![Game Cycle 2](game2.png)
![Game Cycle 3](game3.png)
![Game Cycle 35](game35.png)
Example results printed to console after a game finishes:
Winning type: Fire
Since this game is initialized by randomly-assigning each pixel a type, we have built in support
to test your game with a fixed board allocation. You will see a private boolean isRandom;
field declaration in PokeLife.java
which initializeBoard
will use to assign each pixel a pre-determined type (the PokeLife
constructor
you will implement will take a isRandom
boolean
flag to toggle the test mode state
our tests to modify the flag for randomness/non-randomness).
Game.java
has a testMode
field which can have 1 of three values
(we have started it with the mode of 1 for you to test the small board first):
- 0 - random 60x60 board with 35 lifetime cycles each with a .5s delay
- 1 - non-random 3x4 (small) board with 4 lifetime cycles each with a 5s delay
- 2 - non-random 60x60 (standard) board with 35 lifetime cycles each with a 2.5s cycle delay
Game.java
's testMode
set to 2
and constructing your PokeLife
with the isRandom
argument set to false
are provided below:
![Game Cycle 1](game1b.png)
![Game Cycle 2](game2b.png)
![Game Cycle 3](game3b.png)
![Game Cycle 35](game35b.png)
Example results printed to console after the game finishes 35 lifetime cycles:
Winning type: Grass
The results of running the game
with Game.java
's testMode
set to 1 (small non-random board test)
and constructing your PokeLife
with the isRandom
argument set to false
are provided below:
![Game Cycle 1](game1c.png)
![Game Cycle 2](game2c.png)
![Game Cycle 3](game3c.png)
![Game Cycle 35](game4c.png)
Example results printed to console after the game finishes 4 lifetime cycles:
Cycle: 0 Fir Ele Ele Gro Gra Fir Gro Ele Fir Gra Wat Wat Gro Gro Ele Gra Gro Gra Gra Ele Cycle: 1 Fir Ele Gro Gra Gra Fir Gro Gro Gro Gra Wat Wat Gro Gra Ele Gra Gro Gra Gra Ele Cycle: 2 Fir Gro Gra Gra Gra Fir Gro Gra Gra Gra Wat Wat Gra Gra Ele Gra Gro Gra Gra Ele Cycle: 3 Gro Gra Gra Gra Gra Gro Gra Gra Gra Gra Wat Gra Gra Gra Ele Gra Gra Gra Gra Ele Winning type: Grass
As you can see, some interesting patterns can occur depending on your board initialization heuristics
and updateType
/lifeCycle
methods you'll be seeing!
In Part C, you'll have an option to extend the game with your own heuristics if you'd like.
You should update the testMode
in Game.java
to 2 and 0 to test the
large non-random board results (which should match the second set of screenshots above) and
random board results (which will be random, similar to the first set of example screenshots).
Note: In 23fa, we updated the board representation in PokeLife
to be
easier to visualize with the graphic output; in particular, we will be using (x, y) coordinates
to refer to pixels on the board as shown in the toString
representation which
corresponds to the graphical output (using 0-based indexing). This is a common convention
in representing coordinates in graphics (but not the only one) where x and y
coordinates will start at (0, 0) on the top-left and increase right/down, respectively. Make sure to
double-check with the examples!
What to submit
You will be submitting 2 files for this assignment, each of which we have provided starting templates for you
in mp8_starter.zip
:
PokePixel.java
(Part A)PokeLife.java
(Part B)- (Optional extensions from Part C)
You will also see the following files included in the starter code, which you do not need to modify:
DrawingPanel.java
- helper class for graphicsGame.java
- main game program
Testing Your Code
We encourage you to test your code iteratively, using a client program (with main
to test calls to your Part A and B code, using the examples given.
We have provided two testing files for Part A and B in mp8_tests.zip
which also include a studentTestArea
function if you'd like to use the debugger for your own testing calls. See the file documentation
for each test for instructions to run as well as comments for test cases if you are failing something (if you are failing a test, make sure to re-read the spec instructions carefully!).
No Collaboration Policy
This assignment is strictly no collaboration, similar to MP1-MP6. Students who violate the collaboration policy will be subject to 0 and escalated to the BoC. We are making a clear reminder here given recent violations that our grading software has flagged, and want to remind you that the Honor Code is taken very seriously at Caltech. It's not worth the risk, and we would much rather you utilize course resources than disadvantage yourself and your peers.
Part A: PokePixel.java
The first class you will finish implementing is a simple class representing one of
the pixels on the board, called a PokePixel
. When you play the game,
each pixel has one of 5 colors, which are defined in the provided Game.java
.
Each color represents one of the 5 types managed by PokePixel
and PokeLife
.
You will finish implementing the following methods to create PokePixel
s
of random types which can be accessed and updated using getters and setters.
1. PokePixel
Constructor
[10]
The PokePixel
constructor takes no arguments, however it must set a
type for that PokePixel
instance (which is the only field in the PokePixel
). The PokePixel class is provided with
a types array. This constructor should randomly select a type, and store
it in the type field of the PokePixel. To get a random integer in Java, use the Random
object
and its nextInt
method, which takes an int n
as an argument and returns a random
int
between 0
(inclusive) and n
(exclusive). Use the
randomly-generated int to access a random index in the TYPES
array (a class constant). For review of using Random
in Java, you may find the RandomDemo.java
program posted with lecture materials useful (along with the combined Week 8 Slide Deck)
Note that the PokePixel
has two class constants: a TYPES
array of String
s
defining all of the valid Pokemon types, and a Random
object rand
which is constructed as a class constant. Both of these are class constants since they
should never be changed, and do not represent unique state between different PokePixel
instances. Use the rand
object (don't re-assign it) in your constructor. Do not hard-code any numbers
in this method (use the length of the TYPES
array to get the random int bounds).
In this application, a pixel can have two weaknesses (e.g. "Water" is weak to both "Grass" and "Electric") but every pixel has exactly one type (e.g. a pixel can't be both "Water" and "Grass").
For reference, our solution is 2 lines in the constructor. An example of using Random
is provided below, but again, you shouldn't re-construct rand
in your constructor
since it's already constructed as a program constant.
Random rand = new Random(); int n = rand.nextInt(2); // 1 n = rand.nextInt(2); // 1 n = rand.nextInt(2); // 0 n = rand.nextInt(2); // 1 n = rand.nextInt(2); // 1
PokePixel ex = new PokePixel(); // At this point, ex.type should be a valid pokemon type randomly selected from TYPES
2. getType
getter method
[3]
This getter method that should simply return the PokePixel's type (a 1-line solution).
PokePixel ex = new PokePixel(); // randomly assigned to Water System.out.println(ex.getType()); // Water PokePixel ex2 = new PokePixel(); // randomly assigned to Fire System.out.println(ex2.getType()); // Fire
3. changeType
setter method
[3]
To handle "updating" a PokePixel
for each iteration of the game loop, we need to be able to change
the type of each individual PokePixel
. Note that a PokePixel
simply represents
a pixel rendered on the board, not an actual "Pokemon"; otherwise, it wouldn't make sense to
update a Pokemon's type (Pokemon types can't change). We use this approach to avoid
re-constructing new PokePixel
instances each time the game loop iterates.
Implement this method to change the type
field to the passed type (you do not need
to enforce valid types, though you may if you'd like with an IllegalArgumentException
). This is also a 1-line solution.
PokePixel originallyFire = new PokePixel(); // Randomly initialized to "Fire" type System.out.println(originallyFire.getType()); // Fire originallyFire.changeType("Grass"); System.out.println(originallyFire.getType()); // Grass
4. toString
method
[5]
The string representation of a PokePixel should be the first three letters
of its type (this is useful if you want to print out PokePixel
s!).
Use the substring(startIndex, endIndex)
String
method (remember these are inclusive, exclusive respectively) to implement this as a 1-line solution, returning the first three characters.
PokePixel ex = new PokePixel(); // randomly assigned some type ex.changeType("Electric"); System.out.println(ex.toString()); // Ele System.out.println(ex); // Ele, just like __str__, we don't need to call toString() when printing ex.changeType("Fire"); System.out.println(ex.toString()); // Fire
Part B: PokeLife.java
The second class you will finish implementing represents a "board" of PokePixel
instances
with state corresponding to the current state of the game of life at some iteration of a main game loop
(similar to the game loop used in the Spore lab).
Once you've finished implementing this class, the provided Game.java
will use it
to initialize a new game, powered by a provided DrawingPanel.java
class, which
implements all of the graphics functionality (which you do not need to worry about understanding;
graphics in Java isn't the most elegant).
Just like in Part A, we recommend testing your PokeLife
methods iteratively to
make sure they behave as described, before running Game.java
. Game.java
just happens to be one example client that uses a PokeLife
instance to
run the game of life variant.
In Part B, you will get practice working with basic arrays in Java, which are like lists in Python,
only they are of fixed size and must contains items all having the same type.
For example, a String[]
is the type for an array of String
s in Java.
In Java, we append []
after the type to define an array of that type. Refer to
Lecture 26 code/recording for more details.
It then follows that String[][]
is the the type for an array of an array of String
s (similar to a 2D list).
Constructing arrays is a bit tedious, so we've given you the code for that. Your tasks will
involve applying what you know about Python lists and indexing to access elements by
index in 1D and 2D arrays. In PokeLife.java
, you'll see 1D String[]
arrays,
2D String[][]
arrays, and a 2D PokePixel
array (the 2D grid of pixels being managed).
1. PokeLife
Constructor
[10]
The goal of the PokePixel
constructor is to initialize the "board"
that the pokemon will be on. Since this requires the advanced two-dimensional
array, we have provided you with an initializeBoard
helper method, which returns a board
for you (note that the provided method is declared private
, since it is only
intended to be used as a helper method within PokeLife
method, not by clients).
Internally, this helper method will also check whether or not the isRandom
state was
set in this constructor; if it was set to false
, then the types will be assigned based
on a simple heuristic function (which you don't need to worry about).
If either width
or height
are <= 0, the constructor
should throw an IllegalArgumentException
with the message, width and height must be > 0
.
Otherwise, the PokeLife
constructor should first set the width, height.
Then, set the isRandom
field to the passed isRandom
argument so that we can test the game with a random vs. non-random allocation.
Finally, assign this.board
to the result of calling the initializeBoard
helper method, which returns a populated PokePixel[][]
based on the fields set (don't over-complicate this; you are only setting four fields on exactly four lines,
one assigned to the result of the helper method). For reference, our solution is 7 lines (4 of which are the field-setters).
PokeLife life = new PokeLife(4, 5, false); // board won't be randomly-assigned System.out.println(life.getWidth()); // see below for getWidth/getHeight System.out.println(life.getHeight()); // exLife.board should also be initialized, but we can't access it here since it's a private field without a getter). // Internally, the initializeBoard method you will call will populate width*height (5x4) // 2D grid of width*height PokePixels (for this example, that would be 20 pixels) PokeLife invalidWidth = new PokeLife(0, 5, false); // should raise IllegalArgumentException with message: width and height must be > 0 PokeLife invalidHeight = new PokeLife(4, 0, false); // should raise IllegalArgumentException with message: width and height must be > 0
2. getWidth
and getHeight
getter methods
[5]
These two getter methods should return the width and height of the current
PokeLife
board, respectively.
PokeLife life = new PokeLife(4, 5, false); System.out.println(life.getWidth()); // 4 System.out.println(life.getHeight()); // 5
3. getPoke
getter method
[5]
In this exercise, you will implement the getPoke
method, which takes a x
coordinate
and y
coordinate and returns the PokePixel
at the corresponding
grid cell (where the top-left corner of the grid is (0, 0), and x/y increase right/down, respectively).
First check if the given x
and y
bounds are valid to avoid
an undesirable ArrayIndexOutOfBoundsException
(a client shouldn't
see this internal exception, since its referring to private state and not their arguments).
A valid x
must be between 0 (inclusive) and the board's width (exclusive).
A valid y
must be between 0 (inclusive) and the board's height (exclusive).
If either is invalid, throw an IllegalArgumentException
with the message Invalid x and/or y coordinate.
.
Otherwise, return the PokePixel
at the given position, where (0, 0)
is the first element in the first row (using 0-based indexing).
Note that a 2D array in Java works like a 2D list in Python. When we
have a 2D array (or list), it's just an array of arrays. So suppose we have a small 2x3 grid.
The next two examples show how to construct, access, and modify a 2D list/array in
both Python and Java; you will not actually write any code to construct a 2D array in MP8,
and while the examples below illustrate 2D arrays, the solution to getPoke
is
a short solution, first to handle the bounds and then to otherwise return the result of 2D array indexing of this.board
.
$ python3 >>> rows = 2 >>> cols = 3 >>> grid = [] >>> # populate a 2D grid with 2 rows and 3 cols >>> # (in other words, a list of two nested 3-element lists) >>> for i in range(rows): ... row = [] ... for j in range(cols): ... row.append(0); ... grid.append(row) ... >>> grid [[0, 0, 0], [0, 0, 0]] >>> second_row = grid[1] # second row >>> second_row [0, 0, 0] >>> yth_cell = second_row[3] >>> yth_cell 0 >>> # update the 3rd cell in the 2nd row (0-based indexing) >>> # This is equivalent to referring to an update of the (1, 2) coordinate/position in getPoke >>> grid[1][2] = 1 >>> grid [[0, 0, 0], [0, 0, 1]] >>> len(grid) # grid is a list holding 2 lists 2 >>> len(grid[0]) # grid[0] is the first of the 2 nested list, having 3 elements 3 >>> second_row [0, 0, 1] >>> yth_cell = second_row[2] >>> yth_cell 1 >>> grid[1][2] = 2 >>> grid >>> second_row [0, 0, 2]
In Java, suppose we have a similar 2D array (we'll assume it's already constructed since you're not constructing any arrays in MP8)
int[][] grid = ...; // assume same contents as above // [[0, 0, 0], [0, 0, 0]] int[] secondRow = grid[1]; // [0, 0, 0] grid.length; // 2 (2 nested lists) grid[0].length; // 3 (3 items per nested list) secondRow.length; // 3 grid[-1]; // java.lang.ArrayIndexOutOfBoundsException raised; only 2 rows grid[2]; // java.lang.ArrayIndexOutOfBoundsException raised; only 2 rows grid[0][3]; // java.lang.ArrayIndexOutOfBoundsException raised; only 3 columns per row, not 4 grid[1][2]; // (1, 2) coordinate, or equivalently, second row, third item in that row (grid[row][col]) grid[1][2] = 1; // update the (1, 2) coordinate grid; // [[0, 0, 0], [0, 0, 1]] // secondRow is a reference to the second (mutable) array; that referenced array changes secondRow; // [0, 0, 1] int ythCell = secondRow[2]; // 1 grid[1][2] = 2; ythCell; // 1 (ythCell is just an int value, not a reference so its unchanged) secondRow; // [0, 0, 2] secondRow[2] != ythCell; // true, 2 != 1
Here are some examples you can test with using a small non-random board associated with Game.java
's testMode = 1
flag
(as noted above and in the provided code comments, the provided initializeBoard
will guarantee the same allocation of types for a given width, height when
isRandom
is passed to false, so the following statements should match):
PokeLife life = new PokeLife(4, 5, false); // board won't be randomly-assigned System.out.println(life); // uses the toString() method, printing out the following Fir Gro Gro Wat Fir Fir Fir Wat Wat Gra Ele Gra Gra Fir Fir Wat Ele Gro Fir Gro // top left is (0, 0) // x increases to the right, y increases down // bottom right is (3, 4) PokePixel px00 = life.getPoke(0, 0); // (0, 0) top left, "Fir" PokePixel px01 = life.getPoke(0, 1); // (0, 1) pixel below px00, "Fir" PokePixel px02 = life.getPoke(0, 2); // (0, 2) pixel below px01, "Wat" PokePixel px10 = life.getPoke(1, 0); // (1, 0) pixel right of px00, "Gro" PokePixel px34 = life.getPoke(3, 4); // (3, 4) bottom right, "Gro" System.out.println(px00.getType()); // Fire at (0, 0) System.out.println(px02.getType()); // Water at (0, 2) System.out.println(px10.getType()); // Ground at (1, 0) // other corner cases for this board PokePixel px30 = life.getPoke(3, 0); // top right, "Wat" PokePixel px04 = life.getPoke(0, 4); // bottom left, "Ele" System.out.println(px30.getType()); // Water System.out.println(px04.getType()); // Electric // All of the following should throw an IllegalArgumentException // with the message: Invalid x and/or y coordinate. life.getPoke(-1, 0); // invalid negative x case life.getPoke(0, -1); // invalid negative y case life.getPoke(-1, -1); // invalid negative x and y case life.getPoke(4, 0); // invalid x upper-bounds case (4 cols, 0-based indexing) life.getPoke(5, 0); // one more test life.getPoke(0, 5); // invalid y upper-bounds case (5 rows) life.getPoke(4, 5); // invalid x and y upper-bounds case
4. getWeaknesses
method
[10]
This method will get you more practice with basic array processing in Java.
The two class constants, TYPES
and WEAKNESSES
hold the types and their weaknesses (a type with multiple weaknesses has a weakness String
separated with ,
). In Python, the appropriate way to map types to
their weaknesses would be a dictionary. However, this is less straightforward in Java
(if you're curious, the data structure for this in Java is called a Map
).
For the scope of a small Java project with simple analogs in Python, we have just
chosen to represent the 1-1 mapping of types and weaknesses with these two arrays,
and each type/weakness(es) pairing is determined by a shared index in both arrays.
So the type String TYPES[0]
is defined as having a weakness(es)
of WEAKNESSES[0]
, and so on.
Finish this method, which takes a String type
as a single argument,
to loop through the TYPES
array until the matching String
is found
(use the String
's equals
method to find the match; don't forget that ==
doesn't
correctly check for String equality in Java!) Once your loop finds the index of the matching type, use that index to
get the corresponding weakness string. You can refer to lecture materials on String equality and the provided
getTypeIndex
method for an example of the .equals
method for Strings.
Since we support types which might have
more than one weakness, we want to return an array of all of the weaknesses (in our version,
there happen to be 1 or 2 weaknesses depending on the type).
We have chosen to use "," to separate weaknesses in the same String
(e.g. "Electric,Grass"
for
a single String
that represents these two weaknesses for one type). Just like in Python, we can split
a String
in Java; the String
's split
method should be used here,
which returns an array of Strings (String[]
) split by a passed delimiter (in this case, ","
).
Return that split string array result for the matching index. If the given type
isn't found,
the method should return null
.
For reference, our solution is about 6 lines ignoring inline comments.
// Assume a PokeLife variable called `life` is defined with the following PokePixel board: Fir Fir Wat Gra Ele Gro Wat Gro Ele Ele Gro Gra Gro Fir Gro Gra Gra Gra Ele Ele System.out.println(TYPES[0]); // Fire System.out.println(WEAKNESSES[0]); // Water,Ground String[] weaknesses = WEAKNESSES[0].split(","); // ["Water", "Ground"] String[] fireWeaknesses = getWeaknesses("Fire"); // ["Water", "Ground"] System.out.println(TYPES[2]); // Grass System.out.println(WEAKNESSES[2]); // Fire String[] weaknesses = WEAKNESSES[2].split(","); // ["Fire"] (if a split String does not have ",", it's just a 1-element String[]) String[] grassWeaknesses = getWeaknesses("Grass"); // ["Fire"]
5. isSurroundedBy
method
[10]
Next, you'll implement the method which determines whether a PokePixel
is "overrun" by some minimum number of neighbors sharing one of its weaknesses.
We have provided you with a getNeighborTypes(int x, int y)
method, which returns a String[]
of the types of the 8
neighbors of the PokePixel
at (x, y).
The isSurroundedBy
method takes an x and y position, a String
type
,
and a target int
count
, and should return whether
there are at least count
neighbors of the given type, to the PokePixel
at
(x, y).
Finish this method to use the neighborTypes
array we've started you with to
loop through the array and count how many String
s equal the passed type
String
.
The method should return true
if at least count
neighbors have the
passed type
, else false
. For reference, our solution is about 6-7 lines.
You do not need to handle the arguments being valid, though you may choose to
if you'd like to improve the error-handling for this method (in this case, an IllegalArgumentException
would make the most sense if x or y are out of bounds or count
is < 1).
In updateType
, you will pass 3 as the count
argument to this method,
but we have parameterized this method to support other counts to allow for
possible extensions of the pixel-changing logic (you should not hard-code 3 anywhere in this isSurroundedBy
method).
// Assume a PokeLife variable called `life` is defined with the following 5x4 PokePixel board: Fir Gro Gro Wat Fir Fir Fir Wat Wat Gra Ele Gra Gra Fir Fir Wat Ele Gro Fir Gro // The board's (1, 1) pixel is "Fir", surrounded by 3 "Fir", 2 "Gro", 1 "Wat", 1 "Gra" and 1 "Ele" System.out.println(life.isSurroundedBy(1, 1, "Fire", 1)); // true System.out.println(life.isSurroundedBy(1, 1, "Fire", 3)); // true System.out.println(life.isSurroundedBy(1, 1, "Fire", 4)); // false System.out.println(life.isSurroundedBy(1, 1, "Water", 1)); // true System.out.println(life.isSurroundedBy(1, 1, "Grass", 2)); // false System.out.println(life.isSurroundedBy(1, 1, "Ground", 2)); // true System.out.println(life.isSurroundedBy(1, 1, "Ground", 3)); // false // The board's (0, 2) pixel is the left-most "Wat", surrounded by 3 "Fir" and 2 "Gra" System.out.println(life.isSurroundedBy(0, 2, "Fire", 1)); // true System.out.println(life.isSurroundedBy(0, 2, "Fire", 2)); // true System.out.println(life.isSurroundedBy(0, 2, "Fire", 3)); // true System.out.println(life.isSurroundedBy(0, 2, "Fire", 4)); // false System.out.println(life.isSurroundedBy(0, 2, "Water", 1)); // false System.out.println(life.isSurroundedBy(0, 2, "Grass", 2)); // true System.out.println(life.isSurroundedBy(0, 2, "Grass", 3)); // false
6. updateType
method
[8]
This method is responsible for checking and updating a given PokePixel
.
It should look through the PokePixel
's weaknesses (use your getWeaknesses
method,
which will return a String[]
holding a String for each weakness),
and then check to see if at least 3 neighbors of that type "beat" the PokePixel
. If
If they do, then the PokePixel
's
type should be updated to that weakness String
, using its changeType
method you implemented in Part A.
Note that if a pixel has more than 1 weakness, such as "Water" being weak to both "Grass" and "Electric",
then you'll want to consider each weakness separately (use a loop over the weaknesses array
returned by your getWeaknesses
). The last weakness, if any, that matches
3 or more neighbors should be the updated type (for a type with two weaknesses,
if 3 neighbors are of one weakness and 3 are of the other, the second weakness in the tie
should be chosen to change the type). Also note that this method is declared public
, though
in practice, it would best be a private
helper method. This is only to
make it easier for testing the method in any testing programs.
// Assume a PokeLife variable called `life` is defined with the following 5x4 PokePixel board: // (Cycle 0 of small non-random test) Fir Ele Ele Gro Gra Fir Gro Ele Fir Gra Wat Wat Gro Gro Ele Gra Gro Gra Gra Ele PokePixel px = life.getPoke(2, 3); // "Ele", surrounded by 4 "Gro" life.updateType(2, 3, px); System.out.println(px.getType()); // Ground // Another example PokePixel board Gro Fir Wat Gra Gro Fir Wat Gro Gro Gra Wat Gra Gra Gra Gra Gra Gra Gra Ele Ele PokePixel px = life.getPoke(1, 1); // "Fir" which is weak to "Wat" and "Gro" System.out.println(px.getType()); // Fire // (1, 1) is surrounded by 3 "Wat" and 3 "Gro" // "Gro" occurs later in WEAKNESSES so is the tie-breaker life.updateType(1, 1, px); System.out.println(px.getType()); // Ground
7. getWinningType
method
[10]
At this point, you have everything implemented for the Game.java
program to use your
PokeLife
class (which uses your PokePixel
class)
to run a game of life simulation! In Game.java
, you'll notice a variable
called lifetime
, which defines the number of times the game loop iterates
(each iteration updates the board using the provided PokeLife
lifeCycle
method).
Currently, when the simulation ends (the game loop has iterated lifetime
times),
the window will show the final result of the simulation. Of course, we'd like to know
which type was considered the winner!
Finish PokeLife
's getWinningType
method to
tally up the counts for each of the types in TYPES
. We have started the
method for you, which includes a nested loop over the 2D board. We have also provided a getTypeIndex
which takes an String type
and returns the int index
of that type in
the TYPES
array. There are 2 TODO
s in the method, which you should
replace as described. For reference, our solution adds no more than 8 lines total when replacing the two TODO
s.
You should only have 3 loop
s total, two of which are the nested loop we start you with.
An example of using this method is provided below, where life
is the
current state of the PokePixel
board.
// Assume a PokeLife variable called `life` is defined with the following 3x3 PokePixel board: // Fir Wat Ele // Ele Gra Fir // Fir Gro Wat String winningType = life.getWinningType(); // tallies should be populated as follows: // TYPES == {"Fire", "Water", "Grass", "Electric", "Ground"} // tallies == {3, 2, 1, 2, 1} System.out.println("Winning type: " + winningType); // Winning type: Fire // Example of updating the board to show ties PokePixel px = life.getPoke(2, 1); // "Fir" PokePixel px2 = life.getPoke(0, 0); // "Fir" px.changeType("Grass"); px2.changeType("Ground"); // Gro Wat Ele // Ele Gra Gra // Fir Gro Wat // tallies == {1, 2, 2, 2, 2} winningType = life.getWinningType(); System.out.println("Winning type: " + winningType); // Winning type: Ground // "Ground" is the type at tallies[4] and is the last occurrence of the 4-way tie
For the small non-random test mode (2), we see that "Grass"
is the winning type
(Game.java
calls your getWinningType
to print the results
after the last game cycle):
... Cycle: 3 Gro Gra Gra Gra Gra Gro Gra Gra Gra Gra Wat Gra Gra Gra Ele Gra Gra Gra Gra Ele Winning type: Grass
Part C: (Optional) Extensions
If you've finished, great work! We are offering students a chance to implement optional
features in this assignment, which can earn up to 2 additional points (max 30/30)
depending on the extent you implement. If you choose to implement optional features,
submit your additions as additional files suffixed by 2 (e.g. PokeLife2.java
,
PokePixel2.java
, and Game2.java
. For optional extensions, you must
at minimum submit a Game2.java
which uses a PokeLife2.java
instance (which may be a copy of PokeLife.java
changing all references of PokePixel.java
to PokePixel2.java
,
or which modifies/adds features compatible with your Part A PokeLife.java
, in
which case you don't need to submit another PokeLife2.java
)
If you choose to implement an extension, submit a partc.txt
file with a brief
summary of your extension, what we should look for when running it, and your design/implementation
strategies (we will use this when determining the number of Part C extra points).
Here are some ideas:
- Factor out types and weaknesses with a
Type.java
class, which is an alternative way of working with types instead of theTYPES
andWEAKNESSES
arrays. ThisType.java
could be used to quickly accesstoString
(e.g. "Fire"),getWeaknesses
, etc. If you choose to do this extension, note thatPokePixel2.java
andPokeLife2.java
would need to be slightly adjusted to work withType
instances instead of the two arrays. For an additional challenge, you could use subclasses to implement different types (e.g.FireType
). -
Implement
PokeLife2.java
which is a copy of Part B, but has a different heuristic inupdateType
and/orisSurroundedBy
to determine whether a pixel is "beaten" by its neighbor types. - Add other types/weaknesses to the game, extending the 5-type collection we gave you.
-
Implement "critical hits" or "misses" with randomness, such that a cell is unchanged
if is considered a miss, or all of its neighbors are updated to have its type if it's a critical hit
(note that this will involve some logical refactoring, since
updateType
changes the type of a cell based on its neighbors instead of neighbors having their types updated based on the current cell in the providedPokeLife
'slifeCycle
method; you are welcome to adjustlifeCycle
to update neighbors instead of a cell, which will be good practice to work with 2D arrays to get neighbors ofcurr
). -
Implement a feature to support "shiny"
PokePixel
s; in Pokemon, a shiny Pokemon is a rare find with a different color but still has the same type as its non-shiny counterpart. You can use aShinyPokePixel
subclass here or handle it inPokePixel2.java
orPokeLife2.java
. - Extend update rules to handle neighbors differently for different types and/or positions, such as whether 4 adjacent Fire pixels form a 2x2 square, and perform a cell "explosion", updating the surrounding areas (this is also excellent practice to think about edge-cases in nested loops!).
Example Runthrough: Game.java
Now that you've completed everything, you're ready to see the results in action!
To run the game, make sure everything is up-to-date by compiling (the $
just represents the commands ran in the terminal; as usual, don't actually type it; some students
may instead see %
in the terminal):
$ javac PokePixel.java $ javac PokeLife.java $ javac DrawingPanel.java $ javac Game.java // can alternatively compile everything in the current directory: $ javac *.java $ java Game // run the game!
You should see an animated game of life pop up, which will run for 4 iterations (the lifetime we happened
to set in Game.java
to get you started with testing a small non-random board). You can
now update the Game.java
testMode
to 0 to test for a full random game. After 35 cycles
complete in the game loop, you'll see your
getWinningType
used to print the results, e.g.:
Winning Type: Fire
Refer back to the top of this spec for screenshots, and note you shouldn't actually write any
additional System.out.println
statements (Game.java
does everything for you).