Desarrollando un Juego de BlackJack usando Java WebSocket y AngularJS

¿Qué es un WebSocket?

Un WebSocket es una aplicación asíncrona, de mensajería bidireccional a través de una única conexión TCP permitiendo una comunicación en ambas direcciones simultáneamente. Este protocolo aprovecha una actualización de la cabecera HTTP, con los WebSockets de HTML5 podemos crear aplicaciones en tiempo real con una arquitectura muy sencilla.

Algunos de los posibles usos de WebSocket son:

  • Aplicaciones de chat
  • Juegos
  • El comercio de acciones o de las aplicaciones financieras
  • Edición de documentos de colaboración
  • Aplicaciones de redes sociales

Desarrollo de un Juego de BlackJack usando Java WebSockets y AngularJS

Ahora vamos a poner a prueba los Sockets desarrollando una juego de cartas muy conocido usando la tecnología Java JSR 356, AngularJS y Maven.

Lo primero es crear una clase que represente una carta:

public class Card {

private String rank;
private String suit;
private int value;
private int sort;

...
}

Los tipos de cartas y los valores los voy a colocar en un Enum:

public enum Suits {
    SPADES, DIAMONDS, CLUBS, HEARTS
}
public enum FaceCards {
    TWO("2", 2),
    THREE("3", 3),
    FOUR("4", 4),
    FIVE("5", 5),
    SIX("6", 6),
    SEVEN("7", 7),
    EIGHT("8", 8),
    NINE("9", 9),
    TEN("10", 10),
    J("J", 10),
    Q("Q", 10),
    K("K", 10),
    A("A", 11);

    private final String rank;
    private final int value;

    private FaceCards(String rank, int value) {
        this.rank = rank;
        this.value = value;
    }

    public String getRank() {
        return rank;
    }

    public int getValue() {
        return value;
    }

}

Ahora creamos una la clase que contenedora de cartas o un Maso este representa las 52 cartas del Juego:

public class Deck {

    private final List<Card> cards;

    public Deck() {
        // Crear paquete de 52 cartas
        this.cards = new ArrayList<>();
        for (Suits p : Suits.values()) {
            //System.out.printf("Suits %s\n", p);
            int i = 0;
            for (FaceCards f : FaceCards.values()) {
                Card card = new Card();
                card.setSuit(p.toString());
                card.setRank(f.getRank());
                card.setSort(i);
                card.setValue(f.getValue());
                //System.out.printf("Face %s - %s\n", f.getRank(), f.getValue());
                i++;
                cards.add(card);
            }
        }
        
        // Barajear las cartas
        Random random = new Random();
        Collections.shuffle(cards, random);

    }

    public List<Card> getCards() {
        return cards;
    }
    
    public Card dealCard() {
        return cards.remove(0);
    }

}

En el constructor se inicia el maso con 52 cartas y se barajan usando colecciones de Java.

Al jugador lo vamos a representar con la clase Player:

public class Player {

    private String playerName;
    private double balance;

Una mano o una partida la vamos a representar con la Clase Hand

public class Hand {

    private final List<Card> cards;
    private int score;

    public Hand() {
        this.cards = new ArrayList<>();
    }

    public List<Card> getCards() {
        return cards;
    }

    public void addCard(Card card) {
        this.cards.add(card);
        if ("A".equalsIgnoreCase(card.getRank())) {
            if (score <= 10) {
                score += 11;
            } else {
                score += 1;
            }
        } else {
            // No es "A" tomar el valor de la carta
            score += card.getValue();
        }
        // Recalcular para cambiar el valor del "A"
        if(score > 21) {
            score = 0;
            for (Card cardTmp : cards) {
                if ("A".equalsIgnoreCase(cardTmp.getRank())) {
                     score += 1;
                } else {
                    // No es "A" tomar el valor de la carta
                    score += cardTmp.getValue();
                }
            }
        }
    }

    public int getScore() {
        return score;
    }

    public boolean isBlackjack() {
        return score == 21;
    }

    public boolean isGameOver() {
        return score > 21;
    }

}

En esta clase tenemos los siguientes métodos:

