PPS-24-Briscala

Agosta Alessandro

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).

GameDSL

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

DSL development followed these steps:

As to the design choices:

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.

DSL_SyntaxBuilder

Method Ordering

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.

View Mixins

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.

CardViewManager

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>

PlayerViewManager

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.

Play Rules

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

Play Rules syntax

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. PlayRule_DSL