Redesign with game rooms
This commit is contained in:
parent
9dd9a212a5
commit
cde596132c
35
client-api/src/main/java/lingo/client/api/Destinations.java
Normal file
35
client-api/src/main/java/lingo/client/api/Destinations.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package lingo.client.api;
|
||||||
|
|
||||||
|
public class Destinations {
|
||||||
|
|
||||||
|
public static final String CHAT = topicDestination("chat");
|
||||||
|
|
||||||
|
public static final String GAME_CLOSED = topicDestination("gameClosed");
|
||||||
|
|
||||||
|
public static final String GAME_HOSTED = topicDestination("gameHosted");
|
||||||
|
|
||||||
|
public static final String GAME_JOINED = topicDestination("gameJoined");
|
||||||
|
|
||||||
|
public static final String GAME_LEFT = topicDestination("gameLeft");
|
||||||
|
|
||||||
|
public static final String GAME_STARTED = topicDestination("gameStarted");
|
||||||
|
|
||||||
|
public static final String OPPONENT_JOINED = topicDestination("opponentJoined");
|
||||||
|
|
||||||
|
public static final String OPPONENT_REPORTS = topicDestination("opponentReports");
|
||||||
|
|
||||||
|
public static final String PLAYER_REPORTS = topicDestination("playerReports");
|
||||||
|
|
||||||
|
public static final String PRACTICE_GAME = topicDestination("practiceGame");
|
||||||
|
|
||||||
|
public static final String PRACTICE_REPORTS = topicDestination("practiceReports");
|
||||||
|
|
||||||
|
public static final String SESSION_USERNAME = topicDestination("sessionUsername");
|
||||||
|
|
||||||
|
public static final String USER_JOINED = topicDestination("userJoined");
|
||||||
|
|
||||||
|
private static String topicDestination(String suffix) {
|
||||||
|
return "/topic/" + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package lingo.client.api;
|
|
||||||
|
|
||||||
public class StompTopics {
|
|
||||||
|
|
||||||
public static final String CHAT = createTopicName("chat");
|
|
||||||
|
|
||||||
public static final String GAME_STARTED = createTopicName("gameStarted");
|
|
||||||
|
|
||||||
public static final String OPPONENT_JOINED = createTopicName("opponentJoined");
|
|
||||||
|
|
||||||
public static final String OPPONENT_LEFT = createTopicName("opponentLeft");
|
|
||||||
|
|
||||||
public static final String OPPONENT_REPORTS = createTopicName("opponentReports");
|
|
||||||
|
|
||||||
public static final String PLAYER_REPORTS = createTopicName("playerReports");
|
|
||||||
|
|
||||||
public static final String PRACTICE_GAME = createTopicName("practiceGame");
|
|
||||||
|
|
||||||
public static final String PRACTICE_REPORTS = createTopicName("practiceReports");
|
|
||||||
|
|
||||||
public static final String USER_JOINED = createTopicName("userJoined");
|
|
||||||
|
|
||||||
private static String createTopicName(String suffix) {
|
|
||||||
return "/topic/lingo/" + suffix;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -29,7 +29,7 @@ import javafx.scene.paint.Color;
|
|||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
import javafx.scene.text.TextAlignment;
|
import javafx.scene.text.TextAlignment;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
import lingo.client.api.StompTopics;
|
import lingo.client.api.Destinations;
|
||||||
import lingo.client.util.FxmlController;
|
import lingo.client.util.FxmlController;
|
||||||
import lingo.client.view.Board;
|
import lingo.client.view.Board;
|
||||||
import lingo.client.view.OpponentBoard;
|
import lingo.client.view.OpponentBoard;
|
||||||
@ -154,13 +154,13 @@ public class MultiplayerPresenter implements FxmlController {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
private void postConstruct() {
|
private void postConstruct() {
|
||||||
executorService.execute(() -> {
|
executorService.execute(() -> {
|
||||||
stompTemplate.subscribe("/user" + StompTopics.OPPONENT_JOINED, new OpponentJoinedHandler(),
|
stompTemplate.subscribe("/user" + Destinations.OPPONENT_JOINED, new OpponentJoinedHandler(),
|
||||||
subscription -> subscriptionsLatch.countDown());
|
subscription -> subscriptionsLatch.countDown());
|
||||||
stompTemplate.subscribe("/user" + StompTopics.OPPONENT_LEFT, new OpponentLeftHandler(),
|
stompTemplate.subscribe("/user" + Destinations.OPPONENT_LEFT, new OpponentLeftHandler(),
|
||||||
subscription -> subscriptionsLatch.countDown());
|
subscription -> subscriptionsLatch.countDown());
|
||||||
stompTemplate.subscribe("/user" + StompTopics.OPPONENT_REPORTS, new OpponentReportHandler(),
|
stompTemplate.subscribe("/user" + Destinations.OPPONENT_REPORTS, new OpponentReportHandler(),
|
||||||
subscription -> subscriptionsLatch.countDown());
|
subscription -> subscriptionsLatch.countDown());
|
||||||
stompTemplate.subscribe("/user" + StompTopics.PLAYER_REPORTS, new PlayerReportHandler(),
|
stompTemplate.subscribe("/user" + Destinations.PLAYER_REPORTS, new PlayerReportHandler(),
|
||||||
subscription -> subscriptionsLatch.countDown());
|
subscription -> subscriptionsLatch.countDown());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package lingo.common;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class Game {
|
public class Game {
|
||||||
|
|
||||||
@ -12,23 +13,25 @@ public class Game {
|
|||||||
public static final int WORD_LENGTH = 5;
|
public static final int WORD_LENGTH = 5;
|
||||||
public static final int[] INVALID_GUESS = new int[] { 9, 9, 9, 9, 9 };
|
public static final int[] INVALID_GUESS = new int[] { 9, 9, 9, 9, 9 };
|
||||||
|
|
||||||
public final String playerOne;
|
private static final AtomicInteger idCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
public final String playerTwo;
|
public final int id;
|
||||||
|
|
||||||
private final Set<String> acceptableGuesses;
|
private Player host;
|
||||||
|
|
||||||
private final List<String> possibleWords;
|
private Player challenger;
|
||||||
|
|
||||||
|
private Set<String> acceptableGuesses;
|
||||||
|
|
||||||
|
private List<String> possibleWords;
|
||||||
|
|
||||||
private String word;
|
private String word;
|
||||||
|
|
||||||
private int wordIndex = 0;
|
private int wordIndex = 0;
|
||||||
|
|
||||||
public Game(String playerOne, String playerTwo, List<String> possibleWords, Set<String> acceptableGuesses) {
|
public Game(Player host) {
|
||||||
this.playerOne = playerOne;
|
this.id = idCounter.incrementAndGet();
|
||||||
this.playerTwo = playerTwo;
|
this.host = host;
|
||||||
this.possibleWords = possibleWords;
|
|
||||||
this.acceptableGuesses = acceptableGuesses;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int indexOf(char[] array, char searchTerm) {
|
private static int indexOf(char[] array, char searchTerm) {
|
||||||
@ -77,6 +80,14 @@ public class Game {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Player getChallenger() {
|
||||||
|
return challenger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
public String newGame() {
|
public String newGame() {
|
||||||
Collections.shuffle(possibleWords);
|
Collections.shuffle(possibleWords);
|
||||||
wordIndex = 0;
|
wordIndex = 0;
|
||||||
@ -88,4 +99,20 @@ public class Game {
|
|||||||
return word;
|
return word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setAcceptableGuesses(Set<String> value) {
|
||||||
|
this.acceptableGuesses = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChallenger(Player value) {
|
||||||
|
this.challenger = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(Player value) {
|
||||||
|
this.host = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPossibleWords(List<String> value) {
|
||||||
|
this.possibleWords = value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
32
common/src/main/java/lingo/common/GameLeftMessage.java
Normal file
32
common/src/main/java/lingo/common/GameLeftMessage.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package lingo.common;
|
||||||
|
|
||||||
|
public class GameLeftMessage {
|
||||||
|
|
||||||
|
private Game game;
|
||||||
|
|
||||||
|
private Player gameLeaver;
|
||||||
|
|
||||||
|
public GameLeftMessage() {}
|
||||||
|
|
||||||
|
public GameLeftMessage(Game game, Player gameLeaver) {
|
||||||
|
this.game = game;
|
||||||
|
this.gameLeaver = gameLeaver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Game getGame() {
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Player getGameLeaver() {
|
||||||
|
return gameLeaver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGame(Game game) {
|
||||||
|
this.game = game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGameLeaver(Player gameLeaver) {
|
||||||
|
this.gameLeaver = gameLeaver;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
common/src/main/java/lingo/common/Player.java
Normal file
25
common/src/main/java/lingo/common/Player.java
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package lingo.common;
|
||||||
|
|
||||||
|
public class Player {
|
||||||
|
|
||||||
|
private final String sessionId;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
public Player(String sessionId) {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSessionId() {
|
||||||
|
return sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
common/src/main/java/lingo/common/SetUsernameMessage.java
Normal file
43
common/src/main/java/lingo/common/SetUsernameMessage.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package lingo.common;
|
||||||
|
|
||||||
|
public class SetUsernameMessage {
|
||||||
|
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
public SetUsernameMessage() {}
|
||||||
|
|
||||||
|
public SetUsernameMessage(boolean success, String username, String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.success = success;
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSuccess(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,12 +2,12 @@ package lingo.server;
|
|||||||
|
|
||||||
import static org.springframework.messaging.simp.SimpMessageHeaderAccessor.SESSION_ID_HEADER;
|
import static org.springframework.messaging.simp.SimpMessageHeaderAccessor.SESSION_ID_HEADER;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import javax.annotation.PostConstruct;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -19,19 +19,23 @@ import org.springframework.messaging.handler.annotation.MessageMapping;
|
|||||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
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.stomp.StompHeaderAccessor;
|
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||||
import org.springframework.stereotype.Controller;
|
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.AbstractSubProtocolEvent;
|
||||||
import org.springframework.web.socket.messaging.SessionConnectedEvent;
|
import org.springframework.web.socket.messaging.SessionConnectedEvent;
|
||||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
||||||
|
|
||||||
import lingo.client.api.StompTopics;
|
import lingo.client.api.Destinations;
|
||||||
import lingo.common.ChatMessage;
|
import lingo.common.ChatMessage;
|
||||||
import lingo.common.Game;
|
import lingo.common.Game;
|
||||||
|
import lingo.common.GameLeftMessage;
|
||||||
|
import lingo.common.Player;
|
||||||
import lingo.common.Report;
|
import lingo.common.Report;
|
||||||
|
import lingo.common.SetUsernameMessage;
|
||||||
|
|
||||||
@Controller
|
@RestController
|
||||||
@MessageMapping("/lingo")
|
|
||||||
public class LingoController implements ApplicationListener<AbstractSubProtocolEvent> {
|
public class LingoController implements ApplicationListener<AbstractSubProtocolEvent> {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(LingoController.class);
|
private static final Logger log = LoggerFactory.getLogger(LingoController.class);
|
||||||
@ -42,25 +46,41 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
|
|||||||
@Autowired
|
@Autowired
|
||||||
private WordRepository wordRepo;
|
private WordRepository wordRepo;
|
||||||
|
|
||||||
private final List<String> waitingList = new ArrayList<>();
|
private final Map<Integer, Game> gameById = new TreeMap<>();
|
||||||
|
|
||||||
private final Map<String, Game> gameBySession = new HashMap<>();
|
private final Map<Player, Game> gameByPlayer = new HashMap<>();
|
||||||
|
|
||||||
private final Map<String, Game> practiceBySession = new HashMap<>();
|
private final Map<Player, Game> practiceByPlayer = new HashMap<>();
|
||||||
|
|
||||||
private final Map<String, String> usernameBySession = new HashMap<>();
|
private final Map<String, Player> playerBySession = new HashMap<>();
|
||||||
|
|
||||||
|
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 String username = usernameBySession.get(sessionId);
|
final Player player = playerBySession.get(sessionId);
|
||||||
return new ChatMessage(username, message);
|
if (player == null) {
|
||||||
|
log.warn("No player for session {}", sessionId);
|
||||||
|
throw new IllegalStateException("No player for session " + sessionId);
|
||||||
|
}
|
||||||
|
return new ChatMessage(player.getUsername(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping("/games")
|
||||||
|
public Collection<Game> getGames() {
|
||||||
|
return gameById.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
|
if (player == null) {
|
||||||
|
log.warn("No player for session {}", sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
guess = guess.toUpperCase();
|
guess = guess.toUpperCase();
|
||||||
log.info("Player {} guessed: {}", sessionId, guess);
|
log.info("{} guessed {}", sessionId, guess);
|
||||||
final Game game = gameBySession.get(sessionId);
|
final Game game = gameByPlayer.get(player);
|
||||||
final int[] result = game.evaluate(guess);
|
final int[] result = game.evaluate(guess);
|
||||||
|
|
||||||
// Generate reports
|
// Generate reports
|
||||||
@ -80,49 +100,123 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
|
|||||||
playerReport.setResult(result);
|
playerReport.setResult(result);
|
||||||
opponentReport.setResult(result);
|
opponentReport.setResult(result);
|
||||||
}
|
}
|
||||||
final String opponentId = sessionId.equals(game.playerOne) ? game.playerTwo : game.playerOne;
|
final Player opponent;
|
||||||
sendToUser(sessionId, StompTopics.PLAYER_REPORTS, playerReport);
|
if (sessionId.equals(game.getHost().getSessionId())) {
|
||||||
sendToUser(opponentId, StompTopics.OPPONENT_REPORTS, opponentReport);
|
opponent = game.getChallenger();
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("/join")
|
|
||||||
public void join(String username, @Header(SESSION_ID_HEADER) String sessionId) {
|
|
||||||
log.info("Player joined: {}, {}", sessionId, username);
|
|
||||||
usernameBySession.put(sessionId, username);
|
|
||||||
send(StompTopics.USER_JOINED, username);
|
|
||||||
joinWaitingList(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void joinWaitingList(String sessionId) {
|
|
||||||
synchronized (waitingList) {
|
|
||||||
if (!waitingList.contains(sessionId)) {
|
|
||||||
waitingList.add(sessionId);
|
|
||||||
waitingList.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void leave(String sessionId) {
|
|
||||||
final String username = usernameBySession.remove(sessionId);
|
|
||||||
if (username != null) {
|
|
||||||
send(StompTopics.CHAT, new ChatMessage(null, username + " left"));
|
|
||||||
}
|
|
||||||
final Game game = gameBySession.remove(sessionId);
|
|
||||||
if (game == null) {
|
|
||||||
leaveWaitingList(sessionId);
|
|
||||||
} else {
|
} else {
|
||||||
log.info("Player {} left their game!", sessionId);
|
opponent = game.getHost();
|
||||||
final String opponentId = sessionId.equals(game.playerOne) ? game.playerTwo : game.playerOne;
|
}
|
||||||
gameBySession.remove(opponentId);
|
sendToPlayer(player, Destinations.PLAYER_REPORTS, playerReport);
|
||||||
sendToUser(opponentId, StompTopics.OPPONENT_LEFT, "You win!");
|
sendToPlayer(opponent, Destinations.OPPONENT_REPORTS, opponentReport);
|
||||||
joinWaitingList(opponentId);
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/hostGame")
|
||||||
|
public synchronized void hostGame(@Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
|
final Player player = playerBySession.get(sessionId);
|
||||||
|
if (player == null) {
|
||||||
|
log.warn("No player for session {}", sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gameByPlayer.containsKey(player)) {
|
||||||
|
log.warn("{} is in a game already", player.getUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Game game = new Game(player);
|
||||||
|
gameById.put(game.id, game);
|
||||||
|
gameByPlayer.put(player, game);
|
||||||
|
log.info("{} hosted a game", player.getUsername());
|
||||||
|
send(Destinations.GAME_HOSTED, game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/joinGame")
|
||||||
|
public synchronized void joinGame(Integer gameId, @Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
|
final Player player = playerBySession.get(sessionId);
|
||||||
|
if (player == null) {
|
||||||
|
log.warn("No player for session {}", sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gameByPlayer.containsKey(player)) {
|
||||||
|
log.warn("{} is in a game already", player.getUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Game game = gameById.get(gameId);
|
||||||
|
if (game == null) {
|
||||||
|
log.warn("No game with id {}", gameId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (game.getChallenger() == null) {
|
||||||
|
game.setChallenger(player);
|
||||||
|
gameByPlayer.put(player, game);
|
||||||
|
log.info("{} joined {}'s game", player.getUsername(), game.getHost());
|
||||||
|
send(Destinations.GAME_JOINED, game);
|
||||||
|
|
||||||
|
// Start the game immediately
|
||||||
|
// TODO: require the players to "ready up"
|
||||||
|
game.setAcceptableGuesses(wordRepo.getGuesses());
|
||||||
|
game.setPossibleWords(wordRepo.getWords());
|
||||||
|
final String firstWord = game.newGame();
|
||||||
|
final String firstLetter = String.valueOf(firstWord.charAt(0));
|
||||||
|
log.info("First word: {}", firstWord);
|
||||||
|
final Player playerOne = game.getHost();
|
||||||
|
final Player playerTwo = game.getChallenger();
|
||||||
|
final String[] playerOneMessage = new String[] { firstLetter, playerTwo.getUsername() };
|
||||||
|
final String[] playerTwoMessage = new String[] { firstLetter, playerOne.getUsername() };
|
||||||
|
sendToPlayer(playerOne, Destinations.OPPONENT_JOINED, playerOneMessage);
|
||||||
|
sendToPlayer(playerTwo, Destinations.OPPONENT_JOINED, playerTwoMessage);
|
||||||
|
send(Destinations.GAME_STARTED, new String[] { playerOne.getUsername(), playerTwo.getUsername() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void leaveWaitingList(String sessionId) {
|
private synchronized void leave(String sessionId) {
|
||||||
synchronized (waitingList) {
|
final Player player = playerBySession.remove(sessionId);
|
||||||
waitingList.remove(sessionId);
|
if (player == null) {
|
||||||
waitingList.notify();
|
log.warn("No player for session {}", sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String username = player.getUsername();
|
||||||
|
usernames.remove(username);
|
||||||
|
final Game game = gameByPlayer.remove(player);
|
||||||
|
if (game == null) {
|
||||||
|
log.info("{} left", username);
|
||||||
|
send(Destinations.CHAT, new ChatMessage(null, username + " left"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
leaveGame(game, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("/leaveGame")
|
||||||
|
public synchronized void leaveGame(@Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
|
final Player player = playerBySession.get(sessionId);
|
||||||
|
if (player == null) {
|
||||||
|
log.warn("No player for session {}", sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Game game = gameByPlayer.remove(player);
|
||||||
|
if (game == null) {
|
||||||
|
log.warn("{} is not in a game", player.getUsername());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
leaveGame(game, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void leaveGame(Game game, Player player) {
|
||||||
|
final Player gameHost = game.getHost();
|
||||||
|
final Player gameChallenger = game.getChallenger();
|
||||||
|
if (gameHost == player) {
|
||||||
|
if (gameChallenger == null) {
|
||||||
|
// Close the game
|
||||||
|
gameById.remove(game.id);
|
||||||
|
send(Destinations.GAME_CLOSED, game);
|
||||||
|
} else {
|
||||||
|
// Leave the game
|
||||||
|
game.setHost(gameChallenger);
|
||||||
|
game.setChallenger(null);
|
||||||
|
send(Destinations.GAME_LEFT, new GameLeftMessage(game, player));
|
||||||
|
}
|
||||||
|
} else if (gameChallenger == player) {
|
||||||
|
// Leave the game
|
||||||
|
game.setChallenger(null);
|
||||||
|
send(Destinations.GAME_LEFT, new GameLeftMessage(game, player));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +232,7 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
|
|||||||
private void onSessionConnected(SessionConnectedEvent event) {
|
private void onSessionConnected(SessionConnectedEvent event) {
|
||||||
final String sessionId = StompHeaderAccessor.wrap(event.getMessage()).getSessionId();
|
final String sessionId = StompHeaderAccessor.wrap(event.getMessage()).getSessionId();
|
||||||
log.info("Session connected: {}", sessionId);
|
log.info("Session connected: {}", sessionId);
|
||||||
|
playerBySession.put(sessionId, new Player(sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSessionDisconnect(SessionDisconnectEvent event) {
|
private void onSessionDisconnect(SessionDisconnectEvent event) {
|
||||||
@ -146,27 +241,31 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
|
|||||||
leave(sessionId);
|
leave(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@SubscribeMapping("/topic/sessionId")
|
||||||
private void postConstruct() {
|
public String onSessionId(@Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
new Thread(new WaitingListListener()).start();
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@MessageMapping("/practiceGame")
|
@MessageMapping("/practiceGame")
|
||||||
public void practiceGame(@Header(SESSION_ID_HEADER) String sessionId) {
|
public void practiceGame(@Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
log.info("Player wants a practice session: {}", sessionId);
|
final Player player = playerBySession.get(sessionId);
|
||||||
final Game game = new Game(sessionId, null, wordRepo.getWords(), wordRepo.getGuesses());
|
log.info("{} wants a practice session", sessionId);
|
||||||
practiceBySession.put(sessionId, game);
|
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 firstWord = game.newGame();
|
||||||
final String firstLetter = String.valueOf(firstWord.charAt(0));
|
final String firstLetter = String.valueOf(firstWord.charAt(0));
|
||||||
log.info("First word: {}", firstWord);
|
log.info("First word: {}", firstWord);
|
||||||
sendToUser(sessionId, StompTopics.PRACTICE_GAME, firstLetter);
|
sendToPlayer(player, Destinations.PRACTICE_GAME, firstLetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@MessageMapping("/practiceGuess")
|
@MessageMapping("/practiceGuess")
|
||||||
public void practiceGuess(String guess, @Header(SESSION_ID_HEADER) String sessionId) {
|
public void practiceGuess(String guess, @Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
|
final Player player = playerBySession.get(sessionId);
|
||||||
guess = guess.toUpperCase();
|
guess = guess.toUpperCase();
|
||||||
log.info("Player {} guessed: {}", sessionId, guess);
|
log.info("{} guessed {}", sessionId, guess);
|
||||||
final Game game = practiceBySession.get(sessionId);
|
final Game game = practiceByPlayer.get(player);
|
||||||
final int[] result = game.evaluate(guess);
|
final int[] result = game.evaluate(guess);
|
||||||
|
|
||||||
// Generate report
|
// Generate report
|
||||||
@ -181,57 +280,38 @@ public class LingoController implements ApplicationListener<AbstractSubProtocolE
|
|||||||
} else {
|
} else {
|
||||||
report.setResult(result);
|
report.setResult(result);
|
||||||
}
|
}
|
||||||
sendToUser(sessionId, StompTopics.PRACTICE_REPORTS, report);
|
sendToPlayer(player, Destinations.PRACTICE_REPORTS, report);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void send(String destination, Object payload) {
|
private void send(String destination, Object payload) {
|
||||||
messagingTemplate.convertAndSend(destination, payload);
|
messagingTemplate.convertAndSend(destination, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendToUser(String user, String destination, Object payload) {
|
private void sendToPlayer(Player player, String destination, Object payload) {
|
||||||
// TODO: cache the headers?
|
sendToSession(player.getSessionId(), destination, payload);
|
||||||
final SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
|
||||||
headerAccessor.setSessionId(user);
|
|
||||||
headerAccessor.setLeaveMutable(true);
|
|
||||||
final MessageHeaders headers = headerAccessor.getMessageHeaders();
|
|
||||||
messagingTemplate.convertAndSendToUser(user, destination, payload, headers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void sendToSession(String sessionId, String destination, Object payload) {
|
||||||
* Task that spawns a game whenever two players are waiting.
|
// TODO: cache the headers?
|
||||||
*/
|
final SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||||
private class WaitingListListener implements Runnable {
|
headerAccessor.setSessionId(sessionId);
|
||||||
|
headerAccessor.setLeaveMutable(true);
|
||||||
|
final MessageHeaders headers = headerAccessor.getMessageHeaders();
|
||||||
|
messagingTemplate.convertAndSendToUser(sessionId, destination, payload, headers);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@MessageMapping("/setUsername")
|
||||||
public void run() {
|
public synchronized void setUsername(String username, @Header(SESSION_ID_HEADER) String sessionId) {
|
||||||
while (true) {
|
final Player player = playerBySession.get(sessionId);
|
||||||
final String playerOne;
|
if (usernames.add(username)) {
|
||||||
final String playerTwo;
|
player.setUsername(username);
|
||||||
synchronized (waitingList) {
|
log.info("{} --> {}", sessionId, username);
|
||||||
while (waitingList.size() < 2) {
|
sendToPlayer(player, Destinations.SESSION_USERNAME, new SetUsernameMessage(true, username, null));
|
||||||
try {
|
send(Destinations.USER_JOINED, new Object[] { username, playerBySession.size() });
|
||||||
waitingList.wait();
|
} else {
|
||||||
} catch (InterruptedException ok) {
|
log.warn("{} -/> {} : Username taken", sessionId, username);
|
||||||
ok.printStackTrace();
|
final SetUsernameMessage response = new SetUsernameMessage(false, null, "Username taken");
|
||||||
}
|
sendToPlayer(player, Destinations.SESSION_USERNAME, response);
|
||||||
}
|
|
||||||
playerOne = waitingList.remove(0);
|
|
||||||
playerTwo = waitingList.remove(0);
|
|
||||||
}
|
|
||||||
final Game game = new Game(playerOne, playerTwo, wordRepo.getWords(), wordRepo.getGuesses());
|
|
||||||
gameBySession.put(playerOne, game);
|
|
||||||
gameBySession.put(playerTwo, game);
|
|
||||||
final String firstWord = game.newGame();
|
|
||||||
final String firstLetter = String.valueOf(firstWord.charAt(0));
|
|
||||||
log.info("First word: {}", firstWord);
|
|
||||||
final String playerOneUsername = usernameBySession.get(playerOne);
|
|
||||||
final String playerTwoUsername = usernameBySession.get(playerTwo);
|
|
||||||
final String[] playerOneMessage = new String[] { firstLetter, playerTwoUsername };
|
|
||||||
final String[] playerTwoMessage = new String[] { firstLetter, playerOneUsername };
|
|
||||||
sendToUser(playerOne, StompTopics.OPPONENT_JOINED, playerOneMessage);
|
|
||||||
sendToUser(playerTwo, StompTopics.OPPONENT_JOINED, playerTwoMessage);
|
|
||||||
send(StompTopics.GAME_STARTED, new String[] { playerOneUsername, playerTwoUsername });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,9 +24,10 @@ public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||||
config.enableSimpleBroker("/topic");
|
registry.enableSimpleBroker("/topic");
|
||||||
config.setApplicationDestinationPrefixes("/app");
|
registry.setApplicationDestinationPrefixes("/app", "/user");
|
||||||
|
registry.setUserDestinationPrefix("/user");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -2,28 +2,57 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Lingo</title>
|
<title>Lingo | Client</title>
|
||||||
<link rel="stylesheet" href="cube-grid.css">
|
<link rel="stylesheet" href="cube-grid.css">
|
||||||
<link rel="stylesheet" href="layout.css">
|
<link rel="stylesheet" href="layout.css">
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="usernameDiv" class="jumbotron">
|
<div>
|
||||||
<h2>What is your name?</h2>
|
|
||||||
<form>
|
|
||||||
<div class="form-group">
|
|
||||||
<input id="username" type="text" class="form-control" maxlength="16" placeholder="Name">
|
|
||||||
</div>
|
|
||||||
<p id="usernameError" class="error-message"></p>
|
|
||||||
<button id="usernameButton" type="button" class="btn btn-default">Submit</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="appDiv" class="hidden">
|
|
||||||
<div class="main column">
|
<div class="main column">
|
||||||
<div class="header row">Lingo</div>
|
<div class="header row">Lingo</div>
|
||||||
<div class="body row nofooter">
|
<div class="body row nofooter">
|
||||||
<div id="canvasDiv" class="hidden">
|
<div id="usernameDiv" class="form">
|
||||||
<canvas id="canvas" class="centered" width="600" height="475" tabindex="1"></canvas>
|
<h2>What is your name?</h2>
|
||||||
|
<input id="nicknameInput" type="text" class="form-control" maxlength="16">
|
||||||
|
<p id="usernameError" class="error-message"></p>
|
||||||
|
</div>
|
||||||
|
<div id="appDiv" class="hidden">
|
||||||
|
<div id="lobbyColumn" class="lobby column primary">
|
||||||
|
<div id="vue-app" class="body row noheader nofooter scroll-y">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Games</h3>
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<div v-if="games.length === 0" class="list-group-item">
|
||||||
|
There are no games.
|
||||||
|
</div>
|
||||||
|
<div v-for="game in games">
|
||||||
|
<div v-if="game.started" v-bind:id="'game-' + game.id" class="list-group-item">
|
||||||
|
<strong>{{ game.playerOne }}</strong>
|
||||||
|
is playing with
|
||||||
|
<strong>{{ game.playerTwo }}</strong>
|
||||||
|
</div>
|
||||||
|
<button v-else v-bind:id="'game-' + game.id" @click="joinGame" type="button" class="list-group-item">
|
||||||
|
<strong>{{ game.playerOne }}</strong>
|
||||||
|
wants to play
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button v-if="inGame" @click="leaveGame" type="button">Leave game</button>
|
||||||
|
<button v-else @click="hostGame" type="button">Create a game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="gameColumn" class="game column">
|
||||||
|
<div class="body row noheader nofooter">
|
||||||
|
<div id="canvasDiv" class="hidden">
|
||||||
|
<canvas id="canvas" class="centered" width="600" height="475" tabindex="1"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -31,7 +60,7 @@
|
|||||||
<div class="header row">Chat</div>
|
<div class="header row">Chat</div>
|
||||||
<div id="messageList" class="body row scroll-y"></div>
|
<div id="messageList" class="body row scroll-y"></div>
|
||||||
<div class="footer row">
|
<div class="footer row">
|
||||||
<input id="messageInput" placeholder="Chat here..." />
|
<input id="messageInput" placeholder="Type here to chat..." />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,48 +24,100 @@ var canvas = document.getElementById('canvas');
|
|||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
var client;
|
var client;
|
||||||
|
var sessionId = null;
|
||||||
|
|
||||||
|
var vm = new Vue({
|
||||||
|
el: '#vue-app',
|
||||||
|
data: {
|
||||||
|
games: [],
|
||||||
|
gameId: null
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inGame: function() {
|
||||||
|
return this.gameId !== null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
hostGame: function(event) {
|
||||||
|
client.send('/app/hostGame');
|
||||||
|
},
|
||||||
|
joinGame: function(event) {
|
||||||
|
// Discard 'game-' prefix
|
||||||
|
var buttonId = event.target.id;
|
||||||
|
var gameId = buttonId.substr(5);
|
||||||
|
client.send('/app/joinGame', {}, gameId);
|
||||||
|
},
|
||||||
|
leaveGame: function(event) {
|
||||||
|
client.send('/app/leaveGame');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function afterConnected(stompConnectedFrame) {
|
||||||
|
console.log('Connected to STOMP endpoint')
|
||||||
|
var sessionIdTopic = '/user/topic/sessionId';
|
||||||
|
var sessionIdSubscription = null;
|
||||||
|
var sessionIdHandler = function(message) {
|
||||||
|
console.log('Session ID: ' + message.body);
|
||||||
|
sessionId = message.body;
|
||||||
|
sessionIdSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
sessionIdSubscription = client.subscribe(sessionIdTopic, sessionIdHandler);
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
|
|
||||||
|
client = Stomp.over(new SockJS('/stomp'));
|
||||||
|
client.connect({}, afterConnected);
|
||||||
|
|
||||||
var usernameDiv = document.getElementById('usernameDiv');
|
var usernameDiv = document.getElementById('usernameDiv');
|
||||||
var usernameInput = document.getElementById('username');
|
|
||||||
var usernameButton = document.getElementById('usernameButton');
|
|
||||||
var usernameError = document.getElementById('usernameError');
|
var usernameError = document.getElementById('usernameError');
|
||||||
var submitUsernameFunction = function() {
|
var usernameInput = document.getElementById('nicknameInput');
|
||||||
var usernameValue = usernameInput.value.trim();
|
|
||||||
if (usernameValue.length === 0) {
|
var usernameTopic = '/user/topic/sessionUsername';
|
||||||
return;
|
var usernameSubscription = null;
|
||||||
|
var usernameHandler = function(message) {
|
||||||
|
var response = JSON.parse(message.body);
|
||||||
|
if (response.success === true) {
|
||||||
|
console.log('Username: ' + response.username);
|
||||||
|
myUsername = response.username;
|
||||||
|
start();
|
||||||
|
usernameDiv.classList.add('hidden');
|
||||||
|
appDiv.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
usernameError.innerHTML = response.errorMessage;
|
||||||
}
|
}
|
||||||
myUsername = usernameValue;
|
};
|
||||||
localStorage.setItem('lingo.username', myUsername);
|
|
||||||
console.log('My username: ' + myUsername);
|
|
||||||
start();
|
|
||||||
usernameDiv.classList.add('hidden');
|
|
||||||
appDiv.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
usernameButton.addEventListener('click', submitUsernameFunction);
|
|
||||||
usernameInput.focus();
|
usernameInput.focus();
|
||||||
usernameInput.addEventListener('keydown', function(e) {
|
usernameInput.addEventListener('keydown', function(e) {
|
||||||
if (e.keyCode === KEYCODE_RETURN) {
|
if (e.keyCode === KEYCODE_RETURN) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
submitUsernameFunction();
|
if (sessionId === null) {
|
||||||
|
usernameError.innerHTML = 'Not connected to server';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var usernameValue = usernameInput.value.trim();
|
||||||
|
if (usernameValue.length === 0) {
|
||||||
|
usernameError.innerHTML = 'Name cannot be empty';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (usernameSubscription === null) {
|
||||||
|
usernameSubscription = client.subscribe(usernameTopic, usernameHandler);
|
||||||
|
client.subscribe('/topic/userJoined', onUserJoined);
|
||||||
|
}
|
||||||
|
client.send('/app/setUsername', {}, usernameValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
usernameInput.addEventListener('keyup', function(e) {
|
usernameInput.addEventListener('keyup', function(e) {
|
||||||
|
if (e.keyCode === KEYCODE_RETURN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
var usernameValue = usernameInput.value.trim();
|
var usernameValue = usernameInput.value.trim();
|
||||||
if (usernameValue.length === 0) {
|
if (usernameValue.length !== 0) {
|
||||||
usernameError.innerHTML = 'Name cannot be empty.';
|
|
||||||
usernameButton.disabled = true;
|
|
||||||
} else {
|
|
||||||
usernameError.innerHTML = '';
|
usernameError.innerHTML = '';
|
||||||
usernameButton.disabled = false;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var storedUsername = localStorage.getItem('lingo.username');
|
|
||||||
if (storedUsername === null) {
|
|
||||||
usernameInput.value = 'Alex Trebek';
|
|
||||||
} else {
|
|
||||||
usernameInput.value = storedUsername;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
@ -80,18 +132,16 @@ function start() {
|
|||||||
reset();
|
reset();
|
||||||
repaint();
|
repaint();
|
||||||
|
|
||||||
client = Stomp.over(new SockJS('/stomp'));
|
client.subscribe('/topic/chat', onChat);
|
||||||
|
client.subscribe('/topic/gameClosed', onGameClosed);
|
||||||
client.connect({}, function(frame) {
|
client.subscribe('/topic/gameHosted', onGameHosted);
|
||||||
subscribeToChat();
|
client.subscribe('/topic/gameJoined', onGameJoined);
|
||||||
subscribeToGameStarted();
|
client.subscribe('/topic/gameLeft', onGameLeft);
|
||||||
subscribeToOpponentJoined();
|
client.subscribe('/topic/gameStarted', onGameStarted);
|
||||||
subscribeToOpponentLeft();
|
client.subscribe('/user/topic/opponentJoined', onOpponentJoined);
|
||||||
subscribeToOpponentReports();
|
client.subscribe('/user/topic/opponentLeft', onOpponentLeft);
|
||||||
subscribeToPlayerReports();
|
client.subscribe('/user/topic/opponentReports', onOpponentReport);
|
||||||
subscribeToUserJoined();
|
client.subscribe('/user/topic/playerReports', onPlayerReport);
|
||||||
client.send('/app/lingo/join', {}, myUsername);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// special keys
|
// special keys
|
||||||
@ -104,7 +154,7 @@ function addKeydownListener() {
|
|||||||
}
|
}
|
||||||
else if (e.which === KEYCODE_RETURN) {
|
else if (e.which === KEYCODE_RETURN) {
|
||||||
if (myGuess.length === 5) {
|
if (myGuess.length === 5) {
|
||||||
client.send("/app/lingo/guess", {}, myGuess);
|
client.send("/app/guess", {}, myGuess);
|
||||||
myGuess = '';
|
myGuess = '';
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
@ -138,8 +188,8 @@ function addChatMessageListener() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
client.send('/app/lingo/chat', {}, text);
|
client.send('/app/chat', {}, text);
|
||||||
addChatMessage('Me', text);
|
addChatMessage(myUsername, text);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -147,7 +197,7 @@ function addChatMessageListener() {
|
|||||||
function addChatMessage(sender, body) {
|
function addChatMessage(sender, body) {
|
||||||
var messageList = document.getElementById('messageList');
|
var messageList = document.getElementById('messageList');
|
||||||
var usernameNode = document.createElement('strong');
|
var usernameNode = document.createElement('strong');
|
||||||
var usernameTextNode = document.createTextNode(sender)
|
var usernameTextNode = document.createTextNode(sender);
|
||||||
usernameNode.appendChild(usernameTextNode);
|
usernameNode.appendChild(usernameTextNode);
|
||||||
var messageTextNode = document.createTextNode(' ' + body);
|
var messageTextNode = document.createTextNode(' ' + body);
|
||||||
var messageItem = document.createElement('div');
|
var messageItem = document.createElement('div');
|
||||||
@ -315,6 +365,17 @@ function isValidResult(result) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeGame(gameId) {
|
||||||
|
var indexToRemove = null;
|
||||||
|
for (var i = 0; i < vm.games.length; i++) {
|
||||||
|
if (vm.games[i].id === gameId) {
|
||||||
|
indexToRemove = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vm.games.splice(indexToRemove, 1);
|
||||||
|
}
|
||||||
|
|
||||||
function repaint() {
|
function repaint() {
|
||||||
// clear the canvas
|
// clear the canvas
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
@ -340,120 +401,198 @@ function reset(firstLetter, clearScore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToChat() {
|
function toggleView() {
|
||||||
client.subscribe('/topic/lingo/chat', function(message) {
|
var lobbyColumn = document.getElementById('lobbyColumn');
|
||||||
var chatMessage = JSON.parse(message.body);
|
var gameColumn = document.getElementById('gameColumn')
|
||||||
var messageSender = chatMessage.username;
|
if (lobbyColumn.classList.contains('primary')) {
|
||||||
var messageBody = chatMessage.message;
|
lobbyColumn.classList.remove('primary');
|
||||||
if (messageSender === null) {
|
} else {
|
||||||
addChatAnnouncement(messageBody);
|
lobbyColumn.classList.add('primary');
|
||||||
} else if (messageSender === myUsername) {
|
}
|
||||||
console.log('Ignoring message sent by myself')
|
if (gameColumn.classList.contains('primary')) {
|
||||||
} else {
|
gameColumn.classList.remove('primary');
|
||||||
console.log('Message from ' + messageSender + ": " + messageBody);
|
} else {
|
||||||
addChatMessage(messageSender, messageBody);
|
gameColumn.classList.add('primary');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToGameStarted() {
|
function onChat(message) {
|
||||||
client.subscribe('/topic/lingo/gameStarted', function(message) {
|
var chatMessage = JSON.parse(message.body);
|
||||||
var report = JSON.parse(message.body);
|
var messageSender = chatMessage.username;
|
||||||
var playerOne = report[0];
|
var messageBody = chatMessage.message;
|
||||||
var playerTwo = report[1];
|
if (messageSender === null) {
|
||||||
if (playerOne === myUsername) {
|
addChatAnnouncement(messageBody);
|
||||||
addChatAnnouncement('You are playing with ' + playerTwo);
|
} else if (messageSender === myUsername) {
|
||||||
} else if (playerTwo === myUsername) {
|
// Ignore messages sent by yourself
|
||||||
addChatAnnouncement('You are playing with ' + playerOne);
|
} else {
|
||||||
} else {
|
console.log('Message from ' + messageSender + ": " + messageBody);
|
||||||
addChatAnnouncement(playerOne + ' is playing with ' + playerTwo);
|
addChatMessage(messageSender, messageBody);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToOpponentJoined() {
|
function onGameClosed(message) {
|
||||||
client.subscribe('/user/topic/lingo/opponentJoined', function(message) {
|
var game = JSON.parse(message.body);
|
||||||
var report = JSON.parse(message.body);
|
var gameId = game.id;
|
||||||
var firstLetter = report[0];
|
var gameHost = game.host.username;
|
||||||
opponentUsername = report[1];
|
if (gameHost === myUsername) {
|
||||||
console.log('Opponent username: ' + opponentUsername);
|
vm.gameId = null;
|
||||||
reset(firstLetter, true);
|
}
|
||||||
canvasDiv.classList.remove('hidden');
|
console.log(gameHost + ' closed Game ' + gameId);
|
||||||
|
removeGame(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameHosted(message) {
|
||||||
|
var game = JSON.parse(message.body);
|
||||||
|
var gameId = game.id;
|
||||||
|
var gameHost = game.host.username;
|
||||||
|
if (gameHost === myUsername) {
|
||||||
|
vm.gameId = gameId;
|
||||||
|
}
|
||||||
|
console.log(gameHost + ' hosted Game ' + gameId);
|
||||||
|
vm.games.push({ id: gameId, playerOne: gameHost, started: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameJoined(message) {
|
||||||
|
var game = JSON.parse(message.body);
|
||||||
|
var gameId = game.id;
|
||||||
|
var gameHost = game.host.username;
|
||||||
|
var gameChallenger = game.challenger.username;
|
||||||
|
if (gameChallenger === myUsername) {
|
||||||
|
vm.gameId = gameId;
|
||||||
|
}
|
||||||
|
console.log(gameChallenger + ' joined ' + gameHost + "'s game");
|
||||||
|
for (var i = 0; i < vm.games.length; i++) {
|
||||||
|
if (vm.games[i].id === gameId) {
|
||||||
|
vm.games[i].playerTwo = gameChallenger;
|
||||||
|
vm.games[i].started = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (gameHost === myUsername || gameChallenger === myUsername) {
|
||||||
|
toggleView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameLeft(message) {
|
||||||
|
var report = JSON.parse(message.body);
|
||||||
|
var game = report.game;
|
||||||
|
var gameId = game.id;
|
||||||
|
var gameHost = game.host.username;
|
||||||
|
var gameLeaver = report.gameLeaver.username;
|
||||||
|
var previousPlayers = [];
|
||||||
|
for (var i = 0; i < vm.games.length; i++) {
|
||||||
|
if (vm.games[i].id === gameId) {
|
||||||
|
previousPlayers.push(vm.games[i].playerOne);
|
||||||
|
previousPlayers.push(vm.games[i].playerTwo);
|
||||||
|
vm.games[i].playerOne = gameHost;
|
||||||
|
vm.games[i].playerTwo = game.challenger ? game.challenger.username : null;
|
||||||
|
vm.games[i].started = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(gameLeaver + ' left ' + gameHost + "'s game");
|
||||||
|
if (gameLeaver === myUsername) {
|
||||||
|
vm.gameId = null;
|
||||||
|
}
|
||||||
|
if (previousPlayers.indexOf(myUsername) != -1) {
|
||||||
|
onOpponentLeft();
|
||||||
|
toggleView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGameStarted(message) {
|
||||||
|
var report = JSON.parse(message.body);
|
||||||
|
var playerOne = report[0];
|
||||||
|
var playerTwo = report[1];
|
||||||
|
if (playerOne === myUsername) {
|
||||||
|
addChatAnnouncement('You are playing with ' + playerTwo);
|
||||||
|
} else if (playerTwo === myUsername) {
|
||||||
|
addChatAnnouncement('You are playing with ' + playerOne);
|
||||||
|
} else {
|
||||||
|
addChatAnnouncement(playerOne + ' is playing with ' + playerTwo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpponentJoined(message) {
|
||||||
|
var report = JSON.parse(message.body);
|
||||||
|
var firstLetter = report[0];
|
||||||
|
opponentUsername = report[1];
|
||||||
|
console.log('Opponent username: ' + opponentUsername);
|
||||||
|
reset(firstLetter, true);
|
||||||
|
canvasDiv.classList.remove('hidden');
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpponentLeft(message) {
|
||||||
|
opponentUsername = null;
|
||||||
|
lastWord = null;
|
||||||
|
canvasDiv.classList.add('hidden');
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpponentReport(message) {
|
||||||
|
var report = JSON.parse(message.body);
|
||||||
|
if (report.correct === true) {
|
||||||
|
var guess = report.guess;
|
||||||
|
var firstLetter = report.firstLetter;
|
||||||
|
console.log('Opponent guessed correctly! ' + guess);
|
||||||
|
opponentScore = opponentScore + 100;
|
||||||
|
lastWord = guess;
|
||||||
|
reset(firstLetter, false);
|
||||||
repaint();
|
repaint();
|
||||||
});
|
} else {
|
||||||
}
|
var result = report.result;
|
||||||
|
console.log('Opponent result: ' + result);
|
||||||
function subscribeToOpponentLeft() {
|
opponentResults.push(result);
|
||||||
client.subscribe('/user/topic/lingo/opponentLeft', function(message) {
|
|
||||||
opponentUsername = null;
|
|
||||||
lastWord = null;
|
|
||||||
canvasDiv.classList.add('hidden');
|
|
||||||
repaint();
|
repaint();
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToOpponentReports() {
|
function onPlayerReport(message) {
|
||||||
client.subscribe('/user/topic/lingo/opponentReports', function(message) {
|
var report = JSON.parse(message.body);
|
||||||
var report = JSON.parse(message.body);
|
console.log('My report: ' + report);
|
||||||
if (report.correct === true) {
|
if (report.correct === true) {
|
||||||
var guess = report.guess;
|
var guess = report.guess;
|
||||||
var firstLetter = report.firstLetter;
|
var firstLetter = report.firstLetter;
|
||||||
console.log('Opponent guessed correctly! ' + guess);
|
console.log('I guessed correctly!');
|
||||||
opponentScore = opponentScore + 100;
|
myScore = myScore + 100;
|
||||||
lastWord = guess;
|
lastWord = guess;
|
||||||
reset(firstLetter, false);
|
reset(firstLetter, false);
|
||||||
repaint();
|
repaint();
|
||||||
|
} else {
|
||||||
|
var guess = report.guess;
|
||||||
|
var result = report.result;
|
||||||
|
console.log('My result: ' + result);
|
||||||
|
// TODO: use isValidResult function
|
||||||
|
if (result[0] === 9) {
|
||||||
|
myGuesses.push('-----');
|
||||||
} else {
|
} else {
|
||||||
var result = report.result;
|
for (var i = 0; i < 5; i++) {
|
||||||
console.log('Opponent result: ' + result);
|
if (result[i] === 2) {
|
||||||
opponentResults.push(result);
|
myProgress[i] = guess[i];
|
||||||
repaint();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function subscribeToPlayerReports() {
|
|
||||||
client.subscribe('/user/topic/lingo/playerReports', function(message) {
|
|
||||||
var report = JSON.parse(message.body);
|
|
||||||
console.log('My report: ' + report);
|
|
||||||
if (report.correct === true) {
|
|
||||||
var guess = report.guess;
|
|
||||||
var firstLetter = report.firstLetter;
|
|
||||||
console.log('I guessed correctly!');
|
|
||||||
myScore = myScore + 100;
|
|
||||||
lastWord = guess;
|
|
||||||
reset(firstLetter, false);
|
|
||||||
repaint();
|
|
||||||
} else {
|
|
||||||
var guess = report.guess;
|
|
||||||
var result = report.result;
|
|
||||||
console.log('My result: ' + result);
|
|
||||||
// TODO: use isValidResult function
|
|
||||||
if (result[0] === 9) {
|
|
||||||
myGuesses.push('-----');
|
|
||||||
} else {
|
|
||||||
for (var i = 0; i < 5; i++) {
|
|
||||||
if (result[i] === 2) {
|
|
||||||
myProgress[i] = guess[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
myGuesses.push(guess);
|
|
||||||
}
|
}
|
||||||
myResults.push(result);
|
myGuesses.push(guess);
|
||||||
repaint();
|
|
||||||
}
|
}
|
||||||
});
|
myResults.push(result);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToUserJoined() {
|
function onUserJoined(message) {
|
||||||
client.subscribe('/topic/lingo/userJoined', function(message) {
|
var report = JSON.parse(message.body);
|
||||||
var username = message.body;
|
var username = report[0];
|
||||||
if (username === myUsername) {
|
var numUsers = report[1];
|
||||||
addChatAnnouncement('You joined');
|
if (username === myUsername) {
|
||||||
|
addChatAnnouncement('Welcome to Lingo!');
|
||||||
|
if (numUsers === 1) {
|
||||||
|
addChatAnnouncement('You are the only player online');
|
||||||
} else {
|
} else {
|
||||||
addChatAnnouncement(username + ' joined');
|
addChatAnnouncement('There are ' + numUsers + ' players online');
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
addChatAnnouncement(username + ' joined');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Lingo</title>
|
<title>Lingo | Home</title>
|
||||||
<link rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css">
|
<link rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -54,7 +54,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.body.row.noheader {
|
.body.row.noheader {
|
||||||
bottom: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer.row {
|
.footer.row {
|
||||||
@ -70,3 +70,23 @@ body {
|
|||||||
.chat.column .body {
|
.chat.column .body {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lobby.column {
|
||||||
|
width: 300px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby.column .body {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lobby.column.primary {
|
||||||
|
width: inherit;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game.column.primary {
|
||||||
|
left: 300px;
|
||||||
|
right: 0;
|
||||||
|
border-left: 1px solid black;
|
||||||
|
}
|
||||||
|
@ -36,7 +36,7 @@ function start() {
|
|||||||
client.connect({}, function(frame) {
|
client.connect({}, function(frame) {
|
||||||
subscribeToPracticeGame();
|
subscribeToPracticeGame();
|
||||||
subscribeToPracticeReports();
|
subscribeToPracticeReports();
|
||||||
client.send('/app/lingo/practiceGame');
|
client.send('/app/practiceGame');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ function addKeydownListener() {
|
|||||||
// return
|
// return
|
||||||
else if (e.which === 13) {
|
else if (e.which === 13) {
|
||||||
if (myGuess.length === 5) {
|
if (myGuess.length === 5) {
|
||||||
client.send("/app/lingo/practiceGuess", {}, myGuess);
|
client.send("/app/practiceGuess", {}, myGuess);
|
||||||
myGuess = '';
|
myGuess = '';
|
||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
@ -223,7 +223,7 @@ function reset(firstLetter, clearScore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToPracticeGame() {
|
function subscribeToPracticeGame() {
|
||||||
client.subscribe('/user/topic/lingo/practiceGame', function(message) {
|
client.subscribe('/user/topic/practiceGame', function(message) {
|
||||||
var firstLetter = message.body;
|
var firstLetter = message.body;
|
||||||
reset(firstLetter, true);
|
reset(firstLetter, true);
|
||||||
repaint();
|
repaint();
|
||||||
@ -231,7 +231,7 @@ function subscribeToPracticeGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToPracticeReports() {
|
function subscribeToPracticeReports() {
|
||||||
client.subscribe('/user/topic/lingo/practiceReports', function(message) {
|
client.subscribe('/user/topic/practiceReports', function(message) {
|
||||||
var report = JSON.parse(message.body);
|
var report = JSON.parse(message.body);
|
||||||
console.log('My report: ' + report);
|
console.log('My report: ' + report);
|
||||||
if (report.correct === true) {
|
if (report.correct === true) {
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: cursive;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -28,7 +28,7 @@ button:hover:disabled {
|
|||||||
/* Main area */
|
/* Main area */
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
font-size: 1.4em;
|
font-size: 1.6em;
|
||||||
color: white;
|
color: white;
|
||||||
background: steelblue;
|
background: steelblue;
|
||||||
text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);
|
text-shadow: 1px 1px 1px rgba(0, 0, 0, 1);
|
||||||
@ -46,6 +46,7 @@ button:hover:disabled {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.first {
|
.message-item.first {
|
||||||
@ -59,23 +60,85 @@ button:hover:disabled {
|
|||||||
#messageInput {
|
#messageInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
font-size: 100%;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
background-color: linen;
|
||||||
|
border-color: black;
|
||||||
|
border-left: none;
|
||||||
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Username page */
|
#messageInput::-webkit-input-placeholder {
|
||||||
|
color: steelblue;
|
||||||
.jumbotron {
|
|
||||||
margin: 48px auto;
|
|
||||||
padding: 48px 60px;
|
|
||||||
width: 80%;
|
|
||||||
background-color: #eee;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.jumbotron h2 {
|
#messageInput::-moz-placeholder {
|
||||||
margin-bottom: 15px;
|
color: steelblue;
|
||||||
font-size: 21px;
|
}
|
||||||
font-weight: 200;
|
|
||||||
|
#messageInput:-ms-input-placeholder {
|
||||||
|
color: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lobby pane */
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
margin: 20px 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-color: #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading {
|
||||||
|
background-color: lightgray;
|
||||||
|
border-color: #ddd;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
color: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-heading+.list-group .list-group-item:first-item {
|
||||||
|
border-top-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel>.list-group .list-group-item {
|
||||||
|
border-width: 1px 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: -1px;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.list-group-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.list-group-item:hover {
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nickname pane */
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding-top: 48px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.error-message {
|
||||||
@ -83,18 +146,17 @@ button:hover:disabled {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
width: 100%;
|
width: 300px;
|
||||||
height: 24px;
|
font-size: 200%;
|
||||||
padding: 6px 12px;
|
letter-spacing: 3px;
|
||||||
font-size: 14px;
|
background-color: transparent;
|
||||||
color: #555;
|
border: none;
|
||||||
border: 1px solid #ccc;
|
border-bottom: 2px solid black;
|
||||||
border-radius: 4px;
|
color: steelblue;
|
||||||
|
outline: none;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Waiting pane */
|
/* Waiting pane */
|
||||||
|
Loading…
x
Reference in New Issue
Block a user