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_WORD_SKIPPED = topicDestination("practiceWordSkipped");
public static final String SESSION_USERNAME = topicDestination("sessionUsername");
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.TreeMap;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Header;
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.SimpMessagingTemplate;
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.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.common.ChatMessage;
@ -36,13 +33,16 @@ import lingo.common.Report;
import lingo.common.SetUsernameMessage;
@RestController
public class LingoController implements ApplicationListener<AbstractSubProtocolEvent> {
public class LingoController {
private static final Logger log = LoggerFactory.getLogger(LingoController.class);
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private SessionManager sessionManager;
@Autowired
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> practiceByPlayer = new HashMap<>();
private final Map<String, Player> playerBySession = new HashMap<>();
private final Set<String> usernames = new HashSet<>();
@MessageMapping("/chat")
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();
if (username == null) {
log.warn("No username for session {}", sessionId);
@ -74,7 +70,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/guess")
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();
if (username == null) {
log.warn("No username for session {}", sessionId);
@ -114,7 +110,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/hostGame")
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();
if (username == null) {
log.warn("No username for session {}", sessionId);
@ -133,7 +129,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/joinGame")
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();
if (username == null) {
log.warn("No username for session {}", sessionId);
@ -171,8 +167,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
}
}
private synchronized void leave(String sessionId) {
final Player player = playerBySession.remove(sessionId);
private synchronized void leave(Player player) {
final String username = player.getUsername();
usernames.remove(username);
final Game game = gameByPlayer.remove(player);
@ -188,7 +183,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/leaveGame")
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);
if (game == null) {
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")
public String onSessionId(@Header(SESSION_ID_HEADER) String sessionId) {
return sessionId;
}
@MessageMapping("/practiceGame")
public void practiceGame(@Header(SESSION_ID_HEADER) String sessionId) {
final Player player = playerBySession.get(sessionId);
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);
@PostConstruct
public void postConstruct() {
sessionManager.addListener(new PlayerLeftListener());
}
private void send(String destination, Object payload) {
@ -300,12 +242,12 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
@MessageMapping("/setUsername")
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)) {
player.setUsername(username);
log.info("{} --> {}", sessionId, username);
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 {
log.warn("{} -/> {} : Username taken", sessionId, username);
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

@ -1,4 +1,24 @@
canvas {
display: block;
margin: 0 auto;
display: block;
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>
<body>
<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/stomp.js/2.3.3/stomp.min.js"></script>

View File

@ -27,6 +27,7 @@ function start() {
addKeydownListener();
addKeypressListener();
addSkipButtonListener();
reset();
repaint();
@ -36,6 +37,7 @@ function start() {
client.connect({}, function(frame) {
subscribeToPracticeGame();
subscribeToPracticeReports();
subscribeToPracticeWordSkipped();
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() {
var x = 25, y = MARGIN_TOP;
drawScore(x, y, myScore);
@ -227,6 +236,7 @@ function subscribeToPracticeGame() {
var firstLetter = message.body;
reset(firstLetter, true);
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();