diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java index cf0b5c40..7aa0157a 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java @@ -1,9 +1,10 @@ // -// $Id: AtlantiBoard.java,v 1.1 2001/10/10 03:35:02 mdb Exp $ +// $Id: AtlantiBoard.java,v 1.2 2001/10/10 06:14:57 mdb Exp $ package com.threerings.venison; import java.awt.*; +import java.awt.event.*; import javax.swing.*; import java.util.Iterator; @@ -15,8 +16,31 @@ import com.threerings.cocktail.cher.dobj.DSet; * Displays the tiles that make up the Venison board. */ public class VenisonBoard - extends JPanel implements VenisonTileCodes + extends JPanel implements TileCodes { + /** The command posted when a tile is placed by the user on the + * board. */ + public static final String TILE_PLACED_CMD = "tile_placed"; + + /** + * Constructs a Venison board. + */ + public VenisonBoard () + { + // create mouse adapters that will let us know when interesting + // mouse events happen + addMouseListener(new MouseAdapter() { + public void mouseClicked (MouseEvent evt) { + VenisonBoard.this.mouseClicked(evt); + } + }); + addMouseMotionListener(new MouseMotionAdapter() { + public void mouseMoved (MouseEvent evt) { + VenisonBoard.this.mouseMoved(evt); + } + }); + } + /** * Sets the tiles that are displayed by this board. Causes the board * to recompute its size based on the new tile layout. @@ -34,15 +58,41 @@ public class VenisonBoard revalidate(); } + /** + * Sets the tile to be placed on the board. The tile will be displayed + * in the square under the mouse cursor where it can be legally placed + * and its orientation will be determined based on the pointer's + * proximity to the edges of the target square. When the user clicks + * the mouse while the tile is in a placeable position, a + * TILE_PLACED command will be dispatched to the + * controller in scope. The coordinates and orientation of the tile + * will have been set to the values specified by the user. + */ + public void setTileToBePlaced (VenisonTile tile) + { + _placingTile = tile; + // update our internal state based on this new placing tile + updatePlacingInfo(true); + repaint(); + } + + // documentation inherited + public void layout () + { + super.layout(); + + // compute our translation coordinates based on our size + _tx = (getWidth() - TILE_WIDTH * _width)/2; + _ty = (getHeight() - TILE_HEIGHT * _height)/2; + } + // documentation inherited public void paintComponent (Graphics g) { super.paintComponent(g); // center the tile display if we are bigger than we need to be - int tx = (getWidth() - TILE_WIDTH * _width)/2; - int ty = (getHeight() - TILE_HEIGHT * _height)/2; - g.translate(tx, ty); + g.translate(_tx, _ty); // iterate over our tiles, painting each of them Iterator iter = _tiles.elements(); @@ -51,8 +101,138 @@ public class VenisonBoard tile.paint(g, _origX, _origY); } + // if we have a placing tile, draw that one as well + if (_placingTile != null && _validPlacement) { + // if the current position is valid, draw the placing tile + _placingTile.paint(g, _origX, _origY); + + // draw a green rectangle around the placing tile + g.setColor(Color.green); + int sx = (_placingTile.x + _origX) * TILE_WIDTH; + int sy = (_placingTile.y + _origY) * TILE_HEIGHT; + g.drawRect(sx, sy, TILE_WIDTH, TILE_HEIGHT); + } + // undo our translations - g.translate(-tx, -ty); + g.translate(-_tx, -_ty); + } + + /** Called by our adapter when the mouse moves. */ + protected void mouseMoved (MouseEvent evt) + { + // we always want to know about our last mouse coordinates + _mouseX = evt.getX() - _tx; + _mouseY = evt.getY() - _ty; + + // if we have a tile to be placed, update its coordinates + if (_placingTile != null) { + if (updatePlacingInfo(false)) { + repaint(); + } + } + } + + /** Called by our adapter when the mouse is clicked. */ + protected void mouseClicked (MouseEvent evt) + { + // if we have a placing tile, we want to dispatch a command + // letting the controller know that the user placed it + if (_placingTile != null) { + } + } + + /** + * Updates the coordinates and orientation of the placing tile based + * on the last known coordinates of the mouse and returns true if the + * coordinates or orientation changed from their previous values. + */ + protected boolean updatePlacingInfo (boolean force) + { + boolean changed = false; + + // convert mouse coordinates into tile coordinates and offset them + // by the origin + int x = divFloor(_mouseX, TILE_WIDTH) - _origX; + int y = divFloor(_mouseY, TILE_HEIGHT) - _origY; + + // if these are different than the values currently in the placing + // tile, update the tile coordinates + if (_placingTile.x != x || _placingTile.y != y || force) { + // update the coordinates of the tile + _placingTile.x = x; + _placingTile.y = y; + + // we've changed the display, so make a note of it + changed = true; + + // we also need to recompute the valid orientations for the + // tile in this new position + _validOrients = TileUtil.computeValidOrients( + _tiles.elements(), _placingTile); + + // if we've changed positions, clear out our valid placement + // flag + _validPlacement = false; + } + + // determine if we should change the orientation based on the + // position of the mouse within the tile boundaries + int rx = _mouseX % TILE_WIDTH; + int ry = _mouseY % TILE_HEIGHT; + int orient = coordToOrient(rx, ry); + + // scan for a legal orientation that is closest to our desired + // orientation + for (int i = 0; i < 4; i++) { + int candOrient = (orient+i)%4; + if (_validOrients[candOrient]) { + if (_placingTile.orientation != candOrient) { + _placingTile.orientation = candOrient; + changed = true; + } + _validPlacement = true; + break; + } + } + + return changed; + } + + /** + * Converts mouse coordinates which are relative to a particular tile, + * into an orientation based on the position within that tile. A tile + * is divided up into four quadrants by lines connecting its four + * corners. If the tile is in a quadrant closes to an edge, it is + * converted to the orientation corresponding with that edge. + * + * @param rx the mouse coordinates modulo tile width. + * @param ry the mouse coordinates modulo tile height. + * + * @return the orientation desired for the tile in which the mouse + * resides. + */ + protected int coordToOrient (int rx, int ry) + { + if (rx > ry) { + if (rx > (TILE_HEIGHT - ry)) { + return EAST; + } else { + return NORTH; + } + } else { + if (rx > (TILE_HEIGHT - ry)) { + return SOUTH; + } else { + return WEST; + } + } + } + + /** Divides the two integers returning the floor of the divided value + * rather than its truncation. */ + protected static int divFloor (int value, int divisor) + { + return (int)Math.floor((double)value/divisor); } // documentation inherited @@ -91,6 +271,10 @@ public class VenisonBoard } } + // spread our bounds out by one + minX -= 1; minY -= 1; + maxX += 1; maxY += 1; + // now we can compute our width and the origin offset _origX = -minX; _origY = -minY; @@ -106,6 +290,7 @@ public class VenisonBoard VenisonBoard board = new VenisonBoard(); TestDSet set = new TestDSet(); + set.addTile(new VenisonTile(CITY_TWO, false, WEST, 0, 0)); set.addTile(new VenisonTile(CITY_FOUR, false, NORTH, 0, 1)); set.addTile(new VenisonTile(CITY_THREE, false, NORTH, 1, 1)); set.addTile(new VenisonTile(CITY_THREE_ROAD, false, NORTH, 1, 2)); @@ -113,6 +298,9 @@ public class VenisonBoard set.addTile(new VenisonTile(CITY_FOUR, false, NORTH, -2, 0)); board.setTiles(set); + VenisonTile placing = new VenisonTile(CITY_TWO, false, NORTH, 0, 0); + board.setTileToBePlaced(placing); + frame.getContentPane().add(board, BorderLayout.CENTER); frame.pack(); SwingUtil.centerWindow(frame); @@ -135,9 +323,26 @@ public class VenisonBoard /** A reference to our tile set. */ protected DSet _tiles; + /** The tile currently being placed by the user. */ + protected VenisonTile _placingTile; + + /** Whether or not the current position and orientation of the placing + * tile is valid. */ + protected boolean _validPlacement = false; + + /** An array indicating which of the four directions are valid + * placements based on the current position of the placing tile. */ + protected boolean[] _validOrients; + + /** Our render offset in pixels. */ + protected int _tx, _ty; + /** The offset in tile coordinates of the origin. */ protected int _origX, _origY; /** The width and height of the board in tile coordinates. */ protected int _width, _height; + + /** The last known position of the mouse. */ + protected int _mouseX, _mouseY; } diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java index 42e982c8..39190c94 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java @@ -1,8 +1,9 @@ // -// $Id: AtlantiTile.java,v 1.1 2001/10/10 03:35:02 mdb Exp $ +// $Id: AtlantiTile.java,v 1.2 2001/10/10 06:14:57 mdb Exp $ package com.threerings.venison; +import java.awt.Color; import java.awt.Graphics; import java.io.IOException; @@ -15,12 +16,8 @@ import com.threerings.cocktail.cher.dobj.DSet; * Represents a single tile in play on the Venison game board. */ public class VenisonTile - implements DSet.Element, VenisonTileCodes + implements DSet.Element, TileCodes, Cloneable { - /** The starting tile. */ - public static final VenisonTile STARTING_TILE = - new VenisonTile(CITY_ONE_ROAD_STRAIGHT, false, NORTH, 0, 0); - /** The tile type. */ public int type; @@ -46,6 +43,15 @@ public class VenisonTile this.y = y; } + /** + * Constructs a tile with the type information set, but in the default + * NORTH orientation and with no position. + */ + public VenisonTile (int type, boolean hasShield) + { + this(type, hasShield, NORTH, 0, 0); + } + /** * Constructs a blank tile, suitable for unserialization. */ @@ -72,11 +78,52 @@ public class VenisonTile int sy = (y + yoff) * TILE_HEIGHT; // draw a rectangle + g.setColor(Color.black); g.drawRect(sx, sy, TILE_WIDTH, TILE_HEIGHT); // and draw our tile id in the middle for now (we'll eventually // draw tile images) - g.drawString(type + "/" + x + "/" + y, sx + 20, sy + 20); + String txt = type + "/" + x + "/" + y + "/" + + ORIENT_NAMES[orientation]; + g.drawString(txt, sx + 20, sy + 20); + + // draw little dots indicating the color of our sides + for (int i = 0; i < 4; i++) { + // adjust for our orientation + switch (TileUtil.getEdge(type, (i+(4-orientation))%4)) { + case GRASS: + g.setColor(Color.green); + break; + case ROAD: + g.setColor(Color.white); + break; + case CITY: + g.setColor(Color.red); + break; + } + switch (i) { + case NORTH: + g.fillOval(sx + TILE_WIDTH/2 - 2, sy + 2, 4, 4); + break; + case EAST: + g.fillOval(sx + TILE_WIDTH - 6, sy + TILE_HEIGHT/2 - 2, 4, 4); + break; + case SOUTH: + g.fillOval(sx + TILE_WIDTH/2 - 2, sy + TILE_HEIGHT - 6, 4, 4); + break; + case WEST: + g.fillOval(sx + 2, sy + TILE_HEIGHT/2 - 2, 4, 4); + break; + } + } + } + + /** + * Returns a copy of this Venison tile object. + */ + public Object clone () + { + return new VenisonTile(type, hasShield, orientation, x, y); } // documentation inherited diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/TileCodes.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/TileCodes.java index 6bab75a1..d87aa602 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/data/TileCodes.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/TileCodes.java @@ -1,5 +1,5 @@ // -// $Id: TileCodes.java,v 1.1 2001/10/10 03:35:02 mdb Exp $ +// $Id: TileCodes.java,v 1.2 2001/10/10 06:14:57 mdb Exp $ package com.threerings.venison; @@ -7,7 +7,7 @@ package com.threerings.venison; * A repository for constants related to the tiles that are used in the * game of Venison. */ -public interface VenisonTileCodes +public interface TileCodes { /** A four-sided city tile. */ public static final int CITY_FOUR = 1; @@ -75,6 +75,7 @@ public interface VenisonTileCodes /** A curved road segment. */ public static final int CURVED_ROAD = 19; + /** A tile orientation constant indicating the tile is in its default * orientation. */ public static final int NORTH = 0; @@ -91,9 +92,23 @@ public interface VenisonTileCodes * degrees clockwise from its default orientation. */ public static final int WEST = 3; + /** A mapping from orientation codes to a string representation. */ + public static final String[] ORIENT_NAMES = + new String[] { "N", "E", "S", "W" }; + /** The tile image width in pixels. */ public static int TILE_WIDTH = 90; /** The tile image height in pixels. */ public static int TILE_HEIGHT = 90; + + + /** A tile edge constant indicating a city edge. */ + public static final int CITY = 0; + + /** A tile edge constant indicating a grass edge. */ + public static final int GRASS = 1; + + /** A tile edge constant indicating a road edge. */ + public static final int ROAD = 2; } diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java b/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java index bce70922..971b782c 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java @@ -1,5 +1,5 @@ // -// $Id: AtlantiManager.java,v 1.2 2001/10/10 03:35:02 mdb Exp $ +// $Id: AtlantiManager.java,v 1.3 2001/10/10 06:14:57 mdb Exp $ package com.threerings.venison; @@ -37,7 +37,7 @@ public class VenisonManager // clear out the tile set _venobj.setTiles(new DSet(VenisonTile.class)); - _venobj.addToTiles(VenisonTile.STARTING_TILE); + _venobj.addToTiles(TileUtil.STARTING_TILE); } protected VenisonObject _venobj; diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java b/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java new file mode 100644 index 00000000..f7c1ebc9 --- /dev/null +++ b/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java @@ -0,0 +1,200 @@ +// +// $Id: TileUtil.java,v 1.1 2001/10/10 06:14:57 mdb Exp $ + +package com.threerings.venison; + +import java.util.ArrayList; +import java.util.List; +import java.util.Iterator; + +/** + * Utility functions relating to the Venison tiles. + */ +public class TileUtil implements TileCodes +{ + /** The starting tile. */ + public static final VenisonTile STARTING_TILE = + new VenisonTile(CITY_ONE_ROAD_STRAIGHT, false, NORTH, 0, 0); + + /** + * Returns a list containing the standard tile set for the Venison + * game. The list is a clone, so it can be bent, folded and modified + * by the caller. + */ + public static List getStandardTileSet () + { + return (List)TILE_SET.clone(); + } + + /** + * Scans the supplied tile set to determine which of the four + * orientations of the supplied target tile would result in a valid + * placement of that tile (valid placement meaning that all of its + * edges match up with neighboring tiles, it abuts at least one tile + * and it does not occupy the same space as any existing tile). The + * position of the target tile is assumed to be the desired placement + * position and the current orientation of the target tile is ignored. + * + * @param tiles an iterator that will iterate over all of the tiles + * currently placed on the board. + * @param target the tile whose valid orientations we wish to compute. + * + * @return an array of boolean values indicating whether or not the + * tile can be placed in each of the cardinal directions (which match + * up with the direction constants specified in {@link TileCodes}. + */ + public static boolean[] computeValidOrients ( + Iterator tiles, VenisonTile target) + { + // this contains a count of tiles that match up with the candidate + // tile in each of its four orientations + int[] matches = new int[4]; + + while (tiles.hasNext()) { + VenisonTile tile = (VenisonTile)tiles.next(); + + // figure out where this tile is in relation to the candidate + int xdiff = tile.x - target.x; + int ydiff = tile.y - target.y; + int sum = Math.abs(xdiff) + Math.abs(ydiff); + + if (sum == 0) { + // they overlap, nothing doing + return new boolean[4]; + + } else if (sum == 1) { + // they're neighbors, we may have a match + int targetEdge = EDGE_MAP[(ydiff+1)*3 + xdiff+1]; + // we want the edge of the placed tile that matches up + // with the tile in the candidate location, but we also + // need to take into account the orientation of the placed + // tile + int tileEdge = (targetEdge+(4-tile.orientation)+2) % 4; + + // we iterate over the four possible orientations of the + // target tile + for (int i = 0; i < 4; i++) { + // we compare the edge of the placed tile (which never + // changes) with the edge of the target tile which is + // adjusted based on the target tile's orientation + if (getEdge(tile.type, tileEdge) == + getEdge(target.type, (targetEdge+(4-i)) % 4)) { + // increment the edge matches + matches[i]++; + + } else { + // if we have a mismatch, we want to ensure that + // we screw this orientation up for good, so we + // deduct a large value from the array to ensure + // that it will remain less than zero regardless + // of which of the other three tiles match in this + // orientation + matches[i] -= 10; + } + } + } + } + + // for every orientation that we have a positive number of edge + // matches, we have a valid orientation + boolean[] orients = new boolean[4]; + for (int i = 0; i < matches.length; i++) { + orients[i] = (matches[i] > 0); + } + return orients; + } + + /** + * Returns the edge type for specified edge of the specified tile + * type. + * + * @param tileType the type of the tile in question. + * @param edge the direction constant indicating the edge in which we + * are interested. + * + * @return the edge constant for the edge in question. + */ + public static int getEdge (int tileType, int edge) + { + return TILE_EDGES[4*tileType + edge]; + } + + /** Used to generate our standard tile set. */ + protected static void addTiles (int count, List list, VenisonTile tile) + { + for (int i = 0; i < count-1; i++) { + list.add(tile.clone()); + } + list.add(tile); + } + + /** The standard tile set for a game of Venison. */ + protected static ArrayList TILE_SET = new ArrayList(); + + // create our standard tile set + static { + addTiles(1, TILE_SET, new VenisonTile(CITY_FOUR, true)); + + addTiles(3, TILE_SET, new VenisonTile(CITY_THREE, false)); + addTiles(1, TILE_SET, new VenisonTile(CITY_THREE, true)); + addTiles(1, TILE_SET, new VenisonTile(CITY_THREE_ROAD, false)); + addTiles(2, TILE_SET, new VenisonTile(CITY_THREE_ROAD, true)); + + addTiles(3, TILE_SET, new VenisonTile(CITY_TWO, false)); + addTiles(2, TILE_SET, new VenisonTile(CITY_TWO, true)); + addTiles(3, TILE_SET, new VenisonTile(CITY_TWO_ROAD, false)); + addTiles(2, TILE_SET, new VenisonTile(CITY_TWO_ROAD, true)); + addTiles(1, TILE_SET, new VenisonTile(CITY_TWO_ACROSS, false)); + addTiles(2, TILE_SET, new VenisonTile(CITY_TWO_ACROSS, true)); + + addTiles(2, TILE_SET, new VenisonTile(DISCONNECTED_CITY_TWO, false)); + addTiles(3, TILE_SET, + new VenisonTile(DISCONNECTED_CITY_TWO_ACROSS, false)); + + addTiles(5, TILE_SET, new VenisonTile(CITY_ONE, false)); + addTiles(3, TILE_SET, new VenisonTile(CITY_ONE_ROAD_RIGHT, false)); + addTiles(3, TILE_SET, new VenisonTile(CITY_ONE_ROAD_LEFT, false)); + addTiles(3, TILE_SET, new VenisonTile(CITY_ONE_ROAD_TEE, false)); + addTiles(3, TILE_SET, new VenisonTile(CITY_ONE_ROAD_STRAIGHT, false)); + + addTiles(4, TILE_SET, new VenisonTile(CLOISTER, false)); + addTiles(2, TILE_SET, new VenisonTile(CLOISTER_ROAD, false)); + + addTiles(1, TILE_SET, new VenisonTile(FOUR_WAY_ROAD, false)); + addTiles(4, TILE_SET, new VenisonTile(THREE_WAY_ROAD, false)); + addTiles(8, TILE_SET, new VenisonTile(STRAIGHT_ROAD, false)); + addTiles(9, TILE_SET, new VenisonTile(CURVED_ROAD, false)); + } + + /** Used to figure out which edges match up to which when comparing + * adjacent tiles. */ + protected static final int[] EDGE_MAP = new int[] { + -1, NORTH, -1, + WEST, -1, EAST, + -1, SOUTH, -1 + }; + + /** A table indicating which tiles have which edges. */ + protected static final int[] TILE_EDGES = new int[] { + -1, -1, -1, -1, // null tile + CITY, CITY, CITY, CITY, // CITY_FOUR + CITY, CITY, GRASS, CITY, // CITY_THREE + CITY, CITY, ROAD, CITY, // CITY_THREE_ROAD + CITY, GRASS, GRASS, CITY, // CITY_TWO + CITY, ROAD, ROAD, CITY, // CITY_TWO_ROAD + GRASS, CITY, GRASS, CITY, // CITY_TWO_ACROSS + CITY, CITY, GRASS, GRASS, // DISCONNECTED_CITY_TWO + GRASS, CITY, GRASS, CITY, // DISCONNECTED_CITY_TWO_ACROSS + CITY, GRASS, GRASS, GRASS, // CITY_ONE + CITY, ROAD, ROAD, GRASS, // CITY_ONE_ROAD_RIGHT + CITY, GRASS, ROAD, ROAD, // CITY_ONE_ROAD_LEFT + CITY, ROAD, ROAD, ROAD, // CITY_ONE_ROAD_TEE + CITY, ROAD, GRASS, ROAD, // CITY_ONE_ROAD_STRAIGHT + GRASS, GRASS, GRASS, GRASS, // CLOISTER + GRASS, GRASS, ROAD, GRASS, // CLOISTER_ROAD + ROAD, ROAD, ROAD, ROAD, // FOUR_WAY_ROAD + GRASS, ROAD, ROAD, ROAD, // THREE_WAY_ROAD + ROAD, GRASS, ROAD, GRASS, // STRAIGHT_ROAD + GRASS, GRASS, ROAD, ROAD, // CURVED_ROAD + }; +}