  • addCard, con este método el jugador recibe una carta.
  • isBlackjack, este método verificar si el jugador tiene 21 puntos
  • isGameOver, este método valida que aun no se ha pasado de los 21 puntos

Ahora vamos a desarrollar una clase con la lógica del Juego:

public class Blackjack21 implements Serializable {

    private final Logger LOG = LoggerFactory.getLogger(Blackjack21.class);
    private String gameStatus;
    private Deck deck;
    private Hand playerHand;
    private Hand dealerHand;
    private final Player player;
    private double bet;
    
    public Blackjack21(Player player, double bet) throws Exception {
        this.bet = bet;
        this.player = player;
        if (bet > player.getBalance()) {
            throw new Exception("El monto apostado " + bet + ", es superior a su saldo " + player.getBalance());
        }
        // Comprometer el saldo del cliente
        player.decreaseBalance(bet);
        // Crear una nueva mano
        deck = new Deck();
        // Jugador
        playerHand = new Hand();
        // Crupier
        dealerHand = new Hand();

        // Repartir primeras 4 cartas
        playerHand.addCard(deck.dealCard());
        dealerHand.addCard(deck.dealCard());
        playerHand.addCard(deck.dealCard());
        dealerHand.addCard(deck.dealCard());
        gameStatus = "OPEN";
        // Validar si el juego no ha terminado en la primera mano
        if (dealerHand.isBlackjack() && playerHand.isBlackjack()) {
            LOG.info("Hay un empate entre el crupier y el jugador se retorna su apuesta inicial");
            player.increaseBalance(bet);
            gameStatus = "TIE";
        }
        if (playerHand.isBlackjack()) {
            LOG.info("El jugador tiene Blackjack! gana 3 a 2");
            player.increaseBalance(bet * 1.5);
            gameStatus = "BLACKJACK";
        }
        //Validar si el Crupier tiene Blackjack
        if (dealerHand.isBlackjack()) {
            LOG.info("El Crupier tiene Blackjack! el jugador pierde");
            gameStatus = "LOSE";
        }
    }
    
    public void hitDealer() {
        if(dealerHand.getScore() <=21 && dealerHand.getScore() >= playerHand.getScore()) {
            // El crupier gana
            LOG.info("El crupier gana! Crupier: " +dealerHand.getScore() + " Player: "+playerHand.getScore());
            gameStatus = "LOSE";
        }
        else {
            Card card = deck.dealCard();
            LOG.info("La carta del crupier es: " + card);
            dealerHand.addCard(card);
            if(dealerHand.isGameOver()) {
                // El jugador gana
                LOG.info("El jugador gana!");
                gameStatus = "WIN";
                player.increaseBalance(this.bet * 2);
            }
            if(dealerHand.isBlackjack()) {
                // El crupier gana
                LOG.info("El crupier gana! Crupier: " +dealerHand.getScore() + " Player: "+playerHand.getScore());
                gameStatus = "LOSE";
            }
        }
        
    }

    public void hit() {
        Card card = deck.dealCard();
        LOG.info("La carta del jugador es: " + card);
        playerHand.addCard(card);
        if (playerHand.isGameOver()) {
            gameStatus = "LOSE";
        }
        if (playerHand.isBlackjack()) {
            gameStatus = "WIN";
            player.increaseBalance(this.bet * 2);
        }
    }

    public void stand() {
        if ("OPEN".equalsIgnoreCase(gameStatus)) {
            gameStatus = "STAND";
        }
    }

    public void split() {
        // FIXME Tengo que implemetar el SPLIT
    }

    public void doubles(double bet) throws Exception {
        if (bet > player.getBalance()) {
            throw new Exception("No tiene saldo para doblar la apuesta " + bet + " < " + player.getBalance());
        }
        player.decreaseBalance(bet);
        this.bet += bet;
        Card card = deck.dealCard();
        playerHand.addCard(card);
        if (playerHand.isGameOver()) {
            gameStatus = "LOSE";
        }
        if (playerHand.isBlackjack()) {
            gameStatus = "WIN";
            player.increaseBalance(this.bet * 2);
        }
    }

    public String getGameStatus() {
        return gameStatus;
    }

    public void setGameStatus(String gameStatus) {
        this.gameStatus = gameStatus;
    }

    public Hand getPlayerHand() {
        return playerHand;
    }

