aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/uk/co/notori
diff options
context:
space:
mode:
authornotori <188390306+n0tori@users.noreply.github.com>2025-03-13 12:28:08 +0000
committernotori <188390306+n0tori@users.noreply.github.com>2025-03-13 12:28:08 +0000
commit3f80ccbfc0dd2f3b4295e506e7ce5931b720e2ee (patch)
tree17b44f56b7aaec5247dbed939e2f567a55b83ce0 /src/main/java/uk/co/notori
project files
Diffstat (limited to 'src/main/java/uk/co/notori')
-rw-r--r--src/main/java/uk/co/notori/gol/KGOLBooklet.java138
-rw-r--r--src/main/java/uk/co/notori/gol/Main.java35
-rw-r--r--src/main/java/uk/co/notori/gol/MainScreen.java185
-rw-r--r--src/main/java/uk/co/notori/gol/MainUI.java329
-rw-r--r--src/main/java/uk/co/notori/gol/Panel.java249
-rw-r--r--src/main/java/uk/co/notori/gol/Util.java134
6 files changed, 1070 insertions, 0 deletions
diff --git a/src/main/java/uk/co/notori/gol/KGOLBooklet.java b/src/main/java/uk/co/notori/gol/KGOLBooklet.java
new file mode 100644
index 0000000..efb620b
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/KGOLBooklet.java
@@ -0,0 +1,138 @@
+package uk.co.notori.gol;
+
+import com.amazon.kindle.booklet.AbstractBooklet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.IOException;
+import java.net.URI;
+
+/**
+ * Kindle Booklet entry for application
+ */
+public class KGOLBooklet extends AbstractBooklet implements ActionListener {
+
+ static {
+ System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+ System.setProperty("org.slf4j.simpleLogger.logFile","/mnt/us/kgol.log");
+ System.setProperty("org.slf4j.simpleLogger.showDateTime","true");
+ System.setProperty("org.slf4j.simpleLogger.showShortLogName","true");
+ System.setProperty("org.slf4j.simpleLogger.dateTimeFormat","yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ log = LoggerFactory.getLogger(KGOLBooklet.class);
+ Util.setKindle(true);
+ }
+
+ private static final Logger log;
+ private Container rootContainer = null;
+
+ public KGOLBooklet() {
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ KGOLBooklet.this.longStart();
+ }
+ },
+ 1000
+ );
+ }
+
+ public void start(URI uri) {
+ log.info("start called with {} ", uri);
+ super.start(uri);
+ }
+
+ // Because this got obfuscated...
+ private Container getUIContainer() {
+ // Check our cached value, first
+ if (rootContainer != null) {
+ return rootContainer;
+ } else {
+ try {
+ Container container = Util.getUIContainer(this);
+ if (container == null) {
+ log.error("Failed to find getUIContainer method, abort!");
+ endBooklet();
+ return null;
+ }
+ rootContainer = container;
+ return container;
+ } catch (Throwable t) {
+ throw new RuntimeException(t.toString());
+ }
+ }
+ }
+
+ private void endBooklet() {
+ try {
+ log.info("Ending Booklet");
+ Runtime.getRuntime().exec("lipc-set-prop com.lab126.appmgrd stop app://uk.co.notori.gol");
+ } catch (IOException e) {
+ log.error("Failed when terminating ", e);
+ }
+ }
+
+ private void longStart() {
+ try {
+ initializeUI();
+ } catch (Throwable t) {
+ log.error(t.getMessage(), new RuntimeException(t));
+ endBooklet();
+ throw new RuntimeException(t);
+ }
+ }
+
+ private void initializeUI() {
+ log.debug("Starting Up");
+ Container root = getUIContainer();
+
+ log.debug("Got UI container: {}", root);
+ assert root != null;
+
+ // clear the container first
+ root.removeAll();
+
+ Font rootFont = new Font("SansSerif", Font.PLAIN, 12);
+ root.setFont(rootFont);
+
+ final KGOLBooklet booklet = this;
+
+ MainScreen mainScreen = new MainScreen(
+ root,
+ new MainScreen.ExitHook() {
+ public void exit() {
+ booklet.endBooklet();
+ }
+ }
+ );
+
+ // force a repaint
+ try {
+ root.requestFocus();
+
+ mainScreen.start();
+
+ } catch (Exception e) {
+ log.error("Error during UI initialization", e);
+ }
+ }
+
+ public void destroy() {
+ // Try to cleanup behind us on exit...
+ try {
+ // NOTE: This can be a bit racey with stop(),
+ // so sleep for a tiny bit so our commandToRunOnExit actually has a chance to run...
+ Thread.sleep(175);
+ Util.updateCCDB("Game of Life", "/mnt/us/documents/GameOfLife.kgol");
+ } catch (Exception ignored) {
+ // Avoid the framework shouting at us
+ }
+
+ super.destroy();
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ log.debug("Action Performed {} ", e);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Main.java b/src/main/java/uk/co/notori/gol/Main.java
new file mode 100644
index 0000000..018ee0d
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Main.java
@@ -0,0 +1,35 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Main class for desktop testing
+ */
+public class Main {
+
+ static {
+ System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "info");
+ System.setProperty("org.slf4j.simpleLogger.logFile", "System.err");
+ System.setProperty("org.slf4j.simpleLogger.showDateTime", "true");
+ System.setProperty("org.slf4j.simpleLogger.showShortLogName", "true");
+ System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ }
+
+ public static void main(String[] args) {
+ Util.setKindle(false);
+
+ JFrame frame = new JFrame("Conway's Game of Life");
+ frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ frame.setSize(800, 600);
+
+
+ new MainScreen(frame, new MainScreen.ExitHook() {
+ public void exit() {
+ System.exit(0);
+ }
+ });
+
+ frame.setVisible(true);
+ }
+}
diff --git a/src/main/java/uk/co/notori/gol/MainScreen.java b/src/main/java/uk/co/notori/gol/MainScreen.java
new file mode 100644
index 0000000..5829f42
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/MainScreen.java
@@ -0,0 +1,185 @@
+package uk.co.notori.gol;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Main screen manager
+ */
+public class MainScreen {
+
+ private static final Logger log = LoggerFactory.getLogger(MainScreen.class);
+
+ private JPanel currentUI;
+ private final Container root;
+ private final ExitHook mainExitHook;
+
+ public interface ExitHook {
+ void exit();
+ }
+
+ public interface NewGameHook {
+ void newGame();
+ }
+
+ public MainScreen(Container root, ExitHook exitHook) {
+ this(root, exitHook, null);
+ }
+
+ public MainScreen(Container root, ExitHook exitHook, String savePath) {
+ this.root = root;
+ this.mainExitHook = exitHook;
+
+ if (!Util.isKindle()) {
+ root.setSize(new Dimension(800, 600));
+ }
+ // start new game grid on load
+ newGame();
+ }
+
+ public void start() {
+ log.info("Starting Game of Life application");
+
+ if (currentUI != null) {
+ currentUI.setVisible(true);
+
+ // force a complete refresh
+ root.invalidate();
+ root.validate();
+ root.repaint();
+
+ // very ugly fix for weird component visibility behaviour
+ if (Util.isKindle()) {
+ scheduleRefresh(300);
+ scheduleRefresh(600);
+ scheduleRefresh(1200);
+ scheduleRefresh(2000);
+ scheduleRefresh(3000);
+
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ refreshButtonsOnly(root);
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during button refresh", e);
+ }
+ }
+ },
+ 3500
+ );
+ }
+ }
+
+ log.info("Application started");
+ }
+
+ // another very ugly fix for weird component visibility behaviour
+ private void scheduleRefresh(final int delay) {
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ refreshComponentsRecursively(root);
+
+ currentUI.invalidate();
+ currentUI.validate();
+ currentUI.repaint();
+
+ root.invalidate();
+ root.validate();
+ root.repaint();
+
+ log.info("Completed UI refresh after " + delay + "ms");
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during UI refresh", e);
+ }
+ }
+ },
+ delay
+ );
+ }
+
+ // another very ugly fix for weird component visibility behaviour
+ private void refreshComponentsRecursively(Container container) {
+ Component[] components = container.getComponents();
+ for (int i = 0; i < components.length; i++) {
+ Component component = components[i];
+ component.invalidate();
+ component.validate();
+ component.repaint();
+
+ // handling for buttons to ensure visibility
+ if (component instanceof JButton) {
+ JButton button = (JButton)component;
+ button.setVisible(true);
+ }
+
+ if (component instanceof Container) {
+ refreshComponentsRecursively((Container)component);
+ }
+ }
+ }
+
+ private void refreshButtonsOnly(Container container) {
+ Component[] components = container.getComponents();
+ for (int i = 0; i < components.length; i++) {
+ Component component = components[i];
+
+ if (component instanceof JButton) {
+ JButton button = (JButton)component;
+ button.setVisible(true);
+ button.invalidate();
+ button.validate();
+ button.repaint();
+ log.info("Refreshed button: " + button.getText());
+ }
+
+ if (component instanceof Container) {
+ refreshButtonsOnly((Container)component);
+ }
+ }
+ }
+
+ public void newGame() {
+ log.info("Starting new game");
+
+ // remove current UI if it exists
+ if (currentUI != null) {
+ root.remove(currentUI);
+ }
+
+ // create main UI with new panel
+ Panel gamePanel = new Panel(root.getSize());
+ currentUI = new MainUI(
+ new ExitHook() {
+ public void exit() {
+ mainExitHook.exit();
+ }
+ },
+ gamePanel
+ );
+
+ currentUI.setSize(root.getSize());
+ currentUI.setPreferredSize(root.getSize());
+
+ root.add(currentUI, BorderLayout.CENTER);
+
+ currentUI.setVisible(true);
+ gamePanel.setVisible(true);
+
+ root.validate();
+ root.repaint();
+ }
+}
diff --git a/src/main/java/uk/co/notori/gol/MainUI.java b/src/main/java/uk/co/notori/gol/MainUI.java
new file mode 100644
index 0000000..dbb3ddd
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/MainUI.java
@@ -0,0 +1,329 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import javax.swing.border.Border;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The main UI for the Game of Life app
+ */
+public class MainUI extends JPanel {
+ private static final Logger log = LoggerFactory.getLogger(MainUI.class);
+
+ public final Panel panel;
+ private final JLabel generationLabel;
+ private final JLabel populationLabel;
+
+ public MainUI(MainScreen.ExitHook exitHook, Panel panel) {
+ super();
+
+ this.panel = panel;
+
+ setLayout(new BorderLayout());
+
+ // Panel area
+ add(panel, BorderLayout.CENTER);
+
+ // calculate appropriate font sizes based on screen dimensions,
+ // not tested on multiple devicesbut in theory works.
+ Dimension screenSize = getScreenSize();
+ int labelFontSize = calculateFontSize(screenSize, 14);
+ int buttonFontSize = calculateFontSize(screenSize, 12);
+ int speedFontSize = calculateFontSize(screenSize, 10);
+ int infoFontSize = calculateFontSize(screenSize, 10);
+
+ // info panel (top)
+ JPanel infoPanel = new JPanel();
+ infoPanel.setLayout(new GridLayout(1, 2));
+
+ generationLabel = new JLabel("Generation: 0", JLabel.CENTER);
+ generationLabel.setFont(new Font("SansSerif", Font.BOLD, labelFontSize));
+ infoPanel.add(generationLabel);
+
+ populationLabel = new JLabel("Population: 0", JLabel.CENTER);
+ populationLabel.setFont(new Font("SansSerif", Font.BOLD, labelFontSize));
+ infoPanel.add(populationLabel);
+
+ add(infoPanel, BorderLayout.NORTH);
+
+ // control panel (bottom)
+ JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
+
+ JLabel creditsLabel = new JLabel("Kindle Game Of Life - made by notori :)", JLabel.CENTER);
+ creditsLabel.setFont(new Font("SansSerif", Font.ITALIC, infoFontSize));
+ creditsLabel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
+ bottomPanel.add(creditsLabel, BorderLayout.NORTH);
+
+ // main control buttons in a grid
+ JPanel controlPanel = new JPanel();
+ controlPanel.setLayout(new GridLayout(1, 3, 10, 10)); // 1 row, 3 columns with gaps
+ controlPanel.setOpaque(true);
+
+ // speed buttons in their own panel
+ JPanel speedPanel = new JPanel();
+ speedPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 8, 5));
+ speedPanel.setOpaque(true);
+ speedPanel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createLineBorder(Color.BLACK, 1),
+ BorderFactory.createEmptyBorder(5, 5, 5, 5)
+ ));
+
+ JLabel speedLabel = new JLabel("Speed:", JLabel.CENTER);
+ speedLabel.setFont(new Font("SansSerif", Font.BOLD, speedFontSize));
+ speedPanel.add(speedLabel);
+
+ // declare all buttons
+ JButton playButton = new JButton("Play");
+ JButton pauseButton = new JButton("Pause");
+ JButton resetButton = new JButton("Reset");
+ JButton slowButton = new JButton("S");
+ JButton mediumButton = new JButton("M");
+ JButton fastButton = new JButton("F");
+ JButton exitButton = new JButton("Exit");
+
+ // configure control buttons
+ playButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ playButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.play();
+ }
+ });
+ controlPanel.add(playButton);
+ styleControlButton(playButton);
+
+ pauseButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ pauseButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.pause();
+ }
+ });
+ controlPanel.add(pauseButton);
+ styleControlButton(pauseButton);
+
+ resetButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ resetButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.reset();
+ }
+ });
+ controlPanel.add(resetButton);
+ styleControlButton(resetButton);
+
+ // configure speed buttons
+ slowButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ slowButton.setPreferredSize(new Dimension(50, 50));
+ slowButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(500);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(slowButton);
+ styleSpeedButton(slowButton);
+
+ mediumButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ mediumButton.setPreferredSize(new Dimension(50, 50));
+ mediumButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(300);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(mediumButton);
+ styleSpeedButton(mediumButton);
+
+ fastButton.setFont(new Font("SansSerif", Font.PLAIN, speedFontSize));
+ fastButton.setPreferredSize(new Dimension(50, 50));
+ fastButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ panel.setSpeed(100);
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+ }
+ });
+ speedPanel.add(fastButton);
+ styleSpeedButton(fastButton);
+
+ // medium speed as default
+ updateSpeedButtonSelection(slowButton, mediumButton, fastButton);
+
+ // exit button
+ exitButton.setFont(new Font("SansSerif", Font.BOLD, buttonFontSize));
+ exitButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ exitHook.exit();
+ }
+ });
+ styleControlButton(exitButton);
+
+ JPanel exitPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
+ exitPanel.add(exitButton);
+
+ // add control panels to the container
+ JPanel controlContainer = new JPanel(new BorderLayout());
+ controlContainer.add(controlPanel, BorderLayout.NORTH);
+ controlContainer.add(speedPanel, BorderLayout.CENTER);
+
+ // add all panels to the bottom panel
+ bottomPanel.add(controlContainer, BorderLayout.CENTER);
+ bottomPanel.add(exitPanel, BorderLayout.SOUTH);
+
+ // add bottom panel to main layout
+ add(bottomPanel, BorderLayout.SOUTH);
+
+ // another very ugly fix for weird component visibility behaviour
+ new java.util.Timer().schedule(
+ new java.util.TimerTask() {
+ public void run() {
+ try {
+ SwingUtilities.invokeAndWait(new Runnable() {
+ public void run() {
+ // Force the buttons to be visible
+ refreshButton(playButton);
+ refreshButton(pauseButton);
+ refreshButton(resetButton);
+ refreshButton(slowButton);
+ refreshButton(mediumButton);
+ refreshButton(fastButton);
+ refreshButton(exitButton);
+
+ controlPanel.invalidate();
+ controlPanel.validate();
+ controlPanel.repaint();
+
+ speedPanel.invalidate();
+ speedPanel.validate();
+ speedPanel.repaint();
+
+ bottomPanel.invalidate();
+ bottomPanel.validate();
+ bottomPanel.repaint();
+
+ creditsLabel.setVisible(true);
+ creditsLabel.invalidate();
+ creditsLabel.validate();
+ creditsLabel.repaint();
+ }
+ });
+ } catch (Exception e) {
+ log.error("Error during button refresh", e);
+ }
+ }
+ },
+ 1000
+ );
+
+ updateCounters(panel.getGeneration(), panel.getPopulation());
+
+ // another very ugly fix for weird component visibility behaviour
+ new javax.swing.Timer(800, new ActionListener() {
+ private int count = 0;
+ public void actionPerformed(ActionEvent e) {
+ if (count++ < 5) {
+ bottomPanel.invalidate();
+ bottomPanel.validate();
+ bottomPanel.repaint();
+ } else {
+ ((javax.swing.Timer)e.getSource()).stop();
+ }
+ }
+ }).start();
+
+ log.debug("MainUI initialized with buttons and panels");
+ }
+
+ /**
+ * Helper method to refresh button visibility
+ */
+ private void refreshButton(JButton button) {
+ button.setVisible(true);
+ button.invalidate();
+ button.validate();
+ button.repaint();
+ }
+
+ /**
+ * Helper method to style control buttons
+ */
+ private void styleControlButton(JButton button) {
+ button.setOpaque(true);
+ button.setBorderPainted(true);
+ button.setContentAreaFilled(true);
+ button.setFocusable(true);
+ button.setVisible(true);
+
+ button.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+ }
+
+ /**
+ * Helper method to style speed buttons
+ */
+ private void styleSpeedButton(JButton button) {
+ button.setOpaque(true);
+ button.setBorderPainted(true);
+ button.setContentAreaFilled(true);
+ button.setFocusable(true);
+ button.setVisible(true);
+
+ button.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ button.setMargin(new Insets(8, 8, 8, 8));
+ button.setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ /**
+ * Helper method to update speed button selection
+ */
+ private void updateSpeedButtonSelection(JButton slowButton, JButton mediumButton, JButton fastButton) {
+ // reset all buttons to normal state
+ slowButton.setFont(new Font(slowButton.getFont().getName(), Font.PLAIN, slowButton.getFont().getSize()));
+ mediumButton.setFont(new Font(mediumButton.getFont().getName(), Font.PLAIN, mediumButton.getFont().getSize()));
+ fastButton.setFont(new Font(fastButton.getFont().getName(), Font.PLAIN, fastButton.getFont().getSize()));
+
+ slowButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+ mediumButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+ fastButton.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ // determine which button is selected
+ JButton selectedButton;
+ if (panel.getDelay() == 500) {
+ selectedButton = slowButton;
+ } else if (panel.getDelay() == 100) {
+ selectedButton = fastButton;
+ } else {
+ selectedButton = mediumButton;
+ }
+
+ // make selected button stand out with border
+ selectedButton.setBorder(BorderFactory.createLineBorder(Color.BLACK, 3));
+ }
+
+ private Dimension getScreenSize() {
+ if (Panel.rootSize != null) {
+ return Panel.rootSize;
+ }
+ return new Dimension(800, 600); // default fallback
+ }
+
+ private int calculateFontSize(Dimension screenSize, int defaultSize) {
+ if (Util.isKindle()) {
+ if (screenSize.width <= 600) {
+ return Math.max(defaultSize - 4, 8);
+ } else if (screenSize.width <= 800) {
+ return Math.max(defaultSize - 2, 10);
+ }
+ }
+ return defaultSize;
+ }
+
+ public void updateCounters(int generation, int population) {
+ generationLabel.setText("Generation: " + String.valueOf(generation));
+ populationLabel.setText("Population: " + String.valueOf(population));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Panel.java b/src/main/java/uk/co/notori/gol/Panel.java
new file mode 100644
index 0000000..00c8d51
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Panel.java
@@ -0,0 +1,249 @@
+package uk.co.notori.gol;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.ActionListener;
+import java.awt.event.ActionEvent;
+import java.util.Random;
+
+/**
+ * The grid for Conway's Game of Life
+ */
+public class Panel extends JPanel implements MouseListener {
+ public static Dimension rootSize;
+
+ private static final int DEFAULT_WIDTH = 40;
+ private static final int DEFAULT_HEIGHT = 40;
+ private static final int CELL_SIZE = 15;
+ private static final Color ALIVE_COLOR = Color.BLACK;
+ private static final Color DEAD_COLOR = Color.WHITE;
+ private static final Color GRID_COLOR = Color.GRAY;
+
+ private boolean[][] grid;
+ private boolean[][] nextGrid;
+ private int width;
+ private int height;
+ private boolean running;
+ private javax.swing.Timer timer;
+ private int generation;
+ private int population;
+
+ private int updateCounter = 0;
+ // update UI every 7 frames approx. every other second at medium speed
+ private static final int UPDATE_FREQUENCY = 7;
+
+ private Random random = new Random();
+
+ /**
+ * Create game Panel
+ */
+ public Panel(Dimension rootSize) {
+ super();
+
+ Panel.rootSize = rootSize;
+
+ // calculate width and height based on screen size
+ // create space for buttons
+ this.width = rootSize.width / CELL_SIZE;
+ this.height = (rootSize.height - 100) / CELL_SIZE;
+
+ grid = new boolean[width][height];
+ nextGrid = new boolean[width][height];
+
+ // size of the grid
+ setPreferredSize(new Dimension(width * CELL_SIZE, height * CELL_SIZE));
+ setLayout(null);
+
+ running = false;
+ generation = 0;
+ population = 0;
+
+ // Set up timer for animation
+ ActionListener animationListener = new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (running) {
+ evolve();
+ updatePopulation();
+ generation++;
+ repaint();
+
+ updateCounter++;
+
+ // update UI counters so it's not visually annoying
+ if (updateCounter >= UPDATE_FREQUENCY) {
+ updateCounter = 0;
+ // update the UI with new generation and population count
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ }
+ }
+ }
+ };
+ timer = new javax.swing.Timer(300, animationListener);
+
+ initializeRandomGrid();
+
+ addMouseListener(this);
+
+ setVisible(true);
+ setOpaque(true);
+
+ timer.setInitialDelay(1000);
+ timer.start();
+ running = true;
+ }
+
+ /**
+ * Initialize the grid with random cells
+ */
+ public void initializeRandomGrid() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ grid[x][y] = random.nextDouble() < 0.3;
+ }
+ }
+ updatePopulation();
+ generation = 0;
+ updateCounter = 0;
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ repaint();
+ }
+
+ public void clearGrid() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ grid[x][y] = false;
+ }
+ }
+ updatePopulation();
+ generation = 0;
+ updateCounter = 0;
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ repaint();
+ }
+
+
+ private void evolve() {
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ int neighbors = countNeighbors(x, y);
+ if (grid[x][y]) {
+ nextGrid[x][y] = neighbors == 2 || neighbors == 3;
+ } else {
+ nextGrid[x][y] = neighbors == 3;
+ }
+ }
+ }
+
+ boolean[][] temp = grid;
+ grid = nextGrid;
+ nextGrid = temp;
+ }
+
+ private int countNeighbors(int x, int y) {
+ int count = 0;
+ for (int dx = -1; dx <= 1; dx++) {
+ for (int dy = -1; dy <= 1; dy++) {
+ if (dx == 0 && dy == 0) continue;
+
+ int nx = (x + dx + width) % width;
+ int ny = (y + dy + height) % height;
+
+ if (grid[nx][ny]) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ private void updatePopulation() {
+ population = 0;
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (grid[x][y]) {
+ population++;
+ }
+ }
+ }
+ }
+
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ // Draw the grid
+ for (int x = 0; x < width; x++) {
+ for (int y = 0; y < height; y++) {
+ if (grid[x][y]) {
+ g.setColor(ALIVE_COLOR);
+ } else {
+ g.setColor(DEAD_COLOR);
+ }
+ g.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
+
+ g.setColor(GRID_COLOR);
+ g.drawRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
+ }
+ }
+ }
+
+ public void play() {
+ if (!running) {
+ running = true;
+ timer.start();
+ }
+ }
+
+ public void pause() {
+ if (running) {
+ running = false;
+ timer.stop();
+
+ // force a final update when pausing
+ if (getParent() instanceof MainUI) {
+ ((MainUI) getParent()).updateCounters(generation, population);
+ }
+ }
+ }
+
+ public void reset() {
+ pause();
+ initializeRandomGrid();
+ generation = 0;
+ updatePopulation();
+ }
+
+ // empty implementations
+ public void mouseClicked(MouseEvent e) {}
+ public void mousePressed(MouseEvent e) {}
+ public void mouseReleased(MouseEvent e) {}
+ public void mouseEntered(MouseEvent e) {}
+ public void mouseExited(MouseEvent e) {}
+
+ public boolean isRunning() {
+ return running;
+ }
+
+ public int getGeneration() {
+ return generation;
+ }
+
+ public int getPopulation() {
+ return population;
+ }
+
+ public void setSpeed(int delay) {
+ timer.setDelay(delay);
+ }
+
+ public int getDelay() {
+ return timer.getDelay();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/uk/co/notori/gol/Util.java b/src/main/java/uk/co/notori/gol/Util.java
new file mode 100644
index 0000000..6d57e61
--- /dev/null
+++ b/src/main/java/uk/co/notori/gol/Util.java
@@ -0,0 +1,134 @@
+package uk.co.notori.gol;
+
+import com.amazon.kindle.booklet.AbstractBooklet;
+import com.amazon.kindle.booklet.BookletContext;
+import com.amazon.kindle.restricted.content.catalog.ContentCatalog;
+import com.amazon.kindle.restricted.runtime.Framework;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+
+import java.awt.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Date;
+
+public class Util {
+ private static boolean kindle;
+
+ public static boolean isKindle() {
+ return kindle;
+ }
+
+ public static void setKindle(boolean kindleValue) {
+ kindle = kindleValue;
+ }
+
+
+ public static BookletContext getBookletContext(AbstractBooklet booklet) {
+ BookletContext bc = null;
+ Method[] methods = AbstractBooklet.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ if (methods[i].getReturnType() == BookletContext.class) {
+ // Double check that it takes no arguments, too...
+ Class<?>[] params = methods[i].getParameterTypes();
+ if (params.length == 0) {
+ try {
+ bc = (BookletContext) methods[i].invoke(booklet, (Object[]) null);
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ break;
+ }
+ }
+ }
+ return bc;
+ }
+
+ public static Container getUIContainer(AbstractBooklet booklet) throws InvocationTargetException, IllegalAccessException {
+ Method getUIContainer = null;
+
+ // Should be the only method returning a Container in BookletContext...
+ Method[] methods = BookletContext.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ if (method.getReturnType() == Container.class) {
+ // Double check that it takes no arguments, too...
+ Class<?>[] params = method.getParameterTypes();
+ if (params.length == 0) {
+ getUIContainer = method;
+ break;
+ }
+ }
+ }
+
+ if (getUIContainer != null) {
+ BookletContext bc = Util.getBookletContext(booklet);
+ Container rootContainer = (Container) getUIContainer.invoke(bc, (Object[]) null);
+ return rootContainer;
+ } else {
+ return null;
+ }
+ }
+
+ // And this was always obfuscated...
+ // NOTE: Pilfered from KPVBooklet (https://github.com/koreader/kpvbooklet/blob/master/src/com/github/chrox/kpvbooklet/ccadapter/CCAdapter.java)
+ /**
+ * Perform CC request of type "query" and "change"
+ * @param req_type request type of "query" or "change"
+ * @param req_json request json string
+ * @return return json object
+ */
+ private static JSONObject ccPerform(String req_type, String req_json) {
+ ContentCatalog CC = Framework.getService(ContentCatalog.class);
+ try {
+ Method perform = null;
+
+ // Enumeration approach
+ Class<?>[] signature = {String.class, String.class, int.class, int.class};
+ Method[] methods = ContentCatalog.class.getDeclaredMethods();
+ for (int i = 0; i < methods.length; i++) {
+ Method method = methods[i];
+ Class<?>[] params = method.getParameterTypes();
+ if (params.length == signature.length) {
+ int j = 0;
+ while (j < signature.length && params[j].isAssignableFrom(signature[j])) {
+ j++;
+ }
+ if (j == signature.length) {
+ perform = method;
+ break;
+ }
+ }
+ }
+
+ if (perform != null) {
+ return (JSONObject) perform.invoke(CC, new Object[]{req_type, req_json, Integer.valueOf(200), Integer.valueOf(5)});
+ } else {
+ System.err.println("Failed to find perform method!");
+ return new JSONObject();
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t.toString());
+ }
+ }
+
+ public static void updateCCDB(String tag, String path) {
+ long lastAccess = System.currentTimeMillis() / 1000L;
+ String escapedPath = JSONObject.escape(path);
+
+ // Query for the file
+ String json_query = "{\"filter\":{\"Equals\":{\"value\":\"" + escapedPath + "\",\"path\":\"location\"}},\"type\":\"QueryRequest\",\"maxResults\":1,\"sortOrder\":[{\"order\":\"descending\",\"path\":\"lastAccess\"},{\"order\":\"ascending\",\"path\":\"titles[0].collation\"}],\"startIndex\":0,\"id\":1,\"resultType\":\"fast\"}";
+ JSONObject json = Util.ccPerform("query", json_query);
+ JSONArray values = (JSONArray) json.get("values");
+ JSONObject value = (JSONObject) values.get(0);
+ String uuid = (String) value.get("uuid");
+
+ // Update the file metadata
+ String json_change = "{\"commands\":[{\"update\":{\"uuid\":\"" + uuid + "\",\"lastAccess\":" + lastAccess + ",\"displayTags\":[\"" + tag + "\"]" + "}}],\"type\":\"ChangeRequest\",\"id\":1}";
+ Util.ccPerform("change", json_change);
+ }
+}