Files
narya/src/java/com/threerings/puzzle/client/PuzzleController.java
T
Ray Greenwell cb7007226b Use the good'ole MouseHijacker to prevent the click that unpauses to
pass through to the board view.


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2996 542714f4-19e9-0310-aa3c-eee0fc999fb1
2004-04-29 00:28:05 +00:00

1012 lines
32 KiB
Java

//
// $Id: PuzzleController.java,v 1.7 2004/04/29 00:28:05 ray Exp $
package com.threerings.puzzle.client;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.KeyAdapter;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import com.samskivert.swing.util.MouseHijacker;
import com.samskivert.util.CollectionUtil;
import com.samskivert.util.ObserverList;
import com.threerings.media.FrameParticipant;
import com.threerings.media.sound.SoundCodes;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.ElementUpdateListener;
import com.threerings.presents.dobj.ElementUpdatedEvent;
import com.threerings.crowd.client.PlaceControllerDelegate;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.parlor.game.GameController;
import com.threerings.puzzle.Log;
import com.threerings.puzzle.data.Board;
import com.threerings.puzzle.data.PuzzleCodes;
import com.threerings.puzzle.data.PuzzleConfig;
import com.threerings.puzzle.data.PuzzleObject;
import com.threerings.puzzle.util.PuzzleContext;
/**
* The puzzle game controller handles logical actions for a puzzle game.
*/
public abstract class PuzzleController extends GameController
implements PuzzleCodes
{
/** The action command to toggle chatting mode. */
public static final String TOGGLE_CHATTING = "toggle_chat";
/** Used by {@link #fireWhenActionCleared}. */
public static interface ClearPender
{
/** {@link #actionCleared} return code. */
public static final int RESTART_ACTION = -1;
/** {@link #actionCleared} return code. */
public static final int CARE_NOT = 0;
/** {@link #actionCleared} return code. */
public static final int NO_RESTART_ACTION = 1;
/**
* Called when the action is fully cleared.
*
* @return One of {@link #RESTART_ACTION}, {@link #CARE_NOT} or
* {@link #NO_RESTART_ACTION}.
*/
public int actionCleared ();
}
// documentation inherited
protected void didInit ()
{
super.didInit();
_panel = (PuzzlePanel)_view;
_pctx = (PuzzleContext)_ctx;
// initialize the puzzle panel
_puzconfig = (PuzzleConfig)_config;
_panel.init(_puzconfig);
// initialize the board view
_pview = _panel.getBoardView();
_pview.setController(this);
}
/**
* Creates and returns a new board model.
*/
protected abstract Board newBoard ();
/**
* Returns the board associated with the puzzle.
*/
public Board getBoard ()
{
return _pboard;
}
/**
* Returns the player's index in the list of players for the game.
*/
public int getPlayerIndex ()
{
return _pidx;
}
/**
* Convenience method for playing a puzzle sound effect.
*/
public void playSound (String packagePath, String key)
{
_pctx.getSoundManager().play(
SoundCodes.GAME_FX, packagePath, key);
}
// documentation inherited
public void setGameOver (boolean gameOver)
{
super.setGameOver(gameOver);
// clear the action if we're informed that the game is over early
// by the client
if (gameOver) {
clearAction();
}
}
/**
* Returns true if the puzzle has action, false if the action is
* cleared or it is suspended.
*/
public boolean hasAction ()
{
return (_astate == ACTION_GOING);
}
/**
* Sets whether we're focusing on the chat window rather than the puzzle.
*/
public void setChatting (boolean chatting)
{
// ignore the request if we're already there
if ((isChatting() == chatting) ||
// ..or if we want to initiate chatting and..
// we either can't right now or we don't have action
(chatting && (!canStartChatting() || !hasAction()))) {
return;
}
// update the panel
_panel.setPuzzleGrabsKeys(!chatting);
// if we're moving focus to chat..
if (chatting) {
// don't let mouse clicks land on the board
final MouseHijacker jack = new MouseHijacker(_panel.getBoardView());
// ...enable "click to unchat" mode in the puzzle board view
MouseAdapter unpauser = new MouseAdapter() {
public void mousePressed (MouseEvent event) {
setChatting(false);
_panel.removeMouseListener(this);
_panel.getBoardView().removeMouseListener(this);
jack.release();
}
};
_panel.addMouseListener(unpauser);
_panel.getBoardView().addMouseListener(unpauser);
}
// update the chatting state
_chatting = chatting;
// dispatch the change to our delegates
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).setChatting(_chatting);
}
});
// and check if we should be suspending the action during this pause
if (supportsActionPause()) {
// clear the action if we're pausing, resume it if we're
// unpausing
if (chatting) {
clearAction();
} else {
safeStartAction();
}
_pview.setPaused(chatting);
}
}
/**
* Get the (untranslated) string to display when the puzzle is paused.
*/
public String getPauseString ()
{
return "m.paused";
}
/**
* Derived classes should override this and return false if their
* action should not be paused when the user switches control to the
* chat area.
*/
protected boolean supportsActionPause ()
{
return true;
}
/**
* Can we start chatting at this juncture?
*/
protected boolean canStartChatting ()
{
// if we're waiting, we can't pause
if (isWaiting()) {
return false;
}
// otherwise, check with the delegates
final boolean[] canChatNow = new boolean[] { true };
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
canChatNow[0] =
((PuzzleControllerDelegate)delegate).canStartChatting() &&
canChatNow[0];
}
});
return canChatNow[0];
}
/**
* Returns true if the puzzle has been defocused because the player
* is doing some chatting.
*/
public boolean isChatting ()
{
return _chatting;
}
// documentation inherited
public void willEnterPlace (PlaceObject plobj)
{
super.willEnterPlace(plobj);
// register the puzzle object's chat
_pctx.getChatDirector().addAuxiliarySource(plobj, PUZZLE_CHAT_TYPE);
// get a casted reference to our puzzle object
_puzobj = (PuzzleObject)plobj;
_puzobj.addListener(_kolist);
_puzobj.addListener(_mlist);
// listen to key events..
_pctx.getKeyDispatcher().addGlobalKeyListener(_globalKeyListener);
// save off our player index
_pidx = _puzobj.getPlayerIndex(_pctx.getUsername());
// generate the starting board
generateNewBoard();
// if the game is already in play, start up the action
if (_puzobj.state == PuzzleObject.IN_PLAY) {
startAction();
}
}
// documentation inherited
public void mayLeavePlace (PlaceObject plobj)
{
super.mayLeavePlace(plobj);
// flush any pending progress events
sendProgressUpdate();
}
// documentation inherited
public void didLeavePlace (PlaceObject plobj)
{
super.didLeavePlace(plobj);
// clean up and clear out
clearAction();
// we're certainly no longer waiting
_waitstamp = -1;
// unregister the puzzle object's chat
_pctx.getChatDirector().removeAuxiliarySource(plobj);
// stop listening to key events..
_pctx.getKeyDispatcher().removeGlobalKeyListener(_globalKeyListener);
// clear out the puzzle object
if (_puzobj != null) {
_puzobj.removeListener(_mlist);
_puzobj.removeListener(_kolist);
_puzobj = null;
}
}
/**
* Puzzles that do not have "action" that starts and stops (via {@link
* #startAction} and {@link #clearAction}) when the puzzle starts and
* stops can override this method and return false.
*/
protected boolean isActionPuzzle ()
{
return true;
}
/**
* A way for controllers to display a puzzle-related system message.
*/
public void systemMessage (String bundle, String msg)
{
_pctx.getChatDirector().displayInfo(bundle, msg, PUZZLE_CHAT_TYPE);
}
// documentation inherited
protected void gameDidStart ()
{
// we have to postpone all game starting activity until the
// current action has ended; only after all the animations have
// been completed will everything be in a state fit for starting
// back up again
fireWhenActionCleared(new ClearPender() {
public int actionCleared () {
// do the standard game did start business
PuzzleController.super.gameDidStart();
return RESTART_ACTION;
}
});
}
// documentation inherited
protected void gameDidEnd ()
{
// clean up and clear out
clearAction();
// wait until the action is cleared before we roll down to our
// delegates and do all that business
fireWhenActionCleared(new ClearPender() {
public int actionCleared () {
PuzzleController.super.gameDidEnd();
return CARE_NOT;
}
});
}
// documentation inherited
protected void gameWillReset ()
{
super.gameWillReset();
// stop the old action
clearAction();
// when the server gets around to resetting the game, we'll get a
// 'state => IN_PLAY' message which will result in gameDidStart()
// being called and starting the action back up
}
/**
* Called when a new board is set.
*/
public void setBoard (Board board)
{
// we don't need to do anything by default
}
/**
* Derived classes should override this method and do whatever is
* necessary to start up the action for their puzzle. This could be
* called when the user is already in the "room" and the game starts,
* or immediately upon entering the room if the game is already
* started (for example if they disconnected and reconnected to a game
* already in progress).
*/
protected void startAction ()
{
// do nothing if we're not an action puzzle
if (!isActionPuzzle()) {
return;
}
// refuse to start the action if our puzzle view is hidden
if (!_panel.getBoardView().isShowing()) {
Log.warning("Refusing to start action on hidden puzzle.");
Thread.dumpStack();
return;
}
// refuse to start the action if it's already going
if (_astate != ACTION_CLEARED) {
Log.warning("Action state inappropriate for startAction() " +
"[astate=" + _astate + "].");
Thread.dumpStack();
return;
}
if (isChatting() && supportsActionPause()) {
Log.info("Not starting action, player is chatting in a puzzle " +
"that supports pausing the action.");
return;
}
Log.debug("Starting puzzle action.");
// register the game progress updater; it may already be updated
// because we can cycle through clearing the action and starting
// it again before the updater gets a chance to unregister itself
if (!_pctx.getFrameManager().isRegisteredFrameParticipant(_updater)) {
_pctx.getFrameManager().registerFrameParticipant(_updater);
}
// make a note that we've started the action
_astate = ACTION_GOING;
// let our panel know what's up
_panel.startAction();
// and if we're not currently chatting, set the puzzle to grab
// keys and for the chatbox to look disabled
if (!isChatting()) {
_panel.setPuzzleGrabsKeys(true);
}
// let our delegates do their business
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).startAction();
}
});
}
/**
* If it is not known whether the puzzle board view has finished
* animating its final bits after a previous call to {@link
* #clearAction}, this method should be used instead of {@link
* #startAction} as it will wait until the action is confirmedly over
* before starting it anew.
*/
protected void safeStartAction ()
{
// do nothing if we're not an action puzzle
if (!isActionPuzzle()) {
return;
}
fireWhenActionCleared(new ClearPender() {
public int actionCleared () {
return RESTART_ACTION;
}
});
}
/**
* Called when the game has ended or when it is going to reset and
* when the client leaves the game "room". This method does not always
* immediately clear the action, but may mark the clear as pending if
* the action cannot yet be cleared (as indicated by {@link
* #canClearAction}). The action will eventually be cleared which will
* result in a call to {@link #actuallyClearAction} which is what
* derived classes should override to do their action clearing
* business.
*/
protected void clearAction ()
{
// do nothing if we're not an action puzzle
if (!isActionPuzzle()) {
return;
}
// no need to clear if we're already cleared or clearing
if (_astate == CLEAR_PENDING || _astate == ACTION_CLEARED) {
return;
}
Log.debug("Attempting to clear puzzle action.");
// put ourselves into a pending clear state and attempt to clear
// the action
_astate = CLEAR_PENDING;
maybeClearAction();
}
/**
* This method is called by the {@link PuzzleBoardView} when all
* action on the board has finished.
*/
protected void boardActionCleared ()
{
// if we have a clear pending, this could be the trigger that
// allows us to clear our action
maybeClearAction();
}
/**
* Queues up code to be invoked when the action is completely cleared
* (including all remaining interesting sprites and animations on the
* puzzle board).
*/
protected void fireWhenActionCleared (ClearPender pender)
{
// if the action is already ended, fire this pender immediately
if (_astate == ACTION_CLEARED) {
if (pender.actionCleared() == ClearPender.RESTART_ACTION) {
Log.debug("Restarting action at behest of pender " +
pender + ".");
startAction();
}
} else {
Log.debug("Queueing action pender " + pender + ".");
_clearPenders.add(pender);
}
}
/**
* Returns whether or not it is safe to clear the action. The default
* behavior is to not allow the action to be cleared until all
* interesting sprites and animations in the board view have finished.
* If derived classes or delegates wish to postpone the clearing of
* the action, they can return false from this method, but they must
* then be sure to call {@link #maybeClearAction} when whatever
* condition that caused them to desire to postpone action clearing
* has finally been satisfied.
*/
protected boolean canClearAction ()
{
final boolean[] canClear = new boolean[1];
canClear[0] = (_pview.getActionCount() == 0);
if (!canClear[0]) {
_pview.dumpActors();
PuzzleBoardView.DEBUG_ACTION = true;
}
// let our delegates do their business
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
canClear[0] = canClear[0] &&
((PuzzleControllerDelegate)delegate).canClearAction();
}
});
return canClear[0];
}
/**
* Called to effect the actual clearing of our action if we've
* received some asynchronous trigger that indicates that it may well
* be safe now to clear the action.
*/
protected void maybeClearAction ()
{
if (_astate == CLEAR_PENDING && canClearAction()) {
actuallyClearAction();
}
}
/**
* Performs the actual process of clearing the action for this puzzle.
* This is only called after it is known to be safe to clear the
* action. Derived classes can override this method and clear out
* anything that is not needed while the puzzle's "action" is not
* going (timers, etc.). Anything that is cleared out here should be
* recreated in {@link #startAction}.
*/
protected void actuallyClearAction ()
{
Log.debug("Actually clearing action.");
// make a note that we've cleared the action
_astate = ACTION_CLEARED;
PuzzleBoardView.DEBUG_ACTION = false;
// let our delegates do their business
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).clearAction();
}
});
// let our panel know what's up
_panel.clearAction();
_panel.setPuzzleGrabsKeys(false); // let the user chat
// deliver one final update to the server
sendProgressUpdate();
// let derived classes do things
try {
actionWasCleared();
} catch (Exception e) {
Log.warning("Choked in actionWasCleared");
Log.logStackTrace(e);
}
// notify any penders that the action has cleared
final int[] results = new int[2];
_clearPenders.apply(new ObserverList.ObserverOp() {
public boolean apply (Object observer) {
switch (((ClearPender)observer).actionCleared()) {
case ClearPender.RESTART_ACTION: results[0]++; break;
case ClearPender.NO_RESTART_ACTION: results[1]++; break;
}
return true;
}
});
_clearPenders.clear();
// if there are no refusals and at least one restart request, go
// ahead and restart the action now
if (results[1] == 0 && results[0] > 0) {
startAction();
}
}
/**
* Called when the action was actually cleared, but before the action
* obsevers are notified.
*/
protected void actionWasCleared ()
{
}
// documentation inherited
public boolean handleAction (ActionEvent action)
{
String cmd = action.getActionCommand();
if (cmd.equals(TOGGLE_CHATTING)) {
setChatting(!isChatting());
} else {
return super.handleAction(action);
}
return true;
}
/**
* Returns the delay in milliseconds between sending each progress
* update event to the server. Derived classes may wish to override
* this to send their progress updates more or less frequently than
* the default.
*/
protected long getProgressInterval ()
{
return DEFAULT_PROGRESS_INTERVAL;
}
/**
* Signal the game to generate and distribute a new board.
*/
protected void generateNewBoard ()
{
// wait for any animations or sprites in the board to finish their
// business before setting the board into place
fireWhenActionCleared(new ClearPender() {
public int actionCleared () {
// update the player board
_pboard = newBoard();
if (_puzobj.seed != 0) {
_pboard.initializeSeed(_puzobj.seed);
}
setBoard(_pboard);
_pview.setBoard(_pboard);
// and repaint things
_pview.repaint();
// let our delegates do their business
DelegateOp dop = new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).setBoard(_pboard);
}
};
applyToDelegates(PuzzleControllerDelegate.class, dop);
return CARE_NOT;
}
});
}
/**
* Adds a waiting entity to the list of entities awaiting the receipt
* of data from the server, and without whose happiness the show must
* not go on.
*/
public void addWaiter (Object waiter)
{
if (!_waiters.contains(waiter)) {
_waiters.add(waiter);
String state = (_waitstamp == -1) ? "(suspending)" : "(suspended)";
Log.info("Adding waiter " + state + " [waiter=" + waiter + "].");
// suspend things if we weren't already suspended
if (_waitstamp == -1) {
_waitstamp = _pview.getTimeStamp();
didSuspend();
}
} else {
Log.warning("Already-waiting waiter attempted to wait again " +
"[waiter=" + waiter + "].");
}
}
/**
* Removes a waiting entity from the list of entities awaiting the
* receipt of data from the server, and without whose happiness the
* show must not go on.
*/
public void removeWaiter (Object waiter)
{
if (_waiters.remove(waiter)) {
// resume things once we've no longer got any waiters
if (_waiters.size() == 0 && _waitstamp != -1) {
long delta = _pview.getTimeStamp() - _waitstamp;
_waitstamp = -1;
didResume(delta);
}
String state = (_waitstamp == -1) ? "(resumed)" : "(suspended)";
Log.info("Removed waiter " + state + " [waiter=" + waiter + "].");
} else {
Log.warning("Requested to remove a waiter that's not waiting " +
"[waiter=" + waiter + "].");
}
}
/**
* Returns whether things are paused while we wait for a new piece
* packet from the server.
*/
public boolean isWaiting ()
{
return (_waitstamp != -1);
}
/**
* Called when the game is suspended while we wait for a new piece
* packet from the server.
*/
public void didSuspend ()
{
// we've somehow whipped through all of our pieces, so send a
// progress update to the server straightaway which will in turn
// prompt it to send us more pieces
sendProgressUpdate();
// let our delegates do their business
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).didSuspend();
}
});
}
/**
* Called when the game is resumed once a new piece packet arrives
* from the server while we were suspended awaiting its arrival.
*
* @param delta the time elapsed in milliseconds while we were
* suspended.
*/
public void didResume (final long delta)
{
// let our delegates do their business
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).didResume(delta);
}
});
}
/**
* Returns the number of progress events currently queued up for
* sending to the server with the next progress update.
*/
public int getEventCount ()
{
return _events.size();
}
/**
* Adds the given progress event and a snapshot of the supplied board
* state to the set of progress events and associated board states for
* later transmission to the server.
*/
public void addProgressEvent (int event, Board board)
{
// make sure they don't queue things up at strange times
if (_puzobj.state != PuzzleObject.IN_PLAY) {
Log.warning("Rejecting progress event; game not in play " +
"[puzobj=" + _puzobj.which() +
", event=" + event + "].");
return;
}
_events.add(new Integer(event));
if (_puzconfig.syncBoardState()) {
_states.add((board == null) ? null : board.clone());
if (board == null) {
Log.warning("Added progress event with no associated board " +
"state, server will not be able to ensure " +
"board state synchronization.");
}
}
}
/**
* Sends the server a game progress update with the list of events, as
* well as board states if {@link #SYNC_BOARD_STATE} is true.
*/
public void sendProgressUpdate ()
{
// make sure we have our puzzle object and events to send
int size = _events.size();
if (size == 0 || _puzobj == null) {
return;
}
// create an array of the events we're sending to the server
int[] events = CollectionUtil.toIntArray(_events);
_events.clear();
// Log.info("Sending progress [round=" + _puzobj.roundId +
// ", events=" + StringUtil.toString(events) + "].");
// create an array of the board states that correspond with those
// events (if state syncing is enabled)
if (_puzconfig.syncBoardState()) {
int scount = _states.size();
Board[] states = new Board[scount];
for (int ii = 0; ii < scount; ii++) {
states[ii] = (Board)_states.remove(0);
}
// send the update progress request
_puzobj.puzzleGameService.updateProgressSync(
_ctx.getClient(), _puzobj.roundId, events, states);
} else {
// send the update progress request
_puzobj.puzzleGameService.updateProgress(
_ctx.getClient(), _puzobj.roundId, events);
}
}
/**
* Called when the puzzle difficulty level is changed.
*/
public void difficultyChanged (final int level)
{
// dispatch this to our delegates
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).difficultyChanged(level);
}
});
}
/**
* Called when a player is knocked out of the game to give the puzzle
* a chance to perform any post-knockout actions that may be desired.
* Derived classes may wish to override this method but should be sure
* to call <code>super.playerKnockedOut()</code>.
*/
protected void playerKnockedOut (final int pidx)
{
// dispatch this to our delegates
applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() {
public void apply (PlaceControllerDelegate delegate) {
((PuzzleControllerDelegate)delegate).playerKnockedOut(pidx);
}
});
}
/** Handles the sending of puzzle progress updates. We can't just
* register an interval for this because sometimes the clock goes
* backwards in time in windows and our intervals don't get called for
* a long period of time which causes the server to think the client
* is disconnected or cheating and resign them from the puzzle. God
* bless you, Microsoft. */
protected FrameParticipant _updater = new FrameParticipant() {
public void tick (long tickStamp) {
if (_astate == ACTION_CLEARED) {
// remove ourselves as the action is now cleared; we can't
// do this in actuallyClearAction() because that might get
// called during the PuzzlePanel's frame tick and it's
// only safe to remove yourself during a tick(), not
// another frame participant
_pctx.getFrameManager().removeFrameParticipant(_updater);
} else if (tickStamp - _lastProgressTick > getProgressInterval()) {
_lastProgressTick = tickStamp;
sendProgressUpdate();
}
}
public boolean needsPaint () {
return false;
}
public Component getComponent () {
return null;
}
protected long _lastProgressTick;
};
/** Listens for players being knocked out. */
protected ElementUpdateListener _kolist = new ElementUpdateListener() {
public void elementUpdated (ElementUpdatedEvent event) {
String name = event.getName();
if (name.equals(PuzzleObject.PLAYER_STATUS)) {
if (event.getIntValue() == PuzzleObject.PLAYER_KNOCKED_OUT) {
playerKnockedOut(event.getIndex());
}
}
}
};
/** Listens for various attribute changes. */
protected AttributeChangeListener _mlist = new AttributeChangeListener() {
public void attributeChanged (AttributeChangedEvent event) {
String name = event.getName();
if (name.equals(PuzzleObject.DIFFICULTY)) {
difficultyChanged(_puzobj.difficulty);
} else if (name.equals(PuzzleObject.SEED)) {
generateNewBoard();
}
}
};
/** A casted reference to the client context. */
protected PuzzleContext _pctx;
/** Our player index in the game. */
protected int _pidx;
/** The puzzle panel. */
protected PuzzlePanel _panel;
/** The puzzle config. */
protected PuzzleConfig _puzconfig;
/** A reference to our puzzle game object. */
protected PuzzleObject _puzobj;
/** The puzzle board view. */
protected PuzzleBoardView _pview;
/** The puzzle board data. */
protected Board _pboard;
/** The list of relevant game events since the last progress update. */
protected ArrayList _events = new ArrayList();
/** Board snapshots that correspond to our board state after each of
* our events has been applied. */
protected ArrayList _states = new ArrayList();
/** The entities waiting on some action before the puzzle can proceed
* apace, and accordingly for whom the puzzle is paused. */
protected ArrayList _waiters = new ArrayList();
/** The time at which we paused waiting for pieces. */
protected long _waitstamp = -1;
/** A flag indicating that we're in chatting mode. */
protected boolean _chatting = false;
/** The current action state of the puzzle. */
protected int _astate = ACTION_CLEARED;
/** The action cleared penders. */
protected ObserverList _clearPenders = new ObserverList(
ObserverList.SAFE_IN_ORDER_NOTIFY);
/** A key listener that currently just toggles pause in the puzzle. */
protected KeyListener _globalKeyListener = new KeyAdapter() {
public void keyReleased (KeyEvent e)
{
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
// toggle pausyness, my pussycat
setChatting(!isChatting());
}
}
};
/** The delay in milliseconds between progress update intervals. */
protected static final long DEFAULT_PROGRESS_INTERVAL = 6000L;
/** A {@link #_astate} constant. */
protected static final int ACTION_CLEARED = 0;
/** A {@link #_astate} constant. */
protected static final int CLEAR_PENDING = 1;
/** A {@link #_astate} constant. */
protected static final int ACTION_GOING = 2;
}