    public Hand getDealerHand() {        
        if("OPEN".equalsIgnoreCase(gameStatus)) {
            Hand hideHand = new Hand();
            hideHand.addCard(dealerHand.getCards().get(0));
            return hideHand;
        }
        else {
            return dealerHand;
        }
        
    }

    public double getBet() {
        return bet;
    }

    public void setBet(double bet) {
        this.bet = bet;
    }   
}

Hasta este punto solo hemos realizado la lógica del juego, ahora vamos a comenzar a implementar el socket.

La clase SocketMessage representa el formato del mensaje con el cual vamos ha realizar la comunicación cliente – servidor.

public class SocketMessage {
    private final Logger LOG = LoggerFactory.getLogger(SocketMessage.class);
    
    private String listener;
    private String type;
    private String message;

    public String getListener() {
        return listener;
    }

    public void setListener(String listener) {
        this.listener = listener;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public static class MessageEncoder implements Encoder.Text<SocketMessage> {

        @Override
        public void init(EndpointConfig config) {
        }

        @Override
        public String encode(SocketMessage message) throws EncodeException {
            return Json.createObjectBuilder()
                    .add("listener", message.getListener())
                    .add("type", message.getType())
                    .add("message", message.getMessage()).build().toString();
        }

        @Override
        public void destroy() {
        }
    }

    public static class MessageDecoder implements Decoder.Text<SocketMessage> {

        private JsonReaderFactory factory = Json.createReaderFactory(Collections.<String, Object>emptyMap());

        @Override
        public void init(EndpointConfig config) {
        }

        @Override
        public SocketMessage decode(String str) throws DecodeException {
            SocketMessage message = new SocketMessage();
            JsonReader reader = factory.createReader(new StringReader(str));
            JsonObject json = reader.readObject();
            message.setListener(json.getString("listener"));
            message.setMessage(json.getString("message"));
            message.setType(json.getString("type"));
            return message;
        }

        @Override
        public boolean willDecode(String str) {
            return true;
        }

        @Override
        public void destroy() {
        }
    }
}

La clase WebSocketsDispatcher será la encarga de despachar los mensajes a todos los clientes conectados al socket.

import java.util.Map;
import javax.websocket.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebSocketsDispatcher {

    private final Logger LOG = LoggerFactory.getLogger(WebSocketsDispatcher.class);

    public void dispatch(String listener, SocketMessage message) {
        try {
            if (listener == null || "ALL".equalsIgnoreCase(listener)) {
                for (Map.Entry<String, Session> entry : ServerEndpoint.getListenerMap().entrySet()) {
                    Session session = entry.getValue();
                    session.getBasicRemote().sendObject(message);

                    LOG.info("To: " + message.getListener() + ";; Who: " + message.getMessage() + " ");
                    
                }
            } else {

                Session session = ServerEndpoint.getListenerMap().get(message.getListener());
                session.getBasicRemote().sendObject(message);

                LOG.info("To: " + message.getListener() + ";; Who: " + message.getMessage() + " ");
                
            }

        } catch (Exception e) {
            LOG.error("Error dispatch {}", e);
            e.printStackTrace();
        }
    }
}

La clase ServerEndpoint crea un punto de acceso usando la especificación de Java JSR 356

@javax.websocket.server.ServerEndpoint(value = "/server", encoders = {SocketMessage.MessageEncoder.class}, decoders = {SocketMessage.MessageDecoder.class})
public class ServerEndpoint {

    private final Logger LOG = LoggerFactory.getLogger(ServerEndpoint.class);
    private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
    private static ConcurrentHashMap<String, Session> sessionsMap = new ConcurrentHashMap<String, Session>();
    private static WebSocketsDispatcher webSocketsDispatcher = new WebSocketsDispatcher();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }

