From 52dfdf733249384bf9f1d0c25ee148676942156a Mon Sep 17 00:00:00 2001 From: mdb Date: Wed, 17 Oct 2001 04:34:14 +0000 Subject: [PATCH] Initial code to compute scores and to handle incremental scoring. git-svn-id: https://samskivert.googlecode.com/svn/trunk@364 6335cc39-0255-0410-8fd6-9bcaacd3b74c --- .../atlanti/client/AtlantiBoard.java | 60 ++++++- .../atlanti/client/AtlantiController.java | 17 +- .../atlanti/data/AtlantiObject.java | 19 ++- .../com/samskivert/atlanti/data/Feature.java | 8 +- .../atlanti/server/AtlantiManager.java | 130 ++++++++++++++- .../samskivert/atlanti/util/FeatureUtil.java | 39 ++++- .../com/samskivert/atlanti/util/TileUtil.java | 149 +++++++++++++++++- 7 files changed, 403 insertions(+), 19 deletions(-) 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. *