I made a couple of little games and cellular automata in Java. For every new 'game', I used the same code as a base, except for the game logic of course. With every iteration of me customizing my existing code to my new idea, my coding skills have much improved and I added new things that I learned. Which is quite a lot.
So I now arrived at the point where I have to start thinking about efficiency, code length but also start to get used to inheritance, encapsulation and most importantly conventions.
This Langton's ant cellular automata got to the point of working pretty well, so I now wanted to see how I can improve such code. But after looking over the code for a while, I realized that maybe it would be better to just rewrite everything. There are 10 classes, so I put the code on GitHub: https://github.com/plankins/LangtonsAnt
Some information about the code:
The rules for the ant are in the Game class:
public void step() {
Point2 p = ant.getPos();
int val = board.getOnBoard(p);
switch (val) {
case 0 -> ant.turnRight();
case 1 -> ant.turnRight();
case 2 -> ant.turnLeft();
case 3 -> ant.turnRight();
}
ant.moveForward();
stepCnt++;
}
Where val
is the tile the ant is currently on. After the ant moves forward, the tile is updated in the Board class:
public void updateTile(Point2 p) {
int var = getOnBoard(p);
int newVar = var+1;
if(newVar > 3){
newVar = 0;
}
setOnBoard(p, newVar);
}
Controls are handled in the InputHandler class, which calls onAction with the enums in Action.java.
They are:
- Space: Pause/Start simulation
- S: Perform one step
- Right/Left Keys: Increase/Decrease steps per frame
- Up/Down Keys: Increase/Decrease delay between each actual step
- P: Prints whole array to console (just for debugging, on much smaller grids)
- C: Clears board
- R: Resets ant's position
- Mouse Buttons: Draw and erase cells with the type of cell that is selected (makes not much sense in Langton's ant, but kept it anyway)
- 1-5 Numbers: Select the colour of cells to draw
I know I haven't been very consistent with conventions and such, but would love to hear what others think about my code.
- What should I do differently?
- How can I improve efficiency? (with efficiency meaning both computational and code length efficiency)
I am planning on moving to JOGL or LWJGL and change my board class to allow for an infinite (a very large) grid and zooming/moving the grid. I briefly looked at HashMaps and sets and that kind of stuff and I feel that thats what I should use. If not, how could I improve this grid situation?
Thank you to everyone who takes the time to review my code!
EDIT: These are the major classes I have 'concerns' about, sorry for misunderstanding the FAQ.
Game.java
public class Game {
private Board board;
private static Game game;
public GameFrame frame;
public Loop loop;
public Ant ant;
public int SIZE;
public int WINDOW_SIZE;
public int GRID_SIZE;
public int mouseX = 0;
public int mouseY = 0;
public int slot = 1;
public int stepCnt = 0;
boolean running = false;
public Game(int windowSize, int gridSize) {
WINDOW_SIZE = windowSize;
GRID_SIZE = gridSize;
SIZE = WINDOW_SIZE / GRID_SIZE;
game = this;
board = new Board(this);
ant = new Ant(gridSize / 2, gridSize / 2, 'u', this);
loop = new Loop(this);
frame = new GameFrame(this);
loop.run();
}
public void drawOnBoard(Action a, int x, int y) {
switch (a) {
case L_MOUSE:
switch (slot) {
//draw on the grid with a total of 6 tile states (including 0), but right now only 4 used.
case 1 -> board.setOnBoard(x, y, 1);
case 2 -> board.setOnBoard(x, y, 2);
case 3 -> board.setOnBoard(x, y, 3);
case 4 -> board.setOnBoard(x, y, 4);
case 5 -> board.setOnBoard(x, y, 5);
}
break;
case R_MOUSE:
//"erase", if you will.
board.setOnBoard(x, y, 0);
break;
case MID_MOUSE:
//set the ant at mouse pos.
ant.getPos().set(x, y);
}
}
public void step() {
Point2 p = ant.getPos();
int val = board.getOnBoard(p);
switch (val) {
//by changing this, one can change the pattern the ant produces.
case 0 -> ant.turnRight();
case 1 -> ant.turnLeft();
case 2 -> ant.turnRight();
case 3 -> ant.turnLeft();
}
ant.moveForward();
stepCnt++;
}
//every user input is handled by this function, with Action enums.
public void onAction(Action a, AWTEvent e) {
System.out.println(a);
if (e instanceof MouseEvent) {
mouseX = Math.floorDiv(((MouseEvent) e).getX(), game.SIZE);
mouseY = Math.floorDiv(((MouseEvent) e).getY(), game.SIZE);
// idek why i used math.floordiv
drawOnBoard(a, mouseX, mouseY);
if (e instanceof MouseWheelEvent) {
switch (a) {
//nothing here yet
case WHEEL_DOWN -> {
}
case WHEEL_UP -> {
}
}
}
} else if (e instanceof KeyEvent) {
double increment = loop.stepDelay / 10;
switch (a) {
case SPACE -> game.toggleSimulation();
case S -> game.step();
case C -> {
board.clearArray();
stepCnt = 0;
}
case UP -> {
if (loop.stepDelay + increment <= 2000) {
loop.stepDelay += increment;
}
}
case DOWN -> {
if (loop.stepDelay - increment > 0) {
loop.stepDelay -= increment;
}
}
case RIGHT -> {
if (loop.stepsPerLoop <= 400000)
loop.stepsPerLoop += 1 + loop.stepsPerLoop / 10;
}
case LEFT -> {
if (loop.stepsPerLoop >= 0)
loop.stepsPerLoop -= 1 + loop.stepsPerLoop / 10;
}
case N1 -> this.slot = 1;
case N2 -> this.slot = 2;
case N3 -> this.slot = 3;
case N4 -> this.slot = 4;
case N5 -> this.slot = 5;
case P -> board.printArray();
case R -> ant.resetPos();
}
}
}
public Board getBoard() {
return this.board;
}
public void toggleSimulation() {
running = !running;
if (running) {
startSimulation();
} else {
stopSimulation();
}
}
public void startSimulation() {
running = true;
System.out.println("start");
}
public void stopSimulation() {
running = false;
System.out.println("stop");
}
}
Ant.java
private Point2 pos;
private char direction;
Game game;
Board board;
public Ant(int x, int y, char direction, Game g) {
this.game = g;
this.board = game.getBoard();
this.pos = new Point2(x, y);
this.direction = direction;
}
public void resetPos() {
this.pos.set(game.GRID_SIZE / 2, game.GRID_SIZE / 2); //somewhere in the middle of the grid, meh.
this.direction = 'u';
}
//DIRECTIONS: pretty logical, chars for Up, Down, Left, Right.
public Point2 getPos() {
return this.pos;
}
public void moveForward() {
board.updateTile(this.pos);
int dx = switch (direction) {
case 'u', 'd' -> 0;
case 'l' -> -1;
case 'r' -> 1;
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
int dy = switch (direction) {
case 'r', 'l' -> 0;
case 'u' -> -1;
case 'd' -> 1;
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
this.pos.transform(dx, dy);
}
public void turnRight() {
direction = switch (direction) {
case 'u' -> 'r';
case 'r' -> 'd';
case 'd' -> 'l';
case 'l' -> 'u';
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
}
public void turnLeft() {
direction = switch (direction) {
case 'u' -> 'l';
case 'l' -> 'd';
case 'd' -> 'r';
case 'r' -> 'u';
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
}
public void turnBack() {
direction = switch (direction) {
case 'u' -> 'd';
case 'l' -> 'r';
case 'd' -> 'u';
case 'r' -> 'l';
default -> throw new IllegalStateException("Unexpected value: " + direction);
};
}
}
Board.java
public class Board {
Game game;
int[][] boardArray;
/*
this is one of the classes which i used as a base, and modified.
yeah dont have anything to add, pretty useful class with functions that are needed often.
*/
public Board(Game g) {
game = g;
boardArray = new int[game.GRID_SIZE][game.GRID_SIZE];
clearArray(boardArray);
}
public int getOnBoard(Point2 p) {
return getOnBoard(p.getX(), p.getY());
}
public int getOnBoard(int x, int y) {
if (isInBounds(x, y))
return boardArray[x][y];
return 0;
}
public void updateTile(Point2 p) {
int var = getOnBoard(p);
int newVar = var+1;
if(newVar > 3){
newVar = 0;
}
setOnBoard(p, newVar);
}
private void setOnBoard(Point2 p, int newVar) {
setOnBoard(p.getX(), p.getY(), newVar);
}
public void setOnBoard(int x, int y, int v) {
if (isInBounds(x, y))
boardArray[x][y] = v;
}
public int countNonEmpty(int[][] arr) {
int i = 0;
for (int x = 0; x < game.GRID_SIZE; x++) {
for (int y = 0; y < game.GRID_SIZE; y++) {
if (arr[x][y] != 0) {
i++;
}
}
}
return i;
}
public void clearArray() {
clearArray(boardArray);
}
public void clearArray(int[][] bArray) {
if (bArray != null) {
for (int x = 0; x < game.GRID_SIZE; x++) {
for (int y = 0; y < game.GRID_SIZE; y++) {
bArray[x][y] = 0;
}
}
}
}
public boolean isInBounds(Point2 p) {
return isInBounds(p.getX(), p.getY());
}
public boolean isInBounds(int x, int y) {
if (x >= 0 && x < game.GRID_SIZE && y >= 0 && y < game.GRID_SIZE) {
return true;
}
//System.out.println("Out of Bounds!: " + x + " - " + y);
return false;
}
public void printArray(int[][] array) {
System.out.println("------------------------");
for (int y = 0; y < game.GRID_SIZE; y++) {
for (int x = 0; x < game.GRID_SIZE; x++) {
System.out.print(array[x][y] + " ");
}
System.out.println();
}
System.out.println("------------------------\n\n\n\n");
}
public void printArray() {
System.out.println("------------------------");
printArray(boardArray);
}
public boolean contains(int v) {
for (int x = 0; x < game.GRID_SIZE; x++) {
for (int y = 0; y < game.GRID_SIZE; y++) {
return boardArray[x][y] == v;
}
}
return false;
}
}
Loop.java
public class Loop {
int stepsPerLoop = 1;
boolean running = true;
long startTime;
int currentFrames;
int fps;
double stepDelay = 20;
Game game;
public Loop(Game g) {
this.game = g;
}
public void run() {
long lastloop = System.nanoTime();
startTime = (lastloop / 1000000);
long laststep = System.nanoTime();
//my "gameloop". what do you think?
while (running) {
long now = System.nanoTime();
if (game.running) {
if ((now - laststep) / 1000000f >= stepDelay) {
for (int i = 0; i < stepsPerLoop; i++) {
game.step();
}
laststep = now;
}
}
/*
as you can see, i split the actual simulation and the rendering.
this way, i can achieve higher simulation speeds while not having to render everytime.
*/
if ((now - lastloop) / 1000000f >= 5) {
game.frame.repaint();
game.frame.setTitle("Sand (fps: " + fps + ") STEP: " + game.stepCnt + " STEPDELAY: " + stepDelay + " STEPSPERLOOP: " + stepsPerLoop);
countFrame();
lastloop = now;
}
}
}
public void countFrame() {
long now = System.currentTimeMillis();
if (now - startTime >= 1000) {
startTime = now;
fps = currentFrames;
currentFrames = 0;
}
currentFrames += 1;
}
}
Main.java
public class Main {
/*
pretty small main class.
*/
public static void main(String[] args) {
int gridSize = 600; //width and height of the actual grid.
int windowSize = 600;
new Game(windowSize, gridSize);
}
}
Point2.java
public class Point2 {
private int x, y;
/*
simple 2d point class i made, so i dont have to pass 2 variables every time.
*/
public Point2(int x, int y) {
this.x = x;
this.y = y;
}
public void set(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public void transform(int dx, int dy) {
this.x += dx;
this.y += dy;
}
}
I would've also added the GamePanel and GameFrame classes etc. but I feel like the question is already way too long. If it is not, I'm happy to also add those to the question. Thanks again.