diff --git a/api/src/main/java/se/cygni/paintbot/eventapi/history/GameHistory.java b/api/src/main/java/se/cygni/paintbot/eventapi/history/GameHistory.java index 1eabc06..62c996c 100644 --- a/api/src/main/java/se/cygni/paintbot/eventapi/history/GameHistory.java +++ b/api/src/main/java/se/cygni/paintbot/eventapi/history/GameHistory.java @@ -22,16 +22,20 @@ public class GameHistory extends ApiMessage { private final List messages; + private final boolean isTrainingGame; @JsonCreator public GameHistory( @JsonProperty("gameId") String gameId, @JsonProperty("playerNames") String[] playerNames, @JsonProperty("gameDate") LocalDateTime gameDate, - @JsonProperty("messages") List messages) { + @JsonProperty("messages") List messages, + @JsonProperty("isTrainingGame") boolean isTrainingGame) { + this.gameId = gameId; this.playerNames = playerNames; this.gameDate = gameDate; this.messages = messages; + this.isTrainingGame = isTrainingGame; } public String getGameId() { @@ -51,4 +55,8 @@ public LocalDateTime getGameDate() { public List getMessages() { return messages; } + + public boolean isTrainingGame() { + return isTrainingGame; + } } diff --git a/api/src/test/java/se/cygni/paintbot/eventapi/history/GameHistoryTest.java b/api/src/test/java/se/cygni/paintbot/eventapi/history/GameHistoryTest.java index 91f90ca..613825b 100644 --- a/api/src/test/java/se/cygni/paintbot/eventapi/history/GameHistoryTest.java +++ b/api/src/test/java/se/cygni/paintbot/eventapi/history/GameHistoryTest.java @@ -26,7 +26,7 @@ public void testSerialization() throws Exception { List gameMessages = new ArrayList<>(); gameMessages.add(new GameStartingEvent(gameId, 3, 46, 34, new GameSettings())); - GameHistory gh = new GameHistory(gameId, players, now, gameMessages); + GameHistory gh = new GameHistory(gameId, players, now, gameMessages, false); String msg = ApiMessageParser.encodeMessage(gh); System.out.println(msg); diff --git a/app/docs/elasticsearch.md b/app/docs/elasticsearch.md index 2e9e5b5..e62ba35 100644 --- a/app/docs/elasticsearch.md +++ b/app/docs/elasticsearch.md @@ -7,8 +7,7 @@ each field and if there are any special index options. Generally for String types we have choosen the index: "not_analyzed". This means that words will not be split. -There are three indexes in Elasticsearch: clientinfo, gamehistory and -gameevent. +There are three indexes in Elasticsearch: clientinfo, tournamenthistory, tournamentevent, gamehistory and gameevent. Since it is not possible to change the mapping after it has been created it is important to define this before the server starts storing data. diff --git a/app/src/main/java/se/cygni/paintbot/event/InternalGameEvent.java b/app/src/main/java/se/cygni/paintbot/event/InternalGameEvent.java index fd4f070..0e6dbf5 100644 --- a/app/src/main/java/se/cygni/paintbot/event/InternalGameEvent.java +++ b/app/src/main/java/se/cygni/paintbot/event/InternalGameEvent.java @@ -7,13 +7,17 @@ public class InternalGameEvent { private final long tstamp; private GameMessage gameMessage; - public InternalGameEvent(long tstamp) { + private final boolean isTraining; + + public InternalGameEvent(long tstamp, boolean isTraining) { this.tstamp = tstamp; + this.isTraining = isTraining; } - public InternalGameEvent(long tstamp, GameMessage gameMessage) { + public InternalGameEvent(long tstamp, GameMessage gameMessage, boolean isTraining) { this.tstamp = tstamp; this.gameMessage = gameMessage; + this.isTraining = isTraining; } public long getTstamp() { @@ -31,4 +35,8 @@ public void onGameAborted(String gameId) { public void onGameChanged(String gameId) { this.gameMessage = GameMessageConverter.onGameChanged(gameId); } + + public boolean isTraining() { + return isTraining; + } } diff --git a/app/src/main/java/se/cygni/paintbot/game/Game.java b/app/src/main/java/se/cygni/paintbot/game/Game.java index 9014a8c..aefc5f3 100644 --- a/app/src/main/java/se/cygni/paintbot/game/Game.java +++ b/app/src/main/java/se/cygni/paintbot/game/Game.java @@ -196,6 +196,10 @@ public boolean isEnded() { return gameEngine.isGameComplete(); } + public boolean isTrainingGame() { + return trainingGame; + } + public GameResult getGameResult() { return gameEngine.getGameResult(); } @@ -233,14 +237,14 @@ public void abort() { playerManager.clear(); gameEngine.abort(); - InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis()); + InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis(), this.trainingGame); gevent.onGameAborted(getGameId()); globalEventBus.post(gevent); globalEventBus.post(gevent.getGameMessage()); } public void publishGameChanged() { - InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis()); + InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis(), this.trainingGame); gevent.onGameChanged(getGameId()); globalEventBus.post(gevent); } diff --git a/app/src/main/java/se/cygni/paintbot/game/GameEngine.java b/app/src/main/java/se/cygni/paintbot/game/GameEngine.java index f535b7d..1c2797c 100644 --- a/app/src/main/java/se/cygni/paintbot/game/GameEngine.java +++ b/app/src/main/java/se/cygni/paintbot/game/GameEngine.java @@ -129,7 +129,8 @@ private void notifyPlayers(Set players, GameMessage message) { InternalGameEvent gevent = new InternalGameEvent( System.currentTimeMillis(), - message); + message, + this.gameFeatures.isTrainingGame()); globalEventBus.post(gevent); } @@ -304,7 +305,7 @@ public GameResult getGameResult() { } public void publishGameChanged() { - InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis()); + InternalGameEvent gevent = new InternalGameEvent(System.currentTimeMillis(), this.gameFeatures.isTrainingGame()); gevent.onGameChanged(gameId); globalEventBus.post(gevent); } diff --git a/app/src/main/java/se/cygni/paintbot/game/GameManager.java b/app/src/main/java/se/cygni/paintbot/game/GameManager.java index 97a3085..de11d0c 100644 --- a/app/src/main/java/se/cygni/paintbot/game/GameManager.java +++ b/app/src/main/java/se/cygni/paintbot/game/GameManager.java @@ -95,7 +95,8 @@ private void registerGame(Game game) { log.info("Registered new game, posting to GlobalEventBus..."); globalEventBus.post(new InternalGameEvent( System.currentTimeMillis(), - new GameCreatedEvent(game.getGameId()))); + new GameCreatedEvent(game.getGameId()), + game.isTrainingGame())); } @Subscribe diff --git a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryCache.java b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryCache.java index 92d5f64..519b558 100644 --- a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryCache.java +++ b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryCache.java @@ -90,7 +90,18 @@ private GameHistory getGameHistory(String gameId) { gameId, getPlayersForGame(gameId), extractGameDate(gameId), - getGameMessagesForGame(gameId)); + getGameMessagesForGame(gameId), + isTrainingGame(gameId)); + } + + private boolean isTrainingGame(String gameId) { + Optional firstMapUpdate = getFirstInternalGameEvent(gameId); + + if(firstMapUpdate.isPresent()) { + return firstMapUpdate.get().isTraining(); + } + + return false; } private List getGameMessagesForGame(String gameId) { diff --git a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageElastic.java b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageElastic.java index c5c8309..f154b1b 100644 --- a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageElastic.java +++ b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageElastic.java @@ -45,6 +45,18 @@ public class GameHistoryStorageElastic implements GameHistoryStorage { @Value("${paintbot.elastic.gamehistory.type}") private String gameHistoryType; + @Value("${paintbot.elastic.tournamenthistory.index}") + private String tournamentHistoryIndex; + + @Value("${paintbot.elastic.tournamenthistory.type}") + private String tournamentHistoryType; + + @Value("${paintbot.elastic.tournamentevent.index}") + private String tournamentEventIndex; + + @Value("${paintbot.elastic.tournamentevent.type}") + private String tournamentEventType; + @Value("${paintbot.elastic.gameevent.index}") private String gameEventIndex; @@ -71,7 +83,14 @@ public void addGameHistory(GameHistory gameHistory) { String eventId = UUID.randomUUID().toString(); try { - IndexRequest indexRequest = new IndexRequest(gameEventIndex, gameEventType, eventId); + IndexRequest indexRequest; + + if (gameHistory.isTrainingGame()) { + indexRequest = new IndexRequest(gameEventIndex, gameEventType, eventId); + } else { + indexRequest = new IndexRequest(tournamentEventIndex, tournamentEventType, eventId); + } + String msg = GameMessageParser.encodeMessage(gameMessage); indexRequest.source(msg, XContentType.JSON); elasticClient.index(indexRequest); @@ -83,10 +102,17 @@ public void addGameHistory(GameHistory gameHistory) { GameHistoryPersisted ghp = new GameHistoryPersisted( gameHistory.getGameId(), gameHistory.getPlayerNames(), - gameHistory.getGameDate() + gameHistory.getGameDate(), + gameHistory.isTrainingGame() ); - IndexRequest indexRequest = new IndexRequest(gameHistoryIndex, gameHistoryType, gameHistory.getGameId()); + IndexRequest indexRequest; + if (gameHistory.isTrainingGame()) { + indexRequest = new IndexRequest(gameHistoryIndex, gameHistoryType, gameHistory.getGameId()); + } else { + indexRequest = new IndexRequest(tournamentHistoryIndex, tournamentHistoryType, gameHistory.getGameId()); + } + String msg = ApiMessageParser.encodeMessage(ghp); indexRequest.source(msg, XContentType.JSON); elasticClient.index(indexRequest); @@ -104,6 +130,16 @@ public Optional getGameHistory(String gameId) { try { SearchResponse esResponse = elasticClient.search(searchRequest); + + if (esResponse.getHits().totalHits == 0) { + searchRequest = new SearchRequest(tournamentHistoryIndex); + searchSourceBuilder = new SearchSourceBuilder() + .query(QueryBuilders.idsQuery(tournamentHistoryType).addIds(gameId)); + searchRequest.source(searchSourceBuilder); + + esResponse = elasticClient.search(searchRequest); + } + if (esResponse.getHits().totalHits > 0) { GameHistoryPersisted ghp = (GameHistoryPersisted) ApiMessageParser.decodeMessage(esResponse.getHits().getAt(0).getSourceAsString()); List gameMessages = getGameEventsForGame(gameId); @@ -112,7 +148,8 @@ public Optional getGameHistory(String gameId) { ghp.getGameId(), ghp.getPlayerNames(), ghp.getGameDate(), - gameMessages + gameMessages, + ghp.isTrainingGame() ); return Optional.of(gameHistory); @@ -143,6 +180,21 @@ private List getGameEventsForGame(String gameId) { return messages; } + if (scrollResp.getHits().totalHits == 0) { + searchRequest = new SearchRequest(tournamentEventIndex) + .scroll(new TimeValue(60000)); + searchSourceBuilder = new SearchSourceBuilder() + .query(qb) + .size(200); + searchRequest.source(searchSourceBuilder); + + try { + scrollResp = elasticClient.search(searchRequest); + } catch (IOException e) { + log.error("Failed to search ElasticSearch"); + return messages; + } + } //Scroll until no hits are returned while (true) { @@ -198,6 +250,12 @@ private GameHistorySearchResult getGameHistorySearchResult(SearchSourceBuilder s List items = new ArrayList<>(); try { SearchResponse esResponse = elasticClient.search(searchRequest); + if (esResponse.getHits().totalHits == 0) { + searchRequest = new SearchRequest(tournamentHistoryIndex); + searchRequest.source(searchSourceBuilder); + + esResponse = elasticClient.search(searchRequest); + } Iterator searchHitIterator = esResponse.getHits().iterator(); int counter = 0; diff --git a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageInMemory.java b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageInMemory.java index 7170b0f..153b42e 100644 --- a/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageInMemory.java +++ b/app/src/main/java/se/cygni/paintbot/persistence/history/GameHistoryStorageInMemory.java @@ -30,6 +30,7 @@ public class GameHistoryStorageInMemory implements GameHistoryStorage { private final EventBus eventBus; private List gameHistories = Collections.synchronizedList(new ArrayList<>()); + private List tournamentHistories = Collections.synchronizedList(new ArrayList<>()); @Autowired public GameHistoryStorageInMemory(EventBus eventBus) { @@ -43,15 +44,28 @@ public GameHistoryStorageInMemory(EventBus eventBus) { @Subscribe public void addGameHistory(GameHistory gameHistory) { log.debug("Adding GameHistory to memory!"); - gameHistories.add(gameHistory); + if (gameHistory.isTrainingGame()) { + gameHistories.add(gameHistory); + } else { + tournamentHistories.add(gameHistory); + } } @Override public Optional getGameHistory(String gameId) { - return gameHistories + Optional gh = gameHistories .stream() .filter(gameHistory -> gameHistory.getGameId().equals(gameId)) .findFirst(); + + if (gh.isEmpty()) { + return tournamentHistories + .stream() + .filter(gameHistory -> gameHistory.getGameId().equals(gameId)) + .findFirst(); + } + + return gh; } @Override @@ -70,6 +84,20 @@ public GameHistorySearchResult listGamesWithPlayer(String playerName) { }) .collect(Collectors.toList()); + if (items.isEmpty()) { + items = tournamentHistories + .stream() + .filter(gameHistory -> ArrayUtils.contains(gameHistory.getPlayerNames(), playerName)) + .map(gameHistory -> { + return new GameHistorySearchItem( + gameHistory.getGameId(), + gameHistory.getPlayerNames(), + gameHistory.getGameDate() + ); + }) + .collect(Collectors.toList()); + } + result.setItems(items); return result; } @@ -83,6 +111,17 @@ public GameHistorySearchResult getHistoricGames() { gameHistory.getPlayerNames(), gameHistory.getGameDate())) .collect(Collectors.toList())); + + if (games.getItems().isEmpty()) { + games = new GameHistorySearchResult( + tournamentHistories.stream() + .map(gameHistory -> new GameHistorySearchItem( + gameHistory.getGameId(), + gameHistory.getPlayerNames(), + gameHistory.getGameDate())) + .collect(Collectors.toList())); + } + return games; } @@ -92,6 +131,11 @@ private void removeOldGames() { GameHistory gameHistory = gameHistories.remove(0); log.debug("Removed gameId: {}", gameHistory.getGameId()); } + + while (tournamentHistories.size() > MAX_NOOF_GAMES_IN_MEMORY - 1) { + GameHistory gameHistory = tournamentHistories.remove(0); + log.debug("Removed gameId: {}", gameHistory.getGameId()); + } } } diff --git a/app/src/main/java/se/cygni/paintbot/persistence/history/domain/GameHistoryPersisted.java b/app/src/main/java/se/cygni/paintbot/persistence/history/domain/GameHistoryPersisted.java index 0ecc8ac..63c9568 100644 --- a/app/src/main/java/se/cygni/paintbot/persistence/history/domain/GameHistoryPersisted.java +++ b/app/src/main/java/se/cygni/paintbot/persistence/history/domain/GameHistoryPersisted.java @@ -18,15 +18,18 @@ public class GameHistoryPersisted extends ApiMessage { private final LocalDateTime gameDate; + private final boolean isTrainingGame; @JsonCreator public GameHistoryPersisted( @JsonProperty("gameId") String gameId, @JsonProperty("playerNames") String[] playerNames, - @JsonProperty("gameDate") LocalDateTime gameDate) { + @JsonProperty("gameDate") LocalDateTime gameDate, + @JsonProperty("isTrainingGame") boolean isTrainingGame) { this.gameId = gameId; this.playerNames = playerNames; this.gameDate = gameDate; + this.isTrainingGame = isTrainingGame; } public String getGameId() { @@ -42,4 +45,8 @@ public String[] getPlayerNames() { public LocalDateTime getGameDate() { return gameDate; } + + public boolean isTrainingGame() { + return isTrainingGame; + } } diff --git a/app/src/main/java/se/cygni/paintbot/tournament/TournamentManager.java b/app/src/main/java/se/cygni/paintbot/tournament/TournamentManager.java index 053697f..e2ebbd0 100644 --- a/app/src/main/java/se/cygni/paintbot/tournament/TournamentManager.java +++ b/app/src/main/java/se/cygni/paintbot/tournament/TournamentManager.java @@ -146,7 +146,7 @@ private void completeTournament() { InternalGameEvent gevent = new InternalGameEvent( System.currentTimeMillis(), - tee); + tee, false); globalEventBus.post(gevent); globalEventBus.post(gevent.getGameMessage()); } diff --git a/app/src/main/resources/application-production.properties b/app/src/main/resources/application-production.properties index 2c88cf9..99e24a1 100644 --- a/app/src/main/resources/application-production.properties +++ b/app/src/main/resources/application-production.properties @@ -8,5 +8,11 @@ paintbot.elastic.gamehistory.type=game paintbot.elastic.gameevent.index=gameevent paintbot.elastic.gameevent.type=event +paintbot.elastic.tournamenthistory.index=tournamenthistory +paintbot.elastic.tournamenthistory.type=tournament + +paintbot.elastic.tournamentevent.index=tournamentevent +paintbot.elastic.tournamentevent.type=event + paintbot.elastic.clientinfo.index=clientinfo paintbot.elastic.clientinfo.type=client diff --git a/app/src/main/resources/tournamentevent_mapping.json b/app/src/main/resources/tournamentevent_mapping.json new file mode 100644 index 0000000..f18cb78 --- /dev/null +++ b/app/src/main/resources/tournamentevent_mapping.json @@ -0,0 +1,154 @@ +{ + "mappings": { + "event": { + "properties": { + "deathReason": { + "type": "string", + "index": "not_analyzed" + }, + "gameId": { + "type": "string", + "index": "not_analyzed" + }, + "gameSettings": { + "properties": { + "addFoodLikelihood": { + "type": "long" + }, + "foodEnabled": { + "type": "boolean" + }, + "gameDurationInSeconds": { + "type": "long" + }, + "headToTailConsumes": { + "type": "boolean" + }, + "maxNoofPlayers": { + "type": "long" + }, + "noofRoundsTailProtectedAfterNibble": { + "type": "long" + }, + "obstaclesEnabled": { + "type": "boolean" + }, + "pointsPerCausedDeath": { + "type": "long" + }, + "pointsPerFood": { + "type": "long" + }, + "pointsPerLength": { + "type": "long" + }, + "pointsPerNibble": { + "type": "long" + }, + "pointsPerTick": { + "type": "boolean" + }, + "removeFoodLikelihood": { + "type": "long" + }, + "spontaneousGrowthEveryNWorldTick": { + "type": "long" + }, + "startFood": { + "type": "long" + }, + "startObstacles": { + "type": "long" + }, + "startSnakeLength": { + "type": "long" + }, + "tailConsumeGrows": { + "type": "boolean" + }, + "timeInMsPerTick": { + "type": "long" + }, + "trainingGame": { + "type": "boolean" + } + } + }, + "gameTick": { + "type": "long" + }, + "height": { + "type": "long" + }, + "map": { + "properties": { + "foodPositions": { + "type": "long" + }, + "height": { + "type": "long" + }, + "obstaclePositions": { + "type": "long" + }, + "paintbotInfos": { + "properties": { + "id": { + "type": "string", + "index": "not_analyzed" + }, + "name": { + "type": "string", + "index": "not_analyzed" + }, + "points": { + "type": "long" + }, + "positions": { + "type": "long" + }, + "tailProtectedForGameTicks": { + "type": "long" + } + } + }, + "width": { + "type": "long" + }, + "worldTick": { + "type": "long" + } + } + }, + "noofPlayers": { + "type": "long" + }, + "playerId": { + "type": "string", + "index": "not_analyzed" + }, + "playerWinnerId": { + "type": "string", + "index": "not_analyzed" + }, + "timestamp": { + "type": "date", + "format": "epoch_millis" + }, + "type": { + "type": "string", + "index": "not_analyzed" + }, + "width": { + "type": "long" + }, + "x": { + "type": "long" + }, + "y": { + "type": "long" + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/resources/tournamenthistory_mapping.json b/app/src/main/resources/tournamenthistory_mapping.json new file mode 100644 index 0000000..615a4f1 --- /dev/null +++ b/app/src/main/resources/tournamenthistory_mapping.json @@ -0,0 +1,28 @@ +{ + "mappings": { + "game": { + "properties": { + "eventIds": { + "type": "string", + "index": "not_analyzed" + }, + "gameDate": { + "type": "date", + "format": "yyy-MM-dd HH:mm:ss.SSS||yyyy-MM-dd||strict_date_optional_time||epoch_millis" + }, + "gameId": { + "type": "string", + "index": "not_analyzed" + }, + "playerNames": { + "type": "string", + "index": "not_analyzed" + }, + "type": { + "type": "string", + "index": "not_analyzed" + } + } + } + } +} \ No newline at end of file