From 54eaddb27d699fd79911fbce3a44c55dbafe2251 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Wed, 26 Nov 2003 01:42:34 +0000 Subject: [PATCH] Holy crap Batman! It's the beginnings of refactoring the basic puzzle stuff into Narya. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2876 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- src/java/com/threerings/puzzle/Log.java | 38 + .../puzzle/client/PlayerStatusView.java | 101 ++ .../puzzle/client/PuzzleAnimationWaiter.java | 57 + .../puzzle/client/PuzzleBoardView.java | 461 +++++++ .../puzzle/client/PuzzleController.java | 1012 ++++++++++++++ .../client/PuzzleControllerDelegate.java | 162 +++ .../puzzle/client/PuzzleGameService.java | 31 + .../threerings/puzzle/client/PuzzlePanel.java | 244 ++++ .../puzzle/client/PuzzleService.java | 52 + .../puzzle/client/ScoreAnimation.java | 226 +++ .../com/threerings/puzzle/data/Board.java | 187 +++ .../threerings/puzzle/data/BoardSummary.java | 57 + .../threerings/puzzle/data/PuzzleCodes.java | 55 + .../threerings/puzzle/data/PuzzleConfig.java | 66 + .../puzzle/data/PuzzleGameCodes.java | 15 + .../puzzle/data/PuzzleGameMarshaller.java | 44 + .../puzzle/data/PuzzleMarshaller.java | 104 ++ .../threerings/puzzle/data/PuzzleObject.dobj | 74 + .../threerings/puzzle/data/PuzzleObject.java | 187 +++ .../threerings/puzzle/data/PuzzlerObject.java | 25 + .../puzzle/data/SolitairePuzzleConfig.java | 11 + .../puzzle/drop/client/DropBlockSprite.java | 237 ++++ .../puzzle/drop/client/DropBoardView.java | 370 +++++ .../drop/client/DropControllerDelegate.java | 1221 +++++++++++++++++ .../puzzle/drop/client/DropPanel.java | 23 + .../puzzle/drop/client/DropSprite.java | 566 ++++++++ .../drop/client/DropSpriteObserver.java | 16 + .../puzzle/drop/client/NextBlockView.java | 93 ++ .../puzzle/drop/data/ByteDropBoard.java | 152 ++ .../puzzle/drop/data/DropBoard.java | 738 ++++++++++ .../puzzle/drop/data/DropBoardSummary.java | 71 + .../puzzle/drop/data/DropCodes.java | 21 + .../puzzle/drop/data/DropConfig.java | 17 + .../puzzle/drop/data/DropLogic.java | 23 + .../puzzle/drop/data/DropPieceCodes.java | 19 + .../puzzle/drop/data/SegmentInfo.java | 43 + .../puzzle/drop/data/ShortDropBoard.java | 151 ++ .../drop/server/DropManagerDelegate.java | 174 +++ .../puzzle/drop/util/DropBoardUtil.java | 46 + .../puzzle/drop/util/DropGameUtil.java | 53 + .../puzzle/drop/util/DropPieceProvider.java | 23 + .../puzzle/drop/util/PieceDestroyer.java | 163 +++ .../puzzle/drop/util/PieceDropLogic.java | 61 + .../puzzle/drop/util/PieceDropper.java | 340 +++++ .../puzzle/server/PuzzleDispatcher.java | 76 + .../puzzle/server/PuzzleGameDispatcher.java | 59 + .../puzzle/server/PuzzleGameProvider.java | 29 + .../puzzle/server/PuzzleManager.java | 810 +++++++++++ .../puzzle/server/PuzzleManagerDelegate.java | 28 + .../puzzle/server/PuzzleProvider.java | 259 ++++ .../com/threerings/puzzle/util/PointSet.java | 227 +++ .../threerings/puzzle/util/PuzzleContext.java | 48 + .../puzzle/util/PuzzleGameUtil.java | 29 + 53 files changed, 9395 insertions(+) create mode 100644 src/java/com/threerings/puzzle/Log.java create mode 100644 src/java/com/threerings/puzzle/client/PlayerStatusView.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleBoardView.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleController.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleGameService.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzlePanel.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleService.java create mode 100644 src/java/com/threerings/puzzle/client/ScoreAnimation.java create mode 100644 src/java/com/threerings/puzzle/data/Board.java create mode 100644 src/java/com/threerings/puzzle/data/BoardSummary.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleCodes.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleConfig.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleGameCodes.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleMarshaller.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleObject.dobj create mode 100644 src/java/com/threerings/puzzle/data/PuzzleObject.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzlerObject.java create mode 100644 src/java/com/threerings/puzzle/data/SolitairePuzzleConfig.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropBoardView.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropPanel.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropSprite.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java create mode 100644 src/java/com/threerings/puzzle/drop/client/NextBlockView.java create mode 100644 src/java/com/threerings/puzzle/drop/data/ByteDropBoard.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropBoard.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropCodes.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropConfig.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropLogic.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java create mode 100644 src/java/com/threerings/puzzle/drop/data/SegmentInfo.java create mode 100644 src/java/com/threerings/puzzle/drop/data/ShortDropBoard.java create mode 100644 src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java create mode 100644 src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java create mode 100644 src/java/com/threerings/puzzle/drop/util/DropGameUtil.java create mode 100644 src/java/com/threerings/puzzle/drop/util/DropPieceProvider.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDropper.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleDispatcher.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleGameProvider.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleManager.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleProvider.java create mode 100644 src/java/com/threerings/puzzle/util/PointSet.java create mode 100644 src/java/com/threerings/puzzle/util/PuzzleContext.java create mode 100644 src/java/com/threerings/puzzle/util/PuzzleGameUtil.java 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; + } +}