    @OnMessage
    public void onMessage(SocketMessage message, Session client)
            throws IOException, EncodeException {
        LOG.info("websockets onMessage {} / {} ", message.getListener(), message.getMessage());

        if ("Handshake".equalsIgnoreCase(message.getType())) {
            Player player = new Player(message.getListener(), 1000);
            client.getUserProperties().put("player", player);
            sessionsMap.put(message.getListener(), client);
        } else if ("Bye".equalsIgnoreCase(message.getType())) {
            // Terminar sesion del usuario
            try {
                client.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Game finished"));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            sessionsMap.remove(message.getListener());
        } else {
            //String oldCmd = (String) client.getUserProperties().get("cmd");
            play(message, client);
        }

        for (Session session : sessions) {
            session.getBasicRemote().sendObject(message);
        }
    }

    private void play(SocketMessage message, Session session) {
        try {
            session.getUserProperties().put("cmd", message.getType());

            Player player = (Player) session.getUserProperties().get("player");
            Blackjack21 blackjack21;
            // Si es dibujar
            if ("DRAW".equalsIgnoreCase(message.getType())) {
                double bet = Double.parseDouble(message.getMessage());
                blackjack21 = new Blackjack21(player, bet);
                session.getUserProperties().put("blackjack21", blackjack21);
            } else {
                blackjack21 = (Blackjack21) session.getUserProperties().get("blackjack21");
            }

            if ("OPEN".equalsIgnoreCase(blackjack21.getGameStatus())) {
                if ("HIT".equalsIgnoreCase(message.getType())) {
                    blackjack21.hit();
                } else if ("STAND".equalsIgnoreCase(message.getType())) {
                    blackjack21.stand();
                } else if ("SPLIT".equalsIgnoreCase(message.getType())) {
                    blackjack21.split();
                } else if ("DOUBLE".equalsIgnoreCase(message.getType())) {
                    double bet = Double.parseDouble(message.getMessage());
                    blackjack21.doubles(bet);

                }

                ObjectMapper mapper = new ObjectMapper();
                HashMap<String, Object> map = new HashMap<>();
                map.put("blackjack21", blackjack21);
                WebSocketsDispatcher wss = ServerEndpoint.getWebSocketsDispatcher();

                String json = mapper.writeValueAsString(map);
                SocketMessage responseMessage = new SocketMessage();
                responseMessage.setListener(message.getListener());
                responseMessage.setMessage(json);
                responseMessage.setType("CURRENT_GAME");
                wss.dispatch(message.getListener(), responseMessage);

            } 
            
            if ("STAND".equalsIgnoreCase(blackjack21.getGameStatus())) {
                // Le toca jugar al crupier

                while ("STAND".equalsIgnoreCase(blackjack21.getGameStatus())) {
                    blackjack21.hitDealer();
                    
                    ObjectMapper mapper = new ObjectMapper();
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("blackjack21", blackjack21);
                    WebSocketsDispatcher wss = ServerEndpoint.getWebSocketsDispatcher();
                    String json = mapper.writeValueAsString(map);
                    SocketMessage responseMessage = new SocketMessage();
                    responseMessage.setListener(message.getListener());
                    responseMessage.setMessage(json);
                    responseMessage.setType("CURRENT_GAME");
                    wss.dispatch(message.getListener(), responseMessage);
                }

            }

        } catch (Exception e) {
            e.printStackTrace();
            LOG.error("Play ERROR {}", e);
        }
    }

    public static WebSocketsDispatcher getWebSocketsDispatcher() {
        return webSocketsDispatcher;
    }

    protected static ConcurrentHashMap<String, Session> getListenerMap() {
        return sessionsMap;
    }
}

Ya tenemos listo el socket del lado del servidor, ahora vamos a desarrollar el cliente web usando AngularJS.

El archivo services.js inicia el servicio del lado del cliente usando AngularJS y WebSockets de JavaScript

angular.module('starter')
        .factory('WSS', function ($rootScope, API) {
            var websocket;
            var open = 0;
            return {
                isOpen: function () {
                    return open;
                },
                subscribe: function () {
                    websocket = new WebSocket(API.WSS_SOCKET);
                    websocket.onopen = function (event) {
                        console.log("ws opened");
                        open = 1;
                        var message = {
                            "listener": "listener:" + MASTER_ID,
                            "message": "subscribe",
                            "type": "Handshake"
                        };
                        websocket.send(JSON.stringify(message));
                    };
                    websocket.onmessage = function (event) {
                        var json = JSON.parse(event.data);
                        if (json.type === "CURRENT_GAME") {
                             $rootScope.$broadcast('rootScope:broadcast', ['blackjack21', JSON.parse(event.data)]);
                        }
                    };
                    websocket.onclose = function (event) {
                        console.log(event.data);
                    };
                    websocket.onerror = function (event) {
                        console.log(event.data);
                    };
                    return websocket;
                },
                closeSocket: function () {
                    if (websocket !== undefined) {
                        var message = {
                            "listener": "listener:" + MASTER_ID,
                            "message": "unsubscribe",
                            "type": "Bye"
                        };
                        websocket.send(JSON.stringify(message));
                    }
                    open = 0;
                },
                sendMessage: function (msg, type) {
                    if (websocket !== undefined) {
                        var message = {
                            "listener": "listener:" + MASTER_ID,
                            "message": msg+"",
                            "type": type
                        };
                        websocket.send(JSON.stringify(message));
                    }
                }
                
            };
        });

Podemos ver como se implementa el metodo sendMessage que enviara los mensajes al socket y la implantación de los métodos escuchadores de WebSockets, donde podemos resaltar el método onmessage que traduce los mensajes enviados desde el servidor y son enviados a toda la aplicación usando  $rootScope.$broadcast de AngularJS.

El archivo constants.js creamos la variable WSS_SOCKET con el URI del Socket

angular.module('starter')
    .constant("API", {
        "WSS_SOCKET": "ws://"+ (document.location.hostname === "" ? "localhost" : document.location.hostname)+ ":" + (document.location.port === "" ? "8080" : document.location.port) + "/blackjack21/server",
    }
);

El fichero mainController.js

angular.module('starter')
        .controller('MainCtrl', ['$rootScope', '$scope', '$state', '$location', '$http', 'API', 'WSS', '$cookies', function ($rootScope, $scope, $state, $location, $http, API, WSS, $cookies) {
                $scope.bet = '1';
                $scope.blackjack21 = {};
                $rootScope.$on('rootScope:broadcast', function (event, data) {
                    //console.log(data);
                    $scope.blackjack21 = {};
                    if (data !== undefined && data.length > 0) {
                        if ('blackjack21' === data[0]) {
                            $scope.blackjack21 = (JSON.parse(data[1].message)).blackjack21;
                            $scope.$apply();
                        } 
                    }
                });
                if (WSS.isOpen() === 0) {
                    $scope.wss = WSS.subscribe();
                }

                $scope.commad = function (type) {
                    WSS.sendMessage($scope.bet, type);
                    //console.log(type);
                };

            }]);

Vamos a crear una interfaz muy simple en HTML,

<input type="text" class="form-control text-right" ng-model="bet" required placeholder="0.00">
    
