diff options
Diffstat (limited to 'src/main/java/uk/co/notori/gol')
| -rw-r--r-- | src/main/java/uk/co/notori/gol/KGOLBooklet.java | 138 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Main.java | 35 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/MainScreen.java | 185 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/MainUI.java | 329 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Panel.java | 249 | ||||
| -rw-r--r-- | src/main/java/uk/co/notori/gol/Util.java | 134 |
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); + } +} |
