<commit_before>//
// $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.parlor.game.server;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.logging.Level;

import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.Interval;
import com.samskivert.util.RepeatCallTracker;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import com.threerings.util.Name;

import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.MessageEvent;

import com.threerings.crowd.chat.server.SpeakUtil;

import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.server.CrowdServer;
import com.threerings.crowd.server.PlaceManager;
import com.threerings.crowd.server.PlaceManagerDelegate;

import com.threerings.parlor.data.ParlorCodes;
import com.threerings.parlor.game.data.GameAI;
import com.threerings.parlor.game.data.GameCodes;
import com.threerings.parlor.game.data.GameConfig;
import com.threerings.parlor.game.data.GameObject;
import com.threerings.parlor.server.ParlorSender;

import com.threerings.util.MessageBundle;

import static com.threerings.parlor.Log.log;

/**
 * The game manager handles the server side management of a game. It manipulates the game state in
 * accordance with the logic of the game flow and generally manages the whole game playing process.
 *
 * <p> The game manager extends the place manager because games are implicitly played in a
 * location, the players of the game implicitly bodies in that location.
 */
public class GameManager extends PlaceManager
    implements ParlorCodes, GameCodes
{
    /**
     * An interface for identifying users. A larger system using the Parlor game services can
     * enable user identification by passing a UserIdentifier to {@link #setUserIdentifier}.
     */
    public interface UserIdentifier
    {
        /** Returns the persistent user id for the specified player, or 0 if they're not a valid
         * user, or a guest, or something like that (for whom we'll track no persistent data). */
        public int getUserId (BodyObject bodyObj);
    }

    /**
     * Configures a means by which the Parlor game services can map users to a persistent user id
     * for things like per-game cookies and ratings.
     */
    public static void setUserIdentifier (UserIdentifier userIder)
    {
        _userIder = userIder;
    }

    /**
     * Returns the configuration object for the game being managed by this manager.
     */
    public GameConfig getGameConfig ()
    {
        return _gameconfig;
    }

    /**
     * A convenience method for getting the game type.
     */
    public int getMatchType ()
    {
        return _gameconfig.getMatchType();
    }

    /**
     * Adds the given player to the game at the first available player index.  This should only be
     * called before the game is started, and is most likely to be used to add players to party
     * games.
     *
     * @param player the username of the player to add to this game.
     * @return the player index at which the player was added, or <code>-1</code> if the player
     * could not be added to the game.
     */
    public int addPlayer (Name player)
    {
        // determine the first available player index
        int pidx = -1;
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            if (!_gameobj.isOccupiedPlayer(ii)) {
                pidx = ii;
                break;
            }
        }

        // sanity-check the player index
        if (pidx == -1) {
            log.warning("Couldn't find free player index for player  [game=" + where() +
                        ", player=" + player +
                        ", players=" + StringUtil.toString(_gameobj.players) + "].");
            return -1;
        }

        // proceed with the rest of the adding business
        return (!addPlayerAt(player, pidx)) ? -1 : pidx;
    }

    /**
     * Adds the given player to the game at the specified player index.  This should only be called
     * before the game is started, and is most likely to be used to add players to party games.
     *
     * @param player the username of the player to add to this game.
     * @param pidx the player index at which the player is to be added.
     * @return true if the player was added successfully, false if not.
     */
    public boolean addPlayerAt (Name player, int pidx)
    {
        // make sure the specified player index is valid
        if (pidx < 0 || pidx >= getPlayerSlots()) {
            log.warning("Attempt to add player at an invalid index [game=" + where() +
                        ", player=" + player + ", pidx=" + pidx + "].");
            return false;
        }

        // make sure the player index is available
        if (_gameobj.players[pidx] != null) {
            log.warning("Attempt to add player at occupied index [game=" + where() +
                        ", player=" + player + ", pidx=" + pidx + "].");
            return false;
        }

        // make sure the player isn't already somehow a part of the game to avoid any potential
        // badness that might ensue if we added them more than once
        if (_gameobj.getPlayerIndex(player) != -1) {
            log.warning("Attempt to add player to game that they're already playing " +
                        "[game=" + where() + ", player=" + player + "].");
            return false;
        }

        // get the player's body object
        BodyObject bobj = CrowdServer.lookupBody(player);
        if (bobj == null) {
            log.warning("Unable to get body object while adding player [game=" + where() +
                        ", player=" + player + "].");
            return false;
        }

        // fill in the player's information
        _gameobj.setPlayersAt(player, pidx);

        // increment the number of players in the game
        _playerCount++;

        // save off their oid
        _playerOids[pidx] = bobj.getOid();

        // let derived classes do what they like
        playerWasAdded(player, pidx);

        return true;
    }

    /**
     * Removes the given player from the game.  This is most likely to be used to allow players
     * involved in a party game to leave the game early-on if they realize they'd rather not play
     * for some reason.
     *
     * @param player the username of the player to remove from this game.
     * @return true if the player was successfully removed, false if not.
     */
    public boolean removePlayer (Name player)
    {
        // get the player's index in the player list
        int pidx = _gameobj.getPlayerIndex(player);

        // sanity-check the player index
        if (pidx == -1) {
            log.warning("Attempt to remove non-player from players list [game=" + where() +
                        ", player=" + player +
                        ", players=" + StringUtil.toString(_gameobj.players) + "].");
            return false;
        }

        // remove the player from the players list
        _gameobj.setPlayersAt(null, pidx);

        // clear out the player's entry in the player oid list
        _playerOids[pidx] = 0;

        if (_AIs != null) {
            // clear out the player's entry in the AI list
            _AIs[pidx] = null;
        }

        // decrement the number of players in the game
        _playerCount--;

        // let derived classes do what they like
        playerWasRemoved(player, pidx);

        return true;
    }

    /**
     * Replaces the player at the specified index and calls {@link #playerWasReplaced} to let
     * derived classes and delegates know what's going on.
     */
    public void replacePlayer (final int pidx, final Name player)
    {
        final Name oplayer = _gameobj.players[pidx];
        _gameobj.setPlayersAt(player, pidx);

        // allow derived classes to respond
        playerWasReplaced(pidx, oplayer, player);

        // notify our delegates
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).playerWasReplaced(pidx, oplayer, player);
            }
        });
    }

    /**
     * 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);
    }

    /**
     * Sets the specified player as an AI with the specified configuration. It is assumed that this
     * will be set soon after the player names for all AIs present in the game. (It should be done
     * before human players start trickling into the game.)
     *
     * @param pidx the player index of the AI.
     * @param ai the AI configuration.
     */
    public void setAI (final int pidx, final GameAI ai)
    {
        if (_AIs == null) {
            // create and initialize the AI configuration array
            _AIs = new GameAI[getPlayerSlots()];
            // set up a delegate op for AI ticking
            _tickAIOp = new TickAIDelegateOp();
        }

        // save off the AI's configuration
        _AIs[pidx] = ai;

        // let the delegates know that the player's been made an AI
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).setAI(pidx, ai);
            }
        });
    }

    /**
     * Returns the name of the player with the specified index or null if no player exists at that
     * index.
     */
    public Name getPlayerName (int index)
    {
        return (_gameobj == null) ? null : _gameobj.players[index];
    }

    /**
     * Returns the player index of the given user in the game, or <code>-1</code> if the player is
     * not involved in the game.
     */
    public int getPlayerIndex (Name username)
    {
        return (_gameobj == null) ? -1 : _gameobj.getPlayerIndex(username);
    }

    /**
     * Get the player index of the specified oid, or -1 if the oid is not a player or is a player
     * that is not presently in the game.
     */
    public int getPresentPlayerIndex (int bodyOid)
    {
        return (_playerOids == null) ? -1 : IntListUtil.indexOf(_playerOids, bodyOid);
    }

    /**
     * Returns the user object oid of the player with the specified index.
     */
    public int getPlayerOid (int index)
    {
        return (_playerOids == null) ? -1 : _playerOids[index];
    }

    /**
     * Returns the persistent user id for the supplied body.
     */
    public int getPlayerPersistentId (BodyObject bobj)
    {
        return (bobj == null) ? 0 : _userIder.getUserId(bobj);
    }

    /**
     * Returns the number of players in the game.
     */
    public int getPlayerCount ()
    {
        return _playerCount;
    }

    /**
     * Returns the number of players allowed in this game.
     */
    public int getPlayerSlots ()
    {
        return _gameconfig.players.length;
    }

    /**
     * Returns whether the player at the specified player index is an AI.
     */
    public boolean isAI (int pidx)
    {
        return (_AIs != null && _AIs[pidx] != null);
    }

    /**
     * Returns whether the player at the specified player index is actively playing the game
     */
    public boolean isActivePlayer (int pidx)
    {
        return _gameobj.isActivePlayer(pidx) && (getPlayerOid(pidx) > 0 || isAI(pidx));
    }

    /**
     * Returns the unique round identifier for the current round.
     */
    public int getRoundId ()
    {
        return _gameobj.roundId;
    }

    /**
     * Sends a system message to the players in the game room.
     */
    public void systemMessage (String msgbundle, String msg)
    {
        systemMessage(msgbundle, msg, false);
    }

    /**
     * Sends a system message to the players in the game room.
     *
     * @param waitForStart if true, the message will not be sent until the game has started.
     */
    public void systemMessage (String msgbundle, String msg, boolean waitForStart)
    {
        if (waitForStart && ((_gameobj == null) || (_gameobj.state == GameObject.PRE_GAME))) {
            // queue up the message.
            if (_startmsgs == null) {
                _startmsgs = new ArrayList<Tuple<String,String>>();
            }
            _startmsgs.add(new Tuple<String,String>(msgbundle, msg));
            return;
        }

        // otherwise, just deliver the message
        SpeakUtil.sendInfo(_gameobj, msgbundle, msg);
    }

    /**
     * This is called when the game is ready to start (all players involved have delivered their
     * "am ready" notifications). It calls {@link #gameWillStart}, sets the necessary wheels in
     * motion and then calls {@link #gameDidStart}.  Derived classes should override one or both of
     * the calldown functions (rather than this function) if they need to do things before or after
     * the game starts.
     *
     * @return true if the game was started, false if it could not be started because it was
     * already in play or because all players have not yet reported in.
     */
    public boolean startGame ()
    {
        // complain if we're already started
        if (_gameobj.isInPlay()) {
            log.warning("Requested to start an already in-play game [game=" + where() + "].");
            Thread.dumpStack();
            return false;
        }

        // TEMP: clear out our game end tracker
        _gameEndTracker.clear();

        // make sure everyone has turned up
        if (!allPlayersReady()) {
            log.warning("Requested to start a game that is still awaiting players " +
                        "[game=" + where() + ", pnames=" + StringUtil.toString(_gameobj.players) +
                        ", poids=" + StringUtil.toString(_playerOids) + "].");
            return false;
        }

        // if we're still waiting for a call to endGame() to propagate, queue up a runnable to
        // start the game which will allow the endGame() to propagate before we start things up
        if (_committedState == GameObject.IN_PLAY) {
            if (_postponedStart) {
                // We've already tried postponing once, doesn't do us any good to throw ourselves
                // into a frenzy trying again.
                log.warning("Tried to postpone the start of a still-ending game multiple times " +
                            "[game=" + where() + "].");
                _postponedStart = false;
                return false;
            }
            log.info("Postponing start of still-ending game [game=" + where() + "].");
            _postponedStart = true;
            // TEMP: track down weirdness
            final Exception firstCall = new Exception();
            // End: temp
            CrowdServer.omgr.postRunnable(new Runnable() {
                public void run () {
                    boolean result = startGame();
                    // TEMP: track down weirdness
                    if (!result && !_postponedStart) {
                        log.log(Level.WARNING, "First call to startGame: ", firstCall);
                    }
                    // End: temp
                }
            });
            return true;
        }

        // Ah, good, not postponing.
        _postponedStart = false;

        // let the derived class do its pre-start stuff
        gameWillStart();

        // transition the game to started
        _gameobj.setState(GameObject.IN_PLAY);

        // when our events are applied, we'll call gameDidStart()
        return true;
    }

    /**
     * Ends the game for the given player.
     */
    public void endPlayerGame (int pidx)
    {
        // go for a little transactional efficiency
        _gameobj.startTransaction();
        try {
            // end the player's game
            if (_gameobj.playerStatus != null) {
                _gameobj.setPlayerStatusAt(GameObject.PLAYER_LEFT_GAME, pidx);
            }

            // let derived classes do some business
            playerGameDidEnd(pidx);

        } finally {
            _gameobj.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);
        }
    }

    /**
     * Called when the game is known to be over. This will call some calldown functions to
     * determine the winner of the game and then transition the game to the {@link
     * GameObject#GAME_OVER} state.
     */
    public void endGame ()
    {
        // TEMP: debug pending rating repeat bug
        if (_gameEndTracker.checkCall(
                "Requested to end already ended game [game=" + where() + "].")) {
            return;
        }
        // END TEMP

        if (!_gameobj.isInPlay()) {
            log.info("Refusing to end game that was not in play [game=" + where() + "].");
            return;
        }

        _gameobj.startTransaction();
        try {
            // let the derived class do its pre-end stuff
            gameWillEnd();

            // determine winners and set them in the game object
            boolean[] winners = new boolean[getPlayerSlots()];
            assignWinners(winners);
            _gameobj.setWinners(winners);

            // transition to the game over state
            _gameobj.setState(GameObject.GAME_OVER);

        } finally {
            _gameobj.commitTransaction();
        }

        // wait until we hear the game state transition on the game object to invoke our game over
        // code so that we can be sure that any final events dispatched on the game object prior to
        // the call to endGame() have been dispatched
    }

    /**
     * Sets the state of the game to {@link GameObject#CANCELLED}.
     *
     * @return true if the game was cancelled, false if it was already over or cancelled.
     */
    public boolean cancelGame ()
    {
        if (_gameobj.state != GameObject.GAME_OVER && _gameobj.state != GameObject.CANCELLED) {
            _gameobj.setState(GameObject.CANCELLED);
            return true;
        }
        return false;
    }

    /**
     * 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 (_gameobj.state == GameObject.GAME_OVER);
    }

    /**
     * Called when the game is to be reset to its starting state in preparation for a new game
     * without actually ending the current game. It calls {@link #gameWillReset} followed by the
     * standard game start processing ({@link #gameWillStart} and {@link #gameDidStart}). Derived
     * classes should override these calldown functions (rather than this function) if they need to
     * do things before or after the game resets.
     */
    public void resetGame ()
    {
        // let the derived class do its pre-reset stuff
        gameWillReset();
        // do the standard game start processing
        gameWillStart();
        // transition to in-play which will trigger a call to gameDidStart()
        _gameobj.setState(GameObject.IN_PLAY);
    }

    /**
     * Called by the client when the player has arrived in the game room and has loaded their
     * bits. Most games will simply call {@link #playerReady} but games that wish to delay their
     * actual start until players take some action must report ASAP with a call to {@link
     * #playerInRoom} to let the server know that they have arrived and will later be calling
     * {@link #playerReady} when they are ready for the game to actually start.
     */
    public void playerInRoom (BodyObject caller)
    {
        int pidx = _gameobj.getPlayerIndex(caller.getVisibleName());
        if (pidx == -1) {
            return;
        }

        // make a note of this player's oid
        _playerOids[pidx] = caller.getOid();

        // this player is not necessarily ready to play yet
        _pendingOids.add(caller.getOid());
    }

    /**
     * Called by the client when the player is ready for the game to start.  This method is
     * dispatched dynamically by {@link #messageReceived}.
     */
    public void playerReady (BodyObject caller)
    {
        playerInRoom(caller);

        // This player is no longer pending
        _pendingOids.remove(caller.getOid());

        // if everyone is now ready to go, get things underway
        if (allPlayersReady()) {
            playersAllHere();
        }
    }

    /**
     * Returns true if all (non-AI) players have delivered their {@link #playerReady}
     * notifications, false if they have not.
     */
    public boolean allPlayersReady ()
    {
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            if (!playerIsReady(ii)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the player at the specified slot is ready (or if there is meant to be no
     * player in that slot), false if there is meant to be a player in the specified slot and they
     * have not yet reported that they are ready.
     */
    public boolean playerIsReady (int pidx)
    {
        return (!_gameobj.isOccupiedPlayer(pidx) ||            // unoccupied slot
                (_playerOids[pidx] != 0 &&                     // player is in the room and...
                 !_pendingOids.contains(_playerOids[pidx])) || // ...has reported ready
                isAI(pidx));                                   // player is AI
    }

    /**
     * Returns true if this game requires a no-show timer. The default implementation returns true
     * for non-party games and false for party games. Derived classes may wish to change or augment
     * this behavior.
     */
    protected boolean needsNoShowTimer ()
    {
        return (getMatchType() == GameConfig.SEATED_GAME);
    }

    /**
     * Derived classes that need their AIs to be ticked periodically should override this method
     * and return true. Many AIs can act entirely in reaction to game state changes and need no
     * periodic ticking which is why ticking is disabled by default.
     *
     * @see #tickAIs
     */
    protected boolean needsAITick ()
    {
        return false;
    }

    /**
     * Called when a player was added to the game.  Derived classes may override this method to
     * perform any game-specific actions they desire, but should be sure to call
     * <code>super.playerWasAdded()</code>.
     *
     * @param player the username of the player added to the game.
     * @param pidx the player index of the player added to the game.
     */
    protected void playerWasAdded (Name player, int pidx)
    {
    }

    /**
     * Called when a player was removed from the game.  Derived classes may override this method to
     * perform any game-specific actions they desire, but should be sure to call
     * <code>super.playerWasRemoved()</code>.
     *
     * @param player the username of the player removed from the game.
     * @param pidx the player index of the player before they were removed from the game.
     */
    protected void playerWasRemoved (Name player, int pidx)
    {
    }

    /**
     * Called when a player has been replaced via a call to {@link #replacePlayer}.
     */
    protected void playerWasReplaced (int pidx, Name oldPlayer, Name newPlayer)
    {
    }

    /**
     * Report to the knocked-out player's room that they were knocked out.
     */
    protected void reportPlayerKnockedOut (int pidx)
    {
        BodyObject user = getPlayer(pidx);
        if (user == null) {
            return; // body object can be null for ai players
        }

        DObject place = CrowdServer.omgr.getObject(user.getPlaceOid());
        if (place != null) {
            place.postMessage(PLAYER_KNOCKED_OUT, new Object[] { new int[] { user.getOid() } });
        }
    }

    @Override // from PlaceManager
    protected void didInit ()
    {
        super.didInit();

        // save off a casted reference to our config
        _gameconfig = (GameConfig)_config;

        // register this game manager
        _managers.add(this);

        // and start up a tick interval if we've not already got one
        if (_tickInterval == null) {
            _tickInterval = new Interval(CrowdServer.omgr) {
                public void expired () {
                    tickAllGames();
                }
            };
            _tickInterval.schedule(TICK_DELAY, true);
        }

        // configure our AIs
        for (int ii = 0; ii < _gameconfig.ais.length; ii++) {
            if (_gameconfig.ais[ii] != null) {
                setAI(ii, _gameconfig.ais[ii]);
            }
        }
    }

    @Override // from PlaceManager
    protected void didStartup ()
    {
        // obtain a casted reference to our game object
        _gameobj = (GameObject)_plobj;
        _gameobj.addListener(_stateListener);

        // stick the players into the game object
        _gameobj.setPlayers(_gameconfig.players);

        // set up an initial player status array
        _gameobj.setPlayerStatus(new int[getPlayerSlots()]);

        // save off the number of players so that we needn't repeatedly iterate through the player
        // name array server-side unnecessarily
        _playerCount = _gameobj.getPlayerCount();

        // instantiate a player oid array which we'll fill in later
        _playerOids = new int[getPlayerSlots()];

        // give delegates a chance to do their thing
        super.didStartup();

        // let the players of this game know that we're ready to roll (if we have a specific set of
        // players)
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            // skip non-existent players and AIs
            if (!_gameobj.isOccupiedPlayer(ii) || isAI(ii)) {
                continue;
            }

            BodyObject bobj = CrowdServer.lookupBody(_gameobj.players[ii]);
            if (bobj == null) {
                log.warning("Unable to deliver game ready to non-existent player [game=" + where() +
                            ", player=" + _gameobj.players[ii] + "].");
                continue;
            }

            // deliver a game ready notification to the player
            ParlorSender.gameIsReady(bobj, _gameobj.getOid());
        }

        // start up a no-show timer if needed
        if (needsNoShowTimer()) {
            _noShowInterval = new Interval(CrowdServer.omgr) {
                public void expired () {
                    checkForNoShows();
                }
            };
            _noShowInterval.schedule(NOSHOW_DELAY);
        }
    }

    @Override // from PlaceManager
    protected void didShutdown ()
    {
        super.didShutdown();

        // unregister this game manager
        _managers.remove(this);

        // remove the tick interval if there are no remaining managers
        if (_managers.size() == 0) {
            _tickInterval.cancel();
            _tickInterval = null;
        }

        if (_gameobj != null) {
            // remove our state listener
            _gameobj.removeListener(_stateListener);
        }
    }

    @Override // from PlaceManager
    protected void bodyLeft (int bodyOid)
    {
        // first resign the player from the game
        int pidx = IntListUtil.indexOf(_playerOids, bodyOid);
        if (pidx != -1 && _gameobj.isInPlay() &&
            _gameobj.isActivePlayer(pidx)) {
            // end the player's game if they bail on an in-progress game
            endPlayerGame(pidx);
        }

        // then complete the bodyLeft() processing which may result in a call to placeBecameEmpty()
        // which will shut the game down
        super.bodyLeft(bodyOid);
    }

    /**
     * When a game room becomes empty, we cancel the game if it's still in progress and close down
     * the game room.
     */
    protected void placeBecameEmpty ()
    {
//         log.info("Game room empty. Going away. [game=" + where() + "].");

        // if we're in play then move to game over
        if (_gameobj.state != GameObject.PRE_GAME && _gameobj.state != GameObject.GAME_OVER &&
            _gameobj.state != GameObject.CANCELLED) {
            _gameobj.setState(GameObject.GAME_OVER);
            // and shutdown directly
            shutdown();

        // cancel the game; which will shut us down
        } else if (!cancelGame()) {
            // or shut down directly if the game is already over
            shutdown();
        }
    }

    /**
     * Called when all players have arrived in the game room. By default, this starts up the game,
     * but a manager may wish to override this and start the game according to different criterion.
     */
    protected void playersAllHere ()
    {
        // if we're a seated game and we haven't already started, start.
        if ((getMatchType() == GameConfig.SEATED_GAME) && _gameobj.state == GameObject.PRE_GAME) {
            startGame();
        }
    }

    /**
     * Called after the no-show delay has expired following the delivery of notifications to all
     * players that the game is ready.  <em>Note:</em> this is not called for party games. Those
     * games have a human who decides when to start the game.
     */
    protected void checkForNoShows ()
    {
        // nothing to worry about if we're already started
        if (_gameobj.state != GameObject.PRE_GAME) {
            return;
        }

        // if there's no one in the room, go ahead and clear it out
        if (_plobj.occupants.size() == 0) {
            log.info("Cancelling total no-show [game=" + where() +
                     ", players=" + StringUtil.toString(_gameobj.players) +
                     ", poids=" + StringUtil.toString(_playerOids) + "].");
            placeBecameEmpty();

        } else {
            // do the right thing if we have any no-show players
            for (int ii = 0; ii < getPlayerSlots(); ii++) {
                if (!playerIsReady(ii)) {
                    handlePartialNoShow();
                    return;
                }
            }
        }
    }

    /**
     * This is called when some, but not all, players failed to show up for a game. The default
     * implementation simply cancels the game.
     */
    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()
        int humansHere = 0;
        for (int ii = 0; ii < _playerOids.length; ii++) {
            if (_playerOids[ii] == 0) {
                _playerOids[ii] = -1;
            } else if (!isAI(ii)) {
                humansHere++;
            }
        }

        if ((humansHere == 0) && !startWithoutHumans()) {
            // if there are no human players in the game, just cancel it
            log.info("Canceling no-show game [game=" + where() +
                     ", players=" + StringUtil.toString(_playerOids) + "].");
            cancelGame();

        } else {
            // go ahead and report that everyone is ready (which will 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=" + where() +
                     ", poids=" + StringUtil.toString(_playerOids) + "].");
            playersAllHere();
        }
    }

    /**
     * @return true if we should start the game even without any humans.  Default implementation
     * always returns false.
     */
    protected boolean startWithoutHumans ()
    {
        return false;
    }

    /**
     * Called when the game is about to start, but before the game start notification has been
     * delivered to the players. Derived classes should override this if they need to perform some
     * pre-start activities, but should be sure to call <code>super.gameWillStart()</code>.
     */
    protected void gameWillStart ()
    {
        // increment the round identifier
        _gameobj.setRoundId(_gameobj.roundId + 1);

        // let our delegates do their business
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).gameWillStart();
            }
        });
    }

    /**
     * Called when the game state changes. This happens after the attribute change event has
     * propagated.
     *
     * @param state the new game state.
     * @param oldState the previous game state.
     */
    protected void stateDidChange (int state, int oldState)
    {
        switch (state) {
        case GameObject.IN_PLAY:
            gameDidStart();
            break;

        case GameObject.GAME_OVER:
            // we do some jiggery pokery to allow derived game objects to have different notions of
            // what it means to be in play
            _gameobj.state = oldState;
            boolean wasInPlay = _gameobj.isInPlay();
            _gameobj.state = state;

            // now call gameDidEnd() only if the game was previously in play
            if (wasInPlay) {
                gameDidEnd();
            }
            break;

        case GameObject.CANCELLED:
            // let the manager do anything it cares to
            gameWasCancelled();

            // and shutdown if there's no one here
            if (_plobj.occupants.size() == 0) {
                shutdown();
            }
            break;
        }
    }

    /**
     * Called after the game start notification was dispatched.  Derived classes can override this
     * to put whatever wheels they might need into motion now that the game is started (if anything
     * other than transitioning the game to {@link GameObject#IN_PLAY} is necessary), but should be
     * sure to call <code>super.gameDidStart()</code>.
     */
    protected void gameDidStart ()
    {
        // clear out our no-show timer if it's still running
        if (_noShowInterval != null) {
            _noShowInterval.cancel();
        }

        // let our delegates do their business
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).gameDidStart();
            }
        });

        // inform the players of any pending messages.
        if (_startmsgs != null) {
            for (Tuple<String,String> mtup : _startmsgs) {
                systemMessage(mtup.left, /* bundle */ mtup.right /* message */);
            }
            _startmsgs = null;
        }

        // and register ourselves to receive AI ticks
        if (_AIs != null && needsAITick()) {
            AIGameTicker.registerAIGame(this);
        }

        // 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=" + where() +
                         ", player=" + getPlayerName(ii) + "].");
                _playerOids[ii] = 0; // unfiddle the blank oid
                endPlayerGame(ii);
            }
        }
    }

    /**
     * Called by the {@link AIGameTicker} if we're registered as an AI game.
     */
    protected void tickAIs ()
    {
        for (int ii = 0; ii < _AIs.length; ii++) {
            if (_AIs[ii] != null) {
                tickAI(ii, _AIs[ii]);
            }
        }
    }

    /**
     * Called by tickAIs to tick each AI in the game.
     */
    protected void tickAI (int pidx, GameAI ai)
    {
        _tickAIOp.setAI(pidx, ai);
        applyToDelegates(_tickAIOp);
    }

    /**
     * Announce to everyone in the game that a player's game has ended.
     */
    protected void announcePlayerGameOver (int pidx)
    {
        systemMessage(GAME_MESSAGE_BUNDLE, 
            MessageBundle.tcompose(getPlayerGameOverMessage(pidx), getPlayerName(pidx)));
    }

    /**
     * Gets the untranslated string to show when a player's game has ended.
     */
    protected String getPlayerGameOverMessage (int pidx)
    {
        return "m.player_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<commit_msg>This is throwing a wrench in bang, reverting it now and will take a better look at it later<commit_after>//
// $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.parlor.game.server;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.logging.Level;

import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.Interval;
import com.samskivert.util.RepeatCallTracker;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Tuple;
import com.threerings.util.Name;

import com.threerings.presents.data.ClientObject;
import com.threerings.presents.dobj.AttributeChangeListener;
import com.threerings.presents.dobj.AttributeChangedEvent;
import com.threerings.presents.dobj.DObject;
import com.threerings.presents.dobj.MessageEvent;

import com.threerings.crowd.chat.server.SpeakUtil;

import com.threerings.crowd.data.BodyObject;
import com.threerings.crowd.data.PlaceObject;
import com.threerings.crowd.server.CrowdServer;
import com.threerings.crowd.server.PlaceManager;
import com.threerings.crowd.server.PlaceManagerDelegate;

import com.threerings.parlor.data.ParlorCodes;
import com.threerings.parlor.game.data.GameAI;
import com.threerings.parlor.game.data.GameCodes;
import com.threerings.parlor.game.data.GameConfig;
import com.threerings.parlor.game.data.GameObject;
import com.threerings.parlor.server.ParlorSender;

import com.threerings.util.MessageBundle;

import static com.threerings.parlor.Log.log;

/**
 * The game manager handles the server side management of a game. It manipulates the game state in
 * accordance with the logic of the game flow and generally manages the whole game playing process.
 *
 * <p> The game manager extends the place manager because games are implicitly played in a
 * location, the players of the game implicitly bodies in that location.
 */
public class GameManager extends PlaceManager
    implements ParlorCodes, GameCodes
{
    /**
     * An interface for identifying users. A larger system using the Parlor game services can
     * enable user identification by passing a UserIdentifier to {@link #setUserIdentifier}.
     */
    public interface UserIdentifier
    {
        /** Returns the persistent user id for the specified player, or 0 if they're not a valid
         * user, or a guest, or something like that (for whom we'll track no persistent data). */
        public int getUserId (BodyObject bodyObj);
    }

    /**
     * Configures a means by which the Parlor game services can map users to a persistent user id
     * for things like per-game cookies and ratings.
     */
    public static void setUserIdentifier (UserIdentifier userIder)
    {
        _userIder = userIder;
    }

    /**
     * Returns the configuration object for the game being managed by this manager.
     */
    public GameConfig getGameConfig ()
    {
        return _gameconfig;
    }

    /**
     * A convenience method for getting the game type.
     */
    public int getMatchType ()
    {
        return _gameconfig.getMatchType();
    }

    /**
     * Adds the given player to the game at the first available player index.  This should only be
     * called before the game is started, and is most likely to be used to add players to party
     * games.
     *
     * @param player the username of the player to add to this game.
     * @return the player index at which the player was added, or <code>-1</code> if the player
     * could not be added to the game.
     */
    public int addPlayer (Name player)
    {
        // determine the first available player index
        int pidx = -1;
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            if (!_gameobj.isOccupiedPlayer(ii)) {
                pidx = ii;
                break;
            }
        }

        // sanity-check the player index
        if (pidx == -1) {
            log.warning("Couldn't find free player index for player  [game=" + where() +
                        ", player=" + player +
                        ", players=" + StringUtil.toString(_gameobj.players) + "].");
            return -1;
        }

        // proceed with the rest of the adding business
        return (!addPlayerAt(player, pidx)) ? -1 : pidx;
    }

    /**
     * Adds the given player to the game at the specified player index.  This should only be called
     * before the game is started, and is most likely to be used to add players to party games.
     *
     * @param player the username of the player to add to this game.
     * @param pidx the player index at which the player is to be added.
     * @return true if the player was added successfully, false if not.
     */
    public boolean addPlayerAt (Name player, int pidx)
    {
        // make sure the specified player index is valid
        if (pidx < 0 || pidx >= getPlayerSlots()) {
            log.warning("Attempt to add player at an invalid index [game=" + where() +
                        ", player=" + player + ", pidx=" + pidx + "].");
            return false;
        }

        // make sure the player index is available
        if (_gameobj.players[pidx] != null) {
            log.warning("Attempt to add player at occupied index [game=" + where() +
                        ", player=" + player + ", pidx=" + pidx + "].");
            return false;
        }

        // make sure the player isn't already somehow a part of the game to avoid any potential
        // badness that might ensue if we added them more than once
        if (_gameobj.getPlayerIndex(player) != -1) {
            log.warning("Attempt to add player to game that they're already playing " +
                        "[game=" + where() + ", player=" + player + "].");
            return false;
        }

        // get the player's body object
        BodyObject bobj = CrowdServer.lookupBody(player);
        if (bobj == null) {
            log.warning("Unable to get body object while adding player [game=" + where() +
                        ", player=" + player + "].");
            return false;
        }

        // fill in the player's information
        _gameobj.setPlayersAt(player, pidx);

        // increment the number of players in the game
        _playerCount++;

        // save off their oid
        _playerOids[pidx] = bobj.getOid();

        // let derived classes do what they like
        playerWasAdded(player, pidx);

        return true;
    }

    /**
     * Removes the given player from the game.  This is most likely to be used to allow players
     * involved in a party game to leave the game early-on if they realize they'd rather not play
     * for some reason.
     *
     * @param player the username of the player to remove from this game.
     * @return true if the player was successfully removed, false if not.
     */
    public boolean removePlayer (Name player)
    {
        // get the player's index in the player list
        int pidx = _gameobj.getPlayerIndex(player);

        // sanity-check the player index
        if (pidx == -1) {
            log.warning("Attempt to remove non-player from players list [game=" + where() +
                        ", player=" + player +
                        ", players=" + StringUtil.toString(_gameobj.players) + "].");
            return false;
        }

        // remove the player from the players list
        _gameobj.setPlayersAt(null, pidx);

        // clear out the player's entry in the player oid list
        _playerOids[pidx] = 0;

        if (_AIs != null) {
            // clear out the player's entry in the AI list
            _AIs[pidx] = null;
        }

        // decrement the number of players in the game
        _playerCount--;

        // let derived classes do what they like
        playerWasRemoved(player, pidx);

        return true;
    }

    /**
     * Replaces the player at the specified index and calls {@link #playerWasReplaced} to let
     * derived classes and delegates know what's going on.
     */
    public void replacePlayer (final int pidx, final Name player)
    {
        final Name oplayer = _gameobj.players[pidx];
        _gameobj.setPlayersAt(player, pidx);

        // allow derived classes to respond
        playerWasReplaced(pidx, oplayer, player);

        // notify our delegates
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).playerWasReplaced(pidx, oplayer, player);
            }
        });
    }

    /**
     * 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);
    }

    /**
     * Sets the specified player as an AI with the specified configuration. It is assumed that this
     * will be set soon after the player names for all AIs present in the game. (It should be done
     * before human players start trickling into the game.)
     *
     * @param pidx the player index of the AI.
     * @param ai the AI configuration.
     */
    public void setAI (final int pidx, final GameAI ai)
    {
        if (_AIs == null) {
            // create and initialize the AI configuration array
            _AIs = new GameAI[getPlayerSlots()];
            // set up a delegate op for AI ticking
            _tickAIOp = new TickAIDelegateOp();
        }

        // save off the AI's configuration
        _AIs[pidx] = ai;

        // let the delegates know that the player's been made an AI
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).setAI(pidx, ai);
            }
        });
    }

    /**
     * Returns the name of the player with the specified index or null if no player exists at that
     * index.
     */
    public Name getPlayerName (int index)
    {
        return (_gameobj == null) ? null : _gameobj.players[index];
    }

    /**
     * Returns the player index of the given user in the game, or <code>-1</code> if the player is
     * not involved in the game.
     */
    public int getPlayerIndex (Name username)
    {
        return (_gameobj == null) ? -1 : _gameobj.getPlayerIndex(username);
    }

    /**
     * Get the player index of the specified oid, or -1 if the oid is not a player or is a player
     * that is not presently in the game.
     */
    public int getPresentPlayerIndex (int bodyOid)
    {
        return (_playerOids == null) ? -1 : IntListUtil.indexOf(_playerOids, bodyOid);
    }

    /**
     * Returns the user object oid of the player with the specified index.
     */
    public int getPlayerOid (int index)
    {
        return (_playerOids == null) ? -1 : _playerOids[index];
    }

    /**
     * Returns the persistent user id for the supplied body.
     */
    public int getPlayerPersistentId (BodyObject bobj)
    {
        return (bobj == null) ? 0 : _userIder.getUserId(bobj);
    }

    /**
     * Returns the number of players in the game.
     */
    public int getPlayerCount ()
    {
        return _playerCount;
    }

    /**
     * Returns the number of players allowed in this game.
     */
    public int getPlayerSlots ()
    {
        return _gameconfig.players.length;
    }

    /**
     * Returns whether the player at the specified player index is an AI.
     */
    public boolean isAI (int pidx)
    {
        return (_AIs != null && _AIs[pidx] != null);
    }

    /**
     * Returns whether the player at the specified player index is actively playing the game
     */
    public boolean isActivePlayer (int pidx)
    {
        return _gameobj.isActivePlayer(pidx) && (getPlayerOid(pidx) > 0 || isAI(pidx));
    }

    /**
     * Returns the unique round identifier for the current round.
     */
    public int getRoundId ()
    {
        return _gameobj.roundId;
    }

    /**
     * Sends a system message to the players in the game room.
     */
    public void systemMessage (String msgbundle, String msg)
    {
        systemMessage(msgbundle, msg, false);
    }

    /**
     * Sends a system message to the players in the game room.
     *
     * @param waitForStart if true, the message will not be sent until the game has started.
     */
    public void systemMessage (String msgbundle, String msg, boolean waitForStart)
    {
        if (waitForStart && ((_gameobj == null) || (_gameobj.state == GameObject.PRE_GAME))) {
            // queue up the message.
            if (_startmsgs == null) {
                _startmsgs = new ArrayList<Tuple<String,String>>();
            }
            _startmsgs.add(new Tuple<String,String>(msgbundle, msg));
            return;
        }

        // otherwise, just deliver the message
        SpeakUtil.sendInfo(_gameobj, msgbundle, msg);
    }

    /**
     * This is called when the game is ready to start (all players involved have delivered their
     * "am ready" notifications). It calls {@link #gameWillStart}, sets the necessary wheels in
     * motion and then calls {@link #gameDidStart}.  Derived classes should override one or both of
     * the calldown functions (rather than this function) if they need to do things before or after
     * the game starts.
     *
     * @return true if the game was started, false if it could not be started because it was
     * already in play or because all players have not yet reported in.
     */
    public boolean startGame ()
    {
        // complain if we're already started
        if (_gameobj.state == GameObject.IN_PLAY) {
            log.warning("Requested to start an already in-play game [game=" + where() + "].");
            Thread.dumpStack();
            return false;
        }

        // TEMP: clear out our game end tracker
        _gameEndTracker.clear();

        // make sure everyone has turned up
        if (!allPlayersReady()) {
            log.warning("Requested to start a game that is still awaiting players " +
                        "[game=" + where() + ", pnames=" + StringUtil.toString(_gameobj.players) +
                        ", poids=" + StringUtil.toString(_playerOids) + "].");
            return false;
        }

        // if we're still waiting for a call to endGame() to propagate, queue up a runnable to
        // start the game which will allow the endGame() to propagate before we start things up
        if (_committedState == GameObject.IN_PLAY) {
            if (_postponedStart) {
                // We've already tried postponing once, doesn't do us any good to throw ourselves
                // into a frenzy trying again.
                log.warning("Tried to postpone the start of a still-ending game multiple times " +
                            "[game=" + where() + "].");
                _postponedStart = false;
                return false;
            }
            log.info("Postponing start of still-ending game [game=" + where() + "].");
            _postponedStart = true;
            // TEMP: track down weirdness
            final Exception firstCall = new Exception();
            // End: temp
            CrowdServer.omgr.postRunnable(new Runnable() {
                public void run () {
                    boolean result = startGame();
                    // TEMP: track down weirdness
                    if (!result && !_postponedStart) {
                        log.log(Level.WARNING, "First call to startGame: ", firstCall);
                    }
                    // End: temp
                }
            });
            return true;
        }

        // Ah, good, not postponing.
        _postponedStart = false;

        // let the derived class do its pre-start stuff
        gameWillStart();

        // transition the game to started
        _gameobj.setState(GameObject.IN_PLAY);

        // when our events are applied, we'll call gameDidStart()
        return true;
    }

    /**
     * Ends the game for the given player.
     */
    public void endPlayerGame (int pidx)
    {
        // go for a little transactional efficiency
        _gameobj.startTransaction();
        try {
            // end the player's game
            if (_gameobj.playerStatus != null) {
                _gameobj.setPlayerStatusAt(GameObject.PLAYER_LEFT_GAME, pidx);
            }

            // let derived classes do some business
            playerGameDidEnd(pidx);

        } finally {
            _gameobj.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);
        }
    }

    /**
     * Called when the game is known to be over. This will call some calldown functions to
     * determine the winner of the game and then transition the game to the {@link
     * GameObject#GAME_OVER} state.
     */
    public void endGame ()
    {
        // TEMP: debug pending rating repeat bug
        if (_gameEndTracker.checkCall(
                "Requested to end already ended game [game=" + where() + "].")) {
            return;
        }
        // END TEMP

        if (!_gameobj.isInPlay()) {
            log.info("Refusing to end game that was not in play [game=" + where() + "].");
            return;
        }

        _gameobj.startTransaction();
        try {
            // let the derived class do its pre-end stuff
            gameWillEnd();

            // determine winners and set them in the game object
            boolean[] winners = new boolean[getPlayerSlots()];
            assignWinners(winners);
            _gameobj.setWinners(winners);

            // transition to the game over state
            _gameobj.setState(GameObject.GAME_OVER);

        } finally {
            _gameobj.commitTransaction();
        }

        // wait until we hear the game state transition on the game object to invoke our game over
        // code so that we can be sure that any final events dispatched on the game object prior to
        // the call to endGame() have been dispatched
    }

    /**
     * Sets the state of the game to {@link GameObject#CANCELLED}.
     *
     * @return true if the game was cancelled, false if it was already over or cancelled.
     */
    public boolean cancelGame ()
    {
        if (_gameobj.state != GameObject.GAME_OVER && _gameobj.state != GameObject.CANCELLED) {
            _gameobj.setState(GameObject.CANCELLED);
            return true;
        }
        return false;
    }

    /**
     * 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 (_gameobj.state == GameObject.GAME_OVER);
    }

    /**
     * Called when the game is to be reset to its starting state in preparation for a new game
     * without actually ending the current game. It calls {@link #gameWillReset} followed by the
     * standard game start processing ({@link #gameWillStart} and {@link #gameDidStart}). Derived
     * classes should override these calldown functions (rather than this function) if they need to
     * do things before or after the game resets.
     */
    public void resetGame ()
    {
        // let the derived class do its pre-reset stuff
        gameWillReset();
        // do the standard game start processing
        gameWillStart();
        // transition to in-play which will trigger a call to gameDidStart()
        _gameobj.setState(GameObject.IN_PLAY);
    }

    /**
     * Called by the client when the player has arrived in the game room and has loaded their
     * bits. Most games will simply call {@link #playerReady} but games that wish to delay their
     * actual start until players take some action must report ASAP with a call to {@link
     * #playerInRoom} to let the server know that they have arrived and will later be calling
     * {@link #playerReady} when they are ready for the game to actually start.
     */
    public void playerInRoom (BodyObject caller)
    {
        int pidx = _gameobj.getPlayerIndex(caller.getVisibleName());
        if (pidx == -1) {
            return;
        }

        // make a note of this player's oid
        _playerOids[pidx] = caller.getOid();

        // this player is not necessarily ready to play yet
        _pendingOids.add(caller.getOid());
    }

    /**
     * Called by the client when the player is ready for the game to start.  This method is
     * dispatched dynamically by {@link #messageReceived}.
     */
    public void playerReady (BodyObject caller)
    {
        playerInRoom(caller);

        // This player is no longer pending
        _pendingOids.remove(caller.getOid());

        // if everyone is now ready to go, get things underway
        if (allPlayersReady()) {
            playersAllHere();
        }
    }

    /**
     * Returns true if all (non-AI) players have delivered their {@link #playerReady}
     * notifications, false if they have not.
     */
    public boolean allPlayersReady ()
    {
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            if (!playerIsReady(ii)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the player at the specified slot is ready (or if there is meant to be no
     * player in that slot), false if there is meant to be a player in the specified slot and they
     * have not yet reported that they are ready.
     */
    public boolean playerIsReady (int pidx)
    {
        return (!_gameobj.isOccupiedPlayer(pidx) ||            // unoccupied slot
                (_playerOids[pidx] != 0 &&                     // player is in the room and...
                 !_pendingOids.contains(_playerOids[pidx])) || // ...has reported ready
                isAI(pidx));                                   // player is AI
    }

    /**
     * Returns true if this game requires a no-show timer. The default implementation returns true
     * for non-party games and false for party games. Derived classes may wish to change or augment
     * this behavior.
     */
    protected boolean needsNoShowTimer ()
    {
        return (getMatchType() == GameConfig.SEATED_GAME);
    }

    /**
     * Derived classes that need their AIs to be ticked periodically should override this method
     * and return true. Many AIs can act entirely in reaction to game state changes and need no
     * periodic ticking which is why ticking is disabled by default.
     *
     * @see #tickAIs
     */
    protected boolean needsAITick ()
    {
        return false;
    }

    /**
     * Called when a player was added to the game.  Derived classes may override this method to
     * perform any game-specific actions they desire, but should be sure to call
     * <code>super.playerWasAdded()</code>.
     *
     * @param player the username of the player added to the game.
     * @param pidx the player index of the player added to the game.
     */
    protected void playerWasAdded (Name player, int pidx)
    {
    }

    /**
     * Called when a player was removed from the game.  Derived classes may override this method to
     * perform any game-specific actions they desire, but should be sure to call
     * <code>super.playerWasRemoved()</code>.
     *
     * @param player the username of the player removed from the game.
     * @param pidx the player index of the player before they were removed from the game.
     */
    protected void playerWasRemoved (Name player, int pidx)
    {
    }

    /**
     * Called when a player has been replaced via a call to {@link #replacePlayer}.
     */
    protected void playerWasReplaced (int pidx, Name oldPlayer, Name newPlayer)
    {
    }

    /**
     * Report to the knocked-out player's room that they were knocked out.
     */
    protected void reportPlayerKnockedOut (int pidx)
    {
        BodyObject user = getPlayer(pidx);
        if (user == null) {
            return; // body object can be null for ai players
        }

        DObject place = CrowdServer.omgr.getObject(user.getPlaceOid());
        if (place != null) {
            place.postMessage(PLAYER_KNOCKED_OUT, new Object[] { new int[] { user.getOid() } });
        }
    }

    @Override // from PlaceManager
    protected void didInit ()
    {
        super.didInit();

        // save off a casted reference to our config
        _gameconfig = (GameConfig)_config;

        // register this game manager
        _managers.add(this);

        // and start up a tick interval if we've not already got one
        if (_tickInterval == null) {
            _tickInterval = new Interval(CrowdServer.omgr) {
                public void expired () {
                    tickAllGames();
                }
            };
            _tickInterval.schedule(TICK_DELAY, true);
        }

        // configure our AIs
        for (int ii = 0; ii < _gameconfig.ais.length; ii++) {
            if (_gameconfig.ais[ii] != null) {
                setAI(ii, _gameconfig.ais[ii]);
            }
        }
    }

    @Override // from PlaceManager
    protected void didStartup ()
    {
        // obtain a casted reference to our game object
        _gameobj = (GameObject)_plobj;
        _gameobj.addListener(_stateListener);

        // stick the players into the game object
        _gameobj.setPlayers(_gameconfig.players);

        // set up an initial player status array
        _gameobj.setPlayerStatus(new int[getPlayerSlots()]);

        // save off the number of players so that we needn't repeatedly iterate through the player
        // name array server-side unnecessarily
        _playerCount = _gameobj.getPlayerCount();

        // instantiate a player oid array which we'll fill in later
        _playerOids = new int[getPlayerSlots()];

        // give delegates a chance to do their thing
        super.didStartup();

        // let the players of this game know that we're ready to roll (if we have a specific set of
        // players)
        for (int ii = 0; ii < getPlayerSlots(); ii++) {
            // skip non-existent players and AIs
            if (!_gameobj.isOccupiedPlayer(ii) || isAI(ii)) {
                continue;
            }

            BodyObject bobj = CrowdServer.lookupBody(_gameobj.players[ii]);
            if (bobj == null) {
                log.warning("Unable to deliver game ready to non-existent player [game=" + where() +
                            ", player=" + _gameobj.players[ii] + "].");
                continue;
            }

            // deliver a game ready notification to the player
            ParlorSender.gameIsReady(bobj, _gameobj.getOid());
        }

        // start up a no-show timer if needed
        if (needsNoShowTimer()) {
            _noShowInterval = new Interval(CrowdServer.omgr) {
                public void expired () {
                    checkForNoShows();
                }
            };
            _noShowInterval.schedule(NOSHOW_DELAY);
        }
    }

    @Override // from PlaceManager
    protected void didShutdown ()
    {
        super.didShutdown();

        // unregister this game manager
        _managers.remove(this);

        // remove the tick interval if there are no remaining managers
        if (_managers.size() == 0) {
            _tickInterval.cancel();
            _tickInterval = null;
        }

        if (_gameobj != null) {
            // remove our state listener
            _gameobj.removeListener(_stateListener);
        }
    }

    @Override // from PlaceManager
    protected void bodyLeft (int bodyOid)
    {
        // first resign the player from the game
        int pidx = IntListUtil.indexOf(_playerOids, bodyOid);
        if (pidx != -1 && _gameobj.isInPlay() &&
            _gameobj.isActivePlayer(pidx)) {
            // end the player's game if they bail on an in-progress game
            endPlayerGame(pidx);
        }

        // then complete the bodyLeft() processing which may result in a call to placeBecameEmpty()
        // which will shut the game down
        super.bodyLeft(bodyOid);
    }

    /**
     * When a game room becomes empty, we cancel the game if it's still in progress and close down
     * the game room.
     */
    protected void placeBecameEmpty ()
    {
//         log.info("Game room empty. Going away. [game=" + where() + "].");

        // if we're in play then move to game over
        if (_gameobj.state != GameObject.PRE_GAME && _gameobj.state != GameObject.GAME_OVER &&
            _gameobj.state != GameObject.CANCELLED) {
            _gameobj.setState(GameObject.GAME_OVER);
            // and shutdown directly
            shutdown();

        // cancel the game; which will shut us down
        } else if (!cancelGame()) {
            // or shut down directly if the game is already over
            shutdown();
        }
    }

    /**
     * Called when all players have arrived in the game room. By default, this starts up the game,
     * but a manager may wish to override this and start the game according to different criterion.
     */
    protected void playersAllHere ()
    {
        // if we're a seated game and we haven't already started, start.
        if ((getMatchType() == GameConfig.SEATED_GAME) && _gameobj.state == GameObject.PRE_GAME) {
            startGame();
        }
    }

    /**
     * Called after the no-show delay has expired following the delivery of notifications to all
     * players that the game is ready.  <em>Note:</em> this is not called for party games. Those
     * games have a human who decides when to start the game.
     */
    protected void checkForNoShows ()
    {
        // nothing to worry about if we're already started
        if (_gameobj.state != GameObject.PRE_GAME) {
            return;
        }

        // if there's no one in the room, go ahead and clear it out
        if (_plobj.occupants.size() == 0) {
            log.info("Cancelling total no-show [game=" + where() +
                     ", players=" + StringUtil.toString(_gameobj.players) +
                     ", poids=" + StringUtil.toString(_playerOids) + "].");
            placeBecameEmpty();

        } else {
            // do the right thing if we have any no-show players
            for (int ii = 0; ii < getPlayerSlots(); ii++) {
                if (!playerIsReady(ii)) {
                    handlePartialNoShow();
                    return;
                }
            }
        }
    }

    /**
     * This is called when some, but not all, players failed to show up for a game. The default
     * implementation simply cancels the game.
     */
    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()
        int humansHere = 0;
        for (int ii = 0; ii < _playerOids.length; ii++) {
            if (_playerOids[ii] == 0) {
                _playerOids[ii] = -1;
            } else if (!isAI(ii)) {
                humansHere++;
            }
        }

        if ((humansHere == 0) && !startWithoutHumans()) {
            // if there are no human players in the game, just cancel it
            log.info("Canceling no-show game [game=" + where() +
                     ", players=" + StringUtil.toString(_playerOids) + "].");
            cancelGame();

        } else {
            // go ahead and report that everyone is ready (which will 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=" + where() +
                     ", poids=" + StringUtil.toString(_playerOids) + "].");
            playersAllHere();
        }
    }

    /**
     * @return true if we should start the game even without any humans.  Default implementation
     * always returns false.
     */
    protected boolean startWithoutHumans ()
    {
        return false;
    }

    /**
     * Called when the game is about to start, but before the game start notification has been
     * delivered to the players. Derived classes should override this if they need to perform some
     * pre-start activities, but should be sure to call <code>super.gameWillStart()</code>.
     */
    protected void gameWillStart ()
    {
        // increment the round identifier
        _gameobj.setRoundId(_gameobj.roundId + 1);

        // let our delegates do their business
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).gameWillStart();
            }
        });
    }

    /**
     * Called when the game state changes. This happens after the attribute change event has
     * propagated.
     *
     * @param state the new game state.
     * @param oldState the previous game state.
     */
    protected void stateDidChange (int state, int oldState)
    {
        switch (state) {
        case GameObject.IN_PLAY:
            gameDidStart();
            break;

        case GameObject.GAME_OVER:
            // we do some jiggery pokery to allow derived game objects to have different notions of
            // what it means to be in play
            _gameobj.state = oldState;
            boolean wasInPlay = _gameobj.isInPlay();
            _gameobj.state = state;

            // now call gameDidEnd() only if the game was previously in play
            if (wasInPlay) {
                gameDidEnd();
            }
            break;

        case GameObject.CANCELLED:
            // let the manager do anything it cares to
            gameWasCancelled();

            // and shutdown if there's no one here
            if (_plobj.occupants.size() == 0) {
                shutdown();
            }
            break;
        }
    }

    /**
     * Called after the game start notification was dispatched.  Derived classes can override this
     * to put whatever wheels they might need into motion now that the game is started (if anything
     * other than transitioning the game to {@link GameObject#IN_PLAY} is necessary), but should be
     * sure to call <code>super.gameDidStart()</code>.
     */
    protected void gameDidStart ()
    {
        // clear out our no-show timer if it's still running
        if (_noShowInterval != null) {
            _noShowInterval.cancel();
        }

        // let our delegates do their business
        applyToDelegates(new DelegateOp() {
            public void apply (PlaceManagerDelegate delegate) {
                ((GameManagerDelegate)delegate).gameDidStart();
            }
        });

        // inform the players of any pending messages.
        if (_startmsgs != null) {
            for (Tuple<String,String> mtup : _startmsgs) {
                systemMessage(mtup.left, /* bundle */ mtup.right /* message */);
            }
            _startmsgs = null;
        }

        // and register ourselves to receive AI ticks
        if (_AIs != null && needsAITick()) {
            AIGameTicker.registerAIGame(this);
        }

        // 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=" + where() +
                         ", player=" + getPlayerName(ii) + "].");
                _playerOids[ii] = 0; // unfiddle the blank oid
                endPlayerGame(ii);
            }
        }
    }

    /**
     * Called by the {@link AIGameTicker} if we're registered as an AI game.
     */
    protected void tickAIs ()
    {
        for (int ii = 0; ii < _AIs.length; ii++) {
            if (_AIs[ii] != null) {
                tickAI(ii, _AIs[ii]);
            }
        }
    }

    /**
     * Called by tickAIs to tick each AI in the game.
     */
    protected void tickAI (int pidx, GameAI ai)
    {
        _tickAIOp.setAI(pidx, ai);
        applyToDelegates(_tickAIOp);
    }

    /**
     * Announce to everyone in the game that a player's game has ended.
     */
    protected void announcePlayerGameOver (int pidx)
    {
        systemMessage(GAME_MESSAGE_BUNDLE,
            MessageBundle.tcompose(getPlayerGameOverMessage(pidx), getPlayerName(pidx)));
    }

    /**
     * Gets the untranslated string to show when a player's game has ended.
     */
    protected String getPlayerGameOverMessage (int pidx)
    {
        return "m.player_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
{'fraction_non_alphanumeric': 0.08820759652610652, 'fraction_numerical': 0.0016480066966621325, 'mean_word_length': 1.9631050653024842, 'pattern_counts': {'":': 0, '<': 59, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2766725', 'n_tokens_mistral': 20835, 'n_tokens_neox': 20056, 'n_words': 7612}