// // $Id$ // // Vilya library - tools for developing networked games // Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved // http://www.threerings.net/code/vilya/ // // This library is free software; you can redistribute it and/or modify it // under the terms of the GNU Lesser General Public License as published // by the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package com.threerings.puzzle.server; import java.util.Arrays; import com.samskivert.util.IntListUtil; import com.samskivert.util.Interval; import com.samskivert.util.RandomUtil; import com.threerings.presents.data.ClientObject; import com.threerings.parlor.game.server.GameManager; 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 static com.threerings.puzzle.Log.log; /** * Extends the {@link GameManager} with facilities for puzzle games. */ public abstract class PuzzleManager extends GameManager implements PuzzleCodes, PuzzleGameProvider { /** * Returns the boards for all players. */ public Board[] getBoards () { return _boards; } /** * Returns the board summary for the given player index. */ public BoardSummary getBoardSummary (int pidx) { return (_puzobj == null || _puzobj.summaries == null) ? null : _puzobj.summaries[pidx]; } /** * Returns whether this puzzle cares to make use of per-player board summaries that are sent * periodically to all users in the puzzle via {@link #sendStatusUpdate}. The default * implementation returns false. */ public boolean needsBoardSummaries () { return false; } /** * Returns whether this puzzle compares board states before it applies progress events, or * after. The default implementation returns true. */ protected boolean compareBeforeApply () { return true; } /** * Handles the server and client states being out of sync when in debug mode. The default * implementation halts the server. */ protected void handleBoardNotEqual () { // bail out so that we know something's royally borked System.exit(0); } /** * 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(); } } /** * Applies updateBoardSummary on all the players' boards. AI board summaries should be updated * by the AI logic. */ public void updateBoardSummaries () { if (_puzobj.summaries != null) { for (int ii = 0; ii < _puzobj.summaries.length; ii++) { if (!isAI(ii) || summarizeAIBoard()) { updateBoardSummary(ii); } } } } @Override protected void playerGameDidEnd (int pidx) { super.playerGameDidEnd(pidx); updateSummaryOnDeath(pidx); } /** * Updates the board summary for a player who has been eliminated and performs an update to * communicate this change. */ protected void updateSummaryOnDeath (int pidx) { if (!isAI(pidx)) { // update the board summary with the player's final board updateBoardSummary(pidx); } // force a status update updateStatus(); } /** * Override to have board summaries for AIs automatically generated. */ protected boolean summarizeAIBoard () { return false; } @Override protected void didStartup () { super.didStartup(); // grab the puzzle object _puzobj = (PuzzleObject)_gameobj; // create and fill in our game service object _puzobj.setPuzzleGameService(_invmgr.registerDispatcher(new PuzzleGameDispatcher(this))); } @Override 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()); // compute the starting difficulty (this has to happen before we set the seed because that // triggers the generation of the boards on the client) _puzobj.setDifficulty(computeDifficulty()); // initialize the seed that goes out with this round _puzobj.setSeed(RandomUtil.rand.nextLong()); // 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 (_statusInterval == null && statusInterval > 0) { // register the status update interval to address subsequent periodic updates _statusInterval = new Interval(_omgr) { @Override public void expired () { sendStatusUpdate(); } }; _statusInterval.schedule(statusInterval, true); } } /** * Returns the frequency with which puzzle status updates are broadcast to the players (which * is accomplished via a call to {@link #sendStatusUpdate} which in turn calls {@link * #updateStatus} wherein derived classes can participate in the status update). Returning * O (the default) indicates that a periodic status update is not desired. */ protected long getStatusInterval () { return 0L; } /** * When a puzzle game starts, the manager is given the opportunity to configure the puzzle * difficulty based on information known about the player. Additionally, when the game resets * due to the player clearing the board, etc. this will be called again, so the difficulty can * be ramped up as the player progresses. In situations where ratings and experience are * tracked, the difficulty can be seeded based on the players prior performance. */ protected int computeDifficulty () { return DEFAULT_DIFFICULTY; } @Override protected void gameDidStart () { super.gameDidStart(); // 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", _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()) { // generate the latest summaries updateBoardSummaries(); // then broadcast them to the clients _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]); } } } _puzobj.setSummaries(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)); } @Override protected void gameDidEnd () { if (_statusInterval != null) { // remove the client update interval _statusInterval.cancel(); _statusInterval = null; } // send along one final status update sendStatusUpdate(); super.gameDidEnd(); } @Override protected void didShutdown () { super.didShutdown(); // make sure our update interval is unregistered if (_statusInterval != null) { // remove the client update interval _statusInterval.cancel(); _statusInterval = null; } // 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; ii < size; ii++) { int gevent = gevents[ii]; Board cboard = (states == null) ? null : states[ii]; // if we have state syncing enabled, make sure the board is correct before applying the // event if (before && (cboard != null)) { compareBoards(pidx, cboard, gevent, before); } // apply the event to the player's board if (!applyProgressEvent(pidx, gevent, cboard)) { log.warning("Unknown event [puzzle=" + where() + ", pidx=" + pidx + ", event=" + gevent + "]."); } // maybe we are comparing boards afterwards if (!before && (cboard != null)) { compareBoards(pidx, cboard, 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) { handleBoardNotEqual(); } 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. * * @param pidx the player index that submitted the progress event. * @param gevent the progress event itself. * @param cboard a snapshot of the board on the client iff the client has board syncing enabled * (which is only enabled when debugging). * * @return true to indicate that the event was handled. */ protected boolean applyProgressEvent (int pidx, int gevent, Board cboard) { return false; } /** * 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. */ @Override protected void assignWinners (boolean[] winners) { for (int ii = 0; ii < winners.length; ii++) { winners[ii] = _puzobj.isActivePlayer(ii); } } /** * Creates and returns a new starting board for the given player. */ protected abstract Board newBoard (int pidx); /** * Creates and returns a new board summary for the given board. Puzzles that do not make use * of board summaries should implement this method and return null. */ protected abstract BoardSummary newBoardSummary (Board board); // documentation inherited from interface PuzzleGameProvider public void updateProgress (ClientObject caller, int sessionId, int[] events) { updateProgressSync(caller, sessionId, 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 sessionId, int[] events, Board[] states) { // bail if the progress update isn't for the current session if (sessionId != _puzobj.sessionId) { // only warn if this isn't a straggling update from the previous session if (sessionId != _puzobj.sessionId-1) { log.warning("Received progress update for invalid session, not applying " + "[game=" + _puzobj.which() + ", invalidSessionId=" + sessionId + ", sessionId=" + _puzobj.sessionId + "]."); } return; } // if the game is over, we wing straggling updates if (!_puzobj.isInPlay()) { log.debug("Ignoring straggling events", "game", _puzobj.which(), "who", caller.who(), "events", events); return; } // 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", _playerOids); return; } // Log.info("Handling progress events [game=" + _puzobj.which() + // ", pidx=" + pidx + ", sessionId=" + sessionId + // ", 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); } @Override 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 } /** A casted reference to our puzzle game object. */ protected PuzzleObject _puzobj; /** The player boards. */ protected Board[] _boards; /** The client update interval. */ protected Interval _statusInterval; /** Tracks the last time we received a progress event from each player in this puzzle. */ protected long[] _lastProgress; }