/*
* Computoser is a music-composition algorithm and a website to present the results
* Copyright (C) 2012-2014 Bozhidar Bozhanov
*
* Computoser is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Computoser 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Computoser. If not, see <http://www.gnu.org/licenses/>.
*/
package com.music.web.websocket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.google.common.collect.TreeMultimap;
import com.music.MetreConfigurer;
import com.music.model.InstrumentGroups;
import com.music.model.persistent.Piece;
import com.music.model.prefs.UserPreferences;
import com.music.service.PieceService;
import com.music.util.music.InstrumentNameExtractor;
import com.music.web.websocket.dto.Answer;
import com.music.web.websocket.dto.GameResults;
import com.music.web.websocket.dto.Instrument;
import com.music.web.websocket.dto.Metre;
import com.music.web.websocket.dto.PossibleAnswers;
public class Game {
private static final int SECONDS = 60;
public static final int PIECES_PER_GAME = 3;
private static final int ANSWERS_PER_QUESTION = 4;
private Random random = new Random();
private static final UserPreferences prefs = new UserPreferences();
private String id;
private Map<String, Player> players = new ConcurrentHashMap<>();
private Long currentPieceId;
private Answer currentCorrectAnswer;
private List<Piece> pieces = new ArrayList<>(); // only the host fills this collection, no need for it to be concurrent
private boolean started;
private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
private PieceService pieceService;
private Future<?> nextExecution;
private Map<Piece, PossibleAnswers> possibleAnswersHistory = new LinkedHashMap<>(); //no need to be concurrent
private GameResults results = new GameResults();
public static final Answer DUMMY_ANSWER = new Answer();
private int secondsBeforeNextPiece = 7;
public Game(String id, PieceService pieceService) {
this.id = id;
this.pieceService = pieceService;
}
public void start() {
started = true;
for (Player player : players.values()) {
player.gameStarted();
}
sendNextPiece();
}
public void playerHasAnswered() {
// if all players have provided answers for all pieces so far, proceed
for (Player player : players.values()) {
if (player.getAnswers().size() < pieces.size()) {
return;
}
}
if (nextExecution.cancel(false)) {
scheduleNext(secondsBeforeNextPiece); // schedule next in X seconds, so that players have the time to see the correct answer
}
}
private void scheduleNext(int seconds) {
nextExecution = executor.schedule(new Runnable() {
@Override
public void run() {
sendNextPiece();
}
}, seconds, TimeUnit.SECONDS);
}
public void sendNextPiece() {
// first send blank answers for players who haven't answered (checked in a thread-safe manner in the Player class)
if (pieces.size() > 0) {
for (Player player : players.values()) {
player.answer(this, DUMMY_ANSWER);
}
}
// if this has been the last question, end the game
if (pieces.size() >= PIECES_PER_GAME) {
stop();
return;
}
Long pieceId = pieceService.getNextPieceId(prefs);
Piece piece = pieceService.getPiece(pieceId);
pieces.add(piece);
currentPieceId = pieceId;
fillCurrentCorrectAnswer(piece);
PossibleAnswers possibleAnswers = generatePossibleAnswers(piece);
possibleAnswersHistory.put(piece, possibleAnswers);
for (Player player : players.values()) {
player.sendNextPiece(pieceId, possibleAnswers, SECONDS);
}
scheduleNext(SECONDS + 1); //+1 sec to account for latency
}
private void fillCurrentCorrectAnswer(Piece piece) {
currentCorrectAnswer = new Answer();
currentCorrectAnswer.setMainInstrument(piece.getMainInstrument());
currentCorrectAnswer.setMetreNumerator(piece.getMetreNumerator());
currentCorrectAnswer.setMetreDenominator(piece.getMetreDenominator());
currentCorrectAnswer.setTempo(piece.getTempo());
}
private void stop() {
nextExecution.cancel(false);
started = false;
calculateResults();
for (Player player : players.values()) {
player.gameFinished(this.results, this);
}
}
private void calculateResults() {
TreeMultimap<Integer, String> rankings = TreeMultimap.create();
for (Player player : players.values()) {
int score = 0;
List<Answer> playerAnswers = new ArrayList<>();
//cannot simply copy the values() of player.getAnswers(), because it is an unordered map (as it needs to be concurrent)
for (Piece piece : pieces) {
Answer answer = player.getAnswers().get(piece.getId());
if (answer.getTempo() > -1) {
int diff = Math.abs(answer.getTempo() - piece.getTempo());
if (diff < 3) {
score += 15;
} else {
score += 5 / Math.log10(diff);
}
}
if (answer.getMainInstrument() == piece.getMainInstrument()) {
score += 10;
}
if (answer.getMetreNumerator() == piece.getMetreNumerator() && answer.getMetreDenominator() == piece.getMetreDenominator()) {
score += 10;
}
playerAnswers.add(answer);
}
results.getScores().put(player.getName(), score);
rankings.put(score, player.getSession().getId());
}
// the ordered player ids
results.setRanking(new ArrayList<>(rankings.values()));
Collections.reverse(results.getRanking());
}
public boolean playerJoined(Player player) {
for (Player otherPlayer : players.values()) {
if (otherPlayer.getName().equals(player.getName())) {
return false;
}
}
for (Player otherPlayer : players.values()) {
otherPlayer.playerJoined(player);
player.playerJoined(otherPlayer); // let the newly joined player know of all the others
}
players.put(player.getSession().getId(), player);
return true;
}
public void playerLeft(String playerId) {
players.remove(playerId);
for (Player player : players.values()) {
if (!player.getSession().getId().equals(playerId)) {
player.playerLeft(playerId);
}
}
}
private PossibleAnswers generatePossibleAnswers(Piece piece) {
PossibleAnswers answers = new PossibleAnswers();
// first select the instrument distractors
int instrument = piece.getMainInstrument();
List<Instrument> instruments = new ArrayList<>(ANSWERS_PER_QUESTION);
instruments.add(new Instrument(instrument, InstrumentNameExtractor.getInstrumentName(instrument)));
while (instruments.size() < ANSWERS_PER_QUESTION) {
int[] set = random.nextBoolean() ? InstrumentGroups.MAIN_PART_INSTRUMENTS : InstrumentGroups.MAIN_PART_ONLY_INSTRUMENTS;
int selectedId = set[random.nextInt(set.length)];
Instrument newInstrument = new Instrument(selectedId, InstrumentNameExtractor.getInstrumentName(selectedId));
if (selectedId != instrument && !instruments.contains(newInstrument)) {
instruments.add(newInstrument);
}
}
Collections.shuffle(instruments, random);
answers.setInstruments(instruments);
// fill the metre distractors
List<Metre> metres = new ArrayList<>(ANSWERS_PER_QUESTION);
metres.add(new Metre(piece.getMetreNumerator(), piece.getMetreDenominator()));
while (metres.size() < ANSWERS_PER_QUESTION) {
int[] metre = MetreConfigurer.getRandomMetre(random);
// disallow metres that have the same ratios as the correct answer
Metre newMetre = new Metre(metre[0], metre[1]);
if (!metres.contains(newMetre) && isNotDerivedMetre(piece, metre)) {
metres.add(newMetre);
}
}
Collections.shuffle(metres, random);
answers.setMetres(metres);
return answers;
}
public boolean isNotDerivedMetre(Piece piece, int[] metre) {
return ((double) metre[0]) / piece.getMetreNumerator() != ((double) metre[1]) / piece.getMetreDenominator();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Map<String, Player> getPlayers() {
return players;
}
public void setPlayers(Map<String, Player> players) {
this.players = players;
}
public Long getCurrentPieceId() {
return currentPieceId;
}
public void setCurrentPieceId(Long currentPieceId) {
this.currentPieceId = currentPieceId;
}
public boolean isStarted() {
return started;
}
public void setStarted(boolean started) {
this.started = started;
}
public Answer getCurrentCorrectAnswer() {
return currentCorrectAnswer;
}
public GameResults getResults() {
return results;
}
public ScheduledExecutorService getExecutor() {
return executor;
}
public void setSecondsBeforeNextPiece(int secondsBeforeNextPiece) {
this.secondsBeforeNextPiece = secondsBeforeNextPiece;
}
}