my Java needed a little refresher so I implemented the Game of Life as a little excercise. The game is working as expected, but I'm sure there is some room for improvement.
Here is the code (skipping the interfaces, packages and imports)
/**
* Controller Implementation responsible for starting the game and keeping it
* running.
*/
public class Controller implements IController {
private IModel model;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
/**
* Initialize a new controller and implicitly a model and view as well.
* Start a loop updating the game.
*/
public Controller() {
this.model = new Model(50, 50, this);
this.model.initializeModel();
}
@Override
public void nextTick() {
this.model.updateModel();
}
@Override
public void randomSeed() {
this.model.initializeModel();
}
@Override
public void start() {
final Runnable tickRunner = new Runnable() {
@Override
public void run() {
nextTick();
}
};
this.scheduler.scheduleAtFixedRate(tickRunner, 0, 50, TimeUnit.MILLISECONDS);
}
}
/**
* Model implementation with a simple array holding the game data. Responsible
* for updating the view.
*/
public class Model implements IModel {
public static int NUM_COLS;
public static int NUM_ROWS;
private final boolean[] currData;
private final boolean[] nextData;
private final Cursor cursor;
private IView view;
/**
* Instantiate a new model
*
* @param x
* number of rows of the game field.
* @param y
* number of columns of the game field.
* @param controller
* the controller to pass to the view.
*/
public Model(final int x, final int y, final IController controller) {
this.view = new SwingView(controller);
NUM_COLS = x;
NUM_ROWS = y;
this.currData = new boolean[NUM_COLS * NUM_ROWS];
this.nextData = new boolean[NUM_COLS * NUM_ROWS];
this.cursor = new Cursor();
}
@Override
public void initializeModel() {
for (int i = 0; i < this.currData.length; i++) {
this.currData[i] = Math.random() >= 0.5;
}
this.view.updateView(this.currData);
}
@Override
public void updateModel() {
getNewData();
System.arraycopy(this.nextData, 0, this.currData, 0, this.currData.length);
this.view.updateView(this.currData);
}
private void getNewData() {
for (int i = 0; i < this.currData.length; i++) {
updateCell(i, getNumberOfAliveNeighbours(i));
}
}
private void updateCell(final int i, final int numberOfNeighbours) {
final boolean isCellAlive = this.currData[i];
if (isCellAlive) {
if (numberOfNeighbours < 2 || numberOfNeighbours > 3) {
this.nextData[i] = false;
} else {
this.nextData[i] = true;
}
} else {
if (numberOfNeighbours == 3) {
this.nextData[i] = true;
}
}
}
private int getNumberOfAliveNeighbours(final int i) {
int aliveNeighbours = 0;
this.cursor.setPosition(i);
if (this.currData[this.cursor.moveRight()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveDown()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveLeft()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveLeft()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveUp()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveUp()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveRight()]) {
++aliveNeighbours;
}
if (this.currData[this.cursor.moveRight()]) {
++aliveNeighbours;
}
return aliveNeighbours;
}
private class Cursor {
private int pos;
private final int MAX_POS = (Model.NUM_ROWS * Model.NUM_COLS);
public Cursor() {
super();
}
public void setPosition(final int pos) {
this.pos = pos;
}
public int moveRight() {
if (this.pos >= this.MAX_POS - 1) {
this.pos = -1;
}
return ++this.pos;
}
public int moveLeft() {
if (this.pos <= 1) {
this.pos = this.MAX_POS;
}
return --this.pos;
}
public int moveDown() {
this.pos += Model.NUM_ROWS;
if (this.pos > this.MAX_POS - 1) {
this.pos -= this.MAX_POS;
}
return this.pos;
}
public int moveUp() {
this.pos -= Model.NUM_ROWS;
if (this.pos < 0) {
this.pos += this.MAX_POS;
}
return this.pos;
}
}
}
/**
* View implementation using Swing for the GUI.
*/
public class SwingView extends JFrame implements IView {
/**
* Auto generated by Eclipse.
*/
private static final long serialVersionUID = -1538005133913482652L;
private final JButton btnRnd = new JButton("(Re)Start");
private Rectangles rectangles;
final IController controller;
/**
* Initialize the Swing view and create controls.
*
* @param controller
* the controller which is triggered by controls.
*/
public SwingView(final IController controller) {
super();
this.setTitle("Game of Life");
this.getContentPane().setLayout(null);
this.setBounds(100, 100, 625, 575);
addControls();
this.setVisible(true);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
this.controller = controller;
}
private void addControls() {
this.btnRnd.setBounds(500, 0, 100, 30);
this.btnRnd.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SwingView.this.controller.randomSeed();
}
});
this.add(this.btnRnd);
}
@Override
public void updateView(final boolean[] data) {
if (this.rectangles == null) {
this.add(new Rectangles(data));
} else {
this.rectangles.setNewData(data);
}
this.revalidate();
this.repaint();
}
private class Rectangles extends JPanel {
/**
* Auto generated by Eclipse.
*/
private static final long serialVersionUID = -8352502831091810753L;
private boolean[] data;
private static final int RECT_SIZE = 10;
public Rectangles(final boolean[] data) {
this.data = data;
this.setBounds(0, 0, data.length * RECT_SIZE, data.length * RECT_SIZE);
}
public void setNewData(final boolean[] data) {
this.data = data;
}
@Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
for (int i = 0; i < this.data.length; i++) {
if (this.data[i]) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.WHITE);
}
g.fillRect((i % Model.NUM_ROWS) * RECT_SIZE, (i / Model.NUM_COLS) * RECT_SIZE, RECT_SIZE, RECT_SIZE);
}
}
}
}
The code is also available here: https://github.com/totoMauz/gameOfLife
My one thouhts and remarks:
I started with a multi-dimensional array but it seemed a little sluggish after a while so I switched to a normal one, this helped a litte.
The MVC pattern seems a little bit forced, but I did it intentionally because it biggest advantage is that you theoretically can switch an implementation. If have yet to see that in practive though...I'm also not sure how to correctly instantiate the components to get the controller to the view.
The field is randomized, I do not intent to extend it in a way that the player can mark starting living positions.
The memory consumption is rather high (~90 MB) and the performance seems to degenerate over time, but I'm not seeing anything obvious (haven't profiled yet)
Thanks :)