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
This commit is contained in:
Michael Bayne
2003-11-26 01:42:34 +00:00
parent 0601c94e42
commit 54eaddb27d
53 changed files with 9395 additions and 0 deletions
+38
View File
@@ -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);
}
}
@@ -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;
}
@@ -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;
}
@@ -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));
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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}.
*
* <p> 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;
}
@@ -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;
}
@@ -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;
}
}
}
@@ -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;
}
@@ -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
});
}
}
@@ -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)
});
}
}
@@ -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);
}
}
@@ -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 <code>puzzleGameService</code> field. */
public static final String PUZZLE_GAME_SERVICE = "puzzleGameService";
/** The field name of the <code>difficulty</code> field. */
public static final String DIFFICULTY = "difficulty";
/** The field name of the <code>playerStatus</code> field. */
public static final String PLAYER_STATUS = "playerStatus";
/** The field name of the <code>summaries</code> field. */
public static final String SUMMARIES = "summaries";
/** The field name of the <code>seed</code> 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 <code>puzzleGameService</code> 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 <code>difficulty</code> 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 <code>playerStatus</code> 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 <code>index</code>th element of
* <code>playerStatus</code> 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 <code>summaries</code> 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 <code>index</code>th element of
* <code>summaries</code> 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 <code>seed</code> 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;
}
}
@@ -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);
}
@@ -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
{
}
@@ -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;
}
@@ -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 <code>pos</code> with
* the screen coordinates in pixels at which a piece at <code>(col,
* row)</code> 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
* <code>(col, row)</code>.
*/
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 <code>(col,
* row)</code>.
*/
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
* <code>(0, 0)</code> 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 <code>pos</code> 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;
}
File diff suppressed because it is too large Load Diff
@@ -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);
}
@@ -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 <code>0.0</code> and <code>1.0</code>
* 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 };
}
@@ -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);
}
@@ -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;
}
@@ -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
* <code>byte</code> 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;
}
@@ -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 <code>(orient, col, row)</code>, where
* <code>orient</code> is the final orientation of the drop block;
* <code>col</code> and <code>row</code> 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 <code>null</code> 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;
}
@@ -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;
}
@@ -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";
}
@@ -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 ();
}
@@ -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 ();
}
@@ -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;
}
@@ -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);
}
}
@@ -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
* <code>short</code> 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;
}
@@ -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.
*
* <p> 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}.
*
* <p> 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.
*
* <p> 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;
}
@@ -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 <code>CW</code> or
* <code>CCW</code>.
* @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 <code>CW</code> or
* <code>CCW</code>.
*
* @return the rotated orientation.
*/
public static int getRotatedOrientation (int orient, int dir)
{
return (orient + ((dir == CW) ? 2 : 6)) % DIRECTION_COUNT;
}
}
@@ -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;
}
@@ -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
{
}
}
@@ -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 <code>a</code> is equivalent to piece
* <code>b</code> 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();
}
@@ -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.
*
* <p> 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);
}
@@ -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 <code>drops</code> 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;
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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
* <code>false</code>.
*/
public boolean needsBoardSummaries ()
{
return false;
}
/**
* Returns whether this puzzle compares board states before it applies
* progress events, or after. The default implementation returns
* <code>true</code>.
*/
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 <code>O</code> (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 <code>-1</code> 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 <code>null</code>.
*/
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 };
}
@@ -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)
{
}
}
@@ -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;
}
@@ -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 <code>(x, y)</code>.
*/
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;
}
@@ -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 ();
}
@@ -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;
}
}