6
\$\begingroup\$

I'm in the process of building an app based on the card game "War" for practice to better understand the MVC model. I would appreciate feedback on if I'm on the right track with my objects interactions so far regarding MVC and if there's any other glaring issues with my code you discover.

Here's my object of cards which is the model, not the full deck and using placeholders for now, followed by the View and controller.

var deckOfCards = {
    player: [],
    computer: [],
    cards: [

        {
            cardValue: 1,
            cardDisplay: "🂡"
    },
        {
            cardValue: 2,
            cardDisplay: "🂢"
    },
        {
            cardValue: 3,
            cardDisplay: "🂣"
    },
        {
            cardValue: 4,
            cardDisplay: "🂤"
    },
        {
            cardValue: 5,
            cardDisplay: "🂥"
    },
        {
            cardValue: 6,
            cardDisplay: "🂦"
    },

var view = {

    init: function () {
        this.computerCard = document.getElementById("computer-card");
        this.playerCard = document.getElementById("player-card");
        this.outCome = document.getElementById("outcome-text");
        this.gameOverOverlay = document.getElementById("gameover-overlay");
        this.currentCardCount = document.getElementById("card-count");
        this.shuffleBtn = document.getElementById("shuffle-btn");
        this.shuffleBtn.addEventListener("click", function () {
            this.style.visibility = "hidden";
            controller.shuffle();
            controller.deal(deckOfCards.cards);
            controller.cardCount();
        });
        this.playCardBtn = document.getElementById("play-card-btn");
        this.playCardBtn.addEventListener("click", function () {
            controller.checkValues();
            view.render();
            controller.cardCount();
        });
        this.resetBtn = document.getElementById("reset-game");
        this.resetBtn.addEventListener("click", function () {
            view.gameOverOverlay.style.display = "none";
            view.shuffleBtn.style.visibility = "visible";
            controller.removeCards();
            controller.clearPlayArea();              
        })
    },

    render: function () {
        this.playerCard.innerHTML = deckOfCards.player[0].cardDisplay;
        this.computerCard.innerHTML = deckOfCards.computer[0].cardDisplay;
    },

    gameOver: function (winner) {
        this.gameOverOverlay.style.display = "block";
        this.outCome.innerHTML = winner;
    }
}

var controller = {

    shuffle: function () {
        deckOfCards.cards.sort(function () {
            return 0.5 - Math.random();
        })
    },

    deal: function (arr) {
        for (var i = 0; i < 26; i++) {
            deckOfCards.player.push(arr[i]);
        }
        for (var j = 26; j < 52; j++) {
            deckOfCards.computer.push(arr[j])
        }
        console.log(deckOfCards.player)
        console.log(deckOfCards.computer);
    },

    cardCount: function () {
        view.currentCardCount.innerHTML = deckOfCards.player.length;
    },

    checkValues: function () {
        var newCard,
            playerCards = deckOfCards.player,
            computerCards = deckOfCards.computer;

        if (playerCards[0].cardValue > computerCards[0].cardValue) {
            playerCards.push(computerCards[0]);
            computerCards.shift();
            newCard = playerCards.shift();
            playerCards.push(newCard)
            this.winner();

        } else if (playerCards[0].cardValue < computerCards[0].cardValue) {
            computerCards.push(playerCards[0]);
            deckOfCards.player.shift();
            newCard = computerCards.shift();
            computerCards.push(newCard)
            this.winner();
    },

    removeCards: function () {
        deckOfCards.player = [];
        deckOfCards.computer = [];
    },

    clearPlayArea: function () {
        view.computerCard.innerHTML = " ";
        view.playerCard.innerHTML = " ";
    },

    winner: function () {
        if (deckOfCards.player.length === 0) {
            view.gameOver("You Lose")
        } else if (deckOfCards.computer.length === 0) {
            view.gameOver("You Win");
        }
    },

    init: function () {
        view.init();
    }
}
controller.init();
\$\endgroup\$
  • \$\begingroup\$ That code appears to be missing parts. Right before var view = you don't close your list of cards or you deckOfCards object. \$\endgroup\$ – Emil S. Jørgensen Aug 16 '17 at 7:08
5
\$\begingroup\$

There are definitely many ways to use MVC pattern. But I think there are some misunderstanding in the pattern's motivations given the implementation. Let me see if I can explain it by defining the motivations of the pattern.

Model

The idea of the "model" is that we should be keeping all our data and also business logic in that layer. I think you have the data part right but the business logic part, you have most of it down at the controller layer. Things like "shuffle", "deal", etc.. That's all very specific to manipulating and doing something with the data. Also, it's very important to realize the model should almost never care about what context it is used in(especially how it's presented) and the model should be easily portable to a completely different environment. For example, we are obviously using our deckOfCards model in the DOM, we should be easily use all that code to a node server and use it in an API.

View

The view is the presentation layer. It should have no business logic and it should only be responsible of displaying(the model). If we swap a different model with same interface, the view layer should function. In our case, the view has all the knowledge of the DOM related interactions and events. But it should ideally never know about how it was instantiated, it should take some model as an input and the output is some presentation of that model.

Controller

The controller is probably the most misunderstood part of the MVC pattern. The controller has full knowledge of the model and the view. That said, the controller should still ideally have no business logic. Its primary job is to set up the stage for the view to interact with the model. A very common pattern is creating a model and then passing the model into the view to do its job. A good practice is the idea of very thin and dumb controller with a very imperative style of code. The controller is the coordinator between all the modules and know how to put things together to do the right things.

Putting it all together

I made some arbitrary choices based on the above criteria. Do realize that there is no perfectly executed MVC pattern, but we can do our best based on the motivation of the pattern. I moved some of the code from your original snippet based on my recommendations. In order to decouple the controller and the view(the view should not know about the controller), I chose to make an abstraction event layer to bubble up an event to whoever that may be listening to the view. This required added these fake "broadcastEvent" and "on" method to the view so that the controller can listen and react to it. In theory, we could use a different pattern with the same view and still be able to use the view as is, in doing so we rid the view from the business logic of telling the model to do stuff. There are other ways to solve the same problem but I'm mostly working with what you have there. I tried my best to give a decent representation of what I was saying with the below code snippet.

var deckOfCards = {
    player: [],
    computer: [],
    cards: [

        {
            cardValue: 1,
            cardDisplay: "&#127137;"
        },
        {
            cardValue: 2,
            cardDisplay: "&#127138;"
        },
        {
            cardValue: 3,
            cardDisplay: "&#127139;"
        },
        {
            cardValue: 4,
            cardDisplay: "&#127140;"
        },
        {
            cardValue: 5,
            cardDisplay: "&#127141;"
        },
        {
            cardValue: 6,
            cardDisplay: "&#127142;"
        }
    ]
}

var model = {
    init: function (data) {
        this.data = data;
    },
    shuffle: function () {
        this.data.cards.sort(function () {
            return 0.5 - Math.random();
        })
    },

    deal: function (arr) {
        for (var i = 0; i < 26; i++) {
            this.data.player.push(arr[i]);
        }
        for (var j = 26; j < 52; j++) {
            this.data.computer.push(arr[j])
        }
        console.log(this.data.player)
        console.log(this.data.computer);
    },

    cardCount: function () {
        view.currentCardCount.innerHTML = this.data.player.length;
    },

    checkValues: function () {
        var newCard,
            playerCards = this.data.player,
            computerCards = this.data.computer;

        if (playerCards[0].cardValue > computerCards[0].cardValue) {
            playerCards.push(computerCards[0]);
            computerCards.shift();
            newCard = playerCards.shift();
            playerCards.push(newCard)
            this.winner();

        } else if (playerCards[0].cardValue < computerCards[0].cardValue) {
            computerCards.push(playerCards[0]);
            this.data.player.shift();
            newCard = computerCards.shift();
            computerCards.push(newCard)
            this.winner();
    },

    removeCards: function () {
        this.data.player = [];
        this.data.computer = [];
    },

    clearPlayArea: function () {
        view.computerCard.innerHTML = " ";
        view.playerCard.innerHTML = " ";
    },

    winner: function () {
        if (this.data.player.length === 0) {
            view.gameOver("You Lose")
        } else if (this.data.computer.length === 0) {
            view.gameOver("You Win");
        }
    },
}

var view = {
    // new
    broadcastEvent: function (eventName) {
        // some implementation to broadcast an event on self
    }, 
    on: function (eventName, fn) {
        // some implementation to allow others to listen to events broadcasted
    },

    init: function (model) {
        // new
        this.model = model;

        this.computerCard = document.getElementById("computer-card");
        this.playerCard = document.getElementById("player-card");
        this.outCome = document.getElementById("outcome-text");
        this.gameOverOverlay = document.getElementById("gameover-overlay");
        this.currentCardCount = document.getElementById("card-count");
        this.shuffleBtn = document.getElementById("shuffle-btn");
        this.shuffleBtn.addEventListener("click", function () {
            this.style.visibility = "hidden";
            this.triggerEvent('shuffleClick');
        });
        this.playCardBtn = document.getElementById("play-card-btn");
        this.playCardBtn.addEventListener("click", function () {
            this.triggerEvent('playClick');
        });
        this.resetBtn = document.getElementById("reset-game");
        this.resetBtn.addEventListener("click", function () {
            view.gameOverOverlay.style.display = "none";
            view.shuffleBtn.style.visibility = "visible";

            this.triggerEvent('resetClick');            
        })
    },

    render: function () {
        this.playerCard.innerHTML = deckOfCards.player[0].cardDisplay;
        this.computerCard.innerHTML = deckOfCards.computer[0].cardDisplay;
    },

    gameOver: function (winner) {
        this.gameOverOverlay.style.display = "block";
        this.outCome.innerHTML = winner;
    }
}

var controller = {

    init: function () {
        model.init(deckOfCards);
        view.init(model);
        view.on('shuffleClick', function () {
            model.shuffle();
            model.deal(deckOfCards.cards);
            model.cardCount();
        });
        view.on('playClick', function () {
            model.checkValues();
            view.render();
            model.cardCount();
        });
        view.on('resetClick', function () {
            model.removeCards();
            model.clearPlayArea(); 
        }); 
    }
}
controller.init();
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.