I have been teaching myself programming, mainly in Java, for about 6 months. I'm looking for some feedback on this implementation of the Game of Life, which runs entirely in the terminal. Specifically, where does it not follow best practices, is it over-engineered, etc?
The entry point:
public class GameOfLife {
public static void main(String[] args) {
Seed seed = new OscillatorSeed(); //could be any object implementing seed interface
LifeBoard gameOfLife = new LifeBoard(seed);
}
}
Seed interface defines specific behavior of concrete seeds:
public interface Seed {
/*
*pattern for seed files
*must have a "board" made of a 2d boolean array
*board is not required to be rectangular
*/
public int getHeight();
public int getWidth();
public boolean[][] getSeed();
}
Here are some concrete seeds:
public class OscillatorSeed implements Seed {
public boolean[][] seed = {
{false, false, false, false, false},
{false, false, false, false, false},
{false, true, true, true, false},
{false, false, false, false, false},
{false, false, false, false, false}
};
public int height = 5;
public int width = 5;
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public boolean[][] getSeed() {
return seed;
}
}
public class GliderSeed implements Seed {
public boolean[][] seed = {
{false, false, false, false, false},
{false, false, false, true, false},
{false, true, false, true, false},
{false, false, true, true, false},
{false, false, false, false, false}
};
public int height = 25;
public int width = 40;
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
public boolean[][] getSeed() {
return seed;
}
}
BoardPrinter
has one static method, print, to paint the game board in the terminal after each generation. It is tailored to my specific console, and clips large boards to fit or inserts blank space for small boards, so the output at each refresh is exactly my console's number of lines-1 each refresh, makes it look smoother. This is my attempt to separate the UI/display code from the model of the game. IS this a reasonable way to do it?
public class BoardPrinter {
public static int TERM_ROWS = 24;
//xterm leaves last line blank, remember to -1 num_rows
static void print(boolean[][] state) {
// debugging code only: System.out.println("" + state.length + state[0].length);
if (state.length <= TERM_ROWS) {
for (int row = 0; row < state.length; row++) {
for (int position = 0; position < state[row].length; position++) {
if (state[row][position] == true) {
System.out.print("*");
} else {
System.out.print("~");
}
}
System.out.print("\n");
}
} else {
for (int row = 0; row < TERM_ROWS-1; row++) {
for (int position = 0; position < state[row].length; position++) {
if (state[row][position] == true) {
System.out.print("*");
} else {
System.out.print("~");
}
}
System.out.print("\n");
}
}
try {
//100 ms delay = 20 fps
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
The actual implementation of the game, rules, etc. I know the block of if
statements should be made more efficient. I would like to do it with a loop of some sort but am still figuring out how to do that. I'm considering making an array with each of the 8 enums in it to use in a for
-loop. Is the use of enum even a reasonable way to do the direction-search? What else am I not considering?
public class LifeBoard {
public boolean[][] oldState;
public boolean[][] newState;
public boolean[][] blankState;
private boolean gameRunning = true;
public LifeBoard(Seed seed) {
oldState = new boolean[seed.getHeight()][seed.getWidth()];
newState = new boolean[oldState.length][oldState[0].length];
for (int i = 0; i < seed.getSeed().length; i++) {
for (int n = 0; n < seed.getWidth(); n++) {
System.arraycopy(seed.getSeed()[i], 0, oldState[i], 0, seed.getSeed().length);
}
}
blankState = new boolean[oldState.length][oldState[0].length];
run();
}
public void run() {
//game runs forever, evaluateCells() should switch the game off when it detects no change for consecutive generations, but I haven't figured out how to implement that yet
gameRunning = true;
while (gameRunning) {
BoardPrinter.print(oldState);
gameRunning = evaluateCells();
for (int n = 0; n < oldState.length; n++) {
System.arraycopy(newState[n], 0, oldState[n], 0, oldState.length);
}
for (int t = 0; t < newState.length; t++) {
System.arraycopy(blankState[t], 0, newState[t], 0, newState.length);
}
}
BoardPrinter.print(oldState);
System.out.println("game over");
}
public boolean evaluateCells() {
for (int row = 0; row < oldState.length; row++) {
for (int position = 0; position < oldState[row].length; position++) {
newState[row][position] = evaluateCell(row,position);
}
}
//return true while life is still evolving, when board is static, return false
//feature not yet implemented
return true;
}
public boolean evaluateCell(int row, int position) {
int score = 0;
//evaluate each neighboring cell
if (getCell(row, position, Direction.ToRight)) {
score += 1;
}
if (getCell(row, position, Direction.ToLeft)) {
score += 1;
}
if (getCell(row, position, Direction.ToTop)) {
score += 1;
}
if (getCell(row, position, Direction.ToBottom)) {
score += 1;
}
if (getCell(row, position, Direction.ToBottomRight)) {
score += 1;
}
if (getCell(row, position, Direction.ToBottomLeft)) {
score += 1;
}
if (getCell(row, position, Direction.ToTopRight)) {
score += 1;
}
if (getCell(row, position, Direction.ToTopLeft)) {
score += 1;
}
//evaluate total of score against rules of the game
//implimentation of game rules
if ((oldState[row][position] == true) && (score < 2)) {
return false; //die of loneliness
} else if ((oldState[row][position] == true) && (score == 2 || score == 3)) {
return true; //continue to live
} else if ((oldState[row][position] == true) && (score > 3)) {
return false; //die of overcrowding
} else if ((oldState[row][position] == false) && (score == 3)) {
return true; //life is born
} else {
return false;
}
}
public boolean getCell(int row, int position, Direction direction) {
int newY = row+direction.getYDir();
int newX = position+direction.getXDir();
if (newX < 0 || newX >= oldState[row].length) {
return false;
}
if (newY < 0 || newY >= oldState.length) {
return false;
}
return oldState[newY][newX];
}
public enum Direction {
ToRight(1,0),
ToLeft(-1,0),
ToBottom(0,1),
ToTop(0,-1),
ToTopRight(1,-1),
ToTopLeft(-1,-1),
ToBottomRight(1,1),
ToBottomLeft(-1,1);
private int xDir;
private int yDir;
private Direction(int xDir, int yDir) {
this.xDir = xDir;
this.yDir = yDir;
}
public int getXDir() {
return xDir;
}
public int getYDir() {
return yDir;
}
}
}
All this code runs on my machine, no obvious problems, and I get the expected results. In the future I would like to add a SeedFactory
that takes a CSV text file and builds a Seed
from that, rather than compiling a separate class for each seed. I would specify java GameOfLife seedfile.seed
, instantiate the factory in the GameOfLife
class, and pass the arg to the factory, let it return a Seed, and pass that to the LifeBoard. A bloated way of doing it?