tv.dyndns.kishibe.qmaclone.server.Game.java Source code

Java tutorial

Introduction

Here is the source code for tv.dyndns.kishibe.qmaclone.server.Game.java

Source

//The MIT License
//
//Copyright (c) 2009 nodchip
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
package tv.dyndns.kishibe.qmaclone.server;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.annotation.Nullable;

import org.apache.commons.math3.distribution.NormalDistribution;
import org.apache.commons.math3.special.Erf;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Closeables;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;

import tv.dyndns.kishibe.qmaclone.client.constant.Constant;
import tv.dyndns.kishibe.qmaclone.client.game.GameMode;
import tv.dyndns.kishibe.qmaclone.client.game.ProblemGenre;
import tv.dyndns.kishibe.qmaclone.client.game.ProblemType;
import tv.dyndns.kishibe.qmaclone.client.game.Transition;
import tv.dyndns.kishibe.qmaclone.client.packet.NewAndOldProblems;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketGameStatus;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketGameStatus.GamePlayerStatus;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketMatchingPlayer;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketMatchingStatus;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketPlayerSummary;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketProblem;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketReadyForGame;
import tv.dyndns.kishibe.qmaclone.client.packet.PacketResult;
import tv.dyndns.kishibe.qmaclone.client.packet.RestrictionType;
import tv.dyndns.kishibe.qmaclone.server.database.Database;
import tv.dyndns.kishibe.qmaclone.server.database.DatabaseException;
import tv.dyndns.kishibe.qmaclone.server.websocket.MessageSender;

public class Game {
    private static final Logger logger = Logger.getLogger(Game.class.getName());
    private static final int SECONDS_FROM_READY_TO_PROBLEM = 10;
    private static final int SECONDS_FROM_PROBLEM_TO_ANSWER = 30;
    private static final int SECONDS_FROM_ANSWER_TO_PROBLEM_OR_RESULT = 5;
    private static final int SECONDS_FROM_RESULT_TO_FINISHED = 600;
    private final GameManager gameManager;
    private static volatile int playerId = 0;
    private final int classLevel;
    private final int sessionId;
    // TODO enum?
    // TODO(nodchip): Atomic***?
    /**
     * ???
     */
    private final AtomicReference<Transition> transition = new AtomicReference<Transition>(Transition.Matching);
    /**
     * ???
     */
    private final AtomicInteger secondsToNextState = new AtomicInteger();
    private final List<Integer> problemIds = Lists.newArrayList(); // ?
    private volatile List<PacketProblem> problems = null;
    private final Set<Integer> selectedProblemIds = Sets.newHashSet();
    // TODO(nodchip):CopyOnWriteArray??
    private final List<PlayerStatus> playerStatuses = Lists.newArrayList();
    private final AtomicInteger numberOfInitialHumanPlayers = new AtomicInteger();
    private final Set<ProblemGenre> selectedGenres = EnumSet.noneOf(ProblemGenre.class); // ?
    private final Set<ProblemType> selectedTypes = EnumSet.noneOf(ProblemType.class);
    private final Set<Integer> setDifficult = Sets.newHashSet();
    private final Set<NewAndOldProblems> setNewAndOldProblems = Sets.newHashSet();
    private final AtomicInteger numberOfRequestStartingGame = new AtomicInteger(); // ?????
    private final List<PacketMatchingPlayer> matchingPlayers = Lists.newArrayList(); // ??
    private final AtomicInteger problemCounter = new AtomicInteger();
    private volatile long questionStartTime;
    private final Random random = new Random();
    private volatile List<PacketResult> packetResult; // ??
    private volatile ComputerPlayer computerPlayer = null;
    private final boolean event;
    private final boolean alone;
    private volatile Set<ProblemGenre> firstGenre;
    private volatile Set<ProblemType> firstType;
    private volatile int firstDifficultSelect;
    private volatile NewAndOldProblems firstNewAndOldProblems;
    private final String theme;
    private final boolean publicEvent;
    private final Set<Integer> unavailableUserCodesForProblems = Sets.newHashSet();
    private final Set<Integer> unavailableCreatorHashes = Sets.newHashSet();
    private final ServerStatusManager serverStatusManager;
    private final ScheduledFuture<?> timer;
    private final NormalModeProblemManager normalModeProblemManager;
    private final ThemeModeProblemManager themeModeProblemManager;
    private final Database database;
    private final ComputerPlayer.Factory computerPlayerFactory;
    private final ThreadPool threadPool;
    private final MessageSender<PacketMatchingStatus> matchingStatusMessageSender;
    private final MessageSender<PacketReadyForGame> readyForGameMessageSender;
    private final MessageSender<PacketGameStatus> gameStatusMessageSender;
    private final RestrictedUserUtils restrictedUserUtils;
    private final GameMode gameMode;

    public static interface Factory {
        Game create(@Assisted("sessionId") int sessionId, @Assisted("classLevel") int classLevel,
                @Assisted("EVENT") boolean event, @Assisted("alone") boolean alone, @Assisted("THEME") String theme,
                @Assisted("publicEvent") boolean publicEvent, @Assisted("gameMode") GameMode gameMode);
    }

