
org.jaitools.demo.jiffle.GameOfLife Maven / Gradle / Ivy
/*
* Copyright (c) 2011, Michael Bedward. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.jaitools.demo.jiffle;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.media.jai.TiledImage;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import org.jaitools.CollectionFactory;
import org.jaitools.imageutils.ImageUtils;
import org.jaitools.jiffle.Jiffle;
import org.jaitools.jiffle.runtime.JiffleDirectRuntime;
import org.jaitools.swing.SimpleImagePane;
/**
* John Conway's Game of Life implemented with Jiffle.
*
* The Game of Life is a cellular automaton, ie. a grid based model where
* the value of each grid cell at time t depends on its state and that
* of its neighbours at time t-1.
* See the Wikipedia article at: http://en.wikipedia.org/wiki/Conway's_Game_of_Life
*
* This program is a basic implementation of the game to demonstrate using
* Jiffle in a simulation setting. It also illustrates the following aspects
* of the language:
*
* - Use of the foreach loop with an integer sequence (start:end).
*
- Pixel neighbour references.
*
- The outside script option to set a value returned for
* pixel locations beyond the bounds of a source image.
* - Naked conditional expressions to return 0/1 values.
*
*
* Jiffle scripts
* The program uses two different Jiffle scripts: one which represents a
* a world with hard edges and a second where the world is a toroid,
* ie. opposite edges of the image are joined to form a continuous surface.
* In both scripts the world is an image where an unoccupied location is
* represented by pixel value 0 and an occupied location by pixel value 1.
*
* World with hard edges
*
* options { outside = 0; }
* n = 0;
* foreach (iy in -1:1) {
* foreach (ix in -1:1) {
* n += world[ix, iy];
* }
* }
* n -= world;
* nextworld = (n == 3) || (world && n==2);
*
*
* The expression {@code world[ix, iy]} accesses a relative neighbour
* location. For example {@code world[-1, 1]} would get the value of a pixel
* at {@code (x-1, y+1)} where x and y are the ordinates of the current pixel.
*
* The two foreach loops iterate use integer sequence syntax
* ({@code startValue:endValue}) to iterate over the 3x3 neighbourhood centred
* on the current pixel and count the number of occupied cells (value of 1).
* The rules of Life are expressed in terms of the number of neighbouring cells
* occupied, so we adjust the value of {@code n} b subtracting the value of
* the current pixel.
*
*
* The options block at the top of the script sets a value to be returned
* for any neighbour positions that are beyond the bounds of the image. Without
* this option, the runtime object would throw a
* {@link org.jaitools.jiffle.runtime.JiffleRuntimeException} at the very first pixel
* when trying to access the relative neighbour position {@code world[-1, -1]}.
*
*
* The final line of the script expresses all of the Game of Life rules in a
* single statement ! It uses naked conditional statements which return
* 1 or 0.
*
*
Toroidal world
*
* n = 0;
* foreach (iy in -1:1) {
* yy = y() + iy;
* yy = if (yy < 0, height() - 1, yy);
* yy = if (yy >= height(), 0, yy);
* foreach (ix in -1:1) {
* xx = x() + ix;
* xx = if (xx < 0, width()-1, xx);
* xx = if (xx >= width(), 0, xx);
* n += world[$xx, $yy];
* }
* }
*
* n -= world;
* nextworld = (n == 3) || (world && n==2);
*
*
* This script treats the source image, represented by the {@code world}
* variable, as a toroid by calculating absolute neighbour positions.
* These are indicated by the {@code $} prefix. When a neighbour position is
* beyond an edge, it is adjusted to the corresponding position from the
* opposite edge. Note that we don't need the outside option in this
* script.
*
* Using the Jiffle runtime objects
* The Game of Life is an iterative algorithm where the output for time t
* becomes the input for time t+1. Here, we accomplish this by simply
* caching the Jiffle runtime objects and using them repeatedly with two images
* which are represented by the variables {@code world} and {@code nextworld}
* in the scripts. The images are swapped between source and destination roles
* at each time step as shown in this code fragment...
*
* activeRuntime.setSourceImage(WORLD_NAME, curWorld);
* activeRuntime.setDestinationImage(NEXT_WORLD_NAME, nextWorld);
* activeRuntime.evaluateAll(null);
*
* TiledImage temp = curWorld;
* curWorld = nextWorld;
* nextWorld = temp;
*
*
* Acknowledgement
* The patterns included with this program are a tiny sample of the pattern
* collection at LifeWiki: http://www.conwaylife.com/wiki/
*
* @author Michael Bedward
* @since 1.1
* @version $Id: GameOfLife.java 1654 2011-06-16 10:46:55Z michael.bedward $
*/
public class GameOfLife extends JFrame {
private static final int WORLD_SIZE = 80;
private static enum WorldType {
TOROID,
EDGES;
}
private WorldType worldType = WorldType.TOROID;
private static final long SHORT_DELAY = 50;
private static final long NORMAL_DELAY = 1000;
private static long stepDelay;
private static final String WORLD_NAME = "world";
private static final String NEXT_WORLD_NAME = "nextworld";
private JiffleDirectRuntime toroidRuntime;
private JiffleDirectRuntime edgeRuntime;
private JiffleDirectRuntime activeRuntime;
private static class PatternInfo {
String name;
String author;
String desc;
String url;
String data;
}
private final List patterns;
private TiledImage curWorld;
private TiledImage nextWorld;
private SimpleImagePane imagePane;
private ScheduledExecutorService runExecutor;
private class StepTask implements Runnable {
public void run() {
step();
}
}
private AtomicBoolean running;
List itemsDisabledWhenRunning;
public static void main(String[] args) {
GameOfLife me = new GameOfLife();
me.start(null);
}
public GameOfLife() {
super("Jiffle demo: Conway's Game of Life");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(500, 500);
running = new AtomicBoolean(false);
patterns = CollectionFactory.list();
loadPatterns();
createRuntimeInstances();
initializeComponents();
}
private void createRuntimeInstances() {
try {
Jiffle jiffle = new Jiffle();
Map imageParams = CollectionFactory.map();
imageParams.put(WORLD_NAME, Jiffle.ImageRole.SOURCE);
imageParams.put(NEXT_WORLD_NAME, Jiffle.ImageRole.DEST);
// First create a runtime for the toroidal world
URL url = getClass().getResource("life-toroid.jfl");
File file = new File(url.toURI());
jiffle.setScript(file);
jiffle.setImageParams(imageParams);
jiffle.compile();
toroidRuntime = jiffle.getRuntimeInstance();
// Now create a second runtime for the hard-edged world
url = getClass().getResource("life-edges.jfl");
file = new File(url.toURI());
jiffle.setScript(file);
jiffle.setImageParams(imageParams);
jiffle.compile();
edgeRuntime = jiffle.getRuntimeInstance();
// Set the active runtime
activeRuntime = worldType == WorldType.EDGES ? edgeRuntime : toroidRuntime;
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void start(String patternName) {
if (!isVisible()) {
setVisible(true);
}
if (patternName == null || patternName.length() == 0) {
patternName = patterns.get(0).name;
}
initializeWorld(patternName);
EventQueue.invokeLater(new Runnable() {
public void run() {
imagePane.setImage(curWorld);
}
});
}
private void run() {
running.set(true);
runExecutor = Executors.newScheduledThreadPool(1);
runExecutor.scheduleWithFixedDelay(new StepTask(), 0, stepDelay, TimeUnit.MILLISECONDS);
}
private void stop() {
if (running.get()) {
runExecutor.shutdown();
running.set(false);
}
}
private void step() {
activeRuntime.setSourceImage(WORLD_NAME, curWorld);
activeRuntime.setDestinationImage(NEXT_WORLD_NAME, nextWorld);
activeRuntime.evaluateAll(null);
TiledImage temp = curWorld;
curWorld = nextWorld;
nextWorld = temp;
EventQueue.invokeLater(new Runnable() {
public void run() {
imagePane.setImage(curWorld);
}
});
}
private void initializeWorld(String patternName) {
curWorld = ImageUtils.createConstantImage(WORLD_SIZE, WORLD_SIZE, 0d);
nextWorld = ImageUtils.createConstantImage(WORLD_SIZE, WORLD_SIZE, 0d);
setPopulation(patternName);
}
private void setPopulation(String patternName) {
PatternInfo info = getPattern(patternName);
String[] lines = info.data.split("\n");
final int h = lines.length;
int maxLen = 0;
for (String line : lines) {
if (line.length() > maxLen) {
maxLen = line.length();
}
}
final int w = maxLen;
final int ox = curWorld.getMinX() + (curWorld.getWidth() - w) / 2;
final int oy = curWorld.getMinY() + (curWorld.getHeight() - h) / 2;
for (int y = oy, iy = 0; iy < h; y++, iy++) {
String line = lines[iy];
int len = line.length();
for (int x = ox, ix = 0; ix < len; x++, ix++) {
if (line.charAt(ix) != '.') {
curWorld.setSample(x, y, 0, 1);
}
}
}
}
private void initializeComponents() {
imagePane = new SimpleImagePane();
getContentPane().add(imagePane);
JMenuItem item;
itemsDisabledWhenRunning = CollectionFactory.list();
JMenu worldMenu = new JMenu("World");
JMenu patternMenu = new JMenu("Set pattern");
for (final PatternInfo info : patterns) {
item = new JMenuItem(info.name);
item.setToolTipText(info.desc);
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
start(info.name);
}
});
patternMenu.add(item);
}
worldMenu.add(patternMenu);
itemsDisabledWhenRunning.add(worldMenu);
final JMenuItem edgesItem = new JCheckBoxMenuItem("Join opposite edges (toroid)");
edgesItem.setSelected(worldType == WorldType.TOROID);
edgesItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (edgesItem.isSelected()) {
activeRuntime = toroidRuntime;
worldType = WorldType.TOROID;
} else {
activeRuntime = edgeRuntime;
worldType = WorldType.EDGES;
}
}
});
worldMenu.add(edgesItem);
itemsDisabledWhenRunning.add(edgesItem);
JMenu runMenu = new JMenu("Run");
item = new JMenuItem("Single step ");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
step();
}
});
item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0));
runMenu.add(item);
itemsDisabledWhenRunning.add(item);
item = new JMenuItem("Run");
item.addActionListener(new RunListener(NORMAL_DELAY));
item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK));
runMenu.add(item);
item = new JMenuItem("Run fast");
item.addActionListener(new RunListener(SHORT_DELAY));
item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, KeyEvent.CTRL_DOWN_MASK));
runMenu.add(item);
item = new JMenuItem("Stop");
item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
stop();
enableMenuItems(false);
}
});
item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK));
runMenu.add(item);
JMenuBar menuBar = new JMenuBar();
menuBar.add(worldMenu);
menuBar.add(runMenu);
setJMenuBar(menuBar);
}
private void enableMenuItems(boolean isRunning) {
for (JComponent jc : itemsDisabledWhenRunning) {
jc.setEnabled(!isRunning);
}
}
private void loadPatterns() {
// just in case
patterns.clear();
try {
URL url = getClass().getResource("patterns");
File patternDir = new File(url.toURI());
File[] files = patternDir.listFiles();
for (File f : files) {
patterns.add(loadPattern(f));
}
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private PatternInfo loadPattern(File f) throws FileNotFoundException, IOException {
PatternInfo info = new PatternInfo();
BufferedReader reader = new BufferedReader(new FileReader(f));
String line = reader.readLine();
while (line.startsWith("!")) {
String lwr = line.toLowerCase();
if (lwr.startsWith("!name:")) {
info.name = line.substring(6).trim();
} else if (lwr.startsWith("!author:")) {
info.author = line.substring(8).trim();
} else if (lwr.startsWith("!www")) {
info.url = line.substring(1).trim();
} else {
info.desc = line.substring(1).trim();
}
line = reader.readLine();
}
StringBuilder sb = new StringBuilder();
while (true) {
if (line == null) {
break;
}
line = line.trim();
sb.append(line).append("\n");
line = reader.readLine();
}
info.data = sb.toString();
reader.close();
return info;
}
private PatternInfo getPattern(String patternName) {
for (PatternInfo info : patterns) {
if (info.name.equalsIgnoreCase(patternName)) {
return info;
}
}
throw new IllegalArgumentException("Pattern not loaded: " + patternName);
}
private class RunListener implements ActionListener {
private final long delay;
public RunListener(long delay) {
this.delay = delay;
}
public void actionPerformed(ActionEvent e) {
if (!running.get()) {
enableMenuItems(true);
} else {
stop();
}
stepDelay = delay;
run();
}
}
}