In this project I’m responsible for the DSL and its ordering, some view components namely: CardView and PlayerView, and the Play Rules.
I’ve primarily worked on the following files: GameDSL, OrderedGameBuilder, CardViewManager, PlayerViewManager, GameElements(specifically on PlayRule).
As per the requirements, the game creation engine must feature a DSL, to be used through simple and intuitive syntax as a configuration tool.
My idea was for the GameDSL to provide a more natural-like syntax for interacting with the GameBuilder class; in order to ultimately provide a complete GameModel ready for use.
DSL development followed these steps:
As to the design choices:
game which the user can use to refer to the game, in order to add configurations.Since the DSL has only a utility role and its usage can be avoided if bypassed by using directly a GameBuilder, I chose to make it a Singleton object with static extension methods for the GameBuilder.
The DSL holds internally a GameBuilder, accessible with the game method, to which it relies on to build a complete GameModel.
object GameDSL:
private var builder: GameBuilder = _
implicit def game: GameBuilder = builder
It then offers extension methods for a “seamless” language-like syntax:
object GameDSL:
...
extension (gameBuilder: GameBuilder)
infix def is(name: String): GameBuilder =
builder = OrderedGameBuilder(name, GameBuilder(name))
builder
Which allows a configuration syntax as such:
game is "Briscola"
Each extension method was also designed such that dotted method call could be avoided, but still available.
game is "Briscola"
game has 4 players
game has player called "Alice"
game.is("Briscola")
.has(4).players
.has(player).called("Alice")
As mentioned before, complex syntax is delegated from the DSL to specific-builders (which in turn may call other builders) using the fluent pattern. For example:
game has player called "Alice"
val entityBuilder: EntityBuilder = game has player
val gameBuilder: GameBuilder = entityBuilder called "Alice"
Such builders are called SyntaxBuilder and use implicit variables called SyntaxSugar which allow for seamless language-like syntax.
In the previous example player is a SyntaxSugar variable.

Just like in any language, the DSL is not immune to errors; in particular it’s prone to semantic errors.
For example, the DSL sees no problem in configuring which player the game starts from, but players may have not been defined yet.
In order to resolve this issue, I’ve modified the DSL to use a OrderedGameBuilder instead of a GameBuilder.
The OrderedGameBuilder is a decorator of a GameBuilder which is tasked with ensuring the correct method’s order of call.
trait OrderedGameBuilder extends GameBuilder:
def validateStep(nextStep: BuilderStep, calledMethod: String): Unit
def isStepValid(requiredStep: BuilderStep): Boolean
In this trait’s hidden implementation, each builder’s method has a mandatory order of call.
Initially, method ordering was thought to be at compile-time, by using phantom types, but its implementation would’ve exceeded the time constraints. For simplicity, it was decided to have this order check at runtime.
OrderedGameBuilder relies upon BuilderStep, an enum which lists all GameBuilder steps’, and each next step.
I was tasked with implementing a UI for each player and their cards in hand, this was done in the following files: CardViewManager and PlayerViewManager.
Both of these files contain traits meant for the Engine View to implement, in order to better abide by the Single Responsibility Principle.
trait CardViewManager:
var cards: Map[String, List[CardModel]]
def addCardToPlayer(playerName: String, card: CardModel): State[Frame, Unit]
def removeCardFromPlayer(playerName: String, card: CardModel): State[Frame, Unit]
def removeCardsFromPlayer(playerName: String): State[Frame, Unit]
private def displayCard(playerName: String, card: CardModel): State[Frame, Unit]
Each card is displayed as a button and labeled with the card’s suit and rank.
Internally, to every card on the table is assigned an ID, to be used for event handling when playing a card.
The ID is structured as such:
<playerName>::<cardName><cardRank><cardSuit>
trait PlayerViewManager:
var players: List[String]
def addPlayer(name: String, numberOfPlayers: Int): State[Frame, Unit]
Each player is displayed as a panel labeled with their name, containing all ordered cards in their hand, with attention to placing even-numbered teams members’ in a front facing manner.
A player’s name is used to visually assign their cards in their hand.
A “Play Rule” is a way for the game to know which player is going to win a turn based on the cards played.
During the design stage of the play rules, I’ve followed the team’s choice of using a type alias PlayRule which internally converts to a lambda.
Such lambda is structured as follows:
List[(PlayerModel, CardModel)] => Option[PlayerModel]
This lambda takes in input the current cards on the playing table, in order of play and linked to which player played them; it then returns the winning player if the rule is applicable.
The choice of returning an Option[PlayerModel] is derived from the initial requirement of reproducing the “Briscola” game, where two play rules apply:
Although these two rules appear straightforward, in many cases they conflict over which player will be awarded the win. As such, I’ve decided to also add a concept of “prevalence” of a rule over another.
Prevalence is used as follows:
rule prevailsOn anotherRule
Rule prevalence states that if the rule is not applicable then anotherRule is to be chosen, otherwise rule is to be chosen only.
This choice allowed to simply implement the briscola’s main play rules.
Since a card game could potentially have many rules, the rules are stored as a List[PlayRule].
Due to the unpredictable nature of rules’ logic and applicability, I’ve chosen to limit a turn’s winning player to only one which led me to add a check which ensures that distinct applicable rules must not result in distinct winning players; such cases must be handled using the before mentioned “rule prevalence” concept.
This check is applied in the following code:
def calculateWinningPlayer(
cardsOnTableByPlayer: List[(PlayerModel, CardModel)]
): Option[PlayerModel] =
val winningPlayers: List[PlayerModel] =
playRules
.map(rule => rule(cardsOnTableByPlayer))
.filter(_.isDefined)
.map(_.get)
if winningPlayers.size > 1 then return None
winningPlayers.headOption
A game play rules’ are unpredictable in nature and as such I’ve decided to allow the user to also configure them by also using a custom DSL; which allows:
Although, due to time constraints, this DSL is minimal and only allows the user to define play rules using the following syntax:
val highestBriscolaTakesRule = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(suit) that takes is briscolaSuit
val highestCardTakesRule = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(rank) that takes follows first card suit
And such rules are applied to a game using the following syntax:
game play rules are :
highestBriscolaTakesRule prevailsOn highestCardTakesRule
It’s worth to mention that the DSL allows some degree of freedom when choosing the winning card, for example these are valid syntaxes:
val exampleRule1 = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(rank) that takes follows first card suit
val exampleRule2 = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(rank) that takes follows last card suit
val exampleRule3 = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(rank) that takes follows first card rank
val exampleRule4 = (cards: List[(PlayerModel, CardModel)]) =>
given List[(PlayerModel, CardModel)] = cards
highest(rank) that takes follows last card rank
Where one chooses which card position (first or last) and property (same rank or same suit), the rule refers to when choosing the prevailing card and the turn-winning player.