    <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('DOUBLE')">DOUBLE</button>
    <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('SPLIT')">SPLIT</button>
    <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('HIT')">HIT</button>
    <button type="button" class="btn btn-default" ng-disabled="blackjack21.gameStatus !== 'OPEN'" ng-click="commad('STAND')">STAND</button>
    <button type="button" class="btn btn-default" ng-click="commad('DRAW')">DRAW</button>

    <h2>{{blackjack21.gameStatus}}</h2>

    <p><i>{{blackjack21.playerHand.score}}</i> </p>
    
    <div class="cardstack" style="font-size: 18px;">
        <div ng-repeat="cards in blackjack21.playerHand.cards" class="card rank{{cards.rank}}{{cards.suit}}">{{cards.rank}} </div>
        <p></p>
    </div>
      
    <p><i>{{blackjack21.dealerHand.score}}</i> </p>
    
    <div class="cardstack" style="font-size: 18px;">
        <div ng-repeat="cards in blackjack21.dealerHand.cards" class="card rank{{cards.rank}}{{cards.suit}}">{{cards.rank}} </div>
        <p></p>
    </div>

Como vemos ya tenemos una aplicación en tiempo real usando Java WebSocket y AngularJS

Si lo quieres ver en funcionamiento:

https://wss.araguaneybits.com:8443/blackjack21

Para descargar el código completo:

https://github.com/jestevez/blackjack21

Ya tenemos un juego de Jackblack un poco Básico nos falta implementar la función SPLIT, aplicar saldos y puntos entre otras cosas. Si quieres colaborar o mejorar el código eres bienvenido.

 

 

Comments are closed.