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 Game Cycle 2 Game Cycle 3 Game Cycle 35

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

The results of running the game with Game.java's testMode set to 2 and constructing your PokeLife with the isRandom argument set to false are provided below:

Game Cycle 1 Game Cycle 2 Game Cycle 3 Game Cycle 35

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 Game Cycle 2 Game Cycle 3 Game Cycle 35

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 graphics
  • Game.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 PokePixels 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 Strings 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 PokePixels!). 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 Strings 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 Strings (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 Strings 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 TODOs in the method, which you should replace as described. For reference, our solution adds no more than 8 lines total when replacing the two TODOs. You should only have 3 loops 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 the TYPES and WEAKNESSES arrays. This Type.java could be used to quickly access toString (e.g. "Fire"), getWeaknesses, etc. If you choose to do this extension, note that PokePixel2.java and PokeLife2.java would need to be slightly adjusted to work with Type 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 in updateType and/or isSurroundedBy 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 provided PokeLife's lifeCycle method; you are welcome to adjust lifeCycle to update neighbors instead of a cell, which will be good practice to work with 2D arrays to get neighbors of curr).
  • Implement a feature to support "shiny" PokePixels; 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 a ShinyPokePixel subclass here or handle it in PokePixel2.java or PokeLife2.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).