32dee3cbaf
more discipline when handling names in our code base. Any user entered name should find its way into a Name object as soon as it comes out of a text field or whatnot, and stay that way until it makes its way into a text field or into a database record (for which String objects are vastly simpler because of JORA magic). Dear God, let me never again make a change this large for the rest of my mortal life. Unfortunately, this means we have to keep an eye out for funny business pretty much everywhere. However, since we will absolutely want to test market stalls and so forth on Azure, we'll have an opportunity to iron out any funny business that might fall under the radar during our internal testing. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@2980 542714f4-19e9-0310-aa3c-eee0fc999fb1
815 lines
27 KiB
Java
815 lines
27 KiB
Java
//
|
|
// $Id: PuzzleManager.java,v 1.8 2004/03/06 11:29:19 mdb Exp $
|
|
|
|
package com.threerings.puzzle.server;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import com.samskivert.util.IntListUtil;
|
|
import com.samskivert.util.IntervalManager;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import com.threerings.presents.data.ClientObject;
|
|
import com.threerings.presents.dobj.AttributeChangedEvent;
|
|
import com.threerings.presents.dobj.DObject;
|
|
import com.threerings.presents.dobj.OidList;
|
|
import com.threerings.presents.server.util.SafeInterval;
|
|
|
|
import com.threerings.crowd.data.BodyObject;
|
|
import com.threerings.crowd.server.CrowdServer;
|
|
import com.threerings.crowd.server.PlaceManagerDelegate;
|
|
|
|
import com.threerings.parlor.game.GameManager;
|
|
|
|
import com.threerings.util.MessageBundle;
|
|
import com.threerings.util.Name;
|
|
import com.threerings.util.RandomUtil;
|
|
|
|
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)CrowdServer.omgr.getObject(ploid);
|
|
}
|
|
// otherwise look them up by name
|
|
Name name = getPlayerName(playerIdx);
|
|
return (name == null) ? null : CrowdServer.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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
public boolean shouldConcludeGame ()
|
|
{
|
|
return (_puzobj.state == PuzzleObject.GAME_OVER);
|
|
}
|
|
|
|
/**
|
|
* 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(CrowdServer.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();
|
|
|
|
// report the winners and losers if appropriate
|
|
int winnerCount = _puzobj.getWinnerCount();
|
|
boolean draw = (winnerCount == _puzobj.getPlayerCount());
|
|
if (shouldConcludeGame() && winnerCount > 0 && !draw) {
|
|
reportWinnersAndLosers();
|
|
}
|
|
|
|
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 = CrowdServer.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 = CrowdServer.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);
|
|
}
|
|
|
|
/**
|
|
* 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], gevent, before);
|
|
}
|
|
|
|
// 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], gevent, before);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compare our server board to the specified sent-back user board.
|
|
*/
|
|
protected void compareBoards (int pidx, Board boardstate,
|
|
int gevent, boolean before)
|
|
{
|
|
if (DEBUG_PUZZLE) {
|
|
Log.info((before ? "About to apply " : "Just applied ") +
|
|
"[game=" + _puzobj.which() + ", pidx=" + pidx +
|
|
", event=" + gevent + "].");
|
|
}
|
|
if (boardstate == null) {
|
|
if (DEBUG_PUZZLE) {
|
|
Log.info("No board state provided. Can't compare.");
|
|
}
|
|
return;
|
|
}
|
|
boolean equal = _boards[pidx].equals(boardstate);
|
|
if (!equal) {
|
|
Log.warning("Client and server board states not equal! " +
|
|
"[game=" + _puzobj.which() +
|
|
", type=" + _puzobj.getClass().getName() + "].");
|
|
}
|
|
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 (!equal) {
|
|
if (DEBUG_PUZZLE) {
|
|
// bail out so that we know something's royally borked
|
|
System.exit(0);
|
|
} else {
|
|
// dump the board state since we're not debugging and
|
|
// didn't just do it above
|
|
_boards[pidx].dumpAndCompare(boardstate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.isInPlay() && _puzobj.isActivePlayer(pidx)) {
|
|
// end the player's game if they bail on an in-progress puzzle
|
|
endPlayerGame(pidx);
|
|
|
|
} else 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)CrowdServer.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 };
|
|
}
|