Display round permalinks after the round is over, if enabled.

This takes advantage of the metrics logging, the metrics processor, and the metrics viewer to provide a permalink to a particular round, instead of requiring players to take a screenshot of the round. This will not be enabled immediately as the viewer is not quite ready for deployment, but this is all that needs done on the game server to support this, so it can just be dynamically enabled when it's ready.
This commit is contained in:
Andy Janata 2018-04-04 16:09:17 -07:00
parent 88ab1ac640
commit 99958c0dcf
9 changed files with 115 additions and 21 deletions

View File

@ -358,6 +358,7 @@ cah.$.LongPollResponse.SIGIL = "?";
cah.$.LongPollResponse.EMOTE = "me";
cah.$.LongPollResponse.CARDCAST_DECK_INFO = "cdi";
cah.$.LongPollResponse.GAME_ID = "gid";
cah.$.LongPollResponse.ROUND_PERMALINK = "rP";
cah.$.LongPollResponse.NICKNAME = "n";
cah.$.LongPollResponse.BLACK_CARD = "bc";
cah.$.LongPollResponse.GAME_STATE = "gs";

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, Andy Janata
* Copyright (c) 2012-2018, Andy Janata
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
@ -988,8 +988,13 @@ cah.Game.prototype.roundComplete = function(data) {
var scoreCard = this.scoreCards_[roundWinner];
$(scoreCard.getElement()).addClass("selected");
$(".confirm_card", this.element_).attr("disabled", "disabled");
cah.log.status_with_game(this, roundWinner + " wins the round. The next round will begin in "
+ (data[cah.$.LongPollResponse.INTERMISSION] / 1000) + " seconds.");
var msg = roundWinner + " wins the round. The next round will begin in "
+ (data[cah.$.LongPollResponse.INTERMISSION] / 1000) + " seconds.";
if (cah.$.LongPollResponse.ROUND_PERMALINK in data) {
msg = msg + " <a href='" + data[cah.$.LongPollResponse.ROUND_PERMALINK]
+ "' rel='noopener' target='_blank'>Permalink</a>";
}
cah.log.status_with_game(this, msg, undefined, true);
// update the previous round display
$(".game_last_round_winner", this.element_).text(roundWinner);

View File

@ -86,6 +86,16 @@ hibernate.cache.use_second_level_cache=false
hibernate.cache.use_query_cache=false
hibernate.cache.provider_class=org.hibernate.cache.NoCacheProvider
# If the server should send round IDs to clients after the round is over, and if the client should
# display a permalink to them. You must be using the Kafka metrics implementation, and have
# pyx-metrics-processor running somewhere to put the data into a database, and have
# pyx-metrics-viewer running somewhere connected to that database. The URL to the viewer is provided
# below. If you don't know what any of that is, you certainly don't have it running, so leave this
# set to false.
pyx.metrics.round.enabled=false
# Format string to the URL to view a previous round. Must contain exactly one %s which will be
# replaced with the round's ID.
pyx.metrics.round.url_format=http://localhost:4080/static/round.html#%s
# Metrics implementation.
pyx.metrics.impl=net.socialgamer.cah.metrics.NoOpMetrics

View File

@ -22,6 +22,8 @@ pyx.chat.game.flood_count=${pyx.game.flood_count}
pyx.chat.game.flood_time=${pyx.game.flood_time}
pyx.build=${buildNumber}
pyx.metrics.round.enabled=${pyx.metrics.round.enabled}
pyx.metrics.round.url_format=${pyx.metrics.round.url_format}
# this is NOT allowed to be changed during a reload, as metrics depend on previous events
pyx.metrics.impl=${pyx.metrics.impl}

View File

@ -169,6 +169,22 @@ public class CahModule extends AbstractModule {
}
}
@Provides
@ShowRoundPermalink
Boolean provideShowRoundPermalink() {
synchronized (properties) {
return Boolean.valueOf(properties.getProperty("pyx.metrics.round.enabled", "false"));
}
}
@Provides
@RoundPermalinkUrlFormat
String provideRoundPermalinkUrlFormat() {
synchronized (properties) {
return properties.getProperty("pyx.metrics.round.url_format", "about:blank#%s");
}
}
@Provides
@InsecureIdAllowed
Boolean provideInsecureIdAllowed() {
@ -235,6 +251,16 @@ public class CahModule extends AbstractModule {
public @interface MaxUsers {
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface ShowRoundPermalink {
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface RoundPermalinkUrlFormat {
}
@BindingAnnotation
@Retention(RetentionPolicy.RUNTIME)
public @interface BroadcastConnectsAndDisconnects {

View File

@ -561,6 +561,7 @@ public class Constants {
* Reason why a player disconnected.
*/
REASON("qr"),
ROUND_PERMALINK("rP"),
ROUND_WINNER("rw"),
/**
* Sigil to display next to user's name.

View File

@ -48,6 +48,8 @@ import org.hibernate.Session;
import com.google.inject.Inject;
import com.google.inject.Provider;
import net.socialgamer.cah.CahModule.RoundPermalinkUrlFormat;
import net.socialgamer.cah.CahModule.ShowRoundPermalink;
import net.socialgamer.cah.CahModule.UniqueId;
import net.socialgamer.cah.Constants.BlackCardData;
import net.socialgamer.cah.Constants.ErrorCode;
@ -112,6 +114,8 @@ public class Game {
private final GameOptions options = new GameOptions();
private final Set<String> cardcastDeckIds = Collections.synchronizedSet(new HashSet<String>());
private final Metrics metrics;
private final Provider<Boolean> showRoundLinkProvider;
private final Provider<String> roundPermalinkFormatProvider;
private final long created = System.currentTimeMillis();
private int judgeIndex = 0;
@ -204,7 +208,8 @@ public class Game {
final Provider<Session> sessionProvider,
final Provider<CardcastService> cardcastServiceProvider,
@UniqueId final Provider<String> uniqueIdProvider,
final Metrics metrics) {
final Metrics metrics, @ShowRoundPermalink final Provider<Boolean> showRoundLinkProvider,
@RoundPermalinkUrlFormat final Provider<String> roundPermalinkFormatProvider) {
this.id = id;
this.connectedUsers = connectedUsers;
this.gameManager = gameManager;
@ -213,6 +218,8 @@ public class Game {
this.cardcastServiceProvider = cardcastServiceProvider;
this.uniqueIdProvider = uniqueIdProvider;
this.metrics = metrics;
this.showRoundLinkProvider = showRoundLinkProvider;
this.roundPermalinkFormatProvider = roundPermalinkFormatProvider;
state = GameState.LOBBY;
}
@ -1489,11 +1496,16 @@ public class Game {
}
final int clientCardId = playedCards.getCards(cardPlayer).get(0).getId();
final String roundId = uniqueIdProvider.get();
final HashMap<ReturnableData, Object> data = getEventMap();
data.put(LongPollResponse.EVENT, LongPollEvent.GAME_ROUND_COMPLETE.toString());
data.put(LongPollResponse.ROUND_WINNER, cardPlayer.getUser().getNickname());
data.put(LongPollResponse.WINNING_CARD, clientCardId);
data.put(LongPollResponse.INTERMISSION, ROUND_INTERMISSION);
if (showRoundLinkProvider.get()) {
data.put(LongPollResponse.ROUND_PERMALINK,
String.format(roundPermalinkFormatProvider.get(), roundId));
}
broadcastToPlayers(MessageType.GAME_EVENT, data);
notifyPlayerInfoChange(getJudge());
@ -1523,7 +1535,7 @@ public class Game {
final Map<String, List<WhiteCard>> cardsBySessionId = new HashMap<>();
playedCards.cardsByUser().forEach(
(key, value) -> cardsBySessionId.put(key.getSessionId(), value));
metrics.roundComplete(currentUniqueId, uniqueIdProvider.get(), judge.getSessionId(),
metrics.roundComplete(currentUniqueId, roundId, judge.getSessionId(),
cardPlayer.getUser().getSessionId(), blackCard, cardsBySessionId);
return null;

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) 2012-2017, Andy Janata
* Copyright (c) 2012-2018, Andy Janata
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted
@ -39,15 +39,6 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import net.socialgamer.cah.CahModule.UniqueId;
import net.socialgamer.cah.HibernateUtil;
import net.socialgamer.cah.cardcast.CardcastModule.CardcastCardId;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.GameManager.MaxGames;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.metrics.Metrics;
import net.socialgamer.cah.metrics.NoOpMetrics;
import org.hibernate.Session;
import org.junit.After;
import org.junit.Before;
@ -56,8 +47,20 @@ import org.junit.Test;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Provides;
import net.socialgamer.cah.CahModule.RoundPermalinkUrlFormat;
import net.socialgamer.cah.CahModule.ShowRoundPermalink;
import net.socialgamer.cah.CahModule.UniqueId;
import net.socialgamer.cah.HibernateUtil;
import net.socialgamer.cah.cardcast.CardcastModule.CardcastCardId;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.GameManager.MaxGames;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.metrics.Metrics;
import net.socialgamer.cah.metrics.NoOpMetrics;
/**
* Tests for {@code GameManager}.
@ -73,6 +76,18 @@ public class GameManagerTest {
private int gameId;
private final ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(1);
private Metrics metricsMock;
private final Provider<Boolean> falseProvider = new Provider<Boolean>() {
@Override
public Boolean get() {
return Boolean.FALSE;
}
};
private final Provider<String> formatProvider = new Provider<String>() {
@Override
public String get() {
return "%s";
}
};
@Before
public void setUp() throws Exception {
@ -100,6 +115,8 @@ public class GameManagerTest {
});
bind(ScheduledThreadPoolExecutor.class).toInstance(threadPool);
bind(Metrics.class).to(NoOpMetrics.class);
bind(Boolean.class).annotatedWith(ShowRoundPermalink.class).toProvider(falseProvider);
bind(String.class).annotatedWith(RoundPermalinkUrlFormat.class).toProvider(formatProvider);
}
@Provides
@ -149,13 +166,16 @@ public class GameManagerTest {
// fill it up with 3 games
assertEquals(0, gameManager.get().intValue());
gameManager.getGames().put(0,
new Game(0, cuMock, gameManager, timer, null, null, null, metricsMock));
new Game(0, cuMock, gameManager, timer, null, null, null, metricsMock, falseProvider,
formatProvider));
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1,
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock));
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock, falseProvider,
formatProvider));
assertEquals(2, gameManager.get().intValue());
gameManager.getGames().put(2,
new Game(2, cuMock, gameManager, timer, null, null, null, metricsMock));
new Game(2, cuMock, gameManager, timer, null, null, null, metricsMock, falseProvider,
formatProvider));
// make sure it says it can't make any more
assertEquals(-1, gameManager.get().intValue());
@ -164,14 +184,16 @@ public class GameManagerTest {
// make sure it re-uses that id
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1,
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock));
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock, falseProvider,
formatProvider));
assertEquals(-1, gameManager.get().intValue());
// remove game 1 out from under it, to make sure it'll fix itself
gameManager.getGames().remove(1);
assertEquals(1, gameManager.get().intValue());
gameManager.getGames().put(1,
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock));
new Game(1, cuMock, gameManager, timer, null, null, null, metricsMock, falseProvider,
formatProvider));
assertEquals(-1, gameManager.get().intValue());
gameManager.destroyGame(2);

View File

@ -41,6 +41,8 @@ import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.junit.Before;
import org.junit.Test;
import com.google.inject.Provider;
import net.socialgamer.cah.data.Game.TooManyPlayersException;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.metrics.Metrics;
@ -58,13 +60,26 @@ public class GameTest {
private GameManager gmMock;
private Metrics metricsMock;
private final ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(1);
private final Provider<Boolean> falseProvider = new Provider<Boolean>() {
@Override
public Boolean get() {
return Boolean.FALSE;
}
};
private final Provider<String> formatProvider = new Provider<String>() {
@Override
public String get() {
return "%s";
}
};
@Before
public void setUp() throws Exception {
cuMock = createMock(ConnectedUsers.class);
gmMock = createMock(GameManager.class);
metricsMock = createMock(Metrics.class);
game = new Game(0, cuMock, gmMock, timer, null, null, null, metricsMock);
game = new Game(0, cuMock, gmMock, timer, null, null, null, metricsMock, falseProvider,
formatProvider);
}
@SuppressWarnings("unchecked")