diff --git a/src/java/com/threerings/puzzle/Log.java b/src/java/com/threerings/puzzle/Log.java
new file mode 100644
index 000000000..9eaa015aa
--- /dev/null
+++ b/src/java/com/threerings/puzzle/Log.java
@@ -0,0 +1,38 @@
+//
+// $Id: Log.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle;
+
+/**
+ * A placeholder class that contains a reference to the log object used by
+ * this package.
+ */
+public class Log
+{
+ public static com.samskivert.util.Log log =
+ new com.samskivert.util.Log("yohoho.puzzle");
+
+ /** Convenience function. */
+ public static void debug (String message)
+ {
+ log.debug(message);
+ }
+
+ /** Convenience function. */
+ public static void info (String message)
+ {
+ log.info(message);
+ }
+
+ /** Convenience function. */
+ public static void warning (String message)
+ {
+ log.warning(message);
+ }
+
+ /** Convenience function. */
+ public static void logStackTrace (Throwable t)
+ {
+ log.logStackTrace(com.samskivert.util.Log.WARNING, t);
+ }
+}
diff --git a/src/java/com/threerings/puzzle/client/PlayerStatusView.java b/src/java/com/threerings/puzzle/client/PlayerStatusView.java
new file mode 100644
index 000000000..fbb6252ee
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PlayerStatusView.java
@@ -0,0 +1,101 @@
+//
+// $Id: PlayerStatusView.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import javax.swing.JPanel;
+
+import com.threerings.parlor.game.GameObject;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleConfig;
+import com.threerings.puzzle.data.PuzzleObject;
+
+/**
+ * The player status view displays a player's current status in the game.
+ */
+public class PlayerStatusView extends JPanel
+{
+ /**
+ * Constructs a player status view.
+ */
+ public PlayerStatusView (GameObject gameobj, int pidx)
+ {
+ // save off references
+ _gameobj = gameobj;
+ _username = _gameobj.players[pidx];
+ _pidx = pidx;
+
+ // configure the panel
+ setOpaque(false);
+ }
+
+ /**
+ * Initializes the player status view with the puzzle config.
+ */
+ public void init (PuzzleConfig config)
+ {
+ // nothing for now
+ }
+
+ /**
+ * Get the player index of the player represented by this view.
+ */
+ public int getPlayerIndex ()
+ {
+ return _pidx;
+ }
+
+ /**
+ * Sets the player board summary.
+ */
+ public void setBoardSummary (BoardSummary summary)
+ {
+ _summary = summary;
+ repaint();
+ }
+
+ /**
+ * Sets the player status.
+ */
+ public void setStatus (int status)
+ {
+ _status = status;
+ repaint();
+ }
+
+ /**
+ * Sets whether to highlight the player status display when rendered.
+ */
+ public void setHighlighted (boolean highlight)
+ {
+ _highlight = highlight;
+ repaint();
+ }
+
+ /** Returns a string representation of this instance. */
+ public String toString ()
+ {
+ return "[user=" + _username + ", pidx=" + _pidx +
+ ", status=" + _status + "]";
+ }
+
+ /** The game object associated with this view. */
+ protected GameObject _gameobj;
+
+ /** The player name. */
+ protected String _username;
+
+ /** The player index. */
+ protected int _pidx;
+
+ /** Whether this display is highlighted. */
+ protected boolean _highlight;
+
+ /** The player board summary. */
+ protected BoardSummary _summary;
+
+ /** The player game status. */
+ protected int _status = PuzzleObject.PLAYER_IN_PLAY;
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java b/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java
new file mode 100644
index 000000000..c605fb5f4
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java
@@ -0,0 +1,57 @@
+//
+// $Id: PuzzleAnimationWaiter.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import com.threerings.media.animation.AnimationWaiter;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.PuzzleObject;
+
+/**
+ * An animation waiter to be used with puzzles that want to modify the
+ * game object or board in some way after the animations end, and would
+ * like to do so in a safe fashion such that their changes aren't
+ * unwittingly performed on game data for a subsequent round of the
+ * puzzle.
+ */
+public abstract class PuzzleAnimationWaiter extends AnimationWaiter
+{
+ /**
+ * Constructs a puzzle animation waiter.
+ */
+ public PuzzleAnimationWaiter (PuzzleObject puzobj)
+ {
+ _puzobj = puzobj;
+ _roundId = puzobj.roundId;
+ }
+
+ /**
+ * Returns whether the puzzle associated with this puzzle animation
+ * waiter is still valid.
+ */
+ public boolean puzzleStillValid ()
+ {
+ return (_puzobj.isInPlay() && (_roundId == _puzobj.roundId));
+ }
+
+ // documentation inherited
+ protected final void allAnimationsFinished ()
+ {
+ allAnimationsFinished(puzzleStillValid());
+ }
+
+ /**
+ * Replacement for {@link AnimationWaiter#allAnimationsFinished} that
+ * also reports whether the puzzle associated with this animation
+ * waiter is still valid.
+ */
+ protected abstract void allAnimationsFinished (boolean puzStillValid);
+
+ /** The initial round id. */
+ protected int _roundId;
+
+ /** The puzzle object that the animations we're observering want to
+ * modify. */
+ protected PuzzleObject _puzobj;
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleBoardView.java b/src/java/com/threerings/puzzle/client/PuzzleBoardView.java
new file mode 100644
index 000000000..2d938c1df
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleBoardView.java
@@ -0,0 +1,461 @@
+//
+// $Id: PuzzleBoardView.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Graphics;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.util.ArrayList;
+
+import javax.swing.UIManager;
+
+import com.samskivert.swing.Label;
+import com.samskivert.swing.util.SwingUtil;
+import com.samskivert.util.StringUtil;
+
+import com.threerings.media.VirtualMediaPanel;
+import com.threerings.media.animation.Animation;
+import com.threerings.media.animation.AnimationAdapter;
+import com.threerings.media.image.Mirage;
+import com.threerings.media.sprite.Sprite;
+import com.threerings.media.sprite.SpriteManager;
+
+import com.threerings.yohoho.client.YoUI;
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.client.ScoreAnimation;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.PuzzleConfig;
+import com.threerings.puzzle.util.PuzzleContext;
+
+/**
+ * The puzzle board view displays a view of a puzzle game.
+ */
+public abstract class PuzzleBoardView extends VirtualMediaPanel
+{
+ /**
+ * Constructs a puzzle board view.
+ */
+ public PuzzleBoardView (PuzzleContext ctx)
+ {
+ super(ctx.getFrameManager());
+
+ // keep this for later
+ _ctx = ctx;
+ }
+
+ /**
+ * Initializes the board with the board dimensions.
+ */
+ public void init (PuzzleConfig config)
+ {
+ // save off our bounds
+ Dimension bounds = getPreferredSize();
+ _bounds = new Rectangle(0, 0, bounds.width, bounds.height);
+ }
+
+ /**
+ * Sets the board to be displayed.
+ */
+ public void setBoard (Board board)
+ {
+ _board = board;
+ }
+
+ /**
+ * Provides the board view with a reference to its controller so that
+ * it may communicate directly rather than by posting actions up the
+ * interface hierarchy which sometimes fails if the puzzle board view
+ * is hidden before we get a chance to post our actions.
+ */
+ public void setController (PuzzleController pctrl)
+ {
+ _pctrl = pctrl;
+ }
+
+ /**
+ * Sets the background image displayed by the board view.
+ */
+ public void setBackgroundImage (Mirage image)
+ {
+ _background = image;
+ }
+
+ /**
+ * Set whether this puzzle is paused or not.
+ */
+ public void setPaused (boolean paused)
+ {
+ if (paused) {
+ _pauseLabel = new Label(
+ YoUI.puzzle.getBundle().get(_pctrl.getPauseString()),
+ Label.BOLD | Label.OUTLINE, Color.WHITE, Color.BLACK,
+ _fonts[_fonts.length - 2]);
+ _pauseLabel.setTargetWidth(_bounds.width);
+ _pauseLabel.layout(this);
+ } else {
+ _pauseLabel = null;
+ }
+ repaint();
+ }
+
+ /**
+ * Adds the given animation to the set of animations currently present
+ * on the board. The animation will be added to a list of action
+ * animations whose count can be queried with {@link
+ * #getActionAnimationCount}. The animation will automatically be
+ * removed from the action list when it completes.
+ */
+ public void addActionAnimation (Animation anim)
+ {
+ super.addAnimation(anim);
+
+ // remember the animation's existence
+ _actionAnims.add(anim);
+
+ // and listen for it to finish so that we can clear it out
+ anim.addAnimationObserver(_actionAnimObs);
+ }
+
+ // documentation inherited
+ public void abortAnimation (Animation anim)
+ {
+ super.abortAnimation(anim);
+
+ // always check to see if it was action-y
+ animationFinished(anim);
+ }
+
+ /**
+ * Called when a potential action animation is finished.
+ */
+ protected void animationFinished (Animation anim)
+ {
+ if (DEBUG_ACTION) {
+ Log.info("Animation cleared " + StringUtil.shortClassName(anim) +
+ ":" + _actionAnims.contains(anim));
+ }
+
+ // if it WAS an action animation, check for a clear
+ if (_actionAnims.remove(anim)) {
+ maybeFireCleared();
+ }
+ }
+
+ /**
+ * Adds the given sprite to the set of sprites currently present on
+ * the board. The sprite will be added to a list of action sprites
+ * whose count can be queried with {@link #getActionSpriteCount}. Callers
+ * should be sure to remove the sprite when their work with it is done
+ * via {@link #removeSprite}.
+ */
+ public void addActionSprite (Sprite sprite)
+ {
+ // add the piece to the sprite manager
+ addSprite(sprite);
+
+ // note that this piece is interesting
+ _actionSprites.add(sprite);
+ }
+
+ /**
+ * Removes the given sprite from the board.
+ */
+ public void removeSprite (Sprite sprite)
+ {
+ super.removeSprite(sprite);
+
+ if (DEBUG_ACTION) {
+ Log.info("Sprite cleared " + StringUtil.shortClassName(sprite) +
+ ":" + _actionSprites.contains(sprite));
+ }
+
+ // we just always check to see if it was action-y
+ if (_actionSprites.remove(sprite)) {
+ maybeFireCleared();
+ }
+ }
+
+ /**
+ * Returns the number of action animations on the board.
+ */
+ public int getActionAnimationCount ()
+ {
+ return _actionAnims.size();
+ }
+
+ /**
+ * Returns the number of action sprites on the board.
+ */
+ public int getActionSpriteCount ()
+ {
+ return _actionSprites.size();
+ }
+
+ /**
+ * Returns the count of action sprites and animations on the board.
+ */
+ public int getActionCount ()
+ {
+ return _actionSprites.size() + _actionAnims.size();
+ }
+
+ /**
+ * Dumps to the logs, a list of interesting sprites and animations
+ * currently active on the puzzle board.
+ */
+ public void dumpActors ()
+ {
+ StringUtil.Formatter fmt = new StringUtil.Formatter() {
+ public String toString (Object obj) {
+ return StringUtil.shortClassName(obj);
+ }
+ };
+ Log.info("Board contents [board=" + StringUtil.shortClassName(this) +
+ ", sprites=" + StringUtil.listToString(_actionSprites, fmt) +
+ ", anims=" + StringUtil.listToString(_actionAnims, fmt) +
+ "].");
+ }
+
+ /**
+ * Creates and returns an animation displaying the given string with
+ * the specified parameters, floating it a short distance up the view.
+ *
+ * @param score the score text to display.
+ * @param color the color of the text.
+ * @param fontSize the size of the text; a value between 0 and {@link
+ * #getPuzzleFontCount} - 1.
+ * @param x the x-position at which the score is to be placed.
+ * @param y the y-position at which the score is to be placed.
+ */
+ public ScoreAnimation createScoreAnimation (
+ String score, Color color, int fontSize, int x, int y)
+ {
+ // create and configure the label
+ Label label = new Label(score);
+ label.setTargetWidth(_bounds.width);
+ label.setStyle(Label.OUTLINE);
+ label.setTextColor(color);
+ label.setAlternateColor(Color.black);
+ label.setFont(getPuzzleFont(fontSize));
+ label.setAlignment(Label.CENTER);
+ label.layout(this);
+
+ // create the score animation
+ ScoreAnimation anim = new ScoreAnimation(label, x, y);
+ anim.setRenderOrder(getScoreRenderOrder());
+ return anim;
+ }
+
+ /**
+ * Returns the render order to be assigned to score animations by
+ * default. Derived classes may wish to override this method to
+ * change the default render order.
+ *
+ * @see #createScoreAnimation
+ */
+ protected int getScoreRenderOrder ()
+ {
+ return 1;
+ }
+
+ /**
+ * Returns the puzzle font to be used for the specified score.
+ */
+ public int getFont (String why, int score, int maxScore)
+ {
+ int fontSize = (int)(score*getPuzzleFontCount()/maxScore);
+ fontSize = Math.min(FONT_SIZES.length-1, fontSize);
+// Log.info("Font for " + why + " (" + score + " of " + maxScore +
+// ") => " + fontSize + ".");
+ return fontSize;
+ }
+
+ /**
+ * Returns the number of different puzzle font sizes so that those who
+ * care to choose a font size out of the range of possible sizes can
+ * do so gracefully.
+ */
+ public int getPuzzleFontCount ()
+ {
+ return FONT_SIZES.length;
+ }
+
+ /**
+ * Positions the supplied animation so as to avoid any active
+ * animations previously registered with this method, and adds the
+ * animation to the list of animations to be avoided by any future
+ * avoid animations.
+ */
+ public void trackAvoidAnimation (Animation anim)
+ {
+ // reposition the animation as appropriate
+ Rectangle abounds = new Rectangle(anim.getBounds());
+ ArrayList avoidables = (ArrayList)_avoidAnims.clone();
+ if (SwingUtil.positionRect(abounds, _bounds, avoidables)) {
+ anim.setLocation(abounds.x, abounds.y);
+ }
+
+ // add the animation to the list of avoidables
+ _avoidAnims.add(anim);
+ // keep an eye on it so that we can remove it when it's finished
+ anim.addAnimationObserver(_avoidAnimObs);
+ }
+
+ // documentation inherited
+ public void paintBehind (Graphics2D gfx, Rectangle dirty)
+ {
+ super.paintBehind(gfx, dirty);
+
+ // render the background
+ renderBackground(gfx, dirty);
+ }
+
+ /**
+ * Fills the background of the board with the background color.
+ */
+ protected void renderBackground (Graphics2D gfx, Rectangle dirty)
+ {
+ gfx.setColor(getBackground());
+ gfx.fill(dirty);
+ }
+
+ // documentation inherited
+ public void paintBetween (Graphics2D gfx, Rectangle dirty)
+ {
+ super.paintBetween(gfx, dirty);
+// PerformanceMonitor.tick(this, "paint");
+ renderBoard(gfx, dirty);
+ }
+
+ // documentation inherited
+ protected void paintInFront (Graphics2D gfx, Rectangle dirty)
+ {
+ super.paintInFront(gfx, dirty);
+
+ // if the action is paused, indicate as much
+ if (_pauseLabel != null) {
+ Dimension d = _pauseLabel.getSize();
+ _pauseLabel.render(gfx, (_bounds.width - d.width) / 2,
+ (_bounds.height - d.height) / 2);
+ }
+ }
+
+ /**
+ * Fires a {@link #ACTION_CLEARED} command iff we have no remaining
+ * interesting sprites or animations.
+ */
+ protected void maybeFireCleared ()
+ {
+ if (DEBUG_ACTION) {
+ Log.info("Maybe firing cleared " +
+ getActionCount() + ":" + isShowing());
+ }
+ if (getActionCount() == 0) {
+ _pctrl.boardActionCleared();
+ }
+ }
+
+ /**
+ * Renders the board contents to the given graphics context.
+ * Sub-classes should implement this method to draw all of their
+ * game-specific business.
+ */
+ protected abstract void renderBoard (Graphics2D gfx, Rectangle dirty);
+
+ /**
+ * Returns the puzzle font of the specified size.
+ *
+ * @param size the desired font size; a value between 0 and {@link
+ * #getPuzzleFontCount} - 1.
+ */
+ protected static Font getPuzzleFont (int size)
+ {
+ return _fonts[size];
+ }
+
+ /** Our client context. */
+ protected PuzzleContext _ctx;
+
+ /** Our puzzle controller. */
+ protected PuzzleController _pctrl;
+
+ /** The board data to be displayed. */
+ protected Board _board;
+
+ /** The board's bounding rectangle. */
+ protected Rectangle _bounds;
+
+ /** The action animations on the board. */
+ protected ArrayList _actionAnims = new ArrayList();
+
+ /** The action sprites on the board. */
+ protected ArrayList _actionSprites = new ArrayList();
+
+ /** The animations that other animations may wish to avoid. */
+ protected ArrayList _avoidAnims = new ArrayList();
+
+ /** Our background image. */
+ protected Mirage _background;
+
+ /** A label to show when the puzzle is paused. */
+ protected Label _pauseLabel;
+
+ /** The distance in pixels that score animations float. */
+ protected int _scoreDist = DEFAULT_SCORE_DISTANCE;
+
+ /** Listens to our action animations and clears them when they're done. */
+ protected AnimationAdapter _actionAnimObs = new AnimationAdapter() {
+ public void animationCompleted (Animation anim, long when) {
+ animationFinished(anim);
+ }
+ };
+
+ /** Automatically removes avoid animations when they're done. */
+ protected AnimationAdapter _avoidAnimObs = new AnimationAdapter() {
+ public void animationCompleted (Animation anim, long when) {
+ if (!_avoidAnims.remove(anim)) {
+ Log.warning("Couldn't remove avoid animation?! " + anim + ".");
+ }
+ }
+ };
+
+ /** The puzzle fonts. */
+ protected static Font[] _fonts;
+
+ /** Temporary action debugging. */
+ protected static boolean DEBUG_ACTION = false;
+
+ // action state constants
+ protected static final int ACTION_GOING = 0;
+ protected static final int CLEAR_PENDING = 1;
+ protected static final int ACTION_CLEARED = 2;
+
+ /** The default vertical distance to float score animations. */
+ protected static final int DEFAULT_SCORE_DISTANCE = 30;
+
+ /** The mid-sized font used as the default size for score
+ * animations. */
+ protected static final int MEDIUM_FONT_SIZE = 1;
+
+ /** The puzzle font sizes. */
+ protected static final double[] FONT_SIZES = {
+ 24d, 30d, 36d, 42d, 52d, 68d };
+
+ static {
+ // create the puzzle fonts
+ Font ofont = UIManager.getFont("Label.serifFont");
+ ofont = ofont.deriveFont((float)FONT_SIZES[0]);
+ _fonts = new Font[FONT_SIZES.length];
+ for (int ii = 0; ii < _fonts.length; ii++) {
+ double scale = FONT_SIZES[ii]/FONT_SIZES[0];
+ _fonts[ii] = ofont.deriveFont(
+ AffineTransform.getScaleInstance(scale * 1.1d, scale));
+ }
+ }
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleController.java b/src/java/com/threerings/puzzle/client/PuzzleController.java
new file mode 100644
index 000000000..994576da8
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleController.java
@@ -0,0 +1,1012 @@
+//
+// $Id: PuzzleController.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import javax.swing.JButton;
+
+import com.samskivert.swing.GroupLayout;
+import com.samskivert.swing.event.CommandEvent;
+import com.samskivert.util.CollectionUtil;
+import com.samskivert.util.ObserverList;
+import com.samskivert.util.StringUtil;
+
+import com.threerings.io.Streamable;
+import com.threerings.util.MessageBundle;
+import com.threerings.util.StreamableArrayList;
+
+import com.threerings.media.FrameParticipant;
+import com.threerings.media.sound.SoundCodes;
+
+import com.threerings.presents.client.InvocationReceiver;
+import com.threerings.presents.dobj.AttributeChangedEvent;
+import com.threerings.presents.dobj.ElementUpdateListener;
+import com.threerings.presents.dobj.ElementUpdatedEvent;
+
+import com.threerings.crowd.client.OccupantDirector;
+import com.threerings.crowd.client.PlaceControllerDelegate;
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.OccupantInfo;
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.parlor.game.GameController;
+import com.threerings.parlor.game.GameObject;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleConfig;
+import com.threerings.puzzle.data.PuzzleObject;
+import com.threerings.puzzle.util.PuzzleContext;
+
+/**
+ * The puzzle game controller handles logical actions for a puzzle game.
+ */
+public abstract class PuzzleController extends GameController
+ implements PuzzleCodes, ElementUpdateListener
+{
+ /** The action command to toggle chatting mode. */
+ public static final String TOGGLE_CHATTING = "toggle_chat";
+
+ /** Used by {@link #fireWhenActionCleared}. */
+ public static interface ClearPender
+ {
+ /** {@link #actionCleared} return code. */
+ public static final int RESTART_ACTION = -1;
+
+ /** {@link #actionCleared} return code. */
+ public static final int CARE_NOT = 0;
+
+ /** {@link #actionCleared} return code. */
+ public static final int NO_RESTART_ACTION = 1;
+
+ /**
+ * Called when the action is fully cleared.
+ *
+ * @return One of {@link #RESTART_ACTION}, {@link #CARE_NOT} or
+ * {@link #NO_RESTART_ACTION}.
+ */
+ public int actionCleared ();
+ }
+
+ // documentation inherited
+ protected void didInit ()
+ {
+ super.didInit();
+
+ _panel = (PuzzlePanel)_view;
+ _pctx = (PuzzleContext)_ctx;
+
+ // initialize the puzzle panel
+ _puzconfig = (PuzzleConfig)_config;
+ _panel.init(_puzconfig);
+
+ // initialize the board view
+ _pview = _panel.getBoardView();
+ _pview.setController(this);
+ }
+
+ /**
+ * Creates and returns a new board model.
+ */
+ protected abstract Board newBoard ();
+
+ /**
+ * Returns the board associated with the puzzle.
+ */
+ public Board getBoard ()
+ {
+ return _pboard;
+ }
+
+ /**
+ * Returns the player's index in the list of players for the game.
+ */
+ public int getPlayerIndex ()
+ {
+ return _pidx;
+ }
+
+ /**
+ * Convenience method for playing a puzzle sound effect.
+ */
+ public void playSound (String packagePath, String key)
+ {
+ _pctx.getSoundManager().play(
+ SoundCodes.GAME_FX, packagePath, key);
+ }
+
+ // documentation inherited
+ public void setGameOver (boolean gameOver)
+ {
+ super.setGameOver(gameOver);
+
+ // clear the action if we're informed that the game is over early
+ // by the client
+ if (gameOver) {
+ clearAction();
+ }
+ }
+
+ /**
+ * Returns true if the puzzle has action, false if the action is
+ * cleared or it is suspended.
+ */
+ public boolean hasAction ()
+ {
+ return (_astate == ACTION_GOING);
+ }
+
+ /**
+ * Sets whether we're focusing on the chat window rather than the puzzle.
+ */
+ public void setChatting (boolean chatting)
+ {
+ // ignore the request if we're already there
+ if ((isChatting() == chatting) ||
+ // ..or if we want to initiate chatting and..
+ // we either can't right now or we don't have action
+ (chatting && (!canStartChatting() || !hasAction()))) {
+ return;
+ }
+
+ // update the panel
+ _panel.setPuzzleGrabsKeys(!chatting);
+
+ // if we're moving focus to chat..
+ if (chatting) {
+ // ...enable "click to unchat" mode in the puzzle board view
+ _panel.getBoardView().addMouseListener(new MouseAdapter() {
+ public void mousePressed (MouseEvent event) {
+ setChatting(false);
+ _panel.getBoardView().removeMouseListener(this);
+ }
+ });
+ }
+
+ // update the chatting state
+ _chatting = chatting;
+
+ // dispatch the change to our delegates
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).setChatting(_chatting);
+ }
+ });
+
+ // and check if we should be suspending the action during this pause
+ if (supportsActionPause()) {
+ // clear the action if we're pausing, resume it if we're
+ // unpausing
+ if (chatting) {
+ clearAction();
+ } else {
+ safeStartAction();
+ }
+ _pview.setPaused(chatting);
+ }
+ }
+
+ /**
+ * Get the (untranslated) string to display when the puzzle is paused.
+ */
+ public String getPauseString ()
+ {
+ return "m.paused";
+ }
+
+ /**
+ * Derived classes should override this and return false if their
+ * action should not be paused when the user switches control to the
+ * chat area.
+ */
+ protected boolean supportsActionPause ()
+ {
+ return true;
+ }
+
+ /**
+ * Can we start chatting at this juncture?
+ */
+ protected boolean canStartChatting ()
+ {
+ // if we're waiting, we can't pause
+ if (isWaiting()) {
+ return false;
+ }
+
+ // otherwise, check with the delegates
+ final boolean[] canChatNow = new boolean[] { true };
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ canChatNow[0] =
+ ((PuzzleControllerDelegate)delegate).canStartChatting() &&
+ canChatNow[0];
+ }
+ });
+ return canChatNow[0];
+ }
+
+ /**
+ * Returns true if the puzzle has been defocused because the player
+ * is doing some chatting.
+ */
+ public boolean isChatting ()
+ {
+ return _chatting;
+ }
+
+ // documentation inherited
+ public void willEnterPlace (PlaceObject plobj)
+ {
+ super.willEnterPlace(plobj);
+
+ // register the puzzle object's chat
+ _pctx.getChatDirector().addAuxiliarySource(plobj, PUZZLE_CHAT_TYPE);
+
+ // get a casted reference to our puzzle object
+ _puzobj = (PuzzleObject)plobj;
+
+ // listen to key events..
+ _pctx.getKeyDispatcher().addGlobalKeyListener(_globalKeyListener);
+
+ // save off our player index
+ _pidx = _puzobj.getPlayerIndex(_pctx.getUsername());
+
+ // generate the starting board
+ generateNewBoard();
+
+ // if the game is already in play, start up the action
+ if (_puzobj.state == PuzzleObject.IN_PLAY) {
+ startAction();
+ }
+ }
+
+ // documentation inherited
+ public void mayLeavePlace (PlaceObject plobj)
+ {
+ super.mayLeavePlace(plobj);
+
+ // flush any pending progress events
+ sendProgressUpdate();
+ }
+
+ // documentation inherited
+ public void didLeavePlace (PlaceObject plobj)
+ {
+ super.didLeavePlace(plobj);
+
+ // clean up and clear out
+ clearAction();
+
+ // we're certainly no longer waiting
+ _waitstamp = -1;
+
+ // unregister the puzzle object's chat
+ _pctx.getChatDirector().removeAuxiliarySource(plobj);
+
+ // stop listening to key events..
+ _pctx.getKeyDispatcher().removeGlobalKeyListener(_globalKeyListener);
+
+ // clear out the puzzle object
+ _puzobj = null;
+ }
+
+ /**
+ * Puzzles that do not have "action" that starts and stops (via {@link
+ * #startAction} and {@link #clearAction}) when the puzzle starts and
+ * stops can override this method and return false.
+ */
+ protected boolean isActionPuzzle ()
+ {
+ return true;
+ }
+
+ /**
+ * A way for controllers to display a puzzle-related system message.
+ */
+ public void systemMessage (String bundle, String msg)
+ {
+ _pctx.getChatDirector().displayInfo(bundle, msg, PUZZLE_CHAT_TYPE);
+ }
+
+ // documentation inherited
+ protected void gameDidStart ()
+ {
+ // we have to postpone all game starting activity until the
+ // current action has ended; only after all the animations have
+ // been completed will everything be in a state fit for starting
+ // back up again
+ fireWhenActionCleared(new ClearPender() {
+ public int actionCleared () {
+ // do the standard game did start business
+ PuzzleController.super.gameDidStart();
+ return RESTART_ACTION;
+ }
+ });
+ }
+
+ // documentation inherited
+ protected void gameDidEnd ()
+ {
+ // clean up and clear out
+ clearAction();
+
+ // wait until the action is cleared before we roll down to our
+ // delegates and do all that business
+ fireWhenActionCleared(new ClearPender() {
+ public int actionCleared () {
+ PuzzleController.super.gameDidEnd();
+ return CARE_NOT;
+ }
+ });
+ }
+
+ // documentation inherited
+ protected void gameWillReset ()
+ {
+ super.gameWillReset();
+
+ // stop the old action
+ clearAction();
+
+ // when the server gets around to resetting the game, we'll get a
+ // 'state => IN_PLAY' message which will result in gameDidStart()
+ // being called and starting the action back up
+ }
+
+ /**
+ * Called when a new board is set.
+ */
+ public void setBoard (Board board)
+ {
+ // we don't need to do anything by default
+ }
+
+ /**
+ * Derived classes should override this method and do whatever is
+ * necessary to start up the action for their puzzle. This could be
+ * called when the user is already in the "room" and the game starts,
+ * or immediately upon entering the room if the game is already
+ * started (for example if they disconnected and reconnected to a game
+ * already in progress).
+ */
+ protected void startAction ()
+ {
+ // do nothing if we're not an action puzzle
+ if (!isActionPuzzle()) {
+ return;
+ }
+
+ // refuse to start the action if our puzzle view is hidden
+ if (!_panel.getBoardView().isShowing()) {
+ Log.warning("Refusing to start action on hidden puzzle.");
+ Thread.dumpStack();
+ return;
+ }
+
+ // refuse to start the action if it's already going
+ if (_astate != ACTION_CLEARED) {
+ Log.warning("Action state inappropriate for startAction() " +
+ "[astate=" + _astate + "].");
+ Thread.dumpStack();
+ return;
+ }
+
+ if (isChatting() && supportsActionPause()) {
+ Log.info("Not starting action, player is chatting in a puzzle " +
+ "that supports pausing the action.");
+ return;
+ }
+
+ Log.debug("Starting puzzle action.");
+
+ // register the game progress updater; it may already be updated
+ // because we can cycle through clearing the action and starting
+ // it again before the updater gets a chance to unregister itself
+ if (!_pctx.getFrameManager().isRegisteredFrameParticipant(_updater)) {
+ _pctx.getFrameManager().registerFrameParticipant(_updater);
+ }
+
+ // make a note that we've started the action
+ _astate = ACTION_GOING;
+
+ // let our panel know what's up
+ _panel.startAction();
+
+ // and if we're not currently chatting, set the puzzle to grab
+ // keys and for the chatbox to look disabled
+ if (!isChatting()) {
+ _panel.setPuzzleGrabsKeys(true);
+ }
+
+ // let our delegates do their business
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).startAction();
+ }
+ });
+ }
+
+ /**
+ * If it is not known whether the puzzle board view has finished
+ * animating its final bits after a previous call to {@link
+ * #clearAction}, this method should be used instead of {@link
+ * #startAction} as it will wait until the action is confirmedly over
+ * before starting it anew.
+ */
+ protected void safeStartAction ()
+ {
+ // do nothing if we're not an action puzzle
+ if (!isActionPuzzle()) {
+ return;
+ }
+
+ fireWhenActionCleared(new ClearPender() {
+ public int actionCleared () {
+ return RESTART_ACTION;
+ }
+ });
+ }
+
+ /**
+ * Called when the game has ended or when it is going to reset and
+ * when the client leaves the game "room". This method does not always
+ * immediately clear the action, but may mark the clear as pending if
+ * the action cannot yet be cleared (as indicated by {@link
+ * #canClearAction}). The action will eventually be cleared which will
+ * result in a call to {@link #actuallyClearAction} which is what
+ * derived classes should override to do their action clearing
+ * business.
+ */
+ protected void clearAction ()
+ {
+ // do nothing if we're not an action puzzle
+ if (!isActionPuzzle()) {
+ return;
+ }
+
+ // no need to clear if we're already cleared or clearing
+ if (_astate == CLEAR_PENDING || _astate == ACTION_CLEARED) {
+ return;
+ }
+
+ Log.debug("Attempting to clear puzzle action.");
+
+ // put ourselves into a pending clear state and attempt to clear
+ // the action
+ _astate = CLEAR_PENDING;
+ maybeClearAction();
+ }
+
+ /**
+ * This method is called by the {@link PuzzleBoardView} when all
+ * action on the board has finished.
+ */
+ protected void boardActionCleared ()
+ {
+ // if we have a clear pending, this could be the trigger that
+ // allows us to clear our action
+ maybeClearAction();
+ }
+
+ /**
+ * Queues up code to be invoked when the action is completely cleared
+ * (including all remaining interesting sprites and animations on the
+ * puzzle board).
+ */
+ protected void fireWhenActionCleared (ClearPender pender)
+ {
+ // if the action is already ended, fire this pender immediately
+ if (_astate == ACTION_CLEARED) {
+ if (pender.actionCleared() == ClearPender.RESTART_ACTION) {
+ Log.debug("Restarting action at behest of pender " +
+ pender + ".");
+ startAction();
+ }
+
+ } else {
+ Log.debug("Queueing action pender " + pender + ".");
+ _clearPenders.add(pender);
+ }
+ }
+
+ /**
+ * Returns whether or not it is safe to clear the action. The default
+ * behavior is to not allow the action to be cleared until all
+ * interesting sprites and animations in the board view have finished.
+ * If derived classes or delegates wish to postpone the clearing of
+ * the action, they can return false from this method, but they must
+ * then be sure to call {@link #maybeClearAction} when whatever
+ * condition that caused them to desire to postpone action clearing
+ * has finally been satisfied.
+ */
+ protected boolean canClearAction ()
+ {
+ final boolean[] canClear = new boolean[1];
+ canClear[0] = (_pview.getActionCount() == 0);
+ if (!canClear[0]) {
+ _pview.dumpActors();
+ _pview.DEBUG_ACTION = true;
+ }
+
+ // let our delegates do their business
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ canClear[0] = canClear[0] &&
+ ((PuzzleControllerDelegate)delegate).canClearAction();
+ }
+ });
+
+ return canClear[0];
+ }
+
+ /**
+ * Called to effect the actual clearing of our action if we've
+ * received some asynchronous trigger that indicates that it may well
+ * be safe now to clear the action.
+ */
+ protected void maybeClearAction ()
+ {
+ if (_astate == CLEAR_PENDING && canClearAction()) {
+ actuallyClearAction();
+ }
+ }
+
+ /**
+ * Performs the actual process of clearing the action for this puzzle.
+ * This is only called after it is known to be safe to clear the
+ * action. Derived classes can override this method and clear out
+ * anything that is not needed while the puzzle's "action" is not
+ * going (timers, etc.). Anything that is cleared out here should be
+ * recreated in {@link #startAction}.
+ */
+ protected void actuallyClearAction ()
+ {
+ Log.debug("Actually clearing action.");
+
+ // make a note that we've cleared the action
+ _astate = ACTION_CLEARED;
+ _pview.DEBUG_ACTION = false;
+
+ // let our delegates do their business
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).clearAction();
+ }
+ });
+
+ // let our panel know what's up
+ _panel.clearAction();
+ _panel.setPuzzleGrabsKeys(false); // let the user chat
+
+ // deliver one final update to the server
+ sendProgressUpdate();
+
+ // let derived classes do things
+ try {
+ actionWasCleared();
+ } catch (Exception e) {
+ Log.warning("Choked in actionWasCleared");
+ Log.logStackTrace(e);
+ }
+
+ // notify any penders that the action has cleared
+ final int[] results = new int[2];
+ _clearPenders.apply(new ObserverList.ObserverOp() {
+ public boolean apply (Object observer) {
+ switch (((ClearPender)observer).actionCleared()) {
+ case ClearPender.RESTART_ACTION: results[0]++; break;
+ case ClearPender.NO_RESTART_ACTION: results[1]++; break;
+ }
+ return true;
+ }
+ });
+ _clearPenders.clear();
+
+ // if there are no refusals and at least one restart request, go
+ // ahead and restart the action now
+ if (results[1] == 0 && results[0] > 0) {
+ startAction();
+ }
+ }
+
+ /**
+ * Called when the action was actually cleared, but before the action
+ * obsevers are notified.
+ */
+ protected void actionWasCleared ()
+ {
+ }
+
+ // documentation inherited
+ public boolean handleAction (ActionEvent action)
+ {
+ String cmd = action.getActionCommand();
+ if (cmd.equals(TOGGLE_CHATTING)) {
+ setChatting(!isChatting());
+
+ } else {
+ return super.handleAction(action);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the delay in milliseconds between sending each progress
+ * update event to the server. Derived classes may wish to override
+ * this to send their progress updates more or less frequently than
+ * the default.
+ */
+ protected long getProgressInterval ()
+ {
+ return DEFAULT_PROGRESS_INTERVAL;
+ }
+
+ /**
+ * Signal the game to generate and distribute a new board.
+ */
+ protected void generateNewBoard ()
+ {
+ // wait for any animations or sprites in the board to finish their
+ // business before setting the board into place
+ fireWhenActionCleared(new ClearPender() {
+ public int actionCleared () {
+ // update the player board
+ _pboard = newBoard();
+ if (_puzobj.seed != 0) {
+ _pboard.initializeSeed(_puzobj.seed);
+ }
+ setBoard(_pboard);
+ _pview.setBoard(_pboard);
+
+ // and repaint things
+ _pview.repaint();
+
+ // let our delegates do their business
+ DelegateOp dop = new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).setBoard(_pboard);
+ }
+ };
+ applyToDelegates(PuzzleControllerDelegate.class, dop);
+
+ return CARE_NOT;
+ }
+ });
+ }
+
+ /**
+ * Adds a waiting entity to the list of entities awaiting the receipt
+ * of data from the server, and without whose happiness the show must
+ * not go on.
+ */
+ public void addWaiter (Object waiter)
+ {
+ if (!_waiters.contains(waiter)) {
+ _waiters.add(waiter);
+ String state = (_waitstamp == -1) ? "(suspending)" : "(suspended)";
+ Log.info("Adding waiter " + state + " [waiter=" + waiter + "].");
+ // suspend things if we weren't already suspended
+ if (_waitstamp == -1) {
+ _waitstamp = _pview.getTimeStamp();
+ didSuspend();
+ }
+
+ } else {
+ Log.warning("Already-waiting waiter attempted to wait again " +
+ "[waiter=" + waiter + "].");
+ }
+ }
+
+ /**
+ * Removes a waiting entity from the list of entities awaiting the
+ * receipt of data from the server, and without whose happiness the
+ * show must not go on.
+ */
+ public void removeWaiter (Object waiter)
+ {
+ if (_waiters.remove(waiter)) {
+ // resume things once we've no longer got any waiters
+ if (_waiters.size() == 0 && _waitstamp != -1) {
+ long delta = _pview.getTimeStamp() - _waitstamp;
+ _waitstamp = -1;
+ didResume(delta);
+ }
+ String state = (_waitstamp == -1) ? "(resumed)" : "(suspended)";
+ Log.info("Removed waiter " + state + " [waiter=" + waiter + "].");
+
+ } else {
+ Log.warning("Requested to remove a waiter that's not waiting " +
+ "[waiter=" + waiter + "].");
+ }
+ }
+
+ /**
+ * Returns whether things are paused while we wait for a new piece
+ * packet from the server.
+ */
+ public boolean isWaiting ()
+ {
+ return (_waitstamp != -1);
+ }
+
+ /**
+ * Called when the game is suspended while we wait for a new piece
+ * packet from the server.
+ */
+ public void didSuspend ()
+ {
+ // we've somehow whipped through all of our pieces, so send a
+ // progress update to the server straightaway which will in turn
+ // prompt it to send us more pieces
+ sendProgressUpdate();
+
+ // let our delegates do their business
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).didSuspend();
+ }
+ });
+ }
+
+ /**
+ * Called when the game is resumed once a new piece packet arrives
+ * from the server while we were suspended awaiting its arrival.
+ *
+ * @param delta the time elapsed in milliseconds while we were
+ * suspended.
+ */
+ public void didResume (final long delta)
+ {
+ // let our delegates do their business
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).didResume(delta);
+ }
+ });
+ }
+
+ /**
+ * Returns the number of progress events currently queued up for
+ * sending to the server with the next progress update.
+ */
+ public int getEventCount ()
+ {
+ return _events.size();
+ }
+
+ /**
+ * Adds the given progress event and a snapshot of the supplied board
+ * state to the set of progress events and associated board states for
+ * later transmission to the server.
+ */
+ public void addProgressEvent (int event, Board board)
+ {
+ // make sure they don't queue things up at strange times
+ if (_puzobj.state != PuzzleObject.IN_PLAY) {
+ Log.warning("Rejecting progress event; game not in play " +
+ "[puzobj=" + _puzobj.which() +
+ ", event=" + event + "].");
+ return;
+ }
+
+ _events.add(new Integer(event));
+ if (_puzconfig.syncBoardState()) {
+ _states.add((board == null) ? null : board.clone());
+ if (board == null) {
+ Log.warning("Added progress event with no associated board " +
+ "state, server will not be able to ensure " +
+ "board state synchronization.");
+ }
+ }
+ }
+
+ /**
+ * Sends the server a game progress update with the list of events, as
+ * well as board states if {@link #SYNC_BOARD_STATE} is true.
+ */
+ public void sendProgressUpdate ()
+ {
+ // make sure we have our puzzle object and events to send
+ int size = _events.size();
+ if (size == 0 || _puzobj == null) {
+ return;
+ }
+
+ // create an array of the events we're sending to the server
+ int[] events = CollectionUtil.toIntArray(_events);
+ _events.clear();
+
+// Log.info("Sending progress [round=" + _puzobj.roundId +
+// ", events=" + StringUtil.toString(events) + "].");
+
+ // create an array of the board states that correspond with those
+ // events (if state syncing is enabled)
+ if (_puzconfig.syncBoardState()) {
+ int scount = _states.size();
+ Board[] states = new Board[scount];
+ for (int ii = 0; ii < scount; ii++) {
+ states[ii] = (Board)_states.remove(0);
+ }
+
+ // send the update progress request
+ _puzobj.puzzleGameService.updateProgressSync(
+ _ctx.getClient(), _puzobj.roundId, events, states);
+
+ } else {
+ // send the update progress request
+ _puzobj.puzzleGameService.updateProgress(
+ _ctx.getClient(), _puzobj.roundId, events);
+ }
+ }
+
+ // documentation inherited
+ public void attributeChanged (AttributeChangedEvent event)
+ {
+ super.attributeChanged(event);
+
+ String name = event.getName();
+ if (name.equals(PuzzleObject.DIFFICULTY)) {
+ difficultyChanged(_puzobj.difficulty);
+
+ } else if (name.equals(PuzzleObject.SEED)) {
+ generateNewBoard();
+ }
+ }
+
+ /**
+ * Called when the puzzle difficulty level is changed.
+ */
+ public void difficultyChanged (final int level)
+ {
+ // dispatch this to our delegates
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).difficultyChanged(level);
+ }
+ });
+ }
+
+ // documentation inherited
+ public void elementUpdated (ElementUpdatedEvent event)
+ {
+ String name = event.getName();
+ if (name.equals(PuzzleObject.PLAYER_STATUS)) {
+ if (event.getIntValue() == PuzzleObject.PLAYER_KNOCKED_OUT) {
+ playerKnockedOut(event.getIndex());
+ }
+ }
+ }
+
+ /**
+ * Called when a player is knocked out of the game to give the puzzle
+ * a chance to perform any post-knockout actions that may be desired.
+ * Derived classes may wish to override this method but should be sure
+ * to call super.playerKnockedOut().
+ */
+ protected void playerKnockedOut (final int pidx)
+ {
+ // dispatch this to our delegates
+ applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
+ public void apply (PlaceControllerDelegate delegate) {
+ ((PuzzleControllerDelegate)delegate).playerKnockedOut(pidx);
+ }
+ });
+ }
+
+ /** Handles the sending of puzzle progress updates. We can't just
+ * register an interval for this because sometimes the clock goes
+ * backwards in time in windows and our intervals don't get called for
+ * a long period of time which causes the server to think the client
+ * is disconnected or cheating and resign them from the puzzle. God
+ * bless you, Microsoft. */
+ protected FrameParticipant _updater = new FrameParticipant() {
+ public void tick (long tickStamp) {
+ if (_astate == ACTION_CLEARED) {
+ // remove ourselves as the action is now cleared; we can't
+ // do this in actuallyClearAction() because that might get
+ // called during the PuzzlePanel's frame tick and it's
+ // only safe to remove yourself during a tick(), not
+ // another frame participant
+ _pctx.getFrameManager().removeFrameParticipant(_updater);
+
+ } else if (tickStamp - _lastProgressTick > getProgressInterval()) {
+ _lastProgressTick = tickStamp;
+ sendProgressUpdate();
+ }
+ }
+
+ public boolean needsPaint () {
+ return false;
+ }
+
+ public Component getComponent () {
+ return null;
+ }
+
+ protected long _lastProgressTick;
+ };
+
+ /** A casted reference to the client context. */
+ protected PuzzleContext _pctx;
+
+ /** Our player index in the game. */
+ protected int _pidx;
+
+ /** The puzzle panel. */
+ protected PuzzlePanel _panel;
+
+ /** The puzzle config. */
+ protected PuzzleConfig _puzconfig;
+
+ /** A reference to our puzzle game object. */
+ protected PuzzleObject _puzobj;
+
+ /** The puzzle board view. */
+ protected PuzzleBoardView _pview;
+
+ /** The puzzle board data. */
+ protected Board _pboard;
+
+ /** The list of relevant game events since the last progress update. */
+ protected ArrayList _events = new ArrayList();
+
+ /** Board snapshots that correspond to our board state after each of
+ * our events has been applied. */
+ protected ArrayList _states = new ArrayList();
+
+ /** The entities waiting on some action before the puzzle can proceed
+ * apace, and accordingly for whom the puzzle is paused. */
+ protected ArrayList _waiters = new ArrayList();
+
+ /** The time at which we paused waiting for pieces. */
+ protected long _waitstamp = -1;
+
+ /** A flag indicating that we're in chatting mode. */
+ protected boolean _chatting = false;
+
+ /** The current action state of the puzzle. */
+ protected int _astate = ACTION_CLEARED;
+
+ /** The action cleared penders. */
+ protected ObserverList _clearPenders = new ObserverList(
+ ObserverList.SAFE_IN_ORDER_NOTIFY);
+
+ /** A key listener that currently just toggles pause in the puzzle. */
+ protected KeyListener _globalKeyListener = new KeyAdapter() {
+ public void keyReleased (KeyEvent e)
+ {
+ if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
+ // toggle pausyness, my pussycat
+ setChatting(!isChatting());
+ }
+ }
+ };
+
+ /** The delay in milliseconds between progress update intervals. */
+ protected static final long DEFAULT_PROGRESS_INTERVAL = 6000L;
+
+ /** A {@link #_astate} constant. */
+ protected static final int ACTION_CLEARED = 0;
+
+ /** A {@link #_astate} constant. */
+ protected static final int CLEAR_PENDING = 1;
+
+ /** A {@link #_astate} constant. */
+ protected static final int ACTION_GOING = 2;
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java b/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java
new file mode 100644
index 000000000..8e1de6dfb
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java
@@ -0,0 +1,162 @@
+//
+// $Id: PuzzleControllerDelegate.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.parlor.game.GameControllerDelegate;
+
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleGameCodes;
+import com.threerings.puzzle.data.PuzzleObject;
+
+/**
+ * A base class for puzzle controller delegates. Provides access to some
+ * delegated puzzle controller methods ({@link #startAction}, {@link
+ * #clearAction}, etc.) and provides a casted reference to the puzzle
+ * object.
+ */
+public class PuzzleControllerDelegate extends GameControllerDelegate
+ implements PuzzleCodes, PuzzleGameCodes
+{
+ /**
+ * Constructs a puzzle controller delegate.
+ */
+ public PuzzleControllerDelegate (PuzzleController ctrl)
+ {
+ super(ctrl);
+
+ // keep around a casted reference to our controller
+ _ctrl = ctrl;
+ }
+
+ // documentation inherited
+ public void willEnterPlace (PlaceObject plobj)
+ {
+ super.willEnterPlace(plobj);
+
+ // get a casted reference to our game object
+ _puzobj = (PuzzleObject)plobj;
+ }
+
+ // documentation inherited
+ public void didLeavePlace (PlaceObject plobj)
+ {
+ super.didLeavePlace(plobj);
+
+ _puzobj = null;
+ }
+
+ /**
+ * Called when a player is knocked out of the game.
+ */
+ public void playerKnockedOut (int pidx)
+ {
+ }
+
+ /**
+ * Called when the user toggles chatting mode.
+ */
+ public void setChatting (boolean chatting)
+ {
+ }
+
+ /**
+ * Can we start chatting at the instant that this method is called?
+ */
+ protected boolean canStartChatting ()
+ {
+ return true;
+ }
+
+ /**
+ * Derived classes should override this method and do whatever is
+ * necessary to start up the action for their puzzle. This could be
+ * called when the user is already in the "room" and the game starts,
+ * or immediately upon entering the room if the game is already
+ * started (for example if they disconnected and reconnected to a game
+ * already in progress).
+ */
+ protected void startAction ()
+ {
+ }
+
+ /**
+ * Delegates that wish to postpone action clearing can override this
+ * method to return false until such time as the action can be
+ * cleared. They must, however, call {@link #maybeClearAction} when
+ * conditions become such that they would once again allow action to
+ * be cleared.
+ */
+ protected boolean canClearAction ()
+ {
+ return true;
+ }
+
+ /**
+ * Calls {@link PuzzleController#maybeClearAction}, preserving its
+ * protected access but making the method available to all
+ * PuzzleControllerDelegate derivations.
+ */
+ protected void maybeClearAction ()
+ {
+ _ctrl.maybeClearAction();
+ }
+
+ /**
+ * Puzzles should override this method and clear out any action on the
+ * board and generally clean up anything that was going on because the
+ * game was in play. This is called when the game has ended or when it
+ * is going to reset and when the client leaves the game "room".
+ * Anything that is cleared out here should be recreated in {@link
+ * #startAction}.
+ */
+ protected void clearAction ()
+ {
+ }
+
+ /**
+ * Called when the puzzle controller sets up a new board for the
+ * player.
+ *
+ * @param board the newly initialized and ready-to-go board.
+ */
+ public void setBoard (Board board)
+ {
+ }
+
+ /**
+ * Called when the game is suspended while we wait for a new piece
+ * packet from the server.
+ */
+ public void didSuspend ()
+ {
+ }
+
+ /**
+ * Called when the game is resumed once a new piece packet arrives
+ * from the server while we were suspended awaiting its arrival.
+ *
+ * @param delta the time elapsed in milliseconds while we were
+ * suspended.
+ */
+ public void didResume (long delta)
+ {
+ }
+
+ /**
+ * Called when the puzzle difficulty level is changed.
+ */
+ public void difficultyChanged (int level)
+ {
+ }
+
+ /** Our puzzle controller. */
+ protected PuzzleController _ctrl;
+
+ /** The puzzle distributed object. */
+ protected PuzzleObject _puzobj;
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleGameService.java b/src/java/com/threerings/puzzle/client/PuzzleGameService.java
new file mode 100644
index 000000000..3572a68ec
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleGameService.java
@@ -0,0 +1,31 @@
+//
+// $Id: PuzzleGameService.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.PuzzleCodes;
+
+/**
+ * Provides services used by puzzle game clients to request that actions
+ * be taken by the puzzle manager.
+ */
+public interface PuzzleGameService extends InvocationService, PuzzleCodes
+{
+ /**
+ * Asks the puzzle manager to apply the supplied progress events for
+ * the specified puzzle round to the player's state.
+ */
+ public void updateProgress (Client client, int roundId, int[] events);
+
+ /**
+ * Debug variant of {@link #updateProgress} that is only used when
+ * {@link #SYNC_BOARD_STATE} is true and which includes the board
+ * states associated with each event.
+ */
+ public void updateProgressSync (
+ Client client, int roundId, int[] events, Board[] states);
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzlePanel.java b/src/java/com/threerings/puzzle/client/PuzzlePanel.java
new file mode 100644
index 000000000..7eaaa48c1
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzlePanel.java
@@ -0,0 +1,244 @@
+//
+// $Id: PuzzlePanel.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import java.awt.BorderLayout;
+import java.awt.Graphics;
+import java.io.IOException;
+import javax.swing.JPanel;
+
+import com.samskivert.swing.Controller;
+import com.samskivert.swing.ControllerProvider;
+import com.samskivert.swing.util.SwingUtil;
+
+import com.threerings.media.image.Mirage;
+import com.threerings.util.KeyTranslator;
+import com.threerings.util.RobotPlayer;
+
+import com.threerings.crowd.client.PlaceView;
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleConfig;
+import com.threerings.puzzle.data.PuzzleGameCodes;
+import com.threerings.puzzle.data.PuzzleObject;
+import com.threerings.puzzle.util.PuzzleContext;
+
+/**
+ * The puzzle panel class should be extended by classes that provide a
+ * view for a puzzle game. The {@link PuzzleController} calls these
+ * methods as necessary to perform its duties in managing the logical
+ * actions of a puzzle game.
+ */
+public abstract class PuzzlePanel extends JPanel
+ implements PlaceView, ControllerProvider, PuzzleCodes, PuzzleGameCodes
+{
+ /**
+ * Constructs a puzzle panel.
+ */
+ public PuzzlePanel (PuzzleContext ctx, PuzzleController controller)
+ {
+ _ctx = ctx;
+ _controller = controller;
+
+ // set up the keyboard manager
+ _xlate = getKeyTranslator();
+ _ctx.getKeyboardManager().setTarget(this, _xlate);
+
+ // leave the keyboard manager disabled to start, and set things up
+ // for chatting
+ setPuzzleGrabsKeys(false);
+
+ // configure the puzzle panel
+ setLayout(new BorderLayout());
+
+ // create the puzzle board view
+ _bview = createBoardView(ctx);
+
+ // create the puzzle board panel
+ _bpanel = createBoardPanel();
+
+ // add the board panel
+ add(_bpanel, BorderLayout.CENTER);
+ }
+
+ /**
+ * Temporarily replaces the puzzle board display with the supplied
+ * overlay panel. The panel can be removed and the board display
+ * restored by calling {@link #popOverlayPanel}.
+ *
+ * @return true if the specified panel will be displayed, false if it
+ * was not able to be pushed because another overlay panel is already
+ * pushed onto the primary panel.
+ */
+ public boolean pushOverlayPanel (JPanel opanel)
+ {
+ // bail if we've already got an overlay
+ if (_opanel != null) {
+ Log.info("Refusing to push overlay panel, we've already got one " +
+ "[opanel=" + _opanel + ", npanel=" + opanel + "].");
+ return false;
+ }
+
+ // swap in the overlay panel
+ _opanel = opanel;
+ remove(_bpanel);
+ add(_opanel);
+
+ // make sure the UI updates
+ SwingUtil.refresh(this);
+
+ return true;
+ }
+
+ /**
+ * Pops the overlay panel off of the main puzzle board display.
+ */
+ public void popOverlayPanel ()
+ {
+ if (_opanel != null) {
+ remove(_opanel);
+ _opanel = null;
+ add(_bpanel);
+
+ // make sure the UI updates
+ SwingUtil.refresh(this);
+ }
+ }
+
+ /**
+ * Initializes the puzzle panel with the puzzle config of the puzzle
+ * whose user interface is being displayed by the panel
+ */
+ public void init (PuzzleConfig config)
+ {
+ _config = config;
+ _bview.init(config);
+ }
+
+ /**
+ * Sets whether this panel receives events periodically from a robot
+ * player.
+ */
+ public void setRobotPlayer (boolean isrobot)
+ {
+ // create a robot player if necessary
+ if (_robot == null) {
+ _robot = new RobotPlayer(this, _xlate);
+ }
+ setPuzzleGrabsKeys(!isrobot);
+ _robot.setRobotDelay(200L);
+ _robot.setActive(isrobot);
+ }
+
+ /**
+ * Sets whether the puzzle grabs keys or if they should go to the chat
+ * window.
+ */
+ public void setPuzzleGrabsKeys (boolean puzgrabs)
+ {
+ // enable or disable the key manager appropriately
+ _ctx.getKeyboardManager().setEnabled(puzgrabs);
+ if (puzgrabs) {
+ getBoardView().requestFocus();
+ }
+ }
+
+ /**
+ * Called by the controller when the action starts.
+ */
+ public void startAction ()
+ {
+ // make the first player a robot player
+ if (ROBOT_TEST && _controller.getPlayerIndex() == 0) {
+ setRobotPlayer(true);
+ }
+ }
+
+ /**
+ * Called by the controller when the action stops.
+ */
+ public void clearAction ()
+ {
+ // deactivate the robot player
+ if (ROBOT_TEST && _controller.getPlayerIndex() == 0) {
+ setRobotPlayer(false);
+ }
+ }
+
+ /**
+ * Creates the puzzle board view that will be used to display the main
+ * puzzle interface. This is called when the puzzle panel is
+ * constructed. The derived panel will still be responsible for adding
+ * the board to the interface hierarchy.
+ */
+ protected abstract PuzzleBoardView createBoardView (PuzzleContext ctx);
+
+ /**
+ * Creates the main panel used to display the puzzle and its various
+ * in-game accoutrements (next block views, player status displays,
+ * etc.) This is called when the puzzle panel is constructed. The
+ * derived panel is responsible for making sure that the board view is
+ * present in the board panel.
+ */
+ protected abstract JPanel createBoardPanel ();
+
+ /**
+ * Returns a key translator with the desired key to controller command
+ * mappings desired for this puzzle.
+ */
+ protected abstract KeyTranslator getKeyTranslator ();
+
+ // documentation inherited from interface
+ public void willEnterPlace (PlaceObject plobj)
+ {
+ }
+
+ // documentation inherited from interface
+ public void didLeavePlace (PlaceObject plobj)
+ {
+ // disable the keyboard manager when we leave
+ _ctx.getKeyboardManager().reset();
+ }
+
+ /**
+ * Returns a reference to the {@link PuzzleBoardView} in use.
+ */
+ public PuzzleBoardView getBoardView ()
+ {
+ return _bview;
+ }
+
+ // documentation inherited
+ public Controller getController ()
+ {
+ return _controller;
+ }
+
+ /** Our puzzle context. */
+ protected PuzzleContext _ctx;
+
+ /** The board view on which the primary puzzle interface is displayed. */
+ protected PuzzleBoardView _bview;
+
+ /** The board panel displayed while the puzzle is in progress. */
+ protected JPanel _bpanel;
+
+ /** The board overlay panel displayed as requested. */
+ protected JPanel _opanel;
+
+ /** The puzzle config. */
+ protected PuzzleConfig _config;
+
+ /** The robot player. */
+ protected RobotPlayer _robot;
+
+ /** Our key translations. */
+ protected KeyTranslator _xlate;
+
+ /** The puzzle game controller. */
+ protected PuzzleController _controller;
+}
diff --git a/src/java/com/threerings/puzzle/client/PuzzleService.java b/src/java/com/threerings/puzzle/client/PuzzleService.java
new file mode 100644
index 000000000..a388954d3
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/PuzzleService.java
@@ -0,0 +1,52 @@
+//
+// $Id: PuzzleService.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService;
+
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.puzzle.data.SolitairePuzzleConfig;
+
+/**
+ * The puzzle services provide a mechanism by which the client can enter
+ * and leave puzzles. These services should not be used directly, but
+ * instead should be accessed via the {@link PuzzleDirector}.
+ */
+public interface PuzzleService extends InvocationService
+{
+ /** Used to communicate responses to {@link #enterPuzzle} requests. */
+ public static interface EnterPuzzleListener extends InvocationListener
+ {
+ /**
+ * Indicates that a {@link #enterPuzzle} request was successful
+ * and provides the place config for the puzzle room.
+ */
+ public void puzzleEntered (PlaceConfig config);
+ }
+
+ /**
+ * Requests that this client start up the specified single-player
+ * puzzle.
+ */
+ public void startPuzzle (Client client, SolitairePuzzleConfig config,
+ InvocationListener listener);
+
+ /**
+ * Requests that this client enter the specified puzzle.
+ */
+ public void enterPuzzle (
+ Client client, int puzzleOid, EnterPuzzleListener listener);
+
+ /**
+ * Requests that this client depart whatever puzzle they occupy.
+ */
+ public void leavePuzzle (Client client);
+
+ /**
+ * Requests that the difficulty level of the puzzle this client is
+ * currently occupying be changed to the specified difficulty level.
+ */
+ public void changeDifficulty (Client client, int level);
+}
diff --git a/src/java/com/threerings/puzzle/client/ScoreAnimation.java b/src/java/com/threerings/puzzle/client/ScoreAnimation.java
new file mode 100644
index 000000000..b209a3c83
--- /dev/null
+++ b/src/java/com/threerings/puzzle/client/ScoreAnimation.java
@@ -0,0 +1,226 @@
+//
+// $Id: ScoreAnimation.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.client;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+
+import com.samskivert.swing.Label;
+
+import com.threerings.media.animation.Animation;
+
+import com.threerings.puzzle.Log;
+
+public class ScoreAnimation extends Animation
+{
+ /**
+ * Constructs a score animation for the given score value centered at
+ * the given coordinates.
+ */
+ public ScoreAnimation (Label label, int x, int y)
+ {
+ this(label, x, y, DEFAULT_FLOAT_PERIOD);
+ }
+
+ /**
+ * Constructs a score animation for the given score value centered at
+ * the given coordinates. The animation will float up the screen for
+ * 30 pixels.
+ */
+ public ScoreAnimation (Label label, int x, int y, long floatPeriod)
+ {
+ this(label, x, y, x, y - DELTA_Y, floatPeriod);
+ }
+
+ /**
+ * Constructs a score animation for the given score value starting at
+ * the given coordinates and floating toward the specified
+ * coordinates.
+ */
+ public ScoreAnimation (Label label, int sx, int sy,
+ int destx, int desty, long floatPeriod)
+ {
+ super(new Rectangle(sx, sy, label.getSize().width,
+ label.getSize().height));
+
+ // save things off
+ _label = label;
+ _startX = _x = sx;
+ _startY = _y = sy;
+ _destx = destx;
+ _desty = desty;
+ _floatPeriod = floatPeriod;
+
+ // calculate our deltas
+ _dx = (destx - sx);
+ _dy = (desty - sy);
+
+ // initialize our starting alpha
+ _alpha = 1.0f;
+ }
+
+ /**
+ * Called to change the direction 180 degrees.
+ */
+ public void flipDirection ()
+ {
+ _dx = -_dx;
+ _dy = -_dy;
+ }
+
+ /**
+ * Returns the label used to render this score animation.
+ */
+ public Label getLabel ()
+ {
+ return _label;
+ }
+
+ // documentation inherited
+ public void setLocation (int x, int y)
+ {
+ super.setLocation(x, y);
+
+ // update our destination coordinates
+ _destx += (x - _startX);
+ _desty += (y - _startY);
+
+ // update our initial coordinates
+ _startX = _x = x;
+ _startY = _y = y;
+
+ // recalculate our deltas
+ _dx = (_destx - x);
+ _dy = (_desty - y);
+ }
+
+ /**
+ * Sets the duration of this score animation to the specified time in
+ * milliseconds. This should be called before the animation is added
+ * to the animation manager.
+ */
+ public void setFloatPeriod (long floatPeriod)
+ {
+ _floatPeriod = floatPeriod;
+ }
+
+ // documentation inherited
+ public void tick (long timestamp)
+ {
+ boolean invalid = false;
+ if (_start == 0) {
+ // initialize our starting time
+ _start = timestamp;
+ // we need to make sure to invalidate ourselves initially
+ invalid = true;
+ }
+
+ long fadeDelay = _floatPeriod/2;
+ long fadePeriod = _floatPeriod - fadeDelay;
+
+ // figure out the current alpha
+ long msecs = timestamp - _start;
+ float oalpha = _alpha;
+ if (msecs > fadeDelay) {
+ long rmsecs = msecs - fadeDelay;
+ _alpha = Math.max(1.0f - (rmsecs / (float)fadePeriod), 0.0f);
+ _alpha = Math.min(_alpha, 1.0f);
+ }
+
+ // get the alpha composite
+ _comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
+
+ // determine the new y-position of the score
+ float pctdone = (float)msecs / _floatPeriod;
+ int ox = _x, oy = _y;
+ _x = _startX + (int)(_dx * pctdone);
+ _y = _startY + (int)(_dy * pctdone);
+
+ // only update our location and dirty ourselves if we actually
+ // moved or our alpha changed
+ if (ox != _x || oy != _y) {
+ // dirty our old location
+ invalidate();
+ _bounds.setLocation(_x, _y);
+ invalid = true;
+
+ } else if (oalpha != _alpha) {
+ invalid = true;
+ }
+
+ if (invalid) {
+ // dirty our current location
+ invalidate();
+ }
+
+ // note whether we're done
+ _finished = (msecs >= _floatPeriod);
+ }
+
+ // documentation inherited
+ public void fastForward (long timeDelta)
+ {
+ if (_start > 0) {
+ _start += timeDelta;
+ }
+ }
+
+ // documentation inherited
+ public void paint (Graphics2D gfx)
+ {
+ Composite ocomp = gfx.getComposite();
+ if (_comp != null) {
+ gfx.setComposite(_comp);
+ }
+ _label.render(gfx, _x, _y);
+ gfx.setComposite(ocomp);
+ }
+
+ // documentation inherited
+ protected void toString (StringBuffer buf)
+ {
+ super.toString(buf);
+
+ buf.append(", x=").append(_x);
+ buf.append(", y=").append(_y);
+ buf.append(", alpha=").append(_alpha);
+ }
+
+ /** The starting animation time. */
+ protected long _start;
+
+ /** The label we're animating. */
+ protected Label _label;
+
+ /** The starting coordinates of the score. */
+ protected int _startX, _startY;
+
+ /** The current coordinates of the score. */
+ protected int _x, _y;
+
+ /** The destination coordinates towards which the animation travels. */
+ protected int _destx, _desty;
+
+ /** The distance to be traveled by the score animation. */
+ protected int _dx, _dy;
+
+ /** The duration for which we float up the screen. */
+ protected long _floatPeriod;
+
+ /** The current alpha level used to render the score. */
+ protected float _alpha;
+
+ /** The composite used to render the score. */
+ protected Composite _comp;
+
+ /** The time in milliseconds during which the score is visible. */
+ protected static final long DEFAULT_FLOAT_PERIOD = 1500L;
+
+ /** The total vertical distance the score travels. */
+ protected static final int DELTA_Y = 30;
+}
diff --git a/src/java/com/threerings/puzzle/data/Board.java b/src/java/com/threerings/puzzle/data/Board.java
new file mode 100644
index 000000000..ae7220494
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/Board.java
@@ -0,0 +1,187 @@
+//
+// $Id: Board.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import java.util.Random;
+
+import com.threerings.io.Streamable;
+
+/**
+ * An abstract base class for generating and storing puzzle board data.
+ */
+public abstract class Board
+ implements Cloneable, Streamable
+{
+ /**
+ * Outputs a string representation of the board contents.
+ */
+ public abstract void dump ();
+
+ /**
+ * Outputs a string representation of the board contents, interlaced
+ * with the supplied comparison board.
+ */
+ public abstract void dumpAndCompare (Board other);
+
+ /**
+ * Returns whether this board is equal to the given comparison board.
+ */
+ public abstract boolean equals (Board other);
+
+ // documentation inherited
+ public Object clone ()
+ {
+ try {
+ Board board = (Board)super.clone();
+ board._rando = (BoardRandom)_rando.clone();
+ return board;
+ } catch (CloneNotSupportedException cnse) {
+ throw new RuntimeException("All is wrong with universe.");
+ }
+ }
+
+ /**
+ * Sets the seed in the board's random number generator and calls
+ * {@link #populate}.
+ */
+ public void initializeSeed (long seed)
+ {
+ _rando = new BoardRandom(seed);
+ populate();
+ }
+
+ /**
+ * Returns the random number generator used by the board to generate
+ * random numbers for our puzzles.
+ */
+ public Random getRandom ()
+ {
+ return _rando;
+ }
+
+ /**
+ * Called after the seed is set in the board to give derived classes a
+ * chance to do things like populating the board with random pieces.
+ */
+ protected void populate ()
+ {
+ }
+
+ /** Used to generate random numbers. */
+ protected static class BoardRandom extends Random
+ implements Cloneable
+ {
+ public BoardRandom (long seed)
+ {
+ super(0L);
+ setSeed(seed);
+ }
+
+ // documentation inherited
+ public synchronized void setSeed (long seed)
+ {
+ _seed = (seed ^ multiplier) & mask;
+ }
+
+ // documentation inherited
+ synchronized protected int next (int bits)
+ {
+ long nextseed = (_seed * multiplier + addend) & mask;
+ _seed = nextseed;
+ return (int)(nextseed >>> (48 - bits));
+ }
+
+ // documentation inherited
+ public void nextBytes (byte[] bytes)
+ {
+ unimplemented();
+ }
+
+ // not overridden: (They seemed innocent enough)
+ // nextInt()
+ // nextLong()
+ // nextBoolean()
+ // nextFloat()
+
+ // documentation inherited
+ public int nextInt (int n)
+ {
+ if (n <= 0) {
+ throw new IllegalArgumentException("n must be positive");
+ }
+
+ if ((n & -n) == n) { // i.e., n is a power of 2
+ return (int)((n * (long)next(31)) >> 31);
+ }
+
+ int bits, val;
+ do {
+ bits = next(31);
+ val = bits % n;
+ } while (bits - val + (n-1) < 0);
+ return val;
+ }
+
+ // documentation inherited
+ public double nextDouble ()
+ {
+ long l = ((long)(next(26)) << 27) + next(27);
+ return l / (double)(1L << 53);
+ }
+
+ // documentation inherited
+ public synchronized double nextGaussian ()
+ {
+ if (_haveNextNextGaussian) {
+ _haveNextNextGaussian = false;
+ return _nextNextGaussian;
+
+ } else {
+ double v1, v2, s;
+ do {
+ v1 = 2 * nextDouble() - 1;
+ v2 = 2 * nextDouble() - 1;
+ s = v1 * v1 + v2 * v2;
+ } while (s >= 1 || s == 0);
+ double multiplier = Math.sqrt(-2 * Math.log(s)/s);
+ _nextNextGaussian = v2 * multiplier;
+ _haveNextNextGaussian = true;
+ return v1 * multiplier;
+ }
+ }
+
+ // documentation inherited
+ public Object clone ()
+ {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException cnse) {
+ throw new RuntimeException("All is wrong with universe.");
+ }
+ }
+
+ /**
+ * I suppose I could copy all the methods from Random, then we
+ * wouldn't need this..
+ */
+ private final void unimplemented ()
+ {
+ throw new RuntimeException(
+ "The Random method you attempted to call " +
+ "has not been implemented by BoardRandom.");
+ }
+
+ /** The internal state related to generating random numbers. */
+ protected long _seed;
+ protected double _nextNextGaussian;
+ protected boolean _haveNextNextGaussian = false;
+
+ private final static long multiplier = 0x5DEECE66DL;
+ private final static long addend = 0xBL;
+ private final static long mask = (1L << 48) - 1;
+ }
+
+ /** The object we use to generate our random numbers. */
+ protected transient BoardRandom _rando;
+}
diff --git a/src/java/com/threerings/puzzle/data/BoardSummary.java b/src/java/com/threerings/puzzle/data/BoardSummary.java
new file mode 100644
index 000000000..c9e9ff66d
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/BoardSummary.java
@@ -0,0 +1,57 @@
+//
+// $Id: BoardSummary.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.io.SimpleStreamableObject;
+
+/**
+ * Provides summarized data representing a player's board in a puzzle
+ * game. Board summaries are maintained by the puzzle server and are
+ * periodically sent to the clients to give them a view into how well
+ * their opponent(s) are doing. The data required to marshal a board
+ * summary object should be notably smaller in size than what would be
+ * required to marshal the entire associated {@link Board}.
+ *
+ *
Note all non-transient members of this and derived classes will
+ * automatically be serialized when the summary is sent over the wire.
+ */
+public abstract class BoardSummary extends SimpleStreamableObject
+{
+ /**
+ * Constructs an empty board summary for use when un-serializing.
+ */
+ public BoardSummary ()
+ {
+ // nothing for now
+ }
+
+ /**
+ * Constructs a board summary that retrieves full board information
+ * from the supplied board when summarizing.
+ */
+ public BoardSummary (Board board)
+ {
+ setBoard(board);
+ }
+
+ /**
+ * Sets the board associated with this board summary.
+ */
+ public void setBoard (Board board)
+ {
+ _board = board;
+ }
+
+ /**
+ * Called by the {@link
+ * com.threerings.puzzle.server.PuzzleManager} to refresh the
+ * board summary information by studying the associated board
+ * contents.
+ */
+ public abstract void summarize ();
+
+ /** The board that we're summarizing. This is only valid on the
+ * server, and on the client only for the actual player's board. */
+ protected transient Board _board;
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleCodes.java b/src/java/com/threerings/puzzle/data/PuzzleCodes.java
new file mode 100644
index 000000000..1172f8889
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleCodes.java
@@ -0,0 +1,55 @@
+//
+// $Id: PuzzleCodes.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.presents.data.InvocationCodes;
+import com.threerings.util.MessageBundle;
+
+/**
+ * Constants relating to the puzzle services.
+ */
+public interface PuzzleCodes extends InvocationCodes
+{
+ /** The message bundle identifier for general puzzle messages. */
+ public static final String PUZZLE_MESSAGE_BUNDLE = "puzzle.general";
+
+ /** The ID for puzzle chat. */
+ public static final String PUZZLE_CHAT_TYPE = "puzzleChat";
+
+ /** The default puzzle difficulty level. */
+ public static final int DEFAULT_DIFFICULTY = 2;
+
+ /** The name of the message event to a placeObject that a player
+ * was knocked out of a puzzle. */
+ public static final String PLAYER_KNOCKED_OUT = "playerKnocked";
+
+ /** The name of the message event to a placeObject that reports
+ * the winners and losers of a game. */
+ public static final String WINNERS_AND_LOSERS = "winnersAndLosers";
+
+ /** Enable this flag to have the client send a copy of its board state
+ * with every event to the server for checking. */
+ public static final boolean SYNC_BOARD_STATE = false;
+
+ /** An error code indicating that a place identified by a particular
+ * place id does not exist. Usually generated by a failed enterPuzzle
+ * request. */
+ public static final String NO_SUCH_PUZZLE = MessageBundle.qualify(
+ PUZZLE_MESSAGE_BUNDLE, "m.no_such_puzzle");
+
+ /** An error code sent when a user requests to enter a new puzzle but
+ * they are in the middle of moving somewhere already. */
+ public static final String ENTER_IN_PROGRESS = "m.enter_in_progress";
+
+ /** An error code sent when a user requests to enter a puzzle, but
+ * they are already in it. */
+ public static final String ALREADY_IN_PUZZLE = "m.already_in_puzzle";
+
+ /** Whether to enable debug logging and assertions for puzzles. Note
+ * that enabling this may result in the server or client exiting
+ * unexpectedly if certain error conditions arise in order to
+ * facilitate debugging, and so this should never be enabled in any
+ * environment even remotely resembling production. */
+ public static final boolean DEBUG_PUZZLE = false;
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleConfig.java b/src/java/com/threerings/puzzle/data/PuzzleConfig.java
new file mode 100644
index 000000000..458b909c2
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleConfig.java
@@ -0,0 +1,66 @@
+//
+// $Id: PuzzleConfig.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.parlor.game.GameConfig;
+
+/**
+ * Encapsulates the basic configuration information for a puzzle game.
+ */
+public abstract class PuzzleConfig extends GameConfig
+ implements Cloneable
+{
+ /**
+ * Returns a translatable label describing this puzzle.
+ */
+ public abstract String getPuzzleName ();
+
+ /**
+ * Returns the puzzle rating type.
+ */
+ public abstract byte getRatingTypeId ();
+
+ /**
+ * Constructs a blank puzzle config.
+ */
+ public PuzzleConfig ()
+ {
+ }
+
+ /**
+ * If this method returns true, a copy of the client board will be
+ * sent with every puzzle event so that the server can compare them
+ * step-by-step to debug out of sync problems.
+ */
+ public boolean syncBoardState ()
+ {
+ return PuzzleCodes.SYNC_BOARD_STATE;
+ }
+
+ /**
+ * Returns the message bundle identifier for the bundle that should be
+ * used to translate the translatable strings used to describe the
+ * puzzle config parameters. The default implementation returns the
+ * base puzzle message bundle, but puzzles that have their own message
+ * bundle should override this method and return their puzzle-specific
+ * bundle identifier.
+ */
+ public String getBundleName ()
+ {
+ return PuzzleCodes.PUZZLE_MESSAGE_BUNDLE;
+ }
+
+ /**
+ * Returns a clone of this puzzle config.
+ */
+ public PuzzleConfig getClone ()
+ {
+ try {
+ return (PuzzleConfig)clone();
+ } catch (CloneNotSupportedException cnse) {
+ // something has gone horribly awry
+ return null;
+ }
+ }
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java b/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java
new file mode 100644
index 000000000..d3b235629
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java
@@ -0,0 +1,15 @@
+//
+// $Id: PuzzleGameCodes.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+/**
+ * Contains codes used by the puzzle game client implementations. This
+ * differs from {@link PuzzleCodes} as that is related to the puzzle
+ * services which span the client and the server.
+ */
+public interface PuzzleGameCodes
+{
+ /** Enable this flag to test the game with a robot player. */
+ public static final boolean ROBOT_TEST = false;
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java b/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java
new file mode 100644
index 000000000..eead1b30a
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java
@@ -0,0 +1,44 @@
+//
+// $Id: PuzzleGameMarshaller.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.dobj.InvocationResponseEvent;
+import com.threerings.puzzle.client.PuzzleGameService;
+import com.threerings.puzzle.data.Board;
+
+/**
+ * Provides the implementation of the {@link PuzzleGameService} interface
+ * that marshalls the arguments and delivers the request to the provider
+ * on the server. Also provides an implementation of the response listener
+ * interfaces that marshall the response arguments and deliver them back
+ * to the requesting client.
+ */
+public class PuzzleGameMarshaller extends InvocationMarshaller
+ implements PuzzleGameService
+{
+ /** The method id used to dispatch {@link #updateProgress} requests. */
+ public static final int UPDATE_PROGRESS = 1;
+
+ // documentation inherited from interface
+ public void updateProgress (Client arg1, int arg2, int[] arg3)
+ {
+ sendRequest(arg1, UPDATE_PROGRESS, new Object[] {
+ new Integer(arg2), arg3
+ });
+ }
+
+ /** The method id used to dispatch {@link #updateProgressSync} requests. */
+ public static final int UPDATE_PROGRESS_SYNC = 2;
+
+ // documentation inherited from interface
+ public void updateProgressSync (Client arg1, int arg2, int[] arg3, Board[] arg4)
+ {
+ sendRequest(arg1, UPDATE_PROGRESS_SYNC, new Object[] {
+ new Integer(arg2), arg3, arg4
+ });
+ }
+
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleMarshaller.java b/src/java/com/threerings/puzzle/data/PuzzleMarshaller.java
new file mode 100644
index 000000000..08979e510
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleMarshaller.java
@@ -0,0 +1,104 @@
+//
+// $Id: PuzzleMarshaller.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService.InvocationListener;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.dobj.InvocationResponseEvent;
+import com.threerings.puzzle.client.PuzzleService;
+import com.threerings.puzzle.client.PuzzleService.EnterPuzzleListener;
+import com.threerings.puzzle.data.SolitairePuzzleConfig;
+
+/**
+ * Provides the implementation of the {@link PuzzleService} interface
+ * that marshalls the arguments and delivers the request to the provider
+ * on the server. Also provides an implementation of the response listener
+ * interfaces that marshall the response arguments and deliver them back
+ * to the requesting client.
+ */
+public class PuzzleMarshaller extends InvocationMarshaller
+ implements PuzzleService
+{
+ // documentation inherited
+ public static class EnterPuzzleMarshaller extends ListenerMarshaller
+ implements EnterPuzzleListener
+ {
+ /** The method id used to dispatch {@link #puzzleEntered}
+ * responses. */
+ public static final int PUZZLE_ENTERED = 1;
+
+ // documentation inherited from interface
+ public void puzzleEntered (PlaceConfig arg1)
+ {
+ omgr.postEvent(new InvocationResponseEvent(
+ callerOid, requestId, PUZZLE_ENTERED,
+ new Object[] { arg1 }));
+ }
+
+ // documentation inherited
+ public void dispatchResponse (int methodId, Object[] args)
+ {
+ switch (methodId) {
+ case PUZZLE_ENTERED:
+ ((EnterPuzzleListener)listener).puzzleEntered(
+ (PlaceConfig)args[0]);
+ return;
+
+ default:
+ super.dispatchResponse(methodId, args);
+ }
+ }
+ }
+
+ /** The method id used to dispatch {@link #startPuzzle} requests. */
+ public static final int START_PUZZLE = 1;
+
+ // documentation inherited from interface
+ public void startPuzzle (Client arg1, SolitairePuzzleConfig arg2, InvocationListener arg3)
+ {
+ ListenerMarshaller listener3 = new ListenerMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, START_PUZZLE, new Object[] {
+ arg2, listener3
+ });
+ }
+
+ /** The method id used to dispatch {@link #enterPuzzle} requests. */
+ public static final int ENTER_PUZZLE = 2;
+
+ // documentation inherited from interface
+ public void enterPuzzle (Client arg1, int arg2, EnterPuzzleListener arg3)
+ {
+ EnterPuzzleMarshaller listener3 = new EnterPuzzleMarshaller();
+ listener3.listener = arg3;
+ sendRequest(arg1, ENTER_PUZZLE, new Object[] {
+ new Integer(arg2), listener3
+ });
+ }
+
+ /** The method id used to dispatch {@link #leavePuzzle} requests. */
+ public static final int LEAVE_PUZZLE = 3;
+
+ // documentation inherited from interface
+ public void leavePuzzle (Client arg1)
+ {
+ sendRequest(arg1, LEAVE_PUZZLE, new Object[] {
+
+ });
+ }
+
+ /** The method id used to dispatch {@link #changeDifficulty} requests. */
+ public static final int CHANGE_DIFFICULTY = 4;
+
+ // documentation inherited from interface
+ public void changeDifficulty (Client arg1, int arg2)
+ {
+ sendRequest(arg1, CHANGE_DIFFICULTY, new Object[] {
+ new Integer(arg2)
+ });
+ }
+
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleObject.dobj b/src/java/com/threerings/puzzle/data/PuzzleObject.dobj
new file mode 100644
index 000000000..33b558b36
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleObject.dobj
@@ -0,0 +1,74 @@
+//
+// $Id: PuzzleObject.dobj,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.parlor.game.GameObject;
+
+/**
+ * Extends the basic {@link GameObject} to add individual player
+ * status. Puzzle games typically contain numerous players that may be
+ * knocked out of the game while the overall game continues on, thereby
+ * necessitating this second level of game status.
+ */
+public class PuzzleObject extends GameObject
+ implements PuzzleCodes
+{
+ /** The player status constant for a player whose game is in play. */
+ public static final int PLAYER_IN_PLAY = 0;
+
+ /** The player status constant for a player whose has been knocked out
+ * of the game. */
+ public static final int PLAYER_KNOCKED_OUT = 1;
+
+ /** Provides general puzzle game invocation services. */
+ public PuzzleGameMarshaller puzzleGameService;
+
+ /** The puzzle difficulty level. */
+ public int difficulty = DEFAULT_DIFFICULTY;
+
+ /** The status of each of the players in the game. The status value
+ * is one of {@link #PLAYER_KNOCKED_OUT} or {@link
+ * #PLAYER_IN_PLAY}. */
+ public int[] playerStatus;
+
+ /** Summaries of the boards of all players in this puzzle (may be null
+ * if the puzzle doesn't support individual player boards). */
+ public BoardSummary[] summaries;
+
+ /** The seed used to germinate the boards. */
+ public long seed;
+
+ // documentation inherited
+ public boolean shouldBroadcast ()
+ {
+ // we do not broadcast to puzzles because the users will get it
+ // on their scene objects
+ return false;
+ }
+
+ /**
+ * Returns the number of active players in the game.
+ */
+ public int getActivePlayerCount ()
+ {
+ int count = 0;
+ int size = players.length;
+ for (int ii = 0; ii < size; ii++) {
+ if (isActivePlayer(ii)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns whether the given player is still an active player, e.g.,
+ * their game has not ended.
+ */
+ public boolean isActivePlayer (int pidx)
+ {
+ return (isOccupiedPlayer(pidx) &&
+ playerStatus != null && playerStatus[pidx] == PLAYER_IN_PLAY);
+ }
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzleObject.java b/src/java/com/threerings/puzzle/data/PuzzleObject.java
new file mode 100644
index 000000000..5bb68a036
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzleObject.java
@@ -0,0 +1,187 @@
+//
+// $Id: PuzzleObject.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.parlor.game.GameObject;
+
+/**
+ * Extends the basic {@link GameObject} to add individual player
+ * status. Puzzle games typically contain numerous players that may be
+ * knocked out of the game while the overall game continues on, thereby
+ * necessitating this second level of game status.
+ */
+public class PuzzleObject extends GameObject
+ implements PuzzleCodes
+{
+ /** The field name of the puzzleGameService field. */
+ public static final String PUZZLE_GAME_SERVICE = "puzzleGameService";
+
+ /** The field name of the difficulty field. */
+ public static final String DIFFICULTY = "difficulty";
+
+ /** The field name of the playerStatus field. */
+ public static final String PLAYER_STATUS = "playerStatus";
+
+ /** The field name of the summaries field. */
+ public static final String SUMMARIES = "summaries";
+
+ /** The field name of the seed field. */
+ public static final String SEED = "seed";
+
+ /** The player status constant for a player whose game is in play. */
+ public static final int PLAYER_IN_PLAY = 0;
+
+ /** The player status constant for a player whose has been knocked out
+ * of the game. */
+ public static final int PLAYER_KNOCKED_OUT = 1;
+
+ /** Provides general puzzle game invocation services. */
+ public PuzzleGameMarshaller puzzleGameService;
+
+ /** The puzzle difficulty level. */
+ public int difficulty = DEFAULT_DIFFICULTY;
+
+ /** The status of each of the players in the game. The status value
+ * is one of {@link #PLAYER_KNOCKED_OUT} or {@link
+ * #PLAYER_IN_PLAY}. */
+ public int[] playerStatus;
+
+ /** Summaries of the boards of all players in this puzzle (may be null
+ * if the puzzle doesn't support individual player boards). */
+ public BoardSummary[] summaries;
+
+ /** The seed used to germinate the boards. */
+ public long seed;
+
+ // documentation inherited
+ public boolean shouldBroadcast ()
+ {
+ // we do not broadcast to puzzles because the users will get it
+ // on their scene objects
+ return false;
+ }
+
+ /**
+ * Returns the number of active players in the game.
+ */
+ public int getActivePlayerCount ()
+ {
+ int count = 0;
+ int size = players.length;
+ for (int ii = 0; ii < size; ii++) {
+ if (isActivePlayer(ii)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns whether the given player is still an active player, e.g.,
+ * their game has not ended.
+ */
+ public boolean isActivePlayer (int pidx)
+ {
+ return (isOccupiedPlayer(pidx) &&
+ playerStatus != null && playerStatus[pidx] == PLAYER_IN_PLAY);
+ }
+
+ /**
+ * Requests that the puzzleGameService field be set to the specified
+ * value. The local value will be updated immediately and an event
+ * will be propagated through the system to notify all listeners that
+ * the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setPuzzleGameService (PuzzleGameMarshaller puzzleGameService)
+ {
+ requestAttributeChange(PUZZLE_GAME_SERVICE, puzzleGameService);
+ this.puzzleGameService = puzzleGameService;
+ }
+
+ /**
+ * Requests that the difficulty field be set to the specified
+ * value. The local value will be updated immediately and an event
+ * will be propagated through the system to notify all listeners that
+ * the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setDifficulty (int difficulty)
+ {
+ requestAttributeChange(DIFFICULTY, new Integer(difficulty));
+ this.difficulty = difficulty;
+ }
+
+ /**
+ * Requests that the playerStatus field be set to the specified
+ * value. The local value will be updated immediately and an event
+ * will be propagated through the system to notify all listeners that
+ * the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setPlayerStatus (int[] playerStatus)
+ {
+ requestAttributeChange(PLAYER_STATUS, playerStatus);
+ this.playerStatus = playerStatus;
+ }
+
+ /**
+ * Requests that the indexth element of
+ * playerStatus field be set to the specified value. The local
+ * value will be updated immediately and an event will be propagated
+ * through the system to notify all listeners that the attribute did
+ * change. Proxied copies of this object (on clients) will apply the
+ * value change when they received the attribute changed notification.
+ */
+ public void setPlayerStatusAt (int value, int index)
+ {
+ requestElementUpdate(PLAYER_STATUS, new Integer(value), index);
+ this.playerStatus[index] = value;
+ }
+
+ /**
+ * Requests that the summaries field be set to the specified
+ * value. The local value will be updated immediately and an event
+ * will be propagated through the system to notify all listeners that
+ * the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setSummaries (BoardSummary[] summaries)
+ {
+ requestAttributeChange(SUMMARIES, summaries);
+ this.summaries = summaries;
+ }
+
+ /**
+ * Requests that the indexth element of
+ * summaries field be set to the specified value. The local
+ * value will be updated immediately and an event will be propagated
+ * through the system to notify all listeners that the attribute did
+ * change. Proxied copies of this object (on clients) will apply the
+ * value change when they received the attribute changed notification.
+ */
+ public void setSummariesAt (BoardSummary value, int index)
+ {
+ requestElementUpdate(SUMMARIES, value, index);
+ this.summaries[index] = value;
+ }
+
+ /**
+ * Requests that the seed field be set to the specified
+ * value. The local value will be updated immediately and an event
+ * will be propagated through the system to notify all listeners that
+ * the attribute did change. Proxied copies of this object (on
+ * clients) will apply the value change when they received the
+ * attribute changed notification.
+ */
+ public void setSeed (long seed)
+ {
+ requestAttributeChange(SEED, new Long(seed));
+ this.seed = seed;
+ }
+}
diff --git a/src/java/com/threerings/puzzle/data/PuzzlerObject.java b/src/java/com/threerings/puzzle/data/PuzzlerObject.java
new file mode 100644
index 000000000..52e09c979
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/PuzzlerObject.java
@@ -0,0 +1,25 @@
+//
+// $Id: PuzzlerObject.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+import com.threerings.crowd.data.BodyObject;
+
+/**
+ * An interface that must be implemented by {@link BodyObject} derivations
+ * that wish to be usable with the puzzle services.
+ */
+public interface PuzzlerObject
+{
+ /**
+ * Returns this puzzler's "puzzle location which is the oid of the
+ * puzzle game object. Should return -1 until some value is set via
+ * {@link #setPuzzleLoc}.
+ */
+ public int getPuzzleLoc ();
+
+ /**
+ * Sets this puzzler's "puzzle location".
+ */
+ public void setPuzzleLoc (int puzzleLoc);
+}
diff --git a/src/java/com/threerings/puzzle/data/SolitairePuzzleConfig.java b/src/java/com/threerings/puzzle/data/SolitairePuzzleConfig.java
new file mode 100644
index 000000000..f763caf39
--- /dev/null
+++ b/src/java/com/threerings/puzzle/data/SolitairePuzzleConfig.java
@@ -0,0 +1,11 @@
+//
+// $Id: SolitairePuzzleConfig.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.data;
+
+/**
+ * Restricts regular puzzle configuration for single player puzzles.
+ */
+public abstract class SolitairePuzzleConfig extends PuzzleConfig
+{
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java b/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java
new file mode 100644
index 000000000..480dec68f
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java
@@ -0,0 +1,237 @@
+//
+// $Id: DropBlockSprite.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import java.awt.Rectangle;
+
+import com.threerings.puzzle.Log;
+
+/**
+ * The drop block sprite represents a block of multiple pieces that can be
+ * rotated to any of the four cardinal compass directions. As such, it
+ * may span multiple columns or rows depending on its orientation. The
+ * block has a "central" piece around which it rotates, with the other
+ * pieces referred to as "external" pieces.
+ */
+public class DropBlockSprite extends DropSprite
+{
+ /**
+ * Constructs a drop block sprite and starts it dropping.
+ *
+ * @param view the board view upon which this sprite will be displayed.
+ * @param col the column of the central piece.
+ * @param row the row of the central piece.
+ * @param orient the orientation of the sprite.
+ * @param pieces the pieces displayed by the sprite.
+ */
+ public DropBlockSprite (
+ DropBoardView view, int col, int row, int orient, int[] pieces)
+ {
+ super(view, col, row, pieces, 0);
+
+ _orient = orient;
+ }
+
+ /**
+ * Constructs a drop block sprite and starts it dropping.
+ *
+ * @param view the board view upon which this sprite will be displayed.
+ * @param col the column of the central piece.
+ * @param row the row of the central piece.
+ * @param orient the orientation of the sprite.
+ * @param pieces the pieces displayed by the sprite.
+ * @param renderOrder the rendering order of the sprite.
+ */
+ public DropBlockSprite (
+ DropBoardView view, int col, int row, int orient, int[] pieces,
+ int renderOrder)
+ {
+ super(view, col, row, pieces, 0, renderOrder);
+
+ _orient = orient;
+ }
+
+ // documentation inherited
+ protected void init ()
+ {
+ super.init();
+
+ setOrientation(_orient);
+ }
+
+ /**
+ * Returns an array of the row numbers containing the block pieces.
+ * The first index is the row of the central piece. The array is
+ * cached and re-used internally and so the caller should make their
+ * own copy if they care to modify it.
+ */
+ public int[] getRows ()
+ {
+ return _rows;
+ }
+
+ /**
+ * Returns an array of the column numbers containing the block pieces.
+ * The first index is the column of the central piece. The array is
+ * cached and re-used internally and so the caller should make their
+ * own copy if they care to modify it.
+ */
+ public int[] getColumns ()
+ {
+ return _cols;
+ }
+
+ /**
+ * Returns the bounds of the block in board piece coordinates. The
+ * bounds rectangle is cached and re-used internally and so the caller
+ * should make their own copy if they care to modify it.
+ */
+ public Rectangle getBoardBounds ()
+ {
+ return _dbounds;
+ }
+
+ /**
+ * Returns the row the external piece is located in.
+ */
+ public int getExternalRow ()
+ {
+ return _erow;
+ }
+
+ /**
+ * Returns the column the external piece is located in.
+ */
+ public int getExternalColumn ()
+ {
+ return _ecol;
+ }
+
+ // documentation inherited
+ public void setColumn (int col)
+ {
+ super.setColumn(col);
+ updateDropInfo();
+ }
+
+ // documentation inherited
+ public void setRow (int row)
+ {
+ super.setRow(row);
+ updateDropInfo();
+ }
+
+ // documentation inherited
+ public void setBoardLocation (int row, int col)
+ {
+ super.setBoardLocation(row, col);
+ updateDropInfo();
+ }
+
+ /**
+ * Updates the sprite image offset to reflect the direction in which
+ * the external piece is hanging.
+ */
+ public void setOrientation (int orient)
+ {
+ super.setOrientation(orient);
+
+ int edx = 0, edy = 0;
+ if (orient == NORTH) {
+ edy = -1;
+ } else if (orient == WEST) {
+ edx = -1;
+ }
+
+ // update the sprite image offset
+ setRowOffset(edy);
+ setColumnOffset(edx);
+
+ // update the external piece position and drop block bounds
+ updateDropInfo();
+ }
+
+ // documentation inherited
+ public void toString (StringBuffer buf)
+ {
+ super.toString(buf);
+ buf.append(", erow=").append(_erow);
+ buf.append(", ecol=").append(_ecol);
+ }
+
+ /**
+ * Re-calculates the external piece position and bounds of the drop
+ * block.
+ */
+ protected void updateDropInfo ()
+ {
+ // update the external piece location
+ _erow = calculateExternalRow();
+ _ecol = calculateExternalColumn();
+
+ // update the piece row and column arrays
+ _rows[0] = _row;
+ _rows[1] = _erow;
+ _cols[0] = _col;
+ _cols[1] = _ecol;
+
+ // calculate the drop block board bounds
+ int maxrow = Math.max(_row, _erow);
+ int mincol = Math.min(_col, _ecol);
+
+ int bpwid, bphei;
+ if (_orient == NORTH || _orient == SOUTH) {
+ bpwid = 1;
+ bphei = 2;
+ } else {
+ bpwid = 2;
+ bphei = 1;
+ }
+
+ // create the bounds rectangle if necessary
+ _dbounds.setBounds(mincol, maxrow, bpwid, bphei);
+ }
+
+ /**
+ * Returns the row the external piece is located in based on the
+ * current central piece location and sprite orientation.
+ */
+ protected int calculateExternalRow ()
+ {
+ if (_orient == NORTH) {
+ return (_row - 1);
+ } else if (_orient == SOUTH) {
+ return (_row + 1);
+ } else {
+ return _row;
+ }
+ }
+
+ /**
+ * Returns the column the external piece is located in based on the
+ * current central piece location and sprite orientation.
+ */
+ protected int calculateExternalColumn ()
+ {
+ if (_orient == WEST) {
+ return (_col - 1);
+ } else if (_orient == EAST) {
+ return (_col + 1);
+ } else {
+ return _col;
+ }
+ }
+
+ /** The drop block bounds in board coordinates. */
+ protected Rectangle _dbounds = new Rectangle();
+
+ /** The drop block piece rows. */
+ protected int[] _rows = new int[2];
+
+ /** The drop block piece columns. */
+ protected int[] _cols = new int[2];
+
+ /** The external piece location in board coordinates. */
+ protected int _ecol, _erow;
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropBoardView.java b/src/java/com/threerings/puzzle/drop/client/DropBoardView.java
new file mode 100644
index 000000000..a85df9aa6
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropBoardView.java
@@ -0,0 +1,370 @@
+//
+// $Id: DropBoardView.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Shape;
+
+import java.util.Iterator;
+
+import com.samskivert.swing.Label;
+
+import com.threerings.media.image.Mirage;
+import com.threerings.media.sprite.Sprite;
+
+import com.threerings.yohoho.client.YoUI;
+import com.threerings.puzzle.util.PuzzleContext;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.client.PuzzleBoardView;
+import com.threerings.puzzle.client.ScoreAnimation;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.PuzzleConfig;
+
+import com.threerings.puzzle.drop.data.DropBoard;
+import com.threerings.puzzle.drop.data.DropConfig;
+import com.threerings.puzzle.drop.data.DropPieceCodes;
+
+/**
+ * The drop board view displays a drop puzzle game in progress for a
+ * single player.
+ */
+public abstract class DropBoardView extends PuzzleBoardView
+ implements DropPieceCodes
+{
+ /** The color used to render normal scoring text. */
+ public static final Color SCORE_COLOR = Color.white;
+
+ /** The color used to render chain reward scoring text. */
+ public static final Color CHAIN_COLOR = Color.yellow;
+
+ /**
+ * Constructs a drop board view.
+ */
+ public DropBoardView (PuzzleContext ctx, int pwid, int phei)
+ {
+ super(ctx);
+
+ // save off piece dimensions
+ _pwid = pwid;
+ _phei = phei;
+
+ // determine distance to float score animations
+ _scoreDist = 2 * _phei;
+ }
+
+ /**
+ * Initializes the board with the board dimensions.
+ */
+ public void init (PuzzleConfig config)
+ {
+ DropConfig dconfig = (DropConfig)config;
+
+ // save off the board dimensions in pieces
+ _bwid = dconfig.getBoardWidth();
+ _bhei = dconfig.getBoardHeight();
+
+ super.init(config);
+ }
+
+ /**
+ * Returns the width in pixels of a single board piece.
+ */
+ public int getPieceWidth ()
+ {
+ return _pwid;
+ }
+
+ /**
+ * Returns the height in pixels of a single board piece.
+ */
+ public int getPieceHeight ()
+ {
+ return _phei;
+ }
+
+ /**
+ * Called by the {@link DropSprite} to populate pos with
+ * the screen coordinates in pixels at which a piece at (col,
+ * row) in the board should be drawn. Derived classes may wish
+ * to override this method to allow specialised positioning of
+ * sprites.
+ */
+ public void getPiecePosition (int col, int row, Point pos)
+ {
+ pos.setLocation(col * _pwid, (row * _phei) - _roff);
+ }
+
+ /**
+ * Called by the {@link DropSprite} to get the dimensions of the area
+ * that will be occupied by rendering a piece segment of the given
+ * orientation and length whose bottom-leftmost corner is at
+ * (col, row).
+ */
+ public Dimension getPieceSegmentSize (int col, int row, int orient, int len)
+ {
+ if (orient == NORTH || orient == SOUTH) {
+ return new Dimension(_pwid, len * _phei);
+ } else {
+ return new Dimension(len * _pwid, _phei);
+ }
+ }
+
+ /**
+ * Dirties the rectangle encompassing the piece segment with the given
+ * direction and length whose bottom-leftmost corner is at (col,
+ * row).
+ */
+ public void dirtySegment (int dir, int col, int row, int len)
+ {
+ int x = _pwid * col, y = (_phei * row) - _roff;
+ int wid = (dir == VERTICAL) ? _pwid : len * _pwid;
+ int hei = (dir == VERTICAL) ? _phei * len : _phei;
+ _remgr.invalidateRegion(x, y, wid, hei);
+ }
+
+ /**
+ * Dirties the rectangle encompassing the specified piece in the
+ * board.
+ */
+ public void dirtyPiece (int col, int row)
+ {
+ _remgr.invalidateRegion(_pwid * col, (_phei * row) - _roff,
+ _pwid, _phei);
+ }
+
+ /**
+ * Dirties a rectangular region of pieces.
+ */
+ public void dirtyPieces (int xx, int yy, int width, int height)
+ {
+ _remgr.invalidateRegion(xx*_pwid, yy*_phei, width*_pwid, height*_phei);
+ }
+
+ /**
+ * Returns the image used to display the given piece at coordinates
+ * (0, 0) with an orientation of {@link #NORTH}. This
+ * serves as a convenience routine for those puzzles that don't bother
+ * rendering their pieces differently when placed at different board
+ * coordinates or in different orientations.
+ */
+ public Mirage getPieceImage (int piece)
+ {
+ return getPieceImage(piece, 0, 0, NORTH);
+ }
+
+ /**
+ * Returns the image used to display the given piece at the specified
+ * column and row with the given orientation.
+ */
+ public abstract Mirage getPieceImage (
+ int piece, int col, int row, int orient);
+
+ // documentation inherited
+ public void setBoard (Board board)
+ {
+ // when a new board arrives, we want to remove all drop sprites
+ // so that they don't modify the new board with their old ideas
+ for (Iterator iter = _actionSprites.iterator(); iter.hasNext(); ) {
+ Sprite s = (Sprite) iter.next();
+ if (s instanceof DropSprite) {
+ // remove it from _sprites safely
+ iter.remove();
+ // but then use the standard removal method
+ removeSprite(s);
+ }
+ }
+
+ super.setBoard(board);
+
+ _dboard = (DropBoard)board;
+ }
+
+ /**
+ * Creates a new drop sprite used to animate the given pieces falling
+ * in the specified column.
+ */
+ public DropSprite createPieces (int col, int row, int[] pieces, int dist)
+ {
+ return new DropSprite(this, col, row, pieces, dist);
+ }
+
+ /**
+ * Creates and returns an animation which makes use of a label sprite
+ * that is assigned a path that floats it a short distance up the
+ * view, with the label initially centered within the view.
+ *
+ * @param score the score text to display.
+ * @param color the color of the text.
+ */
+ public ScoreAnimation createScoreAnimation (String score, Color color)
+ {
+ return createScoreAnimation(
+ score, color, MEDIUM_FONT_SIZE, 0, _bhei - 1, _bwid, _bhei);
+ }
+
+ /**
+ * Creates and returns an animation showing the specified score
+ * floating up the view, with the label initially centered within the
+ * view.
+ *
+ * @param score the score text to display.
+ * @param color the color of the text.
+ * @param fontSize the size of the text; a value between 0 and {@link
+ * #getPuzzleFontSizeCount} - 1.
+ */
+ public ScoreAnimation createScoreAnimation (
+ String score, Color color, int fontSize)
+ {
+ return createScoreAnimation(
+ score, color, fontSize, 0, _bhei - 1, _bwid, _bhei);
+ }
+
+ /**
+ * Creates and returns an animation showing the specified score
+ * floating up the view.
+ *
+ * @param score the score text to display.
+ * @param color the color of the text.
+ * @param x the left coordinate in board coordinates of the rectangle
+ * within which the score is to be centered.
+ * @param y the bottom coordinate in board coordinates of the
+ * rectangle within which the score is to be centered.
+ * @param width the width in board coordinates of the rectangle within
+ * which the score is to be centered.
+ * @param height the height in board coordinates of the rectangle
+ * within which the score is to be centered.
+ */
+ public ScoreAnimation createScoreAnimation (
+ String score, Color color, int x, int y, int width, int height)
+ {
+ return createScoreAnimation(
+ score, color, MEDIUM_FONT_SIZE, x, y, width, height);
+ }
+
+ /**
+ * Creates and returns an animation showing the specified score
+ * floating up the view.
+ *
+ * @param score the score text to display.
+ * @param color the color of the text.
+ * @param fontSize the size of the text; a value between 0 and {@link
+ * #getPuzzleFontSizeCount} - 1.
+ * @param x the left coordinate in board coordinates of the rectangle
+ * within which the score is to be centered.
+ * @param y the bottom coordinate in board coordinates of the
+ * rectangle within which the score is to be centered.
+ * @param width the width in board coordinates of the rectangle within
+ * which the score is to be centered.
+ * @param height the height in board coordinates of the rectangle
+ * within which the score is to be centered.
+ */
+ public ScoreAnimation createScoreAnimation (String score, Color color,
+ int fontSize, int x, int y,
+ int width, int height)
+ {
+ // create the score animation
+ ScoreAnimation anim =
+ createScoreAnimation(score, color, fontSize, x, y);
+
+ // position the label within the specified rectangle
+ Dimension lsize = anim.getLabel().getSize();
+ Point pos = new Point();
+ centerRectInBoardRect(
+ x, y, width, height, lsize.width, lsize.height, pos);
+ anim.setLocation(pos.x, pos.y);
+
+ return anim;
+ }
+
+ /**
+ * Populates pos with the most appropriate screen
+ * coordinates to center a rectangle of the given width and height (in
+ * pixels) within the specified rectangle (in board coordinates).
+ *
+ * @param bx the bounding rectangle's left board coordinate.
+ * @param by the bounding rectangle's bottom board coordinate.
+ * @param bwid the bounding rectangle's width in board coordinates.
+ * @param bhei the bounding rectangle's height in board coordinates.
+ * @param rwid the width of the rectangle to position in pixels.
+ * @param rhei the height of the rectangle to position in pixels.
+ * @param pos the point to populate with the rectangle's final
+ * position.
+ */
+ protected void centerRectInBoardRect (
+ int bx, int by, int bwid, int bhei, int rwid, int rhei, Point pos)
+ {
+ getPiecePosition(bx, by + 1, pos);
+ pos.x += (((bwid * _pwid) - rwid) / 2);
+ pos.y -= ((((bhei * _phei) - rhei) / 2) + rhei);
+
+ // constrain to fit wholly within the board bounds
+ pos.x = Math.max(Math.min(pos.x, _bounds.width - rwid), 0);
+ }
+
+ /**
+ * Rotates the given drop block sprite to the specified orientation,
+ * updating the image as necessary. Derived classes that make use of
+ * block dropping functionality should override this method to do the
+ * right thing.
+ */
+ public void rotateDropBlock (DropBlockSprite sprite, int orient)
+ {
+ // nothing for now
+ }
+
+ // documentation inherited
+ public void paintBetween (Graphics2D gfx, Rectangle dirtyRect)
+ {
+ gfx.translate(0, -_roff);
+ renderBoard(gfx, dirtyRect);
+ renderRisingPieces(gfx, dirtyRect);
+ gfx.translate(0, _roff);
+ }
+
+ // documentation inherited
+ public Dimension getPreferredSize ()
+ {
+ int wid = _bwid * _pwid;
+ int hei = _bhei * _phei;
+ return new Dimension(wid, hei);
+ }
+
+ /**
+ * Renders the row of rising pieces to the given graphics context.
+ * Sub-classes that make use of board rising functionality should
+ * override this method to draw the rising piece row.
+ */
+ protected void renderRisingPieces (Graphics2D gfx, Rectangle dirtyRect)
+ {
+ // nothing for now
+ }
+
+ /**
+ * Sets the board rising offset to the given y-position.
+ */
+ protected void setRiseOffset (int y)
+ {
+ if (y != _roff) {
+ _roff = y;
+ _remgr.invalidateRegion(_bounds);
+ }
+ }
+
+ /** The drop board. */
+ protected DropBoard _dboard;
+
+ /** The piece dimensions in pixels. */
+ protected int _pwid, _phei;
+
+ /** The board rising offset. */
+ protected int _roff;
+
+ /** The board dimensions in pieces. */
+ protected int _bwid, _bhei;
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java b/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java
new file mode 100644
index 000000000..ad6914a12
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java
@@ -0,0 +1,1221 @@
+//
+// $Id: DropControllerDelegate.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+
+import java.util.List;
+
+import com.samskivert.util.IntListUtil;
+import com.samskivert.util.StringUtil;
+import com.threerings.util.DirectionUtil;
+
+import com.threerings.media.FrameParticipant;
+import com.threerings.media.animation.Animation;
+import com.threerings.media.animation.AnimationAdapter;
+import com.threerings.media.sprite.Sprite;
+
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.crowd.util.CrowdContext;
+
+import com.threerings.puzzle.util.PuzzleContext;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.client.PuzzleController;
+import com.threerings.puzzle.client.PuzzleControllerDelegate;
+import com.threerings.puzzle.client.PuzzlePanel;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+
+import com.threerings.puzzle.drop.data.DropBoard;
+import com.threerings.puzzle.drop.data.DropCodes;
+import com.threerings.puzzle.drop.data.DropConfig;
+import com.threerings.puzzle.drop.data.DropLogic;
+import com.threerings.puzzle.drop.data.DropPieceCodes;
+import com.threerings.puzzle.drop.util.DropBoardUtil;
+import com.threerings.puzzle.drop.util.DropPieceProvider;
+import com.threerings.puzzle.drop.util.PieceDropLogic;
+import com.threerings.puzzle.drop.util.PieceDropper.PieceDropInfo;
+import com.threerings.puzzle.drop.util.PieceDropper;
+
+/**
+ * Games that wish to make use of the drop puzzle services will need to
+ * create an extension of this delegate class, customizing it for their
+ * particular game and then adding it via {@link
+ * PuzzleController#addDelegate}.
+ *
+ *
It handles logical actions for a puzzle game that generally + * consists of a two-dimensional board containing pieces, with new pieces + * either falling into the board as a "drop block", or rising into the + * bottom of the board in new piece rows. + * + *
Derived classes must implement {@link #getPieceVelocity} and {@link + * #evolveBoard}. + * + *
Block-dropping puzzles will likely want to override {@link + * #createNextBlock}, {@link #blockDidLand}, and {@link + * #getPieceDropLogic}. + * + *
Board-rising puzzles will likely want to override {@link
+ * #getRiseVelocity}, {@link #getRiseDistance}, {@link
+ * #getPieceDropLogic}, and {@link #boardDidRise}.
+ */
+public abstract class DropControllerDelegate extends PuzzleControllerDelegate
+ implements DropCodes, DropPieceCodes, FrameParticipant
+{
+ /** The action command for moving the block to the left. */
+ public static final String MOVE_BLOCK_LEFT = "move_block_left";
+
+ /** The action command for moving the block to the right. */
+ public static final String MOVE_BLOCK_RIGHT = "move_block_right";
+
+ /** The action command for rotating the block counter-clockwise. */
+ public static final String ROTATE_BLOCK_CCW = "rotate_block_ccw";
+
+ /** The action command for rotating the block clockwise. */
+ public static final String ROTATE_BLOCK_CW = "rotate_block_cw";
+
+ /** The action command for starting to dropping the block. */
+ public static final String START_DROP_BLOCK = "start_drop_block";
+
+ /** The action command for ending dropping the block. */
+ public static final String END_DROP_BLOCK = "end_drop_block";
+
+ /** The action command for raising the next rising row. */
+ public static final String RAISE_ROW = "raise_row";
+
+ /**
+ * Creates a delegate with the specified drop game logic and
+ * controller.
+ */
+ public DropControllerDelegate (PuzzleController ctrl, DropLogic logic)
+ {
+ super(ctrl);
+
+ // keep this for later
+ _ctrl = ctrl;
+
+ // obtain the drop logic parameters
+ DropLogic dlogic = (DropLogic)logic;
+ _usedrop = dlogic.useBlockDropping();
+ _userise = dlogic.useBoardRising();
+
+ if (_userise) {
+ // prepare for board rising
+ _risevel = getRiseVelocity();
+ _risedist = getRiseDistance();
+ }
+ }
+
+ // documentation inherited
+ public void init (CrowdContext ctx, PlaceConfig config)
+ {
+ super.init(ctx, config);
+
+ // save things off
+ PuzzlePanel panel = (PuzzlePanel)_ctrl.getPlaceView();
+ _ctx = (PuzzleContext)ctx;
+ _dview = (DropBoardView)panel.getBoardView();
+ _dpanel = (DropPanel)panel;
+ _dboard = (DropBoard)_ctrl.getBoard();
+
+ // obtain the board dimensions
+ DropConfig dconfig = (DropConfig)config;
+ _bwid = dconfig.getBoardWidth();
+ _bhei = dconfig.getBoardHeight();
+
+ // create the piece dropper if appropriate
+ PieceDropLogic pdl = getPieceDropLogic();
+ if (pdl != null) {
+ _dropper = new PieceDropper(pdl);
+ }
+ }
+
+ /**
+ * Get the DropPieceProvider for this puzzle. This is currently
+ * only needed if you are using the alwaysfilled property of dropboards.
+ */
+ protected DropPieceProvider getDropPieceProvider ()
+ {
+ return null;
+ }
+
+ /**
+ * Returns the speed with which the next board row should rise into
+ * place, in pixels per millisecond.
+ */
+ protected float getRiseVelocity ()
+ {
+ return DEFAULT_RISE_VELOCITY;
+ }
+
+ /**
+ * Returns the distance in pixels that each board row will traverse
+ * when rising into place.
+ */
+ protected int getRiseDistance ()
+ {
+ return DEFAULT_RISE_DISTANCE;
+ }
+
+ /**
+ * Starts up the action; tries evolving the board to get things going.
+ */
+ protected void startAction ()
+ {
+ super.startAction();
+
+// Log.info("Starting drop action");
+
+ // save off the player index
+ _pidx = _ctrl.getPlayerIndex();
+
+ // add ourselves as a frame participant
+ _ctx.getFrameManager().registerFrameParticipant(this);
+
+// if (_userise) {
+// // make sure the board has its next row of pieces
+// advanceRisingPieces();
+// // we'll set up the risestamp on the first rise tick
+// }
+
+ // if we've a drop block left over from our previous action, set
+ // it on its merry way once more
+ if (_blocksprite != null) {
+ long delta = _dview.getTimeStamp() - _blockStamp;
+ Log.info("Restarting drop sprite [delta=" + delta + "].");
+ _blocksprite.fastForward(delta);
+ _blockStamp = 0L;
+ _dview.addSprite(_blocksprite);
+
+ // if we cleared the action while the drop sprite was
+ // bouncing, we need to land the block to get things going
+ // again
+ if (_blocksprite.isBouncing()) {
+ Log.info("Ended on a bounce, landing the block and " +
+ "starting things up.");
+ checkBlockLanded("bounced", true, true);
+ }
+ }
+
+ // evolve the board to kick-start the game into action
+ tryEvolveBoard();
+ }
+
+ // documentation inherited
+ protected boolean canClearAction ()
+ {
+// Log.info("Drop can clear " + _evolving);
+ return !_evolving && super.canClearAction();
+ }
+
+ /**
+ * Clears out all of the action in the board; removes any drop block
+ * sprites, any pieces rising in the board, and resets the animation
+ * timestamps.
+ */
+ protected void clearAction ()
+ {
+ super.clearAction();
+
+// Log.info("Clearing drop action.");
+
+ // do away with the bounce interval
+ _bounceStamp = 0;
+ _bounceRow = Integer.MIN_VALUE;
+
+ // kill any active drop block
+ if (_blocksprite != null) {
+ _dview.removeSprite(_blocksprite);
+ _blockStamp = _dview.getTimeStamp();
+ }
+
+ // reset intermediate rising timestamps
+ _rpstamp = 0;
+ _zipstamp = 0;
+ _fastDrop = false;
+
+ // remove ourselves as a frame participant
+ _ctx.getFrameManager().removeFrameParticipant(this);
+ }
+
+ // documentation inherited
+ public void gameDidEnd ()
+ {
+ super.gameDidEnd();
+
+ // clear out the drop block sprite
+ _blocksprite = null;
+
+ // reset ourselves back to pre-game conditions
+ _risestamp = 0;
+// _dview.setRisingPieces(null);
+ }
+
+ // documentation inherited
+ public boolean handleAction (ActionEvent action)
+ {
+ // handle any block-related movement actions
+ if (handleBlockAction(action)) {
+ return true;
+ }
+
+ String cmd = action.getActionCommand();
+ if (cmd.equals(START_DROP_BLOCK)) {
+ handleDropBlock(true);
+
+ } else if (cmd.equals(END_DROP_BLOCK)) {
+ handleDropBlock(false);
+
+ } else {
+ return super.handleAction(action);
+ }
+
+ return true;
+ }
+
+ // documentation inherited
+ public void setBoard (Board board)
+ {
+ super.setBoard(board);
+
+ // update the casted board reference
+ _dboard = (DropBoard)board;
+ }
+
+ // documentation inherited
+ protected boolean handleBlockAction (ActionEvent action)
+ {
+ String cmd = action.getActionCommand();
+ boolean handled = false;
+
+ if (cmd.equals(MOVE_BLOCK_LEFT)) {
+ handleMoveBlock(LEFT);
+ handled = true;
+
+ } else if (cmd.equals(MOVE_BLOCK_RIGHT)) {
+ handleMoveBlock(RIGHT);
+ handled = true;
+
+ } else if (cmd.equals(ROTATE_BLOCK_CCW)) {
+ handleRotateBlock(CCW);
+ handled = true;
+
+ } else if (cmd.equals(ROTATE_BLOCK_CW)) {
+ handleRotateBlock(CW);
+ handled = true;
+ }
+
+ if (handled && _blocksprite != null) {
+ // land the block if it's been placed on something solid as a
+ // result of one of the above actions
+ String source = "fiddled [cmd=" + cmd + "]";
+ if (checkBlockLanded(source, false, false)) {
+ startBounceTimer(source);
+ }
+ }
+
+ return handled;
+ }
+
+ /**
+ * Handles block moved events.
+ */
+ protected void handleMoveBlock (int dir)
+ {
+ if (_blocksprite == null) {
+ return;
+ }
+
+ // gather information regarding the attempted move
+ Rectangle bb = _blocksprite.getBoardBounds();
+ int row = _blocksprite.getRow(), col = _blocksprite.getColumn();
+ int dx = (dir == LEFT) ? -1 : 1;
+
+ // if the sprite has made it to the bottom of the board then we
+ // don't want to allow it to "virtually" fall any further because
+ // of the bounce interval
+ float pctdone = (row >= (_bhei - 1)) ? 0 :
+ _blocksprite.getPercentDone(_dview.getTimeStamp());
+
+ // get the drop block position resulting from the move
+ Point pos = _dboard.getForgivingMove(
+ bb.x, bb.y, bb.width, bb.height, dx, 0, pctdone);
+ if (pos != null) {
+ int frow = row + (pos.y - bb.y);
+ int fcol = col + (pos.x - bb.x);
+ // Log.info("Valid move [row=" + frow + ", col=" + col + "].");
+ _blocksprite.setBoardLocation(frow, fcol);
+ }
+ }
+
+ /**
+ * Handles block rotation events.
+ */
+ protected void handleRotateBlock (int dir)
+ {
+ if (_blocksprite == null) {
+ return;
+ }
+
+ // gather information regarding the attempted rotation
+ int[] rows = _blocksprite.getRows();
+ int[] cols = _blocksprite.getColumns();
+ // if the sprite has made it to the bottom of the board then we
+ // don't want to allow it to "virtually" fall any further because
+ // of the bounce interval
+ float pctdone = (rows[0] >= (_bhei - 1)) ? 0 :
+ _blocksprite.getPercentDone(_dview.getTimeStamp());
+
+ // get the drop block position resulting from the rotation
+ int[] info = _dboard.getForgivingRotation(
+ rows, cols, _blocksprite.getOrientation(), dir,
+ getRotationType(), pctdone);
+ if (info != null) {
+// Log.info("Found valid rotation " +
+// "[orient=" + DirectionUtil.toShortString(info[0]) +
+// ", col=" + info[1] + ", row=" + info[2] +
+// ", blocksprite=" + _blocksprite + "].");
+
+ // update the piece image
+ _dview.rotateDropBlock(_blocksprite, info[0]);
+
+ // place the block in its newly rotated location
+ _blocksprite.setBoardLocation(info[2], info[1]);
+
+ // let derived classes do what they will
+ blockDidRotate(dir);
+ }
+ }
+
+ /**
+ * Called when the drop block has rotated in the specified direction
+ * to allow derived classes to engage in any game-specific antics.
+ */
+ protected void blockDidRotate (int dir)
+ {
+ }
+
+ /**
+ * Returns the rotation type used by this drop game. Either {@link
+ * DropBoard#RADIAL_ROTATION} or {@link DropBoard#INPLACE_ROTATION}.
+ */
+ protected int getRotationType ()
+ {
+ return DropBoard.RADIAL_ROTATION;
+ }
+
+ /**
+ * Handles drop block events.
+ */
+ protected void handleDropBlock (boolean fast)
+ {
+ _fastDrop = fast;
+
+ // only allow changing the piece velocity if we're not bouncing
+ if (_blocksprite != null && _bounceStamp == 0) {
+ // Log.info("Updating drop block velocity [fast=" + fast + "].");
+ _blocksprite.setVelocity(getPieceVelocity(fast));
+ }
+ }
+
+ /**
+ * Returns the drop sprite velocity to assign to a new drop sprite.
+ */
+ protected abstract float getPieceVelocity (boolean fast);
+
+ /**
+ * Handles creation and dropping of the next dropping block.
+ */
+ protected void dropNextBlock ()
+ {
+ if (_blocksprite != null || _ctrl.isWaiting() || !_ctrl.hasAction()) {
+ Log.info("Not dropping block [bs=" + (_blocksprite != null) +
+ ", waiting=" + _ctrl.isWaiting() +
+ ", action=" + _ctrl.hasAction() + "].");
+ return;
+ }
+
+ // determine whether or not the game should be ended because we
+ // can't drop the next block
+ if (checkDropEndsGame()) {
+ return;
+ }
+
+ if (!_ctrl.isGameOver() && _puzobj.isActivePlayer(_pidx)) {
+ // create the next block
+ _blocksprite = createNextBlock();
+ if (_blocksprite != null) {
+ // reset the drop block fast-drop state
+ _fastDrop = false;
+
+ // configure and add the drop block sprite
+ _blocksprite.setVelocity(getPieceVelocity(_fastDrop));
+ _blocksprite.addSpriteObserver(_dropMovedHandler);
+ _dview.addSprite(_blocksprite);
+
+ // update the next block display
+ _dpanel.setNextBlock(peekNextPieces());
+
+ // make sure the block has somewhere to go
+ if (checkBlockLanded("next-block", false, true)) {
+ startBounceTimer("next-block");
+ }
+ }
+
+ // clear out our last bounce row
+ _bounceRow = Integer.MIN_VALUE;
+ }
+ }
+
+ /**
+ * Called by {@link #dropNextBlock} to determine whether the game
+ * should be ended rather than dropping the next block because the
+ * board is filled and a new block cannot enter. If true is returned,
+ * the drop controller assumes that the derived class will have ended
+ * or reset the game as appropriate and will simply abandon its
+ * attempt to drop the next block.
+ */
+ protected boolean checkDropEndsGame ()
+ {
+ return false;
+ }
+
+ /**
+ * Called only for block-dropping puzzles when it's time to create the
+ * next drop block. Returns the drop block sprite if it was
+ * successfully created, or null if it was not.
+ */
+ protected DropBlockSprite createNextBlock ()
+ {
+ // nothing for now
+ return null;
+ }
+
+ /**
+ * Take a peek at the next pieces.
+ */
+ protected int[] peekNextPieces ()
+ {
+ return null;
+ }
+
+ /**
+ * Called when a drop sprite posts a piece moved event.
+ */
+ protected void handleDropSpriteMoved (
+ DropSprite sprite, long when, int col, int row)
+ {
+ if (sprite instanceof DropBlockSprite) {
+ if (checkBlockLanded("piece-moved", false, true)) {
+ startBounceTimer("piece-moved");
+ }
+ // keep dropping the drop block
+ sprite.drop();
+
+ } else {
+ if (sprite.getDistance() > 0) {
+ sprite.drop();
+
+ } else {
+ // remove the sprite
+ _dview.removeSprite(sprite);
+
+ // apply the pieces to the board
+ applyDropSprite(sprite, col, row);
+
+ // perform any new destruction and falling
+ tryEvolveBoard();
+ }
+ }
+ }
+
+ /**
+ * Applies the pieces in the given sprite to the specified column and
+ * row in the board. Called when a drop sprite has finished
+ * traversing its entire distance.
+ */
+ protected void applyDropSprite (DropSprite sprite, int col, int row)
+ {
+ // set the pieces in the board
+ int[] pieces = sprite.getPieces();
+ _dboard.setSegment(VERTICAL, col, row, pieces);
+ // dirty the updated board pieces
+ _dview.dirtySegment(VERTICAL, col, row, pieces.length);
+ }
+
+ /**
+ * Calls {@link #tryEvolveBoard(boolean)} with debugging deactivated.
+ */
+ protected void tryEvolveBoard ()
+ {
+ tryEvolveBoard(false);
+ }
+
+ /**
+ * Attempts to evolve the board. This involves first calling {@link
+ * #canEvolveBoard} and only calling {@link #evolveBoard} if the
+ * former returned true. If the board is fully stabilized, {@link
+ * #boardDidStabilize} will be called to reinstate the puzzle action.
+ */
+ protected void tryEvolveBoard (boolean debug)
+ {
+ // if we can't evolve the board because things are going on, we
+ // bail out immediately
+ if (!canEvolveBoard()) {
+ if (debug) {
+ Log.info("Can't evolve board " +
+ "[acount=" + _dview.getActionCount() + "].");
+ }
+ return;
+ }
+
+ // if we do not evolve the board in any way, let the derived class
+ // know that the board stabilized so that they can drop in a new
+ // piece if they like or take whatever other action is appropriate
+ _evolving = evolveBoard();
+ if (debug) {
+ Log.info("Evolved board [evolving=" + _evolving + "].");
+ }
+
+ // if we're no longer evolving and the action has not ended, go
+ // ahead and let our derived class know that the board has
+ // stabilized so that it can drop in the next piece or somesuch
+ if (!_evolving) {
+ if (_ctrl.hasAction()) {
+ // this will trigger further puzzle activity
+ if (debug) {
+ Log.info("Board did stabilize");
+ }
+ boardDidStabilize();
+
+ } else {
+ if (debug) {
+ Log.info("Maybe clearing action.");
+ }
+ // this will ensure that if we have been postponing action
+ // due to board evolution, that it will now be cleared
+ maybeClearAction();
+ }
+ }
+ }
+
+ /**
+ * Called to determine whether it is safe to evolve the board. The
+ * default implementation does not allow board evolution if there are
+ * sprites or animations active on the board.
+ */
+ protected boolean canEvolveBoard ()
+ {
+ return (_dview.getActionCount() == 0);
+ }
+
+ /**
+ * Evolves the board to an unchanging state. If the board is in a
+ * state where pieces should react with one another to cause changes
+ * to the board state (such as piece dropping via {@link #dropPieces},
+ * piece destruction, and/or piece joining), this is where that
+ * process should be effected.
+ *
+ *
When no further evolution is possible and the board has
+ * stabilized this method should return false to indicate that such
+ * action should be taken. That will result in a follow-up call to
+ * {@link #boardDidStabilize} (assuming that the action was not
+ * cleared prior to the final stabilization of the board).
+ */
+ protected abstract boolean evolveBoard ();
+
+ /**
+ * Called when the board has been fully evolved and is once again
+ * stable. The default implementation updates the player's local board
+ * summary and drops the next block into the board, but derived
+ * classes may wish to perform custom actions if they don't use drop
+ * blocks or have other requirements.
+ */
+ protected void boardDidStabilize ()
+ {
+ updateSelfSummary();
+ dropNextBlock();
+ }
+
+ /**
+ * Updates the player's own local board summary to reflect the local
+ * copy of the player's board which is likely to be more up to date
+ * than the server-side board from which all other player board
+ * summaries originate.
+ */
+ protected void updateSelfSummary ()
+ {
+ if (_puzobj.summaries != null) {
+ BoardSummary bsum = _puzobj.summaries[_pidx];
+ bsum.setBoard(_dboard);
+ bsum.summarize();
+ _dpanel.setSummary(_pidx, bsum);
+ }
+ }
+
+ /**
+ * Called when an animation finishes doing its business. Derived
+ * classes may wish to override this method but should be sure to call
+ * super.animationDidFinish().
+ */
+ protected void animationDidFinish (Animation anim)
+ {
+ tryEvolveBoard();
+ }
+
+ /**
+ * Checks whether the drop block can continue dropping and lands its
+ * pieces if not. Returns whether at least one piece of the block has
+ * landed; note that the other piece may need subsequent dropping.
+ *
+ * @param commit if true, the block landing is committed, if false, it
+ * is only checked, not committed.
+ * @param atTop whether the block sprite is to be treated as being at
+ * the top of its current row.
+ */
+ protected boolean checkBlockLanded (
+ String source, boolean commit, boolean atTop)
+ {
+ if (_blocksprite == null) {
+ return true;
+ }
+
+ // check to see that both pieces can continue dropping
+ int[] rows = _blocksprite.getRows();
+ int[] cols = _blocksprite.getColumns();
+
+ // TODO: we may need to limit pctdone here to account for landing
+ // on the bottom of the board.
+ float pctdone = (atTop) ? 0.0f :
+ _blocksprite.getPercentDone(_dview.getTimeStamp());
+
+// Log.info("Checking landed [source=" + source +
+// ", bounceRow=" + _bounceRow +
+// ", rows=" + StringUtil.toString(rows) +
+// ", cols=" + StringUtil.toString(cols) +
+// ", orient=" + DirectionUtil.toShortString(
+// _blocksprite.getOrientation()) +
+// ", commit=" + commit + ", pctdone=" + pctdone + "].");
+
+ if (_dboard.isValidDrop(rows, cols, pctdone)) {
+ return false;
+ }
+
+ // if we're committing the landing, remove the sprite and update
+ // the board and all that
+ if (commit) {
+ // give sub-classes a chance to do any pre-landing business
+ blockWillLand();
+
+ // stamp the pieces into the board
+ int[] pieces = _blocksprite.getPieces();
+ boolean error = false;
+ for (int ii = 0; ii < pieces.length; ii++) {
+ if (rows[ii] >= 0) {
+ int col = cols[ii], row = rows[ii];
+
+ // sanity-check the block to make sure it's located in
+ // a valid position, and that we aren't somehow
+ // overwriting an existing piece
+ if (col < 0 || col >= _bwid || row >= _bhei) {
+ Log.warning("Placing drop block piece outside board " +
+ "bounds!? [x=" + col + ", y=" + row +
+ ", pidx=" + ii +
+ ", blocksprite=" + _blocksprite + "].");
+ error = true;
+
+ } else {
+ int cpiece = _dboard.getPiece(col, row);
+ if (cpiece != PIECE_NONE) {
+ Log.warning("Placing drop block piece onto " +
+ "occupied board position!? [x=" + col +
+ ", y=" + row + ", pidx=" + ii +
+ ", blocksprite=" + _blocksprite + "].");
+ error = true;
+ }
+ }
+
+ if (!error) {
+ // stuff the piece into the board
+ _dboard.setPiece(col, row, pieces[ii]);
+ _dview.dirtyPiece(col, row);
+ }
+ }
+
+ if (DEBUG_PUZZLE && error) {
+ _dboard.dump();
+ Log.warning("Bailing out in a flaming pyre of glory.");
+ System.exit(0);
+ }
+ }
+
+ // remove the drop block sprite
+ _dview.removeSprite(_blocksprite);
+ _blocksprite = null;
+
+ // give sub-classes a chance to do any post-landing business
+ blockDidLand();
+ }
+
+ return true;
+ }
+
+ /**
+ * Called only for block-dropping puzzles when the drop block is about
+ * to land on something. Derived classes may wish to override this
+ * method to perform game-specific actions such as queueing up a
+ * "block placed" progress event.
+ */
+ protected void blockWillLand ()
+ {
+ // nothing for now
+ }
+
+ /**
+ * Called only for block-dropping puzzles when the drop block lands on
+ * something. Derived classes may wish to override this method to
+ * perform any game-specific actions.
+ */
+ protected void blockDidLand ()
+ {
+ // nothing for now
+ }
+
+ /**
+ * Called when a block lands. We give the user a smidgen of time to
+ * continue to fiddle with the block before we actually land it. If
+ * the block is still landed when the bounce timer expires, we commit
+ * the landing, otherwise we let the block keep falling.
+ */
+ protected void startBounceTimer (String source)
+ {
+ int bounceRow = IntListUtil.getMaxValue(_blocksprite.getRows());
+// Log.info("startBounceTimer [source=" + source +
+// ", bounceStamp=" + _bounceStamp +
+// ", time=" + _dview.getTimeStamp() +
+// ", bounceRow=" + _bounceRow +
+// ", nbounceRow=" + bounceRow + "].");
+
+ // forcibly land the block if we bounce twice at the same row
+ if (_bounceStamp == 0 && _bounceRow == bounceRow) {
+ if (checkBlockLanded("double-bounced", true, true)) {
+ tryEvolveBoard();
+ }
+ return;
+ }
+
+ // if the bounce "timer" is already started, the user probably did
+ // something like rotate the piece while it was bouncing (which is
+ // why we give them the bounce interval), so we don't reset
+ if (_bounceStamp == 0) {
+ // slow the piece down so that it doesn't fly past the
+ // coordinates at which it's potentially landing; we have to
+ // do this before we tell the sprite that it's bouncing
+ // because changing the velocity fiddles with the rowstamp and
+ // we're going to reset the rowstamp when we tell the sprite
+ // that it's bouncing
+ _blocksprite.setVelocity(getPieceVelocity(false));
+
+ // set up our bounce interval (it depends on the current piece
+ // velocity and so must be set at the time we bounce)
+ _bounceInterval = (int)
+ ((_dview.getPieceHeight() * BOUNCE_FRACTION) /
+ getPieceVelocity(false));
+// Log.info("bounceInterval=" + _bounceInterval +
+// ", phei=" + _dview.getPieceHeight() +
+// ", vel=" + getPieceVelocity(false));
+
+ // make a note of the time we started bouncing
+ _bounceStamp = _dview.getTimeStamp();
+
+ // and the row at which we're bouncing
+ _bounceRow = bounceRow;
+
+ // put the block sprite into bouncing mode
+ _blocksprite.setBouncing(true);
+ }
+ }
+
+ /**
+ * Called when the bounce timer expires. Herein we either commit the
+ * landing of a block if it is still landed or let it keep falling if
+ * it is no longer landed.
+ */
+ protected void bounceTimerExpired ()
+ {
+// Log.info("bounceTimerExpired [bounceStamp=" + _bounceStamp +
+// ", time=" + _dview.getTimeStamp() +
+// ", bounceRow=" + _bounceRow + "].");
+
+ // make sure we weren't cancelled for some reason
+ if (_bounceStamp != 0) {
+ if (checkBlockLanded("bounced", true, true)) {
+ tryEvolveBoard();
+
+ } else if (_blocksprite != null) {
+ // take the block sprite out of bouncing mode
+ _blocksprite.setBouncing(false);
+ }
+ _bounceStamp = 0;
+ }
+ }
+
+ /**
+ * Drops any pieces that need dropping and returns whether any pieces
+ * were dropped. Derived classes that would like to drop their pieces
+ * should include a call to this method in their {@link #evolveBoard}
+ * implementation, and must also override {@link #getPieceDropLogic}
+ * to provide their game-specific piece dropper implementation.
+ */
+ protected boolean dropPieces ()
+ {
+ // get a list of the piece columns to be dropped
+ List drops = _dropper.getDroppedPieces(_dboard, getDropPieceProvider());
+ int size = drops.size();
+ if (size == 0) {
+ return false;
+ }
+
+ // drop each column
+ for (int ii = 0; ii < size; ii++) {
+ PieceDropInfo pdi = (PieceDropInfo)drops.get(ii);
+ // Log.info("Dropping column segment [pdi=" + pdi + "].");
+
+ // clear the dropping pieces from the board
+ _dboard.setSegment(
+ VERTICAL, pdi.col, pdi.row, pdi.pieces.length, PIECE_NONE);
+
+ // create a piece sprite animating the pieces falling
+ DropSprite sprite = _dview.createPieces(
+ pdi.col, pdi.row, pdi.pieces, pdi.dist);
+ sprite.setVelocity(1.5f * getPieceVelocity(true));
+ sprite.addSpriteObserver(_dropMovedHandler);
+ _dview.addActionSprite(sprite);
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the piece dropper used to drop any pieces that need
+ * dropping in the board. Derived classes that intend to make use of
+ * {@link #dropPieces} must implement this method and return a
+ * reference to their game-specific piece dropper implementation.
+ */
+ protected PieceDropLogic getPieceDropLogic ()
+ {
+ return null;
+ }
+
+ // documentation inherited
+ public void tick (long tickStamp)
+ {
+// if (_userise && (tickStamp >= _risesent + RISE_INTERVAL)) {
+// _risesent += RISE_INTERVAL;
+// raiseBoard(tickStamp);
+// }
+
+ // check the bounce timer
+ if ((_bounceStamp != 0) &&
+ ((tickStamp - _bounceStamp) >= _bounceInterval)) {
+ bounceTimerExpired();
+ }
+ }
+
+ // documentation inherited
+ public Component getComponent ()
+ {
+ return null;
+ }
+
+ // documentation inherited
+ public boolean needsPaint ()
+ {
+ return false;
+ }
+
+ // documentation inherited
+ public void didResume (long delta)
+ {
+ super.didResume(delta);
+
+ // fast-forward the board rising timestamps
+ _risestamp += delta;
+ if (_zipstamp != 0) {
+ _zipstamp += delta;
+ }
+
+ Log.info("Drop puzzle resuming, attempting to evolve board.");
+
+ // we're un-paused, so we should try evolving the board to start
+ // things up again
+ tryEvolveBoard(true);
+ }
+
+ /**
+ * Sets whether the board rising is paused.
+ */
+ public void setRisingPaused (boolean paused)
+ {
+ if (paused && _rpstamp == 0) {
+ // pause the board
+ _rpstamp = _dview.getTimeStamp();
+
+ } else if (!paused && _rpstamp != 0) {
+ // un-pause the board
+ long delta = _dview.getTimeStamp() - _rpstamp;
+ _risestamp += delta;
+ if (_zipstamp != 0) {
+ _zipstamp += delta;
+ }
+ _rpstamp = 0;
+ }
+ }
+
+ /**
+ * Causes the board to zip quickly to the next row.
+ */
+ public void zipToNextRow ()
+ {
+ // don't overwrite an existing zip
+ if (_zipstamp == 0) {
+ // if we're paused, inherit the pause time, otherwise use the
+ // current time
+ if (_rpstamp != 0) {
+ _zipstamp = _rpstamp;
+ } else {
+ _zipstamp = _dview.getTimeStamp();
+ }
+ }
+ }
+
+ /**
+ * Called periodically on the frame tick. Raises the board row based
+ * on the time since the current row traversal began.
+ */
+ /*
+ protected void raiseBoard (long tickStamp)
+ {
+ // don't raise if rising is paused or the action is cleared
+ if (_rpstamp != 0 || !_ctrl.hasAction()) {
+ return;
+ }
+
+ // initialize the rise stamp the first time we're risen
+ if (_risestamp == 0) {
+ _risestamp = tickStamp;
+ _risesent = _risestamp;
+ }
+
+ // determine how far we've risen
+ long msecs = tickStamp - _risestamp;
+ float travpix = msecs * _risevel;
+
+ // account for any zipping effect
+ long zipsecs = 0;
+ if (_zipstamp > 0) {
+ zipsecs = tickStamp - _zipstamp;
+ // make sure we don't zip past the top
+ float zippix = (zipsecs * _risevel * 15);
+ if (travpix < _risedist) {
+ travpix += zippix;
+ travpix = Math.min(travpix, _risedist);
+ }
+ }
+
+ float pctdone = travpix / _risedist;
+
+ boolean rose = false;
+ if (pctdone >= 1.0f) {
+ rose = true;
+ if (_zipstamp > 0) {
+ // clear out any zip stamp
+ _zipstamp = 0;
+ _risestamp = tickStamp;
+ pctdone = 1f;
+
+ } else {
+ long used = (long)(_risedist / _risevel);
+ _risestamp += used;
+ }
+
+ // give sub-classes a chance to do their thing
+ boardWillRise();
+ }
+
+ // update the board display
+ int ypos = ((int)(_risedist * pctdone)) % _risedist;
+ _dview.setRiseOffset(ypos);
+
+ if (rose) {
+ // check to see if this means doom and defeat (even though the
+ // game might be over, we still want to advance the piece
+ // packet one last time and do the last rise so that the
+ // server can tell that we kicked the proverbial bucket)
+ boolean canRise = checkCanRise();
+
+ // apply the rising row pieces to the board
+ int[] pieces = _dview.getRisingPieces();
+ _dboard.applyRisingPieces(pieces);
+
+ // set up the next row of rising pieces
+ _dview.setRisingPieces(null);
+ advanceRisingPieces();
+
+ // give sub-classes a chance to do their thing
+ boardDidRise();
+
+ if (canRise) {
+ // evolve the board
+ tryEvolveBoard();
+
+ } else {
+ Log.debug("Sticking fork in it [risers=" +
+ StringUtil.toString(pieces) + ".");
+
+ // let the controller know that we're done for
+ _ctrl.resetGame();
+ }
+ }
+
+// Log.info("Board rise [msecs=" + msecs + ", roff=" + ypos +
+// ", pctdone=" + pctdone + ", zipsecs=" + zipsecs + "].");
+ }
+ */
+
+ /**
+ * Called to determine whether or not rising a new row into the board
+ * is legal. The default implementation will return false if the top
+ * row of the board contains any pieces.
+ */
+ protected boolean checkCanRise ()
+ {
+ return !_dboard.rowContainsPieces(0, PIECE_NONE);
+ }
+
+ /**
+ * Called only for board-rising puzzles before effecting the rising of
+ * the board by one row. Derived classes may wish to override this
+ * method to add any desired behaviour, but should be sure to call
+ * super.boardWillRise().
+ */
+ protected void boardWillRise ()
+ {
+ // nothing for now
+ }
+
+ /**
+ * Called only for board-rising puzzles when the board has finished
+ * rising one row. Derived classes may wish to override this method
+ * to add any desired behaviour, but should be sure to call
+ * super.boardDidRise().
+ */
+ protected void boardDidRise ()
+ {
+ // nothing for now
+ }
+
+ /** The yohoho context. */
+ protected PuzzleContext _ctx;
+
+ /** Our puzzle controller. */
+ protected PuzzleController _ctrl;
+
+ /** The drop panel. */
+ protected DropPanel _dpanel;
+
+ /** The drop board view. */
+ protected DropBoardView _dview;
+
+ /** The drop board. */
+ protected DropBoard _dboard;
+
+ /** Whether or not we are in the middle of board evolution. */
+ protected boolean _evolving;
+
+ /** Whether the game is using drop block functionality. */
+ protected boolean _usedrop;
+
+ /** Whether the game is using board rising functionality. */
+ protected boolean _userise;
+
+ /** The board dimensions in pieces. */
+ protected int _bwid, _bhei;
+
+ /** Our player index in the game. */
+ protected int _pidx;
+
+ /** The distance the board row travels in pixels. */
+ protected int _risedist;
+
+ /** The speed with which the board rises in pixels per millisecond. */
+ protected float _risevel;
+
+ /** The drop block sprite associated with the landing block, if any. */
+ protected DropBlockSprite _blocksprite;
+
+ /** The piece dropper used to drop pieces in the board if the puzzle
+ * chooses to make use of piece dropping functionality. */
+ protected PieceDropper _dropper;
+
+ /** The time at which the board rise was paused. */
+ protected long _rpstamp;
+
+ /** The time at which the last board rise began. */
+ protected long _risestamp;
+
+ /** The time at which we last fired off a board rising event. */
+ protected long _risesent;
+
+ /** The time at which we were requested to start zipping. */
+ protected long _zipstamp;
+
+ /** The duration of the bounce interval. */
+ protected int _bounceInterval;
+
+ /** The time at which we last started bouncing, or 0. */
+ protected long _bounceStamp;
+
+ /** The row at which we last bounced, or {@link Integer#MIN_VALUE}. */
+ protected int _bounceRow;
+
+ /** The timestamp used to keep track of when the drop block was
+ * removed so that we can fast-forward it when restored. */
+ protected long _blockStamp;
+
+ /** Whether the drop blocks are currently dropping quickly. */
+ protected boolean _fastDrop;
+
+ /** Used to evolve the board following the completion of animations. */
+ protected AnimationAdapter _evolveObserver = new AnimationAdapter() {
+ public void animationCompleted (Animation anim, long when) {
+ animationDidFinish(anim);
+ }
+ };
+
+ /** Used to listen to drop sprites and react to their move events. */
+ protected DropSpriteObserver _dropMovedHandler = new DropSpriteObserver() {
+ public void pieceMoved (
+ DropSprite sprite, long when, int col, int row) {
+ handleDropSpriteMoved(sprite, when, col, row);
+ }
+ };
+
+ /** The default board row rising velocity. */
+ protected static final float DEFAULT_RISE_VELOCITY = 100f / 1000f;
+
+ /** The default board row rising distance. */
+ protected static final int DEFAULT_RISE_DISTANCE = 20;
+
+ /** The delay in milliseconds between board rising intervals. */
+ protected static final long RISE_INTERVAL = 50L;
+
+ /** Defines the distance of a piece that we allow to bounce before we
+ * land it. */
+ protected static final float BOUNCE_FRACTION = 0.125f;
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropPanel.java b/src/java/com/threerings/puzzle/drop/client/DropPanel.java
new file mode 100644
index 000000000..8bcc6a9a2
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropPanel.java
@@ -0,0 +1,23 @@
+//
+// $Id: DropPanel.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import com.threerings.puzzle.data.BoardSummary;
+
+/**
+ * Puzzles using the drop services need implement this interface to
+ * display drop puzzle related information.
+ */
+public interface DropPanel
+{
+ /**
+ * Sets the next block to be displayed.
+ */
+ public void setNextBlock (int[] pieces);
+
+ /**
+ * Updates the board summary display for the given player.
+ */
+ public void setSummary (int pidx, BoardSummary summary);
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropSprite.java b/src/java/com/threerings/puzzle/drop/client/DropSprite.java
new file mode 100644
index 000000000..5a272eab6
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropSprite.java
@@ -0,0 +1,566 @@
+//
+// $Id: DropSprite.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Shape;
+
+import com.samskivert.util.ObserverList;
+import com.threerings.util.DirectionUtil;
+
+import com.threerings.media.image.Mirage;
+import com.threerings.media.sprite.Sprite;
+import com.threerings.media.tile.TileManager;
+
+import com.threerings.puzzle.Log;
+
+/**
+ * The drop sprite is a sprite that displays one or more pieces falling
+ * toward the bottom of the board.
+ */
+public class DropSprite extends Sprite
+{
+ /**
+ * Constructs a drop sprite and starts it dropping.
+ *
+ * @param view the board view upon which this sprite will be displayed.
+ * @param col the column of the sprite.
+ * @param row the row of the bottom-most piece.
+ * @param pieces the pieces displayed by the sprite.
+ * @param dist the distance the sprite is to drop in rows.
+ */
+ public DropSprite (
+ DropBoardView view, int col, int row, int[] pieces, int dist)
+ {
+ this(view, col, row, pieces, dist, -1);
+ }
+
+ /**
+ * Constructs a drop sprite and starts it dropping.
+ *
+ * @param view the board view upon which this sprite will be displayed.
+ * @param col the column of the sprite.
+ * @param row the row of the bottom-most piece.
+ * @param pieces the pieces displayed by the sprite.
+ * @param dist the distance the sprite is to drop in rows.
+ * @param renderOrder the render order.
+ */
+ public DropSprite (
+ DropBoardView view, int col, int row, int[] pieces, int dist,
+ int renderOrder)
+ {
+ _view = view;
+ _col = col;
+ _row = row;
+ _pieces = pieces;
+ _dist = (dist == 0) ? 1 : dist;
+ _orient = NORTH;
+ _unit = _view.getPieceHeight();
+ setRenderOrder(renderOrder);
+ }
+
+ // documentation inherited
+ protected void init ()
+ {
+ super.init();
+
+ // size the bounds to fit our pieces
+ updateBounds();
+ // set up the piece location
+ setBoardLocation(_row, _col);
+ // calculate vertical render offset based on the number of pieces
+ setRowOffset(-(_pieces.length - 1));
+ }
+
+ /**
+ * Returns the remaining number of columns to drop.
+ */
+ public int getDistance ()
+ {
+ return _dist;
+ }
+
+ /**
+ * Returns the column the piece is located in.
+ */
+ public int getColumn ()
+ {
+ return _col;
+ }
+
+ /**
+ * Returns the row the piece is located in.
+ */
+ public int getRow ()
+ {
+ return _row;
+ }
+
+ /**
+ * Returns the pieces the sprite is displaying.
+ */
+ public int[] getPieces ()
+ {
+ return _pieces;
+ }
+
+ /**
+ * Returns the velocity of this sprite.
+ */
+ public float getVelocity ()
+ {
+ return _vel;
+ }
+
+ /**
+ * Sets the row and column the piece is located in.
+ */
+ public void setBoardLocation (int row, int col)
+ {
+ _row = row;
+ _col = col;
+ updatePosition();
+ }
+
+ /**
+ * Sets the column the piece is located in.
+ */
+ public void setColumn (int col)
+ {
+ _col = col;
+ updatePosition();
+ }
+
+ /**
+ * Set the row the piece is located in.
+ */
+ public void setRow (int row)
+ {
+ _row = row;
+ updatePosition();
+ }
+
+ /**
+ * Sets the column offset of the sprite image.
+ */
+ public void setColumnOffset (int count)
+ {
+ _offx = count;
+ updateRenderOffset();
+ updateRenderOrigin();
+ }
+
+ /**
+ * Sets the row offset of the sprite image.
+ */
+ public void setRowOffset (int count)
+ {
+ _offy = count;
+ updateRenderOffset();
+ updateRenderOrigin();
+ }
+
+ /**
+ * Sets the pieces the sprite is displaying.
+ */
+ public void setPieces (int[] pieces)
+ {
+ _pieces = pieces;
+ }
+
+ /**
+ * Sets the velocity of this sprite. The time at which the current
+ * row was entered is modified so that the sprite position will remain
+ * the same when calculated using the new velocity since the piece
+ * sprite may have its velocity modified in the middle of a row
+ * traversal.
+ */
+ public void setVelocity (float velocity)
+ {
+ // bail if we've already got the requested velocity
+ if (_vel == velocity) {
+ return;
+ }
+
+ if (_rowstamp > 0) {
+ // get our current distance along the row
+ long now = _view.getTimeStamp();
+ float pctdone = getPercentDone(now);
+
+ // revise the current row entry time to account for the new velocity
+ float travpix = pctdone * _unit;
+ long msecs = (long)(travpix / velocity);
+ _rowstamp = now - msecs;
+ }
+
+ // update the velocity
+ _vel = velocity;
+ }
+
+ /**
+ * Starts the piece dropping toward the next row.
+ */
+ public void drop ()
+ {
+ // Log.info("Dropping piece [piece=" + this + "].");
+
+ // drop one row by default
+ if (_dist <= 0) {
+ _dist = 1;
+ }
+
+ if (_stopstamp > 0) {
+ // we're dropping from a stand-still
+ long delta = _view.getTimeStamp() - _stopstamp;
+ _rowstamp += delta;
+ _stopstamp = 0;
+
+ } else {
+ // we're continuing a previous drop, so make use of any
+ // previously existing time
+ _rowstamp = _endstamp;
+ }
+ }
+
+ /**
+ * Returns true if this drop sprite is dropping, false if it has been
+ * {@link #stop}ped or has not yet been {@link #drop}ped.
+ */
+ public boolean isDropping ()
+ {
+ return (_stopstamp == 0) && (_rowstamp != 0);
+ }
+
+ /**
+ * Stops the piece from dropping.
+ */
+ public void stop ()
+ {
+ if (_stopstamp == 0) {
+ _stopstamp = _view.getTimeStamp();
+ // Log.info("Stopped piece [piece=" + this + "].");
+ }
+ }
+
+ /**
+ * Puts the drop sprite into (or takes it out of) bouncing
+ * mode. Bouncing mode is used to put the sprite into limbo after it
+ * lands but before we commit the landing, giving the user a last
+ * moment change move or rotate the piece. While the sprite is
+ * "bouncing" it will be rendered one pixel below it's at rest state.
+ */
+ public void setBouncing (boolean bouncing)
+ {
+ if (_bouncing = bouncing) {
+ // if we've activated bouncing, shift the sprite slightly to
+ // illustrate its new state
+ shiftForBounce();
+
+ // to prevent funny business in the event that we were a long
+ // ways past the end of the row when we landed, we warp the
+ // sprite back to the exact point of landing for the purposes
+ // of the bounce and any subsequent antics
+ _endstamp = _rowstamp = _view.getTimeStamp();
+
+// Log.info("Adjusted rowstap due to bounce " +
+// "[time=" + _endstamp + "].");
+ }
+ }
+
+ /**
+ * Returns true if this sprite is bouncing.
+ */
+ public boolean isBouncing ()
+ {
+ return _bouncing;
+ }
+
+ /**
+ * Updates the sprite's location to illustrate that it is currently in
+ * the "bouncing" state.
+ */
+ protected void shiftForBounce ()
+ {
+ setLocation(_ox, _srcPos.y+1);
+ }
+
+ // documentation inherited
+ public boolean inside (Shape shape)
+ {
+ return shape.contains(_bounds);
+ }
+
+ /**
+ * Returns a value between 0.0 and 1.0
+ * representing how far the piece has moved toward the next row
+ * as of the given time stamp.
+ */
+ public float getPercentDone (long timestamp)
+ {
+ // if we've never been ticked and so haven't yet initialized our
+ // row start timestamp, just let the caller know that we've not
+ // traversed our row at all
+ if (_rowstamp == 0) {
+ return 0.0f;
+ }
+
+ long msecs = Math.max(0, timestamp - _rowstamp);
+ float travpix = msecs * _vel;
+ float pctdone = (travpix / _unit);
+
+// Log.info("getPercentDone [timestamp=" + timestamp +
+// ", rowstamp=" + _rowstamp + ", msecs=" + msecs +
+// ", travpix=" + travpix + ", pctdone=" + pctdone +
+// ", vel=" + _vel + "].");
+
+ return pctdone;
+ }
+
+ // documentation inherited
+ public void paint (Graphics2D gfx)
+ {
+ // get the column and row increment based on the sprite's orientation
+ int oidx = _orient/2;
+ int incx = ORIENT_DX[oidx];
+ int incy = ORIENT_DY[oidx];
+
+ // determine offset from the start of each actual row and column
+ int dx = _ox - _srcPos.x, dy = _oy - _srcPos.y;
+
+ int pcol = _col, prow = _row;
+ for (int ii = 0; ii < _pieces.length; ii++) {
+ // ask the board for the render position of this piece
+ _view.getPiecePosition(pcol, prow, _renderPos);
+ // draw the piece image
+ paintPieceImage(gfx, ii, pcol, prow, _orient,
+ _renderPos.x + dx, _renderPos.y + dy);
+ // increment the target column and row
+ pcol += incx;
+ prow += incy;
+ }
+ }
+
+ /**
+ * Paints the specified piece with the supplied parameters.
+ */
+ protected void paintPieceImage (Graphics2D gfx, int pieceidx,
+ int col, int row, int orient, int x, int y)
+ {
+ Mirage image = _view.getPieceImage(_pieces[pieceidx], col, row, orient);
+ image.paint(gfx, x, y);
+ }
+
+ // documentation inherited
+ public void tick (long timestamp)
+ {
+ super.tick(timestamp);
+
+ // initialize our rowstamp if we haven't done so already
+ if (_rowstamp == 0) {
+ _rowstamp = timestamp;
+ }
+
+ // if we're bouncing or paused, do nothing here
+ if (_bouncing || _stopstamp > 0) {
+ return;
+ }
+
+ PieceMovedOp pmop = null;
+
+ // figure out how far along the current board coordinate we should be
+ float pctdone = getPercentDone(timestamp);
+ if (pctdone >= 1.0f) {
+ // note that we've reached the next row
+ advancePosition();
+
+ // update remaining drop distance
+ _dist--;
+
+ // calculate any remaining time to be used
+ long used = (long)(_unit / _vel);
+ _endstamp = _rowstamp + used;
+ _rowstamp = _endstamp;
+
+ // update our percent done because we've moved down a row
+ pctdone -= 1.0;
+
+ // inform observers that we've reached our destination
+ pmop = new PieceMovedOp(this, timestamp, _col, _row);
+ }
+
+ // Log.info("Drop sprite tick [dist=" + _dist + ", pctdone=" + pctdone +
+ // ", row=" + _row + ", col=" + _col + "].");
+
+ // constrain the sprite's position to the destination row
+ pctdone = Math.min(pctdone, 1.0f);
+
+ // calculate the latest sprite position
+ int nx = _srcPos.x + (int)((_destPos.x - _srcPos.x) * pctdone);
+ int ny = _srcPos.y + (int)((_destPos.y - _srcPos.y) * pctdone);
+
+ // only update the sprite's location if it actually moved
+ if (_ox != nx || _oy != ny) {
+ setLocation(nx, ny);
+ }
+
+ // lastly notify our observers if we made it to the next row
+ if (pmop != null) {
+ _observers.apply(pmop);
+ }
+ }
+
+ /**
+ * Called when the sprite has finished traversing its current row to
+ * advance its board coordinates to the next row.
+ */
+ protected void advancePosition ()
+ {
+ setRow(_row + 1);
+ // Log.info("Moved to row " + _row);
+ }
+
+ // documentation inherited
+ public void fastForward (long timeDelta)
+ {
+ if (_rowstamp > 0) {
+ _rowstamp += timeDelta;
+ }
+ }
+
+ // documentation inherited
+ public void toString (StringBuffer buf)
+ {
+ super.toString(buf);
+ buf.append(", orient=").append(DirectionUtil.toShortString(_orient));
+ buf.append(", row=").append(_row);
+ buf.append(", col=").append(_col);
+ buf.append(", offx=").append(_offx);
+ buf.append(", offy=").append(_offy);
+ buf.append(", dist=").append(_dist);
+ }
+
+ /**
+ * Updates internal pixel coordinates used when the piece is moving.
+ */
+ protected void updatePosition ()
+ {
+ _view.getPiecePosition(_col, _row, _srcPos);
+ _view.getPiecePosition(_col, _row+1, _destPos);
+ setLocation(_srcPos.x, _srcPos.y);
+ }
+
+ // documentation inherited
+ public void setOrientation (int orient)
+ {
+ invalidate();
+ super.setOrientation(orient);
+ updateBounds();
+ invalidate();
+ }
+
+ /**
+ * Updates the bounds for this sprite based on the sprite display
+ * dimensions in the view.
+ */
+ protected void updateBounds ()
+ {
+ Dimension size = _view.getPieceSegmentSize(
+ _col, _row, _orient, _pieces.length);
+ _bounds.width = size.width;
+ _bounds.height = size.height;
+ }
+
+ /**
+ * Adjusts our render origin such that our location is not in the
+ * upper left of the sprite's rendered image but is in fact offset by
+ * some number of rows and columns.
+ */
+ protected void updateRenderOffset ()
+ {
+ _oxoff = -(_view.getPieceWidth() * _offx);
+ _oyoff = -(_view.getPieceHeight() * _offy);
+ }
+
+ /** Used to dispatch {@link DropSpriteObserver#pieceMoved}. */
+ protected static class PieceMovedOp implements ObserverList.ObserverOp
+ {
+ public PieceMovedOp (DropSprite sprite, long when, int col, int row)
+ {
+ _sprite = sprite;
+ _when = when;
+ _col = col;
+ _row = row;
+ }
+
+ public boolean apply (Object observer)
+ {
+ if (observer instanceof DropSpriteObserver) {
+ ((DropSpriteObserver)observer).pieceMoved(
+ _sprite, _when, _col, _row);
+ }
+ return true;
+ }
+
+ protected DropSprite _sprite;
+ protected long _when;
+ protected int _col, _row;
+ }
+
+ /** The default piece velocity. */
+ protected static final float DEFAULT_VELOCITY = 30f/1000f;
+
+ /** The time at which we started the current row. */
+ protected long _rowstamp;
+
+ /** The time at which we reached the end of the previous row. */
+ protected long _endstamp;
+
+ /** The time at which we were stopped en route to our next row. */
+ protected long _stopstamp;
+
+ /** The board view upon which this sprite is displayed. */
+ protected DropBoardView _view;
+
+ /** The unit distance the sprite moves to reach the next row. */
+ protected int _unit;
+
+ /** The screen coordinates of the top-left of the row currently
+ * occupied by the sprite. */
+ protected Point _srcPos = new Point();
+
+ /** The screen coordinates of the top-left of the row toward which the
+ * sprite is falling. */
+ protected Point _destPos = new Point();
+
+ /** The piece render position; used as working data when determining
+ * where to render each piece in the sprite. */
+ protected Point _renderPos = new Point();
+
+ /** The number of rows remaining to drop. */
+ protected int _dist;
+
+ /** The piece velocity. */
+ protected float _vel = DEFAULT_VELOCITY;
+
+ /** The offsets in columns or rows at which the piece is rendered. */
+ protected int _offx, _offy;
+
+ /** The current piece location in the board. */
+ protected int _row, _col;
+
+ /** The pieces this sprite is displaying. */
+ protected int[] _pieces;
+
+ /** Indicates that the drop sprite is bouncing; see {@link
+ * #setBouncing}. */
+ protected boolean _bouncing;
+
+ // used to compute the column and row increment while rendering the
+ // sprite's pieces based on its orientation
+ // W N E S
+ protected static final int[] ORIENT_DX = { -1, 0, 1, 0 };
+ protected static final int[] ORIENT_DY = { 0, -1, 0, 1 };
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java b/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java
new file mode 100644
index 000000000..087eea917
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java
@@ -0,0 +1,16 @@
+//
+// $Id: DropSpriteObserver.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+/**
+ * Provides notifications for drop puzzle specific stuff.
+ */
+public interface DropSpriteObserver
+{
+ /**
+ * Called when the drop sprite has moved completely to the specified
+ * board coordinates.
+ */
+ public void pieceMoved (DropSprite sprite, long when, int col, int row);
+}
diff --git a/src/java/com/threerings/puzzle/drop/client/NextBlockView.java b/src/java/com/threerings/puzzle/drop/client/NextBlockView.java
new file mode 100644
index 000000000..1beca3418
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/client/NextBlockView.java
@@ -0,0 +1,93 @@
+//
+// $Id: NextBlockView.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.client;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+
+import javax.swing.JComponent;
+import javax.swing.border.Border;
+
+import com.threerings.media.image.Mirage;
+import com.threerings.util.DirectionCodes;
+
+/**
+ * The next block view displays an image representing the next drop block
+ * to appear in the game.
+ */
+public class NextBlockView extends JComponent
+ implements DirectionCodes
+{
+ /**
+ * Constructs a next block view.
+ */
+ public NextBlockView (DropBoardView view, int pwid, int phei, int orient)
+ {
+ // save things off
+ _view = view;
+ _pwid = pwid;
+ _phei = phei;
+ _orient = orient;
+
+ // configure the component
+ setOpaque(false);
+ }
+
+ /**
+ * Sets the pieces displayed by the view.
+ */
+ public void setPieces (int[] pieces)
+ {
+ _pieces = pieces;
+ repaint();
+ }
+
+ // documentation inherited
+ public void paintComponent (Graphics g)
+ {
+ super.paintComponent(g);
+
+ // draw the pieces
+ Graphics2D gfx = (Graphics2D)g;
+ if (_pieces != null) {
+ Dimension size = getSize();
+ int xpos = (_orient == VERTICAL) ? 0 : (size.width - _pwid);
+ int ypos = (_orient == VERTICAL) ? (size.height - _phei) : 0;
+
+ for (int ii = 0; ii < _pieces.length; ii++) {
+ Mirage image = _view.getPieceImage(_pieces[ii]);
+ image.paint(gfx, xpos, ypos);
+ if (_orient == VERTICAL) {
+ ypos -= _phei;
+ } else {
+ xpos -= _pwid;
+ }
+ }
+ }
+ }
+
+ // documentation inherited
+ public Dimension getPreferredSize ()
+ {
+ int wid = (_orient == VERTICAL) ? _pwid : (2 * _pwid);
+ int hei = (_orient == VERTICAL) ? (2 * _phei) : _phei;
+ return new Dimension(wid, hei);
+ }
+
+ /** The drop board view from which we obtain piece images. */
+ protected DropBoardView _view;
+
+ /** The pieces displayed by this view. */
+ protected int[] _pieces;
+
+ /** The piece dimensions in pixels. */
+ protected int _pwid, _phei;
+
+ /** The view orientation; one of {@link #HORIZONTAL} or {@link
+ * #VERTICAL}. */
+ protected int _orient;
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/ByteDropBoard.java b/src/java/com/threerings/puzzle/drop/data/ByteDropBoard.java
new file mode 100644
index 000000000..cea1916e8
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/ByteDropBoard.java
@@ -0,0 +1,152 @@
+//
+// $Id: ByteDropBoard.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import java.util.Arrays;
+
+import com.threerings.puzzle.Log;
+
+/**
+ * The byte drop board extends the {@link DropBoard}, making use of a
+ * byte array of pieces to store the board contents.
+ */
+public class ByteDropBoard extends DropBoard
+{
+ /**
+ * Constructs an empty byte drop board for use when unserializing.
+ */
+ public ByteDropBoard ()
+ {
+ this(null, 0, 0);
+ }
+
+ /**
+ * Constructs a byte drop board of the given dimensions with its
+ * pieces initialized to 0.
+ */
+ public ByteDropBoard (int bwid, int bhei)
+ {
+ this(new byte[bwid*bhei], bwid, bhei);
+ fill(PIECE_NONE);
+ }
+
+ /**
+ * Constructs a byte drop board of the given dimensions with its
+ * pieces initialized to the given piece.
+ */
+ public ByteDropBoard (int bwid, int bhei, byte piece)
+ {
+ this(new byte[bwid*bhei], bwid, bhei);
+ fill(piece);
+ }
+
+ /**
+ * Constructs a byte drop board with the given board and dimensions.
+ */
+ public ByteDropBoard (byte[] board, int bwid, int bhei)
+ {
+ _board = board;
+ _bwid = bwid;
+ _bhei = bhei;
+ }
+
+ // documentation inherited from interface
+ public int getPiece (int col, int row)
+ {
+ try {
+ return _board[(row*_bwid) + col];
+ } catch (Exception e) {
+ Log.warning("Failed getting piece [col=" + col +
+ ", row=" + row + ", error=" + e + "].");
+ Log.logStackTrace(e);
+ return -1;
+ }
+ }
+
+ // documentation inherited from interface
+ public void fill (int piece)
+ {
+ Arrays.fill(_board, (byte)piece);
+ }
+
+ /**
+ * Sets the board data and board dimensions.
+ */
+ public void setBoard (byte[] board, int bwid, int bhei)
+ {
+ _board = board;
+ _bwid = bwid;
+ _bhei = bhei;
+ }
+
+ /**
+ * Sets the board pieces.
+ */
+ public void setBoard (byte[] board)
+ {
+ int size = (_bwid*_bhei);
+ if (board.length < size) {
+ Log.warning("Attempt to set board with invalid data size " +
+ "[len=" + board.length + ", expected=" + size + "].");
+ return;
+ }
+
+ _board = board;
+ }
+
+ // documentation inherited from interface
+ public boolean setPiece (int col, int row, int piece)
+ {
+ if (col >= 0 && row >= 0 && col < _bwid && row < _bhei) {
+ _board[(row*_bwid) + col] = (byte)piece;
+ return true;
+
+ } else {
+ Log.warning("Attempt to set piece outside board bounds " +
+ "[col=" + col + ", row=" + row + ", p=" + piece + "].");
+ return false;
+ }
+ }
+
+ // documentation inherited from interface
+ public void copyInto (DropBoard board)
+ {
+ // make sure the target board is a valid target
+ if (board.getWidth() != _bwid || board.getHeight() != _bhei) {
+ Log.warning("Can't copy board into destination board with " +
+ "different dimensions [src=" + this +
+ ", dest=" + board + "].");
+ return;
+ }
+
+ // copy our pieces directly into the board, avoiding any unsightly
+ // object allocation which is largely the point of this method,
+ // after all.
+ byte[] dest = ((ByteDropBoard)board).getBoard();
+ System.arraycopy(_board, 0, dest, 0, (_bwid*_bhei));
+ }
+
+ // documentation inherited
+ public Object clone ()
+ {
+ ByteDropBoard board = (ByteDropBoard)super.clone();
+ int size = _bwid*_bhei;
+ board._board = new byte[size];
+ System.arraycopy(_board, 0, board._board, 0, size);
+ return board;
+ }
+
+ /**
+ * Returns the raw board data associated with this board. One
+ * shouldn't fiddle about with this unless one knows what one is
+ * doing.
+ */
+ public byte[] getBoard ()
+ {
+ return _board;
+ }
+
+ /** The board data. */
+ protected byte[] _board;
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropBoard.java b/src/java/com/threerings/puzzle/drop/data/DropBoard.java
new file mode 100644
index 000000000..b218a0a35
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropBoard.java
@@ -0,0 +1,738 @@
+//
+// $Id: DropBoard.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+
+import java.util.Arrays;
+
+import org.apache.commons.lang.Strings;
+
+import com.threerings.util.DirectionCodes;
+import com.threerings.util.DirectionUtil;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.drop.client.DropControllerDelegate;
+import com.threerings.puzzle.drop.data.DropPieceCodes;
+import com.threerings.puzzle.drop.util.DropBoardUtil;
+
+/**
+ * An abstract class that provides for various useful logical operations
+ * to be enacted on a two-dimensional board and provides an easier
+ * mechanism for referencing pieces by position.
+ */
+public abstract class DropBoard extends Board
+ implements DropPieceCodes
+{
+ /** The rotation constant for rotation around a central piece. */
+ public static final int RADIAL_ROTATION = 0;
+
+ /** The rotation constant for rotation wherein the block occupies the
+ * same columns when rotating. */
+ public static final int INPLACE_ROTATION = 1;
+
+ /** An operation that does naught but clear pieces, which proves to be
+ * generally useful. */
+ public static final PieceOperation CLEAR_OP = new PieceOperation () {
+ public boolean execute (DropBoard board, int col, int row) {
+ board.setPiece(col, row, PIECE_NONE);
+ return true;
+ }
+ };
+
+ /**
+ * An interface to be implemented by classes that would like to apply
+ * some operation to each piece in a column or row segment in the
+ * board.
+ */
+ public interface PieceOperation
+ {
+ /**
+ * Called for each piece in the board segment the operation is
+ * being applied to.
+ *
+ * @return true if the operation should continue to be applied if
+ * being applied to multiple pieces, or false if it should
+ * terminate after this application.
+ */
+ public boolean execute (DropBoard board, int col, int row);
+ }
+
+ /**
+ * Returns the width of the board in columns.
+ */
+ public int getWidth()
+ {
+ return _bwid;
+ }
+
+ /**
+ * Returns the height of the board in rows.
+ */
+ public int getHeight()
+ {
+ return _bhei;
+ }
+
+ /**
+ * Returns the piece at the given column and row in the board.
+ */
+ public abstract int getPiece (int col, int row);
+
+ /**
+ * Returns the distance the piece at the given column and row can drop
+ * until it hits a non-empty piece (defined as {@link #PIECE_NONE}).
+ */
+ public int getDropDistance (int col, int row)
+ {
+ int dist = 0;
+ for (int yy = row + 1; yy < _bhei; yy++) {
+ if (getPiece(col, yy) != PIECE_NONE) {
+ return dist;
+ }
+ dist++;
+ }
+ return dist;
+ }
+
+ /**
+ * Returns whether the given row in the board is empty.
+ */
+ public boolean isRowEmpty (int row)
+ {
+ for (int col = 0; col < _bwid; col++) {
+ if (getPiece(col, row) != PIECE_NONE) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether all of the pieces at the given coordinates can be
+ * dropped one row.
+ */
+ public boolean isValidDrop (int[] rows, int[] cols, float pctdone)
+ {
+ int bottom = _bhei - 1;
+ for (int ii = 0; ii < rows.length; ii++) {
+ // pieces at bottom can't be dropped
+ if (rows[ii] >= bottom) {
+ return false;
+ }
+
+ // pieces with pieces below them can't be dropped
+ int row = rows[ii] + 1;
+ if (row >= 0 && getPiece(cols[ii], row) != PIECE_NONE) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if the specified coordinate is within the bounds of
+ * the board, false if it is not.
+ */
+ public boolean inBounds (int col, int row)
+ {
+ return (col >= 0 && row >= 0 && col < getWidth() && row < getHeight());
+ }
+
+ /**
+ * Returns whether the specified block in the board is empty. The
+ * block is allowed to occupy space off the top of the board as long
+ * as it is within the horizontal board bounds.
+ *
+ * @param x the left coordinate of the block.
+ * @param y the bottom coordinate of the block.
+ * @param wid the width of the block.
+ * @param hei the height of the block.
+ */
+ public boolean isBlockEmpty (int col, int row, int wid, int hei)
+ {
+ for (int ypos = row; ypos > (row - hei); ypos--) {
+ for (int xpos = col; xpos < (col + wid); xpos++) {
+ // only allow movement off the top of the board that's
+ // within the horizontal screen bounds and in a column
+ // that's not topped out
+ if (ypos < 0) {
+ if ((xpos < 0 || xpos >= _bwid) ||
+ (getPiece(xpos, 0) != PIECE_NONE)) {
+ return false;
+ } else {
+ continue;
+ }
+ }
+
+ // don't allow movement outside the side or bottom bounds
+ if (xpos < 0 ||
+ xpos >= _bwid ||
+ ypos >= _bhei) {
+ return false;
+ }
+
+ // make sure no piece is present
+ if (getPiece(xpos, ypos) != PIECE_NONE) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Rotates the given block in the given direction and returns its
+ * final state as (orient, col, row), where
+ * orient is the final orientation of the drop block;
+ * col and row are the final column and row
+ * coordinates, respectively, of the central drop block piece.
+ */
+ public int[] getForgivingRotation (
+ int[] rows, int[] cols, int orient, int dir, int rtype, float pctdone)
+ {
+ int px = cols[0], py = rows[0];
+ int epx = cols[1], epy = rows[1];
+
+// Log.info("Starting rotation [px=" + px + ", py=" + py +
+// ", orient=" + orient + ", pctdone=" + pctdone + "].");
+
+ // try rotating the block in the given direction through all four
+ // possible orientations
+ for (int ii = 0; ii < 4; ii++) {
+ int oidx = orient/2;
+
+ // adjust the position of the central piece
+ px += ROTATE_DX[rtype][dir][oidx];
+ py += ROTATE_DY[rtype][dir][oidx];
+
+ // update the orientation
+ orient = DropBoardUtil.getRotatedOrientation(orient, dir);
+ oidx = orient/2;
+
+ // because isBlockEmpty() always assumes the origin of the
+ // block is in the lower-left, we need to adjust the
+ // coordinates of the drop block's "central" piece accordingly
+ int ox = px + ORIENT_ORIGIN_DX[oidx];
+ int oy = py + ORIENT_ORIGIN_DY[oidx];
+
+ // if we're less than 50 percent through with our fall, we
+ // want to check our current coordinates for validity; if
+ // we're more, we want to check the row below our current
+ // coordinates
+ if (pctdone > 0.5) {
+ oy += 1;
+ }
+
+ // try each of three coercions: nothing, one left, one right
+ for (int c = 0; c < COERCE_DX.length; c++) {
+ int cx = COERCE_DX[c];
+
+ // check if our hypothetical new coordinates are empty
+ if (isBlockEmpty(ox + cx, oy,
+ ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) {
+// Log.info("Block is empty [ox=" + ox + ", cx=" + cx +
+// ", oy=" + oy + ", oidx=" + oidx +
+// ", orient=" + DirectionUtil.toShortString(orient) +
+// ", owid=" + ORIENT_WIDTHS[oidx] +
+// ", ohei=" + ORIENT_HEIGHTS[oidx] + "].");
+ return new int[] { orient, px + cx, py };
+ }
+ }
+
+ // if our piece is facing south and we're using radial
+ // rotation then we need to try popping the piece up a row to
+ // check for a fit
+ if (rtype == RADIAL_ROTATION && orient == SOUTH) {
+ // check if our hypothetical new coordinates are empty
+ if (isBlockEmpty(ox, oy - 1,
+ ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) {
+// Log.info("Popped-up block is empty [ox=" + ox +
+// ", oy=" + (oy - 1) + ", oidx=" + oidx +
+// ", orient=" + DirectionUtil.toShortString(orient) +
+// ", owid=" + ORIENT_WIDTHS[oidx] +
+// ", ohei=" + ORIENT_HEIGHTS[oidx] +
+// ", bhei=" + _bhei + "].");
+ return new int[] { orient, px, py - 1 };
+ }
+ }
+ }
+
+ // this should never happen since even in the most tightly
+ // constrained case where the block is entirely surrounded by
+ // other pieces there are always two valid orientations.
+ Log.warning("**** We're horked and couldn't rotate at all!");
+// System.exit(0);
+ return null;
+ }
+
+ /**
+ * Returns a {@link Point} object containing the coordinates to place
+ * the bottom-left of the given block at after moving it the given
+ * distance on the x- and y-axes, or null if the move is
+ * not valid. Note that only the final block position is checked.
+ *
+ * @param board the board the piece is moving within.
+ * @param col the leftmost column of the block.
+ * @param row the bottommost row of the block.
+ * @param wid the width of the block.
+ * @param hei the height of the block.
+ * @param dx the distance to move the block in columns.
+ * @param dy the distance to move the block in rows.
+ * @param pctdone the percentage of the inter-block distance that the
+ * piece has fallen thus far.
+ */
+ public Point getForgivingMove (
+ int col, int row, int wid, int hei, int dx, int dy, float pctdone)
+ {
+ // try placing the block in the desired position and, failing
+ // that, at the same horizontal position but one row farther down
+ int xpos = col + dx, ypos = row + dy;
+
+ // if we're above the halfway mark, we check our current neighbors
+ // to see if we can move there; if we're below the halfway mark we
+ // check the next row down
+ if (pctdone >= 0.5) {
+ ypos += 1;
+ }
+
+ // if the block we wish to occupy is empty, we're all good
+ return (isBlockEmpty(xpos, ypos, wid, hei)) ?
+ new Point(xpos, row + dy) : null;
+ }
+
+ /**
+ * Populates the given array with the column levels for this board.
+ */
+ public void getColumnLevels (byte[] columns)
+ {
+ int bwid = getWidth(), bhei = getHeight();
+ for (int col = 0; col < bwid; col++) {
+ int dist = getDropDistance(col, -1);
+ columns[col] = (byte)(bhei - dist);
+ }
+ }
+
+ /**
+ * Called by the {@link DropControllerDelegate} when it's time to
+ * apply a rising row of pieces to the board. Shifts all of the
+ * pieces in the given board up one row and places the given row of
+ * pieces at the bottom of the board.
+ */
+ public void applyRisingPieces (int[] pieces)
+ {
+ // shift all pieces up one row
+ int end = _bhei - 1;
+ for (int yy = 0; yy < end; yy++) {
+ for (int xx = 0; xx < _bwid; xx++) {
+ setPiece(xx, yy, getPiece(xx, yy + 1));
+ }
+ }
+
+ // apply the row pieces to the board
+ int ypos = _bhei - 1;
+ for (int xx = 0; xx < _bwid; xx++) {
+ setPiece(xx, ypos, pieces[xx]);
+ }
+ }
+
+ /**
+ * Returns true if the specified row (which count down, with zero at
+ * the top of the board) contains any pieces.
+ *
+ * @param row the row to check for pieces.
+ * @param blankPiece the blank piece value, non-instances of which
+ * will be sought.
+ */
+ public boolean rowContainsPieces (int row, int blankPiece)
+ {
+ for (int x = 0; x < _bwid; x++) {
+ if (getPiece(x, row) != blankPiece) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Fills the board contents with the given piece.
+ */
+ public abstract void fill (int piece);
+
+ /**
+ * Sets the piece at the given coordinates.
+ *
+ * @return true if the piece was set, false if it was invalid.
+ */
+ public abstract boolean setPiece (int col, int row, int piece);
+
+ /**
+ * Sets the pieces within the specified rectangle to the given piece.
+ */
+ public void setRect (int x, int y, int width, int height, int piece)
+ {
+ for (int yy = y; yy > (y - height); yy--) {
+ for (int xx = x; xx < (x + width); xx++) {
+ setPiece(xx, yy, piece);
+ }
+ }
+ }
+
+ /**
+ * Sets the pieces in the given board segment to the specified piece.
+ *
+ * @param dir the direction of the segment; one of {@link #HORIZONTAL}
+ * or {@link #VERTICAL}.
+ * @param col the starting column of the segment.
+ * @param row the starting row of the segment.
+ * @param len the length of the segment in pieces.
+ * @param piece the piece to set in the segment.
+ *
+ * @return false if the segment was only partially applied because
+ * some pieces were outside the bounds of the board, true if it was
+ * completely applied.
+ */
+ public boolean setSegment (int dir, int col, int row, int len, int piece)
+ {
+ _setPieceOp.init(piece);
+ applyOp(dir, col, row, len, _setPieceOp);
+ return !_setPieceOp.getError();
+ }
+
+ /**
+ * Sets the pieces in the given board segment to the specified pieces.
+ *
+ * @param dir the direction of the segment; one of {@link #HORIZONTAL}
+ * or {@link #VERTICAL}.
+ * @param col the starting column of the segment.
+ * @param row the starting row of the segment.
+ * @param piece the piece to set in the segment.
+ */
+ public void setSegment (int dir, int col, int row, int[] pieces)
+ {
+ _setSegmentOp.init(dir, pieces);
+ applyOp(dir, col, row, pieces.length, _setSegmentOp);
+ }
+
+ /**
+ * Applies a specified {@link PieceOperation} to all pieces in the
+ * specified row or column starting at the specified coordinates and
+ * spanning the remainder of the row or column (depending on the
+ * application direction) in the board.
+ *
+ * @param dir the direction to iterate in; one of {@link #HORIZONTAL}
+ * or {@link #VERTICAL}.
+ * @param col the starting column of the segment.
+ * @param row the starting row of the segment.
+ * @param op the piece operation to apply to each piece.
+ */
+ public void applyOp (int dir, int col, int row, PieceOperation op)
+ {
+ int len = (dir == HORIZONTAL) ? _bwid - col : row + 1;
+ applyOp(dir, col, row, len, op);
+ }
+
+ /**
+ * Applies a specified {@link PieceOperation} to all pieces in a row
+ * or column segment starting at the specified coordinates and of the
+ * specified length in the board.
+ *
+ * @param dir the direction to iterate in; one of {@link #HORIZONTAL}
+ * or {@link #VERTICAL}.
+ * @param col the starting leftmost column of the segment.
+ * @param row the starting bottommost row of the segment.
+ * @param len the number of pieces in the segment.
+ * @param op the piece operation to apply to each piece.
+ */
+ public void applyOp (int dir, int col, int row, int len, PieceOperation op)
+ {
+ if (dir == HORIZONTAL) {
+ int end = Math.min(col + len, _bwid);
+ for (int ii = col; ii < end; ii++) {
+ if (!op.execute(this, ii, row)) {
+ break;
+ }
+ }
+
+ } else {
+ int end = Math.max(row - len, -1);
+ for (int ii = row; ii > end; ii--) {
+ if (!op.execute(this, col, ii)) {
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Applies a specified {@link PieceOperation} to the specified piece
+ * in the board.
+ *
+ * @param col the column of the piece.
+ * @param row the row of the piece.
+ * @param op the piece operation to apply to the piece.
+ */
+ public void applyOp (int col, int row, PieceOperation op)
+ {
+ op.execute(this, col, row);
+ }
+
+ // documentation inherited from interface
+ public void dump ()
+ {
+ dumpAndCompare(null);
+ }
+
+ // documentation inherited from interface
+ public void dumpAndCompare (Board other)
+ {
+ if (other != null && !(other instanceof DropBoard)) {
+ throw new IllegalArgumentException(
+ "Can't compare drop board to non-drop-board.");
+ }
+
+ DropBoard dother = (DropBoard)other;
+ int padwid = getPadWidth();
+ if (other != null) {
+ // padwid = (padwid * 2) + 1;
+ padwid *= 2;
+ }
+
+ for (int y = 0; y < _bhei; y++) {
+ StringBuffer buf = new StringBuffer();
+ for (int x = 0; x < _bwid; x++) {
+ int piece = getPiece(x, y);
+ String str = formatPiece(piece);
+ if (dother != null) {
+ int opiece = dother.getPiece(x, y);
+ if (opiece != piece) {
+ str += "|" + formatPiece(opiece);
+ }
+ }
+ buf.append(Strings.rightPad(str, padwid));
+ }
+ Log.warning(buf.toString());
+ }
+ }
+
+ /**
+ * Returns the number of characters to which a single piece should be
+ * padded when dumping the board for debugging purposes.
+ */
+ protected int getPadWidth ()
+ {
+ return DEFAULT_PAD_WIDTH;
+ }
+
+ /**
+ * Returns a string representation of the given piece for use when
+ * dumping the board.
+ */
+ protected String formatPiece (int piece)
+ {
+ return (piece == PIECE_NONE) ? "." : String.valueOf(piece);
+ }
+
+ /** Returns a string representation of this instance. */
+ public String toString ()
+ {
+ StringBuffer buf = new StringBuffer();
+ buf.append("[wid=").append(_bwid);
+ buf.append(", hei=").append(_bhei);
+ return buf.append("]").toString();
+ }
+
+ // documentation inherited from interface
+ public boolean equals (Board other)
+ {
+ // make sure we're comparing the same class type
+ if (!this.getClass().getName().equals(other.getClass().getName())) {
+ throw new IllegalArgumentException(
+ "Can't compare board of different class types " +
+ "[src=" + this.getClass().getName() +
+ ", other=" + other.getClass().getName() + "].");
+ }
+
+ // we're certainly not equal if our dimensions differ
+ DropBoard dother = (DropBoard)other;
+ if (dother.getWidth() != _bwid ||
+ dother.getHeight() != _bhei) {
+ return false;
+ }
+
+ // check each board piece
+ for (int xx = 0; xx < _bwid; xx++) {
+ for (int yy = 0; yy < _bhei; yy++) {
+ if (getPiece(xx, yy) != dother.getPiece(xx, yy)) {
+ return false;
+ }
+ }
+ }
+
+ // we're equal
+ return true;
+ }
+
+ /**
+ * Returns whether the given coordinates are within the board bounds.
+ */
+ public boolean isValidPosition (int x, int y)
+ {
+ return (x >= 0 &&
+ y >= 0 &&
+ x < _bwid &&
+ y < _bhei);
+ }
+
+ /**
+ * Returns the bounds of this board. Note that a single rectangle is
+ * re-used internally and so the caller should not modify the
+ * returned rectangle.
+ */
+ public Rectangle getBounds ()
+ {
+ if (_bounds == null) {
+ _bounds = new Rectangle(0, 0, _bwid, _bhei);
+ }
+ return _bounds;
+ }
+
+ /**
+ * Returns the size of the board in pieces.
+ */
+ public int size ()
+ {
+ return (_bwid*_bhei);
+ }
+
+ /**
+ * Copies the contents of this board directly into the supplied board,
+ * overwriting the destination board in its entirety.
+ */
+ public abstract void copyInto (DropBoard board);
+
+ /** An operation that sets the pieces in a board segment to a
+ * specified array of pieces. */
+ protected static class SetSegmentOperation implements PieceOperation
+ {
+ /**
+ * Sets the array of pieces to be placed in the board segment.
+ */
+ public void init (int dir, int[] pieces)
+ {
+ _dir = dir;
+ _pieces = pieces;
+ _idx = (dir == HORIZONTAL) ? _pieces.length - 1 : 0;
+ }
+
+ // documentation inherited
+ public boolean execute (DropBoard board, int col, int row)
+ {
+ if (_dir == HORIZONTAL) {
+ board.setPiece(col, row, _pieces[_idx--]);
+ } else {
+ board.setPiece(col, row, _pieces[_idx++]);
+ }
+ return true;
+ }
+
+ /** The orientation in which the pieces are to be placed. */
+ protected int _dir;
+
+ /** The current piece index. */
+ protected int _idx;
+
+ /** The pieces to set in the board. */
+ protected int[] _pieces;
+ }
+
+ /** An operation that sets all pieces to a specified piece. */
+ protected static class SetPieceOperation implements PieceOperation
+ {
+ /**
+ * Sets the piece to be placed in the board segment.
+ */
+ public void init (int piece)
+ {
+ _piece = piece;
+ _error = false;
+ }
+
+ /**
+ * Returns true if we attempted to set a piece outside the bounds
+ * of the board during the course of our operation.
+ */
+ public boolean getError ()
+ {
+ return _error;
+ }
+
+ // documentation inherited
+ public boolean execute (DropBoard board, int col, int row)
+ {
+ if (!board.setPiece(col, row, _piece)) {
+ _error = true;
+ }
+ return true;
+ }
+
+ /** The piece to set in the board. */
+ protected int _piece;
+
+ /** Set to true if an error occurred setting a piece. */
+ protected boolean _error;
+ }
+
+ /** The board dimensions in pieces. */
+ protected int _bwid, _bhei;
+
+ /** The bounds of this board. */
+ protected transient Rectangle _bounds;
+
+ // used to reconfigure the block when rotating it
+ protected static final int[][][] ROTATE_DX = {
+ // W N E S W N E S
+ {{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL
+ {{ -1, 1, 0, 0 }, { -1, 0, 0, 1 }}, // INPLACE
+ // CCW CW
+ };
+
+ // used to reconfigure the block when rotating it
+ protected static final int[][][] ROTATE_DY = {
+ // W N E S W N E S
+ {{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL
+ {{ -1, 0, 0, 1 }, { 0, 0, -1, 1 }}, // INPLACE
+ // CCW CW
+ };
+
+ // used to compute the bounds of the isBlockEmpty() block based on the
+ // drop block's orientation and "root" block position
+ protected static final int[] ORIENT_WIDTHS = { 2, 1, 2, 1 };
+ protected static final int[] ORIENT_HEIGHTS = { 1, 2, 1, 2 };
+
+ // used to compute the origin of the isBlockEmpty() block based on the
+ // drop block's orientation and "root" block position
+ protected static final int[] ORIENT_ORIGIN_DX = { -1, 0, 0, 0 };
+ protected static final int[] ORIENT_ORIGIN_DY = { 0, 0, 0, 1 };
+
+ // used to coerce the block when rotating either a space to the left
+ // or right (or not at all)
+ protected static final int[] COERCE_DX = { 0, 1, -1 };
+
+ /** The operation used to set the pieces in a board segment. */
+ protected static final SetSegmentOperation _setSegmentOp =
+ new SetSegmentOperation();
+
+ /** The operation used to set a piece in a board segment. */
+ protected static final SetPieceOperation _setPieceOp =
+ new SetPieceOperation();
+
+ /** The number of characters to which each board piece should be
+ * padded when outputting for debug purposes. */
+ protected static final int DEFAULT_PAD_WIDTH = 3;
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java b/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java
new file mode 100644
index 000000000..3570e00c1
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java
@@ -0,0 +1,71 @@
+//
+// $Id: DropBoardSummary.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+
+/**
+ * Provides a summary of a {@link DropBoard}.
+ */
+public class DropBoardSummary extends BoardSummary
+{
+ /** The row levels for each column. */
+ public byte[] columns;
+
+ /**
+ * Constructs an empty drop board summary for use when un-serializing.
+ */
+ public DropBoardSummary ()
+ {
+ // nothing for now
+ }
+
+ /**
+ * Constructs a drop board summary that retrieves board information
+ * from the supplied board when summarizing.
+ */
+ public DropBoardSummary (Board board)
+ {
+ super(board);
+
+ // create the columns array
+ columns = new byte[_dboard.getWidth()];
+ }
+
+ /**
+ * Returns the column number of the column within the given column
+ * range that contains the most pieces.
+ */
+ public int getHighestColumn (int startx, int endx)
+ {
+ byte value = columns[startx];
+ int idx = startx;
+ for (int xx = startx + 1; xx <= endx; xx++) {
+ if (columns[xx] > value) {
+ value = columns[xx];
+ idx = xx;
+ }
+ }
+ return idx;
+ }
+
+ // documentation inherited
+ public void setBoard (Board board)
+ {
+ super.setBoard(board);
+
+ _dboard = (DropBoard)board;
+ }
+
+ // documentation inherited
+ public void summarize ()
+ {
+ // update the board column levels
+ _dboard.getColumnLevels(columns);
+ }
+
+ /** The drop board we're summarizing. */
+ protected transient DropBoard _dboard;
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropCodes.java b/src/java/com/threerings/puzzle/drop/data/DropCodes.java
new file mode 100644
index 000000000..11fdd8590
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropCodes.java
@@ -0,0 +1,21 @@
+//
+// $Id: DropCodes.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import com.threerings.puzzle.data.PuzzleGameCodes;
+
+/**
+ * Contains codes used by the drop game services.
+ */
+public interface DropCodes extends PuzzleGameCodes
+{
+ /** The message bundle identifier for drop puzzle messages. */
+ public static final String DROP_MESSAGE_BUNDLE = "puzzle.drop";
+
+ /** The name of the control stream that provides drop pieces. */
+ public static final String DROP_STREAM = "drop";
+
+ /** The name of the control stream that provides rise pieces. */
+ public static final String RISE_STREAM = "rise";
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropConfig.java b/src/java/com/threerings/puzzle/drop/data/DropConfig.java
new file mode 100644
index 000000000..2d35eedd0
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropConfig.java
@@ -0,0 +1,17 @@
+//
+// $Id: DropConfig.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+/**
+ * Provides access to the configuration information for a drop puzzle
+ * game.
+ */
+public interface DropConfig
+{
+ /** Returns the board width in pieces. */
+ public int getBoardWidth ();
+
+ /** Returns the board height in pieces. */
+ public int getBoardHeight ();
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropLogic.java b/src/java/com/threerings/puzzle/drop/data/DropLogic.java
new file mode 100644
index 000000000..addfb80a3
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropLogic.java
@@ -0,0 +1,23 @@
+//
+// $Id: DropLogic.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+/**
+ * Describes the features and configuration desired for a given drop
+ * puzzle game.
+ */
+public interface DropLogic
+{
+ /**
+ * Returns whether the puzzle game would like to make use of the
+ * manipulable block dropping functionality.
+ */
+ public boolean useBlockDropping ();
+
+ /**
+ * Returns whether the puzzle game would like to make use of the
+ * rising board functionality.
+ */
+ public boolean useBoardRising ();
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java b/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java
new file mode 100644
index 000000000..94b04144b
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java
@@ -0,0 +1,19 @@
+//
+// $Id: DropPieceCodes.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import com.threerings.util.DirectionCodes;
+
+/**
+ * The drop piece codes interface contains constants common to the drop
+ * game package.
+ */
+public interface DropPieceCodes extends DirectionCodes
+{
+ /** The piece constant denoting an empty board piece. */
+ public static final byte PIECE_NONE = -1;
+
+ /** The number of pieces in a drop block. */
+ public static final int DROP_BLOCK_PIECE_COUNT = 2;
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java b/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java
new file mode 100644
index 000000000..8e4fd0073
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java
@@ -0,0 +1,43 @@
+//
+// $Id: SegmentInfo.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import com.samskivert.util.StringUtil;
+
+import com.threerings.util.DirectionCodes;
+
+/**
+ * Describes a segment of pieces in a {@link DropBoard}.
+ */
+public class SegmentInfo
+{
+ /** The segment's direction; one of {@link DirectionCodes#HORIZONTAL}
+ * or {@link DirectionCodes#VERTICAL}. */
+ public int dir;
+
+ /** The segment's lower-left board coordinates. */
+ public int x, y;
+
+ /** The segment's length in pieces. */
+ public int len;
+
+ /**
+ * Constructs a segment info object.
+ */
+ public SegmentInfo (int dir, int x, int y, int len)
+ {
+ this.dir = dir;
+ this.x = x;
+ this.y = y;
+ this.len = len;
+ }
+
+ /**
+ * Returns a string representation of this instance.
+ */
+ public String toString ()
+ {
+ return StringUtil.fieldsToString(this);
+ }
+}
diff --git a/src/java/com/threerings/puzzle/drop/data/ShortDropBoard.java b/src/java/com/threerings/puzzle/drop/data/ShortDropBoard.java
new file mode 100644
index 000000000..8bb618417
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/data/ShortDropBoard.java
@@ -0,0 +1,151 @@
+//
+// $Id: ShortDropBoard.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.data;
+
+import java.util.Arrays;
+
+import com.threerings.puzzle.Log;
+
+/**
+ * The short drop board extends the {@link DropBoard}, making use of a
+ * short array of pieces to store the board contents.
+ */
+public class ShortDropBoard extends DropBoard
+{
+ /**
+ * Constructs an empty short drop board for use when unserializing.
+ */
+ public ShortDropBoard ()
+ {
+ this(null, 0, 0);
+ }
+
+ /**
+ * Constructs a short drop board of the given dimensions with its
+ * pieces initialized to PIECE_NONE.
+ */
+ public ShortDropBoard (int bwid, int bhei)
+ {
+ this(new short[bwid*bhei], bwid, bhei);
+ fill(PIECE_NONE);
+ }
+
+ /**
+ * Constructs a short drop board of the given dimensions with its
+ * pieces initialized to the given piece.
+ */
+ public ShortDropBoard (int bwid, int bhei, short piece)
+ {
+ this(new short[bwid*bhei], bwid, bhei);
+ fill(piece);
+ }
+
+ /**
+ * Constructs a short drop board with the given board and dimensions.
+ */
+ public ShortDropBoard (short[] board, int bwid, int bhei)
+ {
+ _board = board;
+ _bwid = bwid;
+ _bhei = bhei;
+ }
+
+ // documentation inherited from interface
+ public int getPiece (int col, int row)
+ {
+ try {
+ return _board[(row*_bwid) + col];
+ } catch (Exception e) {
+ Log.warning("Failed getting piece [col=" + col +
+ ", row=" + row + ", error=" + e + "].");
+ Log.logStackTrace(e);
+ return -1;
+ }
+ }
+
+ // documentation inherited from interface
+ public void fill (int piece)
+ {
+ Arrays.fill(_board, (short)piece);
+ }
+
+ /**
+ * Sets the board data and board dimensions.
+ */
+ public void setBoard (short[] board, int bwid, int bhei)
+ {
+ _board = board;
+ _bwid = bwid;
+ _bhei = bhei;
+ }
+
+ /**
+ * Sets the board pieces.
+ */
+ public void setBoard (short[] board)
+ {
+ int size = (_bwid*_bhei);
+ if (board.length < size) {
+ Log.warning("Attempt to set board with invalid data size " +
+ "[len=" + board.length + ", expected=" + size + "].");
+ return;
+ }
+
+ _board = board;
+ }
+
+ // documentation inherited from interface
+ public boolean setPiece (int col, int row, int piece)
+ {
+ if (col >= 0 && row >= 0 && col < _bwid && row < _bhei) {
+ _board[(row*_bwid) + col] = (short)piece;
+ return true;
+
+ } else {
+ Log.warning("Attempt to set piece outside board bounds " +
+ "[col=" + col + ", row=" + row + ", p=" + piece + "].");
+ return false;
+ }
+ }
+
+ // documentation inherited from interface
+ public void copyInto (DropBoard board)
+ {
+ // make sure the target board is a valid target
+ if (board.getWidth() != _bwid || board.getHeight() != _bhei) {
+ Log.warning("Can't copy board into destination board with " +
+ "different dimensions [src=" + this +
+ ", dest=" + board + "].");
+ return;
+ }
+
+ // copy our pieces directly into the board, avoiding any unsightly
+ // object allocation which is largely the point of this method,
+ // after all.
+ short[] dest = ((ShortDropBoard)board).getBoard();
+ System.arraycopy(_board, 0, dest, 0, (_bwid*_bhei));
+ }
+
+ // documentation inherited
+ public Object clone ()
+ {
+ int size = _bwid*_bhei;
+ short[] data = new short[size];
+ System.arraycopy(_board, 0, data, 0, size);
+ return new ShortDropBoard(data, _bwid, _bhei);
+ }
+
+ /**
+ * Returns the raw board data associated with this board. One
+ * shouldn't fiddle about with this unless one knows what one is
+ * doing.
+ */
+ public short[] getBoard ()
+ {
+ return _board;
+ }
+
+ /** The board data. */
+ protected short[] _board;
+}
diff --git a/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java b/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java
new file mode 100644
index 000000000..402f4226e
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java
@@ -0,0 +1,174 @@
+//
+// $Id: DropManagerDelegate.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.server;
+
+import java.util.List;
+
+import com.threerings.util.MessageBundle;
+
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.crowd.data.PlaceObject;
+
+import com.threerings.parlor.game.GameManager;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleObject;
+import com.threerings.puzzle.server.PuzzleManager;
+import com.threerings.puzzle.server.PuzzleManagerDelegate;
+
+import com.threerings.puzzle.drop.data.DropBoard;
+import com.threerings.puzzle.drop.data.DropBoardSummary;
+import com.threerings.puzzle.drop.data.DropCodes;
+import com.threerings.puzzle.drop.data.DropConfig;
+import com.threerings.puzzle.drop.data.DropLogic;
+import com.threerings.puzzle.drop.util.DropPieceProvider;
+import com.threerings.puzzle.drop.util.PieceDropLogic;
+import com.threerings.puzzle.drop.util.PieceDropper;
+
+/**
+ * Provides the necessary support for a puzzle game that involves a
+ * two-dimensional board containing pieces, with new pieces either falling
+ * into the board as a "drop block", or rising into the bottom of the
+ * board in new piece rows, groups of blocks can be "broken" and garbage
+ * can be sent to other players' boards as a result. This is implemented
+ * as a delegate so that the natural hierarchy need not be twisted to
+ * differentiate between puzzles that use piece dropping and those that
+ * don't. Because we have need to structure our hierarchy around things
+ * like whether a puzzle is a duty puzzle, this becomes necessary.
+ *
+ *
A puzzle game using these services will then need to extend this + * delegate, implementing the necessary methods to customize it for the + * particulars of their game and then register it with their game manager + * via {@link GameManager#addDelegate}. + * + *
It also keeps track of, for each player, board level information, + * and player game status. Miscellaneous utility routines are provided + * for checking things like whether the game is over, whether a player is + * still active in the game, and so forth. + * + *
Derived classes are likely to want to override {@link
+ * #getPieceDropLogic}.
+ */
+public abstract class DropManagerDelegate extends PuzzleManagerDelegate
+ implements PuzzleCodes, DropCodes
+{
+ /**
+ * Provides the delegate with a reference to the manager for which it
+ * is delegating as well as the logic object that it uses to determine
+ * how to manage the drop puzzle.
+ */
+ public DropManagerDelegate (PuzzleManager puzmgr, DropLogic logic)
+ {
+ super(puzmgr);
+
+ // save off the puzzle manager
+ _puzmgr = puzmgr;
+
+ // configure the game-specific settings
+ _usedrop = logic.useBlockDropping();
+ _userise = logic.useBoardRising();
+ if (_usedrop && _userise) {
+ Log.warning("Can't use dropping blocks and board rising "+
+ "functionality simultaneously in a drop puzzle game! " +
+ "Falling back to straight dropping.");
+ _userise = false;
+ }
+ }
+
+ // documentation inherited
+ public void didInit (PlaceConfig config)
+ {
+ _dconfig = (DropConfig)config;
+
+ // save things off
+ _bwid = _dconfig.getBoardWidth();
+ _bhei = _dconfig.getBoardHeight();
+
+ super.didInit(config);
+ }
+
+ // documentation inherited
+ public void didStartup (PlaceObject plobj)
+ {
+ super.didStartup(plobj);
+
+ // initialize the drop board array
+ _dboards = new DropBoard[_puzmgr.getPlayerCount()];
+
+ // create the piece dropper if appropriate
+ PieceDropLogic pdl = getPieceDropLogic();
+ if (pdl != null) {
+ _dropper = new PieceDropper(pdl);
+ }
+ }
+
+ // documentation inherited
+ public void gameWillStart ()
+ {
+ super.gameWillStart();
+
+ // get casted references to all player drop boards
+ Board[] board = _puzmgr.getBoards();
+ for (int ii = 0; ii < _puzmgr.getPlayerCount(); ii++) {
+ _dboards[ii] = (DropBoard)board[ii];
+ }
+ }
+
+ /**
+ * Drops any pieces that need dropping on the given player's board and
+ * returns whether any pieces were dropped.
+ */
+ protected boolean dropPieces (DropBoard board)
+ {
+ return (_dropper.dropPieces(board, null).size() > 0);
+ }
+
+ /**
+ * Returns the piece drop logic used to drop any pieces that need
+ * dropping in the board.
+ */
+ protected PieceDropLogic getPieceDropLogic ()
+ {
+ return null;
+ }
+
+ /**
+ * This method should be called by derived classes whenever the player
+ * successfully places a drop block.
+ */
+ protected void placedBlock (int pidx)
+ {
+ // update the player's board levels
+ _puzmgr.updateBoardSummary(pidx);
+ }
+
+ /** The puzzle manager. */
+ protected PuzzleManager _puzmgr;
+
+ /** The drop game board for each player. */
+ protected DropBoard[] _dboards;
+
+ /** The drop game config object. */
+ protected DropConfig _dconfig;
+
+ /** Whether the game is using drop block functionality. */
+ protected boolean _usedrop;
+
+ /** Whether the game is using board rising functionality. */
+ protected boolean _userise;
+
+ /** The board dimensions in pieces. */
+ protected int _bwid, _bhei;
+
+ /** The piece dropper used to drop pieces in the board if the puzzle
+ * chooses to make use of piece dropping functionality. */
+ protected PieceDropper _dropper;
+
+ /** Used to limit the maximum number of board update loops permitted
+ * before assuming something's gone horribly awry and aborting. */
+ protected static final int MAX_UPDATE_LOOPS = 100;
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java b/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java
new file mode 100644
index 000000000..7d652cf1a
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java
@@ -0,0 +1,46 @@
+//
+// $Id: DropBoardUtil.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+import com.threerings.util.DirectionCodes;
+
+import com.threerings.puzzle.drop.data.DropBoard;
+
+public class DropBoardUtil
+ implements DirectionCodes
+{
+ /**
+ * Returns the orientation resulting from rotating the block in the
+ * given direction the specified number of times.
+ *
+ * @param orient the current orientation.
+ * @param dir the direction to rotate in; one of CW or
+ * CCW.
+ * @param count the number of rotations to perform.
+ *
+ * @return the rotated orientation.
+ */
+ public static int getRotatedOrientation (int orient, int dir, int count)
+ {
+ for (int ii = 0; ii < (count % 4); ii++) {
+ orient = getRotatedOrientation(orient, dir);
+ }
+ return orient;
+ }
+
+ /**
+ * Returns the orientation resulting from rotating the block in
+ * the given direction.
+ *
+ * @param orient the current orientation.
+ * @param dir the direction to rotate in; one of CW or
+ * CCW.
+ *
+ * @return the rotated orientation.
+ */
+ public static int getRotatedOrientation (int orient, int dir)
+ {
+ return (orient + ((dir == CW) ? 2 : 6)) % DIRECTION_COUNT;
+ }
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java b/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java
new file mode 100644
index 000000000..0ae7d4477
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java
@@ -0,0 +1,53 @@
+//
+// $Id: DropGameUtil.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+import java.awt.event.KeyEvent;
+
+import com.threerings.util.KeyTranslatorImpl;
+
+import com.threerings.puzzle.drop.client.DropControllerDelegate;
+import com.threerings.puzzle.util.PuzzleGameUtil;
+
+/**
+ * Drop puzzle game related utilities.
+ */
+public class DropGameUtil
+{
+ /**
+ * Returns a key translator configured with mappings suitable for a
+ * drop puzzle game.
+ */
+ public static KeyTranslatorImpl getKeyTranslator ()
+ {
+ // start with the standard puzzle key mappings
+ KeyTranslatorImpl xlate = PuzzleGameUtil.getKeyTranslator();
+
+ // add all press key mappings
+ xlate.addPressCommand(KeyEvent.VK_LEFT,
+ DropControllerDelegate.MOVE_BLOCK_LEFT,
+ MOVE_RATE, MOVE_DELAY);
+ xlate.addPressCommand(KeyEvent.VK_RIGHT,
+ DropControllerDelegate.MOVE_BLOCK_RIGHT,
+ MOVE_RATE, MOVE_DELAY);
+ xlate.addPressCommand(KeyEvent.VK_UP,
+ DropControllerDelegate.ROTATE_BLOCK_CCW, 0);
+ xlate.addPressCommand(KeyEvent.VK_DOWN,
+ DropControllerDelegate.ROTATE_BLOCK_CW, 0);
+ xlate.addPressCommand(KeyEvent.VK_SPACE,
+ DropControllerDelegate.START_DROP_BLOCK, 0);
+
+ // add all release key mappings
+ xlate.addReleaseCommand(KeyEvent.VK_SPACE,
+ DropControllerDelegate.END_DROP_BLOCK);
+
+ return xlate;
+ }
+
+ /** The move key repeat rate in moves per second. */
+ protected static final int MOVE_RATE = 7;
+
+ /** The delay in milliseconds before the move keys begin to repeat. */
+ protected static final long MOVE_DELAY = 300L;
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/DropPieceProvider.java b/src/java/com/threerings/puzzle/drop/util/DropPieceProvider.java
new file mode 100644
index 000000000..63f20c16d
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/DropPieceProvider.java
@@ -0,0 +1,23 @@
+//
+// $Id: DropPieceProvider.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+/**
+ * Does something extraordinary.
+ */
+public interface DropPieceProvider
+{
+ /**
+ * Get the next piece to add to the drop board.
+ */
+ public int getNextPiece ()
+ throws OutOfPiecesException;
+
+ /**
+ * I lost my touch.
+ */
+ public static class OutOfPiecesException extends Exception
+ {
+ }
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java b/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java
new file mode 100644
index 000000000..2092fb985
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java
@@ -0,0 +1,163 @@
+//
+// $Id: PieceDestroyer.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.threerings.puzzle.drop.data.DropBoard;
+import com.threerings.puzzle.drop.data.DropBoard.PieceOperation;
+import com.threerings.puzzle.drop.data.DropPieceCodes;
+import com.threerings.puzzle.drop.data.SegmentInfo;
+
+/**
+ * Handles destroying contiguous piece segments in a drop board.
+ */
+public class PieceDestroyer
+ implements DropPieceCodes
+{
+ /**
+ * An interface to be implemented by specific puzzles to detail the
+ * parameters and methodology by which pieces are destroyed in the
+ * puzzle board.
+ */
+ public interface DestroyLogic
+ {
+ /**
+ * Returns the minimum length of a contiguously piece segment that
+ * should be destroyed.
+ */
+ public int getMinimumLength ();
+
+ /**
+ * Returns whether piece a is equivalent to piece
+ * b for the purposes of including it in a contiguous
+ * piece segment to be destroyed.
+ */
+ public boolean isEquivalent (int a, int b);
+ }
+
+ /**
+ * Constructs a piece destroyer that destroys pieces as specified by
+ * the supplied destroy logic.
+ */
+ public PieceDestroyer (DestroyLogic logic)
+ {
+ _logic = logic;
+ }
+
+ /**
+ * Destroys all pieces in the given board that are in contiguous rows
+ * or columns of pieces, returning a list of {@link SegmentInfo}
+ * objects detailing the destroyed piece segments. Note that a single
+ * list is used internally to gather the segment info, and so callers
+ * that care to modify the list should create their own copy; also,
+ * the pieces in the segments may overlap, i.e., two segments may
+ * contain the same piece.
+ */
+ public List destroyPieces (DropBoard board, PieceOperation destroyOp)
+ {
+ // find all horizontally-oriented destroyed segments
+ int bwid = board.getWidth(), bhei = board.getHeight();
+ _destroyed.clear();
+ int end = bwid - _logic.getMinimumLength() + 1;
+ for (int yy = (bhei - 1); yy >= 0; yy--) {
+ int xx = 0;
+ while (xx < end) {
+ xx += findSegment(board, HORIZONTAL, xx, yy);
+ }
+ }
+
+ // find all vertically-oriented destroyed segments
+ end = _logic.getMinimumLength() - 2;
+ for (int xx = 0; xx < bwid; xx++) {
+ int yy = bhei - 1;
+ while (yy > end) {
+ yy -= findSegment(board, VERTICAL, xx, yy);
+ }
+ }
+
+ // destroy the pieces
+ int size = _destroyed.size();
+ for (int ii = 0; ii < size; ii++) {
+ SegmentInfo si = (SegmentInfo)_destroyed.get(ii);
+ board.applyOp(si.dir, si.x, si.y, si.len, destroyOp);
+ }
+
+ return _destroyed;
+ }
+
+ /**
+ * Searches for a contiguously colored piece segment with the
+ * specified orientation and root coordinates in the supplied board
+ * and returns the length of the segment traversed.
+ */
+ protected int findSegment (DropBoard board, int dir, int x, int y)
+ {
+ _lengthOp.reset();
+ board.applyOp(dir, x, y, _lengthOp);
+ int len = _lengthOp.getLength();
+ if (len >= _logic.getMinimumLength()) {
+ _destroyed.add(new SegmentInfo(dir, x, y, len));
+ }
+ return len;
+ }
+
+ /**
+ * A piece operation that calculates the length of the contiguous
+ * piece segment to which it is applied.
+ */
+ protected class SegmentLengthOperation
+ implements PieceOperation
+ {
+ /**
+ * Resets the operation for application to a new piece segment.
+ */
+ public void reset ()
+ {
+ _len = 0;
+ }
+
+ /**
+ * Returns the length of the contiguous piece segment.
+ */
+ public int getLength ()
+ {
+ return _len;
+ }
+
+ // documentation inherited
+ public boolean execute (DropBoard board, int col, int row)
+ {
+ int piece = board.getPiece(col, row);
+ if (_len == 0) {
+ _len = 1;
+ _piece = piece;
+ return (piece != PIECE_NONE);
+
+ } else if (_logic.isEquivalent(piece, _piece)) {
+ _len++;
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+ /** The root segment piece. */
+ protected int _piece;
+
+ /** The segment length in pieces. */
+ protected int _len;
+ }
+
+ /** The puzzle-specific destroy logic with which we do our business. */
+ protected DestroyLogic _logic;
+
+ /** The piece operation used to determine segment length. */
+ protected SegmentLengthOperation _lengthOp = new SegmentLengthOperation();
+
+ /** The list of destroyed piece segments. */
+ protected ArrayList _destroyed = new ArrayList();
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java b/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java
new file mode 100644
index 000000000..7861e3727
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java
@@ -0,0 +1,61 @@
+//
+// $Id: PieceDropLogic.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+import com.threerings.util.DirectionCodes;
+import com.threerings.puzzle.drop.data.DropBoard;
+
+/**
+ * An interface to be implemented by games that would like to be able to
+ * drop their pieces during game play.
+ */
+public interface PieceDropLogic
+{
+ /**
+ * Should the board always be filled?
+ *
+ * @return false for normal behavior.
+ */
+ public boolean boardAlwaysFilled ();
+
+ /**
+ * Returns whether the given piece is potentially droppable.
+ */
+ public boolean isDroppablePiece (int piece);
+
+ /**
+ * Returns whether the given piece has constraints upon it that
+ * impact its droppability.
+ */
+ public boolean isConstrainedPiece (int piece);
+
+ /**
+ * Returns whether the given piece terminates a column climb when
+ * determining the height of a piece column to be dropped.
+ *
+ * @param allowConst whether to allow dropping constrained pieces
+ * (though only in the first encountered constrained block.)
+ * @param piece the piece to consider.
+ * @param pre whether the climbability check is being performed
+ * before the height is incremented, or after.
+ */
+ public boolean isClimbablePiece (
+ boolean allowConst, int piece, boolean pre);
+
+ /**
+ * Returns the x-axis coordinate of the specified edge of the
+ * given constrained piece.
+ *
+ *
TODO: This should go away once the sword and sail games
+ * have standardized on WEST/EAST or BLOCK_LEFT/BLOCK_RIGHT to
+ * reference block edges.
+ *
+ * @param board the board to search.
+ * @param col the column of the constrained piece.
+ * @param row the row of the constrained piece.
+ * @param dir the edge direction to find; one of {@link
+ * DirectionCodes#LEFT} or {@link DirectionCodes#RIGHT}.
+ */
+ public int getConstrainedEdge (DropBoard board, int col, int row, int dir);
+}
diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDropper.java b/src/java/com/threerings/puzzle/drop/util/PieceDropper.java
new file mode 100644
index 000000000..7059dfef5
--- /dev/null
+++ b/src/java/com/threerings/puzzle/drop/util/PieceDropper.java
@@ -0,0 +1,340 @@
+//
+// $Id: PieceDropper.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.drop.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.samskivert.util.StringUtil;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.drop.data.DropBoard;
+import com.threerings.puzzle.drop.data.DropPieceCodes;
+
+/**
+ * Handles dropping pieces in a board.
+ */
+public class PieceDropper
+ implements DropPieceCodes
+{
+ /**
+ * A class to hold information detailing the pieces to be dropped
+ * in a particular column.
+ */
+ public static class PieceDropInfo
+ {
+ /** The starting row of the bottom piece being dropped. */
+ public int row;
+
+ /** The column number. */
+ public int col;
+
+ /** The distance to drop the pieces. */
+ public int dist;
+
+ /** The pieces to be dropped. */
+ public int[] pieces;
+
+ /**
+ * Constructs a piece drop info object.
+ */
+ public PieceDropInfo (int col, int row, int dist)
+ {
+ this.col = col;
+ this.row = row;
+ this.dist = dist;
+ }
+
+ /** Returns a string representation of this instance. */
+ public String toString ()
+ {
+ return StringUtil.fieldsToString(this);
+ }
+ }
+
+ /**
+ * Constructs a piece dropper that uses the supplied piece drop logic
+ * to specialise itself for a particular puzzle.
+ */
+ public PieceDropper (PieceDropLogic logic)
+ {
+ _logic = logic;
+ }
+
+ /**
+ * Destructively modifies the supplied board to contain pieces dropped
+ * in the first of potentially multiple drop positions and returns a
+ * list of {@link PieceDropInfo} objects detailing all column segments
+ * to be dropped. Note that a single list is used internally to store
+ * the drop info objects, and so callers that care to do anything
+ * long-term with the drop info should create their own copy of the
+ * list or somesuch.
+ *
+ * @param DropPieceProvider if the board should always be filled
+ * (as specified by the PieceDropLogic) this will provide
+ * information on the newly filled in pieces.
+ */
+ public List dropPieces (DropBoard board, DropPieceProvider provider)
+ {
+ int bhei = board.getHeight(), bwid = board.getWidth();
+ _drops.clear();
+ for (int yy = bhei - 1; yy >= 0; yy--) {
+ for (int xx = 0; xx < bwid; xx++) {
+ // find all drops in this column
+ getColumnDrops(board, _drops, xx, yy);
+ }
+ }
+
+ if (_logic.boardAlwaysFilled()) {
+ addFillingDrops(board, provider, _drops);
+ }
+
+ return _drops;
+ }
+
+ /**
+ * Analyzes but does not modify the supplied board and returns a list
+ * of {@link PieceDropInfo} objects detailing all column segments to
+ * be dropped. Note that a single list is used internally to store
+ * the drop info objects, and so callers that care to do anything
+ * long-term with the drop info should create their own copy of the
+ * list or somesuch.
+ *
+ * @param DropPieceProvider if the board should always be filled
+ * (as specified by the PieceDropLogic) this will provide
+ * information on the newly filled in pieces.
+ */
+ public List getDroppedPieces (DropBoard board, DropPieceProvider provider)
+ {
+ // grab a snapshot of the board within which we're dropping
+ // pieces to avoid modifying the source board directly while we're
+ // figuring out what to drop where
+ if (_board == null) {
+ _board = (DropBoard)board.clone();
+ } else {
+ board.copyInto(_board);
+ }
+ return dropPieces(_board, provider);
+ }
+
+ /**
+ * Populates the drops list with {@link PieceDropInfo}
+ * objects detailing the column segments to be dropped per empty space
+ * below. Destructively modifies the given board to reflect the final
+ * positions of the column segments once dropped.
+ */
+ protected void getColumnDrops (DropBoard board, List drops, int x, int y)
+ {
+ // skip empty or fixed pieces
+ int piece = board.getPiece(x, y);
+ if (!_logic.isDroppablePiece(piece)) {
+ return;
+ }
+
+ if (_logic.isConstrainedPiece(piece)) {
+ // get the distance to drop the block
+ int dist = getBlockDropDistance(board, x, y);
+ if (dist == 0) {
+ return;
+ }
+
+ // scoot along the bottom edge of the block dropping each column
+ int bwid = board.getWidth();
+ int start = _logic.getConstrainedEdge(board, x, y, LEFT);
+ int end = _logic.getConstrainedEdge(board, x, y, RIGHT);
+ for (int xpos = start; xpos <= end; xpos++) {
+ addDropInfo(board, drops, true, xpos, y, dist);
+ }
+
+ } else {
+ // get the distance to drop the pieces
+ int dist = board.getDropDistance(x, y);
+ if (dist == 0) {
+ return;
+ }
+
+ // add the column segment to the list of drops
+ addDropInfo(board, drops, false, x, y, dist);
+ }
+ }
+
+ /**
+ * Adds a {@link PieceDropInfo} object to the drop list detailing
+ * dropping of the column segment at the specified location.
+ *
+ * @param board the working board.
+ * @param allowConst whether to allow dropping constrained pieces in
+ * the specified column segment.
+ * @param x the column x-coordinate.
+ * @param y the column segment bottom y-coordinate.
+ * @param dist the distance to drop the pieces.
+ */
+ protected void addDropInfo (DropBoard board, List drops,
+ boolean allowConst, int x, int y, int dist)
+ {
+ // sanity check our column
+ if (x < 0 || x >= board.getWidth()) {
+ Log.warning("Requested to add bogus drop info [board=" + board +
+ ", drops=" + StringUtil.toString(drops) +
+ ", allowConst=" + allowConst +
+ ", x=" + x + ", y=" + y + ", dist=" + dist + "].");
+ Thread.dumpStack();
+ }
+
+ // traverse up the column looking for an empty or block piece
+ // that will terminate this column segment
+ int height = getDropHeight(board, allowConst, x, y, dist);
+
+ // create the piece drop info object
+ PieceDropInfo pdi = new PieceDropInfo(x, y, dist);
+
+ // copy in the relevant pieces
+ pdi.pieces = new int[height];
+ int idx = 0;
+ for (int yy = y; yy > (y - height); yy--) {
+ pdi.pieces[idx++] = board.getPiece(x, yy);
+ }
+
+ // update the working copy of the board with the eventual
+ // piece locations
+ dropPieces(board, pdi);
+
+ // add the column segment to the pot
+ drops.add(pdi);
+ }
+
+ /**
+ * If we want to keep the board filled at all times,
+ * this method will be called to create drop objects for the
+ * newly-filled in pieces.
+ */
+ protected void addFillingDrops (
+ DropBoard board, DropPieceProvider provider, List drops)
+ {
+ boolean out = false;
+ // drop in new pieces for empty spaces at the top
+ for (int xx=0, bwid=board.getWidth(); xx < bwid; xx++) {
+ // get the distance to drop the pieces
+ int dist = board.getDropDistance(xx, -1);
+ if (dist != 0) {
+ PieceDropInfo pdi = new PieceDropInfo(xx, -1, dist);
+ pdi.pieces = new int[dist];
+ for (int ii=0; ii < dist; ii++) {
+ try {
+ pdi.pieces[ii] = provider.getNextPiece();
+ } catch (DropPieceProvider.OutOfPiecesException oop) {
+ // well, how far did we get?
+ int[] something = new int[ii];
+ System.arraycopy(pdi.pieces, 0, something, 0, ii);
+ pdi.pieces = something;
+ out = true;
+ break;
+ }
+ }
+
+ dropPieces(board, pdi);
+ drops.add(pdi);
+
+ // if we're outta pieces, bail.
+ if (out) {
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the height of the piece segment to be dropped at the
+ * given coordinates.
+ */
+ protected int getDropHeight (
+ DropBoard board, boolean allowConst, int x, int y, int dist)
+ {
+ int height = 0;
+ for (int yy = y; yy >= 0; yy--) {
+ int curpiece = board.getPiece(x, yy);
+ if (!_logic.isClimbablePiece(allowConst, curpiece, true)) {
+ return height;
+ }
+
+ height++;
+
+ if (!_logic.isClimbablePiece(allowConst, curpiece, false)) {
+ return height;
+ }
+ }
+
+ return height;
+ }
+
+ /**
+ * Updates the given board to reflect the eventual destination of the
+ * given pieces.
+ */
+ protected void dropPieces (DropBoard board, PieceDropInfo pdi)
+ {
+ // clear out the original piece positions
+ if (!board.setSegment(
+ VERTICAL, pdi.col, pdi.row, pdi.pieces.length, PIECE_NONE)) {
+ Log.warning("Bogosity encountered when clearing pieces for drop " +
+ "[bwid=" + board.getWidth() +
+ ", bhei=" + board.getHeight() + ", pdi=" + pdi + "].");
+ }
+ // place the pieces in their destination positions
+ applyPieces(board, pdi);
+ }
+
+ /**
+ * Returns the number of spaces the block whose bottom edge
+ * contains the specified coordinate can be dropped based on the
+ * contents of the given board. The droppable distance for a
+ * block is determined by finding the minimum drop distance across
+ * all columns the block spans.
+ */
+ protected int getBlockDropDistance (DropBoard board, int x, int y)
+ {
+ // get the smallest drop distance across all of the block columns
+ int bwid = board.getWidth();
+ int start = Math.max(_logic.getConstrainedEdge(board, x, y, LEFT), 0);
+ int end = Math.min(
+ _logic.getConstrainedEdge(board, x, y, RIGHT), bwid - 1);
+ int dist = board.getHeight() - 1;
+
+ for (int xpos = start; xpos <= end; xpos++) {
+ dist = Math.min(dist, board.getDropDistance(xpos, y));
+ }
+
+ return dist;
+ }
+
+ /**
+ * Applies the pieces in the given piece drop info object to the given
+ * board at their eventual destination positions.
+ */
+ protected void applyPieces (DropBoard board, PieceDropInfo pdi)
+ {
+ int start = pdi.row, end = (pdi.row - pdi.pieces.length);
+ int idx = 0;
+ boolean error = false;
+ for (int yy = (start + pdi.dist); yy > (end + pdi.dist); yy--) {
+ error = !board.setPiece(pdi.col, yy, pdi.pieces[idx++]) || error;
+ }
+ if (error) {
+ Log.warning("Bogosity encountered while applying dropped " +
+ "pieces to board [bwid=" + board.getWidth() +
+ ", bhei=" + board.getHeight() + ", pdi=" + pdi + "].");
+ }
+ }
+
+ /** The piece drop logic used to allow puzzle-specific piece dropping
+ * hooks. */
+ protected PieceDropLogic _logic;
+
+ /** The list of piece drop info objects detailing the piece drops
+ * resulting from the last call to {@link #getDroppedPieces}. */
+ protected ArrayList _drops = new ArrayList();
+
+ /** The board on which pieces are being dropped. */
+ protected DropBoard _board;
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleDispatcher.java b/src/java/com/threerings/puzzle/server/PuzzleDispatcher.java
new file mode 100644
index 000000000..64d616003
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleDispatcher.java
@@ -0,0 +1,76 @@
+//
+// $Id: PuzzleDispatcher.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.presents.client.Client;
+import com.threerings.presents.client.InvocationService.InvocationListener;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.server.InvocationDispatcher;
+import com.threerings.presents.server.InvocationException;
+import com.threerings.puzzle.client.PuzzleService;
+import com.threerings.puzzle.client.PuzzleService.EnterPuzzleListener;
+import com.threerings.puzzle.data.PuzzleMarshaller;
+import com.threerings.puzzle.data.SolitairePuzzleConfig;
+
+/**
+ * Dispatches requests to the {@link PuzzleProvider}.
+ */
+public class PuzzleDispatcher extends InvocationDispatcher
+{
+ /**
+ * Creates a dispatcher that may be registered to dispatch invocation
+ * service requests for the specified provider.
+ */
+ public PuzzleDispatcher (PuzzleProvider provider)
+ {
+ this.provider = provider;
+ }
+
+ // documentation inherited
+ public InvocationMarshaller createMarshaller ()
+ {
+ return new PuzzleMarshaller();
+ }
+
+ // documentation inherited
+ public void dispatchRequest (
+ ClientObject source, int methodId, Object[] args)
+ throws InvocationException
+ {
+ switch (methodId) {
+ case PuzzleMarshaller.START_PUZZLE:
+ ((PuzzleProvider)provider).startPuzzle(
+ source,
+ (SolitairePuzzleConfig)args[0], (InvocationListener)args[1]
+ );
+ return;
+
+ case PuzzleMarshaller.ENTER_PUZZLE:
+ ((PuzzleProvider)provider).enterPuzzle(
+ source,
+ ((Integer)args[0]).intValue(), (EnterPuzzleListener)args[1]
+ );
+ return;
+
+ case PuzzleMarshaller.LEAVE_PUZZLE:
+ ((PuzzleProvider)provider).leavePuzzle(
+ source
+
+ );
+ return;
+
+ case PuzzleMarshaller.CHANGE_DIFFICULTY:
+ ((PuzzleProvider)provider).changeDifficulty(
+ source,
+ ((Integer)args[0]).intValue()
+ );
+ return;
+
+ default:
+ super.dispatchRequest(source, methodId, args);
+ }
+ }
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java b/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java
new file mode 100644
index 000000000..1f526b952
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java
@@ -0,0 +1,59 @@
+//
+// $Id: PuzzleGameDispatcher.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import com.threerings.presents.client.Client;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.data.InvocationMarshaller;
+import com.threerings.presents.server.InvocationDispatcher;
+import com.threerings.presents.server.InvocationException;
+import com.threerings.puzzle.client.PuzzleGameService;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.PuzzleGameMarshaller;
+
+/**
+ * Dispatches requests to the {@link PuzzleGameProvider}.
+ */
+public class PuzzleGameDispatcher extends InvocationDispatcher
+{
+ /**
+ * Creates a dispatcher that may be registered to dispatch invocation
+ * service requests for the specified provider.
+ */
+ public PuzzleGameDispatcher (PuzzleGameProvider provider)
+ {
+ this.provider = provider;
+ }
+
+ // documentation inherited
+ public InvocationMarshaller createMarshaller ()
+ {
+ return new PuzzleGameMarshaller();
+ }
+
+ // documentation inherited
+ public void dispatchRequest (
+ ClientObject source, int methodId, Object[] args)
+ throws InvocationException
+ {
+ switch (methodId) {
+ case PuzzleGameMarshaller.UPDATE_PROGRESS:
+ ((PuzzleGameProvider)provider).updateProgress(
+ source,
+ ((Integer)args[0]).intValue(), (int[])args[1]
+ );
+ return;
+
+ case PuzzleGameMarshaller.UPDATE_PROGRESS_SYNC:
+ ((PuzzleGameProvider)provider).updateProgressSync(
+ source,
+ ((Integer)args[0]).intValue(), (int[])args[1], (Board[])args[2]
+ );
+ return;
+
+ default:
+ super.dispatchRequest(source, methodId, args);
+ }
+ }
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java b/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java
new file mode 100644
index 000000000..f076bf2a4
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java
@@ -0,0 +1,29 @@
+//
+// $Id: PuzzleGameProvider.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.server.InvocationProvider;
+
+import com.threerings.puzzle.client.PuzzleGameService;
+import com.threerings.puzzle.data.Board;
+
+/**
+ * Handles the server side of the puzzle game services.
+ */
+public interface PuzzleGameProvider extends InvocationProvider
+{
+ /**
+ * Called when the client has sent a {@link
+ * PuzzleGameService#updateProgress} service request.
+ */
+ public void updateProgress (ClientObject caller, int roundId, int[] events);
+
+ /**
+ * Called when the client has sent a {@link
+ * PuzzleGameService#updateProgressSync} service request.
+ */
+ public void updateProgressSync (
+ ClientObject caller, int roundId, int[] events, Board[] states);
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleManager.java b/src/java/com/threerings/puzzle/server/PuzzleManager.java
new file mode 100644
index 000000000..46a380c05
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleManager.java
@@ -0,0 +1,810 @@
+//
+// $Id: PuzzleManager.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.samskivert.util.IntListUtil;
+import com.samskivert.util.IntTuple;
+import com.samskivert.util.IntervalManager;
+import com.samskivert.util.ResultListener;
+import com.samskivert.util.StringUtil;
+import com.samskivert.util.Tuple;
+
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.dobj.AttributeChangedEvent;
+import com.threerings.presents.dobj.DObject;
+import com.threerings.presents.dobj.DSet;
+import com.threerings.presents.dobj.OidList;
+import com.threerings.presents.server.util.SafeInterval;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.OccupantInfo;
+import com.threerings.crowd.server.PlaceManagerDelegate;
+
+import com.threerings.parlor.game.GameManager;
+import com.threerings.parlor.game.GameObject;
+import com.threerings.parlor.game.PartyGameConfig;
+
+import com.threerings.util.MessageBundle;
+import com.threerings.util.RandomUtil;
+import com.threerings.util.StreamableArrayList;
+
+import com.threerings.yohoho.data.ActivityCodes;
+import com.threerings.yohoho.data.YoOccupantInfo;
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.yohoho.server.PirateManager;
+import com.threerings.yohoho.server.YoSceneManager;
+import com.threerings.yohoho.server.YoServer;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleConfig;
+import com.threerings.puzzle.data.PuzzleGameMarshaller;
+import com.threerings.puzzle.data.PuzzleObject;
+
+/**
+ * Extends the {@link GameManager} with facilities for the puzzle games
+ * that are used in Yohoho. Only features generic to all of our games are
+ * in this base class and additional features are supported both through
+ * the inheritance hierarchy and through delegating helpers (because Java
+ * conveniently doesn't support multiple inheritance).
+ */
+public abstract class PuzzleManager extends GameManager
+ implements PuzzleCodes, PuzzleGameProvider
+{
+ /**
+ * Returns the boards for all players.
+ */
+ public Board[] getBoards ()
+ {
+ return _boards;
+ }
+
+ /**
+ * Returns the user object for the player with the specified index or
+ * null if the player at that index is not online.
+ */
+ public BodyObject getPlayer (int playerIdx)
+ {
+ // if we have their oid, use that
+ int ploid = _playerOids[playerIdx];
+ if (ploid > 0) {
+ return (BodyObject)YoServer.omgr.getObject(ploid);
+ }
+ // otherwise look them up by name
+ String name = getPlayerName(playerIdx);
+ return (name == null) ? null : (BodyObject)YoServer.lookupBody(name);
+ }
+
+ /**
+ * Returns the board summary for the given player index.
+ */
+ public BoardSummary getBoardSummary (int pidx)
+ {
+ return (_puzobj == null || _puzobj.summaries == null) ? null :
+ _puzobj.summaries[pidx];
+ }
+
+ /**
+ * Returns whether this puzzle cares to make use of per-player board
+ * summaries that are sent periodically to all users in the puzzle via
+ * {@link #sendStatusUpdate}. The default implementation returns
+ * false.
+ */
+ public boolean needsBoardSummaries ()
+ {
+ return false;
+ }
+
+ /**
+ * Returns whether this puzzle compares board states before it applies
+ * progress events, or after. The default implementation returns
+ * true.
+ */
+ protected boolean compareBeforeApply ()
+ {
+ return true;
+ }
+
+ /**
+ * Calls {@link BoardSummary#summarize} on the given player's board
+ * summary to refresh the summary information in preparation for
+ * sending along to the client(s).
+ *
+ * @param pidx the player index of the player whose board is to be
+ * summarized.
+ */
+ public void updateBoardSummary (int pidx)
+ {
+ if (_puzobj.summaries != null && _puzobj.summaries[pidx] != null) {
+ _puzobj.summaries[pidx].summarize();
+ }
+ }
+
+ /**
+ * Ends the game for the given player.
+ */
+ public void endPlayerGame (int pidx)
+ {
+ // don't update board summaries for AI players since they keep
+ // their own board summary up to date
+ if (!isAI(pidx)) {
+ // update the board summary with the player's final board
+ updateBoardSummary(pidx);
+ }
+
+ // go for a little transactional efficiency
+ _puzobj.startTransaction();
+ try {
+ // end the player's game
+ _puzobj.setPlayerStatusAt(PuzzleObject.PLAYER_KNOCKED_OUT, pidx);
+
+ // let derived classes do some business
+ playerGameDidEnd(pidx);
+
+ // force a status update
+ updateStatus();
+
+ // notify the players
+ String message = MessageBundle.tcompose(
+ "m.player_game_over", getPlayerName(pidx));
+ systemMessage(PUZZLE_MESSAGE_BUNDLE, message);
+
+ } finally {
+ _puzobj.commitTransaction();
+ }
+
+ // if it's time to end the game, then do so
+ if (shouldEndGame()) {
+ endGame();
+
+ } else {
+ // otherwise report that the player was knocked out to other
+ // people in his/her room
+ reportPlayerKnockedOut(pidx);
+ }
+ }
+
+ /**
+ * Called when a player has been marked as knocked out but before the
+ * knock-out status update has been sent to the players. Any status
+ * information that needs be updated in light of the knocked out
+ * player can be updated here.
+ */
+ protected void playerGameDidEnd (int pidx)
+ {
+ }
+
+ // documentation inherited
+ protected Class getPlaceObjectClass ()
+ {
+ return PuzzleObject.class;
+ }
+
+ // documentation inherited
+ protected void didInit ()
+ {
+ super.didInit();
+
+ // save off a casted reference to our puzzle config
+ _puzconfig = (PuzzleConfig)_config;
+ }
+
+ // documentation inherited
+ protected void didStartup ()
+ {
+ super.didStartup();
+
+ // grab the puzzle object
+ _puzobj = (PuzzleObject)_gameobj;
+
+ // create and fill in our game service object
+ PuzzleGameMarshaller service = (PuzzleGameMarshaller)
+ _invmgr.registerDispatcher(new PuzzleGameDispatcher(this), false);
+ _puzobj.setPuzzleGameService(service);
+ }
+
+ // documentation inherited
+ protected void handlePartialNoShow ()
+ {
+ // mark the no-show players; this will cause allPlayersReady() to
+ // think that everyone has arrived, but still allow us to tell who
+ // has not shown up in gameDidStart()
+ for (int ii = 0; ii < _playerOids.length; ii++) {
+ if (_playerOids[ii] == 0) {
+ _playerOids[ii] = -1;
+ }
+ }
+
+ // go ahead and start the game; gameDidStart() will take care of
+ // giving the boot to anyone who isn't around
+ Log.info("Forcing start of partial no-show game " +
+ "[game=" + _puzobj.which() +
+ ", players=" + StringUtil.toString(_puzobj.players) +
+ ", poids=" + StringUtil.toString(_playerOids) + "].");
+ startGame();
+ }
+
+ // documentation inherited
+ protected void gameWillStart ()
+ {
+ int size = getPlayerSlots();
+ if (_boards == null) {
+ // create our arrays
+ _boards = new Board[size];
+ _lastProgress = new long[size];
+ } else {
+ Arrays.fill(_boards, null);
+ }
+
+ // start everyone out with reasonable last progress stamps
+ Arrays.fill(_lastProgress, System.currentTimeMillis());
+
+ // initialize the seed that goes out with this round
+ _puzobj.setSeed(RandomUtil.rand.nextLong());
+
+ // initialize the player status
+ _puzobj.setPlayerStatus(new int[size]);
+
+ // initialize the player boards
+ initBoards();
+
+ // let the game manager start up its business
+ super.gameWillStart();
+
+ // send along an initial status update before we start up the
+ // status update interval
+ sendStatusUpdate();
+
+ long statusInterval = getStatusInterval();
+ if (_uiid == -1 && statusInterval > 0) {
+ // register the status update interval to address subsequent
+ // periodic updates
+ _uiid = IntervalManager.register(new SafeInterval(YoServer.omgr) {
+ public void run () {
+ sendStatusUpdate();
+ }
+ }, statusInterval, null, true);
+ }
+ }
+
+ /**
+ * Returns the frequency with which puzzle status updates are
+ * broadcast to the players (which is accomplished via a call to
+ * {@link #sendStatusUpdate} which in turn calls {@link #updateStatus}
+ * wherein derived classes can participate in the status update).
+ * Returning O (the default) indicates that a periodic
+ * status update is not desired.
+ */
+ protected long getStatusInterval ()
+ {
+ return 0L;
+ }
+
+ // documentation inherited
+ protected void gameDidStart ()
+ {
+ super.gameDidStart();
+
+ // any players who have not claimed that they are ready should now
+ // be given le boote royale
+ for (int ii = 0; ii < _playerOids.length; ii++) {
+ if (_playerOids[ii] == -1) {
+ Log.info("Booting no-show player [game=" + _puzobj.which() +
+ ", player=" + getPlayerName(ii) + "].");
+ _playerOids[ii] = 0; // unfiddle the blank oid
+ endPlayerGame(ii);
+ }
+ }
+
+ // log the AI skill levels for games involving AIs as it's useful
+ // when tuning AI algorithms
+ if (_AIs != null) {
+ Log.info("AIs on the job [game=" + _puzobj.which() +
+ ", skillz=" + StringUtil.toString(_AIs) + "].");
+ }
+ }
+
+ /**
+ * Updates (in one puzzle object transaction) all periodically updated
+ * status information.
+ */
+ protected void sendStatusUpdate ()
+ {
+ _puzobj.startTransaction();
+ try {
+ // Log.info("Updating status [game=" + _puzobj.which() + "].");
+ updateStatus();
+ } finally {
+ _puzobj.commitTransaction();
+ }
+ }
+
+ /**
+ * A puzzle periodically (default of once every 5 seconds but
+ * configurable by puzzle) updates status information that is visible
+ * to the user. Derived classes can override this method and effect
+ * their updates by generating events on the puzzle object and they
+ * will be packaged into the update transaction.
+ */
+ protected void updateStatus ()
+ {
+ // if we're a board summary updating kind of puzzle, do that
+ if (needsBoardSummaries()) {
+ // they're already modified in-situ, so we just rebroadcast
+ // the latest versions
+ _puzobj.setSummaries(_puzobj.summaries);
+ }
+ }
+
+ /**
+ * Send a system message with the puzzle bundle.
+ */
+ protected void systemMessage (String msg)
+ {
+ systemMessage(msg, false);
+ }
+
+ /**
+ * Send a system message with the puzzle bundle.
+ *
+ * @param waitForStart if true, the message will not be sent until the
+ * game has started.
+ */
+ protected void systemMessage (String msg, boolean waitForStart)
+ {
+ systemMessage(PUZZLE_MESSAGE_BUNDLE, msg, waitForStart);
+ }
+
+ /**
+ * Creates and initializes boards and board summaries (if desired per
+ * {@link #needsBoardSummaries}) for each player.
+ */
+ protected void initBoards ()
+ {
+ long seed = _puzobj.seed;
+ BoardSummary[] summaries = needsBoardSummaries() ?
+ new BoardSummary[getPlayerSlots()] : null;
+
+ // set up game information for each player
+ for (int ii = 0, nn = getPlayerSlots(); ii < nn; ii++) {
+ boolean needsPlayerBoard = needsPlayerBoard(ii);
+ if (needsPlayerBoard) {
+ // create the game board
+ _boards[ii] = newBoard(ii);
+ _boards[ii].initializeSeed(seed);
+ if (summaries != null) {
+ summaries[ii] = newBoardSummary(_boards[ii]);
+ }
+ }
+ }
+ // these will be sent to the players on the first status update
+ _puzobj.summaries = summaries;
+ }
+
+ /**
+ * Returns whether this puzzle needs a board for the given player
+ * index. The default implementation only creates boards for occupied
+ * player slots. Derived classes may wish to override this method if
+ * they have specialized board needs, e.g., they need only a single
+ * board for all players.
+ */
+ protected boolean needsPlayerBoard (int pidx)
+ {
+ return (_puzobj.isOccupiedPlayer(pidx));
+ }
+
+ // documentation inherited
+ protected void gameDidEnd ()
+ {
+ if (_uiid != -1) {
+ // remove the client update interval
+ IntervalManager.remove(_uiid);
+ _uiid = -1;
+ }
+
+ // send along one final status update
+ sendStatusUpdate();
+
+ super.gameDidEnd();
+ }
+
+ /**
+ * Report winner and loser oids to each room that any of the
+ * winners/losers is in.
+ */
+ protected void reportWinnersAndLosers ()
+ {
+ OidList winners = new OidList();
+ OidList losers = new OidList();
+ OidList places = new OidList();
+
+ Object[] args = new Object[] { winners, losers };
+
+ for (int ii=0, nn=_playerOids.length; ii < nn; ii++) {
+ BodyObject user = getPlayer(ii);
+ if (user != null) {
+ places.add(user.location);
+ (_puzobj.isWinner(ii) ? winners : losers).add(user.getOid());
+ }
+ }
+
+ // now send a message event to each room
+ for (int ii=0, nn = places.size(); ii < nn; ii++) {
+ DObject place = YoServer.omgr.getObject(places.get(ii));
+ if (place != null) {
+ place.postMessage(WINNERS_AND_LOSERS, args);
+ }
+ }
+ }
+
+ /**
+ * Report to the knocked-out player's room that they were knocked out.
+ */
+ protected void reportPlayerKnockedOut (int pidx)
+ {
+ BodyObject user = getPlayer(pidx);
+ OidList knocky = new OidList(1);
+ knocky.add(user.getOid());
+
+ DObject place = YoServer.omgr.getObject(user.location);
+ if (place != null) {
+ place.postMessage(PLAYER_KNOCKED_OUT, new Object[] { knocky });
+ }
+ }
+
+ // documentation inherited
+ protected void didShutdown ()
+ {
+ super.didShutdown();
+
+ // make sure our update interval is unregistered
+ if (_uiid != -1) {
+ // remove the client update interval
+ IntervalManager.remove(_uiid);
+ _uiid = -1;
+ }
+
+ // clear out our service registration
+ _invmgr.clearDispatcher(_puzobj.puzzleGameService);
+
+ // clear out our status indicator
+ YoServer.status.removeFromPuzzles(new Integer(_puzobj.getOid()));
+ }
+
+ /**
+ * Returns whether game conclusion antics such as rating updates
+ * should be performed when an in-play game is ended. Derived classes
+ * may wish to override this method to customize the conditions under
+ * which the game is concluded.
+ */
+ protected boolean shouldConcludeGame ()
+ {
+ return (_puzobj.state == PuzzleObject.GAME_OVER);
+ }
+
+ /**
+ * Applies progress updates received from the client. If puzzle
+ * debugging is enabled, this also compares the client board dumps
+ * provided along with each puzzle event.
+ */
+ protected void applyProgressEvents (int pidx, int[] gevents, Board[] states)
+ {
+ int size = gevents.length;
+ boolean before = compareBeforeApply();
+
+ for (int ii = 0, pos = 0; ii < size; ii++) {
+ int gevent = gevents[ii];
+
+ // if we have state syncing enabled, make sure the board is
+ // correct before applying the event
+ if (before && (states != null)) {
+ compareBoards(pidx, states[ii]);
+ }
+
+ // apply the event to the player's board
+ if (!applyProgressEvent(pidx, gevent)) {
+ Log.warning("Unknown event [puzzle=" + where() +
+ ", pidx=" + pidx + ", event=" + gevent + "].");
+ }
+
+ // maybe we are comparing boards afterwards
+ if (!before && (states != null)) {
+ compareBoards(pidx, states[ii]);
+ }
+ }
+ }
+
+ /**
+ * Compare our server board to the specified sent-back user board.
+ */
+ protected void compareBoards (int pidx, Board boardstate)
+ {
+// Log.info("About to apply progress event [game=" + _puzobj.which() +
+// ", pidx=" + pidx + "].");
+ if (DEBUG_PUZZLE) {
+ // if we're debugging, dump the board state every time
+ // we're about to apply an event
+ _boards[pidx].dumpAndCompare(boardstate);
+ }
+
+ if (!_boards[pidx].equals(boardstate)) {
+ Log.warning("Client and server board states not equal! " +
+ "[game=" + _puzobj.which() +
+ ", type=" + _puzobj.getClass().getName() +
+ "].");
+ if (!DEBUG_PUZZLE) {
+ // otherwise, dump the board state only if the
+ // client's board differs from the server's
+ _boards[pidx].dumpAndCompare(boardstate);
+ } else {
+ // and if we're debugging, bail out so that we
+ // know something's royally borked
+ System.exit(0);
+ }
+ }
+ }
+
+ /**
+ * Called by {@link #updateProgress} to give the server a chance to
+ * apply each game event received from the client to the respective
+ * player's server-side board and, someday, confirm their validity.
+ * Derived classes that make use of the progress updating
+ * functionality should be sure to override this method to perform
+ * their game-specific event application antics. They should first
+ * perform a call to super() to see if the event is handled there.
+ *
+ * @return true to indicate that the event was handled.
+ */
+ protected boolean applyProgressEvent (int pidx, int gevent)
+ {
+ return false;
+ }
+
+ // documentation inherited
+ protected void bodyLeft (int bodyOid)
+ {
+ super.bodyLeft(bodyOid);
+
+ int pidx = IntListUtil.indexOf(_playerOids, bodyOid);
+ if (pidx != -1) {
+ if (_puzobj.state == PuzzleObject.AWAITING_PLAYERS &&
+ isPartyGame()) {
+ // handle a player leaving a party game that hasn't yet begun
+ if (removePlayer(getPlayerName(pidx))) {
+ // if they were the creator, choose a new creator
+ if (getPlayerCount() > 0 && _puzobj.creator == pidx) {
+ int npidx = getNextCreator(pidx);
+ _puzobj.setCreator(npidx);
+ // inform occupants of the creator change
+ String message = MessageBundle.tcompose(
+ "m.creator_replaced", getPlayerName(npidx));
+ systemMessage(message);
+ }
+ }
+ }
+ }
+
+ if (_puzobj.state != PuzzleObject.GAME_OVER) {
+ // inform remaining users that the user left
+ BodyObject user = (BodyObject)YoServer.omgr.getObject(bodyOid);
+ if (user != null) {
+ systemMessage(MessageBundle.tcompose(
+ "m.user_left", user.username));
+ }
+ }
+ }
+
+ /**
+ * Returns the player index of the next feasible creating player
+ * following the given player index, or -1 if there is no
+ * such available player.
+ */
+ protected int getNextCreator (int pidx)
+ {
+ int size = getPlayerSlots();
+ int npidx = pidx;
+ do {
+ npidx = (npidx + 1) % size;
+ } while (npidx != pidx && !_puzobj.isOccupiedPlayer(npidx));
+ return (npidx == pidx) ? -1 : npidx;
+ }
+
+ /**
+ * Called when a player leaves the game in order to determine whether
+ * the game should be ended based on its current state, which will
+ * include updated player status for the player in question. The
+ * default implementation returns true if the game is in play and
+ * there is only one player left. Derived classes may wish to
+ * override this method in order to customize the required end-game
+ * conditions.
+ */
+ protected boolean shouldEndGame ()
+ {
+ return (_puzobj.isInPlay() && _puzobj.getActivePlayerCount() == 1);
+ }
+
+ /**
+ * Overrides the game manager implementation to mark all active
+ * players as winners. Derived classes may wish to override this
+ * method in order to customize the winning conditions.
+ */
+ protected void assignWinners (boolean[] winners)
+ {
+ for (int ii = 0; ii < winners.length; ii++) {
+ winners[ii] = _puzobj.isActivePlayer(ii);
+ }
+ }
+
+ /**
+ * Creates and returns a new starting board for the given player.
+ */
+ protected abstract Board newBoard (int pidx);
+
+ /**
+ * Creates and returns a new board summary for the given board.
+ * Puzzles that do not make use of board summaries should implement
+ * this method and return null.
+ */
+ protected abstract BoardSummary newBoardSummary (Board board);
+
+ // documentation inherited
+ public void attributeChanged (AttributeChangedEvent event)
+ {
+ super.attributeChanged(event);
+
+ if (event.getName().equals(PuzzleObject.DIFFICULTY)) {
+ difficultyChanged(_puzobj.difficulty);
+ }
+ }
+
+ /**
+ * Called when the puzzle difficulty level is changed.
+ */
+ public void difficultyChanged (final int level)
+ {
+ // let our delegates do their business
+ applyToDelegates(new DelegateOp() {
+ public void apply (PlaceManagerDelegate delegate) {
+ ((PuzzleManagerDelegate)delegate).difficultyChanged(level);
+ }
+ });
+ }
+
+ // documentation inherited from interface PuzzleGameProvider
+ public void updateProgress (ClientObject caller, int roundId, int[] events)
+ {
+ updateProgressSync(caller, roundId, events, null);
+ }
+
+ /**
+ * Called when the puzzle manager receives a progress update. It
+ * checks to make sure that the progress update is valid and the
+ * puzzle is still in play and then applies the updates via {@link
+ * #applyProgressEvents}.
+ */
+ public void updateProgressSync (
+ ClientObject caller, int roundId, int[] events, Board[] states)
+ {
+ // determine the caller's player index in the game
+ int pidx = IntListUtil.indexOf(_playerOids, caller.getOid());
+ if (pidx == -1) {
+ Log.warning("Received progress update for non-player?! " +
+ "[game=" + _puzobj.which() + ", who=" + caller.who() +
+ ", ploids=" + StringUtil.toString(_playerOids) + "].");
+ return;
+ }
+
+ // bail if the progress update isn't for the current round
+ if (roundId != _puzobj.roundId) {
+ // only warn if this isn't a straggling update from the
+ // previous round
+ if (roundId != _puzobj.roundId-1) {
+ Log.warning("Received progress update for invalid round, " +
+ "not applying [game=" + _puzobj.which() +
+ ", invalidRoundId=" + roundId +
+ ", roundId=" + _puzobj.roundId + "].");
+ }
+ return;
+ }
+
+ // if the game is over, we wing straggling updates
+ if (!_puzobj.isInPlay()) {
+ Log.debug("Ignoring straggling events " +
+ "[game=" + _puzobj.which() +
+ ", user=" + getPlayerName(pidx) +
+ ", events=" + StringUtil.toString(events) + "].");
+ return;
+ }
+
+// Log.info("Handling progress events [game=" + _puzobj.which() +
+// ", pidx=" + pidx + ", roundId=" + roundId +
+// ", count=" + events.length + "].");
+
+ // note that we received a progress update from this player
+ _lastProgress[pidx] = System.currentTimeMillis();
+
+ // apply the progress events to the player's puzzle state
+ applyProgressEvents(pidx, events, states);
+ }
+
+ // documentation inherited
+ protected void tick (long tickStamp)
+ {
+ super.tick(tickStamp);
+
+ // every five seconds, we call the inactivity checking code
+ if (_puzobj != null && _puzobj.isInPlay() && checkForInactivity()) {
+ int pcount = getPlayerSlots();
+ for (int ii = 0; ii < pcount && _puzobj.isInPlay(); ii++) {
+ if (!isAI(ii)) {
+ checkPlayerActivity(tickStamp, ii);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns whether {@link #checkPlayerActivity} should be called
+ * periodically while the game is in play to make sure players are
+ * still active.
+ */
+ protected boolean checkForInactivity ()
+ {
+ return false;
+ }
+
+ /**
+ * Called periodically for each human player to give puzzles a chance
+ * to make sure all such players are engaging in reasonable levels of
+ * activity. The default implementation does naught.
+ */
+ protected void checkPlayerActivity (long tickStamp, int pidx)
+ {
+ // nothing for now
+ }
+
+ /**
+ * Get the performance level for the specified percentile score.
+ */
+ public static byte getPerformanceLevel (int pctile)
+ {
+ for (int ii=0; ii < PERFORMANCE_CUTOFFS.length; ii++) {
+ if (pctile <= PERFORMANCE_CUTOFFS[ii]) {
+ return (byte) ii;
+ }
+ }
+ return (byte) PERFORMANCE_CUTOFFS.length;
+ }
+
+ /** A casted reference to our puzzle config object. */
+ protected PuzzleConfig _puzconfig;
+
+ /** A casted reference to our puzzle game object. */
+ protected PuzzleObject _puzobj;
+
+ /** The player boards. */
+ protected Board[] _boards;
+
+ /** The client update interval identifier. */
+ protected int _uiid = -1;
+
+ /** Used to track the last time we received a progress event from each
+ * player in this puzzle. */
+ protected long[] _lastProgress;
+
+ /** The experience points granted to a player on puzzle completion. */
+ protected static final int EXPERIENCE = 10;
+
+ /** The percentile cutoffs for performance levels. Above the last one
+ * is "incredible". These could be defined in {@link PuzzleCodes}, but
+ * then users could find out these super-secret cutoff values! */
+ protected static final int[] PERFORMANCE_CUTOFFS = new int[] {
+ 5, 20, 50, 75, 95 };
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java b/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java
new file mode 100644
index 000000000..783df0ab3
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java
@@ -0,0 +1,28 @@
+//
+// $Id: PuzzleManagerDelegate.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import com.threerings.parlor.game.GameManagerDelegate;
+
+/**
+ * Extends the {@link GameManagerDelegate} mechanism with puzzle manager
+ * specific methods (of which there are currently none).
+ */
+public class PuzzleManagerDelegate extends GameManagerDelegate
+{
+ /**
+ * Constructs a puzzle manager delegate.
+ */
+ public PuzzleManagerDelegate (PuzzleManager puzmgr)
+ {
+ super(puzmgr);
+ }
+
+ /**
+ * Called when the puzzle difficulty level is changed.
+ */
+ public void difficultyChanged (int level)
+ {
+ }
+}
diff --git a/src/java/com/threerings/puzzle/server/PuzzleProvider.java b/src/java/com/threerings/puzzle/server/PuzzleProvider.java
new file mode 100644
index 000000000..0bf38fd49
--- /dev/null
+++ b/src/java/com/threerings/puzzle/server/PuzzleProvider.java
@@ -0,0 +1,259 @@
+//
+// $Id: PuzzleProvider.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.server;
+
+import com.samskivert.util.StringUtil;
+
+import com.threerings.presents.client.InvocationService.InvocationListener;
+import com.threerings.presents.data.ClientObject;
+import com.threerings.presents.dobj.RootDObjectManager;
+import com.threerings.presents.server.InvocationException;
+import com.threerings.presents.server.InvocationProvider;
+
+import com.threerings.crowd.data.BodyObject;
+import com.threerings.crowd.data.OccupantInfo;
+import com.threerings.crowd.data.PlaceConfig;
+import com.threerings.crowd.data.PlaceObject;
+import com.threerings.crowd.server.PlaceManager;
+import com.threerings.crowd.server.PlaceRegistry;
+
+import com.threerings.parlor.game.GameManager;
+
+import com.threerings.puzzle.Log;
+import com.threerings.puzzle.client.PuzzleService.EnterPuzzleListener;
+import com.threerings.puzzle.data.Board;
+import com.threerings.puzzle.data.BoardSummary;
+import com.threerings.puzzle.data.PuzzleCodes;
+import com.threerings.puzzle.data.PuzzleObject;
+import com.threerings.puzzle.data.PuzzlerObject;
+import com.threerings.puzzle.data.SolitairePuzzleConfig;
+
+/**
+ * Handles the server end of the puzzle services.
+ */
+public class PuzzleProvider
+ implements InvocationProvider, PuzzleCodes
+{
+ /**
+ * Constructs a puzzle provider instance.
+ */
+ public PuzzleProvider (RootDObjectManager omgr, PlaceRegistry plreg)
+ {
+ _omgr = omgr;
+ _plreg = plreg;
+ }
+
+ /**
+ * Processes a request from a client to start a puzzle.
+ */
+ public void startPuzzle (
+ ClientObject caller, SolitairePuzzleConfig config,
+ InvocationListener listener)
+ throws InvocationException
+ {
+ BodyObject user = (BodyObject)caller;
+
+ Log.debug("Processing start puzzle [caller=" + user.who() +
+ ", config=" + config + "].");
+
+ try {
+ // just this fellow will be playing
+ config.players = new String[] { user.username };
+
+ // create the game manager and begin its initialization
+ // process
+ GameManager gmgr = (GameManager)_plreg.createPlace(config, null);
+
+ // the game manager will take care of notifying the player
+ // that the game has been created once it has been started up
+
+ } catch (InstantiationException ie) {
+ Log.warning("Error instantiating puzzle manager " +
+ "[for=" + caller.who() + ", config=" + config + "].");
+ Log.logStackTrace(ie);
+ throw new InvocationException(INTERNAL_ERROR);
+ }
+ }
+
+ /**
+ * Processes a request from a client to enter a puzzle.
+ */
+ public void enterPuzzle (
+ ClientObject caller, int puzzleOid, EnterPuzzleListener listener)
+ throws InvocationException
+ {
+ // do the entry and send the response
+ listener.puzzleEntered(enterPuzzle((BodyObject)caller, puzzleOid));
+ }
+
+ /**
+ * Processes a request from a client to leave their current puzzle.
+ */
+ public void leavePuzzle (ClientObject caller)
+ {
+ BodyObject user = (BodyObject)caller;
+ int puzzleOid = ((PuzzlerObject)user).getPuzzleLoc();
+
+ // make sure they're currently in a puzzle
+ if (puzzleOid == -1) {
+ Log.warning("Received leave puzzle request from user that " +
+ "isn't in a puzzle [user=" + user.who() + "].");
+ return;
+ }
+
+ // make sure the puzzle in question actually exists
+ PlaceManager pmgr = _plreg.getPlaceManager(puzzleOid);
+ if (pmgr == null) {
+ Log.info("Requested to leave a non-existent puzzle " +
+ "[user=" + user.who() + "].");
+ return;
+ }
+
+ // depart their old puzzle
+ departPuzzle(user);
+
+ // set their puzzle location to -1 to indicate that they're no
+ // longer in a puzzle
+ ((PuzzlerObject)user).setPuzzleLoc(-1);
+ }
+
+ /**
+ * Processes a request from a client to change the difficulty level of
+ * their current puzzle.
+ */
+ public void changeDifficulty (ClientObject caller, int level)
+ {
+ BodyObject user = (BodyObject)caller;
+ int puzzleOid = ((PuzzlerObject)user).getPuzzleLoc();
+
+ // make sure they're currently in a puzzle
+ if (puzzleOid == -1) {
+ Log.warning("Received change difficulty request from user that " +
+ "isn't in a puzzle [user=" + user.who() + "].");
+ return;
+ }
+
+ // make sure the puzzle in question actually exists
+ PlaceManager pmgr = _plreg.getPlaceManager(puzzleOid);
+ if (pmgr == null) {
+ Log.info("Requested to change difficulty of a non-existent " +
+ "puzzle [user=" + user.who() + "].");
+ return;
+ }
+
+ // change the puzzle difficulty level
+ PuzzleObject puzobj = (PuzzleObject)pmgr.getPlaceObject();
+ puzobj.setDifficulty(level);
+ }
+
+ /**
+ * Moves the specified body from whatever puzzle they currently occupy
+ * to the puzzle identified by the supplied oid.
+ *
+ * @return the config object for the new location.
+ *
+ * @exception InvocationException thrown if the entry was not
+ * successful for some reason (which will be communicated as an error
+ * code in the exception's message data).
+ */
+ public PlaceConfig enterPuzzle (BodyObject user, int puzzleOid)
+ throws InvocationException
+ {
+ int bodoid = user.getOid();
+ int puzzleLoc = ((PuzzlerObject)user).getPuzzleLoc();
+
+ // make sure the place in question actually exists
+ PlaceManager pmgr = _plreg.getPlaceManager(puzzleOid);
+ if (pmgr == null) {
+ Log.info("Requested to move to non-existent place " +
+ "[user=" + user.who() + ", puzzle=" + puzzleOid + "].");
+ throw new InvocationException(NO_SUCH_PUZZLE);
+ }
+
+ // acquire a lock on the body object to ensure that rapid fire
+ // enterPuzzle requests don't break things
+ if (!user.acquireLock("enterPuzzleLock")) {
+ // if we're still locked, a previous enterPuzzle request
+ // hasn't been fully processed
+ throw new InvocationException(ENTER_IN_PROGRESS);
+ }
+
+ // make sure they're not already in the puzzle they're entering
+ if (puzzleLoc == puzzleOid) {
+ throw new InvocationException(ALREADY_IN_PUZZLE);
+ }
+
+ // depart any previously occupied puzzle
+ departPuzzle(user);
+
+ // set the body's new puzzle location
+ PlaceObject place = pmgr.getPlaceObject();
+ ((PuzzlerObject)user).setPuzzleLoc(place.getOid());
+
+ // prepare to update their new puzzle location
+ place.startTransaction();
+ try {
+ // generate a new occupant info record (which will add it to
+ // the target location)
+ pmgr.buildOccupantInfo(user);
+
+ // add the body object id to the place object's occupant list
+ place.addToOccupants(bodoid);
+ } finally {
+ place.commitTransaction();
+ }
+
+ // and finally queue up a lock release event to release the lock
+ // once all these events are processed
+ user.releaseLock("enterPuzzleLock");
+
+ return pmgr.getConfig();
+ }
+
+ /**
+ * Removes the user's occupant information from the puzzle object that
+ * they are now departing.
+ */
+ protected void departPuzzle (BodyObject user)
+ {
+ int puzzleOid = ((PuzzlerObject)user).getPuzzleLoc();
+ if (puzzleOid == -1) {
+ return;
+ }
+ int bodoid = user.getOid();
+
+ // remove them from the occupant list of the previous puzzle
+ try {
+ PlaceObject pold = (PlaceObject)_omgr.getObject(puzzleOid);
+ if (pold != null) {
+ Integer key = new Integer(bodoid);
+ // remove their occupant info (which is keyed on oid) and
+ // remove them from the occupant list
+ pold.startTransaction();
+ try {
+ pold.removeFromOccupantInfo(key);
+ pold.removeFromOccupants(bodoid);
+ } finally {
+ pold.commitTransaction();
+ }
+
+ } else {
+ Log.info("Body's prior puzzle no longer around? " +
+ "[boid=" + bodoid +
+ ", puzoid=" + puzzleOid + "].");
+ }
+
+ } catch (ClassCastException cce) {
+ Log.warning("Body claims to be in puzzle which references " +
+ "non-PlaceObject!? [boid=" + bodoid +
+ ", puzoid=" + puzzleOid + "].");
+ }
+ }
+
+ /** The distributed object manager with which we interoperate. */
+ protected RootDObjectManager _omgr;
+
+ /** The place registry with which we interoperate. */
+ protected PlaceRegistry _plreg;
+}
diff --git a/src/java/com/threerings/puzzle/util/PointSet.java b/src/java/com/threerings/puzzle/util/PointSet.java
new file mode 100644
index 000000000..2710519de
--- /dev/null
+++ b/src/java/com/threerings/puzzle/util/PointSet.java
@@ -0,0 +1,227 @@
+//
+// $Id: PointSet.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.util;
+
+import java.awt.Point;
+import java.util.Iterator;
+
+import com.threerings.puzzle.Log;
+
+/**
+ * The point set class provides an efficient implementation of a set
+ * containing two-dimensional point values as (x, y).
+ */
+public class PointSet
+{
+ /**
+ * Creates a point set that can contain points within the given range
+ * of values.
+ *
+ * @param rangeX the maximum x-axis range.
+ * @param rangeY the maximum y-axis range.
+ */
+ public PointSet (int rangeX, int rangeY)
+ {
+ _rangeX = rangeX;
+ _rangeY = rangeY;
+ _points = new boolean[rangeX][rangeY];
+ }
+
+ /**
+ * Adds a point to the set and returns whether the point was already
+ * present in the set.
+ *
+ * @param x the point x-coordinate.
+ * @param y the point y-coordinate.
+ *
+ * @return true if the point was already present, false if not.
+ */
+ public boolean add (int x, int y)
+ {
+ boolean present = _points[x][y];
+ _points[x][y] = true;
+ if (!present) {
+ _count++;
+ }
+ return present;
+ }
+
+ /**
+ * Adds all points in the given set to this set.
+ *
+ * @param set the set containing points to add.
+ */
+ public void addAll (PointSet set)
+ {
+ Iterator iter = set.iterator();
+ Point pt;
+ while ((pt = (Point)iter.next()) != null) {
+ add(pt.x, pt.y);
+ }
+ }
+
+ /**
+ * Clears all points from this set.
+ */
+ public void clear ()
+ {
+ if (_count == 0) {
+ // no need to clear anything
+ return;
+ }
+
+ for (int xx = 0; xx < _rangeX; xx++) {
+ for (int yy = 0; yy < _rangeY; yy++) {
+ _points[xx][yy] = false;
+ }
+ }
+ _count = 0;
+ }
+
+ /**
+ * Returns whether this set contains the given point.
+ *
+ * @param x the point x-coordinate.
+ * @param y the point y-coordinate.
+ *
+ * @return true if the set contains the point, false if not.
+ */
+ public boolean contains (int x, int y)
+ {
+ return (_points[x][y]);
+ }
+
+ /**
+ * Returns whether this set is empty.
+ *
+ * @return true if the set is empty, false if not.
+ */
+ public boolean isEmpty ()
+ {
+ return (_count == 0);
+ }
+
+ /**
+ * Returns an iterator that iterates over the points in this set,
+ * returning them as {@link Point} objects. Note that the iterator
+ * uses a single point object internally, and so callers should create
+ * their own copy of the point if they plan to do something fancy with
+ * it.
+ *
+ * @return the iterator over the set's points.
+ */
+ public Iterator iterator ()
+ {
+ return new PointIterator();
+ }
+
+ /**
+ * Removes the given point from the set and returns whether the point
+ * was present in the set.
+ *
+ * @param x the point x-coordinate.
+ * @param y the point y-coordinate.
+ *
+ * @return true if the point was present, false if not.
+ */
+ public boolean remove (int x, int y)
+ {
+ boolean present = _points[x][y];
+ _points[x][y] = false;
+ if (present) {
+ _count--;
+ }
+ return present;
+ }
+
+ /**
+ * Returns the number of points in the set.
+ *
+ * @return the number of points.
+ */
+ public int size ()
+ {
+ return _count;
+ }
+
+ /**
+ * Returns a string representation of the point set.
+ */
+ public String toString ()
+ {
+ StringBuffer buf = new StringBuffer();
+ buf.append("[");
+ Iterator iter = iterator();
+ Point val;
+ while ((val = (Point)iter.next()) != null) {
+ buf.append("(").append(val.x);
+ buf.append(",").append(val.y);
+ buf.append(")");
+
+ if (iter.hasNext()) {
+ buf.append(", ");
+ }
+ }
+ return buf.append("]").toString();
+ }
+
+ protected class PointIterator implements Iterator
+ {
+ public boolean hasNext ()
+ {
+ return (_curCount < _count);
+ }
+
+ public Object next ()
+ {
+ if (_curCount == _count) {
+ return null;
+ }
+
+ while (!_points[_curX][_curY]) {
+ advance();
+ }
+
+ _curCount++;
+ _point.setLocation(_curX, _curY);
+
+ if (_curCount < _count) {
+ advance();
+ }
+
+ return _point;
+ }
+
+ public void remove ()
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ protected void advance ()
+ {
+ if ((++_curX) >= _rangeX) {
+ _curX = 0;
+ _curY++;
+ }
+
+ if (_curY >= _rangeY) {
+ Log.warning("Advanced past point range.");
+ _curY = 0;
+ }
+ }
+
+ protected int _curCount = 0;
+ protected int _curX = 0, _curY = 0;
+ protected Point _point = new Point();
+ }
+
+ /** The dimensions of the point array. */
+ protected int _rangeX, _rangeY;
+
+ /** The points in the set. */
+ protected boolean _points[][];
+
+ /** The number of points in the set. */
+ protected int _count;
+}
diff --git a/src/java/com/threerings/puzzle/util/PuzzleContext.java b/src/java/com/threerings/puzzle/util/PuzzleContext.java
new file mode 100644
index 000000000..00495f9f3
--- /dev/null
+++ b/src/java/com/threerings/puzzle/util/PuzzleContext.java
@@ -0,0 +1,48 @@
+//
+// $Id: PuzzleContext.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.util;
+
+import com.threerings.util.KeyDispatcher;
+import com.threerings.util.KeyboardManager;
+
+import com.threerings.media.FrameManager;
+import com.threerings.media.sound.SoundManager;
+
+import com.threerings.crowd.chat.client.ChatDirector;
+
+/**
+ * Provides access to entities needed by the puzzle services.
+ */
+public interface PuzzleContext
+{
+ /**
+ * Returns the username of the local user.
+ */
+ public String getUsername ();
+
+ /**
+ * Provides access to the frame manager.
+ */
+ public FrameManager getFrameManager ();
+
+ /**
+ * Provides access to the keyboard manager.
+ */
+ public KeyboardManager getKeyboardManager ();
+
+ /**
+ * Provides access to the key dispatcher.
+ */
+ public KeyDispatcher getKeyDispatcher ();
+
+ /**
+ * Provides access to the sound manager.
+ */
+ public SoundManager getSoundManager ();
+
+ /**
+ * Provides access to the chat director.
+ */
+ public ChatDirector getChatDirector ();
+}
diff --git a/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java b/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java
new file mode 100644
index 000000000..11f688674
--- /dev/null
+++ b/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java
@@ -0,0 +1,29 @@
+//
+// $Id: PuzzleGameUtil.java,v 1.1 2003/11/26 01:42:34 mdb Exp $
+
+package com.threerings.puzzle.util;
+
+import java.awt.event.KeyEvent;
+import com.threerings.util.KeyTranslatorImpl;
+import com.threerings.puzzle.client.PuzzleController;
+
+/**
+ * Puzzle game related utilities.
+ */
+public class PuzzleGameUtil
+{
+ /**
+ * Returns a key translator configured with basic puzzle game
+ * mappings.
+ */
+ public static KeyTranslatorImpl getKeyTranslator ()
+ {
+ KeyTranslatorImpl xlate = new KeyTranslatorImpl();
+
+ // add the standard pause keys
+ xlate.addPressCommand(
+ KeyEvent.VK_P, PuzzleController.TOGGLE_CHATTING);
+
+ return xlate;
+ }
+}