Java tutorial
//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(); } }