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 66634ddc..7e6ea45d 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiBoard.java @@ -1,5 +1,5 @@ // -// $Id: AtlantiBoard.java,v 1.9 2001/10/17 04:34:14 mdb Exp $ +// $Id: AtlantiBoard.java,v 1.10 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -173,6 +173,18 @@ public class VenisonBoard "find no associated tile! [key=" + key + "]."); } + /** + * Turn off the ability to place a piecen on the most recently played + * tile. This function assumes that the mouse will not be over a valid + * piecen placement at the time this function is called (it expects + * that it will be over a button of some sort that says something to + * the effect of "skip placement for this turn"). + */ + public void cancelPiecenPlacement () + { + _placingPiecen = false; + } + /** * 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 diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java index df0658fb..cb98c1b0 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java @@ -55,10 +55,10 @@ public class VenisonController _venobj = (VenisonObject)plobj; // find out what our index is and use that as our piecen color - int oidx = ListUtil.indexOfEqual(_venobj.players, _self.username); - if (oidx != -1) { + _selfIndex = ListUtil.indexOfEqual(_venobj.players, _self.username); + if (_selfIndex != -1) { // use our player index as the piecen color directly - _panel.board.setNewPiecenColor(oidx); + _panel.board.setNewPiecenColor(_selfIndex); } // grab the tiles and piecens from the game object and configure @@ -137,6 +137,13 @@ public class VenisonController _venobj.getOid(), PLACE_TILE_REQUEST, args); _ctx.getDObjectManager().postEvent(mevt); + // if we have no piecens to place, we immediately disable + // piecen placement in the board + int pcount = TileUtil.countPiecens(_venobj.piecens, _selfIndex); + if (pcount >= PIECENS_PER_PLAYER) { + _panel.board.cancelPiecenPlacement(); + } + // enable the noplace button _panel.noplace.setEnabled(true); @@ -153,6 +160,9 @@ public class VenisonController _panel.noplace.setEnabled(false); } else if (action.getActionCommand().equals(PLACE_NOTHING)) { + // turn off piecen placement in the board + _panel.board.cancelPiecenPlacement(); + // the user doesn't want to place anything this turn. send a // place nothing request to the server MessageEvent mevt = new MessageEvent( @@ -177,4 +187,7 @@ public class VenisonController /** A reference to our body object. */ protected BodyObject _self; + + /** Our player index or -1 if we're not a player. */ + protected int _selfIndex; } diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiPanel.java b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiPanel.java index aebc5046..4e54cd07 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiPanel.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiPanel.java @@ -33,6 +33,9 @@ public class VenisonPanel */ public VenisonPanel (ParlorContext ctx, VenisonController controller) { + // give ourselves a wee bit of a border + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + HGroupLayout gl = new HGroupLayout(HGroupLayout.STRETCH); gl.setOffAxisPolicy(HGroupLayout.STRETCH); setLayout(gl); @@ -47,9 +50,15 @@ public class VenisonPanel // create our side panel VGroupLayout sgl = new VGroupLayout(VGroupLayout.STRETCH); sgl.setOffAxisPolicy(VGroupLayout.STRETCH); + sgl.setJustification(VGroupLayout.TOP); JPanel sidePanel = new JPanel(sgl); + // add a player info view to the side panel + sidePanel.add(new JLabel("Scores:"), VGroupLayout.FIXED); + sidePanel.add(new PlayerInfoView(), VGroupLayout.FIXED); + // add a turn indicator to the side panel + sidePanel.add(new JLabel("Current turn:"), VGroupLayout.FIXED); sidePanel.add(new TurnIndicatorView(), VGroupLayout.FIXED); // add a "place nothing" button diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/client/PlayerInfoView.java b/projects/atlanti/src/java/com/samskivert/atlanti/client/PlayerInfoView.java new file mode 100644 index 00000000..6a0397c7 --- /dev/null +++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/PlayerInfoView.java @@ -0,0 +1,116 @@ +// +// $Id: PlayerInfoView.java,v 1.1 2001/10/17 23:27:52 mdb Exp $ + +package com.threerings.venison; + +import java.awt.Color; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +/** + * Displays each of the players in the game, their piece color and their + * score. + */ +public class PlayerInfoView + extends JPanel implements PlaceView, AttributeChangeListener +{ + /** + * Constructs a new player info panel, ready for insertion into a + * Venison game panel. + */ + public PlayerInfoView () + { + setLayout(new GridBagLayout()); + + // we don't add our player info until we know how many players are + // in the game (which we find out once we enter the game room) + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + // we want to grab a reference to the game object and add + // ourselves as an attribute change listener + _venobj = (VenisonObject)plobj; + _venobj.addListener(this); + + // we need a score labels array so that we can keep track of the + // score labels + _scoreLabels = new JLabel[_venobj.players.length]; + + // now that we're here, we can add an entry for every player + for (int i = 0; i < _venobj.players.length; i++) { + addPlayer(i, _venobj.players[i]); + } + + // if we have scores, update them + if (_venobj.scores.length == _venobj.players.length) { + updateScores(); + } + } + + /** + * Adds a player to the display. + */ + protected void addPlayer (int idx, String username) + { + GridBagConstraints c = new GridBagConstraints(); + + // create a label for their username + c.fill = GridBagConstraints.HORIZONTAL; + c.weightx = 1.0; + JLabel unlabel = new JLabel(username); + unlabel.setForeground(Feature.PIECEN_COLOR_MAP[idx]); + add(unlabel, c); + + // create a label for their score + c.weightx = 0.0; + c.fill = GridBagConstraints.NONE; + c.gridwidth = GridBagConstraints.REMAINDER; + _scoreLabels[idx] = new JLabel("0"); + _scoreLabels[idx].setForeground(Color.black); + add(_scoreLabels[idx], c); + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + // remove our listening self + _venobj.removeListener(this); + _venobj = null; + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + // we care about the scores (which change) + if (event.getName().equals(VenisonObject.SCORES)) { + updateScores(); + } + } + + /** + * Reads the scores from the game object and updates the view. + */ + protected void updateScores () + { + for (int i = 0; i < _venobj.scores.length; i++) { + _scoreLabels[i].setText(Integer.toString(_venobj.scores[i])); + } + } + + /** A reference to the game object. */ + protected VenisonObject _venobj; + + /** References to our score label components. */ + protected JLabel[] _scoreLabels; +} diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiCodes.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiCodes.java index 7e0e7a24..89bb78ef 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiCodes.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiCodes.java @@ -1,5 +1,5 @@ // -// $Id: AtlantiCodes.java,v 1.2 2001/10/17 02:19:54 mdb Exp $ +// $Id: AtlantiCodes.java,v 1.3 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -8,6 +8,9 @@ package com.threerings.venison; */ public interface VenisonCodes { + /** The number of piecens provided to each player. */ + public static final int PIECENS_PER_PLAYER = 7; + /** The name of the command posted by the {@link VenisonBoard} when * the user places a tile into a valid position. */ public static final String TILE_PLACED = "tile_placed"; 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 d4a5ad2a..be3ce8c9 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiTile.java @@ -1,5 +1,5 @@ // -// $Id: AtlantiTile.java,v 1.8 2001/10/17 02:19:54 mdb Exp $ +// $Id: AtlantiTile.java,v 1.9 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -159,11 +159,8 @@ public class VenisonTile * @param featureIndex the index of the feature to update. * @param claimGroup the claim group to associate with the feature. */ - public void setFeatureGroup (int featureIndex, int claimGroup) + public void setClaimGroup (int featureIndex, int claimGroup) { -// Log.info("Setting feature group [tile=" + this + -// ", fidx=" + featureIndex + ", cgroup=" + claimGroup + "]."); - // update the claim group slot for this feature claims[featureIndex] = claimGroup; @@ -179,7 +176,7 @@ public class VenisonTile * Piecen#featureIndex} field is assumed to be initialized to the * feature index of this tile on which the piecen is to be placed. * - *

Note that this will call {@link TileUtil#setFeatureGroup} to + *

Note that this will call {@link TileUtil#setClaimGroup} to * propagate the claiming of this feature to all neighboring tiles if * a non-null tiles array is supplied to the function. * @@ -210,8 +207,10 @@ public class VenisonTile int claimGroup = TileUtil.nextClaimGroup(); Log.info("Creating claim group [cgroup=" + claimGroup + ", tile=" + this + ", fidx=" + piecen.featureIndex + "]."); - TileUtil.setFeatureGroup( - tiles, this, piecen.featureIndex, claimGroup, 0); + TileUtil.setClaimGroup( + tiles, this, piecen.featureIndex, claimGroup); + // update our piecen with the claim group as well + piecen.claimGroup = claimGroup; } } @@ -254,7 +253,7 @@ public class VenisonTile // if we have a piecen on this tile, render it as well if (piecen != null && piecen.featureIndex == i) { features[i].paintPiecen( - g, orientation, piecen.color, piecen.claimGroup); + g, orientation, piecen.owner, piecen.claimGroup); } } @@ -262,6 +261,12 @@ public class VenisonTile g.setColor(Color.black); g.drawRect(0, 0, TILE_WIDTH, TILE_HEIGHT); + // if we have a shield, draw a square in the lower right + if (hasShield) { + g.setColor(Color.orange); + g.drawRect(TILE_WIDTH-15, TILE_HEIGHT-15, 10, 10); + } + // translate back out g.translate(-sx, -sy); } diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/Piecen.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/Piecen.java index 19c82704..78157828 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/data/Piecen.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/Piecen.java @@ -1,5 +1,5 @@ // -// $Id: Piecen.java,v 1.1 2001/10/17 02:19:54 mdb Exp $ +// $Id: Piecen.java,v 1.2 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -42,8 +42,8 @@ public class Piecen /** A color constant. */ public static final int GREEN = 4; - /** The color of this piecen. */ - public int color; + /** The owner of this piecen. */ + public int owner; /** The x and y coordinates of the tile on which this piecen is * placed. */ @@ -59,9 +59,9 @@ public class Piecen /** * Construts a piecen with the specified configuration. */ - public Piecen (int color, int x, int y, int featureIndex) + public Piecen (int owner, int x, int y, int featureIndex) { - this.color = color; + this.owner = owner; this.x = x; this.y = y; this.featureIndex = featureIndex; @@ -85,7 +85,7 @@ public class Piecen public void writeTo (DataOutputStream out) throws IOException { - out.writeInt(color); + out.writeInt(owner); out.writeInt(x); out.writeInt(y); out.writeInt(featureIndex); @@ -95,7 +95,7 @@ public class Piecen public void readFrom (DataInputStream in) throws IOException { - color = in.readInt(); + owner = in.readInt(); x = in.readInt(); y = in.readInt(); featureIndex = in.readInt(); @@ -127,7 +127,7 @@ public class Piecen */ public String toString () { - return "[color=" + color + ", pos=" + x + "/" + y + - ", fidx=" + featureIndex + "]"; + return "[owner=" + owner + ", pos=" + x + "/" + y + + ", feat=" + featureIndex + ", claim=" + claimGroup + "]"; } } 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 b6e7f71b..c73559f8 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.5 2001/10/17 02:19:54 mdb Exp $ +// $Id: TileCodes.java,v 1.6 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -118,4 +118,8 @@ public interface TileCodes /** A constant indicating a cloister. */ public static final int CLOISTER = 3; + + + /** A flag used to mark a tile as part of a completed city. */ + public static final int COMPLETED_CITY = 0x01; } 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 24495b6d..f03c0720 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.10 2001/10/17 05:01:51 mdb Exp $ +// $Id: AtlantiManager.java,v 1.11 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -9,9 +9,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import com.samskivert.util.HashIntMap; import com.samskivert.util.StringUtil; +import com.threerings.presents.dobj.ElementAddedEvent; +import com.threerings.presents.dobj.ElementRemovedEvent; +import com.threerings.presents.dobj.ElementUpdatedEvent; import com.threerings.presents.dobj.DSet; +import com.threerings.presents.dobj.SetListener; import com.threerings.presents.dobj.MessageEvent; import com.threerings.parlor.turn.TurnGameManager; @@ -20,7 +25,7 @@ import com.threerings.parlor.turn.TurnGameManager; * The main coordinator of the Venison game on the server side. */ public class VenisonManager - extends TurnGameManager implements VenisonCodes + extends TurnGameManager implements VenisonCodes, SetListener { // documentation inherited protected Class getPlaceObjectClass () @@ -62,6 +67,11 @@ public class VenisonManager _tilesInBox = TileUtil.getStandardTileSet(); Collections.shuffle(_tilesInBox); + // shave off most of the tiles for the moment + while (_tilesInBox.size() > 15) { + _tilesInBox.remove(0); + } + // clear out our board tiles _tiles.clear(); @@ -76,8 +86,9 @@ public class VenisonManager _venobj.setPiecens(new DSet(Piecen.class)); // and add the starting tile - _venobj.addToTiles(VenisonTile.STARTING_TILE); - _tiles.add(VenisonTile.STARTING_TILE); + VenisonTile start = TileUtil.getStartingTile(); + _venobj.addToTiles(start); + _tiles.add(start); } protected void turnWillStart () @@ -116,10 +127,47 @@ public class VenisonManager } /** - * Determines whether or not the placement of this tile has completed - * features that were previously incomplete. + * At the end of the game, we need to compute the final scores. */ - protected void scoreCompletedFeatures (VenisonTile tile) + protected void gameDidEnd () + { + super.gameDidEnd(); + + // create a piecen array that we can manipulate while scoring + Piecen[] piecens = new Piecen[_venobj.piecens.size()]; + Iterator iter = _venobj.piecens.elements(); + for (int i = 0; iter.hasNext(); i++) { + piecens[i] = (Piecen)iter.next(); + } + + // compute the final scores by iterating over each tile and + // scoring its features + iter = _venobj.tiles.elements(); + while (iter.hasNext()) { + VenisonTile tile = (VenisonTile)iter.next(); + scoreFeatures(tile, piecens); + } + + // lastly, we have to score the farms (cue the ominous drums)... + scoreFarms(); + + // update the final scores + _venobj.setScores(_venobj.scores); + } + + /** + * Scores the features on this tile. + * + * @param tile the tile whose features should be scored. + * @param piecens during the final tally, we don't look for piecens on + * the board and remove them when we score them, we instead look for + * the piecens in this supplied array and remove them from there both + * to preserve the appearance of the board during final scoring and to + * circumvent the need to process an element removed event every time + * we remove a piecen. Passing a non-null piecens array also signifies + * that we are doing the final tally rather than an incremental score. + */ + protected void scoreFeatures (VenisonTile tile, Piecen[] piecens) { // potentially score all features on the tile for (int i = 0; i < tile.features.length; i++) { @@ -133,12 +181,12 @@ public class VenisonManager // see if any piecens are even on a feature in this group int cgroup = tile.claims[i]; - int[] cgv = getClaimGroupVector(cgroup); + int[] cgv = getClaimGroupVector(cgroup, piecens); if (cgv == null) { // if not, we don't have anything to score - Log.info("Not scoring unclaimed feature " + - "[ttype=" + tile.type + ", feat=" + f + - ", cgroup=" + cgroup + "]."); +// Log.info("Not scoring unclaimed feature " + +// "[ttype=" + tile.type + ", feat=" + f + +// ", cgroup=" + cgroup + "]."); continue; } @@ -146,46 +194,47 @@ public class VenisonManager // this feature int score = TileUtil.computeFeatureScore(_tiles, tile, i); - // if the score is positive, it's a completed feature, so we - // dole out the points and clear the associated piecens - if (score > 0) { - Log.info("Scoring feature [ttype=" + tile.type + - ", feature=" + f + ", score=" + score + "]."); + // if the score is positive, it's a completed feature and we + // score it regardless, we score incomplete features only + // during the final tally + if (score > 0 || piecens != null) { + // convert the score into a positive value + if (score > 0) { + Log.info("Scoring complete feature " + + "[ttype=" + tile.type + ", feature=" + f + + ", score=" + score + + ", cgv=" + StringUtil.toString(cgv) + "]."); + + } else { + Log.info("Scoring incomplete feature [ttype=" + tile.type + + ", feature=" + f + ", score=" + score + "]."); + } + score = Math.abs(score); // adjust the scores for (int p = 0; p < cgv.length; p++) { _venobj.scores[p] += (score * cgv[p]); } - - // broadcast the new scores - _venobj.setScores(_venobj.scores); Log.info("New scores: " + StringUtil.toString(_venobj.scores)); - // and free up the scored piecens - Iterator iter = _venobj.piecens.elements(); - while (iter.hasNext()) { - Piecen p = (Piecen)iter.next(); - if (p.claimGroup == cgroup) { - Log.info("Removing piecen " + p + "."); - _venobj.removeFromPiecens(p.getKey()); - } + // broadcast the new scores if this isn't the final tally + if (piecens == null) { + _venobj.setScores(_venobj.scores); } + // and free up the scored piecens + removePiecens(cgroup, piecens); + } else { - Log.info("Not scoring incomplete feature " + - "[ttype=" + tile.type + ", feat=" + f + - ", score=" + score + "]."); +// Log.info("Not scoring incomplete feature " + +// "[ttype=" + tile.type + ", feat=" + f + +// ", score=" + score + "]."); } } // we also may have completed a cloister, so we check that as well for (int dx = -1; dx < 2; dx++) { for (int dy = -1; dy < 2; dy++) { - // skip ourselves - if (dx == 0 && dy == 0) { - continue; - } - // find our neighbor and make sure they exist VenisonTile neighbor = TileUtil.findTile(_tiles, tile.x + dx, tile.y + dy); @@ -205,43 +254,177 @@ public class VenisonManager // tile has a piecen if (p == null) { - Log.info("Skipping non-piecen having " + - "cloister tile [tile=" + neighbor + - ", feat=" + f + "]."); +// Log.info("Skipping non-piecen having " + +// "cloister tile [tile=" + neighbor + +// ", feat=" + f + "]."); continue; } // piecen is on cloister feature if (neighbor.claims[i] != p.claimGroup) { - Log.info("Skipping cloister tile with piecen on " + - "non-cloister [tile=" + neighbor + - ", feat=" + f + "]."); +// Log.info("Skipping cloister tile with piecen on " + +// "non-cloister [tile=" + neighbor + +// ", feat=" + f + "]."); continue; } - // score the cloister to see if it is completed + // score the cloister int score = TileUtil.computeFeatureScore( _tiles, neighbor, i); - // it is, yay! - if (score > 0) { + // if it's completed or if we're doing the final + // tally, we score it + if (score > 0 || piecens != null) { + if (score > 0) { + Log.info("Scored completed cloister " + + "[tile=" + neighbor + + ", f=" + f + ", p=" + p + "]."); + } else { + Log.info("Scored incomplete cloister " + + "[tile=" + neighbor + + ", f=" + f + ", p=" + p + "]."); + } + + // coerce the score into positive land + score = Math.abs(score); + // add the score to the owning player - _venobj.scores[p.color] += score; - _venobj.setScores(_venobj.scores); - // and clear out the piecen - _venobj.removeFromPiecens(p.getKey()); - Log.info("Scored cloister [tile=" + neighbor + - ", f=" + f + ", p=" + p + "]."); + _venobj.scores[p.owner] += score; + + // only broadcast the updated scores if this isn't + // the final tally + if (piecens == null) { + _venobj.setScores(_venobj.scores); + } + + // and clear out the piecen (only removing it from + // the piecen set if we're not in the final tally) + removePiecen(p, piecens == null); } else { - Log.info("Not scoring incomplete cloister " + - "[tile=" + neighbor + ", feat=" + f + "]."); +// Log.info("Not scoring incomplete cloister " + +// "[tile=" + neighbor + ", feat=" + f + "]."); } } } } } + /** + * Scores the farms, which is the final act of scoring. + */ + protected void scoreFarms () + { + HashIntMap cities = new HashIntMap(); + + // clear out the claims for incompleted cities and claim unclaimed + // completed cities + TileUtil.prepCitiesForScoring(_tiles); + + // do the big process-ola + int tsize = _tiles.size(); + for (int i = 0; i < tsize; i++) { + VenisonTile tile = (VenisonTile)_tiles.get(i); + + // iterate over all of the city features in this tile + for (int f = 0; f < tile.features.length; f++) { + // get the claim group for this feature + int cityClaim = tile.claims[f]; + + // skip unclaimed and non-city features + if (tile.features[f].type != TileCodes.CITY || + cityClaim == 0) { + continue; + } + + // get the list associated with this claim group + int[] claims = (int[])cities.get(cityClaim); + if (claims == null) { + // create a claim vector if we've not got one. if a + // city had 35 separately claimed farms around it, all + // the piecens in the game would be in play and the + // city would not have been claimed which would be an + // extremely pathological case, but we love pathology + // (especially when we don't have a resizable int list + // class handy) + claims = new int[35]; + cities.put(cityClaim, claims); + } + + // iterate over all of the grass features that are + // connected to city features on this tile and add their + // claim groups the list for this city feature + int[] grasses = FeatureUtil.CITY_GRASS_MAP[tile.type-1]; + for (int g = 0; g < grasses.length; g++) { + int farmClaim = tile.claims[grasses[g]]; + + // only worry about claimed grass regions + if (farmClaim == 0) { + Log.info("Ignoring unclaimed farm group " + + "[tile=" + tile + + ", fidx=" + grasses[g] + "]."); + continue; + } + + // and the farm claim group to the list + for (int c = 0; c < claims.length; c++) { + // don't add the farm claim twice + if (claims[c] == farmClaim) { + break; + } else if (claims[c] == 0) { + claims[c] = farmClaim; + Log.info("Noting city/farm abuttal " + + "[tile=" + tile + + ", cityClaim=" + cityClaim + + ", farmClaim=" + farmClaim + "]."); + break; + } + } + } + } + } + + // now for each city, we look to see who has the most piecens that + // are connected to the city by farms + Iterator iter = cities.keys(); + while (iter.hasNext()) { + int cityClaim = ((Integer)iter.next()).intValue(); + int[] farmClaims = (int[])cities.get(cityClaim); + int[] pcount = new int[_players.length]; + int max = 0; + + Iterator piter = _venobj.piecens.elements(); + while (piter.hasNext()) { + Piecen p = (Piecen)piter.next(); + // see if the piecen is on any of the farms + for (int c = 0; c < farmClaims.length; c++) { + if (p.claimGroup == farmClaims[c]) { + Log.info("Counting piecen [cityClaim=" + cityClaim + + ", farmClaim=" + farmClaims[c] + + ", piecen=" + p + "]."); + // increment their count and track the max + if (max < ++pcount[p.owner]) { + max = pcount[p.owner]; + } + } + } + } + + Log.info("Counted city [cityClaim=" + cityClaim + + ", counts=" + StringUtil.toString(pcount) + "]."); + + // now score four points for every player that has the max + for (int i = 0; i < pcount.length; i++) { + if (pcount[i] == max) { + Log.info("Scoring city for player [cgroup=" + cityClaim + + ", player=" + _players[i] + + ", pcount=" + pcount[i] + "]."); + _venobj.scores[i] += 4; + } + } + } + } + /** * Returns an int array with zeros and ones in the appropriate places * so that a score can be multiplied by a player's position in the @@ -252,24 +435,44 @@ public class VenisonManager * vector, so it cannot be called again without overwriting values * returned previously. * + * @param claimGroup the claim group that we're scoring. + * @param piecens an array to use when looking for matching piecens + * instead of looking at the piecens set in the game object. + * * @return an array for the specified claim group or null if no * players have a piecen claiming the specified claim group. */ - protected int[] getClaimGroupVector (int claimGroup) + protected int[] getClaimGroupVector (int claimGroup, Piecen[] piecens) { // clear out the vector Arrays.fill(_claimGroupVector, 0); // iterate over the piecens int max = 0; - Iterator iter = _venobj.piecens.elements(); - while (iter.hasNext()) { - Piecen piecen = (Piecen)iter.next(); - if (piecen.claimGroup == claimGroup) { - // color == player index... somewhat sketchy - if (++_claimGroupVector[piecen.color] > max) { - // keep track of the highest scorer - max = _claimGroupVector[piecen.color]; + if (piecens == null) { + Iterator iter = _venobj.piecens.elements(); + while (iter.hasNext()) { + Piecen piecen = (Piecen)iter.next(); + if (piecen.claimGroup == claimGroup) { + // color == player index... somewhat sketchy + if (++_claimGroupVector[piecen.owner] > max) { + // keep track of the highest scorer + max = _claimGroupVector[piecen.owner]; + } + } + } + + } else { + for (int i = 0; i < piecens.length; i++) { + Piecen piecen = piecens[i]; + if (piecen == null) { + continue; + } else if (piecen.claimGroup == claimGroup) { + // color == player index... somewhat sketchy + if (++_claimGroupVector[piecen.owner] > max) { + // keep track of the highest scorer + max = _claimGroupVector[piecen.owner]; + } } } } @@ -282,6 +485,94 @@ public class VenisonManager return (max == 0) ? null : _claimGroupVector; } + /** + * Removes piecens either from the supplied array or from the game + * object piecen set if no array is supplied. + */ + protected void removePiecens (int claimGroup, Piecen[] piecens) + { + if (piecens == null) { + Iterator iter = _venobj.piecens.elements(); + while (iter.hasNext()) { + Piecen p = (Piecen)iter.next(); + if (p.claimGroup == claimGroup) { + removePiecen(p, true); + } + } + + } else { + for (int i = 0; i < piecens.length; i++) { + if (piecens[i] == null) { + continue; + } else if (piecens[i].claimGroup == claimGroup) { + piecens[i] = null; + } + } + } + } + + /** + * Removes the piecen from the board and optionally removes it from + * the piecen set. + * + * @param piecen the piecen to be removed. + * @param removeFromPiecens if true, the piecen will also be removed + * from the piecens set in the game object. + */ + protected void removePiecen (Piecen piecen, boolean removeFromPiecens) + { + // locate the tile that contains this piecen + int tidx = _tiles.indexOf(piecen); + if (tidx == -1) { + Log.warning("Requested to remove piecen that is not " + + "associated with any tile [piecen=" + piecen + "]."); + } else { + VenisonTile tile = (VenisonTile)_tiles.get(tidx); + // and clear the piecen + tile.clearPiecen(); + } + + // also remove from the piecens dset if requested + if (removeFromPiecens) { + _venobj.removeFromPiecens(piecen.getKey()); + } + } + + // documentation inherited + public void elementAdded (ElementAddedEvent event) + { + // we react to piecen additions by potentially scoring the placed + // piecen. we allow the piecen to be added to the piecens set + // before scoring so that the players can see the piecen pop up on + // their screen and then disappear with a scoring notice rather + // than never show up at all; plus it simplifies our code + if (event.getName().equals(VenisonObject.PIECENS)) { + Piecen piecen = (Piecen)event.getElement(); + + // make sure this is a valid placement + VenisonTile tile = (VenisonTile)_venobj.tiles.get(piecen.getKey()); + if (tile == null) { + Log.warning("Can't find tile for piecen scoring " + + piecen + "."); + + } else { + // check to see if we added the piecen to a completed + // feature, in which case we score and remove it + scoreFeatures(tile, null); + } + } + } + + // documentation inherited + public void elementUpdated (ElementUpdatedEvent event) + { + } + + // documentation inherited + public void elementRemoved (ElementRemovedEvent event) + { + } + /** Handles place tile requests. */ protected class PlaceTileHandler implements MessageHandler { @@ -303,7 +594,7 @@ public class VenisonManager // placing a piece may have completed road or city // features. if it did, we score them now - scoreCompletedFeatures(tile); + scoreFeatures(tile, null); Log.info("Placed tile " + tile + "."); @@ -319,10 +610,17 @@ public class VenisonManager public void handleEvent (MessageEvent event) { Piecen piecen = (Piecen)event.getArgs()[0]; - - // make sure this is a valid placement VenisonTile tile = (VenisonTile)_venobj.tiles.get(piecen.getKey()); - if (tile == null) { + int pidx = getTurnHolderIndex(); + int pcount = TileUtil.countPiecens(_venobj.piecens, pidx); + + // do some checking before we place the piecen + if (pcount >= PIECENS_PER_PLAYER) { + Log.warning("Requested to place piecen for player that " + + "has all of their piecens in play " + + "[event=" + event + "]."); + + } else if (tile == null) { Log.warning("Can't find tile for requested piecen " + "placement " + piecen + "."); diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/util/FeatureUtil.java b/projects/atlanti/src/java/com/samskivert/atlanti/util/FeatureUtil.java index c42515a4..68f24afe 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/util/FeatureUtil.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/util/FeatureUtil.java @@ -1,5 +1,5 @@ // -// $Id: FeatureUtil.java,v 1.2 2001/10/17 04:34:13 mdb Exp $ +// $Id: FeatureUtil.java,v 1.3 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -61,6 +61,30 @@ public class FeatureUtil implements TileCodes WNW_F, WEST, ENE_F, }; + /** A mapping for city tiles to the grass features that are adjacent + * to the city tiles. */ + public static final int[][] CITY_GRASS_MAP = new int[][] { + { }, + { 1 }, // CITY_THREE + { 1, 2 }, // CITY_THREE_ROAD + { 1 }, // CITY_TWO + { 1 }, // CITY_TWO_ROAD + { 1, 2 }, // CITY_TWO_ROAD_ACROSS + { 0 }, // TWO_CITY_TWO + { 0 }, // TWO_CITY_TWO_ACROSS + { 0 }, // CITY_ONE + { 0 }, // CITY_ONE_ROAD_RIGHT + { 0 }, // CITY_ONE_ROAD_LEFT + { 0 }, // CITY_ONE_ROAD_TEE + { 0 }, // CITY_ONE_ROAD_STRAIGHT + { }, + { }, + { }, + { }, + { }, + { }, + }; + /** * Returns the feature array for the tile of the specified type. */ diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java b/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java index 82498594..bd2f72f9 100644 --- a/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java +++ b/projects/atlanti/src/java/com/samskivert/atlanti/util/TileUtil.java @@ -1,5 +1,5 @@ // -// $Id: TileUtil.java,v 1.9 2001/10/17 05:01:51 mdb Exp $ +// $Id: TileUtil.java,v 1.10 2001/10/17 23:27:52 mdb Exp $ package com.threerings.venison; @@ -12,11 +12,22 @@ import java.util.List; import com.samskivert.util.IntTuple; +import com.threerings.presents.dobj.DSet; + /** * Utility functions relating to the Venison tiles. */ public class TileUtil implements TileCodes { + /** + * Returns an instance of the starting tile (properly cloned so that + * it can be messed with by the server). + */ + public static VenisonTile getStartingTile () + { + return (VenisonTile)VenisonTile.STARTING_TILE.clone(); + } + /** * 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 @@ -24,7 +35,14 @@ public class TileUtil implements TileCodes */ public static List getStandardTileSet () { - return (List)TILE_SET.clone(); + // we need to deep copy the default tile set, so we can't just use + // clone + List tiles = new ArrayList(); + int tsize = TILE_SET.size(); + for (int i = 0; i < tsize; i++) { + tiles.add(((VenisonTile)TILE_SET.get(i)).clone()); + } + return tiles; } /** @@ -189,119 +207,48 @@ public class TileUtil implements TileCodes */ public static void inheritClaims (List tiles, VenisonTile tile) { - // obtain our neighboring tiles - VenisonTile[] neighbors = new VenisonTile[4]; - neighbors[NORTH] = findTile(tiles, tile.x, tile.y-1); - neighbors[EAST] = findTile(tiles, tile.x+1, tile.y); - neighbors[SOUTH] = findTile(tiles, tile.x, tile.y+1); - neighbors[WEST] = findTile(tiles, tile.x-1, tile.y); + List flist = new ArrayList(); - // for each feature in the tile, determine whether or not the - // neighboring tile's matching feature is claimed + // for each feature in the tile, load up its claim group and make + // sure all features in that group (which will include our new + // feature) now have the same claim number for (int i = 0; i < tile.features.length; i ++) { - int ftype = tile.features[i].type; - int fmask = tile.features[i].edgeMask; - int cgroup = 0; + int claimGroup = 0; - // iterate over all of the possible adjacency possibilities, - // first looking for a claim group to inherit - for (int c = 0; c < FeatureUtil.ADJACENCY_MAP.length; c += 3) { - int mask = FeatureUtil.ADJACENCY_MAP[c]; - int dir = FeatureUtil.ADJACENCY_MAP[c+1]; - int opp_mask = FeatureUtil.ADJACENCY_MAP[c+2]; + // clear out the tilefeatures list before enumerating + flist.clear(); - // if this feature doesn't have this edge, skip it - if ((fmask & mask) == 0) { - continue; - } + // enumerate the claim group for this feature + enumerateGroup(tiles, tile, i, flist); - // translate the target direction accordingly - dir = (dir + tile.orientation) % 4; - - // make sure we have a neighbor in the appropriate - // direction - if (neighbors[dir] == null) { - continue; - } - - // it looks like we have a match, so translate the target - // stuff into our orientation - mask = FeatureUtil.translateMask(mask, tile.orientation); - opp_mask = FeatureUtil.translateMask( - opp_mask, tile.orientation); - - // inherit the group of the opposing feature - cgroup = neighbors[dir].getFeatureGroup(opp_mask); - - // and bail as soon as we find a non-zero claim group - if (cgroup != 0) { - Log.info("Inherited claim [tile=" + tile + - ", fidx=" + i + ", cgroup=" + cgroup + - ", source=" + neighbors[dir] + "]."); + // find the first non-zero claim number + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + int fcg = feat.tile.claims[feat.featureIndex]; + if (fcg != 0) { + claimGroup = fcg; break; } } - // if we didn't inherit a claim group, skip to the next - // feature - if (cgroup == 0) { + // if we found no non-zero claim number, we've nothing to + // inherit + if (claimGroup == 0) { continue; } - // initialize the feature's claim group - tile.claims[i] = cgroup; - - // otherwise, iterate over all of the possible adjacency - // possibilities, propagating our group to connected features - for (int c = 0; c < FeatureUtil.ADJACENCY_MAP.length; c += 3) { - int mask = FeatureUtil.ADJACENCY_MAP[c]; - int dir = FeatureUtil.ADJACENCY_MAP[c+1]; - int opp_mask = FeatureUtil.ADJACENCY_MAP[c+2]; - - // if this feature doesn't have this edge, skip it - if ((fmask & mask) == 0) { - continue; - } - - // translate the target direction accordingly - dir = (dir + tile.orientation) % 4; - - // make sure we have a neighbor in the appropriate - // direction - if (neighbors[dir] == null) { - continue; - } - - // it looks like we have a match, so translate the target - // stuff into our orientation - mask = FeatureUtil.translateMask(mask, tile.orientation); - opp_mask = FeatureUtil.translateMask( - opp_mask, tile.orientation); - - // make sure the neighbor in question isn't the one from - // which we inherited the group - int ogroup = neighbors[dir].getFeatureGroup(opp_mask); - if (ogroup == cgroup) { - continue; - } - - // if we've already been assigned to a group, we propagate - // our group to the opposing feature - int fidx = neighbors[dir].getFeatureIndex(opp_mask); - if (fidx >= 0) { - Log.info("Propagating group [fidx=" + i + - ", cgroup=" + cgroup + - ", dir=" + dir + "]."); - setFeatureGroup(tiles, neighbors[dir], - fidx, cgroup, mask); - - } else { - Log.warning("Can't join-propagate feature " + - "[self=" + tile + - ", target=" + neighbors[dir] + - ", fidx=" + fidx + ", cgroup=" + cgroup + - ", destEdge=" + opp_mask + - ", srcEdge=" + mask + "]."); + // otherwise, assign our new claim number to all members of + // the group (potentially causing some to inherit the new + // claim number) + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + // set the claim group in the tile + feat.tile.claims[feat.featureIndex] = claimGroup; + // also set the claim group on the piecen if the tile has + // an associated piecen that is on this feature + Piecen p = feat.tile.piecen; + if (p != null && p.featureIndex == feat.featureIndex) { + p.claimGroup = claimGroup; } } } @@ -316,71 +263,18 @@ public class TileUtil implements TileCodes * being set. * @param featureIndex the index of the feature. * @param claimGroup the claim group value to set. - * @param entryEdgeMask the edge from which we are propagating this - * claim group (to avoid traversing back over that edge when - * propagating the group further). */ - public static void setFeatureGroup ( - List tiles, VenisonTile tile, int featureIndex, - int claimGroup, int entryEdgeMask) + public static void setClaimGroup ( + List tiles, VenisonTile tile, int featureIndex, int claimGroup) { - // set the claim group for this feature on this tile - tile.setFeatureGroup(featureIndex, claimGroup); + // load up this feature group + List flist = new ArrayList(); + enumerateGroup(tiles, tile, featureIndex, flist); - // now propagate this feature to connected features - int ftype = tile.features[featureIndex].type; - int fmask = tile.features[featureIndex].edgeMask; - - // iterate over all of the possible adjacency possibilities - for (int c = 0; c < FeatureUtil.ADJACENCY_MAP.length; c += 3) { - int mask = FeatureUtil.ADJACENCY_MAP[c]; - int dir = FeatureUtil.ADJACENCY_MAP[c+1]; - int opp_mask = FeatureUtil.ADJACENCY_MAP[c+2]; - VenisonTile neighbor = null; - - // if this feature doesn't have this edge, skip it - if ((fmask & mask) == 0) { - continue; - } - - // figure out if this would be the tile from which we - // propagated into our current tile and skip it if so - opp_mask = FeatureUtil.translateMask(opp_mask, tile.orientation); - if ((opp_mask & entryEdgeMask) != 0) { - continue; - } - - // make sure we have a neighbor in this direction - dir = (dir + tile.orientation) % 4; - switch (dir) { - case NORTH: neighbor = findTile(tiles, tile.x, tile.y-1); break; - case EAST: neighbor = findTile(tiles, tile.x+1, tile.y); break; - case SOUTH: neighbor = findTile(tiles, tile.x, tile.y+1); break; - case WEST: neighbor = findTile(tiles, tile.x-1, tile.y); break; - } - if (neighbor == null) { - continue; - } - - // it looks like we have a match, so translate the target mask - // into our orientation - mask = FeatureUtil.translateMask(mask, tile.orientation); - - // propagate, propagate, propagate - int fidx = neighbor.getFeatureIndex(opp_mask); - if (fidx >= 0) { - // only set the feature in the neighbor if they aren't - // already set to the appropriate group (prevent loops) - if (neighbor.claims[fidx] != claimGroup) { - setFeatureGroup(tiles, neighbor, fidx, claimGroup, mask); - } - - } else { - Log.warning("Can't propagate feature [self=" + tile + - ", target=" + neighbor + ", fidx=" + fidx + - ", cgroup=" + claimGroup + ", srcEdge=" + mask + - ", destEdge=" + opp_mask + "]."); - } + // and assign the claim number to all features in the group + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + feat.tile.claims[feat.featureIndex] = claimGroup; } } @@ -401,112 +295,40 @@ public class TileUtil implements TileCodes public static int computeFeatureScore ( List tiles, VenisonTile tile, int featureIndex) { - // determine what kind of feature it is Feature feature = tile.features[featureIndex]; - switch (feature.type) { - case ROAD: - return computeFeatureScore(tiles, tile, feature, new ArrayList()); - - case CITY: { - int score = - computeFeatureScore(tiles, tile, feature, new ArrayList()); - // cities receive a 2x multiplier if they are larger than size - // two and are completed - return (score > 2) ? score*2 : score; - } - - case CLOISTER: + if (feature.type == CLOISTER) { // cloister's score specially return computeCloisterScore(tiles, tile); - default: - case GRASS: + } else if (feature.type == GRASS) { // grass doesn't score return 0; } - } - /** - * A helper function for {@link - * #computeFeatureScore(List,VenisonTile,int)}. - */ - protected static int computeFeatureScore ( - List tiles, VenisonTile tile, Feature feature, List seen) - { - // if we've already counted this tile, bail - if (seen.contains(tile)) { - return 0; - } + // if we're here, it's a road or city feature, which we score by + // loading up the group and counting its size + List flist = new ArrayList(); + boolean complete = enumerateGroup(tiles, tile, featureIndex, flist); + int score = flist.size(); - // otherwise add this tile to the seen list - seen.add(tile); - - // cities have a base score of one but get a one point bonus if - // they have a shield on them - int score = (feature.type == CITY && tile.hasShield) ? 2 : 1; - boolean missedNeighbor = false; - - // now figure out what connected features there are, if any - int ftype = feature.type; - int fmask = feature.edgeMask; - - // iterate over all of the possible adjacency possibilities - for (int c = 0; c < FeatureUtil.ADJACENCY_MAP.length; c += 3) { - int mask = FeatureUtil.ADJACENCY_MAP[c]; - int dir = FeatureUtil.ADJACENCY_MAP[c+1]; - int opp_mask = FeatureUtil.ADJACENCY_MAP[c+2]; - VenisonTile neighbor = null; - - // if this feature doesn't have this edge, skip it - if ((fmask & mask) == 0) { - continue; - } - - // make sure we have a neighbor in this direction - dir = (dir + tile.orientation) % 4; - switch (dir) { - case NORTH: neighbor = findTile(tiles, tile.x, tile.y-1); break; - case EAST: neighbor = findTile(tiles, tile.x+1, tile.y); break; - case SOUTH: neighbor = findTile(tiles, tile.x, tile.y+1); break; - case WEST: neighbor = findTile(tiles, tile.x-1, tile.y); break; - } - if (neighbor == null) { - // if we don't have a neighbor in a direction that we - // need, we're an incomplete feature. alas - missedNeighbor = true; - continue; - } - - // translate the target mask into our orientation - mask = FeatureUtil.translateMask(mask, tile.orientation); - opp_mask = FeatureUtil.translateMask(opp_mask, tile.orientation); - - // propagate, propagate, propagate - int fidx = neighbor.getFeatureIndex(opp_mask); - if (fidx < 0) { - Log.warning("Tile mismatch while scoring [self=" + tile + - ", target=" + neighbor + ", fidx=" + fidx + - ", srcEdge=" + mask + - ", destEdge=" + opp_mask + "]."); - - } else { - // add the score for this neighbor - int nscore = computeFeatureScore( - tiles, neighbor, neighbor.features[fidx], seen); - // if our neighbor returned a negative score, we convert - // it back to positive and make a note to make it negative - // at the end - if (nscore < 0) { - score += -nscore; - missedNeighbor = true; - } else { - score += nscore; + // for city groups, we need to add a bonus of one for every tile + // that contains a shield and mutiply by two if the city is + // complete and larger than two tiles + if (feature.type == CITY) { + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + if (feat.tile.hasShield) { + score++; } } + if (complete && score > 2) { + score *= 2; + } } - return missedNeighbor ? -score : score; + // incomplete scores are reported in the negative + return complete ? score : -score; } /** @@ -531,6 +353,161 @@ public class TileUtil implements TileCodes return (score == 9) ? 9 : -score; } + /** + * Clears out the claim group information for incomplete cities so + * that we can ignore them during farm scoring. Assigns new claim + * group values to any completed cities that were unclaimed. + * + * @param tiles a sorted list of tiles on the board. + */ + public static void prepCitiesForScoring (List tiles) + { + List flist = new ArrayList(); + + // iterate over the tiles, marking every city completed or not + int tsize = tiles.size(); + for (int i = 0; i < tsize; i++) { + VenisonTile tile = (VenisonTile)tiles.get(i); + + // iterate over each feature on this tile + for (int f = 0; f < tile.features.length; f++) { + // skip non-city features + if (tile.features[f].type != TileCodes.CITY) { + continue; + } + + // clear out the feature group list before processing + flist.clear(); + + // enumerate the features in the group with this feature + if (enumerateGroup(tiles, tile, f, flist)) { + // if it's complete, we want to ensure that all of + // these features have a claim number + if (tile.claims[f] != 0) { + // it's complete and has a claim number. move on + continue; + } + + // assign the claim number to all features in the group + int claimGroup = nextClaimGroup(); + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + feat.tile.claims[feat.featureIndex] = claimGroup; + if (t == 0) { + Log.info("Claiming complete city " + + "[claim=" + feat.tile.claims[ + feat.featureIndex] + "]."); + } + } + + } else { + // it's incomplete, so we want to clear out the claim + // number from all tiles in the group + for (int t = 0; t < flist.size(); t++) { + TileFeature feat = (TileFeature)flist.get(t); + if (t == 0) { + Log.info("Clearing incomplete city " + + "[claim=" + feat.tile.claims[ + feat.featureIndex] + "]."); + } + feat.tile.claims[feat.featureIndex] = 0; + } + } + } + } + } + + /** + * Enumerates all of the features that are in the group of which the + * specified feature is a member. + * + * @param tiles a sorted list of the tiles on the board. + * @param tile the tile that contains the feature whose group is to be + * enumerated. + * @param featureIndex the index of the feature whose group is to be + * enumerated. + * @param group the list into which instances of {@link FeatureGroup} + * will be placed that represent the features that are members of the + * group. + * + * @return true if the group is complete (has no unconnected + * features), false if it is not. + */ + protected static boolean enumerateGroup ( + List tiles, VenisonTile tile, int featureIndex, List group) + { + // create a tilefeature for this feature + TileFeature feat = new TileFeature(tile, featureIndex); + + // determine whether or not this feature is already in the group + if (group.contains(feat)) { + return true; + } + + // otherwise add this feature to the group and process this + // feature's neighbors + group.add(feat); + + boolean complete = true; + int ftype = tile.features[featureIndex].type; + int fmask = tile.features[featureIndex].edgeMask; + + // iterate over all of the possible adjacency possibilities + for (int c = 0; c < FeatureUtil.ADJACENCY_MAP.length; c += 3) { + int mask = FeatureUtil.ADJACENCY_MAP[c]; + int opp_mask = FeatureUtil.ADJACENCY_MAP[c+2]; + + // if this feature doesn't have this edge, skip it + if ((fmask & mask) == 0) { + continue; + } + + // look up our neighbor + VenisonTile neighbor = null; + int dir = FeatureUtil.ADJACENCY_MAP[c+1]; + dir = (dir + tile.orientation) % 4; + switch (dir) { + case NORTH: neighbor = findTile(tiles, tile.x, tile.y-1); break; + case EAST: neighbor = findTile(tiles, tile.x+1, tile.y); break; + case SOUTH: neighbor = findTile(tiles, tile.x, tile.y+1); break; + case WEST: neighbor = findTile(tiles, tile.x-1, tile.y); break; + } + + // make sure we have a neighbor in this direction + if (neighbor == null) { + // if we don't have a neighbor in a direction that we + // need, we're an incomplete feature. alas + complete = false; + continue; + } + + // translate the target mask into our orientation + mask = FeatureUtil.translateMask(mask, tile.orientation); + opp_mask = FeatureUtil.translateMask(opp_mask, tile.orientation); + + // obtain the index of the feature on the opposing tile + int nFeatureIndex = neighbor.getFeatureIndex(opp_mask); + if (nFeatureIndex < 0) { + Log.warning("Tile mismatch while grouping [tile=" + tile + + "featIdx=" + featureIndex + + ", neighbor=" + neighbor + + ", nFeatIdx=" + nFeatureIndex + + ", srcEdge=" + mask + + ", destEdge=" + opp_mask + "]."); + continue; + } + + // add this feature and its neighbors to the group + if (!enumerateGroup(tiles, neighbor, nFeatureIndex, group)) { + // if our neighbor was incomplete, we become incomplete. + // as dr. evil might say, "you incomplete me." + complete = false; + } + } + + return complete; + } + /** * Locates and returns the tile with the specified coordinates. * @@ -546,6 +523,22 @@ public class TileUtil implements TileCodes return (tidx >= 0) ? (VenisonTile)tiles.get(tidx) : null; } + /** + * Returns the number of piecens on the board owned the specified + * player. + */ + public static int countPiecens (DSet piecens, int playerIndex) + { + int count = 0; + Iterator iter = piecens.elements(); + while (iter.hasNext()) { + if (((Piecen)iter.next()).owner == playerIndex) { + count++; + } + } + return count; + } + /** * Returns the edge type for specified edge of the specified tile * type. @@ -572,10 +565,46 @@ public class TileUtil implements TileCodes /** 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()); + for (int i = 0; i < count; i++) { + list.add(tile); + } + } + + /** Used to keep track of actual features on tiles. */ + protected static final class TileFeature + { + /** The tile that contains the feature. */ + public VenisonTile tile; + + /** The index of the feature in the tile. */ + public int featureIndex; + + /** Constructs a new tile feature. */ + public TileFeature (VenisonTile tile, int featureIndex) + { + this.tile = tile; + this.featureIndex = featureIndex; + } + + /** Properly implement equality. */ + public boolean equals (Object other) + { + if (other == null || + !(other instanceof TileFeature)) { + return false; + + } else { + TileFeature feat = (TileFeature)other; + return (feat.tile == tile && + feat.featureIndex == featureIndex); + } + } + + /** Generate a string representation. */ + public String toString () + { + return "[tile=" + tile + ", fidx=" + featureIndex + "]"; } - list.add(tile); } /** Used to generate claim group values. */