Create practice controller, add skip button

This commit is contained in:
Charles Gould 2017-01-28 19:07:16 -05:00
parent fc849d2b0d
commit 84c47862d5
7 changed files with 261 additions and 77 deletions

View File

@ -24,6 +24,8 @@ public class Destinations {
public static final String PRACTICE_REPORTS = topicDestination("practiceReports"); public static final String PRACTICE_REPORTS = topicDestination("practiceReports");
public static final String PRACTICE_WORD_SKIPPED = topicDestination("practiceWordSkipped");
public static final String SESSION_USERNAME = topicDestination("sessionUsername"); public static final String SESSION_USERNAME = topicDestination("sessionUsername");
public static final String USER_JOINED = topicDestination("userJoined"); public static final String USER_JOINED = topicDestination("userJoined");

View File

@ -9,10 +9,11 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import javax.annotation.PostConstruct;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.MessageMapping;
@ -20,12 +21,8 @@ import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping; import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import lingo.client.api.Destinations; import lingo.client.api.Destinations;
import lingo.common.ChatMessage; import lingo.common.ChatMessage;
@ -36,13 +33,16 @@ import lingo.common.Report;
import lingo.common.SetUsernameMessage; import lingo.common.SetUsernameMessage;
@RestController @RestController
public class LingoController implements ApplicationListener<AbstractSubProtocolEvent> { public class LingoController {
private static final Logger log = LoggerFactory.getLogger(LingoController.class); private static final Logger log = LoggerFactory.getLogger(LingoController.class);
@Autowired @Autowired
private SimpMessagingTemplate messagingTemplate; private SimpMessagingTemplate messagingTemplate;
@Autowired
private SessionManager sessionManager;
@Autowired @Autowired
private WordRepository wordRepo; private WordRepository wordRepo;
@ -50,15 +50,11 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
private final Map<Player, Game> gameByPlayer = new HashMap<>(); private final Map<Player, Game> gameByPlayer = new HashMap<>();
private final Map<Player, Game> practiceByPlayer = new HashMap<>();
private final Map<String, Player> playerBySession = new HashMap<>();
private final Set<String> usernames = new HashSet<>(); private final Set<String> usernames = new HashSet<>();
@MessageMapping("/chat") @MessageMapping("/chat")
public ChatMessage chat(String message, @Header(SESSION_ID_HEADER) String sessionId) { public ChatMessage chat(String message, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
final String username = player.getUsername(); final String username = player.getUsername();
if (username == null) { if (username == null) {
log.warn("No username for session {}", sessionId); log.warn("No username for session {}", sessionId);
@ -74,7 +70,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/guess") @MessageMapping("/guess")
public void guess(String guess, @Header(SESSION_ID_HEADER) String sessionId) { public void guess(String guess, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
final String username = player.getUsername(); final String username = player.getUsername();
if (username == null) { if (username == null) {
log.warn("No username for session {}", sessionId); log.warn("No username for session {}", sessionId);
@ -114,7 +110,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/hostGame") @MessageMapping("/hostGame")
public synchronized void hostGame(@Header(SESSION_ID_HEADER) String sessionId) { public synchronized void hostGame(@Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
final String username = player.getUsername(); final String username = player.getUsername();
if (username == null) { if (username == null) {
log.warn("No username for session {}", sessionId); log.warn("No username for session {}", sessionId);
@ -133,7 +129,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/joinGame") @MessageMapping("/joinGame")
public synchronized void joinGame(Integer gameId, @Header(SESSION_ID_HEADER) String sessionId) { public synchronized void joinGame(Integer gameId, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
final String username = player.getUsername(); final String username = player.getUsername();
if (username == null) { if (username == null) {
log.warn("No username for session {}", sessionId); log.warn("No username for session {}", sessionId);
@ -171,8 +167,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
} }
} }
private synchronized void leave(String sessionId) { private synchronized void leave(Player player) {
final Player player = playerBySession.remove(sessionId);
final String username = player.getUsername(); final String username = player.getUsername();
usernames.remove(username); usernames.remove(username);
final Game game = gameByPlayer.remove(player); final Game game = gameByPlayer.remove(player);
@ -188,7 +183,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/leaveGame") @MessageMapping("/leaveGame")
public synchronized void leaveGame(@Header(SESSION_ID_HEADER) String sessionId) { public synchronized void leaveGame(@Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
final Game game = gameByPlayer.remove(player); final Game game = gameByPlayer.remove(player);
if (game == null) { if (game == null) {
log.warn("{} is not in a game", player); log.warn("{} is not in a game", player);
@ -218,67 +213,14 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
} }
} }
@Override
public void onApplicationEvent(AbstractSubProtocolEvent event) {
if (event instanceof SessionConnectedEvent) {
onSessionConnected((SessionConnectedEvent) event);
} else if (event instanceof SessionDisconnectEvent) {
onSessionDisconnect((SessionDisconnectEvent) event);
}
}
private void onSessionConnected(SessionConnectedEvent event) {
final String sessionId = StompHeaderAccessor.wrap(event.getMessage()).getSessionId();
log.info("Session connected: {}", sessionId);
playerBySession.put(sessionId, new Player(sessionId));
}
private void onSessionDisconnect(SessionDisconnectEvent event) {
final String sessionId = event.getSessionId();
log.info("Session disconnected: {}", sessionId);
leave(sessionId);
}
@SubscribeMapping("/topic/sessionId") @SubscribeMapping("/topic/sessionId")
public String onSessionId(@Header(SESSION_ID_HEADER) String sessionId) { public String onSessionId(@Header(SESSION_ID_HEADER) String sessionId) {
return sessionId; return sessionId;
} }
@MessageMapping("/practiceGame") @PostConstruct
public void practiceGame(@Header(SESSION_ID_HEADER) String sessionId) { public void postConstruct() {
final Player player = playerBySession.get(sessionId); sessionManager.addListener(new PlayerLeftListener());
log.info("{} wants a practice session", sessionId);
final Game game = new Game(player);
game.setAcceptableGuesses(wordRepo.getGuesses());
game.setPossibleWords(wordRepo.getWords());
practiceByPlayer.put(player, game);
final String firstWord = game.newGame();
final String firstLetter = String.valueOf(firstWord.charAt(0));
log.info("First word: {}", firstWord);
sendToPlayer(player, Destinations.PRACTICE_GAME, firstLetter);
}
@MessageMapping("/practiceGuess")
public void practiceGuess(String guess, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId);
guess = guess.toUpperCase();
log.info("{} guessed {}", player, guess);
final Game game = practiceByPlayer.get(player);
final int[] result = game.evaluate(guess);
// Generate report
final Report report = new Report();
report.setGuess(guess);
if (Game.isCorrect(result)) {
final String newWord = game.newWord();
final String firstLetter = String.valueOf(newWord.charAt(0));
log.info("New word: {}", newWord);
report.setCorrect(true);
report.setFirstLetter(firstLetter);
} else {
report.setResult(result);
}
sendToPlayer(player, Destinations.PRACTICE_REPORTS, report);
} }
private void send(String destination, Object payload) { private void send(String destination, Object payload) {
@ -300,12 +242,12 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/setUsername") @MessageMapping("/setUsername")
public synchronized void setUsername(String username, @Header(SESSION_ID_HEADER) String sessionId) { public synchronized void setUsername(String username, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId); final Player player = sessionManager.getPlayer(sessionId);
if (usernames.add(username)) { if (usernames.add(username)) {
player.setUsername(username); player.setUsername(username);
log.info("{} --> {}", sessionId, username); log.info("{} --> {}", sessionId, username);
sendToPlayer(player, Destinations.SESSION_USERNAME, new SetUsernameMessage(true, username, null)); sendToPlayer(player, Destinations.SESSION_USERNAME, new SetUsernameMessage(true, username, null));
send(Destinations.USER_JOINED, new Object[] { username, playerBySession.size() }); send(Destinations.USER_JOINED, new Object[] { username, sessionManager.getPlayerCount() });
} else { } else {
log.warn("{} -/> {} : Username taken", sessionId, username); log.warn("{} -/> {} : Username taken", sessionId, username);
final SetUsernameMessage response = new SetUsernameMessage(false, null, "Username taken"); final SetUsernameMessage response = new SetUsernameMessage(false, null, "Username taken");
@ -313,4 +255,18 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
} }
} }
private class PlayerLeftListener implements SessionManager.Listener {
@Override
public void playerJoined(Player player) {
// Ignore joining players for now
}
@Override
public void playerLeft(Player player) {
leave(player);
}
}
} }

View File

@ -0,0 +1,100 @@
package lingo.server;
import static org.springframework.messaging.simp.SimpMessageHeaderAccessor.SESSION_ID_HEADER;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.RestController;
import lingo.client.api.Destinations;
import lingo.common.Game;
import lingo.common.Player;
import lingo.common.Report;
@RestController
public class PracticeController {
private static final Logger log = LoggerFactory.getLogger(PracticeController.class);
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private SessionManager sessionManager;
@Autowired
private WordRepository wordRepo;
private final Map<Player, Game> practiceByPlayer = new HashMap<>();
@MessageMapping("/practiceGame")
public void practiceGame(@Header(SESSION_ID_HEADER) String sessionId) {
final Player player = sessionManager.getPlayer(sessionId);
log.info("{} is practicing", sessionId);
final Game game = new Game(player);
game.setAcceptableGuesses(wordRepo.getGuesses());
game.setPossibleWords(wordRepo.getWords());
practiceByPlayer.put(player, game);
final String firstWord = game.newGame();
final String firstLetter = String.valueOf(firstWord.charAt(0));
log.info("First word: {}", firstWord);
sendToPlayer(player, Destinations.PRACTICE_GAME, firstLetter);
}
@MessageMapping("/practiceGuess")
public void practiceGuess(String guess, @Header(SESSION_ID_HEADER) String sessionId) {
final Player player = sessionManager.getPlayer(sessionId);
guess = guess.toUpperCase();
log.info("{} guessed {}", player, guess);
final Game game = practiceByPlayer.get(player);
final int[] result = game.evaluate(guess);
// Generate report
final Report report = new Report();
report.setGuess(guess);
if (Game.isCorrect(result)) {
final String newWord = game.newWord();
final String firstLetter = String.valueOf(newWord.charAt(0));
log.info("New word: {}", newWord);
report.setCorrect(true);
report.setFirstLetter(firstLetter);
} else {
report.setResult(result);
}
sendToPlayer(player, Destinations.PRACTICE_REPORTS, report);
}
@MessageMapping("/practiceSkip")
public void practiceSkip(@Header(SESSION_ID_HEADER) String sessionId) {
final Player player = sessionManager.getPlayer(sessionId);
final Game game = practiceByPlayer.get(player);
final String newWord = game.newWord();
final String firstLetter = String.valueOf(newWord.charAt(0));
log.info("New word: {}", newWord);
sendToPlayer(player, Destinations.PRACTICE_WORD_SKIPPED, firstLetter);
}
private void sendToPlayer(Player player, String destination, Object payload) {
sendToSession(player.getSessionId(), destination, payload);
}
private void sendToSession(String sessionId, String destination, Object payload) {
// TODO: cache the headers?
final SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
final MessageHeaders headers = headerAccessor.getMessageHeaders();
messagingTemplate.convertAndSendToUser(sessionId, destination, payload, headers);
}
}

View File

@ -0,0 +1,85 @@
package lingo.server;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.AbstractSubProtocolEvent;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import lingo.common.Player;
@Component
public class SessionManager implements ApplicationListener<AbstractSubProtocolEvent> {
public interface Listener {
void playerJoined(Player player);
void playerLeft(Player player);
}
private static final Logger log = LoggerFactory.getLogger(SessionManager.class);
private final Map<String, Player> playerBySession = new HashMap<>();
private final Set<Listener> listeners = new HashSet<>();
public void addListener(Listener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
public Player getPlayer(String sessionId) {
return playerBySession.get(sessionId);
}
public int getPlayerCount() {
return playerBySession.size();
}
@Override
public void onApplicationEvent(AbstractSubProtocolEvent event) {
if (event instanceof SessionConnectedEvent) {
onSessionConnected((SessionConnectedEvent) event);
} else if (event instanceof SessionDisconnectEvent) {
onSessionDisconnect((SessionDisconnectEvent) event);
}
}
private void onSessionConnected(SessionConnectedEvent event) {
final String sessionId = StompHeaderAccessor.wrap(event.getMessage()).getSessionId();
final Player player = new Player(sessionId);
log.info("Player connected: {}", player);
playerBySession.put(sessionId, player);
synchronized (listeners) {
for (Listener listener : listeners) {
listener.playerJoined(player);
}
}
}
private void onSessionDisconnect(SessionDisconnectEvent event) {
final String sessionId = event.getSessionId();
final Player player = playerBySession.remove(sessionId);
log.info("Player disconnected: {}", player);
synchronized (listeners) {
for (Listener listener : listeners) {
listener.playerLeft(player);
}
}
}
public void removeListener(Listener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
}

View File

@ -2,3 +2,23 @@ canvas {
display: block; display: block;
margin: 0 auto; margin: 0 auto;
} }
#skipDiv {
margin-top: 20px;
text-align: center
}
#skipButton {
width: 200px;
font-size: 28px;
font-variant: small-caps;
background-color: black;
color: white;
cursor: pointer;
padding: 10px;
border: none;
}
.hidden {
display: none;
}

View File

@ -7,6 +7,9 @@
</head> </head>
<body> <body>
<canvas id="canvas" width="300" height="400"></canvas> <canvas id="canvas" width="300" height="400"></canvas>
<div id="skipDiv" class="hidden">
<button id="skipButton" type="button">Skip Word</button>
</div>
<script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.1/sockjs.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.1/sockjs.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

View File

@ -27,6 +27,7 @@ function start() {
addKeydownListener(); addKeydownListener();
addKeypressListener(); addKeypressListener();
addSkipButtonListener();
reset(); reset();
repaint(); repaint();
@ -36,6 +37,7 @@ function start() {
client.connect({}, function(frame) { client.connect({}, function(frame) {
subscribeToPracticeGame(); subscribeToPracticeGame();
subscribeToPracticeReports(); subscribeToPracticeReports();
subscribeToPracticeWordSkipped();
client.send('/app/practiceGame'); client.send('/app/practiceGame');
}); });
} }
@ -77,6 +79,13 @@ function addKeypressListener() {
}); });
} }
// skip button
function addSkipButtonListener() {
document.getElementById('skipButton').addEventListener('click', function(e) {
client.send("/app/practiceSkip");
});
}
function drawMyBoard() { function drawMyBoard() {
var x = 25, y = MARGIN_TOP; var x = 25, y = MARGIN_TOP;
drawScore(x, y, myScore); drawScore(x, y, myScore);
@ -227,6 +236,7 @@ function subscribeToPracticeGame() {
var firstLetter = message.body; var firstLetter = message.body;
reset(firstLetter, true); reset(firstLetter, true);
repaint(); repaint();
document.getElementById('skipDiv').classList.remove('hidden');
}); });
} }
@ -263,4 +273,12 @@ function subscribeToPracticeReports() {
}); });
} }
function subscribeToPracticeWordSkipped() {
client.subscribe('/user/topic/practiceWordSkipped', function(message) {
var firstLetter = message.body;
reset(firstLetter, false);
repaint();
});
}
main(); main();