-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathGameOfLife.java
More file actions
208 lines (180 loc) · 7.1 KB
/
GameOfLife.java
File metadata and controls
208 lines (180 loc) · 7.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.BitSet;
import java.util.random.RandomGenerator;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
/**
* Animate and display variations of Conway's Game of Life. The game starts
* running as soon as the object is constructed and keeps running until the
* method {@link #terminate()} is called to stop the animation timer.
*
* <p>Modernized for Java 21+ with records, sealed types, {@code BitSet}
* for the rule encoding, and contemporary Swing idioms.
*
* @author Ilkka Kokkarinen
*/
public class GameOfLife extends JPanel {
private static final int PIX_SIZE = 3;
private static final int MARGIN = 40;
private static final RandomGenerator RNG = RandomGenerator.getDefault();
// --- Rule encoding ---
/**
* A Life-like cellular automaton rule, encoded as two {@link BitSet}s
* for O(1) lookup instead of the old {@code String.indexOf} approach.
*
* @param birth neighbour counts that cause a dead cell to become alive
* @param survival neighbour counts that let a living cell survive
*/
record Rule(BitSet birth, BitSet survival) {
/** Parse a rule from the traditional "B.../S..." digit strings. */
static Rule of(String birthDigits, String survivalDigits) {
return new Rule(parseBits(birthDigits), parseBits(survivalDigits));
}
private static BitSet parseBits(String digits) {
var bits = new BitSet(9); // neighbour counts range 0..8
for (int i = 0; i < digits.length(); i++) {
bits.set(digits.charAt(i) - '0');
}
return bits;
}
boolean shouldLive(boolean currentlyAlive, int neighbours) {
return currentlyAlive ? survival.get(neighbours) : birth.get(neighbours);
}
}
// --- Preset rule definitions ---
/**
* A named Game of Life variant bundling its rule, initial fill
* probability, and suggested window position.
*/
record Variant(String title, Rule rule, double fillProbability, int windowX, int windowY) { }
// Eight neighbours as (dx, dy) offsets, unrolled for clarity.
private static final int[][] NEIGHBOUR_OFFSETS = {
{-1, -1}, {-1, 0}, {-1, 1},
{ 0, -1}, { 0, 1},
{ 1, -1}, { 1, 0}, { 1, 1}
};
// --- Instance state ---
private boolean[][] board;
private boolean[][] nextBoard;
private final int size;
private final Rule rule;
private final Timer timer;
private final BufferedImage img;
private final int aliveRGB = Color.BLACK.getRGB();
private final int deadRGB = Color.WHITE.getRGB();
/**
* Constructor using the original Conway's Game of Life ruleset.
*
* @param size the side length of the square grid, in cells
*/
public GameOfLife(int size) {
this(size, Rule.of("3", "23"), 0.30);
}
/**
* Constructor for generalized Life-like cellular automata.
*
* @param size the side length of the square grid, in cells
* @param rule the birth/survival rule
* @param prob probability that each interior cell is initially alive
*/
public GameOfLife(int size, Rule rule, double prob) {
this.size = size;
this.rule = rule;
int pix = size * PIX_SIZE;
this.img = new BufferedImage(pix, pix, BufferedImage.TYPE_INT_RGB);
setPreferredSize(new Dimension(pix, pix));
setBorder(BorderFactory.createRaisedBevelBorder());
setBackground(new Color(deadRGB));
board = new boolean[size][size];
nextBoard = new boolean[size][size];
for (int x = MARGIN; x < size - MARGIN; x++) {
for (int y = MARGIN; y < size - MARGIN; y++) {
board[x][y] = RNG.nextDouble() < prob;
}
}
// Tick every 500 ms; stagger initial delay to avoid lockstep.
timer = new Timer(500, _ -> advance());
timer.setInitialDelay(RNG.nextInt(500));
timer.start();
}
/** Stop the internal animation timer so that the JVM can exit. */
public void terminate() {
timer.stop();
System.out.println("Game of Life timer terminated");
}
// --- Simulation step ---
private int countNeighbours(int x, int y) {
int count = 0;
for (var offset : NEIGHBOUR_OFFSETS) {
int nx = x + offset[0];
int ny = y + offset[1];
if (nx >= 0 && nx < size && ny >= 0 && ny < size && board[nx][ny]) {
count++;
}
}
return count;
}
private void advance() {
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
int neighbours = countNeighbours(x, y);
nextBoard[x][y] = rule.shouldLive(board[x][y], neighbours);
int rgb = nextBoard[x][y] ? aliveRGB : deadRGB;
for (int px = 0; px < PIX_SIZE; px++) {
for (int py = 0; py < PIX_SIZE; py++) {
img.setRGB(x * PIX_SIZE + px, y * PIX_SIZE + py, rgb);
}
}
}
}
// Swap board references for the next generation.
var tmp = board;
board = nextBoard;
nextBoard = tmp;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(img, 0, 0, this);
}
// --- Frame factory ---
private static void createFrame(Variant variant, GameOfLife game) {
var frame = new JFrame(variant.title());
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
game.terminate();
}
});
frame.add(game);
frame.pack();
frame.setLocation(variant.windowX(), variant.windowY());
frame.setVisible(true);
}
// --- Entry point ---
private static final int SIZE = 150;
public static void main(String[] args) {
var variants = java.util.List.of(
new Variant("Conway's Game of Life", Rule.of("3", "23"), 0.20, 100, 100),
new Variant("Day & Night", Rule.of("3678", "34678"), 0.40, 100, 600),
new Variant("Mazectric", Rule.of("3", "1234"), 0.05, 600, 100),
new Variant("Diamoeba", Rule.of("35678", "5678"), 0.50, 600, 600),
new Variant("Serviettes", Rule.of("234", ""), 0.03, 1100, 100),
new Variant("Gnarl", Rule.of("1", "1"), 0.01, 1100, 600)
);
SwingUtilities.invokeLater(() -> {
for (var variant : variants) {
var game = new GameOfLife(SIZE, variant.rule(), variant.fillProbability());
createFrame(variant, game);
}
});
}
}