All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.puzzle.server.PuzzleManager Maven / Gradle / Ivy

The newest version!
//
// $Id$
//
// Vilya library - tools for developing networked games
// Copyright (C) 2002-2012 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 - 2024 Weber Informatics LLC | Privacy Policy