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 a8f05a7f..66634ddc 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.8 2001/10/17 02:19:54 mdb Exp $
+// $Id: AtlantiBoard.java,v 1.9 2001/10/17 04:34:14 mdb Exp $
package com.threerings.venison;
@@ -47,6 +47,14 @@ public class VenisonBoard
});
}
+ /**
+ * Sets the piecen color to use when creating new piecens.
+ */
+ public void setNewPiecenColor (int color)
+ {
+ _newPiecenColor = color;
+ }
+
/**
* Sets the tiles to be displayed by this board. Any previous tiles
* are forgotten and the new tiles are initialized according to their
@@ -141,6 +149,30 @@ public class VenisonBoard
}
}
+ /**
+ * When a piecen is removed (after scoring it), this method should be
+ * called to update the board display.
+ */
+ public void clearPiecen (Object key)
+ {
+ // locate the tile associated with this piecen key
+ int tsize = _tiles.size();
+ for (int i = 0; i < tsize; i++) {
+ VenisonTile tile = (VenisonTile)_tiles.get(i);
+ if (tile.getKey().equals(key)) {
+ // clear the piecen out of the tile
+ tile.clearPiecen();
+ // and repaint
+ repaint();
+ // and get on out
+ return;
+ }
+ }
+
+ Log.warning("Requested to clear piecen for which we could " +
+ "find no associated tile! [key=" + key + "].");
+ }
+
/**
* 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
@@ -264,7 +296,7 @@ public class VenisonBoard
if (_placedTile.claims[fidx] == 0) {
if (_placedTile.piecen == null ||
_placedTile.piecen.featureIndex != fidx) {
- Piecen p = new Piecen(Piecen.BLUE, 0, 0, fidx);
+ Piecen p = new Piecen(_newPiecenColor, 0, 0, fidx);
_placedTile.setPiecen(p, null);
changed = true;
}
@@ -493,16 +525,17 @@ 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_TWO, true, WEST, 0, 0));
set.addTile(new VenisonTile(CITY_TWO, false, WEST, -1, 1));
- set.addTile(new VenisonTile(CITY_TWO, false, WEST, -1, -1));
- set.addTile(new VenisonTile(CURVED_ROAD, false, WEST, 0, 2));
+ set.addTile(new VenisonTile(CITY_ONE, false, SOUTH, -1, -1));
+ VenisonTile zero = new VenisonTile(CURVED_ROAD, false, WEST, 0, 2);
+ set.addTile(zero);
VenisonTile one = new VenisonTile(TWO_CITY_TWO, false, NORTH, 0, 1);
set.addTile(one);
set.addTile(new VenisonTile(CITY_THREE, false, WEST, 1, 1));
set.addTile(new VenisonTile(CITY_THREE_ROAD, false, EAST, 1, 2));
set.addTile(new VenisonTile(CITY_THREE, false, NORTH, -1, 0));
- VenisonTile two = new VenisonTile(CITY_FOUR, false, NORTH, -2, 0);
+ VenisonTile two = new VenisonTile(CITY_ONE, false, EAST, -2, 0);
set.addTile(two);
board.setTiles(set);
@@ -514,8 +547,18 @@ public class VenisonBoard
CollectionUtil.addAll(tiles, set.elements());
Collections.sort(tiles);
+ zero.setPiecen(new Piecen(Piecen.GREEN, 0, 0, 2), tiles);
one.setPiecen(new Piecen(Piecen.BLUE, 0, 0, 0), tiles);
- two.setPiecen(new Piecen(Piecen.BLUE, 0, 0, 0), tiles);
+ two.setPiecen(new Piecen(Piecen.RED, 0, 0, 1), tiles);
+
+ Log.info("Incomplete road: " +
+ TileUtil.computeFeatureScore(tiles, zero, 2));
+
+ Log.info("Completed city: " +
+ TileUtil.computeFeatureScore(tiles, two, 1));
+
+ Log.info("Incomplete city: " +
+ TileUtil.computeFeatureScore(tiles, one, 2));
frame.getContentPane().add(board, BorderLayout.CENTER);
frame.pack();
@@ -556,6 +599,9 @@ public class VenisonBoard
* placements based on the current position of the placing tile. */
protected boolean[] _validOrients;
+ /** The color to use when creating new piecens. */
+ protected int _newPiecenColor = Piecen.BLUE;
+
/** Our render offset in pixels. */
protected int _tx, _ty;
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 7e1eba55..df0658fb 100644
--- a/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java
+++ b/projects/atlanti/src/java/com/samskivert/atlanti/client/AtlantiController.java
@@ -4,6 +4,7 @@
package com.threerings.venison;
import java.awt.event.ActionEvent;
+import com.samskivert.util.ListUtil;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DSet;
@@ -53,6 +54,13 @@ public class VenisonController
// get a casted reference to our game object
_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) {
+ // use our player index as the piecen color directly
+ _panel.board.setNewPiecenColor(oidx);
+ }
+
// grab the tiles and piecens from the game object and configure
// the board with them
_panel.board.setTiles(_venobj.tiles);
@@ -67,10 +75,8 @@ public class VenisonController
{
super.turnDidChange(turnHolder);
- // if it's our turn, set the tile to be placed. otherwise clear it
- // out
+ // if it's our turn, set the tile to be placed
if (turnHolder.equals(_self.username)) {
- Log.info("Setting tile to be placed: " + _venobj.currentTile);
_panel.board.setTileToBePlaced(_venobj.currentTile);
}
}
@@ -113,6 +119,11 @@ public class VenisonController
// documentation inherited
public void elementRemoved (ElementRemovedEvent event)
{
+ if (event.getName().equals(VenisonObject.PIECENS)) {
+ // a piecen was removed, update the board
+ Log.info("Clearing piecen " + event.getKey() + ".");
+ _panel.board.clearPiecen(event.getKey());
+ }
}
// documentation inherited
diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiObject.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiObject.java
index a83d3f22..d622eef4 100644
--- a/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiObject.java
+++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/AtlantiObject.java
@@ -1,8 +1,9 @@
//
-// $Id: AtlantiObject.java,v 1.6 2001/10/17 02:19:54 mdb Exp $
+// $Id: AtlantiObject.java,v 1.7 2001/10/17 04:34:14 mdb Exp $
package com.threerings.venison;
+import com.samskivert.util.StringUtil;
import com.threerings.presents.dobj.DSet;
import com.threerings.parlor.turn.TurnGameObject;
@@ -20,6 +21,9 @@ public class VenisonObject extends TurnGameObject
/** The field name of the piecens field. */
public static final String PIECENS = "piecens";
+ /** The field name of the scores field. */
+ public static final String SCORES = "scores";
+
/** A set containing all of the tiles that are in play in this
* game. */
public DSet tiles = new DSet(VenisonTile.class);
@@ -32,6 +36,9 @@ public class VenisonObject extends TurnGameObject
* board. */
public DSet piecens = new DSet(Piecen.class);
+ /** The scores for each player. */
+ public int[] scores;
+
/**
* Requests that the tiles field be set to the specified
* value.
@@ -113,6 +120,15 @@ public class VenisonObject extends TurnGameObject
requestElementUpdate(PIECENS, elem);
}
+ /**
+ * Requests that the scores field be set to the specified
+ * value.
+ */
+ public void setScores (int[] value)
+ {
+ requestAttributeChange(SCORES, value);
+ }
+
// documentation inherited
protected void toString (StringBuffer buf)
{
@@ -120,5 +136,6 @@ public class VenisonObject extends TurnGameObject
buf.append(", tiles=").append(tiles);
buf.append(", currentTile=").append(currentTile);
buf.append(", piecens=").append(piecens);
+ buf.append(", scores=").append(StringUtil.toString(scores));
}
}
diff --git a/projects/atlanti/src/java/com/samskivert/atlanti/data/Feature.java b/projects/atlanti/src/java/com/samskivert/atlanti/data/Feature.java
index 1a4983dc..050063a5 100644
--- a/projects/atlanti/src/java/com/samskivert/atlanti/data/Feature.java
+++ b/projects/atlanti/src/java/com/samskivert/atlanti/data/Feature.java
@@ -1,5 +1,5 @@
//
-// $Id: Feature.java,v 1.1 2001/10/17 02:19:54 mdb Exp $
+// $Id: Feature.java,v 1.2 2001/10/17 04:34:13 mdb Exp $
package com.threerings.venison;
@@ -334,9 +334,13 @@ public class Feature
return poly;
}
+ /**
+ * Generates a string representation of this feature.
+ */
public String toString ()
{
- return "[type=" + type + ", edgeMask=" + edgeMask + "]";
+ return "[type=" + FeatureUtil.typeToString(type) +
+ ", edgeMask=" + FeatureUtil.edgeMaskToString(edgeMask) + "]";
}
/** Maps feature types to colors. */
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 caff9411..08a9830f 100644
--- a/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java
+++ b/projects/atlanti/src/java/com/samskivert/atlanti/server/AtlantiManager.java
@@ -1,12 +1,16 @@
//
-// $Id: AtlantiManager.java,v 1.8 2001/10/17 02:19:54 mdb Exp $
+// $Id: AtlantiManager.java,v 1.9 2001/10/17 04:34:14 mdb Exp $
package com.threerings.venison;
+import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
+import com.samskivert.util.StringUtil;
+
import com.threerings.presents.dobj.DSet;
import com.threerings.presents.dobj.MessageEvent;
@@ -61,6 +65,12 @@ public class VenisonManager
// clear out our board tiles
_tiles.clear();
+ // create a claim group vector
+ _claimGroupVector = new int[_players.length];
+
+ // clear out the scores
+ _venobj.setScores(new int[_players.length]);
+
// clear out the tile and piecen set
_venobj.setTiles(new DSet(VenisonTile.class));
_venobj.setPiecens(new DSet(Piecen.class));
@@ -105,6 +115,110 @@ public class VenisonManager
}
}
+ /**
+ * Determines whether or not the placement of this tile has completed
+ * features that were previously incomplete.
+ */
+ protected void scoreCompletedFeatures (VenisonTile tile)
+ {
+ // potentially score all features on the tile
+ for (int i = 0; i < tile.features.length; i++) {
+ // we only need to worry about ROAD and CITY features because
+ // those are the only features on this tile that we might have
+ // completed
+ Feature f = tile.features[i];
+ if (f.type != TileCodes.CITY && f.type != TileCodes.ROAD) {
+ continue;
+ }
+
+ // see if any piecens are even on a feature in this group
+ int cgroup = tile.claims[i];
+ int[] cgv = getClaimGroupVector(cgroup);
+ 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 + "].");
+ continue;
+ }
+
+ // we do have something to score, so we compute the score for
+ // 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 + "].");
+
+ // 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());
+ }
+ }
+
+ } else {
+ Log.info("Not scoring incomplete feature " +
+ "[ttype=" + tile.type + ", feat=" + f +
+ ", score=" + score + "].");
+ }
+ }
+ }
+
+ /**
+ * 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
+ * vector to determine whether or not they receive any points for the
+ * scoring of a particular claim group.
+ *
+ *
Note that this function returns a static (to this instance) + * vector, so it cannot be called again without overwriting values + * returned previously. + * + * @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) + { + // 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]; + } + } + } + + // now cut out everyone with scores less than the highest score + for (int i = 0; i < _claimGroupVector.length; i++) { + _claimGroupVector[i] = (_claimGroupVector[i] < max) ? 0 : 1; + } + + return (max == 0) ? null : _claimGroupVector; + } + /** Handles place tile requests. */ protected class PlaceTileHandler implements MessageHandler { @@ -114,17 +228,22 @@ public class VenisonManager // make sure this is a valid placement if (TileUtil.isValidPlacement(_tiles, tile)) { - // add the tile to the lsit + // add the tile to the list and resort it _tiles.add(tile); + Collections.sort(_tiles); // inherit its claim groups TileUtil.inheritClaims(_tiles, tile); - Log.info("Placed tile " + tile + "."); - // add the tile to the tiles set _venobj.addToTiles(tile); + // placing a piece may have completed road or city + // features. if it did, we score them now + scoreCompletedFeatures(tile); + + Log.info("Placed tile " + tile + "."); + } else { Log.warning("Received invalid placement " + event + "."); } @@ -182,4 +301,7 @@ public class VenisonManager /** A sorted list of the tiles that have been placed on the board. */ protected List _tiles = new ArrayList(); + + /** Used to score features groups. */ + protected int[] _claimGroupVector; } 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 8867e948..c42515a4 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.1 2001/10/17 02:19:54 mdb Exp $ +// $Id: FeatureUtil.java,v 1.2 2001/10/17 04:34:13 mdb Exp $ package com.threerings.venison; @@ -86,6 +86,39 @@ public class FeatureUtil implements TileCodes return features; } + /** + * Returns a string describing the supplied type code. + */ + public static String typeToString (int type) + { + return TYPE_CODES[type]; + } + + /** + * Returns a string describing the supplied edge mask. + */ + public static String edgeMaskToString (int edgeMask) + { + StringBuffer buf = new StringBuffer(); + if ((edgeMask & NORTH_F) != 0) buf.append("NORTH_F|"); + if ((edgeMask & EAST_F) != 0) buf.append("EAST_F|"); + if ((edgeMask & SOUTH_F) != 0) buf.append("SOUTH_F|"); + if ((edgeMask & WEST_F) != 0) buf.append("WEST_F|"); + if ((edgeMask & NNW_F) != 0) buf.append("NNW_F|"); + if ((edgeMask & NNE_F) != 0) buf.append("NNE_F|"); + if ((edgeMask & SSW_F) != 0) buf.append("SSW_F|"); + if ((edgeMask & SSE_F) != 0) buf.append("SSE_F|"); + if ((edgeMask & WNW_F) != 0) buf.append("WNW_F|"); + if ((edgeMask & WSW_F) != 0) buf.append("WSW_F|"); + if ((edgeMask & ENE_F) != 0) buf.append("ENE_F|"); + if ((edgeMask & ESE_F) != 0) buf.append("ESE_F|"); + // strip off the trailing bar if there is one + if (buf.length() > 0) { + buf.deleteCharAt(buf.length()-1); + } + return buf.toString(); + } + /** * Translates the feature edge mask into the orientation specified. * For a forward translation, provide a positive valued orientation @@ -300,6 +333,10 @@ public class FeatureUtil implements TileCodes { ENE_F, SSE_F, WSW_F, NNW_F }, }; + /** String representations of the feature type codes. */ + protected static final String[] TYPE_CODES = { + "CITY", "GRASS", "ROAD", "CLOISTER" }; + /** A table of features that are used on more than one tile. */ protected static Feature[] _reusedFeatures; 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 26892e6f..7a826d6d 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.7 2001/10/17 02:19:54 mdb Exp $ +// $Id: TileUtil.java,v 1.8 2001/10/17 04:34:13 mdb Exp $ package com.threerings.venison; @@ -384,6 +384,153 @@ public class TileUtil implements TileCodes } } + /** + * Computes the score for the specified feature and returns it. If the + * feature is complete (has no unconnected edges), the score will be + * positive. If it is incomplete, the score will be negative. + * + * @param tiles a sorted list of the tiles on the board. + * @param tile the tile that contains the feature whose score should + * be computed. + * @param featureIndex the index of the feature in the containing + * tile. + * + * @return a positive score for a completed feature group, a negative + * score for a partial feature group. + */ + 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: + // cloister's score specially + return computeCloisterScore(tiles, tile); + + default: + case 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; + } + + // 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; + } + } + } + + return missedNeighbor ? -score : score; + } + + /** + * A helper function for {@link + * #computeFeatureScore(List,VenisonTile,int)}. + */ + protected static int computeCloisterScore (List tiles, VenisonTile tile) + { + int score = 0; + + // all we need to know are how many neighbors this guy has (we + // count ourselves as well, just for code simplicity) + for (int dx = -1; dx < 1; dx++) { + for (int dy = -1; dy < 1; dy++) { + if (findTile(tiles, tile.x + dx, tile.y + dy) != null) { + score++; + } + } + } + + // incomplete cloisters return a negative score + return (score == 9) ? 9 : -score; + } + /** * Locates and returns the tile with the specified coordinates. *