Some things to consider:
- It seems strange to ask
Game whether a given card is legal. In my mind, from an OO perspective, it makes sense to ask the card if it is legal, given some context. - The unspoken interface of this function, is "I return a boolean which indicates whether a card is valid" and "I modify the player by setting their action if invalid". This really seems to be beyond the scope of checking whether a card is "valid". It seems like it might be valuable to separate these concepts. One way to do this, that I could imagine, might be having a
CardValidity object returned back that has fields like isValid (true/false) and invalidReason (e.g. "color mismatch"). The game would then be in charge of dealing with setting the next player action.
Note: the above are just my opinion and may be of no value
Let's assume you're going to ignore the above, and just want to make the isCardLegal function manage all the state it currently manages. I could imagine having a Fluent Interface that manages each possible outcome. The end implementation might look like this:
Game.prototype.isCardLegal = function(card,player){ ValidityRuleEngine(card, player, this). invalidOn(player.flags.forcedDraw > 0 && card.value !== '7', 'forced draw'). validOn(card.value === 'J'). invalidOn(this.forcedColor && this.forcedColor !== card.color, 'color mismatch'). validOn(this.forcedColor && this.forcedColor === card.color). invalidOn(this.lastCardOnStack().color !== card.color && this.lastCardOnStack().value !== card.value, 'invalid card'). checkValidityAndUpdatePlayer(); };
IMO, this is a step in the right direction, since the rules are very explicit now. Of course the ValidityRuleEngine would return itself for each of the calls to invalidOn and validOn, checking if an end condition is met (the ValidityRuleEngine would have to manage this state as well). validOn would just set the end condition to true if its argument is true, invalidOn would set to false. checkValidityAndUpdatePlayer() would do exactly what it says: return the validity (the end condition) and update the next playerTask.
One additional step you could take, at this point is to create functions that "name" each of the conditions. Like such:
var forcedDrawAndCardIsntSeven = function (gameContext) { return gameContext.flags.forcedDraw > 0 && gameContext.card.value !== 7; }; var cardIsAJack = function(gameContext) { return gameContext.card.value == 'J'; }; ...
Then invalidOn and validOn could be changed to accept a function rather than a boolean, and pass along the game context (which they should construct from values passed in ValidityRuleEngine). That would change the logic to look like:
Game.prototype.isCardLegal = function(card,player){ ValidityRuleEngine(card, player, this). invalidOn(forcedDrawAndCardIsntSeven, 'forced draw'). validOn(cardIsAJack). invalidOn(forcedColorThatDoesntMatchCard, 'color mismatch'). validOn(forcedColorThatMatchesCard). invalidOn(lastCardDoesntMatchCurrent, 'invalid card'). checkValidityAndUpdatePlayer(); };
In my opinion, this is nice for a few reasons:
- It's very explicit. It's clear as to what cases are invalid, what the reason why they are invalid, and which cases are valid.
- It's maintainable. If cases for validity/invalidity need to be added or removed, the line is simply removed from the fluent calls (along with the function for that rule, assuming that approach is even used).
- It's extendable. If new entities are taken into consideration (maybe dice, or a coin flip), simply pass it into the rules engine. Frankly, the rules engine doesn't even need to be built here! It can be specified elsewhere.
Overall, I hope some of this helps. If readers have any feedback for this review itself, it would be much appreciated.