    @Inject
    public Game(GameManager gameManager, ServerStatusManager serverStatusManager,
            NormalModeProblemManager normalModeProblemManager, ThemeModeProblemManager themeModeProblemManager,
            Database database, ComputerPlayer.Factory computerPlayerFactory, ThreadPool threadPool,
            RestrictedUserUtils restrictedUserUtils, @Assisted("sessionId") int sessionId,
            @Assisted("classLevel") int classLevel, @Assisted("EVENT") boolean event,
            @Assisted("alone") boolean alone, @Nullable @Assisted("THEME") String theme,
            @Assisted("publicEvent") boolean publicEvent, @Assisted("gameMode") GameMode gameMode,
            MessageSender<PacketMatchingStatus> matchingStatusMessageSender,
            MessageSender<PacketReadyForGame> readyForGameMessageSender,
            MessageSender<PacketGameStatus> gameStatusMessageSender) {
        this.gameManager = gameManager;
        this.serverStatusManager = serverStatusManager;
        this.normalModeProblemManager = normalModeProblemManager;
        this.themeModeProblemManager = themeModeProblemManager;
        this.database = database;
        this.computerPlayerFactory = computerPlayerFactory;
        this.threadPool = threadPool;
        this.sessionId = sessionId;
        this.classLevel = classLevel;
        this.event = event;
        this.alone = alone;
        this.theme = theme;
        this.publicEvent = publicEvent;
        this.gameMode = Preconditions.checkNotNull(gameMode);
        this.restrictedUserUtils = Preconditions.checkNotNull(restrictedUserUtils);
        this.matchingStatusMessageSender = Preconditions.checkNotNull(matchingStatusMessageSender);
        this.readyForGameMessageSender = Preconditions.checkNotNull(readyForGameMessageSender);
        this.gameStatusMessageSender = Preconditions.checkNotNull(gameStatusMessageSender);

        if (sessionId == 0) {
            String object = MoreObjects.toStringHelper(this).add("gameManager", gameManager)
                    .add("serverStatusManager", serverStatusManager)
                    .add("normalModeProblemManager", normalModeProblemManager)
                    .add("themeModeProblemManager", themeModeProblemManager).add("database", database)
                    .add("computerPlayerFactory", computerPlayerFactory).add("threadPool", threadPool)
                    .add("sessionId", sessionId).add("classLevel", classLevel).add("EVENT", event)
                    .add("alone", alone).add("THEME", theme).add("publicEvent", publicEvent).toString();
            logger.log(Level.SEVERE, "??ID?????: " + object);
        }

        secondsToNextState.set(Constant.WAIT_SECOND_FOR_MATCHING);

        timer = threadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                updateState();
            }
        }, 1, 1, TimeUnit.SECONDS);
        updateState();
    }

    /**
     * ??
     */
    private synchronized void updateState() {
        Transition next = null;
        switch (transition.get()) {
        case Matching:
            next = updateMatchingState();
            break;
        case Ready:
            next = updateReadyState();
            break;
        case Problem:
            next = updateProblemState();
            break;
        case Answer:
            next = updateAnswerState();
            break;
        case Result:
            next = updateResultState();
            break;
        case Finished:
            next = Transition.Finished;
            break;
        }

        transition.set(next);

        updateMatchingStatus();
        updateReadyForGame();
        updateGameStatus();
    }

    private synchronized Transition updateMatchingState() {
        if (Transition.Matching.compareTo(transition.get()) < 0) {
            return transition.get();
        }

        if (secondsToNextState.decrementAndGet() < 0) {
            // ???
            return transitFromMachingToReady();
        }

        return Transition.Matching;
    }

    public int getRestMatchingSecond() {
        if (transition.get() != Transition.Matching) {
            return 0;
        }

        return secondsToNextState.get();
    }

    public Transition getTransition() {
        return transition.get();
    }

    public int getNumberOfHumanPlayer() {
        int numberOfHumanPlayer = 0;
        for (PlayerStatus status : playerStatuses) {
            numberOfHumanPlayer += status.isHuman() ? 1 : 0;
        }
        return numberOfHumanPlayer;
    }

    public synchronized int getNumberOfPlayer() {
        return playerStatuses.size();
    }

    public synchronized List<PacketProblem> getProblem() {
        return problems;
    }

    /**
     * ?
     * 
     * @param playerSummary
     *          
     * @param genre
     *          ?
     * @param type
     *          ?
     * @param greeting
     *          
     * @param imageFileName
     *          ???
     * @param classLevel
     *          
     * @param difficultSelect
     *          
     * @param rating
     *          
     * @param userCode
     *          
     * @param newAndOldProblem
     *          ?/?
     * @return 
     */
    public synchronized PlayerStatus addPlayer(PacketPlayerSummary playerSummary, Set<ProblemGenre> genres,
            Set<ProblemType> types, String greeting, String imageFileName, int classLevel, int difficultSelect,
            int rating, int userCode, int volatility, int playCount, NewAndOldProblems newAndOldProblem) {
        if (matchingPlayers.isEmpty()) {
            firstGenre = genres;
            firstType = types;
            firstDifficultSelect = difficultSelect;
            firstNewAndOldProblems = newAndOldProblem;
        }

        // ???????
        if (!Strings.isNullOrEmpty(theme)) {
            genres.clear();
            types.clear();
        }

        // ?????ServiceImpl??
        PlayerStatus status = new PlayerStatus(playerSummary, Game.playerId++, this.playerStatuses.size(),
                this.sessionId, true, greeting, imageFileName, classLevel, rating, userCode, volatility, playCount);

        playerStatuses.add(status);
        PacketMatchingPlayer matchingPlayer = new PacketMatchingPlayer();
        matchingPlayer.playerSummary = playerSummary;
        matchingPlayer.isRequestSkip = false;
        matchingPlayer.greeting = greeting;
        matchingPlayer.imageFileName = imageFileName;
        matchingPlayers.add(matchingPlayer);

        for (int i = 0; i < Constant.MAX_PROBLEMS_PER_PLAYER; ++i) {
            int problemID = selectProblem(genres, types, classLevel, difficultSelect, theme, newAndOldProblem);
            problemIds.add(problemID);
        }
        selectedGenres.addAll(genres);
        selectedTypes.addAll(types);
        setDifficult.add(difficultSelect);
        setNewAndOldProblems.add(newAndOldProblem);

        numberOfInitialHumanPlayers.incrementAndGet();

        // ????Ready???
        if (playerStatuses.size() >= Constant.MAX_PLAYER_PER_SESSION || alone) {
            // ??????????
            transition.set(transitFromMachingToReady());
        }

        return status;
    }

    /**
     * ???
     * 
     * @param genre
     *          
     * @param type
     *          
     * @param classLevel
     *          
     * @param difficultSelect
     *          
     * @param THEME
     *          
     * @param newAndOldProblems
     *          ?/?
     * @return ??
     */
    private synchronized int selectProblem(Set<ProblemGenre> genres, Set<ProblemType> types, int classLevel,
            int difficultSelect, String theme, NewAndOldProblems newAndOldProblems) {
        if (event) {
            genres = firstGenre;
            types = firstType;
            difficultSelect = firstDifficultSelect;
            newAndOldProblems = firstNewAndOldProblems;
        }

        try {
            if (theme == null) {
                boolean tegaki = event;
                return normalModeProblemManager.selectProblem(genres, types, classLevel, difficultSelect,
                        selectedProblemIds, true, newAndOldProblems, tegaki, unavailableUserCodesForProblems,
                        unavailableCreatorHashes).id;
            } else {
                // 
                return themeModeProblemManager.selectProblem(theme, difficultSelect, classLevel,
                        selectedProblemIds).id;
            }
        } catch (Exception e) {
            logger.log(Level.WARNING, "????????", e);
        }

        // ?????????????
        return random.nextInt(200000);
    }

    /**
     * ???????
     * 
     * @param playerListId
     *          ???ID
     */
    public synchronized void requestStartingGame(int playerListId) {
        PlayerStatus status = (PlayerStatus) playerStatuses.get(playerListId);
        if (status.isRequestStartingGame()) {
            return;
        }

        status.setRequestStartingGame();
        matchingPlayers.get(playerListId).isRequestSkip = true;

        // ?????????
        if (numberOfRequestStartingGame.incrementAndGet() >= playerStatuses.size()) {
            // ??????????
            transition.set(transitFromMachingToReady());
        }
    }

    private volatile PacketMatchingStatus matchnigStatus;

    private synchronized void updateMatchingStatus() {
        PacketMatchingStatus matchingStatus = new PacketMatchingStatus();

        Transition t = transition.get();
        if (t != Transition.Matching) {
            matchingStatus.restSeconds = 0;
        } else {
            matchingStatus.restSeconds = secondsToNextState.get();
        }

        if (t == Transition.Matching || t == Transition.Ready) {
            // matchingPlayers?Serialize????????????
            matchingStatus.players = Lists.newArrayList(matchingPlayers);
        }

        this.matchnigStatus = matchingStatus;
        matchingStatusMessageSender.send(matchingStatus);
    }

    /**
     * ??
     * 
     * @return ?
     */
    public synchronized PacketMatchingStatus getMatchingStatus() {
        Preconditions.checkNotNull(matchnigStatus, "??null??: sessionId=" + sessionId);
        return matchnigStatus;
    }

    public MessageSender<PacketMatchingStatus> getMatchingStatusMessageSender() {
        return matchingStatusMessageSender;
    }

    private volatile PacketReadyForGame readyForGame;

    private synchronized void updateReadyForGame() {
        PacketReadyForGame readyForGame = new PacketReadyForGame();
        if (transition.get() != Transition.Ready) {
            readyForGame.restSeconds = 0;
        } else {
            readyForGame.restSeconds = secondsToNextState.get();
        }

        this.readyForGame = readyForGame;

        readyForGameMessageSender.send(readyForGame);
    }

    public MessageSender<PacketReadyForGame> getReadyForGameMessageSender() {
        return readyForGameMessageSender;
    }

    /**
     * Ready?Problem??????
     * 
     * @return 
     */
    public synchronized PacketReadyForGame getReadyForGameStatus() {
        Preconditions.checkNotNull(readyForGame, "readyForGame  == null: sessionId=" + sessionId);
        return readyForGame;
    }

    /**
     * Matching?Ready??
     */
    private synchronized Transition transitFromMachingToReady() {
        // ????????Ready??????????
        if (Transition.Ready.compareTo(transition.get()) <= 0) {
            return transition.get();
        }

        // ????????????
        if (1 < getNumberOfHumanPlayer()) {
            for (PlayerStatus player : playerStatuses) {
                String message = MoreObjects.toStringHelper(this).add("method", "transitFromMachingToReady")
                        .add("sessionId", sessionId).add("userCode", player.getUserCode()).toString();
                logger.log(Level.INFO, message);
            }
        }

        secondsToNextState.set(SECONDS_FROM_READY_TO_PROBLEM);

        gameManager.notifyMatchingCompleted();

        // ?
        serverStatusManager.changeStatics(1, numberOfInitialHumanPlayers.get());

        int difficultSelect = (setDifficult.size() == 1) ? setDifficult.iterator().next()
                : Constant.DIFFICULT_SELECT_NORMAL;
        NewAndOldProblems newAndOldProblems = (setNewAndOldProblems.size() == 1)
                ? setNewAndOldProblems.iterator().next()
                : NewAndOldProblems.Both;

        problems = prepareProblems(difficultSelect, newAndOldProblems, problemIds, selectedGenres, selectedTypes,
                classLevel, theme);

        computerPlayer = computerPlayerFactory.create(problemIds);

        // COM
        while (playerStatuses.size() < Constant.MAX_PLAYER_PER_SESSION) {
            PacketPlayerSummary playerSummary = computerPlayer.newPlayer(difficultSelect);
            String greeting = computerPlayer.getGreeting();

            PlayerStatus status = new PlayerStatus(playerSummary, -1, playerStatuses.size(), this.sessionId, false,
                    greeting, computerPlayer.selectIconFileName(), Constant.MAX_CLASS_LEVEL / 2, 0, -1, -1, -1);
            playerStatuses.add(status);

            PacketMatchingPlayer matchingPlayer = new PacketMatchingPlayer();
            matchingPlayer.playerSummary = playerSummary;
            matchingPlayer.isRequestSkip = false;
            matchingPlayer.greeting = greeting;
            matchingPlayer.imageFileName = status.getImageFileName();
            matchingPlayers.add(matchingPlayer);
        }

        return Transition.Ready;
    }

    @VisibleForTesting
    List<PacketProblem> prepareProblems(int difficultSelect, NewAndOldProblems newAndOldProblems,
            List<Integer> problemIds, Set<ProblemGenre> selectedGenres, Set<ProblemType> selectedTypes,
            int classLevel, String theme) {
        // ??????????????
        // BugTrack-QMAClone/381 - QMAClone wiki
        // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F381

        // ??????????????
        // BugTrack-QMAClone/418 - QMAClone wiki
        // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F418#1328272472

        // ?selectedGenres?selectedTypes?????????
        // BugTrack-QMAClone/424 - QMAClone wiki
        // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F424
        if (selectedGenres.isEmpty()) {
            selectedGenres = EnumSet.of(ProblemGenre.Random);
        }
        if (selectedTypes.isEmpty()) {
            selectedTypes = EnumSet.of(ProblemType.Random);
        }

        int numberOfProblemsToAdd = Constant.MAX_PROBLEMS_PER_SESSION - problemIds.size();

        List<ProblemGenre> genres = Lists.newArrayList(selectedGenres);
        for (int i = 0; i < numberOfProblemsToAdd; ++i) {
            genres.add(genres.get(i));
        }
        Collections.shuffle(genres);

        List<ProblemType> types = Lists.newArrayList(selectedTypes);
        for (int i = 0; i < numberOfProblemsToAdd; ++i) {
            types.add(types.get(i));
        }
        Collections.shuffle(types);

        for (int i = 0; i < numberOfProblemsToAdd; ++i) {
            int problemID = selectProblem(EnumSet.of(genres.get(i)), EnumSet.of(types.get(i)), classLevel,
                    difficultSelect, theme, newAndOldProblems);
            problemIds.add(problemID);
        }

        List<PacketProblem> problems;
        try {
            problems = database.getProblem(problemIds);
        } catch (DatabaseException e) {
            logger.log(Level.SEVERE, "?????????", e);
            return null;
        }

        // ???
        Collections.shuffle(problems);

        // ?
        for (PacketProblem problem : problems) {
            problem.prepareShuffledAnswersAndChoices();
        }

        return problems;
    }

    /**
     * Ready?
     */
    private synchronized Transition updateReadyState() {
        if (Transition.Ready.compareTo(transition.get()) < 0) {
            return transition.get();
        }

        if (secondsToNextState.decrementAndGet() < 0) {
            // Problem???
            return transitFromReadyToProblem();
        }

        return Transition.Ready;
    }

    /**
     * Problem??
     */
    private synchronized Transition transitFromReadyToProblem() {
        // ????????????
        if (Transition.Problem.compareTo(transition.get()) <= 0) {
            return transition.get();
        }

        calculateRanking();

        return transitFromReadyOrAnswerToProblem();
    }

    /**
     * ????
     * 
     * @return 
     */
    private int getRestProblemMs() {
        // questionStartTime???????synchronized??
        // ???64bit???64bit?JDK?????????
        long currentTime = Calendar.getInstance().getTimeInMillis();
        int rest = (int) (currentTime - questionStartTime);
        rest = SECONDS_FROM_PROBLEM_TO_ANSWER * 1000 - rest;
        return rest;
    }

    /**
     * ????
     * 
     * @param playerListId
     *          ID
     * @param answer
     *          ?
     */
    public synchronized void receiveAnswer(int playerListId, String answer) {
        // Problem???????
        if (transition.get() != Transition.Problem) {
            return;
        }

        PlayerStatus player = playerStatuses.get(playerListId);
        player.clearSkipCount();
        if (player.isAnswered()) {
            return;
        }
        player.setAnswer(answer, Math.max(1, getRestProblemMs()));

        // HUM???????????????
        boolean allHumAnswered = true;
        for (PlayerStatus status : playerStatuses) {
            if (status.isHuman() && !status.isAnswered()) {
                allHumAnswered = false;
                break;
            }
        }
        if (allHumAnswered) {
            transition.set(transitFromProblemToAnswer());
        }
    }

    /**
     * ????
     */
    private synchronized Transition transitFromReadyOrAnswerToProblem() {
        secondsToNextState.set(SECONDS_FROM_PROBLEM_TO_ANSWER);

        for (PlayerStatus player : playerStatuses) {
            player.incSkipCount();
            player.clearAnswer();
        }

        // 
        questionStartTime = Calendar.getInstance().getTimeInMillis();

        return Transition.Problem;
    }

    /**
     * Problem?
     */
    private synchronized Transition updateProblemState() {
        if (getRestProblemMs() < 0) {
            return transitFromProblemToAnswer();
        }

        int computerTiming = (classLevel == Constant.CLASS_LEVEL_NORMAL) ? Constant.MAX_CLASS_LEVEL / 2
                : classLevel;
        if (getRestProblemMs() < 20000 + 10000 * computerTiming / Constant.MAX_CLASS_LEVEL) {
            int index = random.nextInt(Constant.MAX_PLAYER_PER_SESSION);

            PlayerStatus playerStatus = playerStatuses.get(index);
            if (!playerStatus.isHuman() && !playerStatus.isAnswered()) {
                PacketProblem problem = problems.get(problemCounter.get());
                String answer = computerPlayer.getAnswer(problem, getPlayerAnswers());
                playerStatus.setAnswer(answer, getRestProblemMs());
            }
        }

        return Transition.Problem;
    }

    private List<String> getPlayerAnswers() {
        List<String> playerAnswers = Lists.newArrayList();
        for (GamePlayerStatus status : getGameStatus().status) {
            playerAnswers.add(status.answer);
        }
        return playerAnswers;
    }

    /**
     * Problem?Answer??
     */
    private synchronized Transition transitFromProblemToAnswer() {
        secondsToNextState.set(SECONDS_FROM_ANSWER_TO_PROBLEM_OR_RESULT);

        // COM?
        // HUM??
        // PacketProblem problem = (PacketProblem)
        // problems[problemCounter];
        final PacketProblem problem = problems.get(problemCounter.get());

        for (PlayerStatus player : playerStatuses) {
            if (!player.isHuman() && !player.isAnswered()) {
                String answer = computerPlayer.getAnswer(problem, getPlayerAnswers());
                int restTime = random.nextInt(Math.max(1, getRestProblemMs()));
                player.setAnswer(answer, restTime);
                // pushAnswer(player.getPlayerListId(), answer);
            }

            if (player.isHuman() && player.isAnswered()) {
                player.clearSkipCount();
            }
        }

        // ??
        final List<String> playerAnswers = new ArrayList<String>();
        for (PlayerStatus player : playerStatuses) {
            boolean correct = problem.isCorrect(player.getAnswer());
            if (correct) {
                int point = calcPoint(problem, player.getClassLevel(), player.getTimeRemain());
                player.addScore(point);
            }

            if (player.isHuman()) {
                if (correct) {
                    ++problem.good;
                } else {
                    ++problem.bad;
                }

                if (player.getAnswer() != null && !player.getAnswer().isEmpty()) {
                    playerAnswers.add(player.getAnswer());
                }
            }
        }

        if (!playerAnswers.isEmpty()) {
            threadPool.execute(new Runnable() {
                public void run() {
                    try {
                        database.addPlayerAnswers(problem.id, problem.type, playerAnswers);
                    } catch (DatabaseException e) {
                        logger.log(Level.WARNING, "???????", e);
                    }
                }
            });
        }

        // 
        // ????????
        if (gameMode == GameMode.WHOLE) {
            threadPool.execute(new Runnable() {
                public void run() {
                    try {
                        normalModeProblemManager.updateMinimumProblem(problem);
                    } catch (DatabaseException e) {
                        logger.log(Level.WARNING, "???????", e);
                    }
                }
            });
        }

        // ??
        for (PlayerStatus player : playerStatuses) {
            if (player.isHuman() && player.shouldBeDropped()) {
                player.drop();
                String message = "????: " + MoreObjects.toStringHelper(this)
                        .add("sessionId", sessionId).add("playerListId", player.getPlayerListId()).toString();
                logger.log(Level.INFO, message);
            }
        }

        // ??0?????????????????????
        // // ???????????
        // if (getNumberOfHumanPlayer() == 0) {
        // return transitFromAnswerToResult();
        // }

        // ?
        calculateRanking();

        // ?
        problemCounter.incrementAndGet();

        return Transition.Answer;
    }

    /**
     * ??
     * 
     * @param problem
     *          ?
     * @param classLevel
     *          
     * @param restTime
     *          
     * @return ?
     */
    private synchronized int calcPoint(PacketProblem problem, int classLevel, int restTime) {
        if (theme == null) {
            return calculateNormalProblemScore(classLevel, restTime);
        } else {
            return calculateThemeModeProblemScore(problem, classLevel, restTime);
        }
    }

    private int calculateThemeModeProblemScore(PacketProblem problem, int classLevel, int restTime) {
        // 
        double accuracyRate = problem.getAccuracyRate() * 0.01;
        double scale = accuracyRate;
        if (scale < 0) {
            scale = 0.5;
        }
        scale = 4.0 - 3.0 * scale;
        double basePoint = scale * Constant.MAX_POINT / Constant.MAX_PROBLEMS_PER_SESSION;

        // ?????????
        int maxQuestionTime = SECONDS_FROM_PROBLEM_TO_ANSWER * 1000;
        int maxClassLevel = Constant.MAX_CLASS_LEVEL;
        double rc = (double) (maxClassLevel - classLevel) / (double) maxClassLevel;
        int perfectBoderTime = (int) (Constant.MAX_PERFECT_BORDER_TIME * rc);

        if (restTime + perfectBoderTime > maxQuestionTime) {
            return (int) basePoint;
        }

        double r = (double) restTime / (double) (maxQuestionTime - perfectBoderTime);
        int compressPoint = (int) (basePoint
                * ((Constant.MAX_POINT_COMPRESS - Constant.MIN_POINT_COMPRESS) * rc + Constant.MIN_POINT_COMPRESS));
        int point = (int) (basePoint * r + compressPoint * (1.0 - r));
        return point;
    }

    private int calculateNormalProblemScore(int classLevel, int restTime) {
        // 
        int maxQuestionTime = SECONDS_FROM_PROBLEM_TO_ANSWER * 1000;
        int maxClassLevel = Constant.MAX_CLASS_LEVEL;
        double rc = (double) (maxClassLevel - classLevel) / (double) maxClassLevel;
        int perfectBoderTime = (int) (Constant.MAX_PERFECT_BORDER_TIME * rc);
        int perfectPoint = Constant.MAX_POINT / Constant.MAX_PROBLEMS_PER_SESSION;

        if (restTime + perfectBoderTime > maxQuestionTime) {
            return perfectPoint;
        }

        double r = (double) restTime / (double) (maxQuestionTime - perfectBoderTime);
        int compressPoint = (int) (perfectPoint
                * ((Constant.MAX_POINT_COMPRESS - Constant.MIN_POINT_COMPRESS) * rc + Constant.MIN_POINT_COMPRESS));
        int point = (int) (perfectPoint * r + compressPoint * (1.0 - r));
        return point;
    }

    /**
     * Answer?
     * 
     * @return 
     */
    private synchronized Transition updateAnswerState() {
        if (secondsToNextState.decrementAndGet() < 0) {
            // ??????????
            return transitFromAnswerToProblemOrResult();
        }

        return Transition.Answer;
    }

    /**
     * Answer?Problem/Result??
     * 
     * @return 
     */
    private synchronized Transition transitFromAnswerToProblemOrResult() {
        if (problemCounter.get() >= Constant.MAX_PROBLEMS_PER_SESSION) {
            return transitFromAnswerToResult();
        } else {
            return transitFromReadyOrAnswerToProblem();
        }
    }

    /**
     * Answer?Result??
     * 
     * @return 
     */
    private synchronized Transition transitFromAnswerToResult() {
        secondsToNextState.set(SECONDS_FROM_RESULT_TO_FINISHED);

        // ?
        PlayerStatus players[] = playerStatuses.toArray(new PlayerStatus[0]);
        Arrays.sort(players, new Comparator<PlayerStatus>() {
            public int compare(PlayerStatus o1, PlayerStatus o2) {
                return o2.getScore() - o1.getScore();
            }
        });

        // BugTrack-QMAClone/401
        // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack-QMAClone%2F401
        for (int i = 0; i < players.length; ++i) {
            players[i].setRank(i + 1);
        }

        for (PlayerStatus player : players) {
            player.setNewRating(player.getRating());

            // volatility?0??
            if (player.getVolatility() == 0) {
                player.setVolatility(300);
            }
            player.setNewVolatility(player.getVolatility());
        }

        if (2 <= numberOfInitialHumanPlayers.get()) {
            List<PlayerStatus> humanPlayers = Lists.newArrayList();
            for (int i = 0; i < numberOfInitialHumanPlayers.get(); ++i) {
                humanPlayers.add(playerStatuses.get(i));
            }

            calculateRating(humanPlayers);
        }

        // ??????????
        List<PacketResult> packetResult = new ArrayList<PacketResult>();
        for (int i = 0; i < players.length; ++i) {
            packetResult.add(players[i].toResult());
        }
        this.packetResult = packetResult;

        if (theme != null) {
            for (PlayerStatus playerStatus : players) {
                int userCode = playerStatus.getUserCode();
                if (userCode < 0) {
                    continue;
                }

                int score = playerStatus.getScore();
                try {
                    database.updateThemeModeScore(userCode, theme, score);
                } catch (DatabaseException e) {
                    logger.log(Level.WARNING, "???????", e);
                }
            }
        }

        return Transition.Result;
    }

    @VisibleForTesting
    void calculateRating(List<PlayerStatus> players) {
        NormalDistribution normalDistribution = new NormalDistribution();

        // 
        // http://topcoder.g.hatena.ne.jp/n4_t/20081222/
        // http://apps.topcoder.com/wiki/display/tc/Algorithm+Competition+Rating+System
        Preconditions.checkState(2 <= players.size());

        int numCoders = players.size();
        double sumRating = 0.0;
        for (PlayerStatus player : players) {
            sumRating += player.getRating();
        }
        double aveRating = sumRating / numCoders;

        // The competition factor is calculated:
        double sumVolatility2 = 0.0;
        double sumDiffRatingAveRating = 0.0;
        for (PlayerStatus player : players) {
            sumVolatility2 += player.getVolatility() * player.getVolatility();
            double diffRatingAveRating = player.getRating() - aveRating;
            sumDiffRatingAveRating += diffRatingAveRating * diffRatingAveRating;
        }
        double cf = Math.sqrt(sumVolatility2 / numCoders + sumDiffRatingAveRating / (numCoders - 1));

        // ??
        Collections.sort(players, new Comparator<PlayerStatus>() {
            @Override
            public int compare(PlayerStatus o1, PlayerStatus o2) {
                int black1;
                int black2;
                try {
                    int userCode1 = o1.getUserCode();
                    int rating1 = o1.getRating();
                    int userCode2 = o2.getUserCode();
                    int rating2 = o2.getRating();
                    black1 = (restrictedUserUtils.checkAndUpdateRestrictedUser(userCode1, "127.0.0.1",
                            RestrictionType.MATCH) && rating1 > 1700) ? 1 : 0;
                    black2 = (restrictedUserUtils.checkAndUpdateRestrictedUser(userCode2, "127.0.0.1",
                            RestrictionType.MATCH) && rating2 > 1700) ? 1 : 0;
                } catch (DatabaseException e) {
                    throw Throwables.propagate(e);
                }
                return black1 != black2 ? black1 - black2 : o2.getScore() - o1.getScore();
            }
        });
        for (int i = 0; i < players.size(); ++i) {
            if (0 < i && players.get(i - 1).getScore() == players.get(i).getScore()) {
                // ?????
                players.get(i).setHumanRank(players.get(i - 1).getHumanRank());
            } else {
                players.get(i).setHumanRank(i + 1);
            }
        }
        // ??????
        // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack-QMAClone%2F490
        // for (PlayerStatus playerStatus : players) {
        // if (badUserManager.isLimitedUser(playerStatus.getUserCode(), null)) {
        // playerStatus.setHumanRank(players.size());
        // }
        // }

        for (PlayerStatus my : players) {
            if (!my.isHuman()) {
                continue;
            }

            double myRating = my.getRating();
            double myVolatility = my.getVolatility();

            // Win Probability Estimation Algorithm:
            double eRank = 0.5;
            for (PlayerStatus player : players) {
                double hisVolatility = player.getVolatility();
                double wp = 0.5;
                wp = 0.5 * (Erf
                        .erf((player.getRating() - myRating)
                                / Math.sqrt(2 * (hisVolatility * hisVolatility + myVolatility * myVolatility)))
                        + 1.0);

                // BugTrack-QMAClone/603 - QMAClone wiki
                // http://kishibe.dyndns.tv/qmaclone/wiki/wiki.cgi?page=BugTrack%2DQMAClone%2F603
                if (my != player && my.getUserCode() == player.getUserCode()) {
                    wp = 0.0;
                }
                eRank += wp;
            }

            // The expected performance of the coder is calculated:
            double ePerf = -normalDistribution.inverseCumulativeProbability((eRank - 0.5) / numCoders);

            // The actual performance of each coder is calculated:
            double aPerf = -normalDistribution.inverseCumulativeProbability((my.getHumanRank() - 0.5) / numCoders);

            // The performed as rating of the coder is calculated:
            double perfAs = myRating + cf * (aPerf - ePerf);

            // The weight of the competition for the coder is calculated:
            double weight = 1.0 / (1 - (0.42 / (my.getPlayCount() + 1) + 0.18)) - 1.0;

            // A cap is calculated:
            double cap = 150 + 1500 / (my.getPlayCount() + 2);

            // The new rating of the coder is calculated:
            double newRating = (myRating + weight * perfAs) / (1.0 + weight);
            newRating = Math.min(newRating, myRating + cap);
            newRating = Math.max(newRating, myRating - cap);

            // The new volatility of the coder is calculated:
            double diffRating = newRating - myRating;
            double newVolatility = Math
                    .sqrt(diffRating * diffRating / weight + myVolatility * myVolatility / (weight + 1));

            my.setNewRating((int) Math.rint(newRating));
            my.setNewVolatility((int) Math.rint(newVolatility));
        }

        // ?????
        Collections.sort(players, new Comparator<PlayerStatus>() {
            @Override
            public int compare(PlayerStatus o1, PlayerStatus o2) {
                return o2.getScore() - o1.getScore();
            }
        });
        for (int i = 0; i < players.size(); ++i) {
            if (0 < i && players.get(i - 1).getScore() == players.get(i).getScore()) {
                // ?????
                players.get(i).setHumanRank(players.get(i - 1).getHumanRank());
            } else {
                players.get(i).setHumanRank(i + 1);
            }
        }
    }

    /**
     * Ready?
     * 
     * @return 
     */
    private synchronized Transition updateResultState() {
        if (secondsToNextState.decrementAndGet() < 0) {
            return transitFromResultToFinished();
        }

        return Transition.Result;
    }

    /**
     * Result?Finished??
     * 
     * @return 
     */
    private synchronized Transition transitFromResultToFinished() {
        // ??
        timer.cancel(false);

        try {
            Closeables.close(matchingStatusMessageSender, true);
            Closeables.close(readyForGameMessageSender, true);
            Closeables.close(gameStatusMessageSender, true);
        } catch (IOException e) {
            Preconditions.checkState(false, "Unreachable");
        }
        return Transition.Finished;
    }

    // ????????
    public synchronized void keepAlive(int playerListId) {
        if (playerListId >= 0) {
            playerStatuses.get(playerListId).clearSkipCount();
        }
    }

    public synchronized List<PacketResult> getPacketResult() {
        return packetResult;
    }

    private volatile PacketGameStatus gameStatus;

    public MessageSender<PacketGameStatus> getGameStatusMessageSender() {
        return gameStatusMessageSender;
    }

    public synchronized PacketGameStatus getGameStatus() {
        Preconditions.checkNotNull(gameStatus, "?null??: sessionId=" + sessionId);
        return gameStatus;
    }

    public synchronized void updateGameStatus() {
        PacketGameStatus status = new PacketGameStatus();
        status.problemCounter = problemCounter.get();
        status.restMs = getRestProblemMs();
        status.transition = transition.get();
        status.status = new PacketGameStatus.GamePlayerStatus[playerStatuses.size()];
        for (int playerIndex = 0; playerIndex < playerStatuses.size(); ++playerIndex) {
            GamePlayerStatus gamePlayerStatus = new GamePlayerStatus();
            PlayerStatus playerStatus = playerStatuses.get(playerIndex);
            gamePlayerStatus.score = playerStatus.getScore();
            gamePlayerStatus.answer = playerStatus.getAnswer();
            gamePlayerStatus.rank = playerStatus.getTempRanking();
            status.status[playerIndex] = gamePlayerStatus;
        }

        for (PlayerStatus playerStatus : playerStatuses) {
            if (playerStatus.isHuman() && !playerStatus.shouldBeDropped()) {
                ++status.numberOfPlayingHumans;
            }
        }

        this.gameStatus = status;
        gameStatusMessageSender.send(status);
    }

    public synchronized List<PacketPlayerSummary> getPlayerSummaries() {
        List<PacketPlayerSummary> summaries = Lists.newArrayList();
        for (PlayerStatus player : playerStatuses) {
            summaries.add(player.getPlayerSummary());
        }
        return summaries;
    }

    public int getSessionId() {
        // sessionId??????synchronized??
        return sessionId;
    }

    private synchronized void calculateRanking() {
        // ?
        PlayerStatus[] players = playerStatuses.toArray(new PlayerStatus[0]);
        Arrays.sort(players, new Comparator<PlayerStatus>() {
            public int compare(PlayerStatus o1, PlayerStatus o2) {
                return o2.getScore() - o1.getScore();
            }
        });
        for (int i = 0; i < players.length; ++i) {
            players[i].setTempRanking(i + 1);
        }
    }

    public boolean isEvent() {
        // event?????????synchronized??
        return event;
    }

    public boolean isPublicEvent() {
        // publicEvent?????????synchronized??
        return publicEvent;
    }

    public synchronized Set<Integer> getTestingProblemIds() {
        if (getTransition() == Transition.Problem) {
            return ImmutableSet.of(problems.get(problemCounter.get()).id);
        }
        return Sets.newHashSet();
    }
}