com.threerings.puzzle.server.PuzzleManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vilya Show documentation
Show all versions of vilya Show documentation
Facilities for making networked multiplayer games.
//
// $Id: PuzzleManager.java 1046 2011-01-01 05:04:14Z dhoover $
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2011 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.PuzzleGameMarshaller;
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(addProvider(this, PuzzleGameMarshaller.class));
}
@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;
}
}
/**
* 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);
}
_boards[pidx].seedFromEvent(pidx, gevent);
// 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 if 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;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy