/* Hipbone - game board, rules, ideas, etc. Copyright (C) Charles Cameron Copyright (C) 2004 John Comeau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package com.jcomeau.hipbone; import java.applet.*; import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.awt.font.*; import java.net.*; import java.util.*; import com.jcomeau.*; public class Hipbone extends Applet implements ComponentListener, MouseListener { static int[] size = {400, 400}; Logger gamelog; URL logurl; Frame dialogFrame = new Frame(); public String RCSId = "$Id: Hipbone.java,v 1.77 2004/04/30 05:45:24 jcomeau Exp $"; public String state = "Uninitialized"; boolean display = false; // leave false if dimensions too small boolean stopped = false; // applet stop() will set this true boolean mouseInside = false; int[] lastMouseDown = {-1, -1, 0}; int[] lastMouseUp = {-1, -1, 0}; Ellipse2D.Double[] joints = new Ellipse2D.Double[10]; int gameHeight, gameWidth; double boardHeight, boardWidth; double jointHeight = 0; double jointWidth = 0; double displayedJointHeight = 0; double displayedJointWidth = 0; int[][] jointPositions = { // locations of 5x5 grid of joints 1 on up {0, 2}, {1, 1}, {1, 3}, {2, 0}, {2, 4}, {3, 1}, {3, 3}, {2, 2}, {3, 2}, {4, 2}}; double[][] jointCenters = new double[10][2]; // fill in later int[][] jointConnections = { // 1-based here for ease of comparison! {2, 3, 8}, {1, 3, 4, 6, 8, 9}, {1, 2, 5, 7, 8, 9}, {2, 6, 8, 9}, {3, 7, 8, 9}, {2, 4, 9, 10}, {3, 5, 9, 10}, {1, 2, 3, 4, 5, 9}, {2, 3, 4, 5, 6, 7, 8, 10}, // 9 links to everything except joint 1 {6, 7, 9}}; int[][] filledTriangles = { // colored-in areas of WaterBird {2, 8, 9}, {3, 8, 9}, {4, 6, 9}, {5, 7, 9}}; Color fillColor = new Color(221, 221, 221); public static void main(String[] args) { // let this be used as an app Common.debugging = true; CloseableFrame frame = new CloseableFrame(); Hipbone applet = new Hipbone(); frame.add(applet); frame.setSize(size[0], size[1]); applet.init(); frame.show(); applet.start(); } public void init() { Common.debugprintln(RCSId); try { logurl = getCodeBase(); } catch (Exception noCodeBase) { logurl = null; } (gamelog = new Logger(this, logurl)).start(); setSize(); // allocate board size and joint size based on window size state = "Initialized"; addComponentListener(this); addMouseListener(this); } public void drawLine(Graphics2D graphics2D, double x0, double y0, double x1, double y1) { graphics2D.draw(new Line2D.Double(x0, y0, x1, y1)); } public void fillTriangle(Graphics2D graphics2D, double x0, double y0, double x1, double y1, double x2, double y2) { GeneralPath triangle = new GeneralPath(); triangle.moveTo((float)x0, (float)y0); triangle.lineTo((float)x1, (float)y1); triangle.lineTo((float)x2, (float)y2); triangle.lineTo((float)x0, (float)y0); Paint savedColor = graphics2D.getPaint(); graphics2D.setPaint(fillColor); Common.debugprintln("painting triangle"); graphics2D.fill(triangle); graphics2D.setPaint(savedColor); } public void fillTriangles(Graphics2D graphics2D) { for (int i = 0; i < filledTriangles.length; i++) { fillTriangle(graphics2D, jointCenters[filledTriangles[i][0] - 1][0], jointCenters[filledTriangles[i][0] - 1][1], jointCenters[filledTriangles[i][1] - 1][0], jointCenters[filledTriangles[i][1] - 1][1], jointCenters[filledTriangles[i][2] - 1][0], jointCenters[filledTriangles[i][2] - 1][1]); } } public void setSizeGlobals() { try { // if this is an applet, it will know its own height and width gameWidth = getWidth(); gameHeight = getHeight(); Common.debugprintln("Height: " + gameHeight + ", Width: " + gameWidth); if (gameHeight * gameWidth == 0) { throw new Exception("No dimensions available"); } } catch (Exception noDimensions) { /* if this is run as an application, it most likely will have to get its * dimensions from the parent container */ try { gameWidth = getParent().getHeight(); gameHeight = getParent().getHeight(); Common.debugprintln("Parent dimensions: " + gameHeight + ", " + gameWidth); if (gameHeight * gameWidth == 0) { throw new Exception("No parent dimensions"); } } catch (Exception noParentDimensions) { Common.debugprintln("Cannot determine dimensions, setting manually"); gameWidth = size[0]; gameHeight = size[1]; } } } public void setSize() { // note: NO ARGS to this method display = false; // no painting till we're done setSizeGlobals(); // set globals with actual size as reported by system // now that we know we have a width and height, set them equal to smaller // we can always find out actual dimensions if needed with getWidth() etc. if (gameWidth > gameHeight) { gameWidth = gameHeight; } else { gameHeight = gameWidth; } // sets size of game elements according to size of window // board dimensions arrived at by trial-and-error process /* constraints: edges of circles must touch board * 5 circles at most in one line across board * centers equally spaced and a multiple of 5 pixels to avoid aliasing * circle diameter is optimally 7/12 distance between centers * board should be as large as window allows for easier readability */ // hints: start at > required size and work down till < // assume height=width throughout since it's a square board with circle nodes // (this won't be true for "dart board" nor most other boards) jointWidth = gameWidth / 4; jointWidth -= (jointWidth % 5); if (jointWidth <= 0) return; // sanity check, leave display off while ((boardWidth = ( (jointWidth * 4) + (displayedJointWidth = (int)((7.0 / 12) * jointWidth)) )) >= gameWidth) jointWidth -= 5; Common.debugprintln("boardWidth = " + boardWidth); Common.debugprintln("jointWidth = " + jointWidth); Common.debugprintln("displayedJointWidth = " + displayedJointWidth); boardHeight = boardWidth; jointHeight = jointWidth; displayedJointHeight = displayedJointWidth; for (int i = 0; i < joints.length; i++) { jointCenters[i][0] = (jointPositions[i][0] * jointWidth) + (displayedJointWidth / 2); jointCenters[i][1] = (jointPositions[i][1] * jointHeight) + (displayedJointHeight / 2); } // create the nodes; ideally each would be a separate subclass of Panel for (int i = 0; i < joints.length; i++) { joints[i] = new Ellipse2D.Double( jointCenters[i][0] - (displayedJointWidth / 2), jointCenters[i][1] - (displayedJointHeight / 2), displayedJointWidth, displayedJointHeight); } // since this is (or is pretending to be) an applet, let's try resize() // update: no, let's not, it can get stuck in endless loop that way //resize((int)boardWidth, (int)boardHeight); // no biggie if it fails //setSizeGlobals(); // now set globals with new size if it worked display = true; // now allow it to be painted } public void stop() { stopped = true; Common.debugprintln("applet stopped"); super.stop(); } public void destroy() { gamelog.pleaseStop = true; super.destroy(); } public void start() { super.start(); stopped = false; } public void paint(Graphics graphics) { if (!display) return; try { Graphics2D graphics2D = (Graphics2D)graphics; if (false) graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); FontMetrics fontMetrics = null; graphics2D.setPaint(Color.white); // fill board background first graphics2D.fill(new Rectangle2D.Double(0, 0, boardHeight, boardWidth)); fillTriangles(graphics2D); graphics2D.setPaint(Color.black); graphics2D.setStroke(new BasicStroke(1)); Common.debugprintln("drawing border square"); drawLine(graphics2D, 0, 0, 0, boardHeight); drawLine(graphics2D, 0, 0, boardWidth, 0); drawLine(graphics2D, 0, boardHeight, boardWidth, boardHeight); drawLine(graphics2D, boardWidth, 0, boardWidth, boardHeight); Common.debugprintln("drawing inner square"); double offset = displayedJointWidth / 2; // radius, plus... offset += (offset * (Math.sqrt(2) / 2)); // where bullet meets bone drawLine(graphics2D, offset, offset, offset, boardHeight - offset); drawLine(graphics2D, offset, offset, boardWidth - offset, offset); drawLine(graphics2D, offset, boardHeight - offset, boardWidth - offset, boardHeight - offset); drawLine(graphics2D, boardWidth - offset, offset, boardWidth - offset, boardHeight - offset); graphics2D.setStroke(new BasicStroke(2)); Common.debugprintln("drawing " + joints.length + " joints"); for (int i = 0; i < joints.length; i++) { for (int j = 0; j < jointConnections[i].length; j++) { int k = jointConnections[i][j] - 1; // zero-base the connection number drawLine(graphics2D, jointCenters[i][0], jointCenters[i][1], jointCenters[k][0], jointCenters[k][1]); } } // fill first, to white out the lines where the circles will be graphics2D.setPaint(Color.white); for (int i = 0; i < joints.length; i++) { graphics2D.fill(joints[i]); } // now draw the bold circles in black /* if we had done this first, the fill would white out all but * the outermost pixel of the circle */ graphics2D.setPaint(Color.black); for (int i = 0; i < joints.length; i++) { graphics2D.draw(joints[i]); } Common.debugprintln("Drawing in numbers"); graphics2D.setFont(condensedFont("TimesNewRoman", Font.BOLD, (int)(displayedJointHeight / 6))); for (int i = 0; i < joints.length; i++) { centerShow(graphics2D, Integer.toString(i + 1), jointCenters[i], (int)(displayedJointHeight / 2) - 3); } Common.debugprintln("Drawing in conceptions"); graphics2D.setFont(graphics2D.getFont().deriveFont(Font.PLAIN)); for (int i = 0; i < joints.length; i++) { String conception = gamelog.getConception(i); if (conception != null && conception.length() > 0) { centerShow(graphics2D, '"' + conception + '"', jointCenters[i], (int)(jointHeight / 10)); } } Common.debugprintln("Drawing in names and play numbers"); graphics2D.setFont(graphics2D.getFont().deriveFont(Font.BOLD)); for (int i = 0; i < joints.length; i++) { String userInfo = gamelog.getPlayer(i % 2) + ": " + (i + 1); int joint = gamelog.playOrder[i] - 1; if (joint > -1) { centerShow(graphics2D, userInfo, jointCenters[joint], -(int)(jointHeight / 20)); } } try { showStatus(state); } catch (Exception noShow) { Common.debugprintln("cannot show status " + state + ": " + noShow); } } catch (Throwable ignored) { Common.debugprintln("Cannot draw yet: " + ignored); // probably init() code not yet finished... so why is it painting? // found out why; running as app, and set visible before applet.init() } } public void centerShow(Graphics2D graphics2D, String text, double[] center, int yOffset) { FontMetrics fontMetrics = graphics2D.getFontMetrics(); int x = (int)center[0] - (int)(fontMetrics.stringWidth(text) / 2); int y = (int)center[1] + yOffset; graphics2D.drawString(text, x, y); } public Font condensedFont(String name, int style, int size) { Hashtable attributes = new Hashtable(); Font baseFont = new Font(name, style, size); attributes.put(TextAttribute.WIDTH, TextAttribute.WIDTH_CONDENSED); return baseFont.deriveFont(attributes); } public int whichJoint(int x, int y) { int[] xy = {x, y}; return whichJoint(xy); } public int whichJoint(int[] xy) { int x, y; x = xy[0]; y = xy[1]; for (int i = 0; i < joints.length; i++) { if (joints[i].contains(x, y)) return i; } return -1; } public void componentMoved(ComponentEvent event) {} public void componentHidden(ComponentEvent event) {} public void componentShown(ComponentEvent event) {} public void componentResized(ComponentEvent event) { Common.debugprintln("resized"); setSize(); repaint(); } public void mouseEntered(MouseEvent event) { mouseInside = true; } public void mousePressed(MouseEvent event) { Common.debugprintln("mouse pressed: " + event); if (event.isPopupTrigger()) { lastMouseDown[0] = -1; lastMouseDown[1] = -1; lastMouseDown[2] = 0; Common.debugprintln("show popup menu"); } else { lastMouseDown[0] = event.getX(); lastMouseDown[1] = event.getY(); lastMouseDown[2] = event.getButton(); } } public void mouseReleased(MouseEvent event) { if (event.isPopupTrigger()) { lastMouseDown[0] = -1; lastMouseDown[1] = -1; lastMouseDown[2] = 0; Common.debugprintln("show popup menu"); } else { lastMouseUp[0] = event.getX(); lastMouseUp[1] = event.getY(); lastMouseUp[2] = event.getButton(); } } public void mouseExited(MouseEvent event) { mouseInside = false; } public void mouseClicked(MouseEvent event) { int down = whichJoint(lastMouseDown); int up = whichJoint(lastMouseUp); boolean sameButton = (lastMouseDown[2] == lastMouseUp[2]); if (down != -1 && down == up && sameButton) { state = "Joint " + (down + 1) + " clicked"; repaint(); new NamesDialog(dialogFrame, this, down + 1); } } }